Part Overview
Step 4: Acquire the Device
5.3 Sprites and Animation
5.3.2 The Sprite Demo
5.3.2.2 Demo Application Class Data Members
Below is the definition of the Sprite demo application class:
class SpriteDemo : public D3DApp {
public:
SpriteDemo(HINSTANCE hInstance);
~SpriteDemo();
IDirect3DTexture9* mBulletTex;
D3DXVECTOR3 mBulletCenter;
std::1ist<BulletInfo> mBulletList;
const float BULLET_SPEED;
const float MAX_SHIP_SPEED;
const float SHIP_ACCEL;
const float SHIP_DRAS;
};
mSprite: The one and only sprite interface through which we do all of our sprite drawing.
mBkgdTex: Texture containing the background image. In case you are not familiar with textures from general computer game experience, they are essentially 2D image data that gets mapped onto polygons; here it will be the 2D image data mapped onto a sprite.
mBkgdCenter: The center of the background measured in image coordinates. For example, if the texture dimensions are 512×512, then the center is (256, 256). Note that in image coordinates, the positive y-axis goes down and the positive x-axis goes to the right, and the units are in pixels.
mShipTex: Texture containing the ship image.
mShipCenter: The center of the ship measured in image coordinates.
mShipPos: The position of the ship in world space.
mShipSpeed: The speed of the ship.
mShipRotation: An angle, in radians, storing the current rotation of the ship measured from the positive y-axis, where positive angles go counterclockwise when looking down the negative z-axis (i.e., clockwise when looking down the positive z-axis).
mBulletTex: Texture containing the bullet image.
mBulletCenter: The center of the ship measured in image coordinates.
mBulletList: A list container storing the bullets the player has fired and that need to be updated and drawn; that is, as the player fires a bullet, we add a bullet to the list. After a short amount of time, when we know the bullet is far away and off the screen, we remove it from the list. Although we use a list here, a fixed size array could also work, where you cap the maximum number of bullets allowed at once. This would be more efficient and prevent frequent memory allocations and deallocations. However, for a demo, the list implementation is fine.
BULLET_SPEED: A constant that specifies the speed of the bullet.
MAX_SHIP_SPEED: A ceiling that specifies the maximum speed of the ship.
SHIP_ACCEL: A constant that specifies how the ship accelerates based on user input.
SHIP_DRAG: A constant in the range [0, 1] that specifies a drag force to decelerate the ship. The drag force is proportional to the velocity of the ship;
that is, the faster the ship is going, the higher the drag force. Mathematically, the drag force is given by -bv. Here b is the drag constant SHIP_DRAG , and v denotes the speed of the ship. The negative sign indicates the drag force always opposes (goes in opposite direction of) the ship's direction.
Note We do not claim that our physics properties are completely realistic. For instance, we do not take mass into consideration. We just experiment with the numbers until we get a satisfactory result.
What follows is an explanation of each method's implementation.
5.3.2.3 SpriteDemo
In the constructor, we create our sprites and textures, and perform general variable initialization.
SpriteDemo::SpriteDemo(HINSTANCE hInstance)
HR(D3DXCreateSprite(gd3dDevice, &mSprite));
HR(D3DXCreateTextureFromFile(
The implementation is rather straightforward. We obtain a pointer to an ID3DXSprite interface with D3DXCreateSprite , create the textures with D3DXCreateTextureFromFile, and initialize some of the data members to default values, as well as set the application constants. Finally, we call
onResetDevice, which sets some device states. We have to call this method because we want these device states to be set during construction in addition to when the device is reset.
We have not talked about D3DXCreateTextureFromFile . This function loads the image data from a file (supported image formats include BMP, DDS, DIB, JPG, PNG, and TGA), and then uses the data to create an IDirect3DTexture9 object (i.e., a texture that can be mapped onto a surface — a sprite in this case). The first parameter is a pointer to the rendering device, the second parameter is the filename of the image, and the third parameter returns a pointer to the created texture.
5.3.2.4 ~SpriteDemo
In the destructor, we delete any dynamic memory and release all COM interfaces. The implementation is self-explanatory.
SpriteDemo::~SpriteDemo()
For this demo, we do not use any fancy features and so there are no capabilities to check.
bool SpriteDemo::checkDeviceCaps()
Recall that in the onLostDevice method, we place any code that needs to be executed before the rendering device is reset.
void SpriteDemo::onLostDevice() {
mGfxStats - >onLostDevice();
HR(mSprite - >OnLostDevice());
}
As shown, each of the sprite objects needs to execute some code prior to a device reset, and so does the GfxStats object.
5.3.2.7 onResetDevice
Recall that in the onResetDevice method, we place any code that needs to be executed after the rendering device is reset. For this demo, we need to set a variety of device states (and in particular, render states), which control how the rendering device draws the graphics. Device states are lost when the device is reset;
therefore, we set the device states in this function so that they are automatically restored after the device is reset. Note that these states will be elaborated on in later chapters of the book; so for now, we just present an intuitive explanation of what the states do (see the comments).
void SpriteDemo::onResetDevice()
D3DXMatrixLookAtLH(&V, &pos, &target, &up);
HR(gd3dDevice - >SetTransform(D3DTS VIEW, &V));
// The following code defines the volume of space the
D3DXMatrixPerspectiveFovLH(&P, D3DX_PI*0.25f, width/height, 1.0f, 5000.0f);
HR(gd3dDevice - >SetTransform(D3DTS_PROJECTION, &P));
// This code sets texture filters, which helps to smooth // out distortions when you scale a texture.
HR(gd3dDevice - >SetSamplerState(
0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR));
HR(gd3dDevice - >SetSamplerState(
0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR));
HR(gd3dDevice - >SetSamplerState(
0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR));
// This line of code disables Direct3D lighting.
HR(gd3dDevice - >SetRenderState(D3DRS_LIGHTING, false));
// The following code specifies an alpha test and // reference value.
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHAREF, 10));
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATER));
// The following code is used to set up alpha blending.
HR(gd3dDevice - >SetTextureStageState(
0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE));
HR(gd3dDevice - >SetTextureStageState(
0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1));
HR(gd3dDevice - >SetRenderState(
D3DRS_SRCBLEND, D3DBLEND_SRCALPHA));
HR(gd3dDevice - >SetRenderState(
D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA));
// Indicates that we are using 2D texture coordinates.
HR(gd3dDevice - >SetTextureStageState(
0, D3DTSS TEXTURETRANSFORMFLAGS, D3DTTFF COUNT2));
}
Although we explain these function calls in much more depth in later chapters, the idea behind the alpha test and alpha blending does deserve further discussion here.
Recall what our ship image looks like (see Figure 5.2). If we render the ship without alpha testing, the black background of the ship is also drawn, as shown in Figure 5.4. To remedy this, we insert a fourth channel into the image called an alpha channel. Many 2D paint programs provide a way to do this; Figure 5.5 shows how it is done in Adobe Photoshop 7.0. Note that an 8-bit alpha channel will typically be inserted, which is represented graphically as a grayscale map, and each pixel value can take on a value from 0 to 255, which represents shades of gray from black to white, respectively.
Figure 5.4: By drawing the sprites without any extra work, the black background is drawn, which is not what we want!
Figure 5.5: Creating an alpha channel in Adobe Photoshop 7.0. You must also save the file to a format that supports an alpha channel, such as a 32-bit BMP file.
Now each color pixel has a corresponding alpha pixel in the alpha channel. In the alpha channel, we manually mark (with a paint program) the pixels corresponding to the main color image we want to be displayed with white (255) and the pixels we do not want to show up with black (0) (see Figure 5.6). Then, the alpha test D3DCMP_GREATER does the following: For each color pixel being processed by Direct3D, it looks at the corresponding alpha value for the pixel, then it asks if the alpha value is greater than the reference value specified by the render state D3DRS_ALPHAREF (in our code, we specified 10 for the reference value). If it is greater, then the alpha test succeeds and the pixel is drawn; else, the alpha test fails and the pixel is not drawn. Thus, with this setup, the pixels we marked black in the alpha channel will not be drawn, since 0 = 10; i.e., the alpha test will fail for those pixels. The pixels in the alpha channel we marked white (255) will be drawn, since 255 > 10, which is the exact result we want.
Figure 5.6: Here we show the sprites and their corresponding alpha channels. In the alpha channel, the white pixels mark the pixels we want to draw and the black pixels indicate the pixels we want blocked (not drawn). We actually draw the bullet with alpha blending, which makes the bullet
semi-transparent.
Alpha blending is somewhat like alpha testing, except that instead of blocking the pixel completely, the alpha channel specifies how the image should be blended together with the background pixels on which the sprite is being drawn, thus making the object somewhat transparent.
5.3.2.8 updateScene
The updateScene method is simple, and merely calls other functions that do the real work; specifically, updating the graphics statistics, polling the mouse and keyboard (i.e., getting their current states), updating the ship, and updating the bullets. It also sets the number of triangles and vertices currently rendered in the scene — each sprite is built from a quad (four vertices) of two triangles.
void SpriteDemo::updateScene(float dt) {
// Two triangles for each sprite - - two for background, // two for ship, and two for each bullet. Similarly, // four vertices for each sprite.
mGfxStats - >setTriCount(4 + mBulletList.size()*2);
mGfxStats - >setVertexCount(8 + mBulletList.size()*4);
mGfxStats - >update(dt);
D3DXVECTOR3 shipDir( - sinf(mShipRotation), cosf(mShipRotation), 0.0f);
// Update position and speed based on time.
mShipPos += shipDir * mShipSpeed * dt;
mShipSpeed - = SHIP_DRAG * mShipSpeed * dt;
}
The first four conditional statements check Direct Input to see if the "A", "D", "W", or "S" keys are pressed. The "A" and "D" keys rotate the ship counterclockwise and clockwise, respectively, when looking down the -z-axis. The "W" and "S" keys accelerate and decelerate the ship along its current direction, respectively.
Note Average acceleration is defined to be the change in velocity over time, i.e., a = ∆v/∆t. This implies that the velocity changes by ∆v = a·∆t over the time period ∆t. Thus, to update the ship's velocity, we just add/subtract a·∆t to it (in our code, a = SHIP_ACCEL and ∆t = dt). Note that acceleration and velocity are usually vector-valued quantities, but because we only accelerate along the direction line of the ship, we can omit the arrowheads and treat them as 1D vectors.
The next two conditional statements ensure that the ship never goes faster than the prescribed MAX_SHIP_SPEED value.
Given the ship's rotation angle, measured from the positive y-axis, we can infer the ship's direction using trigonometry, as shown in Figure 5.7.
Figure 5.7: Computing the direction vector given the rotation angle measured from the positive y-axis in the counterclockwise direction while looking down the -z-axis (i.e., clockwise while looking down the +z-axis).
Once we have the ship's direction, we can move along that direction given the ship's speed.
Note Average velocity is defined to be the change in position (displacement) overtime, i.e., v = ∆x/∆t. This implies that the position changes by ∆x = v·∆t over the time period ∆t. Thus, to update the ship's position, we just add/subtract v·∆t to it (in our code, v = mShipSpeed and Δt = dt). Note that velocity and displacement are usually vector-valued quantities, but because we only move along the direction line of the ship, we can omit the arrowheads and treat them as 1D vectors.
Finally, the speed is modified by a drag force, which always opposes the motion of the ship (hence the subtraction). The drag force works like friction to slow the ship down when the user is not accelerating.
5.3.2.10 updateBullets
Each time a bullet is fired, we add a bullet to a list that maintains the position and velocity of all the bullets currently active. Based on the bullets' velocities, we then update the position of the bullets with respect to time (in the same way we updated the ship's position with respect to time based on its velocity). After a specified amount of time, when we know the bullet is far away and off the screen, we can remove it from the list.
void SpriteDemo::updateBullets(float dt)
std::list<BulletInfo>::iterator i = mBulletList.begin();
while( i != mBulletList.end() )
}
5.3.2.11 drawScene
This method is self-explanatory; the hard work is done by other helper methods.
void SpriteDemo::drawScene() {
// Clear the back buffer and depth buffer.
HR(gd3dDevice - >Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0));
HR(gd3dDevice - >BeginScene());
HR(mSprite - >Begin(D3DXSPRITE_OBJECTSPACE|
D3DXSPRITE_DONOTMODIFY_RENDERSTATE));
D3DXMatrixScaling(&texScaling, 10.0f, 10.0f, 0.0f);
HR(gd3dDevice - >SetTransform(D3DTS_TEXTURE0, &texScaling));
// Position and size the background sprite - - remember that // we always draw the ship in the center of the client area // rectangle. To give the illusion that the ship is moving, // we translate the background in the opposite direction.
D3DXMATRIX T, S;
D3DXMatrixTranslation(&T, - mShipPos.x, - mShipPos.y, - mShipPos.z);
D3DXMatrixScaling(&S, 20.0f, 20.0f, 0.0f);
HR(mSprite - >SetTransform(&(S*T)));
// Draw the background sprite.
HR(gd3dDevice - >SetTransform(D3DTS_TEXTURE0, &texScaling));
}
The first few lines set a texture coordinate scaling transform. (Right now you probably don't even know what texture coordinates are, but loosely, they specify how a texture is mapped over a surface.) Scaling the texture coordinates by 10 in both the x- and y-directions has the effect of tiling the texture in both directions 10 times.
Figure 5.8 illustrates tiling.
Figure 5.8: The background texture tiled twice in both the x- and y-directions. If you look at the image carefully, you will see that the image is repeated in each of the four quadrants. By repeating the texture (tiling), we get "extra" resolution without adding additional memory; of course, the disadvantage is that the texture does repeat, which breaks the realism.
The reason we tile is because we are going to be stretching a texture over a large sprite. To cover the entire sprite surface, the texture will need to be stretched much larger than its actual dimensions. This stretching will cause distortions; you are probably familiar with the loss of detail that occurs when you enlarge an image in a paint program, and the same thing happens here. By having Direct3D tile the texture 10 times, we essentially make the texture have 10 times the resolution, and thus the details are preserved over the background sprite. To do this, note that the texture must tile nicely; otherwise discontinuities will be observed on the edges where the images are tiled. To help you understand this, we recommend that you experiment with different texture scaling sizes; for example, try 1, 2, 5, 10, and 20.
The next key idea with the above code is the translation transformation. Recall that we always keep the ship in the center of the client area rectangle. Thus, we do not translate the ship itself as it moves, but instead translate the background sprite in the opposite direction; visually, this has the same effect as translating the ship directly and keeping the background stationary.
In addition to translating the background, we also apply a scaling transformation (represented by a scaling matrix). This simply makes the background bigger — the background represents the terrain over which the ship will be flying, so it ought to be big. In this code, we scale it by 20 times.
The next bit of code draws the background; see §5.3.1 for a description of the parameters for ID3DXSprite::Draw . Finally, the code finishes up by restoring the default texture coordinate scaling transformation back to 1 (i.e., no scaling).
5.3.2.13 drawShip
The following code draws the ship sprite:
void SpriteDemo::drawShip() {
// Turn on the alpha test.
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHATESTENABLE, true));
// Set ship's orientation.
D3DXMATRIX R;
D3DXMatrixRotationZ(&R, mShipRotation);
HR(mSprite - >SetTransform(&R));
// Draw the ship.
HR(mSprite - >Draw(mShipTex, 0, &mShipCenter, 0, D3DCOLOR_XRGB(255, 255, 255)));
HR(mSprite - >Flush());
// Turn off the alpha test.
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHATESTENABLE, false));
}
Recall that in §5.3.2.7, we set some alpha test render states, and also gave an explanation of what the alpha test does for us. Although we set some properties of the alpha test previously, we did not actually enable it. So the very first thing we do before we draw the ship sprite is enable the alpha test.
Although we do not explicitly translate the ship (we translate the background in the opposite direction instead), we do explicitly rotate the ship. This is accomplished by applying a rotation transformation based on the current angle the sprite makes with the y-axis.
Finally, we draw the ship sprite and then disable the alpha test because we are done with it.
5.3.2.14 drawBullets
The process of drawing the bullet sprites is similar to drawing the ship sprite:
void SpriteDemo::drawBullets() {
// Turn on alpha blending.
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHABLENDENABLE, true));
// For each bullet...
std::list<BulletInfo>::iterator i = mBulletList.begin();
while( i != mBulletList.end() )
HR(gd3dDevice - >SetRenderState(D3DRS_ALPHABLENDENABLE, false));
}
The first thing to note is that we do not use alpha testing with the bullets; instead, we use alpha blending. Both techniques require an alpha channel, but unlike the alpha test, which simply accepts or rejects pixels, alpha blending blends the texture image pixels with the back buffer pixels on which the textured sprite is drawn.
This is useful for effects (such as gunfire, laser beams, and smoke) that are not completely opaque.
In alpha blending, the alpha channel pixels can be thought of as weights that specify how much color to use from the texture image (source color) and from the back buffer pixels (destination color) when generating the new back buffer pixel color. Alpha pixels close to white weight the source pixels more heavily, and alpha pixels close to black weight the destination pixels more heavily. For example, pure white uses 100% source color, and pure black uses 100% destination color.
Thus, if you do not want to overwrite parts of the back buffer, then specify black in the alpha channel for those pixels (sort of like alpha testing).
Now, once alpha blending is enabled, to draw the bullets we simply loop through each one in the list, set its rotation and translation matrix to orient and position it, and then draw it. Lastly, before exiting the method, we disable alpha blending.