News
DirectX
Links
Contact Me

In Association with Amazon.com
In Association with Amazon.ca

$5 via PayPal


Direct3D Lesson 9



Direct3D Lesson 9:Moving Bitmaps in 3D Space

This tutorial builds a simple particle system. We use the dhEngine class to get the initialization code out of the way so we can focus on the real task at hand. If you don't understand the basic initialization then you should review the earlier tutorials before tackling this one.

To keep things simple, our particle system has a static number of particles (50) and they are always visible (or alive). Many particle systems have a 'lifespan' attribute in each particle so that they 'die' after a certain amount of time and are reborn. Ours will simply move around in 3D space until you exit the program.

Each particle (a star in this case) has a colour and position. For this system it's convenient to represent the position is terms of distance from the center and angle of rotation. In most cases the position would be represented by X, Y & Z coordinates. How you represent the data isn't important, just make sure it makes sense to you and that the solution fits the problem.

Since our system has a static number of particles, we use an array. If your system was highly dynamic it might be best to use a linked list.

//Maximum number of stars
const	int cMaxStars=50;

//All the information we need for our simple particle system of stars
struct star_struct{
	char r, g, b;        // Stars Color
	float dist;          // Stars Distance From Center
	float angle;         // Stars Current Angle
};

//Array to hold our stars
star_struct g_stars[cMaxStars];

Now we declare the vertex structures & vertices. Each particle will be represented as a quad (2 triangles) and we need untransformed coordinates for positioning and texture coordinates to map the texture. There is no need for a diffuse component so we leave it out.

Since our quad is equal in height & width and is centered on the origin, each of the X & Y coordinates will be the same (not counting sign). This makes it convenient to define a constant (cStarSize) and use it for the dimensions. This way if we want to tweak the size of the particle, we can just change the constant.

//X,Y,Z & texture coords.
struct my_vertex{
    float x, y, z;
    float tu,tv;
};

#define D3D8T_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_TEX1)

//Dimensions of our star.  by defining it here we can quickly tweak our stars
const float cStarSize=0.75f;

my_vertex g_vertices[] ={
    {-cStarSize,  -cStarSize, 0.0f, 0.0f, 1.0f}, // x, y, z, tu, tv
    {-cStarSize,   cStarSize, 0.0f, 0.0f, 0.0f},
    { cStarSize,  -cStarSize, 0.0f, 1.0f, 1.0f},

    { cStarSize,  -cStarSize, 0.0f, 1.0f, 1.0f},
    {-cStarSize,   cStarSize, 0.0f, 0.0f, 0.0f},
    { cStarSize,   cStarSize, 0.0f, 1.0f, 0.0f}
};

Variable step timing allows the program to run at the same speed regardless of the CPU & graphics card in the current PC. Every frame we calculate the time that has passed since the previous frame and use that value to scale our movement.

//Time (in ticks) when the last frame was drawn.  Used to control speed
DWORD g_last_frame_time;

All of the variables that are particle-specific are stored in the star_struct. There are a few variables that affect the whole system.

float	g_zoom=0.0f;      // Viewing Distance Away From Stars
float g_tilt=0.0f;      // Tilt The View (X-Axis)
float	g_spin=0.0f;      // Spin Twinkling Stars (Z-axis), used for twinkle effect

bool g_twinkle=false;   //Are we twinkling?

As usual, we do all of the one-time set up for our scene in init_scene(). We load our texture, set up our Render States & Texture Stage States. We also create our vertex buffer (which holds a single quad) and fill in our vertex data.

