Index And Vertex Buffers

Introduction

In the first tutorial in the Primitives set, we learned the basics of drawing primitives in XNA. What we did, though, has one very serious limitation: it takes a ton of data to store it all. If you did the second tutorial, which gave you some practice with drawing primitives, then I'm sure you noticed how much data there was to type in and manage. This, of course, is exaggerated even more when you are working with a very large model with thousands or even hundreds of thousands of triangles. In this tutorial, we will discuss combining the vertex buffers we learned about in that first tutorial with index buffers. Using these allows us to more effectively store our data, and we will see that this will greatly reduce the amount of data that needs to be sent over to the graphics device too, which will speed up our applications as well. Vertex and index buffers are really the only way to store your data, so let's go ahead and learn how to use them.

The Need for Index Buffers

Working with index buffers complicates things a bit more than simply using vertex buffers. So I think it is fair to explain why we would want to use them.

For starters, the way we have been creating our list of vertices is kind of inefficient. To better explain, the image below shows all of the vertices that we would need to define, using our original method. There's eighteen total.

BadWay.png

Let's take a closer look at this though. There are really only seven vertices in this drawing, as shown below.

GoodWay.png

Wouldn't it be better if we could just create our list of vertices (with no repeats), and then instead of repeating them over and over, just number the vertices, and for each triangle, state which vertices it uses? Yep, you guessed it! It's much better. And that is the basic idea behind vertex and index buffers. We will create a list of vertices and put them in a vertex buffer, and then create a list of indices that each triangle uses. It will save us a lot of work in the end.

In addition, storing all of our geometry this way saves a ton of space on the graphics card (and that also means that there's less of it to send) so our performance will get a boost, too.

Creating and Using Index and Vertex Buffers

For lack of a better idea that isn't overly complicated, in this tutorial, we're just going to remake the icosahedron from the last tutorial but use index buffers. This means that we will still need to type in a fair amount of vertex data anyway (there are 20 vertices, and 60 indices), but I'd recommend you just copy and paste that straight over from my code below. (You already know how to do that part….)

We'll break down the process into three major steps, which are to create the necessary variables for our vertex and index buffers, setting up the vertex and index buffers, and then finally, drawing with the vertex and index buffers.

Creating the Necessary Variables

Our first step is to create the necessary instance variables. We will need to add the following two variables to our class:

VertexBuffer vertexBuffer;
IndexBuffer indexBuffer;

The vertexBuffer is the same thing we used in the earlier tutorials, but the indexBuffer is new.

Setting Up the Vertex and Index Buffers

Go back to your LoadContent() method, where we set up all of the vertices in the previous tutorials. The old vertex stuff from the previous tutorial is all useless now, so if you are working from that code, just go ahead and delete it all. Of course, feel free to put these things in their own method if you would like. Usually, in a practical application of these things, you are getting all of your information from a file, or generate it randomly, rather than typing it all in by hand.

The next step is to set up all of the vertex data for our vertex buffer. So in the LoadContent() method, add the following code:

            // A temporary array, with 12 items in it, because
            // the icosahedron has 12 distinct vertices
            VertexPositionColor[] vertices = new VertexPositionColor[12];
 
            // vertex position and color information for icosahedron
            vertices[0] = new VertexPositionColor(new Vector3(-0.26286500f, 0.0000000f, 0.42532500f), Color.Red);
            vertices[1] = new VertexPositionColor(new Vector3(0.26286500f, 0.0000000f, 0.42532500f), Color.Orange);
            vertices[2] = new VertexPositionColor(new Vector3(-0.26286500f, 0.0000000f, -0.42532500f), Color.Yellow);
            vertices[3] = new VertexPositionColor(new Vector3(0.26286500f, 0.0000000f, -0.42532500f), Color.Green);
            vertices[4] = new VertexPositionColor(new Vector3(0.0000000f, 0.42532500f, 0.26286500f), Color.Blue);
            vertices[5] = new VertexPositionColor(new Vector3(0.0000000f, 0.42532500f, -0.26286500f), Color.Indigo);
            vertices[6] = new VertexPositionColor(new Vector3(0.0000000f, -0.42532500f, 0.26286500f), Color.Purple);
            vertices[7] = new VertexPositionColor(new Vector3(0.0000000f, -0.42532500f, -0.26286500f), Color.White);
            vertices[8] = new VertexPositionColor(new Vector3(0.42532500f, 0.26286500f, 0.0000000f), Color.Cyan);
            vertices[9] = new VertexPositionColor(new Vector3(-0.42532500f, 0.26286500f, 0.0000000f), Color.Black);
            vertices[10] = new VertexPositionColor(new Vector3(0.42532500f, -0.26286500f, 0.0000000f), Color.DodgerBlue);
            vertices[11] = new VertexPositionColor(new Vector3(-0.42532500f, -0.26286500f, 0.0000000f), Color.Crimson);

This is all the same vertex information as in the last tutorial, but notice that we didn't have to create so many vertices.

Next, we will create the actual vertex buffer and tell it to use the data that we just created. We will do this with the two lines below, so add these to the LoadContent() method, just after the previous code:

           // Set up the vertex buffer
            vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColor), 12, BufferUsage.WriteOnly);
            vertexBuffer.SetData<VertexPositionColor>(vertices);

