Cylinders, Spheres, and Boxes with Direct3D11 and SlimDX
NOTE: The framerate is atrocious because I am running on an old Intel G45/43 integrated chipset… On a real 3D chipset, like my GTX 560 Ti, you would have several thousand frames per second. As you can see, in addition to the Grid mesh that we implemented previously for the Hills Demo, we have added a box, spheres, and cylinders. We are also only keeping a single instance of each type of geometry in our vertex and index buffers, and drawing them with different world matrices to render multiple objects in our scene. Lastly, we are drawing in wireframe mode, which involves setting up some different render states from our previous examples. The bulk of the code for this example is identical to our previous installments, so I am only going to cover the new portions. If you have been following along with previous posts, you should have no trouble understanding the full code, at https://github.com/ericrrichards/dx11.git.
New Shape Types
We are going to extend our GeometryGenerator class to create the new types of geometry for us. We will need functions to create a six-sided box, spheres, and cylinders. We will start with the CreateBox function. This function returns us a 3D box, centered on the origin, with the specified dimensions. There is not much to say about this function; it is just a lot of manually setting up vertex positions and indices. Note that we have created a new constructor for our Vertex structure, that allows us to pass each of the components of the Position, Normal, tangent and Textures coordinate vectors in, rather than creating the vectors ourselves, which saves on space, at the cost of a little bit of readability.
public static MeshData CreateBox(float width, float height, float depth) { var ret = new MeshData(); var w2 = 0.5f*width; var h2 = 0.5f*height; var d2 = 0.5f*depth; // front ret.Vertices.Add(new Vertex(-w2, -h2, -d2, 0, 0, -1, 1, 0, 0, 0, 1)); ret.Vertices.Add(new Vertex(-w2, +h2, -d2, 0, 0, -1, 1, 0, 0, 0, 0)); ret.Vertices.Add(new Vertex(+w2, +h2, -d2, 0, 0, -1, 1, 0, 0, 1, 0)); ret.Vertices.Add(new Vertex(+w2, -h2, -d2, 0, 0, -1, 1, 0, 0, 1, 1)); // back ret.Vertices.Add(new Vertex(-w2, -h2, +d2, 0, 0, 1, -1, 0, 0, 1, 1)); ret.Vertices.Add(new Vertex(+w2, -h2, +d2, 0, 0, 1, -1, 0, 0, 0, 1)); ret.Vertices.Add(new Vertex(+w2, +h2, +d2, 0, 0, 1, -1, 0, 0, 0, 0)); ret.Vertices.Add(new Vertex(-w2, +h2, +d2, 0, 0, 1, -1, 0, 0, 1, 0)); // top ret.Vertices.Add(new Vertex(-w2, +h2, -d2, 0, 1, 0, 1, 0, 0, 0, 1)); ret.Vertices.Add(new Vertex(-w2, +h2, +d2, 0, 1, 0, 1, 0, 0, 0, 0)); ret.Vertices.Add(new Vertex(+w2, +h2, +d2, 0, 1, 0, 1, 0, 0, 1, 0)); ret.Vertices.Add(new Vertex(+w2, +h2, -d2, 0, 1, 0, 1, 0, 0, 1, 1)); // bottom ret.Vertices.Add(new Vertex(-w2, -h2, -d2, 0, -1, 0, -1, 0, 0, 1, 1)); ret.Vertices.Add(new Vertex(+w2, -h2, -d2, 0, -1, 0, -1, 0, 0, 0, 1)); ret.Vertices.Add(new Vertex(+w2, -h2, +d2, 0, -1, 0, -1, 0, 0, 0, 0)); ret.Vertices.Add(new Vertex(-w2, -h2, +d2, 0, -1, 0, -1, 0, 0, 1, 0)); // left ret.Vertices.Add(new Vertex(-w2, -h2, +d2, -1, 0, 0, 0, 0, -1, 0, 1)); ret.Vertices.Add(new Vertex(-w2, +h2, +d2, -1, 0, 0, 0, 0, -1, 0, 0)); ret.Vertices.Add(new Vertex(-w2, +h2, -d2, -1, 0, 0, 0, 0, -1, 1, 0)); ret.Vertices.Add(new Vertex(-w2, -h2, -d2, -1, 0, 0, 0, 0, -1, 1, 1)); // right ret.Vertices.Add(new Vertex(+w2, -h2, -d2, 1, 0, 0, 0, 0, 1, 0, 1)); ret.Vertices.Add(new Vertex(+w2, +h2, -d2, 1, 0, 0, 0, 0, 1, 0, 0)); ret.Vertices.Add(new Vertex(+w2, +h2, +d2, 1, 0, 0, 0, 0, 1, 1, 0)); ret.Vertices.Add(new Vertex(+w2, -h2, +d2, 1, 0, 0, 0, 0, 1, 1, 1)); ret.Indices.AddRange( new[]{ 0,1,2,0,2,3, 4,5,6,4,6,7, 8,9,10,8,10,11, 12,13,14,12,14,15, 16,17,18,16,18,19, 20,21,22,20,22,23 }); return ret; }
Next, we’ll create our function to create a sphere for us. We take advantage of the parametric equation for a sphere, which describes a spherical surface with two angles (normally phi and theta) and a radius. We tessellate our sphere by breaking it into slices and stacks, which are roughly analogous to longitude and latitude. The only tricky portion of this code comes in how we handle the poles. We do not want to duplicate the pole vertices, which makes it slightly more complicated to calculate the indices for the triangles at the top and bottom.
public static MeshData CreateSphere(float radius, int sliceCount, int stackCount) { var ret = new MeshData(); ret.Vertices.Add(new Vertex(0,radius,0, 0,1,0, 1,0,0, 0,0)); var phiStep = MathF.PI/stackCount; var thetaStep = 2.0f*MathF.PI/sliceCount; for (int i = 1; i <= stackCount-1; i++) { var phi = i*phiStep; for (int j = 0; j <= sliceCount; j++) { var theta = j*thetaStep; var p = new Vector3( (radius*MathF.Sin(phi)*MathF.Cos(theta)), (radius*MathF.Cos(phi)), (radius* MathF.Sin(phi)*MathF.Sin(theta)) ); var t = new Vector3(-radius*MathF.Sin(phi)*MathF.Sin(theta), 0, radius*MathF.Sin(phi)*MathF.Cos(theta)); t.Normalize(); var n = p; n.Normalize(); var uv = new Vector2(theta/(MathF.PI*2), phi / MathF.PI); ret.Vertices.Add(new Vertex(p, n, t, uv)); } } ret.Vertices.Add(new Vertex(0,-radius, 0, 0, -1, 0, 1, 0, 0, 0, 1)); for (int i = 1; i <= sliceCount; i++) { ret.Indices.Add(0); ret.Indices.Add(i+1); ret.Indices.Add(i); } var baseIndex = 1; var ringVertexCount = sliceCount + 1; for (int i = 0; i < stackCount-2; i++) { for (int j = 0; j < sliceCount; j++) { ret.Indices.Add(baseIndex + i*ringVertexCount + j); ret.Indices.Add(baseIndex + i*ringVertexCount + j+1); ret.Indices.Add(baseIndex + (i+1)*ringVertexCount + j); ret.Indices.Add(baseIndex + (i+1)*ringVertexCount + j); ret.Indices.Add(baseIndex + i*ringVertexCount + j+1); ret.Indices.Add(baseIndex + (i+1)*ringVertexCount + j + 1); } } var southPoleIndex = ret.Vertices.Count - 1; baseIndex = southPoleIndex - ringVertexCount; for (int i = 0; i < sliceCount; i++) { ret.Indices.Add(southPoleIndex); ret.Indices.Add(baseIndex+i); ret.Indices.Add(baseIndex+i+1); } return ret; }
Finally, we have our function to create a cylinder. We will again take advantage of parametric equations and tessellation using stacks and slices. We supply a top and bottom radius, which will allow us to use this same function to create cones as well as normal cylinders. First, we build the vertices and indices for the side walls of our cylinder, then we create the top and bottom cap discs using helper functions.
public static MeshData CreateCylinder(float bottomRadius, float topRadius, float height, int sliceCount, int stackCount) { var ret = new MeshData(); var stackHeight = height/stackCount; var radiusStep = (topRadius - bottomRadius)/stackCount; var ringCount = stackCount + 1; for (int i = 0; i < ringCount; i++) { var y = -0.5f*height + i*stackHeight; var r = bottomRadius + i*radiusStep; var dTheta = 2.0f*MathF.PI/sliceCount; for (int j = 0; j <= sliceCount; j++) { var c = MathF.Cos(j*dTheta); var s = MathF.Sin(j*dTheta); var v = new Vector3(r*c, y, r*s); var uv = new Vector2((float)j/sliceCount, 1.0f - (float)i/stackCount); var t = new Vector3(-s, 0.0f, c); var dr = bottomRadius - topRadius; var bitangent = new Vector3(dr*c, -height, dr*s); var n = Vector3.Cross(t, bitangent); n.Normalize(); ret.Vertices.Add(new Vertex(v, n, t, uv)); } } var ringVertexCount = sliceCount + 1; for (int i = 0; i < stackCount; i++) { for (int j = 0; j < sliceCount; j++) { ret.Indices.Add(i*ringVertexCount + j); ret.Indices.Add((i+1)*ringVertexCount + j); ret.Indices.Add((i+1)*ringVertexCount + j + 1); ret.Indices.Add(i*ringVertexCount + j); ret.Indices.Add((i+1)*ringVertexCount + j + 1); ret.Indices.Add(i*ringVertexCount + j + 1); } } BuildCylinderTopCap(topRadius, height, sliceCount, ref ret); BuildCylinderBottomCap(bottomRadius, height, sliceCount, ref ret); return ret; }
private static void BuildCylinderTopCap(float topRadius, float height, int sliceCount, ref MeshData ret) { var baseIndex = ret.Vertices.Count; var y = 0.5f*height; var dTheta = 2.0f*MathF.PI/sliceCount; for (int i = 0; i <= sliceCount; i++) { var x = topRadius*MathF.Cos(i*dTheta); var z = topRadius*MathF.Sin(i*dTheta); var u = x/height + 0.5f; var v = z/height + 0.5f; ret.Vertices.Add(new Vertex(x,y,z, 0, 1, 0, 1, 0,0, u, v)); } ret.Vertices.Add( new Vertex(0,y,0, 0, 1, 0, 1, 0, 0, 0.5f, 0.5f)); var centerIndex = ret.Vertices.Count - 1; for (int i = 0; i < sliceCount; i++) { ret.Indices.Add(centerIndex); ret.Indices.Add(baseIndex + i + 1); ret.Indices.Add(baseIndex + i); } }
private static void BuildCylinderBottomCap(float bottomRadius, float height, int sliceCount, ref MeshData ret) { var baseIndex = ret.Vertices.Count; var y = -0.5f * height; var dTheta = 2.0f * MathF.PI / sliceCount; for (int i = 0; i <= sliceCount; i++) { var x = bottomRadius * MathF.Cos(i * dTheta); var z = bottomRadius * MathF.Sin(i * dTheta); var u = x / height + 0.5f; var v = z / height + 0.5f; ret.Vertices.Add(new Vertex(x, y, z, 0, -1, 0, 1, 0, 0, u, v)); } ret.Vertices.Add(new Vertex(0, y, 0, 0, -1, 0, 1, 0, 0, 0.5f, 0.5f)); var centerIndex = ret.Vertices.Count - 1; for (int i = 0; i < sliceCount; i++) { ret.Indices.Add(centerIndex); ret.Indices.Add(baseIndex + i ); ret.Indices.Add(baseIndex + i + 1); } }
That takes care of our new geometry.
Drawing in WireFrame
Since we want to draw in wireframe, we need to instruct the graphics device to do so. We do this by creating the appropriate RasterizerState in our Init function. The important part here is FillMode = FillMode.Wireframe. The default fill mode is FillMode.Solid. We also set the rasterizer to ignore backward facing triangles, as that would clutter up our view.
public override bool Init() { if (!base.Init()) { return false; } BuildGeometryBuffers(); BuildFX(); BuildVertexLayout(); var wireFrameDesc = new RasterizerStateDescription { FillMode = FillMode.Wireframe, CullMode = CullMode.Back, IsFrontCounterclockwise = false, IsDepthClipEnabled = true }; _wireframeRS = RasterizerState.FromDescription(Device, wireFrameDesc); return true; }
Instancing
We do not want to have a seperate set of geometry for each object in our scene, as that would unnecessarily hog memory. Since we are only modifying the sizes and positions of the objects when we draw them, we can store a single set of geometry for each object type (box, grid, sphere, cylinder), and use different world matrices to draw them in the correct positions. We create these matrices in the constructor for our ShapeDemo class.
public ShapesDemo(IntPtr hInstance) : base(hInstance) { // other setup... _gridWorld = Matrix.Identity; _boxWorld = Matrix.Scaling(2.0f, 1.0f, 2.0f)*Matrix.Translation(0, 0.5f, 0); _centerSphere = Matrix.Scaling(2.0f, 2.0f, 2.0f)*Matrix.Translation(0, 2, 0); for (int i = 0; i < 5; ++i) { _cylWorld[i*2] = Matrix.Translation(-5.0f, 1.5f, -10.0f + i*5.0f); _cylWorld[i*2 + 1] = Matrix.Translation(5.0f, 1.5f, -10.0f + i*5.0f); _sphereWorld[i*2] = Matrix.Translation(-5.0f, 3.5f, -10.0f + i*5.0f); _sphereWorld[i*2 + 1] = Matrix.Translation(5.0f, 3.5f, -10.0f + i*5.0f); } }
This will allow us to draw our large central sphere resting upon the box, and the two rows of columns with spheres topping them. Next, we need to actually create our geometry and assign it to our vertex and index buffers. Rather than use separate buffers for each object, we lump them all together into a single vertex/index buffer, and store the offsets so that we can then draw each individual object correctly. This saves us the overhead of resetting the vertex and index buffers between draw calls.
private void BuildGeometryBuffers() { var box = GeometryGenerator.CreateBox(1.0f, 1.0f, 1.0f); var grid = GeometryGenerator.CreateGrid(20.0f, 30.0f, 60, 40); var sphere = GeometryGenerator.CreateSphere(0.5f, 20, 20); var cylinder = GeometryGenerator.CreateCylinder(0.5f, 0.3f, 3.0f, 20, 20); _boxVertexOffset = 0; _gridVertexOffset = box.Vertices.Count; _sphereVertexOffset = _gridVertexOffset + grid.Vertices.Count; _cylinderVertexOffset = _sphereVertexOffset + sphere.Vertices.Count; _boxIndexCount = box.Indices.Count; _gridIndexCount = grid.Indices.Count; _sphereIndexCount = sphere.Indices.Count; _cylinderIndexCount = cylinder.Indices.Count; _boxIndexOffset = 0; _gridIndexOffset = _boxIndexCount; _sphereIndexOffset = _gridIndexOffset + _gridIndexCount; _cylinderIndexOffset = _sphereIndexOffset + _sphereIndexCount; var totalVertexCount = box.Vertices.Count + grid.Vertices.Count + sphere.Vertices.Count + cylinder.Vertices.Count; var totalIndexCount = _boxIndexCount + _gridIndexCount + _sphereIndexCount + _cylinderIndexCount; var vs = new List<Vertex>(); foreach (var vertex in box.Vertices) { vs.Add(new Vertex(vertex.Position, Color.Black)); } foreach (var v in grid.Vertices) { vs.Add(new Vertex(v.Position, Color.Black)); } foreach (var v in sphere.Vertices) { vs.Add(new Vertex(v.Position, Color.Black)); } foreach (var v in cylinder.Vertices) { vs.Add(new Vertex(v.Position, Color.Black)); } var vbd = new BufferDescription(Vertex.Stride*totalVertexCount, ResourceUsage.Immutable, BindFlags.VertexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); _vb = new Buffer(Device, new DataStream(vs.ToArray(), false, false), vbd); var indices = new List<int>(); indices.AddRange(box.Indices); indices.AddRange(grid.Indices); indices.AddRange(sphere.Indices); indices.AddRange(cylinder.Indices); var ibd = new BufferDescription(sizeof (int)*totalIndexCount, ResourceUsage.Immutable, BindFlags.IndexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); _ib = new Buffer(Device, new DataStream(indices.ToArray(), false, false), ibd); }
Finally, we draw the objects. Note that we must set the proper world matrix for each object, and then call _tech.GetPassByIndex(p).Apply(ImmediateContext) in order to upload the changed matrix to the GPU before drawing. When we draw, we pass in the indexes to the index and vertex buffers, in order to draw just the subset of geometry that we need, rather than the entire buffer, as we did previously.
public override void DrawScene() { base.DrawScene(); ImmediateContext.ClearRenderTargetView(RenderTargetView, Color.LightSteelBlue); ImmediateContext.ClearDepthStencilView(DepthStencilView, DepthStencilClearFlags.Depth|DepthStencilClearFlags.Stencil, 1.0f, 0); ImmediateContext.InputAssembler.InputLayout = _inputLayout; ImmediateContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; ImmediateContext.Rasterizer.State = _wireframeRS; ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_vb, Vertex.Stride, 0)); ImmediateContext.InputAssembler.SetIndexBuffer(_ib, Format.R32_UInt, 0); var viewProj = _view*_proj; var techDesc = _tech.Description; for (int p = 0; p < techDesc.PassCount; p++) { _fxWVP.SetMatrix(_gridWorld*viewProj); _tech.GetPassByIndex(p).Apply(ImmediateContext); ImmediateContext.DrawIndexed(_gridIndexCount, _gridIndexOffset, _gridVertexOffset); _fxWVP.SetMatrix(_boxWorld*viewProj); _tech.GetPassByIndex(p).Apply(ImmediateContext); ImmediateContext.DrawIndexed(_boxIndexCount, _boxIndexOffset, _boxVertexOffset); _fxWVP.SetMatrix(_centerSphere*viewProj); _tech.GetPassByIndex(p).Apply(ImmediateContext); ImmediateContext.DrawIndexed(_sphereIndexCount, _sphereIndexOffset, _sphereVertexOffset); foreach (var matrix in _cylWorld) { _fxWVP.SetMatrix(matrix*viewProj); _tech.GetPassByIndex(p).Apply(ImmediateContext); ImmediateContext.DrawIndexed(_cylinderIndexCount, _cylinderIndexOffset, _cylinderVertexOffset); } foreach (var matrix in _sphereWorld) { _fxWVP.SetMatrix(matrix*viewProj); _tech.GetPassByIndex(p).Apply(ImmediateContext); ImmediateContext.DrawIndexed(_sphereIndexCount, _sphereIndexOffset, _sphereVertexOffset); } } SwapChain.Present(0, PresentFlags.None); }
Next time, we will focus on loading a pre-defined 3D model from a simple text file format and rendering that to the screen.