3D Programming Demos and Tutorials

Basic Light-Sourcing
by Simon Brown

 

A useful addition
One further use for face normals is to compute face brightness with respect to the angle to a light source.  Once you know how to compute face-normals and determine triangle visibility with them, you already know the basic maths behind this type of light sourcing.  Hence you will need to have read the previous tutorial on backface culling before beginning this one.

The concept
We learned in the previous tutorial how to compute a face normal, and how to use that normal in conjunction with a camera-to-object vector to determine visibility.  We can use exactly the same technique to calculate the amount of light falling on a face from a light source.

If instead of using a vector from the camera to the object, we position a light source in global space, then transform it's position into camera space, we can then use a light-source-to-object vector in conjunction with the face normal to calculate how much light is hitting the face.  This is a fairly simple form of lighting, only considering the angle the face makes with the direction to the light source.  This element is typically called diffuse light.  It is also possible to consider both the camera-to-object vector and the light-source-to-object vector to compute specular lighting. This tutorial will only deal with the diffuse lighting element.

Also, I will leave up to you the implementation of how to use the lighting values we will produce, since that is platform dependent, and depends on the type of shading used.  Basically the following calculation will give a floating point value between 0.0 and 1.0 for the amount of light hitting a face.  It is also very simple to extend this method to take into account the distance between the object and light-source.  

The details - what is a light?
A light, as far as we're concerned here, is simply a position in global 3D space.  So the only elements it has are an x, y and z coordinate.  You could also easily adapt this method to include an intensity and a range for the light, and even coefficients for controlling fall-off and attenuation.

So, we have a light at x, y, z and a triangle with a face normal already calculated. The first thing we need to do is to compute a vector from the light-source to the triangle. This means we need to transform the light's position into camera space.  An alternative to this method would be to use the lights position in global space and the triangles position in global space.  There is little advantage to either method, so either will do.

The good news is that we already have a matrix for transforming a point from global space into camera space.  We can simply use the same matrix we used when we transformed our triangles from global to camera space.

So lets look at a data structure we could use to store a light.

struct light
{
   float x, y, z;         // x,y,z coordinate of light in global space
   float cx, cy, cz;      // x,y,z coordinate of light once transformed into camera space
};

And naturally the code for transforming the light using the matrix is the same as we saw before in the matrices tutorial, so there's no need to repeat it here.

We now need to use the transformed light coordinates cx, cy and cz to compute a vector from the light to any point on our triangle. The other data we will use is the camera space coordinates of the triangle and the face normal, again in camera space. So we can work out the vector like so-

// Local variables
float lv [3] = { 0.0f };

// Take Vector from light source to point 0 of our triangle
lv [0] = v[0].cx - omni.cx;
lv [1] = v[0].cy - omni.cy;
lv [2] = v[0].cz - omni.cz;


// Compute magnitude of vector
float mag = sqrt ( ( lv [0] * lv [0] ) + ( lv [1] * lv [1] ) + ( lv [2] * lv [2] ) );


// Normalize vector
for ( int i = 0; i < 3; i++ )
{
   lv [i] = lv [i] / mag;
}

where omni is of type light, lv is the light-source to object vector we are calculating, v is an array of vertices, v[0] is the first vertex of our triangle (in camera space), and v[0].cx is the camera space x-coordinate of the first transformed vertex.  All we are doing here is calculating a vector as we've seen before, then calculating it's length so we can normalize it (make it length 1.0).

Now all that remains is to work out the angle between the vector lv and the face normal.  Here's how-

// Compute angle between vectors
float result = ( cnx * lv [0] ) + ( cny * lv [1] ) + ( cnz * lv [2] );

// Check if surface is facing away from light source
if ( result > 0 )
{
   v.brightness = 0;
} else {
   v.brightness = - result;
}

where cnx, cny and cnz are the x, y and z component of the camera space face normal associated with our triangle, and v.brightness is a floating point value for storing the overall brightness of a face.  As you can see, we have used the dot product again, which computes the cosine of the angle between the two vectors.  In backface culling, we were only interested whether the result was positive (triangle gets culled) or negative (triangle is drawn), whereas in this case we are interested in the actual magnitude of the result.  Again, any result > 0 means the face is facing away from the light source, and hence receives no light from it.  If the result is less than 0 we simply store it in the variable brightness and flip the sign.  As the dot product calculates the cosine of the angle, which will naturally be in the range -1.0 to 1.0 there is no need to extract the actual angle, since we already have what we were looking for, a brightness value between 0.0 and 1.0.  All we need to do is switch the sign as we store the value.

Conclusion
If you understood the previous tutorial on backface culling then I'm sure this one was a piece of cake.  What you do with the brightness value depends on whether you are using flat or gouraud shading, and whether you are using texture mapping.  In the simplest case, that of flat shading, you might extract the RGB components from the face colour, multiply them by the brightness value, and then rebuild the new RGB value for the face colour.

In the next tutorial I intend to briefly leave behind platform independance and show you the techniques and code we have been through so far in action in a real program, probably using DirectDraw7.  This should help fill in any gaps that the previous tutorials may have left.

All content copyright © Simon Brown 1999-2005.
back to the introduction