Last time we talked about how to initialize Direct3D – how to create a Device object. We never got to any actual rendering code, though, which is the fun part. In this installment, we’ll talk about the basic rendering process, including how to do the absolute basics of drawing a 3D scene: clearing the screen.
Recall the game loop. Here’s the code:
static void Main() { Game app = new Game(); app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } app.DisposeGraphics(); }
Right smack in the middle of the thing is a call to Render. This is the method that we’ll write that actually does the drawing of our 3D scene. Roughly speaking, it needs to do five things:
1. Clear the back buffer. 2. Ready Direct3D to begin drawing. 3. Draw the scene. 4. Indicate to Direct3D that we’re done drawing. 5. Copy the back buffer to the display.
We’re going to talk about everything except step 3. The actual drawing of the shapes that make up the scene requires understanding things like VertexBuffers and coordinate systems, so we’ll save that for an upcoming discussion. But the other four steps are crucial to getting rendering working, though. Here’s the code that implements them:
protected void Render() { // Clear the back buffer device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0); // Ready Direct3D to begin drawing device.BeginScene(); // Draw the scene - 3D Rendering calls go here // Indicate to Direct3D that we’re done drawing device.EndScene(); // Copy the back buffer to the display device.Present(); }
The trend in recent years has been towards more and more abstraction in programming: the CLR even lets us ignore to a large degree what operating system we’re on. But when working with Direct3D, it’s good not to forget that it’s all about the hardware; even with today’s gigantic CPU power, good-looking 3D graphics are still enormously computationally intensive, and we have to pay attention to what we’re doing if we want to get reasonable results.
For this reason, Direct3D gives us the capability to make use of back buffers. Simply put, this is a region of memory that we draw to, instead of drawing directly to the screen. This keeps us from sending lots and lots of information back and forth across the AGP or PCI bus to the graphics card. Rather, we simply draw to memory, and then blast the whole frame over to the card all at once. Much more efficient.
Of course, before we draw on the back buffer, it’s not a bad idea to clear it, so we don’t have an unwanted junk in our scene. The call to do that looks like this:
device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0);
Note that the call is against the Device object that we created during InitializeGraphics.
The first argument to the call tells Direct3D what it we’re clearing. There are several options, but for our simple example we’re interested in clearing the back buffer, so we pass ClearFlags.Target to indicate we’re clearing the target of our rendering.
The second parameter tells Direct3D what color to clear the surface to. Note that we’re actually using a System.Drawing.Color enumerated value here – one of the nice things that Managed DirectX gives us is the ability to work with familiar types, although it’s not totally consistent about this: there are still places where it makes us use integers to indicate colors.
The last two arguments have to do with advanced topics that we’ll talk about later. Setting them to 1.0F and 0 will work for most scenarios.
We need to let Direct3D know that we’re about to begin drawing so it can do some internal bookkeeping. This is very easy – it only takes a single line of code:
device.BeginScene();
If you’re beginning to notice that Direct3D has an awful lot of methods on the Device object, you’re right. This sort of “global variable” type of programming caught me off guard at first – I kept looking for calls like Scene.Begin() rather than device.BeginScene() – but I got used to it eventually.
This is the bit where we actually draw shapes into the scene: rockets, nail guns, spheres, pyramids, whatever. Again, we’re going to skip this part because there are a number of other things we need to talk about first, but don’t worry – we’ll get there soon enough.
This one is easy. We just match the call to BeginScene with a call to EndScene, like so:
device.EndScene();
Just like BeginScene, this tells Direct3D to update some internal structures.
This is the payoff – this is the only step that actually affects anything on the screen. By calling
device.Present();
we’re telling Direct3D to take the back buffer and eventually get it to the graphics card, where it will be shown on the monitor. I say “eventually” because it turns out that there are a number of flags we might want to set that would delay the back buffer from becoming immediately visible, but improve the quality of our animation. For example, it might make sense to wait until the monitor’s electron gun is transitioning from the lower right corner back up to the upper left. This period is known a the vertical retrace, and copying stuff into the graphics card while the monitor is doing this can avoid several weird visual artifacts.
For the purposes of our (so far) simple program, though, we can just think of Present as being the bit that throws our scene up onto the display.
And that’s it – a real Direct3D app! Admittedly, it currently just blanks the window to black, but it’s wickedly efficient at doing so!
At the bottom of this article I’ve put the complete code for what we’ve talked about so far. Next time, we’ll talk about VertexBuffers, which we’ll need in order to actually draw anything interesting on the screen.
using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.Direct3D { public class Game : System.Windows.Forms.Form { private Device device; static void Main() { Game app = new Game(); app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } app.DisposeGraphics(); } protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); return true; } protected void Render() { // Sets the surface to black everywhere device.Clear(ClearFlags.Target, Color.Black, 1.0F, 0); device.BeginScene(); // 3D Rendering calls go here device.EndScene(); device.Present(); } protected void DisposeGraphics() { device.Dispose(); } } }