Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
66 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

Contents
 Using Textures
 in Direct3D

 Loading 3D Models
 into Direct3D

 Using Direct3D
 Lighting


 Printable version
 Discuss this article
 Get the source

The Series
 Part 1
 Part 2
 Part 3

Using Direct3D Lighting

Okay, onto our final section for this article. Lighting in general is a very important topic to understand, and unfortunately, it is quite complicated as well.

It is often a good idea to look at cinema for lighting - cinema has been around for about a century now, and has progressed into a fine art form, and one of the many things that makes or breaks a scene in a film is the lighting, yet the key aspect is that you don’t necessarily notice it. Ambient lighting is a very subtle effect that will often set the atmosphere for a film - how many horror films have the scary scenes in broad daylight/with the lights on (well, I know there are a few!). Shadows and the type of lighting (strobe, direct, soft, bright, dark) are also huge factors. In my opinion, computer gaming is only just starting to catch up with true artistic lighting, In the last year or so many of the level-architecture articles on websites have specifically brought lighting up as a major topic - whereas before it was just "put the light where it looks best"… Games such as Max-Payne are the first to put the best lighting algorithms (ray tracing/radiosity) to great use, and I expect many future games to follow this pattern.

Don’t get your hopes up straight away though - the Direct3D lighting engine, whilst complicated, is still very, very simple. The first point to notice is that it wont generate shadows, secondly, it doesn’t handle reflection or refraction, thirdly, it’s only an approximation - accurate only at each vertex. The more complicated solutions require the use of light maps, and other pre-calculated methods (which are too complicated to go into here). I read somewhere that many of the Max-Payne maps required several hours of pre-processing just for the lighting algorithm, so you can appreciate why it’s not done in real-time J

For now, we’ll be happy with the Direct3D lighting engine, once you have mastered this then you can begin to consider other models.

You have actually already seen the effects of the lighting engine in all 3 parts of this series. Whenever we specified a colour for a vertex, and got the gradient of colours across a triangle we were actually seeing D3D lighting at work. To save on processing times, Direct3D will linearly interpolate across a triangle the colours from its 3 vertices - it assumes that the light won’t change considerably between them. This, to a certain degree, won’t matter for small triangles, but for larger triangles this causes a big problem - if none of the vertices fall within the lights range then it wont be lit, even if a large area of the triangle is actually within the lights range.

In order to proceed with lighting you must use a little maths, the proof behind these equations isn’t really too important, all you need to know is how to use the equations to get the results you want. As I’ve already stated, Direct3D performs its calculations on a per-vertex basis, thus we must include some extra information with every vertex - a normal vector. This vector indicates what direction the vertex is facing, which may seem a little strange - but it makes perfect sense really: A triangle facing away from the light should get no light, whereas a triangle facing the light directly should get lots of light, and how do we tell if the triangle is facing the light or not? Use the normal…

Typically the normal will represent the direction the triangle is facing, however, it doesn’t have to! Whilst it often looks a little strange, you can do strange things to the normal and get some very odd effects - not very good for realistic scenes, but fine for more humorous scenes. You also have to take much more care over indices when using D3D lighting - if a two (or more) triangles share the same vertex, what direction is it facing? One way of doing this is to generate a normal for each triangle, then average them out to give a final direction for the shared vertex. This method usually works a treat, but there are times when it generates results that look wrong for all the triangles concerned…

If we have a triangle defined by the three vertices v0,v1,v2 then the normal is going to be found using the following function:

Private Function GetNormal(v0 As D3DVECTOR, v1 As D3DVECTOR, v2 As D3DVECTOR) As D3DVECTOR
    '//0. Any Variables
        Dim v01 As D3DVECTOR, v02 As D3DVECTOR, vNorm As D3DVECTOR

    '//1. Get the vectors 0->1 and 0->2
        D3DXVec3Subtract v01, v1, v0
        D3DXVec3Subtract v02, v2, v0

    '//2. Get the cross product
        D3DXVec3Cross vNorm, v01, v02

    '//3. Normalize this vector
        D3DXVec3Normalize vNorm, vNorm
        
    '//4. Return the value:
        GetNormal = vNorm
