Faculté Informatique & Communications
Cours d'informatique

EPFL
EPFL

Tutoriel OpenGL + wxWidgets

Introduction

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.

Dessin 3D

Quelques concepts de base

Boucle principale

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 :

  1. gestion des évènements ;
  2. évolution du système ;
  3. dessin de la nouvelle image.

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.

Matrice courante

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().

Quelques commandes

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)

Pour installer

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

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`

Premier exemple : dessiner une sphère

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).

Deuxième exemple : évènements clavier

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 :

  1. 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);
  2. 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     )
  3. 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 :

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)

Troisième exemple : dessin de plusieurs objets

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.

Quatrième exemple : gestion du point de vue

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)

Simulation en temps réel

Un peu de théorie

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 :

  1. calcul (ou mise à jour) : on détermine l'état suivant du système, à partir de l'état courant et du pas de temps dt ; c'est dans cette phase qu'interviennent les équations de la simulation ;
  2. affichage à l'écran (ou sur tout autre périphérique de sortie) : on envoie les données vers la carte vidéo (ou un fichier du disque, ...) ;
  3. gestion des interactions (clavier, souris).

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.

Cinquième 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)

Compléments

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 :

Bibliographie


Auteur : J.-C. Chappelier
Dernière mise à jour : $Date: 2008/04/11 13:07:41 $   ($Revision: 1.4 $)