Designing a Flexible Vertex Element System for XNA Using Attributes

Published December 02, 2008 by Michael Popoloski, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
Introduction
Anyone who's done 3D graphics work with DirectX or XNA has undoubtedly had to learn about the concept of vertex formats. All vertex data passed to the graphics card needs to be described in order for the data it contains to be used correctly, and each graphics API has slightly different ways of doing so. In the past, when programmable shaders were not a factor, DirectX used a system called the Flexible Vertex Format (FVF), which was a system of flags that could be combined in complex ways to describe the location and format of the different bits of data contained in the vertex structure. With the introduction of shaders, the old system became too impractical for general use. The vertex element system was devised that is still used for both Direct3D9 and XNA. Direct3D10 has introduced a slightly newer system based upon this one, but I'm only going to be focusing on the one used by XNA.


The Problem
XNA allows you to describe your custom vertex structures by creating an array of vertex elements that correspond to each of the data elements in your vertex structure. Each element in the array contains information such as the data format, the intended usage, the offset into structure in bytes, and other bits of data that are used less often, such as tessellator modes. While this system is very flexible and works quite well in practice, it still has two main issues that could be improved:

  • Hard coding the offset of each vertex element can be quite unsafe. Providing the wrong value won't be caught by the compiler, and will probably just cause your rendering to appear strange, making it a difficult bug to track down.
  • In a more academic view, it's wasteful to have to provide information in your code that already exists. Whenever you need to duplicate code, you increase your maintenance load and introduce the possibility that the two can become out-of-sync with each other.

So that's the problem. How do we go about fixing it?

The Solution
I'm always looking for ways to apply the high level features of C# to real-world problems. In this case, attributes immediately came to mind as an elegant way to make this situation easier. Attributes are a concept in .NET that allow you to apply metadata to types and code elements. On their own, they have no intrinsic meaning, but using reflection you can look them up and use the data they contain to alter how your code executes.

To better see how attributes can help, let's look at a simple example using both the vertex element system and the newly proposed attribute system.



[StructLayout(LayoutKind.Sequential)]
public struct VertexPositionColor
{
public Vector3 Position;
public Color Color;
public static readonly VertexElement[] VertexElements;
static VertexPositionColor()
{
VertexElements = new VertexElement[]
{
new VertexElement(0, 0, VertexElementFormat.Vector3,
VertexElementMethod.Default, VertexElementUsage.Position, 0), new
VertexElement(0, 12, VertexElementFormat.Color,
VertexElementMethod.Default, VertexElementUsage.Color, 0)
};
}
}