End Function

That’s fairly harmless really - the D3DX helper library handles all the complicated maths for us - the cross product, subtraction and normalizing. However, if you want to avoid using the D3DX functions then the function will look like this instead:

Private Function GetNormal2(v0 As D3DVECTOR, v1 As D3DVECTOR, v2 As D3DVECTOR) As D3DVECTOR
    '//0. Any Variables
        Dim L As Double
        Dim v01 As D3DVECTOR, v02 As D3DVECTOR, vNorm As D3DVECTOR
    
    '//1. Get the vectors 0->1 and 0->2
        v01.X = v1.X - v0.X
        v01.Y = v1.Y - v0.Y
        v01.Z = v1.Z - v0.Z
        
        v02.X = v2.X - v0.X
        v02.Y = v2.Y - v0.Y
        v02.Z = v2.Z - v0.Z
    
    '//2. Get the cross product
        vNorm.X = (v01.Y * v02.Z) - (v01.Z * v02.Y)
        vNorm.X = (v01.Z * v02.X) - (v01.X * v02.Z)
        vNorm.X = (v01.X * v02.Y) - (v01.Y * v02.X)
    
    '//3. Normalize this vector
        L = Sqr((vNorm.X * vNorm.X) + (vNorm.Y * vNorm.Y) + (vNorm.Z * vNorm.Z))
        vNorm.X = vNorm.X / L
        vNorm.Y = vNorm.Y / L
        vNorm.Z = vNorm.Z / L
        
    '//4. Return the value:
           GetNormal = vNorm
End Function

The above is for reference, should you write an editor that isn’t linked to the DX8 runtime library, or should you want to try and optimise parts…

One important factor that I haven’t mentioned yet, is that the vertices, v0,v1,v2 need to be in a clockwise order - you should be aware of this, due to the implications of culling by the D3D renderer, but for the maths above, if the triangle vertices were in an anti-clockwise order then the resulting normal would point in the opposite direction - which, in most cases would mean that your vertices don’t get lit…

Now that I’ve covered generating normals, we need to know what to do with them. The following excerpt is the vertex FVF declaration, and the vertex type:

Const FVF_VERTEX = (D3DFVF_XYZ Or _
                    D3DFVF_NORMAL Or _
                    D3DFVF_TEX1)

Private Type VERTEX
    P As D3DVECTOR
    N As D3DVECTOR
    T As D3DVECTOR2
End Type

The ‘P’ member is the vertex’s position, the ‘N’ member is the vertex normal and the ‘T’ member is the texture coordinate.

To demonstrate D3D lighting I’m going to use the two methods already demonstrated in this article - texturing and model loading… simply because it allows me to show you the effects easily.

There are 4 types of light provided for you by Direct3D - point, spot, direction and ambient lights. The first 3 require that you set up a special D3DLIGHT8 object that describes the light, the fourth requires that you set a render state. The following list covers the 4 types of light, in order of processing speed:

Ambient Lights

Ambient lights have no source, no direction and no range - they affect every vertex rendered. The basic result is that no vertices are rendered darker than the currently specified ambient colour - setting it to a dark grey will result in everything being visible a small amount. We set the ambient light value using the following code:

D3DDevice.SetRenderState D3DRS_AMBIENT, D3DColorXRGB(100, 100, 100)

Directional Lights

Directional lights are good for general shading of a scene, such that everything is evenly lit up, but you also get a dark side to every object. They can be used very effectively as a sun object.

Directional lights have direction and colour only, no range, no position and no attenuation (see Point lights for more details). A completed Directional light structure looks like this:

Dim lghtDirectional As D3DLIGHT8

lghtDirectional.Type = D3DLIGHT_DIRECTIONAL
lghtDirectional.Direction = MakeVector(0, -1, 0)
lghtDirectional.position = MakeVector(1, 1, 1) 'shouldn't be left as 0
lghtDirectional.Range = 1 'shouldn't be left as 0
lghtDirectional.diffuse = CreateD3DColorVal(1, 0, 1, 0) 'green light

