float4x4

Computer graphics and stuff

Hardware instancing for PC in XNA 4, with textures!

1 Comment »

Over a year ago, I posted a tutorial on hardware instancing in XNA 3.1. Version 4 of XNA works a bit different and the code will no longer compile. This is not necessarily a bad thing though. XNA 4 really improves on 3.1 in a lot of ways, including hardware instancing. In the following tutorial, we’ll see how to get it running in XNA 4 and use textures rather than colours.

Hardware instancing allows you to render a ton of similar objects on the screen while keeping a decent framerate. The downside to this is that your control over these objects is somewhat limited. The vertexbuffer and indexbuffer of your geometry will be the same for each object in the instance batch that you draw on the screen. The same goes for your shader parameters such as textures or matrices. However, by using a second vertexbuffer you can pass some specific data for every single object that you will draw in the batch.

Let’s start things off by creating an XNA 4 game and create some members that will hold our three buffers. The geometry vertex- and indexbuffer and a vertexbuffer to store our instance data.

VertexBuffer instanceBuffer;
VertexBuffer geometryBuffer;
IndexBuffer indexBuffer;

We’re going to create the data for our geometry in the GenerateGeometryBuffers function, call it in your LoadContent override.

private void GenerateGeometryBuffers()
{
 VertexPositionTexture[] vertices = new VertexPositionTexture[24];
 vertices[0].Position = new Vector3(-1, 1, -1);
 vertices[0].TextureCoordinate = new Vector2(0, 0);
 vertices[1].Position = new Vector3(1, 1, -1);
 vertices[1].TextureCoordinate = new Vector2(1, 0);
 vertices[2].Position = new Vector3(-1, 1, 1);
 vertices[2].TextureCoordinate = new Vector2(0, 1);
 vertices[3].Position = new  Vector3(1, 1, 1);
 vertices[3].TextureCoordinate = new Vector2(1, 1);

 vertices[4].Position = new Vector3(-1, -1, 1);
 vertices[4].TextureCoordinate = new Vector2(0, 0);
 vertices[5].Position = new Vector3(1, -1, 1);
 vertices[5].TextureCoordinate = new Vector2(1, 0);
 vertices[6].Position = new Vector3(-1, -1, -1);
 vertices[6].TextureCoordinate = new Vector2(0, 1);
 vertices[7].Position = new Vector3(1, -1, -1);
 vertices[7].TextureCoordinate = new Vector2(1, 1);

 vertices[8].Position = new Vector3(-1, 1, -1);
 vertices[8].TextureCoordinate = new Vector2(0, 0);
 vertices[9].Position = new Vector3(-1, 1, 1);
 vertices[9].TextureCoordinate = new Vector2(1, 0);
 vertices[10].Position = new Vector3(-1, -1, -1);
 vertices[10].TextureCoordinate = new Vector2(0, 1);
 vertices[11].Position = new Vector3(-1, -1, 1);
 vertices[11].TextureCoordinate = new Vector2(1, 1);

 vertices[12].Position = new Vector3(-1, 1, 1);
 vertices[12].TextureCoordinate = new Vector2(0, 0);
 vertices[13].Position = new Vector3(1, 1, 1);
 vertices[13].TextureCoordinate = new Vector2(1, 0);
 vertices[14].Position = new Vector3(-1, -1, 1);
 vertices[14].TextureCoordinate = new Vector2(0, 1);
 vertices[15].Position = new Vector3(1, -1, 1);
 vertices[15].TextureCoordinate = new Vector2(1, 1);

 vertices[16].Position = new Vector3(1, 1, 1);
 vertices[16].TextureCoordinate = new Vector2(0, 0);
 vertices[17].Position = new Vector3(1, 1, -1);
 vertices[17].TextureCoordinate = new Vector2(1, 0);
 vertices[18].Position = new Vector3(1, -1, 1);
 vertices[18].TextureCoordinate = new Vector2(0, 1);
 vertices[19].Position = new Vector3(1, -1, -1);
 vertices[19].TextureCoordinate = new Vector2(1, 1);

 vertices[20].Position = new Vector3(1, 1, -1);
 vertices[20].TextureCoordinate = new Vector2(0, 0);
 vertices[21].Position = new Vector3(-1, 1, -1);
 vertices[21].TextureCoordinate = new Vector2(1, 0);
 vertices[22].Position = new Vector3(1, -1, -1);
 vertices[22].TextureCoordinate = new Vector2(0, 1);
 vertices[23].Position = new Vector3(-1, -1, -1);
 vertices[23].TextureCoordinate = new Vector2(1, 1);

 geometryBuffer = new VertexBuffer(GraphicsDevice, VertexPositionTexture.VertexDeclaration, 24, BufferUsage.WriteOnly);
 geometryBuffer.SetData(vertices);

 int[] indices = new int[36];
 indices[0] = 0; indices[1] = 1; indices[2] = 2;
 indices[3] = 1; indices[4] = 3; indices[5] = 2;

 indices[6] = 4; indices[7] = 5; indices[8] = 6;
 indices[9] = 5; indices[10] = 7; indices[11] = 6;

 indices[12] = 8; indices[13] = 9; indices[14] = 10;
 indices[15] = 9; indices[16] = 11; indices[17] = 10;

 indices[18] = 12; indices[19] = 13; indices[20] = 14;
 indices[21] = 13; indices[22] = 15; indices[23] = 14;

 indices[24] = 16; indices[25] = 17; indices[26] = 18;
 indices[27] = 17; indices[28] = 19; indices[29] = 18;

 indices[30] = 20; indices[31] = 21; indices[32] = 22;
 indices[33] = 21; indices[34] = 23; indices[35] = 22;

 indexBuffer = new IndexBuffer(GraphicsDevice, typeof(int), 36, BufferUsage.WriteOnly);
 indexBuffer.SetData(indices);
}

