Home » Uncategorized » Lesson 11: Loading Quake II (MD2) Models

Lesson 11: Loading Quake II (MD2) Models

In this tutorial we will demonstrate how to load Quake II models.  The video game was released around 1997-1999, so models might be hard to find. You can however use/buy the original game. If you search a lot, you may also find some models online. Quake II models are referred to as MD2 models.  Other than Quake II this 3d model format was also used in Sin and Soldier of Fortune.  Unlike the model formats we discussed earlier, this model format is mostly used for animation. The general features of this model format are:

MD2 model features:

  • Model’s geometric data (triangles);
  • Frame-by-frame animations;
  • Structured data for drawing the model using GL_TRIANGLE_FAN and GL_TRIANGLE_STRIP primitives (called “OpenGL commands”)

Unlike Wavefront OBJ or the PLY format, the MD2 file format starts with an MD2 header.  On an abstract level the format looks like this:

The MD2 header format is defined as:

Offset Data type Name Description
0 int ident Magic number. Must be equal to “IDP2″
4 int version MD2 version. Must be equal to 8
8 int skinwidth Width of the texture
12 int skinheight Height of the texture
16 int framesize Size of one frame in bytes
20 int num_skins Number of textures
24 int num_xyz Number of vertices
28 int num_st Number of texture coordinates
32 int num_tris Number of triangles
36 int num_glcmds Number of OpenGL commands
40 int num_frames Total number of frames
44 int ofs_skins Offset to skin names (each skin name is an unsigned char[64] and are null terminated)
48 int ofs_st Offset to s-t texture coordinates
52 int ofs_tris Offset to triangles
56 int ofs_frames Offset to frame data
60 int ofs_glcmds Offset to OpenGL commands
64 int ofs_end Offset to end of file

Similar to the TGA loader, we can load the entire block directly into a C struct.

typedef struct _MD2_Header
        {
                int ident;        // identifies as quake II header  (IDP2)
                int version;      // mine is 8
                int skinwidth;    // width of texture
                int skinheight;   // height of texture
                int framesize;    // number of bytes per frame
                int numSkins;     // number of textures
                int numXYZ;       // number of points
                int numST;        // number of texture
                int numTris;      // number of triangles
                int numGLcmds;
                int numFrames;    // total number of frames
                int offsetSkins;  // offset to skin names (64 bytes each)
                int offsetST;     // offset of texture s-t values
                int offsetTris;   // offset of triangle mesh
                int offsetFrames; // offset of frame data (points)
                int offsetGLcmds;
                int offsetEnd;    // end of file
        } MD2_Header;

An MD2 file format stores animation by several “key frames”, e.g. a multitude of point clouds. Depending on which set of vertexes you display you get a different model position. MD2 stores various of such point clouds for running, walking, jumping etc.  In order to get a smooth animation, you may wish to interpolate between the keypoints frames. In addition to the previous tutorials we will also load texture coordinates in this tutorial. A texture is an image which is added on top of the 3d model, to make it appear more realistic. In the end however, all these model collections are similar: they all hold vertexes, faces, vertex normal’s and potentially texture coordinates.

We have created a class for loading MD2 models and PCX images. PCX images or textures generally came along with these models. This code needs re factoring, but it serves as simple example. You may want to re-implement it using this as reference, do not use in production code! We will need two files:  md2.cpp and main.cpp.  md2.cpp will contain the class which parses the md2 model format, while main.cpp will set the scene.

main.cpp

/*
 *
 * Demonstrates how to load and display an MD2 file.
 * Using triangles and normals as static object. No texture mapping.
 * https://talkera.org/opengl/
 *
 * OBJ files must be triangulated!!!
 * Non triangulated objects wont work!
 * You can use Blender to triangulate
 *
 */
 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 #include "md2.h"

 
#define KEY_ESCAPE 27
 
using namespace std;
 
/************************************************************************
  Window
 ************************************************************************/
 
typedef struct {
    int width;
        int height;
        char* title;
 
        float field_of_view_angle;
        float z_near;
        float z_far;
} glutWindow;
 
 
 
/***************************************************************************
 * Program code
 ***************************************************************************/
 
Model_MD2 obj;
float g_rotation;
glutWindow win;
 
void display() 
{
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glLoadIdentity();
        gluLookAt( 0,1,-100, 0,0,0, 0,1,0);
        glPushMatrix();
                glRotatef(g_rotation,0,1,0);
                glRotatef(-90,1,0,0);
                g_rotation++;
                obj.Draw();
        glPopMatrix();
        glutSwapBuffers();
}
 
 
void initialize () 
{
    glMatrixMode(GL_PROJECTION);
        glViewport(0, 0, win.width, win.height);
        GLfloat aspect = (GLfloat) win.width / win.height;
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
        gluPerspective(win.field_of_view_angle, aspect, win.z_near, win.z_far);
    glMatrixMode(GL_MODELVIEW);
    glShadeModel( GL_SMOOTH );
    glClearColor( 0.5f, 0.5f, 0.5f, 0.5f );
    glClearDepth( 1.0f );
    glEnable( GL_DEPTH_TEST );
    glDepthFunc( GL_LEQUAL );
    glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );
 
    GLfloat amb_light[] = { 0.5, 0.5, 0.5, 1.0 };
    GLfloat diffuse[] = { 0.6, 0.6, 0.6, 1 };
    GLfloat specular[] = { 0.7, 0.7, 0.3, 1 };
    glLightModelfv( GL_LIGHT_MODEL_AMBIENT, amb_light );
    glLightfv( GL_LIGHT0, GL_DIFFUSE, diffuse );
    glLightfv( GL_LIGHT0, GL_SPECULAR, specular );
    glEnable( GL_LIGHT0 );
    glEnable( GL_COLOR_MATERIAL );
    glShadeModel( GL_SMOOTH );
    glLightModeli( GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE );
    glDepthFunc( GL_LEQUAL );
    glEnable( GL_DEPTH_TEST );
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0); 
}
 
 
void keyboard ( unsigned char key, int x, int y ) 
{
  switch ( key ) {
    case KEY_ESCAPE:        
      exit ( 0 );   
      break;      
    default:      
      break;
  }
}
 
int main(int argc, char **argv) 
{
        // set window values
        win.width = 640;
        win.height = 480;
        win.title = "OpenGL MD2 Loader.";
        win.field_of_view_angle = 45;
        win.z_near = 1.0f;
        win.z_far = 500.0f;
 
        // initialize and run program
        glutInit(&argc, argv);                                      // GLUT initialization
        glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH );  // Display Mode
        glutInitWindowSize(win.width,win.height);                                       // set window size
        glutCreateWindow(win.title);                                                            // create Window
        glutDisplayFunc(display);                                                                       // register Display Function
        glutIdleFunc( display );                                                                        // register Idle Function
    glutKeyboardFunc( keyboard );                                                               // register Keyboard Handler
        initialize();
        obj.Load("tris.md2","red.pcx");
        glutMainLoop();                                                                                         // run GLUT mainloop
        return 0;
}

md2.h

class Model_MD2 
{
  public: 
    void LoadPCX(char* textureFilename);
    int Load(char *filename, char* textureFilename);
        void Draw();
        void Play(int animation);

        void Stand();
        void Run();
        void Attack();
        void Pain1();
        void Pain2();
        void Pain3();
        void Jump();
        void Flip();
        void Salute(); // 10
        void Taunt(); // 16
        void Wave(); // 10
        void Point(); //11
        void Crstnd(); // 18 
        void Crwalk(); // 5
        void Crattack(); // 8
        void Crpain(); // 3
        void Crdeath(); // 4
        void Death1(); // 5
        void Death2(); // 5
        void Death3(); // 7

        Model_MD2();
    
        float Points[1000000];
    float Faces_Triangles[512][14096];
        float Normals[1000000];
    float Faces_Textures[512][14096];

        float TextureCoords[120048];

        int TotalConnectedPoints;
        int TotalConnectedTriangles;    

        int Scale;
        int AngleX, AngleY, AngleZ;

  private:

        typedef struct _MD2_Header
        {
                int ident;        // identifies as quake II header  (IDP2)
                int version;      // mine is 8
                int skinwidth;    // width of texture
                int skinheight;   // height of texture
                int framesize;    // number of bytes per frame
                int numSkins;     // number of textures
                int numXYZ;       // number of points
                int numST;        // number of texture
                int numTris;      // number of triangles
                int numGLcmds;
                int numFrames;    // total number of frames
                int offsetSkins;  // offset to skin names (64 bytes each)
                int offsetST;     // offset of texture s-t values
                int offsetTris;   // offset of triangle mesh
                int offsetFrames; // offset of frame data (points)
                int offsetGLcmds;
                int offsetEnd;    // end of file
        } MD2_Header;
                
        typedef struct _framePoint
        {
                 unsigned char v[3];             // the vertex
                 unsigned char lightNormalIndex;
        } framePoint;