void init_scene(void){
HRESULT hr;
unsigned char *vb_vertices;
D3DXMATRIX view_matrix;
D3DXMATRIX projection_matrix;

   //Load our star texture.  Star texture taken from NeHe's similar tutorial
   hr=D3DXCreateTextureFromFile(g_engine.GetDevice(),"Star.bmp",&g_star_texture);
   if(FAILED(hr)){
      g_engine.FatalError("Error loading texture");
   }

   g_engine.SetRenderState(D3DRS_LIGHTING,FALSE);
   g_engine.SetRenderState(D3DRS_ZENABLE,FALSE);

   //Additive blending
   g_engine.SetRenderState(D3DRS_ALPHABLENDENABLE,TRUE);
   g_engine.SetRenderState(D3DRS_SRCBLEND,D3DBLEND_ONE);
   g_engine.SetRenderState(D3DRS_DESTBLEND,D3DBLEND_ONE);

   //Modulate the texture colour by the TFACTOR.  We'll set the TFACTOR
   //to the single colour we want to each star to be.  This allows us
   //to quickly change colours without messing with vertex data.
   g_engine.SetTextureStageState(0,D3DTSS_COLOROP, D3DTOP_MODULATE);
   g_engine.SetTextureStageState(0,D3DTSS_COLORARG1, D3DTA_TEXTURE);
   g_engine.SetTextureStageState(0,D3DTSS_COLORARG2, D3DTA_TFACTOR );

   //High quality filtering to make things look nice
   g_engine.SetTextureStageState(0,D3DTSS_MAGFILTER, D3DTEXF_LINEAR);
   g_engine.SetTextureStageState(0,D3DTSS_MINFILTER, D3DTEXF_LINEAR);


   D3DXMatrixLookAtLH(&view_matrix,&D3DXVECTOR3( 0.0f, 0.0f,-10.0f ),
                                   &D3DXVECTOR3( 0.0f, 0.0f, 0.0f ),
                                   &D3DXVECTOR3( 0.0f, 1.0f, 0.0f ));

   g_engine.GetDevice()->SetTransform(D3DTS_VIEW,&view_matrix);


   g_engine.BuildDefaultPerpectiveMatrix(&projection_matrix,g_width,g_height);

   g_engine.GetDevice()->SetTransform( D3DTS_PROJECTION, &projection_matrix );



   hr=g_engine.GetDevice()->CreateVertexBuffer(6*sizeof(my_vertex), //Size of memory to be allocated
                                                            // Number of vertices * size of a vertex
                                       D3DUSAGE_WRITEONLY, // We never need to read from it so
                                                           // we specify write only, it's faster
                                       D3D8T_CUSTOMVERTEX, //Our custom vertex specifier (coordinates & a colour)
                                       D3DPOOL_MANAGED,   //Tell DirectX to manage the memory of this resource
                                       &g_vb);      //Pointer to our vb, after this call
                                                          //It will point to a valid vertex buffer
   if(FAILED(hr)){
      g_engine.FatalError("Error creating vertex buffer");
   }


   //Now we go through the same process to fill in our VB for the square.
   hr=g_vb->Lock(0, //Offset, we want to start at the beginning
                     0, //SizeToLock, 0 means lock the whole thing
                     &vb_vertices, //If successful, this will point to the data in the VB
                     0);  //Flags, nothing special
   if(FAILED(hr)){
      g_engine.FatalError("Error Locking vertex buffer");
   }
   
   memcpy(vb_vertices, g_vertices, sizeof(g_vertices) );

   g_vb->Unlock();

   init_stars();

}

Now we cover the scene clean up code. We didn't allocate much, so there isn't much to clean up. One vertex buffer and one texture.

void kill_scene(void){

   if(g_star_texture!=NULL){
      g_star_texture->Release();
      g_star_texture=NULL;
   }

   if(g_vb!=NULL){
      g_vb->Release();
      g_vb=NULL;
   }

}

Most particle systems start with no 'live' particles. They have a generator that creates new particles periodically. In our system, all of the particles start out live, so we initialize them all at the beginning.

void init_stars(void){
int count;

   //Initialize our particle system
   for(count=0;count < cMaxStars;count++){
      g_stars[count].angle=0.0f;          //Start them all at angle=0
      g_stars[count].dist=(float(count)/cMaxStars)*5.0f;  //Set starting positions
		g_stars[count].r=rand()%256;  //Random Red
		g_stars[count].g=rand()%256;  //Random Green
		g_stars[count].b=rand()%256;  //Random Blue
   }

}

The Render() function is where all the fun is. We loop through the particle system drawing each particle and updating its position. Drawing them individually like this is inefficient, but for the polygon counts we're dealing with (50 quads:100 triangles per scene) it really doesn't matter. Developing a highly optimized solution for this simple problem would be a waste of time, save the complex solutions for the situations that require them.

The first thing we do is calculate how much time has passed since the last frame we drew. From this we derive a time scale value which we will use on all of our movement. This way the program will still run properly on a 2GHz Athlon and a P133, it'll just look smoother on the Athlon.

