From GPWiki
Jump to: navigation, search

Direct Light Sources

A crucial part of implementing lighting in computer graphics is to be able to model the sources and their corresponding distribution of light energy. This is the focus of the following chapter.

Like other forms of energy, such as heat and sound, light energy has a source. The previous chapter introduced the notion that lighting models are an approximation based on observation; in line with this mentality the light sources used are also approximations.

The visual results we want to replicate should rely on the relationship between the source of light energy and the receiving geometry. This is a simplification and in the real-world the visual results might be relative to many other factors such as scattering of light and complex reflections between surfaces; however these complex relationships fall into the realm of physically modelled lighting and the reasons for avoiding this were covered in the previous chapter. It is still possible to get plausible and visually acceptable results by avoiding these complex calculations.

Accurately computing the distribution of energy from an arbitrary source of light is still beyond the scope of real-time computer graphics. This is especially true given the intention of keeping any lighting system as truly dynamic and disallowing any pre-processing.

Consequently approximate lighting with approximated sources often requires artistic placement. What might seem like a numerically accurate placement of light sources may not look correct to the human eye, instead a more artistic approach can yield far more acceptable results.

Light sources can be categorised into four categories:

  1. Directional
  2. Point
  3. Spot
  4. Area

It is worth noting that the first three correspond to the three main types of light source available in previous versions of Direct3D (via the fixed-function pipeline) and that they appear in many other API’s and art packages as standard types.

Each light source has different performance characteristics that it is important to be aware of. The previous list is ordered from fastest (1) to slowest (4). Because performance is critical in real-time computer graphics, it can place a limit on the number of light sources that can be used in a particular scene. There are no specific rules for this limit such that performance-profiling and testing become very important.

The knock-on effect of limiting the number of lights is that their placement becomes important – questions such as “how can I provide the appropriate lighting using the minimum number of lights” need to be asked. The application can help by using aggressive culling techniques to minimize the burden on the artist but these typically require substantial work on the part of the programmer. [Lengyel06] provides an interesting overview of these techniques.

Sample implementations of each type of light source will be provided as vertex shaders throughout the remainder of this chapter. This approach makes for a more focused and simpler introduction but does make the quality of the results dependent on the resolution of the geometry being rendered. This problem, and the solution, is discussed in the next chapter.

Attenuation

Due to various factors regarding the medium in which light energy travels it is to be expected that an object will not receive all of the light energy released in its direction from the source.

Distance is the most dominant factor in this situation, as the energy travels further it covers an increasingly large volume of space. Consider the following diagram:

Diagram 2.1.png
Diagram 2.1

The above diagram is based on a spot-light (discussed later) but is as applicable to other types of. The arc marked ‘A’ is closer to the light source than the arc marked as ‘B;’ the important observation is that arc ‘B’ is longer than arc ‘A.’ As the distance from the source is increased the area over which light energy is distributed increases and consequently each point will receive a smaller share of the available energy.

Atmospheric effects can also serve to scatter and absorb some of the light energy travelling along a particular ray – and beyond a certain point there will be no more energy for an object to receive. This can be a very complex effect to simulate and suits the global illumination transport methods discussed in the previous chapter. Luckily this particular effect is more of a special case such that omitting it doesn’t significantly degrade image quality.

Attenuation is the process of modelling this effect and simply states that the energy received by a surface is a function of the distance between the source and the receiver. A simple function in terms of distance is easy to implement and fast to execute – a more accurate solution would tend towards physically modelled lighting simulations.

A standard attenuation function is that used by the fixed-function pipeline from previous versions of Direct3D. This is a simple quadratic equation with three coefficients for shaping the actual distribution.

Equation 2.1.png

This can be implemented in HLSL as follows:

float Attenuation
    ( 
        float distance, 
        float range, 
        float a, 
        float b, 
        float c
    )
{
    float Atten = 1.0f / ( a * distance * distance + b * distance + c );
 
    // Use the step() intrinsic to clamp light to 
    // zero out of its defined range
    return step(distance, range) * saturate( atten );
}
Graph 2.2.png
Graph 2.2

