Maintenant que nous avons vu dans le détail les deux formats, nous allons aborder l'implémentation de notre loader.
Certains passages ne seront pas détaillés, étant donné que le but de ce tutoriel est de comprendre comment charger les formats OBJ et MTL.
Tout d'abord vous l'avez bien vu, on a souvent besoin de coordonnées de points, de couleurs, etc. donc on va créer une classe contenant 4 flottants (x, y, z et a ; XYZ pour les coordonnées et A pour l'opacité avec RGB=XYZ) :
Code : C++ - OBJlib.h24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 | class FloatVector
{
/*
Classe FloatVector : simple vecteur XYZ ou XYZA (dans le cas de couleurs).
*/
public:
FloatVector(float px=0,float py=0,float pz=0,float pa=0);
/* FloatVector(float px=0,float py=0,float pz=0,float pa=0);
Constructeur, prend en paramètres des flottants correspondant respectivement à x, y, z et a.
*/
~FloatVector();
/* ~FloatVector();
Destructeur, totalement inutile.
*/
FloatVector operator=(const FloatVector &fv);
/* FloatVector operator=(const FloatVector &fv);
Affecte au vecteur courant le contenu du vecteur passé en argument.
Retourne le vecteur courant ainsi modifié.
*/
float x,y,z,a;
};
|
Code : C++ - OBJlib.cpp 88
89
90
91
92
93
94
95
96
97
98
99
100
101
102 | FloatVector::FloatVector(float px,float py,float pz,float pa):x(px),y(py),z(pz),a(pa)
{
}
FloatVector::~FloatVector()
{
}
FloatVector FloatVector::operator=(const FloatVector &fv)
{
x=fv.x;
y=fv.y;
z=fv.z;
a=fv.a;
return *this;
}
|
Attaquons-nous aux matériaux, on se limitera à sa couleur et à son nom. Pour la couleur nous allons donc prendre un
FloatVector et pour le nom un
std::string :
Code : C++ - OBJlib.h46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 | class Material
{
/*
Classe Material : définition d'un matériau, composé d'une couleur et d'un nom spécifique.
*/
public:
Material(float r,float g,float b,std::string n);
/* Material(float r,float g,float b,std::string n);
Constructeur, les trois premiers arguments représentent la couleur RGB du matériau et n est son nom.
*/
Material(Material *mat);
/* Material(Material *mat);
Constructeur alternatif, affecte au matériau courant le contenu du matériau passé en argument.
*/
~Material();
/* ~Material();
Destructeur, totalement inutile.
*/
FloatVector coul;
std::string name;
};
|
Code : C++ - OBJlib.cpp104
105
106
107
108
109
110
111
112
113
114 | Material::Material(float r,float g,float b,string n):name(n)
{
coul.x=r;
coul.y=g;
coul.z=b;
}
Material::Material(Material *mat)
{
coul=mat->coul;
name=mat->name;
}
|
Il reste maintenant le plus intéressant, commençons par une classe représentant un modèle statique.
Tout d'abord réfléchissons au mode d'affichage, dans la lib nous utiliserons les
Vertex Arrays (tutoriel de
Yno).
Notre classe
MeshObj contiendra alors un
GLuint pour la texture, un entier pour le nombre de quads à dessiner, des tableaux dynamiques pour les coordonnées de sommets, de texture, de normales ainsi que les couleurs par sommet. Enfin, elle contiendra un
std::vector de
Material :
Code : C++ - OBJlib.h 69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113 | class MeshObj
{
/*
Classe MeshObj : définition d'un modèle statique.
*/
public:
MeshObj(std::string,MeshObj *first=NULL);
/* MeshObj(std::string,MeshObj *first=NULL);
Constructeur, prend en arguments le nom du modèle à charger et le pointeur de la première frame si le modèle appartient à une animation (sinon laissez-le à NULL).
*/
~MeshObj();
/* ~MeshObj();
Destructeur, libère toute la mémoire qui lui a été allouée.
*/
void charger_obj(std::string,MeshObj *first=NULL);
/* void charger_obj(std::string,MeshObj *first=NULL);
Charge un fichier OBJ et son MTL, prend en arguments le nom du modèle à charger et le pointeur de la première frame si le modèle appartient à une animation (sinon laissez-le à NULL). Cette fonction est appelée par le constructeur.
Aucune valeur de retour.
*/
void charger_mtl(std::string);
/* void charger_mtl(std::string);
Charge un fichier MTL, prend en argument le nom du fichier à charger. Cette fonction est appelée par charger_obj.
Aucune valeur de retour.
*/
void draw_model(bool nor=true,bool tex=false);
/* void draw_model(bool nor=true,bool tex=false);
Dessine le modèle, prend en arguments deux booléens représentant respectivement les normales et la texture. Si nor vaut true alors on prend en compte les normales, et si tex vaut true alors on applique la texture.
Aucune valeur de retour.
*/
void setMaterialsAndTex(std::vector<Material*> mats,GLuint tex);
/* void setMaterialsAndTex(std::vector<Material*> mats,GLuint tex);
Définit directement les matériaux et la texture du modèle, prend en arguments un vector<Material*> et la texture. Cette fonction est appelée par giveMaterialsAndTex.
Aucune valeur de retour.
*/
void giveMaterialsAndTex(MeshObj *target);
/* void giveMaterialsAndTex(MeshObj *target);
Modifie les matériaux et la texture de target en les remplaçant par ses propres matériaux et sa texture. Cette fonction est appelée par charger_obj uniquement lorsque first!=NULL.
Aucune valeur de retour.
*/
private:
GLuint texture;
int n_data;
float *vertice,*normals,*textures,*colours;
std::vector<Material*> materiaux;
};
|
Ne faites pas attention aux deux dernières méthodes de cette classe, elle n'ont pas de rapport avec le parsage des formats OBJ et MTL.
Occupons-nous du constructeur et du destructeur :
Code : C++ - OBJlib.cpp119
120
121
122
123
124
125
126
127
128
129
130
131
132
133 | MeshObj::MeshObj(string s,MeshObj *first)
{
charger_obj(s,first);
}
MeshObj::~MeshObj()
{
free(vertice);
free(normals);
free(textures);
free(colours);
for(unsigned int i=0;i<materiaux.size();i++)
delete materiaux[i];
materiaux.clear();
}
|
Format OBJ
Ca y est, nous arrivons enfin à la méthode
MeshObj::charger_obj 
! Nous savons que dans le format OBJ on définit d'abord chaque point, puis ensuite on les assemble pour former des faces. Nous allons donc créer un
std::vector de
FloatVector pour les coordonnées de sommets, de normales, de textures et pour les couleurs ; ainsi qu'un
std::vector d'entiers non signés représentant les indices des points à assembler. Au premier abord, ça peut paraître dur mais en réalité ce sera assez simple à mettre en place. Déjà, regardons le code que nous obtenons :
Code : C++ - OBJlib.cpp | vector<FloatVector> ver,nor,tex,col;
vector<unsigned int> iv,it,in;
|
Maintenant on ouvre le fichier passé en argument :
Code : C++ - OBJlib.cpp | ifstream fichier(nom.c_str(),ios::in);
|
Nous allons le lire ligne après ligne, donc nous allons créer un
std::string et par la même occasion un autre
std::string qui correspond au nom du matériau en cours :
Code : C++ - OBJlib.cpp
On peut enfin lire le fichier, à condition que celui-ci existe ! C'est pourquoi il faudra faire un test au préalable.
Ensuite il faut différencier plusieurs cas :
- les lignes commençant par 'v'
- les lignes commençant par 'f'
- les lignes commençant par "mtllib"
- les lignes commençant par "usemtl".
Occupons-nous des premières. Elles se divisent en trois catégories : "v " qui définissent les coordonnées des points, "vt" pour les textures et "vn" pour les normales :
Code : C++ - OBJlib.cpp150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170 | if(ligne[0]=='v') //Coordonnées de points (vertex, texture et normale)
{
if(ligne[1]==' ') //Vertex
{
char x[255],y[255],z[255];
sscanf(ligne.c_str(),"v %s %s %s",x,y,z);
ver.push_back(FloatVector(strtod(x,NULL),strtod(y,NULL),strtod(z,NULL)));
}
else if(ligne[1]=='t') //Texture
{
char x[255],y[255];
sscanf(ligne.c_str(),"vt %s %s",x,y);
tex.push_back(FloatVector(strtod(x,NULL),strtod(y,NULL)));
}
else if(ligne[1]=='n') //Normale
{
char x[255],y[255],z[255];
sscanf(ligne.c_str(),"vn %s %s %s",x,y,z);
nor.push_back(FloatVector(strtod(x,NULL),strtod(y,NULL),strtod(z,NULL)));
}
}
|
Ce code est assez clair (juste les [255] qui sont un peu bourrins

), au final on se retrouve avec les vector de coordonnées de points, de texture et de normales.
Maintenant regardons du côté des définitions de faces.
Dans certains modèles il n'y a pas de texture, donc on se retrouvera avec des "//" (car on omet les numéros de textures, ce qui est logique

), on va les remplacer par "/1/".
C'est ma fonction doubleSlash :
Code : C++ - OBJlib.cpp 8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | string doubleSlash(string s)
{
//Remplace "//" par "/1/".
string s1="";
for(unsigned int i=0;i<s.size();i++)
{
if(i<s.size()-1&&s[i]=='/'&&s[i+1]=='/')
{
s1+="/1/";
i++;
}
else
s1+=s[i];
}
return s1;
}
|
Puis on remplace les slashes par des espaces, c'est ma fonction remplacerSlash :
Code : C++ - OBJlib.cpp24
25
26
27
28
29
30
31
32
33
34
35
36 | string remplacerSlash(string s)
{
//Remplace les '/' par des espaces.
string ret="";
for(unsigned int i=0;i<s.size();i++)
{
if(s[i]=='/')
ret+=' ';
else
ret+=s[i];
}
return ret;
}
|
Ensuite on éclate la chaîne en ses espaces, c'est ma fonction splitSpace :
Code : C++ - OBJlib.cpp37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 | vector<string> splitSpace(string s)
{
//Eclate une chaîne au niveau de ses espaces.
vector<string> ret;
string s1="";
for(unsigned int i=0;i<s.size();i++)
{
if(s[i]==' '||i==s.size()-1)
{
if(i==s.size()-1)
s1+=s[i];
ret.push_back(s1);
s1="";
}
else
s1+=s[i];
}
return ret;
}
|
Revenons à nos faces :
Code : C++ - OBJlib.cpp171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 | else if(ligne[0]=='f') //Indice faces
{
ligne=doubleSlash(ligne); //On remplace "//" par "/1/" dans toute la ligne
ligne=remplacerSlash(ligne); //On remplace les '/' par des espaces, ex : pour "f 1/2/3 4/5/6 7/8/9" on obtiendra "f 1 2 3 4 5 6 7 8 9"
vector<string> termes=splitSpace(ligne.substr(2)); //On éclate la chaîne en ses espaces (le substr permet d'enlever "f ")
int ndonnees=(int)termes.size()/3;
for(int i=0;i<(ndonnees==3?3:4);i++) //On aurait très bien pu mettre i<ndonnees mais je veux vraiment limiter à 3 ou 4
{
iv.push_back(strtol(termes[i*3].c_str(),NULL,10)-1);
it.push_back(strtol(termes[i*3+1].c_str(),NULL,10)-1);
in.push_back(strtol(termes[i*3+2].c_str(),NULL,10)-1);
}
if(ndonnees==3) //S'il n'y a que 3 sommets on duplique le dernier pour faire un quad ayant l'apparence d'un triangle
{
iv.push_back(strtol(termes[0].c_str(),NULL,10)-1);
it.push_back(strtol(termes[1].c_str(),NULL,10)-1);
in.push_back(strtol(termes[2].c_str(),NULL,10)-1);
}
for(unsigned int i=0;i<materiaux.size();i++)
if(materiaux[i]->name==curname)
{
for(int j=0;j<4;j++)
col.push_back(materiaux[i]->coul); //On ajoute la couleur correspondante
break;
}
}
|
Maintenant on va traiter la ligne commençant par "mtllib". Si le fichier .OBJ ne se trouve pas dans le même répertoire que l'exécutable, son .MTL ne le sera pas non plus. Il faut donc récupérer le dossier où se trouve le .OBJ, c'est ma fonction get_directory :
Code : C++ - OBJlib.cpp56
57
58
59
60
61
62
63
64
65
66
67
68
69
70 | string get_directory(string s)
{
string s1="",s2="";
for(unsigned int i=0;i<s.size();i++)
{
if(s[i]=='/'||s[i]=='\\')
{
s1+=s2+"/";
s2="";
}
else
s2+=s[i];
}
return s1;
}
|
Pour savoir si la ligne commence par "mtllib", on testera simplement si la ligne commence par 'm', "mtllib" étant le seul mot-clef du format OBJ commençant par un 'm' :
Code : C++ - OBJlib.cpp | else if(ligne[0]=='m'&&first==NULL)//fichier MTL et si c'est la première frame (comme ça on ne charge pas plusieurs fois le même MTL et la même texture)
charger_mtl(get_directory(nom)+ligne.substr(7));
|
La raison du substr(7) est que "mtllib " fait sept caractères.
Nous verrons la fonction
charger_mtl en détail plus tard.
Maintenant il ne reste plus que "usemtl", de la même manière nous ne testerons que la première lettre de la ligne :
Code : C++ - OBJlib.cpp | else if(ligne[0]=='u')//utiliser un MTL
curname=ligne.substr(7);
|
Le parsage est terminé, après avoir fermé le fichier on applique les indices de sommets pour avoir toutes les faces :
Code : C++ - OBJlib.cpp207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234 | vector<float> tv(0),tc(0),tn(0),tt(0);
for(unsigned int i=0;i<iv.size();i++)
if(iv[i]<ver.size())
{
tv.push_back(ver[iv[i]].x);
tv.push_back(ver[iv[i]].y);
tv.push_back(ver[iv[i]].z);
tc.push_back(col[i].x);
tc.push_back(col[i].y);
tc.push_back(col[i].z);
tc.push_back(col[i].a);
}
for(unsigned int i=0;i<in.size();i++)
if(in[i]<nor.size())
{
tn.push_back(nor[in[i]].x);
tn.push_back(nor[in[i]].y);
tn.push_back(nor[in[i]].z);
}
for(unsigned int i=0;i<it.size();i++)
if(it[i]<tex.size())
{
tt.push_back(tex[it[i]].x);
tt.push_back(tex[it[i]].y);
}
|
Pour utiliser les VBA nous devrons avoir des tableaux de flottants, et non un
std::vector. Une simple fonction peut faire la conversion :
Code : C++ - OBJlib.cpp71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86 | float* vector2float(vector<float>& tableau)
{
float* t=NULL;
t=(float*)malloc(tableau.size()*sizeof(float));
if(t==NULL||tableau.empty())
{
float *t1=(float*)malloc(sizeof(float)*3);
for(int i=0;i<3;i++)
t1[i]=0.;
return t1;
}
for(unsigned int i=0;i<tableau.size();i++)
t[i]=tableau[i];
return t;
}
|
On va l'employer comme suit :
Code : C++ - OBJlib.cpp | vertice=vector2float(tv);
normals=vector2float(tn);
textures=vector2float(tt);
colours=vector2float(tc);
|
En ce qui concerne le nombre d'éléments, nous prendrons juste le nombre d'éléments du vecteur
iv :
Code : C++ - OBJlib.cpp
Enfin viennent quelques libérations de mémoire :
Code : C++ - OBJlib.cpp248
249
250
251
252
253
254
255 | ver.clear();
nor.clear();
tex.clear();
col.clear();
iv.clear();
it.clear();
in.clear();
|
Et voilà, nous avons chargé notre fichier .OBJ

