Time to Render
Well, you have your device created now, so you can start getting something rendered onscreen. You already have the rendering method where you can write all the code needed to render your scene, so add the code in Listing 4.6.
Listing 4.6. The Render Method
bool beginSceneCalled = false;
// Clear the render target and the zbuffer
device.Clear(ClearFlags.ZBuffer | ClearFlags.Target, 0, 1.0f, 0);
try
{
device.BeginScene();
beginSceneCalled = true;
}
finally
{
if (beginSceneCalled)
device.EndScene();
}
Before you can do any actual rendering, you most likely want to clear the device to prepare it for a new frame. In this case, clear the render target (ClearFlags.Target) to a particular color and the depth buffer (ClearFlags.ZBuffer) to a particular depth. The second parameter is the color you want to clear the device with. It can be either a member of the System.Drawing.Color object (or any of its many built-in colors) or an integer value in the format of 0xAARRGGBB. In this case, each component of the color (alpha, red, green, and blue) can be represented by two hexadecimal digits, ranging from 00 to FF, where 00 is completely off and FF is completely on. The preceding call uses 0, which is the same as 0x00000000, or everything completely off or black. If you want to clear to blue using an integer value, you use 0x000000ff; for green, 0x0000ff00; or any other combination you feel appropriate.
The third parameter is the new value of every member of the depth buffer. A common value here is to reset everything to a depth of 1.0f. The last parameter is for clearing a stencil, which hasn't been created for this game, so I do not discuss it at this time. After the device is cleared, you want to inform Direct3D that you are ready to begin drawing, which is the purpose of the BeginScene call. Direct3D sets up its internal state and prepares itself for rendering your graphics. For now, you don't have anything to render, so you can simply call EndScene to let Direct3D know you're done rendering. You must call EndScene for every call to BeginScene in your application, and you cannot call BeginScene again until after you've called EndScene. This is why the TRy/finally block ensures that the EndScene method is called for every BeginScene call.
Just because you've notified Direct3D that you're done rendering doesn't actually mean Direct3D updates the display. It's entirely possible for you to have multiple render targets, and you don't want the display updated until you've rendered them all. To handle this scenario, Direct3D splits up the ending of the scene and the updating of the display. The Present call handles updating the display for you; it is called automatically by the sample framework. The overload used in the framework updates the entire display at once, which is the desired behavior for your game. The other overload allows you to update small subsections of the entire display; however, for your beginning simple games, you never need this flexibility.
If you run the application now, you should notice that the window is cleared to a black color, and that's really about it. You can have the "background" of the game be this single color if you want, but that's not exciting. Instead, you should make the background of the game be something called a "sky box." In the simplest terms, a sky box is simply a large cube with the inside six walls of the cube textured to look like the sky or whatever background your scene should have. It allows you to look around your scene from any direction and see a valid view of your background .
Loading and Rendering a Mesh
1. Determine the media path.
2. Declare the mesh variables.
3. Load the mesh and textures from files.
4. Render the mesh.
Step 1 is determining the media path. In the included CD, you will notice a Blockers folder in the media folder. This folder includes all the game media, so you need to copy it onto your hard drive somewhere (or use the installation application from the CD). After you copy the media to your hard drive, you need to add an application configuration file to your project, much as you did in previous chapters. Choose Project, Add New Item, and add an application configuration file to your project. Replace the Extensible Markup Language (XML) that was automatically generated with the following:
Obviously, you want the MediaPath value to match wherever you have copied the media on your hard drive. If you used the installation program, the value shown here will work for you. With the configuration file in your project, you need a variable to actually access this key, so add this variable to your game engine class:
public static readonly string MediaPath =
ConfigurationSettings.AppSettings.Get("MediaPath");
For Step 2, you declare the mesh variables. One of the media files in the folder is an X file called level.x. The X files are used to store geometry data, which can then be easily rendered with DirectX. This particular X file is simply your sky box, with a series of textures, marking each wall of the "level." In Direct3D, the Mesh class will be used to hold the actual geometry data, and the Texture class will be used to hold each texture. A single mesh can have 0 to many textures, so you want to declare the following variables in your game engine class:
// The information for the mesh that will display the level
private Mesh levelMesh = null;
private Texture[] levelTextures = null;
As you can see, you have a single Mesh object that will be used to store the geometry and an array of textures to store the texture for the walls. Before continuing, however, you might find yourself wondering, what exactly is a mesh? When rendering 3D graphics, everything that is rendered onscreen consists of one to many triangles. A triangle is the smallest closed polygon that will always be coplanar. (Rendering primitives that are not coplanar isn't very efficient.) You can build virtually any object by using a large number of triangles. A mesh is simply a data store for this triangle (or geometry) data.
Step 3 is loading the mesh and textures from files. You are now ready to load your mesh data into the variables you just created. Take the code in Listing 4.7, and include it in the OnCreateDevice method you've already written.
Listing 4.7. Loading the Sky Box Mesh
// Create the mesh for the level
ExtendedMaterial[] mtrls;
levelMesh = Mesh.FromFile(MediaPath + "level.x", MeshFlags.Managed, device,
out mtrls);
// Store the materials for later use, and create the textures
if ((mtrls != null) && (mtrls.Length > 0))
{
levelTextures = new Texture[mtrls.Length];
for (int i = 0; i < mtrls.Length; i++)
{
// Create the texture
levelTextures[i] = TextureLoader.FromFile(device, MediaPath +
mtrls[i].TextureFilename);
}
}
For the mesh you are creating, you want to load this from a file, using the media path variable that was declared earlier in this chapter. The second parameter of this call is any flag you might want to pass in. Because this mesh is simply static, you can use the MeshFlags.Managed flag, which informs Direct3D that it should manage the mesh. You also need the created device when creating this mesh, so that is what you pass in as the third parameter. The final parameter is extended material information about the mesh. Because a mesh can have more than one set of material information, it is returned as an array.
After the mesh is created, ensure that the extended material array has members. If it does, you can create the texture array, scroll through each of the members of the extended material array, and load the appropriate texture from a file. Notice that the texture loading method has only two parameters: the rendering device and the location of the file. The texture filename normally doesn't include path information, so to ensure that it loads the texture from the correct location, it's a good idea to include the media path here as well.
Step 4 is rendering the mesh. The actual rendering of this mesh is pretty simple. Take the code in Listing 4.8, and include it in your render method. Place the code between the calls to BeginScene and EndScene to ensure that it is rendered correctly.
Listing 4.8. Rendering the Sky Box Mesh
// First render the level mesh, but before that is done, you will need
// to turn off the zbuffer. This isn't needed for this drawing
device.RenderState.ZBufferEnable = false;
device.RenderState.ZBufferWriteEnable = false;
device.Transform.World = Matrix.Scaling(15,15,15);
device.RenderState.Lighting = false;
for(int i = 0; i < levelTextures.Length; i++)
{
device.SetTexture(0, levelTextures[i]);
levelMesh.DrawSubset(i);
}
// Turn the zbuffer back on
device.RenderState.ZBufferEnable = true;
device.RenderState.ZBufferWriteEnable = true;
// Now, turn back on our light
device.RenderState.Lighting = true;
There's actually quite a bit of new stuff in here. First, if you remember the discussion on depth buffers earlier in this chapter, you learned that the depth buffer stores the depth of each of the objects in a scene. Because the sky box is a single object that will always be behind any other objects in the scene, you can simply turn off the depth buffer and render it first. In many cases, simply turning the depth buffer off (setting the ZBufferEnable render state to false) is adequate; however, in some cases, drivers still write to the depth buffer even if it is off. To handle this case, you can simply turn off writing to the depth buffer as well.
Next, you want to actually size the sky box. The mesh that has been loaded has all the triangle information stored in what is called object space. The mesh itself has no knowledge of any other objects that might or might not be in a scene. To enable you to render your objects anywhere in your scene, Direct3D provides a transform, which allows you to move from one coordinate system to another. In this case, you want to scale the sky box 15 units for each of the axes. (I discuss transforms more in depth in later chapters.)
Because the sky box is textured, you don't want any lighting calculations that the scene might need to affect the box. To ensure that no lighting calculations are used when rendering the sky box, you simply set the Lighting render state to false. Next, you are ready to draw your mesh.
You'll notice here that you want to go through each texture in your array. First, call the SetTexture method to let Direct3D know which texture you expect to be using for rendering this portion of the mesh. Then, you call DrawSubset on the mesh itself, passing in the index to the texture inside the array you are currently rendering.
|