Character Animation with DirectX 8.0
or One Step Nearer to Quake
by Johannes Leimbach

Abstract:

Content : Importing and animating a Quake2 MD2 model
Target audience : Intermediate Direct3D8 programmer
Download : Demo Project

Table of contents:

  1. Introduction
  2. The MD2-Format
  3. How to do it
  4. The wrapper class
  5. How does it work ?
  6. How does it go on ?
  7. Closing/Last words
  8. Links
  9. Download Demo Project (ca. 300 KB)

Introduction

Who hasn't had this problem? You've read (and understood) some tutorials on 3D game programming, and you want to create your own mega-seller with great outdoor worlds, greattextures, a unique AI and scary monsters.

And monsters.

If you're like me, that would be the point where you had your first big problems... It is not that easy to import animated objects into a game. If you've made the mistake of looking at the DirectX SDK sample "skinned mesh", you may have even ended up so astonished by the complexity of it that you gave up and took on an easier task instead.

But importing a model can be easier than in the DirectX sample. Much easier. Very much easier.

The solution lies in Quake, Quake II to be specific. Quake's game models are availible in the MD2 Format which can be imported easily using your own converter! Yeah!

The MD2 Format

An MD2 File basically consists of a big list of triangles which then "form" the animation. As the MD2 format is a little bit old it has some restrictions/disadvantages. It is not skeleton based, which means it is impossible (or very difficult) to turn, for example, the head independently from the body. (Half-Life does this.)

But the advantage of MD2 is that it is very easy to use and import; it can be used by almost every game without great problems. There are also a lot of web pages offering many MD2 models for download (see the links section).

Okay, enough talk, it's time to write our own MD2 loader!

How to do it

You need alot of structs to import a Quake model:

//Remark :
//I won't explain all structs, they are used _once_ for loading the model.
//You don't have to know everything, you just have to know where to find it...

struct make_index_list 
{ 
  int a, b, c;
  float a_s, a_t, b_s, b_t, c_s, c_t;
};

struct make_vertex_list 
{
  float x, y, z;
};

struct make_frame_list
{
  make_vertex_list *vertex;
};

struct vec3_t
{
  float v[3];
};

struct dstvert_t 
{
  short s, t;
};

struct dtriangle_t 
{ 
  short index_xyz[3];
  short index_st[3];
};

struct dtrivertx_t
{ 
  BYTE v[3]; 
  BYTE lightnormalindex;
};

//We use that to identify one animation
//The only variably which is important for us is "name", it is filled with the names
//of the animation frames, p.ex. "run1", or "attack2". More below

struct daliasframe_t
{ 
  float scale[3];
  float translate[3]; 
  char name[16]; 
  dtrivertx_t verts[1]; 
};

//Here all information for the model is saved. Most is unimportant, the important
//stuff is commented..
//This struct is read first from file...

struct SMD2Header
{ 
  int ident; 
  int version; 
  int skinwidth; 
  int skinheight; 
  int framesize; 
  int num_skins; 
  int num_xyz;     //Vertex count 
  int num_st; 
  int num_tris;    //Triangle count
  int num_glcmds; 
  int num_frames;  //Numer of frames/animations within the file
  int ofs_skins; 
  int ofs_st; 
  int ofs_tris; 
  int ofs_frames; 
  int ofs_glcmds; 
  int ofs_end; 
};

struct trivert_t
{ 
  vec3_t v; 
  int lightnormalindex; 
};

struct frame_t
{ 
  vec3_t mins, maxs; 
  char name[16]; 
  trivert_t v[MAX_VERTS];
};

// The vertex we use for D3D
struct MODELVERTEX 
{ 
   D3DXVECTOR3 m_vecPos; //Position
   D3DCOLOR m_dwDiffuse; //Color
   D3DXVECTOR2 m_vecTex; //texturecoordinates
};

//This is the definition for the vertex declared above (FVF=flexible vertex format)
#define D3DFVF_MODELVERTEX ( D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 | D3DFVF_TEXCOORDSIZE2(0) )

struct SMesh 
{
  //std::vector is declared in "vector" and it is a comfortable wrapper for dynamic arrays
  std::vector<MODELVERTEX> vertex;
};

That was a lot, wasn't it?

The wrapper class

Of course, this still isn't enough to actually read a MD2 model. So, let's do that now!

//These are some restriction for MD2
//You can alter the values if you like to...
const int MAX_TRIANGLES = 4096; 
const int MAX_VERTS = 2048; 
const int MAX_FRAMES = 512; 
const int MAX_MD2SKINS = 32; 
const int MAX_SKINNAME = 64;


