04 Creating a VertexBuffer

Last time we talked about the basics of rendering - how to get Direct3D ready to draw stuff to the screen. We were even able to clear the screen to an arbitrary color. Not bad, but this time we'll do better: we'll create a VertexBuffer, which is the last prerequisite to being able to draw arbitrary shapes.

To render a shape to the screen, we need to understand about vertices, which is the plural of vertex. A vertex in the geometric sense is a place where two edges of a polygon come together – a point, if you like. However, in Direct3D a vertex is a structure that can carry more information than simply position in space – it can have a color, carry texture information, or a number of other pieces of data.

Because it’s important to represent things in as efficient a manner as possible (remember, we have to shove this information over to the graphics card many times a second) vertices in Direct3D are described by something called a Flexible Vertex Format, or FVF. An FVF is simply a way of saying, “Hey, these vertices contain a 3D position and texture information” or “Hey, these vertices contain a 2D position and color information.”

We don’t really have to deal directly with FVFs in Managed Direct3D the way that a COM programmer working in C++ would. Instead, we have a set of classes defined inside of the CustomVertex class. I’ve listed some of these classes in this table:

There are several others, but these will do to go on. You can see that the various formats allow us to contain different things in a particular vertex – either color information or not, and either transformed coordinates or not.

Using “transformed” coordinates simply means that we’ve already done the work to figure out where things go on the screen. Normally we’d rely on Direct3D to do this work based on the position of our objects and a virtual camera. For now we’re going to work with transformed coordinates because using untransformed coordinates means dealing with a number of other things that we haven’t gotten to yet.

Regardless of the vertex format we choose, the way we deal with coordinates is the same: with a VertexBuffer. A VertexBuffer is, well, a buffer that holds vertices. It’s fairly common to create a VertexBuffer right after we create the device. Recall InitializeGraphics. Here’s an updated version:

private VertexBuffer vertices; protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); vertices = CreateVertexBuffer(device); return true; }

There's only one new line, which calls CreateVertexBuffer. CreateVertexBuffer looks like this:

protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.TransformedColored.Format; CustomVertex.TransformedColored[] verts = new CustomVertex.TransformedColored[3]; verts[0] = new CustomVertex.TransformedColored( this.Width / 2, this.Height / 4, 0.5F, 1, Color.Blue.ToArgb()); verts[1] = new CustomVertex.TransformedColored( this.Width * 3 / 4, this.Height * 3 / 4, 0.5F, 1, Color.Green.ToArgb()); verts[2] = new CustomVertex.TransformedColored( this.Width / 4, this.Height * 3 / 4, 0.5F, 1, Color.Red.ToArgb()); VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.TransformedColored), verts.Length, device, 0, CustomVertex.TransformedColored.Format, Pool.Default ); GraphicsStream stm = buf.Lock(0, 0, 0); stm.Write(verts); buf.Unlock(); return buf; }

This procedure starts off by setting the Device’s VertexFormat, like so:

device.VertexFormat = CustomVertex.TransformedColored.Format;

This is another one of those places where Managed Direct3D sort of threw my object-oriented expectations – the VertexFormat is a global setting. That means that the Device is now expecting every vertex that gets drawn to have transformed coordinates and to contain color information. If I were using a mix of different types of vertices, I’d have to change it every time I drew a different type with the Device, and I probably wouldn’t want to put it in CreateVertexBuffer, which only gets called once. Fortunately, I’m just going to be drawing a single triangle in this demo, so we can get away with it this time.

The next step is to actually create the vertices that we plan to put into the VertexBuffer:

CustomVertex.TransformedColored[] verts = new CustomVertex.TransformedColored[3];

This is simply creating an array of CustomVertex.TransformedColored objects, which we then populate:

verts[0] = new CustomVertex.TransformedColored( this.Width / 2, this.Height / 4, 0.5F, 1, Color.Blue.ToArgb()); verts[1] = new CustomVertex.TransformedColored( this.Width * 3 / 4, this.Height * 3 / 4, 0.5F, 1, Color.Green.ToArgb()); verts[2] = new CustomVertex.TransformedColored( this.Width / 4, this.Height * 3 / 4, 0.5F, 1, Color.Red.ToArgb());

In the case of the TransformedColored vertices that we’re using, the constructor takes five arguments – an x, y, and z position, a w value, and an integer representing the color. We’ll ignore z for now – remember that we’re working in transformed coordinates (think “screen coordinates”) and so z doesn’t really come into it. The w value represents advanced capabilities that we don’t need to go into.

The other three parameters are fairly self-explanatory. The x and y say where this vertex goes on the screen (upper left is 0,0) and the color is the color of the vertex, although Direct3D does confusingly make us use the ToArgb() method to convert it to an integer, rather than letting us use the Color directly as it does in other places. What the heck – it’s their first cut at a managed interface.

What I’ve done here is defined three vertices in a roughly equilateral triangle. The top point is one-quarter of the way down from the top edge of the screen, the left point is one-quarter of the way from the left edge of the screen, and the right point is one-quarter of the way from the right edge of the screen.

We still need to create the VertexBuffer itself.

VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.TransformedColored), verts.Length, device, 0, CustomVertex.TransformedColored.Format, Pool.Default );

Note that a VertexBuffer can only hold one type of vertex. We indicate what that type is with the first argument, tell it how many vertices it’ll be holding with the second, pass in the device with the third, specify advanced options with the fourth (we’re ignoring these), tell it the format of the vertices with the fifth, and ask for default pooling with the final. Pooling is an advanced topic, and Pool.Default is the right choice for now.

It’s a bit strange that we have to specify the type and the format as the first and fifth parameters, but it’s an upshot of the fact that we can actually define our own vertex formats for special situations. Just use the Format field of whatever CustomVertex structure you pass for the first argument and you’ll be okay.

Whew! All that’s left is to populate the VertexBuffer. That’s easy enough:

GraphicsStream stm = buf.Lock(0, 0, 0); stm.Write(verts); buf.Unlock();

Basically, we get a reference to a GraphicsStream by locking the VertexBuffer, then call its Write method to blast the three TransformedColored vertices into the VertexBuffer. Just as importantly, we need to Unlock the VertexBuffer when we’re done. Lock and Unlock are important, because they tell Direct3D that we’re modifying the VertexBuffer and it shouldn’t (for example) try to read the contents while we are.

With the VertexBuffer created, we can finally render with it. To do that, we’ll need to modify our Render method. (Remember Render?) But we’ll have to wait for next time for that.