Using Direct3D with VBBy Rod Stephens Direct3D: A First Program
This program draws the single triangle shown in Figure 1. It's not much, but it shows how to prepare Direct3D for drawing. Once you know how to draw one triangle, you can draw lots of others. By drawing enough triangles, appropriately colored, shaded, and textured, you can draw any three-dimension scene.
The exact division of this work is a bit arbitrary. I try to put the DirectDraw and Direct3D initialization code in separate routines. InitializeScene and InitializeObjects together define the scene. Depending on the program, some code may slide from one routine to another. For example, if the camera position changes every time the program draws the scene, it must be defined in RenderLoop so there's no point defining a camera position in InitializeScene. Don't worry if you don't catch every nuance. This is a lot of code to draw a crummy triangle, but most of the code in this example just gets Direct3D running. You can use similar initialization code in most other programs. If it seems a bit mysterious, that's okay for now. For now the most important routines are InitializeObjects which defines the triangle the program will draw and RenderLoop which draws it. If you want to experiment with the program, make changes in InitializeObjects. You can come back to study the other routines later. DeclarationsThe program begins by declaring several variables and constants it will need later. The variables here begin with m_ to remind you that they are declared Private at the module level. That means they are available in all of the code in the form module but not outside the module.
Form_LoadWhen the program begins, the Form_Load event handler runs. It calls other routines to do all the interesting work. InitializeDirectDraw and InitializeDirect3D prepare the Direct3D system for use. InitializeScene initializes all the program-specific scene stuff except for the actual drawing coordinates. InitializeObjects creates the coordinate data describing the triangle. From_Load then uses the Show method to make the form visible. It calls routine RenderLoop to display the triangle. That routine does not return until the program should exit. When it does return, Form_Load unloads the form so the program ends.
Form_UnloadWhen the user clicks the form's close button (the little X in the upper right corner), the form unloads and triggers the Form_Unload event handler. This unloads the form's visible component, but the code is still running. If you do nothing else, subroutine RenderLoop continues running and the program doesn't end. To prevent this, Form_Unload sets m_Running to False so RenderLoop exits and the program can end properly. Comment out this line and see what happens.
InitializeDirectDrawThis routine initializes DirectDraw and assigns some variables. It first creates a DirectX7 object (m_dx) and a DirectDraw object (m_dd). The program uses these to access DirectX and DirectDraw features. SetCooperativeLevel determines the program's top-level behavior. It can indicate things like exclusive of the entire screen or, in this case, normal windows behavior. The routine then creates the primary surface that the user will see. The surf_desc structure defines the surface. The DirectX object's GetWindowRect routines fills in a RECT structure with the Left, Right, Top, and Bottom coordinate values for the PictureBox that will contain the drawing. The program uses these coordinates later. InitializeDirectDraw then creates a render surface. This is the surface the program will draw on. The program will then copy the results to the primary surface so the user can see them. Notice that the program uses the dimensions of the PictureBox here to tell DirectDraw how big to make the render surface. The routine saves the dimensions of the render surface in a RECT structure. Unlike the PictureBox's coordinate values, the render surface's coordinates start with Left = 0 and Top = 0. Finally, the routine saves a reference to the Direct3D object provided by DirectDraw.
InitializeDirect3DThis routine begins by verifying that the system is using more than 8-bit color (256 colors). Color modes with 256 colors or fewer use color palettes to define their colors. Color palettes add another level of complexity to problem and are generally a hassle. These days most computers have enough graphic memory that they can easily use the higher color modes like 24- and 32-bit color. 24-bit color provides photo-realistic images with more colors than most people can distinguish so that's the mode I usually use. InitializeDirect3D then tries to create a Direct3D device with the name IID_IDirect3DHALDevice. If it fails, it tries to create an IID_IDirect3DRGBDevice device. If neither of these work on your system, consult the CreateDevice function's help to see what other values you can try. Next the routine users SetViewport to tell the device on which part of the render surface to draw. This area is called the viewport. Usually you will set the lHeight and lWidth parameters to give the entire size of the surface, and you leave lX and lY equal to zero so the image begins in the upper left corner. The routine finishes by saving the viewport's position in the first entry in the m_ViewportRect array. Subroutine DrawObjects uses this entry to clear the viewport before drawing on it. Although this program only has one viewport, this information is stored in an array because the Direct3D device's Clear method takes an array as a parameter.
InitializeSceneInitializeScene defines scene-related values that do not change as the program runs. This program doesn't change much each time it redraws its triangle so almost all of the scene definition can happen here. The routine starts by creating a material. The R, G, and B components in the material definition set the amounts of red, green, and blue light the material reflects. In this example, the material reflects all light. A red material, for example, would reflect most of the red light and little of the green and blue light. This material might set R = 1.0, G = 0.0, and B = 0.0. The call to SetMaterial makes Direct3D use that material for drawing all objects until further notice. Because this example draws only one triangle, it does not need to ever change the material. The routine then creates a projection matrix. This determines how the three-dimensional image is projected from three dimensions onto the viewport. First the routine uses the DirectX object's ProjectionMatrix method to create a projection matrix. It uses the device's SetTransform method to make the device use the projection matrix. InitializeScene uses the ViewMatrix to initialize a matrix to represent a viewing position. Its three parameters are a "from" position, a "to" position, the world's "up" direction, and a roll value measuring clockwise rotation around the viewing direction. You can think of "from" as the position where you are standing and "to" as the object you are looking at as shown in Figure 2.
InitializeObjectsSubroutine InitializeObjects creates the data that describes the objects that will be drawn. In this program, that is a single triangle. The routine stored the vertices of the triangle in the m_Vertex array of the D3DVERTEX type. This data structure's x, y, and z members give the vertex's coordinates in three-dimensional space. In this example, the triangle connects the points (-10, -10, 0), (0, 10, 0), and (10, -10, 0). The ordering of the three points in each triangle is very important. Direct3D uses the ordering to help determine whether it needs to draw a triangle. Position the wrist of your left hand over the triangle's first point and point your fingers toward the second point. Now curl your fingers toward the third point. Your thumb gives the direction of a vector perpendicular to the triangle. This vector is called the triangle's normal or surface normal. (Technically the normal should be normalized meaning it should be stretched or shrunk to have length 1.0, but any perpendicular vector works for many Direct3D operations.) When Direct3D must draw a triangle, it examines the normal. If the normal points generally toward the viewing position, Direct3D draws the triangle. If the normal points generally away from the viewing position, Direct3D does not draw the triangle. This all makes good sense if you consider the case of a closed solid. Suppose you are drawing a tetrahedron or other solid made up of triangular faces. Also suppose the points that make up each triangle are ordered so the triangle's normal given by the left-hand rule points out of the solid. This is called the outward orientation of the triangles. Now when Direct3D starts to draw a triangle, it determines whether the normal points toward or away from the viewing position. If the normal points toward the viewing position, the face is on the near side of the solid so Direct3D draws it. If the normal points away from the viewing position, the face is on the far side of the solid so its view is blocked by the faces on the near side. In that case, Direct3D does not need to draw the triangle. Now you can probably see why the order of the points is important. If you get it backwards, the triangle will not be drawn when it should be. For a solid, the triangle will be invisible when it is in front and it will be drawn when it should be hidden. If you want a triangle to be visible from both sides, you should draw it twice: once with each orientation. In this example, the viewing position and the triangle never move so the triangle is either always drawn or always omitted. Try modifying InitializeObjects so the triangle's points are listed in reverse order and see what happens.
RenderLoopRenderLoop enters a loop. As long as the variable m_Running is True, the routine calls RenderObjects to draw the objects on the render surface. It then use the primary surface's Blt method to copy the results from the render surface to the primary surface that the user sees. When the user closes the form by clicking the X in the upper right corner, the Form_Unload event handler sets m_Running to False so this loop ends.
RenderObjectsRenderObjects is where the program actually draws. It begins by clearing the viewport to erase the previous scene. Parameters tell the Clear method which viewports to clear, where the viewports are located, and the color to use. The routine then calls the device's BeginScene method, draws the triangle, and calls the device's EndScene method. That's all there is to it.
SummaryThis is an awful lot of work to draw a single triangle. It hardly seems worth it. Fortunately most of this code can stay the same when you draw more complicated scenes. Most of the changes come in the InitializeObjects and RenderObjects routines. That's where they should be since those routines determine what is drawn and how. For the most part, you can ignore the other routines and concentrate on building the scene you want to display.
|