Expanding our BasicModel Class
I had promised that we would move on to discussing shadows, using the shadow mapping technique. However, when I got back into the code I had written for that example, I realized that I was really sick of handling all of the geometry for our stock columns & skull scene. So I decided that, rather than manage all of the buffer creation and litter the example app with all of the buffer counts, offsets, materials and world transforms necessary to create our primitive meshes, I would take some time and extend the BasicModel class with some factory methods to create geometric models for us, and leverage the BasicModel class to encapsulate and manage all of that data. This cleans up the example code considerably, so that next time when we do look at shadow mapping, there will be a lot less noise to deal with.
The heavy lifting for these methods is already done; our GeometryGenerator class already does the work of generating the vertex and index data for these geometric meshes. All that we have left to do is massage that geometry into our BasicModel’s MeshGeometry structure, add a default material and textures, and create a Subset for the entire mesh. As the material and textures are public, we can safely initialize the model with a default material and null textures, since we can apply a different material or apply diffuse or normal maps to the model after it is created.
The full source for this example can be downloaded from my GitHub repository, at https://github.com/ericrrichards/dx11.git, under the ShapeModels project.
Refresher on GeometryGenerator
If you are new to this series of posts, or simply have forgotten, way back in July, when we had yet to even cover lighting, we created a utility class to generate mesh geometry for some common 3D solids. This class had functions to create a 3D boxes, spheres, cylinders and cones, and 2D grids. The definition, stripped of all the implementation, looked like this:
public class GeometryGenerator { public static MeshData CreateBox(float width, float height, float depth) public static MeshData CreateSphere(float radius, int sliceCount, int stackCount) public static MeshData CreateCylinder(float bottomRadius, float topRadius, float height, int sliceCount, int stackCount) public static MeshData CreateGrid(float width, float depth, int m, int n) }
The MeshData class here is simply a container for a list of vertices and a list of indices. The functions used the mathematical formulas for the solids to generate position, normal, texture and tangent coordinates, along with the indices to triangulate the meshes. Until now, we have been using this class to generate our geometry, and then we have had to handle creating vertex and index buffers for the geometry, so that we can submit them for rendering. Generally, this means that we also have to keep track of the number of indices, number of vertices, materials, textures, and world matrices for each mesh that we create in this way. Additionally, we have usually slammed all of our different mesh data into a single vertex and index buffer pair, so that we also have to keep track of the starting offsets for each object as well. Thus we end up with a bunch of member variables in our example programs to represent each object, like so:
// All for our central box mesh private Buffer _shapesVB; private Buffer _shapesIB; private ShaderResourceView _stoneTexSRV; private readonly Material _boxMat; private readonly Matrix _boxWorld; private int _boxVertexOffset; private int _boxIndexOffset; private int _boxIndexCount;
That’s not great, and I’m sick of copying all this junk, along with the code to create the buffers, and to set all the shader variables and render these objects. Our BasicModel class handles all of this stuff, so we might as well take advantage of it.
BuildFromMeshData()
All of the GeometryGenerator methods return us a MeshData object. The process that we will use to transform this MeshData into a BasicModel is the same, regardless of whether the mesh is a sphere, box or cone, so we will factor it out into a helper function, BuildFromMeshData().
private static BasicModel BuildFromMeshData(Device device, GeometryGenerator.MeshData mesh) { var ret = new BasicModel(); var subset = new MeshGeometry.Subset { FaceCount = mesh.Indices.Count / 3, FaceStart = 0, VertexCount = mesh.Vertices.Count, VertexStart = 0 }; ret._subsets.Add(subset); var max = new Vector3(float.MinValue); var min = new Vector3(float.MaxValue); foreach (var vertex in mesh.Vertices) { max = Vector3.Maximize(max, vertex.Position); min = Vector3.Minimize(min, vertex.Position); } ret.BoundingBox = new BoundingBox(min, max); ret._vertices.AddRange(mesh.Vertices.Select(v => new PosNormalTexTan(v.Position, v.Normal, v.TexC, v.TangentU)).ToList()); ret._indices.AddRange(mesh.Indices.Select(i => (short)i)); ret.Materials.Add(new Material { Ambient = Color.Gray, Diffuse = Color.White, Specular = new Color4(16, 1, 1, 1) }); ret.DiffuseMapSRV.Add(null); ret.NormalMapSRV.Add(null); ret._modelMesh.SetSubsetTable(ret._subsets); ret._modelMesh.SetVertices(device, ret._vertices); ret._modelMesh.SetIndices(device, ret._indices); return ret; }
Our first step is to create a Subset for the entire mesh, and add it to the newly created Model. Next, we calculate the extents of the mesh geometry and construct a BoundingBox enclosing the mesh. After that, we transform the MeshData vertex and indices into the format used by the BasicModel, and set the respective collections of the Model.
Next, we add a default, white material, and null diffuse and normal maps. Remember, these properties are public, so we can assign different materials and textures later if we need to (and, in fact, if you are using our NormalMap effect, you must replace this null normal map, or else you get odd results…). Lastly, we initialize the Model’s MeshGeometry object, creating the subset table, vertex buffer and index buffer, and return the constructed BasicModel.
Factory Methods – CreateBox, CreateSphere, CreateCylinder, CreateGrid
With the BuildFromMeshData function factored out, our factory methods become very simple. All we need to do is call the appropriate GeometryGenerator function to create the mesh data, and then return the Model created by the helper function.
public static BasicModel CreateBox(Device device, float width, float height, float depth) { var box = GeometryGenerator.CreateBox(width, height, depth); return BuildFromMeshData(device, box); } public static BasicModel CreateSphere(Device device, float radius, int slices, int stacks) { var sphere = GeometryGenerator.CreateSphere(radius, slices, stacks); return BuildFromMeshData(device, sphere); } public static BasicModel CreateCylinder(Device device, float bottomRadius, float topRadius, float height, int sliceCount, int stackCount) { var cylinder = GeometryGenerator.CreateCylinder(bottomRadius, topRadius, height, sliceCount, stackCount); return BuildFromMeshData(device, cylinder); } public static BasicModel CreateGrid(Device device, float width, float depth, int xVerts, int zVerts) { var grid = GeometryGenerator.CreateGrid(width, depth, xVerts, zVerts); return BuildFromMeshData(device, grid); }
ShapesModelDemo
The advantage of using our new BasicModel factory methods is primarily in how much shorter and clearer our code becomes. Our initialization and drawing code is markedly shorter, and our demo class has much less state that it needs to handle directly.
Our Init() method for this demo is much more straightforward, even with the addition of non-default materials and textures:
public override bool Init() { if (!base.Init()) return false; Effects.InitAll(Device); InputLayouts.InitAll(Device); RenderStates.InitAll(Device); _texMgr = new TextureManager(); _texMgr.Init(Device); _gridModel = BasicModel.CreateGrid(Device, 20, 20, 40, 40); _gridModel.Materials[0] = new Material() { Diffuse = Color.SaddleBrown, Specular = new Color4(16, .9f, .9f, .9f)}; _gridModel.DiffuseMapSRV[0] = _texMgr.CreateTexture("Textures/floor.dds"); _gridModel.NormalMapSRV[0] = _texMgr.CreateTexture("textures/floor_nmap.dds"); _boxModel = BasicModel.CreateBox(Device, 1, 1, 1); _boxModel.Materials[0] = new Material() { Ambient = Color.Red, Diffuse = Color.Red, Specular = new Color4(64.0f, 1.0f, 1.0f, 1.0f)}; _boxModel.NormalMapSRV[0] = _texMgr.CreateTexture("Textures/bricks_nmap.dds"); _sphereModel = BasicModel.CreateSphere(Device, 1, 20, 20); _sphereModel.Materials[0] = new Material() { Ambient = Color.Blue, Diffuse = Color.Blue, Specular = new Color4(64.0f, 1.0f, 1.0f, 1.0f)}; _sphereModel.NormalMapSRV[0] = _texMgr.CreateTexture("Textures/stones_nmap.dds"); _cylinderModel = BasicModel.CreateCylinder(Device, 1, 1, 3, 20, 20); _cylinderModel.Materials[0] = new Material() { Ambient = Color.Green, Diffuse = Color.Green, Specular = new Color4(64.0f, 1.0f, 1.0f, 1.0f)}; _cylinderModel.NormalMapSRV[0] = _texMgr.CreateTexture("Textures/stones_nmap.dds"); _grid = new BasicModelInstance() { Model = _gridModel, TexTransform = Matrix.Scaling(10, 10, 1), World = Matrix.Identity }; _box = new BasicModelInstance() { Model = _boxModel, World = Matrix.Translation(-3, 1, 0) }; _sphere = new BasicModelInstance() { Model = _sphereModel, World = Matrix.Translation(0, 1, 0) }; _cylinder = new BasicModelInstance() { Model = _cylinderModel, World = Matrix.Translation(3, 1.5f, 0) }; return true; }
Likewise, our DrawScene() method nearly becomes trivial, when we are able to leverage the BasicModelInstance Draw() function.
public override void DrawScene() { ImmediateContext.ClearRenderTargetView(RenderTargetView, Color.Silver); ImmediateContext.ClearDepthStencilView(DepthStencilView, DepthStencilClearFlags.Depth | DepthStencilClearFlags.Stencil, 1.0f, 0); var viewProj = _camera.ViewProj; Effects.NormalMapFX.SetDirLights(_dirLights); Effects.NormalMapFX.SetEyePosW(_camera.Position); var floorTech = Effects.NormalMapFX.Light3TexTech; var activeTech = Effects.NormalMapFX.Light3Tech; ImmediateContext.InputAssembler.InputLayout = InputLayouts.PosNormalTexTan; ImmediateContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; if (Util.IsKeyDown(Keys.W)) { ImmediateContext.Rasterizer.State = RenderStates.WireframeRS; } for (int p = 0; p < activeTech.Description.PassCount; p++) { var pass = activeTech.GetPassByIndex(p); _box.Draw(ImmediateContext, pass, viewProj); _sphere.Draw(ImmediateContext, pass, viewProj); _cylinder.Draw(ImmediateContext, pass, viewProj); } for (int p = 0; p < floorTech.Description.PassCount; p++) { var pass = activeTech.GetPassByIndex(p); _grid.Draw(ImmediateContext, pass, viewProj); } SwapChain.Present(0, PresentFlags.None); ImmediateContext.Rasterizer.State = null; }
Next Time…
We really will move on to shadow mapping…