Main Site Links Resources Tutorials
News VB Gaming Code Downloads DirectX 7
Contact Webmaster VB Programming Product Reviews DirectX 8
  General Multimedia Articles DirectX 9

Basic 3D Geometry
Author : Jack Hoxley
Written : 8th December 2000
Contact : [Web] [Email]
Download : Graph_05.Zip [14 Kb]

Contents of this lesson:
1. Introduction
2. 3D graphics theory
3. Depth Buffers
4. Geometry and Vertex buffers
5. Matrices
6. Rendering

1. Introduction

Welcome to lesson 5. Hopefully by this point you should be capable of setting up a Direct3D8 application and rendering some 2D triangles with texture; perfect if you want to do a 2D game, but hardly exciting when it comes down to cutting edge 3D graphics. Which is where this lesson is aimed at. By the end of this tutorial you'll be well on the road to creating your first 3D world - maybe not as far as cutting edge yet, but it's a step in the right direction.

Unfortunately for you, this lesson is going to be big - there are 3 brand new important features that I need to introduce you to: 3D world space, Vertex Buffers, matrices, Depth Buffers and culling. These topics are the very basis of 3D graphics; so you're going to need to learn them - and learn them well.

On with the learning:

2. 3D graphics theory

Yet more theory to start a lesson; this time it's very important - ignore this lot and you'll be stumped later on. As I mentioned above, this stuff is the very foundation of a 3D engine.

3D World Space
This isn't greatly difficult really - it's just an extension of what you learnt in lesson 03. You can set up world space in many different ways; usually so that the +Y axis is "up" and -Y is "down" (although position of the camera can always muddle this up). 3D coordinates are specified on 3 axis - X, Y and Z. If you're looking straight down the Z-Axis, +Y will be up, +X will be right, +Z will be behind you, -Y will be down, -X will be left and -Z will be in front of you (the distance). Of course, the camera position can change all of this...

Vertex Buffers
Vertex buffers are similiar to textures in that they store pre-made data. Using vertex buffers can help a great deal when it comes to generating complicated scenes - instead of remembering 100's of different arrays for each part you can compile it all into a vertex buffer and then render it with very few calls. These aren't complicated to setup or use - but you should know that they exist.

This is a fun topic, matrices (plural of matrix) are actually quite an advanced mathematics technique - which you're only likely to come across if you do (or have done) advanced maths courses. It doesn't matter if you dont understand the mathematical usage of them fully (I dont), as you only really need a basic knowledge of how they work, and more importantly how to use them. There are three main types of matrix you'll use (there are more): World Matrix, View Matrix and Projection Matrix. The world matrix affects vertices - if you apply a rotated world matrix all subsequently rendered vertices will be rotated by that amount (but you're original data wont be altered). The view matrix is basically a camera - it defines where the "eye" is, and where it's looking at (two cordinates in 3D space); you can also specify which direction is up - in most cases you make +Y the up axis; but you dont have to. Finally, the projection matrix describes how and what Direct3D renders onto the screen - the field of view (FOV) and the view angle. We'll be using all of these later on, where I'll discuss their usage further.

Depth Buffers
These are very useful indeed; easy to set up, and once set up require no additional attention - they just sit there and work. Think about this: you render two triangles in 3D space - how do you know which triangle is in front of the other, or if they intersect which parts of which triangle you render? A depth buffer solves this - when you send a triangle to be renderered it stores the distance from the camera in the depth buffer - if another triangle is rendered it decides which pixels it'll occupy and decide what depth it's at - if this depth is less than any existing pixel it replaces it, otherwise it's not drawn (there's something in front of it). Depth buffers come in different depths - usually 16 bit, 24 bit and sometimes 32 bit; the higher the depth the better the quality and accuracy but the more memory it uses and time it requires to calculate. Depth buffers are also known as Z-Buffers.

