.:[ Please always use http://www.32bits.co.uk to access the site. Thanks! ]:.
Last updated: 10:55 AM Monday October 2008
Note: This tutorial has two sets of sourcecode to download!
![]()
We've drawn a triangle, and we've looked at the basic steps needed to draw primitives with the 3D pipeline. This tutorial will go through some more principles, such as culling and vertex buffers in detail. To do that I've split the code up into seperate workspaces, so it's clear what we're working on at each stage. First up, lets take a look at Primitives.exe, which displays something like this:
Fig 2.1: Some 3D Primitives in action
We covered the principles behind this code in the last tutorial, so I'm just going to pick out the interesting parts that have changed. In fig2.1, the top left object is 2 triangles created with a TriList, the bottom left object is a rectangle created with a TriStrip, and the bottom right object is a circle created with a TriFan. Here's the code:
[ Download complete VC6 workspace as .zip ]
Primitives.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
The first additions of interest are the nicely blended colours, called "diffuse colours". This is a built-in feature of D3D, and allows you to specify an RGB value for each vertice you create. When you use the vertices to create a primitive, the diffuse colours of each vertex are smoothly blended together, with the RGB colour you specify most intense at the vertice itself. Note that this blending is only performed for vertices that make up a primitive, and not for any other vertices regardless of how close they are. Take a look at the 2 triangles on the top left in Fig2.1. You can clearly see which vertices have had their colours blended together - because we used a TriList to draw this object the 3 vertices of the left triangle have their diffuse colours blended, and totally seperately the 3 vertices of the right triangle have their diffuse colours blended. Because the 2 triangles are drawn as a TriList, and as you know from tutorial 1 a TriList does not share vertices, no blending takes place between the 2 triangles.So let's see how to add colours to our primitive. I've already given you a big clue on how it's done by saying "specify an RGB value for each vertice you create", which should tell you we need to fiddle with out custom vertex struct & define. It's as easy as this:
typedef struct _tagSimpleVertex
{
float x,y,z;
float rhw;
D3DCOLOR dwDiffuse;
} SIMPLEVERTEX;
#define FVF_SIMPLEVERTEX (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)
I've added a D3DCOLOR member to our custom vertex struct, which is really just a DWORD that's been typedef'd. If you have Visual Assist, click on the D3DCOLOR type and you can see the typedef in the "Definitions" bar. As you can guess, this struct member represents the RGB colour of a vertex. Because we've added an extra member to our struct, we need to update our custom FVF define so D3D knows what to do with the extra data. To do this, we just OR the new D3DFVF_DIFFUSE flag with our existing D3DFVF_XYZRHW flag. Let's take a look at how we actually use this new vertex flag:
TriList[0].x=50.0f; TriList[0].y=200.0f; TriList[0].z=0.0f;
TriList[0].rhw=1.0f;
TriList[0].dwDiffuse=D3DCOLOR_XRGB(255,255,0);
If only everything was this easy! As you can see, I've just tacked on an extra line of code for each vertex to assign an RGB value. In case you're wondering what D3DCOLOR_XRGB is, it's part of a family of macros that does some clever bit shifting on each of the 3 integers to produce a DWORD. I would recommend you always use these D3DCOLOR_ macros (we'll see some more soon) instead of the more standard RGB macro when dealing with D3D colours - the D3DCOLOR_ macros do some extra things to take other D3D features into account.Next I want to take a look at the circle in the bottom right of Fig 2.1. As you may need to generate something similar at some point, let's take a quick look at the code I used to create it. First up I decided how many points on the circle I wanted to specify (remember, a TriFan requires one starting vertice that all the triangles in the fan share, and one triangle is drawn for every vertice specified by using it, the 1st vertice, and the previous vertice). The more points I use the smoother the circle gets, but the more vertices I need (and therefore the slower the rendering process). I decided to go with one point every 9 degrees to give a nice smooth outline. 360 degrees in a circle divided by one point every 9 degrees = 40 points, plus one point for the center of the circle and one point for the closing point of the TriFan gives us a grand total of 42 vertices. Now that we know how many vertices we are going to generate we can create the array and place the first point:
SIMPLEVERTEX TriFan[42];
TriFan[0].x=350;
TriFan[0].y=350;
TriFan[0].z=0.0f;
TriFan[0].rhw=1.0f;
TriFan[0].dwDiffuse=D3DCOLOR_XRGB(255,255,255);
Because all triangle primitives on a TriFan share the first vertice, we need to manually specify where we want it placed - creating the first vertice as part of the circle generation code would make it lie on the circumference of the circle, thus making all the rest of the triangles in the fan offset in relation to the circumference of the circle rather than it's center:
Fig 2.2: First vertice of a generated TriFan circle
For this tutorial it's not a major issue, it just affects the RGB blending. However if you were intending to texturemap this circle, it would be easier to calculate the texture coordinates if the first vertice lay in the center rather than on the circumference. Here's the code to actually generate the circle:
// Programatically generate points on a circle for our TriFan
for(int nIndex=9; nIndex <= 369; nIndex+=9)
{
TriFan[nIndex/9].x=(60.0f*((float)cos(D3DXToRadian((float)nIndex))))+350.0f;
TriFan[nIndex/9].y=(60.0f*((float)sin(D3DXToRadian((float)nIndex))))+350.0f;
TriFan[nIndex/9].z=0.0f;
TriFan[nIndex/9].rhw=1.0f;
TriFan[nIndex/9].dwDiffuse=D3DCOLOR_XRGB(nIndex/2,nIndex/2,nIndex/2);
}
The for loop itself is quite simple. As above, I decided to create 40 vertices for the circle which works out at a vertice every 9 degrees. We iterate up until 369 degrees as we need to create a final vertice that overlaps the 2nd vertice to close the circle - try changing nIndex <= 369 to nIndex <= 360 and you'll see what happens. The more complex part is calculating the X and Y coordinates for each vertice. If we know the radius of the circle we wish to create, we can caluclate a point on it's circumference for any given angle using the following formula:X Zero Based Coordinate = Radius * cos(Angle In Radians)
Y Zero Based Coordinate = Radius * sin(Angle In Radians)
This gives us an X and Y coordinate assuming that the starting coordinate for the center of the circle is at 0,0. To move the circle around on-screen, we simply add an offset to the X and Y values to move the center of the circle. In the code above, I added 350 to the result of the angle calculations to move the circle so it was centered around 350,350. To prevent loss of precision and compiler warnings, I casted the nIndex int to a float, and the result of the sin and cos calculations from double's to float's. The D3DXToRadian macro is a handy tool to quickly convert degrees to radians, which D3D uses a lot. In case you're interested you can manually convert degrees to radians using the following formula:
Radians = Degree Value * (PI / 180.0f)
PI is as precise as you wish to make it. I prefer just to use the macro, though. For the diffuse colour of the circle's vertices, I just divide the current counter (nIndex) value by 2, giving a rough grayscale appearance to demonstrate how the fan is created. At this point if you're still not sure how a TriFan is created, have a play with the code and write your own TriFan.
That's the first 2 objects out the way, now onto the rectangle in the bottom left of Fig 2.1. I created this using a TriStrip with the following vertices:
Fig 2.3: TriStrip
This is totally the wrong way to create a rectangle from a TriStrip, but it clearly shows the difference between a TriStrip and a TriList. As we discussed above, because a TriList doesn't share vertices, the diffuse colour is kept seperate from triangle to triangle. However with a TriStrip, where the vertices are shared, the diffuse colour is blended normally across all the triangles you create (as can be seen from Vertices 2, 3 and 4 in Fig 2.3). Just for clarity, this is the proper way to draw a rectangle with a TriStrip:
Fig 2.4: Correct TriStrip Rectangle
You'll note that the colour diffusion is the wrong way round, because I was too lazy to create another image set ;) Here we've used a TriStrip to create a rectangle in 4 vertices, whereas a TriList would require 6. You may be wondering why I've placed vertice 1 at the bottom left of the rectangle, instead of top left (or indeed top right). This is due to "backface culling".Culling is firmly a 3D principle, but I'm introducing it here as it helps to understand this as quickly as possible. Because we haven't done any true 3D objects yet, some of this will stay as theory for the moment.
Imagine we had a cube that looks something like this:
Fig 2.5: Conceptual cube
The large wireframe cube on the right represents the logical view of a cube, and shows all 6 faces, or sides (front, back, left, right, top, bottom). The small coloured cube on the left represents what we actually see on-screen. Regardless of what angle we view the cube at, we will only ever be able to see half the faces at any one time - in our cube's case, thats 3 faces (6 triangles) out of 6 faces (12 triangles). The faces we can't see are called the "back faces". Because we can't see the remaining 3 faces, there is no point in wasting processor time & system resources rendering them. And thus "backface culling" is used.Backface culling works out which triangles are not visible on-screen, and "culls" them from the scene (ie: doesn't actually draw them). This gives us a nice speed increase, because we're only drawing half the amount of triangles we were before. So how does it work?
There are 2 methods of culling - ClockWise (abbreviated to CW) and CounterClockwise (CCW), and they relate directly to the order in which you specify your vertices. As we know, we use triangles to create objects on-screen, and regardless of the primitive type D3D requires 3 vertices before it can draw a triangle. We can specify these vertices in a clockwise or counterclockwise order:
Fig 2.6: Specifying a vertex order
If we specify our vertices in a clockwise order, we need to use CCW culling. If we specify our vertices in a counterclockwise order, we need to use CW culling. Taking CCW culling as an example, when you call IDirect3DDevice9::DrawPrimitive() and D3D processes your vertices, it will not draw any triangle which has it's vertices specified in a counterclockwise order. In other words, use the opposite of your vertice order for your culling method.No doubt your next 2 questions are "So how does this vertice order relate to culling?" and "If I specify my vertices in a clockwise order but use CCW culling, surely nothing will be culled?". Both are good questions! This part has a tendancy to confuse people who haven't dealt with a 3D GFX API before, so watch out. Grab a pen and a piece of paper, and draw a big triangle like the ones in Fig 2.6 in the middle. Label the vertices as per the left hand (clockwise vertice) triangle in Fig 2.6. Now hold the piece of paper up in front of your face, so you are looking directly at the triangle you just drew. Now turn the piece of paper round so that you're looking at the blank reverse side of the paper - you've just rotated the paper 180 degrees around the Y axis. Hopefully you can see the vertices you marked through the paper - notice how they are now in a counterclockwise order? With CCW culling, when rotated 180 degrees (and thus out of view) the triangle wouldn't be drawn on-screen. If this triangle was part of the back of our cube from Fig2.5, you should be able to see how D3D would save time by not drawing the back face of the cube.
Fig 2.7: Practical example of Backface culling
Because all 3D objects are just triangles, you should be able to see how this principle applies to every 3D object regardless of size and complexity. Make sure you understand this subject clearly, as it will become important when we move onto 3D objects shortly. There are some more issues you need to be aware of that are specifically 3D related, and we'll be returning to culling when we start on 3D objects. I'll show you the mathematical proof behind culling then.We control the D3D culling mode with the IDirect3DDevice9::SetRenderState() method. It's prototype is as follows:
HRESULT SetRenderState(D3DRENDERSTATETYPE State, DWORD Value);
You'll be seeing a lot of this method, as it's one of the ways we directly control D3D's graphics pipeline. To set the culling mode, we use the following call:rslt = g_pDevice->SetRenderState(D3DRS_CULLMODE, Value);
where Value is one of:
D3DCULL_CCW - Counterclockwise Backface Culling
D3DCULL_CW - Clockwise Backface Culling
D3DCULL_NONE - Do not cull, render primitives even if they are hidden
More than once I've been testing some scrappy code out, and found that even though no errors or failed HRESULT's were occuring I still couldn't get any output on-screen. Most of the time it was due to me specifying the vertices in a random or non-clockwise order, and forgetting to SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);. Be aware that by default D3D9 uses CCW culling, so remember to explicitly turn it off if you don't want it. As a side note, because of this I generally specify vertices in a clockwise order.
If you take a look in the GameInit() function in Primitives.cpp, you'll see I make the following call:
g_pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
As above, this turns off culling to allow the badly arranged TriStrip rectangle in Fig 2.3 to be drawn. If you comment out this line and rebuild the code, you'll see that the TriStrip rectangle is culled.Before we wrap this tutorial up, I want to show you one last item. You may have already figured this out for yourself, in which case you've done well! Let's re-examine at a couple of prototypes we covered in tutorial 1. Firstly, the IDirect3DDevice9::CreateVertexBuffer() method:
HRESULT CreateVertexBuffer(UINT Length, DWORD Usage, DWORD FVF, D3DPOOL Pool,
IDirect3DVertexBuffer9** ppVertexBuffer, HANDLE* pHandle);
Note that this method does not require you to specify the type of primitive you're placing in the vertex buffer. Next, the IDirect3DDevice9::DrawPrimitive() method:HRESULT DrawPrimitive(D3DPRIMITIVETYPE PrimitiveType, UINT StartVertex, UINT PrimitiveCount);
Here you must specify the type of primitive you wish to draw, but you can select a subset of vertices from the vertex buffer currently bound to the stream source. Judicious usage of these two methods would allow us to create one massive vertex buffer that could hold all 3 of our vertex arrays (TriStrip[], TriList[] & TriFan[]), copy the 3 arrays' data into it, and then use specific sections of it to draw our different primitive types.It turns out that this ability is a good thing, because it's generally discouraged to use more vertex buffers than you absolutely require. If you look in the DirectX Utilities program group on your Start menu and load the DirectX Caps viewer, you can see for yourself just how many vertices one vertex buffer (and one call to DrawPrimitive()) can support - 65536, to be precise. Note that the value you see in the Caps viewer is a zero based index, 0 to 65,535.
So let's see how to change our code to use just the one vertex buffer:
[ Download complete VC6 workspace as .zip ]
PrimitivesOneVB.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]
Using one vertex buffer to draw our 3 shapes does reduce the clarity of the code slightly, but it's still easy enough to follow. First of all we only need to create one vertex buffer at global scope:
LPDIRECT3DVERTEXBUFFER9 g_pVertexBuffer;
Nothing unusual there. Let's move on to GameInit(). We create our vertex arrays as normal, no change there. The first major difference is when we create the vertex buffer and copy data to it:
// Now create one single vertex buffer for all our primitives
rslt=g_pDevice->CreateVertexBuffer(sizeof(TriList)+sizeof(TriStrip)+sizeof(TriFan), 0, FVF_SIMPLEVERTEX, D3DPOOL_DEFAULT, &g_pVertexBuffer, NULL);
Because we're going to use just one vertex buffer for all 3 arrays, we use the total size of the 3 array sizes added together for the vertex buffer's size. That's the only change we need to make here. Next we lock the buffer as normal:
BYTE* pVerticeLock=0;
rslt=g_pVertexBuffer->Lock(0, sizeof(TriList)+sizeof(TriStrip)+sizeof(TriFan), &pVerticeLock, 0)
Again, as we're using the one vertex buffer we need to lock enough memory for all 3 of our arrays. There are two possible changes we could make here. First, instead of locking the entire vertex buffer we could lock individual sections using the first parameter of IDirect3DVertexBuffer9::Lock() to offset the lock in the buffer. This would allow us to copy the individual arrays in seperately, making sure we aren't able overwrite any memory we shouldn't. The second possibility is to use the size of the vertex buffer as the second parameter of IDirect3DVertexBuffer9::Lock() - in this example code I just used the sizeof() the 3 arrays for clarity.Now that our vertex buffer has been locked, we need to copy the data into it. Time for some good old C++ pointer math:
// ...and copy our array of TriList vertices straight into it
CopyMemory(pVerticeLock, &TriList, sizeof(TriList));
// Next, increment the pointer by the size of the TriList vertex array we just copied...
pVerticeLock+=sizeof(TriList);
// ... and copy the TriStrip array, effectively appending it to the TriList data.
CopyMemory(pVerticeLock, &TriStrip, sizeof(TriStrip));
// Increment the pointer by the size of the TriStrip array...
pVerticeLock+=sizeof(TriStrip);
// ...and again copy the TriFan array in.
CopyMemory(pVerticeLock, &TriFan, sizeof(TriFan));
Initially, pVerticeLock contains a pointer to the start of the vertex buffer so we can just use it to copy our TriList[] array straight in. Next we need to increment the memory location our pointer points to by the size of the TriList[] array. Our pointer now points to somewhere in the vertex buffer memory area immediately after our TriList data. Next we copy in the TriStrip[] array, and increment the pointer by the size of the array. The pointer now points to somewhere in the vertex buffer immediately after our TriStrip data. Finally we copy the TriFan[] array into the buffer, and unlock the vertex buffer.These steps are nothing more complicated that a little pointer math, which you should understand (a good understanding of C++ was a requirement, remember!). If you're a little rusty, this diagram should clear it up:
Fig 2.8: Vertex buffer pointer math
In the diagram, pVertice represents the pVerticeLock pointer (shortened for space reasons).We've got our data in the vertex buffer, the final piece of the puzzle is how to get it out and draw our primitives. From the Render() function:
g_pDevice->SetFVF(FVF_SIMPLEVERTEX);
g_pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(SIMPLEVERTEX));
// First tell D3D to take the first 6 vertices and draw a TriList with them.
// 6 vertices because D3DPT_TRIANGLELIST = TriList, therefore 3 vertices per primitive * 2 primitives
g_pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
Our calls to SetFVF() and SetStreamSource() remain the same as before. For our first object, the TriList triangles, the DrawPrimitive() call remains the same. I'll explain the code comment in more detail. The final parameter of DrawPrimitive() is the number of primitives this call should draw. Because we specified D3DPT_TRIANGLELIST as our primitive type, D3D knows that it should use the first 6 vertices - 2 primitives multiplied by 3 vertices per primitive for a TriList = 6 vertices. Next we draw the TriStrip:
g_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 6, 3);
Here we make use of DrawPrimitive()'s second parameter, the zero based index of the first vertex in the vertex buffer to use for this primitive. As we know our TriStrip is the second array we copied in the vertex buffer, and the TriList that preceeds it was 6 vertices, we tell D3D to start using vertices from index number 6 (the 7th vertice in the buffer). As per Fig2.3, we want D3D to draw 3 primitives so we supply 3 for the last parameter. Finally our TriFan:
g_pDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 11, 40);
Just as before, we tell D3D to start from vertice index number 11 (the 12th vertice), and draw 40 primitives in a TriFan formation. Hopefully you can now see that using a single vertex buffer for all your objects is nothing more than a few calls to sizeof() and a little pointer math!That's all for this tutorial, you've learnt a lot of important concepts here. You've been introduced to culling, which will become very important soon, you're now an expert on vertex buffers which we always need to use, you've drawn 3 major primitive types and you've made some practical changes to custom vertex formats & FVF flags. You're more advanced than you realise! I'll leave you with a few exercises to get you thinking on your own, because the next couple of tutorials are going to get complex! Some hints are below.
Basic
1. Fix the badly arranged TriStrip rectangle (fig 2.3) so that the vertices are specified correctly for CCW culling (as per fig2.4). When you're happy you've done it, turn on CCW culling and make sure you can still see the rectangle! I suggest you use the first sourcecode set with multiple vertex buffers initially, then port your code changes to the single vertex buffer code when you're happy it works.2. In the single vertex buffer source, change the call to IDirect3DVertexBuffer9::Lock() so that it uses the size of the vertex buffer rather than the sizeof(TriList) + sizeof(TriStrip) + sizeof(TriFan) code. You'll need to refer to the SDK documentation.
Intermediate to Advanced
3. Prove to yourself you understand the concepts of culling, primitives and vertex buffers. Create a new project, and paste in the standard D3D setup code. Then create vertex arrays, a vertex buffer and calls to DrawPrimitive() to draw the following shapes. This should be well within your abilities if you've followed the tutorials and understand the material. Take it slow, and test each part of the code as you write it. You may benefit from drawing each shape on paper, dividing it up, picking the correct primitive type, then assigning coordinates to each vertex before you start writing code. The target for this exercise is to use the smallest number of vertices possible by picking the correct primitive type, and specifying the vertices in the correct order so they are visible with CCW culling enabled. Try to use only one vertex buffer.
Fig 2.9: Practical coding exercise - Good luck!
Hint for Exercise 1: Pretty much all the info you need is in this tutorial. Draw it out on paper first, then write the code.
Hint for Exercise 2: Don't be afraid to use the SDK docs. You'll need to investigate the IDirect3DVertexBuffer9 COM interface methods, because you need to see what happens if you call sizeof() on your pVerticeLock var.
Hint for Exercise 3: Sorry, you're not getting any help on this one! Use all the information you already have, it's more than enough to do it!
Click here to send feedback, bug reports, comments and ideas etc!