Experiments In Game Programming
Main
 Home
XNA - C#
 Coming Soon!
 Level Editing - GTK
DirectX 9 - C++
 Downloads
 Disclaimer
 Introduction
Part 1 - DirectX
 1 - Breakout
 2 - Create DX
 3 - 2d Images
 4 - 3d Models
 5 - Cameras & Lights
 6 - Animation Timing
 7 - Keyboard/Mouse
 8 - Sound
Part 2 - Breakout
 1 - Art and Sounds
 2 - The Menu
 3 - Starting Breakout
 4 - The Level
 5 - The Paddle
 6 - The Ball
 7 - Finishing Touches

Untitled Document

Chapter 3

 

Drawing 2d Images

 

 

DirectX 9 has some very nice, and not too difficult to use functions to facilitate 2d drawing. In this chapter we are going to learn how to use these functions to load an image, and display it in different positions and sizes on the screen.

Topics Covered in this chapter:
  • What is necessary for 2d Drawing in Direct3d
  • Creating our 2d Surface Class
  • Drawing some images using our 2d Surface Class

DirectX handles 2d drawing using off-screen surfaces. An off-screen surface is an area of video or system memory that holds graphic information. In order to do 2d drawing we need to create an off-screen surface, load an image onto it, and then, during our render call, copy the contents of the surface to the backbuffer. When the render call is completed with end render, the buffer is presented and our image will be drawn where ever we copied it to on the backbuffer. Two important things to remember when drawing 2d surfaces are: (1) you can certainly draw one surface on top of another, so the order you draw them in is important to how they will display, (2) Surfaces that have drawing destinations that exceed the bounds of the backbuffer will not get rendered. That means if your surface overlaps the edge of the screen its not going to show up without adjusting the width and height of the destination so that it no longer overlaps. We won't be covering how to do that now, but I may include a finished 2dSurface class at the end of part one that does have that functionality.

Lets take a look at the 2dSurface class from the chapter 3 code. The header is basically the same as you have seen in the above classes, the definitions of classes that will be used, a definition of the functions in the 2dSurface class, and declarations of the private variables. From this point on I will not be showing the header files unless they have a new concept in them. I still recommend you take a look at them, and if you are following along with the code there is no shame in copying and pasting in the headers because they are only function definitions after all. I will also be skipping over the constructors and deconstructors because in all of the classes we will be writing they will all do the same thing. That being, nulling our objects before we use them, and deleting them after we are done. Again it is important that they are there, but there isn't an awful lot more to say about them.

The first function we are going to take a look at is loadSurface. loadSurface takes 2 parameters, our device, and the name of the image file. The first varaible we see is imageScale, defaulted to 100% it will be used later when we wish to change the size the image is rendered. The first function we call is D3DXGetImageInfoFromFile, we call this with our image file, and a temporary D3DXIMAGE_INFO structure imageInfo. After the call, imageInfo contains the width and height of our image. We need the width and height to create a surface of the right size to load our image onto. Now that the imageInfo structure contains the dimentions of the image we call CreateOffscreenPlainSurface, to create a surface for us to load our image onto. CreateOffscreenPlainSurface requires a width and height for the surface, a color format, the memory pool, a pointer to our surface, and lastly a NULL. Don't worry about the last NULL, my understanding is it is an extra handle that generally isn't used. Once the surface is created we call D3DXLoadSurfaceFromFile to actually load the image from the file on to our surface. D3DXLoadSurfaceFromFile requires a surface to load the image to, a filename, and a filter. It also has options for a destination on the surface, a source area from the image we are loading, a color to make transparent, and a D3DXIMAGE_INFO structure. We, however, are only going to use the surface and the filename, and set the filter to default. Once we have loaded the surface with our image we set the default source and destination.
bool Surface::loadSurface(LPDIRECT3DDEVICE9 device, std::string filename)
{
     imageScale = 100; //set our image scale to 100%

     HRESULT hResult;
     // Get the width and height of the image
     D3DXIMAGE_INFO imageInfo;
     hResult = D3DXGetImageInfoFromFile(filename.c_str(), &imageInfo);
     if FAILED (hResult){
         return false;
     }
     //record the height and width
     height = imageInfo.Height;
     width = imageInfo.Width;
    

     //create the Off Screen Surface
     hResult = device->CreateOffscreenPlainSurface(width, //surface width
     height, //surface height
     D3DFMT_A8R8G8B8, //surface format, D3DFMT_A8R8G8B8 is a 32 bit format with      8 alpha bits
     D3DPOOL_DEFAULT, //create it in the default memory pool
     &surface, //the pointer to our surface
     NULL
     );

     if (FAILED(hResult))
     return false;

     //load the surface from the a file
     hResult = D3DXLoadSurfaceFromFile(surface, //the surface we just created
     NULL, //palette entry, NULL for non 256 color formats
     NULL, //dest rect, NULL for the whole surface
     filename.c_str(), // the file we wish to load
     NULL, // Source Rect, NULL for the whole file
     D3DX_DEFAULT, //Filter
     0, // Color key, color that should be used as transparent.
     NULL // pointer to a D3DXIMAGE_INFO structure, for holding info about the      image.
     );

     if (FAILED(hResult))
     return false;

     //set rects
     destRect.left = 0;
     destRect.top = 0;
     destRect.bottom = destRect.top + height;
     destRect.right = destRect.left + width;

     srcRect.left = 0;
     srcRect.top = 0;
     srcRect.bottom = destRect.top + height;
     srcRect.right = destRect.left + width;

     return true;
}
The next function we look at is render. Render handles drawing our image by copying the image on our surface onto the backbuffer, and requires the device as a parameter. First, we adjust the scale of our destination rectangle by multiplying the width and height by the imageScale. Then we get the backbuffer from our device, and lastly we call StretchRect to copy our surface to the backbuffer. As well as the surface and the backbuffer, StretchRect also requires a source and destination, and a texture filter which we will set to D3DTEXF_NONE.
void Surface::render(LPDIRECT3DDEVICE9 pDevice)
{
     //Scale the destination based on current imageScale
     destRect.bottom = destRect.top + (int)(height * (imageScale / 100));
     destRect.right = destRect.left + (int)(width * (imageScale / 100));

     IDirect3DSurface9* backbuffer = NULL;
     pDevice->GetBackBuffer(0,0,D3DBACKBUFFER_TYPE_MONO, &backbuffer);
    
     pDevice->StretchRect(surface, &srcRect, backbuffer, &destRect,      D3DTEXF_NONE);
}

