Experiments In Game Programming
Main
 Home
XNA - C#
 Coming Soon!
 Level Editing - GTK
DirectX 9 - C++
 Downloads
 Disclaimer
 Introduction
Part 1 - DirectX
 1 - Breakout
 2 - Create DX
 3 - 2d Images
 4 - 3d Models
 5 - Cameras & Lights
 6 - Animation Timing
 7 - Keyboard/Mouse
 8 - Sound
Part 2 - Breakout
 1 - Art and Sounds
 2 - The Menu
 3 - Starting Breakout
 4 - The Level
 5 - The Paddle
 6 - The Ball
 7 - Finishing Touches

Untitled Document

Chapter 5

 

Cameras and Lights

 

 

As we saw in the last chapter we couldn't view our model without first setting up a view, and it would have rendered all black if we didn't have any lights. In this chapter we will create a Camera class to control the view, and also a Light Manager class to simplify creating and keeping track of multiple lights.

Topics Covered in this Chapter:
  • Creating A Camera Class
  • Creating A Light Class
  • Using our Camera and Light Classes.

Camera's and lights are fairly simple to create and control. A camera consists of 2 vectors, a position, and a target. A spotlight also consists of a position and target vector, and a point light only requires a position. We are going to write 2 classes, a camera class, and a light manager class. To keep it simple, the light manager class is only going to control point lights. However with a few simple modifications you could have it controlling directional lights as well.

Start by taking a look at the camera class in the chapter 5 code. The first function after the constructors, is create. Create takes the device and a near and far clipping range for the camera. The near and far clipping determines the depth at which the view starts and stops rendering. The main function of create is to create a projection matrix which sets up how the perspective is going to be calculated. We create the projection matrix with D3DXMatrixPerspectiveFovLH. This requires the angle of the field of view, (in the source below it is set to D3DX_PI / 4.0f, IE PI/4 or 90 degrees), The aspect ratio of the view (for simplicity I have set it to 4/3 which is the standard monitor ratio), and the near and far clipping discussed above. Once we have created our projection matrix, we use SetTransform, with D3DTS_PROJECTION to set the projection.
bool Camera::create(LPDIRECT3DDEVICE9 device, float nearView, float farView)
{
     myDevice = device;
     nearClip = nearView;//nearest point at which the objects stop rendering
     farClip = farView;//farthest point at which the objects stop rendering
     //the position of our camera
     position.x = 0.0f;
     position.y = 0.0f;
     position.z = 0.0f;

     //the lookat target of our camera
     target.x = 0.0f;
     target.y = 0.0f;
     target.z = 0.0f;

     float aspect = 1.333f; // the aspect ratio of the screen

     //Setup the Projection Matrix
     D3DXMatrixPerspectiveFovLH(&projectionMatrix, D3DX_PI / 4.0f, aspect,      nearClip, farClip);
     //Set the Projection
     myDevice->SetTransform(D3DTS_PROJECTION, &projectionMatrix);

     return true;
}

The next 2 functions are setPostion, and setTarget. Both take a vector to set the position in world space at where the camera is located at, or the position it is pointing at. Once the position or target is set, resetView is called to update the view on the device.

void Camera::setPosition(D3DXVECTOR3 newPosition)
{
     position = newPosition;
     resetView();
}


void Camera::setLookAt(D3DXVECTOR3 newTarget)
{
     target = newTarget;
     resetView();
}

The last function in the Camera class is resetView. resetView sets up the view matrix with a call to D3DXMatrixLookAtLH, and then sets it with SetTransform, and D3DTS_VIEW. The view matrix basically tells the device which direction it is looking in, and which direction is up.
void Camera::resetView(){
     //Setup the View
     D3DXMatrixLookAtLH(&viewMatrix,
     &position, //camera's position
     &target, //camera's target
     &D3DXVECTOR3(0.0f, 1.0f, 0.0f));// the up direction
     //Set the View
     myDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
}

 Next we look at the LightManager class. The LightManager class is just about as simple as the Camera class, the only complication being that we are going to allow for multiple lights, and store them using a standard vector storage class. The vector class is nice because we can push and pull objects on and off the vector and the memory is managed for us. Mallocing memory to dynamically add objects to arrays is not really something I enjoy. However the vector storage class is not to be confused with the D3DXVECTOR, which I have been referring to as a vector which is just points in space; hopefully the context will tell you which one I am talking about. Anyway, as you saw in the previous chapter, lights have half a dozen important properties we need to set when we create them. First, we have the color values, for Diffuse (basic color), Specular (highlight color) and Ambient properties, then the position and range and attenuation (how the light changes over distance) properties. The LightManager has functions to set all of these properties, as well as manage as many lights as we want. Generally each light adds a performance hit, so I wouldn't recommend more than a couple.

I have setup the constructor in the LightManager to take and save its own pointer to the device so that it isn't needed every time you make a call to the light manager. The constructor also turns lighting on in the render state.
LightManager::LightManager(LPDIRECT3DDEVICE9 device){
     //save the device for future use
     myDevice = device;
     //Turn on lighting
     myDevice->SetRenderState(D3DRS_LIGHTING, TRUE);
}

 createLight is the next function, and it sets up a new point light and sets default values for the color values as well as position, range and attenuation. The newly created light, is pushed onto our lights vector, and then SetLight, and LightEnable are called to set the light on the device, and then turn it on. Although SetLight looks like it is being called with something rather complicated it is just the index of the light, and the light pointer.The index happens to be the size of the vector - 1, and the pointer I am getting back out of the vector and needs to be cast as type D3DLIGHT9.