This isn't greatly important unless you're designing and coding you're own geometry. By default Direct3D doesn't render any triangles that are facing away from the camera - this is good as it speeds things up a great deal. Although you can change what it culls, Direct3D normally culls triangles with vertices in a counter-clockwise direction and keeps clockwise ordered vertices. It's simple logic to work out which one yours are - if the vertices go round in a clockwise order (left-Right-Bottom for example) or counter-clockwise order (Bottom-Right-Left). You can tell Direct3D not to cull any triangles - but this will produce quite a large performance loss, yet it's extremely useful when debugging....

3. Depth Buffers

Depth buffers, as I mentioned above, are extremely simple to setup, and once their operating you can pretty much ignore their existence. To initialise a depth buffer you can use this code; a slight modification of the existing initialisation code:


D3DWindow.EnableAutoDepthStencil = 1
D3DWindow.AutoDepthStencilFormat = D3DFMT_D16 '//16 bit Z-Buffer

'//Then, after you've created the D3DDevice:

'//We need to enable our Z Buffer

D3DDevice.SetRenderState D3DRS_ZENABLE, 1

Well done, you now have a functioning depth buffer attached to you're render device. If you remember to do this each time you'll be fine. The only thing to note is that you must have hardware support for the selected Z-Buffer depth (if you're using a hardware device). If you select a depth that isn't available two things will happen : 1) the device falls back to a mode it supports or 2) it disables depth buffering. Both of which are unpreferable. To check if the device supports the Z-Buffer setup you want you can use the following code:

        D3DRTYPE_SURFACE, D3DFMT_D16) = D3D_OK Then
'We can use a 16 bit Z-Buffer
    D3DWindow.AutoDepthStencilFormat = D3DFMT_D16 '//16 bit Z-Buffer
'We could now check for different modes available...
End If

If you want to search for other depth buffer modes you can use this little piece of enumeration code:

        D3DRTYPE_SURFACE, D3DFMT_D16) = D3D_OK Then
    Debug.Print "16 bit Z-Buffers are supported (D3DFMT_D16)"
End If

    Debug.Print "Lockable 16 bit Z-Buffers are supported (D3DFMT_D16_LOCKABLE)"
End If

        D3DRTYPE_SURFACE, D3DFMT_D24S8) = D3D_OK Then
    Debug.Print "32 bit divided between 24 bit Depth and 8 bit stencil are supported (D3DFMT_D24S8)"
End If

        D3DRTYPE_SURFACE, D3DFMT_D24X4S4) = D3D_OK Then
    Debug.Print "32 bit divided between 24 bit depth, 4 bit stencil and 4 bit unused (D3DFMT_D24X4S4)"
End If

        D3DRTYPE_SURFACE, D3DFMT_D24X8) = D3D_OK Then
    Debug.Print "24 bit Z-Buffer supported (D3DFMT_D24X8)"
End If

        D3DRTYPE_SURFACE, D3DFMT_D32) = D3D_OK Then
    Debug.Print "Pure 32 bit Z-buffer supported (D3DFMT_D32)"
End If

There; you now know about as much as you'll ever need to know about depth buffers.

4. Geometry and Vertex Buffers

Geometry creation in full 3D is not really that different from when we did it with 2D, the only difference being that we use 3 dimensions rather than 2. Later on when we do lighting it gets a little bit more complicated.

The first thing that we must do is define a new vertex format to use; we'll be using the equivelent of the D3DLVERTEX type from DirectX7; where we give it a position and a colour and Direct3D transforms it.

'//We're using 3D vertices, but we're not using Lighting just yet...
'   which means we need to specify the vertex colour

Private Type LITVERTEX
    x As Single
    y As Single
    z As Single
    color As Long
    Specular As Long
    tu As Single
    tv As Single
End Type

'//The Descriptor for this vertex format...

Dim Cube(35) As LITVERTEX

Notice that I've also put in the declaration for our geometry; 36 vertices for a cube - sounds a lot; it is. Methods later on (using indices/index buffers) will allow us to generate the same cube for only 8 vertices. If we were using triangle strips we could do this cube for 24 vertices - but writing it to a vertex buffer would have been more awkward.

