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.

En-tête
Données

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 :

 
Sélectionnez
/* header md2 */
typedef struct
{
typedef struct
{
  int ident;          /* magic number: "IDP2" */
  int version;        /* version: must be 8 */

  int skinwidth;      /* texture width */
  int skinheight;     /* texture height */

  int framesize;      /* size in bytes of a frame */

  int num_skins;      /* number of skins */
  int num_vertices;   /* number of vertices per frame */
  int num_st;         /* number of texture coordinates */
  int num_tris;       /* number of triangles */
  int num_glcmds;     /* number of opengl commands */
  int num_frames;     /* number of frames */

  int offset_skins;   /* offset skin data */
  int offset_st;      /* offset texture coordinate data */
  int offset_tris;    /* offset triangle data */
  int offset_frames;  /* offset frame data */
  int offset_glcmds;  /* offset OpenGL command data */
  int offset_end;     /* offset end of file */

} 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) :

 
Sélectionnez
/* vecteur */
typedef float vec3_t[3];

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 :

 
Sélectionnez
/* texture */
typedef struct
{
  char name[64];   /* nom du fichier texture */

} 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 :

 
Sélectionnez
/* coordonnées de texture */
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.

 
Sélectionnez
/* données triangle */
typedef struct
{
  unsigned short vertex[3];   /* indices vertices du triangle */
  unsigned short st[3];       /* indices coordonnées de texture */

} 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).

 
Sélectionnez
/* données sommet */
typedef struct
{
  unsigned char v[3];         /* position dans l'espace (relative au modèle) */
  unsigned char normalIndex;  /* index normale du vertex */

} 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.

 
Sélectionnez
/* données frame */
typedef struct
{
  vec3_t          scale;      /* redimensionnement */
  vec3_t          translate;  /* vecteur translation */
  char            name[16];   /* nom de la frame */
  md2_vertex_t    *verts;     /* liste de vertices */

} 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) :

 
Sélectionnez
vec3_t v;           /* sommet réel */
md2_vertex_t vtx;   /* sommet compressé */
md2_frame_t frame;  /* une frame du modèle */

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 :

 
Sélectionnez
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;
    }

  /* read header */
  fread (&mdl->header, 1, sizeof (md2_header_t), fp);

  if ((mdl->header.ident != 844121161) ||
      (mdl->header.version != 8))
    {
      /* error! */
      fprintf (stderr, "error: bad version!");
      fclose (fp);
      return 0;
    }

  /* memory allocation */
  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);

  /* read model data */
  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);

  /* 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 = (md2_vertex_t *)
	malloc (sizeof (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 (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 :

 
Sélectionnez
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;

  /* 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 ();
}

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) :

 
Sélectionnez
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.

 
Sélectionnez
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;
    }
}

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 :

 
Sélectionnez
typedef struct
{
  float   s;          /* coordonnée de texture s */
  float   t;          /* coordonnée de texture t */
  int     index;      /* index vertex */

} 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 :

 
Sélectionnez
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;

  /* 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 = (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 ();
    }
}

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.

Version PDF : télécharger (41.7 Ko)