Next up we’ll want to fill our instance vertex buffer with data. However, in XNA 4, a vertexbuffer has a vertexdeclaration bound to it. This means you don’t need to set it on your graphicsdevice before rendering.  It also means that you need to know the vertexdeclaration before creating a vertexbuffer. Since we’ll be using this buffer for our own instance data a custom vertexdeclaration is required.

A vertexdeclaration defines what type of elements the GPU can expect to read from a vertexbuffer via vertexelements. For example, the VertexPositionColored vertexdeclaration is composed of a position element and a colour element. We are not going to store a position or colour in our declaration, but a world matrix and some texture information. To achieve this, we add the instanceVertexDeclaration member to our game class and call GenerateInstanceVertexDeclaration in our LoadContent function. We’ll store our information in the InstanceInfo structure.

VertexDeclaration instanceVertexDeclaration;
private void GenerateInstanceVertexDeclaration()
{
 VertexElement[] instanceStreamElements = new VertexElement[5];
 instanceStreamElements[0] = new VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1);
 instanceStreamElements[1] = new VertexElement(sizeof(float) * 4, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 2);
 instanceStreamElements[2] = new VertexElement(sizeof(float) * 8, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 3);
 instanceStreamElements[3] = new VertexElement(sizeof(float) * 12, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 4);
 instanceStreamElements[4] = new VertexElement(sizeof(float) * 16, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 5);
 instanceVertexDeclaration = new VertexDeclaration(instanceStreamElements);
}
struct InstanceInfo
{
 public Matrix World;
 public Vector2 AtlasCoordinate;
};

People with XNA 3.1 experience will notice the changes made to the vertexelement constructor. Let’s break down the way they are defined. The first element is the offset. This is the position where your current element’s data exists within the vertexdeclaration. Hence, it is always the total number of bytes defined before the current element. The second parameter is the vertexelementformat. This defines the type of your element and thus has an impact on the following elements. In our example we use Vector4, this type contains 4 floats, so the offset of our next element should always be sizeof(float) * 4 higher.

The atlas texture

The third element is the VertexElementUsage and defines what the element represents. This can be a normal, a position, a colour or in our case a texture coordinate. We use texture coordinates to store our additional data, 4 Vector4 elements for our world matrix and 1 Vector2 element for our texture. The last parameter is the index of the usage parameter. Our shader can access several texture coordinates, TEXCOORD0, TEXCOORD1, … TEXCOORDn. The index defines the number n at the end TEXCOORD.