We'll now re-write our InitGeometry( ) function so that it creates a cube. Two things to note with this code - 1) I didn't bother with checking the the vertex order (for culling) and 2) all vertices are generated around the origin - the middle of the cube is going to be [0, 0, 0] and the rest of the vertices are equally distributed around this (+- 1m); when you've finished reading about matrices you'll understand why.

Code In This Font/Size in these tables.
'//I used the Immediate window to
'   get the RGB() values for each of these...
Const C000 As Long = 255     '//Red
Const C001 As Long = 65280  '//Green
Const C100 As Long = 16711680   '//Blue
Const C101 As Long = 16711935 '//Magenta
Const C010 As Long = 65535 '//Yellow
Const C011 As Long = 16776960 '//Cyan
Const C110 As Long = 16777215 '//White
Const C111 As Long = 8421631 '//Orange

'//1. Fill the structures with the data

        Cube(0) = CreateLitVertex(-1, 1, 1, C011, 0, 0, 0)
        Cube(1) = CreateLitVertex(1, 1, 1, C111, 0, 0, 0)
        Cube(2) = CreateLitVertex(-1, -1, 1, C001, 0, 0, 0)
        Cube(3) = CreateLitVertex(1, 1, 1, C111, 0, 0, 0)
        Cube(4) = CreateLitVertex(-1, -1, 1, C001, 0, 0, 0)
        Cube(5) = CreateLitVertex(1, -1, 1, C101, 0, 0, 0)
        Cube(6) = CreateLitVertex(-1, 1, -1, C010, 0, 0, 0)
        Cube(7) = CreateLitVertex(1, 1, -1, C110, 0, 0, 0)
        Cube(8) = CreateLitVertex(-1, -1, -1, C000, 0, 0, 0)
        Cube(9) = CreateLitVertex(1, 1, -1, C110, 0, 0, 0)
        Cube(10) = CreateLitVertex(-1, -1, -1, C000, 0, 0, 0)
        Cube(11) = CreateLitVertex(1, -1, -1, C100, 0, 0, 0)
        Cube(12) = CreateLitVertex(-1, 1, -1, C010, 0, 0, 0)
        Cube(13) = CreateLitVertex(-1, 1, 1, C011, 0, 0, 0)
        Cube(14) = CreateLitVertex(-1, -1, -1, C000, 0, 0, 0)
        Cube(15) = CreateLitVertex(-1, 1, 1, C011, 0, 0, 0)
        Cube(16) = CreateLitVertex(-1, -1, -1, C000, 0, 0, 0)
        Cube(17) = CreateLitVertex(-1, -1, 1, C001, 0, 0, 0)
        Cube(18) = CreateLitVertex(1, 1, -1, C110, 0, 0, 0)
        Cube(19) = CreateLitVertex(1, 1, 1, C111, 0, 0, 0)
        Cube(20) = CreateLitVertex(1, -1, -1, C100, 0, 0, 0)
        Cube(21) = CreateLitVertex(1, 1, 1, C111, 0, 0, 0)
        Cube(22) = CreateLitVertex(1, -1, -1, C100, 0, 0, 0)
        Cube(23) = CreateLitVertex(1, -1, 1, C101, 0, 0, 0)
        Cube(24) = CreateLitVertex(-1, 1, 1, C011, 0, 0, 0)
        Cube(25) = CreateLitVertex(1, 1, 1, C111, 0, 0, 0)
        Cube(26) = CreateLitVertex(-1, 1, -1, C010, 0, 0, 0)
        Cube(27) = CreateLitVertex(1, 1, 1, C111, 0, 0, 0)
        Cube(28) = CreateLitVertex(-1, 1, -1, C010, 0, 0, 0)
        Cube(29) = CreateLitVertex(1, 1, -1, C110, 0, 0, 0)
        Cube(30) = CreateLitVertex(-1, -1, 1, C001, 0, 0, 0)
        Cube(31) = CreateLitVertex(1, -1, 1, C101, 0, 0, 0)
        Cube(32) = CreateLitVertex(-1, -1, -1, C000, 0, 0, 0)
        Cube(33) = CreateLitVertex(1, -1, 1, C101, 0, 0, 0)
        Cube(34) = CreateLitVertex(-1, -1, -1, C000, 0, 0, 0)
        Cube(35) = CreateLitVertex(1, -1, -1, C100, 0, 0, 0)

