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

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

Introduction

Le format MD2 est le format de modèle introduit par ID Software avec Quake 2 en 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

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 :

/* header md2 */
typedef struct
{
    int ident;          /* numéro magique : "IDP2" */
    int version;        /* version du format : 8 */

    int skinwidth;      /* largeur texture */
    int skinheight;     /* hauteur texture */

    int framesize;      /* taille d'une frame en octets */

    int num_skins;      /* nombre de skins */
    int num_vertices;   /* nombre de vertices 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 skins */
    int offset_st;      /* offset données coordonnées de texture */
    int offset_tris;    /* offset données triangles */
    int offset_frames;  /* offset données frames */
    int offset_glcmds;  /* offset données commandes OpenGL */
    int offset_end;     /* offset fin de fichier */

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

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 :

/* texture */
typedef struct
{
    char name[68];   /* nom du fichier texture */

} md2_skin_t;

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

} md2_texCoord_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 triangle */
typedef struct
{
    unsigned short vertex[3];   /* indices vertices du triangle */
    unsigned short st[3];       /* indices coordonnées de texture */

} md2_triangle_t;

Sommets

Les sommets sont composés d'un triplet de coordonnées « compressées » stockées 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 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;

Frames

Les frames possèdent des informations spécifiques à elles-mêmes 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.

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

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

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( char *filename, md2_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( md2_header_t ), fp );

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

    /* allocation de mémoire */
    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 );

    /* lecture des données */
    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 );

    /* lecture des frames */
    fseek( fp, mdl->header.offset_frames, SEEK_SET );
    for( i = 0; i < mdl->header.num_frames; i++ )
    {
        /* allocation de mémoire pour les vertices de cette frame */
        mdl->frames[i].verts = (md2_vertex_t *)malloc( sizeof( md2_vertex_t ) * mdl->header.num_vertices );

        /* lecture des données de la frame */
        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;
}

Rendu du modèle

Exemple de code pour le rendu d'une frame n d'un modèle mdl :

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;

    /* vérification de la validité de n */
    if( (n < 0) || (n > mdl->header.num_frames - 1) )
        return;

    /* activation de la texture du modèle */
    glBindTexture( GL_TEXTURE_2D, mdl->tex_id );

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

                /* coordonnées de texture */
                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;

                /* application des coordonnées de texture */
                glTexCoord2f( s, t );

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

                /* calcul de la position relative du sommet au modèle */
                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) :

md2_frame_t *pframe1, *pframe2;
md2_vertex_t *pvert1, *pvert2;
vec3_t v_curr, v_next, v;

for( /* ... */ )
{
    pframe1 = &mdl->frames[ actuel ];
    pframe2 = &mdl->frames[ actuel + 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 *actuel, float *interp )
{
    if( (*actuel < start) || (*actuel > end) )
        *actuel = start;

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

        if( *actuel >= end )
            *actuel = 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 :

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 :

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;

    /* vérification de la validité de n */
    if( (n < 0) || (n > mdl->header.num_frames - 1) )
        return;

    /* activation de la texture du modèle */
    glBindTexture( GL_TEXTURE_2D, mdl->tex_id );

    pglcmds = mdl->glcmds;

    /* dessin du modèle */
    while( (i = *(pglcmds++)) != 0 )
    {
        if( i < 0 )
        {
            glBegin( GL_TRIANGLE_FAN );
            i = -i;
        }
        else
        {
            glBegin( GL_TRIANGLE_STRIP );
        }

        /* dessigne chaque vertex du groupe */
        for( /* rien */; i > 0; i--, pglcmds += 3 )
        {
            packet = (md2_glcmd_t *)pglcmds;
            pframe = &mdl->frames[n];
            pvert = &pframe->verts[ packet->index ];

            /* application des coordonnées de texture */
            glTexCoord2f( packet->s, packet->t );

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

            /* calcul de la position relative du sommet au modèle */
            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 :

Source code exemple : md2.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/md2-specs-fr.html