杏花女哭灵全集15集:C > OpenGL > The MD2 Model File Format

来源:百度文库 编辑:偶看新闻 时间:2024/04/29 18:46:50

The Quake II's MD2 file format

written by David Henry, december 21st of 2002

Introduction

“Yeah a new MD2 tutorial yet...” Yes but mine will show you how to render them by adifferent way ;-) But what the heck is an MD2 anyway? the MD2 file format is a 3D modelfile format used in Id Software's Quake II engine! And here I'll show you how to loadand display it to the screen using OpenGL!

You probably think “Damn, this guy stucks in the 1997 ol' days” but there are goodreasons to use it. First because the MD2 file format is a good 3D model file format forlearning because of it simplicity (if you're a beginner, you may not understand this,but look at other model file formats and you'll see ;-)) and then, because this format isabsolutely free!!! (but not models, be careful!).

So what? here is a little overview of what we'll see in this article:

  • The MD2 File Format
  • Developping a CMD2MODEL class
  • Reading and storing a MD2 model
  • Displaying it to the screen
  • Animation

Also, the source code is totaly free and downloadable at the end of this document.

Before starting I would like to say that I am assuming that you're familiar with the C++and the OpenGL API. Ok let's start with some theory about the MD2 file format!

The MD2 File Format

Like most file formats, the MD2 file format is composed of two things: a file header andthe data. The header contains some very important variables used when loading the file likethe size of the file data to load, a magic number or the version of the format, so its sizemust be always the same. That's why generally the header is a structure. In opposition, thesize of the data can vary from one file to another and contains multiple structures for vertices,triangles, texture coordinates, etc... Figure 1 represents the file architecture:

Here is the MD2 header structure definition (called md2_t):

// md2 header
typedef struct
{
int ident; // magic number. must be equal to "IDP2"
int version; // md2 version. must be equal to 8

int skinwidth; // width of the texture
int skinheight; // height of the texture
int framesize; // size of one frame in bytes

int num_skins; // number of textures
int num_xyz; // number of vertices
int num_st; // number of texture coordinates
int num_tris; // number of triangles
int num_glcmds; // number of opengl commands
int num_frames; // total number of frames

int ofs_skins; // offset to skin names (64 bytes each)
int ofs_st; // offset to s-t texture coordinates
int ofs_tris; // offset to triangles
int ofs_frames; // offset to frame data
int ofs_glcmds; // offset to opengl commands
int ofs_end; // offset to end of file

} md2_t;

Ok I'll explain briefly all these variables.

First you have what is called a “magic number”. When loading the file in memory, checkthis value and be sure it's equal to “IPD2”. If it isn't equal to “IPD2” then you can closethe file and stop the loading. It is not an MD2 file. The next variable indicates the fileversion and must be equal to 8.

Then we've got the dimensions of the texture (respectively the width and the height).We won't use these variables because the md2 model's texture is stored in another file,most of the time a PCX or a TGA file, and we obtain the texture's dimensions from these files.

framesize specifies the size in bytes of each frame. Yes but what the hell is aframe? A frame is like a picture in a movie. Looping many frames at a certain speed, you getan animation! So one frame stores model's vertices and triangles in a particular position. Soa classic md2 file is composed of 199 frames distributed in 21 animations. A frame contains alist of vertices for all triangles of this frame (each frame has the same number of triangles).For the moment, remember that we will need this variable to know how much memory we need toallocate for storing each frame.

The next variables are quite similar.

num_skins tells you the number of textures avalaible for this model. For exemple,you can have a texture for the red team and an other for the blue team in a team game. The nameof each texture is stored in an array of 64 bytes at the ofs_skins offset in thefile. However we won't use these names because they are specific to the Quake2 directory, forexemple: “player/ogro/igdosh.pcx”.

num_xyz is the total amount of vertices of the model. It correspond to the sumof the number of vertices in each frame.

num_st is the number of texture coordinates which are stored in the file at theoffset ofs_st. Note that this number isn't inevitably equal to the number of vertices.In our code we will use another way to obtain these textures coordinates and in real-time so wewon't need to load the texture coordinate array from the file.

num_tris gives us the total amount of triangles in the model.

num_glcmds is the number of OpenGL command. The GL command list is an array ofintegers that allows us to render the model using only triangle fans and triangle strip(GL_TRIANGLE_STRIP and GL_TRIANGLE_FAN), instead of classic triangles(GL_TRIANGLES). GL commands are very powerful. It is easy to get a rendering about 10or 15 fps faster!

Finaly there is num_frames yet. It specifies the total number of frames that holdsthe model. In fact, each of them are refered to as keyframes, which are frames taken from discretetime intervals because it would be impossible to hold 200 or 300 frames per animation! Consequently,we only keep some of these for each animation and we'll calculate all intermediate frames we'llneed when rendering, using linear interpolation (I'll explain that later). Look at Figure 2 to seean exemple. Here is represented a simplistic model with one animation which need 20 frames to befully displayed, but only 5 of these are kept. Frames from number 1 to 4, 6 to 9, 11 to 14 and 16to 19 must be calculated before rendering to get a smooth animation.

The last bloc of header's variables contains offsets to access to different types of model's data.ofs_skins points on model's texture names, ofs_st on texture coordinates,ofs_tris points on vertices, ofs_frames on the first frame of the model,ofs_glcmds on OpenGL command list and of course, ofs_end which tells you theend of the file (we won't need it).