        typedef struct _frame
        {
         float scale[3];                 // vetex scaling
         float translate[3];             // vertex translation
         char name[16];                  // name of this model
         framePoint fp[1];               // start of a list of framePoints
        } frame;

        typedef struct _mesh
  {
     unsigned short meshIndex[3];     // indices to triangle vertices
     unsigned short stIndex[3];       // indices to texture coordinates
  } mesh;

        int framenr;
        int updatecounter;
        frame *frm;

        int frame_length[3];
        int frame_start[3];
        int animation;

        int texWidth;
        int texHeight;
        int imgWidth;
        int imgHeight;
        unsigned char* texture;                 
        GLuint  texturen[1];
};

md2.cpp

/*
 * Just the class to load Quake 2 .MD2 and PCX files in OpenGL/GLUT.
 *
 *
 * Needs A LOT OF refactoring and cleaning! 
 * 
 *
 */


#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "md2.h"

typedef struct _pcxHeader
{
   short id[2];
   short offset[2];
   short size[2];
} pcxHeader;

struct Mesh_UV
 {
  unsigned short s;
  unsigned short t;
 };


float* calculateNormal( float *coord1, float *coord2, float *coord3 )
{
   /* calculate Vector1 and Vector2 */
   float va[3], vb[3], vr[3], val;
   va[0] = coord1[0] - coord2[0];
   va[1] = coord1[1] - coord2[1];
   va[2] = coord1[2] - coord2[2];

   vb[0] = coord1[0] - coord3[0];
   vb[1] = coord1[1] - coord3[1];
   vb[2] = coord1[2] - coord3[2];

   /* cross product */
   vr[0] = va[1] * vb[2] - vb[1] * va[2];
   vr[1] = vb[0] * va[2] - va[0] * vb[2];
   vr[2] = va[0] * vb[1] - vb[0] * va[1];

   /* normalization factor */
   val = sqrt( vr[0]*vr[0] + vr[1]*vr[1] + vr[2]*vr[2] );

 //  glNormal3f( vr[0]/val, vr[1]/val, vr[2]/val );
        float norm[3];
        norm[0] = vr[0]/val;
        norm[1] = vr[1]/val;
        norm[2] = vr[2]/val;


        return norm;
}


Model_MD2::Model_MD2()
{
        this->Scale = 1;
        framenr = 0;
        updatecounter = 0;

        frame_length[0] = 39;   // stand
        frame_length[1] = 5;    // run
        frame_length[2] = 7;    // attack
        frame_length[3] = 3;    //      "pain1" 
        frame_length[4] = 3;    //      "pain2" 3
        frame_length[5] = 3;    // "pain3" 3
        frame_length[6] = 5;    // "jump" 5
        frame_length[7] = 11;   // "flip" 11
        frame_length[8] = 10;   // "salute" 10
        frame_length[9] = 16;   // "taunt" 16
        frame_length[10] = 10;  // "wave" 10


        frame_start[0] = 0;             // stand
        frame_start[1] = 39;    // run
        frame_start[2] = 46;    // shoot
        frame_start[3] = 50;    // pain1
        frame_start[4] = 39 + 5+ 7 + 3;
        frame_start[5] = frame_length[0] + frame_length[1] + frame_length[2] + frame_length[3] + frame_length[4];


        animation = 0;
}



