[bquote]This article explores the inner workings of sprite drawing in DirectX 8. Using a low level approach, programmers can gain more control over their rendering code and address the shortcomings of any DirectX blitter.[/bquote]
With DirectDraw deprecated from DirectX 8, programmers are now forced to use 3D techniques to draw 2D graphics (or resort to using DirectDraw 7). Instead of "blitting a sprite", they must now "draw a textured quad". Of course, with wrapper functions there is no difference from an API point of view and this is what we'll be developing here: a set of routines to draw sprites in a 3D environment.[size="5"]What about ID3DXSprite?
Microsoft provided the [font="Courier New"][color="#000080"]ID3DXSprite[/color][/font] interface to simplify the task of "drawing a textured quad". So why are we reinventing the wheel? Because this wheel has a bug in it.
Sprites drawn with [font="Courier New"][color="#000080"]ID3DXSprite[/color][/font] are not the same size than their original bitmap. This problem becomes glaringly obvious when you try to tile your bitmaps: tiles will not line up properly if they are bigger or smaller than their target rectangles.
Robert Dunlop (X-Zone) suggests that you expand your target rectangle "by 0.5 on all sides to allow proper mapping of texels to pixels." This will "compensate for the texel alignment rules of Direct3D" that are causing this problem [2, 3]
But it doesn't work.
Here's an example to illustrate. Compare figures 2, 3, and 4 with the original image (figure 1). Note the size differences and inaccuracies.
Sprites created by Ari Feldman.
Figure 2 is smaller than the original. Figures 3 and 4 have noticeable inaccuracies at the top and left sides (and they are bigger). ID3DXSprite images are antialiased by default, hence the blurriness in figure 4. (I have no idea how to turn this off.)
Interestingly, there is a very simple solution to this problem: extend the right and bottom sides of your target rectangle by 1-pixel. Figure 4 is the only accurate reproduction of the original.
But why does it work? It all has to do with a simple documentation error.
[size="5"]Bad Documentation
The DirectX 8 documentation for rectangles is wrong. You can find this page under DirectX Graphics:
- Introduction to DirectX Graphics
- Getting Started With DirectX Graphics
- Rectangles According to the documentation, the coordinates (right, bottom) refer to the bottom-right pixel of the rectangle. This is wrong. The coordinates (right, bottom) are actually 1-pixel outside the rectangle.
The documentation describes an inclusive-inclusive coordinate system. But DirectX uses inclusive-exclusive coordinates; the last pixel (right, bottom) is not part of the rectangle.
To most people, inclusive-inclusive coordinates make more sense. But inclusive-exclusive coordinates are easier to work with and most APIs favour them. For example, when calculating the width (and height):
The use of inclusive-exclusive coordinates also explains why extending the right and bottom by 1 fixes the problem.width = right - left; // inclusive-exclusive coordinates
width = right - left + 1; // inclusive-inclusive coordinates
[size="5"]The Basic Blit
A new blitter is needed to address these problems. We start by defining a custom vertex type and a corresponding FVF for the vertices of our textured quad ([font="Courier New"][color="#000080"]D3DTLVERTEX[/color][/font] and [font="Courier New"][color="#000080"]D3DFVF_TLVERTEX[/color][/font] in the demo).
We can use transformed, lit vertices because we already know the screen coordinates of our sprite. This lets us bypass the usual 3D transformations (world, view, and projection) and lighting calculations in the rendering pipeline [1b]. It's more efficient but it also means more work on our part.
Each vertex corresponds to a texture coordinate (tu, tv), indicating how the texture should be mapped (see [font="Courier New"][color="#000080"]Blit[/color][/font] in the demo). Texture coordinates range from 0.0 to 1.0, with (0, 0) being the top-left pixel. The system can process values outside this range [1b]. It is possible to specify a source rectangle that's larger than the texture (strange things happen when you do).
Texture coordinates let you use a portion of the texture. It is easy to map a source rectangle (as in [font="Courier New"][color="#000080"]ID3DXSprite::Draw[/color][/font]) into texture coordinates. This lets the user draw a subsection of the sprite. (The demo does not implement this feature.)
The colour argument should be set to 0xFFFFFFFF (ARGB) for a standard blit. The RGB and alpha channels are modified by this argument: specifying 0x80FFFFFF will draw images at 50% transparency and 0xFF00FF00 gives them a green tint. Different colours can be set for each vertex to create lighting effects.
More realistic lighting can be achieved through the use of light maps. A light map is a texture, or group of textures, that contain lighting information [1d]. Light mapping is a big topic; consult the DirectX documentation for more information.
We can now to draw our sprite (or "textured quad") but we might want to scale or rotate it first.
[size="5"]Transforming the Vertices
Because of the vertex format we've chosen, we cannot use the world matrix to transform our sprite. Fortunately, transforming the sprite is as easy as transforming its vertices: a texture can be mapped onto any primitive regardless of its orientation or position.
Scaling and rotation must be performed with respect to a point, usually the centre of the sprite. We need to translate the sprite such that this point is on the origin, scale and/or rotate it, and then translate it back to its original position. All these operations can be concatenated into one transformation matrix.
The quad has four vertices and there are four rows in a Direct3D matrix. We can make a matrix out of these four vertices and use Direct3D's matrix multiplication functions to transform our vertices. (See [font="Courier New"][color="#000080"]TransformVertices[/color][/font] in the demo.)
Reflection is identical to scaling with a negative number. Another way to reflect sprites is to reverse the texture coordinates (tu, tv). To reflect about the
- x-axis, make tu = (1 - tu)
- y-axis, make tv = (1 - tv)
Sprite (x, y)Normal (tu, tv)Reflect (1 - tu, tv)(left, top)(0, 0)(1, 0)(right, top)(1, 0)(0, 0)(right, bottom)(1, 1)(0, 1)(left, bottom)(0, 1)(1, 1)
[size="5"]Preparing to Blit
There are three things we need to set up before we call our blit function (see [font="Courier New"][color="#000080"]InitBlit[/color][/font] in the demo).
- Enable alpha blending. Colour keying (or setting a transparent colour) is done through alpha blending in Direct3D. Alpha blending gives us varying degrees of transparency.
- Allow alpha and colour modulation for transparency and tinting effects. The colour argument in [font="Courier New"][color="#000080"]Blit[/color][/font] will not work without this.
- Turn off back face culling so that we can see the back of primitives. This lets us reflect our sprite (to change the direction that it is facing). With culling on (the default), a reflected sprite is not drawn.
[size="5"]Performance Tips
The [font="Courier New"][color="#000080"]SetTexture[/color][/font] command represents a render-state change, something that you want to minimise. You'll improve performance if you can use one [font="Courier New"][color="#000080"]SetTexture[/color][/font] to draw all identical sprites in a frame. You can, however, get away with calling [font="Courier New"][color="#000080"]SetTexture[/color][/font] for every bitmap in the frame because the performance hit is not too high. (See [font="Courier New"][color="#000080"]Blit[/color][/font] in the demo.)
Alternatively, one texture can contain multiple sprites but beware of size restrictions (covered next). You'll need to implement a source rectangle argument for your blit function first (discussed earlier in The Basic Blit).
More "Performance Optimizations" can be found in the DirectX documentation.
[size="5"]Big Bad Textures
It is convenient to keep related sprite images in one bitmap file but we cannot simply load a big bitmap into a texture because of size restrictions. Most video cards place a maximum limit on the size of textures, which can be as low as 256x256 on older cards.
The DirectX documentation also suggests keeping textures small: "the smaller the textures are, the better chance they have of being maintained in the main CPU's secondary cache" [1d].
What we need is a way to break up a (potentially large) bitmap into smaller images to be stored in textures. The easiest way to do this is to load the entire bitmap into a Direct3D surface (which have no size restrictions) and then use [font="Courier New"][color="#000080"]CopyRects[/color][/font] to copy surface sub-images into textures.
The demo program uses [font="Courier New"][color="#000080"]D3DFMT_A1R5G5B5[/color][/font], a 16-bit surface format where five bits are reserved for each colour and one bit is reserved for alpha transparency. Other suitable surface formats are [font="Courier New"][color="#000080"]D3DFMT_A8R8G8B8[/color][/font] and [font="Courier New"][color="#000080"]D3DFMT_A4R4G4B4[/color][/font]. (See [font="Courier New"][color="#000080"]CreateSurfaceFromFile[/color][/font] in the demo.
You need an alpha component in your surface format for transparency, and an RGB component for the colours to display correctly. The exception to this rule is [font="Courier New"][color="#000080"]D3DFMT_A8R3G3B2[/color][/font], which (in my experience) has insufficient RGB values to produce a good range of colours and often results in an incomprehensible grey image.
The same surface format is used for the textures. (You will not be able to copy data between textures and surface otherwise.)
In most video cards, the width and height of a texture must be a power of two. A 20x40 image will be stored in a 32x64 texture. This extra area within our texture must be set to a transparent colour or random colours may surround your sprites.
Newer video cards let you create textures that are not limited to powers of two.
To clear the texture we need to create a blank surface and then copy its content over to the texture. Can't we clear the texture directly, you ask? We can but textures placed in the [font="Courier New"][color="#000080"]D3DPOOL_DEFAULT[/color][/font] pool cannot be locked for clearing [1g]. Placing our textures in [font="Courier New"][color="#000080"]D3DPOOL_MANAGED[/color][/font] memory will allow us to lock the texture but the copy method works for both cases so it is more flexible. See [font="Courier New"][color="#000080"]CreateTextureFromSurface[/color][/font] in the demo.
Finally, we can copy the bitmap image over to the texture.
[size="5"]Final Word
DirectDraw programmers looking to migrate to the new API will have hopefully gained some useful insight into the inner workings of Direct3D.
While it isn't hard to rewrite the [font="Courier New"][color="#000080"]ID3DXSprite[/color][/font] routines, there is a lot of mundane work to do so you might as well take advantage of my mundane work. The discussed techniques are incorporated in the demo program.
[size="5"]Further Reading
- DirectX 8.0 Documentation: DirectX Graphics, Microsoft Corp, October 4, 2000.
- Introduction: Getting Started.
- Using: Surfaces, Textures, and Vertex Formats.
- Understanding: Transformation and Lighting Engine.
- Programming Tips: Performance Optimizations.
- Tutorials: Vertices, Matrices, and Texture Maps.
- Samples: Billboard and PointSprites.
- C/C++ Reference.
- Robert Dunlop, DirectX 8 Graphics FAQ, X-Zone/MVPs, July 12, 2001.
- Robert Dunlop, A Simple Blit Function for DirectX 8, X-Zone/MVPs, July 12, 2001.
- George Geczy, 2D Programming in a 3D World: Developing a 2D Game Engine Using DirectX8 Direct3D, Gamasutra, June 29, 2001. (A printable version is available.)
- Herbert Wolverson, Using Direct3D For 2D Tile Rendering, GameDev.net, accessed July 17, 2001.
[size="5"]Acknowledgements
Thanks to Mario Knezovic (mario.knezovic@bonespark.com) for helping me with this article. Mario provided the inclusive-exclusive explanation to this problem. (I thought Microsoft's rectangles were incorrectly implemented but this is not the case.)
[hr]
[size="5"]About the Demo
Download the attached resource file to obtain the demo (and source code) for this article.
The demo draws six sprites to show off the blitting techniques discussed in this article. These images are arranged like so
123456The sprites are
- Microsoft's blit: [font="Courier New"][color="#000080"]ID3DXSprite[/color][/font].
- Dunlop's blit ([font="Courier New"][color="#000080"]BlitX[/color][/font]): no extensions.
- Dunlop's blit ([font="Courier New"][color="#000080"]BlitX[/color][/font]): extend all sides by 0.5.
- The new blitter ([font="Courier New"][color="#000080"]Blit[/color][/font]): extend right and bottom by 1.
- The new blitter ([font="Courier New"][color="#000080"]Blit[/color][/font]): scaling (-1, 2.4).
- The new blitter ([font="Courier New"][color="#000080"]Blit[/color][/font]): rotation and colour modulation.
Tip: use [Alt] + [Print Screen] to copy the active window and then paste the image into any Windows paint program. Tools like zoom make it easier to compare images.
Sprites 5 and 6 show off the effects available in [font="Courier New"][color="#000080"]Blit[/color][/font]: scaling, rotation, and colour modulation.
- Rectangles According to the documentation, the coordinates (right, bottom) refer to the bottom-right pixel of the rectangle. This is wrong. The coordinates (right, bottom) are actually 1-pixel outside the rectangle.
- Getting Started With DirectX Graphics