Dissecting Sprites in Direct3D
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.
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.
What about ID3DXSprite?
Microsoft provided the ID3DXSprite 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 ID3DXSprite 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.
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.
The DirectX 8 documentation for rectangles is wrong. You can find this page under DirectX Graphics:
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):
width = right - left; // inclusive-exclusive coordinates width = right - left + 1; // inclusive-inclusive coordinates
The use of inclusive-exclusive coordinates also explains why extending the right and bottom by 1 fixes the problem.
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 (D3DTLVERTEX and D3DFVF_TLVERTEX 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 Blit 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 ID3DXSprite::Draw) 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.
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 TransformVertices 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
For example, reflecting about the x-axis looks like this
Preparing to Blit
There are three things we need to set up before we call our blit function (see InitBlit in the demo).
Important: set BackBufferWidth and BackBufferHeight (in D3DPRESENT_PARAMETERS) to zero if you are using Windowed mode. Otherwise, your sprites will be rescaled and they will not match their original size./p>
The SetTexture command represents a render-state change, something that you want to minimise. You'll improve performance if you can use one SetTexture to draw all identical sprites in a frame. You can, however, get away with calling SetTexture for every bitmap in the frame because the performance hit is not too high. (See Blit 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.
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 CopyRects to copy surface sub-images into textures.
The demo program uses D3DFMT_A1R5G5B5, 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 D3DFMT_A8R8G8B8 and D3DFMT_A4R4G4B4. (See CreateSurfaceFromFilein 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 D3DFMT_A8R3G3B2, 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 D3DPOOL_DEFAULT pool cannot be locked for clearing [1g]. Placing our textures in D3DPOOL_MANAGED memory will allow us to lock the texture but the copy method works for both cases so it is more flexible. See CreateTextureFromSurface in the demo.
Finally, we can copy the bitmap image over to the texture.
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 ID3DXSprite 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.
Thanks to Mario Knezovic (email@example.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.)
About the Demo
Download d3dblit.zip 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
The sprites are
Compare sprites 1 to 4 with the original bitmap in sprites.bmp.
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 Blit: scaling, rotation, and colour modulation.