//That's the name of our wrapper class
class CMD2Model
{
//Private means that only functions from CMD2Modell can access
//the elements
private :
  //Only used once for loading
  make_index_list*  m_index_list;
  make_frame_list*  m_frame_list; 

  //Number of frames,vertice and triangles
  long              m_frames, m_vertices, m_triangles;
  //Here all animations/frames are saved
  SMesh             m_data [MAX_FRAMES]; 
  //Internally called from Load
  int               Init(); 

//public means everyone can access the following values
public: 
  CMD2Model(); 
  ~CMD2Model(); 

  //Loads the file :-)
  BOOL              Load (char* );
  //Frees up memory
  void              Destroy (); 

  //Draws frame nr. frame, more above
  BOOL              Render (int frame); 

  //Some general functions...
  inline int        GetFrameCount() { return m_frames; }
  inline int        GetVertexCount() { return m_vertices; }
  inline int        GetTriangleCount() { return m_triangles;} 
}; 

OK, let's implement the functions we've just declared!

// construktor 
CMD2Model::CMD2Model () 
{ 
  //set all values to 0!
  m_index_list = NULL; 
  m_frame_list = NULL; 
  m_frames = m_vertices = m_triangles = 0; 
}

// Destructor
CMD2Model::~CMD2Model () 
{
  //Here we free up the memory to avoid leaks
  if( m_frame_list != NULL ) 
  { 
    for( int i = 0; i < m_frames; i++ ) 
    {
      delete [] m_frame_list[i].vertex; 
      delete [] m_frame_list; 
    } 
  } 

  if( m_index_list) 
    delete [] m_index_list; 
}

//It does the same as the dtor (destructor). The advantage is 
//that you can call Destory whenever you want to !!!
void CMD2Model::Destroy () 
{ 
  //We don't delete NULL pointers ! That's evil
  if( m_frame_list != NULL ) 
  { 
    for( int i = 0; i < m_frames; i++ ) 
    { 
      delete [] m_frame_list[i].vertex; 
      delete [] m_frame_list; 
      m_frame_list = NULL; 
    } 
    if( m_index_list != NULL ) 
    { 
      delete [] m_index_list; 
      m_index_list = NULL; 
    } 
  } 
}


//Uhh, now we get to the most ugly function within the entire tutorial : Load
//It loads the MD2 model ...
//OK, close your eyes, let's do it fast and painless.
//(and hope you're never asked what it does exactly *g*)

