Lighting

  CU_Lighting.zip (49.2 KiB, 3,571 hits)

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 pixel shaders and normal maps, but before we can get to advanced lighting techniques, we need to understand how basic 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, 2nd Ed. 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. 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 (however, with lightmaps and shaders, we can get around this). More polygons means slower performance. There’s always a tradeoff between quality and performance.

DirectX has 3 types of lights: 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.

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.

Lit high poly crateLit low poly crate

#ifndef CUCUSTOMVERTEX_H 
#define CUCUSTOMVERTEX_H
#include "stdafx.h"
namespace cuCustomVertex 
{
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
Summary: Position 
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 
typedef struct Position 
{ 
public: 
    Position() : X(0), Y(0), Z(0) {} 
    Position( float x, float y, float z )  
        : X(x), Y(y), Z(z) {} 
    float X, Y, Z; 
} Position;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
Summary: Position and color 
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 
typedef struct PositionColor 
{ 
public: 
    PositionColor() : X(0), Y(0), Z(0), Color(0) {} 
    PositionColor( float x, float y, float z, DWORD color )  
        : X(x), Y(y), Z(z), Color(color) {} 
    float X, Y, Z; 
    DWORD Color; 
} PositionColor;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
Summary: Position, color, texture coordinates 
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 
typedef struct PositionColorTextured 
{ 
public: 
    PositionColorTextured() : X(0), Y(0), Z(0), Color(0), Tu(0), Tv(0) {} 
    PositionColorTextured( float x, float y, float z, DWORD color, float tu, float tv )  
        : X(x), Y(y), Z(z), Color(color), Tu(tu), Tv(tv) {} 
    float X, Y, Z; 
    DWORD Color; 
    float Tu, Tv; 
} PositionColorTextured;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 
Summary: Position, color, texture coordinates 
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 
typedef struct PositionNormalTextured 
{ 
public: 
    PositionNormalTextured() : X(0), Y(0), Z(0), Nx(0), Ny(0), Nz(0), Tu(0), Tv(0) {} 
    PositionNormalTextured( float x, float y, float z, float nx, float ny, float nz, float tu, float tv )  
        : X(x), Y(y), Z(z), Nx(nx), Ny(ny), Nz(nz), Tu(tu), Tv(tv) {} 
    float X, Y, Z; 
    float Nx, Ny, Nz; 
    float Tu, Tv; 
} PositionNormalTextured;
} 
#endif

I’m getting a bit tired of specifying all sorts of different vertex definitions, so I decided to write out a bunch of vertex definitions that I can just grab at will. In this tutorial, we will be using the PositionNormalTextured vertex definition since our vertices need a position, normal, and texture coordinates.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Summary:
This callback function will be called immediately after the Direct3D device has been created. This is
the best location to create D3DPOOL_MANAGED resources. Resources created here should be released in
the OnDestroyDevice callback.
Parameters:
[in] pDevice – Pointer to a DIRECT3DDEVICE9 instance
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
void CGameApp::OnCreateDevice( LPDIRECT3DDEVICE9 pDevice )
{

cuCustomVertex::PositionNormalTextured vertices[] =
{

cuCustomVertex::PositionNormalTextured(-1.0f, 1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 0.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, 1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 1.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, –1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 1.0f, 1.0f),
cuCustomVertex::PositionNormalTextured(-1.0f, 1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 0.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, –1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 1.0f, 1.0f),
cuCustomVertex::PositionNormalTextured(-1.0f, –1.0f, –1.0f, 0.0f, 0.0f, –1.0f, 0.0f, 1.0f),

cuCustomVertex::PositionNormalTextured( 1.0f, 1.0f, –1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, –1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, 1.0f, –1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, –1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, –1.0f, –1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f),

cuCustomVertex::PositionNormalTextured(-1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, 1.0f, –1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f),
cuCustomVertex::PositionNormalTextured(-1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f),
cuCustomVertex::PositionNormalTextured( 1.0f, 1.0f, –1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f),
cuCustomVertex::PositionNormalTextured(-1.0f, 1.0f, –1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f),

};
// Create low-poly crate
m_vb.CreateBuffer( pDevice, 18, D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1, sizeof( cuCustomVertex::PositionNormalTextured ) );
m_vb.SetData( 18, vertices, 0 );

// Create high-poly crate
cuCustomVertex::PositionNormalTextured* verticesDense = NULL;
CTriangleStripPlane::GeneratePositionNormalTextured( &verticesDense, 21, 21 );
m_vbDense.CreateBuffer( pDevice, 21 * 21, D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1, sizeof( cuCustomVertex::PositionNormalTextured ) );
m_vbDense.SetData( 21 * 21, verticesDense, 0 );

// Create high-poly index buffer
USHORT* indices = NULL;
int numIndices = CTriangleStripPlane::GenerateIndices( &indices, 21, 21 );
m_ib.CreateBuffer( pDevice, numIndices, D3DFMT_INDEX16 );
m_ib.SetData( numIndices, indices, 0 );
m_vbDense.SetIndexBuffer( &m_ib );

// Load the texture
char texture[MAX_PATH] = {0};
CUtility::GetMediaFile( “panel.jpg”, texture );
if ( !texture )
{

SHOWERROR( “Unable to find texture file.”, __FILE__, __LINE__ );

}
if ( FAILED( D3DXCreateTextureFromFile( pDevice, texture, &m_pTexture ) ) )
{

SHOWERROR( “Unable to create texture.”, __FILE__, __LINE__ );

}

}

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 that we defined above. 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 CTriangleStripPlane class creates a single indexed triangle stip 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.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Summary: Initialize application-specific resources and states here.
Returns: TRUE on success, FALSE on failure
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
BOOL CGameApp::Initialize()
{

// Create the light
ZeroMemory( &m_light, sizeof( D3DLIGHT9 ) );
m_light.Type = D3DLIGHT_SPOT;
m_light.Diffuse.r = 1.0f;
m_light.Diffuse.g = 1.0f;
m_light.Diffuse.b = 1.0f;
m_light.Direction.x = 0.0f;
m_light.Direction.y = –0.5f;
m_light.Direction.z = 1.0f;
m_light.Range = 1000.0f;
m_light.Falloff = 1.0f;
m_light.Attenuation0 = 1.0f;
m_light.Theta = D3DXToRadian( 10.0f );
m_light.Phi = D3DXToRadian( 15.0f );
return TRUE;

}

Lights in DirectX are described by the D3DLIGHT9 structure. The D3DLIGHT9 structure 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(Phi and Theta). Check out MSDN for the descriptions of each member.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Summary:
This callback function will be called immediately after the Direct3D device has been created. This is
the best location to create D3DPOOL_DEFAULT resources. Resources created here should be released in
the OnLostDevice callback.
Parameters:
[in] pDevice – Pointer to a DIRECT3DDEVICE9 instance
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
void CGameApp::OnResetDevice( LPDIRECT3DDEVICE9 pDevice )
{

// Clip…

// Set render states
pDevice->SetRenderState( D3DRS_FILLMODE, m_pFramework->GetFillMode() );
pDevice->SetRenderState( D3DRS_SHADEMODE, D3DSHADE_GOURAUD );
pDevice->SetRenderState( D3DRS_LIGHTING, TRUE );
pDevice->SetRenderState( D3DRS_AMBIENT, D3DCOLOR_XRGB( 100, 100, 100 ) );
pDevice->LightEnable( 0, TRUE );

// Set a material
D3DMATERIAL9 material;
ZeroMemory( &material, sizeof( D3DMATERIAL9 ) );
material.Diffuse.r = material.Ambient.r = 1.0f;
material.Diffuse.g = material.Ambient.g = 1.0f;
material.Diffuse.b = material.Ambient.b = 1.0f;
material.Diffuse.a = material.Ambient.a = 1.0f;
pDevice->SetMaterial( &material );

}

To enable lighting, we need to set the D3DRS_LIGHTING to TRUE and activate our light by calling IDirect3DDevice9::LightEnable. 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. Ambient lighting is created through the D3DRS_AMBIENT render state.

As stated earlier in this tutorial, we need to assign a material to the device in order for the vertices to reflect light. Materials are defined with the D3DMATERIAL9 structure. In this tutorial, we want the vertices to reflect all incoming light so we’ll 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 calling IDirect3DDevice9::SetMaterial.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Summary: Updates the current frame.
Parameters:
[in] pDevice – Pointer to a DIRECT3DDEVICE9 instance
[in] elapsedTime – Time elapsed since last frame
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
void CGameApp::OnUpdateFrame( LPDIRECT3DDEVICE9 pDevice, float elapsedTime )
{

// Move the light back and forth
static float xVelocity = 3.0f;
float x = m_light.Position.x;
x += xVelocity * elapsedTime;
if ( x < –4.0f || x > 4.0f )
{

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

}
m_light.Position.x = x;
m_light.Position.y = 3.0f;
m_light.Position.z = –5.0;

// Set the light to index 0 on the device
pDevice->SetLight( 0, &m_light );

}

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 IDirect3DDevice9::SetLight in order to assign the new light properties to the device.

Make sure to hit F2 to enable wireframe so you can see the difference in geometry.

Laters, C