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
andGL_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; i texWidth = 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; i numXYZ; 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; i numST; 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; i numTris; 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) :