A Colored Cube in DirectX 11 and SlimDX
Now that we have the demo framework constructed, we can move on to a more interesting example. This time, we are going to use the demo framework that we just developed to render a colored cube in DirectX 11, using a simple vertex and pixel shader. This example corresponds with the BoxDemo from Chapter 6 of Frank Luna’s Introduction to Game Programming with Direct3D 11.0 .
I’m going to spare you the theory, and assume you have some knowledge of Vertex and Index Buffers, and the Effect Framework, and jump right into the code. First, we’ll need to define our Vertex structure, to represent the points that make up our box. Each point will have a position in 3D space and a color. We’ll be using the default Gourad shading without lighting to interpolate the color of the pixels of the triangles of our box. Here is our Vertex structure:
[StructLayout(LayoutKind.Sequential)] struct Vertex { public Vector3 Pos; public Color4 Color; public Vertex(Vector3 pos, Color color) { Pos = pos; Color = color; } public const int Stride = 28; }
Note that we are specifying LayoutKind.Sequential for this structure. From the MSDN documentation: http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.layoutkind.aspx
The members of the object are laid out sequentially, in the order in which they appear when exported to unmanaged memory. The members are laid out according to the packing specified in StructLayoutAttribute.Pack , and can be noncontiguous.
This may not be strictly necessary, but since we are submitting this structure to Direct3D through the SlimDX wrapper, it doesn’t hurt to specify this attribute, to insure that the structure is marshalled correctly from our .NET code to the underlying C++ Direct3D API. We also include the size, in bytes, of the Vertex structure in the constant Stride (floats are 4 bytes). Unlike C++, the C# sizeof operator will not operate on structs, and for setting up our VertexBuffer, we will need to know the size of our Vertex data. Now we can move on to the implementation of our colored box demo. We will be subclassing the D3DApp class that we created previously. Class definition, with method implementations omitted:
public class BoxApp : D3DApp { private Buffer _boxVB; private Buffer _boxIB; private Effect _fx; private EffectTechnique _tech; private EffectMatrixVariable _fxWVP; private InputLayout _inputLayout; // Matrices private Matrix _world; private Matrix _view; private Matrix _proj; // Camera variables private float _theta; private float _phi; private float _radius; private Point _lastMousePos; private bool _disposed; public BoxApp(IntPtr hInstance) : base(hInstance) protected override void Dispose(bool disposing) public override bool Init() public override void OnResize() public override void UpdateScene(float dt) public override void DrawScene() protected override void OnMouseDown(object sender, MouseEventArgs mouseEventArgs) protected override void OnMouseUp(object sender, MouseEventArgs e) protected override void OnMouseMove(object sender, MouseEventArgs e) private void BuildGeometryBuffers() private void BuildFX() private void BuildVertexLayout() }
- BoxApp Constructor – Here, we call the base constructor and trivially initialize our member variables to default values.
- Dispose – Here, we release our vertex and index buffers, our effect, and the input layout description for our Vertex structure.
-
protected override void Dispose(bool disposing) { if (!_disposed) { if (disposing) { Util.ReleaseCom(_boxVB); Util.ReleaseCom(_boxIB); Util.ReleaseCom(_fx); Util.ReleaseCom(_inputLayout); } _disposed = true; } base.Dispose(disposing); }
- Init() – Here, we call the base class Init method, to initialize DirectX and create the application window. We then call our helper functions to create our box geometry, create our effect, and construct the Vertex layout description.
-
public override bool Init() { if (!base.Init()) { return false; } BuildGeometryBuffers(); BuildFX(); BuildVertexLayout(); return true; }
- OnResize – Besides the base class resizing functionality, we also need to update our projection matrix, as the screen aspect ratio may have changed.
-
public override void OnResize() { base.OnResize(); // Recalculate perspective matrix _proj = Matrix.PerspectiveFovLH(0.25f * MathF.PI, AspectRatio, 1.0f, 1000.0f); }
NOTE: MathF is a helper utility class, which wraps some of the System.Math methods to return float values, rather than doubles, to avoid littering our code with explicit casts.
- UpdateScene – Here, we recalculate our view matrix to reflect any changes in the positioning of our virtual camera. Our camera is setup to orbit our colored box, so we can see it from all sides
-
public override void UpdateScene(float dt) { base.UpdateScene(dt); // Get camera position from polar coords var x = _radius * MathF.Sin(_phi) * MathF.Cos(_theta); var z = _radius * MathF.Sin(_phi) * MathF.Sin(_theta); var y = _radius * MathF.Cos(_phi); // Build the view matrix var pos = new Vector3(x, y, z); var target = new Vector3(0); var up = new Vector3(0, 1, 0); _view = Matrix.LookAtLH(pos, target, up); }
- DrawScene – Here, we render our box. First, we clear the screen to a default color, and clear the depth/stencil buffer to default values. Next, we tell the Direct3D device the format for our Vertex structure, and inform the device that our data is laid out as a TriangleList. We then activate our vertex and index buffers. Next we build our combined world-view-projection matrix, and upload the result to our Effect shaders. Finally, we activate our shader, and draw the box (using the vertex and index buffers that we set earlier), and tell Direct3D to present the backbuffer.
-
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.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_boxVB, Vertex.Stride, 0)); ImmediateContext.InputAssembler.SetIndexBuffer(_boxIB, Format.R32_UInt, 0); var wvp = _world * _view * _proj; _fxWVP.SetMatrix(wvp); for (int p = 0; p < _tech.Description.PassCount; p++) { _tech.GetPassByIndex(p).Apply(ImmediateContext); ImmediateContext.DrawIndexed(36, 0, 0); } SwapChain.Present(0, PresentFlags.None); }
- BuildGeometryBuffers – Here we create our vertex and index buffers. Here is where our Vertex.Stride constant comes in handy, when we are creating the BufferDescription for our vertex buffer. Note that we specify ResourceUsage.Immutable, as our geometry will remain static throughout the lifetime of the application.
-
private void BuildGeometryBuffers() { var vertices = new[] { new Vertex(new Vector3(-1.0f, -1.0f, -1.0f), Color.White), new Vertex(new Vector3(-1, 1, -1), Color.Black), new Vertex(new Vector3(1,1,-1), Color.Red ), new Vertex( new Vector3(1,-1,-1), Color.Green ), new Vertex(new Vector3(-1,-1,1),Color.Blue ), new Vertex(new Vector3(-1,1,1), Color.Yellow ), new Vertex(new Vector3(1,1,1), Color.Cyan ), new Vertex(new Vector3(1,-1,1),Color.Magenta ) }; var vbd = new BufferDescription( Vertex.Stride*vertices.Length, ResourceUsage.Immutable, BindFlags.VertexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0 ); _boxVB = new Buffer(Device, new DataStream(vertices, true, false), vbd); var indices = new uint[] { // front 0,1,2, 0,2,3, // back 4,6,5, 4,7,6, // left 4,5,1, 4,1,0, // right 3,2,6, 3,6,7, //top 1,5,6, 1,6,2, // bottom 4,0,3, 4,3,7 }; var ibd = new BufferDescription( sizeof (uint)*indices.Length, ResourceUsage.Immutable, BindFlags.IndexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0 ); _boxIB = new Buffer(Device, new DataStream(indices, false, false), ibd); }
- BuildFX –Here, we build our Effect shader. First, we need to load and compile our shader file from disk, using ShaderBytecode.CompileFromFile. If there was a compilation error in our shader, we would see a message box pop with the error message. Next, we create the effect, specifying our device and the compiled shader code. After we have created the effect, we no longer need the compiled bytecode, so we release it. Additionally, we grab handles to our effect technique, and the shader variable for the combined world-view-projection matrix.
-
private void BuildFX() { var shaderFlags = ShaderFlags.None; #if DEBUG shaderFlags |= ShaderFlags.Debug; shaderFlags |= ShaderFlags.SkipOptimization; #endif string errors = null; ShaderBytecode compiledShader = null; try { compiledShader = ShaderBytecode.CompileFromFile( "FX/color.fx", null, "fx_5_0", shaderFlags, EffectFlags.None, null, null, out errors); _fx = new Effect(Device, compiledShader); } catch (Exception ex) { if (!string.IsNullOrEmpty(errors)) { MessageBox.Show(errors); } MessageBox.Show(ex.Message); return; } finally { Util.ReleaseCom(compiledShader); } _tech = _fx.GetTechniqueByName("ColorTech"); _fxWVP = _fx.GetVariableByName("gWorldViewProj").AsMatrix(); }
- BuildVertexLayout – We need to create an InputLayout structure to inform the Direct3D device how to map our Vertex data to the types in our shader code. DirectX11 does this based on the HLSL semantics applied to the inputs for our vertex shader function. Because of this, we need to create the Effect shader prior to creating the InputLayout, which is different from the process I have previously used in DX 9.
-
private void BuildVertexLayout() { var vertexDesc = new[] { new InputElement("POSITION", 0, Format.R32G32B32_Float, 0, 0, InputClassification.PerVertexData, 0), new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 12, 0, InputClassification.PerVertexData, 0) }; Debug.Assert(_tech != null); var passDesc = _tech.GetPassByIndex(0).Description; _inputLayout = new InputLayout(Device, passDesc.Signature, vertexDesc); }
- OnMouseDown, OnMouseUp, OnMouseMove – These functions implement the camera controls. You may orbit the camera by clicking with the left mouse button and dragging, or zoom in and out by dragging with the right mouse button.
Driver Program
This is almost identical to our previous example program, merely swapping in our new derived class.
class Program { static void Main(string[] args) { Configuration.EnableObjectTracking = true; var app = new BoxApp(Process.GetCurrentProcess().Handle); if (!app.Init()) { return; } app.Run(); } }
If you attempt to run this application now, you would receive an error, because we haven’t yet written our shader code. We will need to create a new text file, called color.fx, in a folder FX, and write our shader there. This is an extremely simple shader; all we do is multiply the incoming vertex position with our combined world-view-projection matrix, and pass through the Vertex color. You’ll notice that I am using vertex and pixel shaders version 4.0 – this is because I am currently working on a machine with an older graphics card that does not support the full DX 11 feature set. If you have a DX11 compliant card, you can use version 5.0 (vs_5_0 & ps_5_0). Also, note that the VertexIn structure has semantics on its elements that match the names we used when creating our InputLayout object – this enables Direct3D to correctly match our Vertex data to the shader inputs.
cbuffer cbPerObject { float4x4 gWorldViewProj; } struct VertexIn { float3 PosL : POSITION; float4 Color : COLOR; }; struct VertexOut { float4 PosH : SV_POSITION; float4 Color: COLOR; }; VertexOut VS(VertexIn vin){ VertexOut vout; vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj); vout.Color = vin.Color; return vout; } float4 PS(VertexOut pin) :SV_Target { return pin.Color; } technique11 ColorTech { pass P0{ SetVertexShader( CompileShader( vs_4_0, VS())); SetGeometryShader(NULL); SetPixelShader(CompileShader( ps_4_0, PS())); } }
As always, you may download the full code for this example from my GitHub repository https://github.com/ericrrichards/dx11.git