The top-most curve in graph 2.2 uses coefficients of a = 0.0, b = 0.1 and c = 1.0 whereas the bottom-most curve uses 1.0 for all three coefficients – any object beyond 10 units from the light source receives almost no light energy at all. It is important to notice that the top curve doesn’t tend towards zero thus clamping at the limit of the light (as the preceding code fragment does) can introduce a very obvious discontinuity in the final image. Either manipulating the coefficients or designing a different function are required to avoid this problem.

Picking coefficients that produce the desired results in the final image is often difficult and requires some degree of artistic input. If this is not possible then modifying the function itself is required – a freedom that wasn’t available in the fixed-function pipeline that this approach originated from. An interesting avenue to explore is to try wildly different attenuation functions for special effects – animated sine waves, for example, make an interesting pulsing effect moving away from the light source.

Directional Light Sources

These are the simplest source of light to model – both in theoretical and practical contexts. However it is crucial to note that they aren’t really sources as such, in fact, they don’t really exist in the real world!

As the name suggests, this type of light is defined by a direction but crucially this type does not have an origin such that there isn’t actually a source for the energy distribution being modelled. What is actually being modelled is a special case approximation that is convenient when rendering graphics in real-time.

In order to understand this special-case it is necessary to make a brief jump forwards to point lights – these do have an origin from which light energy is distributed. Take the following diagram:

Diagram 2.3.png
Diagram 2.3

As the light source moves further away from the surface being lit the individual rays of light tend towards parallel. They will never actually be parallel to each other, but given a sufficiently large distance between source and receiver they are so close to parallel as to not make a difference. This can be shown mathematically using simple trigonometry:

Graph 2.3.png
Graph 2.4

Using the simple trigonometric equation:

Equation 2.2.png

The opposite side to the angle being measured is the distance and the adjacent side would be the distance from the point where the ray is perpendicular to the surface. If the ray’s were truly parallel they would be at 90o angles given this equation, and graph 2.4 shows that as distance increases the angle rapidly converges towards 90o. Because of this a directional light is assumed to have a point of origin infinitely far away such that the angle converges so close to 90o that it makes no observable difference to the image. This approximation is aided by the limited resolution of computer graphics displays – after a certain point the image we are rendering is simply incapable of showing any discernable difference.

Without a defined source it becomes impossible to consider attenuation as described in the previous section. This does not prove to be a problem as it was stated that the location of the light source would be a huge distance away from the target surface; thus referring back to the graphs from the previous section it can be shown that the attenuation would be so small as to be unimportant and is therefore another term that can cancelled out.

With so many terms cancelled out it might be difficult to understand how it could possibly be a useful model. In some respects it is best considered as a general form of lighting such as sunlight or moonlight in an outdoor scene. These uses will typically cover large areas of a scene and don’t tend to require detailed local lighting (such as a street lamp).

VS_OUT vsDirectionalLight( in VS_IN v )
{
    VS_OUT vOut = (VS_OUT)0;
 
    vOut.p = mul( float4( v.p, 1.0f ), mWorldViewProj );
 
    float3 vNorm = normalize( mul( float4( v.n, 1.0f ), mInvTposeWorld ) );
 
    float NdotL = max( 0.0f, dot( vNorm, -vLightDirection ) );
 
    vOut.c = float4( NdotL * cLightColour, 1.0f );
 
    return vOut;
}

As shown in the above vertex shader, the implementation of a directional light source is trivial. A simple dot-product between the vertex normal and the direction of the light is sufficient. There is a potential trip-up in the above code with the light’s direction vector – it is necessary to negate the vector passed from the application. It is conventional for the application to pass in a vector from the light, but the mathematics needs a vector to the light.

Image 2.5.png
Image 2.5

The above image shows the results of a single directional light source applied to a simple scene. Notice how the direction is apparent (one side of an object is lit and the opposite is not) but there is no ‘area of influence’ and the light energy affects the entire scene.

Point Light Sources

This type of light, often referred to as an “omni-light”, is one of the most versatile sources as well as being simple to implement. This type is defined by a single point (hence the name) which is the source of light energy as shown in the following diagram:

Diagram 2.6.png
Diagram 2.6

Simply put, the further away an object is the less energy it receives – regardless of direction from the source.

