07 Rendering in Three Dimensions

Last time, we talked about coordinate systems and transforms between coordinate systems. With all that stuff firmly lodged in your brain, we can finally start to render objects in three dimensions.

The first thing we need to do is change the way we define our object. Since we want to let Direct3D do all the hard work of making things look three-dimensional, we’re going to stop defining our vertices in screen coordinates and start defining them in three-dimensional object space. Here’s our new definition for CreateVertexBuffer:

protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionColored.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // What type of vertices 3, // How many device, // The device 0, // Default usage CustomVertex.PositionColored.Format, // Vertex format Pool.Default); // Default pooling CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( 0, 1, 0, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0, 0, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0, 0, Color.Blue.ToArgb()); buf.Unlock(); return buf; }

Most of it is still the same as before. There are a few differences, though. For one thing, we’re no longer using TransformedColored vertices – now we’re using PositionColored vertices. The difference is that TransformedColored vertices are expressed in screen coordinates, and PositionColored vertices are expressed in object space. Note the values for x, y, and z we’ve specified for the triangle’s corners – they’re values like 0, 1, and 0.5, which are convenient numbers, but obviously don’t map directly to pixels!

There’s one other subtle difference. This time around, I’ve used an override of Lock that returns an array of vertices, rather than returning a GraphicsStream. I find this way of working more convenient than messing with GraphicsStream, so I thought I’d show it to you.

OK. We’ve defined our triangle’s vertices in object space, but we still need to get it on the screen. Well, that’s going to take only a few more lines of code – and we’ll animate it while we’re at it, so the triangle slowly rotates around it’s vertical axis.

Recall the Render method. When last we left it looked something like this:

protected void Render() { device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0); device.BeginScene(); device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); device.EndScene(); device.Present(); }

We need to augment it a little bit. Let’s make a couple of minor changes:

protected void Render() { device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); device.BeginScene(); SetupMatrices(); device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); device.EndScene(); device.Present(); }

Note that I’ve cleared the background to something other than black. Why will become obvious before the end of this article. The other change is to add a call to SetupMatrices. SetupMatrices looks something like this:

protected void SetupMatrices() { float angle = Environment.TickCount / 500.0F; device.Transform.World = Matrix.RotationY(angle); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 3.25F); }

Let’s break it down. First, we calculate an angle:

float angle = Environment.TickCount / 500.0F;

Environment.TickCount is a handy function that goes up once every millisecond. By dividing by 500, we’re setting an angle that changes by two radians per second, or roughly one revolution per three seconds, a decent rate. A rate which we use immediately in the next line:

device.Transform.World = Matrix.RotationY(angle);

OK. Remember that the world transform is the transform that helps our object locate itself relative to other objects. By using the Matrix.RotationY helper function, what we’ve done is specify a transform that takes the object and moves it into world coordinates by spinning it around the y axis by the angle we specify. Since this angle changes with time, our object will rotate as we render frames.

The next line sets up the View and Projection matrices:

device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 3.25F);

You’ll recall LookAtLH and PerspectiveFovLH from our discussion last time. We’ve fed these functions fairly standard values, to set up a camera located 3 units “back” from the screen, looking more or less at the origin, and we’ve defined “up” as being in the positive y direction. The projection matrix is a standard one – about the only thing you’d normally change here are the near and far clipping planes, like we talked about.

Technically, we don’t really need to set the View and Projection matrices every time. They’re not changing, after all. But it’s important to note that they – like the World transform - are global settings of the Device object. That is, if you don’t control all the code in your process, and someone else changes them, they’ll still be changed when you go to use the Device. This could result in your objects winding up in weird and unexpected locations. So I just set them every time out of paranoia.

And that’s it! If you run this code (complete listing below), you’ll see the rotating triangle. And you’ll notice two weird things about it: 1) that it’s black, not red, green, and blue, and 2) that it disappears periodically. Well, we’ve already talked about the reason for number two. Recall from our discussion about rendering with VertexBuffers that a process called culling goes on, where by default Direct3D doesn’t bother to render backwards-facing triangles, on the assumption that they’re part of a solid object and therefore would be obscured by the rest of the object whenever they are facing backwards. Of course, this assumption isn’t true here. Add this line of code

device.RenderState.CullMode = Cull.None;

somewhere to always be able to see the triangle.

The answer to the other question – why doesn’t the triangle get colored like it did before? – is actually quite simple: because our scene has no light. Without lighting, everything is black. This is why it’s a great idea to set the background to something other than black, because if you make a lighting mistake, at least you can still tell that you rendered something. We’ll talk how to set up lighting next time.

The Code

using System; using System.Drawing; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.Direct3D { public class Game : System.Windows.Forms.Form { static void Main() { Game app = new Game(); app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } //app.DisposeGraphics(); } private Device device; private VertexBuffer vertices; protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); vertices = CreateVertexBuffer(device); return true; } protected VertexBuffer CreateVertexBuffer(Device device) { device.VertexFormat = CustomVertex.PositionColored.Format; VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // What type of vertices 3, // How many device, // The device 0, // Default usage CustomVertex.PositionColored.Format, // Vertex format Pool.Default); // Default pooling CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( 0, 1, 0, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0, 0, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0, 0, Color.Blue.ToArgb()); buf.Unlock(); return buf; } protected void SetupMatrices() { float angle = Environment.TickCount / 500.0F; device.Transform.World = Matrix.RotationY(angle); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 5.0F); } protected void Render() { // Clear the back buffer device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); // Ready Direct3D to begin drawing device.BeginScene(); // Set the Matrices SetupMatrices(); // Draw the scene - 3D Rendering calls go here device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); // Indicate to Direct3D that we’re done drawing device.EndScene(); // Copy the back buffer to the display device.Present(); } } }