int CMD2Model::Load( char *filename ) 
{ 
  FILE *modelfile = NULL; 
  char g_skins[MAX_MD2SKINS][64]; 
  dstvert_t base_st[MAX_VERTS]; 
  BYTE buffer[MAX_VERTS*4+128]; 
  SMD2Header modelheader; 
  dtriangle_t tri; 
  daliasframe_t *out; 

  //Open the file in binary mode.
  //If it does not exist or loading fails for an unknown reason
  //we return 0
  if( (modelfile = fopen (filename, "rb")) == NULL ) 
    return 0; 


  // Read the header
  fread( &modelheader, 1, sizeof(SMD2Header), modelfile ); 
  modelheader.framesize = (int)&((daliasframe_t *)0)->verts[modelheader.num_xyz];

  // Create some variables according to the header
  m_frames = modelheader.num_frames; 
  m_vertices = modelheader.num_xyz; 
  m_triangles = modelheader.num_tris; 
  m_index_list = new make_index_list [modelheader.num_tris]; 
  m_frame_list = new make_frame_list [modelheader.num_frames]; 

  for( int i = 0; i < modelheader.num_frames; i++)
    m_frame_list[i].vertex = new make_vertex_list [modelheader.num_xyz];

  // read skin (texture) information
  fread( g_skins, 1, modelheader.num_skins * MAX_SKINNAME, modelfile ); 
  // Read indexe for the model
  fread( base_st, 1, modelheader.num_st * sizeof(base_st[0]), modelfile ); 

  int max_tex_u = 0, max_tex_v = 0; 

  for( i = 0; i < modelheader.num_tris; i++ )
  { 
    // read vertice
    fread( &tri, 1, sizeof(dtriangle_t), modelfile); 
    (m_index_list)[i].a = tri.index_xyz[2]; 
    (m_index_list)[i].b = tri.index_xyz[1]; 
    (m_index_list)[i].c = tri.index_xyz[0]; 

    // read texture cordinates
    (m_index_list)[i].a_s = base_st[tri.index_st[2]].s; 
    (m_index_list)[i].a_t = base_st[tri.index_st[2]].t; 
    (m_index_list)[i].b_s = base_st[tri.index_st[1]].s; 

    (m_index_list)[i].b_t = base_st[tri.index_st[1]].t; 
    (m_index_list)[i].c_s = base_st[tri.index_st[0]].s; 
    (m_index_list)[i].c_t = base_st[tri.index_st[0]].t; 

    max_tex_u = max( max_tex_u, base_st[tri.index_st[0]].s ); 
    max_tex_u = max( max_tex_u, base_st[tri.index_st[1]].s ); 
    max_tex_u = max( max_tex_u, base_st[tri.index_st[2]].s ); 
    max_tex_v = max( max_tex_v, base_st[tri.index_st[0]].t );
    max_tex_v = max( max_tex_v, base_st[tri.index_st[1]].t ); 
    max_tex_v = max( max_tex_v, base_st[tri.index_st[2]].t ); 
  } 


  //As in MD2 files texture coordinates are given on a per pixel base, we recalculate
  //it here!
  for ( i = 0; i < modelheader.num_tris; i++ ) 
  { 
    m_index_list[ i ].a_s /= max_tex_u; 
    m_index_list[ i ].b_s /= max_tex_u; 
    m_index_list[ i ].c_s /= max_tex_u; 
    m_index_list[ i ].a_t /= max_tex_v; 
    m_index_list[ i ].b_t /= max_tex_v; 
    m_index_list[ i ].c_t /= max_tex_v; 
  }


  //g_D3D.m_toolz.FTrace is one of my helper functions
  //It just writes something into log.log
  g_D3D.m_toolz.FTrace ("Animation-names for : "); 
  g_D3D.m_toolz.FTrace (filename); 
  g_D3D.m_toolz.FTrace ("\n\n"); 

  // Read vertexdata of all animation frames
  for( i = 0; i < modelheader.num_frames; i++ )
  { 
    out = (daliasframe_t *)buffer; 
    fread( out, 1, modelheader.framesize, modelfile ); 

    //If that animation has a valid name, we save it into the log file
    if (out->name) 
    {
      g_D3D.m_toolz.FTrace (out->name); 
      g_D3D.m_toolz.FTrace ("\n"); 
    } 

    for( int j = 0; j < MODELHEADER.NUM_XYZ; J++ ) 
    { 
      (m_frame_list)[i].vertex[j].x = out->verts[j].v[0] * out->scale[0] + out->translate[0]; 
      (m_frame_list)[i].vertex[j].y = out->verts[j].v[1] * out->scale[1] + out->translate[1];
      (m_frame_list)[i].vertex[j].z = out->verts[j].v[2] * out->scale[2] + out->translate[2]; 
    } 
  } 

  fclose (modelfile); 
  return Init();
} 


//Puh, done ! All who are still here please raise up your hands.... Thanx, so many still there ! Great..
//This was the most complicated task of the entire wrapper
//BUT STOP -what's that ??

//Last line of Load () : 
//return Init ();

//NO ! Another function - it is pretty difficult as well, but if you have survived Load,
//Init shouldn't be much of a problem

//Let's go

int CMD2Model::Init() 
{
  // For every animation we use an own SMesh
  for ( int i = 0; i < GetFrameCount(); i++ ) 
  { 
    MODELVERTEX pVertex; 
    D3DXCOLOR LightColor(1.0f, 1.0f, 1.0f, 1.0f ); 


    //Now we copy the vertexdata to m_data.
    //The smart reader will have noticed that I have swapped
    //y and z. That's ok, because in a MD2 file these 
    //coordinates are swapped (In fact Quake has an other
    //coordinate system than Direct3D, but that's stuff
    //for an other tutorial...

    for( int j = 0; j < GetTriangleCount(); j++) 
    {
      pVertex.m_vecPos.x = m_frame_list[i].vertex[m_index_list[j].a].x;
      pVertex.m_vecPos.y = m_frame_list[i].vertex[m_index_list[j].a].z; 
      pVertex.m_vecPos.z = m_frame_list[i].vertex[m_index_list[j].a].y; 
      pVertex.m_vecTex.x = m_index_list[j].a_s; 
      pVertex.m_vecTex.y = m_index_list[j].a_t; 
      pVertex.m_dwDiffuse = LightColor; 
      m_data[i].vertex.push_back (pVertex);

      pVertex.m_vecPos.x = m_frame_list[i].vertex[m_index_list[j].b].x;
      pVertex.m_vecPos.y = m_frame_list[i].vertex[m_index_list[j].b].z; 
      pVertex.m_vecPos.z = m_frame_list[i].vertex[m_index_list[j].b].y; 
      pVertex.m_vecTex.x = m_index_list[j].b_s;
      pVertex.m_vecTex.y = m_index_list[j].b_t; 
      pVertex.m_dwDiffuse = LightColor; 
      m_data[i].vertex.push_back (pVertex); 

      pVertex.m_vecPos.x = m_frame_list[i].vertex[m_index_list[j].c].x; 
      pVertex.m_vecPos.y = m_frame_list[i].vertex[m_index_list[j].c].z; 
      pVertex.m_vecPos.z = m_frame_list[i].vertex[m_index_list[j].c].y; 
      pVertex.m_vecTex.x = m_index_list[j].c_s;
      pVertex.m_vecTex.y = m_index_list[j].c_t; 
      pVertex.m_dwDiffuse = LightColor;
      m_data[i].vertex.push_back (pVertex); 
    } 
  } 
  //READY !!!
  return 1; 
} 


