Due to being busy and the erratic streams of 500 errors have kept me away from usual forum spamming, but hopefully I'll have more time as of next week.
I've been carrying on with updating the forum FAQ, including this latest entry...
As usual, comments and suggestions are greatly appreciated:
D3D #3: I only want to do 2D graphics...
With the API's name being "Direct3D" it is a common misconception that you must only use it for 3D graphics. This is completely untrue! Whilst Direct3D may be biased towards 3D graphics, it is perfectly capable of displaying 2D content.
The biggest problem is that many people insist on using DirectDraw - technology that is no longer supported by Microsoft and has not been updated since the last century (DirectDraw7 was released in 1999). For various reasons it is becoming increasingly difficult to find DirectDraw related information, and asking questions about it online is yielding less replies simply due to less people being familiar with it (or have forgotten whatever knowledge they once had!).
Using Direct3D offers many advantages over the more traditional DirectDraw route - several important features such as sprite scaling, rotation and translation as well as alpha blending are effectively "free" with Direct3D. When using DirectDraw these effects were much more complex and involved affairs.
There are essentially three ways of displaying 2D geometry via Direct3D:
- By sending "pre-transformed" data directly to the GPU.
This allows your application to define coordinates as pixels in screen-space and completely skip any vertex processing on the GPU. This has the advantage of less processing, but also means that your vertex data is much more static - changing it will require resource modification (which you should avoid where possible). To use this method you either declare your vertices as having the D3DFVF_XYZRHW format, or the POSITIONT semantic (depending on whether you're using FVF's or Vertex Declarations).
- Regular 3D geometry via an orthographic projection.
This method is more difficult to implement as it becomes harder to directly map directly to screen coordinates (e.g. if you require that all sprites are NxM pixels in size). The advantage is that you re-enable the regular transform and lighting sections of the pipeline - in either fixed function or programmable modes. This can allow you to perform many useful effects (such as matrix transforms like scaling, rotation, translation, shearing) completely in hardware. Also, there ceases to be any functional difference between this and "normal" 3D work - so pretty much any and every effect you see discussed in 3D contexts will work in 2D. To use this method you need to modify your view and transformation matrices - use the D3DXMatrixOrtho**() functions instead of the more conventional D3DXMatrixPerspective**() forms.
- A hybrid of the first two options.
This is only applicable if you're using vertex shaders, but can be a very powerful trick. By combining the ability to pass pre-transformed coordinates as well as being able to execute a vertex program you can get the best of both previous suggestions. Because you have much more control over how your vertex data is packed you can send only the data that is needed, as well as the format that it is needed. A simple example would be to send the POSITION data as a simple X/Y coordinate packed as a float2. If the hardware supports it, a half2 for a lower storage requirement could also be used. The vertex shader is required to output homogenous space coordinates which suffers from the same problem as option 2 when it comes to directly lining up sprites with screen pixels.
You should also bare a few things in mind when it comes to designing and writing 2D applications using the Direct3D API. The bottom-line is that people make the mistake of using Direct3D in the same way they would use DirectDraw or other 2D-specific API's. This often leads to exceptionally poor performance, which makes for plenty of great forum threads where people incorrectly assume that DirectDraw is a better/faster API. Write your "2D in 3D" applications in a 3D-like way to make sure that you get the most from your hardware and API. The following guidelines cover many common problems:
- Use geometry rather than trying to directly copy pixels.
Representing your tiles/sprites/backgrounds as textured triangles is far more efficient than trying to directly copy parts of textures/surfaces to the back-buffer (or another off-screen surface).
- Use textures rather than surfaces.
In conjunction with the previous point, using IDirect3DTexture9's instead of IDirect3DSurface9's are much better for performance. Many developers familiar with DirectDraw latch onto the 'surface' interface in Direct3D as it shares the name of a basic/primary type used in DirectDraw.
- Don't change resources unless it's absolutely necessary.
Make sure you read D3D #14: Speeding up locking of resources. Various traditional 2D effects were achieved by modifying the raw pixel data - this is very expensive under Direct3D. Instead, you should prefer texture blending and/or pixel shaders to achieve similar operations. If you're using geometry (as suggested in #1, above) then constantly locking and modifying the vertex properties is equally bad.
- Batching and state-change optimization are important.
These are general Direct3D best-practice's, but especially important here as many 2D programmers write horribly inefficient code that breaks these rules. A common example is drawing a "tile map" one tile at a time. Specifically, changing the texture and vertex buffer and issuing a Draw**() call for every 2-triangle tile. Batching all tiles together into a single, larger, vertex/index buffer is substantially better for performance.
- Texture palettes/atlases are a good idea.
This is the process of grouping together many textures onto a single larger texture. For example, putting an 8x8 grid of 30x30 pixel sprites onto a single 256x256 is far more efficient than having 64 different 30x30 textures.
- Correct use of texture coordinates.
A big difference between DirectDraw style 2D and Direct3D is the use of floating point texture coordinates in the range 0.0 to 1.0 that makes it difficult to directly address individual pixels (or areas of pixels). Two key problems arise from this; firstly when extracting source data from a texture you need to be careful about using linear filtering where multiple pixels will be sampled, secondly when rendering directly to the screen it is possible that texels will not directly map to pixels. This usually manifests itself as blurry rendering and/or having extra pixels around the border that you weren't expecting. Make sure you read and understand the 'Directly Mapping Texels to Pixels' article in the DirectX SDK.
As a closing note... just because most API's and libraries are written for 3D usage does not mean that 2D graphics or games are dead. Far from it even - a creative person adhering to the best practices listed above can create amazing results. In fact, the features and performance of modern GPU's could be seen as a great opportunity for "2D in 3D"...