The Ultimate DirectX Tutorial
Lesson 8: Rendering with Vertex Lighting
Lesson Overview

So far, we have programmed very simple 3D scenes using no lights. This meant that everything was lighted equally and brightly, leaving no room for realism. In this lesson, we are going to cover how to use lights to illuminate your 3D scene, allowing your models to look somewhat like they exist in the real world.

But before we dive into the code, we will cover a little bit of theory on how exactly light works in Direct3D. Then, once the mechanics are covered, we will put together another classic cube, this time illuminating it with realistic lighting.

Light in Nature and Light in 3D

If you have played many games, you may have noticed that the quality of lighting in those 3D games is not exactly as it would be in the real world. Indeed, 3D lighting currently comes nowhere near the real world. The amount of detailed mathematics involved in real-world lighting is so immensly complex that I can hardly imagine a computer dealing with it at all, not to mention dealing with it in real-time.

The Complexity of Real-World Lighting

Imagine what it would take to render exact, real-world light. First of all, a light would fall directly onto a surface, illuminating it somewhat. The light would pass through the air, bouncing off dust particles and scattering through the air in an incomprehensibly complex pattern. This would cause other surfaces to be illuminated, even though no light falls on them directly. Then there is the fact that objects reflect more light in a certain direction, giving it a shine and somewhat hiding the color beneath.

I could go on for five or six lessons just explaining the light phenomena seen in Image 9.1 alone (and there is a lot more to light than just that). However, to spare you the fried brains, I'm just going to get to the point:

Direct3D uses lights to approximate real-world lighting. It does this using a system of calculation that is much less time-consuming, and that can therefore be run in real-time. This system is built up of a set of types of lighting and types of light sources.

Types of Lighting

There are three types of lighting that can be used to approximate real-world light. They are called diffuse lighting, ambient lighting and specular lighting.

Diffuse lighting is light that more or less falls directly onto a surface. If you hold up your hand in a room with one light source, you will see that one side of your hand is lit, while the other side is not. That side of your hand is lit with what is called diffuse light.

Diffuse Light

Ambient lighting consists of light that is everywhere. Generally speaking, it is quite dark (though not totally dark). When you hold your hand up in that room with one light source, the dark side of your hand is ambiently lit, and the light that is reflected there is refered to as ambient light.

Ambient Light

Specular lighting is often refered to as specular highlight or, even more commonly, reflection. (Wow, big word, huh?) When light reflects off an object, ususally most of the light will go in one specific direction (rather than scatter everywhere evenly). The result is that each object has a little shine on it that comes from the light source.

Specular Light

Types of Light Sources

In order to produce the three types of light in the correct volumes and in the correct directions, there are several types of light sources, each with different properties, that produce the various types of light.

In order to produce the three types of light, there are several types of lights (or light sources) that emanate light. The various types of light sources, each emanating specific types and colors of light and then combined together with other light sources, can produce a realistic lighting scene. Well, perhaps not quite like the light from the first image above.

I think the best way to thoroughly explain light sources is to describe each type of light source, so here we go. The following are all types of light sources:

1. Ambient Lights
2. Directional Lights
3. Point Lights
4. Spot Lights

Ambient Lights

Ambient lights do just what they say: they make ambient light. Ambient lights are relatively simple. The only real adjustable property of an ambient light is its color, as it has no specific source or direction.

Usually this color is white or gray. The darker the gray, the darker the environment seems to be. Using pure white ambient light is the equivalent to just not using lights at all.

Directional Lights

Directional lights are similar to ambient lights in that both ambient lights and directional lights shine light from everywhere at once. The only difference is that directional lights, as the name implies, gives light in a specifc direction.

In other words, you can use this light source to illuminate a specifc side of every object in your 3D scene. This next image shows this.

Directional Light

As you can see, the light comes from everywhere, but only shines in one direction. This can be used to create a sunlight effect, where the sunlight comes from one broad direction.

Directional lights produce ambient, diffuse and specular light. If you already have an ambient light source in place, a directional light will produce additional ambient light when it is enabled, and not produce it when it is disabled.

Point Lights