//Now the initiation is -finally- over !!
//Now there won't be any complicated stuff ! Really !!
//All we have to do now is rendering the model...

BOOL CMD2Model::Render( int frame ) 
{ 
  //Did we try to play a frame which does not exist
  if( frame >= GetFrameCount()-1 ) 
    return 0; 

  //Declare the proper vertex shaer
  //g_D3D.m_lpD3DDevice is globally declared. It is a pointer
  //to a LPDIRECT3DDEVICE8
  g_D3D.m_lpD3DDevice->SetVertexShader (D3DFVF_MODELVERTEX);

  //Draw
  HRESULT h = g_D3D.m_lpD3DDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST,               //Type
                                                   GetTriangleCount (),              //Count
                                                   (BYTE**)&m_data[frame].vertex[0], //Pointer to data
                                                   sizeof(MODELVERTEX));             //Size vertex
  return (SUCCEEDED(h));
}

How does it work?

Now for the easy part. To load, draw, and destroy a model, you just need to do the following:

CMD2Model model;
model.Load ("player.md2");
model.Render (0);
model.Destroy ();

Easy, isn't it?

If you take a deeper look at Render you'll see that you have to specify a framenumber. What purpose does this parameter have? That's very easy, too.

To explain it better, I'll show you the animation names extracted from "player.md2"

Frame 1: stand01
Frame 2: stand02
Frame 3: stand03
Frame 4: stand04
Frame 5: stand05
Frame 6: run1
Frame 7: run2
Frame 8: run3
Frame 9: run4
Frame 10: run5
Frame 11: attack1
Frame 12: attack2
Frame 13: attack3
Frame 14: attack4
[CUT]

Has the lightbulb turned on now? If you want to play a running model, simply play frames 6-9. If you want to play an attack sequence, simply play frames 11-14.

What do we do next?

There are only a few things missing. It isn't very user friendly to play frames 6-9 for a running player over a specified period of time, then switch to frame 1 when the player stops.

In the demo project I've solved that problem. If you want to play the run-animation you use the function:

Render ("run");

You don't have to worry about specific frames, you just have to specify the name of the animation you want to play, and the rest is done automatically!!! Cool, eh?

We currently don't have anything for collision detection, but a simple bounding box should work for that.

We're also not yet rendering the objects with a texture, so let's address that problem:

g_D3D.m_lpD3DDevice->SetTexture (0, D3DTexture);
model.Render (0);

You have to set the texture externally. The advantage to doing it this way is that you can load 1 model, and use it for 5 different players, each with a different texture, yielding a savings in memory and loading time. See the demo project for more.

Conclusion

We're already at the end of my tutorial :-( I hope you enjoyed reading it as much as I loved writing it, though it was a little bit complicated because of the #@!'= damn loading routine...

If you liked it/didn't like it/want to offer me a job/whatever, drop me a line :

johannes.leimbach@gmx.de

Links

Milkshape:

Milkshape is an easy to use 3D program, but the best thing about it is its import and export formats:

Import

  • Halflife SMD
  • Quake2 MD2
  • Quake3 MD3
  • Unreal 3D
  • PlayStation TMD
  • Serious Sam MDL
  • Autodesc ASC und 3DS

Export

  • All above mentioned formats
  • DirectX X-Format Ascii (!!!!!)
  • ~10 more

Flipcode

One of the best English game pages

Gamedev

Perhaps even better than flipcode... has got great forums, help within minutes ;-)

c-plusplus.de:

The best German C++ programming page with a _very_ good forum

One digit ahead:

A great hobby programmer team -- guess who is part of it??

http://home.planet.nl/~monstrous:

Some very nice articles about landscapes and physics...

http://www.planetquake.com/polycount/:

Modeldatabase

www.3dcafe.com

Model database

www.wotsit.org

A great page which describes almost all file formats - from A as ASC to Z as ZIP

www.id-software.com:

The creators of all our favourite games such as Quake, Doom and all the other games which may not be sold in Germany :-(

www.softgames.de:

Cool game links and some game dev tutorials. German.

Discuss this article in the forums


Date this article was posted to GameDev.net: 1/16/2002
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
General
Sweet Snippets

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!