Lighting

  CU_MDX_Lighting.zip (81.9 KiB, 2,749 hits)


  CUnitFramework.zip (101.1 KiB, 20,514 hits)

Extract the C-Unit Framework
inside the project (not solution) directory.

If you’ve played games like Doom 3, Splinter Cell, and Thief, you know how lighting can add a great amount of depth and realism to the game. Modern games are making great advances in real-time lighting through the use of shaders and normal maps, but before we go all crazy with lighting techniques, we should to understand how basic vertex lighting works in computer graphics and DirectX. In this tutorial, we’ll learn how to set up lights in DirectX so we can give our scene some improved realism.

Lights in the DirectX lighting system are per-vertex lights. That is, they do not “shine” across a surface, they actually “shine” on the vertices of polygons and then the light is interpolated across the surface. Lighting is calculated with the help of a vector stored in each vertex, called a vertex normal. The vertex normal is a unit vector perpendicular to the surface of a polygon that we want lit. For an in-depth explanation of the math involved in calculating the lighting for a vertex, I recommend buying Real-Time Rendering. Low poly quadBasically, light is calculated by measuring the angle between the direction vector of the light and the vertex normal. So if the light is facing the vertex head on, the angle between these vectors is 180 degrees and the light will be the brightest. Then as the angle approaches 90 degrees, the light gets dimmer. And when the angle is less the 90 degrees, the vertex is facing away from the light and won’t be lit at all.

Since lights have to hit vertices for the geometry to be lit, we can’t make sparse geometry and expect it to be lit realistically by the fixed-function lighting system. Say we had a quad made of 2 triangles such as the one in the diagram to the right. If a spot-light went across the surface of the quad and never touched a vertex, the quad would never be lit even though the light went directly over the quad. Therefore, more realistically lit geometry needs more polygons than unlit geometry. More polygons means slower performance. There’s always a tradeoff between quality and performance.

DirectX has 3 types of lights defined in the LightType enumeration: point, spot, and directional. A point light is just like a light bulb in that light emanates equally in all directions from a single point in space. A spot light is exactly what you would think it would be. You specify a direction and angle(s) for your cone of light and voila, you have a spot light. A directional light is kind of like the sun. It’s basically a big light that is really far away and points in a single direction. The light vectors in a directional light are parallel and is the least processor intensive.

One thing to remember about lighting in computer graphics is that there are no shadows unless you program them. So if one object is in front of another object, the object in the back will still be lit because its vertices are still facing the light.

While using lighting, surfaces use Materials to describe their interaction with light. Instead of specifying a diffuse color for each vertex, we describe a material for the vertices. Materials describe how vertices react to light (what colors are reflected and absorbed, shininess, etc.) So if I set a material that reflects all red light and absorbs all other light, the object would appear red (assuming the light is white). If the material reflects all light, the object will appear white, or if the object is textured, the texture will be lit.

In this tutorial, we’ll render two crates. One with 2 triangles per side and another with 800 triangles per side. Of course, 800 triangles for one side of a crate is a bit ridiculous, but it will demonstrate the effect that mesh density has on lighting. As a spot light moves across the two boxes, you can see that with a low polygon crate, the light is interpolated over half the surface since one polygon covers half the surface as shown in the diagram on the right side. With the high-polygon crate, the light shines only in its cone of influence because more vertices are hit by the light as shown on the left side.

You will also be able to change the light into a directional light or a point light, as well as adjust the cone angle of the light when it is a spotlight.

Lit high poly crateLit low poly crate

///
/// This event will be fired immediately after the Direct3D device has been
/// created, which will happen during application initialization. This is the best
/// location to create_ Pool.Managed resources since these resources need to be
/// reloaded whenever the device is destroyed. Resources created
/// here should be released in the OnDetroyDevice callback.
///

/// The Direct3D device
public override void OnCreateDevice( Device device )
{

// Clip…

// Well this isn’t going to be fun…
PositionNormalTextured[] verts = {

// Front face
new PositionNormalTextured( –1.0f, 1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 0.0f, 0.0f ),
new PositionNormalTextured( 1.0f, 1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 1.0f, 0.0f ),
new PositionNormalTextured( 1.0f, –1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 1.0f, 1.0f ),
new PositionNormalTextured( –1.0f, 1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 0.0f, 0.0f ),
new PositionNormalTextured( 1.0f, –1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 1.0f, 1.0f ),
new PositionNormalTextured( –1.0f, –1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 0.0f, 1.0f ),
// Right face
new PositionNormalTextured( 1.0f, 1.0f, –1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f ),
new PositionNormalTextured( 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f ),
new PositionNormalTextured( 1.0f, –1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f ),
new PositionNormalTextured( 1.0f, 1.0f, –1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f ),
new PositionNormalTextured( 1.0f, –1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f ),
new PositionNormalTextured( 1.0f, –1.0f, –1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f ),
// Top face
new PositionNormalTextured( –1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f ),
new PositionNormalTextured( 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f ),
new PositionNormalTextured( 1.0f, 1.0f, –1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f ),
new PositionNormalTextured( –1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f ),
new PositionNormalTextured( 1.0f, 1.0f, –1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f ),
new PositionNormalTextured( –1.0f, 1.0f, –1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f )

};

// Define vertex buffer
m_vb = new VertexBuffer( device, verts.Length * PositionNormalTextured.StrideSize, Usage.WriteOnly, PositionNormalTextured.Format, Pool.Managed, null );
GraphicsBuffer buffer = m_vb.Lock( 0, 0, LockFlags.None );
buffer.Write( verts );
m_vb.Unlock();
buffer.Dispose();

// Create the high-poly crate
verts = TriangleStripPlane.GeneratePositionNormalTextured( 21, 21 );
m_vbDense = new VertexBuffer( device, verts.Length * PositionNormalTextured.StrideSize, Usage.WriteOnly, PositionNormalTextured.Format, Pool.Managed, null );
buffer = m_vbDense.Lock( 0, 0, LockFlags.None );
buffer.Write( verts );
m_vbDense.Unlock();
buffer.Dispose();

// Create high-poly index buffer
ushort[] indices = TriangleStripPlane.GenerateIndices16( 21, 21 );
m_numIndices = indices.Length;
m_ib = new IndexBuffer( device, m_numIndices * sizeof( ushort ), Usage.WriteOnly, Pool.Managed, true, null );
GraphicsBuffer<ushort> iBuffer = m_ib.Lock<ushort>( 0, 0, LockFlags.None );
iBuffer.Write( indices );
m_ib.Unlock();
iBuffer.Dispose();

// Load the texture
m_tex = new Texture( device, Utility.GetMediaFile( “panel.jpg” ) );

}

As mentioned at the beginning of this tutorial, vertices need a normal in order to take advantage of lighting. Therefore, we will use the PositionNormalTextured vertex type. Once the vertices are defined, we shove them into a vertex buffer.

Creating each vertex of the high-density crate seems a bit tedious to me, so I wrote a class to do it for me programmatically. The TriangleStripPlane class creates an indexed triangle strip plane out of the specified number of vertices. I’m not going to go over how it is created as it is not relevant to this tutorial. I’ll go over it later in the terrain tutorial, but you can view it in the source code if you’re so inclined.

///
/// This event will be fired immediately after the Direct3D device has been
/// reset, which will happen after a lost device scenario, a window resize, and a
/// fullscreen toggle. This is the best location to create_ Pool.Default resources
/// since these resources need to be reloaded whenever the device is reset. Resources
/// created here should be released in the OnLostDevice callback.
///

/// The Direct3D device
public override void OnResetDevice( Device device )
{

// Clip…

// Create the light
device.Lights[0].LightType = LightType.Spot;
device.Lights[0].Diffuse = Color.White;
device.Lights[0].Range = 1000f;
device.Lights[0].Direction = new Vector3( 0.0f, –1f, 1.0f );
device.Lights[0].Position = new Vector3( 0.0f, 2.0f, –2.0f );
device.Lights[0].Falloff = 1f;
device.Lights[0].InnerConeAngle = Geometry.DegreeToRadian( 10.0f );
device.Lights[0].OuterConeAngle = Geometry.DegreeToRadian( 40.0f );
device.Lights[0].Attenuation0 = 1f;
device.Lights[0].Update();
device.Lights[0].Enabled = true;

// Set render states
device.RenderState.Lighting = true;
device.RenderState.Ambient = Color.FromArgb( 60, 60, 60 );
device.RenderState.FillMode = m_framework.FillMode;

// Create the material
Material material = new Material();
material.DiffuseColor = ColorValue.FromColor( Color.White );
material.AmbientColor = ColorValue.FromColor( Color.White );
device.Material = material;

// Clip…

}

Lights in DirectX are described by the Light class. The Light class contains various properties that affect different characteristics of the light. For example, if you want to use a spot light, you can specify angles for both the outer cone and the inner cone. Notice that when I set these values, I use the Geometry.DegreeToRadian method. Once all the properties are set, we need to call Light.Update so the new values take effect. To activate this light, we set its Enabled property to true. We also need to enable lighting in general. We can do this by setting the Lighting renderstate to true. While using lighting, it is often useful to set an ambient light. An ambient light lights vertices that are not directly hit by a normal light. This prevents the surrounding geometry from looking completely dark.

As stated earlier in this tutorial, we need to assign a Material in order for the vertices to reflect light. In this tutorial, I want the vertices to reflect all incoming light so I set the Diffuse and Ambient material colors to white. DirectX can only use one material at a time so we need to tell DirectX to use the material we just defined by setting the device’s Material property.

/// Updates a frame prior to rendering.
/// The Direct3D device
/// Time elapsed since last frame
public override void OnUpdateFrame( Device device, float elapsedTime )
{

// Move the light back and forth
float x = device.Lights[0].XPosition;
x += xVelocity * elapsedTime;
if ( x < –4.0f || x > 4.0f )
{

x -= xVelocity * elapsedTime;
xVelocity = -xVelocity;

}
device.Lights[0].XPosition = x;
device.Lights[0].Update();

}

We can update_ the properties of the light if we want the light to change. Here I am simply moving the light back and forth. Every time the light is updated, we need to call Light.Update or else the changes won’t take effect.

DirectX lighting is a simple means of lighting your scene. The number of active lights, however is limited depending on the user’s video card. To create_ more lights than this maximum value, you would need to create_ a system that activates lights that you can see or are close to and deactivates all the other lights. I think I’ll leave that as an exercise for you 🙂