Home > Articles

Rendering Using Simple Techniques

  • Print
  • + Share This
This chapter is from the book

In This Chapter

  • Using Vertex Buffers
  • Texturing Our Objects
  • In Brief

The rendering done up to this point hasn't been done very efficiently. New lists of vertices were allocated every time the scene was rendered, and everything was stored in system memory. With modern graphics cards having an abundance of memory built into the card, you can get vast performance improvements by storing your vertex data in the video memory of the card. Having the vertex data stored in system memory requires copying the data from the system to the video card every time the scene will be rendered, and this copying of data can be quite time consuming. Removing the allocation from every frame could only help as well.

Using Vertex Buffers

Direct3D has just the mechanism needed for this: vertex buffers. A vertex buffer, much like its name, is a memory store for vertices. The flexibility of vertex buffers makes them ideal for sharing transformed geometry in your scene. So how can the simple triangle application from Chapter 1, "Introducing Direct3D," be modified to use vertex buffers?

Creating a vertex buffer is quite simple. There are three constructors that can be used to do so; we will look at each one.

public VertexBuffer ( Microsoft.DirectX.Direct3D.Device device , System.Int32 
  sizeOfBufferInBytes , Microsoft.DirectX.Direct3D.Usage usage , 
  Microsoft.DirectX.Direct3D.VertexFormats vertexFormat , 
  Microsoft.DirectX.Direct3D.Pool pool )

public VertexBuffer ( System.Type typeVertexType , System.Int32 numVerts , 
  Microsoft.DirectX.Direct3D.Device device , 
  Microsoft.DirectX.Direct3D.Usage usage , 
  Microsoft.DirectX.Direct3D.VertexFormats vertexFormat , 
  Microsoft.DirectX.Direct3D.Pool pool )

The various parameter values can be

  • device—The Direct3D device you are using to create this vertex buffer. The vertex buffer will only be valid on this device.

  • sizeOfBufferInBytes—The size you want the vertex buffer to be, in bytes. If you are using the constructor with this parameter, the buffer will be able to hold any type of vertex.

  • typeVertexType—If you want your vertex buffer to only contain one type of vertex, you can specify that type here. This can be the type of one of the built-in vertex structures in the CustomVertex class, or it can be a user defined vertex type. This value cannot be null.

  • umVerts—When specifying the type of vertex you want to store in your buffer, you must also specify the maximum number of vertices the buffer will store of that type. This value must be greater than zero.

  • usage—Defines how this vertex buffer can be used. Not all members of the Usage type can be used when creating a vertex buffer. The following values are valid:

    • DoNotClip—Used to indicate that this vertex buffer will never require clipping. You must set the clipping render state to false when rendering from a vertex buffer using this flag.

    • Dynamic—Used to indicate that this vertex buffer requires dynamic memory use. If this flag isn't specified, the vertex buffer is static. Static vertex buffers are normally stored in video memory, while dynamic buffers are stored in AGP memory, so choosing this option is useful for drivers to determine where to store the memory. See the DirectX SDK documentation for more information on Dynamic usage.

    • Npatches—Used to indicate that this vertex buffer will be used to draw N-Patches.

    • Points—Used to indicate that this vertex buffer will be used to draw points.

    • RTPatches—Used to indicate that this vertex buffer will be used to draw high order primitives.

    • SoftwareProcessing—Used to indicate that vertex processing should happen in software. Otherwise, vertex processing should happen in hardware.

  • WriteOnly—Used to indicate that this vertex buffer will never be read from. Unless you have a very good reason for needing to read vertex buffer data, you should always select this option. Data stored in video memory that does not include this option suffers a severe performance penalty.

  • vertexFormat—Defines the format of the vertices that will be stored in this buffer. You can choose VertexFormat.None if you plan on having this be a generic buffer.

    • pool—Defines the memory pool where you want the vertex buffer to be located. You must specify one of the following memory pool locations:

    • Default—The vertex buffer is placed in the memory pool most appropriate for the data it contains. This is normally in either video memory or AGP memory, depending on the usage parameter. Vertex buffers created in this memory pool will be automatically disposed before a device reset.

    • Managed—The vertex buffer's data is automatically copied to device accessible memory as needed. The data is also backed by a system memory buffer, and can always be locked.

    • SystemMemory—The vertex buffer's data is placed in system memory, where it is not accessible from the device.

    • Scratch—A system memory pool that is not bound by a device, and thus cannot be used by the device. It is useful for manipulating data without being bound to a particular device format.