The mathematics behind point lights is a bit more involved than directional lights as the direction to the light changes based on the orientation between the vertex being lit and the position of the light. By comparison this term was constant for directional light sources.

Because point lights have a specific source it is possible to work out the distance between the source of light energy and the receiving vertex such that attenuation becomes a relevant term. Light energy is emitted equally in all directions such that attenuation is only a function of the distance which keeps it simple.

VS_OUT vsPointLight( in VS_IN v )
{
    VS_OUT vOut = (VS_OUT)0;
 
    vOut.p = mul( float4( v.p, 1.0f ), mWorldViewProj );
 
    float3 pWorld = mul( float4( v.p, 1.0f ), mWorld ).xyz;
 
    // Compute the attenuation
    float  fAtten = Attenuation
                    ( 
                        distance( pWorld, pLightPosition ),
                        fLightRange,
                        fAttenuation.x,
                        fAttenuation.y,
                        fAttenuation.z
                    );
 
    // Compute the direction to the light
    float3 vLight = normalize( pLightPosition - pWorld );
 
    // Compute the normal
    float3 vNorm = normalize( mul( float4( v.n, 1.0f ), mInvTposeWorld ) );
 
    float NdotL = max( 0.0f, dot( vNorm, vLight ) );
 
    vOut.c = float4( fAtten * NdotL * cLightColour, 1.0f );
 
    return vOut;
}

The above fragment of code is a complete implementation for a point light. Attenuation, as discussed at the start of this chapter, is handled by a helper function so as to keep the core vertex shader as simple as possible.

Image 2.7.png
Image 2.7

Depending on the scene being lit it can be quite apparent where the point-light is located within the scene. The previous image does not actually have a marker showing the point light’s location, but it is fairly obvious nonetheless.

Spot Light Sources

The third useful category of lights is effectively a hybrid of the two previously discussed. It has a point of origin but also has a direction along which light energy is projected. A common real-world example is a torch; however, this is really a point light in which most directions of light emission are blocked by the physical casing surrounding the light itself. Other chapters in this book cover shadowing techniques and can show how a point light could replace a dedicated spotlight which makes for a simple and uniform implementation it is a lot slower.

Attenuation for point lights is a 1D function defined in terms of distance from the source but spot lights extend this to a 2D function defined in terms of distance from the centre of the beam and distance from the source.

Diagram 2.8.png
Diagram 2.8

The preceding diagram shows the cone-shaped volume of a spot-light. Distance from the source to the current location being evaluated is easy to visualize, but the distance from the centre of the beam is better demonstrated by the following diagram:

Diagram 2.9.png
Diagram 2.9

Notice that both previous diagrams refer to an inner and outer cone – characteristics that may not be immediately intuitive. Consider the following diagram:

Diagram 2.10.png
Diagram 2.10

Referring back to the idea that a spot light could just be a point light within a surrounding that blocks light it is important to realise that the containing object (e.g. the casing of the torch) is going to reflect some light energy. The line across the top of diagram 2.10 represents the surface receiving the light – notice how the incoming energy, represented by arrows, is clustered towards the area immediately above the light source. Further along the surface the arrows become less dense until the geometry surrounding the light source completely occludes them.

This characteristic is supported in the real-world by many types of torch and household lamp having a reflective silver backing. Due to the angles involved, light energy gets focussed towards the direction normal to the reflecting surface which is also the direction for the spot-light. This gives rise to a significantly brighter central beam of light which is important to model – having an inner and outer cone yields good results despite being a simplification of the actual physics involved.

The spot light model being represented here is the same as existed in the fixed function pipeline found in previous versions of Direct3D. This decision allows re-use of previously tested and understood details.

Attenuation over distance can be controlled in the same way as point-lights, but the attenuation based on distance from the centre of the beam is more involved. Outside of the beam there is no light such that attenuation is zero and the inner beam is simplified by having an attenuation of one. The complexity is transitioning between the inner cone and the edge of the beam which is handled by the middle of the three cases below:

Equation 2.3.png

The angles θ and φ were introduced as part of diagram 2.8, but in the above equation they are divided by two in all cases. This reflects the fact that the variable angle – the one between the current sample and the spot-light’s direction, α – is mirrored about the direction whereas θ and φ in diagram 2.8 are not. A simple division by two solves this.

