In this recipe, we will create a mesh renderer that renders meshes loaded from a compiled mesh object (.CMO
) file. We will use the Visual Studio graphics content pipeline to compile an Autodesk FBX model that has been exported from the open source 3D modeling and animation software, Blender (blender.org).
The class Common.Mesh
within the provided sample framework will be used to load the .CMO
file format and to store the loaded mesh. The loaded mesh will contain the vertex and index buffers along with material and lighting parameters and can also include the name of textures and pixel shaders to use.
This recipe assumes that a right-handed coordinate system is being used; see the previous recipe, Using a right-handed coordinate system.
Before we get started, there are a few files we need from the downloaded package and a class within the Common
project we will review.
Common.Mesh
class is a C# implementation for deserializing the compiled mesh object (.CMO
) that is generated by the Visual Studio graphics content pipeline. Most notably, this class includes the following static method: Common.Mesh.LoadFromFile
. This class is partly based on a C# port of the ModelLoadCMO
class in DirectXTK (https://directxtk.codeplex.com).MeshContentTask.targets
and MeshContentTask.props
files must be copied to the same directory as your solution file.Ch03_02LoadMeshMale_base_mesh.fbx
file is the example mesh we will be loading for this recipe.The completed project for this recipe can be found in the code bundle of this chapter, Ch03_03LoadMesh, downloadable from Packt's website.
We will begin by adding the additional build targets and then including the FBX model and checking that it correctly compiles.
</Project>
tag. Here we are assuming that the additional build target files are located in the directory above this project.<Import Project="..MeshContentTask.targets" />
Male_base_mesh.fbx
file into the project directory and include it in the project.binDebugMale_base_mesh.cmo
(or the binDebugAppX
directory for Windows Store apps). Any messages from the content pipeline will also appear in the build output window.The modified MeshContentTask.*
files are based upon the Visual Studio 2012 Update 2 and Visual Studio 2013 releases. If for some reason they are not working correctly for you, a copy of the compiled mesh object is included in Ch03_03LoadMeshMale_base_mesh.cmo
. This can then be included in the project. Once this is done, select Copy if newer as the copy for the output directory option.
Now that we have our compiled mesh, we need to create a mesh renderer.
MeshRenderer
class.// The vertex buffer List<Buffer> vertexBuffers = new List<Buffer>(); // The index buffer List<Buffer> indexBuffers = new List<Buffer>(); // Texture resources List<ShaderResourceView> textureViews = new List<ShaderResourceView>(); // Control sampling behavior with this state SamplerState samplerState; // The loaded mesh Common.Mesh mesh; public Common.Mesh Mesh { get { return this.mesh; } } // The per material buffer to use so that the mesh // parameters can be used public Buffer PerMaterialBuffer { get; set; }
Common.Mesh
instance.public MeshRenderer(Common.Mesh mesh) { this.mesh = mesh; }
CreateDeviceDependentResources
with the following code. First release the existing vertex, index buffers, and texture views.// Dispose of each vertex, index buffer and texture vertexBuffers.ForEach(vb => RemoveAndDispose(ref vb)); vertexBuffers.Clear(); indexBuffers.ForEach(ib => RemoveAndDispose(ref ib)); indexBuffers.Clear(); textureViews.ForEach(tv => RemoveAndDispose(ref tv)); textureViews.Clear(); RemoveAndDispose(ref samplerState);
// Initialize vertex buffers for (int indx = 0; indx < mesh.VertexBuffers.Count; indx++) { var vb = mesh.VertexBuffers[indx]; Vertex[] vertices = new Vertex[vb.Length]; for (var i = 0; i < vb.Length; i++) { // Create vertex vertices[i] = new Vertex(vb[i].Position, vb[i].Normal, vb[i].Color, vb[i].UV); } vertexBuffers.Add(ToDispose(Buffer.Create(device, BindFlags.VertexBuffer, vertices.ToArray()))); vertexBuffers[vertexBuffers.Count - 1].DebugName = "VertexBuffer_" + indx.ToString(); }
// Initialize index buffers foreach (var ib in mesh.IndexBuffers) { indexBuffers.Add(ToDispose(Buffer.Create(device, BindFlags.IndexBuffer, ib))); indexBuffers[indexBuffers.Count - 1].DebugName = "IndexBuffer_" + (indexBuffers.Count - 1).ToString(); }
// Initialize texture views // The CMO file format supports up to 8 per material foreach (var m in mesh.Materials) { // Diffuse Color for (var i = 0; i < m.Textures.Length; i++) { if (SharpDX.IO.NativeFile.Exists(m.Textures[i])) textureViews.Add(ToDispose( ShaderResourceView.FromFile( device, m.Textures[i]))); else textureViews.Add(null); } } // Create our sampler state samplerState = ToDispose(new SamplerState(device, new SamplerStateDescription() { AddressU = TextureAddressMode.Clamp, AddressV = TextureAddressMode.Clamp, AddressW = TextureAddressMode.Clamp, BorderColor = new Color4(0, 0, 0, 0), ComparisonFunction = Comparison.Never, Filter = Filter.MinMagMipLinear, MaximumAnisotropy = 16, MaximumLod = float.MaxValue, MinimumLod = 0, MipLodBias = 0.0f }));
DoRender
method to render our mesh. We will be rendering the model's submeshes grouped by material, so we begin by iterating the mesh's available materials. After retrieving the submeshes for the material, we update the PerMaterialBuffer
instance and then render the submesh as shown in the following code:// Draw sub-meshes grouped by material for (var mIndx = 0; mIndx < mesh.Materials.Count; mIndx++) { // If the material buffer is assigned and submeshes // user the material, update the PerMaterialBuffer if (PerMaterialBuffer != null && subMeshesForMaterial.Length > 0) {... update material buffer
} // For each sub-mesh foreach (var subMesh in subMeshesForMaterial) {... render each sub-mesh
} }
PerMaterialBuffer
is assigned. If the first texture view is not null, we will set the HasTexture
property of the PerMaterial
buffer to true
.// update the PerMaterialBuffer constant buffer var material = new ConstantBuffers.PerMaterial() { Ambient = new Color4(mesh.Materials[mIndx].Ambient), Diffuse = new Color4(mesh.Materials[mIndx].Diffuse), Emissive = new Color4(mesh.Materials[mIndx].Emissive), Specular = new Color4(mesh.Materials[mIndx].Specular), SpecularPower = mesh.Materials[mIndx].SpecularPower, UVTransform = mesh.Materials[mIndx].UVTransform, }; // Bind textures to the pixel shader int texIndxOffset = mIndx * Common.Mesh.MaxTextures; material.HasTexture = (uint)(textureViews[texIndxOffset] != null ? 1 : 0); // 0=false context.PixelShader.SetShaderResources(0, textureViews.GetRange(texIndxOffset, Common.Mesh.MaxTextures).ToArray()); // Set texture sampler state context.PixelShader.SetSampler(0, samplerState); // Update material buffer context.UpdateSubresource(ref material, PerMaterialBuffer);
// Ensure the vert ex buffer and index buffers are in range if (subMesh.VertexBufferIndex < vertexBuffers.Count && subMesh.IndexBufferIndex < indexBuffers.Count) { // Retrieve and set the vertex and index buffers var vertexBuffer = vertexBuffers[ (int)subMesh.VertexBufferIndex]; context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertexBuffer, Utilities.SizeOf<Vertex>(), 0)); context.InputAssembler.SetIndexBuffer(indexBuffers[ (int)subMesh.IndexBufferIndex], Format.R16_UInt, 0); // Set topology context.InputAssembler.PrimitiveTopology = SharpDX.Direct3D.PrimitiveTopology.TriangleList; } // Draw the sub-mesh (includes the triangle count) // The submesh has a start index into the vertex buffer context.DrawIndexed((int)subMesh.PrimCount * 3, (int)subMesh.StartIndex, 0);
Now we can load and render our mesh within our D3DApp
class.
D3DApp.Run()
, where we have created our previous renders, insert the following code to load the mesh file and then create the mesh renderer.// Create and initialize the mesh renderer var loadedMesh = Common.Mesh.LoadFromFile("Male_base_mesh.cmo"); var mesh = ToDispose(new MeshRenderer(loadedMesh.First())); mesh.Initialize(this); mesh.World = Matrix.Identity;
PerObject
constant buffer, set the PerMaterialBuffer
property of the mesh renderer, and then tell it to render.// Update the matrices perObject.World = mesh.World * worldMatrix; ... context.UpdateSubresource(ref perObject, perObjectBuffer); // Provide with material constant buffer mesh.PerMaterialBuffer = perMaterialBuffer; // Render the mesh mesh.Render();
The Visual Studio graphics content pipeline uses [VSInstallDir]Common7IDEExtensionsMicrosoftVsGraphicsvsgraphics.exe
to display models/scenes and also to compile them into objects ready for consumption in your application at runtime. Unfortunately, Microsoft has only provided MSBuild targets for C++ projects; however, with a few tweaks of the original MSBuild target files (also located in the same directory), we can now do the same for our C# projects.
The compiled mesh object file structure may include multiple vertex buffers, index buffers, submeshes, materials, texture references, the pixel shaders used, bones, and animations. The exact binary file layout is shown within the comments in the CommonMesh.cs
class file. The mesh class uses a BinaryReader
instance and some helpful extension methods to load the CMO file.
Our mesh renderer is now available to manage the rendering tasks for the mesh object's vertex and index buffers, materials, and textures. This renderer simply iterates the available materials, updating the material buffer and then for each submesh that uses this material, it loads the appropriate vertex and index buffers.
Currently, our solution is grouping materials that are used within a single mesh and its submeshes. For a full-engine implementation, it would be more likely that materials are shared between different meshes and therefore it would be necessary to correctly group multiple meshes that use a material.
You can open the FBX file within Visual Studio and play with the material settings to see how they affect the final render. At this point, it is also worth trying to use models that include textures.
You will find two more content pipeline MSBuild targets that have been converted to work with C# projects in the downloaded code: one for compiling images (ImageContentTask) and one for compiling shader graphs (the .dgls
files) to shader byte code (ShaderGraphContentTask).
Note that many downloaded meshes may have counter-clockwise vertex winding. When debugging rendering issues, it can sometimes be helpful to try disabling back-face culling in the rasterizer state. This is done as shown in the following code snippet:
// No culling
context.Rasterizer.State = ToDispose(new RasterizerState(device, new RasterizerStateDescription() {
FillMode = FillMode.Solid,
CullMode = CullMode.None,
}));
13.59.38.41