Loading a static mesh from a file

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.

Getting ready

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.

  1. The 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).
  2. The MeshContentTask.targets and MeshContentTask.props files must be copied to the same directory as your solution file.
  3. The Ch03_02LoadMeshMale_base_mesh.fbx file is the example mesh we will be loading for this recipe.
  4. Alternatively, you can export any selected objects within Blender to Autodesk FBX by navigating to File | Export | Autodesk FBX (.fbx) within Blender.
  5. On the resulting export configuration panel, tick Selected Objects and select –Z Forward (for right-handed coordinates) as shown in the following screenshot:
    Getting ready

    Blender Export FBX configuration for use with Visual Studio graphics content pipeline

  6. Enter a filename and click on Export FBX.

The completed project for this recipe can be found in the code bundle of this chapter, Ch03_03LoadMesh, downloadable from Packt's website.

How to do it…

We will begin by adding the additional build targets and then including the FBX model and checking that it correctly compiles.

  1. First we need to install the additional build targets in to our project file. Unload your project by right-clicking on the project in the Solution Explorer and selecting Unload Project.
  2. Next, right-click on the project again and select Edit ProjectName.csproj.
  3. We need to insert the following code into the project file just before the closing </Project> tag. Here we are assuming that the additional build target files are located in the directory above this project.
    <Import Project="..MeshContentTask.targets" />
  4. Right-click on the project and select Reload Project.
  5. Now, add the Male_base_mesh.fbx file into the project directory and include it in the project.
  6. Select the FBX file within the Solution Explorer and then within the Build Action in the Properties window, select MeshContentTask.
  7. Build the project (F6) and confirm that in the build output directory, there is the compiled mesh file: 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.

Note

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.

Mesh Renderer

Now that we have our compiled mesh, we need to create a mesh renderer.

  1. Follow the steps given in the Creating a Direct3D renderer class recipe in Chapter 2, Rendering with Direct3D, to create our MeshRenderer class.
  2. Add the following private member fields and public property:
    // 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; }
  3. Next, we will create a single constructor that accepts a Visual Studio graphics content pipeline CMO mesh via a Common.Mesh instance.
    public MeshRenderer(Common.Mesh mesh)
    {
        this.mesh = mesh;
    }
  4. Override 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);
  5. We read each of the vertex buffers from the CMO file into a new buffer.
    // 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();
    }
  6. Next we load each of the index buffers.
    // 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();
    }
  7. And lastly, create Shader Resource Views (SRVs) for each of the textures and a default sampler state.
    // 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
    }));
  8. We can now implement the 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
        }
    }
  9. We update the material buffer and assign any textures if 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);
  10. To render each submesh, we set the vertex and index buffers in the Input Assembler (IA) and then draw the vertices.
    // 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.

  11. Within 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;

    Tip

    For Windows Store apps, use Common.Mesh.LoadFromFileAsync.

  12. And finally within the render loop, we update the 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();
  13. Compile and run (F5) the previous code, and we should now see something like the following screenshot (note that you may want to change the default pixel shader to Blinn-Phong first):
    Mesh Renderer

    Final output of the mesh renderer

How it works…

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.

Note

The MeshContentTask method supports compiling Autodesk FBX (.fbx), Collada (.dae), and Wavefront (.obj), to compile mesh objects (.CMO).

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.

There's more…

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,
}));

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.38.41