Le format MD2
(modèles de Quake 2)

Écrit par David Henry, le 3 décembre 2004

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 :

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

Tailles des variables

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.

Gestion de l'indianisme

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

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.

Types de données

Vecteur

Le vecteur, composé de trois coordonnées flottantes (x, y, z) :

/* Vecteur */
typedef float vec3_t[3];

Informations de texture

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

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 :

/* Coordonnées de texture */
struct md2_texCoord_t
{
  short s;
  short t;
};

Triangles

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

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

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

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.

/* 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];

Commandes OpenGL

Les commandes OpenGL se trouvent sous forme d'une liste d'entiers (int).

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, 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;
}

Rendu du modèle

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

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

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

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 :

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

Constantes

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