Character Animation with DirectX 8.0
Abstract:Content : Importing and animating a Quake2 MD2 modelTarget audience : Intermediate Direct3D8 programmer Download : Demo Project Table of contents:
IntroductionWho 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 FormatAn 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 itYou 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 classOf 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 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. ConclusionWe'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 : Links
One of the best English game pages Perhaps even better than flipcode... has got great forums, help within minutes ;-) The best German C++ programming page with a _very_ good forum 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 Model database A great page which describes almost all file formats - from A as ASC to Z as ZIP The creators of all our favourite games such as Quake, Doom and all the other games which may not be sold in Germany :-( Cool game links and some game dev tutorials. German. Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|