So why didn’t we use index 0 for the first VertexElement? That is because our geometry’s VertexBuffer is using TEXCOORD0 already through the VertexPositionTexture‘s elements. Both VertexBuffers will be sent to our GPU at the same time so overlapping usages are not allowed. So why did we define a Vector2 for our texture if there is already texture coordinate information available in the geometry buffer? Because we want our objects to have different textures!

Remember that we cannot change the geometry data or shader parameters during a batch call. We can only set a texture parameter once before we draw thousands of objects. The technique we’re going to use is often referred to as a texture atlas. A texture atlas is a big texture that contains different textures tiled up next to each other. We can set this atlas as the texture parameter of the shader before our batch draw call. The texture information that will be kept inside the instance buffer will hold the coordinates of the texture within the texture atlas.

For example, the atlas coordinates of the red coffee cup are (0, 1).  This is just one of the techniques you can use to switch textures. Because this is a tutorial aimed towards hardware instancing we aren’t going to spend a lot of time on a high quality atlas technique. You can probably notice some visual artefacts and if you are looking for a good atlas example, this isn’t it!

Time to prepare some instance data through the GenerateInstanceInformation function. Call this function in your LoadContent method, but make sure you call it after the VertexDeclaration is created. We’ll also define a count member for the amount of objects to draw.

Int32 count = 20000;

private void GenerateInstanceInformation()
{
 InstanceInfo[] instances = new InstanceInfo[count];
 Random rnd = new Random();
 for (int i = 0; i < count; i++)
 {
  instances[i].World =
   Matrix.CreateScale((float)rnd.NextDouble() + 0.5f) *
   Matrix.CreateFromYawPitchRoll((float)rnd.NextDouble() * MathHelper.TwoPi, (float)rnd.NextDouble() * MathHelper.TwoPi, (float)rnd.NextDouble() * MathHelper.TwoPi) *
   Matrix.CreateTranslation(new Vector3(-rnd.Next(300), -rnd.Next(250), -rnd.Next(400)));
  instances[i].AtlasCoordinate = new Vector2(rnd.Next(0, 2), rnd.Next(0, 2));
 }
 instanceBuffer = new VertexBuffer(GraphicsDevice, instanceVertexDeclaration, count, BufferUsage.WriteOnly);
 instanceBuffer.SetData(instances);
}

So now that we have our 2 VertexBuffers, we’ll need to assign them to our GPU. In XNA 4, to do this you need to use a VertexBufferBinding. Personally I feel XNA 4 handles this much more elegantly than 3.1 did, because they’re much easier to declare and understand. Create a binding member to the game class and add the following code in your LoadContent method to create the binding:

VertexBufferBinding[] bindings;
bindings = new VertexBufferBinding[2];
bindings[0] = new VertexBufferBinding(geometryBuffer);
bindings[1] = new VertexBufferBinding(instanceBuffer, 0, 1);

The next step is creating our shader. The shader code will more or less be the same as the one in my previous article about hardware instancing. The main difference being that we’ll use textures from a texture atlas rather than colours. Add a new effect to your content project and use the following piece of code:

float4x4 WVP;
texture cubeTexture;

sampler TextureSampler = sampler_state
{
 texture = <cubeTexture>;
 mipfilter = LINEAR;
 minfilter = LINEAR;
 magfilter = LINEAR;
};

struct InstancingVSinput
{
 float4 Position : POSITION0;
 float2 TexCoord : TEXCOORD0;
};

struct InstancingVSoutput
{
 float4 Position : POSITION0;
 float2 TexCoord : TEXCOORD0;
};

