• 01/16/02 11:18 PM
    Sign in to follow this  

    Character Animation with DirectX 8.0

    Graphics and GPU Programming

    Myopic Rhino
    [size="5"]Abstract:

    Content : Importing and animating a Quake2 MD2 model
    Target audience : Intermediate Direct3D8 programmer
    Download : Demo Project (see attached resource file)


    [size="5"]Table of contents:
    1. [alink='1']Introduction[/alink]
    2. [alink='2']The MD2-Format [/alink]
    3. [alink='3']How to do it [/alink]
    4. [alink='4']The wrapper class [/alink]
    5. [alink='5']How does it work ? [/alink]
    6. [alink='6']How does it go on ? [/alink]
    7. [alink='7']Closing/Last words [/alink]
    8. [alink='8']Links[/alink]

    [size="5"][aname='1']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 available in the MD2 Format which can be imported easily using your own converter! Yeah!


    [size="5"][aname='2']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 [alink='8']links section[/alink]).

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


    [size="5"][aname='3']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 vertex;
    };
    That was a lot, wasn't it?


    [size="5"][aname='4']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.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.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.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).a = tri.index_xyz[2];
    (m_index_list).b = tri.index_xyz[1];
    (m_index_list).c = tri.index_xyz[0];

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

    (m_index_list).b_t = base_st[tri.index_st[1]].t;
    (m_index_list).c_s = base_st[tri.index_st[0]].s;
    (m_index_list).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).vertex[j].x = out->verts[j].v[0] * out->scale[0] + out->translate[0];
    (m_frame_list).vertex[j].y = out->verts[j].v[1] * out->scale[1] + out->translate[1];
    (m_frame_list).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.vertex[m_index_list[j].a].x;
    pVertex.m_vecPos.y = m_frame_list.vertex[m_index_list[j].a].z;
    pVertex.m_vecPos.z = m_frame_list.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.vertex.push_back (pVertex);

    pVertex.m_vecPos.x = m_frame_list.vertex[m_index_list[j].b].x;
    pVertex.m_vecPos.y = m_frame_list.vertex[m_index_list[j].b].z;
    pVertex.m_vecPos.z = m_frame_list.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.vertex.push_back (pVertex);

    pVertex.m_vecPos.x = m_frame_list.vertex[m_index_list[j].c].x;
    pVertex.m_vecPos.y = m_frame_list.vertex[m_index_list[j].c].z;
    pVertex.m_vecPos.z = m_frame_list.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.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));
    }

    [size="5"][aname='5']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"
    [bquote]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][/bquote]
    Has the light bulb 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.


    [size="5"][aname='6']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.


    [size="5"][aname='7']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 :

    [email="johannes.leimbach@gmx.de"]johannes.leimbach@gmx.de[/email]


    [size="5"][aname='8']Links

    Milkshape:
    [bquote]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 3DSExport
      • All above mentioned formats
      • DirectX X-Format Ascii (!!!!!)
      • ~10 more [/bquote]Flipcode
        [bquote]One of the best English game pages[/bquote]
        Gamedev
        [bquote]Perhaps even better than flipcode... has got great forums, help within minutes ;-)[/bquote]
        c-plusplus.de:
        [bquote]The best German C++ programming page with a _very_ good forum[/bquote]
        One digit ahead:
        [bquote]A great hobby programmer team -- guess who is part of it??[/bquote]
        http://home.planet.nl/~monstrous:
        [bquote]Some very nice articles about landscapes and physics...[/bquote]
        http://www.planetquake.com/polycount/:
        [bquote]Model database[/bquote]
        www.3dcafe.com
        [bquote]Model database[/bquote]
        www.wotsit.org
        [bquote]A great page which describes almost all file formats - from A as ASC to Z as ZIP[/bquote]
        www.id-software.com:
        [bquote]The creators of all our favourite games such as Quake, Doom and all the other games which may not be sold in Germany :-([/bquote]
        www.softgames.de:
        [bquote]Cool game links and some game dev tutorials. German.[/bquote]


      Report Article
    Sign in to follow this  


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

    There are no reviews to display.