The middle case, when between the inner and outer cone, generates a values between 0.0 and 1.0 linearly distributed between the two defined cones. This is enhanced by raising the value to an exponent which allows for a variety of different fall-off patterns as shown by the following graph:

Graph 2.11.png
Graph 2.11

Choosing a low falloff exponent will make the spot light as a whole more pronounced due to the quick drop-off at the far right of graph 2.11 and will make the brighter inner cone less obvious. By contrast, a high falloff exponent will emphasise the inner cone’s brightness and make the edge of the outer cone more subtle and making it less obvious where the spot light’s influence finishes.

All of the above can be implemented in HLSL as follows:

VS_OUT vsSpotLight( in VS_IN v )
{
    VS_OUT vOut = (VS_OUT)0;
 
    vOut.p = mul( float4( v.p, 1.0f ), mWorldViewProj );
 
    float3 vNorm = normalize( mul( float4( v.n, 1.0f ), mInvTposeWorld ) );
 
    float3 pWorld = mul( float4( v.p, 1.0f ), mWorld ).xyz;
 
    // Compute the distance attenuation factor, for demonstration
    // purposes this is simply linear rather than the inverse quadratic.
    // This allows for the spot-light attenuation to be more apparent.
    float fDistance = distance( pWorld, pLightPosition );
    float fLinearAtten = lerp( 1.0f, 0.0f, fDistance / fLightRange );
 
    // Compute the direction to the light
    float3 vLight = normalize( pLightPosition - pWorld );
 
    // Determine the angle between the current sample
    // and the light's direction
    float cosAlpha      = max( 0.0f, dot( vLight, -vLightDirection ) );
 
    // Compute the spot attenuation factor
    float fSpotAtten = 0.0f; // default value simplifies branch:
    if( cosAlpha > fTheta )
    {
        fSpotAtten = 1.0f;
    }
    else if( cosAlpha > fPhi )
    {
        fSpotAtten = pow( (cosAlpha - fPhi) / (fTheta - fPhi), fFalloff );
    }
 
    // The final attenuation is the product of both types 
    // previously evaluated:
    float fAtten = fLinearAtten * fSpotAtten;
 
    // Determine the final colour:
    float NdotL = max( 0.0f, dot( vNorm, vLight ) );
    vOut.c = float4( fAtten * NdotL * cLightColour, 1.0f );    
 
    return vOut;
}

The mathematics for spot light attenuation use theta and phi as inputs and then compute their cosines – but in the above HLSL code relies on the application providing them in cosine form. Simply put, there is no point having the vertex shader evaluate a constant term on every iteration when the application can do it just once.

Image 2.12.png
Image 2.12

The preceding image shows three variations on a spot light. The top image shows a compact spot-light, the middle shows a broader spot light with a very low falloff exponent (giving the harsh outline) and the bottom image shows a broad spot light with a high falloff exponent.

Area Lights

The previous two light sources, spot and point, both have a single source of energy – an infinitely small location in 3D space. Whilst this is very convenient for computing the desired effects it is not accurate – in the real-world a small area will emit light energy rather than an infinitely small point. For example, a standard light-bulb might appear to match that of a point light but upon closer inspection it is a small filament that is emitting light energy. This filament has shape and area.

The term “area light” covers this type of light-source and due to it’s similarities with the real-world it becomes a desirable source to model. However they are more difficult to implement and due to the complexity they also tend to have substantial performance penalties.

Accurately modelling area lights is mathematically complex but there is a convenient simplification that yields sufficient results. By clustering point lights within the volume of the area light it is possible to have a simple implementation with modest performance penalties – depending on the arrangement and density of the point lights the cost of a single area light becomes a multiple of a point light. This approach is very similar to the multiple lights in a single pass discussed towards the end of the first chapter in this section.

Fluorescent strip-lights are a good demonstration of how this technique works. None of the sources discussed so far would be effective at simulating the effect of this common real-world light source. By positioning a number of point lights along the length of the strip-light it is possible to get close to the desired results.

Diagram 2.13.png
Diagram 2.13

