Thursday, September 25, 2008

FBX and XNA - Part 6 - 40 Million Triangles Per Second

Now that we have pretty 3D models on the screen, the first impression is "Golly thats slow".
One test model I have is about 18000 polygons or 35000 triangles. It displays at about 21 frames per second (FPS) on my PC. In a game that would mean that 2 models on the screen would be but-ugly slow. So lets move lots of the processing onto the Graphics Processing Unit (GPU).

The GPU uses a language call HLSL (High Level Shader Language). What you do is write a somthing.fx file, add it to your project and then load the Effect with the Content loader. Sounds easy. The trick is what to put in the .fx file and how to drive it. Fortunately Microsoft has already provided a SkinnedModelProcessor and it has a file called SkinnedModel.fx We just is that as-is.

HLSL defines method names in a .fx file for doing two things. First is converting a model X,Y,Z point to world space so it can be used as one of the points in a triangle or line drawing. Second is to determine the color of a point on a triangle as it is filled in on the screen (A Shader).

SkinnedModel.fx has a section like this...

// Vertex shader input structure.
struct VS_INPUT
{
float4 Position : POSITION0;
float3 Normal : NORMAL0;
float2 TexCoord : TEXCOORD0;
float4 BoneIndices : BLENDINDICES0;
float4 BoneWeights : BLENDWEIGHT0;
};
It indicates that each triangle corner is supposed to have a Position, a normal to the surface, a texture coordinate. That is just like our previous code. The BoneIndices is the index of the Deformer (Bone) that influences the point location with a weight of BoneWeights. The trick here is that a float4 has a X,Y,Z,W values meaning that there can be up to 4 deformers and their associated matrices that can influence the point.

Note: The Position has a vector4 which means it has a X,Y,Z and the mysterious W. Just set all the W's to 1.0f and it will work fine.

So we modify our code to generate this new structure in place of the previous DrawData point list. We use the structure...

    [Serializable]
public struct SkinnedModelRec
{
public Vector4 Position;
public Vector3 Normal;
///
/// The UV texture coord.
///

public Vector2 TexCoord;
///
/// We can have up to 4 bones influencing one vertex point and normal.
///

public Vector4 BoneIndices;
///
/// We can have up to 4 bones per vertex. These are the weights. Leave them 0 of no bone.
///

public Vector4 BoneWeights;

///
/// Description of the elements of this structure.
///

public static readonly VertexElement[] VertexElements =
new VertexElement[] {
new VertexElement(0,0,VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.Position, 0),
new VertexElement(0, sizeof(float) * 4, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0),
new VertexElement(0, sizeof(float) * 7, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0),
new VertexElement(0, sizeof(float) * 9, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.BlendIndices, 0),
new VertexElement(0, sizeof(float) * 13, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.BlendWeight, 0)
};

... some methods ...
}
Notice the vertex element definition. It tells XNA where the elements in the structure go in relation to the defeinitions in SkinnedModel.fx

Next we load all the points in the model, and the draw indices into the GPU


            TheVertexBuffer = new VertexBuffer(gd, typeof(SkinnedModelRec), DrawData.Length, BufferUsage.WriteOnly);
TheVertexDeclaration = new VertexDeclaration(gd, SkinnedModelRec.VertexElements);
TheIndexBuffer = new IndexBuffer(gd, typeof(int), DrawIndices.Length, BufferUsage.WriteOnly);
TheVertexBuffer.SetData(DrawData);
TheIndexBuffer.SetData(DrawIndices);
And then we can draw them. I realize that all the code is not here. This is just to get the idea of the style.



private void DrawSingleMesh(GraphicsDevice gd, PTake CurrentTake, Effect effect, PMesh mesh, PModelTracker tracker)
{
//mesh.CalcCurrentState(CurrentTake, tracker);
mesh.CalcAllDeformerMatrices(CurrentTake, tracker);

mesh.SetupGPU(gd);

gd.RenderState.CullMode = CullMode.None;
gd.VertexDeclaration = mesh.TheVertexDeclaration;
gd.Vertices[0].SetSource(mesh.TheVertexBuffer, 0, SkinnedModelRec.SizeInBytes);
gd.Indices = mesh.TheIndexBuffer;

// We draw all the triangles in a single texture at once. Then switch to next texture.
// Sometimes textures repeat later in the list.
foreach (PTextureRun tr in mesh.DrawDataTextureSegments)
{
if (tr.TextureId (lessthan messes up HTML) 0)
{
// Some polygons have no texture and are just white with illumination.
// We should probably not allow untextured polygons.
//effect.TextureEnabled = false;
effect.Parameters["Texture"].SetValue(DefaultTexture);
}
else
{
//effect.TextureEnabled = true;
effect.Parameters["Texture"].SetValue(Textures[tr.TextureId].Tex);
}

// Max bones in the SkinnedModel.fx is 59
Matrix[] bonesMatricies = new Matrix[59];
for (int i = 0; i lessthan mesh.DrawDeformers.Length; i++)
{
bonesMatricies[i] = mesh.DrawDeformers[i].Transform;
}
effect.Parameters["Bones"].SetValue(bonesMatricies);

effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();

gd.DrawIndexedPrimitives(PrimitiveType.TriangleList,
0,
tr.MinVertexIdx,
tr.MaxVertexIdx,
tr.StartIdx,
tr.PrimCount);

pass.End();
}
effect.End();
}
}
Easy as that. Then I tested it with a loop. We can now put up lots of our model on the screen and hold 60 FPS. The result is 40 million triangles per second. Now that's where we need to be to play a game.

TF

No comments: