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

Using Index Buffers to store 3D Geometry
Author : Jack Hoxley
Written : 27th December 2000
Contact : [Web] [Email]
Download : Graph_09.Zip [12 Kb]

Contents of this lesson:
1. Introduction
2. Generating our geometry
3. Rendering our geometry

1. Introduction

Welcome back to the second of the advanced geometry tutorials, things should be starting to fall into place now - and you should be more than capable of creating a very basic 3D engine with your current knowledge. However, there are still a few things to learn before you can create a reasonable, modern 3D engine for your game.

This lesson is going to discuss one trick that you may well find extremely useful if you go onto develop your own engine. Indices - these are a series of numbers, stored in an index buffer, that reference vertices in a vertex buffer. They may not seem very useful, but you can reduce your vertex count 10 fold using them. Consider the cube that I've shown you in several previous lessons. With your current knowledge you could create it using 36 vertices, 18 if you're clever; or you could just generate it using a 3D modeller and import it in as an .X file. The first way is very inefficient, you're using up a large number of vertices for a very very simple piece of geometry. The second way is okay, but can be a bit of a pain when it comes to changing the colours and textures. Now consider the use of an index buffer...

An index buffer is basically a series of numbers (of integer datatype); these numbers reference (any number of times) vertices stored in a vertex buffer. For example, we could generate a vertex buffer in with 8 vertices defining a cube, we could then create an index buffer with 36 entries in it (same number of vertices as used before) to reuse those vertices. Think of it like this - the indices for triangle 1 could be 0, 1 and 3 - which, to the renderer, says: "Make a triangle using vertices 0, 1 and 3". So, all of a sudden our cube becomes 8 vertices instead of the original 36 vertices.

Another way of thinking about it is to realise that of those 36 vertices we've previously used anything from 3-6 vertices have shared the same world-space position, and the same colour; so why not represent those 3-6 vertices with only 1...

One of the main uses I've found for indices in the past is when making custom file formats. You can store the data in a file for 3500 vertices and end up with a rather large data file, or you could store 1200 vertices and a list of 3500 indices and get a relatively small file.

Despite the fact that indices and index buffers are extremely useful, there are also several downsides to using them, the main one being that all indices sharing the same vertex must have the same properties - same position, same colour, same texture coordinates, same normal.
• In our cube example we couldn't redesign it so that one face was blue, one face was green and so on.... each face would blend into each other
• Texturing becomes really difficult. How would you set texture coordinates for our cube so that you got different parts of a texture on each face - or a different texture on each face. There are some tricks involving mult-pass texturing and multiple stage texturing, but their not always available and are often quite slow.
• Lighting normals must be the same, this isn't too difficult as you average the normals for the 3 sides of our cube using addition (as illustrated in the lighting tutorial). Because all the normals will be averaged it sometimes looks a little odd - not always, but sometimes the lights appear to behave incorrectly and unnaturally - something you want to avoid when making realistic / semi-realistic environments.

The arguments for and against have been laid out, it's up to you to decide if and when you want to use index buffers. Either way, it's useful to know how they work...

2. Generating our Geometry

Thankfully this part actually gets much easier when using index buffers; Hopefully you've been following the series from the beginning, and you should remember the several occasions that I've made a cube in the sample application - and in the "InitialiseGeometry( )" procedure we have 150 lines of repetitive code for generating a simple cube. You may not have realised it, but it was an absolute pain to search for an incorrect vertex in there - and writing the code from scratch was annoying enough. Using indices we create 8 vertices and 36 indices, if the vertex positions are wrong I have 8 of them to look through - not too hard, and if the indices are wrong I just have to juggle a few numbers around.

Private Function InitialiseGeometry() As Boolean
On Error GoTo BailOut: '//Setup our Error handler

'//1. Generate the 8 vertices for our cube (2x2x2 in size)

    CubeVertices(0) = CreateLitVertex(-1, -1, -1, &HFF0000, 0, 0, 0)
    CubeVertices(1) = CreateLitVertex(-1, 1, -1, &HFF00&, 0, 0, 0)
    CubeVertices(2) = CreateLitVertex(1, -1, -1, &HFF&, 0, 0, 0)
    CubeVertices(3) = CreateLitVertex(1, 1, -1, &HFF00FF, 0, 0, 0)
    CubeVertices(4) = CreateLitVertex(-1, -1, 1, &HFFFF00, 0, 0, 0)
    CubeVertices(5) = CreateLitVertex(-1, 1, 1, &HFFFF, 0, 0, 0)
    CubeVertices(6) = CreateLitVertex(1, -1, 1, &HFFCC00, 0, 0, 0)
    CubeVertices(7) = CreateLitVertex(1, 1, 1, &HFFFFFF, 0, 0, 0)

'//2. Create the vertex buffer
    Set VBuffer = D3DDevice.CreateVertexBuffer(Len(CubeVertices(0)) * 8, 0, Lit_FVF, D3DPOOL_DEFAULT)
    If VBuffer Is Nothing Then GoTo BailOut:
    D3DVertexBuffer8SetData VBuffer, 0, Len(CubeVertices(0)) * 8, 0, CubeVertices(0)

'//3. Generate our indices

    CubeIndices(0) = 0: CubeIndices(1) = 1: CubeIndices(2) = 2
    CubeIndices(3) = 1: CubeIndices(4) = 3: CubeIndices(5) = 2
    CubeIndices(6) = 2: CubeIndices(7) = 3: CubeIndices(8) = 6
    CubeIndices(9) = 3: CubeIndices(10) = 7: CubeIndices(11) = 6
    CubeIndices(12) = 6: CubeIndices(13) = 7: CubeIndices(14) = 4
    CubeIndices(15) = 7: CubeIndices(16) = 5: CubeIndices(17) = 4
    CubeIndices(18) = 4: CubeIndices(19) = 5: CubeIndices(20) = 0
    CubeIndices(21) = 5: CubeIndices(22) = 1: CubeIndices(23) = 0
    CubeIndices(24) = 1: CubeIndices(25) = 5: CubeIndices(26) = 3
    CubeIndices(27) = 5: CubeIndices(28) = 7: CubeIndices(29) = 3
    CubeIndices(30) = 2: CubeIndices(31) = 6: CubeIndices(32) = 0
    CubeIndices(33) = 6: CubeIndices(34) = 4: CubeIndices(35) = 0

'//4. Create our index buffer
    Set IBuffer = D3DDevice.CreateIndexBuffer(Len(CubeIndices(0)) * 36, 0, D3DFMT_INDEX16, D3DPOOL_DEFAULT)

    D3DIndexBuffer8SetData IBuffer, 0, Len(CubeIndices(0)) * 36, 0, CubeIndices(0)
InitialiseGeometry = True
Exit Function
Debug.Print "Error occured in InitGeometry() Code"
InitialiseGeometry = False
End Function

There, easy isn't it. I've written all the indices so that there is 1 triangle per line - 2 triangles per face with a heading. The indices still need to be in the correct order - clockwise usually, and you cant put invalid vertex numbers in- so you still have to be a little bit careful.

The first stage, whilst it doesn't have to be, is creating the vertices and vertex buffers - we've covered them in previous lessons and aren't that difficult so I'll skip straight along to indices and index buffers.

The first part of generating indices is to fill the correct sized array with the correct values - the array is of type integer and has the same number of elements that we'd normally use when creating vertices - for a cube this would be 36.

The second part creates a blank area in memory for holding our array of indices; this must be done before we try and fill the buffer with our data - otherwise some strange memory errors are going to appear. To create an index buffer we use the D3DDevice.CreateIndexBuffer( ) call, which takes the following parameters:

LengthInBytes As Long = How much memory we need to allocate, this is basically "Len( first element in array ) * number of elements in the array"
Usage As Long = This allows the device to place our index buffer in the most efficient place available; see the "D3DUSAGE_" flags in the object browser. Leave as 0 for most applications
Format As D3DFORMAT = What format the indices are, D3DFMT_INDEX16 and D3DFMT_INDEX32 are the only valid flags; use D3DFMT_INDEX16 if you're using an integer array, and D3DFMT_INDEX32 if you're using a long array - but it's unlikely you'll ever need to go above 32767 indices anyway (even if the driver lets you) so an integer array will almost always suffice.
Pool As CONST_D3DPOOL = Where you want the index buffer to be stored. This can usually be left as D3DPOOL_DEFAULT, but if you intend to keep changing the contents it's best to place it in system memory - D3DPOOL_SYSTEMMEM; D3DPOOL_MANAGED allows DirectX and the driver to shift it's location around as it sees fit....

Finally we fill the index buffer with our data; this is almost identical to filling a vertex buffer with data. It uses the hidden procedure "D3DIndexBuffer8SetData", which takes these parameters:

IBuffer as Direct3DIndexBuffer8 = The index buffer we're using, this must be created first - as we've just done.
Offset as Long = Where we start writing the data (in bytes); this should be 0 if this is the first write; otherwise you can set it to other values should you wish to insert data later on.
Size as Long = How big the Index buffer is - the same as we specified when creating it; this shouldn't be any bigger than you created it, otherwise it'll write to memory that doesn't belong to it and it'll give you an error, if not crash the whole program.
Flags as Long = A combination of the CONST_D3DLOCKFLAGS enumeration; this can be left to 0 for a first write, other values should be specified when resetting data later on.
Data as Any = the data that we want to fill it, defined as "Any" so that we can pass any datatype to the procedure. This will be the first element in our array of indices that we want copying; if you wish to start elsewhere specify a different array entry - CubeIndices(12) for example...

At this point in time we'll have a functioning vertex buffer and index buffer - all the information we need to render our geometry; if we really wanted we could discard the vertex array and index array - but they may well prove to be useful later on, it all depends on your engine/application though....

3. Rendering our Geometry

Rendering the geometry isn't greatly difficult, it just requires that we tell the device where our vertex buffer is, where our index buffer is and what to render. This is accomplished through the following code, which goes between the D3DDevice.BeginScene and D3DDevice.EndScene calls (which I shouldn't really need to remind you about).

D3DDevice.SetStreamSource 0, VBuffer, Len(CubeVertices(0))
D3DDevice.SetIndices IBuffer, 0
D3DDevice.DrawIndexedPrimitive D3DPT_TRIANGLELIST, 0, 36, 0, 12

First we tell it which vertex buffer we're using and what size the information is - something you should be able to do by now. Then we tell it which index buffer to use, and a base value - this value is most interesting if you're using different vertex buffers but the same index buffer. This value is added to all indices before their referenced to the vertex buffer - effectively setting it so that you're referencing a whole new section of the vertex buffer. For example, if this were set to 10, the first triangle 0,1,2 would actually reference 10,11,12 in the vertex buffer... (This is called the BaseVertexIndex)

Lastly we actually render the geometry - Using a new rendering call. The parameters go like this:
PrimitiveType As CONST_D3DPRIMITIVETYPE = How the vertices/indices are arranged - you should be able to do this; it was described in the basic geometry section. This should be the same as how you've designed the indices.
MinIndex As Long = This is the same as the BaseVertexIndex value in the SetIndices call.
NumIndices As Long = The number of indices Direct3D will use, starting from BaseVertexIndex + MinIndex
StartIndex As Long = The location in the index buffer to start reading indices.
PrimativeCount As Long = The number of triangles being rendered.

The above call, with the 12 and 36 numbers changed should be suitable for most uses; but one final note - D3DPT_POINTLIST is not supported for indices and should not be used.

There, all you need to know about drawing index based geometry - not too complicated really, and quite a neat trick to learn. I suggest you download the source code for this lesson, as a lot of the missed out things are the basic framework we have built up over the last 8 lessons.

If you think you've understood that, or just dont care, you can move onto Lesson 10 : Advanced Geometry part 3 - Vertex / Mesh animation.

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