InstancingVSoutput InstancingVS(InstancingVSinput input, float4x4 instanceTransform : TEXCOORD1, float2 atlasCoord : TEXCOORD5)
{
 InstancingVSoutput output;
 float4 pos = input.Position;
 pos = mul(pos, transpose(instanceTransform));
 pos = mul(pos, WVP);
 output.Position = pos;
 output.TexCoord = float2((input.TexCoord.x / 2.0f) + (1.0f / 2.0f * atlasCoord.x),
 (input.TexCoord.y / 2.0f) + (1.0f / 2.0f * atlasCoord.y));
 return output;
}

float4 InstancingPS(InstancingVSoutput input) : COLOR0
{
 return tex2D(TextureSampler, input.TexCoord);
}

technique Instancing
{
 pass Pass0
 {
 VertexShader = compile vs_3_0 InstancingVS();
 PixelShader = compile ps_3_0 InstancingPS();
 }
}

Notice how the TEXCOORD indices line up with our VertexDeclarations? The texture coordinates from the geometry use 0. The matrix which is 4 rows of Vector4‘s use 1 through 4. The texture atlas coordinates use 5. Just remember that this isn’t a particularly high quality texture atlas! Now add the texture of the coloured coffee cups to the project and load both of them into their respective members.

Texture2D texture;
Effect effect;
effect = Content.Load<Effect>("HardwareInstancing");
texture = Content.Load<Texture2D>("default_256");

Finally we can move on to the fun stuff: drawing! As always, you need to know from where to draw and how to project the vertices. For this we’ll define two matrices and set them in the LoadContent method.

Matrix view;
Matrix projection;
effect = Content.Load<Effect>("HardwareInstancing");
texture = Content.Load<Texture2D>("default_256");

The last step that remains is filling out Draw method. The following snippet of code simply applies our shader with the correct parameters. Our bindings are then being set through the SetVertexBuffers method. These must be set correctly for DrawInstancedPrimitives to work. This means that one buffer needs 0 as instance frequency and one needs 1+.

GraphicsDevice.Clear(Color.CornflowerBlue);

effect.CurrentTechnique = effect.Techniques["Instancing"];
effect.Parameters["WVP"].SetValue(view * projection);
effect.Parameters["cubeTexture"].SetValue(texture);

GraphicsDevice.Indices = indexBuffer;

effect.CurrentTechnique.Passes[0].Apply();

GraphicsDevice.SetVertexBuffers(bindings);

GraphicsDevice.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, 24, 0, 12, count);

If all went well, you should see something like this when you run the project:

Code:

