Last time we talked about how to recover a Device after it has been lost – a small but important detail in building your Direct3D application. To continue the theme of talking about little things that can make a big difference, I’d like to talk about index buffers.
To understand the problem that an index buffer solves, we don’t have to look much further than a cube. If you think about how you’d model a cube, you’ll realize that it takes twelve triangles: six squares each made up of two triangles. Here’s a picture of that:
If you were to try to render this cube using just a VertexBuffer, you’d need to send information about 36 vertices, since each of the twelve triangles is made up of three points. Multiply each vertex by the amount of information that makes up a vertex (three floats for position, two floats of texture information, three move floats for normal information, etc.) and you can see that our cube could easily eat up a nontrivial amount of memory. And if our scene consists of hundreds or thousands of these cubes, then memory usage quickly skyrockets.
You might be thinking, “So what? Memory is cheap these days. My desktop computer has a gigabyte of RAM.” But recall that with Direct3D, we’re actually working with the video hardware at a low level. In effect, we’re writing programs that run both on the CPU and on the GPU (the Graphics Processing Unit, the chip that powers your video card). While GPUs are powerful these days, they’re (probably) not as powerful as the CPU, nor does the graphics card have as much memory as the computer it’s plugged in to. Furthermore, any information that needs to get into the graphics card’s memory has to make its way over the system bus. Whether that’s PCI or AGP, it’s still not infinitely fast, and the less information we have to shuttle around, the better.
Now, you could be somewhat clever and say, “No problem – this is just what a TriangleStrip is good for.” (Remember TriangleStrips?) With a TriangleStrip, we only have to send complete information about the first triangle, then one additional vertex per subsequent triangle. Because each subsequent triangle shares two vertices with the previous triangle, we save transmitting, storing, and processing a whole bunch of redundant information.
However, we can only do so well with a TriangleStrip when working with our cube. If you think about it for a bit, you’ll realize that there’s no way to represent the whole cube as a single strip of triangles. We can do it with two strips, but not with just one. While this may not seem like much of a savings, remember: sometimes we’re drawing hundreds or thousands of instances of a particular shape, and the savings have a multiplying effect. And there are plenty of shapes we might want to draw for which TriangleStrips help even less than they do with the cube.
The solution to our problem will be familiar to anyone that’s done database programming before. In a database, if you have a situation where information is being stored redundantly, you can get rid of that redundancy through a process called normalization. The basic idea is that you store unique information once, and then simply refer to it via an identifier.
In the case of our cube, we really only have eight unique points – the corners of the cube. Every single triangle that makes up that cube consists of three of those points. So what we really want is a way to specify each of the corners exactly once, and then provide a list of triangles in the form, “This one is corner 1, 2, and 3. This one is corner 5, 8, and 2.” And so on and so forth. This is exactly what we get with an IndexBuffer.
Here’s a picture that explains the basic idea. In blue, we have an IndexBuffer, which is basically just a bunch of offsets into a VertexBuffer. The IndexBuffer can refer to a particular vertex more than once, which is where the space savings come in: it’s cheaper to store the number “3” twice than it is to store all the information associated with vertex 3 multiple times.
Working with an IndexBuffer is very similar to working with a VertexBuffer. You simply create an IndexBuffer object, Lock it, fill it with data, and then Unlock it. In this case, however, what we fill it with are the indices of the vertices of the object we’d like to draw. Those vertices are stored in a VertexBuffer, same as always, but now we don’t have to store any redundant ones – if we need to use a vertex more than once, we simply refer to it several times by storing its index several times in the IndexBuffer.
An example might make this clearer. Here’s the modified code for creating the VertexBuffer:
protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionColored.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // What type of vertices 8, // How many device, // The device 0, // Default usage CustomVertex.PositionColored.Format, // Vertex format Pool.Default); // Default pooling CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( -0.5F, 0.5F, -0.5F, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0.5F, -0.5F, Color.Orange.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, -0.5F, -0.5F, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, -0.5F, -0.5F, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0.5F, 0.5F, Color.Blue.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0.5F, 0.5F, Color.Violet.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, -0.5F, 0.5F, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, -0.5F, 0.5F, Color.Orange.ToArgb()); buf.Unlock(); return buf; }
This should all be familiar by now. The only difference is that I’m creating eight vertices for the cube this time instead of the usual three for a single triangle.
The new code comes in when we create the IndexBuffer. Here’s the code for that:
protected IndexBuffer CreateIndexBuffer(Device device) { IndexBuffer buf = new IndexBuffer( typeof(short), // What will the indices be? 36, // How many of them will there be? device, // The device 0, // Advanced setting Pool.Default // Advanced setting ); short[] arr = (short[]) buf.Lock(0, 0); int i = 0; // Front face arr[i++] = 0; arr[i++] = 3; arr[i++] = 1; arr[i++] = 0; arr[i++] = 2; arr[i++] = 3; // Top face arr[i++] = 0; arr[i++] = 1; arr[i++] = 4; arr[i++] = 1; arr[i++] = 5; arr[i++] = 4; // Left face arr[i++] = 0; arr[i++] = 4; arr[i++] = 2; arr[i++] = 2; arr[i++] = 4; arr[i++] = 6; // Right face arr[i++] = 1; arr[i++] = 7; arr[i++] = 5; arr[i++] = 1; arr[i++] = 3; arr[i++] = 7; // Back face arr[i++] = 4; arr[i++] = 5; arr[i++] = 6; arr[i++] = 5; arr[i++] = 7; arr[i++] = 6; // Bottom face arr[i++] = 2; arr[i++] = 6; arr[i++] = 3; arr[i++] = 3; arr[i++] = 6; arr[i++] = 7; buf.Unlock(); return buf; }
Like I said, it’s a lot like creating a VertexBuffer. We start with the constructor call:
IndexBuffer buf = new IndexBuffer( typeof(short), // What will the indices be? 36, // How many of them will there be? device, // The device 0, // Advanced setting Pool.Default // Advanced setting );
The arguments here indicate what type of data we’ll be storing, the number of indices we plan to store, the Device object (of course), and a couple of other parameters that are beyond the scope of this discussion. Here we’re using a 16-bit short to store indices – our only other choice is a 32-bit long.
Once the IndexBuffer is created, we Lock it, write data into it, and Unlock it:
short[] arr = (short[]) buf.Lock(0, 0); int i = 0; // Front face arr[i++] = 0; arr[i++] = 3; arr[i++] = 1; arr[i++] = 0; arr[i++] = 2; arr[i++] = 3; // other faces here buf.Unlock();
What this is saying is, “The first data point is vertex #0, the second data point is vertex #3, the third data point is vertex #1, etc. etc.” Notice that we haven’t actually said which vertex #0 is – that part comes later when we’re rendering.
Speaking of which, here’s our updated Render method:
protected void Render() { // Clear the back buffer device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); // Ready Direct3D to begin drawing device.BeginScene(); // Set the Matrices SetupMatrices(); // Draw the scene - 3D Rendering calls go here device.SetStreamSource(0, vertices, 0); // New code: device.Indices = indices; device.DrawIndexedPrimitives( PrimitiveType.TriangleList, // Type of primitives we're drawing 0, // Base vertex 0, // Minimum vertex index 8, // Number of vertices used 0, // Start index 12); // Number of primitives // Indicate to Direct3D that we’re done drawing device.EndScene(); // Copy the back buffer to the display device.Present(); }
I’ve indicated the new code, but I’d also like to point out that we still make the call to SetStreamSource. Remember – an IndexBuffer allows us to cut down on the number of vertices we need to supply, but we still have to have some vertices, and therefore need a VertexBuffer.
The first line of new code
device.Indices = indices;
simply associates the IndexBuffer we’ve created with the Device. It’s important because when we do operations that require an IndexBuffer, the Device needs to know which one to use.
The second line of new code is where the action is.
device.DrawIndexedPrimitives( PrimitiveType.TriangleList, // Type of primitives we're drawing 0, // Base vertex 0, // Minimum vertex index 8, // Number of vertices used 0, // Start index 12); // Number of primitives
You recall DrawPrimitives, the method we’ve been using to draw triangle lists, triangle strips, or any of the other primitives we’ve talked about, to the screen. DrawIndexedPrimitives is the equivalent, only it requires the presence of an IndexBuffer.
Some of the arguments to DrawIndexedPrimitives are the same as the arguments to DrawPrimitives. For example, we still specify a PrimitiveType as the first argument, and we can still use most of the same values here (TriangleList, TriangleStrip, etc.) that we used when calling DrawPrimitives. The one exception is PrimitiveType.PointList, which is not valid when calling DrawIndexedPrimitives. And the last argument is still a number of primitives – that is, triangles, lines, or whatever you’re drawing. It is not the number of vertices/indexes – in this respect it is just like DrawPrimitives.
The second argument to DrawIndexedPrimitives (I’ll abbreviate it DIP from here), is the base vertex index. It’s meant to act as a sort of starting point within the VertexBuffer, to make it easy for us to move around within the buffer, using different regions of it during different calls. For simple cases like what we’re looking at, passing zero here is sufficient – it just means that we’re going to start with the rendering at the beginning of the vertex buffer. If we had passed nonzero here, it means that we would not be using some of the vertices from the beginning of the VertexBuffer, but instead would be starting from somewhere farther in.
The next pair of arguments tell DIP what range of indices we’re going to use. The minimum vertex index is the smallest index that we plan to use. It’s relative to the base vertex index passed in the previous argument. So if we pass 4 for this argument and 6 for the base vertex index, the hardware knows we’re not planning to use any of the vertices before the tenth vertex (6 + 4) in the vertex buffer. It’s used as a performance optimization, so that the rendering hardware can make some guesses about which vertices we’ll be using, and skip processing other vertices in the VertexBuffer.
Going hand-in-hand with the minimum vertex index is the next argument, the number of vertices used. This tells Direct3D how big a chunk of the VertexBuffer we plan to make use of. It is not legal for indexes in the IndexBuffer to be less than (base vertex - minimum vertex index), or to be greater than (base vertex - minimum vertex index + number of vertices used). Again, this is a performance optimization.
The fifth argument to the DIP call tells Direct3D where in the IndexBuffer to start, in case we don’t want to start at the beginning. Note that this is different from the base vertex argument – it’s an offset into the IndexBuffer, not the VertexBuffer.
With all these offsets and indices, some pictures might help a little bit. Here’s the first one:
This picture shows the “normal” case that we’ve been talking about so far – a base vertex index of zero (start from the beginning of the VertexBuffer), a minimum vertex index of two (we won’t use any vertices before the third), three for the number of indices (we’re using vertices 2-4), and a start index of zero (start at the beginning of the IndexBuffer).
The curly bracket above the IndexBuffer is meant to show that this particular call to DIP uses three entries from the IndexBuffer, which might correspond to rendering a single triangle.
Here’s a slightly more complicated example:
In this example, I’ve specified a non-zero base vertex index. You’ll notice that it’s pretty much the same as the last example, except it shifts the section of the VertexBuffer we use up by two, so now we’re using vertices four, five, and six.
To finish our discussion of the parameters, let’s take a look at what happens when we specify a non-zero start index:
Notice what’s happened – the curly brace has moved, specifying a whole different set of indices, resulting in a completely different set of vertices. That’s because – as we discussed – the start index indicates that Direct3D offset into the IndexBuffer.
Note that we had to adjust the number of vertices and minimum vertex index appropriately. Note particularly that I had to adjust the number of vertices to five, not three – Direct3D wants to know the size of the range of vertices you’re going to use, not how many you actually reference.
Now that you understand how to use an IndexBuffer, I should mention that using indexed primitives is not without drawbacks. One of the biggest is a direct consequence of the thing we like most about IndexBuffers: that they enable vertex sharing. What I’m talking about is the fact that if you want a vertex to have two different values on two different faces of an object, you wind up being unable to share that vertex. It only makes sense – you can’t really give a single vertex two normals, but you can easily give two different vertices with the same position two different normals.
In practice this turns out not to be a big problem. Most objects are going to have lots and lots of vertices that are shared by more than one triangle where all the information is exactly the same for every triangle that shares that vertex. As long as you’re careful at the “edge” vertices, you should have no problem.
Whew! That turned out to be a fairly lengthy description of what’s actually a fairly simple topic. But the gritty bits were devoted to our discussion of the base vertex index and start index parameters of DIP, which you’ll probably set to zero in most cases anyway.
Per my standard practice, I’m including a complete sample program below. I’m also including the same file in a convenient zip download, available here. Notice that in my code, I’ve set up the call to DIP to specify PrimitiveType.TriangleList. As an exercise, try drawing the same cube, but changing this parameter to PrimitiveType.TriangleStrip. You’ll need to adjust the indices in the IndexBuffer accordingly, and you’ll need to render the cube in two strips. Hint: the IndexBuffer should get smaller.
As usual, feel free to contact me with questions, comments, corrections, or suggestions for future tutorial series. Next time, we’ll apply our newly gleaned IndexBuffer knowledge to allow us to construct Mesh objects from scratch, which has all sorts of cool possibilities.
using System; using System.Drawing; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.Direct3D { public class Game : System.Windows.Forms.Form { static void Main() { Game app = new Game(); app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } } private Device device; private VertexBuffer vertices; private IndexBuffer indices; protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); // Disable lighting - we'll get all shading information from // the colors stored in the vertices device.RenderState.Lighting = false; // We'll pull the camera back farther than usual for this one // Notice the narrower FOV - PI/50 instead of the usual PI/4. // This is to make the fish-eye distortion less noticable. In // effect, we're using a zoom lens. // Try changing the FOV to PI/4 and the camera z to -5 to // see the distortion device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0, -50), new Vector3(0, 0, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/50.0F, this.Width / this.Height, 1.0F, 100.0F); vertices = CreateVertexBuffer(device); indices = CreateIndexBuffer(device); return true; } protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionColored.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // What type of vertices 8, // How many device, // The device 0, // Default usage CustomVertex.PositionColored.Format, // Vertex format Pool.Default); // Default pooling CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( -0.5F, 0.5F, -0.5F, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0.5F, -0.5F, Color.Orange.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, -0.5F, -0.5F, Color.Yellow.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, -0.5F, -0.5F, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0.5F, 0.5F, Color.Blue.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0.5F, 0.5F, Color.Violet.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, -0.5F, 0.5F, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, -0.5F, 0.5F, Color.Orange.ToArgb()); buf.Unlock(); return buf; } protected IndexBuffer CreateIndexBuffer(Device device) { IndexBuffer buf = new IndexBuffer( typeof(short), // What will the indices be? 36, // How many of them will there be? device, // The device 0, // Advanced setting Pool.Default // Advanced setting ); short[] arr = (short[]) buf.Lock(0, 0); int i = 0; // Front face arr[i++] = 0; arr[i++] = 3; arr[i++] = 1; arr[i++] = 0; arr[i++] = 2; arr[i++] = 3; // Top face arr[i++] = 0; arr[i++] = 1; arr[i++] = 4; arr[i++] = 1; arr[i++] = 5; arr[i++] = 4; // Left face arr[i++] = 0; arr[i++] = 4; arr[i++] = 2; arr[i++] = 2; arr[i++] = 4; arr[i++] = 6; // Right face arr[i++] = 1; arr[i++] = 7; arr[i++] = 5; arr[i++] = 1; arr[i++] = 3; arr[i++] = 7; // Back face arr[i++] = 4; arr[i++] = 5; arr[i++] = 6; arr[i++] = 5; arr[i++] = 7; arr[i++] = 6; // Bottom face arr[i++] = 2; arr[i++] = 6; arr[i++] = 3; arr[i++] = 3; arr[i++] = 6; arr[i++] = 7; buf.Unlock(); return buf; } protected void SetupMatrices() { float xangle = Environment.TickCount / 500.0F; float yangle = Environment.TickCount / 800.0F; Matrix xrot = Matrix.RotationX(xangle); Matrix yrot = Matrix.RotationY(yangle); device.Transform.World = Matrix.Multiply(xrot, yrot); } protected void Render() { // Clear the back buffer device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0); // Ready Direct3D to begin drawing device.BeginScene(); // Set the Matrices SetupMatrices(); // Draw the scene - 3D Rendering calls go here device.SetStreamSource(0, vertices, 0); device.Indices = indices; device.DrawIndexedPrimitives( PrimitiveType.TriangleList, // The type of primitives we're drawing 0, // Base vertex 0, // Minimum vertex index 8, // Number of vertices used 0, // Start index 12); // Number of primitives // Indicate to Direct3D that we’re done drawing device.EndScene(); // Copy the back buffer to the display device.Present(); } } }