Looks great doesn't it. Just imagine how complicated it'd be to make a sphere or a person... which is why you'll love using externally created objects and loading them straight in. You'll notice that I've used constants for all the colours - a cube is made up of 8 corners, so I designed 8 constants for colour - a colour for each corner. As 3-4 vertices can share the same point I just placed the constant as the colour; if you change the constant all required vertices will change colour as well.

You may also have noticed that I haven't mentioned vertex buffers yet. Well, here they are. Vertex buffers are basically an allocated amount of memory filled with vertex data; specially formatted by Direct3D so it can access them easier and faster (most of the time). The first step is to allocate enough memory for the vertex buffer, then we fill it with the Cube( ) vertex data that we've just defined. To do this, we use the following code:

'//2. Create us a blank vertex buffer of the required size

Set VBuffer = D3DDevice.CreateVertexBuffer(Len(Cube(0)) * 36, 0, Lit_FVF, D3DPOOL_DEFAULT)
If VBuffer Is Nothing Then Exit Function '//Error handler

'//3. Fill the created vertex buffer with the data
D3DVertexBuffer8SetData VBuffer, 0, Len(Cube(0)) * 36, 0, Cube(0)

Note that we must specify the size of the vertex buffer in bytes - this MUST be accurate or we'll cause a mess; too small and you'll get errors when filling it up, too big and you'll waste memory. The size is going to be "Size of one vertex * number of vertices" - we use the Len( ) function to work out the size, then we multiply this by the number of vertices we have (36). we must also supply the vertex format (FVF) so that Direct3D knows what it's looking at; finally we then decide how Direct3D should manage our vertex buffer; D3DPOOL_DEFAULT should suffice, otherwise you can specify D3DPOOL_MANAGED (Let it choose, and move it around as it sees fit) or D3DPOOL_SYSTEMMEM (place it in system memory (RAM)).

Next, we fill the vertex buffer using a hidden function (sort of hidden anyway). We give it the name of our vertex buffer and the size of the area we wish to fill and the offset (should we wish to start somewhere other than the beginning). finally we pass the array of data that we want to pass; this must be equal in size and format to that which we created the vertex buffer for - or nasty things will probably happen.

Assuming nothing went wrong above we can move onto more interesting things....

5. Matrices

Matrices are an extremely useful thing to understand; they allow you to do an enourmous amount of things to 3D worlds. Matrices are quite a complicated mathematical topic - so I wont go into how/why matrices work - just how you can use them. A matrix, in Direct3D, is a 4x4 matrix - think of it like a grid, with 4 entries along the top and 4 entries down the side. Direct3D uses these numbers to alter various things during low-level processing - between the time you make calls to render and the time they appear on screen.

There are a few things that you should bare in mind when using matrices:
1. You can combine matrices together to form a new matrix; for example, if you multiply a rotation and a translation matrix you will get a single matrix that rotates and translates anything it's given.
2. Unlike normal numbers where (A * B) = (B * A) multiplying matrices [A] * [B] does not equal [B] * [A] - so it's important to do things the right way around.
3. There are lots of functions in the D3DX8 library to help with manipulating matrices

There are three types of matrix that you'll always have to use; others are for different effects. The main types are outlined below:

World Matrix
This matrix alters the vertices - this is the one you'll use the most often. All transformations of vertices work around the origin [0,0,0] - it rotates vertices around the origin, scales vertices around the origin. This is why (earlier) we created our cube around the origin. As already mentioned it is possible to combine multiple matrices to form a single all-in-one matrix; but as you also should remember, the order you multiply them matters. If you rotate something around the X axis and the Z axis you'll not get the same result if you'd rotated around the Z then the X. For example, if you have a car wheel and rotate it around the X axis you'll get it rotating like a wheel should, if you then rotate it around the Z axis it'll appear to be tilted in one direction - but it'll still rotate like a wheel should. If you rotate around the Z axis the wheel will appear to tilt, if you then rotate it around the X axis the wheel will seem to wobble as it goes around; the effect is best seen when animated though.

The following code is from the sample for this lesson:

Dim matWorld As D3DMATRIX
Dim matTemp As D3DMATRIX

D3DXMatrixIdentity matWorld '//Reset our world matrix
D3DXMatrixIdentity matTemp
D3DXMatrixRotationX matTemp, RotateAngle * (pi / 180)
D3DXMatrixMultiply matWorld, matWorld, matTemp
D3DXMatrixIdentity matTemp
D3DXMatrixRotationZ matTemp, RotateAngle * (pi / 180)
D3DXMatrixMultiply matWorld, matWorld, matTemp
D3DDevice.SetTransform D3DTS_WORLD, matWorld

okay, not too complicated really. First we create two matrices, one is our "Master" matrix, the other is a temporary one, because we're doing more than one transformation we need something to store each step before multiplying it. Next we reset our master matrix - this is very important as matrix multiplying and transforming is a cumulative thing - if you alter it each frame without reseting it very quickly things will go very strange. The Identity matrix isn't actually a matrix where everything is set to '0', all the X=Y entries are filled with '1's [1,1] [2,2] [3,3] [4,4]. After we've reset our matrix we start manipulating it; the available transformations are:

'//Normal Rotation
D3DXMatrixRotationX(Out As D3DMATRIX, angle As Single)
D3DXMatrixRotationY(Out As D3DMATRIX, angle As Single)
D3DXMatrixRotationZ(Out As D3DMATRIX, angle As Single)

'//Rotation through a custom axis
' You must specify an axis using the VAxis setting
' FYI: X=[1,0,0] Y=[0,1,0] and Z=[0,0,1]

D3DXMatrixRotationAxis(MOut As D3DMATRIX, VAxis As D3DVECTOR, angle As Single)

'//Misc Transforms
'This scales vertices by the amounts specified

D3DXMatrixScaling(MOut As D3DMATRIX, x As Single, y As Single, z As Single)
'this moves vertices by the amounts specified
D3DXMatrixTranslation(MOut As D3DMATRIX, x As Single, y As Single, z As Single)

'Others are available, and are listed in the object browser - and can be found by going

One thing to note about the angles specified when rotating - they're in radians not degrees. If you're happy using radians thats fine, but if you prefer to use degrees you can use this simple conversion: Degrees * (Pi / 180) where Pi is 3.14159265358979 (which in turn can be worked out using (4 * atn(1)))

Finally we get to the point where we apply the transformation to our rendering device. This can be done as often as you wish, but the less times the better. Once this matrix is applied all vertices rendered after that point will be transformed accordingly - and will continue to be until another matrix is set.

View Matrix
The view matrix can be thought of as the camera; whilst the projection matrix (see later) controls some of the more complicated parts of the "camera", the view matrix is the one you alter if you wish to move the camera around (as most games do). Setting the view matrix is extremely simple, but you need to think about it a little first - if nothing is appearing on screen then the chances are that you've got the camera set up wrong...

To alter the view matrix we'll need this code:

'//The function prototype

'//And this is an example:
Dim matView as D3DMATRIX
Dim vecFrom as D3DVECTOR
Dim vecTo as D3DVECTOR
Dim vecUP as D3DVECTOR

'//The camera is at this point in 3D worldspace
vecFrom.X = 0
vecFrom.Y = 10
vecFrom.Z = 10

'//And it is looking at this point:
vecTo.X = 0
vecTo.Y = 0
vecTo.Z = 0

