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 :
/* 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) :
/* 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 :
/* 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 :
/* 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.
/* 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).
/* 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.
/* 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];
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 :
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 :
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) :
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.
void
Animate (int
start, int
end, int
*
frame, float
*
interp)
{
if
((*
frame <
start) ||
(*
frame >
end))
*
frame =
start;
if
(*
interp >=
1.0
f)
{
/* move to next frame */
*
interp =
0.0
f;
(*
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 :
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;
/* 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)