Point Lights

Point lights have a position and a range, but no direction - they emit light in all directions. A simple real world analogy would be a light-bulb. To set up a point-light you need to fill out a D3DLIGHT8 structure:

Dim lghtPoint As D3DLIGHT8

lghtPoint.Type = D3DLIGHT_POINT
lghtPoint.diffuse = CreateD3DColorVal(1, 1, 0, 0)
lghtPoint.position = MakeVector(0, 0, -10)
lghtPoint.Range = 25#
lghtPoint.Attenuation0 = 0#
lghtPoint.Attenuation1 = 1#
lghtPoint.Attenuation2 = 0#

The attenuation values are quite important to understand. As we all know, light from a given source decreases the further from the light source that we are - the light attenuates. The three value that D3D allow us to use control how the light attenuates - constant, linear and quadratic (0 through 2 respectively). You should never let all three = 0 at the same time, otherwise you’ll get internal divide-by-0 errors. Experiment with different values to see what happens, a value of 1 in Attenuation0 will remove any attenuation, whilst negative values in the other two will cause the light to get brighter the further away from the light source it gets J

For those mathematicians amongst us, the general attenuation formula is:

A = 1 / (Attenuation0 + D*Attenuation1 + D2*Attenuation2)

Where D is the distance from the light source to the current vertex. As you can see, the denominator is a standard quadratic equation in the form of aX2 + bX + c.

The other point to note is that when specifying colours they are on a 0.0 to 1.0 scale, rather than the standard 0-255 scale. This is because you can specify negative values, and values >1, allowing extra bright lights, and "dark" lights that remove colour rather than add it.

Spot Lights

Finally, we get onto spot lights. These are the slowest type of lighting available, but in some cases look by far the best (A tunnel with several spot lights shining down from the ceiling for example). Hopefully you can visualise in your head a spot-light, and how they interact with the world - a cone of light projected from one point in one direction, brightest in the middle, getting darker towards the edge… It is the fact that it is based on a cone that it requires more calculation time - we need to work out IF it’s in the cone, and how close to the "centre" of the cone (for brightness). A completed D3DLIGHT8 object for a spot-light will look like this:

Dim lghtSpot As D3DLIGHT8

lghtSpot.Type = D3DLIGHT_SPOT
lghtSpot.Range = 50#
lghtSpot.diffuse = CreateD3DColorVal(1, 0, 0, 1)
lghtSpot.Direction = MakeVector(0, 0, 1)
lghtSpot.position = MakeVector(0, 0, -10)
lghtSpot.Theta = 0.25 * PI
lghtSpot.Phi = 0.5 * PI
lghtSpot.Attenuation0 = 0.1
lghtSpot.Attenuation1 = 1#
lghtSpot.Attenuation2 = 0#

As you can see, a spotlight has range, direction, position and colour. It also has two new values - phi and theta. These two values indicate the angle (in radians) of the spot-lights cone. Theta is the inner cone, Phi is the outer cone. Phi must be a positive value between 0 and p (180o) and Theta must also be a positive value between 0 and Phi. If you think about this, it makes perfect sense… An outer angle greater than 180o makes little sense really (it would start shining behind itself), and an inner angle greater than the outer angle doesn’t make much sense either. Remember that these values MUST be set in radians - if you use degrees, all sorts of funky things will start happening! If you really cant get your head around radians then you can multiply a value in degrees by (p/180) where p is the mathematical constant 3.14159…(can be calculated by typing "4*atn(1)" in the immediate window in VB).

Now that we’ve learnt how to configure a D3DLIGHT8 object for all 3 main types of light, we’re going to need to let the device know about them. You can register as many light objects as you want with D3D, BUT you can only have a certain number enabled (on) at any one time. You can detect how many lights may be turned on at any one time by using the D3DCAPS8.MaxActiveLights value. If you enable more lights than are supported the call tends to fail, or if it does succeed then it wont actually process the light when doing the calculations… This value tends to be 8 - 16 on the GeForce cards, for most older cards it’ll return -1, which indicates that an unlimited number of lights can be "on" at any one time; However, the more lights you enable the slower your program will run.