Then we get into the matrix calculations. If you scan the code you'll notice that it's different than the other tutorials have shown. This is because our particles are 2D quads moving & rotating in 3D space. We have to make sure that they are facing the camera at all times or they won't be visible. This technique is known as billboarding.

The first set of matrix calculations look normal. We Translate and Rotate to put the particle in position. The only thing we use this position matrix for is 3 values which are the X, Y, and Z coordinates (the 41,42, and 43 members of the matrix respectively). This gives us our position in 3D space. The orientation is likely to be wrong, the quad could be facing any direction. There is an easy solution to this problem.

The View Matrix represents the position & orientation of our camera. If we Transpose the View Matrix, the result is a matrix that faces the camera. By putting the location values from the position matrix into the transposed matrix we have a final matrix to draw our star. Later in the function we use those same values to draw the Twinkle effect, which is a larger, less bright, spinning version of the star.

We set the star's colour by setting the TEXTUREFACTOR. If you recall, we set the Texture States so that the final colour is the texture colour (which is grayscale) modulated by the TEXTUREFACTOR. If we set the TEXTUREFACTOR to bright blue, then the star will be drawn in shades of blue.

Finally, we update the position of the stars and re-spawn them if they've reached the center. Here's the code:

void Render(void){
int count;
D3DXMATRIX location_matrix;
D3DXMATRIX temp_matrix;
D3DXMATRIX world_matrix;
D3DXMATRIX view_matrix;
D3DXMATRIX trans_matrix;
DWORD colour;
static DWORD current_frame_time=0;
float time_scale;
float angle_delta;


   //Calculate how much time has passed and use that to scale all movement
   //This keeps things running at the same rate no matter how fast your
   //computer is
   current_frame_time=GetTickCount();
   time_scale=(current_frame_time-g_last_frame_time)/800.0f;
   g_last_frame_time=current_frame_time;


   //Clear the back buffer
   g_engine.ClearD();

   if(SUCCEEDED(g_engine.BeginScene())){

      g_engine.GetDevice()->SetTexture(0,g_star_texture);

      g_engine.GetDevice()->SetVertexShader(D3D8T_CUSTOMVERTEX);

      g_engine.GetDevice()->SetStreamSource(0,g_vb,sizeof(my_vertex));

      //Loop through and draw each star
      for(count=0;count < cMaxStars;count++){

         //First we perform all of our calculations to determine the star's
         //position in 3D space.

         D3DXMatrixIdentity(&location_matrix);

         //Move star out from center
         D3DXMatrixTranslation(&temp_matrix,g_stars[count].dist,0,0);
         D3DXMatrixMultiply(&location_matrix,&location_matrix,&temp_matrix);

         //Revolve around center
         D3DXMatrixRotationZ(&temp_matrix,g_stars[count].angle); 
         D3DXMatrixMultiply(&location_matrix,&location_matrix,&temp_matrix);

         //Tilt on Y axis
         D3DXMatrixRotationX(&temp_matrix,g_tilt);
         D3DXMatrixMultiply(&location_matrix,&location_matrix,&temp_matrix);

         //Zoom into Screen
         D3DXMatrixTranslation(&temp_matrix,0,0,g_zoom);
         D3DXMatrixMultiply(&location_matrix,&location_matrix,&temp_matrix);


         //Now we know the star's position.  Since each star is actually a flat
         //quad we have to make it face the camera.  This is known as a billboard.
         //To do this we Transpose the View matrix, which gives us a matrix that
         //is oriented towards the camera.
         g_engine.GetDevice()->GetTransform(D3DTS_VIEW,&view_matrix);
         D3DXMatrixTranspose(&trans_matrix,&view_matrix);

         //Now we have the position of our star in the location_matrix
         //and the orientation in the trans_matrix

         //We copy the trans matrix (we need to maintain it's values for the
         //Twinkle effect which follows).
         world_matrix=trans_matrix;

         //Now we plug in the location values into our world matrix.
         world_matrix._41 = location_matrix._41;
         world_matrix._42 = location_matrix._42;
         world_matrix._43 = location_matrix._43;


         g_engine.GetDevice()->SetTransform(D3DTS_WORLD,&world_matrix );

         //We calculate the colour and then set it as the TFACTOR (TEXTUREFACTOR)
         //This causes the star to be drawn in that colour since we're modulating
         //the texture (which is grayscale) by this colour.
         colour=D3DCOLOR_ARGB(0xFF,g_stars[count].r,g_stars[count].g,g_stars[count].b);

         g_engine.SetRenderState(D3DRS_TEXTUREFACTOR,colour);

         g_engine.GetDevice()->DrawPrimitive(D3DPT_TRIANGLELIST,0,2);

         if (g_twinkle){   //If we want to twinkle

            //Complement the star's colour and make it half as bright.
            //If the star was bright red, it's "twinkle" is medium Aqua
            colour=D3DCOLOR_ARGB(0xFF,(~g_stars[count].r)>>1,
                                      (~g_stars[count].g)>>1,
                                      (~g_stars[count].b)>>1);

            //As before, this sets the colour we're drawing with since our
            //Texture states are set to modulte the TEXTUREFACTOR(TFACTOR)
            //with the texture colour (which is grayscale)
            g_engine.SetRenderState(D3DRS_TEXTUREFACTOR,colour);

            //Spin the 'Twinkle' on the Z axis, the main star doesn't spin
            //so this causes the twinkle effect
            D3DXMatrixRotationZ(&temp_matrix,g_spin);
            D3DXMatrixMultiply(&world_matrix,&trans_matrix,&temp_matrix);

            //Scale the Twinkle for better visual effect
            D3DXMatrixScaling(&temp_matrix,1.3f,1.3f,1.3f); 
            D3DXMatrixMultiply(&world_matrix,&world_matrix,&temp_matrix);

            //Now we have our final orientation & scaling done for the twinkle
            //effect, so we plug in the location info from our location matrix
            //and we have our final world matrix
            world_matrix._41 = location_matrix._41;
            world_matrix._42 = location_matrix._42;
            world_matrix._43 = location_matrix._43;

            g_engine.GetDevice()->SetTransform(D3DTS_WORLD,&world_matrix );

            g_engine.GetDevice()->DrawPrimitive(D3DPT_TRIANGLELIST,0,2);

         }


         //Update the positions of our stars
         angle_delta=((float)count+1.0f)/(float)cMaxStars;
         g_stars[count].angle+=angle_delta*time_scale;// Changes The Angle Of A Star
         g_stars[count].dist-=time_scale/1.2f;
         if (g_stars[count].dist<0.0f){
            g_stars[count].dist+=5.0f;			// Move The Star 5 Units From The Center
            g_stars[count].r=rand()%256;		// Give It A New Red Value
            g_stars[count].g=rand()%256;		// Give It A New Green Value
            g_stars[count].b=rand()%256;		// Give It A New Blue Value
         }


      }
      g_spin-=time_scale;

      g_engine.EndScene();
   }

   g_engine.GetDevice()->Present( NULL, NULL, NULL, NULL );

   g_engine.GetDevice()->SetTexture( 0,NULL);


}