void Model_MD2::LoadPCX(char* textureFilename)
{
        // Load texture
        FILE* texFile = fopen(textureFilename,"rb");
        
        if (texFile)
        {
      int imgWidth, imgHeight, texFileLen, imgBufferPtr, i;
               pcxHeader *pcxPtr;
               unsigned char *imgBuffer, *texBuffer, *pcxBufferPtr, *paletteBuffer;

               /* find length of file */
               fseek( texFile, 0, SEEK_END );
               texFileLen = ftell( texFile );
               fseek( texFile, 0, SEEK_SET );

               /* read in file */
               texBuffer = (unsigned char*) malloc( texFileLen+1 );
               fread( texBuffer, sizeof( char ), texFileLen, texFile );

               /* get the image dimensions */
               pcxPtr = (pcxHeader *)texBuffer;
               imgWidth = pcxPtr->size[0] - pcxPtr->offset[0] + 1;
               imgHeight = pcxPtr->size[1] - pcxPtr->offset[1] + 1;

                           this->imgWidth = imgWidth;
                           this->imgHeight = imgHeight;

               /* image starts at 128 from the beginning of the buffer */
               imgBuffer = (unsigned char*) malloc( imgWidth * imgHeight );
               imgBufferPtr = 0;
               pcxBufferPtr = &texBuffer[128];
               /* decode the pcx image */
               while( imgBufferPtr  0xbf )
                  {
                     int repeat = *pcxBufferPtr++ & 0x3f;
                     for( i=0; itexWidth = pow( 2, (double) i );
                  i = 0;
                  while( imageHeight )
                  {
                     imageHeight /= 2;
                     i++;
                  }
                  this->texHeight = pow( 2, (double) i );
               }
               /* now create the OpenGL texture */
               {
                  int i, j;
                  this->texture = (unsigned char*) malloc( this->texWidth * this->texHeight * 3 );
                  for (j = 0; j texture[3*(j * this->texWidth + i)+0]
                              = paletteBuffer[ 3*imgBuffer[j*imgWidth+i]+0 ];
                        this->texture[3*(j * this->texWidth + i)+1]
                              = paletteBuffer[ 3*imgBuffer[j*imgWidth+i]+1 ];
                        this->texture[3*(j * this->texWidth + i)+2]
                              = paletteBuffer[ 3*imgBuffer[j*imgWidth+i]+2 ];
                     }
                  }
               }

   
       /* cleanup */
       free( paletteBuffer );
       free( imgBuffer );




            glGenTextures( 1, &texturen[0] );
            glBindTexture( GL_TEXTURE_2D, texturen[0] );

            /* Generate The Texture */
            glTexImage2D( GL_TEXTURE_2D, 0, 3, this->texWidth,
                          this->texHeight, 0, GL_RGB,
                          GL_UNSIGNED_BYTE, this->texture );

                printf(" %i %i ", this->texWidth, this->texHeight);
            /* Linear Filtering */
            glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
            glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
        }

}