Creating Vertex Buffers from Unmanaged COM Pointers

Much like the device, there is an overload for our vertex buffer that takes an IntPtr. This value is used to pass in the actual COM interface pointer for the unmanaged IDirect3DVertexBuffer9 interface. This is useful when you want to use a vertex buffer created from an external (unmanaged) source. Any fully managed application will never use this constructor, and this value cannot be null.

In looking at the colored triangle application from Chapter 1, it should be relatively easy to move the triangle data into a vertex buffer. First, declare the vertex buffer member variable directly after our device:

private Device device = null;
private VertexBuffer vb = null;

Instead of creating the data every time the scene will be rendered, you will get greater performance by doing this only once. You can do this immediately after the device has been created, so move the triangle creation code there:

  // Create our device
  device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing
   , presentParams);

  CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[3];
  verts[0].SetPosition(new Vector3(0.0f, 1.0f, 1.0f));
  verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
  verts[1].SetPosition(new Vector3(-1.0f, -1.0f, 1.0f));
  verts[1].Color = System.Drawing.Color.Black.ToArgb();
  verts[2].SetPosition(new Vector3(1.0f, -1.0f, 1.0f));
  verts[2].Color = System.Drawing.Color.Purple.ToArgb();
  vb = new VertexBuffer(typeof(CustomVertex.PositionColored), 3, device, Usage.Dynamic |
      Usage.WriteOnly, CustomVertex.PositionColored.Format, Pool.Default);
  vb.SetData(verts, 0, LockFlags.None);

The only changes here are the two new lines after our triangle creation code. We first create a vertex buffer holding three members of the vertex structure type we've already declared. We want the buffer to be write-only, dynamic, and stored in the default memory pool for best performance. We then need to actually put our triangle list into our vertex buffer, and we accomplish this easily with the SetData method. This method accepts any generic object (much like DrawUserPrimitives) as its first member. The second member is the offset where we want to place our data, and since we want to fill in all of the data, we will use zero here. The last parameter is how we want the buffer locked while we are writing our data. We will discuss the various locking mechanisms shortly; for now, we don't care about how it's locked.

Compiling the application now will naturally give you a compile error since the DrawUserPrimitives call in OnPaint requires the "verts" variable. We need a way to tell Direct3D that we want to draw from our vertex buffer rather than the array we had declared before.

Naturally, this method does exist. We can call the SetStreamSource method from the device to have Direct3D read our vertex buffer when drawing our primitives. The prototypes for the two overloads of this function are

public void SetStreamSource ( System.Int32 streamNumber , 
  Microsoft.DirectX.Direct3D.VertexBuffer streamData , 
  System.Int32 offsetInBytes , System.Int32 stride )

public void SetStreamSource ( System.Int32 streamNumber , 
  Microsoft.DirectX.Direct3D.VertexBuffer streamData , 
  System.Int32 offsetInBytes )

The only difference between the two overloads is that one contains an extra member for the stride size of the stream. The first parameter is the stream number we will be using for this data. For now, we will always use zero as this parameter; however, we will discuss multiple streams in a later chapter. The second parameter is the vertex buffer that holds the data we want to use as our source. The third parameter is the offset (in bytes) into the vertex buffer that we want to be the beginning of the data Direct3D will draw from. The stride size parameter (which is only in one overload) is the size of each vertex in the buffer. If you've created your vertex buffer with a type, using the overload with this parameter isn't necessary.

Now replace the drawing call with the following code:

  device.SetStreamSource(0, vb, 0);
  device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);

As just described, we set stream zero to our vertex buffer, and since we want to use all of the data, we have our offset as zero as well. Notice that we've also changed the actual drawing call. Since we have all of our data in a vertex buffer, we no longer need to call the DrawUserPrimitives method because that function is only designed to draw user-defined data passed directly into the method. The more generic DrawPrimitives function will draw our primitives from our stream source. The DrawPrimitives method has three parameters, the first being the primitive type we've already discussed. The second parameter is the starting vertex in the stream. The last parameter is the number of primitives we will be drawing.

Even this simple demonstration of drawing a triangle with a vertex buffer shows approximately a 10% increase in performance based on frame rate. We will get into measuring performance and frame rates in later chapters. However, there is still a problem with this application that makes itself readily apparent when you attempt to resize your window. The triangle simply disappears as soon as your window is resized.

There are a few things going on here that are causing this behavior, and two of them have been mentioned briefly already. Remembering back to the previous chapter, we know that when our window is resized, our device is automatically reset. However, when a resource is created in the default memory pool (such as our vertex buffer), it is automatically disposed when the device is reset. So while our window is being resized, our device is being reset, and our vertex buffer disposed. One of the nifty features of Managed DirectX is that it will automatically re-create your vertex buffer for you after the device has been reset. However, there will be no data in the buffer, so the next time it's time to draw, nothing is shown.

The vertex buffer has an event we can capture, "created", that will inform us when it has been re-created and is ready to be filled with data. We should update our application to capture this event, and use that to fill our buffer with data. Add the following function to the application:

  private void OnVertexBufferCreate(object sender, EventArgs e)
  {
    VertexBuffer buffer = (VertexBuffer)sender;

    CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[3];
    verts[0].SetPosition(new Vector3(0.0f, 1.0f, 1.0f));
    verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
    verts[1].SetPosition(new Vector3(-1.0f, -1.0f, 1.0f));
    verts[1].Color = System.Drawing.Color.Black.ToArgb();
    verts[2].SetPosition(new Vector3(1.0f, -1.0f, 1.0f));
    verts[2].Color = System.Drawing.Color.Purple.ToArgb();

    buffer.SetData(verts, 0, LockFlags.None);
  }

This function has the standard event handler signature, taking in an object that is the item firing this event, plus the generic argument list. This event will never have an argument list, so you can safely ignore this member. We first get the vertex buffer that is firing this event by casting the sender member back to a vertex buffer. We then follow the exact same path we took last time by creating our data and calling SetData. We should replace the triangle creation code from our InitializeGraphics function now, and use the following two lines instead:

  vb.Created += new EventHandler(this.OnVertexBufferCreate);
  OnVertexBufferCreate(vb, null);

This code hooks the created event from the vertex buffer, and ensures that our OnVertexBufferCreate method is called whenever our vertex buffer has been created. Since the event hasn't been hooked up when the object is first created, we need to manually call our event handler function for the first time. Now run the application once more and try resizing the window.

You've now successfully updated the simple triangle application from an inefficient one to a much more efficient one using video memory and vertex buffers. However, it still seems quite boring, a single triangle spinning around. Now, you can try to make something even more exciting. A common first thing to try would be to render a box, so it's time to do that. The code will get slightly more complicated now, but all the basic principles will still apply.

Understanding Resource Lifetimes

All graphics resources will automatically be disposed if they are stored in video memory during a device reset; however, only vertex and index buffers will be re-created for you after the reset. Also note that all resources are also disposed when the device itself is disposed.

All geometry in a 3D scene is composed of triangles, so how would you render a box or cube? Well, to make a square you can use two triangles, and a box is composed of six squares. The only coordinates we really need are the eight corner vertices of the box, and we can create a box from those. Let's change our geometry creation code like in Listing 3.1:

Listing 3.1 Creating a Cube

CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[36];