After all of that, the remaining bit is trivial. It's our window message handler. Nothing fancy, it should be familiar to you.

LRESULT CALLBACK default_window_proc(HWND hwnd,UINT msg,WPARAM wparam,LPARAM lparam){
char virt_key = (char)wparam;

   switch(msg){
      case WM_KEYDOWN:
         switch(virt_key){
            case 'T': //Toggle Twinkling
               g_twinkle=!g_twinkle;
               break;
            case VK_UP:
               g_zoom-=0.25f;
               break;
            case VK_DOWN:
               g_zoom+=0.25f;
               break;
            case VK_NEXT:
               g_tilt+=0.1f;
               break;
            case VK_PRIOR:
               g_tilt-=0.1f;
               break;
            case VK_ESCAPE:
               g_app_done=true;
               break;
         }
         return 0;
      case WM_CLOSE:    //User hit the Close Window button, end the app
         g_app_done=true;
         return 0;
      case WM_DESTROY:  //This window is being destroyed, tell Windows we're quitting
         PostQuitMessage(0);
         return 0;
   }

   return (DefWindowProc(hwnd,msg,wparam,lparam));

}

As a final note, I'd like to thank Jim Adams (jimadams@att.net). Jim posted a very nice bit on billboarding on the GameDev.net forums. The way I was doing it was more complex, as soon as I saw Jim's post it was painfully obvious, but I wonder how long it would have taken me to get it on my own.

Jim is also working on a book: Programming Role-Playing Games with DirectX 8. I'm looking forward to its publication. Check out his site here.