D3DDevice.SetLight 0, lghtPoint
D3DDevice.SetLight 1, lghtDirectional
D3DDevice.SetLight 2, lghtSpot

D3DDevice.LightEnable 0, 1
D3DDevice.LightEnable 1, 1
D3DDevice.LightEnable 2, 1

D3DDevice.SetRenderState D3DRS_LIGHTING, 1

Not too complicated really, the last function, LightEnable, takes the index and a simple 1/0 value for on/off respectively. Any geometry processed after the "D3DDevice.LightEnable x, 1" line will be lit by that light (given that the geometry is within the influence of that light). It is perfectly acceptable to turn a light on for only one model - such that only it gets lit by that light.

To show off the lighting we’ll need a new model to play with. The cube mesh I made/showed earlier isn’t complex enough to show off the new lighting code; instead I’m going to use a much higher vertex-density mesh. This will mean that it runs considerably slower on most machines, but this is only a demo…

First off: A solid, unlit version of the geometry

Second: A wireframe version, to show you the complexity of the geometry (3384 vertices)

Third: The (red) Point light which is located directly below the camera in this shot, notice the distribution of the lighting.

Fourth: The (Green) Directional Light. Notice that the lighting is evenly distributed, and that the entire "bottom" is unlit. Also notice, that if shadows were cast, the bottom of the cone would not be highlighted in green.

Fifth: The (Blue) Spotlight, notice a very distinct spot - indicating the presence of a cone.

Lastly: A nasty mess of all the colours - where the red and blue lights colour the same section we get magenta, in other parts we have a yellow colour.

One thing that is quite clearly visible on the last two is that the lighting on the very tip of the cone isn’t lit in the same way that the rest of the cone/model is. This is done deliberately here to show off how textures can affect the final colour. Take the colour red (as on the nose), it can be represented as RGB(255,0,0). If we then use a lighting colour of RGB(0,1,0) we’ll get various shades of green, and only green, interpolated across the triangle. This is important - if there was no texture applied, there would ONLY be green pixels. To get the final pixel colour we MULTIPLY the interpolated lighting colour with the texel colour: RGB(RtRl, GtGl, BtBl) where Xt = texture and Xl = lighting. If we go back to the original example of a red texel colour, RGB(255,0,0), and a green light, RGB(0,1,0) that multiplied colour works out as RGB(255*0, 0*0, 0*1) = RGB(0,0,0) = Black! Which is what you can see in the above screenshots. The bottom line is this: If the texture doesn’t contain any (or very little) of the channel that the light uses, then the resulting pixel will be black. This is easily solved by using ambient lighting; but can also be a useful tool for lighting effects.

Conclusion

Okay, so this 3 part series is now complete. I really, really hope that you liked it - either way, drop me an email at Jollyjeffers@Greenonions.netscapeonline.co.uk, constructive comments are always welcome. From the emails I’ve received recently this series has been very successful.

You can download the source code for this tutorial HERE. I strongly suggest that you do this as many of the topics discussed here are much easier to understand when you see it "in action".

As for DirectX programming - you should now have enough knowledge to write a very simple game / engine. Don’t be a fool and try a "simple Quake clone", it’s not going to happen. However, a nice 3D pong/breakout clone, or a simple maze/puzzle game would make for a good learning project. There is absolutely tonnes and tonnes of stuff left to learn! I have been working with DirectX for 2-3 years now (version 5,6,7,8) and I don’t think I’ve ever learn everything in every release of the API - close, but not quite!

As a final note, this may be the last in this series, but I do have a website that will continue to be updated - where you can find more in depth tutorials on the content covered in this series, more advanced tutorials, and generally newer content. Check it out at http://www.vbexplorer.com/directx4vb/index.asp

Many thanks for getting this far with my series.

Jack Hoxley