Yeah we've finished with the header! Now let's look at structures needed to store model data! Yes,like the header, we'll use structures to hold frames, vertices and OpenGL commands.

The first data type very useful in most 3D applications is the vector! We don't need a complicatedVector Class so I will keep things simple: a simple array of 3 float will represent a vector!

typedef float vec3_t[3];

Each model is composed of (num_frame * num_xyz) vertices. Here is the structure that holda single vertex:

// vertex
typedef struct
{
unsigned char v[3]; // compressed vertex (x, y, z) coordinates
unsigned char lightnormalindex; // index to a normal vector for the lighting

} vertex_t;

You may have noticed that v[3] contains vertex' (x,y,z) coordinates and becauseof the unsigned char type, these coordinates can only range from 0 to 255. In fact these 3Dcoordinates are compressed (3 bytes instead of 12 if we would use float or vec3_t). To uncompress it,we'll use other data proper to each frame. lightnormalindex is an index to a precalculatednormal table. Normal vectors will be used for the lighting.

The last piece of information needed for a vertex is its texture coordinates. They are alsopacked into a structure:

// texture coordinates
typedef struct
{
short s;
short t;

} texCoord_t;

Like for vertices, data is compresed. Here we use short (2 bytes) instead of float (4 bytes)for storing texture coordinates. But to use them, we must convert them to float because texturecoordinates range from 0.0 to 1.0, and if we kept short values, we could have only 0 or 1 andany intermediate value! So how to uncompress them? It's quite simple. Divide the short value bythe texture size:

  • RealST[i].s = (float)texCoord[i].s / header.skinwidth;
  • RealST[i].t = (float)texCoord[i].t / header.skinheight;

supposing that RealST is an object of a structure similar to texCoord_t but withfloat instead of short types and texCoord is an array of texCoord_tloaded from a MD2 file.

Each frame (or keyframe) of the model is stored in a structure defined like that:

// frame
typedef struct
{
float scale[3]; // scale values
float translate[3]; // translation vector
char name[16]; // frame name
vertex_t verts[1]; // first vertex of this frame

} frame_t;

Each frame is stored as a frame_t structure, holding all specific data to this frame.So a classical model (i.e. a player model) has 199 frame_t objects. I said oneminute ago that we will uncompress vertices using frame data. Here is the data! To uncompresseach vertex, we will scale it multiplying its coordinates by the scale[3] valuesand then translate it by the translate[3] vector (we could also write vec3_ttranslate instead of float translate[3]).

name[16] is simply the name of the frame. Finaly, verts[1] isthe first vertex of the frame. Other vertices of this frame are stored just after the firstvertex, so we can access to them like that:

  • frame.verts[ 2 ] // get the second vertex of the frame
  • frame.verts[ i ] // get the i th vertex of the frame
  • frame.verts[ num_xyz - 1 ] // get the last vertex of this frame

Thus we get the real vertex coordinates:

  • vertex.x = (frame.verts[i].v[0] * frame.scale[0]) + frame.translate[0]
  • vertex.y = (frame.verts[i].v[1] * frame.scale[1]) + frame.translate[1]
  • vertex.z = (frame.verts[i].v[2] * frame.scale[2]) + frame.translate[2]

where i ranges from 0 to (num_xyz - 1).

Look at Figure 3 to see a representation of relations between animations, frames and vertices:

So each animation contains n frames which contain each num_xyz vertices.

We need now to link each vertex with its texture coordinates couple. But instead of linking onevertex_t with one texCoord_t, they are linked by triplet to form a triangle,or a mesh:

// triangle
typedef struct
{
short index_xyz[3]; // indexes to triangle's vertices
short index_st[3]; // indexes to vertices' texture coorinates

} triangle_t;

This is how they are stored in the file. Notice that index_xyz and index_stare indexes on the data and not the data themselves! The data must be stored separately in vertex_tand texCoord_t arrays or if you prefer uncompress them during the model loading, in similarstructures with float types. Supposing that Vertices[] is an array of vertex_t,TexCoord[] an array of texCoord_t, Meshes[] an array oftriangle_t and anorms[] an array of vec3_t which stores allprecalculed normal vectors. You could draw the model using this method:

glBegin( GL_TRIANGLES );
// draw each triangle
for( int i = 0; i < header.num_tris; i++ )
{
// draw triangle #i
for( int j = 0; j < 3; j++ )
{
// k is the frame to draw
// i is the current triangle of the frame
// j is the current vertex of the triangle

glTexCoord2f( (float)TexCoord[ Meshes[i].index_st[j] ].s / header.skinwidth,
(float)TexCoord[ Meshes[i].index_st[j] ].t / header.skinheight );

glNormal3fv( anorms[ Vertices[ Meshes[i].index_xyz[j] ].lightnormalindex ] );

glVertex3f( (Vertices[ Meshes[i].index_xyz[j] ].v[0] * frame[k].scale[0]) + frame[k].translate[0],
(Vertices[ Meshes[i].index_xyz[j] ].v[1] * frame[k].scale[1]) + frame[k].translate[1],
(Vertices[ Meshes[i].index_xyz[j] ].v[2] * frame[k].scale[2]) + frame[k].translate[2] );
}
}
glEnd();