public class Game1 : Microsoft.Xna.Framework.Game
 {
 GraphicsDeviceManager graphics;

 
 VertexBuffer instanceBuffer;
 VertexBuffer geometryBuffer;
 IndexBuffer indexBuffer;
 Int32 count = 20000;

 VertexDeclaration instanceVertexDeclaration;

 VertexBufferBinding[] bindings;

 Texture2D texture;
 Effect effect;

 Matrix view;
 Matrix projection;

 struct InstanceInfo
 {
 public Matrix World;
 public Vector2 AtlasCoordinate;
 };

 public Game1()
 {
 graphics = new GraphicsDeviceManager(this);
 Content.RootDirectory = "Content";
 }


 protected override void Initialize()
 {

 base.Initialize();
 }


 protected override void LoadContent()
 {
 GenerateGeometryBuffers();
 GenerateInstanceVertexDeclaration();
 GenerateInstanceInformation();

 effect = Content.Load<Effect>("HardwareInstancing");
 texture = Content.Load<Texture2D>("default_256");

 bindings = new VertexBufferBinding[2];
 bindings[0] = new VertexBufferBinding(geometryBuffer, 0);
 bindings[1] = new VertexBufferBinding(instanceBuffer, 0, 1);

 view = Matrix.CreateLookAt(new Vector3(1, 1, 1), Vector3.Zero, Vector3.UnitZ);
 projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, (float)Window.ClientBounds.Width / (float)Window.ClientBounds.Height, 0.1f, 2000.0f);

 }

 private void GenerateGeometryBuffers()
 {
 VertexPositionTexture[] vertices = new VertexPositionTexture[24];
 vertices[0].Position = new Vector3(-1, 1, -1);
 vertices[0].TextureCoordinate = new Vector2(0, 0);
 vertices[1].Position = new Vector3(1, 1, -1);
 vertices[1].TextureCoordinate = new Vector2(1, 0);
 vertices[2].Position = new Vector3(-1, 1, 1);
 vertices[2].TextureCoordinate = new Vector2(0, 1);
 vertices[3].Position = new Vector3(1, 1, 1);
 vertices[3].TextureCoordinate = new Vector2(1, 1);


 vertices[4].Position = new Vector3(-1, -1, 1);
 vertices[4].TextureCoordinate = new Vector2(0, 0);
 vertices[5].Position = new Vector3(1, -1, 1);
 vertices[5].TextureCoordinate = new Vector2(1, 0);
 vertices[6].Position = new Vector3(-1, -1, -1);
 vertices[6].TextureCoordinate = new Vector2(0, 1);
 vertices[7].Position = new Vector3(1, -1, -1);
 vertices[7].TextureCoordinate = new Vector2(1, 1);


 vertices[8].Position = new Vector3(-1, 1, -1);
 vertices[8].TextureCoordinate = new Vector2(0, 0);
 vertices[9].Position = new Vector3(-1, 1, 1);
 vertices[9].TextureCoordinate = new Vector2(1, 0);
 vertices[10].Position = new Vector3(-1, -1, -1);
 vertices[10].TextureCoordinate = new Vector2(0, 1);
 vertices[11].Position = new Vector3(-1, -1, 1);
 vertices[11].TextureCoordinate = new Vector2(1, 1);

 vertices[12].Position = new Vector3(-1, 1, 1);
 vertices[12].TextureCoordinate = new Vector2(0, 0);
 vertices[13].Position = new Vector3(1, 1, 1);
 vertices[13].TextureCoordinate = new Vector2(1, 0);
 vertices[14].Position = new Vector3(-1, -1, 1);
 vertices[14].TextureCoordinate = new Vector2(0, 1);
 vertices[15].Position = new Vector3(1, -1, 1);
 vertices[15].TextureCoordinate = new Vector2(1, 1);

 vertices[16].Position = new Vector3(1, 1, 1);
 vertices[16].TextureCoordinate = new Vector2(0, 0);
 vertices[17].Position = new Vector3(1, 1, -1);
 vertices[17].TextureCoordinate = new Vector2(1, 0);
 vertices[18].Position = new Vector3(1, -1, 1);
 vertices[18].TextureCoordinate = new Vector2(0, 1);
 vertices[19].Position = new Vector3(1, -1, -1);
 vertices[19].TextureCoordinate = new Vector2(1, 1);

 vertices[20].Position = new Vector3(1, 1, -1);
 vertices[20].TextureCoordinate = new Vector2(0, 0);
 vertices[21].Position = new Vector3(-1, 1, -1);
 vertices[21].TextureCoordinate = new Vector2(1, 0);
 vertices[22].Position = new Vector3(1, -1, -1);
 vertices[22].TextureCoordinate = new Vector2(0, 1);
 vertices[23].Position = new Vector3(-1, -1, -1);
 vertices[23].TextureCoordinate = new Vector2(1, 1);

 geometryBuffer = new VertexBuffer(GraphicsDevice, VertexPositionTexture.VertexDeclaration, 24, BufferUsage.WriteOnly);
 geometryBuffer.SetData(vertices);

 int[] indices = new int[36];
 indices[0] = 0; indices[1] = 1; indices[2] = 2;
 indices[3] = 1; indices[4] = 3; indices[5] = 2;

 indices[6] = 4; indices[7] = 5; indices[8] = 6;
 indices[9] = 5; indices[10] = 7; indices[11] = 6;

 indices[12] = 8; indices[13] = 9; indices[14] = 10;
 indices[15] = 9; indices[16] = 11; indices[17] = 10;

 indices[18] = 12; indices[19] = 13; indices[20] = 14;
 indices[21] = 13; indices[22] = 15; indices[23] = 14;

 indices[24] = 16; indices[25] = 17; indices[26] = 18;
 indices[27] = 17; indices[28] = 19; indices[29] = 18;

 indices[30] = 20; indices[31] = 21; indices[32] = 22;
 indices[33] = 21; indices[34] = 23; indices[35] = 22;

 indexBuffer = new IndexBuffer(GraphicsDevice, typeof(int), 36, BufferUsage.WriteOnly);
 indexBuffer.SetData(indices);
 }

 private void GenerateInstanceVertexDeclaration()
 {
 VertexElement[] instanceStreamElements = new VertexElement[5];
 instanceStreamElements[0] = new VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 1);
 instanceStreamElements[1] = new VertexElement(sizeof(float) * 4, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 2);
 instanceStreamElements[2] = new VertexElement(sizeof(float) * 8, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 3);
 instanceStreamElements[3] = new VertexElement(sizeof(float) * 12, VertexElementFormat.Vector4, VertexElementUsage.TextureCoordinate, 4);
 instanceStreamElements[4] = new VertexElement(sizeof(float) * 16, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 5);
 instanceVertexDeclaration = new VertexDeclaration(instanceStreamElements);
 }

 private void GenerateInstanceInformation()
 {
 InstanceInfo[] instances = new InstanceInfo[count];
 Random rnd = new Random();
 for (int i = 0; i < count; i++)
 {
 instances[i].World =
 Matrix.CreateScale((float)rnd.NextDouble() + 0.5f) *
 Matrix.CreateFromYawPitchRoll((float)rnd.NextDouble() * MathHelper.TwoPi, (float)rnd.NextDouble() * MathHelper.TwoPi, (float)rnd.NextDouble() * MathHelper.TwoPi) *
 Matrix.CreateTranslation(new Vector3(-rnd.Next(300), -rnd.Next(250), -rnd.Next(400)));
 instances[i].AtlasCoordinate = new Vector2(rnd.Next(0, 2), rnd.Next(0, 2));
 }
 instanceBuffer = new VertexBuffer(GraphicsDevice, instanceVertexDeclaration, count, BufferUsage.WriteOnly);
 instanceBuffer.SetData(instances);
 }

 protected override void Draw(GameTime gameTime)
 {
 GraphicsDevice.Clear(Color.Black);

 effect.CurrentTechnique = effect.Techniques["Instancing"];
 effect.Parameters["WVP"].SetValue(view * projection);
 effect.Parameters["cubeTexture"].SetValue(texture);

 GraphicsDevice.Indices = indexBuffer;

 effect.CurrentTechnique.Passes[0].Apply();

 GraphicsDevice.SetVertexBuffers(bindings);

 GraphicsDevice.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, 24, 0, 12, count);
 

 base.Draw(gameTime);
 }
 }

