HomeASP.NET Managed DirectX First Steps: Direct 3D Bas...
Managed DirectX First Steps: Direct 3D Basics and DirectX vs. GDI+
Have you ever wondered how to create graphics using DirectX? This article explains how to do it. It is taken from chapter three of the book Beginning .NET Game Programming in C# by David Weller et. al. (Apress, 2004, ISBN: 1590593197).
DIRECTX REFERS TO A COLLECTION OF MICROSOFT-CREATED APIs and technologies that help developers more directly access the hardware features of a computer. It was originally released in 1996 as a way for game programmers to access the graphics hardware without requiring the developer to switch out of Windows and into DOS mode. Starting in 2002, Microsoft released a version of DirectX that allowed developers to access the DirectX libraries using .NET languages (such as C#, Visual Basic .NET, and managed C++), which is commonly referred to as Managed DirectX (Microsoft and developers often use the acronym MDX).
This chapter will take you on an introductory tour of the graphical components of DirectX and also teach you a few differences between graphics programming in DirectX and GDI+. We’ll follow a different approach from the other chapters though: There’ll be no sample game, and we’ll instead concentrate on the basic features of DirectX (particularly Direct3D) and how to go through its initialization routines, creating a sample application that will demonstrate each of these features.
The sample application, as you’ll see in the section “The Application Proposal,” will comprise a main window, which will display your 3-D board capabilities, and a set of separate windows that will each test a specific feature, like use of lights, 3-D transformations, and full-screen drawings. In each of these test windows we’ll present sequentially the drawings of a walking man, shown in Figure 3-1, providing the illusion of movement.
Figure 3-1.The working man, presented as this chapter's sample application
DirectX allows the programmer to access hardware devices, such as 3-D acceleration boards and advanced sound systems, using unified interfaces. Developers can take advantage of each hardware-specific feature to enhance the multimedia operation speed without having to worry about each device’s details. Think of DirectX as a set of high-level APIs for gaming, multimedia, and graphics programming.
The latest version of DirectX SDK can be downloaded from http://msdn.micro-soft.com/directx; this download includes the DirectX APIs, the Managed DirectX interfaces, the DirectX Software Development Kit (SDK), a comprehensive set of samples packaged in a nifty sample browser, and detailed documentation about all DirectX features.
In the next section, we’ll present an overview of DirectX that will give you enough information to go on exploring Direct3D features in the later sections.
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
In this section, we’ll discuss some common terms used in the DirectX world and see how they fit together to provide you a framework for building great games.
Using hardware acceleration is a wonderful thing, because you can go from dozens of frames a second, such as in the previous two sample applications, to hundreds of frames drawn per second. In the tests in this chapter, the basic samples easily reach 300 frames per second, and can go to almost a thousand depending on the hardware capabilities!
Of course, there’s a price to pay. Even the simplest games must go through some complex routines, and you’ll have to learn some new concepts, even if you don’t want to take full advantage of the hardware acceleration features.
When you manage to understand these initialization routines and the basic concepts, you can use the Direct3D interface to create your 2-D games without even worrying about more advanced concepts like depth buffers or vertex blending.
Let’s start with an overview of the main concepts used by DirectX and how they’re related.
Presenting the DirectX Top-Level Objects
A good library for writing games doesn’t just deal with computer graphics; it also deals with handling input, generating sounds and music, and handling communication between clients and servers in a multiplayer gaming context.
Here is a quick overview of the libraries available with Managed DirectX:
Microsoft.DirectX is the top-level namespace, but also contains common mathematical constructs such as vectors and matrices.
Microsoft.DirectX.Direct3D is the most commonly used library and contains classes and structures designed to help you create and render 3-D images.
Microsoft.DirectX.Direct3DX is a set of “helper libraries” that have many common functions used when creating Direct3D applications.
Microsoft.DirectX.DirectDraw exists mostly for backward-compatibility with older versions of DirectX. All the functionality of DirectDraw was subsumed into the Direct3D namespace. In the past, DirectDraw was the primary API used to create 2-D games.
Microsoft.DirectX.DirectInput is the namespace where all input devices are controlled and managed. It even has support for force-feedback joysticks.
Microsoft.DirectX.DirectPlay allows you to write multiplayer games using efficient network communication packets.
Microsoft.DirectX.DirectPlay.Lobby extends DirectPlay to support a client/ server style of multiplayer gameplay.
Microsoft.DirectX.DirectPlay.Voice adds voice communication features to DirectPlay. It is highly flexible and allows you to add your own sound decoders (codecs) if you’re brave enough.
Microsoft.DirectX.DirectSound gives sound capabilities to your application, including the ability to simulate 3-D sounds and effects. It also has all the other cool “knobs and whistles” you would want in a sound library, including the ability to add echo, reverb, and other effects.
Microsoft.DirectX.AudioVideoPlayback gives you the ability to do simple control of audio and video playback within your application.
Microsoft.DirectX.Diagnostics is used to let you programmatically investigate the features of your environment.
Microsoft.DirectX.Security gives you secure control over all input and output components of DirectX.
Microsoft.DirectX.Security.Permissions is a component of the Security namespace that lets you establish security actions and policies.
Direct3D allows access to the 3-D acceleration layer.
In this chapter, we’ll concentrate on the Direct3D namespace, and you’ll learn some helper functions from Direct3DX. In upcoming chapters, we’ll also examine DirectSound, DirectInput, and DirectPlay.
Understanding Adapters
As you’ve probably guessed, DirectX has a lot of new terminology that you’ll see mentioned over and over again. One of those terms is adapter. A graphics card generally has one adapter in it, from the DirectX perspective (although many graphics cards support multiple adapters now). It’s not unusual to have a single computer driving multiple monitors anymore, and this is usually done with multiple adapters attached to the computer. Conveniently, DirectX provides some functions that allow you to list all display adapters attached to a system and gather some information about them.
You don’t do any direct operations over an adapter; the functions are here just for informational purposes, or to allow you to choose between adapters when you have more than one.
Usually you’ll have only one adapter (the default), but with machines with secondary adapters you can use the adapter identifier (a sequential number) to switch from one adapter to another.
To gather the adapter information, you can use the following code sample:
public void ListAdapters() { AdapterDetails adapterInfo; // Add each adapter to the LstAdapters list box. foreach(AdapterInformation info in Manager.Adapters) { LstAdapters.Items.Add(info.Information.Description); } }
Note that these code samples will require you to reference the Managed DirectX assemblies; see the sidebar “Referencing DirectX Libraries” for more details. In Managed DirectX, many of the methods are reengineered to provide a more intuitive interface than their unmanaged counterparts. For example, many Get methods have been replaced by properties such as the Adapters.Count property in the preceding code, which replaces the previous GetAdapterCount method. Additionally, some functions that returned values as parameters have been rewritten to return values as the result of the function. There’s also a new object, the Manager, presented in the previous code sample, that handles basic interactions with Direct3D. These kinds of modifications make the code cleaner for the managed version of DirectX.
The code listing uses the Adapters.Count property to run across the adapters and gather the description of each one. Although Description can vary for the same device and driver when dealing with different vendors, it and the DriverName property of the AdapterDetail structure are the only human-readable information available. The other members of this structure are numeric values that identify the driver version, revision, and other internal control numbers, and won’t be of interest to you (refer to DirectX SDK help for further information).
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
When writing DirectX programs, you’ll need to add references to the DirectX libraries to your project. This can be done in one of two ways in Visual Studio: either by using the DirectX Wizard or by selecting the Add Reference option after right-clicking the project to bring up the Add Reference dialog box (see Figure 3-2).
Figure 3-2.Adding references to your project
If you’re not using Visual Studio, you can include references by using the /r: option in the command line.
Understanding Devices
Having access to the adapter isn’t enough. You still want the flexibility to create multiple connections into an adapter, each one capable of handling all the fancy 3-D magic that modern graphics processors can do these days. In DirectX, that connection is called a device. Each adapter can have multiple devices, but each device is one of three different types:
Hardware (hardware abstraction layer, or HAL): When creating HAL devices, you have direct access to the hardware acceleration features (and the resultant increase in speed). If you try to create a device of this type but have no 3-D acceleration board, DirectX will raise an error and won’t create the device.
Reference (Reference Rasterizer): This type of device, included in the DirectX SDK, provides most of the features available for the DirectX functions, and doesn’t depend on any hardware support—everything is done in software. Although this type of device is very flexible, it’s very slow, and should only be used for debugging purposes, because it allows you to test many features not supported by your hardware. Don’t even think about creating a game with it, as the frame rate is very low—between 1 and 5 frames per second, usually.
Software (software device): This isn’t used unless you need plug-in support for a custom renderer.
When creating a device, you must specify the adapter being used (usually the default, defined as “0” [zero]), the type of the device as described in the preceding list, the handle of the window that will be used as a viewport, and two other parameters that will define the details about the device creation, the behavior flags and the presentation parameters, as shown in the next code sample:
The behavior flags must be one of the following flags defined by the CreateFlags enumeration:
SoftwareVertexProcessing: This option tells DirectX that all vertex calculations will be made by software. This option is the slowest, but is always available.
HardwareVertexProcessing: This option forces DirectX to rely on hardware capabilities to make all the vertex-processing operations. If the hardware isn’t able to perform the vertices calculation, the creation of the device will fail.
MixedVertexProcessing: As the constant name states, this uses a mix of available hardware features and software-implemented ones to achieve the best results. If the hardware offers no vertex-processing features, this call will fail, too.
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
As we’ve touched on the concept of vertex processing, it’s probably a good idea to describe some of the basics about DirectX and, more specifically, the DirectX pipeline. Because we won’t be covering the details of the pipeline, nor how to manipulate the programmable parts of it, you won’t miss anything if you skip over this part. However, if you’re curious (like most programmers are), this section will give you a very generalized notion of the details of DirectX.
Modern graphics cards have several different stages of processing. In the past, those stages generally consisted of three parts. The first part transformed the vertices of 3-D models, which were generated with their own notion of a coordinate system, into a coordinate system that mapped into the “world” (scene) that the model existed in, and then ultimately into a specific viewpoint into that world. During those transformations, the graphics pipeline also performed techniques to alter the color and intensity of the vertices depending on the light sources in the scene. This stage was generally referred to as the transform and lighting stage (T&L).
Once the T&L was complete, the scene was trimmed down to throw out the parts that weren’t going to be in the final image the viewer would see. This was generally called the clipping stage.
The final part was where all the “magic” happened and the 3-D world got converted into a 2-D image that you could display on a screen. This was generally called rasterization.
Modern graphics processors have much more control over the pipeline, and DirectX gives you many different ways to control these stages creatively. For instance, during the T&L stage, you have more abilities to manipulate the vertices in different ways, including the ability to perform high-speed manipulations on sets of vertices. In effect, you bypass the “fixed” T&L stage and can do interesting things like simulating cloth, face morphing, or fancy underwater effects. This technique is generally called vertex shading.
But DirectX doesn’t stop there—it also lets you “romp and stomp” inside the rasterization stage, allowing you to directly manipulate different parts of it. Pixel shading, for instance, allows you to apply special lighting effects on a per-pixel basis. In addition, you can create fog effects and blend separate frame buffers, basically tweaking the output right up until it’s sent to the monitor.
Figure 3-3 shows an example of a pixel-shading technique in which a normal 3-D scene is given a “hand-drawn” effect, all in real time. This demo can be found on ATI’s Web site
Figure 3-3. Non-photorealistic rendering using pixel shaders
All these concepts are well beyond the scope of this book, but you can find plenty of information on them in modern graphics programming books and on the Internet. Look in Appendix A for some recommendations.
The preceding flags are mutually exclusive, but they can be combined with the following flags to pass additional information to DirectX when creating a device:
FPU_Preserve: This flag informs DirectX to perform all the calculations using double-precision floating points, which can lead to slower performance.
MultiThreaded: Use this flag to inform DirectX that you need a multi-thread-safe environment.
PureDevice: This flag is used only in combination with the HardwareVertexProcessing flag, and specifies that the hardware can do rasterization, matrix transformations, and lighting and shading calculations. It’s the best choice for any application, and most modern graphics cards offer this feature.
The last set of parameters for creating a device, the presentation parameters flags, is a complex structure whereby the programmer can define many low-level details about the device being created. We’ll present here the most commonly used attributes. For a full list, refer to the DirectX SDK help feature.
EnableAutoDepthStencil and AutoDepthStencilFormat: These structure members tell DirectX that you want to use a depth buffer and which format to be used in such buffer (according to the Format enumeration), respectively. The depth buffer helps with defining the relative distance of the object in relation to the screen, which is used to draw nearby objects in front of far ones. Although this seems to be a concept exclusive to the 3-D gaming world, that’s not entirely true: Even some very basic 2-D games have so-called layers—usually the background and any objects that must appear behind the player (such as trees or bushes) stay in a back layer, and the player and other objects stay in the front layers.
BackBufferCount, BackBufferFormat, BackBufferWidth, and BackBufferHeight: These members define the number of back buffers (from 1 to 3), the format of such buffers (defined by the Format enumeration), and their width and height. The back buffer format (as with the depth stencil buffer) must be valid, that is, one that can be checked by the CheckDeviceType method of the Direct3D object. If the buffer can’t be created, the creation of the device will fail. The back buffers are used to render the scene being drawn in the background thread automatically, in order to allow a smooth transition between frames drawn (no partial drawing is shown to the player). This parameter is closely related to the SwapEffect attribute, which will tell DirectX how to swap the back buffers to the screen, and to the Windowed attribute, which will force some limitations to the possible values.
SwapEffect: A constant of the SwapEffect enumeration that defines the behavior of the buffers swap operation. This enumeration includes the following options:
SwapEffect.Discard: The back buffers content isn’t preserved in the swap operation, allowing the application to choose the best performing technique, sometimes leading to big performance gains in the swapping operation. However, the scene must be completely redrawn for each frame.
SwapEffect.Flip: Creates a circular list of buffers to be swapped to screen (called a swap chain), allowing synchronization with the video refresh rate in a smooth way when running full screen. The flip term means that you have no copy of the memory block—DirectX just repositions the video memory start pointer to the next buffer. When running in windowed mode, there’s no real flip; the video memory gets copied to the window, which is an operation with slower performance. In this operation, the front buffer becomes one of the back buffers, so the game can rely on this to redraw only part of the scene.
SwapEffect.Copy: This setting preserves the contents of the back buffer, just copying it over the front buffer (the screen). This setting forces BackBufferCount to be set to 1, because there’s no need to have more buffers. This is the simplest of the buffer swap operations, although it’s the one with the worst performance. The most important gain for the programmer is that the application isn’t forced to perform complex control operations over multiple back buffers.
Windowed: When set to true, indicates that the application will run in a window; a setting of false indicates the application will run full screen. When running in windowed mode, BackBufferFormat must match the current display resolution, and BackBufferWidth and BackBufferHeight may not be specified, as they are assumed to be the window client area dimensions. When running in full screen, the width and height of the back buffer must match one of the possible display modes (explained in the next section) for the device.
DeviceWindowHandle: The handle of the window to be used by DirectX. If it’s set to null, DirectX will use the active window.
Understanding Display Modes
Although the term adapter refers to the hardware and its driver, and the term device refers to the main object used to access a specific window and draw over it, we use the term display modes to define the objects (the DisplayMode class) that store basic information about the screen status, including width, height, refresh rate, and a format flag that returns extra information about how colors are controlled by the display. The formats for rendering displays are as follows:
A8R8G8B8: Color format in which each pixel on screen is defined using a 32-bit ARGB value—255 possible values for each red, green, and blue (RGB) color component, and an extra alpha (A) value that defines the transparency of each pixel (255 is fully opaque and is 0 is totally transparent).
X8R8G8B8: Color format with 32-bit RGB values, and an extra byte (indicated by the “X”) for color definition, not used.
R5G6B5: Color format using 16 bits, where each RGB color component can assume 32 different values; an extra bit for green makes this show 64 possible values, reaching a total of about 64,000 colors.
X1R5G5B5: 16-bit color format in which each color component takes 5 bits (32 possible values), making a total of a little more than 32,000 colors.
When choosing the display mode for games, it’s important to balance the number of desired colors against the memory used to display them. The 32-bit format spends almost twice as much time to display the same number of pixels when using the copy swap modes than do the 16-bit formats. However, the 32-bit format enables a huge number of colors, which may be needed with games that have more sophisticated artwork. The rule of thumb is always use 16-bit format, unless you need more colors, so you’ll get the best performance.
NOTE When running in windowed mode, you must use the computer’s current resolution and color depth, so this discussion applies only to full-screen modes.
Creating a Simple Direct3D Program
Now that you understand the basic concepts involved in creating a DirectX device to render your graphics, let’s look at the basic structure for Managed DirectX programs. This basic structure will always be the same, even for the most sophisticated programs.
All the drawing operations on Direct3D are made with the use of a Device object and must occur between the calls of the BeginScene and EndScene methods. These methods internally lock the back buffer you use while rendering and unlock it when you finish. Calling the Present method of the Device object, after ending the scene, will display the contents of the back buffer to the screen (front buffer), according to the behavior parameters set when creating the device.
The basic structure for a Direct3D program is shown in the following pseudo-code:
Set the presentation parameters for the device to be created Create the Device object Repeat in a loop, until Game Over Clear the Device Begin the Scene Draw the Scene (render) End the Scene Present the Scene to the user Dispose the Device object
This will map to the following code (this is a simplified version of the code you can find in Microsoft’s DirectX Sample Browser, which is installed when you install the DirectX SDK):
public class SimpleDxApp : Form { private Device device; public void InitializeGraphics() { PresentParameters presentParams = new Present PresentParameters(); presentParams.Windowed=true; presentParams.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams); } protected override void OnPaint(System.Windows.Forms PaintEventArgs e) { this.Render(); // Render on painting. } private void Render() { device.Clear(ClearFlags.Target, System.Drawing.Color.Blue, 1.0f, 0); device.BeginScene(); // Rendering of scene objects can happen here. device.EndScene(); device.Present(); } static void Main() { using (SimpleDxApp frm =new SimpleDxApp()) {//dispose frm object when done frm.InitializeGraphics(); frm.Show(); while(frm.Created) { Application.DoEvents(); } } } }
That’s it. Of course, some details aren’t presented here, the most important one being the error trapping. For instance, in the scene-drawing sequence, you have three related methods—Begin, End, and Present—that must be executed as a whole; if one of them fails, the others will fail, too. But you’ll see the details in the section “The Coding Phase.”
If you run this code (see details about setting the correct reference to the Managed DirectX type library in the section “The Coding Phase”), all you get is a blue window, because you don’t know yet what you can use in the Render procedure to draw something. But DirectX will already be up and running, ready for you!
To complete your first program, let’s see some basic concepts regarding Direct3D drawing in the next sections.
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
Even if you have no interest in creating 3-D games, you must understand the basic concepts of a 3-D coordinate system, because everything you do in Direct3D is defined by points and images in a 3-D world. Of course, you can ignore the z axis and pretend that you’re in a 2-D world—and you’ll see how to do this—but the z value will still be there (it will just always be a value of zero).
When you’re dealing with three Cartesian dimensions, there are two types of coordinate systems: left-handed and right-handed. These names refer to the z-axis position relative to the x and y axis. To determine this position, point the fingers of one hand to the x-axis positive direction and move them in the counterclockwise direction to the y-axis positive position; the positive z-axis direction will be the direction to which your thumb points. Figure 3-4 illustrates this concept.
Figure 3-4. The Cartesian 3-D coordinate systems
To put it a different way, imagine the origin of your coordinate system starting in the lower left at (0.0), with the y axis going up and the x axis going to the right. In a left-handed coordinate system, the z value gets bigger (the positive direction) when you go from the screen to a point away from you, the right-handed 3-D system is the opposite: The z values increase toward you from the screen.
Direct3D uses the left-hand coordinate system, which means that positive values for z are visible, and the greater they are for a given object, the farther the object is (and, depending on the projection chosen, the smaller it appears on the screen); and negative values aren’t shown (unless you change your “camera position,” which is also possible in Direct3D).
Who’s Left? Who’s Right?
Although DirectX uses a left-handed coordinate system, many math books use right-handed coordinate systems that reverse the x and z axes. These differences won’t affect you as you learn DirectX, but you’ll eventually need to understand how to do mathematical calculations that will transform between different coordinate systems (in fact, a huge amount of work done in a graphics processor relates to coordinate conversions between different coordinate systems). Some modern books use geometric algebra, which discusses the mathematics in a coordinate-free context. Those authors argue that such approaches are an ideal way to learn an otherwise complex subject.
Now that you have an understanding of 3-D coordinate systems, the next step to explore is how they present 3-D objects to your 2-D screen.
Fortunately, all the hard mathematical work is done by DirectX, but you have to know the concept of projections and how they apply to DirectX in order to give the basic instructions about how to present the objects on screen. In a nutshell, a projection is a volume of space that represents an area that can be viewed on a screen.
Direct3D supports two different types of projections:
Perspective projection: The most common type of projection, it takes into account the z distance and adjusts the objects accordingly. This projection makes objects appear smaller when far from the screen—the objects get deformed, like in the real world. For example, the borders of a straight road appear to come together in the horizon. Figure 3-5 shows a graphical representation of the perspective projection.
Figure 3-5.Perspective projection
Orthogonal projection: In this type of projection, the z component is just ignored, and the objects don’t get bigger when closer to the screen or smaller when they are farther away. This projection is mostly used for 2-D games or simpler 3-D games. Figure 3-6 presents an orthogonal projection.
Figure 3-6.Orthogonal projection
When defining the projection type, you must choose the type of coordinating system and pass the parameters for the projection, according to its type. Direct3D offers six main functions (besides four others for creating custom coordinates systems) that allow you to specify the projection for your game. These functions return matrices that will be used by Direct3D to calculate the conversion from 3-D coordinates to screen coordinates.
Matrix.OrthoRH, Matrix.OrthoLH: Returns the matrix with the transformations that need to be applied to the object’s coordinates to define an orthogonal projection (RH stands for right-handed, LH for left-handed). Each function receives the width and the height of the viewport (usually, the screen or window size) and the range of z values that will be viewed (points before the first z value and after the last one won’t be viewed).
Matrix.PerspectiveRH, Matrix.PerspectiveLH: Returns the transformation matrix for perspective projection, passing the width and height of the viewport and the z distance viewed (first and last points) for right-handed and left-handed coordinate systems.
Matrix.PerspectiveFovRH, Matrix.PerspectiveFovLH: Returns the transformation matrix for perspective projection, passing the angle in radians of your field of view (FOV) and the z distances; for right-handed and left-handed coordinate systems.
Figure 3-7 shows graphically the FOV angle and the z distance viewed (defined by view planes).
Figure 3-7.The field of view angle and view planes for perspective projection
In the next section, we’ll explain the matrix concept and learn how it helps you to convert coordinates of a 3-D world to screen coordinates, allowing you to easily perform complex operations on your game objects.
Understanding Matrices and 3-D Transformations
Knowing how to work with transformation matrices is possibly the most important concept when dealing with Direct3D. Using matrices, you can perform rotation, scaling, or translation of any object on the 3-D world (or in the 2-D world, if you choose to ignore the z component), and these operations, correctly applied, will help you to define your projection type (as shown in the previous section) or even move the camera to see the same scene from different points.
Let’s discuss the use of transformation matrices to do a simple translation, and then extrapolate the idea for more complex operations. Suppose that you want to move a triangle up the y axis, as shown in Figure 3-8.
Figure 3-8. Mounting a triangle on the y axis
Let’s assume the triangle vertices are defined by the points shown here.
VERTEX
X
Y
Z
1
50
10
0
2
60
10
0
3
53
25
0
To translate 40 units over the y-axis positive direction, all you need is to sum 40 to each y position, and you have the new coordinates for the vertices, shown here:
VERTEX
X
Y
Z
1
50
50
0
2
60
50
0
3
53
65
0
The same results can be achieved by representing each vertex as a matrix with one row and four columns, with the vertex coordinates as the first three columns and 1 as the value in the last one, and multiplying this matrix by a special matrix constructed to produce the translation transformation to the vertex matrix.
Figure 3-9 presents the same operation applied to the first vertex.
Figure 3-9.Applying a matrix multiplication to a 3-D vertex
To calculate the resulting matrix, you must take each value in the row of the first matrix and multiply them by each of the values in the corresponding column in the second matrix, and then perform the sum of all results. So, in the previous sample, the calculations are as follows:
We don’t want to get into much deeper detail here, but suffice it to say that you can perform translations by putting the desired values for translation over the x, y, and z in the last row of the transformation matrix; perform scaling by replacing the 1s on the diagonal to fractional values (to shrink) or greater values (to expand); and perform rotation around any axis using a combination of sine and cosine values in specific positions in the matrix.
TIP For those who want to know more about the transformation matrices, DirectX SDK help has full coverage of this topic, showing each of the matrices and explaining how to use them. You can also look in Appendix A to find more books on the mathematics of matrix transformations. We’ll cover matrix transformation in a little more depth in later chapters.
Luckily enough, you don’t need to understand all these details to use the transformations in your program. All you need to know is the following:
Transformation matrices can be multiplied by each other without losing information. If you want to translate and rotate an object at the same time, you can simply multiply the translation matrix to the rotation matrix and multiply the result for your vertices, acquiring the desired result.
The Device object has three special properties: one is used to receive the projection matrix (which was explained in the previous section), <Device>.Transform.Projection; another to indicate the transformations desired in your 3-D world (explained here), <Device>.Transform.World; and the third to specify the camera position (explained in the next section), <Device>.Transform.View.
The D3DX utility library has functions to create all the transformation matrices for you, functions for matrices multiplication, and a function that returns an identity matrix (a special matrix that returns the vertices without transformations, which is used to clean the old world matrix before updating it). You’ll see these functions in the section “The Code Phase.”
Positioning the Camera
As an extra feature when dealing with 3-D coordinate systems, DirectX allows you to position the camera to see the same scene from different points. The camera in DirectX is referred to as the view matrix.
You can calculate the view matrix and set it to the <Device>. Transform. View property, or you can use the helper functions Matrix.LookAtLH and Matrix.LookAtRH. These helper functions define the camera position and the direction it’s looking at by three points: the 3-D position of the camera, the 3-D position the camera is looking at, and the current “up” direction, usually the y axis.
If you don’t define a view (camera) matrix, DirectX will provide a default one for you, but it’s an important concept to have in mind. Do you remember the first Prince of Persia game in which, at a given level, the prince drank a special potion and the screen turns upside down? Imagine creating this feature with a single line of code, rotating the view matrix by 180 degrees (multiplying it by a rotation matrix). This scenario shows the benefit of using Direct3D even for 2-D games.
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
You’re ready to start working now: You know what adapters and devices are, you understand what display modes are, you know the basic Direct3D program structure, and you know all you need to know (for now) about projections, cameras, and transformations. The stage is all ready for the play. All you need now is to meet the actors: the drawing primitives.
Drawing primitives, or 3-D primitives, are vertex collections that define single 3-D objects. Direct3D uses the simplest polygon—a triangle—as a base to create all other 3-D objects. This is done because a primitive defined with only three points is guaranteed to be in a single plane and to be convex, and these characteristics are the key to performing the fastest rendering possible.
So, for example, if you want to draw a square on screen, you’ll have to use two triangles. If you want to create a cube, you’ll use 12 triangles (2 for each facet), as shown in Figure 3-10.
Figure 3-10.A cube made with triangles
Along with triangles, Direct3D allows you to define lists of lines and lists of points, which are useful mainly for debugging purposes in that they help you to see the wireframe image for your objects and check the hidden surfaces when you use triangles.
The steps for creating a simple set of triangles in Direct3D are as follows:
Create a vertex buffer.
Fill the buffer with each of the vertices of the object, according to the defined vertex type.
Draw the buffer on the device, using the desired primitive type.
You can see an example of this simple set of steps in tutorial #2 in the DirectX SDK (“Rendering Vertices”). For now, let’s just consider that all the vertices are defined only by x, y, and z coordinates (you’ll see more details about this later), so you can concentrate on the drawing primitive types.
A primitive type can be one of the following values of the PrimitiveType enumeration:
PointList: Each vertex is rendered isolated from the others, so you can see a list of floating points. Figure 3-11 presents a set of vertices rendered as a point list.
Figure 3-11.Vertices rendered as a point list
LineList: The vertices are rendered in pairs, with lines connecting each pair. This call fails if you pass in a vertex buffer with an odd number of vertices. Figure 3-12 illustrates the use of a line list primitive type.
Figure 3-12.The same vertices rendered as a line list
LineStrip: All the vertices in the buffer are rendered as a single polyline. This is useful when debugging, because this primitive type allows you to see a wireframe image of your objects, regardless of the number of vertices. Figure 3-13 presents a line strip primitive type sample.
Figure 3-13. The same rendered as a line strip
TriangleList: The vertices are rendered in groups of three, as isolated triangles. This provides you the greatest flexibility when rendering complex scenes, but there’s the drawback of having duplicated vertices if you want to draw connected triangles. Figure 3-14 shows the use of the triangle list primitive type to render vertices.
Figure 3-14.The same vertices rendered as a triangle list
TriangleStrip: You’ll use this primitive type when drawing connected triangles. It’s the usual choice for rendering scenes, because it’s more efficient, since you don’t have to repeat the duplicated vertices. Every new vertex (after the first two) added to the buffer creates a new triangle, using the last two defined vertices. Figure 3-15 presents a triangle strip primitive type example.
Figure 3-15.A complex polygon created with a triangle strip
TriangleFan: In this primitive, all the triangles share a common vertex— the first one in the buffer—and each new vertex added creates a new triangle, using the first vertex and the last defined one. Figure 3-16 illustrates the last of the primitive types, the triangle fan.
Figure 3-16. A triangle fan example
Conceptually, everything rendered in the graphics pipeline ultimately is a triangle. Not only that, but the way the triangle is drawn determines which side of the triangle is the front and which is the back. This is particularly important when you begin to add effects such as coloring or texturing to a triangle. Imagine if you had a collection of triangles that represented a sphere. Then imagine you wanted to apply some kind of shaded coloring to the triangle, perhaps as if you were painting the sphere with a shiny color. It wouldn’t make sense to paint the same color on the inside of the sphere, would it? Well, that same concept applies to rendering the triangles. The system removes triangles that aren’t seen in a process called culling. The process of removing triangles that can’t be seen is called back-face culling (because you can’t see the backs of the triangles facing you).
In DirectX, you actually have control over the culling mode. You could, for instance, tell DirectX to not cull backface triangles. This ability to turn on/off backface culling is important for some applications. To see an example of this, look at example 3 in the DirectX SDK tutorials (“Using Matrices”) and comment out this line:
dev.RenderState.CullMode = Cull.None;
You’ll then see that the triangle appears and disappears, because the back-face of the triangle is being culled (DirectX culls back-facing triangles by default).
When drawing triangles, you also need to take special care about the triangle vertex ordering when you want Direct3D to draw only the front part of a triangle. You must define whether you want the front face to be the clockwise-ordered one or the counterclockwise one; so you must draw all triangles using the same ordering for the vertices.
Okay, you’re probably thinking, “These primitive types are interesting, but what if I just want to draw a single image, say, a bitmap file on disk, to the screen? Can’t I just draw it directly on screen?”
The answer is not quite. You can create a square (composed with two triangles) and apply the image on it as a texture. You can even state that a specific color must be treated as transparent, so it appears that you’re dealing with non-rectangular objects. That’s what you’ll see in the next section. However, there is a simpler way to draw a square and put a bitmap in it, using a special Sprite class, which you’ll get a chance to investigate in later chapters.
Coloring and Texturing with Flexible Vertex Formats
Direct3D gives you the power to choose how you can define the vertices that will compose your drawing primitives, using the so-called flexible vertex formats (FVF).
Before creating a vertex buffer (explained in the previous section), you must specify which kind of information each vertex will hold, creating a custom vertex structure and using it when creating a new VertexBuffer object, as presented in the next code sample:
vertBuffer = new VertexBuffer(typeof(CustomVertex), numVerts, device, Usage.WriteOnly, customVertexFlags, Pool.Default);
The parameters for this code line are as follows:
CustomVertex is the actual description of your custom vertex buffer, and the typeof keyword passes along the type information about the CustomVertex type. You’ll see what this type looks like shortly.
numVerts is the number of vertices you’ll want the buffer to hold.
device is the Direct3D reference to the current display device.
Usage defines the purpose of the vertex buffer, allowing Direct3D to perform any extra control it needs. You’ll usually use WriteOnly for this, meaning that you’re only writing to the buffer and passing it later to the device, and won’t read from it. This flag allows Direct3D to choose the best memory allocation for fast writing and rendering.
customVertexFlags is a collection of flags that describes the type of information contained in the custom vertex structure. These flags are defined in the VertexFormat enumerated type.
Pool provides extra information to Direct3D, defining where the resource must be placed (system memory or managed memory, for example). Usually you’ll use the Default enumeration member for this parameter.
The VertexFormat parameter is a combination of flags that will tell Direct3D what information you’re using in your vertices, allowing you to include special information on how to create lighting effects or texture information on each vertex. Among the many possible values on the VertexFormat enumeration, the ones you’ll be using in this book are as follows:
Diffuse: You’ll include information for a diffuse color in the vertex. A diffuse color is the kind of color an object gives when white light shines on it—the kind of color an object would have in “real world” lighting.
Position: Your vertex coordinates need transformation (remember the matrices?) from world coordinates to screen coordinates before being displayed. This flag can’t be used with the VertexFormat.Transformed flag.
Transformed: Your vertices are already in screen coordinates, so you won’t need any projection information. This enumeration member can’t be combined with the Position one.
VertexFormat.Texture0 through VertexFormat.Texture8: Your vertices include from zero to eight different texture coordinates. Texture coordinates are used to tell the rendering engine how to display a texture (instead of a plain color) relative to the vertices. You’ll get a chance to investigate texture coordinates shortly.
There is one additional technique to note at this point. The best time to create various buffers and objects is just after the DirectX device gets created. Once again, the event handling system of the .NET Framework gives you a convenient event just for this situation, called the DeviceReset event. Every time the DirectX device gets resized or toggled to/from full-screen mode, the device gets reset. When this happens, you need to re-create your vertex buffers. The best way to do this is by registering an event handler for the DeviceReset event like this:
device.DeviceReset += new System.EventHandler (this.OnCreateDevice);
The following code sample shows a complete example, from defining the vertex structure to creating the vertex buffer. Note that Managed DirectX also contains definitions for many common custom vertex formats. The custom vertex struct that follows, in fact, is defined as CustomVertex.TransformedTextured. You’re going to continue using your own special struct for these examples, so that you can get used to how to manually create custom vertex structures.
… // Your Custom vertex format will need to be transformed, and // has information about texturing and diffuse colors. private VertexBuffer vertexBuffer; private const VertexFormats customVertexFormat = VertexFormats.Transformed | VertexFormats.Texture1; private const int numVerts = 36; //need to hold 36 vertices public struct CustomVertex { public float X; public float Y; public float Z; public float rhw; public float tu; public float tv; } … public void OnCreateDevice(object sender, EventArgs e) { Device device = (Device)sender; vertexBuffer = new VertexBuffer(typeof(CustomVertex), numVerts, device, Usage.WriteOnly, customVertexFlags, Pool.Default); vertexBuffer.Created += new System.EventHandler (this.OnCreateVertexBuffer); this.OnCreateVertexBuffer(vertexBuffer, null); //force the call }
The color parameter specifies a color for each vertex. The vertex colors generate gradients between each vertex, as shown in the square in Figure 3-17. The upper-left corner will be rendered with blue, the upper-right with red, the lower-left with yellow, and the lower-right with green.
Figure 3-17.Applying colors to square vertices
You must specify the colors through their RGB components using the Color.FromARGB function. The color codes are the same ones defined in the System.Drawing.Color component. You can’t use the old GDI’s RGB function to specify such color, because it’s intrinsically different from the new Color.FromARGB function, and you can have unexpected results, like the blue and red components being inverted.
Now let’s look at texturing. As you would imagine, texturing is a way of applying an appearance to a polygon, usually by means of a separate image. Two-dimensional textures are generally described in tu and tv coordinates (sometimes you’ll simply see u and v as the coordinate system). All textures have rectangular shapes, and these values range from (0, 0) for the upper-left corner of the texture to (1, 1) for the lower-right corner. The texture is applied to the object according to the values set to all vertices. In Figure 3-18, you see three vertices with valid tu and tv values, the texture loaded, and the result rendered by the device.
Figure 3-18.Texture mapping with (tu, tv) pairs of values
The Device object needs to have the information about which texture it must use for each call of the DrawPrimitives function (explained in the previous section), which will receive the vertex buffer with the vertex and texture coordinates. For this, you must pass a previously loaded texture to the SetTexture method of the device.
You can load the texture from a file using the FromFile method of the TextureLoader helper object, which can receive different parameters depending on the need of the program. To load opaque textures, it will simply receive the filename and the device to which the texture will be rendered. When calling the method to load transparent textures, the functions receive many other parameters, allowing greater control over the loaded texture, including a color key that will specify the transparent color for the texture loaded.
You’ll see the details about how to implement texture features on your program in the next sections. In the following section, we’ll outline the proposal for the sample application of this chapter.
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
Our proposal for this chapter sample is to create a simple application that will help you to understand the basics of DirectX, so you can apply this knowledge to creating games in coming chapters.
To accomplish this, you’ll create an application that will test your machines and return the capabilities of the installed hardware and software, and also run some tests that will give you the necessary information on how to:
Create an application that runs in windowed mode.
Create an application that runs in full-screen mode.
Create an application that shows a transparent sprite using texture capabilities.
Create an application that deals with lighting, using different light colors. Although we won’t explore this feature extensively in this book, it’s quite useful to learn the basics of lighting, so you can create interesting effects in your games.
Create an application that deals with basic matrix transformations, which will be very useful in your games because they provide a built-in capability to translate (move around the screen), rotate around an axis, and scale any preloaded images to different sizes.
You’ll create a separate window for each of the tests listed previously, and all the tests will execute the same drawing procedure—one that will present the walking man textures at full speed on screen in order to give you an idea of how fast your 3-D acceleration board really is.
In the next section, we’ll discuss some extra details about this sample application.
The Application Project
This application project will be very straightforward; you can’t add too much detail to it for now, because you’ll be focusing on what you can do with Direct3D in this chapter.
The coding phase will be divided into six steps, as described in the following list, each one exploring additional features involved in the Direct3D application:
Create a main window with four list boxes that will show you the machine adapters, the devices for each adapter, the display modes for each device, and the device capabilities. From the main window, present the other windows that will do each of the tests defined in the project proposal. The main window is shown in the Figure 3-19.
Figure 3-19.The main window interface
2. Create a DirectX windowed test that will use a set of textures to produce the illusion of a walking man.
3. Adjust the code from the previous step to create a DirectX application that runs in full-screen mode.
4. Create a new DirectX windowed test, from the test created in step 2, to test the use of transparent textures. For this test, you’ll create an image with transparent parts that can be moved with the mouse, so you can see that it’s really transparent.
5. From the test created in step 2, create a new test that will exemplify the use of lighting. For this test you’ll create a control window that will allow you to change each of the RGB components of the diffuse light colors in each of the figure vertices. Figure 3-20 presents the interface that you’ll use to control the light colors.
Figure 3-20.The Light Control window
6. Your last test will demonstrate the use of matrix transformations on 3-D shapes. For this you’ll create a cube and a window that will control the matrix transformations on it. You’ll also add an option to make the figure move automatically while the shape rotates. The matrix transformations control window is shown in Figure 3-21.
Figure 3-21.The MatrixControl window
In the next section, you’ll start coding your application, starting from the main window code.
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
Before you start any coding in your project, you need to set a reference to the Direct3D and DX3D components of Managed DirectX. To add the references, choose Project ? Add Reference, and locate the appropriate components on the list in the .NET components tab. If the components aren’t in the list, then you possibly don’t have the Managed DirectX interface installed on your computer. Because this interface is included with DirectX 9.0, you’ll need to download and install the latest version of the DirectX SDK from the Microsoft DirectX developer site at http://msdn.microsoft.com/directx.
First Step: Coding the Main Window
You’ll start by coding the main window, which will allow you to see your hardware capabilities, and then you can code the tests one by one, from the simpler to the more complex ones.
After creating the main window, as shown in the visual prototype in the project phase, you need to know the functions that list the adapters, devices, display modes supported, and capabilities. A quick look in SDK help shows you these methods and properties of the Manager object:
Adapters.Count: Returns the number of adapters in the machine. Remember that it’s possible for a single graphics board to have more than one adapter.
Adapter[n].Information: Returns the adapter characteristics, according to an ordinal adapter number.
GetDeviceCaps: Returns the device capabilities in a complex structure. The function receives the ordinal number of the adapter and the type of the device (Hardware or Reference). Remember, Reference is software-based and always supported; Hardware is hardware-based and depends on the boards installed.
Adapters[n].SupportedDisplayModes: Returns the characteristics of a specific display mode, given its ordinal number.
CheckDeviceType: Checks if a specific display mode is supported by the current device.
A quick look in the DirectX SDK help will also show you that most of these functions don’t return a readable description (which could be used to fill the list), so you’ll need to create some functions to return display names where appropriate.
Because all the information between the lists are related (the devices supported may vary for each adapter, and the display modes and device characteristics may vary depending on the device), it’s better to force an update of the related list every time a new item is selected on a high-order list. Your program’s basic structure will be as follows:
On the "load" event: Load the adapters list Select the first list item, in order to fire the selection changed event On the adapter list "selected item changed" event: Reload the device list Select the first list item, in order to fire the selection changed event On the device list "selected item changed" event: Reload the display modes list Reload the device capabilities list
Because you’ll be using the Device object all over the form, you can create the variable at form level.
Device device;
In the Load event, you can call the ListAdapters function:
private void FrmDirectX_Load(object sender, System.EventArgs e) { // Fill the Adapters list. ListAdapters(); } public void ListAdapters() { AdapterDetails adapterInfo; // Add each adapter to the LstAdapters list box. foreach(AdapterInformation info in Manager.Adapters) { AdaptersListBox.Items.Add (info. Information. Description); } // Select the first available index, in order to fire the change event. AdaptersListBox.SelectedIndex = 0; } private void FrmDirectX_Closing(object sender, CancelEventArgs e) { if(device != null) { device.Dispose(); } device = null; {
If you run your code now, you’ll see the first list filled with the adapters’ descriptions. The devices list, which must be filled for each adapter chosen, will always have one or two members: the Reference Rasterizer, which will always be present, and a hardware abstraction layer (HAL) rasterizer, which will be present only if supported by a 3-D board. To check the presence of hardware acceleration, you can query the device capacities using the previously shown function, and if there’s no error, then you can add the HAL to your list.
The function for filling the devices list and the code for calling it (in the event that handles the selected item change at the adapters list) is shown in the following code sample:
private void AdaptersListBox_SelectedIndexChanged (object sender,System.EventArgs e){ // Update the devices list every time a new adapter is chosen. ListDevices(AdaptersListBox.SelectedIndex); } public void ListDevices(int adapter) { Caps deviceCaps; // Add each supported device to the DevicesListBox list box. DevicesListBox.Items.Clear(); // The Reference Rasterizer will always be supported. DevicesListBox.Items.Add("Reference Rasterizer (REF)"); // If there’s no error when getting the HAL capabilities, // then you have a hardware acceleration board installed. try { deviceCaps = Manager.GetDeviceCaps(adapter, DeviceType.Hardware); DevicesListBox.Items.Add("Hardware Acceleration (HAL)"); } catch { } // Select the first available index, in order to fire the change event. DevicesListBox.SelectedIndex = 0; }
The display modes will depend on the adapter and the device chosen, so you can create a function (ListDisplayModes) that will receive this information as parameters, and call it on the selection change event of the devices list box.
private void DevicesListBox_SelectedIndexChanged (object sender, System.EventArgs e) { // The first entry in DevicesListBox is the Reference Rasterizer. DeviceType deviceType = (DevicesListBox.SelectedIndex == 0) ? DeviceType.Reference : DeviceType.Hardware; ListDisplayModes(AdaptersListBox.SelectedIndex, deviceType, Format.X8R8G8B8); ListDisplayModes(AdaptersListBox.SelectedIndex, deviceType, Format.X1R5G5B5); ListDisplayModes(AdaptersListBox.SelectedIndex, deviceType, Format.R5G6B5); ListDeviceCaps(AdaptersListBox.SelectedIndex, deviceType); }
Listing the display modes isn’t as straightforward as listing the adapters. First you must check if every mode returned by the adapter is supported by the device, and then you must compose each list item with a combination of various properties that will uniquely identify each display mode as listed here:
Width, Height: The width and height of the screen. If creating a full-screen device, these properties will define the resolution of the screen; when in windowed mode, Direct3D will manage to create the device without errors only if the current display is one of these resolutions.
Format: The format of the display mode, as explained in the section “Understanding Display Modes.”
RefreshRate: The monitor refresh rate, in MHz, or 0 if the default. Usually you don’t have to care about this, but it’s possible for a device to support the same resolution with different refresh rates, so it’s better to list it in your list box, or you could finish with duplicated entries.
Because the Format property returns a member of the Format enumeration, you simply use the ToString() method, available by default to all classes to display the enumeration value. You can now complete the Display Modes list box as follows:
private void ListDisplayModes(int adapter, DeviceType renderer, Format adapterFormat) { DisplayModesListBox.Items.Clear(); foreach(DisplayMode dispMode in Manager.Adapters[adapter]. Supported DisplayModes) { // Check to see if the display mode is supported by the device. if(Manager.CheckDeviceType (adapter, renderer, dispMode.Format, dispMode.Format, false)) { // Fill the display modes list with the width, height, // mode name, and refresh rate. DisplayModesListBox.Items.Add(dispMode.Width + "x" + dispMode.Height +" ( "+ DirectXLists.DisplayModeName(dispMode.Format) + " - " + dispMode.RefreshRate + "Khz)"); } } }
Running your program now, you can see the first three list boxes filled with information, as shown in Figure 3-22.
Figure 3-22.The filled Adapters, Rendering Devices, and Display Modes list boxes
The last list box, which will list the device capabilities, will be a tougher one to fill if you want to have explicit control over what you list. The simplest way to list device capabilities is to simply call the ToString() function of the devCaps variable. However, you want to learn how to access different capabilities in a customized way. Because the function GetDeviceCaps returns a complex structure with many dozens of flags, organized in many different enumerations, you must create functions to return readable strings for each property. You’ll use the descriptions provided in SDK help to create the functions that will list the most important flags for the purposes of this example, but there are some you’ll leave aside. If you want to create a comprehensive list, just follow the steps explained here.
The first function you create checks for some simple flags in the Caps structure and adds the appropriate strings to the Device Capabilities list box.
public static void ListGeneralCaps(Caps devCaps, ListBox listCaps) { listCaps.Items.Add(" -----General Caps ----------------- --------"); if(devCaps.MaxActiveLights == -1) { listCaps.Items.Add("Maximum Active Lights: Unlimited"); } else { listCaps.Items.Add("Maximum Active Lights: " + devCaps.MaxActiveLights); } if(devCaps.MaxPointSize == 1){ listCaps.Items.Add("Device does not support point size control"); } else { listCaps.Items.Add("Maximum point primitive size: "+ devCaps.MaxPointSize); } listCaps.Items.Add("Maximum Primitives in each DrawPrimitives call: " + devCaps.MaxPrimitiveCount); listCaps.Items.Add("Maximum textures simultaneously bound: " + devCaps.MaxSimultaneousTextures); listCaps.Items.Add("Maximum Texture aspect ratio: " + devCaps.MaxTextureAspectRatio); listCaps.Items.Add("Maximum Texture size: " + devCaps.MaxTextureWidth + "x" + devCaps.MaxTextureHeight); listCaps.Items.Add("Maximum matrixes blending: " + devCaps.MaxVertexBlendMatrices); listCaps.Items.Add("Maximum vertex shaders registers: " + devCaps.MaxVertexShaderConst); }
To help you understand specific device capabilities, create many other functions with the same basic structure: a simple sequence of if commands, each one testing for a specific flag within the composed flag members. The following code shows an example of such a function, one that lists the flags that compose the Caps member of DriverCaps:
public static void ListDriverCaps(DriverCaps driverCaps, ListBox listCaps) { listCaps.Items.Add(" -----Driver Caps ------------------------"); if(driverCaps.SupportsDynamicTextures); listCaps.Items.Add("The driver support Dynamic textures"); } if(driverCaps.CanCalibrateGamma) { listCaps.Items.Add("The driver can automatically adjust the gamma ramp"); } if(driverCaps.SupportsFullscreenGamma) { {listCaps.Items.Add("The driver supports dynamic " + "gamma ramp adjustment in full-screen mode. "); } }
Each if statement in this kind of function tests a specific Boolean value inside the composed flag. In this sample, DriverCaps is a structure with many composed flags, each one being a Boolean value associated with a specific driver feature.
You create similar functions to list the flags for the TextureCaps, RasterCaps, DeviceCaps, and TextureCaps members. Because they present the same structure, and the information they add to the list box is basically the one found in SDK help, we won’t reproduce them here; for those interested, they can be found in the downloadable source code.
You can create a special function now that will retrieve the Caps structure for the current device and call the functions created as mentioned previously:
private void ListDeviceCaps(int adapter, DeviceType deviceType) { DeviceCapsListBox.Items.Clear(); Caps caps = Manager.GetDeviceCaps(adapter, deviceType); // List some general driver capabilities. DirectXLists.ListGeneralCaps(caps, DeviceCapsListBox); // List the device capabilities. DirectXLists.ListDevCaps(caps.DeviceCaps, DeviceCapsListBox); // List specific driver capabilities. DirectXLists.ListDriverCaps(caps.DriverCaps, DeviceCapsListBox); // List rasterizer capabilities. DirectXLists.ListRasterCaps(caps.RasterCaps, DeviceCapsListBox); // List texture capabilities. DirectXLists.ListTextureCaps(caps.TextureCaps, DeviceCapsListBox); }
You must include a call to this function in the SelectedItemChanged event handler for the Devices list box, so the list gets updated for every new device chosen in the list. Figure 3-23 presents the finished main window of this chapter sample.
Figure 3-23.The finished main window
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
This first test is very important, because it will establish the base of all future tests and programs. So you’ll make it very simple: Just initialize the Direct3D, create the device, draw a simple image, and count the frame rate. In order to allow you to see something happening, you’ll load an array of images (loaded as textures) and render them one at a time, over a square (composed of two triangles), so you’ll see the illusion of a walking guy.
You’ll use the basic Direct3D program structure, explained in the “Creating a Simple Direct3D Program” section, dividing the code into two groups:
In your main window (coded in the click event for the corresponding button): The code that simply creates (and destroys) the test window and call to the initialization, finalization, and rendering routines defined in the test window
In the test window: All the Direct3D routines—initialization, finalization, and rendering
The code for the main window, which will be very similar to other tests, is show here:
using (WindowTest windowTest = new WindowTest()) { int desiredFrameRate = 10; int lastTick = 0; windowTest.Show(); // Initialize Direct3D and the Device object. if(!windowTest.InitD3D(windowTest.Handle)) { MessageBox.Show("Could not initialize Direct3D."); windowTest.Dispose(); return; } else { // Load the textures and create the square to show them. if(!windowTest.CreateTextures()) { MessageBox.Show("Could not initialize vertices and textures."); return; } } // Uncomment the lines below to see a smooth walking man. //desiredFrameRate = 10; if(System.Environment.TickCount - lastTick >= 1000 / desiredFrameRate) { windowTest.Render(); // Frame rate calculation. windowTest.Text = "Window Test. Frame rate: " + DirectXLists.CalcFrameRate().ToString(); lastTick = System.Environment.TickCount; } Application.DoEvents(); // If you have no errors, then enter the rendering loop. while(!windowTest.EndTest) { // Uncomment the lines below to see a smooth walking man. // Force a Frame rate of 10 frames to second on maximum //if(System.Environment.TickCount - lastTick >= 1000 / desiredFrameRate) { windowTest.Render(); // Frame rate calculation. windowTest.Text = "Window Test. Frame rate: " + DirectXLists.CalcFrameRate().ToString(); //lastTick = System.Environment.TickCount; //} Application.DoEvents(); } }
In the rendering procedure you use a helper function, CalcFrameRate, that you create in order to make your code cleaner. In this function (shown in the next code listing), you use System.Environment. Tick Count to retrieve the current tick of the processor clock (with the precision rate of about 15 milliseconds), so you can calculate the frame rate. Note that this function isn’t very accurate, but since you’ll only use frame rate calculations to give you an idea of the speed at which you’re drawing the scene, and won’t include it in your final games, we think that using it is a valid approach.
Following the sequence of the code just shown, let’s see the initialization routines for the WindowTest class. The InitD3D procedure will create the Direct3D object, define the presentation parameters for the window creation based on the current display mode, and create the Device object. If you don’t understand any part of the following code, refer to the first sections of this chapter for detailed explanations.
The most important part in the preceding code is the definition of the presentation parameters, which will rule the device creation. Let’s analyze this one line at a time.
In the first line of the code listing, you create the presentation parameters as an object of the PresentParameters type:
PresentParameters presentParams = new PresentParameters();
Then you state that you want to run in windowed mode. Because you didn’t specify the window size, the device will use the whole client area of the target window (defined by the handle used when creating Device).
presentParams.Windowed = true;
In the next line, you instruct the device to choose the best memory allocation when doing the screen flips, even if your back buffer got discarded. Note that this option doesn’t force the back buffer to be discarded, it just tells the device that you are re-creating the whole scene in the Render procedure, so it doesn’t need to preserve the contents of the back buffer when flipping.
presentParams.SwapEffect = SwapEffect.Discard;
The last line specifies the format of your back buffer. Because you’re running in windowed mode, it’s a must for you to use the current display mode format, because the window will be rendered using the same resolution and colors of the rest of the screen.
presentParams.BackBufferFormat = DispMode.Format;
The next function, following the main program sequence, is the one that will load the textures from disk and create a square in which to display them. To create such a function, first refer to the flexible vertices format (FVF) definition in the “Coloring and Texturing with Flexible Vertex Formats” section. You see that you’ll need to create a custom vertex type that will hold texture information in addition to the x, y, and z coordinates. And because you don’t want to make any 3-D transformations, you’ll create the vertex with an extra flag (rhw, which stands for “reciprocal of homogeneous w”) that informs the device that the coordinates are already transformed (they are screen coordinates). The definition of your VertexFormat is made using a constant value and creating the corresponding structure.
// Simple textured vertices constant and structure. private const VertexFormats customVertexFlags = VertexFormats.Transformed | VertexFormats.Texture1; private struct CustomVertex { public float X; public float Y; public float Z; public float rhw; public float tu; public float tv; }
In order to help you fill the VertexFormat structure for each new vertex, it’s a good idea to create a helper function that fills the structure members and returns the vertex, as show in the following code snippet:
Now you can start thinking about the CreateTextures routine. Based on the basic concepts shown earlier, you can create a draft for the function as follows:
Define the array of textures (must be public to the form, because it’ll be used in the Render procedure).
Create the textures for each array element.
Create and open the vertex buffer.
Define the vertices.
Close the buffer.
The textures you’ll be using show a draft of the walking man, and are numbered from walk1.bmp to walk10.bmp, as shown in Figure 3-24.
The code for the previous steps is shown next.
NOTE Notice that you create a separate function to generate the vertices, so the code becomes more readable and more easy to expand with different vertices.
Figure 3-24.Walking man textures, from walk1.bmp to walk 10.bmp (courtesy of Igor Sinkovec)
private const int numVerts = 4; private VertexBuffer vertBuffer = null; private Texture[] Textures = new Texture[10]; public bool CreateTextures() { CustomVertex[] verts; try { string textureFile; // Load the textures, named from walk1.bmp to walk10.bmp. for(int i=1; i<=10; i++) { textureFile = Application.StartupPath +@"\walk"+i.ToString() +".bmp"; textures[i-1] = TextureLoader.FromFile(device, textureFile); } // Define the vertex buffer to hold your custom vertices. vertBuffer = new VertexBuffer(typeof(CustomVertex), numVerts, device, Usage.WriteOnly, customVertexFlags, Pool.Default); // Locks the memory, which will return the array to be filled. verts = vertBuffer.Lock(0, 0) as CustomVertex[]; // Defines the vertices. SquareVertices(verts); // Unlocks the buffer, which saves your vertex information to the device. vertBuffer.Unlock(); return true; } catch { return false; } } private void SquareVertices(CustomVertex[] vertices) { // Create a square, composed of 2 triangles. vertices[0] = CreateFlexVertex(60, 60, 0, 1, 0, 0); vertices[1] = CreateFlexVertex(240, 60, 0, 1, 1, 0); vertices[2] = CreateFlexVertex(60, 240, 0, 1, 0, 1); vertices[3] = CreateFlexVertex(240, 240, 0, 1, 1, 1); }
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
With all the textures and vertices loaded, all you need now is to code the Render procedure to load one texture at a time and a finalization routine to dispose the used objects. The Render routine follows the structure of the scene starting, ending, and being presented, as shown earlier.
private static int x = 0; public void Render() { if(device == null) { return; } // Clears the device with blue color. device.Clear(ClearFlags.Target, Color.Blue, 1.0F, 0); device.BeginScene(); // Show one texture a time, in order to create the illusion of a walking guy. device.SetTexture(0, textures[x]); x = (x == 9) ? 0 : x+1; //If x is 9, set to 0, otherwise increment x // Define which vertex buffer should be used. device.SetStreamSource(0, vertBuffer, 0); device.VertexFormat = customVertexFlags; // Draw the vertices of the vertex buffer, rendering them as a // triangle strip, using the given texture. device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, numVerts - 2); device.EndScene(); // Using an extra try-catch will prevent any errors if the device was disposed. try { // Present the rendered scene. device.Present(); } catch { // Normally, you would put special exception handling in here. } }
Note that we don’t include any mention of back buffers or screen swapping (flipping) operations here, so why do you care about these in the Device object creation? In fact, everything is done here, but is performed in the background by the device: The back buffer is cleared using the Clear command, it’s locked for drawing using the BeginScene method, it’s unlocked after you render the scene with the EndScene function, and it’s finally flipped to the screen, and maybe discarded, using the Present method.
The final routine just disposes of all objects created in the previous functions, and it’s called by the main program automatically whenever the WindowTest form exits or is closed.
This last function ends the sample. After coding a simple escape routine, which will end the form when the Esc key is pressed, you can run your sample and see the results, as presented in Figure 3-25.
To make your sample run in full-screen mode, all you need to do is change the presentation parameters in the InitD3D routine. In order to have all sample code sections separated from each other, you’ll create a new button in the main window to fire the full-screen mode. Because most of the code will be the same, you can copy all the code from the windowed mode and simply apply the following updates.
Let’s analyze the code for setting the presentation parameters, line by line.
The initial lines are the same from the windowed mode; just gather information about the current display mode and create the presentation parameters object.
Following the definition, you set the parameters for creating the back buffer. In this example, you’ll be using the current format, width, and height (you must specify these three parameters); but you could be using any of the formats or resolutions shown in your Display Modes list on the main screen.
The last line is the same as the one in the windowed mode: It sets the flipping operation to the one that has the best performance, instructing the device not to care about preserving the back buffer.
presentParams.SwapEffect = SwapEffect.Discard;
NOTE Using the Discard swap effect forces the use of only one back buffer, so you don’t need to set the BackBufferCount property to 1. Another important point is that you don’t worry about setting the Windowed property to false, because running full screen is the default.
It’s enough to make your code run in full-screen mode, but you can make a simple improvement in your SquareVertices function to create a square that covers the entire screen, stretching the walking man textures to generate a nicer effect. You can gather the screen resolution, using the same method you saw before, with a display mode object. Your final function will be as follows:
Just run the program now and press the Full Screen button in the main window to see the textures applied to the entire screen, with no visible loss in the frame rate, as presented in Figure 3-26.
Figure 3-26.Running your DirectX program in full-screen mode
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
You’ll use the same code employed for the windowed mode test as the basis for your transparency test. You’ll still have the walking man as a background texture, and will load another texture over it, with a color set to transparent, so you can see the man running behind parts of it.
For this purpose, create a window drawing and fill the panes and surrounding areas with blue color, as shown in Figure 3-27.
Figure 3-27.A window, with a flat blue color to be used as a transparent texture
Follow these steps for including your new transparent texture in the sample:
In the InitD3D routine, set the device parameters to indicate that you’ll be using transparent textures.
Create a new function that will load the transparent texture.
Create a function to generate a new square in which you’ll render the transparent texture.
Change the click button event in the main window to call this function.
Adjust the Render procedure to show the transparent texture.
As defined in the game project, you must call the function that creates the square on the MouseMove event of the test window, so you can move the square with the transparent texture to different parts of the window and see the resulting effects.
Let’s start with the InitD3D function. All you need to do is to set three new parameters of the device.
These parameters tell how the rendering must blend together the source and the destination bitmaps to achieve the final transparency effect, and the last one informs the device where you want the blending to occur. The parameters shown in the preceding code must apply to almost all cases, and will be used throughout the rest of this book.
Please note that the blending operation slows performance, so in your real games you’d only set the AlphaBlendEnable property to True just before drawing the transparent textures, and reset it after finishing them. Because this is a test, just leave it set all the time—performance isn’t your preoccupation here.
The function for loading the transparent texture is slightly different from the one you saw in the previous samples, as you can see in the next code piece, which needs to be included in the CreateTextures procedure to add transparency support:
// You will use blue as the transparent color. Color colorKeyVal = Color.FromArgb(255, 0, 0, 255); // Load the transparent texture. TranspTexture = TextureLoader.FromFile(device, Application.StartupPath + @"\TranspSample.bmp", 64, 64, D3DX.Default, 0, Format.Unknown, Pool.Managed, Filter.Point, Filter.Point, colorKeyVal.ToArgb());
Well, okay, this is VERY different. Although you can load an opaque texture specifying only the device and the filename, the overloaded version of the function to load transparent textures will have a lot more features and flexibility (but it’ll have worse performance, too). We won’t enter into the details about every parameter, because we won’t use most of them in this book. All you need to know for now is the following:
The 64, 64 parameters represent the width and height of the texture being loaded. These must be values supported by the device, usually a power of 2 (16, 32, 64, 128, and so on, with some new boards going up to 4096). These values are automatically calculated in the simpler version of the function.
The filter parameters presented here are the best performing ones. If you want a little more quality, you can change them from Filter.Point to Filter.Default.
The ColorKey parameter receives the color that will be transparent. In this case, the alpha component of the color is significant: If you are loading images from file formats that don’t support transparency (such as bitmaps), this value will be always 255 (opaque).
The next step is to create a new square to load your transparent texture into. You can copy the functions used in the first sample, and adapt them to receive the x and y coordinates for the texture. Remember, you’ll move the texture with the mouse, and the only way to do it (for now, because we haven’t discussed transformation matrices yet) is updating the vertex positions, one by one.
public bool CreateTransparentVertices(float x, float y) { CustomVertex[] verts; try { // If the vertex buffer was previously created, dispose them. if(TranspVertBuffer != null) TranspVertBuffer.Dispose (); TranspVertBuffer = new VertexBuffer(typeof (CustomVertex), numVerts, device, Usage.WriteOnly, customVertexFlags, Pool.Default); verts = TranspVertBuffer.Lock(0, 0) as CustomVertex []; TranspVertices(x, y, verts); TranspVertBuffer.Unlock(); return true; } catch { return false; } } private void TranspVertices(float X, float Y, CustomVertex [] vertices) { // Create a square, composed of 2 triangles. //Our transparent texture is 42 pixels wide and 60 long. vertices[0] = CreateFlexVertex(X, Y, 0, 1, 0, 0); vertices[1] = CreateFlexVertex(X + 42, Y, 0, 1, 1, 0); vertices[2] = CreateFlexVertex(X, Y + 60, 0, 1, 0, 1); vertices[3] = CreateFlexVertex(X + 42, Y + 60, 0, 1, 1, 1); }
To adjust the click event for the button on the main form, all you need to do is call the preceding function, passing a default position for the transparent window. The full procedure for the Click button is as follows:
using (TransparentTest transparentTest = new TransparentTest()) { transparentTest.Show(); // Initialize Direct3D and the Device object. if(!transparentTest.InitD3D(transparentTest.Handle)) { MessageBox.Show("Could not initialize Direct3D."); transparentTest.Dispose(); return; } else { // Load the textures and create the square to show them. if(!(transparentTest.CreateTextures() && transparentTest.CreateTransparentVertices(0, 0))) { MessageBox.Show("Could not initialize vertices and textures."); transparentTest.DisposeD3D(); transparentTest.Dispose(); return; } } // If you have no errors, then enter the rendering loop. while(!transparentTest.EndTest) { transparentTest.Render(); // Frame rate calculation. transparentTest.Text = "Transparency Test. Frame rate " + DirectXLists.CalcFrameRate().ToString Application.DoEvents(); } }
Adjusting the rendering function is just as easy, as there’s no difference in the rendering when displaying a simple texture or a transparent one. You can just add the following lines of code in the Render procedure, just below the lines that draw your walking man:
Because the background of your transparent bitmap is blue, maybe it’s a good idea to change the window background to black, just to create a different look from the previous samples. You can do this by simply adjusting the call to the Clear method of the Device object to:
All you need to do now is code the MouseMove event to call CreateTranspVertices. Because you receive the mouse x and y positions as arguments on the event, all you need is this code:
And that’s it. Running your sample will allow you to test your transparent window by moving it with the mouse over the walking man, as shown in Figure 3-28.
Figure 3-28.Testing the transparent window
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
You can use the same code you created for testing DirectX in windowed mode to also do your diffuse colored light test.
Although all you need to do to test the use of diffuse light is change the flexible vertex format to support a color value per vertex, and set such values for the vertices, you’ll stick to your project and create a light control window in which you can choose the RGB components for the light color on each vertex.
The light control window, shown in Figure 3-20, is composed of four tabs, and each tab has three numeric up-down controls. You name these controls starting with Red1, Green1, Blue1 for the first vertex through to Red4, Green4, Blue4 for the fourth vertex. You’ll use the values of each control directly on the color definition for the vertices.
The steps for converting the first sample to implement light control are as follows:
Adjust the flexible vertex format structure and constant used in the vertex buffer creation to accept the color component for each vertex.
Adjust the helper function CreateFlexVertex to accept the color parameter.
Adjust the SquareVertices function to create the vertices using colors as defined by the numeric up-down controls.
Adjust the click button procedure to create the control window and the test window, and initialize the values of the vertices colors.
Create an event procedure that will update the vertex colors when any color component for any vertex changes.
NOTE The first two steps are very connected; every time you change the structure you’ll need to change the constant and your helper function (you’ll do it again in the next test, when you’ll deal with matrices).
The new code for implementing light control is shown next:
private const VertexFormats customVertexFlags = VertexFormats.Transformed | VertexFormats.Diffuse | VertexFormats.Texture1; private struct CustomVertex { public float X; public float Y; public float Z; public float rhw; public int color; public float tu; public float tv; } private CustomVertex CreateFlexVertex(float X, float Y, float Z, float rhw, Color color, float tu, float tv) { CustomVertex custVertex = new CustomVertex(); custVertex.X = X; custVertex.Y = Y; custVertex.Z = Z; custVertex.rhw = rhw; custVertex.color = color.ToArgb(); custVertex.tu = tu; custVertex.tv = tv; return custVertex; }
The SquareVertices function will be the same used in the previous samples (except for the full screen one), with the solo update in passing the color parameter for the CreateFlexVertex helper function.
To define the color, you’ll use the Color.FromARGB function you used before (when choosing a blue color for clearing the device).
The test start procedure, defined in the Click button on the main form, will be very similar to the ones you saw before: It follows the same structure, but creates both test and control windows, and takes special care in initializing the values of all the numeric up-down controls to 255 to fill the vertices with white light, so the walking man image starts with no color distortion (the default value is zero, which would prevent you from seeing anything).
LightControl winLightControl = new LightControl(); using (LightTest lightTest = new LightTest()) { winLightControl.Show(); lightTest.Show(); // Initialize Direct3D and the Device object. if(!winLightControl.InitD3D(lightTest.Handle)) { MessageBox.Show("Could not initialize Direct3D."); winLightControl.Dispose(); }
else { // Load the textures and create the vertices. if(!winLightControl.CreateTextures()) { MessageBox.Show("Could not initialize the textures and vertices."); winLightControl.DisposeD3D(); winLightControl.Dispose(); } } // Start with full white light in all vertices. winLightControl.Red1.Value = 255; winLightControl.Green1.Value = 255; winLightControl.Blue1.Value = 255; winLightControl.Red2.Value = 255; winLightControl.Green2.Value = 255; winLightControl.Blue2.Value = 255; winLightControl.Red3.Value = 255; winLightControl.Green3.Value = 255; winLightControl.Blue3.Value = 255; winLightControl.Red4.Value = 255; winLightControl.Green4.Value = 255; winLightControl.Blue4.Value = 255; // Ends the test if ESC is pressed in any of the 2 windows. while(!winLightControl.EndTest && !lightTest.EndTest) { winLightControl.Render(); // Frame rate calculation. lightTest.Text = "Light Test. Frame Rate: " + DirectXLists.CalcFrameRate().ToString(); Application.DoEvents(); } }
The last step to make your code fully operational is including a call to update the vertex colors every time one vertex color has changed. Because the values of the controls are being read directly in the CreateVertices procedure, you can simply call this procedure on an event that handles changing in all numeric up-down controls:
Just run your program now, and play a little with the vertex light colors. Figure 3-29 shows a sample color distorted window.
Figure 3-29.Your old friend walking man in a disco
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
Adapting the sample to test the matrix transformations, according to what we discussed earlier in this chapter, will be your last and hardest challenge; but if you missed some previous point, this is the perfect way to reinforce the concepts.
Because you’re facing a lot of modifications in many procedures, let’s see all the code for this sample, starting with the vertex definition. Rather than using the flexible vertex format structure that you’ve already seen, you’ll instead use one of the several predefined vertex formats defined in Managed DirectX. In this case, you want to use a simple vertex format that supports position and texture, but you’ll abandon the rhw parameter that indicated in the previous samples that you were working on screen (already transformed) coordinates. In this sample, you’ll test all the transformations from world coordinates to screen coordinates. Such a format is defined in the CustomVertex class as the static type PositionedTextured, which contains values for the x, y, and z coordinates, as well as the tu and tv texture coordinates.
Now, instead of calling the CreateFlexVertex method, you simply invoke the custom vertex constructor, like this:
CustomVertex.PositionedTextured cv = new CustomVertex.PositionTextured(0, 0, 0, 0, 0);
Returning to the first example, note that you have an initialization function and a finalization function, which creates the objects you need and destroys them when the window is being closed. Although the DisposeD3D finalization procedure needs no modifications (it just disposes every object), the InitD3D procedure for this sample deserves a closer look, because you have some significant modifications, which appear in bold in the subsequent code:
public bool InitD3D(IntPtr winHandle) { DisplayMode DispMode = Manager.Adapters[Manager.Adapters.Default.Adapter]. CurrentDisplayMode; PresentParameters presentParams = new PresentParameters (); // Define the presentation parameters. presentParams.Windowed = true; presentParams.SwapEffect = SwapEffect.Discard; presentParams.BackBufferFormat = DispMode.Format; presentParams.EnableAutoDepthStencil = true; presentParams.AutoDepthStencilFormat = DepthFormat.D16; // Try to create the device. try { device = new Device (Manager.Adapters.Default.Adapter, DeviceType.Hardware, winHandle, CreateFlags.SoftwareVertexProcessing, presentParams); // Turn off culling => front and back of the triangles are visible. device.RenderState.CullMode = Cull.None; // Turn off D3D lighting. device.RenderState.Lighting = false; // Turn on ZBuffer. device.RenderState.ZBufferEnable = true; device.VertexFormat = customVertex; // Set the Projection Matrix to use an orthogonal view. device.Transform.Projection = Matrix.OrthoLH(300, 200, -200, +200); return true; } catch { return false; } }
Because you’re working in a 3-D world now, you need to instruct Direct3D to calculate which drawing primitives are shown and which aren’t. This is made by setting the EnableAutoDepthStencil member of the presentation parameter to true (yes, you want a depth stencil to be used) and setting the AutoDepthStencilFormat to DepthFormat.D16 (16 bits will be used in the calculation, because this is the value most commonly supported by the current 3-D boards). You’ll also need to turn on the z-buffer (another name for depth buffer or depth stencil) calculation for the device.
There are two other important settings here: the one that disables the drawing primitives’ culling (so the textures will be drawn in the front face) and the one that turns off the lighting for your 3-D world (in other words, the one that tells the device to light everything equally). A nice test is to comment out each of these lines and see the resulting effects.
The last bold line defines an orthogonal projection matrix to be used when converting the world coordinates to screen ones, with a viewport of 300 pixels wide and 200 pixels tall. This is the simplest projection type, but the z-axis translation will have no effect (you won’t see the cube getting smaller when it’s far from the screen).
After initializing the objects, you need to load the vertices and textures. You can create a CreateCube function that will initialize and lock the vertex buffer, and then set up an event handler to respond anytime the vertex buffer needs to be re-created (for instance, whenever the device gets reset). That handler, called OnVertexBufferCreate, will also be initially called to give all the vertices their initial values.
The OnVertexBufferCreate function will create each of the vertices of the cube, providing their 3-D coordinates. It’s always a good idea to have a paper and a pencil at hand when creating simple 3-D models, so you can draft the figure and understand better how the vertices fit together. Just take a look at Figure 3-30 and compare it to the first lines of the OnVertexBufferCreate function; because the lines that created the other vertices are very similar, we are showing here just the vertices for the first two facets.
Figure 3-30.The cube 3-D coordinates for the first two facets
TIP Observe that many duplicated vertices appear in the previous sample code. Opt to use a triangle list, as it would be difficult (and a lot less clear for these purposes) to use a composition of triangle strips; but in a real game it’s always good practice to try to reduce the number of vertices.
Your render procedure will have no difference from the previous samples, except for the inclusion of an automatic generation of a rotation matrix (in bold), as defined in the game project, which will move the cube around according to the processor clock tick.
public void Render() { int Tick; if ((device==null)) return; // Move the cube automatically. if (chkAuto.Checked) { Tick = Environment.TickCount; device.Transform.World = Matrix.RotationAxis( new Vector3((float)Math.Cos((double)Tick/3000.0F), 1, (float)Math.Sin((double)Tick/3000.0F)), Tick/3000.0F); } device.Clear(ClearFlags.Target|ClearFlags.ZBuffer, Color.FromArgb(255, 0, 0, 255), 1.0F, 0); device.BeginScene(); // Show one texture at a time in order to create the illusion of a walking guy. device.SetTexture(0, textures[x]); x = (x == 9) ? 0 : x+1; //If x is 9, set to 0, otherwise increment x device.SetStreamSource(0, vertBuffer, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, numVerts/3); device.EndScene(); try { device.Present(); } catch { // This can lead to an error if the window is closed // while the scene is being rendered. } }
Note that the rest of the rendering code is exactly the same as that of the previous samples.
The last part of your test is to update the Transform.World matrix device member to the values set in the numeric up-down controls, as defined in the visual prototype in the project phase.
Using the trick you learned in the light sample, you can create a single procedure that will handle the events for all the controls. In order to make your code more understandable, create three helper functions that will add the rotation, translation, and scale transformations to the world matrix.
private void Transformations_ValueChanged(object sender, System.EventArgs e) { if(device != null) { device.Transform.World = Matrix.Identity; RotationMatrices((float)RotationX.Value, (float) RotationY.Value, (float)RotationZ.Value); TranslationMatrices((float)TranslationX.Value, (float)TranslationY.Value, (float)TranslationZ.Value); ScaleMatrices((float)ScaleX.Value,(float) ScaleY.Value, (float)ScaleZ.Value); } } // The following functions create the transformation matrices for each operation. public void RotationMatrices(float x, float y, float z) { device.Transform.World = Matrix.Multiply (device.Transform.World, Matrix.RotationX((float)(x * Math.PI / 180))); device.Transform.World = Matrix.Multiply (device.Transform.World, Matrix.RotationY((float)(y * Math.PI / 180))); device.Transform.World = Matrix.Multiply (device.Transform.World, Matrix.RotationZ((float)(z * Math.PI / 180))); } public void TranslationMatrices(float x, float y, float z) { device.Transform.World = Matrix.Multiply (device.Transform.World, Matrix.Translation(x, y, z)); } public void ScaleMatrices(float x, float y, float z) { device.Transform.World = Matrix.Multiply (device.Transform.World, Matrix.Scaling(x / 100, y / 100, z / 100)); }
The most important part of this code is to remember that you can add transformations by multiplying the matrices (using the Multiply method of the Matrix object). In the Transformations_ValueChanged event procedure, you use the Matrix.Identity function to reset any transformations in the Transform.World matrix, so you can be sure that any matrix multiplication that occurred in the last call to this function is ignored and doesn’t affect the current matrices.
To finish the code and start the test, all you must take care of is to provide good starting values for your matrix transformations; setting the Scale up-down controls with the default value of zero, for example, will simply make your object disappear from screen.
The code for the click event on the button of the main form is as follows:
using (MatrixControl matrixControl = new MatrixControl()) { MatrixTest matrixTest = new MatrixTest(); matrixControl.Show(); matrixTest.Show(); // Initialize Direct3D and the Device object. if(!matrixControl.InitD3D(matrixTest.Handle)) { MessageBox.Show("Could not initialize Direct3D."); matrixControl.Dispose(); return; } else { // Load the textures and create the cube to show them. if(!matrixControl.CreateCube()) { MessageBox.Show("Could not initialize geometry."); matrixControl.DisposeD3D(); matrixControl.Dispose(); return; } } // Start with a simple rotation, to position the cube more nicely, // and with no scale (100% of the original size). matrixControl.RotationX.Value = 45; matrixControl.RotationY.Value = 45; matrixControl.RotationZ.Value = 45; matrixControl.ScaleX.Value = 100; matrixControl.ScaleY.Value = 100; matrixControl.ScaleZ.Value = 100; // Ends the test if ESC is pressed in any of the 2 windows. while(!matrixControl.EndTest && !matrixTest.EndTest) { matrixControl.Render(); // Frame rate calculation. matrixTest.Text = "Matrix Tests. Frame Rate: " + DirectXLists.CalcFrameRate().ToString(); Application.DoEvents(); } }
Now you can finally run the test. Modifying the values of the numeric up-down controls in the control window will let you see the transformation occurring dynamically; choosing the Auto Move check box will make the cube perform some nice moves automatically on screen. Figure 3-31 shows an example result of this last test.
Figure 3-31.A moving cube with a walking man in each face
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
Because this chapter features no games, there’s no such thing as “polishing the application.” But there’s at least one thing you can improve in the samples that will surely be useful in the next chapters: finding a way to create smooth animations.
Although it is very interesting seeing the walking man running at 400 steps per second, in a real game this kind of behavior will be, at a minimum, strange. So you’d better define a specific frame rate to improve your graphics animation.
Including an if command in the loop that calls the Render procedure to check the processor clock and just render a new scene at previously defined intervals will suffice to give the desired effect in your test, and maybe even in some basic games. In more sophisticated ones, where different objects can have different animations running at different speeds, the control of what image must be shown at a given time will be the responsibility of each game object.
So let’s get into a practical example. Which frame rate would be nice? Well, the best cartoons use a 32 frames-per-second (fps) rate of animation, but usually 16 fps provides a good frame rate. The actual best frame rate must be calculated for each game (or each game object), because different animations require different frame rates. For instance, you can do a walking man with 5, 10, or 20 frames. The more frames, the smoother the final animation will be, and the higher the frame rate must be. For this specific walking man animation, the rate to acquire the best results is only 10 fps. So you’ll use that.
In the following code sample, you define the frame rate for the animation by setting the number of frames with the DesiredFrameRate variable:
int desiredFrameRate = 10; int lastTick = 0; while(!windowTest.EndTest) { // Force a Frame rate of 10 frames a second at most. if(System.Environment.TickCount - lastTick >= 1000 / desiredFrameRate) { windowTest.Render(); // Frame rate calculation. windowTest.Text = "Window Test. Frame rate: " + DirectXLists.CalcFrameRate().ToString(); lastTick = System.Environment.TickCount; } Application.DoEvents(); }
The result (a screen drawn at a fixed frame rate, and a man walking at normal speed) is shown in Figure 3-32.
Figure 3-32.Your walking man, tired of running, now walks at a lazy rate of 10 fps.
Note that you still continue with the loop running at full speed. In your tests, all the loop does when it’s not rendering is process the application events, but you could use an else clause with this if statement to process any internal calculation only when the screen isn’t being drawn. The basic idea is shown in the following code:
if(System.Environment.TickCount - lastTick >= 1000 / desiredFrameRate) { //Do the game scene rendering. } else { //Do the game physics. //Calculate collisions. // Initialize anything that can help the scene to draw faster. // etc. }
More About DirectX and GDI+
After learning the basics and seeing the power behind the DirectX world, it’s time to think how GDI+ and DirectX fit together and how to choose either one (or both) as a basic technology for a game project.
In a general way, you can say that GDI+:
Is a technology to draw 2-D graphics
Is the “native” library for working in Windows
Is more easily ported to other devices (like Pocket PC)
Won’t use any extended graphics or acceleration features, even when there’s a hardware accelerator present
Is easy to work with
And you can say that DirectX:
Is mainly aimed at working with 3-D graphics, but has many features that can be used in 2-D graphics
Has special requirements to run games (needs installation)
Is more easily ported to the Xbox console
Can use all the power of graphics acceleration devices
Needs a little more effort to get started
Summary
In many situations, choosing DirectX is a must, such as when you are coding a 3-D game engine, or when you want to code a fast-paced action game that will need to use hardware acceleration features. But there are other situations in which using GDI+ is perfectly fine. Let’s see some examples.
Imagine again that you are coding a Sid Meyer’s Civilization I clone. Is there really a need to use DirectX? Remember that, although many people have 3-D boards nowadays, not everyone has one, and creating a game that doesn’t require such hardware will broaden your game audience. And because a game like this isn’t graphics intensive (the graphics aren’t very sophisticated, and the frame rate isn’t a problem), it’ll be better to center your efforts on creating more sophisticated algorithms, which can run faster and make better gameplay. No gamer likes to wait for the computer to “think” about a better move.
When talking about simpler games, like Minesweeper or Solitaire, there’s no need at all to use DirectX. A simpler solution, besides providing the benefits explained in the previous paragraph, will lead to a game that is easier to debug and easier to maintain, maybe resulting in a more sophisticated version.
Even when talking about arcade games, when you deal with games with few animations (Breakout-like games are a good example), you can stay with GDI+ without fear of choosing the wrong platform.
Simply put, GDI+ is good for many kinds of games, but DirectX has an incredible number of benefits if you’re willing to invest a little more time in development. So before starting any new game project, think carefully about which platform is the best for your goals.
And let’s highlight an important point: You can use both techniques in a game. All you need to do is isolate the GDI+ code from the DirectX code by not using any GDI+ code between the BeginScene and EndScene methods. The better approach is to create a separate function for any GDI+ code, which will be called after the call to the Render procedure.
Acknowledgments
The walking man drawings used in this chapter were made by Igor Sinkovec, a graphic artist and game programmer.
This chapter is from Beginning .NET Game Programming in C# by David Weller et. al.(Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.