Point lights are lights that have no specific direction, but instead have location, and emanate light in all directions from one specific point. They emanate ambient, diffuse and specular light.

Point Light

Most lights in your game (as in lamps, fires, etc.) will use this type of light source.

Spot Lights

A spot light is a rather specialized type of light. It has both location and direction, and thus produces a beam of light. This is useful for things like flashlights or vehicle headlights, and not much else.

Spot Light

Materials

A material is a description of how surfaces reflect light, and which colors are involved. In other words, you won't necessarily see this color. You will only see it when you shine a light on it.

Of course, a good 3D game always has lights shining on its objects, so materials are usually taken into consideration.

Let me use a couple of pictures to explain what a material is exactly. Let's say we have a white square. Let's give it a white material and illuminate it with a white light.

A Pure White Square

Just what we had before, a white square. The surface is exactly as it would have been without using lighting. However, let's now give the square a red material, while keeping the white light.

A Pure Red Square

Now we have a red square. What happened here? Well, the red material made the surface of the square entirely red. Only red light was reflect off, and so the surface appeared red. If we shined a completely blue light on this square, nothing would show up at all (because no blue reflects off).

However, if we added some blue and some green into the material, we would get a much more controllable color, as well as a much more realistic one.

A Reddish Square

This may look somewhat different to the pure red square, but by itself it's still quite red. Now what happens if we add a bluish light to the square. (A bluish light, not a blue light.)

Purple Square

Purple! Actaully it's still reddish, but any reddish surface with a bluish light on it appears purple. Realistic.

The moral of this little demonstration is to not have materials or lights that have only one primary color. Use white (or gray) as much as possible. I know we aren't up to the point of putting any color into lights or materials, but keep that in mind.

Fiddling with materials and lights is a bit like mixing paint in a room with funky lights. All kinds of things can happen. The paint will even sometimes go black, even though it's really a bright yellow (pure yellow paint mixed with a pure blue light).

Vertex Normals

The topic I will discuss in this section is one of those topics I wish Direct3D could make do without, for it can get annoying sometimes. The trouble is, it is absolutely necessary.

Consider this simple problem:

Suppose you have a single, square surface which is lit from one side by a soft, diffuse light.

A Diffusely Lit Square

It's a wonderful square we have, but it would be better if it rotated somewhat (stationary objects always bore me):

Some Less Diffusely Lit Squares

Naturally, the farther the square tilts, the less the square will be lit. The problem is, exactly how lit should this square of ours be as it tilts farther away from the light?

The answer lies in a geometrical term called a normal. A normal is a vector that is perpendicular to a surface.

So how is this an answer exactly? Take a look at this next diagram.

Sizing Up the Normals

As each surface tilts farther away from the light, the normal vector becomes more exposed to the light due to its 90° angle. Direct3D uses the angle of the normal in relation to the angle of the light to determine how brightly lit the surface should be. The less the difference in the angles, the more light there will be shining on the surface.

Unfortunately, Direct3D does not determine the normal of a surface for you. This is one of the more annoying parts about lighting. You must determine each surface normal for yourself. You do this by appointing a normal vector to each vertex around the surface. These normals are called vertex normals.

Vertex Normals

Each of the normals here are vectors set to (0, 1, 0), meaning they point directly up. A normal, which is usually one unit long, tends to point away from the triangle it goes to make up. So if this square were on its side, it might be (1, 0, 0).

Now, if this were a cube, it would look something like this:

A Cube with Normals

Notice that each vertex has three normals sticking out of it. This is because it controls the light on all three surfaces it connects to. Unfortunately, a vertex can only have one normal. This means that we will need to create three vertices for each corner of a cube.

Ok, enough with the theory. Let's get this into practice.

Creating Lights

Unfortunately, lighting is not so simple that it can be done with a single function call. In fact, there are quite a few things to do. Let's list them, then go over them in detail.

1. Setting Up a New Flexible Vertex Format
2. Turning the Lighting On
3. Setting the Ambient Light
4. Creating and Setting the Diffuse Light
5. Creating and Setting the Material

The first three are very simple, but the last two will require a whole function to themselves. But let's take the first ones first.

1. Setting Up a New Flexible Vertex Format

Once again the FVF code and the vertex format will change. This time we are going to take out the diffuse color and add a new property in: the vertex normal. Here is the new FVF code and vertex format:

struct CUSTOMVERTEX {FLOAT X, Y, Z; D3DVECTOR NORMAL;};
#define CUSTOMFVF (D3DFVF_XYZ | D3DFVF_NORMAL)

D3DVECTOR NORMAL;

This is a new one, although it is really small and straightforward. Here it is:

typedef struct D3DVECTOR {
float x, y, z;
} D3DVECTOR, *LPD3DVECTOR;

One word. Simple. Once you look at the finished program, you'll see how this is used when defining vertices. But for now let's move on.

2. Turning the Lighting On

This one is even easier. In fact, the function call is already there in the previous lessons. We just have to change one of its two parameters from FALSE to TRUE:

d3ddev->SetRenderState(D3DRS_LIGHTING, TRUE);

3. Setting the Ambient Light

Perhaps this one isn't quite as easy as step 2, but I don't think you'll mind.

What this step does is make sure that your objects are visible somewhat, even if no lights are shining on them directly. Although other lights can add to this ambience when they are included, this step sets the amount of light that will be present when no lights are included.

This step uses the same function, SetRenderState, however the first parameter changes to a different flag, D3DRS_AMBIENT. The second parameter is the color of the ambient light.

d3ddev->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(50, 50, 50));    // ambient light

As metioned before, a dark gray resembles a darkish environment, whereas a lighter gray (perhaps 200, 200, 200) resembles a well-lit, probably outdoor, environment.

4. Creating and Setting the Diffuse Light

This step is longer than normal. It's actually not that bad, it's just longer. I'll just show it to you and then go over each part of it. Because of the size and flexibility of this step, I'm going to give it it's own function called init_light(). Here it is:

// this is the function that sets up the lights and materials
void init_light(void)
{
D3DLIGHT9 light;    // create the light struct

ZeroMemory(&light, sizeof(light));    // clear out the light struct for use
light.Type = D3DLIGHT_DIRECTIONAL;    // make the light type 'directional light'
light.Diffuse = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f);    // set the light's color
light.Direction = D3DXVECTOR3(-1.0f, -0.3f, -1.0f);

d3ddev->SetLight(0, &light);    // send the light struct properties to light #0
d3ddev->LightEnable(0, TRUE);    // turn on light #0
}

The comments make it pretty simple, but I will go over each of the above in detail:

D3DLIGHT9

D3DLIGHT9 is a struct that contains all the information about a light, no matter its type. In this lesson, we will cover the properties required for a directional light. The other types will be covered in the next lesson. However, so far we have been introduced to the following contents of the struct:

Type

This one is easy. It consists of three flags that define each of the three types of light sources (excluding the ambient light source, which we have covered already). This table shows the available flags for this value.

ValueDescription
D3DLIGHT_DIRECTIONALCreates a directional light, a light that comes from everywhere at once, but shines only in one direction.
D3DLIGHT_POINTCreates a point light, a light that emanates equally in all directions from one exact point.
D3DLIGHT_SPOTCreates a spot light, a light that emanates in one direction from one exact point.

For this lesson, we will use D3DLIGHT_DIRECTIONAL, as the others have properties not covered so far.

Diffuse

This one is actually a struct in itself, which consists of four FLOATs. They are labeled r, g, b and a. We can guess what they stand for. Red, Green, Blue and Alpha. Easy?

Up until now, you have used colors on a scale from 0 to 255. However, when working with D3DLIGHT9, you usually use values from 0.0f to 1.0f. 1.0f is full color and 0.0f is no color, with decimals in between.

In this lesson, we have each primary color set to 0.5f and the Alpha (which is covered later) to 1.0f.

Direction

This value contains a D3DXVECTOR3 struct. This is almost the exact same struct we used for the vertex normals when rebuilding the FVF code. The only difference is that we can initialize it's values right in the same line of code.

The struct contains the exact direction the directional light will point in. In our code, we set the x-direction to -1.0f, the y-direction to -0.3f and the z-direction -1.0f.

