2D in Direct3D using Textured Quads
This article is for people trying to make a 2D graphics engine using Direct3D 9. It assumes you already have a basic Direct3D 9 application up and running, possibly using ID3DXSprite functions. If you do not, the code for a fully working application using the methods presented in this article is included. This article is divded into the following sections:
1. The conceptSo you've decided to write a 2d game. Great, I love 2d games. You've also decided to use Direct3D as your API of choice. Groovy, you get to use cool tricks like fully hardware-supported alpha blending. If you're using Direct3D 8 instead, the code is pretty much the same (just change all the 8's to 9's and update CreateVertexBuffer() and SetStreamSource() calls). Now, you're probably wondering "How exactly do I put a 2d graphic on screen using an API designed exclusively for 3d graphics?" Don't worry. It's not particularly hard. Anyways, you only have to actually write the code once ;-) The basic idea is to draw a rectangular polygon on screen textured with your 2d image. The great thing about this is that the image can have its own alpha channel, so you don't get the opaque rectangular frame around your image (accomplishing the same thing as colour-keying) and at the same time, you can have certain areas of the image be partially transparent. In order to draw a quad on the screen, you have to give the positions of the four corners of the quad. This means with a little toying around with the coordinates, you can easily add transformations like skewing and perspective effects. Those, however, are left as an exercise for the reader. The main benefit, however, to using textured quads for a 2d engine is speed. You're actually using the 3d hardware to do exactly what it was designed for: spitting out a bunch of textured shapes on screen really fast. Of course, with so many benefits to using textured quads, there must be some drawbacks. And there are a few. The most significant drawback to using 3d hardware for drawing is that most 3d cards cannot support textures whose side lengths are not powers of 2. Furthermore, some cards cannot support non-square textures. What this means is that you should try to make your graphics have dimensions such as: 1x1, 2x2, 4x4, 8x8, 16x16, 32x32, 64x64, 128x128, etc. The maximum size you should use is 512x512. Most video cards can support textures up to 2048x2048, but some can't, and a texture that big would consume a massive amount of memory on the video card. The CTexture class I provide at the end of this article will provide a work-around for these limitations, allowing you to use rectangular textures of weird dimensions. Even so, using non-square, non-power-of-2 textures is pretty much guaranteed to result in more processing time and more memory consumption. 2. The vertex formatIt's time to get down to business: putting a quad on the screen. Every quad is made up of two triangle and four vertices as shown below: v0 v1 |---------------| |\ | | \ | | \ | | \ | | \ | | \ | | \ | | \| |---------------| v3 v2 I like to keep that particular diagram as a comment in my code as it comes in handy when I want to change the colour tint of a particular corner on a quad I'm drawing (more on this later). The first thing you need in order to write your own quad-based engine for 2d drawing is a flexible vertex format. The vertex format decides exactly what information is available about the vertices that make up the quads you draw. This one should do you well: const DWORD D3DFVF_TLVERTEX = D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1; Put it somewhere where all of your graphics code can see it. It's very important. Here's a quick run-down of exactly what the each of the flags in the above vertex format does: D3DFVF_XYZRHW - Specifies what position information the vertex will hold. In this case, X, Y, Z coordinates, as well as a "reciprocal of homogenous W" coordinate. All you really need to know about that is it lets you use screen coordinates to draw your quads instead of 3d-space coordinates. As you may have guessed, the Z coordinate is irrelevant. It will always be set to 0. D3DFVF_DIFFUSE - Allows you to specify a colour and alpha for the vertex, letting you shade the corners of a quad in different colours. ID3DXSprite offers limited support for this, allowing you to shade the whole a quad a certain colour. The code in this article allows you to change the colour and alpha value of each corner of a quad. D3DFVF_TEX1 - Specifies that the vertex is to hold texture coordinates. You need this to put a texture on the quads you draw. Now that you've got your vertex format defined, you need a structure to hold a vertex. This one will work swimmingly with the above vertex format:
//Custom vertex
struct TLVERTEX
{
float x;
float y;
float z;
float rhw;
D3DCOLOR colour;
float u;
float v;
};
You'll need this structure to be available to all of your drawing code as well. A brief description of the members: x, y, z - The coordinates the vertex is located at. x and y are the screen coordinates at which the vertex is located. In 2d-land, z is always 0.0f. While x and y are floats, they should contain integral values, or they will be rounded when it comes time to draw the quad. rhw - This is always 1.0f. It allows you to use screen coordinates to specify the position of the vertex rather than 3d-space coordinates, colour - The colour/alpha value of the vertex in standard 32-bit AARRGGBB format. u, v - The texture coordinates of the vertex. For the vertices on the left, u is 0.0f. On the right, u is 1.0f. On the top, v is 0.0f. On the bottom, v is 1.0f. If you didn't get all of that, don't worry about it. As long as you copy-and-pasted the const and struct properly, you'll have no problem using the code accompanying this article ;) 3. Initialization codeSo you've got your Direct3D object, presented your parameters, created your device, whatever. All you need to do now is add a couple lines and you'll be drawing textured quads in no time. First things first, you need to make a vertex buffer. In the same scope your IDirect3DDevice9 pointer is located, add: IDirect3DVertexBuffer9* vertexBuffer; Groovy. Now, in your initalization function, AFTER the device is created, add the following: //Set vertex shader DEVICE->SetVertexShader(NULL); DEVICE->SetFVF(D3DFVF_TLVERTEX); //Create vertex buffer DEVICE->CreateVertexBuffer(sizeof(TLVERTEX) * 4, NULL, D3DFVF_TLVERTEX, D3DPOOL_MANAGED, &vertexBuffer, NULL); DEVICE->SetStreamSource(0, vertexBuffer, 0, sizeof(TLVERTEX)); NOTE 1: Replace DEVICE with the name of your IDirect3DDevice9* object
Also the following render states should be set if they are not: DEVICE->SetRenderState(D3DRS_LIGHTING, FALSE); DEVICE->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); DEVICE->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); DEVICE->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); DEVICE->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); NOTE: Replace DEVICE with the name of your IDirect3DDevice9* object Alright, now we're ready to go. Let's load some textures. 4. Loading a textureUsing D3DX this is incredibly simple. Here's a code dump: //Load texture from file with D3DX //Supported formats: BMP, PPM, DDS, JPG, PNG, TGA, DIB IDirect3DTexture9 *LoadTexture(char *fileName) { IDirect3DTexture9 *d3dTexture; D3DXIMAGE_INFO SrcInfo; //Optional //Use a magenta colourkey D3DCOLOR colorkey = 0xFFFF00FF; // Load image from file if (FAILED(D3DXCreateTextureFromFileEx (DEVICE, fileName, 0, 0, 1, 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, D3DX_FILTER_NONE, D3DX_DEFAULT, colorkey, &SrcInfo, NULL, &d3dTexture))) { return NULL; } //Return the newly made texture return d3dTexture; } NOTE: Replace DEVICE with the name of your IDirect3DDevice9* object You pass it a graphics file, it passes you a pointer to a texture. Couldn't be easier. Upon failure, a NULL pointer is returned. While the loaded graphics file will retain its own alpha map, there is also a magenta colour key, meaning all pixels with a colour value of 0xFFFF00FF will be given an alpha value of 0 upon loading. Please note that if you load an image file whose dimensions are not powers of 2, and your video card cannot support non-power-of-2 textures, this function will automatically put the image in a texture with dimensions that are powers of 2. For example, if you tried to load a 100x100 image, it would be put into a 128x128 texture. The extra space on the texture will have an alpha value of 0, so it will not show up when the texture is drawn. However, it will cause the drawing functions below to draw the image with a scaled size. That is, if you tried to draw the 100x100 image in a 100x100 square on screen, the drawing function would have to scale the 128x128 texture to fit inside it. This would make the image appear smaller than it actually is. Using the drawing functions in the CTexture class (provided at the end of the article) will prevent this scaling. Even so, I urge the reader to avoid the whole mess and use square, power-of-2-sized graphics. 5. Drawing a textureTime to get something on the screen. Two main drawing functions will be used -- BlitD3D() and BlitExD3D(). The only difference between the two is that BlitExD3D() supports colour modulation of all four corners of the quad, while BlitD3D() can only tint the whole quad. Please be aware that the rotation code provided here is horrible, and should be rewritten. Use it at your own risk. //Draw a textured quad on the back-buffer void BlitD3D (IDirect3DTexture9 *texture, RECT *rDest, D3DCOLOR vertexColour, float rotate) { TLVERTEX* vertices; //Lock the vertex buffer vertexBuffer->Lock(0, 0, (void**)&vertices, NULL); //Setup vertices //A -0.5f modifier is applied to vertex coordinates to match texture //and screen coords. Some drivers may compensate for this //automatically, but on others texture alignment errors are introduced //More information on this can be found in the Direct3D 9 documentation vertices[0].colour = vertexColour; vertices[0].x = (float) rDest->left - 0.5f; vertices[0].y = (float) rDest->top - 0.5f; vertices[0].z = 0.0f; vertices[0].rhw = 1.0f; vertices[0].u = 0.0f; vertices[0].v = 0.0f; vertices[1].colour = vertexColour; vertices[1].x = (float) rDest->right - 0.5f; vertices[1].y = (float) rDest->top - 0.5f; vertices[1].z = 0.0f; vertices[1].rhw = 1.0f; vertices[1].u = 1.0f; vertices[1].v = 0.0f; vertices[2].colour = vertexColour; vertices[2].x = (float) rDest->right - 0.5f; vertices[2].y = (float) rDest->bottom - 0.5f; vertices[2].z = 0.0f; vertices[2].rhw = 1.0f; vertices[2].u = 1.0f; vertices[2].v = 1.0f; vertices[3].colour = vertexColour; vertices[3].x = (float) rDest->left - 0.5f; vertices[3].y = (float) rDest->bottom - 0.5f; vertices[3].z = 0.0f; vertices[3].rhw = 1.0f; vertices[3].u = 0.0f; vertices[3].v = 1.0f; //Handle rotation if (rotate != 0) { RECT rOrigin; float centerX, centerY; //Find center of destination rectangle centerX = (float)(rDest->left + rDest->right) / 2; centerY = (float)(rDest->top + rDest->bottom) / 2; //Translate destination rect to be centered on the origin rOrigin.top = rDest->top - (int)(centerY); rOrigin.bottom = rDest->bottom - (int)(centerY); rOrigin.left = rDest->left - (int)(centerX); rOrigin.right = rDest->right - (int)(centerX); //Rotate vertices about the origin bufferVertices[index].x = rOrigin.left * cosf(rotate) - rOrigin.top * sinf(rotate); bufferVertices[index].y = rOrigin.left * sinf(rotate) + rOrigin.top * cosf(rotate); bufferVertices[index + 1].x = rOrigin.right * cosf(rotate) - rOrigin.top * sinf(rotate); bufferVertices[index + 1].y = rOrigin.right * sinf(rotate) + rOrigin.top * cosf(rotate); bufferVertices[index + 2].x = rOrigin.right * cosf(rotate) - rOrigin.bottom * sinf(rotate); bufferVertices[index + 2].y = rOrigin.right * sinf(rotate) + rOrigin.bottom * cosf(rotate); bufferVertices[index + 3].x = rOrigin.left * cosf(rotate) - rOrigin.bottom * sinf(rotate); bufferVertices[index + 3].y = rOrigin.left * sinf(rotate) + rOrigin.bottom * cosf(rotate); //Translate vertices to proper position bufferVertices[index].x += centerX; bufferVertices[index].y += centerY; bufferVertices[index + 1].x += centerX; bufferVertices[index + 1].y += centerY; bufferVertices[index + 2].x += centerX; bufferVertices[index + 2].y += centerY; bufferVertices[index + 3].x += centerX; bufferVertices[index + 3].y += centerY; } //Unlock the vertex buffer vertexBuffer->Unlock(); //Set texture DEVICE->SetTexture (0, texture); //Draw image DEVICE->DrawPrimitive (D3DPT_TRIANGLEFAN, 0, 2); } NOTE: Replace DEVICE with the name of your IDirect3DDevice9* object Here's a description of the arguments passed to the function: texture - A pointer to the texture to use on the quad. Whatever is passed here will show up on screen. rDest - A rectangle containing the screen coordinates of the quad. If the rectangle is of a different size than the texture, the image will be scaled. vertexColour - The colour used to tint the quad in 32-bit 0xAARRGGBB format. A value of 0xFFFFFFFF will not modify the appearance of the texture. fRotate - How much the quad should be rotated about its center. The value should be in radians. A value of 0 will, of course, not rotate the quad at all. The extended blitting function follows: //Draw a textured quad on the back-buffer void BlitExD3D (IDirect3DTexture9 *texture, RECT *rDest, D3DCOLOR *vertexColours /* -> D3DCOLOR[4] */, float rotate) { TLVERTEX* vertices; //Lock the vertex buffer vertexBuffer->Lock(0, 0, (void**)&vertices, NULL); //Setup vertices //A -0.5f modifier is applied to vertex coordinates to match texture //and screen coords. Some drivers may compensate for this //automatically, but on others texture alignment errors are introduced //More information on this can be found in the Direct3D 9 documentation vertices[0].colour = vertexColours[0]; vertices[0].x = (float) rDest->left - 0.5f; vertices[0].y = (float) rDest->top - 0.5f; vertices[0].z = 0.0f; vertices[0].rhw = 1.0f; vertices[0].u = 0.0f; vertices[0].v = 0.0f; vertices[1].colour = vertexColours[1]; vertices[1].x = (float) rDest->right - 0.5f; vertices[1].y = (float) rDest->top - 0.5f; vertices[1].z = 0.0f; vertices[1].rhw = 1.0f; vertices[1].u = 1.0f; vertices[1].v = 0.0f; vertices[2].colour = vertexColours[2]; vertices[2].x = (float) rDest->right - 0.5f; vertices[2].y = (float) rDest->bottom - 0.5f; vertices[2].z = 0.0f; vertices[2].rhw = 1.0f; vertices[2].u = 1.0f; vertices[2].v = 1.0f; vertices[3].colour = vertexColours[3]; vertices[3].x = (float) rDest->left - 0.5f; vertices[3].y = (float) rDest->bottom - 0.5f; vertices[3].z = 0.0f; vertices[3].rhw = 1.0f; vertices[3].u = 0.0f; vertices[3].v = 1.0f; //Handle rotation if (rotate != 0) { RECT rOrigin; float centerX, centerY; //Find center of destination rectangle centerX = (float)(rDest->left + rDest->right) / 2; centerY = (float)(rDest->top + rDest->bottom) / 2; //Translate destination rect to be centered on the origin rOrigin.top = rDest->top - (int)(centerY); rOrigin.bottom = rDest->bottom - (int)(centerY); rOrigin.left = rDest->left - (int)(centerX); rOrigin.right = rDest->right - (int)(centerX); //Rotate vertices about the origin bufferVertices[index].x = rOrigin.left * cosf(rotate) - rOrigin.top * sinf(rotate); bufferVertices[index].y = rOrigin.left * sinf(rotate) + rOrigin.top * cosf(rotate); bufferVertices[index + 1].x = rOrigin.right * cosf(rotate) - rOrigin.top * sinf(rotate); bufferVertices[index + 1].y = rOrigin.right * sinf(rotate) + rOrigin.top * cosf(rotate); bufferVertices[index + 2].x = rOrigin.right * cosf(rotate) - rOrigin.bottom * sinf(rotate); bufferVertices[index + 2].y = rOrigin.right * sinf(rotate) + rOrigin.bottom * cosf(rotate); bufferVertices[index + 3].x = rOrigin.left * cosf(rotate) - rOrigin.bottom * sinf(rotate); bufferVertices[index + 3].y = rOrigin.left * sinf(rotate) + rOrigin.bottom * cosf(rotate); //Translate vertices to proper position bufferVertices[index].x += centerX; bufferVertices[index].y += centerY; bufferVertices[index + 1].x += centerX; bufferVertices[index + 1].y += centerY; bufferVertices[index + 2].x += centerX; bufferVertices[index + 2].y += centerY; bufferVertices[index + 3].x += centerX; bufferVertices[index + 3].y += centerY; } //Unlock the vertex buffer vertexBuffer->Unlock(); //Set texture DEVICE->SetTexture (0, texture); //Draw image DEVICE->DrawPrimitive (D3DPT_TRIANGLEFAN, 0, 2); } NOTE: Replace DEVICE with the name of your IDirect3DDevice9* object The only argument here that is different from the BlitD3D() function is vertexColours. vertexColours must be a pointer to a D3DCOLOR array of size 4. The indices of the array correspond to the numbers of the vertices in the kick-ass diagram provided at the beginning of this article. Each vertex in the quad will have its colour and alpha modulated based on its corresponding value in vertexColours. 6. The CTexture classOkay, we can load textures and we can draw textures. What more could a programmer want? A programmer could want a way to keep track of the textures. The CTexture class features a built-in resource manager so a single texture is not loaded more than once. It also keeps track of information about the texture for quick and easy access at runtime. First off, the class has a struct inside its private scope: LOADEDTEXTURE. This struct contains an IDirect3DTexture9* pointer as well as information about the texture, such as its filename and dimensions. It's declaration is: //Loaded texture struct struct LOADEDTEXTURE { int referenceCount; //# of CTexture instances containing this texture IDirect3DTexture9* texture; //The texture string sFilename; //The filename of the texture int width; //Width of the texture int height; //Height of the texture }; You'll notice that the struct keeps a reference count of CTexture instances that use the texture it contains. This is helpful as we can periodically run through the list of loaded textures and delete any unreferenced ones, or even delete the texture as soon as it becomes unreferenced. By default, the CTexture class does not release textures as soon as they become unreferenced, but it's not a particularly taxing job to make it do so. However, I do not recommend it. It would be best to wait until you have some free processing time (such as the loading time between levels) before you start deleting textures. This way you can clear all the unreferenced ones at once without causing any hiccups in the game. Next up, also in the private scope, we have the list of loaded textures:
//Linked list of all loaded textures
static list <LOADEDTEXTURE*> loadedTextures;
We want it to be static so there is only ever one list, instead of a list in every instance of CTexture. Don't forget to initialize the list outside of the class declaration, as it is static. Each instance of CTexture needs to know if it has already loaded a texture or not, and which texture that is. This is easily taken care of by adding a flag and LOADEDTEXTURE pointer in the private scope, as well as a constructor in the public scope. public: //Set default member values CTexture() { bLoaded = FALSE; texture = NULL; } private: BOOL bLoaded; //Texture loaded flag LOADEDTEXTURE* texture; //The texture Alright, it's time once again to load a texture. The following function looks through the loadedTextures list to see if the requested texture has been loaded. If so, it assigns that to the CTexture. If not, it loads the texture using the LoadTexture() function provided above and adds it to the list before assigning it to the CTexture. This function goes in the public scope of CTexture: //Load texture from file int CTexture::Init (string sFilename) { D3DSURFACE_DESC surfaceDesc; LOADEDTEXTURE* newTexture; list<LOADEDTEXTURE*>::iterator itTextures; //Make sure texture is not already loaded if (bLoaded) return FALSE; //Convert filename to lowercase letters sFilename = strlwr((char *)sFilename.c_str ()); //Check if texture is in the loaded list for (itTextures = loadedTextures.begin (); itTextures != loadedTextures.end (); itTextures++) if ((*itTextures)->sFilename == sFilename) { //Get LOADEDTEXTURE object texture = *itTextures; //Increment reference counter (*itTextures)->referenceCount++; //Set loaded flag bLoaded = TRUE; //Successfully found texture return TRUE; } //Texture was not in the list, make a new texture newTexture = new LOADEDTEXTURE; //Load texture from file newTexture->texture = LoadTexture ((char*)sFilename.c_str()); //Make sure texture was loaded if (!newTexture->texture) return FALSE; //Get texture dimensions newTexture->texture->GetLevelDesc(0, &surfaceDesc); //Set new texture parameters newTexture->referenceCount = 1; newTexture->sFilename = sFilename; newTexture->width = surfaceDesc.Width; newTexture->height = surfaceDesc.Height; //Push new texture onto list loadedTextures.push_back (newTexture); //Setup current texture instance texture = loadedTextures.back(); bLoaded = TRUE; //Successfully loaded texture return TRUE; } You'll notice that the filename is converted to lowercase at the beginning of the function. This is because filenames are not case-sensitive in Windows. We do not want the program to load the same texture twice if sFilename is "hello.jpg" one time and "Hello.JPG" the next. Next up, a function to call when we're done with a particular instance of the texture. This function doesn't do much besides decrease the reference count of the LOADEDTEXTURE and clear the loaded flag of the CTexture instance. Here it is: //Unload a texture int CTexture::Close() { //Make sure texture is loaded if (!bLoaded) return FALSE; //Decrement reference counter and nullify pointer texture->referenceCount--; texture = NULL; //Clear loaded flag bLoaded = FALSE; //Successfully unloaded texture return TRUE; } So now we can load and clear our texture instances. We still have no way of getting them out of memory. Thats where the following two functions come in handy. The first goes through the list and releases all unreferenced textures. This can safely be called at any time, as none of the textures it releases are in use. The next releases all of the texture in the list. It should only be called at program termination to catch any textures other parts of the program failed to release. Here is the function that releases all unreferenced textures: Declaration:
//Release all unreferenced textures
static int GarbageCollect();
Function: //Release all unreferenced textures int CTexture::GarbageCollect() { list<LOADEDTEXTURE*>::iterator it; list<LOADEDTEXTURE*>::iterator itNext; //Go through loaded texture list for (it = loadedTextures.begin(); it != loadedTextures.end ();) if ((*it)->referenceCount <= 0) { //Get next iterator itNext = it; itNext++; //Release texture if ((*it)->texture) (*it)->texture->Release(); (*it)->texture = NULL; //Delete LOADEDTEXTURE object delete (*it); loadedTextures.erase (it); //Move to next element it = itNext; } else it++; //Increment iterator //Successfully released unreferenced textures return TRUE; } And this function will release all loaded textures: Declaration:
//Release all unreferenced textures
static int CleanupTextures();
Function: //Release all textures int CTexture::CleanupTextures() { list<LOADEDTEXTURE*>::iterator it; //Go through loaded texture list for (it = loadedTextures.begin(); it != loadedTextures.end (); it++) { //Release texture if ((*it)->texture) (*it)->texture->Release(); (*it)->texture = NULL; //Delete LOADEDTEXTURE object delete (*it); } //Clear list loadedTextures.clear (); //Successfully released all textures return TRUE; } Make sure both of the above functions are declared static in the class declaration. Alright, we're well equipped to manage our graphical resources. The only thing left to do is put them on the screen. Here are the functions for the CTexture class that use the two drawing functions provided above: //Draw texture with limited colour modulation void CTexture::Blit (int X, int Y, D3DCOLOR vertexColour, float rotate) { RECT rDest; //Setup destination rectangle rDest.left = X; rDest.right = X + texture->width; rDest.top = Y; rDest.bottom = Y + texture->height; //Draw texture BlitD3D (texture->texture, &rDest, vertexColour, rotate); } //Draw texture with full colour modulation void CTexture::BlitEx (int X, int Y, D3DCOLOR* vertexColours, float rotate) { RECT rDest; //Setup destination rectangle rDest.left = X; rDest.right = X + texture->width; rDest.top = Y; rDest.bottom = Y + texture->height; //Draw texture BlitExD3D (texture->texture, &rDest, vertexColours, rotate); } Like the BlitExD3D() provided earlier, CTexture::BlitEx() takes a pointer to a D3DCOLOR [4] array for its vertexColours argument. You may notice that these functions have no support for scaling. This helps them account for texture sizes being not what they seem (e.g. a 100x100 image being loaded onto a 128x128 texture). If you want to add scaling, make sure you account for this. It shouldn't be too much trouble. This concludes my article. You can check out the included sample code for a demonstration of the methods presented here in action. Please send any bugs, suggestions, feedback, or better rotation code to SiberianKiss@gdnMail.net. AppendixThis appendix is to address a few issues that were not sufficiently explained in the article (or in some cases left out altogether). The article is intended for beginners using Direct3D for 2D applications. For this reason, when writing the code I decided to go with flexibility over speed in many areas. However, I neglected to mention other, faster methods. I will attempt to do so here, as well as point out some omissions from the sample code. Issue 1: CleanupThis is just a little one, but apparently not very well known. When a call is made to IDirect3DDevice9::SetStreamSource(), a reference count is increased in the passed vertex buffer. If you do not call SetStreamSource() with another vertex buffer, or NULL, the Release() method of the previously passed vertex buffer will fail. This is just a small memory leak, and is cleaned up by DirectX upon program termination anyways. However if you're using a lot of different vertex buffers, you will notice some slowdown as your video memory is consumed. Also, it's just good programming practice to release everything properly yourself. For you copy-and-pasters, you'll want to add the following to your DirectX cleanup code:
//Clear stream source
DEVICE->SetStreamSource (0, NULL, 0, 0);
It must be before this:
//Release vertex buffer
if (vertexBuffer)
vertexBuffer->Release ();
NOTE: Replace DEVICE with the name of your IDirect3DDevice9* object And voila, no memory lost. Issue 2: Direct3D Uses Inclusive-Exclusive CoordinatesWow I can't believe people complained about this one. It's fairly common knowledge. When you're setting up a destination rectangle, the bottom and right coordinates are: RECT.bottom = RECT.top + height RECT.right = RECT.left + width You even save two cycles over inclusive-inclusive coordinates by losing the "- 1"s. Issue 3: Locking the Vertex BufferHere's where we get into some optimization. Locking is more expensive than simply using transformation and scaling matrices to move the vertexes of the quad around, but it also affords more flexibility. If you want to use colour modulation, you're pretty much stuck locking the vertex buffer in order to get access to that sweet, sweet colour member (don't worry, you're probably not losing too much processing time). However, if you're not using colour modulation and want an extra bit of speed, read on. This method uses transformation matrices to move around the vertices in the buffer. Understand that this method is not particularly compatible with the drawing methods presented in the first article. In fact, it requires a change to a fairly major item from the first article: the vertex format. You should use this one instead: //Custom vertex format const DWORD D3DFVF_TLVERTEX = D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1; //Custom vertex struct TLVERTEX { float x; float y; float z; D3DCOLOR colour; float u; float v; }; In order to use matrix transformations, an orthographic projection matrix must first be set up. This must be done at initialization, after the device is open for business. Here is the code:
D3DXMATRIX matOrtho;
D3DXMATRIX matIdentity;
//Setup orthographic projection matrix
D3DXMatrixOrthoLH (&matOrtho, RESOLUTION_WIDTH, RESOLUTION_HEIGHT, 1.0f, 10.0f);
D3DXMatrixIdentity (&matIdentity);
DEVICE->SetTransform (D3DTS_PROJECTION, &matOrtho);
DEVICE->SetTransform (D3DTS_WORLD, &matIdentity);
DEVICE->SetTransform (D3DTS_VIEW, &matIdentity);
NOTE: Replace DEVICE with the name of your IDirect3DDevice9* object NOTE 2: Replace RESOLUTION_WIDTH with the width of the backbuffer NOTE 3: Replace RESOLUTION_HEIGHT with the height of the backbuffer Please note that this method does not allow colour modulation (though a similar effect can be acheived with a simple vertex shader). It simply uses the vertex colours that are already specified in the buffer. Before using this method, we must ensure that the colour values in the buffer are all white, and that there is a valid quad in the buffer. This function will see to that: //Setup the quad void SetupQuad () { TLVERTEX* vertices = NULL; vertexBuffer->Lock(0, 4 * sizeof(TLVERTEX), (VOID**)&vertices, 0); //Setup vertices vertices[0].colour = 0xffffffff; vertices[0].x = 0.0f; vertices[0].y = 0.0f; vertices[0].z = 1.0f; vertices[0].u = 0.0f; vertices[0].v = 0.0f; vertices[1].colour = 0xffffffff; vertices[1].x = 1.0f; vertices[1].y = 0.0f; vertices[1].z = 1.0f; vertices[1].u = 1.0f; vertices[1].v = 0.0f; vertices[2].colour = 0xffffffff; vertices[2].x = 1.0f; vertices[2].y = -1.0f; vertices[2].z = 1.0f; vertices[2].u = 1.0f; vertices[2].v = 1.0f; vertices[3].colour = 0xffffffff; vertices[3].x = 0.0f; vertices[3].y = -1.0f; vertices[3].z = 1.0f; vertices[3].u = 0.0f; vertices[3].v = 1.0f; vertexBuffer->Unlock(); } If you are not using colour modulation at all in your program, this only needs to be called once, upon program startup. Here is code to actually draw the textured quad: //Draw a textured quad on the backbuffer void Blit(IDirect3DTexture9* texture, RECT* rDest, float rotate) { float X; float Y; D3DXMATRIX matTranslation; D3DXMATRIX matScaling; D3DXMATRIX matTransform; //Get coordinates X = rDest->left - (float)(RESOLUTION_WIDTH) / 2; Y = -rDest->top + (float)(RESOLUTION_HEIGHT) / 2; //Setup translation and scaling matrices D3DXMatrixScaling (&matScaling, (float)(rDest->right - rDest->left), (float)(rDest->bottom - rDest->top), 1.0f); D3DXMatrixTranslation (&matTranslation, X, Y, 0.0f); matTransform = matScaling * matTranslation; //Check if quad is rotated if (rotate) { D3DXMATRIX matRotate; //Create rotation matrix about the z-axis D3DXMatrixRotationZ (&matRotate, rotate); //Multiply matrices together matTransform *= matRotate; } //Draw the quad DEVICE->SetTransform (D3DTS_WORLD, &matTransform); DEVICE->SetTexture (0, texture); DEVICE->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2); } NOTE: Replace DEVICE with the name of your IDirect3DDevice9* object NOTE 2: Replace RESOLUTION_WIDTH with the width of the backbuffer NOTE 3: Replace RESOLUTION_HEIGHT with the height of the backbuffer Though slightly less powerful, this function should be faster than the ones presented in the article. It also has better-looking rotation. Please see NoLocking.zip for a demonstration of this technique in action. Issue 4: BatchingIt's time for a bit of batching. If you manage this well, this could potentially give you a rather large speed increase. In the Blit() functions provided in the article, SetTexture() is called every time you want to draw something, even if you're drawing the same texture on screen many times in a row. You can save some valuable cycles by simply not calling it if you're drawing the same texture multiple times. Another feature the blitting methods could use is setting up a source rectangle to blit from. This way you can have many images on one texture and draw all that are needed in the current frame with a single call to SetTexture(). Since texture coordinates are not specified in pixels (or texels), but rather as a value from 0.0f to 1.0f, a quick conversion must be done from pixel coordinates to texture coordinates. Furthermore, when drawing multiple quads from the same texture, it is possible to put them all in one large vertex buffer and draw them all in a single call. To do this, we must use the triangle-list primitive type, as opposed to the triangle fans used up to this point. To accomodate this, we will also need an index buffer to so we do not create duplicate vertices in the buffer. More information about index buffers can be found in the Two Kings tutorial here: http://www.two-kings.de/tutorials/d3d08/d3d08.html So, first thing we need to do is add another vertex buffer and an index buffer, as well as some state information about the vertex buffer and texture. Put this in a scope visible to all of your graphics code: //Vertex buffer and index buffer for batched drawing IDirect3DVertexBuffer9* vertexBatchBuffer; IDirect3DIndexBuffer9* indexBatchBuffer; //Max amount of vertices that can be put in the batching buffer const int BATCH_BUFFER_SIZE = 1000; //Vertices currently in the batching buffer int numBatchVertices; TLVERTEX* batchVertices; //Info on texture used for batched drawing float batchTexWidth; float batchTexHeight; You can tweak the BATCH_BUFFER_SIZE constant to a number that works best with your app. The lower it is, the more often it has to be flushed. The higher it is, the longer it takes to lock. Make sure it's a multiple of four though, so you can completely fill it with quads. Now we need to initialize our new buffers. Do this around the same time as you initialize the other vertex buffer (just make sure it's after the device is created):
//Create batching vertex and index buffers
d3dDevice->CreateVertexBuffer(BATCH_BUFFER_SIZE * sizeof(TLVERTEX),
D3DUSAGE_WRITEONLY, D3DFVF_TLVERTEX, D3DPOOL_MANAGED, &vertexBatchBuffer, NULL);
d3dDevice->CreateIndexBuffer (BATCH_BUFFER_SIZE * 3, D3DUSAGE_WRITEONLY,
D3DFMT_INDEX16, D3DPOOL_MANAGED, &indexBatchBuffer, NULL);
numBatchVertices = 0;
You may have noticed that the vertex and index buffers are both static. The vertex buffer is static because there will likely be a lot of switching between it and our original vertex buffer. The index buffer is static because we only change its contents once. Also, by making them static, we can keep them in the managed pool which makes for easier handling of alt-tabbing in a fullscreen app. Of course, you must remember to set the buffers free when you're done with them. Add this to your cleanup routine:
//Release batching buffers
if (vertexBatchBuffer)
vertexBatchBuffer->Release ();
if (indexBatchBuffer)
indexBatchBuffer->Release ();
Since you'll only be putting quads in the vertex buffer, you'll only need to fill up the index buffer once and never modify it again. Use this function to fill it (call it after the index buffer has been initialized):
//Fill the index buffer
void FillIndexBuffer ()
{
int index = 0;
short* indices = NULL;
//Lock index buffer
indexBatchBuffer->Lock(0, BATCH_BUFFER_SIZE * 3,
(void**) &indices, 0);
for (int vertex = 0; vertex < BATCH_BUFFER_SIZE; vertex += 4)
{
indices[index] = vertex;
indices[index + 1] = vertex + 2;
indices[index + 2] = vertex + 3;
indices[index + 3] = vertex;
indices[index + 4] = vertex + 1;
indices[index + 5] = vertex + 2;
index += 6;
}
//Unlock index buffer
indexBatchBuffer->Unlock ();
}
Alright. Now you just have to let your computer know you're ready to batch a bunch of quads together. Call this function every time you want to draw a series of images from a single texture: //Get ready for batch drawing void BeginBatchDrawing (IDirect3DTexture9* texture) { D3DXMATRIX matIdentity; D3DSURFACE_DESC surfDesc; //Lock the batching vertex buffer numBatchVertices = 0; vertexBatchBuffer->Lock (0, BATCH_BUFFER_SIZE * sizeof(TLVERTEX), (void **) &batchVertices, 0); //Get texture dimensions texture->GetLevelDesc (0, &surfDesc); batchTexWidth = (float) surfDesc.Width; batchTexHeight = (float) surfDesc.Height; //Set texture d3dDevice->SetTexture (0, texture); //Set world matrix to an identity matrix D3DXMatrixIdentity (&matIdentity); d3dDevice->SetTransform (D3DTS_WORLD, &matIdentity); //Set stream source to batch buffer d3dDevice->SetStreamSource (0, vertexBatchBuffer, 0, sizeof(TLVERTEX)); } You should probably make this function set a flag somewhere so the program can tell it's in the middle of a batch drawing process. I'm leaving it out of the example to keep things simple (the demos already have far too many global variables for my liking). Now it's time to put some quads in the vertex buffer. This function will do just that for you:
//Add a quad to the batching buffer
void AddQuad (RECT* rSource, RECT* rDest, D3DCOLOR colour)
{
float X;
float Y;
float destWidth;
float destHeight;
//Calculate coordinates
X = rDest->left - (float)(d3dPresent.BackBufferWidth) / 2;
Y = -rDest->top + (float)(d3dPresent.BackBufferHeight) / 2;
destWidth = (float)(rDest->right - rDest->left);
destHeight = (float)(rDest->bottom - rDest->top);
//Setup vertices in buffer
batchVertices[numBatchVertices].colour = colour;
batchVertices[numBatchVertices].x = X;
batchVertices[numBatchVertices].y = Y;
batchVertices[numBatchVertices].z = 1.0f;
batchVertices[numBatchVertices].u = rSource->left / batchTexWidth;
batchVertices[numBatchVertices].v = rSource->top / batchTexHeight;
batchVertices[numBatchVertices + 1].colour = colour;
batchVertices[numBatchVertices + 1].x = X + destWidth;
batchVertices[numBatchVertices + 1].y = Y;
batchVertices[numBatchVertices + 1].z = 1.0f;
batchVertices[numBatchVertices + 1].u = rSource->right / batchTexWidth;
batchVertices[numBatchVertices + 1].v = rSource->top / batchTexHeight;
batchVertices[numBatchVertices + 2].colour = colour;
batchVertices[numBatchVertices + 2].x = X + destWidth;
batchVertices[numBatchVertices + 2].y = Y - destHeight;
batchVertices[numBatchVertices + 2].z = 1.0f;
batchVertices[numBatchVertices + 2].u = rSource->right / batchTexWidth;
batchVertices[numBatchVertices + 2].v = rSource->bottom / batchTexHeight;
batchVertices[numBatchVertices + 3].colour = colour;
batchVertices[numBatchVertices + 3].x = X;
batchVertices[numBatchVertices + 3].y = Y - destHeight;
batchVertices[numBatchVertices + 3].z = 1.0f;
batchVertices[numBatchVertices + 3].u = rSource->left / batchTexWidth;
batchVertices[numBatchVertices + 3].v = rSource->bottom / batchTexHeight;
//Increase vertex count
numBatchVertices += 4;
//Flush buffer if it's full
if (numBatchVertices == BATCH_BUFFER_SIZE)
{
//Unlock vertex buffer
vertexBatchBuffer->Unlock();
//Draw quads in the buffer
d3dDevice->DrawIndexedPrimitive (D3DPT_TRIANGLELIST, 0, 0,
numBatchVertices, 0, numBatchVertices / 2);
//Reset vertex count
numBatchVertices = 0;
//Lock vertex buffer
vertexBatchBuffer->Lock (0, BATCH_BUFFER_SIZE * sizeof(TLVERTEX),
(void **) &batchVertices, 0);
}
}
As you can see, the function has basic colour modulation functionality. It can easil bey extended this to behave similarly to the BlitExD3D() function from the article, but I'll leave that up to you. Also, when the vertex buffer gets full, this function automatically draws whatever's inside it, and prepares to receive a new batch of quads. //Finish batch drawing void EndBatchDrawing() { //Unlock vertex buffer vertexBatchBuffer->Unlock(); //Draw the quads in the buffer if it wasn't just flushed if (numBatchVertices) d3dDevice->DrawIndexedPrimitive (D3DPT_TRIANGLELIST, 0, 0, numBatchVertices, 0, numBatchVertices / 2); //Set stream source to regular buffer d3dDevice->SetStreamSource (0, vertexBuffer, 0, sizeof(TLVERTEX)); //Reset vertex count numBatchVertices = 0; } A demonstration of batching building on the NoLocking.zip app can be found in Batching.zip. The batching code presented relies heavily upon global variables and has very little error checking. I urge the reader to encapsulate the batching functionality in a class, and add a lot of error checking (asserting that the vertex buffer is not overflowing in AddQuad() would be a good start). Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|