Full screen lights

For lights that are truly global and have no position and size (ambient and directional are the traditional types), we create a full screen quad that executes the pixel shader at every pixel. The light shader reads the parameters from the G-Buffer, computes the light value at this pixel and then accumulates into a destination light buffer.

We need to initialise the destination light buffer, so we run a full screen light shader that does this. I have 2 standard variants, one sets the destination light buffer to the global ambient and the surfaces emissive colour, the other replaces the ambient term with a simple hemispherical light source.

AmbientEmissiveLightShader( float3 AmbientColour )
{
  LB = Kamb * AmbientColour + Kemm * Cs1;
}

A hemispherical light model uses the surface normal and the world up vector to blend between 2 colours (so called sky and ground colours)

HemisphereEmmisiveLightShader( float3 GroundColour, float3 SkyColour, float3 WorldUp)
{
  float dp = N dot WorldUp;
  float blend = 0.5 + dp * 0.5;
  float3 hemicol = Lerp(GroundColour, SkyColour,blend);
  LB = Kamb * hemicol + Kemm * Cs1;
}

Another full screen light that we are used to is the classic directional light. This light type (known as Solar under Renderman) assumes the light is infinitely far away, and the light vector is parallel for the entire scene. The means the vector L is a constant for the entire light shader. For local viewer, the incident vector I is different at every pixel, with a infinite viewer this is also constant but produces inferior specular highlights. I assume a local viewer throughout my examples but it is trivial to change to a infinite viewer (I = <0,0,1> for infinite viewer as we light in view space).

DirectionalLight( float3 LightDirection)
{
  // for local viewer I = - the normalized surface
  // point in view space for infinite viewer I =
  // <0,0,1> in view space
  I = normalize( -P );
  L = LightDirection;
  LB += Illuminate();
}

Shaped Lights

The second category of lights, are the lights that have position and shape (like point lights, spotlights etc). These are the most interesting category and usually end up faster than full screen lights, the reason is that for deferred lighting the cost of a light is proportional to the number of pixels covered with shaped lights there is the potential for that to be less than every pixel on the screen. This leads to the unusual situation that several medium sized point lights may be faster than 1 directional light.

Shaped lights can be implemented via a full screen quad in exactly the same way of directional lights just with a different algorithm computing the lights direction and attenuation, but the attenuation allows us to pre-calculate where the light no longer makes any contribution.

Standard DirectX/OpenGL attenuation [11][12] uses a quadratic equation that I’ve always found hard to tweak and the constant term means that the light can affect all surfaces regardless of how far away they are, this would require the use of a full screen light. The attenuation model I use is a simple texture lookup based on distance. The distance is divided by the maximum distance that the light can possible effect and then this is used to lookup a 1D texture. The last texel should be 0, (no constant term) if the following optimisations are to be used.

float Attenuate( float3 LightPosition, float
                 MaximumLightRange )
{
  float distance = | LightPosition – P |;
  distance /= MaximumLightRange;
  return tex1D( AttenuationTexture, distance );
}
 
PointLight( float3 LightPosition, float
            MaximumLightRange )
{
  I = normalize( -P );
  L = normalize( LightPosition – P);
  float atten = Attenuate( LightPosition,
                MaximumLightRange);
  LB += Illuminate() * atten;
}

We can also do spotlights easily by adding an angular attenuation term and using the dot product to index into a texture. The AngularFalloffCoord is used as the second coordinate which is used to select the angular falloff (each combination in inner and outer angle will have a different 1D function in the 2D texture).

float AngularAttenuate( float3 LightDirection, float AngularFalloffCoord )
{
  float dotp = -P dot LightDirection;
  return tex2D( AngularAttenTexture. float2(bias(dotp)
              , AngularFalloffCoord ) );
}
 
SpotLight( float3 LightPosition, float
           MaximumLightRange, float3 LightDirection,
           float AngularFalloffCoord )
{
  I = normalize( -P );
  L = normalize( LightPosition – P);
  float atten = Attenuate( LightPosition,
                MaximumLightRange);
  float anglularatten = AngularAttenuate(
                        LightDirection,
                        AngularFalloffCoord );
  LB += Illuminate() * atten * angularatten;
}

With the attenuation models we now have the maximum distance the light can possible effect and the maximum angle. We can now calculate which pixels are beyond these ranges and try not to run the pixel shaders. Regardless of any optimisation we make regarding actual pixels we need to run the lights shader at, the per-pixel shader stays roughly the same.

The first approach used is a screen aligned quad just big enough to cover the light which we calculate by projecting the sphere formed by the light position and the maximum distance, to the screen. This requires no changes at all from using a full screen quad, as all we are doing is roughly removing the pixels where the attenuation has reduced the lighting contribution to zero.