VertexDeclaration declaration = new VertexDeclaration(GraphicsDevice,
VertexPositionColor.VertexElements);
Notice how we declare two vertex elements, one for each piece of data in our
vertex structure(the position and color)
. Also, because we are hardcoding the byte offset for each data element, we
need to apply the StructLayout attribute to the structure to ensure that its
fields don 't get moved around by the compiler. Now, we 'll look at attribute
version. struct VertexPositionColor
{
[VertexElement(VertexElementUsage.Position)
public Vector3 Position;
[VertexElement(VertexElementUsage.Color)]
public Color Color;
} VertexDeclaration declaration = GraphicsDevice.CreateVertexDeclaration(typeof
(VertexPositionColor));


Wow, we were able to cut that down quite nicely. We no longer need to build up an array of data elements, since we will be using reflection to find the information for us. Further, we no longer need to specify the StructLayout attribute, since the byte offset will be calculated by the program at runtime. OK, so now we can just use these attributes to mark each element instead of building the element array by hand. How exactly does this all work?


The Attribute
Before we look at the method that actually does the messy work of building up the vertex element data, let's take a look at the attribute that we'll be using to mark each data element.



[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class VertexElementAttribute: Attribute
{
public int Stream
{
get;
set;
}
public VertexElementMethod Method
{
get;
set;
}
public VertexElementFormat Format
{
get;
set;
}
public VertexElementUsage Usage
{
get;
private set;
}
internal int Offset
{
get;
set;
}
public VertexElementAttribute(VertexElementUsage usage)
{
Usage = usage;
Format = VertexElementFormat.Unused;
}
public VertexElementAttribute(VertexElementUsage usage, VertexElementFormat
format)
{
Usage = usage;
Format = format;
}
}


What do we have here? Well, for starters, we can see that the attribute exposes the same set of data that XNA's VertexElement system does. While we don't provide constructors for all of this information, the user can optionally specify it by using named parameters, a C# feature that can be used with attributes. Another thing that might pop out at you is that Offset is marked as internal. Since we will be calculating this field, it's only necessary as a temporary storage location for the creation method. If you were to place this system inside of a separate DLL as part of an engine or framework, the Offset member would be completely hidden from the client's view, which is what we want since they shouldn't be messing with it manually.

Finally, we have a few constructors containing the parameters that are absolutely necessary for the vertex element system to do its job. Some of you may ask, "Why is format an optional parameter?" Well, we can easily determine the format in some cases, such as if the underlying field is a Vector3 or a Color. However, some of the formats cannot be determined so easily, so we allow the user the option of specifying the format in non-trivial cases.

OK, that's the extent of the attribute, something that's not very amazing to look at actually. Now let's move on to the real meat of the attribute system, the CreateVertexDeclaration method.


The Reflection
For anyone who's never seen reflection code (especially those coming from a C++ background), what I'm about to show you may shock and amaze. For the rest of you, this should be old hat.



public static class AttributeSystem
{
static Dictionary cachedData = new Dictionary();
public static VertexDeclaration CreateVertexDeclaration(this
GraphicsDevice device, Type vertexType)
{
if (cachedData.ContainsKey(vertexType))
return new VertexDeclaration(device, cachedData[vertexType]);
if (!vertexType.IsValueType)
throw new InvalidOperationException(
"Vertex types must be value types.");
List objectAttributes = new List();
FieldInfo[] fields = vertexType.GetFields(BindingFlags.NonPublic |
BindingFlags.Public | BindingFlags.Instance);
foreach (FieldInfo field in fields)
{
VertexElementAttribute[] attributes = (VertexElementAttribute[])
field.GetCustomAttributes(typeof(VertexElementAttribute), false);
if (field.Name.Contains("<") && field.Name.Contains(">"))
{
int index1 = field.Name.IndexOf('<');
int index2 = field.Name.IndexOf('>');
string propertyName = field.Name.Substring(index1 + 1, index2 -
index1 - 1);
PropertyInfo property = vertexType.GetProperty(propertyName,
field.FieldType);
if (property != null)
attributes = (VertexElementAttribute[])
property.GetCustomAttributes(typeof(VertexElementAttribute),
false);
}
if (attributes.Length == 1)
{
if (attributes[0].Format == VertexElementFormat.Unused)
{
if (field.FieldType == typeof(Vector2))
attributes[0].Format = VertexElementFormat.Vector2;
else if (field.FieldType == typeof(Vector3))
attributes[0].Format = VertexElementFormat.Vector3;
else if (field.FieldType == typeof(Vector4))
attributes[0].Format = VertexElementFormat.Vector4;
else if (field.FieldType == typeof(Color))
attributes[0].Format = VertexElementFormat.Color;
}
attributes[0].Offset = Marshal.OffsetOf(vertexType, field.Name)
.ToInt32();
objectAttributes.Add(attributes[0]);
}
}
if (objectAttributes.Count < 1)
throw new InvalidOperationException(
"The vertex type must have at least one field or property marked with the VertexElement attribute.");
List elements = new List();
Dictionary usages = new Dictionary();
foreach (VertexElementAttribute attribute in objectAttributes)
{
if (!usages.ContainsKey(attribute.Usage))
usages.Add(attribute.Usage, 0);
int index = usages[attribute.Usage];
usages[attribute.Usage]++;
elements.Add(new VertexElement((short)attribute.Stream, (short)
attribute.Offset, attribute.Format, attribute.Method,
attribute.Usage, (byte)index));
}
VertexElement[] elementArray = elements.ToArray();
cachedData.Add(vertexType, elementArray);
return new VertexDeclaration(device, elementArray);
}
}



So that's the extent of the attribute system in its entirety. I'm not going to go step by step through the code, but I will point out some of the more interesting bits and explain the rationale behind them.

  1. I've placed the method in a static class and made it an extension method. This isn't actually required for the method to be useful, but I like extension methods so that's how I made it.
  2. Another feature I added that is entirely optional is the use of a dictionary to cache reflected data. This could save you some processing power if you are recreating vertex declarations all the time, but it isn't strictly necessary for the system to work.
  3. Our method requires that the vertex type be a value type (struct keyword in C#). While this will be obvious to C# developers, it could end up being unintuitive for those coming over to XNA from C++. Suffice it to say, struct isn't the same as class in C#. Read up on value types and reference types before working with C# further.
  4. While iterating over each field seems to be the logical way to handle things, I also wanted to support automatic properties, since I absolutely love them. To do this, you need to check the name of each field to see if it contains the '<' and '>' characters. As far as I've seen, this is only possible for backing store fields generated by the compiler for automatic properties, so I extract the actual property name and look for the attribute there.
  5. You can see that I only provide default support for four formats: Vector2, Vector3, Vector4, and Color. All others will need to be specified manually. I suppose providing automatic support for more would be possible. I'll leave that as an exercise for the reader.
  6. That strange bit of code near the end is how I calculate usage indices for each data element. Basically, each occurrence of a particular usage is given an increasing usage index, starting from 0. While this seems correct to me and works fine, I've been told that it's possible that this doesn't always work. If you want, you could always add a UsageIndex property to the attribute and give the user the option of specifying the usage index manually.

The Downsides
To be honest, I can't think of any. All the power of the original method is still there, but wrapped up in a safer and easier to use format. If you think of anything that I missed, be sure to let me know.


The End
That's about all I have to say about that. When I hit upon this system I felt quite pleased with myself, and I was surprised to learn that nobody else on the internet had already developed something similar. It's simple, elegant, and makes the code easier to read and maintain. Hopefully you will find it as cool as I do. Even if you don't, be sure to let me know what you think. I'm always eager to hear what others think of my work.


The References
As always, see MSDN for a complete reference to both .NET and XNA.
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement