Shader Instancing on WP7 using the SkinnedEffect trick

Ok so, WP7 doesn't allow custom shader code at the moment so you might think that getting something like instancing working would be impossible. Luckily it's pretty easy to append BoneIndices and BoneWeights to an existing vertex buffer for use with a SkinnedEffect shader.


Shader-instancing uses the same shader code as skinning. By setting the number of bones to one and setting the bone weight to 1.0 (100%), you can use the bone as the transformation matrix for each of your instances.


The basic idea is as follows:


  • Get the original model's vertex buffer
  • Calculate the new vertex stride with an extra Byte4 for the BoneIndices and a Vector4 for the BoneWeights
  • Create a new vertex buffer large enough for the maximum number of instances per draw call, using the original model's vertex count and the new stride size
  • Populate this new vertex buffer by replicating the vertex data for each instance, setting the correct bone index for each instance and setting the bone weight to 1.0
  • Do the same for the IndexBuffer - create an index buffer large enough for all instances and replicate the data for each instance

To draw the instances, pass an array of transformation matrices as the bones of a SkinnedEffect shader while setting the number of bones to one.


You can see the code below. Most of the code has been taken from the instancing sample on creators.xna.com, with modifications for XNA4 and SkinnedEffect. I've also moved the code away from the Content Pipeline into a class that you use by simply passing an existing model and GraphicsDevice. This might not be perfect but it should be pretty easy to adapt it to your needs. Also, the code is extremely simple and naive because it expects your mesh to have a single meshpart with one texture only. This is fine for my simple models but if you need something more complex you'll have to tweak the code a bit.


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using Microsoft.Xna.Framework.Graphics;

using Microsoft.Xna.Framework;

 

namespace DataTypes

{

    public class InstancedModel

    {

        const int maxShaderMatrices = 60;

 

        VertexBuffer instancedVertexBuffer;

        IndexBuffer instancedIndexBuffer;

        VertexDeclaration instancedVertexDeclaration;

        int originalVertexCount = 0;

        int originalIndexCount = 0;

        int vertexStride = 0;

        int maxInstances = 0;

 

        Matrix[] tempMatrices = new Matrix[maxShaderMatrices];

        Model originalModel;

        GraphicsDevice graphicsDevice;

 

        public InstancedModel(GraphicsDevice graphics, Model model)

        {

            graphicsDevice = graphics;

            originalModel = model;

 

            SetupInstancedVertexData();

        }

 

        void SetupInstancedVertexData()

