Tuesday, July 16, 2013

Hills Demo with SlimDX and C#

This time around, we are going to be porting the Hills Demo of Frank Luna’s Introduction to 3D Game Programming with Direct3D 11.0 from C++ to C# and SlimDX.  hillDemo

This will be very similar to the previous entry, where we drew a colored box, with a few changes:
  • We’ll start developing a class to generate mesh geometry (to replace the functionality that used to be present in previous versions of DirectX in the D3DX libraries).
  • We’ll be using pre-compiled effect files, rather than compiling them at runtime.
Most of our code will be unchanged from the previous demo application, A Colored Cube in DirectX 11.  Principally, we will be altering our BuildFX and BuildGeometryBuffers helper methods, called from our virtual Init() function.

BuildGeometryBuffers()

private void BuildGeometryBuffers() {
    var grid = GeometryGenerator.CreateGrid(160.0f, 160.0f, 50, 50);
    var vertices = new List<Vertex>();
    foreach (var vertex in grid.Vertices) {
        var pos = vertex.Position;
        pos.Y = GetHeight(pos.X, pos.Z);
        Color4 color;

        if (pos.Y < -10.0f) {
            color = new Color4(1.0f, 1.0f, 0.96f, 0.62f);
        } else if (pos.Y < 5.0f) {
            color = new Color4(1.0f, 0.48f, 0.77f, 0.46f);
        } else if (pos.Y < 12.0f) {
            color = new Color4(1.0f, 0.1f, 0.48f, 0.19f);
        } else if (pos.Y < 20.0f) {
            color = new Color4(1.0f, 0.45f, 0.39f, 0.34f);
        } else {
            color = new Color4(1,1,1,1);
        }
        vertices.Add(new Vertex(pos, color));
    }
    var vbd = new BufferDescription(Vertex.Stride*vertices.Count, ResourceUsage.Immutable, BindFlags.VertexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0);
    _vb = new Buffer(Device, new DataStream(vertices.ToArray(), false, false), vbd);

    var ibd = new BufferDescription(sizeof (int)*grid.Indices.Count, ResourceUsage.Immutable, BindFlags.IndexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0);
    _ib = new Buffer(Device, new DataStream(grid.Indices.ToArray(), false, false), ibd);
    _gridIndexCount = grid.Indices.Count;

}

First, we use our GeometryGenerator class (discussed later), to generate a flat grid 160.0 units square, with 50 vertices in the X axis and 50 vertices in the Z axis.  The GeometryGenerator class uses a slightly different vertex structure than we need in this case (it has added support for normals and texture coordinates, which will come in handy later, but are overkill for what we are doing now, which is drawing an unlit, untextured terrain), so we will not use this generated geometry directly.

Instead, we iterate over each vertex in the grid, and modify its Y axis coordinate using a height function which we will develop next.  Based on the height of the vertex, we assign the vertex color, to simulate different terrain types (a sandy color for the lowest areas, a light green for the lowlands, a darker green for the upper slopes of the hills, a darkish brown for the above-treeline areas, and white, for the snowy peaks).

We then create a vertex buffer using the generated position/color vertices, and create the index buffer using the indices supplied by the mesh that the GeometryGenerator hands back to us (these indices will be the same, even though we have modified the vertex data, as we have maintained the relative positions of the vertexes in the grid).

GeometryGenerator


At the moment, all we require this class to do is to create an XZ grid of vertices for us.  Later on, we’ll add additional functions to create arbitrary boxes, cylinders, and spheres for us, but we will cover that later when we need to make use of them.  The GeometryGenerator will hand us back the generated geometry in a structure that contains a list of vertices and a list of indices.  This structure is as follows:
public class MeshData {
    public List<Vertex> Vertices = new List<Vertex>();
    public List<int> Indices = new List<int>(); 
}

Note that the Vertex structure used here is not the same as the vertex structure we will be using in our demo application.  This vertex structure supports some more advanced options, which we will use when we get into lighting, texturing, and other more advanced effects, like displacement mapping.  The vertex structure for the MeshData structure is as follows:
[StructLayout(LayoutKind.Sequential)]
public struct Vertex {
    public Vector3 Position { get; set; }
    public Vector3 Normal { get; set; }
    public Vector3 TangentU { get; set; }
    public Vector2 TexC { get; set; }
    public Vertex(Vector3 pos, Vector3 norm, Vector3 tan, Vector2 uv) : this() {
        Position = pos;
        Normal = norm;
        TangentU = tan;
        TexC = uv;
    }
}