Now we a couple of simple functions to cover.

d3ddev->SetLight(0, &light);

SetLight() is a function used to tell the Direct3D device about the light we are building. This function simply takes the properties we just constructed and stores them away on the graphics device.

The first parameter is an arbitrary number we give to the light. We are giving it 0. We could give it 42, but I find that number to be a little too philosophical. We'll stick to 0 for now. It doesn't matter what it is, so long as we remember it later.

The second parameter is the address of the struct we built. We'll just put '&light' in this.

Next function.

d3ddev->LightEnable(0, TRUE);

This turns the light on. By default, the light is off.

The first parameter is the light number. We used 0 before, so we'll use 0 here.

The second parameter is the state of the light. TRUE for on, FALSE for off. This function is useful if you have lights that turn on and off during the game. However, for our example, we will just turn it on and leave it on.

And that's all there is to building a directional light. However, there is one final step to do before you run the program.

5. Creating and Setting the Material

The last thing to do before we run the program once more is to add a material. Materials do not work in vertices like many other things do. What happens is we set a material, and then any vertex drawn after that will be drawn with that material. It will be drawn that way until we set a different material.

Let's do this like we did the light. I'll give you the code, then explain it. For this example I'm going to combine the light and the material into one function, which should make things simpler. Here is the function now:

// this is the function that sets up the lights and materials
void init_light(void)
{
D3DLIGHT9 light;    // create the light struct
D3DMATERIAL9 material;    // create the material struct

ZeroMemory(&light, sizeof(light));    // clear out the light struct for use
light.Type = D3DLIGHT_DIRECTIONAL;    // make the light type 'directional light'
light.Diffuse = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f);    // set the light's color
light.Direction = D3DXVECTOR3(-1.0f, -0.3f, -1.0f);

d3ddev->SetLight(0, &light);    // send the light struct properties to light #0
d3ddev->LightEnable(0, TRUE);    // turn on light #0

ZeroMemory(&material, sizeof(D3DMATERIAL9));    // clear out the struct for use
material.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);    // set diffuse color to white
material.Ambient = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);    // set ambient color to white

d3ddev->SetMaterial(&material);    // set the globably-used material to &material

}

Let's go over the new players:

D3DMATERIAL9

This is a very simple struct, which contains color structs for each type of lighting. What I mean by color struct is a struct that consists of r, g, b and a, just like in light.Diffuse. In our D3DMATERIAL9 struct, we use two structs:

material.Diffuse

This sets the color of diffuse light that will reflect off the surfaces rendered. The color put in here only has an effect on diffuse light. No other type.

For simplicity (and realism) we will set all the diffuse values of this material to 1.0f.

material.Ambient

Because we also have ambient light in our scene, we will need to include a material color for that type of lighting as well. We will set these values to 1.0f, just as in diffuse.

d3ddev->SetMaterial(&material);

This is the function that sets the material. It's only parameter is simply the address of the struct we put together.

And that really is all there is to a basic light. Let's try it out and make it into a finished program.

The Finished Program

In this lesson we're going to use the traditional cube. As usual, I've taken the code from the last lesson and made the changes in bold. Notice that the vertex and index buffers are both changed.

[Main.cpp]

If you run this code, you should get something like this.

A Lit Cube
Summary

Lighting is a very important part of 3D game programming. Without it, worlds just look fake. Lighting helps to bring the realism out and make it a more enjoyable experience.

Although you have learned the very basics of lighting, there is much, much more to the subject. We have more types of lighting and light sources to learn about. These will be covered next, and it will become easier. In the meantime, I suggest these exercises:

1. Make the cube red, but have it look realistic under colored lighting.
2. Make two directional-lights, giving an extra effect.
3. Try toggling the light and see what happens.
4. Have the direction of the light rotate in a circle.
5. If you dare, recreate the Hypercraft from Lesson 7, complete with vertex normals!
6. As an experiment, try scaling an object with light on it. It will produce an odd effect.

Once you're done, it's time to move onward. DirectX has only begun!

Next Lesson: More on Lighting

GO! GO! GO!