Spécifications du format MDL (modèles de Quake)

Écrit le 27/12/2004 par DukeNukem
Dernière mise à jour : 06/02/2006

Introduction

Le format MDL est le format de modèle utilisé dans Quake (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

Les types de variables utilisés ici ont les tailles suivantes :

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 */
typedef struct
{
    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;

} mdl_header_t;

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 composante 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ésentes 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 image ou d'un groupe d'images (texture animée).

/* texture simple */
typedef struct
{
    int     group;   /* 0 = simple */
    ubyte   *data;   /* données texture */

} mdl_skin_t;

ou :

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

} mdl_groupskin_t;

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 */
typedef struct
{
    int      onseam;
    int      s;
    int      t;

} mdl_texCoord_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 résultat 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 */
typedef struct
{
    int     facesfront;     /* 0 = face arrière, 1 = face avant */
    int     vertex[3];      /* indices des sommets */

} mdl_triangle_t;

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 triplet 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 sommet */
typedef struct
{
     unsigned char   v[3];
     unsigned char   normalIndex;

} mdl_vertex_t;

Frames

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

/* données frame */
typedef struct
{
    mdl_vertex_t    bboxmin;
    mdl_vertex_t    bboxmax;
    char            name[16];
    mdl_vertex_t    *verts;     /* liste des sommets de la frame */

} mdl_simpleframe_t;

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 :

typedef struct
{
    int                 type;   /* 0 = simple */
    mdl_simpleframe_t   frame;

} mdl_frame_t;

ou :

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

} mdl_groupframe_t;

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( char *filename, mdl_model_t *mdl )
{
    FILE *fp;
    int i;

    fp = fopen( filename, "rb" );
    if( !fp )
        return 0;

    /* lecture de l'en-tête */
    fread( &mdl->header, 1, sizeof( mdl_header_t ), fp );

    if( (mdl->header.ident != 1330660425) || (mdl->header.version != 6) ) {
        /* erreur ! */
        fclose( fp );
        return 0;
    }

    /* allocation de mémoire */
    mdl->skins = (mdl_skin_t *)malloc( sizeof( mdl_skin_t ) * mdl->header.num_skins );
    mdl->texcoords = (mdl_texCoord_t *)malloc( sizeof( mdl_texCoord_t ) * mdl->header.num_verts );
    mdl->triangles = (mdl_triangle_t *)malloc( sizeof( mdl_triangle_t ) * mdl->header.num_tris );
    mdl->frames = (mdl_frame_t *)malloc( sizeof( mdl_frame_t ) * mdl->header.num_frames );
    mdl->tex_id = (GLuint *)malloc( sizeof( GLuint ) * mdl->header.num_skins );

    mdl->iskin = 0;

    /* lecture des textures */
    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] = MakeTexture( i, mdl );

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

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

    /* lecture des frames */
    for( i = 0; i < mdl->header.num_frames; i++ )
    {
        /* allocation de mémoire pour les sommets de cette frame */
        mdl->frames[i].frame.verts = (mdl_vertex_t *)malloc( sizeof( mdl_vertex_t ) * mdl->header.num_verts );

        /* lecture des données de la frame */
        fread( &mdl->frames[i].type, sizeof( long ), 1, fp );
        fread( &mdl->frames[i].frame.bboxmin, sizeof( mdl_vertex_t ), 1, fp );
        fread( &mdl->frames[i].frame.bboxmax, sizeof( mdl_vertex_t ), 1, fp );
        fread( mdl->frames[i].frame.name, sizeof( char ), 16, fp );
        fread( mdl->frames[i].frame.verts, sizeof( 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, mdl_model_t *mdl )
{
    int i, j;
    GLfloat s, t;
    vec3_t v;
    mdl_vertex_t *pvert;

    /* vérifie que n est valide */
    if( (n < 0) || (n > mdl->header.num_frames - 1) )
        return;

    /* active la texture du modèle */
    glBindTexture( GL_TEXTURE_2D, mdl->tex_id[ mdl->iskin ] );

    /* dessine le modèle */
    glBegin( GL_TRIANGLES );
        /* dessine chaque triangle */
        for( i = 0; i < mdl->header.num_tris; i++ )
        {
            /* dessine chaque sommet */
            for( j = 0; j < 3; j++ )
            {
                pvert = &mdl->frames[n].frame.verts[ mdl->triangles[i].vertex[j] ];

                /* calcule les coordonnées de texture réelles */
                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 / 2;  /* face arrière */

                /* s et t sont ramenés à une valeur comprise entre 0,0 et 1,0 */
                s = (s + 0.5) / mdl->header.skinwidth;
                t = (t + 0.5) / mdl->header.skinheight;

                /* application de la texture */
                glTexCoord2f( s, t );

                /* vecteur normal */
                glNormal3fv( anorms_table[ pvert->normalIndex ] );

                /* calcul de la position réelle du sommet */
                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) :

mdl_vertex_t *pvert1, *pvert2;
vec3_t v;

for( /* ... */ )
{
    pvert1 = &mdl->frames[ actuel ].frame.verts[ mdl->triangles[i].vertex[j] ];
    pvert2 = &mdl->frames[ actuel + 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êmes 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 *actuel, float *interp )
{
    if( (*actuel < start) || (*actuel > end) )
        *actuel = start;

    if( *interp >= 1.0f )
    {
        *interp = 0.0f;
        (*actuel)++;

        if( *actuel >= end )
            *actuel = start;
    }
}

Constantes

Quelques constantes définissant des dimensions maximales :

Source code exemple : mdl.c

Ce document est disponible selon les termes de la licence GNU Free Documentation License (GFDL)
© David Henry – contact : tfc_duke (AT) club-internet (POINT) fr
Source : http://tfc.duke.free.fr/coding/mdl-specs-fr.html