Shadows

There are two main techniques used for shadows. Shadow volumes are the same as a conventional renderer whereas shadow maps have a few changes.

Shadow Maps

Click for a bigger version
 

Shadow maps are very easy to support under deferred lighting and have very good performance. The key is using the little used variant known as forward shadow mapping [16]. In standard shadow mapping the shadow map is projected onto the object and the depths compared. With forward shadow mapping the objects position is projected into shadow map space and then depths compared there.

The first step is to calculate the shadow map; this is exactly the same as a conventional renderer. All objects in the lights view are rendered to a depth texture (depending on hardware this might be an actual depth buffer or a high precision texture), for point lights 6 depth textures are rendered (the faces of the cube surrounding the light source).

When the light that generated the shadow map is rendered, the shadow map is attached to the light shader in the standard fashion (a cube map for the point light case). A shadow warp matrix is computed that takes points in view space to the shadow space. Then the light shader for each pixel transforms the surface position (in view space) via the warp matrix into a shadow space point. This shadow space point provides both the surface depth and coordinates to project on to the shadow map, which provides the shadow maps depth.

float PointSampleShadowMap( float4x4 ShadowWarpMatrix )
{
  float4 PinShadow = mul( P, matShadowWarp );
  float PDepth = PinShadow.z / PinShadow.w;
  float SDepth = tex2Dproj( ShadowTexture, PinShadow );
  if(PDepth < SDepth)
    return 1; // not in shadow
  else
    return 0; // in shadow
}

For percentage closest filtering we jitter the samples before lookup and then average the result. It’s good for performance to remember that compares are per component, and that the dot product of a vector with itself when the values of each component of 0 or 1 is a sidewise accumulator [17]. The cost of a 4 sample percentage closest filter is little more than the cost of the extra texture lookups.

float PCF4SampleShadowMap( float4x4 ShadowWarpMatrix )
{
  float4 PinShadow = mul( P, matShadowWarp );
  float PDepth = PinShadow.z / PinShadow.w;
  float4 SDepth;
  SDepth.x = tex2Dproj( ShadowTexture, PinShadow +
                        Jitter0 );
  SDepth.y = tex2Dproj( ShadowTexture, PinShadow +
                        Jitter1 );
  SDepth.z = tex2Dproj( ShadowTexture, PinShadow +
                        Jitter2 );
  SDepth.w = tex2Dproj( ShadowTexture, PinShadow +
                        Jitter3 );
  float4 compare = (PDepth < SDepth.xyzw);
  return (compare dot compare) *0.25;
}

More Control over Lighting

One big advantage with using deferred lighting is that all surfaces and objects are affected equally. But at times this is also a problem, in many cinematic lighting rigs extreme effort is used making sure lights and shadows don’t affect particular things (like lighting only the movie’s star faces, or making sure random shadows don’t hit things). This is easy using standard lighting systems but deferment makes it harder, the solution is a ID parameter stored in the G-Buffer.

One use of this is to allow each object to decide which lights affect it. Every object or surface has its own ID and this ID is checked via the light shader. The light shader contains a texture that has a Boolean result whether this object ID (used as a UV coordinate) should be affected.

This also allows multiple light equations by having several illuminate functions for different lighting equations and using the ID to select which one is used, currently this is very expensive but when we have pixel shader dynamic branching this could become fast.

Another approach for different lighting equations is to use hybrid deferred lighting system, where some objects bypass (fully or partially) the deferred light system and light directly in the lit buffer, obviously this loses most of the benefits but it can be helpful it you have a few objects that don’t fit into your deferred lighting model.

High dynamic range imaging [18]

Deferred lighting would appear to be trivial to extend to the use high dynamic range images, with 16 bit per surface colour channels, a destination lit buffer that can be any precision we choose and the post-processing phase to perform tone-mapping, it would appear that HDRI should be the default for this type of rendering. Unfortunately current hardware can’t alpha blend into textures that have more than 8 bits of precisions; which mean there is no easy way to accumulate the lighting contributions.

A solution is to keep a second temp buffer (same size and resolution of the lit buffer) which is the output of all light shaders, the lights shader have as an input the ‘real’ lit buffer and do the light accumulation at the end of the shader, then the light shader geometry is re-processed but with a simple pixel shader that copies the temp lit buffer and replaces the exact same pixels in the ‘real’ lit buffer. This only adds a small cost per light shader to support HDRI (the extra vertex cost and the running of a trivial pixel shader) as the copy operation only copies pixels that have changed but the state changes may be prohibitively expensive.