Friday, July 19, 2013 Eric Richards

Cylinders, Spheres, and Boxes with Direct3D11 and SlimDX

This time up, we are going to add some additional shape types to our GeometryGenerator class, and look at how to redraw the same geometry at different locations and scales in our scene.  This example corresponds to the ShapesDemo from Frank Luna’s Introduction to 3D Game Programming with Direct3D 11.0. shapes

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;
}
box 

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;
}
sphere 

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

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.


Bookshelf

I have way too many programming and programming-related books. Here are some of my favorites.