Spécifications du format MD2
Date de publication : 03/12/2004 , Date de mise à jour : 15/12/2005
Par
David Henry (Autres articles)
Ce tutoriel détaille les spécifications du format de modèles MD2 (modèles de Quake II), et explique comment les charger.
Les codes sources sont en C.
1. Introduction
2. L'en-tête
3. Types de données
3.1. Vecteur
3.2. Informations de texture
3.3. Coordonnées de texture
3.4. Triangles
3.5. Sommets
3.6. Frames
3.7. Commandes OpenGL
4. Lecture d'un fichier MD2
5. Rendu du modèle
6. Animation
7. Utilisation des commandes OpenGL
8. Constantes
1. Introduction
Le format MD2 est le format de modèle introduit par id Software avec
Quake 2 en novembre 1997. C'est un format relativement simple à comprendre et à utiliser.
Les modèles MD2 possèdent les caractéristiques suivantes :
- Données géométriques du modèle (triangles) ;
- Animations par frame ;
- données structurées pour afficher le modèle avec des primitives
GL_TRIANGLE_FAN et GL_TRIANGLE_STRIP (appelées
« commandes OpenGL »).
La texture du modèle se trouve dans un fichier séparé. Un modèle MD2 ne peut avoir
qu'une seule texture à la fois.
L'extension de fichier des modèles MD2 est « md2 ». Un fichier MD2 est
un fichier binaire composé de deux parties : l'en-tête et les données. L'en-tête du
fichier apporte des informations sur les données afin de pouvoir les manipuler.
Les types de variables utilisés ici ont les tailles suivantes :
- char : 1 octet
- short : 2 octets
- int : 4 octets
- float : 4 octets
2. L'en-tête
L'en-tête (header en anglais) est contenu dans une structure située au début du
fichier :
typedef struct
{
typedef struct
{
int ident;
int version;
int skinwidth;
int skinheight;
int framesize;
int num_skins;
int num_vertices;
int num_st;
int num_tris;
int num_glcmds;
int num_frames;
int offset_skins;
int offset_st;
int offset_tris;
int offset_frames;
int offset_glcmds;
int offset_end;
} md2_header_t; |
ident est le numéro magique du fichier. Il sert à identifier le type
de fichier. ident doit être égal à 844121161 ou à "IDP2". On peut obtenir
la valeur numérique avec l'expression (('2'<<24) + ('P'<<16) +
('D'<<8) + 'I').
version est le numéro de version. Il doit être égal à 8.
skinwidth et skinheight sont respectivement la largeur
et la hauteur de la texture du modèle.
framesize est la taille en octets d'une frame entière.
num_skins est le nombre de textures associées au modèle.
num_vertices est le nombre de sommets du modèle pour une frame.
num_st est le nombre de coordonnées de texture du modèle.
num_tris est le nombre de triangles du modèle.
num_glcmds est le nombre de commandes OpenGL.
num_frames est le nombre de frames que possède le modèle.
offset_skins indique la position en octets dans le fichier du début des
données relatives aux textures.
offset_st indique le début des données des coordonnées de texture.
offset_tris indique le début des données des triangles.
offset_frames indique le début des données des frames.
offset_glcmds indique le début des données des commandes OpenGL.
offset_end indique la position de la fin du fichier.
3. Types de données
3.1. Vecteur
Le vecteur, composé de trois coordonnées flottantes (x, y, z) :
3.2. Informations de texture
Les informations de texture sont en fait la liste des noms des fichiers de texture
associés au modèle :
typedef struct
{
char name[64];
} md2_skin_t; |
3.3. Coordonnées de texture
Les coordonnées de textures sont regroupées dans une structure et sont stockées sous
forme de short. Pour obtenir les coordonnées réelles en flottant, il faut
diviser s par skinwidth et t par
skinheight :
typedef struct
{
short s;
short t;
} md2_texCoord_t; |
3.4. Triangles
Les triangles possèdent chacun un tableau d'indices de sommets et un tableau
d'indices de coordonnées de texture.
typedef struct
{
unsigned short vertex[3];
unsigned short st[3];
} md2_triangle_t; |
3.5. Sommets
Les sommets sont composés d'un tripplet de coordonnées « compressées »
stockés sur un octet par composante, et d'un index de vecteur normal. Le tableau de
normales se trouve dans le fichier anorms.h de Quake 2 et
est composé de 162 vecteurs en coordonnées flottantes (3 float).
typedef struct
{
unsigned char v[3];
unsigned char normalIndex;
} md2_vertex_t; |
3.6. Frames
Les frames possèdent des informations spécifiques à elles-même et la liste des
sommets du modèle de cette frame. Les informations servent à décompresser les sommets
pour obtenir leurs coordonnées réelles.
typedef struct
{
vec3_t scale;
vec3_t translate;
char name[16];
md2_vertex_t *verts;
} md2_frame_t; |
Pour décompresser les coordonnées des sommets, il faut multiplier chaque composante
par la composante respective de scale (redimensionnement) puis ajouter
la composante respective de translate (translation) :
vec3_t v;
md2_vertex_t vtx;
md2_frame_t frame;
v[i] = (vtx.v[i] * frame.scale[i]) + frame.translate[i]; |
3.7. Commandes OpenGL
Les commandes OpenGL se trouvent sous forme d'une liste d'entiers (int).
4. Lecture d'un fichier MD2
En supposant que md2_model_t est une structure contenant les données
d'un modèle MD2, et que *mdl est un pointeur sur une zone mémoire déjà
allouée, voici un exemple de fonction lisant les données d'un fichier MD2 :
int
ReadMD2Model (const char *filename, md2_model_t *mdl)
{
FILE *fp;
int i;
fp = fopen (filename, "rb");
if (!fp)
{
fprintf (stderr, "error: couldn't open \"%s\"!", filename);
return 0;
}
fread (&mdl->header, 1, sizeof (md2_header_t), fp);
if ((mdl->header.ident != 844121161) ||
(mdl->header.version != 8))
{
fprintf (stderr, "error: bad version!");
fclose (fp);
return 0;
}
mdl->skins = (md2_skin_t *)malloc (sizeof (md2_skin_t) * mdl->header.num_skins);
mdl->texcoords = (md2_texCoord_t *)malloc (sizeof (md2_texCoord_t) * mdl->header.num_st);
mdl->triangles = (md2_triangle_t *)malloc (sizeof (md2_triangle_t) * mdl->header.num_tris);
mdl->frames = (md2_frame_t *)malloc (sizeof(md2_frame_t) * mdl->header.num_frames);
mdl->glcmds = (int *)malloc (sizeof (int) * mdl->header.num_glcmds);
fseek (fp, mdl->header.offset_skins, SEEK_SET);
fread (mdl->skins, sizeof (md2_skin_t), mdl->header.num_skins, fp);
fseek (fp, mdl->header.offset_st, SEEK_SET);
fread (mdl->texcoords, sizeof (md2_texCoord_t), mdl->header.num_st, fp);
fseek (fp, mdl->header.offset_tris, SEEK_SET);
fread (mdl->triangles, sizeof (md2_triangle_t), mdl->header.num_tris, fp);
fseek (fp, mdl->header.offset_glcmds, SEEK_SET);
fread (mdl->glcmds, sizeof (int), mdl->header.num_glcmds, fp);
fseek (fp, mdl->header.offset_frames, SEEK_SET);
for (i = 0; i < mdl->header.num_frames; ++i)
{
mdl->frames[i].verts = (md2_vertex_t *)
malloc (sizeof (md2_vertex_t) * mdl->header.num_vertices);
fread (mdl->frames[i].scale, sizeof (vec3_t), 1, fp);
fread (mdl->frames[i].translate, sizeof (vec3_t), 1, fp);
fread (mdl->frames[i].name, sizeof (char), 16, fp);
fread (mdl->frames[i].verts, sizeof (md2_vertex_t), mdl->header.num_vertices, fp);
}
fclose (fp);
return 1;
} |
5. Rendu du modèle
Exemple de code pour le rendu d'une frame n d'un modèle mdl :
void
RenderFrame (int n, md2_model_t *mdl)
{
int i, j;
GLfloat s, t;
vec3_t v;
md2_frame_t *pframe;
md2_vertex_t *pvert;
if ((n < 0) || (n > mdl->header.num_frames - 1))
return;
glBindTexture (GL_TEXTURE_2D, mdl->tex_id);
glBegin (GL_TRIANGLES);
for (i = 0; i < mdl->header.num_tris; ++i)
{
for (j = 0; j < 3; ++j)
{
pframe = &mdl->frames[n];
pvert = &pframe->verts[mdl->triangles[i].vertex[j]];
s = (GLfloat)mdl->texcoords[mdl->triangles[i].st[j]].s / mdl->header.skinwidth;
t = (GLfloat)mdl->texcoords[mdl->triangles[i].st[j]].t / mdl->header.skinheight;
glTexCoord2f (s, t);
glNormal3fv (anorms_table[pvert->normalIndex]);
v[0] = (pframe->scale[0] * pvert->v[0]) + pframe->translate[0];
v[1] = (pframe->scale[1] * pvert->v[1]) + pframe->translate[1];
v[2] = (pframe->scale[2] * pvert->v[2]) + pframe->translate[2];
glVertex3fv (v);
}
}
glEnd ();
} |
6. Animation
L'animation du modèle se fait par frame. Une frame est une séquence d'une animation.
Pour éviter les saccades, on procède à une interpolation linéaire entre les coordonnées
du sommet de la frame actuelle et celles de la frame suivante (de même pour le vecteur
normal) :
md2_frame_t *pframe1, *pframe2;
md2_vertex_t *pvert1, *pvert2;
vec3_t v_curr, v_next, v;
for ()
{
pframe1 = &mdl->frames[current];
pframe2 = &mdl->frames[current + 1];
pvert1 = &pframe1->verts[mdl->triangles[i].vertex[j]];
pvert2 = &pframe2->verts[mdl->triangles[i].vertex[j]];
v_curr[0] = (pframe1->scale[0] * pvert1->v[0]) + pframe1->translate[0];
v_curr[1] = (pframe1->scale[1] * pvert1->v[1]) + pframe1->translate[1];
v_curr[2] = (pframe1->scale[2] * pvert1->v[2]) + pframe1->translate[2];
v_next[0] = (pframe2->scale[0] * pvert2->v[0]) + pframe2->translate[0];
v_next[1] = (pframe2->scale[1] * pvert2->v[1]) + pframe2->translate[1];
v_next[2] = (pframe2->scale[2] * pvert2->v[2]) + pframe2->translate[2];
v[0] = v_curr[0] + interp * (v_next[0] - v_curr[0]);
v[1] = v_curr[1] + interp * (v_next[1] - v_curr[1]);
v[2] = v_curr[2] + interp * (v_next[2] - v_curr[2]);
} |
v est le sommet final à dessiner. interp est le pourcentage
d'interpolation entre les deux frames. C'est un float compris entre 0,0 et 1,0.
Lorsqu'il vaut 1,0, actuel est incrémenté de 1 et interp est
réinitialisé à 0,0. Il est inutile d'interpoler les coordonnées de texture, car ce sont
les même pour les deux frames. Il est préférable que interp soit fonction du
nombre d'images par seconde sorti par le programme.
void
Animate (int start, int end, int *frame, float *interp)
{
if ((*frame < start) || (*frame > end))
*frame = start;
if (*interp >= 1.0f)
{
*interp = 0.0f;
(*frame)++;
if (*frame >= end)
*frame = start;
}
} |
7. Utilisation des commandes OpenGL
Les commandes OpenGL sont des données structurées de façon à pouvoir dessiner le modèle
uniquement avec les primitives GL_TRIANGLE_FAN et GL_TRIANGLE_STRIP.
C'est une liste d'entiers (int) qui se lit par packets :
- Le premier entier est le nombre de sommets à dessiner pour une nouvelle primitive.
S'il est positif, il s'agit d'une primitive GL_TRIANGLE_STRIP, s'il est négatif,
c'est une primitive GL_TRIANGLE_FAN.
- Les entiers suivant se prennent par paquet de 3 pour autant de sommets qu'il y a à
dessiner. Les deux premiers sont les coordonnées de texture en flottant et le troisième
est l'index du sommet à dessiner.
- Lorsque le nombre de sommets est 0, alors on a fini de dessiner le modèle.
On peut modéliser ces packets par une structure :
typedef struct
{
float s;
float t;
int index;
} md2_glcmd_t; |
L'intérêt de cette méthode est qu'on gagne en temps d'exécution car on ne dessine plus des
primitives GL_TRIANGLES et on ne calcule plus les coordonnées de texture (plus
besoin de diviser par skinwidth et skinheight). Voici un exemple
d'utilisation :
void
RenderFrameWithGLCmds (int n, md2_model_t *mdl)
{
int i, *pglcmds;
vec3_t v;
md2_frame_t *pframe;
md2_vertex_t *pvert;
md2_glcmd_t *packet;
if ((n < 0) || (n > mdl->header.num_frames - 1))
return;
glBindTexture (GL_TEXTURE_2D, mdl->tex_id);
pglcmds = mdl->glcmds;
while ((i = *(pglcmds++)) != 0)
{
if (i < 0)
{
glBegin (GL_TRIANGLE_FAN);
i = -i;
}
else
{
glBegin (GL_TRIANGLE_STRIP);
}
for (; i > 0; --i, pglcmds += 3)
{
packet = (md2_glcmd_t *)pglcmds;
pframe = &mdl->frames[n];
pvert = &pframe->verts[packet->index];
glTexCoord2f (packet->s, packet->t);
glNormal3fv (anorms_table[pvert->normalIndex]);
v[0] = (pframe->scale[0] * pvert->v[0]) + pframe->translate[0];
v[1] = (pframe->scale[1] * pvert->v[1]) + pframe->translate[1];
v[2] = (pframe->scale[2] * pvert->v[2]) + pframe->translate[2];
glVertex3fv (v);
}
glEnd ();
}
} |
8. Constantes
Quelques constantes définissant des dimensions maximales :
- Nombre maximum de triangles : 4096
- Nombre maximum de sommets : 2048
- Nombre maximum de coordonnées de texture : 2048
- Nombre maximum de frames : 512
- Nombre maximum de skins : 32
- Nombre de normales précalculées : 162
Code source d'exemple : md2.c (14,3 Ko),
anorms.h (6,7 Ko). Pas d'application de texture.
 
Ce document est disponible selon les termes de la licence
GNU Free Documentation License (GFDL)
© David Henry - contact : tfc_duke (AT) club-internet (DOT) fr
|