The preceding diagram shows how this technique works; the top diagram shows the ideal distribution for a strip-light, the middle shows this approximated with only five point lights and the bottom with seventeen point lights. There is a fine balance to be controlled when using this technique – as more point lights are used the better the approximation but the more severe the performance penalties.

Relating to the theme of ‘Level Of Detail’ schemes, this balance can be controlled dynamically by the application such that a better approximation can be used where necessary – less point lights used when the area light is further from the camera and a larger number used when it contributes more to the overall image.

To implement this approximation to area-lights it is necessary to use shader model 4’s looping constructs. By assuming the application sets up the distribution of lights in an array the shader need only iterate over each array element and evaluate a point-light. It is important to note that each point light emits the total energy divided by the number of points lights used in the approximation. This is important because a constant emission per light will result in a brightness dependent on the number of lights used – in the context of LOD algorithms this would make the image change brightness according to the current level of detail.

The HLSL code for this type of light source is as follows:

VS_OUT vsAreaLight( in VS_IN v )
{
    VS_OUT vOut = (VS_OUT)0;
 
    vOut.p = mul( float4( v.p, 1.0f ), mWorldViewProj );
 
    // Compute as many values outside the loop as possible:
 
    float3 pWorld = mul( float4( v.p, 1.0f ), mWorld ).xyz;
 
    float3 vNorm = normalize( mul( float4( v.n, 1.0f ), mInvTposeWorld ) );
 
    float3 cTotal = float3( 0.0f, 0.0f, 0.0f );
 
    // Now enter the loop that evaluates each lights
    // contribution to the final result.
    for( uint i = 0; i < iAreaLightCount; ++i )
    {
        // Compute the attenuation
        float  fAtten = Attenuation
                        ( 
                            distance( pWorld, pAreaLight[i] ),
                            fLightRange,
                            fAttenuation.x,
                            fAttenuation.y,
                            fAttenuation.z,
                            false
                        );
 
        // Compute the direction to the light
        float3 vLight = normalize( pAreaLight[i] - pWorld );
 
        // Determine this light's contribution:
        float3 cLight = cLightColour;
        cLight *= max( 0.0f, dot( vNorm, vLight ) );
        cLight *= fAtten;
 
        // Add this to the total so far
        cTotal += cLight;
    }
 
    vOut.c = float4( cTotal, 1.0f );
 
    return vOut;
}

The vertex shader does not actually generate an arrangement of lights to fill a given area (although it could be designed to do this if necessary) – rather it simple reads positions from a pre-defined array. Consequently it is purely application-side work to change the distribution for the area light.

Image 2.14.png
Image 2.14

The number of point lights used to represent an area light is obviously an important factor – one that image 2.14 demonstrates very obviously. The top image has only three point lights that appear to be independent; the middle image has ten point lights and generates a better result despite obvious boundaries between lights still existing. The bottom image uses 25 point lights and gives a result much closer to the desired one.

Performance

This chapter began with a clear statement that different sources of light had different performance profiles. By examining the assembly listing output by the compiler the following table can summarise the relative costs:

Light Source Approximate Instruction Count
Directional 20
Point 38
Spot 46
Area 48

Due to the use of a loop for the area light implementation the value of 48 instructions is not entirely accurate as it could be anything from 48 to 549 instructions that are actually executed – all depending on how many point lights actually form a single area light. In the case of image 2.14 the actual executed instructions would be 87, 234 and 549.

It is also useful to note that this complex vertex shader will be evaluated for all vertices that make up a mesh – an obvious statement, but as will be shown in the following chapter a per-pixel light won’t be evaluated for any surfaces oriented away from the viewer. In all situations where performance is important any unnecessary computations should really be avoided!

References

[Lengyel06] “Advanced Light and Shadow Culling Methods” presented by Eric Lengyel at the 2006 Game Developers Conference. http://www.cmpevents.com/sessions/GD/S1508i1.ppt

Navigate to other chapters in this section:

Foundation & Theory Direct Light Sources Techniques For Dynamic Per-Pixel Lighting Phong and Blinn-Phong Cook-Torrance Oren-Nayar Strauss Ward Ashikhmin-Shirley Comparison and Summary