Hanoï en 3D - Pearson France

publicité
19
Hanoï en 3D
Au chapitre précédent, nous n’avons manipulé que des formes élémentaires : un triangle et un cube,
lui-même composé de triangles. Dans un programme plus complexe comme celui des Tours de
Hanoï, naturellement, il nous faudra des formes plus élaborées – encore que nous allons nous limiter
à des choses relativement simples. Si OpenGL nous permet de représenter à peu près n’importe quoi,
encore faut-il lui dire quoi afficher. L’un des problèmes majeurs en programmation 3D est donc de
“nourrir” OpenGL, c’est-à-dire de passer d’une forme quelconque – comme un disque – à un
ensemble d’objets fondamentaux – comme des triangles – qu’OpenGL sera capable de traiter.
Pour cela, il existe fondamentalement deux approches :
●●
La forme peut être décrite par une ou plusieurs équations mathématiques à partir desquelles votre
programme va calculer un certain nombre de points puis relier ces points pour obtenir des
triangles.
●●
La forme voulue est réalisée dans un logiciel de modélisation dédié à cette tâche, sauvegardée sur
disque dans un fichier, lequel fichier sera interprété par votre programme pour fournir les triangles
nécessaires à OpenGL.
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 261
03/02/11 15:52
262
Pour aller plus loin
La première solution présente l’avantage de rendre le programme relativement autonome, c’est-àdire qu’il ne dépend pas de la présence de fichiers "annexes" pour son fonctionnement. Par contre,
elle donne en général naissance à des algorithmes assez compliqués, faisant intervenir des notions
mathématiques relativement sophistiquées. Aussi, par souci de simplicité, nous allons mettre en
œuvre la seconde approche.
19.1. Du triangle à la lumière
Au-delà de la modélisation des objets à afficher, il est une caractéristique que nous avons ignorée
jusqu’à maintenant, mais qui est présente tout autour de nous et essentielle pour notre perception
des volumes : la lumière. Regardez la Figure 19.1 : les deux sphères ont en réalité la même couleur.
Seulement, celle de gauche est affichée sans tenir compte d’aucun éclairage, comme nous l’avons
fait au chapitre précédent, tandis que celle de droite est affichée en exploitant une source de lumière.
Figure 19.1
Différence entre
ignorer (à gauche)
ou prendre
en compte (à droite)
la lumière.
À strictement parler, cette image est loin d’être parfaite. Notamment, on s’attendrait à voir l’ombre
de la sphère de droite sur le plan. Nous allons ignorer le problème spécifique de l’ombre générée par
les objets, pour nous contenter de modéliser l’effet d’une source de lumière.
La modélisation d’une source lumineuse et de la façon dont elle affecte la perception visuelle d’un
objet peut se révéler d’une particulière complexité si on recherche absolument à obtenir un résultat
réaliste. Nous allons nous contenter d’une technique extrêmement simple, suffisante pour nos
besoins. Mais, aussi simple soit-elle, cette technique a besoin d’une information géométrique fondamentale : la normale.
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 262
03/02/11 15:52
Chapitre 19
Hanoï en 3D
263
19.1.1. Notion de normales
Si on considère la surface, la “peau” d’un objet, on appelle normale en un point de cette surface une
direction (les mathématiciens diront un vecteur) perpendiculaire à la surface en ce point, et pointant
en général vers l’extérieur de l’objet. La Figure 19.2 donne quelques exemples de normales.
Figure 19.2
Des normales sur
une sphère et au
coin d’un cube.
Sur une sphère, chaque point possède une normale différente. Sur un cube, chaque côté n’a qu’une
normale (ou, pour être strict, tous les points d’un même côté ont la même normale) mais, par contre,
les coins possèdent trois normales : un coin ou plus généralement les points le long d’une arête vive
possèdent autant de normales qu’il y a de faces partageant ce point (ou cette arête).
Dans le cadre d’OpenGL, la normale fait partie des attributs d’un vertex au même titre que ses coordonnées ou sa couleur. Si on dessine un objet d’une seule couleur, comme c’était le cas des deux
sphères de la Figure 19.1 et comme ce le sera pour nos disques des Tours de Hanoï, la connaissance
de la normale permet d’ajuster la couleur affichée pour donner l’impression de volume, en tenant
compte d’une direction d’éclairage.
Le problème est maintenant de savoir comment obtenir ces fameuses normales, en chacun des
vertices qui constituent un objet. Rassurez-vous, les logiciels de modélisation et d’image de synthèse
savent très bien calculer ces directions : il nous suffira donc de les lire depuis un fichier.
19.1.2. Blender et le format de fichier Wavefront OBJ
Voyons justement comment obtenir les fichiers à partir desquels nous allons construire les différents
modèles à afficher. Nous allons utiliser pour cela le logiciel Blender (http://www.blender.org), un
logiciel d’image de synthèse et d’animation libre et gratuit, disponible sur pratiquement tous les
systèmes.
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 263
03/02/11 15:52
264
Pour aller plus loin
Figure 19.3
Le logiciel Blender, en préparation de la Figure 19.2.
Blender enregistre les fichiers dans son propre format, bien trop complexe pour nos besoins. Il nous
faut quelque chose de simple. Le choix est alors fait de modéliser chacun des éléments qu’il nous
faut dans un fichier séparé, en l’enregistrant au format OBJ développé par Wavefront (http://
fr.wikipedia.org/wiki/Objet_3D_%28format_de_fichier%29). Il s’agit d’un fichier texte, donc
aisément lisible (voir la section 15.2). Un exemple très abrégé pourrait ressembler à ceci :
v 1.769225 -0.174253 0.415734
v 1.683198 -0.165781 0.461939
# ...et quelques centaines ainsi...
v 1.777785 -0.000000 0.415734
vn 0.058687 -0.004791 0.998260
vn 0.058870 0.000946 0.998260
# ...et encore quelques centaines ainsi...
vn 0.827540 -0.080599 0.555559
f 140//1 1086//2 8//3
f 685//4 7//5 1087//6
# ...et toujours quelques centaines ainsi...
f 4//1076 20//1077 1077//134
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 264
03/02/11 15:52
Chapitre 19
Hanoï en 3D
265
Chaque ligne commençant par un v représente les coordonnées d’un point. Chaque ligne commençant par vn représente une normale, avec ses coordonnées. Enfin, chaque ligne commençant par f est
la description d’une face, en donnant la liste des points qui la constituent, à chaque point étant associée une normale. C’est cette association qui donne naissance au vertex. Dans l’exemple précédent,
la première face est un triangle (car il y a trois vertices), son premier point est le point d’indice 140
dans la liste des v, auquel est associé la normale d’indice 1 dans la liste des vn.
Pour être juste, le format OBJ est capable de bien plus que cela, mais c’est tout ce dont nous avons
besoin. Si vous utilisez Blender, obtenir un tel fichier se fait en passant par le menu File > Export >
Wavefront, puis en n’activant que les options Selection only (pour n’exporter que l’objet sélectionné),
Normals (pour avoir les normales) et Triangulate, et aucune autre. La dernière est particulièrement
importante pour que les faces ne soient bien que des triangles, pas des quadrilatères ou autre polygone.
D’autres logiciels devraient proposer des options similaires lors d’un enregistrement au format OBJ.
19.2. Préparation du programme
Après tous ces prolégomènes, revenons à la programmation. Nous allons repartir de l’état dans
lequel nous avons laissé le programme au Chapitre 17, pour tout simplement lui ajouter une possibilité de représentation, en plus du mode texte (section 11.3.1), de l’image (Chapitre 13) et de la scène
“plate” (le canevas, Chapitre 14). Cette nouvelle capacité sera stockée dans une bibliothèque
nommée 3d et donnera un résultat illustré par la Figure 19.4.
Figure 19.4
Hanoï en 3D !
Comme il faut bien se donner une référence quelque part, nous posons les termes suivants :
●●
L’origine, le point de coordonnées (0, 0, 0), correspond exactement à la base de l’aiguille du milieu.
●●
Les aiguilles s’étendent le long de l’axe Z ; donc, sur l’image cet axe va vers le haut.
●●
La longueur du plateau s’étend le long de l’axe X ; donc, sur l’image il va vers la droite en montant.
●●
Et, par conséquent, la largeur du plateau s’étend le long de l’axe Y, qui semble donc s’éloigner de
nous vers le haut à gauche.
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 265
03/02/11 15:52
266
Pour aller plus loin
Dans le répertoire jeu_hanoi, qui contient normalement les sous-répertoires dessin, gui, scene, etc.,
créez un nouveau sous-répertoire nommé 3d. Placez dedans un fichier projet 3d.pro avec le contenu
suivant :
1. TEMPLATE = lib
2. QT += core gui opengl
3. CONFIG += release
4. MOC_DIR = .moc
5. OBJECTS_DIR = .objs
6. DESTDIR = ../bin
7. LIBS += -L../bin -ljeu -ldessin
8. INCLUDEPATH += .. .
9. DEFINES += GLEW_STATIC
10. SOURCES += vue_gl.cpp glew.c mesh.cpp dessin_3d.cpp
11. HEADERS += vue_gl.h mesh.h dessin_3d.h
Comme au chapitre précédent, nous allons nous appuyer sur les bibliothèques GLM et Glew. À
partir des dossiers issus de l’extraction des archives correspondantes, ou bien à partir du dossier du
programme du chapitre précédent, dupliquez dans 3d les répertoires glm, GL et le fichier glew.c,
comme nous l’avions fait pour le cube.
19.2.1. Les objets 3D
Pour nous simplifier la vie, le programme (ou plus précisément la bibliothèque 3d que nous allons
créer) va aller chercher les fichiers contenant les descriptions des objets 3D dans le même répertoire
où il va se trouver lui-même, le sous-répertoire bin dans jeu_hanoi. Nous allons créer :
●● cylindre.obj,
décrivant un simple cylindre qui sera utilisé pour l’aiguille de chaque tour ;
●● hemisphere.obj,
●● support.obj,
décrivant une demi-sphère pour faire l’arrondi au sommet de chaque aiguille ;
pour afficher un plateau sur lequel disques et aiguilles reposeront ;
●●
huit fichiers disque_01.obj à disque_08.obj, chacun représentant un disque d’une taille donnée ;
●●
enfin, un fichier suzanne.obj, dont l’obscure fonction sera explicitée plus loin.
Vous pouvez créer vous-même ces fichiers, ou bien les récupérer à partir de l’archive contenant les
codes sources de ce livre, dont l’adresse a été donnée à la fin du premier chapitre. Les objets sont
modélisés de telle sorte que :
●●
Le haut du plateau est à la position 0 sur l’axe Z ; autrement dit, il est “dans” le plan XY, le milieu
étant exactement à l’origine des coordonnées.
●●
Le centre de l’hémisphère et le milieu du bas du cylindre sont à l’origine ; tous deux ont un
diamètre de 1, la hauteur du cylindre valant également 1.
●●
Enfin, les disques sont centrés sur l’origine.
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 266
03/02/11 15:52
Chapitre 19
Hanoï en 3D
267
La Figure 19.5 illustre ces positionnements tels que définis dans les fichiers des modèles.
Figure 19.5
Positionnements
initiaux des
modèles.
Ces choix auront une certaine importance pour pouvoir placer et adapter les objets selon nos besoins,
ce que nous allons voir bientôt.
19.2.2. Le mesh
Les informations que nous allons lire depuis les fichiers d’objets seront stockées dans des tampons
(buffers) OpenGL. À chaque objet va correspondre un tampon pour les points, un tampon pour les
normales et un tampon pour les triangles, représentés par des indices de vertices comme au chapitre
précédent. Pour nous simplifier un peu les choses, tous ces tampons seront stockés dans une
classe Mesh.
Le mot anglais mesh se traduit par filet, grillage ou maillage. Dans le cadre de la programmation 3D,
ce terme désigne une structure de donnée contenant la modélisation d’un objet, le plus souvent à
base de triangles, comme un filet dont les mailles viendraient épouser les formes de l’objet. Implicitement, la notion de “maille” suggère que l’on dispose d’un ensemble de points, ces points étant
reliés entre eux pour constituer, justement, les mailles. Pour utiliser un terme mathématique, on dira
qu’un mesh contient une information topologique plus ou moins détaillée, permettant à partir d’un
sommet ou d’une face de trouver les sommets ou les faces voisines, ainsi que les arêtes.
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 267
03/02/11 15:52
268
Pour aller plus loin
La classe Mesh est donc déclarée ainsi :
1. #ifndef MESH_H
2. #define MESH_H
3. #include <GL/gl.h>
4. #include <QtCore/QObject>
5. #include <QtOpenGL/QGLBuffer>
6. #include <glm/glm.hpp>
7. class Mesh: public QObject
8. {
9.
public:
10.
Mesh(QObject* parent = 0);
11.
GLuint Points_Buffer() const;
12.
GLuint Normales_Buffer() const;
13.
GLuint Triangles_Buffer() const;
14.
void Lire_Obj(
15.
16.
QString const& fichier);
void Reserver(
17.
int const nb_vertices,
18.
int const nb_triangles);
19.
int Nb_Vertices() const;
20.
int Nb_Triangles() const;
21.
void Map();
22.
glm::vec4* Points() const;
23.
glm::vec3* Normales() const;
24.
glm::uvec3* Triangles() const;
25.
void Unmap();
26.
27.
void Dessiner();
private:
28.
QGLBuffer points_buffer;
29.
QGLBuffer normales_buffer;
30.
QGLBuffer triangles_buffer;
31.
glm::vec4* points;
32.
glm::vec3* normales;
33.
glm::uvec3* triangles;
34.
int nb_vertices;
35.
int nb_triangles;
Pour les tampons, on utilise la classe QGLBuffer
proposée par Qt (lignes 5 et 28-30). Par ailleurs, la
classe Mesh fournit diverses facilités, comme les
méthodes Map() et Unmap(), qui vont correspondre
aux fonctions OpenGL glMapBuffer() et glUnmapBuffer(). De cette façon, nous pourrons avoir
accès aux contenus des tampons, et éventuellement
les adapter à notre situation particulière (ce que nous
ferons plus loin).
La méthode Dessiner() (ligne 26) va se charger
d’invoquer les différents glBindBuffer(), glEnableClientState() et autre glDrawElements() pour
effectivement faire dessiner l’objet par OpenGL. Le
code de la méthode paintGL() de la classe Vue_Gl,
que nous verrons bientôt, s’en trouvera allégé d’autant
et donc plus facile à relire.
Vous l’aurez deviné, la méthode Lire_Obj()
(ligne 14) a pour tâche de lire les informations d’un
fichier au format OBJ et de les stocker dans les différents tampons. Le soin vous est laissé de consulter son
code (non sans avoir essayé de l’écrire vous-même !),
mais notez deux petites subtilités.
Pour OpenGL, l’association entre un point et une
normale se fait par les indices : le point no 1 est
associé à la normale no 1, et ainsi de suite. Or, dans un
fichier OBJ, points et normales ne sont pas toujours
donnés dans le même ordre : il faut donc utiliser un
tableau temporaire pour l’un des deux, afin de tout
remettre en ordre quand on lit les triangles.
Les indices donnés pour les triangles, justement,
commencent à 1 et non pas à 0 comme c’est l’usage
pour les tableaux en C++. Il faut donc ajuster (diminuer de 1) les indices lus dans le fichier.
36. }; // class Mesh
37. #endif // MESH_H
38.
Nantis de cette classe utilitaire (nos amis anglo-saxons parlent de helper class, classe qui aide), nous
pouvons nous pencher sur l’essentiel du programme.
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 268
03/02/11 15:52
Chapitre 19
Hanoï en 3D
269
19.3. Affichage général
L’affichage sera assuré par une classe Vue_Gl assez semblable à celle décrite au chapitre précédent,
à laquelle on va ajouter les informations nécessaires au jeu – comme nous l’avons fait dans la classe
Scene pour l’affichage basé sur un canevas d’items.
19.3.1. Initialisation
L’initialisation du contexte OpenGL se déroule, comme toujours, dans la méthode initializeGL().
Entre autres, c’est là que vont effectivement être lus les fichiers d’objet. Cette méthode commence
ainsi :
144. void Vue_Gl::initializeGL()
145. {
146.
glewInit();
147.
QDir chemin(qApp->applicationDirPath());
148.
// création du support
149.
float const larg_support = 2.0f*(this->jeu->Hauteur()+1)+2.0f;
150.
float const long_support = 3*2.0f*(this->jeu->Hauteur()+1)+4.0f;
151.
this->base_tours = new Mesh(this);
152.
this->base_tours->Lire_Obj(
153.
QFileInfo(chemin, “support.obj”).absoluteFilePath());
applicationDirPath(), héritée de
QApplication, donne une chaîne de caractères
la classe QCoreApplication dont dérive la classe
contenant le répertoire dans lequel se trouve le
fichier exécutable. On crée à partir de là une instance de la classe QDir (ligne 144), laquelle représente justement un répertoire. On l’utilise comme premier paramètre au constructeur de la classe
QFileInfo, le second paramètre étant le nom d’un fichier auquel on souhaite accéder. Cette classe
propose nombre de méthodes fort utiles, dont la méthode absoluteFilePath(), laquelle retourne
une chaîne de caractères contenant le chemin (path) absolu (absolute) vers le fichier (file) demandé.
C’est cette chaîne que l’on va donner à la méthode Lire_Obj() de notre classe Mesh, ici pour lire le
modèle du support (lignes 152-153). Notez que, dans un “vrai” programme, il faudrait déjà vérifier la présence et la lisibilité de ce fichier, par exemple à l’aide des méthodes exists() et isReadable() de QFileInfo.
La méthode
Seulement, cela ne va pas suffire : la taille du support dépend du nombre de disques. Or nous n’avons
qu’un seul modèle : il va donc falloir l’adapter un peu, ce qui est en fait fort simple, étant donné la
façon dont le plateau a été modélisé. Le milieu du plateau étant initialement à l’origine, les points
correspondant aux coins ont des coordonnées X et Y soit positives soit négatives selon le côté où ils
se trouvent. Il suffit alors de diminuer ou d’augmenter ces coordonnées, d’une certaine quantité
adaptée à la taille voulue (voyez les lignes 149 et 150), pour “agrandir” le plateau.
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 269
03/02/11 15:52
270
Pour aller plus loin
Nous allons donc devoir modifier le contenu du tampon contenu dans l’instance de Mesh :
155.
this->base_tours->Map();
156.
glm::vec4* base_tours_pts = this->base_tours->Points();
157.
for(int vertex = 0; vertex < this->base_tours->Nb_Vertices(); ++vertex)
158.
{
159.
glm::vec4& pt = base_tours_pts[vertex];
160.
if ( pt.x < 0.0f )
161.
pt.x -= long_support / 2.0f;
162.
else
163.
pt.x += long_support / 2.0f;
164.
if ( pt.y < 0.0f )
165.
pt.y -= larg_support / 2.0f;
166.
else
167.
pt.y += larg_support / 2.0f;
168.
}
169.
this->base_tours->Unmap();
À la suite de l’appel à la méthode Map(), on récupère un pointeur sur les points du modèle (ligne 156).
Rappelez-vous le chapitre précédent : un pointeur peut s’utiliser un peu comme un tableau de type
std::vector<>, ce dont nous n’allons pas nous priver. La boucle initiée en ligne 157 va parcourir les
points et adapter leurs coordonnées X et Y selon qu’ils se trouvent, d’une part, à gauche ou à droite
(lignes 160 à 163), d’autre part, en haut ou en bas (lignes 164 à 167), par rapport à l’origine. Notez
l’utilisation d’une référence ligne 159, afin d’éviter de ressaisir six fois base_tours_pts[vertex]. Et
ne surtout pas oublier l’appel à Unmap() quand on a terminé !
La hauteur du cylindre sera ajustée de la même façon, sauf qu’on ne va modifier que la coordonnée
en Z des seuls points qui sont “en haut” du cylindre. L’hémisphère et les disques n’ont besoin
d’aucune modification : nous verrons un peu plus loin par quel moyen nous allons les positionner.
La seule petite difficulté peut concerner la construction du nom du fichier pour les disques. Nous
pourrions créer un par un les meshs en citant les noms de fichier disque_01.obj, disque_02.obj, etc.
Mais, au-delà du fait que c’est affreusement laborieux et dépourvu du moindre ludisme, cela pourrait
devenir insupportable si on décidait un jour de représenter le jeu tel que décrit dans la légende, avec
ses 64 disques. Nous allons donc écrire une boucle :
191.
for(int disque = 0; disque < this->jeu->Hauteur(); ++disque)
192.
{
193.
Mesh* dsq = new Mesh(this);
194.
QString const nom_disque =
195.
QString(“disque_%1.obj”).arg(disque+1, 2, 10, QChar(’0’));
196.
dsq->Lire_Obj(
197.
QFileInfo(chemin, nom_disque).absoluteFilePath());
198.
199.
this->disques[disque] = dsq;
}
© 2011 Pearson France – Initiation à la programmation avec Python et C++ – Yves Bailly
Book_Prog_initia.indb 270
03/02/11 15:52
Téléchargement