XNA’s Vertex Structs and Custom Vertex Formats

I’ve been doing a lot of reading the past few days trying to figure out the relationship between shaders and the vertex definitions in XNA code. XNA provides a number of vertex definitions for you, including VertexPositionColor, VertexPositionColorTexture, VertexPositionTexture, and VertexPositionNormalTexture. It’s pretty clear what they each hold — a position, and some extra information about vertex colors, textures, or normals. But what if you wanted to pass additional information to your shader? How would you tell XNA to do that? You could pass the information to your shader by calling the SetValue() method of a parameter, but that only lets you pass one value (or texture, etc.) What if you need that information on a per vertex level?

If you want to do anything fancy, bump mapping say, you’ll need to define what’s called a vertex format. When XNA passes your vertices to your graphics card, it does it as a stream of bytes, and you have to provide how those bytes should be broken down into vertices by the shaders. Consider a few vertices in the VertexPositionColor format. Every vertex has a Vector3 position (that’s 3 floats each 4 bytes long) and a single color (4 bytes long). Thus, in the vertex stream, the shader should look every 3*4+4 or 16 bytes for a new vertex. If we look at the VertexPositionColorTexture vertex format, it’s pretty clear we can’t fit all that information into 16 bytes. Position (a Vector3) takes 12 bytes, Color takes 4, and Texture (a Vector2) takes 8 bytes. That’s a total of 24 bytes per vertex. If our shader tried to break apart the vertex stream every 16 bytes, we’d have massive corruption of our data. So we have to tell our graphics card how and where to split up the vertex stream. In XNA, this is surprisingly easy to do, all we need is a custom struct:

// Simple struct to hold position and two texture coordinates
public struct MyVertexPositionTextureBump
{
    public Vector3 Position;
    public Vector2 TexCoords;
    public Vector2 BumpCoords;
 
    // We include a constructor to save time when creating number of these
    public MyVertexPositionTextureBump(Vector3 Pos, Vector2 TexC, Vector2 BumpC)
    {
        Position = Pos;
        TexCoords = TexC;
        BumpCoords = BumpC;
    }
}

Alright, with that struct in place, we can get started defining vertices. Every vertex will have a position, texture coordinates, and coordinates in our bump map. Except, there’s one problem, we haven’t told the graphics card how to split up our vertices once they arrive on the card. For that, we need to add a collection of VertexElements. Each VertexElement is a single piece of information about an individual vertex. It could represent the position, or a single color, or any other single piece of data. It contains information about the type, size, and offset of that information in the vertex. Let’s construct one now:

// Vertex Element array for our struct above
public static readonly VertexElement[] VertexElements =
{
    new VertexElement(0,0, VertexElementFormat.Vector3,
        VertexElementMethod.Default,
        VertexElementUsage.Position, 0),
    new VertexElement(0,sizeof(float)*3, VertexElementFormat.Vector2,
        VertexElementMethod.Default,
        VertexElementUsage.TextureCoordinate, 0),
    new VertexElement(0,sizeof(float)*(3+2), VertexElementFormat.Vector2,
        VertexElementMethod.Default,
        VertexElementUsage.TextureCoordinate, 1),
};

Pretty simple. There’s one VertexElement entry for each of our variables in our struct. What are all the parameters for the constructor? Well, they’re pretty straightforward. The first parameter is the index of the vertex stream. If you were getting into some crazy graphics techniques far above my head, you’d need to use something besides 0 here. For most of the applications you’ll develop you can use 0 for the first parameter and never worry. The second parameter is the offset of this element within the vertex stream. In the example above, our first element is a Vector3 (3 floats) which means the second element starts immediately after 3 floats. Grab a sheet of graph paper and sketch out boxes for the 3 floats (12 bytes) and draw a vertical line after them, this is the start location or offset of the second element. Likewise, for the third element, we need to start 20 bytes (5*sizeof(float)) into the stream.

The third parameter is obvious, it’s the data type we’re passing in. Vector3 and Vector2 in this case. The fourth parameter, like the first parameter, is for more complex graphics applications that I don’t entirely understand. The fifth parameter is how we define where we link into the shader. Shaders have a number of inputs for each vertex. They are things like POSITIONn, TEXTUREn, TEXCOORDn, COLORn, etc. These are called semantics, and a more thorough treatment can be found at this MSDN page. For now, we are only using POSITION and TEXCOORD, but we have more than one TEXCOORD, so how do we use them in the shader? Well, that’s where the sixth (and final) parameter comes into play. Every semantic in your shader has a type (POSITION) and an index (0, 1, …). The sixth parameter is this index. So, for our struct above we’ve defined values for POSITION0, TEXCOORD0, and TEXCOORD1 in the shader.

Our struct should contain one more thing, its size in bytes:

// I use arithmetic here to show clearly where these numbers come from
// You can just type 28 if you want
public static readonly int SizeInBytes = sizeof(float) * (3+2+2);

And that’s our custom vertex declaration! You should be able to use it instead of one of the provided XNA vertex types. Here’s the complete struct:

// Simple struct to hold position and two texture coordinates
public struct MyVertexPositionTextureBump
{
    public Vector3 Position;
    public Vector2 TexCoords;
    public Vector2 BumpCoords;
 
    // We include a constructor to save time when creating number of these
    public MyVertexPositionTextureBump(Vector3 Pos, Vector2 TexC, Vector2 BumpC)
    {
        Position = Pos;
        TexCoords = TexC;
        BumpCoords = BumpC;
    }
 
    // I use arithmetic here to show clearly where these numbers come from
    // You can just type 28 if you want
    public static readonly int SizeInBytes = sizeof(float) * (3+2+2);
 
    // Vertex Element array for our struct above
    public static readonly VertexElement[] VertexElements =
    {
        new VertexElement(0,0, VertexElementFormat.Vector3,
            VertexElementMethod.Default,
            VertexElementUsage.Position, 0),
        new VertexElement(0,sizeof(float)*3, VertexElementFormat.Vector2,
            VertexElementMethod.Default,
            VertexElementUsage.TextureCoordinate, 0),
        new VertexElement(0,sizeof(float)*(3+2), VertexElementFormat.Vector2,
            VertexElementMethod.Default,
            VertexElementUsage.TextureCoordinate, 1),
    };
}

Oh, one more thing, why did I use static readonly in front of the VertexElements array? We want to make sure we are only calculating this once, and never ever changing it. Same thing with SizeInBytes, this should never ever change at runtime, or you risk seriously corrupting the state of your game. And we definitely don’t want this to be a part of every single vertex, so we make it static.

Category: Design and Data Structures, XNA In Depth | Tags: , , , , , 3 comments »

3 Responses to “XNA’s Vertex Structs and Custom Vertex Formats”

  1. Surge

    This is a helpful post. Thank you for the explanation of the readonly, this is a disaster waiting t happen.

  2. Mahhari

    dude, wicked post! I read a couple books on this and none were this clear. Good job! you made my night!

  3. Hardware Geometry Instancing in XNA 4 « Science Fact

    [...] the Effect itself. If you'd like to know more about custom Vertex Formats in XNA you can look here for a nice explanation of them. Now we have a format for our instances we need a second Vertex [...]


Leave a Reply



 

Back to top