        {

            // Read the existing vertex data, then destroy the existing vertex buffer.

            ModelMesh mesh = originalModel.Meshes[0];

            ModelMeshPart part = mesh.MeshParts[0];

 

            originalVertexCount = part.VertexBuffer.VertexCount;

            VertexDeclaration originalVertexDeclaration = part.VertexBuffer.VertexDeclaration;

 

            int indexOverflowLimit = ushort.MaxValue / originalVertexCount;

            maxInstances = Math.Min(indexOverflowLimit, maxShaderMatrices);

 

 

            byte[] oldVertexData = new byte[originalVertexCount * originalVertexDeclaration.VertexStride];

 

            part.VertexBuffer.GetData(oldVertexData);

 

            // Adjust the vertex stride to include our additional index channel.

            int oldVertexStride = part.VertexBuffer.VertexDeclaration.VertexStride;

            vertexStride = oldVertexStride + (sizeof(byte) * 4) + (sizeof(float) * 4); //add Byte4 for BoneIndices and Vector4 for BoneWeights

 

            // Allocate a temporary array to hold the replicated vertex data.

            byte[] newVertexData = new byte[originalVertexCount * vertexStride * maxInstances];

 

            int outputPosition = 0;

 

            // Replicate one copy of the original vertex buffer for each instance.

            for (int instanceIndex = 0; instanceIndex < maxInstances; instanceIndex++)

            {

                int sourcePosition = 0;

 

                // Convert the instance index from float into an array of raw bits.

                byte[] blendIndices = new byte[4];

                blendIndices[0] = (byte)instanceIndex;

                blendIndices[1] = (byte)instanceIndex;

                blendIndices[2] = (byte)instanceIndex;

                blendIndices[3] = (byte)instanceIndex;

 

                byte[] blendWeight = BitConverter.GetBytes(1.0f);

                for (int i = 0; i < originalVertexCount; i++)

                {

                    // Copy over the existing data for this vertex.

                    Array.Copy(oldVertexData, sourcePosition,

                               newVertexData, outputPosition, oldVertexStride);

 

                    outputPosition += oldVertexStride;

                    sourcePosition += oldVertexStride;

 

                    // Set the value of our new index channel.

                    blendIndices.CopyTo(newVertexData, outputPosition);

                    outputPosition += blendIndices.Length;

 

                    //copy blend weights

                    blendWeight.CopyTo(newVertexData, outputPosition);

                    outputPosition += blendWeight.Length;

                    blendWeight.CopyTo(newVertexData, outputPosition);

                    outputPosition += blendWeight.Length;

                    blendWeight.CopyTo(newVertexData, outputPosition);

                    outputPosition += blendWeight.Length;

                    blendWeight.CopyTo(newVertexData, outputPosition);

                    outputPosition += blendWeight.Length;

                }

            }

 

            int instanceIndexOffset = oldVertexStride;

            VertexElement[] extraElements =

            {

                new VertexElement((short)oldVertexStride, VertexElementFormat.Byte4, VertexElementUsage.BlendIndices, 0),

                new VertexElement((short)oldVertexStride + (sizeof(byte) * 4), VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 0)

            };

 

            int length = originalVertexDeclaration.GetVertexElements().Length + extraElements.Length;

 

            VertexElement[] elements = new VertexElement[length];

            originalVertexDeclaration.GetVertexElements().CopyTo(elements, 0);

            extraElements.CopyTo(elements, originalVertexDeclaration.GetVertexElements().Length);

 

            // Create a new vertex declaration.

            instancedVertexDeclaration = new VertexDeclaration(elements);

 

            // Create a new vertex buffer, and set the replicated data into it.

            instancedVertexBuffer = new VertexBuffer(graphicsDevice, instancedVertexDeclaration, newVertexData.Length, BufferUsage.None);

            instancedVertexBuffer.SetData(newVertexData);

 

 

            //handle vertex indices

            originalIndexCount = part.IndexBuffer.IndexCount;

            ushort[] oldIndices = new ushort[originalIndexCount];

            part.IndexBuffer.GetData(oldIndices);

 

            // Allocate a temporary array to hold the replicated index data.

            ushort[] newIndices = new ushort[originalIndexCount * maxInstances];

 

            outputPosition = 0;

            // Replicate one copy of the original index buffer for each instance.

            for (int instanceIndex = 0; instanceIndex < maxInstances; instanceIndex++)

            {

                int instanceOffset = instanceIndex * originalVertexCount;

 

                for (int i = 0; i < part.IndexBuffer.IndexCount; i++)

                {

                    newIndices[outputPosition] = (ushort)(oldIndices[i] +

                                                          instanceOffset);

 

                    outputPosition++;

                }

            }

 

            // Create a new index buffer, and set the replicated data into it.

            instancedIndexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, newIndices.Length, BufferUsage.None);

            instancedIndexBuffer.SetData(newIndices);

        }

 

        public void Draw(Matrix[] transformMatrices, int totalInstances, SkinnedEffect skinnedEffect)

        {

            BasicEffect effect = (BasicEffect)originalModel.Meshes[0].MeshParts[0].Effect;

            skinnedEffect.Texture = effect.Texture;

 

            graphicsDevice.SetVertexBuffer(instancedVertexBuffer);

            graphicsDevice.Indices = instancedIndexBuffer;

 

            for (int i = 0; i < totalInstances; i += maxInstances)

            {

                // How many instances can we fit into this batch?

                int instanceCount = totalInstances - i;

 

                if (instanceCount > maxInstances)

                    instanceCount = maxInstances;

 

                // Upload transform matrices as shader constants.

                Array.Copy(transformMatrices, i, tempMatrices, 0, instanceCount);

 

                skinnedEffect.SetBoneTransforms(tempMatrices);

 

                foreach (EffectPass pass in skinnedEffect.CurrentTechnique.Passes)

                {

                    pass.Apply();

                    graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, instanceCount * originalVertexCount, 0, instanceCount * originalIndexCount / 3);

                }

            }

        }

    }

}

 

Particle Effects on Windows Phone 7

Being a complete noob at games programming, I initially approached particle rendering by calling Draw() for each particle. I'm not sure if you know this, but calling Draw() several thousand times per frame results in a slideshow.

So here's my solution to particle explosions. Each particle is represented by two vertices that form a line. As the particle gets older, its velocity decreases, along with its length. This produces a sort of tracer bullet effect.

To avoid multiple Draw() calls, I create a large vertex buffer that holds the total number of vertices that represent all of the particles i.e. particles * 2. Each vertex is translated to the position of the particle before being inserted into this giant vertex buffer.

