Lighting (XNA Game Studio 4.0 Programming) Part 1

To produce more realistic 3D objects, you need to add simulated lighting and shading to the objects you are drawing.There are many different types of lighting models that simulate how light works in the real world. Simulated lighting models have to strike a balance between realism and performance. Many of the lighting models used within 3D graphics are not based on how light physically works in the real world; instead, the models try to simulate how the light looks reflected off different types of objects.

We look at some of the common lighting models that are used in 3D graphics including those used in the built-in BasicEffect.

Ambient Lighting

The simple light to simulate is light that has no general direction and has a constant intensity in all directions on an object.This light is scattered many times meaning it has bounced off many other objects before hitting the final object you are shading.

In the real world, this type of light occurs when you are outside but in the shade. Although you are not in the direct sunlight, there is plenty of light that bounces around off other objects to light you and your sounding objects.

In 3D graphics, ambient lighting is used to give the lowest possible level of lighting an object can have in your scene when it is not lit by other types of lighting.The light value is represented by a Vector3 that describes the color in three colors: red, green, and blue using the X, Y, and Z properties.A value of 0 means no light, and a value of 1 means it is fully lit, which is not likely for your ambient light.


To see how ambient lighting looks when used in a game, let’s create a sample that displays four objects: a cylinder, a sphere, a torus, and a flat plane on the ground.You use these objects through your lighting examples while you continually add different types of lighting to the example.

The first step is to add the following member variables to your game class:

tmp14-6_thumb

The ambientEffect variable stores your custom effect. Update the name of the effect variable in each of the examples with the type of lighting you are using.The next two variables, model and modelTransforms, are used to store the Model and transform hierarchy for the ModelMesh’s contained within the Model.

The next three Matrix values are used to store the matrices to transform the vertices of the Model from local space into screen projection space.

The final two variables are used for the lighting calculations.The ambientLightColor represents the light color and intensity of the ambient light.The diffuseColor array contains the colors for the three objects you plan to draw. This is the color of the object if it was fully lit. Unlike the vertex color example, you set the color to draw the objects by passing the value into the shader as a global constant. If the model contained vertex colors, use those values instead.

Next, in the game’s Initialize method, set the initial values for the previous variables.

tmp14-7_thumb

 

 

tmp14-8_thumb

First, set the matrices to sensible values to frame the models close to the screen. Then, set the color of the ambient light. Use values of 0.4f or 40 percent.This value is combined with each of the color values set in the next array. Set red, green, blue, and purple values for each of the objects.

Next, load the custom effect file and the model file. In the game’s LoadContent method, add the following lines of code:

tmp14-9_thumb

Most of the code should look familiar.You load the Effect file that you will create shortly and a Model that contains the four objects that will display the lighting models. The final two lines of code populate a Matrix array with the local Matrix transforms for each of the MeshParts. Because the fbx file contains multiple objects, they are represented as separate MeshParts, each with its own local Matrix that is used to transform the object into world space.

The final piece of your game code is to update the game’s Draw method with the following code:

tmp14-10_thumb

 

 

tmp14-11_thumb

Let’s break the code down.

The first variable diffuseIndex is used to offset into the color array to set the color of each of the MeshParts in the Model.

The next section sets the Effect parameters that don’t change over the course of drawing the different parts of the Model. In this case, you set the View and Projection matrix along with the AmbientLightColor. Because setting effect parameters can be expensive, set only new values when you have to. In this case, set them at the highest level and once per use of the Effect.

The next section of code loops over all of the ModelMeshs in the Model.The World matrix is then set by combining the local transform from the ModelMesh with the world matrix for the scene.

The final section of code loops over all the ModelMeshParts within each of the ModelMeshs. Set the VertexBuffer and IndexBuffer, and then set Effect parameters that change per object you draw. In this case, set the color for each object before you call EffectPass.Apply. Each mesh is then drawn by calling DrawIndexedPrimitives.