int LightManager::createLight(){
     D3DLIGHT9 *newLight = new D3DLIGHT9;
    
     newLight->Type = D3DLIGHT_POINT;
    
     //Create a new light, with some default settings, full diffuse and specular      with no ambient.
     newLight->Diffuse.r = newLight->Diffuse.g = newLight->Diffuse.b =      1.0f;
     newLight->Specular.r = newLight->Specular.g = newLight->Specular.b      = 1.0f;
     newLight->Ambient.r = newLight->Ambient.g = newLight->Ambient.b =      0.0f;
     newLight->Position = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );
     newLight->Range = 60.0f;
     newLight->Attenuation0 = 1.0f;
     newLight->Attenuation1 = 0.0f;
     newLight->Attenuation2 = 0.0f;
    
     //push the light onto our light vector
     lights.push_back((D3DLIGHT9*) newLight);

     //set and enable the light
     myDevice->SetLight((int)lights.size()-1, (D3DLIGHT9 *)lights[((int)lights.size()-1)]);
     myDevice->LightEnable((int)lights.size()-1, true);

     return ((int)lights.size() - 1);

}

The next 2 functions in the LightManger are enableLight, and disableLight. Both take the number of the light you wish to enable or disable, and call LightEnable with true or false respectively to turn the light on or off.

void LightManager::enableLight(int lightNumber){
     myDevice->LightEnable(lightNumber , TRUE);
}


void LightManager::disableLight(int lightNumber){
     myDevice->LightEnable(lightNumber , FALSE);
}

 The next 3 functions are setDiffuse, setSpecular, and setAmbient, all three take the same parameters (light number and red/green/blue value) and set their respective color values.
void LightManager::setDiffuse(int lightNumber, float r, float g, float b){
     lights[lightNumber]->Diffuse.r = r;
     lights[lightNumber]->Diffuse.g = g;
     lights[lightNumber]->Diffuse.b = b;
     //after we change the light we need to reset it.
     myDevice->SetLight(lightNumber, (D3DLIGHT9 *)lights[lightNumber]);
}

void LightManager::setSpecular(int lightNumber, float r, float g, float b){
     lights[lightNumber]->Specular.r = r;
     lights[lightNumber]->Specular.g = g;
     lights[lightNumber]->Specular.b = b;
     //after we change the light we need to reset it.
     myDevice->SetLight(lightNumber, (D3DLIGHT9 *)lights[lightNumber]);
}


void LightManager::setAmbient(int lightNumber, float r, float g, float b){
     lights[lightNumber]->Ambient.r = r;
     lights[lightNumber]->Ambient.g = g;
     lights[lightNumber]->Ambient.b = b;
     //after we change the light we need to reset it.
     myDevice->SetLight(lightNumber, (D3DLIGHT9 *)lights[lightNumber]);
}

The last 2 functions in the light manager are setPosition, and setRange. setPosition takes a position vector to specify the x,y,z position of the light in world space. setRange sets the range of the light (objects outside the range of the light do not get lit).
void LightManager::setPosition(int lightNumber, D3DXVECTOR3 newPosition){
     lights[lightNumber]->Position = newPosition;
     //after we change the light we need to reset it.
     myDevice->SetLight(lightNumber, (D3DLIGHT9 *)lights[lightNumber]);
}

void LightManager::setRange(int lightNumber, float newRange){
     lights[lightNumber]->Range = newRange;
     //after we change the light we need to reset it.
     myDevice->SetLight(lightNumber, (D3DLIGHT9 *)lights[lightNumber]);
}

 We now have a Camera class, and a Light Manager class. To test both of them, we are going to replace the call we made to quickAndDirtyViewSetup in the last chapter with a camera object, and a couple of lights managed by our LightManger.

We start by adding the classes to the GameMain header, and defining a Camera and LightManager object.
#pragma once
class dxMgr;
class dxText;
class Surface;
class Model;
class LightManager;
class Camera;

....

private:

dxMgr* dxManager;
dxText* textManager;
Surface* testSurface;
Model* testModel;
LightManager* lights;
Camera* camera;

};

 Last we remove the call to quickAndDirtyViewSetup, and add calls to create and set a Camera, and a Light Manager to the init function in GameMain. I have called create light 3 times and have set lights 2 and 3 to the left and right of the center. Light 1 remains at the default start location, which happens to be the center of our scene.

bool GameMain::init(HWND wndHandle)
{
     ....

     //Create a new Camera
     camera = new Camera();
     camera->create(dxManager->getD3DDevice(), 1.0f, 1000.f);
     camera->setLookAt(D3DXVECTOR3(0.0f,0.0f,0.0f));
     camera->setPosition(D3DXVECTOR3(0.0f,0.0f,50.0f));;


     //Create a light manager
     lights = new LightManager(dxManager->getD3DDevice());
     int light_one = lights->createLight();
     int light_two = lights->createLight();
     int light_three = lights->createLight();
     lights->setPosition(light_two, D3DXVECTOR3(25.0f,0.0f,25.0f)); //set the      light 25 units in the x, and 25 in the z
     lights->setPosition(light_three, D3DXVECTOR3(-25.0f,0.0f,5.0f)); //set      the light -25 in the x, and 25 in the z


    
     return true;
}

 Thats all we need to do our test. Compiling and running the chapter 5 code shows us the 2 spheres from chapter 4, now lit by 3 lights, and viewing for farther back.



Summing Up - Chapter 5

In this chapter we created a Camera, and a Light Manager Class. Both of which were not overly complicated, but will definitely be handy when we start programming an actual game. Before moving on I recommend playing with some of the light settings, such as colors and attenuation to get a feel for how they work. If you feel up to it try adding directional light support to the light manager, that's a light of type D3DLIGHT_DIRECTIONAL, that has an additional parameter vector of direction.
 ©2008 David Whittaker