// Front face
verts[0] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Red.ToArgb());
verts[1] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Red.ToArgb());
verts[2] = new CustomVertex.PositionColored(1.0f, 1.0f, 1.0f, Color.Red.ToArgb());
verts[3] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Red.ToArgb());
verts[4] = new CustomVertex.PositionColored(1.0f, -1.0f, 1.0f, Color.Red.ToArgb());
verts[5] = new CustomVertex.PositionColored(1.0f, 1.0f, 1.0f, Color.Red.ToArgb());

// Back face (remember this is facing *away* from the camera, so vertices should be
  clockwise order)
verts[6] = new CustomVertex.PositionColored(-1.0f, 1.0f, -1.0f, Color.Blue.ToArgb());
verts[7] = new CustomVertex.PositionColored(1.0f, 1.0f, -1.0f, Color.Blue.ToArgb());
verts[8] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Blue.ToArgb());
verts[9] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Blue.ToArgb());
verts[10] = new CustomVertex.PositionColored(1.0f, 1.0f, -1.0f, Color.Blue.ToArgb());
verts[11] = new CustomVertex.PositionColored(1.0f, -1.0f, -1.0f, Color.Blue.ToArgb());

// Top face
verts[12] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Yellow.ToArgb());
verts[13] = new CustomVertex.PositionColored(1.0f, 1.0f, -1.0f, Color.Yellow.ToArgb());
verts[14] = new CustomVertex.PositionColored(-1.0f, 1.0f, -1.0f, Color.Yellow.ToArgb());
verts[15] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Yellow.ToArgb());
verts[16] = new CustomVertex.PositionColored(1.0f, 1.0f, 1.0f, Color.Yellow.ToArgb());
verts[17] = new CustomVertex.PositionColored(1.0f, 1.0f, -1.0f, Color.Yellow.ToArgb());

// Bottom face (remember this is facing *away* from the camera, so vertices should be 
  clockwise order)
verts[18] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Black.ToArgb());
verts[19] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Black.ToArgb());
verts[20] = new CustomVertex.PositionColored(1.0f, -1.0f, -1.0f, Color.Black.ToArgb());
verts[21] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Black.ToArgb());
verts[22] = new CustomVertex.PositionColored(1.0f, -1.0f, -1.0f, Color.Black.ToArgb());
verts[23] = new CustomVertex.PositionColored(1.0f, -1.0f, 1.0f, Color.Black.ToArgb());

// Left face
verts[24] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Gray.ToArgb());
verts[25] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Gray.ToArgb());
verts[26] = new CustomVertex.PositionColored(-1.0f, -1.0f, 1.0f, Color.Gray.ToArgb());
verts[27] = new CustomVertex.PositionColored(-1.0f, 1.0f, -1.0f, Color.Gray.ToArgb());
verts[28] = new CustomVertex.PositionColored(-1.0f, -1.0f, -1.0f, Color.Gray.ToArgb());
verts[29] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Gray.ToArgb());

// Right face (remember this is facing *away* from the camera, so vertices should be 
  clockwise order)
verts[30] = new CustomVertex.PositionColored(1.0f, 1.0f, 1.0f, Color.Green.ToArgb());
verts[31] = new CustomVertex.PositionColored(1.0f, -1.0f, 1.0f, Color.Green.ToArgb());
verts[32] = new CustomVertex.PositionColored(1.0f, -1.0f, -1.0f, Color.Green.ToArgb());
verts[33] = new CustomVertex.PositionColored(1.0f, 1.0f, -1.0f, Color.Green.ToArgb());
verts[34] = new CustomVertex.PositionColored(1.0f, 1.0f, 1.0f, Color.Green.ToArgb());
verts[35] = new CustomVertex.PositionColored(1.0f, -1.0f, -1.0f, Color.Green.ToArgb());

buffer.SetData(verts, 0, LockFlags.None);

