The Trilight

A simple general-purpose lighting model for games

Tom Forsyth, 22nd March 2007

 

Additional materials: the Excel spreadsheet and the Windows demo.

 

A “trilight” lighting equation is a generalisation of a bunch of lighting models people have used in games over the years:

Lambert lighting

 

res = colour0 * clamp(N.L)

 

Your standard half-sphere lighting.

Wrap-around lighting

 

res = colour0 * clamp(N.L+factor)/(1+factor)

 

The problem with pure Lambert lighting is that 50% of your model is completely dark, so the back side doesn’t really show up very well without adding more lights. A wrap-around light is a very crude simulation of the light scattering back from the surroundings and lighting the back side to some extent. This has been used by a variety of games for ages, but the first place I’ve seen it actually documented was in an aside in an article about Half Life 2’s lighting model.

 

factor” ranges from 0 to 1, with many games just hard-wiring it to 1.

 

Hemispherical lighting

 

res = lerp ( colour2, colour0, (N.L+1)/2 )

            = colour2 + (colour0-colour2) * (N.L+1)/2

            = (colour0+colour2)/2 + (colour0-colour2) * (N.L)/2

 

This has also been used in many games for ages, but the first time I remember seeing it actually documented was in a talk by Chas Boyd of Microsoft talking about more interesting light models. This is good for outdoor scenes – typically the colours are a light blue for the sky and a dark greenish/brown for the ground. These are the preset colours in the trilight demo.

 

Note – I’ve used colour0 and colour2, missing out colour1 to be consistent with the trilight’s terminology below.

 

Bi-directional lighting

 

res = colour0 * clamp(N.L) + colour2 * clamp(-N.L)

 

Yet another lighting model used for ages. The first place I’ve seen it documented is in an article on Tabula Rasa’s rendering model in an upcoming GPU Gems book. This is distinct from the hemispherical lighting model because a lit sphere will have a ring around the middle between the two lights. In hemispherical lighting, this ring is the average of the two colours. In bi-directional lighting, this ring is black. This is fairly simple to see when playing with the demo.

 

This lighting model is a cheap approximation of the classic movie lighting of “key” and “fill” lights. The usual example is at night - a bright yellow “key” light from a streetlamp and a dark-blue “fill” light from the night sky to give shape to the dark parts of the character. In fact usually the fill is not directly opposite the key light, but this is a very similar effect and is basically free.

 

The Trilight Model

 

res = colour0 * clamp(N.L) + colour1 * (1-abs(N.L)) + colour2 * clamp(-N.L)

 

The idea of the trilight is to unify all the above lighting models into one. As a bonus, it also gives more control than any one of them. In the same way that I’ve never seen the above lighting models documented until fairly recently, but I know they’ve been used for ages, I suspect this model has been used by others even before I did (which I think was sometime in 2002). But I thought it was time to write the thing down so that everyone could use it.

 

The trilight is obviously a superset of Lambert and bi-directional – simply set colour1 and/or colour2 to black.

 

The trilight is identical to hemispherical lighting if colour1 = (colour0+colour2)/2. Proof:

 

For N.L >= 0:

trilight = colour0 * (N.L) + colour1 * (1-(N.L))

            = colour0 * (N.L) + colour1 - colour1 * (N.L)

            = (colour0-colour1) * (N.L) + colour1

            = (colour0-(colour0+colour2)/2) * (N.L) + (colour0+colour2)/2

            = ((colour0-colour2)/2) * (N.L) + (colour0+colour2)/2

            = same as hemispherical

For N.L < 0:

trilight = colour1 * (1+(N.L)) - colour2 * (N.L)

            = -colour2 * (N.L) + colour1 + colour1 * (N.L)

            = (colour1-colour2) * (N.L) + colour1

            = ((colour0+colour2)/2-colour2) * (N.L) + (colour0+colour2)/2

            = ((colour0-colour2)/2) * (N.L) + (colour0+colour2)/2

= same as hemispherical

 

 

 

The trilight is very close to wrap-around lighting when colour2 = black, colour1 = factor*colour0/2. This is only an approximation, but it’s pretty close as you can see from the demo and from the following:

 

For N.L >= 0:

trilight = colour0 * (N.L) + colour1 * (1-(N.L))

= colour0 * (N.L) + factor *colour0* (1-(N.L))/2

            = colour0 * ((N.L) + factor /2 - factor *(N.L)/2)

            = colour0 * ((N.L)*(2- factor) + factor))/2

For N.L < 0:

trilight = colour1 * (1+(N.L)) - colour2 * (N.L)

            = colour1 * (1+(N.L))

            = factor *colour0 * (1+(N.L))/2

            = colour0 * (factor /2) * ((N.L)+1)

 

hemi = colour0 * clamp((N.L)+factor)/(1+factor)

 

 

Note that when factor is 0 or 1, the two match exactly. The greatest discrepancy is when factor=0.5. Looking at the Excel spreadsheet you can see how the two curves differ at this value, and by toggling on and off trilight emulation in the demo, you can see that there is a redistribution of shade from light to dark areas, causing a slight washing-out of contrast. However, changing the “factor” value is done in response to moving from a stark, purely-Lambertian lighting environment into an environment with more back-lighting from indirect bounces, so the whole thing is just an approximation anyway. As previously mentioned, many games just hard-wire the factor to be 1.

 

Advantages of the Trilight model

 

The first advantage to using the trilight type is that it can emulate all these lighting types without changing shaders, which reduces the cost of rendering calls and also reduces the combinatorial nightmare that many coders using shader face.

 

But the most fundamental advantage is that the code can continuously blend between them as required simply by blending the values of the three colours together for each light type. So you can have a light that is partially Lambertian and partially bi-directional, and lerp between the two under gameplay or location or scripting control.

 

In general, any time two different systems can be unified and parameterised, and you can blend smoothly between them, the life of a coder gets that little bit simpler.

 

The Demo

 

You probably want to make the demo fullscreenthere’s a lot of sliders in the bottom right. From top to bottom:

 

 

LMB: rotate model.

MMB: rotate camera.

RMB: rotate light.

 

The shader code used is in trilight.fx if you wish to inspect it.