int Model_MD2::Load(char* filename, char* textureFilename)
{

        LoadPCX(textureFilename);
        // Load model
    MD2_Header *mdh;

        mesh* m;

        for (int i = 0; i AngleX = 0;
        this->AngleY = 0;
        this->AngleZ = 0;
        this->Scale = 1;
    this->TotalConnectedTriangles = 0; 

    char* pch = strstr(filename,".md2");
     
    if (pch != NULL)
    {
           FILE* file = fopen(filename,"r");
    
       if (file)
       {

                   // get size of file
           fseek( file, 0, SEEK_END );
                   size_t  fileSize = ftell( file );
                   fseek( file, 0, SEEK_SET );

                   // read in entire file
                   char* buffer;
                   buffer = (char*) malloc( fileSize+1 );
                   fread( buffer, sizeof( char ), fileSize, file );


                   // start analyzing the buffer
               mdh = (MD2_Header *)buffer;

                   printf("mdh framesize %i \n", mdh->framesize);

                   for (int z = 0; znumFrames; z++)
                   {
                           frm = (frame *)&buffer[ mdh->offsetFrames + z*mdh->framesize  ];
                          m = (mesh *)&buffer[mdh->offsetTris];
                          int ti = 0;
                          int point_index = 0;
        
                          /* offset to points of frame */
                          for(int i=0; inumXYZ; i++ )
                          {
                                this->Points[point_index]   = frm->scale[0] * frm->fp[i].v[0] + frm->translate[0]; // X
                                this->Points[point_index+1] = frm->scale[1] * frm->fp[i].v[1] + frm->translate[1]; // Y
                                this->Points[point_index+2] = frm->scale[2] * frm->fp[i].v[2] + frm->translate[2]; // Z
                                point_index += 3;
                          }
                          this->TotalConnectedTriangles = mdh->numTris * 3;
                          int n = 0;
                
                          //-------------------------------------------------------------
                          //-- create texture coordinate list ---------------------------

                          ti = 0;
                                                
                          for(int i=0; inumST; i++ )
                          {
                                 Mesh_UV* mUV = (Mesh_UV *)&buffer[mdh->offsetST + i*4 ];
                        
                                  this->TextureCoords[ti] = (float) mUV->s / this->texWidth;   
                                  this->TextureCoords[ti+1] = (float) mUV->t / this->texHeight;        
                                // printf(" (%i %i) ", mUV->s, mUV->t);
                                // printf(" %f %f \n ", this->TextureCoords[ti], this->TextureCoords[ti+1]);
                                ti+=2;
                          }


                          //---
                          m = (mesh *)&buffer[mdh->offsetTris];

                          n = 0;
                          ti = 0;


                          for(int i=0; inumTris; i++ )
                          {

                        
                                 this->Faces_Triangles[z][n] = Points[ 3*m[i].meshIndex[0] ];
                                 this->Faces_Triangles[z][n+1] = Points[ 3*m[i].meshIndex[0]+1 ];
                                 this->Faces_Triangles[z][n+2] = Points[ 3*m[i].meshIndex[0]+2 ];            
                                                 
                                 this->Faces_Textures[z][ti] = this->TextureCoords[ 2*m[i].stIndex[0] ];
                                 this->Faces_Textures[z][ti+1] = this->TextureCoords[ 2*m[i].stIndex[0]+1 ];

                                n+=3;
                                ti += 2;

                                 this->Faces_Triangles[z][n] = Points[ 3*m[i].meshIndex[1] ];
                                 this->Faces_Triangles[z][n+1] = Points[3* m[i].meshIndex[1]+1 ];
                                 this->Faces_Triangles[z][n+2] = Points[3* m[i].meshIndex[1]+2 ];

                                 this->Faces_Textures[z][ti] = TextureCoords[ 2*m[i].stIndex[1] ];
                                 this->Faces_Textures[z][ti+1] = TextureCoords[ 2*m[i].stIndex[1] + 1 ];

                                 n+=3;
                                ti += 2;
        
                                 this->Faces_Triangles[z][n] = Points[3* m[i].meshIndex[2] ];
                                 this->Faces_Triangles[z][n+1] = Points[3* m[i].meshIndex[2]+1 ];
                                 this->Faces_Triangles[z][n+2] = Points[3* m[i].meshIndex[2]+2 ];

                                 this->Faces_Textures[z][ti] = TextureCoords[ 2*m[i].stIndex[2] ];
                                 this->Faces_Textures[z][ti+1] = TextureCoords[ 2*m[i].stIndex[2] + 1 ];

                                 n+=3;
                                ti += 2;
                        


        //                       m = (mesh *)&buffer[mdh->offsetTris + 12*i ];
                          }
                   }
                   //mdh->offsetST

                   printf("\n");

          mdh = (MD2_Header *)buffer;

        for (int i = 0; iFaces_Textures[0][i], this->Faces_Textures[0][i+1]);
        }

                int normal_index = 0;
        for (int triangle_index = 0; triangle_index  10)
        {
                //if (framenr == frame_length[animation]) 
                //      framenr = 0;
                //else
                //{
                        //if (framenr animation = animation;
}


void Model_MD2::Crstnd()
{
        updatecounter++;
        if (updatecounter > 10)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr  20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr animation = 1;
}

void Model_MD2::Attack()
{
        updatecounter++;
        if (updatecounter > 20)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr animation = 1;
}

void Model_MD2::Run()
{
        updatecounter++;
        if (updatecounter > 1)
        {
                //if (framenr == (framenr+frame_length[animation]) ) framenr = 0; else framenr++;
                if (framenr animation = 1;

}


void Model_MD2::Stand()
{
        updatecounter++;
        if (updatecounter > 1)
        {
                if (framenr > 28) framenr = 0; else framenr++;
                updatecounter = 0;
        }

        this->animation = 0;

}


void Model_MD2::Draw()
{
        /*
        glRasterPos2i(0,0);
        glDrawPixels(this->texWidth , this->texHeight, GL_RGB, GL_UNSIGNED_BYTE, this->texture);
*/



    glEnable(GL_TEXTURE_2D);    
        glBindTexture(GL_TEXTURE_2D, texturen[0]);

        
        glEnableClientState(GL_VERTEX_ARRAY);   
        glEnableClientState(GL_TEXTURE_COORD_ARRAY);
        glEnableClientState(GL_NORMAL_ARRAY);
        glNormalPointer(GL_FLOAT, 0, Normals);

        glTexCoordPointer(2,GL_FLOAT,0, this->Faces_Textures[ framenr] );
        glVertexPointer(3,GL_FLOAT,     0,Faces_Triangles[ framenr ]);   
        glDrawArrays(GL_TRIANGLES, 0, TotalConnectedTriangles); 

        glDisableClientState(GL_NORMAL_ARRAY);
        glDisableClientState(GL_VERTEX_ARRAY);    
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

        glDisable(GL_TEXTURE_2D);
}

Finally you can compile using:

g++ md2.cpp main.cpp -o md2 -lGLU -lGL -lglut -fpermissive

The output should be something like (depending on your model) :