Écrit par David Henry, le 3 décembre 2004
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 :
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 :
Ils correspondent aux types du langage C sur architecture x86. Veillez à vérifier les dimensions des types de variables si vous comptez exécuter votre programme sur une autre architecture.
Le format de fichier MD2 étant un format binaire, vous aurez à gérer l'indiansime des données du modèle. Les fichiers MD2 sont sockés en little-endian (x86). Si vous comptez cibler une architecture big-endian (PowerPC, SPARC, ...), ou simplement voulez garder votre programme portable, vous aurez à effectuer les conversions nécessaires pour chaque mot ou double mot lu depuis le fichier.
L'en-tête (header en anglais) est contenu dans une structure située au début du fichier :
/* En-tête MD2 */ struct md2_header_t { int ident; /* Numéro magique : "IDP2" */ int version; /* Version du format : 8 */ int skinwidth; /* Largeur de la texture */ int skinheight; /* Hauteur de la texture */ int framesize; /* Taille d'une frame en octets */ int num_skins; /* Nombre de skins (ou textures) */ int num_vertices; /* Nombre de sommets par frame */ int num_st; /* Nombre de coordonnées de texture */ int num_tris; /* Nombre de triangles */ int num_glcmds; /* Nombre de commandes OpenGL */ int num_frames; /* Nombre de frames */ int offset_skins; /* Offset données des skins */ int offset_st; /* Offset données des coordonnées de texture */ int offset_tris; /* Offset données des triangles */ int offset_frames; /* Offset données des frames */ int offset_glcmds; /* Offset données des commandes OpenGL */ int offset_end; /* Offset fin de fichier */ };
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.
Le vecteur, composé de trois coordonnées flottantes (x, y, z) :
/* Vecteur */ typedef float vec3_t[3];
Les informations de texture sont en fait la liste des noms des fichiers de texture associés au modèle :
/* Nom de texture */ struct md2_skin_t { char name[64]; /* nom du fichier 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
:
/* Coordonnées de texture */ struct md2_texCoord_t { short s; short t; };
Les triangles possèdent chacun un tableau d'indices de sommets et un tableau d'indices de coordonnées de texture.
/* Données d'un triangle */ struct md2_triangle_t { unsigned short vertex[3]; /* indices sommets du triangle */ unsigned short st[3]; /* indices coordonnées de texture */ };
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).
/* Données d'un sommet */ struct md2_vertex_t { unsigned char v[3]; /* position compressée, en espace objet */ unsigned char normalIndex; /* index vecteur normal du sommet */ };
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.
/* Model frame */ struct md2_frame_t { vec3_t scale; /* facteur de redimensionnement */ vec3_t translate; /* vecteur translation */ char name[16]; /* nom de la frame */ struct md2_vertex_t *verts; /* liste des sommets de la frame */ };
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; /* position réelle du sommet */ struct md2_vertex_t vtx; /* sommet compressé */ struct md2_frame_t frame; /* une frame du modèle */ v[i] = (vtx.v[i] * frame.scale[i]) + frame.translate[i];
Les commandes OpenGL se trouvent sous forme d'une liste d'entiers (int).
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, struct md2_model_t *mdl) { FILE *fp; int i; fp = fopen (filename, "rb"); if (!fp) { fprintf (stderr, "Error: couldn't open \"%s\"!\n", filename); return 0; } /* Read header */ fread (&mdl->header, 1, sizeof (struct md2_header_t), fp); if ((mdl->header.ident != 844121161) || (mdl->header.version != 8)) { /* Error! */ fprintf (stderr, "Error: bad version or identifier\n"); fclose (fp); return 0; } /* Memory allocations */ mdl->skins = (struct md2_skin_t *) malloc (sizeof (struct md2_skin_t) * mdl->header.num_skins); mdl->texcoords = (struct md2_texCoord_t *) malloc (sizeof (struct md2_texCoord_t) * mdl->header.num_st); mdl->triangles = (struct md2_triangle_t *) malloc (sizeof (struct md2_triangle_t) * mdl->header.num_tris); mdl->frames = (struct md2_frame_t *) malloc (sizeof (struct md2_frame_t) * mdl->header.num_frames); mdl->glcmds = (int *)malloc (sizeof (int) * mdl->header.num_glcmds); /* Read model data */ fseek (fp, mdl->header.offset_skins, SEEK_SET); fread (mdl->skins, sizeof (struct md2_skin_t), mdl->header.num_skins, fp); fseek (fp, mdl->header.offset_st, SEEK_SET); fread (mdl->texcoords, sizeof (struct md2_texCoord_t), mdl->header.num_st, fp); fseek (fp, mdl->header.offset_tris, SEEK_SET); fread (mdl->triangles, sizeof (struct 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); /* Read frames */ fseek (fp, mdl->header.offset_frames, SEEK_SET); for (i = 0; i < mdl->header.num_frames; ++i) { /* Memory allocation for vertices of this frame */ mdl->frames[i].verts = (struct md2_vertex_t *) malloc (sizeof (struct md2_vertex_t) * mdl->header.num_vertices); /* Read frame data */ 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 (struct md2_vertex_t), mdl->header.num_vertices, fp); } fclose (fp); return 1; }
Exemple de code pour le rendu d'une frame n
d'un modèle mdl
:
void RenderFrame (int n, const struct md2_model_t *mdl) { int i, j; GLfloat s, t; vec3_t v; struct md2_frame_t *pframe; struct md2_vertex_t *pvert; /* Check if n is in a valid range */ if ((n < 0) || (n > mdl->header.num_frames - 1)) return; /* Enable model's texture */ glBindTexture (GL_TEXTURE_2D, mdl->tex_id); /* Draw the model */ glBegin (GL_TRIANGLES); /* Draw each triangle */ for (i = 0; i < mdl->header.num_tris; ++i) { /* Draw each vertex */ for (j = 0; j < 3; ++j) { pframe = &mdl->frames[n]; pvert = &pframe->verts[mdl->triangles[i].vertex[j]]; /* Compute texture coordinates */ 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; /* Pass texture coordinates to OpenGL */ glTexCoord2f (s, t); /* Normal vector */ glNormal3fv (anorms_table[pvert->normalIndex]); /* Calculate vertex real position */ 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 (); }
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) :
struct md2_frame_t *pframe1, *pframe2; struct 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) { /* Move to next frame */ *interp = 0.0f; (*frame)++; if (*frame >= end) *frame = start; } }
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 :
GL_TRIANGLE_STRIP
, s'il est négatif,
c'est une primitive GL_TRIANGLE_FAN
.On peut modéliser ces packets par une structure :
/* GL command packet */ struct md2_glcmd_t { float s; /* coordonnée de texture s */ float t; /* coordonnée de texture t */ int index; /* index du sommet */ };
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, const struct md2_model_t *mdl) { int i, *pglcmds; vec3_t v; struct md2_frame_t *pframe; struct md2_vertex_t *pvert; struct md2_glcmd_t *packet; /* Check if n is in a valid range */ if ((n < 0) || (n > mdl->header.num_frames - 1)) return; /* Enable model's texture */ glBindTexture (GL_TEXTURE_2D, mdl->tex_id); /* pglcmds points at the start of the command list */ pglcmds = mdl->glcmds; /* Draw the model */ while ((i = *(pglcmds++)) != 0) { if (i < 0) { glBegin (GL_TRIANGLE_FAN); i = -i; } else { glBegin (GL_TRIANGLE_STRIP); } /* Draw each vertex of this group */ for (/* Nothing */; i > 0; --i, pglcmds += 3) { packet = (struct md2_glcmd_t *)pglcmds; pframe = &mdl->frames[n]; pvert = &pframe->verts[packet->index]; /* Pass texture coordinates to OpenGL */ glTexCoord2f (packet->s, packet->t); /* Normal vector */ glNormal3fv (anorms_table[pvert->normalIndex]); /* Calculate vertex real position */ 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 (); } }
Quelques constantes définissant des dimensions maximales :
Code source d'exemple : md2.c (16 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)
gmail (POINT) com