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 wxWidgets 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 wxWidgets 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 wxWidgets 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 SDL ou glut à la place de wxWidgets.
Ce tutoriel ne constitue qu'une brève présentation de ces deux bibliothèques (wxWidgets 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 wxWidgets, 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 wxWidgets, 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 wxWidgets, mais une couche plus basse, indépendante, à laquelle on peut «parler» et que l'on peut incorporer dans les objets de wxWidgets.
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) |
wxWidgets 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 wxWidgets 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 :
`wx-config --cxxflags`
lors de la compilation (attention les «`» dans la commande ci-dessus sont des «apostrophes inversées» («backquotes»)), et
`wx-config --libs gl,core,base`
lors de l'édition de liens (attention ici aussi aux «apostrophes inversées»).
Je vous conseille pour cela d'utiliser un «Makefile» dans lequel vous aurez ajouté :
CXXFLAGS = `wx-config --cxxflags` LDLIBS = `wx-config --libs gl,core,base`
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.
wxWidgets est en fait une bibliothèque très riche qui permet de définir
toutes sortes d'objets graphiques (boutons, règles, ascenseurs,
...)
,
mais ce n'est pas ce que nous aborderons ici..
De ce fait, wxWidgets 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 :
Application principale --> Fenêtre(s) principale(s) --> (sous-)Fenêtre 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.
// On inclut les bibliothèques nécessaires #include "wx/wxprec.h" #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/glcanvas.h" // Pour combiner wxWidgets et OpenGL // ---------------------------------------------------------------------- // Nouvelle application class GUI : public wxApp { public: bool OnInit(); }; // ---------------------------------------------------------------------- // sous-fenêtre de dessin OpenGL class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() {} }; // ---------------------------------------------------------------------- // fenetre(s) principale(s) class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: Vue_OpenGL* fogl; }; /* =================================================================== Implementation de Vue_OpenGL =================================================================== */ // ====================================================================== Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) {} /* =================================================================== Implementation de Fenetre =================================================================== */ // ====================================================================== Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Affiche (montre) la fenetre Show(true); } /* =================================================================== Implementation de l'application principale =================================================================== */ // ====================================================================== bool GUI::OnInit() { // Crée la fenêtre principale Fenetre* f = new Fenetre(wxT("TITRE"), // avec un titre wxSize(800, 600)); // et de taille 800x600 SetTopWindow(f); return (f != 0); } /* =================================================================== Equivalent de main() =================================================================== */ IMPLEMENT_APP(GUI)
A noter cet aspect un peu particulier : il n'y a pas explicitement de main(). En fait le main() est caché (et compris) dans la ligne
IMPLEMENT_APP(GUI)
Tout ceci est bien beau, mais
ne fait absolument rien !! En effet, comme dit plus haut,
pour qu'il se passe quelque chose, il faut dire au programme quoi
faire en présence de tel ou tel évènement.
Ce que nous n'avons pour le moment pas fait !
La fonction de base qu'il faut au moins définir est la fonction associée à l'évènement «dessine». Je vous conseille d'ajouter de plus une méthode spécifique à l'initialisation d'OpenGL.
Naturellement, dans l'architecture proposée ci-dessus, ces fonctions sont des méthodes de la classe Vue_OpenGL.
Commençons par la première (dessin) : cela se fait en ajoutant les lignes suivantes à la classe Vue_OpenGL :
class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() {} protected: void dessine(wxPaintEvent& evenement); DECLARE_EVENT_TABLE() };
Il faut ensuite associer cette méthode à l'évènement «dessine». Ce qui se fait comme suit (hors de la classe, typiquement dans la partie implémentation de celle-ci) :
/* =================================================================== Implementation de Vue_OpenGL =================================================================== */ BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT(Vue_OpenGL::dessine) END_EVENT_TABLE()
Il ne reste plus qu'à écrire la méthode en question.
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).
Nous l'ajoutons donc comme membre privé à la
classe :
protected: void dessine(wxPaintEvent& evenement); private: GLUquadric* sphere; DECLARE_EVENT_TABLE() };
Le dessin de la sphère se fait ensuite comme suit (voir les commentaires dans le code) :
void Vue_OpenGL::dessine(wxPaintEvent&) { if (!GetContext()) return; // indique que le code openGL qui suit est pour ce composant GL_Window SetCurrent(); // 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();
De plus, il ne faut pas oublier d'initialiser notre le «quadrique» dans le constructeur de la classe :
Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) , sphere(gluNewQuadric()) {}
ni de libérer la chose en question dans le destructeur :
virtual ~Vue_OpenGL() { gluDeleteQuadric(sphere); }
Voilà ! Nous avons fini avec le dessin, et pouvons donc passer à la fonction de d'initialisation d'OpenGL. Cela se fait en ajoutant la méthode en question à la classe Vue_OpenGL :
class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() { gluDeleteQuadric(sphere); } void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); private: GLUquadric* sphere; DECLARE_EVENT_TABLE() };
Pour l'implémentation de cette méthode, je vous propose la chose suivante (encore une fois sans détailler) :
void Vue_OpenGL::InitOpenGL() { // Initialisation OpenGL SetCurrent(); // 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); }
A noter le SetCurrent() (également au début de la méthode de dessin), absolument nécessaire pour indiquer à qui s'adressent les commandes OpenGL qui suivent.
Il ne reste plus qu'à appeler cette initialisation. Cela doit se faire après que la fenêtre principale ait été activée. Je vous propose donc de le mettre à la fin du constructeur de cette fenêtre :
Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); }
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 "wx/wxprec.h" #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/glcanvas.h" // Pour combiner wxWidgets et OpenGL // ---------------------------------------------------------------------- // Nouvelle application class GUI : public wxApp { public: bool OnInit(); }; // ---------------------------------------------------------------------- // sous-fenêtre de dessin OpenGL pour une sphère class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() { gluDeleteQuadric(sphere); } void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); private: GLUquadric* sphere; DECLARE_EVENT_TABLE() }; // ---------------------------------------------------------------------- // fenetre(s) principale(s) class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: Vue_OpenGL* fogl; }; /* =================================================================== Implementation de Vue_OpenGL =================================================================== */ BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT(Vue_OpenGL::dessine) END_EVENT_TABLE() // ====================================================================== Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) , sphere(gluNewQuadric()) {} // ====================================================================== void Vue_OpenGL::dessine(wxPaintEvent&) { if (!GetContext()) return; // indique que le code openGL qui suit est pour ce composant GL_Window SetCurrent(); // 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(); SwapBuffers(); } // ====================================================================== void Vue_OpenGL::InitOpenGL() { // Initialisation OpenGL SetCurrent(); // 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); } /* =================================================================== Implementation de Fenetre =================================================================== */ // ====================================================================== Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); } /* =================================================================== Implementation de l'application principale =================================================================== */ // ====================================================================== bool GUI::OnInit() { // Crée la fenêtre principale Fenetre* f = new Fenetre(wxT("Premier exemple"), // avec un titre wxSize(800, 600)); // et de taille 800x600 SetTopWindow(f); return (f != 0); } /* =================================================================== Equivalent de main() =================================================================== */ IMPLEMENT_APP(GUI)
NOTE 1 : 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.
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).
Concernant la libération mémoire de ces pointeurs alloués (par exemple le Vue_OpenGL* fogl; de Fenetre), c'est en fait la bibliothèque wxWidgets elle-même qui se charge de les libérer (tous ceux qu'elle utilise, bien sûr, pas les nôtres propres. Ainsi, il reste de notre responsabilité de libérer l'objet purement interne GLUquadric* sphere; de la classe Vue_OpenGL).
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 "wx/wxprec.h" #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/glcanvas.h" // Pour combiner wxWidgets et OpenGL // ---------------------------------------------------------------------- // Nouvelle application class GUI : public wxApp { public: bool OnInit(); }; // ---------------------------------------------------------------------- // sous-fenetre de dessin OpenGL class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() {} void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); DECLARE_EVENT_TABLE() }; // ---------------------------------------------------------------------- // fenetre(s) principale(s) class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: Vue_OpenGL* fogl; DECLARE_EVENT_TABLE() }; /* =================================================================== Implementation de Vue_OpenGL =================================================================== */ BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) END_EVENT_TABLE() // ====================================================================== Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) {} // ====================================================================== void Vue_OpenGL::dessine(wxPaintEvent&) { if (!GetContext()) return; SetCurrent(); // 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(); SwapBuffers(); } // ====================================================================== void Vue_OpenGL::InitOpenGL() { // Initialisation OpenGL SetCurrent(); // 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); } /* =================================================================== Implementation de Fenetre =================================================================== */ // ====================================================================== Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); } /* =================================================================== Implementation de l'application principale =================================================================== */ // ====================================================================== bool GUI::OnInit() { // Crée la fenêtre principale Fenetre* f = new Fenetre(wxT("Cube tournant"), wxSize(800, 600)); SetTopWindow(f); return (f != 0); } /* =================================================================== Equivalent de main() =================================================================== */ IMPLEMENT_APP(GUI)
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.
Comme toujours, avant de traiter un évènement particulier (clavier), il faut prévoir la fonction/méthode correspondante (à noter que les prototypes de ces fonctions sont imposés par la bibliothèque wxWidget. Pour plus de détails, voir la documentation de référence. Voir aussi la bibliographie donnée en bas de page).
Ajoutons donc une méthode, appelée par exemple «OnExit», à la classe Fenetre de sorte à gérer l'évènement «quitter» :
class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: void OnExit(wxCommandEvent& event) { Close(true); } Vue_OpenGL* fogl; DECLARE_EVENT_TABLE() };
Dans wxWidgets, une solution simple consiste à faire générer cette évènement «quitter», soit par un menu soit par les touches «Ctrl» et «Q». Cela se fait simplement en ajoutant un menu à la fenêtre et en lui faisant générer l'évènement en question :
Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Barre de menu wxMenu* winMenu = new wxMenu; winMenu->Append(wxID_EXIT, wxT("&Close")); wxMenuBar* menuBar = new wxMenuBar; menuBar->Append(winMenu, wxT("&Window")); SetMenuBar(menuBar); // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); }
NOTE : L'évènement en question se nomme wxID_EXIT.
Il ne reste plus qu'à déclarer l'association évènement (du menu) et méthode :
BEGIN_EVENT_TABLE(Fenetre, wxFrame) EVT_MENU( wxID_EXIT, Fenetre::OnExit ) END_EVENT_TABLE()
NOTE : Vous pouvez compiler (et exécuter) à 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 avoir un menu avec le bouton pour quitter.
En fait, nous n'avons finalement ici géré aucun évènement clavier par nous même (mais avons laissé wxWidget le faire pour nous). Comment gérer un évènement clavier spécifique ?
Nous allons ici le faire pour les flèches. Mais il faut avant cela savoir CE que l'on veut faire.
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, pour simplifier, des attributs de la classe Vue_OpenGL. Dans une conception plus globale, il faudrait sûrement revoir ce point.
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; double phi;
Pour faire tourner le cube, il suffit d'incrémenter la valeur d'un de ces angles lors de l'appui sur une touche. Pour gérer les évènements clavier, il faut, comme toujours :
prévoir la méthode associée :
class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() {} void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnKeyDown(wxKeyEvent& evenement);
associer effectivement cette méthode à l'évènement en question :
BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_KEY_DOWN( Vue_OpenGL::OnKeyDown )
et bien sûr (et surtout), écrire ce qu'il faut faire dans ce cas (i.e. écrire la méthode). Ici cela se fait en récupérant la touche tapée au clavier via event.GetKeyCode(). Il n'y a alors plus qu'à traiter les différents cas. Ici nous voulons simplement faire tourner le cube. Utilisons pour cela l'approche modulaire et créons une nouvelle méthode qui fait tourner la caméra d'un angle alpha autour d'un des axes précisés plus haut. Si l'on suppose que de telles méthode existent (et s'appellent RotateTheta et RotatePhi), alors le code de la méthode de gestion des évènement clavier devient (on tourne ici par exemple de 2 degrés) :
void Vue_OpenGL::OnKeyDown( wxKeyEvent& event ) { switch( event.GetKeyCode() ) { case WXK_LEFT: RotatePhi( 2.0); Refresh(false); break; case WXK_RIGHT: RotatePhi( -2.0); Refresh(false); break; case WXK_UP: RotateTheta( 2.0); Refresh(false); break; case WXK_DOWN: RotateTheta(-2.0); Refresh(false); break; } event.Skip(); }
Reste donc à écrire ces méthodes Rotate (pensez aussi à les déclarer dans la classe) :
// ====================================================================== void Vue_OpenGL::RotateTheta(GLdouble deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } // ====================================================================== void Vue_OpenGL::RotatePhi(GLdouble 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.)
NOTE : Vous pouvez compiler (et exécuter)
à ce stade. Le cube devrait se dessiner...
...et tourner lorsque l'on appuie sur les flèches.
Pour finir, je vous propose encore de gérer un autre évènement assez courant qui est le redimensionnement de la fenêtre. Cela se fait assez simplement comme suit :
ajout de la méthode :
class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() {} void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnKeyDown(wxKeyEvent& evenement); void OnSize(wxSizeEvent& evenement);
association de la méthode à l'évènement correspondant :
BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_KEY_DOWN( Vue_OpenGL::OnKeyDown ) EVT_SIZE( Vue_OpenGL::OnSize )
gestion de l'évènement (écriture de la méthode) :
// ====================================================================== void Vue_OpenGL::OnSize(wxSizeEvent& event) { // Nécessaire pour mettre à jour le contexte sur certaines plateformes wxGLCanvas::OnSize(event); if (GetContext()) { // set GL viewport (not called by wxGLCanvas::OnSize on all platforms...) int w, h; GetClientSize(&w, &h); SetCurrent(); glViewport(0, 0, (GLint) w, (GLint) h); }
Un tout dernier truc assez pratique : mettre le «focus» sur la fenêtre quand on y entre (cela évite de cliquer) :
class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() {} void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnKeyDown(wxKeyEvent& evenement); void OnSize(wxSizeEvent& evenement); void OnEnterWindow(wxMouseEvent& evenement) { SetFocus(); } void RotateTheta(GLdouble deg); void RotatePhi(GLdouble deg); private: double theta; double phi; DECLARE_EVENT_TABLE() };
BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_KEY_DOWN( Vue_OpenGL::OnKeyDown ) EVT_SIZE( Vue_OpenGL::OnSize ) EVT_ENTER_WINDOW( Vue_OpenGL::OnEnterWindow ) END_EVENT_TABLE()
Voilà pour ce second exemple ! Le code complet est :
#include "wx/wxprec.h" #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/glcanvas.h" // Pour combiner wxWidgets et OpenGL // ---------------------------------------------------------------------- // Nouvelle application class GUI : public wxApp { public: bool OnInit(); }; // ---------------------------------------------------------------------- // sous-fenetre de dessin OpenGL class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() {} void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnKeyDown(wxKeyEvent& evenement); void OnSize(wxSizeEvent& evenement); void OnEnterWindow(wxMouseEvent& evenement) { SetFocus(); } void RotateTheta(GLdouble deg); void RotatePhi(GLdouble deg); private: double theta; double phi; DECLARE_EVENT_TABLE() }; // ---------------------------------------------------------------------- // fenetre(s) principale(s) class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: void OnExit(wxCommandEvent& event) { Close(true); } Vue_OpenGL* fogl; DECLARE_EVENT_TABLE() }; /* =================================================================== Implementation de Vue_OpenGL =================================================================== */ BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_KEY_DOWN( Vue_OpenGL::OnKeyDown ) EVT_SIZE( Vue_OpenGL::OnSize ) EVT_ENTER_WINDOW( Vue_OpenGL::OnEnterWindow ) END_EVENT_TABLE() // ====================================================================== Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) , theta(35.0), phi(20.0) {} // ====================================================================== void Vue_OpenGL::dessine(wxPaintEvent&) { if (!GetContext()) return; SetCurrent(); // 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(); SwapBuffers(); } // ====================================================================== void Vue_OpenGL::OnSize(wxSizeEvent& event) { // Nécessaire pour mettre à jour le contexte sur certaines plateformes wxGLCanvas::OnSize(event); if (GetContext()) { // set GL viewport (not called by wxGLCanvas::OnSize on all platforms...) int w, h; GetClientSize(&w, &h); SetCurrent(); glViewport(0, 0, (GLint) w, (GLint) h); } } // ====================================================================== void Vue_OpenGL::OnKeyDown( wxKeyEvent& event ) { switch( event.GetKeyCode() ) { case WXK_LEFT: RotatePhi( 2.0); Refresh(false); break; case WXK_RIGHT: RotatePhi( -2.0); Refresh(false); break; case WXK_UP: RotateTheta( 2.0); Refresh(false); break; case WXK_DOWN: RotateTheta(-2.0); Refresh(false); break; } event.Skip(); } // ====================================================================== void Vue_OpenGL::RotateTheta(GLdouble deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } // ====================================================================== void Vue_OpenGL::RotatePhi(GLdouble deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } } // ====================================================================== void Vue_OpenGL::InitOpenGL() { // Initialisation OpenGL SetCurrent(); // 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); } /* =================================================================== Implementation de Fenetre =================================================================== */ BEGIN_EVENT_TABLE(Fenetre, wxFrame) EVT_MENU( wxID_EXIT, Fenetre::OnExit ) END_EVENT_TABLE() // ====================================================================== Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Barre de menu wxMenu* winMenu = new wxMenu; winMenu->Append(wxID_EXIT, wxT("&Close")); wxMenuBar* menuBar = new wxMenuBar; menuBar->Append(winMenu, wxT("&Window")); SetMenuBar(menuBar); // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); } /* =================================================================== Implementation de l'application principale =================================================================== */ // ====================================================================== bool GUI::OnInit() { // Crée la fenêtre principale Fenetre* f = new Fenetre(wxT("Cube tournant"), wxSize(800, 600)); SetTopWindow(f); return (f != 0); } /* =================================================================== Equivalent de main() =================================================================== */ IMPLEMENT_APP(GUI)
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 "wx/wxprec.h" #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/glcanvas.h" // Pour combiner wxWidgets et OpenGL // ---------------------------------------------------------------------- // Nouvelle application class GUI : public wxApp { public: bool OnInit(); }; // ---------------------------------------------------------------------- // sous-fenetre de dessin OpenGL class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() { gluDeleteQuadric(sphere); } void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnSize(wxSizeEvent& evenement); void OnKeyDown(wxKeyEvent& evenement); void OnEnterWindow(wxMouseEvent& evenement) { SetFocus(); } void RotateTheta(GLdouble deg); void RotatePhi(GLdouble deg); private: GLUquadric* sphere; double theta; double phi; DECLARE_EVENT_TABLE() }; // ---------------------------------------------------------------------- // fenetre(s) principale(s) class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: void OnExit(wxCommandEvent& event) { Close(true); } Vue_OpenGL* fogl; DECLARE_EVENT_TABLE() }; /* =================================================================== Implementation de Vue_OpenGL =================================================================== */ BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_SIZE( Vue_OpenGL::OnSize ) EVT_KEY_DOWN( Vue_OpenGL::OnKeyDown ) EVT_ENTER_WINDOW( Vue_OpenGL::OnEnterWindow ) END_EVENT_TABLE() // ====================================================================== Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) , sphere(gluNewQuadric()) , theta(35.0), phi(20.0) {} // ====================================================================== void Vue_OpenGL::dessine(wxPaintEvent&) { if (!GetContext()) return; SetCurrent(); // 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(); SwapBuffers(); } // ====================================================================== void Vue_OpenGL::OnSize(wxSizeEvent& event) { // Nécessaire pour mettre à jour le contexte sur certaines plateformes wxGLCanvas::OnSize(event); if (GetContext()) { // set GL viewport (not called by wxGLCanvas::OnSize on all platforms...) int w, h; GetClientSize(&w, &h); SetCurrent(); glViewport(0, 0, (GLint) w, (GLint) h); } } // ====================================================================== void Vue_OpenGL::OnKeyDown( wxKeyEvent& event ) { switch( event.GetKeyCode() ) { case WXK_LEFT: RotatePhi( 2.0); Refresh(false); break; case WXK_RIGHT: RotatePhi( -2.0); Refresh(false); break; case WXK_UP: RotateTheta( 2.0); Refresh(false); break; case WXK_DOWN: RotateTheta(-2.0); Refresh(false); break; } event.Skip(); } // ====================================================================== void Vue_OpenGL::RotateTheta(GLdouble deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } // ====================================================================== void Vue_OpenGL::RotatePhi(GLdouble deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } } // ====================================================================== void Vue_OpenGL::InitOpenGL() { // Initialisation OpenGL SetCurrent(); // 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); } /* =================================================================== Implementation de Fenetre =================================================================== */ BEGIN_EVENT_TABLE(Fenetre, wxFrame) EVT_MENU( wxID_EXIT, Fenetre::OnExit ) END_EVENT_TABLE() // ====================================================================== Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Barre de menu wxMenu* winMenu = new wxMenu; winMenu->Append(wxID_EXIT, wxT("&Close")); wxMenuBar* menuBar = new wxMenuBar; menuBar->Append(winMenu, wxT("&Window")); SetMenuBar(menuBar); // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); } /* =================================================================== Implementation de l'application principale =================================================================== */ // ====================================================================== bool GUI::OnInit() { // Crée la fenêtre principale Fenetre* f = new Fenetre(wxT("Exemple 3"), wxSize(800, 600)); SetTopWindow(f); return (f != 0); } /* =================================================================== Equivalent de main() =================================================================== */ IMPLEMENT_APP(GUI)
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 à la classe Vue_OpenGL :
double theta; double phi; double r;
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 WXK_HOME: r = 5.0; // On revient à un point fixé theta = 35.0; phi = 20.0; Refresh(false); break; case WXK_PAGEUP: deplace(-1.0); // On se rapproche Refresh(false); break; case WXK_PAGEDOWN: deplace(1.0); // On s'éloigne Refresh(false); break;
avec :
void Vue_OpenGL::deplace(GLdouble 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 "wx/wxprec.h" #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/glcanvas.h" // Pour combiner wxWidgets et OpenGL // ---------------------------------------------------------------------- // Nouvelle application class GUI : public wxApp { public: bool OnInit(); }; // ---------------------------------------------------------------------- // sous-fenetre de dessin OpenGL class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() { gluDeleteQuadric(sphere); } void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnSize(wxSizeEvent& evenement); void OnKeyDown(wxKeyEvent& evenement); void OnEnterWindow(wxMouseEvent& evenement) { SetFocus(); } void RotateTheta(GLdouble deg); void RotatePhi(GLdouble deg); void deplace(double dr); private: GLUquadric* sphere; double theta; double phi; double r; DECLARE_EVENT_TABLE() }; // ---------------------------------------------------------------------- // fenetre(s) principale(s) class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: void OnExit(wxCommandEvent& event) { Close(true); } Vue_OpenGL* fogl; DECLARE_EVENT_TABLE() }; /* =================================================================== Implementation de Vue_OpenGL =================================================================== */ BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_SIZE( Vue_OpenGL::OnSize ) EVT_KEY_DOWN( Vue_OpenGL::OnKeyDown ) EVT_ENTER_WINDOW( Vue_OpenGL::OnEnterWindow ) END_EVENT_TABLE() // ====================================================================== Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) , sphere(gluNewQuadric()) , theta(35.0), phi(20.0), r(5.0) {} // ====================================================================== void Vue_OpenGL::dessine(wxPaintEvent&) { if (!GetContext()) return; SetCurrent(); // 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(); SwapBuffers(); } // ====================================================================== void Vue_OpenGL::OnSize(wxSizeEvent& event) { // Nécessaire pour mettre à jour le contexte sur certaines plateformes wxGLCanvas::OnSize(event); if (GetContext()) { // set GL viewport (not called by wxGLCanvas::OnSize on all platforms...) int w, h; GetClientSize(&w, &h); SetCurrent(); glViewport(0, 0, (GLint) w, (GLint) h); } } // ====================================================================== void Vue_OpenGL::OnKeyDown( wxKeyEvent& event ) { switch( event.GetKeyCode() ) { case WXK_LEFT: RotatePhi( 2.0); Refresh(false); break; case WXK_RIGHT: RotatePhi( -2.0); Refresh(false); break; case WXK_UP: RotateTheta( 2.0); Refresh(false); break; case WXK_DOWN: RotateTheta(-2.0); Refresh(false); break; case WXK_HOME: r = 5.0; // On revient à un point fixé theta = 35.0; phi = 20.0; Refresh(false); break; case WXK_PAGEUP: deplace(-1.0); // On se rapproche Refresh(false); break; case WXK_PAGEDOWN: deplace(1.0); // On s'éloigne Refresh(false); break; } event.Skip(); } // ====================================================================== void Vue_OpenGL::RotateTheta(GLdouble deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } // ====================================================================== void Vue_OpenGL::RotatePhi(GLdouble deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } } // ====================================================================== void Vue_OpenGL::deplace(GLdouble dr) { r += dr; if (r < 1.0) r = 1.0; else if (r > 1000.0) r = 1000.0; } // ====================================================================== void Vue_OpenGL::InitOpenGL() { // Initialisation OpenGL SetCurrent(); // 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); } /* =================================================================== Implementation de Fenetre =================================================================== */ BEGIN_EVENT_TABLE(Fenetre, wxFrame) EVT_MENU( wxID_EXIT, Fenetre::OnExit ) END_EVENT_TABLE() // ====================================================================== Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Barre de menu wxMenu* winMenu = new wxMenu; winMenu->Append(wxID_EXIT, wxT("&Close")); wxMenuBar* menuBar = new wxMenuBar; menuBar->Append(winMenu, wxT("&Window")); SetMenuBar(menuBar); // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); } /* =================================================================== Implementation de l'application principale =================================================================== */ // ====================================================================== bool GUI::OnInit() { // Crée la fenêtre principale Fenetre* f = new Fenetre(wxT("Exemple 4"), wxSize(800, 600)); SetTopWindow(f); return (f != 0); } /* =================================================================== Equivalent de main() =================================================================== */ IMPLEMENT_APP(GUI)
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 wxWidgets.
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 wxWidgets, 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é à la classe Vue_OpenGL (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.
Cela se fait en ajoutant un «timer» et une fonction associée à la gestion de ses évènements («tic-tac» de l'horloge). Commençons par écrire cette fonction
Elle doit simplement calculer la nouvelle position à chaque instant. Bien que pour le mouvement uniforme implémenté ici ce ne soit pas nécessaire, je choisis ici de vous donner un exemple utilisant la notion de «pas de temps» comme décrit dans l'introduction théorique (on pourrait bien sûr dans ce cas extrêmement simple utiliser simplement directement le temps absolu).
Ce qui nous donne :
void Vue_OpenGL::OnTimer(wxTimerEvent& event) { // mise à jour de la position à partir du pas de temps (via une // vitesse, arbitraire ici) position += event.GetInterval() / 15.0; while (position > 360.0) position -= 360.0; // demande l'affichage Refresh(false); }
Le prototype de cette «fonction» (ici méthode de la classe Vue_OpenGL en fait. Pensez à ajouter son prototype dans la classe) nous est imposé par la bibliothèque wxWidgets.
Il ne reste maintenant plus qu'à créer un «timer» et à associer cette fonction son évènement «pas de temps».
On a ici deux choix possibles pour rattacher le «timer» : soit à la Fenetre principale, soit à la Vue_OpenGL. Dans le cas présent cela ne change strictement rien, mais dans une application plus conséquente il faudrait réfléchir à ce que l'on souhaite.
Dans le premier cas (attachement à Fenetre) cela signifie que tous les dessins (etc.) associés à la fenêtre en question sont liés à ce même «timer» (on peut par exemple ici penser à plusieurs vues différentes de la même simulation).
Dans le second cas, nous aurions un «timer» différent par sous-fenêtre de dessin (on peut par exemple ici penser à plusieurs simulation se déroulant en parallèle). Les deux cas sont possibles, c'est juste une question de besoins.
Nous avons ici pris le parti d'attacher le timer à la sous-fenêtre Vue_OpenGL :
class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() { gluDeleteQuadric(sphere); } void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnSize(wxSizeEvent& evenement); void OnKeyDown(wxKeyEvent& evenement); void OnEnterWindow(wxMouseEvent& evenement) { SetFocus(); } void OnTimer(wxTimerEvent& event); void RotateTheta(GLdouble deg); void RotatePhi(GLdouble deg); void deplace(double dr); private: GLUquadric* sphere; double theta; double phi; double r; // le "Timer" wxTimer* timer; static int TIMER_ID; // le paramètre dépendant du temps double position; DECLARE_EVENT_TABLE() };
int Vue_OpenGL::TIMER_ID(12); BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_SIZE( Vue_OpenGL::OnSize ) EVT_KEY_DOWN( Vue_OpenGL::OnKeyDown ) EVT_ENTER_WINDOW( Vue_OpenGL::OnEnterWindow ) EVT_TIMER(TIMER_ID, Vue_OpenGL::OnTimer ) END_EVENT_TABLE()
Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) , sphere(gluNewQuadric()) , theta(35.0), phi(20.0), r(5.0) , timer(new wxTimer(this, TIMER_ID)) , position(0.0) {}
Reste à démarrer le «timer». Cela se fait comme suit :
timer->Start(20);
à la fin de InitOpenGL(), à la place du Refresh(false);
Le paramètre de Start() est le nombre minimum de millisecondes à attendre avant de déclencher le «timer». Cela contrôle en fait le nombre de fois par seconde que l'on souhaite voir s'exécuter la simulation.
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 :
// Pause sur la touche "espace" case ' ': if (timer->IsRunning()) { timer->Stop(); } else { timer->Start(); } 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 "wx/wxprec.h" #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/glcanvas.h" // Pour combiner wxWidgets et OpenGL // ---------------------------------------------------------------------- // Nouvelle application class GUI : public wxApp { public: bool OnInit(); }; // ---------------------------------------------------------------------- // sous-fenetre de dessin OpenGL class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() { gluDeleteQuadric(sphere); } void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnSize(wxSizeEvent& evenement); void OnKeyDown(wxKeyEvent& evenement); void OnEnterWindow(wxMouseEvent& evenement) { SetFocus(); } void OnTimer(wxTimerEvent& event); void RotateTheta(GLdouble deg); void RotatePhi(GLdouble deg); void deplace(double dr); private: GLUquadric* sphere; double theta; double phi; double r; // le "Timer" wxTimer* timer; static int TIMER_ID; // le paramètre dépendant du temps double position; DECLARE_EVENT_TABLE() }; // ---------------------------------------------------------------------- // fenetre(s) principale(s) class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: void OnExit(wxCommandEvent& event) { Close(true); } Vue_OpenGL* fogl; DECLARE_EVENT_TABLE() }; /* =================================================================== Implementation de Vue_OpenGL =================================================================== */ int Vue_OpenGL::TIMER_ID(12); BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_SIZE( Vue_OpenGL::OnSize ) EVT_KEY_DOWN( Vue_OpenGL::OnKeyDown ) EVT_ENTER_WINDOW( Vue_OpenGL::OnEnterWindow ) EVT_TIMER(TIMER_ID, Vue_OpenGL::OnTimer ) END_EVENT_TABLE() // ====================================================================== Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) , sphere(gluNewQuadric()) , theta(35.0), phi(20.0), r(5.0) , timer(new wxTimer(this, TIMER_ID)) , position(0.0) {} // ====================================================================== void Vue_OpenGL::dessine(wxPaintEvent&) { if (!GetContext()) return; SetCurrent(); // 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(); SwapBuffers(); } // ====================================================================== void Vue_OpenGL::OnSize(wxSizeEvent& event) { // Nécessaire pour mettre à jour le contexte sur certaines plateformes wxGLCanvas::OnSize(event); if (GetContext()) { // set GL viewport (not called by wxGLCanvas::OnSize on all platforms...) int w, h; GetClientSize(&w, &h); SetCurrent(); glViewport(0, 0, (GLint) w, (GLint) h); } } // ====================================================================== void Vue_OpenGL::OnKeyDown( wxKeyEvent& event ) { switch( event.GetKeyCode() ) { case WXK_LEFT: RotatePhi( 2.0); Refresh(false); break; case WXK_RIGHT: RotatePhi( -2.0); Refresh(false); break; case WXK_UP: RotateTheta( 2.0); Refresh(false); break; case WXK_DOWN: RotateTheta(-2.0); Refresh(false); break; case WXK_HOME: r = 5.0; // On revient à un point fixé theta = 35.0; phi = 20.0; Refresh(false); break; case WXK_PAGEUP: deplace(-1.0); // On se rapproche Refresh(false); break; case WXK_PAGEDOWN: deplace(1.0); // On s'éloigne Refresh(false); break; // Pause sur la touche "espace" case ' ': if (timer->IsRunning()) { timer->Stop(); } else { timer->Start(); } break; } event.Skip(); } // ====================================================================== void Vue_OpenGL::RotateTheta(GLdouble deg) { theta += deg; while (theta < -180.0) { theta += 360.0; } while (theta > 180.0) { theta -= 360.0; } } // ====================================================================== void Vue_OpenGL::RotatePhi(GLdouble deg) { phi += deg; while (phi < 0.0) { phi += 360.0; } while (phi > 360.0) { phi -= 360.0; } } // ====================================================================== void Vue_OpenGL::deplace(GLdouble dr) { r += dr; if (r < 1.0) r = 1.0; else if (r > 1000.0) r = 1000.0; } // ====================================================================== void Vue_OpenGL::InitOpenGL() { // Initialisation OpenGL SetCurrent(); // 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); // lance le Timer timer->Start(20); } // ====================================================================== void Vue_OpenGL::OnTimer(wxTimerEvent& event) { // mise à jour de la position à partir du pas de temps (via une // vitesse, arbitraire ici) position += event.GetInterval() / 15.0; while (position > 360.0) position -= 360.0; // demande l'affichage Refresh(false); } /* =================================================================== Implementation de Fenetre =================================================================== */ BEGIN_EVENT_TABLE(Fenetre, wxFrame) EVT_MENU( wxID_EXIT, Fenetre::OnExit ) END_EVENT_TABLE() // ====================================================================== Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Barre de menu wxMenu* winMenu = new wxMenu; winMenu->Append(wxID_EXIT, wxT("&Close")); wxMenuBar* menuBar = new wxMenuBar; menuBar->Append(winMenu, wxT("&Window")); SetMenuBar(menuBar); // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); } /* =================================================================== Implementation de l'application principale =================================================================== */ // ====================================================================== bool GUI::OnInit() { // Crée la fenêtre principale Fenetre* f = new Fenetre(wxT("Simulation temps reel"), wxSize(800, 600)); SetTopWindow(f); return (f != 0); } /* =================================================================== Equivalent de main() =================================================================== */ IMPLEMENT_APP(GUI)
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 "wx/wxprec.h" #ifndef WX_PRECOMP #include "wx/wx.h" #endif #include "wx/glcanvas.h" // Pour combiner wxWidgets et OpenGL #include <cmath> // pour la fonction sin() // ---------------------------------------------------------------------- // Nouvelle application class GUI : public wxApp { public: bool OnInit(); }; // ---------------------------------------------------------------------- // sous-fenetre de dessin OpenGL class Vue_OpenGL : public wxGLCanvas { public: Vue_OpenGL( wxWindow* parent , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition ); virtual ~Vue_OpenGL() {} void InitOpenGL(); protected: void dessine(wxPaintEvent& evenement); void OnSize(wxSizeEvent& evenement); void OnEnterWindow(wxMouseEvent& evenement) { SetFocus(); } DECLARE_EVENT_TABLE() }; // ---------------------------------------------------------------------- // fenetre(s) principale(s) class Fenetre: public wxFrame { public: Fenetre( wxString const& titre , wxSize const& taille = wxDefaultSize , wxPoint const& position = wxDefaultPosition , long style = wxDEFAULT_FRAME_STYLE ); virtual ~Fenetre() {} protected: void OnExit(wxCommandEvent& event) { Close(true); } Vue_OpenGL* fogl; DECLARE_EVENT_TABLE() }; /* =================================================================== Implementation de Vue_OpenGL =================================================================== */ BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) EVT_PAINT( Vue_OpenGL::dessine ) EVT_SIZE( Vue_OpenGL::OnSize ) EVT_ENTER_WINDOW( Vue_OpenGL::OnEnterWindow ) END_EVENT_TABLE() // ====================================================================== Vue_OpenGL::Vue_OpenGL( wxWindow* parent , wxSize const& taille , wxPoint const& position ) : wxGLCanvas(parent, wxID_ANY, position, taille, wxSUNKEN_BORDER|wxFULL_REPAINT_ON_RESIZE , wxT("") ) {} // ====================================================================== void Vue_OpenGL::dessine(wxPaintEvent&) { if (!GetContext()) return; SetCurrent(); // 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(); SwapBuffers(); } // ====================================================================== void Vue_OpenGL::OnSize(wxSizeEvent& event) { // Nécessaire pour mettre à jour le contexte sur certaines plateformes wxGLCanvas::OnSize(event); if (GetContext()) { // set GL viewport (not called by wxGLCanvas::OnSize on all platforms...) int w, h; GetClientSize(&w, &h); SetCurrent(); glViewport(0, 0, (GLint) w, (GLint) h); } } // ====================================================================== void Vue_OpenGL::InitOpenGL() { // Initialisation OpenGL SetCurrent(); // 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); } /* =================================================================== Implementation de Fenetre =================================================================== */ BEGIN_EVENT_TABLE(Fenetre, wxFrame) EVT_MENU( wxID_EXIT, Fenetre::OnExit ) END_EVENT_TABLE() // ====================================================================== Fenetre::Fenetre( wxString const& titre , wxSize const& taille , wxPoint const& position , long style ) : wxFrame(0, wxID_ANY, titre, position, taille, style) , fogl(new Vue_OpenGL(this)) { // Barre de menu wxMenu* winMenu = new wxMenu; winMenu->Append(wxID_EXIT, wxT("&Close")); wxMenuBar* menuBar = new wxMenuBar; menuBar->Append(winMenu, wxT("&Window")); SetMenuBar(menuBar); // Affiche (montre) la fenetre Show(true); // Initialise OpenGL fogl->InitOpenGL(); } /* =================================================================== Implementation de l'application principale =================================================================== */ // ====================================================================== bool GUI::OnInit() { // Crée la fenêtre principale Fenetre* f = new Fenetre(wxT("Fonction sinus"), wxSize(800, 600)); SetTopWindow(f); return (f != 0); } /* =================================================================== Equivalent de main() =================================================================== */ IMPLEMENT_APP(GUI)
Comment gérer des évènements «souris» ?
On peut associer une fonction soit à un évènement particulier (par exemple EVT_LEFT_DOWN) soit à n'importe quel évènement «souris» (EVT_MOUSE_EVENTS) et traité ensuite tel ou tel évènement dans la fonction en question.
Par exemple pour tourner la caméra avec la souris, bouton gauche enfoncé :
BEGIN_EVENT_TABLE(Vue_OpenGL, wxGLCanvas) ... EVT_MOUSE_EVENTS(Vue_OpenGL::OnMouse) END_EVENT_TABLE() ... // ====================================================================== void Vue_OpenGL::OnMouse(wxMouseEvent& event) { if (event.LeftIsDown()) { long x, y; event.GetPosition(&x, &y); int w, h; GetClientSize(&w, &h); if ((x >= 0) and (y >= 0) and (x <= w) and (y <= h)) { theta = 180.0 * (1.0 + 2.0 * (x-w) / double(w)); phi = 180.0 * (1.0 + 2.0 * (y-h) / double(h)); Refresh(false); } } event.Skip(); }
Voir ici pour plus de détails.
Comment faire une fenêtre en plein écran ?
Il suffit d'appeler la méthode ShowFullScreen de la fenêtre principale :
ShowFullScreen(true);
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.