irect3D is Microsoft’s high-performance, three-dimensional graphics library. Using Direct3D, you can build amazing scenes containing hundreds or even thousands of three-dimensional shapes, three-dimensional charts and graphs, surfaces that change in real time, and even arcade-style action games. In fact, Direct3D is the graphical part of XNA (which believe it or not stands for “XNA’s Not Acronymed”), Microsoft’s game platform development environment for Windows and Xbox 360. You can find more information on XNA at the XNA Developer Center.
Direct3D is a huge topic, so this is the first in a series of articles. This article explains how to get started with Direct3D?how to download and install the Direct3D SDK, create a Direct3D device, and draw three-dimensional objects on the device. You’ll see how to color the objects explicitly and how to remove surfaces that should be hidden. Later articles will explain such topics as textures, shading, and game-related topics.
So if you’re ready to produce some remarkable three-dimensional graphics with amazing performance, grab some caffeine, pull up a high-speed Internet connection, and read on.
The Direct3D library is designed to let you display three-dimensional graphics relatively easily, and optimized to use your computer’s hardware where possible?so it gives pretty good performance and can display and animate fairly complicated scenes in real time.
Before you can draw three-dimensional objects, you need to understand how to define the data for those objects in three-dimensional space, and how Direct3D processes that data. (Strictly speaking, you can build a Direct3D device and display a blank window without understanding that stuff, but if you learn it now you can move quickly from creating the device to drawing something without any distractions. I’m going to start here.)
Direct3D manipulates information about objects in three dimensions. Each point has X, Y, and Z coordinates that determine its position in the data space. That space is called world coordinate space.
To understand how coordinates are oriented, imagine that your computer’s screen is a window into world coordinate space. As you look through the window, the X-axis increases to the right and the Y-axis increases upward. So far this is just how you would draw a graph in a mathematics class. In Direct3D, the Z-axis projects away from you into the monitor.
Direct3D uses a “left-handed coordinate system.” That means if you position your left hand so your fingers point along the X-axis (palm up with fingers pointed right), and then curl your fingers toward the Y-axis (upward), then your thumb points along the Z-axis as shown in Figure 1. (If you need to curl your fingers so they bend backwards, you have your hand upside down. Flip it over and you should be okay.)
|Figure 1. Left-handed Coordinates: Using left-handed coordinates, if you relax your hand and point your fingers along the X-axis, and then curl them toward the Y-axis, your thumb points roughly along the Z-axis.|
Keeping track of which directions the axes point can be important, particularly later when you learn how to draw scenes that contain hidden surfaces. It’s often helpful to sketch out the points you are trying to draw on paper before you start trying to plug in their coordinates.
Direct3D draws points, lines, and triangles. If you want to build something more complex, such as a pentagon or cube, you must build it out of more primitive objects. For example, you can make a square with two triangles that share an adjacent edge. As long as the two triangles that make up the square have the same color, they will merge seamlessly, so the user won’t see an edge between them. After you know how to build a square, you can use six squares to build a cube.
There are several ways you can specify an object’s color. For example, you can explicitly define the colors that a triangle should have at each of its corners. Direct3D will interpolate across the triangle’s surface to generate colors for the interior. Another option is to specify a texture map to make Direct3D map pixels in an image onto the triangle. A third way to specify color is with a lighting model. Here you give Direct3D information about the “material” that makes up a triangle and you define light sources in three-dimensional space. Direct3D then uses some calculations to figure out what color the object should be based on its relationships with the light sources and with the current viewing position (basically where your “eye” or “camera” is sitting in space looking at the object).
Assuming you know where your objects are and what colors they should be (don’t worry, I’ll provide more detail later), you should understand how Direct3D displays three-dimensional objects on a two-dimensional screen. To map the data from three to two dimensions, Direct3D uses three transformations.
The first transformation, called the world transformation, transforms the data in world coordinate space. The world transformation lets you modify the data before it is displayed. For example, a world transformation might rotate your objects around an axis or stretch the data in one or more directions. Of course, you can leave the data untransformed if you don’t want to make any last-minute changes to the way it will appear.
The second transformation, called the view transformation, adjusts the data for the particular viewing position that you want to use when displaying the data. You can think of the viewing position as a camera sitting in space looking at the objects. This transformation determines where the camera is and the direction in which it is looking. It also defines an “up” direction for the camera, because the camera might be oriented normally, rotated, or even upside down. In essence, this transformation flips your objects around so they are right-side up as determined by the camera’s position. The view transformation converts the data from world coordinates into camera coordinates.
The final transformation, called the projection transformation, maps the data from three-dimensional space into two-dimensional coordinates suitable for display on your monitor. Typically, this perspective projection makes objects closer to the camera appear larger than those farther away, just as they do in a real photograph.
Installing the SDK
Before you can do anything with Direct3D you need to install the DirectX SDK. Before you can even do that, you need to decide which version of DirectX you want to use.
The most recent major version is DirectX 10. Unfortunately, DirectX 10 was designed to support the graphical needs of Windows Vista, is supported only for Vista, and Microsoft has no plans to make it compatible with other previous operating systems. However, DirectX 10 (and Vista) should eventually provide the best performance on computers that have hardware support for DirectX 10, although I have yet to see any computers with DirectX 10 hardware in the wild.
DirectX 9, on the other hand, supports several operating systems, including Windows XP, Windows Server 2003, and Windows Vista.
|Author’s Note: I wrote the code for this article with DirectX 9 in Windows XP, but all of the code shown here should work with little or no change in both DirectX 9 and 10, so you probably can’t make a really bad choice no matter which version you choose.|
After you decide which version you want to use, go to Microsoft’s web site and download the SDK installation kit. You can either go to the Microsoft Download Center and search for “DirectX SDK,” or you can go to the DirectX Resource Center, and look for the latest download.
Even though DirectX 10 is available, Microsoft will continue to support DirectX 9 and in fact plans to release updated versions of DirectX 9. The current version is 9.21.1148 dated November 2007 and weighs in at around 47 MB.
Download the version you want to use, and run the executable install file. The kit should decompress itself and install with little effort.
Creating a Device
Older versions of DirectX were fairly hard to use, providing features only as API-like functions with somewhat confusing structures and data types. The latest versions of DirectX are much easier to use, providing managed code and classes that greatly simplify the confusion. Still, there are several non-obvious steps that you need to follow to get things off the ground.
This first sample program will create a single form that displays a blank background. The program is controlled by a Main method that continuously redraws the form as long as the form is loaded. You won’t see anything except the blank background, but this program sets up the tools you need to do something more interesting in the next section.
Start by creating a new Windows Forms application. I’m using Visual Basic 2005 for this article but similar steps should apply to other versions of Visual Basic or to C#.
After you create the new application, open the Project menu and select the Properties command at the bottom. On the Application tab, uncheck the “Enable application framework” box, and set the startup object to Sub Main. Next, go to the References tab, click the Add button, and add references to the Microsoft.DirectX and Microsoft.DirectX.Direct3D namespaces.
Open the Project menu again, select Add Module, and create the Main method shown in the following code:
Module SubMain ' Prepare Direct3D and run the event loop. Public Sub Main() ' Make the form. Dim frm As New RenderForm ' Initialize Direct3D. If frm.InitializeGraphics() Then frm.Show() ' While the form is valid, ' render the scene and process messages. Do While frm.Created frm.Render() Application.DoEvents() Loop End If End Sub End Module
|Author’s Note: In C#, you can add similar code to the main method in Program.cs.|
The Main method above starts by creating a new instance of the program’s form class, named RenderForm. That class provides two methods that the main program uses to produce Direct3D: InitializeGraphics and Render, described shortly.
After it creates the form, the Main method calls the form’s InitializeGraphics function to create the Direct3D device and prepares Direct3D for use.
If the call to InitializeGraphics succeeds, the program displays the form and then enters a loop. As long as the form exists (in other words, until the user closes it), the program repeatedly calls the form’s Render method to draw the form’s blank background. It also calls DoEvents so the program has time to process other events such as the user trying to close the form.
The program’s form class does most of the work. The following code shows its InitializeGraphics function:
' The Direct3D device. Private m_Device As Device ' Initialize the graphics device. Return True if successful. Public Function InitializeGraphics() As Boolean Dim params As New PresentParameters params.Windowed = True params.SwapEffect = SwapEffect.Discard ' Best: Hardware device and hardware vertex processing. Try m_Device = New Device(0, DeviceType.Hardware, Me, _ CreateFlags.HardwareVertexProcessing, params) Debug.WriteLine("Hardware, HardwareVertexProcessing") Catch End Try ' Good: Hardware device and software vertex processing. If m_Device Is Nothing Then Try m_Device = New Device(0, DeviceType.Hardware, Me, _ CreateFlags.SoftwareVertexProcessing, params) Debug.WriteLine("Hardware, SoftwareVertexProcessing") Catch End Try End If ' Adequate?: Software device and software vertex processing. If m_Device Is Nothing Then Try m_Device = New Device(0, DeviceType.Reference, Me, _ CreateFlags.SoftwareVertexProcessing, params) Debug.WriteLine("Reference, SoftwareVertexProcessing") Catch ex As Exception ' If we still can't make a device, give up. MessageBox.Show("Error initializing Direct3D" & _ vbCrLf & vbCrLf & ex.Message, _ "Direct3D Error", MessageBoxButtons.OK) Return False End Try End If ' We succeeded. Return True End Function
The form class starts by defining a Direct3D Device object. This object represents the Direct3D drawing surface and provides the methods that the program needs to draw.
The InitializeGraphics function tries to create the Direct3D device. It starts by creating a PresentParameters object to hold information about the type of presentation the program wants. In this example, the Direct3D device displays output in a window (as opposed to using the whole screen) and discards old buffered graphics whenever it displays a new scene.
Next InitializeGraphics tries to create the device. Note that the example tries to create three different devices, in decreasing order of performance. First it tries to create a hardware-assisted device to process the vertex data that determines where shapes are drawn. This type of device gives the best performance but works only if the computer’s hardware can support Direct3D.
If the program cannot create this device, it tries to make a hardware device that uses software to process vertex data. That device type isn’t as fast as the hardware-based device, but still gives good performance.
If creation of those device types fails, InitializeGraphics tries to create a reference device that emulates Direct3D functionality in software. This device has limited performance?in fact the performance may not be good enough for some applications. The software-emulation device isn’t really intended for commercial applications, but it’s better than nothing.
If the InitializeGraphics function cannot create any of these types of devices, it displays an error message and returns False to tell the main program that it failed.
The following code shows the structure of the RenderForm class’s simple Render method that does the actual drawing:
' Draw. Public Sub Render() ' Clear the back buffer. m_Device.Clear(ClearFlags.Target, Color.Black, 1, 0) ' Make a scene. m_Device.BeginScene() ' Draw stuff here... ' End the scene and display. m_Device.EndScene() m_Device.Present() End Sub
|Figure 2. Blank Screen: The d3dCreateDevice program displays a blank scene.|
The Render method starts by calling the device’s Clear method to clear the drawing surface. The first parameter indicates the type of drawing object that the method should clear. This example uses Clear to clear the drawing target. Other options for this parameter cause the method to clear the stencil buffer or z-buffer, neither of which are needed for this example. The final parameters to Clear give more information for use by stencils and the z-buffer.
Next, the Render method calls the device’s BeginScene method to tell the device that a new drawing is on the way. A more interesting example would then draw objects in three-dimensional space before calling EndScene to let the device know that the scene is complete. Finally the method calls Present to display the scene.
The sample program d3dCreateDevice uses this code to display a blank RenderForm. Figure 2 shows the unimpressive result: a form with a black background.
Drawing a Triangle
Now that you can prepare the Direct3D engine and create a device on which to draw, the next step is to create some data and draw it. To draw something, you need to supply the coordinates of the points you will use to Direct3D. You do that by creating a buffer to hold the coordinate data and then passing the buffer to Direct3D.
The d3dDrawTriangle sample program, which is available for download, is very similar to the previous example. It differs from program d3dCreateDevice in three ways.
First, its InitializeGraphics method calls the CreateVertexBuffer method to create the data buffer.
Second, the program’s Render method calls SetupMatrices to create the world, view, and projection transformations that Direct3D should use to display the graphics. The previous program didn’t need to do this because it didn’t draw anything, but program d3dDrawTriangle does draw something and needs to define these transformations.
Finally, the program’s Render method calls methods to tell Direct3D where the data is and how to draw it:
The following code shows how the program defines its data.
' Data variables. Private Const NUM_TRIANGLES As Integer = 1 Private Const NUM_POINTS As Integer = 3 * NUM_TRIANGLES ' The vertex buffer that holds drawing data. Private m_VertexBuffer As VertexBuffer = Nothing ' Initialize the graphics device. Return True if successful. Public Function InitializeGraphics() As Boolean ... Create a device as before ... ' Turn off D3D lighting because ' we set the vertex colors explicitly. m_Device.RenderState.Lighting = False ' Create the vertex data. CreateVertexBuffer() ' We succeeded. Return True End Function
The code starts with module-level constants that define the number of triangles that it will draw and the number of points those triangles will need. It also defines a module-level VertexBuffer object.
The InitializeGraphics function creates a Direct3D device as before. It then sets the device’s Lighting property to False to indicate that this program does not use a lighting model. Instead the data explicitly defines colors for the triangle’s corners. InitializeGraphics calls CreateVertexBuffer to create the data and then returns as before.
The following code shows how the CreateVertexBuffer method creates the buffer:
' Create a vertex buffer for the device. Public Sub CreateVertexBuffer() ' Create a buffer for 3 vertexes. m_VertexBuffer = New VertexBuffer( _ GetType(CustomVertex.PositionColored), _ NUM_POINTS, m_Device, 0, _ CustomVertex.PositionColored.Format, _ Pool.Default) ' Lock the vertex buffer. ' Lock returns an array of positionColored objects. Dim vertices As CustomVertex.PositionColored() = _ CType(m_VertexBuffer.Lock(0, 0), _ CustomVertex.PositionColored()) ' Make the vertexes. vertices(0).X = 1 vertices(0).Y = -1 vertices(0).Z = 0 vertices(0).Color = Color.Red.ToArgb vertices(1).X = -1 vertices(1).Y = -1 vertices(1).Z = 0 vertices(1).Color = Color.Blue.ToArgb vertices(2).X = 0 vertices(2).Y = 1 vertices(2).Z = 0 vertices(2).Color = Color.Green.ToArgb m_VertexBuffer.Unlock() End Sub
The code first creates a VertexBuffer object, passing its constructor a type indicating that the buffer should contain PositionColored objects. That type of object contains X, Y, and Z coordinates for each point, plus a color value. The method also passes the constructor the number of points it wants the buffer to hold.
CreateVertexBuffer then locks the buffer to gain access to the objects it contains and fills in the coordinates and colors for the triangle’s three corners. The method finishes by unlocking the buffer.
In this program, the Render method performs the transformations and displays the image defined by the vertices. Here’s the code:
' Draw. Public Sub Render() ' Clear the back buffer. m_Device.Clear(ClearFlags.Target, Color.Black, 1, 0) ' Make a scene. m_Device.BeginScene() ' Draw stuff here... ' Setup the world, view, and projection matrices. SetupMatrices() ' Set the device's data stream source (the vertex buffer). m_Device.SetStreamSource(0, m_VertexBuffer, 0) ' Tell the device the format of the vertices. m_Device.VertexFormat = CustomVertex.PositionColored.Format ' Draw the primitives in the data stream. m_Device.DrawPrimitives(PrimitiveType.TriangleList, _ 0, NUM_TRIANGLES) ' End the scene and display. m_Device.EndScene() m_Device.Present() End Sub
The method clears the device and begins a scene as the previous example did. It then calls the SetupMatrices method (described shortly) to define the world, view, and projection transformations.
Render then calls the device’s SetStreamSource method to tell Direct3D where the data is and sets its VertexFormat property to tell Direct3D the type of data in the buffer.
Next the method calls DrawPrimitives to make Direct3D draw the triangle. The TriangleList parameter indicates that the buffer contains points that define independent triangles. Direct3D looks through the buffer and uses each triple of points to define a triangle.
The Render method finishes as the previous example’s version did?by ending the scene and displaying the result.
The final new piece to this program is the SetupMatrices method shown in the following code:
' Setup the world, view, and projection matrices. Private Sub SetupMatrices() ' World Matrix: ' Just the identity. m_Device.Transform.World = Matrix.Identity() ' View Matrix: ' This is defined by giving: ' An eye point (0, 0, -3) ' A point to look at (0, 0, 0) ' An "up" direction <0, 1, 0> m_Device.Transform.View = Matrix.LookAtLH( _ New Vector3(0, 0, -3), _ New Vector3(0, 0, 0), _ New Vector3(0, 1, 0)) ' Projection Matrix: ' Perspective transformation defined by: ' Field of view Pi / 4 ' Aspect ratio 1 ' Near clipping plane Z = 1 ' Far clipping plane Z = 100 m_Device.Transform.Projection = _ Matrix.PerspectiveFovLH(Math.PI / 4, 1, 1, 100) End Sub
SetupMatrices sets the world transformation to the identity matrix. This transformation leaves all its data unchanged so the objects in three-dimensional space are not transformed before Direct3D applies the other transformations.
The method calls the Matrix class’s LookAtLH function to define the viewing transformation. The “LH” at the end of the method’s name indicates that it’s designed for use in a left-handed coordinate system. This method builds a transformation that is defined by a camera position, a point toward which the camera should be pointed, and an “up” vector that determines the camera’s roll (imagine holding the camera sideways or upside down).
In this program, the camera is at position (0, 0, -3). If you recall the earlier discussion of Direct3D’s left-handed coordinate system, the Z-axis normally points into the computer’s screen so the point value refers to a point moved out of the screen toward where you are sitting relative to the origin (0, 0, 0). If you make the Z coordinate smaller (for example, -10), the camera moves further away from the origin, so objects look smaller. If you make the Z coordinate smaller (for example, -2), then the camera moves closer to the origin?and objects there look bigger.
The call to LookAtLH makes the camera look toward the origin and points the camera so “up” is the direction <0, 1, 0>. That direction points toward the positive Y-axis, which is the vertical axis in the Direct3D coordinate system.
|Figure 3. Colorful but Static: The d3dDrawTriangle program displays a colored triangle.|
Finally SetupMatrices calls the Matrix class’s PerspectiveFovLH method to build the projection transformation. This builds a projection transformation based on a field of view for a left-handed coordinate system. The parameters to this function give the angle of view in the Y direction in radians, the aspect ratio (the width-to-height ratio), and the distances to near and far clipping planes that determine whether an object should be drawn.
When you put all this together, you get the triangle shown in Figure 3. If you look at the code in the CreateVertexBuffer method, you can match up the coordinates of the points in the buffer with those in Figure 3 and get a better feel for how the coordinate system works. The first point is red and in the lower-right corner at (1, -1, 0), the second point is blue and in the lower-left corner at (-1, -1, 0), and the third point is green and at the top at (0, 1, 0).
|Figure 4. On the Move: The d3dRotatingTriangle program uses a world transformation to rotate its triangle before displaying it.|
Program d3dRotatingTriangle uses exactly the same code as program d3dDrawTriangle except that its SetupMatrices method uses the following code to define the world transformation. The code uses Environment.TickCount to see how many milliseconds have passed since the computer last booted. It multiplies that number by 2 * Pi and divides by 2,000 to get the number of radians that it should rotate the data. This makes the data rotate one complete revolution every two seconds:
' World Matrix: ' Rotate the object around the Y axis by ' 2 * Pi radians per 2000 ticks (2 seconds). Const TICKS_PER_REV As Integer = 2000 Dim angle As Double = Environment.TickCount * _ (2 * Math.PI) / TICKS_PER_REV m_Device.Transform.World = _ Matrix.RotationY(CSng(angle))
Figure 4 shows the result.
Making a Cube
Now that you understand the basics of how to create the Direct3D device, create data buffers, define transformations, and display the results, you can display just about anything.
|Figure 5. Cubist Art: The d3dDrawCube program draws a single triangle list containing 12 triangles that make up a cube.|
The d3dDrawTriangle and d3dRotatingTriangle sample programs each display a single triangle from the list of vertices, but it’s easy enough to add other triangles to the list. Figure 5 shows the d3dDrawCube program. Like programs d3dDrawTriangle and d3dRotatingTriangle, this program displays a single triangle list. In this program, however, the list defines 12 triangles that together make up the cube’s sides.
The code used by the d3dDrawCube program contains a few additions you haven’t seen in the other programs. Obviously, it contains code to define the triangles that make up the cube. The MakeTriangle helper method shown in the following code makes it easy to define a triangle. It takes as parameters the vertex array, the index at which it should place the triangle’s first point, the triangle’s color, and the coordinates of its points. The code simply fills in the data in the vertex array:
' Add a triangle to the vertex buffer. Private Sub MakeTriangle( _ ByVal vertices() As _ CustomVertex.PositionColored, _ ByRef i As Integer, ByVal clr As Color, _ ByVal x0 As Single, ByVal y0 As Single, ByVal z0 As Single, _ ByVal x1 As Single, ByVal y1 As Single, ByVal z1 As Single, _ ByVal x2 As Single, ByVal y2 As Single, ByVal z2 As Single) vertices(i).X = x0 vertices(i).Y = y0 vertices(i).Z = z0 vertices(i).Color = clr.ToArgb i += 1 vertices(i).X = x1 vertices(i).Y = y1 vertices(i).Z = z1 vertices(i).Color = clr.ToArgb i += 1 vertices(i).X = x2 vertices(i).Y = y2 vertices(i).Z = z2 vertices(i).Color = clr.ToArgb i += 1 End Sub
The program’s CreateVertexBuffer method uses this code to create the triangles it needs:
' Top +Y. MakeTriangle(vertices, i, Color.Green, _ 1, 1, -1, -1, 1, -1, -1, 1, 1) MakeTriangle(vertices, i, Color.Green, _ -1, 1, 1, 1, 1, 1, 1, 1, -1) ' Bottom. -Y MakeTriangle(vertices, i, Color.White, _ 1, -1, -1, 1, -1, 1, -1, -1, 1) MakeTriangle(vertices, i, Color.White, _ -1, -1, 1, -1, -1, -1, 1, -1, -1) ... Code to create other triangles omitted...
The following code shows the change to this program’s Render method. This code is very similar to the code used by the previous programs except it passes the number of triangles (12) to the call to DrawPrimitives to tell Direct3D how many triangles are in the triangle list:
' Draw the primitives in the data stream. m_Device.DrawPrimitives(PrimitiveType.TriangleList, _ 0, NUM_TRIANGLES)
Program d3dDrawCube contains two other significant differences from the previous programs. First, this program introduces the concept of culling. Culling is a process that lets Direct3D quickly eliminate some of the triangles from the drawing. Culling can be very fast if you are drawing a convex polyhedron made up of consistently oriented faces.
A polyhedron is convex if every angle between two faces is at least zero. In other words, it contains no nooks and crannies that dent into the solid. Or to think of it another way, for every face on the solid you can place the solid on a flat surface so that every point on that face touches the flat surface. Cubes, tetrahedrons, and rectangular boxes are all convex.
You can determine a triangle’s orientation by arranging its points so they are ordered clockwise or counterclockwise when seen from the outside of the polyhedron. If you a look at Figure 5, you can imagine the points defining one of the triangles ordered either clockwise or counterclockwise.
Here’s where culling comes in. If you are drawing a convex polyhedron and the faces are oriented, then you can tell which ones to draw simply by looking at whether their points are arranged clockwise or counterclockwise as seen from the camera. Suppose all of the faces are oriented clockwise as seen from outside the solid. Then if a triangle’s points are arranged counterclockwise as seen by the camera, then the camera must be looking through the solid to that triangle. That means the triangle is a backface, a face on the back of the solid, so it doesn’t need to be drawn. For a convex solid, removing all the backfaces is sufficient to remove all the hidden triangles, so the displayed image presents the solid as you would expect.
To provide this kind of culling, the d3dDrawCube program’s InitializeGraphics method uses the following code:
' Cull triangles that are oriented counter clockwise. m_Device.RenderState.CullMode = Cull.CounterClockwise
The second significant change this program contains is the ability to display its data as solid colored triangles, as a wireframe (as shown in Figure 5), or as a collection of points. When users click any of the program’s three option buttons, the following event handler sets the Direct3D device’s RenderState.FillMode property to the appropriate value. The Render method draws the data as usual and Direct3D produces the desired representation:
' Remember the selected fill mode. Private Sub FillMode_CheckedChanged( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles radSolid.CheckedChanged, radWireframe.CheckedChanged, _ radPoints.CheckedChanged If m_Device Is Nothing Then Exit Sub If radSolid.Checked Then m_Device.RenderState.FillMode = FillMode.Solid ElseIf radWireframe.Checked Then m_Device.RenderState.FillMode = FillMode.WireFrame Else m_Device.RenderState.FillMode = FillMode.Point End If End Sub
The InitializeGraphics method also uses the following code to make Direct3D draw points larger than the default, so they’re easy to see when it displays the data as points instead of solid faces or a wireframe:
' Make points bigger so they're easy to see. m_Device.RenderState.PointSize = 4
Direct3D supports several other drawing primitives in addition to the triangle list, including:
|Figure 6. Triangle Strip: A triangle strip contains a series of triangles that share common edges.|
- Point List?A list of points drawn as dots.
- Line List?A list of line segments drawn separately.
- Line Strip?A connected series of line segments.
- Triangle List?A list of independent triangles.
- Triangle Strip?A series of triangles where each triangle shares the last edge of the previous triangle as shown in Figure 6.
- Triangle Fan?A series of triangles that all share a common corner as shown in Figure 7.
|Figure 7. Triangle Fan: A triangle fan contains a series of triangles that share a common corner.|
Triangle strips and fans may give slightly better performance than triangle lists because they contain fewer duplicated points. The difference may be small but might add up for complicated scenes containing thousands of triangles.
|Figure 8. Surface in Triangle Strips: The d3dDrawTriangleStrips program uses triangle strips to draw a three-dimensional surface.|
Notice that the triangles in a triangle strip have alternating orientations. In Figure 6, the first triangle defined by points 1, 2, and 3 is oriented clockwise but the second triangle defined by points 2, 3, and 4 is oriented counterclockwise. For culling purposes, Direct3D orients the odd-numbered triangles normally and switches the orientation of the even-numbered triangles.
The sample program d3dDrawTriangleStrips, shown in Figure 8, uses triangle strips to draw a three-dimensional surface. It uses the following code to generate the strips. For each Z value between ZMIN and ZMAX, the program defines a triangle strip. For each strip, it loops through X values between XMIN and XMAX and makes two triangles in the strip for the unit square with a corner at point (x, z):
' Make the vertexes. Dim i As Integer = 0 For z As Integer = ZMIN To ZMAX - 1 For x As Integer = XMIN To XMAX MakePoint(vertices, i, Color.Green, x, F(x, z), z) MakePoint(vertices, i, Color.Green, x, F(x, z + 1), z + 1) Next x Next z
The following code shows the function F, which gives a Y value for each X and Z value:
' A three-dimensional function to draw. Private Function F(ByVal x As Single, ByVal z As Single) As Single Return (Sin(x) + Sin(z)) * 0.5 End Function
For each triangle strip, the program calls the DrawPrimitive method, passing it the TriangleStrip flag, the index of the first point in the triangle strip, and the number of triangles that the strip contains. It then increases the index variable by the number of points in each strip so it points to the first point in the next strip:
' Draw the primitives in the data stream. Dim index As Integer = 0 For i As Integer = 1 To NUM_TRIANGLE_STRIPS m_Device.DrawPrimitives(PrimitiveType.TriangleStrip, _ index, NUM_TRIANGLES_PER_STRIP) index += NUM_POINTS_PER_STRIP Next i
|Figure 9. Surface in Lines: The d3dDrawLineStrips program uses line strips to draw a three-dimensional surface.|
The sample program d3dDrawLineStrips, shown in Figure 9, draws the same surface with line strips. Here’s how the program generates its point data. For each Z value, it creates a line strip with that Z value and that has X values between XMIN and XMAX. It then repeats those steps, swapping the roles of X and Z to create line strips in the other direction:
' Make the vertexes. Dim i As Integer = 0 For z As Integer = ZMIN To ZMAX For x As Integer = XMIN To XMAX MakePoint(vertices, i, _ Color.Red, x, F(x, z), z) Next x Next z For x As Integer = XMIN To XMAX For z As Integer = ZMIN To ZMAX MakePoint(vertices, i, _ Color.Red, x, F(x, z), z) Next z Next x
The code below shows how the d3dDrawLineStrips draws its data, which is very similar to the code used by the d3dDrawTriangleStrips program:
Dim index As Integer = 0 For i As Integer = 1 To NUM_LINE_STRIPS m_Device.DrawPrimitives(PrimitiveType.LineStrip, _ index, NUM_LINES_PER_STRIP) index += NUM_POINTS_PER_STRIP Next i
The d3dColoredSurface program shown in Figure 10 uses triangle strips to draw a similar scene, but here, each point’s color depends on its Y coordinate. It colors the large Y coordinates red and small Y coordinates blue.
The final program example described in this column, d3dDrawTriangleFans, uses two triangle fans to draw the spinning top-like shape shown in Figure 11.
Most of the examples so far assume that you are drawing a single convex polyhedron, which means that backface removal via culling is sufficient to remove hidden surfaces. However, if you draw more than one polyhedron, or a non-convex solid, backface removal isn’t enough. Depending on the camera position, two faces that are both visible may overlap, and it won’t always be clear which one should be drawn in front of the other.
Direct3D provides a Z-buffer that you can use to sort out the problems that arise when two faces overlap in this way. The Z-buffer keeps track of the distances from the camera to each face in the image in the direction of every pixel displayed. When it draws a new face, it checks the distance from the camera to the face for each pixel. If the face is farther away than some other face at that pixel, then Direct3D leaves the pixel’s color alone?in other words, it shows the previously drawn closer face. If the new face is closer than any other face drawn so far, Direct3D colors the pixel for the new face and updates the Z-buffer with that face’s distance.
|Figure 12. Dangerous Intersection: The d3dIntersectingTetrahedrons program uses a Z-buffer to display intersecting solids correctly.|
The result is that each pixel drawn in the image has the proper color for the non-culled face closest to the camera and solves the problem of multiple solids, non-convex solids, and even faces that pierce each other.
The d3dIntersectingTetrahedrons sample program shown in Figure 12 draws two tetrahedrons that intersect. It uses a Z-buffer to draw all of the faces correctly. (In fact, it’s interesting to turn off the Z-buffer and see what happens.)
This program works much as the previous examples do with just a few changes. First, its CreateVertexBuffer method creates different data. It builds a triangle list containing the eight triangles that draw the tetrahedrons. Each triangle is properly oriented so Direct3D can cull triangles that are oriented counterclockwise.
Next, the InitializeGraphics method adds two parameters to the PresentParameters object it uses to create the graphics device. The following code shows the two new parameters:
params.EnableAutoDepthStencil = True ' Depth stencil on. params.AutoDepthStencilFormat = DepthFormat.D16
InitializeGraphics also turns on Z-buffering with the following code:
' Turn on the Z-buffer. m_Device.RenderState.ZBufferEnable = True
To make viewing the tetrahedrons more interesting, the SetupMatrices method for this example uses a different world transformation than the previous examples. Rather than rotating the data around the Y-axis, it uses the following code to rotate the data around a line that passes through the origin in the direction of the vector <1, 1, 1>:
m_Device.Transform.World = Matrix.RotationAxis( _ New Vector3(1, 1, 1), CSng(angle))
In the previous examples, the Render method started by clearing the drawing surface. In this example, it must also clear the Z-buffer so Direct3D can start recording fresh distances between the camera and the faces. This code clears both the drawing target and the Z-buffer:
' Clear the back buffer and the Z-buffer. m_Device.Clear(ClearFlags.Target Or ClearFlags.ZBuffer, _ Color.Black, 1, 0)
Finally, the Render method uses a slightly modified call to DrawPrimitives so it can draw the eight triangles in its triangle list:
|Figure 13. Locked Blocks: The d3dLockedBlocks program uses 36 triangles to draw three interlocked blocks.|
m_Device.DrawPrimitives( _ PrimitiveType.TriangleList, 0, _ NUM_TRIANGLES)
The sample program d3dLockedBlocks, shown in Figure 13, uses similar code to display 36 triangles that define three interlocked blocks.
Working with the Examples
Direct3D is a lot to digest all at once. Building a Direct3D device isn’t too difficult, but it is a fairly persnickety process and you need to get a lot of details correct.
Unfortunately, Direct3D error messages tend to be somewhat vague. Many errors throw an InvalidCallException. If you examine the exception, the more detailed message is often “Error in the application,” which is about as helpful as telling a baseball player going up to bat, “triple to left.”
To make matters worse, the exception may appear far from the code that caused it. For example, if you forget to call BeginScene, the program works fine until you try to draw something with a call to DrawPrimitives.
If you follow the examples here carefully?or just copy and modify the sample applications and give them new data?you should be able to get them running relatively quickly. After you master the six drawing primitives supported by Direct3D (point list, line list, line strip, triangle list, triangle strip, and triangle fan), you can draw all kinds of complicated scenes.
These examples use explicitly defined colors for every face displayed. For a complex scene, you would need to define a large number of colors very carefully to prevent adjacent triangles from blending into each other in strange ways. To see what I mean, run program d3dDrawTriangleStrips and select solid shading. All the triangles in the surface turn green, so all you’ll see is a big blob.
The solution to this problem is to use texture and lighting models to allow Direct3D to define the colors of the triangles for you. If you define materials and lights properly, the result is more realistic and requires less work on your part.
Defining textures and lighting is the subject of the next article in this series on Direct3D. Until it is posted, you might want to go to Microsoft’s DirectX Graphics page to learn more. If you come up with some interesting Direct3D applications, email me at [email protected] and let me know. I’ll post them so others can enjoy them and be inspired to make their own creations.