This is identical to what we've done before, just adjusted for the right number of vertices.

Next is the index buffer. The process for creating the index buffer is actually fairly similar to that of the vertex buffer. We will create all of the data that we need, create an index buffer object, and hand the index data off to the index buffer. So once again, add the following code to your LoadContent() method, right after the code that we just added:

short[] indices = new short[60];
indices[0] = 0; indices[1] = 6; indices[2] = 1;
indices[3] = 0; indices[4] = 11; indices[5] = 6;
indices[6] = 1; indices[7] = 4; indices[8] = 0;
indices[9] = 1; indices[10] = 8; indices[11] = 4;
indices[12] = 1; indices[13] = 10; indices[14] = 8;
indices[15] = 2; indices[16] = 5; indices[17] = 3;
indices[18] = 2; indices[19] = 9; indices[20] = 5;
indices[21] = 2; indices[22] = 11; indices[23] = 9;
indices[24] = 3; indices[25] = 7; indices[26] = 2;
indices[27] = 3; indices[28] = 10; indices[29] = 7;
indices[30] = 4; indices[31] = 8; indices[32] = 5;
indices[33] = 4; indices[34] = 9; indices[35] = 0;
indices[36] = 5; indices[37] = 8; indices[38] = 3;
indices[39] = 5; indices[40] = 9; indices[41] = 4;
indices[42] = 6; indices[43] = 10; indices[44] = 1;
indices[45] = 6; indices[46] = 11; indices[47] = 7;
indices[48] = 7; indices[49] = 10; indices[50] = 6;
indices[51] = 7; indices[52] = 11; indices[53] = 2;
indices[54] = 8; indices[55] = 10; indices[56] = 3;
indices[57] = 9; indices[58] = 11; indices[59] = 0;
 
indexBuffer = new IndexBuffer(graphics.GraphicsDevice, typeof(short), indices.Length, BufferUsage.WriteOnly);
indexBuffer.SetData(indices);

The first group of lines sets up all of the index data. Like with the vertex data, you will usually load this information from a file or calculate it with some sort of algorithm, rather than specifying each one individually, like we do here.

The last two lines set up the index buffer. In the first line, we create the index buffer in a manner that is almost identical to when we created the vertex buffer. Notice that the type we are using is the type of short. If you have a very large model, you might want to consider changing this to int, and changing the type of the indices array to an array of ints as well, because if you have lots of vertices (over about 65,000), you will run out of indices to use in the short type. (In the meantime, if you can get away with using a short, as will be the case in most places, you'll only use half as much memory for your index buffer. In the last line, we set the data for the index buffer in a similar manner to the way we set the data for the vertex buffer.

Drawing with the Buffers

We are now set to draw with our buffers. This is actually a pretty simple step and will be very similar to what we have done before. We need to do two things differently, though. First, we'll need to tell the computer what index buffer we're going to use, and second, we'll use a different command to draw—DrawIndexedPrimitives instead of DrawPrimitives. I'll show you my complete Draw method code, then explain it.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
 
    basicEffect.World = world;
    basicEffect.View = view;
    basicEffect.Projection = projection;
    basicEffect.VertexColorEnabled = true;
 
    GraphicsDevice.SetVertexBuffer(vertexBuffer);
    GraphicsDevice.Indices = indexBuffer;
 
    RasterizerState rasterizerState = new RasterizerState();
    rasterizerState.CullMode = CullMode.None;
    GraphicsDevice.RasterizerState = rasterizerState;
 
    foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
    {
        pass.Apply();
        GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 12, 0, 20);
    }
 
    base.Draw(gameTime);
}

So you'll see here that I have added one line of code that says GraphicsDevice.Indices = indexBuffer;. This simply tells the computer to use our index buffer to draw. (By the way, if you get really bizarre triangles, it might be because you're using the wrong index buffer, or at least, an improperly set up index buffer.)

The other part that is different from what we've done is the line that says:

GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 12, 0, 20);

Here's where we do the actual drawing, using index buffers. We indicate the type of primitive, just like before, then we specify the base vertex and minimum vertex index, both of which will always be 0 unless you're trying to do something really strange with your model, then the number of vertices that are available to draw with (12, in this case, but this would be whatever you need for drawing) then the index to start getting vertices from (again, unless you're doing something bizarre, this will usually be 0 as well) and finally, we indicate the number of primitives to draw (20, in our case, but this would just be the number of triangles that you have in your model).

At this point, you should be able to run your game and see your icosahedron being drawn, like in the image below:

screenshot1.png

What's Next?

This tutorial has covered a lot of important and interesting topics in the category of drawing primitives. Vertex and index buffers are used for all sorts of things. In the next tutorial in this category, we will take a look at how these same methods can easily be used to draw a large variety of primitive types.


Troubleshooting.png Having problems with this tutorial? Try the troubleshooting page!