Lighting Variables

P = Surface Position in view space
N = Shading Normal in view space
E = Eye point in view space ( usually <0,0,0> )
I = Incident vector (from the eye to the surface position)
L = Light Vector
H = Half vector ( normalize(N + L) )

Cs0 = Surface Colour 0
Cs1 = Surface Colour 1
Cs = Surface Colour (used whenever which surface colours is not specified)
Cl = Light Colour
LB = Light buffer (the output of light shaders)

Kd = Diffuse Coefficient
Ks = Specular Coefficient
Kamb = Ambient coefficient
Kemm = Emissive coefficient

Light Model

One disadvantage of deferred lighting is that it’s comparatively expensive to have multiple light models, while programmable light models are considered one of the main advantages of the programmable pipeline the increase in quality and number of lights may compensate. The usual method for deferred lighting is to pick one model that can manage most of the surface types your scene will have. Here you have to make trade off between number of parameters to store and the cost of evaluating the entire light model per pixel against the uber light model you would like.

The choice of light model will affect the renderer more than any other, so it’s worth customizing the existing models rather than a simple implementation straight out of a research paper. I choose to use a superset of the classic Phong / Blinn model for this article, the reason being it makes it easy to understand what’s going without having to get into BDRF theory and it’s quick to both implement and execute.

The Phong / Blinn model splits lighting into 4 major components, Ambient, Emissive, Diffuse and Specular. The ambient and emissive portions are only calculated once but the diffuse and specular are calculated per light.

Pseudo Code:

Once Per Scene
LB = Kamb * AmbientColour + Kemm * Cs

For Every Light:
Diffuse = N dot L
Specular = (N dot H) ^ shininess
if( Diffuse > 0 )
  LB += Cs * Diffuse * Cl * Kd;
  if( Specular > 0 )
    LB += Specular * Cl * Ks;

This basic model has a number of problems; the first (and most famous) is that it has a tendency to make everything look like plastic. Other better light models (like Blinn / Cook / Torrance) can be evaluated directly but we can often produce close approximations via modifying the traditional light model to use the capabilities of modern graphics cards.

One thing video cards do very well is read textures quickly; we can use this capability to replace a lot of the fixed portions with a table lookup. More advanced light models often have different shapes produced via the N dot L and the N dot H term, models like Oren / Nayer produce a flatter diffuse based on a roughness term. One quick improvement is to replace the fixed diffuse and specular dot product shape function (the diffuse has one that is identity under Phong / Blinn) with a table lookup based on the material parameters.

The result of a dot product can range -1 to 1, but the conventional model only uses the positive half. While you can use this property to save texture space I prefer to let the dot product shape function decide what happens when the surface normal is back faced to the light. This allows us to simulate ‘wrap’ surfaces which are good approximations to a small amount of subsurface transparency and subsurface scattering (the material is assumed to transmit some of the light from the front of the surface to the back). In a second channel of the dot product shaping texture we store a cascading multiplier that controls the next element in the standard light equation, this is used to eliminate the specular highlight when the diffuse component is back faced even though the N dot H is front faced. It can also be used for custom lighting where the specular highlights are also affected by the N dot L term.

We now demonstrate the standard Blinn / Phong model using this new and improved model.

// diffuse dot product shaping function
float2 Fa( float dotp )
{
  // if light is front facing
  if( dotp > 0 )
    return float2(dotp,1); // identity shape and
      cascade multiplier = 1
  else
    return float2(0,0); // kill the lighting and
      cascade multiplier = 0
}

// specular dot product shaping function
float Fb( float dotp, float shininess )
{
  // if light is front facing
  if( dotp > 0 )
    return pow(dotp, shininess); // shape = power
    function(^shininess)
  else
    return 0; // kill the lighting
}

For Every Light
  Diffuse = Fa(N dot L)
  Specular = Fb(N dot H, shininess)
  LB += Cs * Diffuse.x * Cl * Kd
  LB += Specular * Cl * Diffuse.y * Ks

We can replace the functions Fa and Fb with textures if we bias the dot products into the range 0-1. To enable flexibility we make them 2D textures and the second parameter is used to access which particular dot product shaping function this surface point uses. Another property we would like is the ability for the specular highlight to take on the colour of the light combined with the surface (plastic) or just the surface only (metallic). We achieve this by adding another parameter that controls a lerp between the 2 options.

The ambient and emissive is executed as the set-up phase, and the main portion of the light model is contained in a function that most lights call. Some lights implement there own main portion which allows for special lighting conditions such as diffuse only lights or specular only lights. The function that implements the main portion of the lighting model uses Cs0 as the surface colour for the diffuse and Cs1 as the specular and emissive surface colour.

float3 Illuminate()
{
  float2 Diffuse = tex2D( DotProductFuncs, float2(
    bias(N dot L), Fa) );
  float2 Specular = tex2D( DotProductFuncs, float2(
    bias(N dot H), Fb) );
  float3 output = Cs0 * Cl * Diffuse.x * Kd;
  output += Specular.x * Lerp(Cs1*Cl, Cs1, Kspecblend)
    * Diffuse.y * Ks;
  return output;
}

This gives the a light model that is capable of emulating (to a high degree) Blinn / Phong, Oren / Nayer, wraps and can have a plastic or metal highlight. It also allows other non-standard surface materials, e.g. by modulating Cs1 by a Fresnel function in the geometry phase you can achieve view dependent specular, and Cs1 can also be determined via a local environment map in the geometry phase. You can make Fb into the diffuse portion by using a constant Fb function and passes N dot L in via the cascade multiplier, or by using a non constant Fb function you have access to a (N dot L)*Fb(N dot H) term. Even though the light model is fixed the use of replaceable dot product functions, gives us a lot of freedom for custom materials.

The Illuminate function is the major definer of the type of G-Buffer used, we need to store all the parameters needed to execute this function. This is a total of 19 parameters (Position, Normal, DiffuseFunc, SpecularFunc, Cs0, Cs1, Kd, Ks, Kspecblend, KAmb and KEmm) which is more than the 16 channels we have available (you could add more channels to the G-Buffer but you would no longer be able to render in a single pass on MRT cards). The reduction to fit the G-Buffer uses the fact the some parameters change less frequently and removes some redundancy. Some parameters usually don’t change within a material, so by storing a material index in the G-Buffer when can lookup the actual parameters in the light shader. Also Kd and Ks can be pre-multiplied into Cs0 and Cs1 and don’t have to stored explicitly (Pre-multiplying Ks does effect the emissive colour in my light model but I can live with this error). Also I’ve added an ID field that is explained later.