.:[ Please always use http://www.32bits.co.uk to access the site. Thanks! ]:.
Last updated: 10:55 AM Monday October 2008
![]()
...to the final tutorial in series 3. This one is going to be a nice and easy tutorial for you, seeing as how the texturemapping one was a bit of a nightmare (and the next series is going to get real tough ;). This time we're going to discuss lighting. Lighting in D3D is really very easy once you understand the basic principles behind it. I'm also totally sick of sticking triangles on screen, so this time we're going to create a cube instead. Here's a screenie of the first part of the tutorial:
Fig 5.1: A lit & texturemapped cube. It must be 1992.
Believe it or not, by the time you complete this tutorial you will know about 90% of the principles required to make a terrain engine! So lets take a look at the source for this tutorial:
[ Download complete VC6 workspace as .zip ]
Lighting.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]D3DCube.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
D3DCube.h [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
Before we start talking about the lighting, I just want to recap how I created the cube. This should all be simple stuff to you now, so I won't be spending a lot of time on it. Instead, you should take a good browse through the D3DCube.cpp/.h and see for yourself.The first things to note are as follows. Number one is that this cube class is derived from CD3DObject - remember that from the last series? The reason for this is a cube is a really useful 3D object, and we'll be needing it more in the future. Number two is that I wrote this cube class very quickly, just to assist with demonstrating lighting. This means it can be improved massively, which I encourage you to do. Finally number three is that this cube is quite blatantly texturemapped. Therefore make sure you understand the concepts of texturemapping from tutorial 4 before continuing!
With that out of the way, lets take a look in D3DCube.h. First up is the custom vertex struct:
typedef struct _tagCubeVertex
{
D3DVECTOR vPos;
D3DVECTOR vNormal;
float tu, tv;
} CUBEVERTEX;
#define D3DFVF_CUBE (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1 | D3DFVF_TEXCOORDSIZE2(1))
The main thing to note here is the lack of a diffuse colour member in the struct (and FVF, of course). The reason for this is that when using lighting in D3D, materials and lights interact with the textures to produce the same type of colouring a vertex diffuse colour would. They can't produce exactly the same output for reasons you'll see shortly, so it is still possible to use vertex diffuse colours. Note that I'm also using D3DVECTOR instead of float x,y,z - this is for a timesaver trick you'll see in a second. You can also see I've added a member called vNormal, with the corresponding D3DFVF_NORMAL define. You'll see what this is for in a second, too.
Note that by default, D3D does not set up the renderstates to allow you to use vertex diffuse colours and materials at the same time. It will allow you to use vertex diffuse and lighting though. If you try and use diffuse & materials with the default renderstates, your object will totally ignore the material and lighting you set and just display as though lighting was disabled. Be careful, and set the right renderstates.
Here's the class members of our CD3DCube class:
class CD3DCube : public CD3DObject
{
private:
CUBEVERTEX m_Vertices[36]; // 36 vertices: 3 verts per tri, 2 tris per face = 6,
// 6 faces = 36 vertices.
LPDIRECT3DTEXTURE9 m_pTexture; // texture surface for texturemapping
LPDIRECT3DVERTEXBUFFER9 m_pVertexBuffer; // Vertex buffer for our vertices
All simple enough. A member var for our texture surface, a member var for our vertex buffer and a member var array for our vertices. Note that I've used 36 vertices for the cube, using the following logic - 3 vertices per triangle, 2 triangles per cube face. A cube has 6 faces, therefore we need 36 vertices. That's the bad, unoptimised but clear way of doing it. It's actually possible to draw a cube using a lot less vertices than that - if you don't require texturemapping you can create a cube with only 8 vertices as all 8 are shared, but that's a subject for another tutorial.Let's move onto the class methods. We can quickly skip over the 2 SetTexture() methods, they simply load a texture surface pointer into our m_pTexture var. Have a browse through them yourself, they're well commented. The interesting points to note is that the overload that loads a texture from disk uses a managed surface, so we don't need to worry about reloading the texture in case of a lost device (we discussed lost devices in Series 1 and Series 2 Tut 5). We can also skip over the RestoreVolatile() and ReleaseVolatile() methods, as there's nothing in them! If you don't recall their purpose, reread Series 2 Tut 5.
That leaves us with the Render() and Initialise() methods. I'll cover the Render() method first, because that's the easiest. You've seen all of the rendering code already in other tutorials, so I won't waste time recapping it. If you're still unsure then any of the tutorials in this series will go through the process for you to review. There is one line of code I'd like to point out:
rslt=pDevice->SetTexture(0, NULL);
It's pretty obvious that this code sets the current texture to NULL, ie: no texture. We do this to be nice to other classes. Because D3D uses textures on a global basis, ie: once you set a texture it's used for texturemapping until you change or remove it, we want to make sure that any other rendering we do after this isn't inadvertantly forced to use the cube texture. Normally we'd be setting a texture anyway (or the primitives wouldn't have texcoords), but just in case we do forget we won't be seeing some strange behaviour. The easier we make things, the better!Now, the Initialise() method. This one is a beast. It's large, but simple. First up, here's the prototype:
HRESULT CD3DCube::Initialise(float Width, float Height, float Depth, float x, float y, float z, LPDIRECT3DDEVICE9& pDevice);
The parameters are as follows: the width, height and depth of the cube in object space units, the XYZ location in object space of the cube, and a pointer to the D3D device. Normally your XYZ values will be your Width, Height and Depth parameters divided by 2, to ensure the cube is centered around 0,0,0 in object space (remember the discussion about centering objects in object space back in tutorial 3?). Onto the method code:
D3DVECTOR p0={ x, y+Height, z};
D3DVECTOR p1={ x+Width, y+Height, z};
D3DVECTOR p2={ x, y, z};
D3DVECTOR p3={ x+Width, y, z};
D3DVECTOR p4={ x, y+Height, z+Depth};
D3DVECTOR p5={ x+Width, y+Height, z+Depth};
D3DVECTOR p6={ x, y, z+Depth};
D3DVECTOR p7={ x+Width, y, z+Depth};
This is just a little trick to make our lives easier. As I mentioned above (and as you can prove yourself), a cube only has 8 unique vertices. Those 8 vertices are shared by loads of faces and vertices, but we still only need 8 to define a cube. Here I've created 8 D3DVECTOR vars (which are just fancy structs holding 3 floats) to represent our 8 unique vertices. If we then use these 8 vars to assign our vertices, should we ever want to change the vertice order all we need to do is edit these 8 vectors, rather than 36 lines of floats. Here's how we define the front of the cube:
// Front of the cube
m_Vertices[0].vPos=p0;
m_Vertices[1].vPos=p1;
m_Vertices[2].vPos=p2;
m_Vertices[3].vPos=p2;
m_Vertices[4].vPos=p1;
m_Vertices[5].vPos=p3;
That should be enough to show you the vertice assignments - FYI they are all in clockwise order for CCW culling. Here's the cube in wireframe with the vertices marked, so you can get a better idea:
Fig 5.2: Cube vertices
The D3DVECTOR vars are marked on the pic. The cube itself is outlined in bright white, and the fainter lines show the tri's used to make the cube. So that's our vertex positions done, what about our normals. "What are normals?" I hear you say. Read on!Normals come in two flavours - face normals and vertex normals. As you can probably guess, a face normal applies to a primitive as a whole, whilst vertex normals apply to individual vertices. When I say "apply", I mean that for every face/vertex you have one normal. Using our favourite shape, here's a diagram of face and vertex normals:
Fig 5.3: Normals
Fig5.3 shows a triangle lying on the Z plane (plane? What's a plane? You haven't read the Cheaters Guide to 3D Maths, have you ;). As you can see, each vertex has a normal, and the face of the triangle has a face normal. The normals point straight up (instead of straight down) because I've decided that this is a front face (opposite to a back face, as in back face culling). We'll discuss making them point in different directions in a minute.
99.9% of the time your normals will all be unit vectors. This means they have a magnitude of 1, as discussed in the 32Bits Cheaters Guide to 3D Maths. It is possible (but very unlikely!) that at some point you'll want to make your normals non-unit vectors to produce some strange effects. I can't think of a good reason to though, especially if you're using shared vertices. FYI, the action of turning a "standard" vector into a unit vector is called "normalisation", which has nothing to do with lighting at all!
So, we know from the code above that a normal is a vector, but what is it used for? In a nutshell, normals are used to calculate the angle between a light source and any object the light hits. This lets us calculate the brightness of any point in the light source. Which set of normals is chosen depends on the type of lighting you want. By default, if you don't specify any vertex normals, D3D calculates the face normal for you. This is always at 90degrees to the face itself, and because we are only using one normal for an entire primitive (as opposed to 3 normals, one for each vertex), the lighting will appear constant across the entire face. This is called flat shading, and is the default "mode" in D3D when you don't supply vertex normals.However, if you supply normals for your vertices you can create a much nicer effect. Because your normals can point in different directions for each vertex, you can change the shading across the face of the primitive (much like specifying different vertex diffuse colours creates a blended colour fill). D3D calculates the lighting values (the brightness, effectively) for each vertex, and then interpolates it across the face of the primitive. Let's take a look at a practical example:
![]()
Fig 5.4: Flat shading with a face normal
Here we can see a primitive with one face normal, at a right angle to the face. When a light source shines on it, because D3D only has one normal to calculate the brightness of the face there is nothing to interpolate, thus the face appears flat-shaded.
Fig 5.5: Gouraud shading with vertex normals
Here's a primitive with vertex normals, represented by the green arrows. They point in different directions, and therefore are all at different angles to the primitive face. D3D takes the light source, and calculates the angle between a vector from the light to the vertex (shown in yellow), and the vertex normal. I've marked this angle in black. Larger angles between the vertex normal and the light vector causes less light to hit the vertex and therefore the vertex becomes darker, as shown by the bottom left vertice. Smaller angles between the vertex normal and the light vector causes more light to hit the vertex, therefore the vertex becomes brighter (bottom right vertice). Once the brightness has been calculated for all 3 vertices, it's interpolated across the rest of the face (just like a vertex diffuse colour). This produces a nice gradient shading effect, called Gouraud shading.Okay, we know what vertex and face normals are, and we know D3D handles the face normal for us if we're doing flat shading. But how do we know where to make our normals point if we're using vertex normals? Well, at this point I'm going to tell you a general rule of how to manually specify vertex normals for the cube. I've done this in the sample code so it's clear what we're acheiving in this tutorial. For the cube in the example code, I specified vertex normals that were at right angles to the primitive faces - in other words, all the vertex normals were the same as their face normals. This produces a sharp lighting effect, and keeps each face of the cube lit seperately. Because (of course!) the cube faces are all at 90degrees to each other, this technique works. However, this is a really bad idea for 2 reasons. Firstly we have to manually calculate the normals ourselves, which is a pain and a waste of time. Secondly, this only works because the cube faces are at a sharp angle to each other - if we tried to set the vertex normals to the face normals of an object that had shallower angles between the faces (take an 8 sided pyramid shape as an example), the lighting would look really wrong. Thirdly, this technique does not work on shared vertices. We haven't discussed shared vertices yet (that will come in a later tutorial), but as you can see it's not a good idea to guess at and manually specify vertex normals. At the end of this tutorial I'll show you how to correctly calculate the vertex normals.
For the moment then, you'll have to take it on faith that vertex normals in the example code are correct. Normals are a major part of lighting though, so by the end of this tutorial you'll understand how to calculate them. Let's see how we specify the vertex normals for our cube in practice:
// Front normals
m_Vertices[0].vNormal=D3DXVECTOR3(0.0f, 0.0f,-1.0f);
m_Vertices[1].vNormal=D3DXVECTOR3(0.0f, 0.0f,-1.0f);
m_Vertices[2].vNormal=D3DXVECTOR3(0.0f, 0.0f,-1.0f);
m_Vertices[3].vNormal=D3DXVECTOR3(0.0f, 0.0f,-1.0f);
m_Vertices[4].vNormal=D3DXVECTOR3(0.0f, 0.0f,-1.0f);
m_Vertices[5].vNormal=D3DXVECTOR3(0.0f, 0.0f,-1.0f);
Here's a sample of the normal code, showing the normals for the front of the cube. As you can see they are all unit vectors at 90degrees to the face - as above, they are all the same as the face normal. Rather than replicate all the code, here's the cube with the normals highlighted in green:
Fig 5.6: Cube with vertex normals
As mention above, because we're not sharing vertices, the normals are all at 90degrees to the face. Back to the code! The last thing we need to do is to specify texture coordinates for the cube. If you understood everything in tutorial 4, you should have no problems reading this code. We're just repeating the texturemapping for a quad in tutorial 4 for every face on the cube. Because the vertices on all the faces have the same set of texture coordinates, we can use a nifty bit of code to save retyping the same texcoords repeatedly. We specify the first set of texcoords manually, then loop through the remaining vertices setting their texcoords to the same as the first set. In other words, the texcoords for vertex 1 - the top left of the front face of the cube - will be (0.0, 0.0). These are exactly the same texcoords for the top left of all the other cube faces, so we can just copy the first vertex's texcoords to the top left vertice of every other cube face. The same principle applies for all the other vertices. Follow the code through and you'll understand it!The last part of our CD3DCube::Initialise() method simply copies our vertex array into a vertex buffer. We've been through this code a million times now, so I'll leave you to read through it.
So, we know how our cube is initialised, so it's time we took a look at the lighting. From this point on, D3D will not light the scene for us. This means that if we were to add no further lighting code and compile & execute our prog, our scene would appear completely black! Lighting is now our responsibility! To that end, the first thing we should do is to set the colour and level of ambient light in the scene. But what is "ambient light"?
Good question, it's time for some definitions. In D3D, scene lighting can be roughly split into 2 categories - direct light and ambient light. Direct light is cause when a light shines directly upon a surface, as you'll see shortly. Ambient light is the overall level of light in a scene where a direct light does not shine on a surface. For example, during the day it's bright enough for you to walk around your house without the lights on, yet sunlight does not directly shine into every corner of your home. This is the ambient light level. D3D allows you to set the ambient light colour and level via the SetRenderState() call shown above. As a couple of practical examples, if you were creating this indoors-during-the-day scene, you might set the ambient light to D3DCOLOR_XRGB(255,255,255), which is a full brightness white light. This produces the same lighting you see when D3D lighting is turned off. However if you were creating a scene inside a nuclear reactor, the scene might be glowing deep red. To produce this effect you may want to use D3DCOLOR_XRGB(200, 0, 0) for your ambient light. Finally, Doom 3 is a good example of a dark scene. Because Doom 3 is completely in shadow and only lit by explicit lighting, it has no ambient light level. Therefore you would use D3DCOLOR_XRGB(0,0,0). The easiest way to explain ambient light is for you to play around with the colour values. In a sentance, ambient light can be explained as the minimum amount of light used in a scene.
Within the boundaries of direct light, we have the following "sub-types". First of all, "diffuse lighting". The diffuse colour of a light is the actual colour the light emits. So, a direct light with a diffuse colour of D3DCOLOR_XRGB(0,255 0) emits a bright green light. Next is "ambient lighting". The definition of ambient lighting for direct lighting is the same as in the previous paragraph - the light itself emits ambient light in the directions it does not directly shine. This property is only useful for certain types of light, which we'll see in a second. Lastly we have "specular highlighting". This is a neat type of lighting effect that can be used to make objects appear metallic and shiny. When a light shines onto a surface at precisely 90 degrees, it can produce a specular highlighting effect where the light's intensity is increased dramatically. The code for this is included in the workspace for this tutorial, but here's a screenshot to show you what specular highlighting does:
Fig 5.7: Cube with specular highlighting
In the screenshot above, the light has hit the vertex at precisely 90 degrees, and specular highlighting has massively increased the brightness at that vertex. As you can see, this gives the cube a metallic, shiny look.That's the sub-types of direct light. But how do we create the direct light itself? Well, we have various options. D3D allows us to choose from 3 different types of lights - point lights, spot lights and directional lights. Unfortunately one of them is useful, one of them is okay but buggy, and one of them is totally pointless.
Starting with the useful one - directional lights. A directional light emits light consistently in one direction across an entire scene. They do not have a source, they just emit a constant amount of light in the direction you specify. They are good for replicating distant light sources such as the sun, which (by the time it's rays hit the earth) lights in a constant direction and intensity.
The buggy one is point lights. Point lights are a built like standard household lightbulbs, they have a source and emit light equally in all directions. I'll be frank here and say that technically they are not buggy. The problem with point lights is that light they emit does not get blocked by primitives. For example, place a point light next to a triangle on screen, and the light will appear to shine right through it. The reason for this is that D3D lighting does not do testing on the raycasting of a light. There are ways to get point lights to only radiate light in certain areas, such as stencil buffers, but that's a topic we'll discuss in a different tutorial.
The useless one is spot lights. They work in exactly the way their name suggests, just like a normal spotlight in a theater (or prison camp, depending on your point of view). They have a source, a strength and a direction, and a couple of extra properties called umbra, penumbra and falloff. I'm not going to bother explaining what those are. Instead, I'll explain why you really don't want to use a spot light. The main reason is procesing power. Creating and maintaining a spot light takes a ridiculous amount of processing power for the result you get, so instead we cheat. The easiest way to create a spotlight effect without a spotlight is to use something called a lightmap. This is simply a texture that looks like a light shining on it. You blend this lightmap texture with the "real" texture you want to use, and you get an effect which looks pretty much the same as a spotlight, but for much less processing cost. Here's a typical lightmap:
Fig 5.8: Typical lightmap
You already know how to do this, it's just a simple blend of 2 textures just like tutorial 4 showed you. This technique forms the basis of many effects such as dark mapping, glow mapping and detail mapping.Now you know the theory and terminology behind lighting. Time to go back to the main code in Lighting.cpp and see how the code fits into all of this! The good news is that once you've set up your vertex normals, the hardest part is out of the way. All we need to do now is tell D3D what we want to do with out lighting. This is all done in the GameInit() function. First up we set our render states:
// Set our culling & lighting renderstates
g_pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
g_pDevice->SetRenderState(D3DRS_LIGHTING, TRUE);
// Set up the level of ambient light in the scene
g_pDevice->SetRenderState( D3DRS_AMBIENT, D3DCOLOR_XRGB(100,100,100));
We want to turn on lighting from now on, so we just make a simple change to our SetRenderState() call to set D3DRS_LIGHTING to TRUE. Next we want to set the colour and amount of ambient light in the scene (as above, this is the general brightness of objects that do not have lights shining on them). Obviously specifying 0 for any of the RGB values means none of that colour component will be used for ambient light, and specifying 255 for any of the RGB values means that the colour component will be at full intensity. For example, specifying D3DCOLOR_XRGB(0, 127, 255) will produce an ambient light with no red component, a medium green component and a full intensity blue component, which will probably result in a dark cyan colour light. As a rule you don't want to specify D3DCOLOR_XRGB(255, 255, 255) for your ambient light, because the scene will then be fully lit and any other lights you specify will have no effect! I can only think of one reason you may want to do it, which is to use specular highlighting in a fully lit scene. We'll get to that!Once we've set our render states, the next step is to create our lights. Now that you know the theory, the practical coding is easy!
// Set up our light
D3DLIGHT9 Light;
ZeroMemory(&Light, sizeof(D3DLIGHT9));
// Create a white, single direction light
Light.Type=D3DLIGHT_DIRECTIONAL;
Light.Diffuse.r=1.0f;
Light.Diffuse.g=1.0f;
Light.Diffuse.b=1.0f;
Light.Position=D3DXVECTOR3( 3.0f, 2.0f, -3.0f);
Light.Direction=D3DXVECTOR3(-0.5f,-1.0f, 1.0f);
The code is fairly straightforward. First of all we create a var of type D3DLIGHT9, which is just a fancy struct with a bunch of member vars representing the configuration of your light. Next, I use the ZeroMemory() function to set all the members of the struct to 0. I've discussed the reason for doing this in previous tutorials, but to quickly recap it allows us to only set the member vars we require and be sure that we're not passing D3D junk in the other vars we don't set (which could potentially crash our code). I'll take this opportunity to show you the D3DLIGHT9 struct:typedef struct _D3DLIGHT9 {
D3DLIGHTTYPE Type; - Type of light
D3DCOLORVALUE Diffuse; - RGB diffuse colour of the light
D3DCOLORVALUE Specular; - Specular colour of the light
D3DCOLORVALUE Ambient; - Ambient colour of the light
D3DVECTOR Position; - Position of the light (spot & pointlights only)
D3DVECTOR Direction; - Direction of the light (spot & directional only)
float Range; - Distance the light shines (spot & point only)
float Falloff; - "Cone" properties of a spotlight
float Attenuation0; - Changes light intensity over distance (spot & point)
float Attenuation1; - Changes light intensity over distance (spot & point)
float Attenuation2; - Changes light intensity over distance (spot & point)
float Theta; - "Cone" properties of a spotlight
float Phi; - "Cone" properties of a spotlight
} D3DLIGHT9;
The blue bits are my additions to give you some insight into the workings of D3D lights. I'm a fan of directional lights and nothing else for the fixed pipeline, so that's what we'll be concentrating on. You'll see everything necessary for directional lights in this tutorial - as for the other types of light, I'll leave that to you to investigate!So that's the light structure, time to set our light up. As discussed above, I've chosen a directional light for this scene. That means that light will pass through the entire scene in one direction. Next we set the light's diffuse colour - this is the actual colour the light will emit, as discussed above. Note that I only specified an RGB value for this light - the alpha value has no effect on lights. Also note that the RGB value is on a scale of 0.0 to 1.0, with 0.0 being black and 1.0 being white. Don't ask why Microsoft suddenly decided that 0-255 wasn't good enough.
Next I do something that is technically pointless, but helps me to visualise the scene. As you should know from the discussion of directional lights above, they actually have no position, just a direction. However I generally set a position on my directional lights so I can picture how light will enter my scene (along with the Direction member, of course). Finally I set the direction of the light. In this case the light is entering the scene slightly to the right (because it's direction is -0.5 on the x axis), aiming slightly downwards (because it's direction is -1.0 on the Y axis), and aiming slightly back into the scene (because it's direction is 1.0 on the Z axis). And there we have our light! Ignore the commented code for the moment, we'll come back to specular lighting shortly.
Now that our light is configured, we need to pass it's details to D3D:
// Tell D3D to copy our light properties into the T&L pipeline...
rslt=g_pDevice->SetLight(0, &Light);
if(FAILED(rslt)) { return D3DError(rslt, __LINE__, __FILE__, "Could not set light."); }
// ...and enable the light
rslt=g_pDevice->LightEnable(0, TRUE);
if(FAILED(rslt)) { return D3DError(rslt, __LINE__, __FILE__, "Could not enable light."); }
First up we call IDirect3DDevice9::SetLight(), passing it the light index number we want to set, and the address of our light struct. Here's it's prototype:
HRESULT SetLight(DWORD Index, CONST D3DLIGHT8* pLight);
Parameter 1 is the index to the light you wish to set. The number of lights you can have set at any one time is usually 8, depending on your gfx card (my GF4 allows me 8, I think my old GF2 allowed 8 as well). It's a zero-based index as usual, therefore 0-7 are the normal values. Check the DirectX Caps Viewer for your card's capabilities. Parameter 2 is a pointer to a filled-out D3DLIGHT9 struct. Simple enough!Next we do what plenty of people forget to do, and wonder why their scene is all black. Passing your light struct to D3D isn't enough - you have to enable it too! I won't show you the prototype for the IDirect3DDevice9::LightEnable() method - parameter 1 is the index of the light you set in the call to IDirect3DDevice9::SetLight(), and parameter 2 is TRUE or FALSE to enable/disable the light. And that, as they say, is that!
Well, almost. There's another side to lights that we haven't discussed yet. It's all well and good having a light shining on your scene, but what about it's interaction with objects? For example, it's all well and good having a blue cube in a red light, but in real life a red light does not contain any blue components, therefore the blue cube would appear black. As we know, D3D lights do not obey primitives, so how do we make this kind of manipulation possible?
We do it through the use of "materials". Materials are how we tell D3D how our primitives interact with lighting. They're very simple to use, and are set up in pretty much the same way as we set up our light above. There is one vitally important issue to bear in mind with materials. You can only have one material enabled at any one time. That is to say, if you create and set a material, D3D will use that material to render every primitive in the pipeline until you remove or change it. Right about now you should be thinking of the vital importance of the ability to index into a vertex buffer and render specific sets of primitives, as shown in tutorial 2 ;).
We'll worry about that later though. For now, let's see how we create and set a material. It's very much like the light code above:
D3DMATERIAL9 Material;
ZeroMemory(&Material, sizeof(D3DMATERIAL9));
Material.Diffuse.r = 0.0f;
Material.Diffuse.g = 0.5f;
Material.Diffuse.b = 1.0f;
Material.Diffuse.a = 1.0f;
// Set the RGBA for Ambient reflection. This colour affects the colour of any faces that do NOT
// have any light fall on them.
Material.Ambient.r = 0.0f;
Material.Ambient.g = 0.5f;
Material.Ambient.b = 1.0f;
Material.Ambient.a = 1.0f;
First, we create a var of type D3DMATERIAL9. Just like D3DLIGHT9, this is a struct containing some members used to define our material. Unlike D3DLIGHT9, they're all useful:
typedef struct _D3DMATERIAL9 {
D3DCOLORVALUE Diffuse;
D3DCOLORVALUE Ambient;
D3DCOLORVALUE Specular;
D3DCOLORVALUE Emissive;
float Power;
} D3DMATERIAL9;
The diffuse colour of a material defines the "actual" colour of the material for any surface that has light shining directly on it. For example, if you wanted a blue cube, you would set this member to RGB (0.0, 0.0, 1.0). All primitives that are being lit by a light will then appear blue. Yes, D3D materials also use the 0.0 to 1.0 scale for colours. No, I don't know why.The ambient colour defines the "actual" colour of the material for any surface that does not have light shining directly on it. It's normal to set this colour to be the same as your material diffuse colour, as it's fairly unlikely that your cube would be blue in direct light and red in ambient light!
Specular colour is basically the "shiny factor" of the material. Fig 5.7 shows the effect of specular highlighting on a material. You normally set this to white to produce a metallic type effect, as that's pretty much the point of specular highlights! The Power member is tied into this specular highlighting, and defines just how shiny a material is. I've included some specular highlighting code in the example code for this tutorial, we'll look at it in a second. Meanwhile, as a side note if you set the specular power to 0 and the specular colour to 0.0 (black), you can produce a nice dull matte effect.
Emissive colour is handy for glowing effects, as it allows materials to appear as if they emit a lightsource themselves. Setting an emissive colour of RGB (0.0, 1.0, 0.0) will cause a material to glow red. Note that emissive colour from a material does not light other objects.
As with our light, we ZeroMemory() the struct so we only need to configure the vars we want. Once our material is set, we just need to tell D3D to use it:
// Tell D3D to use our material
rslt=g_pDevice->SetMaterial(&Material);
if(FAILED(rslt)) { return D3DError(rslt, __LINE__, __FILE__, "Could not set material."); }
Simple or what! We just call IDirect3DDevice9::SetMaterial(), pass it the address of the material we just set up, and that's it! Our light and material are now in effect until we tell D3D otherwise. Just to clarify one point here - when you use SetLight() and SetMaterial(), D3D copies the contents of the struct into internal structures. Therefore it's perfectly OK to create your light and material structs as local function vars - as long as you don't want to keep them for later, of course!Now lets take a look at specular highlights. I've included the code for them in the tutorial workspace, it's just commented out (all the code is in GameInit()). Let's go through it. First of all, uncomment the following line:
g_pDevice->SetRenderState(D3DRS_SPECULARENABLE, TRUE);
We have to enable specular highlighting in the T&L pipeline before it will work - don't forget this, or you'll be debugging a non-existant bug ;). Next, uncomment this code:
Light.Specular.r = 1.0f;
Light.Specular.g = 1.0f;
Light.Specular.b = 1.0f;
Light.Specular.a = 1.0f;
This sets the specular colour of the light, ie: the light colour that will be used for specular highlights. You'd usually set this to white. Next, uncomment this code:
Material.Specular.r = 1.0f;
Material.Specular.g = 1.0f;
Material.Specular.b = 1.0f;
Material.Specular.a = 1.0f;
Material.Power=100.0f;
This sets the specular colour of the material. Again, just as the diffuse colour of lights & materials, if we use a light with a specular colour that does not contain any of the RGB components of our material's specular colour, we won't see any specular highlights. For example, if we have a blue cube in a white light, and the light has a specular colour of RGB(1.0, 1.0, 1.0), we could set the material's specular colour to either (1.0, 1.0, 1.0), or (0.0, 0.0, 1.0). Finally we set the specular power, which basically controls how shiny the specular highlight is.
One last note about specular highlighting. It's actually calculated at the vertices and interpolated across the primitive, just as normal lighting is. However if you uncomment the specular code above and run the app, you'll notice some artifacts (technical word for graphic bugs ;). On some occasions, the specular shine will appear to skip across the face, and become sharply cut where primitives meet. This is a bug due to the manner in which we create the cube. Because we haven't learnt how to share vertices yet, you'll have to live with it. However we'll be looking at index buffers in the next series, and remove this bug in the process.
And that is all there is to lights & materials! Let's recap some of the main points:
- You must enable the lighting renderstate with IDirect3DDevice9::SetRenderState(D3DRS_LIGHTING, TRUE);
- If you want specular highlights, you must enable them with IDirect3DDevice9::SetRenderState(D3DRS_SPECULARENABLE, TRUE);
- Lights and materials work closely together. A blue light shining on a red material will make objects appear black, because red does not contain any blue components and therefore will not reflect any blue light.
- The above point is true for ALL types of lighting, especially specular highlighting.
- Don't forget to set the ambient light level with IDirect3DDevice9::SetRenderState(D3DRS_AMBIENT, <colour>);, or any parts of your scene that are not directly lit will appear black.
- Once a material is set, it applies to everything rendered until it is changed or removed.
- Don't forget to enable your light once you've set it.
- Lights do not obey primitives for clipping. A light will appear to pass it's light through a primitive. Stencil buffers and other techniques are required to prevent this.
At this point I was intending to give an overview of how to correctly calculate normals. However it's going to make this tutorial a bit longer, and delay it's release. It's also a generally useful tutorial, so instead of adding it in here I'll release it shortly as an addition to the 32Bits Cheaters Guide to Maths. I'll write and release it as soon as possible.
Meanwhile, here's a few exercises for you to test yourself with:
Basic:
Exercise 1: Change the properties of the light and material in this tutorial, and see how lights & materials interact. Try green lights and green materials, blue lights and red materials, and white lights with different materials.Exercise 2: Add another directional light to the scene, so the cube is lit from 2 different angles. See how the lights interact with each other.
Intermediate:
Exercise 3: Play around with the specular properties of the light and material. See how the specular components of the light and the material interacts.Exercise 4: Create a new project, make your own 3D objects (try a pyramid for example) that derive from CD3DObject - use CD3DCube for an example. Add your own lights & materials to the scene to see how they interact.
Advanced:
Exercise 5: Try adding a point light and spot light to your code in exercise 4. You'll need to refer to the SDK docs for each type of light - there are examples in there for each type of light. You might find you prefer spotlights to lightmapping!
You'll find the hardest part of D3D lighting is making the damn things appear where you want them, and to shine in the correct directions to produce the right effects. A bit of practice and it becomes much easier though! If you made it this far through the first 3 series, then you've done really well. You're on the road to doing some really great things with D3D9, and you know most of the basics. There are a few blanks we still need to fill in, but the next series will cover those and show you some really kewl stuff in the process!
If you're looking for things to do until the next series begins, I would suggest that you make yourself completely comfortable with creating 3D objects of any type, and lighting and texturemapping them. It's the bread and butter of D3D, and we'll be using the techniques shown in this series a lot in the future. Enjoy your coding!
Click here to send feedback, bug reports, comments and ideas etc!