'//Unless you want to do something clever you
' Can leave this as it is for all applications

vecUp.X = 0
vecUp.Y = 1 
vecUp.Z = 0

'//Now we generate Our final Matrix

D3DXMatrixLookAtLH matView, vecFrom, vecTo, vecUp

D3DDevice.SetTransform D3DTS_VIEW, matView

As you can see, it's quite easy to update the position of the camera. Check out the DirectX 7 immediate mode tutorials on camera placement - whilst it's using DirectX 7 interfaces, the maths will still be the same to get the camera to follow a "person" around, or move according to the user...

Projection Matrix
Finally we come to the projection matrix; this matrix isn't used as often as the other two, but it's equally as important. The projection matrix controls how the scene appears when we look through our camera - you can set the field of view (the closest and furthest objects visible) and the view angle (wide angle or tele-photo). Unless you're intending to make a game with a zoom feature or some weird head-messing-up game you're unlikely to set this matrix very often; normally it's setup during initialisation and left.

'//The function prototype:
D3DXMatrixPerspectiveFovLH( MOut As D3DMATRIX, fovy As Single, aspect As Single, zn As Single, zf As Single)
'Where Mout is the result
'fovy is the view angle
'Aspect is the aspect ratio
'zn is the nearest boundary for polygons - anything between here and the camera position are not rendered
'zf is the furthest boundary - anything beyond here is not drawn

'//Finally we create our matrix
D3DXMatrixPerspectiveFovLH matProj, pi / 4, 1, 0.1, 500

'//And then we commit it to our Direct3D device
D3DDevice.SetTransform D3DTS_PROJECTION, matProj

That's not greatly difficult is it. The only two stumbling blocks are the "fovy" and "aspect" parameters; the fovy is an angle specified in radians - for this it's much easier to keep it in radians, as you're only ever likely to use a few values - all of them "pi / something" where something is going to be in the range of 2 - 9, with 2 being wide angle and 9 being telephoto, dont set it to 0, as you'll get nothing displayed, and quite probably a divide by 0 error (pi / 0 ). Then there's the aspect ratio - you can usually leave this as 1 (1:1 ratio); if you make it less than 1 it appears to stretch geometry vertically, greater than 1 appears to stretch geometry horizontally.

Matrices aren't that difficult really - but you'll use them for almost every full-3D scene that you render; so you'll probably learn them quite quickly. One final thing to remember, which is common sense really: if world matrices transform vertices you cant use them with transformed and lit vertices...

6. Rendering

Rendering isn't a big change, just a few new lines for you to look at, so I wont bother spending much time going over it all again...

'//First off we'll alter the device clearing code:
D3DDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, 0, 1#, 0 

'//Secondly, we must change how we render our primatives:
D3DDevice.SetStreamSource 0, VBuffer, Len(Cube(0))
D3DDevice.DrawPrimitive D3DPT_TRIANGLELIST, 0, 12

Not greatly difficult really; as we're now using Z-buffers (or Depth buffers - whichever you prefer) we'll need to clear that buffer as well as the rendering target - if you forget to clear the Z-Buffer you can get some weird effects happening with the draw-order. Lastly, when using vertex buffers to hold our geometry we must render them slightly differently; commit the vertex buffer to the specified pipeline/stream and tell Direct3D how big each entry will be; once that's done we can use the normal (and simpler) D3DDevice.DrawPrimative statement. Assuming everything has gone okay (it should of) you should be looking at something similiar to this:

I created a slightly different geometry set so that you could see each face
being rendered.

As per normal you can download the complete source code for this tutorial from the top of this page, I advise that you do play around with the code and get familiar with how things work.

Assuming you're ready to continue - onto Lesson 06 : Drawing Text - Custom Rasterizers and Normal 2D text.

DirectX 4 VB 2000 Jack Hoxley. All rights reserved.
Reproduction of this site and it's contents, in whole or in part, is prohibited,
except where explicitly stated otherwise.
Design by Mateo
Contact Webmaster