Our CreateGrid function is fairly simple.  You will supply a width (X), a depth(Z) and the number of vertices in the X and Z axes, and the function will return a MeshData structure representing a flat grid centered on (0,0,0), with the vertex and index data populated.
public static MeshData CreateGrid(float width, float depth, int m, int n) {
    var ret = new MeshData();

    var halfWidth = width*0.5f;
    var halfDepth = width*0.5f;

    var dx = width/(n - 1);
    var dz = depth/(m - 1);

    var du = 1.0f/(n - 1);
    var dv = 1.0f/(m - 1);

    for (var i = 0; i < m; i++) {
        var z = halfDepth - i*dz;
        for (var j = 0; j < n; j++) {
            var x = -halfWidth + j*dx;
            ret.Vertices.Add(new Vertex(new Vector3(x, 0, z), new Vector3(0,1,0), new Vector3(1, 0, 0), new Vector2(j*du, i*dv)));
        }
    }
    for (var i = 0; i < m-1; i++) {
        for (var j = 0; j < n-1; j++) {
            ret.Indices.Add(i*n+j);
            ret.Indices.Add(i*n+j+1);
            ret.Indices.Add((i+1)*n+j);

            ret.Indices.Add((i+1)*n+j);
            ret.Indices.Add(i*n+j+1);
            ret.Indices.Add((i+1)*n+j+1);
        }
    }

    return ret;
}

GetHeight(x,z)


Our height function returns a nice-looking sinusoidal heightmap, which gives us some interesting peaks and valleys that look a bit like a real terrain.  For a real implementation, you might want to look into Perlin Noise, which, with some tweaking, will give some very visually-pleasing and reasonably realistic random heightmaps.  I would suggest taking a look at the implementation from Chapter 4 of Programming an RTS Game with Direct3D, by Carl Granberg.
private float GetHeight(float x, float z) {
    return 0.3f*(z*MathF.Sin(0.1f*x) + x*MathF.Cos(0.1f*z));
}

BuildFX()


As opposed to the previous demo, where we loaded the raw text .fx file and compiled the shaders at runtime, this time, we are going to compile the shaders as part of the build process, and load the compiled shader bytecode instead.  This is slightly more complicated to do, however, it allows us to catch any compilation errors in our shader at build time, rather than run-time.  It also allows you to specify any debug-specific compilation options at build, which cleans up our shader loading function.
private void BuildFX() {
    ShaderBytecode compiledShader = null;
    try {
        compiledShader = new ShaderBytecode(new DataStream(File.ReadAllBytes("fx/color.fxo"), false, false));
        _fx = new Effect(Device, compiledShader);
    } catch (Exception ex) {
        MessageBox.Show(ex.Message);
        return;
    } finally {
        Util.ReleaseCom(compiledShader);
    }

    _tech = _fx.GetTechniqueByName("ColorTech");
    _fxWVP = _fx.GetVariableByName("gWorldViewProj").AsMatrix();
}

Note that we are loading the compiled shader code as a byte array, creating a DataStream (a SlimDX wrapper around byte arrays) and building the ShaderByteCode object directly from that data.  Otherwise, our FX setup code is exactly as before.

Compiling the shader during the build process


NOTE: For this to work correctly, you must have the DirectX SDK installed (Or the Windows 8 Platform SDK, as that is now included in Windows 8), and the C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Utilities\bin\x64 directory included in your PATH environment variable (your particular path may vary).

Open the project properties page for your demo application (typically ALT-ENTER in VS 2012).  Select the Build Events tab.

image

Edit the Pre-build event command line.  The pre- and post-build events normally start in your active configuration output folder, i.e. $(Project)\bin\Debug (or \bin\Release, if using a release build).  So first, we will need to change to the FX directory using cd.  Then, we will compile our shader file using fxc, the DirectX shader compiler.  The command switches that we are using here instruct the compiler to output the compiled shader to color.fxo, using the Effect Framework version 5.0, with all optimizations disabled, debugging information output, and to also output the annotated GPU shader assembly code listing (this will be color.cod).  These are some suitable options for a debug build.  To see a list of all the compiler options, open a cmd window, and enter fxc /?.

If there are any compilation errors in your shader input file, you will see these reported in the Visual Studio output when you build, and the build will abort.

As always, you may view and download the full code for this demo application from my GitHub repository, https://github.com/ericrrichards/dx11.git.











No comments :

Post a Comment