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.
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.
);
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));
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.
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.
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:
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.