| |
|
|
||||
![]() |
||||||
| |
|
|||||
|
2D
Programming in a 3D World: Implementing the Fundamentals in Direct3D 8 From this point forward I will refer to the new DirectX Graphics component as Direct3D 8, for the sake of clarity. Some of the changes made with Direct3D 8 result in a much simpler and cleaner setup and initialization. To some extent the philosophy is a "black box" approach where many details are hidden from the developer, and this does mean that less code is required to get the API up and running. See Listing 2 for an entire Windows application that initializes Direct3D 8 and gets us ready to roll. For the sake of this article, only a bare minimum is presented here, and only for full-screen mode, but you can see that the startup sequence is quite straightforward. Now, how do you get those graphics to the screen? In 3D terminology, what needs to be done is to "render a textured quad with an orthogonal view." This is actually quite simple in plain English -- a quad is a square or rectangle, and a texture is just a bitmap image. Drawing textures on polygons (triangles, squares, whatever) is a basic function of 3D hardware. The orthogonal view means that you'll be looking straight down on it, no perspective effects. This concept is also sometimes called "billboard textures," referring to the fact that the object you are looking at is actually flat and two-dimensional, like a billboard sign.
If you are particularly ambitious you can implement a 2D engine in Direct3D 8 by writing a library of routines to create, calculate, and display textured quads. Be prepared, however, for some late-night sessions of learning new terminology, new concepts, and few good examples. Luckily, the Microsoft DirectX team has given us some hidden gems inside their D3DX Utility Library. This is an additional library of code separate from the DirectX API itself, but providing some very useful helper and support functions. Some of the functions will load your graphics to a surface from a good selection of file formats (including .JPEG, .PNG and .TGA as well as the standard .BMP bitmaps), and some will do various conversions and calculations automatically. For the 2D programmer, the most significant D3DX utility functions are part of the ID3DXSprite interface, which "provides a set of methods that simplify the process of drawing sprites using Microsoft Direct 3D." If you research this interface in the DirectX SDK docs, the first thing you'll notice is that they are rather sparsely documented. The second thing is a complete lack of examples -- in fact, none of the three dozen or so example programs provided with the SDK uses this interface. Then if you dig deeper and try things out, you'll find out that the documentation that is there is actually incorrect in a few places. But all this should not discourage you because of one fact: it does work, and in fact it works quite well. The heart of the ID3DXSprite interface is the Draw() function, which provides all the functionality to do a good ol' blit. Inside this routine there is a lot going on; specifically, it sets a series of "render states" to get the hardware ready to draw how you want it to, it creates the vertices that define the location of the on-screen quad, it sets the texture coordinates, it sets the camera view angles, and then it sends all the info to the 3D hardware which then draws the texture (your image) in the right place on the screen. But all this happens under the hood, and you can get this to work even if you can't tell a vertex from a vortex. We will shortly write a wrapper function to ID3DXSprite::Draw that will mimic all the functionality of the old DirectDraw Blt(). Before we go any further, it is time to clear up some potential confusion. Some of you may be asking if the ID3DXSprite interface has anything to do with point sprites, which are often mentioned in Direct X docs that discuss the new features of version 8. The answer is nothing at all, nada, zip. They're entirely different things, so forget you ever heard about point sprites. You may also be wondering about the ID3DXSprite interface itself -- a pointer to this interface is created by calling D3DXCreateSprite(). The docs say that D3DXCreateSprite() "creates a sprite object," and that it returns an address "representing the created sprite." Very confusing and inaccurate terminology, since this function doesn't actually do anything with "sprites" at all, and (with the exception of some internal data structures) it doesn't even "create" anything. Instead, it returns a pointer to the ID3DXSprite interface that you use for accessing the sprite functions. All you need to do is call it once when you create (or reset) the device, and release it when you close your application. Another area of potential confusion, especially for programmers from the world of DirectDraw, is the difference between surfaces and textures. A texture is really just an object that contains one or more surfaces. It may contain multiple surfaces in the case of MIP-mapping, which is storing smaller versions of a bitmap for use when an object is in the distance. MIP-mapping is used in 3D work to minimize the size of bitmaps that are moved around and used when objects are far away and detail is not needed. We don't care about this for 2D work, so we will always use textures with a single top-level surface, called level 0. The GetSurfaceLevel() method retrieves a pointer to the surface of a texture, so you can work with the surface data if need be. There is, however, one more big difference between surfaces and textures, and you actually have to pay some attention to this one: size restrictions. The width and height of a texture is restricted in most hardware to being a power of two for each dimension, and some hardware imposes maximums and other rules on these sizes. For the width and height, power-of-two sizing means that sizes like 128, 256, 512 and 1024 are acceptable numbers. So a texture of 256 pixels wide by 256 pixels high is acceptable, and most cards will allow different width/height values such as 256x128 or 64x16. This power-of-two limitation shouldn't be a major concern, because your bitmap doesn't need to fill the available space of the texture. If you have a 96x80 pixel graphic, put it in a 128x128 texture and just specify the location of your graphic when you use the texture. If you have two 60x60 images, template them together into a single graphic and load it into a 128x64 texture. There will be some wasted space, but in the future you'll start thinking about creating graphics in sizes that will work well with texture limits. The maximum-size restriction is a bigger issue. Modern hardware allows textures of 1024x1024 or larger, even as big as 4096x4096 (which takes 64MB of video memory in 32-bit color), but some older hardware -- particularly 3dfx Voodoo products -- have a maximum texture size of 256x256. If you currently load larger images, either as a background image or as a larger templated file of small sprites, then you'll have to chop these into smaller images to be compatible. Most game designs today no longer load a large background image, since maps are usually made up of a collection of individual tiles and smaller images. There are CAPS details you can get from Direct3D 8 that will report the maximum texture sizes and other restrictions (see D3DPTEXTURECAPS_POW2, D3DPTEXTURECAPS_SQUAREONLY, MaxTextureWidth / MaxTextureHeight, and MaxTextureAspectRatio in the SDK docs). The D3DX utility library also lets you get a bit lazy and will automatically calculate acceptable sizes for you, so if you call D3DXCreateTexture() and ask for a 100x60 texture, the function will check the hardware capabilities and create a legal size behind your back. Same with the functions that load an image into a texture from a file. And should you have a large source image that you can't or don't want to break up into smaller parts, you can load the large file into a surface with D3DXLoadSurfaceFromFile(), and then use CopyRects() to move partial correctly-sized sections of the image to the surface of smaller textures, which you then draw with. A final warning before we get to the code for all this is that you should be careful of the amount of video memory you are using. If you have a 1024x1024 image template of sprites that you are loading, 32 bits per pixel, and an 800x600 screen resolution, then you will need almost 6MB of video memory (room for the front buffer, back buffer, and the texture). However if you use a number of the smaller 256x256 textures, then DirectX will automatically remove (flush) textures from video memory if the space is required to load new textures. Note that if this happens multiple times each frame you will take a performance hit.
Listing 3 provides a set of wrapper functions to simplify implementing a 2D engine in Direct3D 8. I already incorporated the ID3DXSprite initialization and release code into the program skeleton I presented in Listing 2. The D3DX utility library provides a lot of functions to help get things rolling, such as the D3DXCreateTextureFromFileEx(), which will easily load a bitmap image into a texture for you. As this function supports a lot of options you may not need, I've simplified it into a LoadTexture() function and commented some of the options you may consider using. The main blit routine is BltSprite(). This version of BltSprite() is designed to mimic the functionality of the old DirectDraw Blt() routine, renamed so that you don't entirely forget that you're not using DirectDraw anymore. This routine covers most of the features used in the DirectX 7 function with the exception of filling a surface using the DDBLT_COLORFILL flag, which is discussed below. Now, believe it or not, you have a fully working 2D engine implemented in Direct3D 8. Boy, that was easy, wasn't it? Now that you've taken a moment to celebrate, it's time to point out a few important things. One is that color keying is no longer part of the implementation, alpha operations are used instead. Color keys are not actually understood by 3D hardware, instead everything is in various amounts of transparency. In a D3DCOLOR ARGB value, the fundamental color format for Direct3D 8, the alpha value is in the most significant byte and represented by a value of 255 (0xFF) for opaque, and 0 for fully transparent. Since by far the most common use of a color key is as a source color key (the color key value is associated with the source image), this is implemented in D3DXCreateTextureFromFileEx() by asking for a ColorKey value which is then searched for in the image and replaced with a D3DCOLOR of 0x00000000 (transparent black). So if your source image uses black as the color key value, specify a ColorKey value of 0xFF000000 (opaque black) and, upon loading the file, D3DX will make all the black pixels transparent. Many developers use magenta for the color key; for that specify 0xFFFF00FF for the value, and DirectX will replace the magenta with transparent black. Specifying a key value of 0 will disable any transparency replacement. I've also taken a shortcut and specified 32-bit color ARGB as the internal format for loaded textures. This is not necessarily your screen format, as the video hardware will automatically convert between texture and screen formats when rendering. In addition, if my chosen format is not supported, D3DXCreate* should adjust the format of the texture to be correct for your hardware -- you can do this yourself with the D3DXCheckTextureRequirements() function, which will return the acceptable value sizes and formats based on the closest match to what you want. In an actual program you would likely scale your format from 16-bit to 32-bit based on total video memory or system speed. ______________________________________________________
|
||||||||||||||||||||||||||
|
|