Shader:

float4x4 WVP;
texture cubeTexture;
 
sampler TextureSampler = sampler_state
{
 texture = <cubeTexture>; 
 mipfilter = LINEAR;
 minfilter = LINEAR;
 magfilter = LINEAR;
};

struct InstancingVSinput
{
 float4 Position : POSITION0;
 float2 TexCoord : TEXCOORD0;
};
 
struct InstancingVSoutput
{
 float4 Position : POSITION0;
 float2 TexCoord : TEXCOORD0;
};
 
InstancingVSoutput InstancingVS(InstancingVSinput input, float4x4 instanceTransform : TEXCOORD1, float2 atlasCoord : TEXCOORD5)
{
 InstancingVSoutput output;
 float4 pos = input.Position;
 pos = mul(pos, transpose(instanceTransform));
 pos = mul(pos, WVP);
 output.Position = pos;
 output.TexCoord = float2((input.TexCoord.x / 2.0f) + (1.0f / 2.0f * atlasCoord.x), 
 (input.TexCoord.y / 2.0f) + (1.0f / 2.0f * atlasCoord.y));
 return output;
}
 
float4 InstancingPS(InstancingVSoutput input) : COLOR0
{
 return tex2D(TextureSampler, input.TexCoord);
}
 
technique Instancing
{
 pass Pass0
 {
 VertexShader = compile vs_3_0 InstancingVS();
 PixelShader = compile ps_3_0 InstancingPS();
 }
}

Leave a Reply