Modèles MD5 - Chargement & Animation

05 Oct 2004 | Programming

Introduction

Le sujet de cette documentation est le format de modèles MD5 version 10(et non l'algorithme de vérification d'intégrité), crée pour le moteur de Doom 3.

Ce format utilise l'animation par 'joints'(='bones'), ce qui le rend plus flexible (mais également un peu plus compliqué) que le MD3(Quake 3).

Les 'joints' forment la partie centrale de l'animation d'un modèle. C'est eux qui définissent la position de la peau (les vertices du modèle qui seront ensuite rendus). Chaque 'vertice' du modèle est influencé par plusieurs poids, qui définissent l'influence qu'un 'joint' a sur tel ou tel 'vertice'.

Une hiérarchie de 'bones' est également définie, c'est à dire que chaque 'joint' a un parent et sa position est liée à celle de son parent. (Si vous bougez votre bras, votre poignet va également bouger)

Il faut également savoir que les MD5 utilisent les quaternions. Nous ne discuterons pas en détail des quaternions ici. Cette page contient une FAQ sur les quaternions.

Le format MD5 utilise des accolades pour délimiter les différentes section et des espaces pour séparer les informations.

Le code source accompagnant ce tutorial est disponible ici. Il contient une Makefile linux, utilise SDL et SDL_image et devrait donc normalement compiler sans problème sur les autres plateformes.

Du format .md5mesh

Survol

Le fichier .md5mesh contient des listes de 'vertices', de triangles, de poids, de 'bones'. Nous l'examinerons en détail ici.
La première ligne nous donne des informations sur la version du format. Nous somme uniquement intéressés par la version 10, la version 6, celle utilisé dans l'alpha de Doom 3, étant trè;s différente.
La deuxiè;me ligne donne, apparement, des informations sur différentes choses, comme les noms des 'bones' qui peuvent être détachés du mesh ou qui éjecte des douilles. Nous n'en tiendrons pas compte.
La ligne 'numJoints' nous donne le nombre de 'joints' que contient le modè;le.
Enfin, le nombre de mesh(list de 'vertices', triangles et poids) que contient le modèle.

MD5Version 10
commandline (...) numJoints 72
numMeshes 3

Ensuite commencent les informations sur les 'joints'. Chaque 'joints' a un parent (c'est le premier nombre. Un -1 indique que le 'joint' n'a pas de parent). Les 3 nombres suivants entre parenthèses donnent la position du 'joint' (en coordonnées objets). Enfin, les 3 derniers nombres entre parenthèses nous donnent les composantes x,y,z du quaternion. La partie r (parfois nommé w) du quaternion est calculée par la formule :

float t = 1.0f-(x*x)-(y*y)-(z*z);
r = (t < 0.0f) ? 0.0f : -(float)sqrt(t);

Voilà à quoi ressemble une ligne d'information sur les 'joints' :

joints {
"origin" -1 ( 0 0 0 ) ( -0.5 -0.5 -0.5 )//

Ensuite, il y a autant de sections 'mesh' que le 'numMeshes' du début. C'est là que sont décrit les meshes:
Tout d'abord, le 'shader' du mesh, c'est à dire les textures qu'il utilise (dans doom3, beaucoup de textures sont utilisés pour notamment le bump-mapping. Elle ont toutes le même nom, avec des préfixes (_d, _local, ...) différents). Nous en déduirons plus tard le nom de la texture a utiliser.
Ensuite, le nombre de 'vertices' du mesh est indiqué, puis chaque 'vertice' est décrit :
Le premier nombre donne simplement le numéro du vertice.
Ensuite, entre parenthèses, vienent les coordonnées de textures du 'vertice'.
L'avant-dernier nombre indique l'index dans le tableau des poids du poids qui influence ce vertice.
Enfin, le dernier nombre donne le nombre de poids qui ont une influence sur ce vertice(ces poids sont obligatoirement consécutifs dans le tableau des poids).

mesh { // meshes: tounge shader "models/monsters/guardian/tongue" numverts 34 vert 0 ( 0 0.7843043804 ) 0 2

Viennent ensuite des informations sur les triangles:
Tout d'abord, le nombre de triangles (numtris).
Puis, chaque triangle est décrit individuellement :
Le premier nombre est le numéro du triangle.
Les trois nombres suivants sont les indices des 'vertices' qui composent la face.

numtris 58
tri 0 2 1 0

Enfin, des informations sur les poids:
Tout d'abord, le nombre de poids, puis chaque poids individuellement. Ensuite, l'index du bone duquel dépend le poids.
Puis, le bias, qui définit la 'force' d'un poids.
Enfin, les 3 nombres entre parenthèses donnent la position du poid.(qui sera mixé avec les positions des autres poids pour donner la position d'un vertice)

numweights 83
weight 0 57 0.499899447 ( -0.0000004443 6.0386610031 2.2817306519 )

Structures utilisées

Nous discuterons ici les différentes structures c/c++ utilisées pour représenter les données du .md5mesh.
Tout d'abord, il y a deux structures qui ne sont pas directement en rapport avec le MD5, 'Face' et 'Vertice' :

struct Face
{
	unsigned int pIndex[3];
	Plane plane;
	bool visible;
};

pIndex est un tableau qui donne les index des trois vertice qui composent la face.
Plane n'est pas utilisé pour l'instant.
visible n'est pas utilisé non plus pour l'instant.

struct Vertice
{
	Vector3 vPosition;
	float s, t;
	Vector3 sTangent, tTangent;
	Vector3 vNormal;
	Vector3 vTangentSpaceLight;
};

vPosition est la position du 'vertice'.
s et t sont les coordonnées de textures.
Le reste n'est d'aucune utilité pour l'instant.

struct _MD5Joint
{
	std::string sName;
	int iNumber;
	int iParent;
	Vector3 vPosition;
	Quaternion qOrientation;
	
	_MD5Joint* pParent;
	_MD5Joint** pChildrens;
	int iNumChildrens;
};

sName est le nom du 'joint'.
iNumber est le numéro du 'joint'.
iParent est le numéro du 'joint' parent.
vPosition est la position du 'joint'.
qOrientation est le quaternion représentant l'orientation(rotation) du 'joint'.
pParent est un pointeur sur le 'joint' parent.
pChildrens est un tableau de iNumChildrens pointeurs sur les 'joints' enfants(qui ont pour parent ce 'joint'-ci.

struct _MD5Mesh
{
	int iNumVerts;
	_MD5Vert* pVerts;
	
	int iNumWeights;
	_MD5Weight* pWeights;
	
	int iNumTris;
	_MD5Triangle* pTriangles;
	
	unsigned int* pIndexes;
	
	Material* pMaterial;
	std::string sColor;
	std::string sNormal;
};

pVerts est un tableau de iNumVerts structures _MD5Vert qui stockent des informations sur les 'vertices' du modè;le.
pWeights est un tableau de iNumWeights structures _MD5Weight qui stockent des informations sur les poids du modè;le.
pTriangles est un tableau de iNumTris structures _MD5Triangle qui stockent des informations sur les triangles du modè;le.
pIndexes est une copie des index pIndex des différent triangles, simplement pour pouvoir le passer en argument à glDrawElements.
Material est un pointeur sur le material du mesh.
sColor et sNormal sont les noms des textures de couleur et de normales.

struct _MD5Vert
{
	int iWeightIndex;
	int iNumWeights;
	float pTexCoords[2];
};

iWeightIndex est l'index dans le tableau des poids du premier poid dont dépend le 'vertice'.
iNumWeights est le nombre de poids qui ont une influence sur le 'vertice'. Ils sont consécutifs au premier (donné par iWeighIndex).

struct _MD5Weight
{
	int iBoneIndex;
	float fBias;
	Vector3 vWeights;
};

iBoneIndex est l'index dans le tableau des 'bones' du 'bone' duquel dépend ce poids.
fBias donne la force du poids. (un poids avec plus de force 'attire' les 'vertices' vers lui)
vWeights donne la position du poids.

Du format .md5anim

Survol

Le fichier .md5anim contient des informations sur une animation disponible pour un modè;le. Le MD5 utilisant des 'bones', les seules (ou presque) informations nécessaires pour une animation sont celles concernant les mouvements (translation et rotation) des 'bone' pendant ladite animation.
C'est pourquoi le fichier .md5anim contient les informations sur les composantes des positions et rotations qui changent.

MD5Version 10 commandline (...) numFrames 201 numJoints 72 frameRate 24 numAnimatedComponents 197

Comme dans le fichier .md5mesh, on a une information sur la version et une 'commandline' qui ne nous sera pas d'une grande utilité.
Suivent des choses plus intéressantes, à savoir le nombre d'images ('frames') que contient notre animation (numFrames), le nombre de 'bones' que contient notre animation (numJoints), le nombre d'image par seconde de l'animation (frameRate) et enfin, le nombre de composantes qui changent chaque frame (numAnimatedComponents, qui sera expliqué plus tard).
Notez que le nombre ainsi que la hiérarchie de l'animation et du fichier .md5mesh sont les mêmes et qu'à priori, ceci est valable pour tout les modèles de Doom3. Nous ignorerons donc les informations sur le nombre de joints et la hiérarchie du fichier .md5anim et utiliseront uniquement celles du fichier .md5mesh.

hierarchy {
     "origin"        -1 0 0  //
     "Body"  0 63 0  // origin ( Tx Ty Tz Qx Qy Qz )

La seule utilitée de la section 'hierarchy' et de nous informer sur quelles composantes de chaque 'joint' sont modifiées pendant l'animation. Cela est indiqué par le deuxième nombre, qui doit être utilisé comme un 'flag'. On peut les 'décrypter' avec la 'formule' suivante :
si 'flags & 1' est vrai, la position sur les X change
si 'flags & 2' est vrai, la position sur les Y change
si 'flags & 4' est vrai, la position sur les Z change
si 'flags & 8' est vrai, la composante X du quaternion de rotation change
si 'flags & 16' est vrai, la composante Y du quaternion de rotation change
si 'flags & 32' est vrai, la composante Z du quaternion de rotation change

Nous ignorerons donc le reste de la section 'hierarchy' pour les raisons ci-dessus.
La section 'bounds' donne les coordonnées min et max des boà®tes englobantes (Bounding Box) du modèle pendant l'animation. Cela n'entre pas en ligne de compte dans ce tutorial.
Nous sautons donc directement à la section 'baseframe'.

baseframe {
    ( 0 0 0 ) ( -0.5 -0.5 -0.5 )

Cette section contient numJoints lignes et donne la position (3 premiers nombres) et les composantes x,y,z du quaternion représentant la rotation de chaque joint à la première image de l'animation.
Ces positions/rotations servent de base pour toutes les autres informations du fichier .md5anim .

frame 0 { 
    111.1 141.4 -11.5 -0.0 0.9 0.3

Suivent ensuite numAnimatedComponents nombres qui représentent les composantes qui sont modifiées par l'animation pour chaque joint(une ligne par 'joint', contenant un nombre variable d'élément, cf ci-dessus).

Structure

struct _MD5Anim
{
	std::string sName;
	int iNumFrames;
	int iNumJoints;
	int iFrameRate;
	int iNumAnimatedComponents;
	float** pFrames;
	float** pBaseFrame; 
	struct _MD5JointInfos
	{
		int iParent;
		int iFlags;
		int iStartIndex;
	};
	_MD5JointInfos* pJointInfos;
};

sName est le nom de l'animation.
iNumFrames est le nombre d'images de l'animation.
iFrameRate est le nombre d'images par seconde de l'animation.
iNumAnimatedComponents est le nombre de composants animés dans l'animation.
pFrames est un tableau de [iNumFrames][iNumAnimatedComponents] float représentant les valeures modifiées (cf flags ci-dessus).
pBaseFrame est un tableau de [iNumJoints][6], stockant, dans l'ordre, Tx, Ty, Tz, Qx, Qy, Qz a la frame de base de l'animation.
_MD5JointInfos est une structure regroupant des informations sur les 'joints', dont le champ iFlags, donnant les 'flags' du 'joint'.
pJointInfos est une invocation pour le grand Cthulhu.

De l'utilisation de ces données

Les deux fonctions les plus intéressantes et qui s'occupent réellement de gérer ces structures sont void MD5Mesh::_SkinMesh et void MD5Mesh::_BuildBone. Intéressons-nous d'abord à cette dernière :

void MD5Mesh::_BuildBone (int iFrame, _MD5Joint* pJoint,const Quaternion& q, const Vector3& v)
{
	Vector3 animatedPosition;
	animatedPosition.x = pAnimations[iCurrentAnimation].pBaseFrame[pJoint->iNumber][0];
	animatedPosition.y = pAnimations[iCurrentAnimation].pBaseFrame[pJoint->iNumber][1];
	animatedPosition.z = pAnimations[iCurrentAnimation].pBaseFrame[pJoint->iNumber][2];
	
	Quaternion animatedOrientation;
	animatedOrientation.x = pAnimations[iCurrentAnimation].pBaseFrame[pJoint->iNumber][3];
	animatedOrientation.y = pAnimations[iCurrentAnimation].pBaseFrame[pJoint->iNumber][4];
	animatedOrientation.z = pAnimations[iCurrentAnimation].pBaseFrame[pJoint->iNumber][5];
	
	
	int flags = pAnimations[iCurrentAnimation].pJointInfos[pJoint->iNumber].iFlags;
	int n=0;
	int sIndex = pAnimations[iCurrentAnimation].pJointInfos[pJoint->iNumber].iStartIndex;

	if (flags & 1) //Tx est anime
	{
		animatedPosition.x = pAnimations[iCurrentAnimation].pFrames[iFrame][sIndex+n];
		n++;
	}
	if (flags & 2) //Ty est anime
	{
		animatedPosition.y = pAnimations[iCurrentAnimation].pFrames[iFrame][sIndex+n];
		n++;
	}
	if (flags & 4) //Tz est anime
	{
		animatedPosition.z = pAnimations[iCurrentAnimation].pFrames[iFrame][sIndex+n];
		n++;
	}
	if (flags & 8) //Qx est anime
	{
		animatedOrientation.x = pAnimations[iCurrentAnimation].pFrames[iFrame][sIndex+n];
		n++;
	}
	if (flags & 16) //Qy est anime
	{
		animatedOrientation.y = pAnimations[iCurrentAnimation].pFrames[iFrame][sIndex+n];
		n++;
	}
	if (flags & 32) //Qz est anime
	{
		animatedOrientation.z = pAnimations[iCurrentAnimation].pFrames[iFrame][sIndex+n];
		n++;
	}

	animatedOrientation.ComputeR();
	if (pJoint->iParent < 0) //pas de parent
	{
		pJoint->vPosition = animatedPosition;
		pJoint->qOrientation = animatedOrientation;
	}
	else //parent
	{
		pJoint->vPosition = q.Rotate(animatedPosition);
		pJoint->vPosition += v;
		pJoint->qOrientation = q*animatedOrientation;
	}

	for (int i = 0; i < pJoint->iNumChildrens; i++)
	{
		_BuildBone(iFrame,pJoint->pChildrens[i], pJoint->qOrientation, pJoint->vPosition);
	}
}

Cette fonction sera appelée une fois pour chaque 'bone' racine. (qui n'a pas de parent) Pour commencer, on prend les positions/rotation de l'image de base (tout est relatif à cette image).
Ensuite, on test ce qu'on doit animer. sIndex nous donne l'index de base pour ce 'bone' dans le tableau des AnimatedComponents.
On doit ensuite multiplier la rotation et additioner la position du 'bone' parent (histoire que la hiérachie serve à quelque chose). Puis, on lance cette même fonction sur tout les 'bones' enfants.

Vector3 pos;
for (int i=0; i<iNumMeshes; i++) 
{
	for (int j=0; j<pMeshes[i].iNumVerts; j++)
	{
		pVerticesListTab[i][j].vPosition.Null();
		//assignation des texcoords.
		//FIXME: On pourrait éviter à§a et ne le faire qu'une seule fois nan ?
		pVerticesListTab[i][j].s = pMeshes[i].pVerts[j].pTexCoords[0];
		pVerticesListTab[i][j].t = pMeshes[i].pVerts[j].pTexCoords[1];

		for (int k=0; k<pMeshes[i].pVerts[j].iNumWeights; k++) 
		{
			int baseIndex = pMeshes[i].pVerts[j].iWeightIndex;
			int boneIndex = pMeshes[i].pWeights[baseIndex+k].iBoneIndex;
		
			pos = pJoints[boneIndex].qOrientation.Rotate(pMeshes[i].pWeights
                        [baseIndex+k].vWeights);
			pVerticesListTab[i][j].vPosition += (pos + pJoints[boneIndex].vPosition)*
                       pMeshes[i].pWeights[baseIndex+k].fBias; } } }

Ici, pour chaque vertice, on fait la somme des poids qui ont une influence sur le vertice en question.
Il s'agit simplement de multiplier la position de chaque poids par son 'bias' et de faire la somme pour tout les poids qui ont une influence sur le vertice.

comments