! A présent voyons du côté du format MTL.
Format MTL
Là aussi nous ouvrons le fichier :
Code : C++ - OBJlib.cpp | ifstream fichier(nom.c_str(),ios::in);
|
On déclare le nom du matériel courant :
Code : C++ - OBJlib.cpp
De même que précédemment, nous lirons le contenu du fichier ligne par ligne. Commençons par créer notre variable
ligne :
Code : C++ - OBJlib.cpp
Dans ce code nous ne tiendront compte que de quatre mots-clefs, ce qui est largement suffisant : "newmtl", "Kd", "map_Kd" et "d".
Pour "newmtl" il suffit de savoir si la ligne commence par 'n' :
Code : C++ - OBJlib.cpp | if(ligne[0]=='n') //nouveau materiau
curname=ligne.substr(7);
|
Ce code a simplement pour effet de modifier
curname (vu que la ligne commence par "newmtl " il faut donc éliminer les sept premiers caractères de la ligne, d'où le
substr).
Pour "Kd" (couleur diffuse), on va tester si le premier caractère est 'K' et le deuxième 'd'. Si c'est le cas, on crée un nouveau matériau aux couleurs lues dans le fichier :
Code : C++ - OBJlib.cpp | else if(ligne[0]=='K'&&ligne[1]=='d') //couleur
{
vector<string> termes=splitSpace(ligne.substr(3));
materiaux.push_back(new Material((float)strtod(termes[0].c_str(),NULL),(float)strtod(termes[1].c_str(),NULL),(float)strtod(termes[2].c_str(),NULL),curname));
}
|
La ligne commence par "Kd " donc ici on élimine les trois premiers caractères avec substr(3) ; et on convertit les différents termes de chaîne de caractères à flottants en utilisant
strtod.
A présent nous allons charger la texture (s'il y a), en sachant que le nom du fichier est écrit après "Map_Kd " et qu'il faut lui rajouter le dossier du fichier .MTL :
Code : C++ - OBJli.cpp | else if(ligne[0]=='m'&&ligne[5]=='d')//map_Kd (texture)
{
string f=get_directory(nom)+ligne.substr(7);
texture=loadTexture(f.c_str());
}
|
Il ne reste plus que l'opacité :
Code : C++ - OBJlib.cpp | else if(ligne[0]=='d') //opacité
{
string n=ligne.substr(2);
materiaux[materiaux.size()-1]->coul.a=strtod(n.c_str(),NULL);
}
|
Notre fichier .MTL est chargé, allons voir du côté du dessin

.
Affichage du modèle
Rappelons-nous le prototype de
MeshObj::draw_model :
Code : C++ - OBJlib.h | void draw_model(bool nor=true,bool tex=false);
/* void draw_model(bool nor=true,bool tex=false);
Dessine le modèle, prend en arguments deux booléens représentant respectivement les normales et la texture. Si nor vaut true alors on prend en compte les normales, et si tex vaut true alors on applique la texture.
Aucune valeur de retour.
*/
|
On va d'abord activer les listes de sommets (le plus important

) :
Code : C++ - OBJlib.cpp | glEnableClientState(GL_VERTEX_ARRAY);
|
Si on veut tenir compte des normales on les active :
Code : C++ - OBJlib.cpp | if(nor)
glEnableClientState(GL_NORMAL_ARRAY);
|
De même pour la texture :
Code : C++ - OBJlib.cpp | if(tex)
{
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glBindTexture(GL_TEXTURE_2D,texture);
}
|
Et on active les couleurs :
Code : C++ - OBJlib.cpp | glEnableClientState(GL_COLOR_ARRAY);
|
Les initialisations sont faites, on va maintenant dessiner les listes correspondant aux différents éléments :
Code : C++ - OBJlib.cpp304
305
306
307
308
309
310
311
312 | glVertexPointer(3,GL_FLOAT,0,vertice);
if(tex)
glTexCoordPointer(2,GL_FLOAT,0,textures);
if(nor)
glNormalPointer(GL_FLOAT,0,normals);
glColorPointer(4,GL_FLOAT,0,colours);
glDrawArrays(GL_QUADS,0,n_data);
|
Enfin, on désactive les états :
Code : C++ - OBJlib.cpp | glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
|
L'essentiel de la lib est faite ici, en prenant cinq minutes vous pourrez aisément refaire les classes
AnimMesh et
VirtualAnim.
Maintenant que nous avons fait connaissance avec le format OBJ, nous allons voir comment nous l'approprier dans nos programmes

.