Faculté Informatique & Communications Cours d'informatique |
![]() ![]() |
Dans tous les programmes que vous avez écrits jusqu'à maintenant, vous disposiez du terminal pour afficher et saisir du texte. Ceci est insuffisant pour permettre d'afficher des graphismes évolués et offrir une interaction «vivante» avec l'utilisateur. Ce document a pour but de vous donner les bases nécessaires pour programmer en utilisant les bibliothèques de fonctions graphiques SDL et OpenGL. Nous abordons également le thème de la simulation dans un environnement graphique en temps réel.
NOTE : Le matériel présenté ici ne faisant pas partie des objectifs du cours (mais étant simplement un outil pour le projet de cette année), nous aurons une approche pratique par l'exemple, très superficielle, plutôt qu'une approche théorique et complète.
OpenGL propose une série de fonctions évoluées, compatibles avec toutes les plates-formes (Unix, Linux, MacOS, ...) et très performantes. Le dessin s'effectue grâce à des primitives géométriques (e.g. polygones) en 2D ou 3D. Ces primitives peuvent êtres colorées, texturées et animées.
De son coté, la bibliothèque SDL adresse le problème de l'interaction avec l'utilisateur. Elle permet en effet de gérer, entre autres :
et ce de façon indépendante de la plate-forme.
NOTE : OpenGL et SDL ne sont, bien entendu, pas les seules bibliothèques permettant d'effectuer ces tâches. Dans le cadre de ce cours vous pouvez aussi utiliser wxWidgets ou glut à la place de SDL.
Ce tutoriel ne constitue qu'une brève présentation de ces deux bibliothèques (SDL et OpenGL). La meilleure méthode d'apprentissage est de lire chaque exemple en entier, puis compiler l'exemple et regarder ce qui se passe en interagissant avec le programme. Dans un troisième temps, il est intéressant de modifier les paramètres des fonctions présentées (couleurs, positions, touches appuyées,...) afin de voir ce qui change. Finalement vous pouvez essayer de refaire le même programme.
La première chose qu'il faut bien comprendre quand on utilise une bibliothèque graphique telle que SDL, c'est qu'il ne s'agit pas d'un programme de dessin séquentiel («dessine ceci», puis «dessine cela», etc.), mais de «programmation événementielle» (i.e. «à base d'«évènements») où tout se passe dans une boucle infinie qui ne fait qu'attendre des évènements (clic de souris, touche au clavier, redimensionnement de la fenêtre ou tout simplement l'évènement «dessine») et appelle les fonctions correspondant à ces évènements.
Une première étape consiste donc à programmer toutes ces fonctions que l'on veut associer à des évènements.
Une seconde étape consiste à initialiser tout ce qu'il faut (e.g. la fenêtre de dessin, les boutons, barres, etc.)
Puis enfin (et seulement en fin !) on lance la fameuse boucle infinie. Ce n'est qu'une fois cette boucle lancée que l'on pourra voir quelque chose. De manière générale, la boucle se déroule de la manière suivante :
Ces étapes seront détaillées plus loin dans ce tutoriel.
NOTE : Cette boucle étant une boucle infinie, il faudra bien sûr penser à associer à un évènement particulier le fait de quitter le programme. Nous reviendrons sur ce point plus tard, dans notre second exemple.
Dans la panoplie d'outils disponibles dans la bibliothèque SDL, il existe une couche spécifique pour la gestion des représentations 2D et 3D. Cette couche s'appelle OpenGL, et c'est sur elle que nous allons maintenant nous focaliser.
NOTE : OpenGL n'est pas à strictement parler un composant de la bibliothèque SDL, mais une couche plus basse, indépendante, à laquelle on peut «parler» et que l'on peut incorporer dans les objets de la SDL.
Pour comprendre les commandes graphiques que vous allez utiliser, la seconde chose qu'il faut bien comprendre est ceci : lorsque vous appelez une commande graphique d'OpenGL, la librairie ne l'exécute pas telle quelle, mais l'applique à la «matrice courante».
Mais qu'est-ce que «la matrice courante» ?
C'est la matrice («affine», i.e. 4x4 pour un espace 3D) représentant la composition des opérations géométriques ayant été effectuées jusque là. Tous les objets et opérations graphiques sont en fait représentés par des matrices 4x4.
ATTENTION ! Cela suppose aussi que l'on utilise l'ordre de composition des fonctions : appliquer f, puis appliquer g, c'est en fait faire g(f(x)). Ceci implique que si vous faites une translation puis une rotation séquentiellement dans le code, et bien à l'écran, c'est comme s'il y avait d'abord eu une rotation, puis seulement ensuite une translation ! Faites donc attention à l'ordre de vos commandes graphiques.
Pour éviter les confusions entre plusieurs objets graphiques, il y a deux commandes très utiles : glPushMatrix() et glPopMatrix().
Ces commandes sauvent et restaurent les matrices courantes dans une pile. En pratique, ça veut dire que quand vous voulez faire des transformations, vous faites d'abord un glPushMatrix(), ensuite vous dessinez, et enfin vous faites un glPopMatrix().
Voici quelques commandes de base sur les matrices OpenGL :
glPushMatrix() | sauve la matrice courante |
glPopMatrix() | restaure la matrice courante |
glTranslated(double x, double y, double z ) | translate de (x,y,z) |
glRotated(double alpha, double x, double y, double z ) | effectue une rotation d'angle alpha autour de l'axe (x,y,z) |
glScaled(double rx, double ry, double rz ) | effectue une affinité de rapport (rx, ry rz) |
SDL et OpenGL sont installées sur les ordinateurs du CO. Si vous voulez utiliser ces bibliothèques chez vous, il faudra les installer.
Pour compiler un programme avec les bibliothèques SDL et OpenGL, il est nécessaire de donner au compilateur des directives dans ce sens.
Concrètement, dans les salles CO vous devez ajouter les options :
-lSDL -lGL -lGLU
lors de l'édition de liens.
Je vous conseille pour cela d'utiliser un «Makefile» dans lequel vous aurez ajouté :
LDLIBS = -lSDL -lGLU -lGL
Commençons par un premier exemple simple consistant à dessiner une sphère.
NOTE : Vous pouvez télécharger ici le code de ce premier exemple. Pour le compiler, voir le paragraphe ci-dessus.
La première chose à faire est donc de construire une fenêtre
permettant de dessiner de l'OpenGl.
SDL peut tout à fait fonctionner sans OpenGL
,
mais ce n'est pas ce que nous aborderons ici..
De ce fait, SDL impose une certaine structuration aux objets
graphiques utilisés, ce qui dans notre cas à pour conséquence que
la fenêtre de dessin OpenGL proprement dite sera en fait inclue
dans une fenêtre plus large (à laquelle on pourra plus tard
attacher d'autres choses comme des menus et) qui est elle même
inclue dans l'application principale.
En clair on a :
Fenêtre principale (SDL) --> contenu OpenGL
Sans expliquer les détails ici (voir les commentaires dans le code) , voici le code correspondant (que l'on pourra bien sûr modulariser en plusieurs fichiers) :
NOTE : Certains paramètres des fonctions ne sont pas expliqués. Soit ces paramètres seront expliqués plus loin, soit ils ne sont pas intéressants pour ce tutoriel. Les fonctions de la SDL commencent toutes par SDL... et celles de OpenGL par gl... ou glu..., les autres fonctions sont définies par le programmeur, c'est-à-dire vous.
// On inclut les bibliothèques nécessaires #include <SDL/SDL.h> #include <GL/gl.h> #include <GL/glu.h> int main() { SDL_Init(SDL_INIT_VIDEO); // Initialise la SDL // On crée une fenêtre de taille 800x600 SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); // et on lui donne un titre SDL_WM_SetCaption("TITRE", 0); initOpenGL(); // Initialise OpenGL while(true) // On crée une boucle infinie { // dans laquelle on effectue le dessin dessiner(); // Et on fait une petite pause pause(); } SDL_Quit(); // On quitte la SDL return 0; }
Tout ceci est bien beau, mais
ne compile pas pour le moment. Il reste en effet à implémenter les
trois fonctions initOpenGL(), dessiner() et
pause().
Ce que nous n'avons pour le moment pas fait !
La première fonction à écrire est celle qui dessine la scène, ici une sphère.
Pour dessiner une sphère en OpenGL, nous allons avoir besoin d'un objet intermédiaire appelé «quadrique» (peut importe ici ce que c'est).
Le dessin de la sphère se fait ensuite comme suit (voir les commentaires dans le code) :
void dessiner() // Dessine la scène (une sphère) { /* On crée un objet de type quadrique. * C'est l'outil utilisé pour le dessin des sphères. */ GLUquadric* sphere(gluNewQuadric()); // commence par effacer l'ancienne image glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* part du système de coordonnées de base * (dessin à l'origine : matrice identité) */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // fixe le point de vue gluLookAt(1.0, 1.0, 5.0, // position (x, y, z) de l'oeil 0.0, 0.0, 0.0, // point visé (x', y', z') 0.0, 0.0, 1.0); /* vecteur representant la direction verticale de la vue (non parallèle à la direction de visée) */ /* défini la couleur (rouge, vert, bleu) et la transparence de tout * ce qui suit. * Ici : bleu pur pas du tout transparent */ glColor4d(0.0, 0.0, 1.0, 1.0); // mode de dessin : ici, fil de fer glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // dessine une sphère de rayon 1 découpée en 30x30 "morceaux" gluSphere(sphere, 1.0, 30, 30); // Finalement, on envoie le dessin à l'écran glFlush(); SDL_GL_SwapBuffers(); gluDeleteQuadric(sphere); // On détruit le quadrique }
Il est très important de détruire le «quadrique», sinon on a une fuite mémoire, laquelle peut finir par saturer l'ordinateur.
Si l'on code le tout dans une classe (conseillé !), il serait intéressant de déclarer le «quadrique» comme attribut de la classe afin de ne pas avoir à le créer et détruire à chaque image.
Voilà ! Nous avons fini avec le dessin, et pouvons donc passer à la fonction de d'initialisation d'OpenGL. Cela se fait dans la fonction initOpenGL(), par exemple de la façon suivante (encore une fois sans détailler ici) :
void initOpenGL() // S'occupe d'initialiser quelques trucs pour OpenGL { // active la gestion de la profondeur glEnable(GL_DEPTH_TEST); // fixe la perspective glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(65.0, 4./3., 1.0, 1000.0); // fixe la couleur du fond à noir glClearColor(0.0, 0.0, 0.0, 1.0); }
Il ne reste plus qu'à écrire la dernière fonction, pause().
Le code de cette fonction sera détaillé plus loin, mais il est nécessaire au fonctionnement de la SDL. Je vous propose donc de copier la fonction et d'y revenir plus tard pour la comprendre.
void pause() { SDL_Event event; SDL_PollEvent(&event); if (event.type == SDL_QUIT) exit(0); }
Voilà ! On a terminé. Vous pouvez maintenant compiler et lancer le résultat pour voir si ça marche (ça devrait).
Voici le programme complet de ce premier exemple :
// On inclut les bibliothèques nécessaires #include <SDL/SDL.h> #include <GL/gl.h> #include <GL/glu.h> #include <cstdlib> // pour exit() // Quelques prototypes de fonctions (voir plus loin) --------------------------- void dessiner(); void initOpenGL(); void pause(); // Fonction main --------------------------------------------------------------- int main() { SDL_Init(SDL_INIT_VIDEO); // Initialise la SDL // On crée une fenêtre de taille 800x600 SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); // et on lui donne un titre SDL_WM_SetCaption("Premier exemple", 0); initOpenGL(); // Initialise OpenGL while(true) // On crée une boucle infinie { // dans laquelle on effectue le dessin dessiner(); // Et on fait une petite pause pause(); } SDL_Quit(); // On quitte la SDL return 0; } // Fonctions ------------------------------------------------------------------- void initOpenGL() // S'occupe d'initialiser quelques trucs pour OpenGL { // active la gestion de la profondeur glEnable(GL_DEPTH_TEST); // fixe la perspective glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(65.0, 4./3., 1.0, 1000.0); // fixe la couleur du fond à noir glClearColor(0.0, 0.0, 0.0, 1.0); } void dessiner() // Dessine la scène (une sphère) { /* On crée un objet de type quadrique. * C'est l'outil utilisé pour le dessin des sphères. */ GLUquadric* sphere(gluNewQuadric()); // commence par effacer l'ancienne image glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* part du système de coordonnées de base * (dessin à l'origine : matrice identité) */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // fixe le point de vue gluLookAt(1.0, 1.0, 5.0, // position (x, y, z) de l'oeil 0.0, 0.0, 0.0, // point visé (x', y', z') 0.0, 0.0, 1.0); /* vecteur representant la direction verticale de la vue (non parallèle à la direction de visée) */ /* défini la couleur (rouge, vert, bleu) et la transparence de tout * ce qui suit. * Ici : bleu pur pas du tout transparent */ glColor4d(0.0, 0.0, 1.0, 1.0); // mode de dessin : ici, fil de fer glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // dessine une sphère de rayon 1 découpée en 30x30 "morceaux" gluSphere(sphere, 1.0, 30, 30); // Finalement, on envoie le dessin à l'écran glFlush(); SDL_GL_SwapBuffers(); gluDeleteQuadric(sphere); // On détruit le quadrique } // Le code de cette fonction sera expliqué plus loin (gestion des évènements) void pause() { SDL_Event event; SDL_PollEvent(&event); if (event.type == SDL_QUIT) exit(0); }
NOTE 1 : Comme vous pouvez le constater, ce code bien que court et simple, n'est pas «orienté objets». Cela sera votre rôle d'encapsuler toutes ces fonctions dans un ensemble de classes ad hoc.
NOTE 2 : Vous avez peut être constaté que nous travaillons ici beaucoup avec les pointeurs (et cela sera vrai pour tout les objets graphiques). C'est normal, c'est justement une illustration de l'utilisation numéro 3 des pointeurs : avoir une durée de vie (tant que le programme tourne) plus grande que la portée (la «petite» fonction/méthode dans laquelle on crée l'objet en question).
Nous allons maintenant nous intéresser aux évènements simples venant du clavier. On va ici s'intéresser d'une part à faire quitter le programme si les touches «Ctrl» et «Q» sont pressées, et d'autre part à faire tourner l'objet à l'aide des flèches du clavier. J'en profiterai au passage pour vous montrer comment dessiner un cube (que l'on voit mieux tourner qu'une sphère ;-)).
Vous pouvez télécharger ici le code de ce second exemple.
Nous partons sur la base définie à l'exercice précédent (en laissant ici vide la méthode de dessin) :
#include <SDL/SDL.h> #include <GL/gl.h> #include <GL/glu.h> #include <cstdlib> // pour exit() // Quelques prototypes de fonctions (voir plus loin) --------------------------- void dessiner(); void initOpenGL(); void pause(); // Fonction main --------------------------------------------------------------- int main() { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); SDL_WM_SetCaption("Cube tournant", 0); initOpenGL(); while(true) { dessiner(); pause(); } SDL_Quit(); return 0; } // Fonctions ------------------------------------------------------------------- void initOpenGL() // S'occupe d'initialiser quelques trucs pour OpenGL { // active la gestion de la profondeur glEnable(GL_DEPTH_TEST); // fixe la perspective glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(65.0, 4./3., 1.0, 1000.0); // fixe la couleur du fond à noir glClearColor(0.0, 0.0, 0.0, 1.0); } void dessiner() // Dessine la scène { // commence par effacer l'ancienne image glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* part du système de coordonnées de base * (dessin à l'origine : matrice identité) */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // dessin a venir ici // Finalement, on envoie le dessin à l'écran glFlush(); SDL_GL_SwapBuffers(); } void pause() { SDL_Event event; SDL_PollEvent(&event); if (event.type == SDL_QUIT) exit(0); }
Le premier évènement que l'on aimerait gérer est celui qui permettrait de quitter le programme, c'est-à-dire interrompre la boucle infinie. Une solution, parmi d'autres,est de créer une fonction evenements() qui renvoie true si le programme doit continuer et false dans le cas contraire.
On peut donc prototyper la fonction evenements() de la manière suivante :
bool evenements();
Dans SDL, la gestion des évènements se déroule en trois étapes. La première est de créer un objet qui pourra contenir des informations sur l'évènement qui a eu lieu :
bool evenements() { SDL_Event monEvenement;
Il faut ensuite demander à la SDL de «remplir» cette variable. Cela se fait via l'appel à la fonction
SDL_PollEvent(SDL_Event*);
On peut alors ajouter :
bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement);
A ce moment, il ne reste plus qu'à regarder quel type d'évènement est contenu dans monEvenement. La SDL propose plein de types d'évènements, ceux qui nous intéressent ici, sont SDL_QUIT qui correspond à un clic sur la «croix» de la fenêtre et SDL_KEYDOWN qui correspond à l'appui sur une touche du clavier. On peut alors écrire la fonction de la manière suivante :
bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement); if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT return false; // On renvoit false (arrêt)
Dans le cas d'un évènement de type SDL_KEYDOWN, il faut savoir quelle touche a été enfoncée. A chaque touche du clavier, est associé un code, pour la touche «Escape», il s'agit de SDLK_ESCAPE et, pour la touche 'q', de SDLK_q . La liste complète des "codes" correspondant aux touches est disponible ici : SDLKey (il est évident qu'il ne faut pas les apprendre. Il faut juste connaître le lien pour les retrouver ;-) ). On obtient donc dans notre cas, le code suivant :
bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement); if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT return false; // On renvoit false (arrêt) else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée case SDLK_ESCAPE: // Si c'est "Esc" case SDLK_q: // Si c'est "q" return false; // On renvoie false (arrêt)
Il ne reste plus alors qu'à modifier le main() pour que la boucle se termine si la fonction renvoie false. On en profite également pour enlever la fonction pause() devenue obsolète.
int main() { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); SDL_WM_SetCaption("Cube tournant", 0); initOpenGL(); // Initialise OpenGL SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées bool continuer(true); // bool pour savoir si on doit continuer // On crée une boucle qui va tourner tant que continuer est vrai do { /* Dans laquelle on teste les évènements et voir si le programme * doit continuer */ continuer=evenements(); // Et on dessine la scène dessiner(); } while(continuer); SDL_Quit(); //On quitte la SDL return 0; }
NOTE : Vous pouvez compiler (et exécuter) le code à ce stade. Ce que je vous encourage à faire pour vérifier (et voir le résultat). Même si cela ne dessine rien, vous devriez constater que le programme se termine bien si l'on appuie sur 'Esc' ou 'q'.
Notre but premier était de dessiner un cube qui tourne. Commençons donc par dessiner le cube, puis nous finirons par la gestion des évènements clavier «flèches».
Pour dessiner le cube, il faut procéder en plusieurs étapes. La première consiste à découper ce cube en faces. En effet OpenGL ne peut dessiner que des lignes, triangles ou quadrilatères (les sphères sont des objets à part). On va donc découper le cube en six faces carrées. Chaque face est ensuite dessinée en indiquant les coordonnées de chacun de ses coins. On peut également spécifier une couleur pour la face. Ce qui donne pour une face le code :
glBegin(GL_QUADS); //On indique à OpenGL qu'on veut dessiner un quadrilatère glColor(0.0,0.0,1.0,1.0); //On spécifie une couleur (bleu). //Les couleurs sont expliquées à la fin de ce tutoriel glVertex3d(0.0,0.0,0.0); //On indique un premier coin en (0,0,0) glVertex3d(1.0,0.0,0.0); //un deuxième en (1,0,0) glVertex3d(1.0,1.0,0.0); //etc... glVertex3d(0.0,1.0,0.0); glEnd(); //On indique à OpenGL qu'on a terminé le quadrilatère
NOTE 1 : Pour dessiner un triangle, il faut utiliser glBegin(GL_TRIANGLES) et spécifier trois points ; pour une ligne, ce sera glBegin(GL_LINES) en spécifiant deux points.
NOTE 2 : On peut également spécifier une couleur par sommet, ce qui donne de jolis effets. Il faut simplement savoir que lorsqu'on appelle glColor() tout sera colorié de cette couleur jusqu'au prochain appel à cette fonction.
NOTE 3 : Si l'on veut dessiner plusieurs quadrilatères à la suite, on a pas besoin d'appeler chaque fois glBegin() et glEnd(). Une fois au début et à la fin suffit. Il faut juste qu'il y ait un nombre correct de sommets (un multiple de 4).
Le dessin de notre cube devient alors (cube d'arête 1 centré à l'origine) :
// dessine les 6 faces du cube glBegin(GL_QUADS); glColor4d(0.0, 0.0, 1.0, 1.0); // bleu glVertex3d( 0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(-0.5,-0.5, 0.5); glVertex3d( 0.5,-0.5, 0.5); glColor4d(0.0, 1.0, 1.0, 1.0); // turquoise (cyan=bleu+vert) glVertex3d(-0.5,-0.5,-0.5); glVertex3d(-0.5, 0.5,-0.5); glVertex3d( 0.5, 0.5,-0.5); glVertex3d( 0.5,-0.5,-0.5); glColor4d(1.0, 0.0, 1.0, 1.0); // violet (magenta=rouge+bleu) glVertex3d( 0.5, 0.5, 0.5); glVertex3d( 0.5, 0.5,-0.5); glVertex3d(-0.5, 0.5,-0.5); glVertex3d(-0.5, 0.5, 0.5); glColor4d(1.0, 0.0, 0.0, 1.0); // rouge glVertex3d(-0.5,-0.5,-0.5); glVertex3d( 0.5,-0.5,-0.5); glVertex3d( 0.5,-0.5, 0.5); glVertex3d(-0.5,-0.5, 0.5); glColor4d(1.0, 1.0, 0.0, 1.0); // jaune (=rouge+vert) glVertex3d( 0.5, 0.5, 0.5); glVertex3d( 0.5,-0.5, 0.5); glVertex3d( 0.5,-0.5,-0.5); glVertex3d( 0.5, 0.5,-0.5); glColor4d(1.0, 1.0, 1.0, 1.0); // blanc (=rouge+vert+bleu) glVertex3d(-0.5,-0.5,-0.5); glVertex3d(-0.5,-0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5,-0.5); glEnd();
Il ne faut bien sûr pas oublier de placer la «caméra» (i.e. le point de vue) avant, par exemple :
// fixe le point de vue gluLookAt(5.0, 0.0, 0.0, // position (x, y, z) de l'oeil 0.0, 0.0, 0.0, // point visé (x', y', z') 0.0, 0.0, 1.0); /* vecteur representant la direction verticale (non parallèle à la direction de visée) */
NOTE : Vous pouvez compiler (et
exécuter) à ce stade. Le cube devrait se dessiner...
...mais il ne peut pas encore bouger.
Pour faire tourner le cube, il faut déclarer deux variables représentant les angles de rotation par rapport aux axes y et z (coordonnées sphériques de la caméra en fait). Dans cet exemple, ces deux variables seront globales ; c'est de la mauvaise programmation, mais cela permet ici de simplifier l'exemple. A vous de faire mieux.
NOTE : Le mécanisme permettant de faire tourner le cube sera détaillé dans le quatrième exemple. Le but ici est de comprendre le fonctionnement des évènements.
double theta(35.0); double phi(20.0);
Pour faire tourner le cube, il suffit d'incrémenter la valeur d'un de ces angles lors de l'appui sur une touche. Cela se fait dans la fonction evenements() en supposant (approche modulaire) qu'il existe deux fonctions permettant de faire tourner la caméra d'un angle alpha autour d'un des axes précisés plus haut (on tourne ici par exemple de 2 degrés) : :
bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement); if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT return false; // On renvoit false (arrêt) else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée case SDLK_ESCAPE: // Si c'est "Esc" case SDLK_q: // Si c'est "q" return false; // On renvoie false (arrêt) case SDLK_UP: // Si c'est la flèche du haut RotateTheta( 2.0); // On incrémente theta break; case SDLK_DOWN: //idem pour les autres RotateTheta(-2.0); break; case SDLK_RIGHT: RotatePhi( -2.0); break; case SDLK_LEFT: RotatePhi( 2.0); break; } return true; /* Pour tous les autres évènements, le programme doit * continuer (on renvoit donc true). */ }
Reste donc à écrire ces fonctions Rotate :
void RotateTheta(double deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } void RotatePhi(double deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } }
(J'y ai ici inclus la normalisation des angles, entre 0 et 360 degrés pour phi et entre -180 et +180 pour theta.)
Pour finir, il n'y a qu'à ajouter la ligne
SDL_EnableKeyRepeat(50, 50);
juste avant la boucle principale pour que la SDL puisse gérer le cas des touches qui restent enfoncées.
Voilà pour ce second exemple ! Le code complet est :
#include <SDL/SDL.h> #include <GL/gl.h> #include <GL/glu.h> // Quelques prototypes de fonctions (voir plus loin) --------------------------- void dessiner(); void initOpenGL(); bool evenements(); void RotateTheta(double angle); void RotatePhi(double angle); // Variables globales ---------------------------------------------------------- double theta(35.0); double phi(20.0); // Fonction main --------------------------------------------------------------- int main() { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); SDL_WM_SetCaption("Cube tournant", 0); initOpenGL(); // Initialise OpenGL SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées bool continuer(true); // bool pour savoir si on doit continuer // On crée une boucle qui va tourner tant que continuer est vrai do { /* Dans laquelle on teste les évènements et voir si le programme * doit continuer */ continuer=evenements(); // Et on dessine la scène dessiner(); } while(continuer); SDL_Quit(); //On quitte la SDL return 0; } // Fonctions ------------------------------------------------------------------- void initOpenGL() // S'occupe d'initialiser quelques trucs pour OpenGL { // active la gestion de la profondeur glEnable(GL_DEPTH_TEST); // fixe la perspective glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(65.0, 4./3., 1.0, 1000.0); // fixe la couleur du fond à noir glClearColor(0.0, 0.0, 0.0, 1.0); } void dessiner() //Dessine la scène (un cube) { // commence par effacer l'ancienne image glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* part du système de coordonnées de base * (dessin à l'origine : matrice identité) */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // fixe le point de vue gluLookAt(5.0, 0.0, 0.0, // position (x, y, z) de l'oeil 0.0, 0.0, 0.0, // point visé (x', y', z') 0.0, 0.0, 1.0); /* vecteur representant la direction verticale (non parallèle à la direction de visée) */ // effectue la rotation glRotated(theta, 0.0, 1.0, 0.0); //(angle, axe) glRotated(phi, 0.0, 0.0, 1.0); // dessine les 6 faces du cube glBegin(GL_QUADS); glColor4d(0.0, 0.0, 1.0, 1.0); // bleu glVertex3d( 0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(-0.5,-0.5, 0.5); glVertex3d( 0.5,-0.5, 0.5); glColor4d(0.0, 1.0, 1.0, 1.0); // turquoise (cyan=bleu+vert) glVertex3d(-0.5,-0.5,-0.5); glVertex3d(-0.5, 0.5,-0.5); glVertex3d( 0.5, 0.5,-0.5); glVertex3d( 0.5,-0.5,-0.5); glColor4d(1.0, 0.0, 1.0, 1.0); // violet (magenta=rouge+bleu) glVertex3d( 0.5, 0.5, 0.5); glVertex3d( 0.5, 0.5,-0.5); glVertex3d(-0.5, 0.5,-0.5); glVertex3d(-0.5, 0.5, 0.5); glColor4d(1.0, 0.0, 0.0, 1.0); // rouge glVertex3d(-0.5,-0.5,-0.5); glVertex3d( 0.5,-0.5,-0.5); glVertex3d( 0.5,-0.5, 0.5); glVertex3d(-0.5,-0.5, 0.5); glColor4d(1.0, 1.0, 0.0, 1.0); // jaune (=rouge+vert) glVertex3d( 0.5, 0.5, 0.5); glVertex3d( 0.5,-0.5, 0.5); glVertex3d( 0.5,-0.5,-0.5); glVertex3d( 0.5, 0.5,-0.5); glColor4d(1.0, 1.0, 1.0, 1.0); // blanc (=rouge+vert+bleu) glVertex3d(-0.5,-0.5,-0.5); glVertex3d(-0.5,-0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5,-0.5); glEnd(); // Finalement, on envoie le dessin à l'écran glFlush(); SDL_GL_SwapBuffers(); } bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement); if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT return false; // On renvoit false (arrêt) else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée case SDLK_ESCAPE: // Si c'est "Esc" case SDLK_q: // Si c'est "q" return false; // On renvoie false (arrêt) case SDLK_UP: // Si c'est la flèche du haut RotateTheta( 2.0); // On incrémente theta break; case SDLK_DOWN: //idem pour les autres RotateTheta(-2.0); break; case SDLK_RIGHT: RotatePhi( -2.0); break; case SDLK_LEFT: RotatePhi( 2.0); break; } return true; /* Pour tous les autres évènements, le programme doit * continuer (on renvoit donc true). */ } void RotateTheta(double deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } void RotatePhi(double deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } }
Nous abordons maintenant le dessin de plusieurs objets car, comme dit dans la partie générale, cela est peu intuitif la première fois et nécessite quelques précautions.
Comme on l'a vu, pour dessiner un objet, il faut connaître les coordonnées de ses sommets. Pour dessiner plusieurs objets, on pourrait calculer la position de chacun des sommets de chacun des objets «à la main». Mais cela devient très vite fastidieux si les objets se déplacent ou si les objets ont des formes complexes. De plus cela ne permet pas de créer du code générique. OpenGL permet de gérer lui-même les positions des objets au moyen de matrices ou de «déplacements» (ce n'est pas le terme officiel, mais c'est plus intuitif).
Dans l'exemple du cube, nous avions donné les coordonnées des sommets par rapport à l'origine. On pourrait donc imaginer «déplacer l'origine» pour pouvoir dessiner un nouveau cube à un autre endroit. Pour se faire, il existe trois fonctions.
NOTE : En OpenGL, les angles sont définis en degrés. Ce qui n'est pas le cas des fonctions de cmath. Il vous faudra donc convertir les angles de radian à de degré et vice-versa si vous voulez utiliser les fonctions trigonométriques de cmath.
Un exemple sera plus clair :
//On dessine un premier carré de sommets (0,0,0),(1,0,0),(1,1,0) et (0,1,0) glBegin(GL_QUADS); //On indique à OpenGL qu'on veut dessiner un quadrilatère glVertex3d(0.0,0.0,0.0); //On indique un premier coin en (0,0,0) glVertex3d(1.0,0.0,0.0); //un deuxième en (1,0,0) glVertex3d(1.0,1.0,0.0); //etc... glVertex3d(0.0,1.0,0.0); glEnd(); glTranslated(10.0,0.0,0.0); //On déplace l'origine au point (10,0,0) /* Tout ce qui sera dessiné à partir de cette ligne sera représenté * par rapport au point (10,0,0) * * Et on dessine un deuxième carré de sommets (0,0,0),(1,0,0),(1,1,0) * et (0,1,0). * Cependant, comme on a déplacé le point de repère, le carré est en réalité * situé en (10,0,0),(11,0,0),(11,1,0) et (10,1,0) */ glBegin(GL_QUADS); //On indique à OpenGL qu'on veut dessiner un quadrilatère glVertex3d(0.0,0.0,0.0); //On indique un premier coin en (0,0,0) glVertex3d(1.0,0.0,0.0); //un deuxième en (1,0,0) glVertex3d(1.0,1.0,0.0); //etc... glVertex3d(0.0,1.0,0.0); glEnd(); // On déplace l'origine au point (0,5,0) par rapport à la dernière origine glTranslated(0.0,5.0,0.0); /* Tout ce qui sera dessiné à partir de cette ligne sera donc * représenté par rapport au point (10,5,0) */ // etc...
Il arrive souvent, que l'on veuille revenir en arrière après un déplacement. Il y a trois commandes qui permettent cela
NOTE : La sauvegarde se fait dans une pile : on peut sauvegarder autant de positions que l'on veut, mais l'on ne peut revenir qu'à la dernière sauvegardée.
Ce qu'il faut faire avant de dessiner un objet c'est donc de sauvegarder la matrice courante. On peut ensuite se translater au point où l'on souhaite dessiner. Ce qui donne pour la première sphère :
// dessin de la première sphère glPushMatrix(); // Sauvegarder l'endroit où l'on se trouve glTranslated(-1.0, 0.0, 0.5); /* Se positionner à l'endroit où l'on veut * dessiner */ glColor4d(0.0, 0.0, 1.0, 1.0); // Choisir la couleur (bleu ici) gluSphere(sphere, 1.0, 30, 30);// Dessiner une sphère
Et pour la seconde sphère :
// dessin de la seconde sphère glPushMatrix(); glTranslated(0.0, 0.0, 0.0); glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici) gluSphere(sphere, 1.5, 50, 50); glPopMatrix();
Voici donc le code complet de ce troisième exemple (vous pouvez télécharger ici le code de ce troisième exemple).
#include <SDL/SDL.h> #include <GL/gl.h> #include <GL/glu.h> // Quelques prototypes de fonctions (voir plus loin) --------------------------- void dessiner(); void initOpenGL(); bool evenements(); void RotateTheta(double angle); void RotatePhi(double angle); // Variables globales ---------------------------------------------------------- double theta(35.0); double phi(20.0); // Fonction main --------------------------------------------------------------- int main() { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); SDL_WM_SetCaption("Exemple 3", 0); initOpenGL(); // Initialise OpenGL SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées bool continuer(true); // bool pour savoir si on doit continuer // On crée une boucle qui va tourner tant que continuer est vrai do { /* Dans laquelle on teste les évènements et voir si le programme * doit continuer */ continuer=evenements(); // Et on dessine la scène dessiner(); } while(continuer); SDL_Quit(); //On quitte la SDL return 0; } // Fonctions ------------------------------------------------------------------- void initOpenGL() // S'occupe d'initialiser quelques trucs pour OpenGL { // active la gestion de la profondeur glEnable(GL_DEPTH_TEST); // fixe la perspective glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(65.0, 4./3., 1.0, 1000.0); // fixe la couleur du fond à noir glClearColor(0.0, 0.0, 0.0, 1.0); } void dessiner() //Dessine la scène (2 sphères) { GLUquadric* sphere(gluNewQuadric()); // Cree le quadrique pour les spheres // commence par effacer l'ancienne image glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* part du système de coordonnées de base * (dessin à l'origine : matrice identité) */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // fixe le point de vue gluLookAt(5.0, 0.0, 0.0, // position (x, y, z) de l'oeil 0.0, 0.0, 0.0, // point visé (x', y', z') 0.0, 0.0, 1.0); /* vecteur representant la direction verticale (non parallèle à la direction de visée) */ // effectue la rotation glRotated(theta, 0.0, 1.0, 0.0); //(angle, axe) glRotated(phi, 0.0, 0.0, 1.0); // dessin de la première sphère glPushMatrix(); // Sauvegarder l'endroit où l'on se trouve glTranslated(-1.0, 0.0, 0.5); /* Se positionner à l'endroit où l'on veut * dessiner */ glColor4d(0.0, 0.0, 1.0, 1.0); // Choisir la couleur (bleu ici) gluSphere(sphere, 1.0, 30, 30);// Dessiner une sphère glPopMatrix(); // Revenir à l'ancienne position // dessin de la seconde sphère glPushMatrix(); glTranslated(0.0, 0.0, 0.0); glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici) gluSphere(sphere, 1.5, 50, 50); glPopMatrix(); // Finalement, on envoie le dessin à l'écran glFlush(); SDL_GL_SwapBuffers(); // On détruit le quadrique gluDeleteQuadric(sphere); } bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement); if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT return false; // On renvoit false (arrêt) else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée case SDLK_ESCAPE: // Si c'est "Esc" case SDLK_q: // Si c'est "q" return false; // On renvoie false (arrêt) case SDLK_UP: // Si c'est la flèche du haut RotateTheta( 2.0); // On incrémente theta break; case SDLK_DOWN: //idem pour les autres RotateTheta(-2.0); break; case SDLK_RIGHT: RotatePhi( -2.0); break; case SDLK_LEFT: RotatePhi( 2.0); break; } return true; /* Pour tous les autres évènements, le programme doit * continuer (on renvoit donc true). */ } void RotateTheta(double deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } void RotatePhi(double deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } }
NOTE : Vous remarquerez que les deux sphères s'entrecoupent. C'est normal au vu des positions et des rayons que l'on a donné. Vous pouvez ainsi constater qu'OpenGL s'occupe de gérer l'affichage des choses qui sont devant et derrière de manière correcte. Ce n'est pas à vous de le faire.
On va maintenant étendre le positionnement du point de vue via les touches au clavier. Plus précisément, je vous propose ici de faire tourner la caméra à l'aide des touches «flèches» comme précédemment, de la faire avancer ou reculer à l'aide des touches «page up» et «page down», et de la faire revenir à un point fixe à l'aide de la touche «home».
Vous pouvez télécharger ici le code de ce quatrième exemple.
Pour déplacer le point de vue/la caméra et non l'objet, il faut alors garder à tout moment en mémoire la position/orientation de celle-ci.
La première chose à faire est donc de prévoir les variables nécessaires pour stocker la «caméra». Nous allons pour simplifier utiliser ici des attributs de la classe Vue_OpenGL, mais dans un programme plus conséquent il faudrait bien sûr plutôt faire une classe pour représenter une «caméra» (et mieux concevoir l'architecture du tout).
Dans la version proposée ici (la caméra vise l'origine), une
«caméra» nécessite trois grandeurs : ses deux angles de
rotation, comme précédemment, et une distance à l'origine
(i.e. coordonnée sphériques en fait).
Dans une version plus complète, on pourrait de plus préciser l'axe
de visée de la caméra (au lieu de viser l'origine).
De façon simpliste (à améliorer), on ajoute alors à notre fichier :
double theta(35.0); double phi(20.0); double r(5.0);
L'utilisation de cette caméra se fait ensuite comme précédemment par son positionnement avant de dessiner les objets :
// place la caméra gluLookAt(r, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0); glRotated(theta, 0.0, 1.0, 0.0); glRotated(phi, 0.0, 0.0, 1.0);
Il faut maintenant penser à la gestion de cette «caméra» via les touches du clavier. Cela se fait de façon similaire à ce que nous avions fait à l'exemple 2. Il suffit d'étendre les différents cas :
case SDLK_PAGEUP: deplace(-1.0); // On se rapproche break; case SDLK_PAGEDOWN: deplace(1.0); // On s'éloigne break; case SDLK_HOME: r = 5.0; // On revient à un point fixé theta = 35.0; phi = 20.0; break;
avec :
void deplace(double dr) { r += dr; if (r < 1.0) r = 1.0; else if (r > 1000.0) r = 1000.0; }
NOTE : Il y a une singularité au moment où la distance atteint 0. Le programmeur consciencieux devra donc éviter cela.
Pour finir (et par esthétisme), j'ai ajouté une troisième sphère ; ce qui donne pour le code complet de ce quatrième exemple (les principales différences avec l'exemple précédent sont indiquées en gras) :
#include <SDL/SDL.h> #include <GL/gl.h> #include <GL/glu.h> // Quelques prototypes de fonctions (voir plus loin) --------------------------- void dessiner(); void initOpenGL(); bool evenements(); void RotateTheta(double angle); void RotatePhi(double angle); // Variables globales ---------------------------------------------------------- double theta(35.0); double phi(20.0); double r(5.0); // Fonction main --------------------------------------------------------------- int main() { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); SDL_WM_SetCaption("Exemple 4", 0); initOpenGL(); // Initialise OpenGL SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées bool continuer(true); // bool pour savoir si on doit continuer // On crée une boucle qui va tourner tant que continuer est vrai do { /* Dans laquelle on teste les évènements et voir si le programme * doit continuer */ continuer=evenements(); // Et on dessine la scène dessiner(); } while(continuer); SDL_Quit(); //On quitte la SDL return 0; } // Fonctions ------------------------------------------------------------------- void initOpenGL() // S'occupe d'initialiser quelques trucs pour OpenGL { // active la gestion de la profondeur glEnable(GL_DEPTH_TEST); // fixe la perspective glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(65.0, 4./3., 1.0, 1000.0); // fixe la couleur du fond à noir glClearColor(0.0, 0.0, 0.0, 1.0); } void dessiner() // Dessine la scène (3 sphères) { GLUquadric* sphere(gluNewQuadric()); // Cree le quadrique pour les spheres // commence par effacer l'ancienne image glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* part du système de coordonnées de base * (dessin à l'origine : matrice identité) */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // place la caméra gluLookAt(r, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0); glRotated(theta, 0.0, 1.0, 0.0); glRotated(phi, 0.0, 0.0, 1.0); // dessin de la première sphère glPushMatrix(); // Sauvegarder l'endroit où l'on se trouve glTranslated(-1.0, 0.0, 0.5); /* Se positionner à l'endroit où l'on veut * dessiner */ glColor4d(0.0, 0.0, 1.0, 1.0); // Choisir la couleur (bleu ici) gluSphere(sphere, 1.0, 30, 30);// Dessiner une sphère glPopMatrix(); // Revenir à l'ancienne position // dessin de la seconde sphère glPushMatrix(); glTranslated(0.0, 0.0, 0.0); glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici) gluSphere(sphere, 1.5, 50, 50); glPopMatrix(); // dessin de la troisième sphère glPushMatrix(); glTranslated(1.0, 0.0, 0.5); glColor4d(0.0, 0.8, 0.0, 1.0); //vert gluSphere(sphere, 1.0, 50, 50); glPopMatrix(); // Finalement, on envoie le dessin à l'écran glFlush(); SDL_GL_SwapBuffers(); // On détruit le quadrique gluDeleteQuadric(sphere); } void deplace(double dr) { r += dr; if (r < 1.0) r = 1.0; else if (r > 1000.0) r = 1000.0; } bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement); if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT return false; // On renvoit false (arrêt) else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée case SDLK_ESCAPE: // Si c'est "Esc" case SDLK_q: // Si c'est "q" return false; // On renvoie false (arrêt) case SDLK_UP: // Si c'est la flèche du haut RotateTheta( 2.0); // On incrémente theta break; case SDLK_DOWN: //idem pour les autres RotateTheta(-2.0); break; case SDLK_RIGHT: RotatePhi( -2.0); break; case SDLK_LEFT: RotatePhi( 2.0); break; case SDLK_PAGEUP: deplace(-1.0); // On se rapproche break; case SDLK_PAGEDOWN: deplace(1.0); // On s'éloigne break; case SDLK_HOME: r = 5.0; // On revient à un point fixé theta = 35.0; phi = 20.0; break; } return true; /* Pour tous les autres évènements, le programme doit * continuer (on renvoit donc true). */ } void RotateTheta(double deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } void RotatePhi(double deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } }
Le terme «temps réel» représente le fait que le temps (physique) qui s'écoule a une signification dans le programme. Jusqu'ici dans vos programmes, l'utilisateur pouvait attendre 1 ou 10 minutes à l'invite d'un «cin» sans que cela ne change en rien le comportement du programme. Dans un processus «temps réel», le programme continue par contre de s'exécuter, que l'utilisateur agisse ou non. Ceci permet par exemple d'animer de façon réaliste les éléments du monde que l'on représente.
Considérons le cas d'une balle qu'on lâche depuis une certaine hauteur. On pourrait, comme dans l'exercice que vous avez fait au premier semestre, calculer à l'avance le temps au bout duquel la balle touchera le sol. Mais dans une simulation physique en temps réel, on voudrait avoir la position de la balle à chaque instant, par exemple pour pouvoir l'afficher.
On doit donc pouvoir être capable de décrire à chaque instant la nouvelle position de la balle en fonction de la position précédente et du temps dt écoulé entre deux calculs. Ce temps est simplement le temps que l'ordinateur a mis pour calculer et afficher la dernière position.
Dans une simulation numérique non temps réel, cet intervalle dt est fixé à une valeur arbitraire, aussi petite que la précision de calcul voulue le nécessite (voir cours d'analyse numérique).
Dans un programme «temps réel», c'est par contre la puissance de la machine qui détermine la valeur de dt : plus la scène est complexe à animer et afficher, plus dt sera grand, et plus la simulation sera approximative et l'animation saccadée.
NOTE : La raison pour laquelle on ne fixe pas à l'avance l'intervalle dt est qu'on a a priori aucune idée du temps que prendra le calcul (et l'affichage !) d'une image et, surtout, qu'on n'a aucune garantie que ce temps restera constant : plus il y a d'éléments à prendre en compte, plus ce temps augmentera. On s'en rend bien compte dans certains jeux vidéos : lorsqu'il y a un phénomène complexe (e.g. une explosion) ou trop d'unités à gérer, c'est le nombre d'images par seconde qui diminue et non le temps qui se dilate.
Concrètement, dt est donné par un «timer», qui est tout simplement une «minuterie interne» de la bibliothèque SDL.
La simulation est donc une boucle qui répète en permanence plusieurs étapes, parmi lesquelles :
En théorie aucun calcul concernant la simulation n'est à effectuer dans ces deux dernières phases.
NOTE : dans SDL, comme dans beaucoup d'autres bibliothèques similaires, le programme est «averti» qu'il doit procéder à l'une ou l'autre étape : c'est ce que l'on appelle de la «programmation événementielle» (i.e. «à base d'«évènements»).
Enfin, lorsqu'une certaine condition d'arrêt est atteinte (e.g. un certain délai dépassé, une précision suffisante ou un évènement particulier [e.g. clavier]), on arrête simplement le programme.
Passons maintenant à un exemple.
On va ici introduire une petite évolution «temps réel» dans notre exemple précédent. Pour cela je propose de faire tourner (simplement, de façon proportionnelle au temps (i.e. mouvement circulaire uniforme)) une petite boule rouge autour des trois sphères précédemment dessinées.
Vous pouvez télécharger ici le code de ce cinquième exemple.
La première chose à faire est donc de prévoir l'élément qui va se déplacer. Il faut ici simplement dessiner une sphère, mais dont la position dépend du temps. Par exemple :
// Dessin de la petite sphère qui tourne glPushMatrix(); glRotated(position, 0.0, 0.0, 1.0); /* on la fait tourner de "position" * autour de Oz... */ glTranslated(2.0, 0.0, 0.0); /* ...sur un cercle de rayon 2.0 */ glColor4d(1.0, 0.0, 0.0, 1.0); // couleur rouge gluSphere(sphere, 0.1, 30, 30); glPopMatrix();
Il s'agit ici d'une sphère dont le centre évolue sur un cercle de rayon 2 centré sur l'origine. Dans cet exemple jouet, le paramètre dépendant du temps est appelé position ajouté de façon globale au programme (dans une bonne conception d'un projet plus large, il faudrait bien sûr faire autrement !).
En l'état, le programme compile et fait un joli dessin, mais pas grand chose ne bouge. C'est normal nous n'avons pas encore ajouté dépendance temporelle à la variable position.
Pour se faire, on peut par exemple incrémenter l'angle de 1 degré à chaque tour de la boucle principale (dans le main()). Cela fonctionne, mais tout cela se passe sans «temps réel» (au sens ou les étapes du programme ne sont pas liées au temps physique se déroulant). Il faut pour cela définir un timer (et un pas de temps) qui va faire évoluer le système à chaque pas de temps :
int dt(20); // Pas de temps en milliseconde ... SDL_TimerID monTimer(0); // Timer
Pour utiliser les «timers», il faut de plus dire à la SDL qu'on en a besoin. Cela se fait en modifiant la ligne d'initialisation de la SDL comme suit :
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
Il faut maintenant écrire la fonction qui sera appelée par le timer. Cette fonction doit obligatoirement avoir le prototype suivant :
Uint32 fonction(Uint32, void*)
Uint32 est un type déclaré dans la SDL, équivalent aux int (peut importe, de toute manière vous n'utiliserez pas les paramètres de cette fonction).
De plus, cette fonction doit impérativement renvoyer autre chose que 0.
Dans notre cas, on peut donc écrire :
Uint32 evolution(Uint32, void*) { /* On gère ici l'évolution du système. Dans le cas simple de cet exemple : la position (angulaire) augemente de 1 degré tous les pas de temps (i.e. 1 degré par 20 ms) */ position += 1.0; while (position > 360.0) position -= 360.0; return 1; }
Il ne reste plus alors qu'à créer un timer et à l'associer à cette fonction
/* On cree un timer, de pas de temps dt, et qui doit appeler la fonction * evolution */ monTimer = SDL_AddTimer(dt, evolution, 0);
Le timer démarre automatiquement et va, toutes les 20ms (dt), appeler la fonction évolution.
NOTE : Vous pouvez compiler (et exécuter)
à ce stade. La boule rouge devrait se dessiner...
...et tourner. Notez que l'on peut toujours, en même temps, utiliser
les évènements clavier pour déplacer le point de vue.
Je vous propose pour finir sur ce sujet de rajouter une touche pour faire une pause dans la simulation. Comment faire ?
Il suffit pour cela de suspendre le «timer» à chaque fois qu'une pause a été demandée (et de le reprendre lors de la prochaine frappe au clavier). Il faut bien sûr associer, dans la fonction de gestion des évènements clavier, une touche (par exemple la barre espace ici) à cette action :
case SDLK_SPACE: if (monTimer != 0) { // Si le timer tourne... SDL_RemoveTimer(monTimer); // ...on l'enleve monTimer = 0; } else { // Sinon... monTimer = SDL_AddTimer(dt, evolution, 0); // ...on le remet } break;
Voici donc le code complet de ce cinquième exemple. Les principales différences avec l'exemple précédent sont indiquées en gras.
#include <SDL/SDL.h> #include <GL/gl.h> #include <GL/glu.h> // Quelques prototypes de fonctions (voir plus loin) --------------------------- void dessiner(); void initOpenGL(); bool evenements(); void RotateTheta(double angle); void RotatePhi(double angle); // Variables globales ---------------------------------------------------------- double theta(35.0); double phi(20.0); double r(5.0); // Pour la simulation temps réel ----------------------------------------------- Uint32 evolution(Uint32, void*); double position(0.0); // La position (angle) de la petite sphère qui tourne unsigned int dt(20); // Le pas de temps, en milliseconde SDL_TimerID monTimer(0); // Timer // Fonction main --------------------------------------------------------------- int main() { //On initialise la SDL, avec "timer" cette fois SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER); SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); SDL_WM_SetCaption("Simulation temps reel", 0); initOpenGL(); // Initialise OpenGL SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées bool continuer(true); // bool pour savoir si on doit continuer /* On cree un timer, de pas de temps dt, et qui doit appeler la fonction * evolution */ monTimer = SDL_AddTimer(dt, evolution, 0); // On crée une boucle qui va tourner tant que continuer est vrai do { /* Dans laquelle on teste les évènements et voir si le programme * doit continuer */ continuer=evenements(); // Et on dessine la scène dessiner(); } while(continuer); SDL_Quit(); //On quitte la SDL return 0; } // Fonctions ------------------------------------------------------------------- Uint32 evolution(Uint32, void*) { /* On gère ici l'évolution du système. Dans le cas simple de cet exemple : la position (angulaire) augemente de 1 degré tous les pas de temps (i.e. 1 degré par 20 ms) */ position += 1.0; while (position > 360.0) position -= 360.0; return 1; } void initOpenGL() // S'occupe d'initialiser quelques trucs pour OpenGL { // active la gestion de la profondeur glEnable(GL_DEPTH_TEST); // fixe la perspective glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(65.0, 4./3., 1.0, 1000.0); // fixe la couleur du fond à noir glClearColor(0.0, 0.0, 0.0, 1.0); } void dessiner() // Dessine la scène (3 sphères) { GLUquadric* sphere(gluNewQuadric()); // Crée le quadrique pour les spheres // commence par effacer l'ancienne image glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* part du système de coordonnées de base * (dessin à l'origine : matrice identité) */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // place la caméra gluLookAt(r, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0); glRotated(theta, 0.0, 1.0, 0.0); glRotated(phi, 0.0, 0.0, 1.0); // dessin de la première sphère glPushMatrix(); // Sauvegarder l'endroit où l'on se trouve glTranslated(-1.0, 0.0, 0.5); /* Se positionner à l'endroit où l'on veut * dessiner */ glColor4d(0.0, 0.0, 1.0, 1.0); // Choisir la couleur (bleu ici) gluSphere(sphere, 1.0, 30, 30);// Dessiner une sphère glPopMatrix(); // Revenir à l'ancienne position // dessin de la seconde sphère glPushMatrix(); glTranslated(0.0, 0.0, 0.0); glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici) gluSphere(sphere, 1.5, 50, 50); glPopMatrix(); // dessin de la troisième sphère glPushMatrix(); glTranslated(1.0, 0.0, 0.5); glColor4d(0.0, 0.8, 0.0, 1.0); //vert gluSphere(sphere, 1.0, 50, 50); glPopMatrix(); // Dessin de la petite sphère qui tourne glPushMatrix(); glRotated(position, 0.0, 0.0, 1.0); /* on la fait tourner de "position" * autour de Oz... */ glTranslated(2.0, 0.0, 0.0); /* ...sur un cercle de rayon 2.0 */ glColor4d(1.0, 0.0, 0.0, 1.0); // couleur rouge gluSphere(sphere, 0.1, 30, 30); glPopMatrix(); // Finalement, on envoie le dessin à l'écran glFlush(); SDL_GL_SwapBuffers(); // On détruit le quadrique gluDeleteQuadric(sphere); } void deplace(double dr) { r += dr; if (r < 1.0) r = 1.0; else if (r > 1000.0) r = 1000.0; } bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement); if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT return false; // On renvoit false (arrêt) else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée case SDLK_ESCAPE: // Si c'est "Esc" case SDLK_q: // Si c'est "q" return false; // On renvoie false (arrêt) case SDLK_UP: // Si c'est la flèche du haut RotateTheta( 2.0); // On incrémente theta break; case SDLK_DOWN: //idem pour les autres RotateTheta(-2.0); break; case SDLK_RIGHT: RotatePhi( -2.0); break; case SDLK_LEFT: RotatePhi( 2.0); break; case SDLK_PAGEUP: deplace(-1.0); // On se rapproche break; case SDLK_PAGEDOWN: deplace(1.0); // On s'éloigne break; case SDLK_HOME: r = 5.0; // On revient à un point fixé theta = 35.0; phi = 20.0; break; case SDLK_SPACE: if (monTimer != 0) { // Si le timer tourne... SDL_RemoveTimer(monTimer); // ...on l'enleve monTimer = 0; } else { // Sinon... monTimer = SDL_AddTimer(dt, evolution, 0); // ...on le remet } break; } return true; /* Pour tous les autres évènements, le programme doit * continuer (on renvoit donc true). */ } void RotateTheta(double deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } void RotatePhi(double deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } }
Qu'ai-je oublié ?...
Plein de choses bien sûr, mais ceci n'est qu'un très modeste tutoriel. Pour aller plus loin, voir dans la bibliographie ci-dessous.
Ce que vous me demanderez sûrement pour le projet :
Comment sont codées les couleurs ?
Elles sont codées en «RVBA», c'est-à-dire «Rouge-Vert-Bleu-Alpha». Les trois premiers nombres (entre 0 et 1 avec les fonctions utilisées ici) codent pour le rouge, le vert et le bleu. Par exemple (1,0,0) c'est un rouge pur, (1,1,0) c'est un jaune (=rouge+vert) pur, (0.5, 0.5, 0.5) c'est un gris à 50%, (0,0,0) c'est noir et (1,1,1) c'est blanc, etc.
Le dernier chiffre est un degré de transparence, qui ne sera pas abordé dans ce tutoriel (donc toujours à 1.0).
Pour plus de détails voir ici (en anglais) et ici (en français mais moins clair à mon avis).
Comment dessiner autre chose que des sphères ?
Pour dessiner des cubes et des sphères, ou toute transformation affine de ces objets, voir les exemples précédents.
Pour le reste, il faut décomposer l'objet à dessiner en facettes triangulaires ou rectangulaires (cf l'exemple du cube). Voici un exemple pour une pyramide faite de quatre triangles (colorés ici) :
glBegin(GL_TRIANGLES); glColor4d(0.0, 0.0, 1.0, 1.0); glVertex3d(0.0, 0.0, 0.0); glVertex3d(0.0, 1.0, 0.0); glVertex3d(1.0, 0.0, 0.0); glColor4d(0.0, 1.0, 0.0, 1.0); glVertex3d(0.0, 0.0, 0.0); glVertex3d(0.0, 1.0, 0.0); glVertex3d(1.0, 1.0, 1.0); glColor4d(1.0, 1.0, 1.0, 1.0); glVertex3d(0.0, 1.0, 0.0); glVertex3d(1.0, 0.0, 0.0); glVertex3d(1.0, 1.0, 1.0); glColor4d(1.0, 0.0, 0.0, 1.0); glVertex3d(0.0, 0.0, 0.0); glVertex3d(1.0, 0.0, 0.0); glVertex3d(1.0, 1.0, 1.0); glEnd();
Pour plus de détails, voir ici.
Comment faire des dessins 2D ?
Pour dessiner en 2D en OpenGl, il suffit en fait de dessiner dans un plan perpendiculaire à la direction de visée. Je n'ai donc pas grand chose de plus à dire ici que ce que j'ai présenté précédemment.
Par contre, pour dessiner de proche en proche (approximation linéaire) des fonction dans le plan, il peut être utile d'utiliser la directive GL_LINE_SPLIT (voir ici pour plus de détails sur les directives de dessin en OpenGL).
Le principe est de déclarer une section de dessin par lignes brisées à l'aide de la commande :
glBegin(GL_LINE_STRIP);
puis d'ajouter les points de la ligne à l'aide de glVertex3d :
glVertex3d(x, y, 0.0);
et de terminer la section de dessin de la ligne brisée par :
glEnd();
Voici un exemple complet qui dessine la fonction sinus (vous pouvez télécharger le code ici) :
#include <SDL/SDL.h> #include <GL/gl.h> #include <GL/glu.h> #include <cmath> // pour la fonction sin() // Quelques prototypes de fonctions (voir plus loin) --------------------------- void dessiner(); void initOpenGL(); bool evenements(); // Fonction main --------------------------------------------------------------- int main() { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(800, 600, 32, SDL_OPENGL); SDL_WM_SetCaption("Fonction sinus", 0); initOpenGL(); // Initialise OpenGL SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées bool continuer(true); // bool pour savoir si on doit continuer // On crée une boucle qui va tourner tant que continuer est vrai do { /* Dans laquelle on teste les évènements et voir si le programme * doit continuer */ continuer=evenements(); // Et on dessine la scène dessiner(); } while(continuer); SDL_Quit(); //On quitte la SDL return 0; } // Fonctions ------------------------------------------------------------------- void initOpenGL() // S'occupe d'initialiser quelques trucs pour OpenGL { // active la gestion de la profondeur glEnable(GL_DEPTH_TEST); // fixe la perspective glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(65.0, 4./3., 1.0, 1000.0); // fixe la couleur du fond à noir glClearColor(0.0, 0.0, 0.0, 1.0); } void dessiner() // Dessine un sinus dans le plan z=0 { // commence par effacer l'ancienne image glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* part du système de coordonnées de base * (dessin à l'origine : matrice identité) */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // place la caméra gluLookAt(0.0, 0.0, 7.5, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); const double xmax(6.0); const double xmin(-xmax); const double ymax(4.5); const double ymin(-ymax); /* dessin du cadre (pourrait être fait autrement, mais ici pour illustrer la primitive LINE_STRIP) */ glColor4d(1.0, 1.0, 1.0, 1.0); glBegin(GL_LINE_STRIP); glVertex3d(xmax, ymin, 0.0); glVertex3d(xmax, ymax, 0.0); glVertex3d(xmin, ymax, 0.0); glVertex3d(xmin, ymin, 0.0); glVertex3d(xmax, ymin, 0.0); glEnd(); // dessin (en bleu) de l'axe des abscisses glColor4d(0.0, 0.0, 1.0, 1.0); glBegin(GL_LINE_STRIP); glVertex3d(xmin, 0.0, 0.0); glVertex3d(xmax, 0.0, 0.0); glEnd(); // dessin de l'axe des ordonnées glBegin(GL_LINE_STRIP); glVertex3d( 0.0, ymin, 0.0); glVertex3d( 0.0, ymax, 0.0); glEnd(); // dessin (en vert et en guise d'exemple) de la fonction sinus glColor4d(0.0, 1.0, 0.0, 1.0); const double pas(0.1); glBegin(GL_LINE_STRIP); for (double x(xmin); x <= xmax; x += pas) { glVertex3d( x, 0.8*ymax*sin(x), 0.0); } glEnd(); // Finalement, on envoie le dessin à l'écran glFlush(); SDL_GL_SwapBuffers(); } bool evenements() { SDL_Event monEvenement; // On demande à la SDL quel évènement a eu lieu SDL_PollEvent(&monEvenement); if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT return false; // On renvoit false (arrêt) else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée case SDLK_ESCAPE: // Si c'est "Esc" case SDLK_q: // Si c'est "q" return false; // On renvoie false (arrêt) } return true; /* Pour tous les autres évènements, le programme doit * continuer (on renvoit donc true). */ }
Comment gérer des évènements «souris» ?
La SDL propose 3 types d'évènements associés à la souris. Le déplacement, l'appui sur un bouton ou le relâchement d'un bouton. Comme pour le reste des évènements,tout se passe dans la fonction evenements()
if (monEvenement.type == SDL_MOUSEMOTION) { // Déplacement de la souris ... monEvenement.x ... // On récupère la position X de la souris ... monEvenement.y ... // On récupère la position Y de la souris ... monEvenement.xrel ... // Déplacement de la souris en X ... monEvenement.yrel ... // Déplacement de la souris en Y } else if (monEvenement.type == SDL_MOUSEBUTTONDOWN) { // Appui sur un bouton switch (monEvenement.button){ case SDL_BUTTON_LEFT: // clic gauche ... case SDL_BUTTON_RIGHT: // clic droit ... case SDL_BUTTON_MIDDLE: //clic bouton du milieu ... } } else if (monEvenement.type == SDL_MOUSEBUTTONUP) {// Relachement d'un bouton ... }
Il est également possible de cacher le curseur de la souris avec la fonction SDL_ShowCursor(SDL_DISABLE). Le lecteur attentif et inventif peut supposer très justement qu'un appel à SDL_ShowCursor(SDL_ENABLE) réaffiche le curseur.
Comment faire une fenêtre en plein écran ?
Il faut modifier l'appel à la fonction de création de la fenêtre :
SDL_SetVideoMode(800, 600, 32, SDL_OPENGL | SDL_FULLSCREEN);
NOTE: Il faut avoir créer un moyen de quitter le programme avant de faire du plein écran. Sinon vous ne pourrez pas quitter votre programme du tout.
Comment modifier la perspective ?
On peut modifier la perspective en modifiant les paramètres de la fonction gluPerspective(70.0, 4./3., 1.0, 1000.0) dans la fonction initOpenGL(). Le premier paramètre correspond à l'angle d'ouverture du cône de vision. Habituellement cette valeur se situe entre 65 et 100. Le deuxième paramètre est le format de la fenêtre, soit ici 4/3 puisque on a une fenêtre 800x600, largeur/hauteur pour d'autres tailles. Les deux derniers paramètres définissent quels objets seront représentés à l'écran. Ici, tout se qui se situe à une distance entre 1 et 1000 de l'oeil sera dessiné à l'écran. Pour des raisons de performance, il est déconseillé de mettre moins que 1 pour la limite inférieure.
Que faire si je ne sais plus où je me situe dans l'espace après avoir fait trop de déplacements ?
Une possibilité est de revenir à l'origine en appelant la fonction glLoadIdentity(), ou à un autre point fixe comme nous l'avons montré dans l'exemple 4.
Une autre possibilité est d'appeler la fonction dessineAxes() définie ci-dessous qui dessine un système d'axe orthonormé de longueur 1 à l'endroit où l'on se situe. L'axe X est en rouge, l'axe Y est en vert et l'axe Z est en bleu. Cette fonction peut s'avérer très utile pour comprendre le fonctionnement des transformations.
void dessineAxes() { glPushMatrix(); glBegin(GL_LINES); glColor4d(1.0,0.0,0.0,1.0); glVertex3i(0,0,0); glVertex3i(1,0,0); glColor4d(0.0,1.0,0.0,1.0); glVertex3i(0,0,0); glVertex3i(0,1,0); glColor4d(0.0,0.0,1.0,1.0); glVertex3i(0,0,0); glVertex3i(0,0,1); glEnd(); glPopMatrix(); }
Que faire si je ne sais pas comment utiliser une fonction ou si je ne sais pas comment réaliser ce que j'aimerais ?
La première chose à faire est de chercher dans la documentation, via les liens fournis ci-dessous. Une grande partie du temps du programmeur est consacré à la recherche d'informations dans une documentation. Si vous n'avez pas trouvé ou que vous n'êtes pas satisfait de la réponse, posez votre question sur le forum du cours.