The resulting vertex buffer can then be rendered using one DrawUserPrimitives() call. If I remember correctly, Shawn Hargraves says that DrawUserPrimitives() is the best way to draw vertex buffers that change frequently i.e. per frame. I also tried using a DynamicVertexBuffer but this seems to be the best way.

In addition to the particle lines, I decided to add some additively blended sprites to give it a more interesting look. The sprite I used is a simple circle with a gradient. To position the sprite I calculate the 2D position from the particle's 3D coordinates using the Graphics.Viewport.Project() method.

You can see the drawing code here below. The code for the creation of particles uses standard object pooling to avoid garbage collection. An object is taken from the pool, given an initial position, direction and velocity and placed into an array of "alive" particles. After a certain time the particle "dies", is removed from the live particles array and is ready to be re-used in the object pool.

You can see a video of the particle effects below. The fireball part of the explosion is a simple textured sphere. As it gets older, its scale increases and its alpha value decreases.




    1 public static int MAX_PARTICLES = 2000;

    2 VertexPositionColor[] explosionVertices;

    3 

    4 protected void InitParticleVertices()

    5 {

    6     explosionVertices = new VertexPositionColor[MAX_PARTICLES * 2];

    7 }

    8 

    9 public void Draw(GraphicsDevice Graphics, BasicEffect shader)

   10 {

   11     if (aliveParticles.Count > 0)

   12     {

   13         int vertexCount = 0;

   14         int particleCount = 0;

   15         foreach (ExplosionParticle particle in aliveParticles)

   16         {

   17             Vector3 v1 = Vector3.Zero;

   18             Vector3 v2 = new Vector3(0, 0, -1);

   19             Vector3 v3;

   20             //shorten the particle line based on its age

   21             Vector3.Multiply(ref v2, particle.Age, out v3);

   22             Vector3.Subtract(ref v2, ref v3, out v2);

   23 

   24             Matrix R = Matrix.CreateRotationY(particle.Angle);

   25             Matrix T = Matrix.CreateTranslation(particle.Pos);

   26 

   27             //rotate and position the line

   28             Matrix W;

   29             Matrix.Multiply(ref R, ref T, out W);

   30 

   31             Vector3.Transform(ref v1, ref W, out v1);

   32             Vector3.Transform(ref v2, ref W, out v2);

   33 

   34             explosionVertices[vertexCount].Position = v1;

   35             explosionVertices[vertexCount].Color = particle.Color;

   36 

   37             vertexCount++;

   38             explosionVertices[vertexCount].Position = v2;

   39             explosionVertices[vertexCount].Color = particle.Color;

   40 

   41 

   42             vertexCount++;

   43             particleCount++;

   44         }

   45 

   46         BlendState oldBlend = Graphics.BlendState;

   47         Graphics.BlendState = Game.additiveBlendState;

   48 

   49         shader.Alpha = 1.0f;

   50         shader.LightingEnabled = false;

   51         shader.DiffuseColor = Color.White.ToVector3();

   52         shader.VertexColorEnabled = true;

   53         shader.World = Matrix.Identity;

   54 

   55         //draw particle lines in one Draw() call

   56         foreach (EffectPass pass in shader.CurrentTechnique.Passes)

   57         {

   58             pass.Apply();

   59             Graphics.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, explosionVertices, 0, aliveParticles.Count);

   60         }

   61 

   62         //draw a sprite for each particle, slightly offset from the particle line

   63         Game.SpriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.Additive);

   64         for (int x = 0; x < aliveParticles.Count; x++)

   65         {

   66             ExplosionParticle particle = aliveParticles[x];

   67             //use Project() to determine the position in 2D screen space

   68             Vector3 screenCoords = Graphics.Viewport.Project(particle.Pos + (2f * particle.dir), shader.Projection, shader.View, Matrix.Identity);

   69 

   70             //As with the line lengths, reduce the size of the sprite based on the age of the particle

   71             float size = 8f;

   72             size *= (1f - particle.Age);

   73 

   74             Color color = Color.White;

   75             if (Utils.GetRandomNumber(0, 2) == 1)

   76             {

   77                 color = particle.Color;

   78             }

   79 

   80             Rectangle rect = new Rectangle((int)screenCoords.X, (int)screenCoords.Y, (int)size, (int)size);

   81             Game.SpriteBatch.Draw(bloomTexture, rect, color);

   82         }

   83         Game.SpriteBatch.End();

   84 

   85 

   86         Graphics.BlendState = oldBlend;

   87         shader.Alpha = 1f;

   88         shader.VertexColorEnabled = false;

   89     }

   90 }