Now that is a lot of vertices to have to type in: 36 to be exact, but don't worry. You can load the source code located on the included CD. As mentioned already, the box will be made of 12 triangles, and each triangle has 3 vertices, which gives the vertex list. Running the code just as it is will throw an exception during the SetData call. Can you guess why? If you guessed that we never updated the original size of our vertex buffer, you were correct. There are a couple extra changes we want to make before we're ready to really run this. Update the lines represented as follows:

vb = new VertexBuffer(typeof(CustomVertex.PositionColored), 36, device, Usage.Dynamic 
  | Usage.WriteOnly, CustomVertex.PositionColored.Format, Pool.Default);

device.Transform.World = Matrix.RotationYawPitchRoll(angle / (float)Math.PI, angle / 
  (float)Math.PI * 2.0f, angle / (float)Math.PI);

device.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);

The major thing we're doing is changing the size of the vertex buffer we've created to hold all the data we want to render. We've also changed the rotation somewhat to make the box spin a little crazier. Finally, we actually change our rendering call to render all 12 primitives, rather than the single triangle we had before. Actually, since our box is a fully formed 3D object that is completely filled, we no longer need to see the back-facing triangles. We can use the default culling mode in Direct3D (counterclockwise). Go ahead and remove the cull mode line from your source. You can try running the example now.

Terrific, we've now got a colorful box spinning around our screen. Each face of the box is a different color, and we can see each face of the box as it rotates without turning our back face culling off. If you wanted to render more than one box in a scene, hopefully you wouldn't create a series of vertex buffers, one for each box. There's a much easier way to do this.

We will draw a total of three boxes now, each side by side. Since our current camera settings have our first box taking up pretty much the entire scene, we should move our camera back a little first. Change the look at function as follows:

device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, 18.0f), new Vector3(), 
  new Vector3(0,1,0));

As you can see, we just moved the position of our camera back so we can see more of the scene. Running the application now will show you the same box spinning around; it will just appear smaller, since the camera is farther away now. In order to draw two more boxes on the sides of this one, we can reuse our existing vertex buffer, and just tell Direct3D to draw the same vertices again. Add the following lines of code after the call to DrawPrimitives:

device.Transform.World = Matrix.RotationYawPitchRoll(angle / (float)Math.PI, angle / 
  (float)Math.PI / 2.0f, angle / (float)Math.PI * 4.0f) * 
  Matrix.Translation(5.0f, 0.0f, 0.0f);
device.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);

device.Transform.World = Matrix.RotationYawPitchRoll(angle / (float)Math.PI, angle / 
  (float)Math.PI * 4.0f, angle / (float)Math.PI / 2.0f) * 
  Matrix.Translation(-5.0f, 0.0f, 0.0f);
device.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);

So, what exactly are we doing here? Direct3D already knows what type of vertices we are planning on drawing, due to the VertexFormat property we set to draw our first box. It also knows what vertex buffer to retrieve the data from due to the SetStreamSource function also used on the first box. So what does Direct3D need to know in order to draw a second (and third) box? The only information that is needed is where and what to draw.

Setting the world transform will "move" our data from object space into world space, so what are we using as our transformation matrix? First, we rotate much like we did in the SetupCamera function; although we use a slightly different rotation function, that's just so the boxes rotate at different angles. The second half of the world transform is new, though. We multiply a Matrix.Translation to our existing rotation matrix. A translation matrix provides a way to move vertices from one point to another in world space. Looking at our translation points, we want to move the second box five units to the right, while we are moving the third box five units to the left.

You will also notice that we multiply our two transformation matrices, which provides us with a cumulative transformation of the arguments. They are done in the order they are specified, so in this case, our vertices will be rotated first, then translated (moved) second. Translating and then rotating would provide very different results. It is important to remember the order of operations when you are transforming vertices.

The code included with the CD shows updated code that draws a total of nine boxes rather than the three shown here. See Figure 3.1.

Figure 3.1Figure 3.1 Colored cubes.

  • + Share This
  • 🔖 Save To Your Account