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 OpenGL et glut. 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 glut 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 glut 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 SDL à la place de glut.
La première chose qu'il faut bien comprendre quand on utilise OpenGL, 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 un boucle infinie qui ne fait qu'attendre des évènements (clic de souris, touche au clavier, mais aussi redimensionnement de la fenêtre ou tout simplement «dessin») 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)
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.
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.
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 ou de glut, 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 :
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) |
glutSolidSphere(double rayon, 30, 30 ) | construit une sphère |
glutSolidCube(double cote) | construit un cube |
glutSolidTorus( double rayon_interieur, double rayon_exterieur, 30, 30 ) | dessine un tore |
Pour compiler (ou plus exactement faire l'édition de liens («linker») d') un programme avec la bibliothèque glut, il est nécessaire de donner au compilateur des directives dans ce sens.
Concrètement, dans les salles CO vous devez ajouter les options :
-L/usr/X11R6/lib/ -lglut -lGLU -lGL -lXmu -lXi
lors de l'édition de liens.
Je vous conseille pour cela d'utiliser un «Makefile» dans lequel vous aurez ajouté :
LDFLAGS = -L/usr/X11R6/lib/ LDLIBS = -lglut -lGLU -lGL -lXmu -lXi
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.
Comme dit plus haut, la première chose à faire est de définir les fonctions à associer à des évènements. Les deux fonctions de base qu'il faut au moins définir sont la fonction de dessin et celle du redimensionnement de la fenêtre.
Commençons par la première (dessin) :
/* Fonction effectuant le dessin. */ void dessine() { // mode de dessin : couleur et 3D ("Z-buffer") glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // part du système de coordonnées de base (matrice identité) 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é 0.0, 1.0, 0.0); /* verticale de la vue (non parallèle à la direction de visée) */ /* défini la couleur (rouge, vert, bleu) et la transparence. * Ici : bleu pur pas du tout transparent */ glColor4d(0.0, 0.0, 1.0, 1.0); // dessine en "fil de fer" une sphère de rayon 1 glutWireSphere(1.0, 30, 30); // ~ affiche glutSwapBuffers(); }
La fonction de redimensionnement se fait ensuite de la façon suivante :
/* Fonction utilisée lors du "re-dessin" de la fenêtre graphique, * en particulier lors de son redimensionnement. * "largeur" et "hauteur" sont les (nouvelles) tailles de la fenêtre. */ void reshape(int largeur, int hauteur) { // Evite une division par zéro if (hauteur <= 0) hauteur = 1; double ratio(largeur); ratio /= hauteur; // ratio est un double : évite la division entière // Reset the coordinate system before modifying glMatrixMode(GL_PROJECTION); glLoadIdentity(); // Set the new viewport size glViewport(0, 0, largeur, hauteur); // Set the correct perspective. gluPerspective(45.0, ratio, 1.0, 1000.0); // Choose the projection matrix to be the matrix // manipulated by the following calls glMatrixMode(GL_MODELVIEW); // Set the projection matrix to be the identity matrix glLoadIdentity(); gluLookAt(1.0, 1.0, 5.0, // position (x, y, z) de l'oeil 0.0, 0.0, 0.0, // point visé 0.0, 1.0, 0.0); /* verticale de la vue (non parallèle à la direction de visée) */ }
On peut maintenant créer le programme principal.
Il faut d'abord inclure le header de glut (lequel inclut OpenGl puisque glut utilise OpenGL) :
#include <GL/glut.h>
Ensuite, dans le main(), on va :
int main(int argc, char* argv[]) { // initialisation de glut glutInit(&argc, argv); // initialisation du mode de dessin glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); // initialisation de la fenêtre (en donnant sa taille) glutInitWindowSize(800, 600); // réalisation de cette fenêtre (et en lui donnant un titre) glutCreateWindow("Premier exemple"); }
glutDisplayFunc(dessine);
glutReshapeFunc(reshape);
glutMainLoop();
Il reste un dernier petit «truc». Parfois, en fonction de l'état de la fenêtre au départ (par rapport aux autres fenêtres de votre environnement) il se peut que le dessin ne soit pas effectué (i.e. en fait que l'évènement «affichage» n'ait pas encore eu lieu). Pour éviter ce petit désagrément, il faut ajouter la fonction suivante :
void idle(void) { glutPostRedisplay(); glutIdleFunc(0); }
et l'associer à la gestion de l'évènement «idle» :
glutIdleFunc(idle);
Voici donc pour finir le programme complet de ce premier exemple :
/* Premier exemple simple d'utilisation de la bibliothèque glut. * A interrompre en tapant "Control c". */ #include <GL/glut.h> /* Fonction utilisée lors du "re-dessin" de la fenêtre graphique, * en particulier lors de son redimensionnement. * "largeur" et "hauteur" sont les (nouvelles) tailles de la fenêtre. */ void reshape(int largeur, int hauteur) { // Evite une division par zéro if (hauteur <= 0) hauteur = 1; double ratio(largeur); ratio /= hauteur; // ratio est un double : évite la division entière // Reset the coordinate system before modifying glMatrixMode(GL_PROJECTION); glLoadIdentity(); // Set the new viewport size glViewport(0, 0, largeur, hauteur); // Set the correct perspective. gluPerspective(45.0, ratio, 1.0, 1000.0); // Choose the projection matrix to be the matrix // manipulated by the following calls glMatrixMode(GL_MODELVIEW); // Set the projection matrix to be the identity matrix glLoadIdentity(); gluLookAt(1.0, 1.0, 5.0, // position (x, y, z) de l'oeil 0.0, 0.0, 0.0, // point visé 0.0, 1.0, 0.0); /* verticale de la vue (non parallèle à la direction de visée) */ } /* Fonction effectuant le dessin. */ void dessine() { // mode de dessin : couleur et 3D ("Z-buffer") glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // part du système de coordonnées de base (matrice identité) 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é 0.0, 1.0, 0.0); /* verticale de la vue (non parallèle à la direction de visée) */ /* défini la couleur (rouge, vert, bleu) et la transparence. * Ici : bleu pur pas du tout transparent */ glColor4d(0.0, 0.0, 1.0, 1.0); // dessine en "fil de fer" une sphère de rayon 1 glutWireSphere(1.0, 30, 30); // ~ affiche glutSwapBuffers(); } /* "truc" pour avoir le dessin correct dès le début */ void idle(void) { glutPostRedisplay(); glutIdleFunc(0); } /* PROGRAMME PRINCIPAL */ int main(int argc, char* argv[]) { // initialisation de glut glutInit(&argc, argv); // initialisation du mode de dessin glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); // initialisation de la fenêtre (en donnant sa taille) glutInitWindowSize(800, 600); // réalisation de cette fenêtre (et en lui donnant un titre) glutCreateWindow("First try"); // association de la fonction de dessin à l'évènement "affichage" (display) glutDisplayFunc(dessine); // "truc" pour avoir le dessin correct dès le début glutIdleFunc(idle); // association de la fonction de redimensionnement à l'évènement "reshape" glutReshapeFunc(reshape); // entrée dans la boucle graphique glutMainLoop(); // ne sert à rien ici (si ce n'est à éviter un warning du compilateur) return 0; }
NOTE : Nous n'avons pas encore associé le fait de quitter le programme à un évènement. Donc, pour arrêter ce programme, vous devez le faire depuis le terminal où vous l'avez lancé en tapant (comme pour arrêter n'importe quelle autre programme) les touches «Control» et «c» en même temps. Vous pouvez aussi plus simplement fermer la fenêtre à l'aide du bouton correspondant.
Nous allons maintenant nous intéresser aux évènements simples venant du clavier. On va ici s'intéresser à faire quitter le programme si la touche «Q» ou la touche «Echappement» sont pressées.
Vous pouvez télécharger ici le code de ce second exemple.
Comme toujours, avant de traiter un évènement particulier, il faut prévoir la fonction correspondante (à noter que les prototypes de ces fonctions sont imposés par la bibliothèque glut. Pour plus de détails, voir la documentation de référence donnée dans la bibliographie). Ici il s'agit simplement de quitter le programme si «Q», «q» ou «Echappement» ont été reçu comme argument :
/* Fonction pour gérer les évènements clavier. * Reçoit comme arguments le caractère correspondant à la touche tapée, * ainsi que les coordonnées de la souris à ce moment là (non utilisées * dans cet exemple). */ void clavier (unsigned char touche, int souris_x, int souris_y) { switch (touche) { case 27 : // touche "ESCAPE" ("d'échappement") case 'q': case 'Q': // déruit la fenêtre glutDestroyWindow(glutGetWindow()); // et quitte violemment du programme exit(0); // pas utile ici, mais pour éviter des questions... ;-) break; } }
Pour finir cet exemple, il suffit simplement d'associer (dans le main()) à la gestion des évènements clavier la fonction nouvellement créée :
glutKeyboardFunc(clavier);
Voici le code complet de ce second exemple. Les différences avec le premier exemple sont indiquées en gras.
/* Second exemple simple d'utilisation de la bibliothèque glut : * ajout de la gestion d'un évènement clavier. Ici : taper 'Q' pour quitter. */ #include <GL/glut.h> #include <cstdlib> // pour la fonction exit() /* Fonction utilisée lors du "re-dessin" de la fenêtre graphique, * en particulier lors de son redimensionnement. * "largeur" et "hauteur" sont les (nouvelles) tailles de la fenêtre. */ void reshape(int largeur, int hauteur) { if (hauteur <= 0) hauteur = 1; double ratio(largeur); ratio /= hauteur; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glViewport(0, 0, largeur, hauteur); gluPerspective(45.0, ratio, 1.0, 1000.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(1.0, 1.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); } /* Fonction effectuant le dessin. */ void dessine() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt(1.0, 1.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); glColor4d(0.0, 0.0, 1.0, 1.0); glutWireSphere(1.0, 30, 30); glutSwapBuffers(); } /* "truc" pour avoir le dessin correct dès le début */ void idle(void) { glutPostRedisplay(); glutIdleFunc(0); } /* Fonction pour gérer les évènements clavier. * Reçoit comme arguments le caractère correspondant à la touche tapée, * ainsi que les coordonnées de la souris à ce moment là (non utilisées * dans cet exemple). */ void clavier (unsigned char touche, int souris_x, int souris_y) { switch (touche) { case 27 : // touche "ESCAPE" ("d'échappement") case 'q': case 'Q': // déruit la fenêtre glutDestroyWindow(glutGetWindow()); // et quitte violemment du programme exit(0); // pas utile ici, mais pour éviter des questions... ;-) break; } } /* PROGRAMME PRINCIPAL */ int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowSize(800, 600); glutCreateWindow("Appuyer sur Q pour quitter"); glutDisplayFunc(dessine); glutIdleFunc(idle); glutReshapeFunc(reshape); // association de la fonction de gestion de clavier à l'évènement "clavier" glutKeyboardFunc(clavier); glutMainLoop(); return 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.
Vous pouvez télécharger ici le code de ce troisième exemple.
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 :
glPushMatrix(); // penser à empiler l'état courant glColor4d(0.0, 0.0, 1.0, 1.0); // choisi la couleur (bleu ici) glTranslated(0.0, 0.0, 0.0); // se positionne au centre de la sphère glutSolidSphere(1.0, 30, 30); // dessine une sphère pleine glPopMatrix(); // restituer l'état courant précédemment empilé
Et pour la seconde sphère :
glPushMatrix(); glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici) glTranslated(1.0, 0.0, -0.5); glutSolidSphere(1.5, 50, 50); glPopMatrix();
NOTE : j'ai ici introduit des sphères «solides» (i.e. colorées) plutôt qu'en «fil de fer» comme précédemment. Pour gérer correctement les parties cachées, il faut ajouter dans le main() :
glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Voici donc le code complet de ce troisième exemple. Les principales différences avec l'exemple précédent sont indiquées en gras.
/* Troisième exemple d'utilisation de la bibliothèque glut : * deux sphères avec gestion des parties cachées. * Pour plus de commentaires, voir les deux exemples précédents. */ #include <GL/glut.h> #include <cstdlib> void reshape(int largeur, int hauteur) { if (hauteur <= 0) hauteur = 1; double ratio(largeur); ratio /= hauteur; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glViewport(0, 0, largeur, hauteur); gluPerspective(45.0, ratio, 1.0, 1000.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); } void dessine() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // dessin de la première sphère glPushMatrix(); // penser à empiler l'état courant glColor4d(0.0, 0.0, 1.0, 1.0); // choisi la couleur (bleu ici) glTranslated(0.0, 0.0, 0.0); // se positionne au centre de la sphère glutSolidSphere(1.0, 30, 30); // dessine une sphère pleine glPopMatrix(); // restituer l'état courant précédemment empilé // dessin de la seconde sphère glPushMatrix(); glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici) glTranslated(1.0,0.0,-0.5); glutSolidSphere(1.5, 50, 50); glPopMatrix(); glutSwapBuffers(); } void idle(void) { glutPostRedisplay(); glutIdleFunc(0); } void clavier (unsigned char touche, int souris_x, int souris_y) { switch (touche) { case 27 : case 'q': case 'Q': glutDestroyWindow(glutGetWindow()); exit(0); break; } } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowSize(800, 600); glutCreateWindow("Appuyer sur Q pour quitter"); glutDisplayFunc(dessine); glutIdleFunc(idle); glutKeyboardFunc(clavier); glutReshapeFunc(reshape); // Permet la gestion des parties cachées glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glutMainLoop(); return 0; }
On va maintenant combiner la gestion des touches au clavier avec le positionnement du point de vue. Plus précisément, je vous propose ici de déplacer la «caméra» (i.e. le point de vue) à l'aide des touches «flèches», «page up» et «page down» du clavier.
Vous pouvez télécharger ici le code de ce quatrième exemple.
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 une variable globale de type structure regroupant toutes les données nécessaires. Dans un programme plus conséquent il faudrait bien sûr :
Comme nous l'avons vu dans les exemples précédents, une «caméra» nécessite trois vecteurs 3D : un pour la position, un pour le point visé et un pour indiquer la dimension verticale de la caméra (i.e. pour fixer la rotation autour de la direction de visée). Il est donc préférable que cette verticale soit perpendiculaire à la direction de visée.
De façon simpliste (à améliorer), on définit alors la structure :
struct Camera { // position de l'oeil double x; double y; double z; // point visé double dx; double dy; double dz; // verticale (non parallèle à la direction de visée) double ux; double uy; double uz; };
puis la variable globale (que l'on initialise) :
Camera ma_camera = { 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 };
L'utilisation de cette «caméra» se fait ensuite dans les fonctions reshape et dessine comme précédemment (on ne fait que remplacer les constantes par des variables) :
gluLookAt(ma_camera.x , ma_camera.y , ma_camera.z , ma_camera.dx, ma_camera.dy, ma_camera.dz, ma_camera.ux, ma_camera.uy, ma_camera.uz );
Il faut maintenant penser à la gestion de cette «caméra» via les touches «flèches», «page up» et «page down» du clavier. Cela se fait de façon similaire à la gestion des touches pour stopper le programme, sauf que les touches en question sont considérées comme «spéciales» et requierent de ce fait une gestion séparée des touches usuelles. On utilise pour cela une autre fonction associée à l'évènement «Special» (sous entendu «special keyboard») au lieu de l'évènement «Keyboard» usuel.
Voici un exemple code correspondant (lequel fait avancer/reculer la caméra sur «flêche avant»/«flêche arrière», et tourner la caméra sur elle-même avec les autres touches. Il n'est bien sûr par nécessaire qu'elle soit aussi compliquée. J'y ai de plus inclu quelques calculs usuels de géometrie dans l'espace, mais un tel code devrait sûrement se trouver ailleurs dans un programme plus ambitieux) :
void clavier_special (int touche, int souris_x, int souris_y) { /* déplace la caméra sans changer le point de vue */ // pas (arbitraire) de déplacement const double step(0.1); // quelques calculs préliminaires qui seraient peut être stockés ailleurs // 1) la direction de visee (vecteur unitaire) double visee_x(ma_camera.dx - ma_camera.x); double visee_y(ma_camera.dy - ma_camera.y); double visee_z(ma_camera.dz - ma_camera.z); const double distance(sqrt(visee_x*visee_x+visee_y*visee_y+visee_z*visee_z)); visee_x /= distance; visee_y /= distance; visee_z /= distance; // 2) la verticale (vecteur unitaire perpendiculaire a la visee) double scal(ma_camera.ux * visee_x + ma_camera.uy * visee_y + ma_camera.uz * visee_z); if (std::abs(scal) > 1e-6) { ma_camera.ux -= scal * visee_x; ma_camera.uy -= scal * visee_y; ma_camera.uz -= scal * visee_z; } double norme(ma_camera.ux * ma_camera.ux + ma_camera.uy * ma_camera.uy + ma_camera.uz * ma_camera.uz); if (norme != 1.0) { norme = sqrt(norme); ma_camera.ux /= norme; ma_camera.uy /= norme; ma_camera.uz /= norme; } // 3) produit vectoriel entre la direction de visee et la verticale double const vx(visee_y * ma_camera.uz - visee_z * ma_camera.uy); double const vy(visee_z * ma_camera.ux - visee_x * ma_camera.uz); double const vz(visee_x * ma_camera.uy - visee_y * ma_camera.ux); /* déplacement */ switch (touche) { case GLUT_KEY_UP : // déplacement avant ma_camera.x += step * visee_x; ma_camera.y += step * visee_y; ma_camera.z += step * visee_z; ma_camera.dx += step * visee_x; ma_camera.dy += step * visee_y; ma_camera.dz += step * visee_z; break; case GLUT_KEY_DOWN : // déplacement arrière ma_camera.x -= step * visee_x; ma_camera.y -= step * visee_y; ma_camera.z -= step * visee_z; ma_camera.dx -= step * visee_x; ma_camera.dy -= step * visee_y; ma_camera.dz -= step * visee_z; break; case GLUT_KEY_LEFT : /* rotation d'un angle "step" autour de la verticale : le point de visée est déplacé de sorte à garder la même distance à l'oeil */ ma_camera.dx = ma_camera.x + distance * // rotation de step autours de ma_camera.u // cf http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToMatrix/ ( ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step) ) * visee_x + ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_y + ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step) ) * visee_y + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_y + ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step) ) * visee_z ); break; case GLUT_KEY_RIGHT : /* rotation d'un angle "-step" autour de la verticale */ ma_camera.dx = ma_camera.x + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step) ) * visee_x + ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_y + ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step) ) * visee_y + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_y + ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step) ) * visee_z ); break; case GLUT_KEY_PAGE_UP: /* rotation d'un angle "step" autour de visee^verticale : le point de visée est déplacé de sorte à garder la même distance à l'oeil */ ma_camera.dx = ma_camera.x + distance * // rotation de step autours de v ( ((1.0-cos(step))*vx*vx + cos(step) ) * visee_x + ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_y + ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_x + ((1.0-cos(step))*vy*vy + cos(step) ) * visee_y + ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_x + ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_y + ((1.0-cos(step))*vz*vz + cos(step) ) * visee_z ); break; case GLUT_KEY_PAGE_DOWN: /* rotation d'un angle "-step" autour de visee^verticale*/ ma_camera.dx = ma_camera.x + distance * ( ((1.0-cos(step))*vx*vx + cos(step) ) * visee_x + ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_y + ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_x + ((1.0-cos(step))*vy*vy + cos(step) ) * visee_y + ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_x + ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_y + ((1.0-cos(step))*vz*vz + cos(step) ) * visee_z ); break; } glutPostRedisplay(); }
et son association à l'évènement correspondant (à mettre dans le main()) :
glutSpecialFunc(clavier_special);
Pour finir (et par esthétisme), j'ai ajouté une troisième sphère.
Voici donc le code complet de ce quatrième exemple. Les principales différences avec l'exemple précédent sont indiquées en gras.
/* Quatrième exemple d'utilisation de la bibliothèque glut : * trois sphères avec déplacement de la caméra. * Pour plus de commentaires, voir les exemples précédents. */ #include <GL/glut.h> #include <cstdlib> #include <cmath> // pour sin() et cos() // une simple structure pour représenter la "caméra" (i.e. le point de vue) struct Camera { // position de l'oeil double x; double y; double z; // point visé double dx; double dy; double dz; // verticale (non parallèle à la direction de visée) double ux; double uy; double uz; }; /* Une variable globale (!) pour le point de vue. * (Le mieux serait bien sûr de faire proprement une classe avec tout ça.) */ Camera ma_camera = { 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 }; void reshape(int largeur, int hauteur) { if (hauteur <= 0) hauteur = 1; double ratio(largeur); ratio /= hauteur; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glViewport(0, 0, largeur, hauteur); gluPerspective(45.0, ratio, 1.0, 1000.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(ma_camera.x, ma_camera.y, ma_camera.z, ma_camera.dx, ma_camera.dy, ma_camera.dz, ma_camera.ux, ma_camera.uy, ma_camera.uz); } void dessine() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt(ma_camera.x, ma_camera.y, ma_camera.z, ma_camera.dx, ma_camera.dy, ma_camera.dz, ma_camera.ux, ma_camera.uy, ma_camera.uz); // dessine une première sphère glPushMatrix(); glColor4d(0.0, 0.0, 1.0, 1.0); glTranslated(-1.0, 0.0, 0.5); glutSolidSphere(1.0, 30, 30); glPopMatrix(); // dessin de la seconde sphère glPushMatrix(); glColor4d(0.0, 1.0, 1.0, 1.0); glTranslated(0.0, 0.0, 0.0); glutSolidSphere(1.5, 50, 50); glPopMatrix(); // dessin de la troisième sphère glPushMatrix(); glColor4d(0.0, 0.0, 1.0, 1.0); glTranslated(1.0, 0.0, 0.5); glutSolidSphere(1.0, 50, 50); glPopMatrix(); glutSwapBuffers(); } void idle(void) { glutPostRedisplay(); glutIdleFunc(0); } void clavier (unsigned char touche, int souris_x, int souris_y) { switch (touche) { case 27 : case 'q': case 'Q': glutDestroyWindow(glutGetWindow()); exit(0); break; } } void clavier_special (int touche, int souris_x, int souris_y) { /* déplace la caméra sans changer le point de vue */ // pas (arbitraire) de déplacement const double step(0.1); // quelques calculs préliminaires qui seraient peut être stockés ailleurs // 1) la direction de visee (vecteur unitaire) double visee_x(ma_camera.dx - ma_camera.x); double visee_y(ma_camera.dy - ma_camera.y); double visee_z(ma_camera.dz - ma_camera.z); const double distance(sqrt(visee_x*visee_x+visee_y*visee_y+visee_z*visee_z)); visee_x /= distance; visee_y /= distance; visee_z /= distance; // 2) la verticale (vecteur unitaire perpendiculaire a la visee) double scal(ma_camera.ux * visee_x + ma_camera.uy * visee_y + ma_camera.uz * visee_z); if (std::abs(scal) > 1e-6) { ma_camera.ux -= scal * visee_x; ma_camera.uy -= scal * visee_y; ma_camera.uz -= scal * visee_z; } double norme(ma_camera.ux * ma_camera.ux + ma_camera.uy * ma_camera.uy + ma_camera.uz * ma_camera.uz); if (norme != 1.0) { norme = sqrt(norme); ma_camera.ux /= norme; ma_camera.uy /= norme; ma_camera.uz /= norme; } // 3) produit vectoriel entre la direction de visee et la verticale double const vx(visee_y * ma_camera.uz - visee_z * ma_camera.uy); double const vy(visee_z * ma_camera.ux - visee_x * ma_camera.uz); double const vz(visee_x * ma_camera.uy - visee_y * ma_camera.ux); /* déplacement */ switch (touche) { case GLUT_KEY_UP : // déplacement avant ma_camera.x += step * visee_x; ma_camera.y += step * visee_y; ma_camera.z += step * visee_z; ma_camera.dx += step * visee_x; ma_camera.dy += step * visee_y; ma_camera.dz += step * visee_z; break; case GLUT_KEY_DOWN : // déplacement arrière ma_camera.x -= step * visee_x; ma_camera.y -= step * visee_y; ma_camera.z -= step * visee_z; ma_camera.dx -= step * visee_x; ma_camera.dy -= step * visee_y; ma_camera.dz -= step * visee_z; break; case GLUT_KEY_LEFT : /* rotation d'un angle "step" autour de la verticale : le point de visée est déplacé de sorte à garder la même distance à l'oeil */ ma_camera.dx = ma_camera.x + distance * // rotation de step autours de ma_camera.u // cf http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToMatrix/ ( ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step) ) * visee_x + ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_y + ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step) ) * visee_y + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_y + ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step) ) * visee_z ); break; case GLUT_KEY_RIGHT : /* rotation d'un angle "-step" autour de la verticale */ ma_camera.dx = ma_camera.x + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step) ) * visee_x + ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_y + ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step) ) * visee_y + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_y + ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step) ) * visee_z ); break; case GLUT_KEY_PAGE_UP: /* rotation d'un angle "step" autour de visee^verticale : le point de visée est déplacé de sorte à garder la même distance à l'oeil */ ma_camera.dx = ma_camera.x + distance * // rotation de step autours de v ( ((1.0-cos(step))*vx*vx + cos(step) ) * visee_x + ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_y + ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_x + ((1.0-cos(step))*vy*vy + cos(step) ) * visee_y + ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_x + ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_y + ((1.0-cos(step))*vz*vz + cos(step) ) * visee_z ); break; case GLUT_KEY_PAGE_DOWN: /* rotation d'un angle "-step" autour de visee^verticale*/ ma_camera.dx = ma_camera.x + distance * ( ((1.0-cos(step))*vx*vx + cos(step) ) * visee_x + ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_y + ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_x + ((1.0-cos(step))*vy*vy + cos(step) ) * visee_y + ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_x + ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_y + ((1.0-cos(step))*vz*vz + cos(step) ) * visee_z ); break; } glutPostRedisplay(); } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowSize(800, 600); glutCreateWindow("Appuyer sur Q pour quitter"); glutDisplayFunc(dessine); glutIdleFunc(idle); glutKeyboardFunc(clavier); glutSpecialFunc(clavier_special); glutReshapeFunc(reshape); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glutMainLoop(); return 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 que l'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 fonction de la bibliothèque glut.
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 glut, 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é ou une précision suffisante), on arrête simplement le programme.
Passons maintenant à un exemple.
On va ici introduire une petite évolution «temps réel» dans notre dernier exemple. Pour cela je propose de faire tourner (simplement, de façon proportionnelle au temps (i.e. mouvement 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 :
glPushMatrix(); glColor4d(1.0, 0.0, 0.0, 0.60); // couleur rouge, transparente à 40% double u1(sin(position)); double u2(cos(position)); glTranslated(2*u1, 0.4*u1*u2, 2*u2); glutSolidSphere(.1, 30, 30); glPopMatrix();
Il s'agit ici d'une sphère dont le centre suit la courbe paramétrique (2*sin(u), 0.4*sin(u)*cos(u), 2*cos(u)). Dans cet exemple jouet, le paramètre u est représenté par la variable globale position (il faudrait bien sûr faire autrement) :
double position(0.0);
En l'état, le programme compile et fait un joli dessin, mais pas grand chose ne bouge. Il faut pour cela ajouter une fonction à l'évènement «timer» du programme. Commençons par écrire cette fonction.
Elle doit simplement calculer la nouvelle position à chaque instant. Je choisis ici de vous donner un exemple utilisant la notion de «pas de temps» comme décrit dans l'introduction théorique (bien que pour le mouvement uniforme implémenté ici ce ne soit pas nécessaire. On pourrait bien sûr dans ce cas extrêmement simple utiliser simplement directement le temps absolu).
Ce qui nous donne :
void simulation(int id) { // mémorise (localement) le nouveau temps unsigned int now(glutGet(GLUT_ELAPSED_TIME)); // calcule la valeur de l'intervalle de temps unsigned int dt(now - temps); // stocke la valeur du "nouveau temps" au niveau global // (pour la prochaine fois) temps = now; // calcule la nouvelle position (mouvement uniforme) position += dt / 1500.00; // demande l'affichage glutPostRedisplay(); }
Le prototype de la fonction simulation nous est imposé par la bibliothèque et le paramètre «id» non utilisé ici peut être utilisé pour passer une valeur lors de l'appel (si nécessaire).
La variable globale temps est utilisée ici pour garder la mémoire du temps absolu au niveau du programme entier (c'est cela l'aspect «temps réel») :
unsigned int temps(0);
Il ne reste maintenant plus qu'à associer cette fonction à l'évènement «timer». Cela se fait dans le main() par :
glutTimerFunc(1, simulation, 0);
Le premier paramètre est le nombre minimum de millisecondes à attendre avant de déclencher le «timer» (i.e. la fonction simulation). Cela contrôle en fait le nombre de fois par seconde que l'on souhaite voir s'exécuter la fonction simulation. Le dernier paramètre (0) est justement celui que cette fonction reçoit lors de son appel (le paramètre «id» dans l'exemple précédent).
Si vous testez le programme à ce stade, vous constaterez qu'il ne fonctionne pas (en ce sens que rien ne bouge). En effet, dès que le «timer» appelle sa fonction associée, celle-ci est automatiquement dissociée de l'évènement «timer». Pour une simulation en continu il est donc nécessaire de le réassocier à chaque fois. Cela se fait en ajoutant cette ligne à la fin de la fonction simulation :
glutTimerFunc(15, simulation, 0);
NOTE : 15 correspond ici à 1000/15, soit 66, recalculs (i.e. appels de la fonction simulation) par seconde.
Vous pouvez maintenant tester le programme à ce stade : ÇA MARCHE ! La petite boule rouge tourne !
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 supprimer la réassociation précédente en fin de simulation à chaque fois qu'une pause a été demandée. Pour cela, on va introduire une variable booléenne (encore globale dans cette exemple jouet, mais à améliorer dans un programme plus conséquent) qui représente si oui ou non la simulation doit tourner :
static bool timer_on(false);
Ensuite on associe, dans la fonction clavier, une touche (par exemple la barre espace ici) à l'arrêt/au redémarrage de la simulation :
case ' ': if (timer_on) { timer_on = false; // arrête la simulation si était en cours } else { // redémarre la simulation si arrêtée glutTimerFunc(1, timer, 0); timer_on = true; temps = glutGet(GLUT_ELAPSED_TIME); }
et enfin on effectue la réassociation du «timer» dans la fonction simulation que si timer_on est à «vrai» :
if (timer_on) glutTimerFunc(15, timer, 0);
Il ne reste plus qu'à mettre les bonnes valeurs initiales dans le main et le tour est joué :
glutTimerFunc(1, timer, 0); timer_on = true; // maintenant il est "on" (on vient de le mettre) temps = glutGet(GLUT_ELAPSED_TIME);
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.
/* Cinquième exemple d'utilisation de la bibliothèque glut : * on ajoute une sphère gravitant autour des trois autres * et la possibilité de faire une pause dans l'animation en * appuyant sur la "barre d'espace". * Pour plus de commentaires, voir les exemples précédents. */ #include <GL/glut.h> #include <cstdlib> #include <cmath> struct Camera { double x; double y; double z; double dx; double dy; double dz; double ux; double uy; double uz; }; // tout ceci devrait être mis dans des classes appropriées Camera ma_camera = { 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 }; unsigned int temps(0); double position(0.0); bool timer_on(false); void reshape(int largeur, int hauteur) { if (hauteur <= 0) hauteur = 1; double ratio(largeur); ratio /= hauteur; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glViewport(0, 0, largeur, hauteur); gluPerspective(45.0, ratio, 1.0, 1000.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(ma_camera.x, ma_camera.y, ma_camera.z, ma_camera.dx, ma_camera.dy, ma_camera.dz, ma_camera.ux, ma_camera.uy, ma_camera.uz); } void dessine() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt(ma_camera.x, ma_camera.y, ma_camera.z, ma_camera.dx, ma_camera.dy, ma_camera.dz, ma_camera.ux, ma_camera.uy, ma_camera.uz); // dessine une première sphère glPushMatrix(); glColor4d(0.0, 0.0, 1.0, 1.0); glTranslated(-1.0, 0.0, 0.5); glutSolidSphere(1.0, 30, 30); glPopMatrix(); // dessin de la seconde sphère glPushMatrix(); glColor4d(0.0, 1.0, 1.0, 1.0); glTranslated(0.0, 0.0, 0.0); glutSolidSphere(1.5, 50, 50); glPopMatrix(); // dessin de la troisième sphère glPushMatrix(); glColor4d(0.0, 0.0, 1.0, 1.0); glTranslated(1.0, 0.0, 0.5); glutSolidSphere(1.0, 50, 50); glPopMatrix(); // dessin de la quatrième sphère (celle qui va bouger) glPushMatrix(); glColor4d(1.0, 0.0, 0.0, 0.60); // couleur rouge, transparente à 40% double u1(sin(position)); double u2(cos(position)); glTranslated(2*u1, 0.4*u1*u2, 2*u2); glutSolidSphere(.1, 30, 30); glPopMatrix(); glutSwapBuffers(); } /* Cette nouvelle fonction sert à faire évoluer le temps. * Ici on fait simplement dépendre "position" linéairement du temps. * Le paramètre "id" n'est pas utilisé ici. */ void timer(int id) { // mémorise (localement) le nouveau temps unsigned int now(glutGet(GLUT_ELAPSED_TIME)); // calcule la valeur de l'intervalle de temps unsigned int dt(now - temps); // stocke la valeur du "nouveau temps" au niveau global // (pour la prochaine fois) temps = now; // calcule la nouvelle position (mouvement uniforme) position += dt / 1500.00; // demande l'affichage glutPostRedisplay(); // si on n'a pas demandé d'arrêter la simulation, on la relance. // 15 correspond à environ 66 recalculs par seconde (1000/15 en fait) if (timer_on) glutTimerFunc(15, timer, 0); } void idle(void) { glutPostRedisplay(); glutIdleFunc(0); } void clavier (unsigned char touche, int souris_x, int souris_y) { switch (touche) { case 27 : case 'q': case 'Q': glutDestroyWindow(glutGetWindow()); exit(0); break; // Pause sur la touche "espace" case ' ': if (timer_on) { timer_on = false; // arrête la simulation si était en cours } else { // redémarre la simulation si arrêtée glutTimerFunc(1, timer, 0); timer_on = true; temps = glutGet(GLUT_ELAPSED_TIME); } } } void clavier_special (int touche, int souris_x, int souris_y) { const double step(0.1); double visee_x(ma_camera.dx - ma_camera.x); double visee_y(ma_camera.dy - ma_camera.y); double visee_z(ma_camera.dz - ma_camera.z); const double distance(sqrt(visee_x*visee_x+visee_y*visee_y+visee_z*visee_z)); visee_x /= distance; visee_y /= distance; visee_z /= distance; double scal(ma_camera.ux * visee_x + ma_camera.uy * visee_y + ma_camera.uz * visee_z); if (std::abs(scal) > 1e-6) { ma_camera.ux -= scal * visee_x; ma_camera.uy -= scal * visee_y; ma_camera.uz -= scal * visee_z; } double norme(ma_camera.ux * ma_camera.ux + ma_camera.uy * ma_camera.uy + ma_camera.uz * ma_camera.uz); if (norme != 1.0) { norme = sqrt(norme); ma_camera.ux /= norme; ma_camera.uy /= norme; ma_camera.uz /= norme; } double const vx(visee_y * ma_camera.uz - visee_z * ma_camera.uy); double const vy(visee_z * ma_camera.ux - visee_x * ma_camera.uz); double const vz(visee_x * ma_camera.uy - visee_y * ma_camera.ux); switch (touche) { case GLUT_KEY_UP : ma_camera.x += step * visee_x; ma_camera.y += step * visee_y; ma_camera.z += step * visee_z; ma_camera.dx += step * visee_x; ma_camera.dy += step * visee_y; ma_camera.dz += step * visee_z; break; case GLUT_KEY_DOWN : ma_camera.x -= step * visee_x; ma_camera.y -= step * visee_y; ma_camera.z -= step * visee_z; ma_camera.dx -= step * visee_x; ma_camera.dy -= step * visee_y; ma_camera.dz -= step * visee_z; break; case GLUT_KEY_LEFT : ma_camera.dx = ma_camera.x + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step) ) * visee_x + ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_y + ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step) ) * visee_y + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_y + ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step) ) * visee_z ); break; case GLUT_KEY_RIGHT : ma_camera.dx = ma_camera.x + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step) ) * visee_x + ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_y + ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step) ) * visee_y + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_x + ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_y + ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step) ) * visee_z ); break; case GLUT_KEY_PAGE_UP: ma_camera.dx = ma_camera.x + distance * ( ((1.0-cos(step))*vx*vx + cos(step) ) * visee_x + ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_y + ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_x + ((1.0-cos(step))*vy*vy + cos(step) ) * visee_y + ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_x + ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_y + ((1.0-cos(step))*vz*vz + cos(step) ) * visee_z ); break; case GLUT_KEY_PAGE_DOWN: ma_camera.dx = ma_camera.x + distance * ( ((1.0-cos(step))*vx*vx + cos(step) ) * visee_x + ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_y + ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_z ); ma_camera.dy = ma_camera.y + distance * ( ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_x + ((1.0-cos(step))*vy*vy + cos(step) ) * visee_y + ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_z ); ma_camera.dz = ma_camera.z + distance * ( ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_x + ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_y + ((1.0-cos(step))*vz*vz + cos(step) ) * visee_z ); break; } glutPostRedisplay(); } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowSize(800, 600); glutCreateWindow("Appuyer sur Q pour quitter"); glutDisplayFunc(dessine); glutIdleFunc(idle); glutKeyboardFunc(clavier); glutSpecialFunc(clavier_special); glutReshapeFunc(reshape); // ajoute les fonctionnalités de gestion du temps (simulation) glutTimerFunc(1, timer, 0); timer_on = true; temps = glutGet(GLUT_ELAPSED_TIME); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glClearColor(0.0, 0.0, 0.0, 0.0); glutMainLoop(); return 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 afficher du texte ?
Par exemple (pour un message constant) :
glPushMatrix(); glColor4d(1.0, 0.0, 0.0, 1.0); glRasterPos3d(2*u1, 0.4*u1*u2+0.15, 2*u2); const char* s = "electron"; while (*s) { glutBitmapCharacter(GLUT_BITMAP_HELVETICA_12, *s); s++; } glPopMatrix();
Autre exemple (pour un message contenu dans une «string») :
string ma_chaine; //... glPushMatrix(); glColor4d(1.0, 1.0, 1.0, 1.0); glRasterPos3d(1, 1, 1); const char* s(ma_chaine.c_str()); while (*s) { glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, *s); s++; } glPopMatrix();
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. Essayez dans l'exemple 5 d'arrêter la boule rouge juste au dessus de l'intersection entre 2 sphères. Vous devriez la voir au travers.
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 des cubes et des tores, ou toute transformation affine de ces objets, voir les commandes de bases indiquées plus haut.
Pour le reste, il faut décomposer l'objet à dessiner en facettes triangulaires. Voici un exemple pour une pyramide faite de 4 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 (en gras la principale différence avec ce qui était présenté précédemment) :
#include <GL/glut.h> #include <cstdlib> // pour la fonction exit() #include <cmath> // pour la fonction sin() static int ma_fenetre; void reshape(int largeur, int hauteur) { if (hauteur <= 0) hauteur = 1; double ratio(largeur); ratio /= hauteur; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glViewport(0, 0, largeur, hauteur); gluPerspective(45.0, ratio, 1.0, 1000.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void dessine() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // part du système de coordonnées de base (matrice identité) glLoadIdentity(); // on recule en z pour voir le plan dans lequel on va dessiner glTranslated(0.0, 0.0, -10.0); // quelques constantes ici pour l'exemple const double xmax(5.0); const double xmin(-xmax); const double ymax(4.0); 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(); // ~ affiche glutSwapBuffers(); } void idle(void) { glutPostRedisplay(); glutIdleFunc(0); } void clavier (unsigned char touche, int souris_x, int souris_y) { switch (touche) { case 27 : case 'q': case 'Q': glutDestroyWindow(ma_fenetre); exit(0); break; } } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); glutInitWindowSize(800, 600); ma_fenetre = glutCreateWindow("2D"); glClearColor(0.0, 0.0, 0.0, 0.0); glutIdleFunc(idle); glutDisplayFunc(dessine); glutReshapeFunc(reshape); glutKeyboardFunc(clavier); glutMainLoop(); return 0; }