You have all of the C# game code you need to draw your model using ambient lighting. Now you need to create the custom effect that you need to use with the previous bit of code. Create a new effect file as you have done previously in this topic and name it AmbientLighting.fx.

Along with the template global variables, add the following global variables:

tmp14-12_thumb

These store the ambient light and object color.

Leave the VertexShaderInput and VertexShaderOutput structures as is from the template along with the vertex shader VertexShaderFunction. Finally, update the pixel shader with the following code:

tmp14-13_thumb

 

tmp14-14_thumb

This is a simple pixel shader. It starts by declaring the variable that is used to store the return final color of the pixel.The ambient lighting is then calculated by multiplying the ambient light by the color of the object.The final color is then returned adding the fourth channel for alpha with a value of 1 for fully opaque.

Running this sample should display something similar to Figure 8.8.

Ambient lighting

Figure 8.8 Ambient lighting

Notice how each of the models is a constant color across the entire mesh.The direction of the triangle is not taken into account with this ambient lighting model.

Triangle Normals

For more realism, take into account the direction the triangle faces in regards to the light. To help determine the direction a triangle is facing, use the normal of the triangle.The normal contains only a direction and, therefore, should always have a length of 1. It is important that you normalize or set the length to 1 of a normal anytime you perform a calculation that alters the normal’s size.

There are two types of normals when working with a triangle.The first is called a face normal and is defined to be perpendicular to the plane that is defined by the three points that make up the triangle. Figure 8.9 shows an example of a face normal.

Face normal

Figure 8.9 Face normal

In real-time 3D graphics, the second type of normal called a vertex normal is used. Each vertex of the triangle defines its own normal.This is useful because you might want the object to appear smooth. In this case, the normal at a vertex is averaged with values from adjacent triangles to enable a smooth transition from one to the other. Figure 8.10 shows an example of vertex normals.

Vertex normals

Figure 8.10 Vertex normals

You can update the previous example to display the normal values of the mesh with just a few code changes. No changes need to be made on the game code side, and you need to make only a couple of changes to the shader.

Update the input and output vertex structures to the following:

tmp14-18_thumb

 

 

tmp14-19_thumb

The Normal value is added to each structure. The NORMAL semantic used in the input structure tells the graphics card that you want the normal data from the model. It is matched to the corresponding data from the VertexBuffer where the VertexDeceleration has set the normal channel.

Note

The model used in this example contains normal data. This exports from the modeling package used to create the model. If your model does not contain normal data, then you see an exception when you try to draw the model.

In the vertex shader, pass the normal data from the input structure to the output struc-ture.Add the following line of code before you return the output structure: output.Normal = input.Normal;

The normal data interpolates between each vertex across the triangle for each pixel. In the pixel shader, read this normal value and use the components of the vector as the red, green, and blue color.

Update the pixel shader with the following line of code that returns the normal data as a color:

tmp14-20_thumb

The normal needs to be normalized because the interpolation can lead to normals with length not equal to 1.The three components of the normal are then combined with an alpha value of 1 to color the pixel. If you run the example, it displays a rainbow of colors similar to Figure 8.11.

Diffuse Lighting

The term diffuse means to scatter or become scattered, so the diffuse light is reflected light that bounces off in many directions causing an object to appear to be flat shaded and not shinny. Ambient lighting, which gives a constant color across the triangles in a mesh diffuse lighting, differs depending on the angle of the triangle to the light source. Use Lambert’s cosine law, which is a common equation used to determine the diffuse color. This law states that the light reflected is proportional to the cosine of the angle between the normal and the light direction.

The type of lighting you are going to model first is called directional light.The light is considered to come from a constant direction in parallel beams of light.This is similar to how sunlight reaches earth.

Geometry colored by their normal values

Figure 8.11 Geometry colored by their normal values

Note

Sunlight is not parallel but for 3D graphics purposes, it can be treated that way because the size and distance of the sun is so great that the light appears to reach earth as parallel beams.

Because Lambert says you can use the cosine of the angle between the normal and the light, you can easily calculate this by taking the dot product of the normal and the light direction vectors. If both are unit length, then the dot product is equal to the cosine of the angle, which is the value you want.

Figure 8.12 shows the directional lights parallel rays hitting the triangle normals and the angle calculation.

Let’s add some diffuse lighting from a directional light to the previous example of ambient lighting. The first thing you need are some additional member variables in your game class.

tmp14-22_thumb

The first variable lightDirection is exactly what the name describes—the direction the light is going in.There are two ways to describe the direction of a directional light. The first is to describe the direction the light is moving in. This is the way we describe the light in the example.The second is to describe the direction to the source of the light like pointing towards the sun. This is the way you need the value in your shader so you can perform the angle calculation using the dot product.

Directional light hitting triangle

Figure 8.12 Directional light hitting triangle

The second variable is the color of the light. Lights don’t always have to be white; they can be different colors. Each color channel affects the same color channel of the object’s diffuse color.

In your game’s Initialize method, add the following lines of code to set the light’s direction and color. Note that the direction is normalized to keep the vector at unit length.

tmp14-24_thumb

The final changes you need to make to your game code is to send the values to the Effect. Set the LightDirection and DiffuseLightColor just after the other effect wide parameters as the following code shows.The light direction is negated to change it from pointing from the light to be the direction to the light.This is the format you need in your shader, so make it here instead of calculating the negation multiple times in the shader.

tmp14-25_thumb

Now, update your custom effect file to calculate the diffuse color in addition to the ambient color.

The first change is to add two new global variables that are used to store the light direction and color.

tmp14-26_thumb

Like the normal example, add the vertex normal to both the input and output vertex structures.

tmp14-27_thumb

The normal values also need to be passed from the input to the output structure in the vertex shader.

tmp14-28_thumb

Finally, update the pixel shader to calculate the diffuse color and output the color for the pixel. Update the pixel shader with the following code:

tmp14-29_thumb

First, the pixel shader normalizes the input normal.You need to normalize this value because the interpolation between vertices can lead to the vector not having a unit length. Then, set the minimum value for the diffuse lighting to the ambient light value. This is the minimum that the pixel can be lit. The additional light from the directional light is added to this value.

To calculate the light from the directional light, calculate the value of the dot product of the normal and the light direction. Use the saturate intrinsic function to clamp the value between 0 and 1. If the dot product is negative, then it means the normal is facing away from the light and should not be shaded so you want a value of 0 and not the negative value of the dot product.

The NdotL value is then multiplied by the color of the directional light and added to the diffuse light amount. The diffuse light amount is then multiplied by the diffuse color of the object itself to obtain the final color of the object.The final color is then returned with an alpha value of 1.

If you run the previous code sample, you should see something similar to Figure 8.13.

Directional diffuse lighting

Figure 8.13 Directional diffuse lighting

Multiple Lights

In the real world, you have more than one light source.You can also have more than one directional light. To add an additional light, add an additional light direction and color to your game class.

tmp14-31_thumb

Next, give them some default values in the game’s Initialize method.

tmp14-32_thumb

Then, send the values to the Effect.

tmp14-33_thumb

In the shader effect file, add the following two new global variables:

tmp14-34_thumb

In the pixel shader, calculate the dot product of the additional light and add the value to your diffuse light value before adding the value to the finalColor.

tmp14-35_thumb

Running the example now should show something similar to Figure 8.14.

Directional diffuse lighting from two light sources Oversaturation

Figure 8.14 Directional diffuse lighting from two light sources Oversaturation

As you add more lighting, the possibility of oversaturation becomes a concern. Notice that lighting is additive. As you add more lights, the final color channel values can go above 1, which is full color. As the values go above 1, no change in the final pixel color output the screen occurs. Differences of colors above 1 appear to be the same color to the user. Portions of an object might lose their definition becoming bright white or another solid color.You can limit oversaturation by lowering the amount of lights and keeping the color intensity values of the lights lower. Notice that the previous example used smaller values for the second light’s color. Often, you have one stronger light with an additional couple of weaker lights.

Next post:

Previous post: