Le format MDL (modèles de Quake)

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

Introduction

Le format MDL est le format de modèle utilisé dans Quake (juin 1996). Un fichier modèle MDL présente les caractéristiques suivantes :

Un fichier MDL peut contenir plusieurs textures.

L'extension de fichier des modèles MDL est « mdl ». Un fichier MDl 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 MDL étant un format binaire, vous aurez à gérer l'indiansime des données du modèle. Les fichiers MDL 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 MDL */
struct mdl_header_t
{
  int ident;            /* numéro magique : "IDPO" */
  int version;          /* version du format : 6 */

  vec3_t scale;         /* redimensionnement */
  vec3_t translate;     /* vecteur translation */
  float boundingradius;
  vec3_t eyeposition;   /* position des yeux */

  int num_skins;        /* nombre de textures */
  int skinwidth;        /* largeur des textures */
  int skinheight;       /* hauteur des textures */

  int num_verts;        /* nombre de sommets */
  int num_tris;         /* nombre de triangles */
  int num_frames;       /* nombre de frames */

  int synctype;         /* 0 = synchrone, 1 = aléatoire */
  int flags;            /* drapeau d'états */
  float size;
};

ident est le numéro magique du fichier. Il sert à identifier le type de fichier. ident doit être égal à 1330660425 ou à "IDPO". On peut obtenir la valeur numérique avec l'expression (('O'<<24) + ('P'<<16) + ('D'<<8) + 'I').

version est le numéro de version. Il doit être égal à 6.

scale et translate servent à obtenir les coordonnées réelles des sommets du modèle. scale est le facteur de redimensionnement et translate le vecteur de translation (ou l'origine du modèle). Il faut d'abord multiplier chaque composantes des coordonnées du sommet par la composante respective de scale, puis ajouter la composante respective de translate :

vreel[i] = (scale[i] * vertex[i]) + translate[i];

i varie de 0 à 2 (composantes x, y et z).

boundingradius est le rayon d'une sphère dans laquelle le modèle tout entier peut-être contenu (utilisé pour la détection de collision par exemple).

eyeposition est la position des yeux (s'il s'agit d'un monstre ou personnage). Vous en faites ce que vous voulez.

num_skins est le nombre de textures présentent dans le fichier. skinwidth et skinheight sont respectivement la largeur et la hauteur de la texture du modèle. Toutes les textures doivent avoir les mêmes dimensions.

num_verts est le nombre de sommets d'une frame du modèle.
num_tris est le nombre de triangles du modèle.
num_frames est le nombre de frames que possède le modèle.

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 viennent directement après l'en-tête du modèle dans le fichier. Il peut s'agir d'une texture composée d'une seule images ou d'un groupe d'images (texture animée).

/* Skin */
struct mdl_skin_t
{
  int group;      /* 0 = simple, 1 = groupe */
  GLubyte *data;  /* données texture */
};

ou :

/* groupe d'images */
struct mdl_groupskin_t
{
    int group;     /* 1 = groupe */
    int nb;        /* nombre d'images du groupe */
    float *time;   /* durées de chaque image */
    ubyte **data;  /* données texture */
};

time est un tableau de dimension nb et data est un tableau de nb tableaux de dimensions skinwidth * skinheight.

Les données des images sont contenues dans le tableau data et sont des images en mode index couleur sur 8 bits. La palette de couleur se trouve généralement dans un fichier LMP (*.lmp). Les fichiers LMP sont des fichiers binaires contenant la palette sur 768 octets (256 couleurs sur 24 bits). Ils ne contiennent rien d'autre.

Une palette de couleur est disponible sous format texte.

Il y a num_skins objets de type mdl_skin_t ou mdl_groupskin_t.

Coordonnées de texture

Les coordonnées de texture sont regroupées dans une structure et sont stockées sous forme de short :

/* Coordonnées de texture */
struct mdl_texcoord_t
{
  int onseam;
  int s;
  int t;
};

Les textures sont généralement découpées en deux parties : l'une pour le devant du modèle et l'autre pour le dos. La partie dorsale doit être décalée de skinwidth/2 par rapport à la partie frontale.

onseam indique si le sommet est situé sur la frontière entre la partie frontale et la partie dorsale du modèle (un peu comme un trait de couture).

Pour obtenir les coordonnées (s, t) réelles (sur un intervalle de 0,0 à 1,0), il faut leur ajouter 0,5 et diviser le resultat par skinwidth pour s et skinheight pour t.

Il y a num_verts couples (s, t) de coordonnées de texture dans un modèle MDL. Ces données viennent après les données de texture.

Triangles

Les triangles possèdent chacun un tableau d'indices de sommets et un drapeau permettant de savoir s'il est situé sur la face avant ou sur la face arrière du modèle.

/* Données triangle */
struct mdl_triangle_t
{
  int facesfront;  /* 0 = face arrière, 1 = face avant */
  int vertex[3];   /* indices des sommets */
};

Dans le cas où un sommet est situé sur la couture entre les deux parties et faisant partie d'un triangle de la face arrière, il faut ajouter skinwidth/2 à s pour corriger les coordonnées de texture.

Il y a num_tris triangles dans un modèle MDL. Les données des triangles suivent les données de coordonnées de texture dans le fichier.

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 et est composé de 162 vecteurs en coordonnées flottantes (3 float).

/* Données d'un sommet */
struct mdl_vertex_t
{
  unsigned char v[3];
  unsigned char normalIndex;
};

Frames

Les frames possèdent une liste de sommets et quelques autres informations spécifiques.

/* Données frame */
struct mdl_simpleframe_t
{
  struct mdl_vertex_t bboxmin; /* bouding box min */
  struct mdl_vertex_t bboxmax; /* bouding box max */
  char name[16];
  struct mdl_vertex_t *verts;  /* vertex list of the frame */
};

bboxmin et bboxmax définissent un volume dans lequel le modèle peut être entièrement contenu. name est le nom de la frame. verts est la liste des sommets de la frame.

Les frames peuvent être des frames simples ou des groupes de frames. Elles sont identifiées par une variable type qui vaut 0 pour une frame simple, et une valeur non nulle pour un groupe de frames :

/* Frame du modèle */
struct mdl_frame_t
{
  int type;                        /* 0 = simple, !0 = groupe */
  struct mdl_simpleframe_t frame;
};

ou :

/* Group of simple frames */
struct mdl_groupframe_t
{
  int type;                         /* !0 = groupe */
  struct mdl_vertex_t min;          /* position min parmi toutes les frames */
  struct mdl_vertex_t max;          /* position max parmi toutes les frames */
  float *time;                      /* durée de chaque frame */
  struct mdl_simpleframe_t *frames; /* liste des frames simples */
};

time et frames sont de dimension nb. min et max correspondent aux positions minimum et maximum parmi tous les sommets de toutes les frames. time est la durée de chaque frame.

Il y a num_frames frames dans un modèle MDL. Les données des frames suivent les données des triangles dans un fichier MDL.

Lecture d'un fichier MDL

En supposant que mdl_model_t est une structure contenant les données d'un modèle MDL, 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 MDL :

int
ReadMDLModel (const char *filename, struct mdl_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 mdl_header_t), fp);

  if ((mdl->header.ident != 1330660425) ||
      (mdl->header.version != 6))
    {
      /* Error! */
      fprintf (stderr, "Error: bad version or identifier\n");
      fclose (fp);
      return 0;
    }

  /* Memory allocations */
  mdl->skins = (struct mdl_skin_t *)
    malloc (sizeof (struct mdl_skin_t) * mdl->header.num_skins);
  mdl->texcoords = (struct mdl_texcoord_t *)
    malloc (sizeof (struct mdl_texcoord_t) * mdl->header.num_verts);
  mdl->triangles = (struct mdl_triangle_t *)
    malloc (sizeof (struct mdl_triangle_t) * mdl->header.num_tris);
  mdl->frames = (struct mdl_frame_t *)
    malloc (sizeof (struct mdl_frame_t) * mdl->header.num_frames);
  mdl->tex_id = (GLuint *)
    malloc (sizeof (GLuint) * mdl->header.num_skins);

  mdl->iskin = 0;

  /* Read texture data */
  for (i = 0; i < mdl->header.num_skins; ++i)
    {
      mdl->skins[i].data = (GLubyte *)malloc (sizeof (GLubyte)
		* mdl->header.skinwidth * mdl->header.skinheight);

      fread (&mdl->skins[i].group, sizeof (int), 1, fp);
      fread (mdl->skins[i].data, sizeof (GLubyte),
	     mdl->header.skinwidth * mdl->header.skinheight, fp);

      mdl->tex_id[i] = MakeTextureFromSkin (i, mdl);

      free (mdl->skins[i].data);
      mdl->skins[i].data = NULL;
    }

  fread (mdl->texcoords, sizeof (struct mdl_texcoord_t),
	 mdl->header.num_verts, fp);
  fread (mdl->triangles, sizeof (struct mdl_triangle_t),
	 mdl->header.num_tris, fp);

  /* Read frames */
  for (i = 0; i < mdl->header.num_frames; ++i)
    {
      /* Memory allocation for vertices of this frame */
      mdl->frames[i].frame.verts = (struct mdl_vertex_t *)
	malloc (sizeof (struct mdl_vertex_t) * mdl->header.num_verts);

      /* Read frame data */
      fread (&mdl->frames[i].type, sizeof (int), 1, fp);
      fread (&mdl->frames[i].frame.bboxmin,
	     sizeof (struct mdl_vertex_t), 1, fp);
      fread (&mdl->frames[i].frame.bboxmax,
	     sizeof (struct mdl_vertex_t), 1, fp);
      fread (mdl->frames[i].frame.name, sizeof (char), 16, fp);
      fread (mdl->frames[i].frame.verts, sizeof (struct mdl_vertex_t),
	     mdl->header.num_verts, fp);
    }

  fclose (fp);
  return 1;
}

Remarque : ce code ne peut gérer les fichiers MDL composé de groupes de frames.

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 mdl_model_t *mdl)
{
  int i, j;
  GLfloat s, t;
  vec3_t v;
  struct mdl_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[mdl->iskin]);

  /* 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)
	  {
	    pvert = &mdl->frames[n].frame.verts[mdl->triangles[i].vertex[j]];

	    /* Compute texture coordinates */
	    s = (GLfloat)mdl->texcoords[mdl->triangles[i].vertex[j]].s;
	    t = (GLfloat)mdl->texcoords[mdl->triangles[i].vertex[j]].t;

	    if (!mdl->triangles[i].facesfront &&
		mdl->texcoords[mdl->triangles[i].vertex[j]].onseam)
	      {
		s += mdl->header.skinwidth * 0.5f; /* Backface */
	      }

	    /* Scale s and t to range from 0.0 to 1.0 */
	    s = (s + 0.5) / mdl->header.skinwidth;
	    t = (t + 0.5) / mdl->header.skinheight;

	    /* Pass texture coordinates to OpenGL */
	    glTexCoord2f (s, t);

	    /* Normal vector */
	    glNormal3fv (anorms_table[pvert->normalIndex]);

	    /* Calculate real vertex position */
	    v[0] = (mdl->header.scale[0] * pvert->v[0]) + mdl->header.translate[0];
	    v[1] = (mdl->header.scale[1] * pvert->v[1]) + mdl->header.translate[1];
	    v[2] = (mdl->header.scale[2] * pvert->v[2]) + mdl->header.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 mdl_vertex_t *pvert1, *pvert2;
vec3_t v;

for (/* ... */)
  {
    pvert1 = &mdl->frames[current].frame.verts[mdl->triangles[i].vertex[j]];
    pvert2 = &mdl->frames[current + 1].frame.verts[mdl->triangles[i].vertex[j]];

    /* ... */

    v[0] = mdl->header.scale[0] * (pvert1->v[0] + interp * (pvert2->v[0] - pvert1->v[0])) + mdl->header.translate[0];
    v[1] = mdl->header.scale[1] * (pvert1->v[1] + interp * (pvert2->v[1] - pvert1->v[1])) + mdl->header.translate[1];
    v[2] = mdl->header.scale[2] * (pvert1->v[2] + interp * (pvert2->v[2] - pvert1->v[2])) + mdl->header.translate[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;
    }
}

Constantes

Quelques constantes définissant des dimensions maximales :

Code source d'exemple : mdl.c (15 Ko), anorms.h (6,7 Ko), colormap.h (4,3 Ko).

Ce document est disponible selon les termes de la licence GNU Free Documentation License (GFDL)
© David Henry – contact : tfc.duke (AT) gmail (POINT) com