The destination rectangle controls where the image will be copied to on the backbuffer. The setPosition function sets the position of top left corner of the destination rectangle.
void Surface::setPosition(int x, int y){
     destRect.left = x;
     destRect.top = y;
     destRect.bottom = destRect.top + height;
     destRect.right = destRect.left + width;
}

The setSize function just sets the imageScale variable that is used before rendering to scale the destination rectangle. imageScale is just an INT and is treated as a percent when scaling the rectangle.
void Surface::setSize(int percent){
     imageScale = percent;
}

 The next two functions are setSrcRect, and setDestRect, and are to be used as an alternative to the setPosition and setSize functions. The first setScrRect is used to choose a part of the source image to display. The second setDestRect is used to set the destination rectangle, if you wish to change the shape or size of it. These functions come in handy if for instance you want to scroll an image larger than the screen, or to make an image display along the edge of the screen without disappearing because the destination is off the buffer.

void Surface::setSrcRect(int left, int top, int width, int height){
     srcRect.left = left;
     srcRect.top = top;
     srcRect.bottom = srcRect.top + height;
     srcRect.right = srcRect.left + width;
}


void Surface::setDestRect(int left, int top, int width, int height){
     destRect.left = left;
     destRect.top = top;
     destRect.bottom = destRect.top + height;
     destRect.right = destRect.left + width;
}

That's all for the 2dSurface class. Lets add it into our GameMain, and setup a test image.

First we add a forward class definintion, and a private varible to hold our surface in the GameMain header.

#pragma once
class dxMgr;
class dxText;
class Surface;
....
private:

dxMgr* dxManager;
dxText* textManager;
Surface* testSurface;
};

Then we create a new surface in the Init function, and load the surface testImage.jpg.
bool GameMain::init(HWND wndHandle)
{
     .....

     //for testing purposes create a new 2d surface
     testSurface = new Surface();
     testSurface->loadSurface(dxManager->getD3DDevice(),"testImage.jpg");
     if (!testSurface){
         return false;
     }


     return true;
}

Now that we have created our surface to draw it we need to add it to our render call in the update function. I have added 2 calls to our images render, once at 100% scale, and then again in a different position at 250% scale.
void GameMain::update(void)
{
     //call our update function
     // begin rendering
     dxManager->beginRender();
     //game render calls go here
     textManager->drawText("Hello World", 30, 30, 300, 300);

     testSurface->setSize(100);//set the size to 100%
     testSurface->setPosition(60,80); //set the top left corner to 60,80
     testSurface->render(dxManager->getD3DDevice()); //draw our image


     testSurface->setSize(250); //set the size to 250%
     testSurface->setPosition(200,300); //set the top left corner to 200,300
     testSurface->render(dxManager->getD3DDevice()); //draw our image


    
     //end render
     dxManager->endRender();

}

Thats all there is to it. Compiling and running this will draw 2 of our test images onto the screen, one in the top left at normal size, and one much larger in the center near the bottom.



Summing Up - Chapter 3

In this chapter we created a class to load and draw images onto the screen. One problem, however, is that the images cannot go over the edge of the backbuffer. You can handle this problem by adjusting your source and destination rectangles. Before moving on, play around with the transparency color, something clearly useful. It might take a few trys to get the color code right. Black being color 0, and white being the highest value for your color mode. Also, take a play with adjusting the source and destinations using the setSrcRect and setDestRect functions instead of using the position and scale functions.

*** Corrective Note *** The method used to draw the image to the backbuffer does NOT support transparency. As an alternative you should use LPD3DXSPRITE interface, you can download my replacement class here Sprite.zip . This does not effect the rest of the tutorials/project as image transparency is not used.

 
 ©2008 David Whittaker