52880.fb2
In this tutorial we will look at Index Buffers. First of all, we'll improve our CCuboid class to use index buffers. Then we'll create a new class called CTerrain, which we can use to generate a very simple terrain. Once the terrain has been created, we'll add a grass texture to make it seem more realistic. You can download the full source code by clicking the "Download Source" link above.
An index buffer is a memory buffer that holds indices that "point" to vertices in your vertex buffer. When a scene is rendered, DirectX performs certain calculations on each vertex such as lighting and transformations. What we want to do is minimise the amount of calculations that DirectX has to do, therefore, we need to minimise the number of vertices. We can do this using an index buffer. Lets say you want to draw a square. Your square will be made up from two triangles, which is six vertices (using a triangle list). But we only really need four vertices to define a square (one for each corner). We had to use six vertices because two of them are shared (have the same value). Because we have shared vertices, it's a good idea to use an index buffer. Here's how it works: we define the four corners of our square as vertices in our vertex buffer. Then we define six indices in our index buffer each of which "point" to a vertex in the vertex buffer. We then render our triangles from the indices in the index buffer and so, only use four vertices. Fig 8.1 below shows this example.
Fig 8.1
In the example above, we have only used four vertices to describe our square, which is a saving of two vertices. The index buffer contains six elements, each of these elements contains an index to a vertex in the vertex buffer. Notice that the order in the index buffer is in a clockwise direction, this is the order in which vertices will be rendered. We then render our scene as before, except this time we use the index buffer in addition to our vertex buffer. So, if we apply this technique to our cube, we will be saving twelve vertices (24 rather than 36). That doesn't sound that much does it? But imagine a scene that contains 100 cubes, that's a saving of 1200 vertex calculations per frame!
The first thing that we are going to do is add an index buffer to our CCuboid class. To do this, we need to add the following code:
Step 1: Creating the Index Buffer
First of all, create two new member variables: one LPDIRECT3DINDEXBUFFER8 called m_pIndexBuffer and a DWORD called m_dwNumOfIndices. m_pIndexBuffer will be a pointer to our index buffer and m_dwNumOfIndices will be the total number of indices for our object, in this case 36 (6 faces x 2 triangles per face x 3 vertices per triangle), which will be set in the constructor.
Then we have a new method called CreateIndexBuffer. We'll call this method once at start-up (in the constructor) before we set our vertex values in our vertex buffer. We populate the values in an index buffer in the same way that we populate vertices in a vertex buffer. First we create the index buffer and get a pointer to it (m_pIndexBuffer). We then store the 36 indices in a temporary WORD array ready to be copied into the index buffer. Then we lock the index buffer, copy the stored indices into it and then unlock it.
LPDIRECT3DINDEXBUFFER8 m_pIndexBuffer;
DWORD m_dwNumOfIndices;
bool CCuboid::CreateIndexBuffer() {
VOID* pBufferIndices;
//Create the index buffer from our device
if (FAILED(m_pD3DDevice->CreateIndexBuffer(m_dwNumOfIndices * sizeof(WORD), 0, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIndexBuffer))) {
return false;
}
//Set values for the index buffer
WORD pIndices[] = {
0, 1, 2, 3, 2, 1, //Top
4, 5, 6, 7, 6, 5, //Face 1
8, 9,10,11,10, 9, //Face 2
12,13,14,15,14,13, //Face 3
16,17,18,19,18,17, //Face 4
20,21,22,23,22,21}; //Bottom
//Get a pointer to the index buffer indices and lock the index buffer
m_pIndexBuffer->Lock(0, m_dwNumOfIndices * sizeof(WORD), (BYTE**)&pBufferIndices, 0);
//Copy our stored indices values into the index buffer
memcpy(pBufferIndices, pIndices, m_dwNumOfIndices * sizeof(WORD));
//Unlock the index buffer
m_pIndexBuffer->Unlock();
return true;
}
Step 2: Modify the Vertex Buffer
In our UpdateVertices method, we need to make a few changes. First, and most importantly, notice that we are only defining 24 vertices in our cvVertices array rather than 36. As in the last tutorial, we initialise our vertex normals to zero.
The other difference in this tutorial is that we will calculate the average normal for shared vertices. We don't really need to do this for a cube's shared vertices, but we will as a demonstration of how to do it. To do this, we need to new arrays: pNumOfSharedPolygons and pSumVertexNormal. These arrays are used in parallel, pNumOfSharedPolygons will keep a count of how many times a given vertex is used (shared) and pSumVertexNormal will add together each triangle normal for faces that share a given vertex. We'll then use these arrays to calculate the average normal for each vertex.
Once we have defined our vertices, we loop through the indices in our index buffer. So, for each triangle we calculate its face normal, increment the vertices count for each of the three vertices and add the face normal to the vertices normal array.
Then we loop through each vertex, and calculate it's average normal by dividing the sum of face normals by the number of times it has been used. We then normalize this value to ensure that the x, y and z parts of the normal vector are between 0 and 1. Finally, we apply the average normal to each vertex and copy the vertices into our vertex buffer as before.
bool CCuboid::UpdateVertices() {
DWORD i;
VOID* pVertices;
WORD* pBufferIndices;
D3DXVECTOR3 vNormal;
DWORD dwVertex1;
DWORD dwVertex2;
DWORD dwVertex3;
WORD* pNumOfSharedPolygons = new WORD[m_dwNumOfVertices]; //Array holds how many times this vertex is shared
D3DVECTOR* pSumVertexNormal = new D3DVECTOR[m_dwNumOfVertices]; //Array holds sum of all face normals for shared vertex
//Clear memory
for (i = 0; i < m_dwNumOfVertices; i++) {
pNumOfSharedPolygons[i] = 0;
pSumVertexNormal[i] = D3DXVECTOR3(0,0,0);
}
CUBOID_CUSTOMVERTEX cvVertices[] = {
//Top Face
{m_rX – (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,}, //Vertex 0
{m_rX – (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,}, //Vertex 1
{m_rX + (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,}, //Vertex 2
{m_rX + (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,}, //Vertex 3
//Face 1
{m_rX – (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,}, //Vertex 4
{m_rX – (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,}, //Vertex 5
{m_rX + (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,}, //Vertex 6
{m_rX + (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,}, //Vertex 7
//Face 2
{m_rX + (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,}, //Vertex 8
{m_rX + (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,}, //Vertex 9
{m_rX + (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,}, //Vertex 10
{m_rX + (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,}, //Vertex 11
//Face 3
{m_rX + (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,}, //Vertex 12
{m_rX + (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,}, //Vertex 13
{m_rX – (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,}, //Vertex 14
{m_rX – (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,}, //Vertex 15
//Face 4
{m_rX – (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,}, //Vertex 16
{m_rX – (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,}, //Vertex 17
{m_rX – (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,}, //Vertex 18
{m_rX – (m_rWidth / 2), m_rY + (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,}, //Vertex 19
//Bottom Face
{m_rX + (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,}, //Vertex 20
{m_rX + (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,}, //Vertex 21
{m_rX – (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ – (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,}, //Vertex 22
{m_rX – (m_rWidth / 2), m_rY – (m_rHeight / 2), m_rZ + (m_rDepth / 2), 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,}, //Vertex 23
};
//Get a pointer to the index buffer indices and lock the index buffer
m_pIndexBuffer->Lock(0, m_dwNumOfIndices * sizeof(WORD), (BYTE**)&pBufferIndices, D3DLOCK_READONLY);
//For each triangle, count the number of times each vertex is used and
//add together the normals of faces that share a vertex
for (i = 0; i < m_dwNumOfIndices; i += 3) {
dwVertex1 = pBufferIndices[i];
dwVertex2 = pBufferIndices[i + 1];
dwVertex3 = pBufferIndices[i + 2];
vNormal = GetTriangeNormal(&D3DXVECTOR3(cvVertices[dwVertex1].x, cvVertices[dwVertex1].y, cvVertices[dwVertex1].z), &D3DXVECTOR3(cvVertices[dwVertex2].x, cvVertices[dwVertex2].y, cvVertices[dwVertex2].z), &D3DXVECTOR3(cvVertices[dwVertex3].x, cvVertices[dwVertex3].y, cvVertices[dwVertex3].z));
pNumOfSharedPolygons[dwVertex1]++;
pNumOfSharedPolygons[dwVertex2]++;
pNumOfSharedPolygons[dwVertex3]++;
pSumVertexNormal[dwVertex1].x += vNormal.x;
pSumVertexNormal[dwVertex1].y += vNormal.y;
pSumVertexNormal[dwVertex1].z += vNormal.z;
pSumVertexNormal[dwVertex2].x += vNormal.x;
pSumVertexNormal[dwVertex2].y += vNormal.y;
pSumVertexNormal[dwVertex2].z += vNormal.z;
pSumVertexNormal[dwVertex3].x += vNormal.x;
pSumVertexNormal[dwVertex3].y += vNormal.y;
pSumVertexNormal[dwVertex3].z += vNormal.z;
}
//Unlock the index buffer
m_pIndexBuffer->Unlock();
//For each vertex, calculate and set the average normal
for (i = 0; i < m_dwNumOfVertices; i++) {
vNormal.x = pSumVertexNormal[i].x / pNumOfSharedPolygons[i];
vNormal.y = pSumVertexNormal[i].y / pNumOfSharedPolygons[i];
vNormal.z = pSumVertexNormal[i].z / pNumOfSharedPolygons[i];
D3DXVec3Normalize(&vNormal, &vNormal);
cvVertices[i].nx = vNormal.x;
cvVertices[i].ny = vNormal.y;
cvVertices[i].nz = vNormal.z;
}
//Get a pointer to the vertex buffer vertices and lock the vertex buffer
if (FAILED(m_pVertexBuffer->Lock(0, sizeof(cvVertices), (BYTE**)&pVertices, 0))) {
return false;
}
//Copy our stored vertices values into the vertex buffer
memcpy(pVertices, cvVertices, sizeof(cvVertices));
//Unlock the vertex buffer
m_pVertexBuffer->Unlock();
//Clean up
delete pNumOfSharedPolygons;
delete pSumVertexNormal;
pNumOfSharedPolygons = NULL;
pSumVertexNormal = NULL;
return true;
}
Step 3: Render
Our render function stays the same apart form two things, first of all, we need to tell DirectX that we want to render our polygons from an index buffer. We do this with a call to SetIndices, passing in the index buffer pointer that we want to use. Also, we need to use the DrawIndexedPrimitive method rather than DrawPrimitive to render our polygons.
//Select index buffer
m_pD3DDevice->SetIndices(m_pIndexBuffer, 0);
//Render polygons from index buffer
m_pD3DDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, m_dwNumOfVertices, 0, m_dwNumOfPolygons);
To create our terrain, we will use the same techniques as we did to create our cube. The terrain will be made up from a 20×20 grid of quads (squares made up from 2 triangles). We will use an index buffer, which will reduce the number of vertices from 2400 to 441. I'm not going to post the code here because it is very similar to the code above. The only real differences are the way the index and vertex buffers are populated. You can download the full source code by clicking the "Download Source" link above. Fig 8.2 below shows our flat terrain grid. You can render your scene in "wireframe" mode with one simple call to SetRenderState which is also shown below.
//Set fill state. Possible values: D3DFILL_POINT, D3DFILL_WIREFRAME, D3DFILL_SOLID
m_pD3DDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
Fig 8.2
Now that we have a flat grid, we can turn this into a simple terrain by setting a random value for y for each vertex. I've done this for each vertex in the grid except the ones that are around the edge so that the four edges are all the same level. If you want to create a more detailed terrain, have a look at http://www.gameprogrammer.com/fractal.html where there is a tutorial on how to generate a random fractal terrain. Fig 8.3 below shows the grid with random y values.
Fig 8.3
The last thing to do is apply a grass texture to the terrain and set the render state to solid rather than wireframe. Once you have done this, you should end up with a scene that looks like the screenshot below.
In this tutorial we've look at index buffers and how they can make a massive saving on how many vertices need to be processed. In the next tutorial we will look at how to create other shapes such as spheres, cylinders and cones.