Ok this is not very easy to visualize and the method uses GL_TRIANGLES. We can getbetter performances using GL_TRIANGLE_SRTIP and GL_TRIANGLE_FAN. But how?Using the OpenGL commands!

This is all about data structures! I can now show you the entire file architecture:

Developing a CMD2Model Class

Thanks to OpenGL commands, we won't need to use triangle_t and texCoord_tstructures, because all this is also included in the OpenGL command list, which we'll use. Icovered them in case you don't want to use OpenGL commands or if you don't want to render using OpenGL.

We're now ready to develop a class which will represent an MD2 model object. Here is the prototype:

// ============================================
// CMD2Model - MD2 model class object.
// ============================================

class CMD2Model
{
public:
// constructor/destructor
CMD2Model( void );
~CMD2Model( void );


// functions
bool LoadModel( const char *filename );
bool LoadSkin( const char *filename );

void DrawModel( float time );
void DrawFrame( int frame );

void SetAnim( int type );
void ScaleModel( float s ) { m_scale = s; }


private:
void Animate( float time );
void ProcessLighting( void );
void Interpolate( vec3_t *vertlist );
void RenderFrame( void );


public:
// member variables
static vec3_t anorms[ NUMVERTEXNORMALS ];
static float anorms_dots[ SHADEDOT_QUANT ][256];

static anim_t animlist[21]; // animation list


private:
int num_frames; // number of frames
int num_xyz; // number of vertices
int num_glcmds; // number of opengl commands

vec3_t *m_vertices; // vertex array
int *m_glcmds; // opengl command array
int *m_lightnormals; // normal index array

unsigned int m_texid; // texture id
animState_t m_anim; // animation
float m_scale; // scale value

};

Each MD2 model will be a CMD2Model object. Hum this class looks quite strangemore especially as there is nor frame_t object neither vertex_t object!And where are texture coordinates stored? some explanations are required...

First we've got classic constructor and destructor that initialize all member variables to 0(excepted m_scale) and free allocated memory during the loading of data.

What about functions? I think they are self-explanatory. LoadModel() will loadthe model from a file and initialize it and LoadSkin() will load the texture andinitialize m_texid.

DrawModel() is the function we'll use to draw the animated model with alltransformation needed. The time parameter is needed to calculate the frame to render from theactual animation.

DrawFrame() is the function we'll use to draw the model at a specific frame.

SetAnim() and ScaleModel() are used to set the current animationand the scale value.

Animate(), ProcessLighting(), Interpolate() andRenderFrame() are private functions because they must be only used inside thepublic DrawModel() function. They process all calculations to render the properframe interpolated and lightened.

Now member variables. anorms is an array of precalculated normal vectors. Each vertex willhave an index stored in the m_lightnormals array to access to its own normal vector.anorms_dots looks like anorms but this time it stores precalculated dotproducts. We will need it when processing lighting. animlist is an array of animations.Here is the anim_t structure prototype:

// animation
typedef struct
{
int first_frame; // first frame of the animation
int last_frame; // number of frames
int fps; // number of frames per second

} anim_t;

You may have noticed that these three last member variables are static. This is becausethey are the same for every MD2 model so we need only one copy of them.

Then we have num_frames which stores the total number of frames,num_xyz the number of vertices per frame and num_glcmds the numberof OpenGL commands.

m_vertices holds 3D coordinates in floating point number for each vertex.The m_glcmds array stores OpenGL command list. For the moment, don't be afraid of these“OpenGL commands”, just think that it is magic. I'll explain when we'll need them to drawmodel's meshes. For these three last array, we will allocate memory dynamically.

m_texid will store the OpenGL texture ID. m_anim store informationabout the current animation to play. It is an animState_t object (look at commentsfor a brief description):

// animation state
typedef struct
{
int startframe; // first frame
int endframe; // last frame
int fps; // frame per second for this animation

float curr_time; // current time
float old_time; // old time
float interpol; // percent of interpolation

int type; // animation type

int curr_frame; // current frame
int next_frame; // next frame

} animState_t;

Finaly, m_scale stores the scale value for all axes. This is better toscale vertices by multiplying them with the m_scale value than using glScalef()because this function would scale normal vectors also and would bring to strange lighting effects.

I have said that we won't use neither triangle_t nor texCoord_tstructures, but what about vertex_t and frame_t structures ? We'll onlyuse these when loading the model in the LoadModel() function and transform frame datato be stored in m_vertices and m_lightnormals arrays.

Before ending this section, I want to give you constructor and destructor definitions:

// ----------------------------------------------
// constructor - reset all data.
// ----------------------------------------------

CMD2Model::CMD2Model( void )
{
m_vertices = 0;
m_glcmds = 0;
m_lightnormals = 0;

num_frames = 0;
num_xyz = 0;
num_glcmds = 0;

m_texid = 0;
m_scale = 1.0;

SetAnim( 0 );
}


// ----------------------------------------------
// destructor - free allocated memory.
// ----------------------------------------------

CMD2Model::~CMD2Model( void )
{
delete [] m_vertices;
delete [] m_glcmds;
delete [] m_lightnormals;
}

For the constructor, we set all member variables (excepts static variables and m_scale)to 0. We initialize m_scale to 1.0 because if we would set it to 0, therewould be nothing rendered! For the destructor, we just desallocate memory...

Ok, we're ready to start really! Let's move to the next section: loading a MD2 model file!

Reading and storing a MD2 model

We load an MD2 model passing its filename in parameter to the LoadModel() function.It returns true if success and false if something fails during the loading. Look at the firstpart of the function:

// ----------------------------------------------
// LoadModel() - load model from file.
// ----------------------------------------------

bool CMD2Model::LoadModel( const char *filename )
{
std::ifstream file; // file stream
md2_t header; // md2 header
char *buffer; // buffer storing frame data
frame_t *frame; // temporary variable
vec3_t *ptrverts; // pointer on m_vertices
int *ptrnormals; // pointer on m_lightnormals


// try to open filename
file.open( filename, std::ios::in | std::ios::binary );

if( file.fail() )
return false;

// read header file
file.read( (char *)&header, sizeof( md2_t ) );


/////////////////////////////////////////////
// verify that this is a MD2 file

// check for the ident and the version number

if( (header.ident != MD2_IDENT) && (header.version != MD2_VERSION) )
{
// this is not a MD2 model
file.close();
return false;
}

/////////////////////////////////////////////

First we define some local variables that we'll need during the loading of the model.file is a file stream to extract model data from a file. header is a md2_tobject which will store the header of the model file. Then we have buffer. It's alarge buffer for storing all frame data. The three last variables are different pointers to accessdata from buffer.

We start by trying to open the specified file in read only mode and return false if it fails.The file opened, we then load the model header. Thus we can check for the magic number (the ident)and the version of the model to be sure that it is a MD2 file. The ident must allways equalto “IDP2” and the version of the model to 8. So we can define MD2_IDENT andMD2_VERSION like this:

// magic number "IDP2" or 844121161
#define MD2_IDENT (('2'<<24) + ('P'<<16) + ('D'<<8) + 'I')

// model version
#define MD2_VERSION 8

Notice that we could also check for the magic number comparing the ident to 844121161 orusing the strcmp() function (ident must then be defined as a char [4]).

Now that we are sure that it's a valid MD2 file, we can continue te loading:

    // initialize member variables
num_frames = header.num_frames;
num_xyz = header.num_xyz;
num_glcmds = header.num_glcmds;


// allocate memory
m_vertices = new vec3_t[ num_xyz * num_frames ];
m_glcmds = new int[ num_glcmds ];
m_lightnormals = new int[ num_xyz * num_frames ];
buffer = new char[ num_frames * header.framesize ];


/////////////////////////////////////////////
// reading file data

// read frame data...
file.seekg( header.ofs_frames, std::ios::beg );
file.read( (char *)buffer, num_frames * header.framesize );

// read opengl commands...
file.seekg( header.ofs_glcmds, std::ios::beg );
file.read( (char *)m_glcmds, num_glcmds * sizeof( int ) );

/////////////////////////////////////////////

Here we first initialize our numerical variables from the model header. Then we can allocatenecessary memory for our m_vertices, m_glcmds, m_lightnormalsand buffer arrays. Notice that there is the same number of elements for m_verticesand m_lightnormals. Thus we can have one index for a vertex which would points bothon its 3D coordinates and in its normal index. We'll get this pointer from the m_glcmdsarray.

Memory is allocated so we can read data from the file. Before reading data, we move to theposition specified by header's offsets. We only read frame data and OpenGL commands. We'llinitialize m_vertices and m_lightnormals with buffer like that:

    // vertex array initialization
for( int j = 0; j < num_frames; j++ )
{
// adjust pointers
frame = (frame_t *)&buffer[ header.framesize * j ];
ptrverts = &m_vertices[ num_xyz * j ];
ptrnormals = &m_lightnormals[ num_xyz * j ];

for( int i = 0; i < num_xyz; i++ )
{
ptrverts[i][0] = (frame->verts[i].v[0] * frame->scale[0]) + frame->translate[0];
ptrverts[i][1] = (frame->verts[i].v[1] * frame->scale[1]) + frame->translate[1];
ptrverts[i][2] = (frame->verts[i].v[2] * frame->scale[2]) + frame->translate[2];

ptrnormals[i] = frame->verts[i].lightnormalindex;
}
}

This is the more difficult to understand. First we loop through each frame. For each frame,we extract frame data from buffer using our frame_t* pointer defined at thebeginning of the function. We also adjust our pointers on *m_vertices and*m_lightnormals so that they point at the beginning of where must be stored thecurrent frame data.

Then we loop through each vertex of the current frame that we are processing. We initializevertex's 3D coordinates with the formula I explained before, in the section about the MD2 fileformat. We also initialize the normal index stored in the vertex's vertex_tstructure.

We have initialized our three numerical variables and our three data arrays, so we've finishedwith the model file! Was it so difficult? We have just to close the file, free bufferand return true:

    // free buffer's memory
delete [] buffer;

// close the file and return
file.close();
return true;
}

Now what about the texture? For the texture, we only have its texture ID to store inm_texid. MD2's textures are stored in classical TGA or PCX files. Loading atexture from a file is beyond the scope of this article, so I won't cover how it works. Iassume that you have a function which loads a texture from a file and returns a valid ID.In the source code that you can download, I have written a simple Texture Manager which canloads and initializes a texture from a bitmap, targa of pcx file. Here is how we load thetexture with the LoadSkin() function:

// ----------------------------------------------
// LoadSkin() - load model texture.
// ----------------------------------------------

bool CMD2Model::LoadSkin( const char *filename )
{
m_texid = LoadTexture( filename );

return (m_texid != LoadTexture( "default" ));
}

Just a few words about my texture manager: first I have written an inline LoadTexture()function for easier code reading. This function access to the Texture Manager's LoadTexture()function. The Texture Manager is a singleton. When initializing, it creates a default texture(which is a black and white checker). When loading a texture from a file, it first checks if thetexture has already been loaded. If yes it returns the texture ID, else it tries to open the fileand load it. If the loading fails, or the file doesn't exist, it returns the default texture ID.So when calling texmgr.LoadTexture( "default" ), this doesn't load a texture but returnsthe default texture ID. When returning, we check the texture ID this function gived us whenloading our texture and return false if it equals to the default texture ID.

This is all for this section. We have loaded all data we need.

Drawing the model

It's time to render the model we've loaded!

The main drawing model function is DrawModel(). However this function won't renderdirectly the model, but will process some transformations and calculus before calling theRenderFrame() function. Let's look at the function definition:

// ----------------------------------------------
// DrawModel() - draw the model.
// ----------------------------------------------

void CMD2Model::DrawModel( float time )
{
glPushMatrix();
// rotate the model
glRotatef( -90.0, 1.0, 0.0, 0.0 );
glRotatef( -90.0, 0.0, 0.0, 1.0 );

// render it on the screen
RenderFrame();
glPopMatrix();
}

Ok, there only are two simple rotations before rendering and for the moment, the timeparameter is not used... But we'll update this function later, when animating! We need to rotate themodel on the X and Z axis because it isn't stored using OpenGL axis. You can comment the two callsto glRotatef() to see why we do that :-)

Remember the m_scale value and ScaleModel() function I discussed earlier.To avoid having a huge model at the screen once the rendering finished, we scale each vertices of thecurrent frame we're rendering. The scaling operation is processed by the Interpolate()function called by RenderFrame(). Normaly vertex interpolation have nothing to do withscaling, but because for the moment we are not animating, the Interpolate() function willonly scale vertices. Later we'll rewrite it to really interpolate vertices from two frames. Here is thecode:

// ----------------------------------------------
// Interpolate() - interpolate and scale vertices
// from the current and the next frame.
// ----------------------------------------------

void CMD2Model::Interpolate( vec3_t *vertlist )
{
for( int i = 0; i < num_xyz ; i++ )
{
vertlist[i][0] = m_vertices[ i + (num_xyz * m_anim.curr_frame) ][0] * m_scale;
vertlist[i][1] = m_vertices[ i + (num_xyz * m_anim.curr_frame) ][1] * m_scale;
vertlist[i][2] = m_vertices[ i + (num_xyz * m_anim.curr_frame) ][2] * m_scale;
}
}

This function initializes an array of vertices with the current frame scaled vertices. So theRenderFrame() function will use the array passed in parameter for rendering and won't usethe original m_vertices array directly. It will also be easier manipulating vertlist thanm_vertices.

Now I would like to talk about lighting a little. There is two way to light the model. The firstway is using OpenGL lighting functions. For that, we just need to set the normal of each vertex we'rerendering. There is no difficulty, the index stored in m_lightnormals give us aprecalculated normal from the anorms table.

The second way to light the model is using glColor() for each vertex to fake lightingand shading. Also this is the way used in Quake II's engine. For this method, there is some work todo. So we'll put all it in the ProcessLighting() function, called by RenderFrame()like the Interpolate() function. But before, we need to create some global variables andinitialize others...

// number of precalculated normals
#define NUMVERTEXNORMALS 162

// number of precalculated dot product results (for lighting)
#define SHADEDOT_QUANT 16

// precalculated normal vectors
vec3_t CMD2Model::anorms[ NUMVERTEXNORMALS ] = {
#include "anorms.h"
};

// precalculated dot product results
float CMD2Model::anorms_dots[ SHADEDOT_QUANT ][256] = {
#include "anormtab.h"
};

static float *shadedots = CMD2Model::anorms_dots[0];
static vec3_t lcolor;

/////////////////////////////////////////////////

vec3_t g_lightcolor = { 1.0, 1.0, 1.0 };
int g_ambientlight = 32;
float g_shadelight = 128;
float g_angle = 0.0;

/////////////////////////////////////////////////

The precalculated normal and dot result lists are two big and not very interesting to show, so theyare stored in header files that we simply include to initialize static arrays.

shadedots is a pointer which will ajusted in the ProcessLighting() function.It will pointer in an element of the anorms_dots array.

lcolor will store RGB values for the final light color.

Finaly, the three last global variables are for the ambient light value (which range from 0 to 255),shading value (from 0 to 255) and the angle from where come te light (0.0 to 360.0).

Here is the ProcessLighting() function definition:

// ----------------------------------------------
// ProcessLighting() - process all lighting calculus.
// ----------------------------------------------

void CMD2Model::ProcessLighting( void )
{
float lightvar = (float)((g_shadelight + g_ambientlight)/256.0);

lcolor[0] = g_lightcolor[0] * lightvar;
lcolor[1] = g_lightcolor[1] * lightvar;
lcolor[2] = g_lightcolor[2] * lightvar;

shadedots = anorms_dots[ ((int)(g_angle * (SHADEDOT_QUANT / 360.0))) & (SHADEDOT_QUANT - 1) ];
}

First we create a local variable which we'll use to initialize the final light color(lcolor) and then we adjust the shadedots pointer. The formulais quite obscure, don't worry about it, it works fine it's all we want ;-) It comes fromthe Quake II's source code.

Now drawing each triangle! Remember at the beginning of this document when I gave a pieceof code rendering each triangle of the current frame. The bad thing is that we were drawingusing GL_TRIANGLES, and for that we need to specify three vertices per triangle.Moreover, it is slower than rendering using GL_TRIANGLE_STRIP orGL_TRIANGLE_FAN which need less vertices to draw more triangles. Figure 5 showsthis idea:

The best would be that we could draw the entire model using GL_TRIANGLE_STRIPand GL_TRIANGLE_FAN. This is what are made gl commands for! The OpenGL commandlist is a particular array of integers. We'll initialize a pointer pointing at the beginningof the list and read each command until the pointer return 0. 0 is the last value of theOpenGL command list. Now how does it work?

  • We read the first value. This value indicates two things: the type of triangle to draw (GL_TRIANGLE_STRIP if the number is positive and GL_TRIANGLE_FAN if negative) and the number n of vertices to draw for this rendering mode.
  • The n * 3 next values store information about vertices to draw.
  • The two first are (s, t) texture coordinates and the third is the vertex index to draw.
  • Once all vertices of this group are processed, we read a new value to get a new group... If the read value is 0, it is done!

It is not very simple the first time but with some practice you'll see that in realityit is quite simple ;-) Look at figure 6 for a representation of OpenGL command list (eachrectangle represent one command which is one integer value):

Ok I've finished with theory. Now the code:

// ----------------------------------------------
// RenderFrame() - draw the current model frame
// using OpenGL commands.
// ----------------------------------------------

void CMD2Model::RenderFrame( void )
{
static vec3_t vertlist[ MAX_MD2_VERTS ]; // interpolated vertices
int *ptricmds = m_glcmds; // pointer on gl commands


// reverse the orientation of front-facing
// polygons because gl command list's triangles
// have clockwise winding
glPushAttrib( GL_POLYGON_BIT );
glFrontFace( GL_CW );

// enable backface culling
glEnable( GL_CULL_FACE );
glCullFace( GL_BACK );


// process lighting
ProcessLighting();

// interpolate
Interpolate( vertlist );

// bind model's texture
glBindTexture( GL_TEXTURE_2D, m_texid );


// draw each triangle!
while( int i = *(ptricmds++) )
{
if( i < 0 )
{
glBegin( GL_TRIANGLE_FAN );
i = -i;
}
else
{
glBegin( GL_TRIANGLE_STRIP );
}


for( /* nothing */; i > 0; i--, ptricmds += 3 )
{
// ptricmds[0] : texture coordinate s
// ptricmds[1] : texture coordinate t
// ptricmds[2] : vertex index to render

float l = shadedots[ m_lightnormals[ ptricmds[2] ] ];

// set the lighting color
glColor3f( l * lcolor[0], l * lcolor[1], l * lcolor[2] );

// parse texture coordinates
glTexCoord2f( ((float *)ptricmds)[0], ((float *)ptricmds)[1] );

// parse triangle's normal (for the lighting)
// >>> only needed if using OpenGL lighting
glNormal3fv( anorms[ m_lightnormals[ ptricmds[2] ] ] );

// draw the vertex
glVertex3fv( vertlist[ ptricmds[2] ] );
}

glEnd();
}

glDisable( GL_CULL_FACE );
glPopAttrib();
}

We start creating two local variables. vertlist[] is an array of 3D floatingpoint coordinates which will contains the interpolated and scaled vertices of the frame torender. The array is static so it's declared only once. It's better for performance improvementthan creating a new array at each call of this function. The size of the array is constantand is the maximum number of vertices that a model can hold.

The second variable is ptricmds. It is the pointer which will read OpenGLcommands.

Then we save polygon attributes, reverse orientation of front-facing polygons because ofthe GL commands and enable backface culling. We process all calculus needed for the lighting,interpolate vertices and scale them, and bind the model texture.

All the rendering is done in the while statement. First we get the triangle type and the numberof vertices to draw. In the for statement we parse each vertex. Because each vertex has 3 valuesstored in the gl command list, we increment the pointer by 3 when all vertices of the group areprocessed.

For each vertex, we set the lighting color using the pointer on the dot product result tablefor the light angle and the final lighting color calculated by the ProcessLighting()function. Textures coordinates are casted from int to float. We obtain the normal vector fromthe anorms table and render the vertex from the array initialized just before.

Notice that if you don't use OpenGL lighting, the call to glNormal3fv() don't doanything and if you use it, the call to glColor3f() doesn't affect anything.

Animating

3D models look nicer when they are animated! So let's animate all that.

Remember the static animlist array. It has been designed to store all minimalanimation data, that is to say the index of the first and last frame, and the fps count forrunning the animation. All this is regrouped into a structure anim_t we've alreadyseen before. Here is the initialisation:

// ----------------------------------------------
// initialize the 21 MD2 model animations.
// ----------------------------------------------

anim_t CMD2Model::animlist[ 21 ] =
{
// first, last, fps

{ 0, 39, 9 }, // STAND
{ 40, 45, 10 }, // RUN
{ 46, 53, 10 }, // ATTACK
{ 54, 57, 7 }, // PAIN_A
{ 58, 61, 7 }, // PAIN_B
{ 62, 65, 7 }, // PAIN_C
{ 66, 71, 7 }, // JUMP
{ 72, 83, 7 }, // FLIP
{ 84, 94, 7 }, // SALUTE
{ 95, 111, 10 }, // FALLBACK
{ 112, 122, 7 }, // WAVE
{ 123, 134, 6 }, // POINT
{ 135, 153, 10 }, // CROUCH_STAND
{ 154, 159, 7 }, // CROUCH_WALK
{ 160, 168, 10 }, // CROUCH_ATTACK
{ 196, 172, 7 }, // CROUCH_PAIN
{ 173, 177, 5 }, // CROUCH_DEATH
{ 178, 183, 7 }, // DEATH_FALLBACK
{ 184, 189, 7 }, // DEATH_FALLFORWARD
{ 190, 197, 7 }, // DEATH_FALLBACKSLOW
{ 198, 198, 5 }, // BOOM
};

We'll use an index to access to animation data, but it is better to define a macro for eachindex for readability of the source code:

// animation list
typedef enum {
STAND,
RUN,
ATTACK,
PAIN_A,
PAIN_B,
PAIN_C,
JUMP,
FLIP,
SALUTE,
FALLBACK,
WAVE,
POINT,
CROUCH_STAND,
CROUCH_WALK,
CROUCH_ATTACK,
CROUCH_PAIN,
CROUCH_DEATH,
DEATH_FALLBACK,
DEATH_FALLFORWARD,
DEATH_FALLBACKSLOW,
BOOM,

MAX_ANIMATIONS

} animType_t;

The current animation data is stored in the m_anim variable but is a little differentfrom the anim_t structure. So to set an animation we must retrieve animation data andinitialize current animation data with it. It's the SetAnim() function's job:

// ----------------------------------------------
// SetAnim() - initialize m_anim from the specified
// animation.
// ----------------------------------------------

void CMD2Model::SetAnim( int type )
{
if( (type < 0) || (type > MAX_ANIMATIONS) )
type = 0;

m_anim.startframe = animlist[ type ].first_frame;
m_anim.endframe = animlist[ type ].last_frame;
m_anim.next_frame = animlist[ type ].first_frame + 1;
m_anim.fps = animlist[ type ].fps;
m_anim.type = type;
}

First we check the type is valide and then we initialize m_anim's members variables.You can pass to type any macro defined just before.

We'll now see a new function: Animate(). This function will be called in theDrawModel() function, so we must rewrite it:

// ----------------------------------------------
// DrawModel() - draw the model.
// ----------------------------------------------

void CMD2Model::DrawModel( float time )
{
// animate. calculate current frame and next frame
if( time > 0.0 )
Animate( time );

glPushMatrix();
// rotate the model
glRotatef( -90.0, 1.0, 0.0, 0.0 );
glRotatef( -90.0, 0.0, 0.0, 1.0 );

// render it on the screen
RenderFrame();
glPopMatrix();
}

Here we animate only if time is greater than 0.0. Otherwise there is no animation, the model isstatic. Look at the Animate() function source code:

// ----------------------------------------------
// Animate() - calculate the current frame, next
// frame and interpolation percent.
// ----------------------------------------------

void CMD2Model::Animate( float time )
{
m_anim.curr_time = time;

// calculate current and next frames
if( m_anim.curr_time - m_anim.old_time > (1.0 / m_anim.fps) )
{
m_anim.curr_frame = m_anim.next_frame;
m_anim.next_frame++;

if( m_anim.next_frame > m_anim.endframe )
m_anim.next_frame = m_anim.startframe;

m_anim.old_time = m_anim.curr_time;
}

// prevent having a current/next frame greater
// than the total number of frames...
if( m_anim.curr_frame > (num_frames - 1) )
m_anim.curr_frame = 0;

if( m_anim.next_frame > (num_frames - 1) )
m_anim.next_frame = 0;

m_anim.interpol = m_anim.fps * (m_anim.curr_time - m_anim.old_time);
}

In a first time, the function calculate the first and next frames using the fps count specifiedto the current animation. In a second time, it check these values and verify that they are correct(they must not be greater than the total number of frames that holds the model. Finaly, theinterpolation percent is calculated from the animation fps count and the time.

We must now review our Interpolate() function, this time to really interpolatevertices. Otherwise, we would have a very poor animation because of the number of frames themodel can holds. With the interpolation, we can create an “infinity” of frames (we create justthat we need when rendering). The formula is quite simple:

  • Xinterpolated = Xinital + InterpolationPercent * (Xfinal - Xinital)

So let's interpolate all vertices of the current and the next frames. The newInterpolate() function looks like this:

// ----------------------------------------------
// Interpolate() - interpolate and scale vertices
// from the current and the next frame.
// ----------------------------------------------

void CMD2Model::Interpolate( vec3_t *vertlist )
{
vec3_t *curr_v; // pointeur to current frame vertices
vec3_t *next_v; // pointeur to next frame vertices

// create current frame and next frame's vertex list
// from the whole vertex list
curr_v = &m_vertices[ num_xyz * m_anim.curr_frame ];
next_v = &m_vertices[ num_xyz * m_anim.next_frame ];

// interpolate and scale vertices to avoid ugly animation
for( int i = 0; i < num_xyz ; i++ )
{
vertlist[i][0] = (curr_v[i][0] + m_anim.interpol * (next_v[i][0] - curr_v[i][0])) * m_scale;
vertlist[i][1] = (curr_v[i][1] + m_anim.interpol * (next_v[i][1] - curr_v[i][1])) * m_scale;
vertlist[i][2] = (curr_v[i][2] + m_anim.interpol * (next_v[i][2] - curr_v[i][2])) * m_scale;
}
}

By the way, we scale interpolated vertices... And that's all! You just need to call onceSetAnim() and ScaleModel() functions with the parameter of your choice,and DrawModel() with the current time in seconds in parameter during the renderingloop. That's not so bad!

Just before ending, I would show you how to render a simple frame in case you'll need(for example: drawing a statue):

// ----------------------------------------------
// RenderFrame() - draw one frame of the model
// using gl commands.
// ----------------------------------------------

void CMD2Model::DrawFrame( int frame )
{
// set new animation parameters...
m_anim.startframe = frame;
m_anim.endframe = frame;
m_anim.next_frame = frame;
m_anim.fps = 1;
m_anim.type = -1;

// draw the model
DrawModel( 1.0 );
}

This function adjust animation variables before calling DrawModel() which willrender the specified frame of the model.

Conclusion

Here we are, it is finally finished! :-)

This article is far from being perfect and can be widely improved like including multiple skinsupport or separating model file data (vertex list, normal list, ...) from model parameters (currentframe, current animation, ...) to avoid storing same model data multiple times when more than oneentity is representated by the same model... It is difficult to create a perfect CMD2Modelclass which would work in any program with a simple cut and paste...

I hope this article helped you to learn about the MD2 model file format and more generally about3D Model files! Also I hope it was not too confusing. Please don't spam my mailbox about my English,it is not my native language. Otherwise, you can contact me at tfc_duke NOSPAM club-internet.frfor anything you want to say about this article (suggestions, mistakes, ...).

You can download source code (Visual C++ 6.0 version) and binaries with a model and itsweapon. Source code of this article is free and is provided without warranty expressed or implied.Use at your own risk! Download: q2md2_us.zip.

NOTE: the code of my MD2Loader has been completly rewritten since I published this article (better C++ code). You can downloadthe latest version: md2loader.zip.

Thanks to Squintik (squintik NOSPAM wanadoo.fr) from Game-Labwho helped me for the english version of this document.

Ressources

  • MD2 file format (Quake 2's models), Quick and short, David Henry (me).
  • OpenGL Game Programing, Ch. 18, K. Hawkins, D. Astle .
  • Focus on 3D Models, Ch. 3, Evan Pipho .
  • Game Tutorials, MD2 Loader, Ben “DigiBen” Humphrey .
  • Game Tutorials, MD2 Animation, Ben “DigiBen” Humphrey .
  • .md2 File Format Specification, Daniel E. Schoenblum .
  • Quake II source code (GPL), ID Software .
  • MD2 Viewer source code, Mete Ciragan .
  • qview source code, Mustata “LoneRunner” Bogdan .
  • Qbism Game Engine source code, Jeff Ford .
  • jawMD2 source code, Jawed Karim .

OPENGL和C++?C#? The selected opengl mode is not supported MD2世嘉好游戏,谢谢! c++.net 中如何使用opengl C++.net 下的 OpenGL编程问题 md2格式的怎么播放? The selected OPENGL mode is not snpported by ridev cnrd The selected OpenGL mode is not supported your video card The selected openGL mode is not suppored by your video card是什么意思 请教一下 The selected opengl mode isnot supported by your video card The selected opengl mode not supported by your video card的中文意思 the selected openGL mode is not suppor by your video card 为什么CS1.5无法建服务器,进游戏也出现The selected OpenGL mode is not CS里出现The selected OpenGL mode is not supported by your video card 怎么办 The selected OpenGL mode is not supported by your video card The selected openGL mode is not supported by your video card 是什么意思哦? The selected OpenGl mode is not supported by your video card.翻译成中文 The selected OpenGL mode is not supported by your video card译成中文是什么意思? The selected OpenGL mode is not supported by your video card是什么意思? The selec ted OpenGL modcis not supported by your video card. 这是什么意思??? The selected OpenGL mode is not supported by your video card是什么意思啊?帮帮我 反恐玩不了他显示The selected OpenGl mode is not supported by your video card. the selected opengl mod is not sapported by your rideo card the selected opengl mod is not sapported by your rideo card