Faculté Informatique & Communications
Cours d'informatique

EPFL
EPFL

Tutoriel OpenGL + SDL

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 SDL et OpenGL. Nous abordons également le thème de la simulation dans un environnement graphique en temps réel.

NOTE : Le matériel présenté ici ne faisant pas partie des objectifs du cours (mais étant simplement un outil pour le projet de cette année), nous aurons une approche pratique par l'exemple, très superficielle, plutôt qu'une approche théorique et complète.

OpenGL propose une série de fonctions évoluées, compatibles avec toutes les plates-formes (Unix, Linux, MacOS, ...) et très performantes. Le dessin s'effectue grâce à des primitives géométriques (e.g. polygones) en 2D ou 3D. Ces primitives peuvent êtres colorées, texturées et animées.

De son coté, la bibliothèque SDL adresse le problème de l'interaction avec l'utilisateur. Elle permet en effet de gérer, entre autres :

et ce de façon indépendante de la plate-forme.

NOTE : OpenGL et SDL ne sont, bien entendu, pas les seules bibliothèques permettant d'effectuer ces tâches. Dans le cadre de ce cours vous pouvez aussi utiliser wxWidgets ou glut à la place de SDL.

Ce tutoriel ne constitue qu'une brève présentation de ces deux bibliothèques (SDL et OpenGL). La meilleure méthode d'apprentissage est de lire chaque exemple en entier, puis compiler l'exemple et regarder ce qui se passe en interagissant avec le programme. Dans un troisième temps, il est intéressant de modifier les paramètres des fonctions présentées (couleurs, positions, touches appuyées,...) afin de voir ce qui change. Finalement vous pouvez essayer de refaire le même programme.

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 SDL, c'est qu'il ne s'agit pas d'un programme de dessin séquentiel («dessine ceci», puis «dessine cela», etc.), mais de «programmation événementielle» (i.e. «à base d'«évènements») où tout se passe dans une boucle infinie qui ne fait qu'attendre des évènements (clic de souris, touche au clavier, redimensionnement de la fenêtre ou tout simplement l'évènement «dessine») et appelle les fonctions correspondant à ces évènements.

Une première étape consiste donc à programmer toutes ces fonctions que l'on veut associer à des évènements.

Une seconde étape consiste à initialiser tout ce qu'il faut (e.g. la fenêtre de dessin, les boutons, barres, etc.)

Puis enfin (et seulement en fin !) on lance la fameuse boucle infinie. Ce n'est qu'une fois cette boucle lancée que l'on pourra voir quelque chose. De manière générale, la boucle se déroule de la manière suivante :

  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 SDL, il existe une couche spécifique pour la gestion des représentations 2D et 3D. Cette couche s'appelle OpenGL, et c'est sur elle que nous allons maintenant nous focaliser.

NOTE : OpenGL n'est pas à strictement parler un composant de la bibliothèque SDL, mais une couche plus basse, indépendante, à laquelle on peut «parler» et que l'on peut incorporer dans les objets de la SDL.

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

SDL et OpenGL sont installées sur les ordinateurs du CO. Si vous voulez utiliser ces bibliothèques chez vous, il faudra les installer.

Pour compiler

Pour compiler un programme avec les bibliothèques SDL et OpenGL, il est nécessaire de donner au compilateur des directives dans ce sens.

Concrètement, dans les salles CO vous devez ajouter les options :

-lSDL -lGL -lGLU

lors de l'édition de liens.

Je vous conseille pour cela d'utiliser un «Makefile» dans lequel vous aurez ajouté :

LDLIBS   = -lSDL -lGLU -lGL

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. SDL peut tout à fait fonctionner sans OpenGL , mais ce n'est pas ce que nous aborderons ici..
De ce fait, SDL impose une certaine structuration aux objets graphiques utilisés, ce qui dans notre cas à pour conséquence que la fenêtre de dessin OpenGL proprement dite sera en fait inclue dans une fenêtre plus large (à laquelle on pourra plus tard attacher d'autres choses comme des menus et) qui est elle même inclue dans l'application principale.

En clair on a :

Fenêtre principale (SDL) --> contenu OpenGL

Sans expliquer les détails ici (voir les commentaires dans le code) , voici le code correspondant (que l'on pourra bien sûr modulariser en plusieurs fichiers) :

NOTE : Certains paramètres des fonctions ne sont pas expliqués. Soit ces paramètres seront expliqués plus loin, soit ils ne sont pas intéressants pour ce tutoriel. Les fonctions de la SDL commencent toutes par SDL... et celles de OpenGL par gl... ou glu..., les autres fonctions sont définies par le programmeur, c'est-à-dire vous.

// On inclut les bibliothèques nécessaires
#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
 
int main()
{
    SDL_Init(SDL_INIT_VIDEO);  // Initialise la SDL
 
    // On crée une fenêtre de taille 800x600
    SDL_SetVideoMode(800, 600, 32, SDL_OPENGL);
    // et on lui donne un titre
    SDL_WM_SetCaption("TITRE", 0);
 
    initOpenGL();        // Initialise OpenGL
 
    while(true)          // On crée une boucle infinie
    {                        // dans laquelle on effectue le dessin
        dessiner();
                        // Et on fait une petite pause
        pause();
    }
 
    SDL_Quit();                // On quitte la SDL
    return 0;
}

Tout ceci est bien beau, mais ne compile pas pour le moment. Il reste en effet à implémenter les trois fonctions initOpenGL(), dessiner() et pause().
Ce que nous n'avons pour le moment pas fait !

La première fonction à écrire est celle qui dessine la scène, ici une sphère.

Pour dessiner une sphère en OpenGL, nous allons avoir besoin d'un objet intermédiaire appelé «quadrique» (peut importe ici ce que c'est).

Le dessin de la sphère se fait ensuite comme suit (voir les commentaires dans le code) :

void dessiner()  // Dessine la scène (une sphère)
{
  /* On crée un objet de type quadrique.
   * C'est l'outil utilisé pour le dessin des sphères. */
  GLUquadric* sphere(gluNewQuadric());
 
  // commence par effacer l'ancienne image
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  /* part du système de coordonnées de base 
   * (dessin à l'origine : matrice identité) */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 
  // fixe le point de vue 
  gluLookAt(1.0, 1.0, 5.0,  // position (x, y, z) de l'oeil
            0.0, 0.0, 0.0,  // point visé  (x', y', z')
            0.0, 0.0, 1.0); /* vecteur representant la direction verticale
                               de la vue (non parallèle à la direction de 
                               visée)                                      */
 
  /* défini la couleur (rouge, vert, bleu) et la transparence de tout
   * ce qui suit.
   * Ici : bleu pur pas du tout transparent                           */ 
  glColor4d(0.0, 0.0, 1.0, 1.0);
 
  // mode de dessin : ici, fil de fer
  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
 
  // dessine une sphère de rayon 1 découpée en 30x30 "morceaux"
  gluSphere(sphere, 1.0, 30, 30);
 
  // Finalement, on envoie le dessin à l'écran
  glFlush();
   SDL_GL_SwapBuffers();
 
   gluDeleteQuadric(sphere); // On détruit le quadrique 
}

Il est très important de détruire le «quadrique», sinon on a une fuite mémoire, laquelle peut finir par saturer l'ordinateur.

Si l'on code le tout dans une classe (conseillé !), il serait intéressant de déclarer le «quadrique» comme attribut de la classe afin de ne pas avoir à le créer et détruire à chaque image.

Voilà ! Nous avons fini avec le dessin, et pouvons donc passer à la fonction de d'initialisation d'OpenGL. Cela se fait dans la fonction initOpenGL(), par exemple de la façon suivante (encore une fois sans détailler ici) :

void initOpenGL()   // S'occupe d'initialiser quelques trucs pour OpenGL
{
  // active la gestion de la profondeur
  glEnable(GL_DEPTH_TEST);    
 
  // fixe la perspective
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65.0, 4./3., 1.0, 1000.0);
 
  // fixe la couleur du fond à noir
  glClearColor(0.0, 0.0, 0.0, 1.0);
}

Il ne reste plus qu'à écrire la dernière fonction, pause().

Le code de cette fonction sera détaillé plus loin, mais il est nécessaire au fonctionnement de la SDL. Je vous propose donc de copier la fonction et d'y revenir plus tard pour la comprendre.

void pause()
{
    SDL_Event event;
    SDL_PollEvent(&event);
    if (event.type == SDL_QUIT)
        exit(0);
}

Voilà ! On a terminé. Vous pouvez maintenant compiler et lancer le résultat pour voir si ça marche (ça devrait).

Voici le programme complet de ce premier exemple :

// On inclut les bibliothèques nécessaires
#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <cstdlib> // pour exit()
 
// Quelques prototypes de fonctions (voir plus loin) ---------------------------
 
void dessiner();
void initOpenGL();
void pause();
 
// Fonction main ---------------------------------------------------------------
 
int main()
{
    SDL_Init(SDL_INIT_VIDEO);  // Initialise la SDL
 
    // On crée une fenêtre de taille 800x600
    SDL_SetVideoMode(800, 600, 32, SDL_OPENGL);
    // et on lui donne un titre
    SDL_WM_SetCaption("Premier exemple", 0);
 
    initOpenGL();        // Initialise OpenGL
 
    while(true)          // On crée une boucle infinie
    {                        // dans laquelle on effectue le dessin
        dessiner();
                        // Et on fait une petite pause
        pause();
    }
 
    SDL_Quit();                // On quitte la SDL
    return 0;
}
 
// Fonctions -------------------------------------------------------------------
 
void initOpenGL()   // S'occupe d'initialiser quelques trucs pour OpenGL
{
  // active la gestion de la profondeur
  glEnable(GL_DEPTH_TEST);    
 
  // fixe la perspective
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65.0, 4./3., 1.0, 1000.0);
 
  // fixe la couleur du fond à noir
  glClearColor(0.0, 0.0, 0.0, 1.0);
}
 
void dessiner()  // Dessine la scène (une sphère)
{
  /* On crée un objet de type quadrique.
   * C'est l'outil utilisé pour le dessin des sphères. */
  GLUquadric* sphere(gluNewQuadric());
 
  // commence par effacer l'ancienne image
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  /* part du système de coordonnées de base 
   * (dessin à l'origine : matrice identité) */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 
  // fixe le point de vue 
  gluLookAt(1.0, 1.0, 5.0,  // position (x, y, z) de l'oeil
            0.0, 0.0, 0.0,  // point visé  (x', y', z')
            0.0, 0.0, 1.0); /* vecteur representant la direction verticale
                               de la vue (non parallèle à la direction de 
                               visée)                                      */
 
  /* défini la couleur (rouge, vert, bleu) et la transparence de tout
   * ce qui suit.
   * Ici : bleu pur pas du tout transparent                           */ 
  glColor4d(0.0, 0.0, 1.0, 1.0);
 
  // mode de dessin : ici, fil de fer
  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
 
  // dessine une sphère de rayon 1 découpée en 30x30 "morceaux"
  gluSphere(sphere, 1.0, 30, 30);
 
  // Finalement, on envoie le dessin à l'écran
  glFlush();
   SDL_GL_SwapBuffers();
 
   gluDeleteQuadric(sphere); // On détruit le quadrique 
}
 
// Le code de cette fonction sera expliqué plus loin (gestion des évènements)
void pause()
{
    SDL_Event event;
    SDL_PollEvent(&event);
    if (event.type == SDL_QUIT)
        exit(0);
}

NOTE 1 : Comme vous pouvez le constater, ce code bien que court et simple, n'est pas «orienté objets». Cela sera votre rôle d'encapsuler toutes ces fonctions dans un ensemble de classes ad hoc.

NOTE 2 : Vous avez peut être constaté que nous travaillons ici beaucoup avec les pointeurs (et cela sera vrai pour tout les objets graphiques). C'est normal, c'est justement une illustration de l'utilisation numéro 3 des pointeurs : avoir une durée de vie (tant que le programme tourne) plus grande que la portée (la «petite» fonction/méthode dans laquelle on crée l'objet en question).

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 <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <cstdlib> // pour exit()
 
// Quelques prototypes de fonctions (voir plus loin) ---------------------------
 
void dessiner();
void initOpenGL();
void pause();
 
// Fonction main ---------------------------------------------------------------
 
int main()
{
  SDL_Init(SDL_INIT_VIDEO);
  SDL_SetVideoMode(800, 600, 32, SDL_OPENGL);
  SDL_WM_SetCaption("Cube tournant", 0);
 
  initOpenGL();
 
  while(true) {
    dessiner();
    pause();
  }
 
  SDL_Quit();
  return 0;
}
 
// Fonctions -------------------------------------------------------------------
 
void initOpenGL()   // S'occupe d'initialiser quelques trucs pour OpenGL
{
  // active la gestion de la profondeur
  glEnable(GL_DEPTH_TEST);    
 
  // fixe la perspective
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65.0, 4./3., 1.0, 1000.0);
 
  // fixe la couleur du fond à noir
  glClearColor(0.0, 0.0, 0.0, 1.0);
}
 
void dessiner()  // Dessine la scène
{
  // commence par effacer l'ancienne image
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  /* part du système de coordonnées de base 
   * (dessin à l'origine : matrice identité) */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 
  // dessin a venir ici
 
  // Finalement, on envoie le dessin à l'écran
  glFlush();
   SDL_GL_SwapBuffers();
}
 
void pause()
{
    SDL_Event event;
    SDL_PollEvent(&event);
    if (event.type == SDL_QUIT)
        exit(0);
}

Le premier évènement que l'on aimerait gérer est celui qui permettrait de quitter le programme, c'est-à-dire interrompre la boucle infinie. Une solution, parmi d'autres,est de créer une fonction evenements() qui renvoie true si le programme doit continuer et false dans le cas contraire.

On peut donc prototyper la fonction evenements() de la manière suivante :

bool evenements();

Dans SDL, la gestion des évènements se déroule en trois étapes. La première est de créer un objet qui pourra contenir des informations sur l'évènement qui a eu lieu :

bool evenements()
{
  SDL_Event monEvenement;   

Il faut ensuite demander à la SDL de «remplir» cette variable. Cela se fait via l'appel à la fonction

SDL_PollEvent(SDL_Event*);

On peut alors ajouter :

bool evenements()
{
  SDL_Event monEvenement;   
 
  // On demande à la SDL quel évènement a eu lieu  
  SDL_PollEvent(&monEvenement); 

A ce moment, il ne reste plus qu'à regarder quel type d'évènement est contenu dans monEvenement. La SDL propose plein de types d'évènements, ceux qui nous intéressent ici, sont SDL_QUIT qui correspond à un clic sur la «croix» de la fenêtre et SDL_KEYDOWN qui correspond à l'appui sur une touche du clavier. On peut alors écrire la fonction de la manière suivante :

bool evenements()
{
  SDL_Event monEvenement;   
 
  // On demande à la SDL quel évènement a eu lieu  
  SDL_PollEvent(&monEvenement); 
 
  if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT
    return false;                     // On renvoit false (arrêt)

Dans le cas d'un évènement de type SDL_KEYDOWN, il faut savoir quelle touche a été enfoncée. A chaque touche du clavier, est associé un code, pour la touche «Escape», il s'agit de SDLK_ESCAPE et, pour la touche 'q', de SDLK_q . La liste complète des "codes" correspondant aux touches est disponible ici : SDLKey (il est évident qu'il ne faut pas les apprendre. Il faut juste connaître le lien pour les retrouver ;-) ). On obtient donc dans notre cas, le code suivant :

bool evenements()
{
  SDL_Event monEvenement;   
 
  // On demande à la SDL quel évènement a eu lieu  
  SDL_PollEvent(&monEvenement); 
 
  if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT
    return false;                     // On renvoit false (arrêt)
 
  else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier
    switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée
    case SDLK_ESCAPE:            // Si c'est "Esc"
    case SDLK_q:                 // Si c'est "q"
      return false;                 // On renvoie false (arrêt)

Il ne reste plus alors qu'à modifier le main() pour que la boucle se termine si la fonction renvoie false. On en profite également pour enlever la fonction pause() devenue obsolète.

int main()
{
  SDL_Init(SDL_INIT_VIDEO);
  SDL_SetVideoMode(800, 600, 32, SDL_OPENGL);
  SDL_WM_SetCaption("Cube tournant", 0);
 
  initOpenGL();                // Initialise OpenGL
 
  SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées
 
  bool continuer(true);        // bool pour savoir si on doit continuer
 
  // On crée une boucle qui va tourner tant que continuer est vrai
  do {
 
    /* Dans laquelle on teste les évènements et voir si le programme 
     * doit continuer                                                */
    continuer=evenements();
 
    // Et on dessine la scène
    dessiner();
 
  } while(continuer);
 
  SDL_Quit();                //On quitte la SDL
  return 0;
}

NOTE : Vous pouvez compiler (et exécuter) le code à ce stade. Ce que je vous encourage à faire pour vérifier (et voir le résultat). Même si cela ne dessine rien, vous devriez constater que le programme se termine bien si l'on appuie sur 'Esc' ou 'q'.

Notre but premier était de dessiner un cube qui tourne. Commençons donc par dessiner le cube, puis nous finirons par la gestion des évènements clavier «flèches».

Pour dessiner le cube, il faut procéder en plusieurs étapes. La première consiste à découper ce cube en faces. En effet OpenGL ne peut dessiner que des lignes, triangles ou quadrilatères (les sphères sont des objets à part). On va donc découper le cube en six faces carrées. Chaque face est ensuite dessinée en indiquant les coordonnées de chacun de ses coins. On peut également spécifier une couleur pour la face. Ce qui donne pour une face le code :

glBegin(GL_QUADS);   //On indique à OpenGL qu'on veut dessiner un quadrilatère
glColor(0.0,0.0,1.0,1.0);  //On spécifie une couleur (bleu).
			   //Les couleurs sont expliquées à la fin de ce tutoriel
 
glVertex3d(0.0,0.0,0.0);   //On indique un premier coin en (0,0,0)
glVertex3d(1.0,0.0,0.0);   //un deuxième en (1,0,0)
glVertex3d(1.0,1.0,0.0);   //etc...
glVertex3d(0.0,1.0,0.0);
 
glEnd();		   //On indique à OpenGL qu'on a terminé le quadrilatère

NOTE 1 : Pour dessiner un triangle, il faut utiliser glBegin(GL_TRIANGLES) et spécifier trois points ; pour une ligne, ce sera glBegin(GL_LINES) en spécifiant deux points.

NOTE 2 : On peut également spécifier une couleur par sommet, ce qui donne de jolis effets. Il faut simplement savoir que lorsqu'on appelle glColor() tout sera colorié de cette couleur jusqu'au prochain appel à cette fonction.

NOTE 3 : Si l'on veut dessiner plusieurs quadrilatères à la suite, on a pas besoin d'appeler chaque fois glBegin() et glEnd(). Une fois au début et à la fin suffit. Il faut juste qu'il y ait un nombre correct de sommets (un multiple de 4).

Le dessin de notre cube devient alors (cube d'arête 1 centré à l'origine) :

  // dessine les 6 faces du cube
  glBegin(GL_QUADS);
  glColor4d(0.0, 0.0, 1.0, 1.0); // bleu
  glVertex3d( 0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5);
  glVertex3d(-0.5,-0.5, 0.5); glVertex3d( 0.5,-0.5, 0.5);
 
  glColor4d(0.0, 1.0, 1.0, 1.0); // turquoise (cyan=bleu+vert)
  glVertex3d(-0.5,-0.5,-0.5); glVertex3d(-0.5, 0.5,-0.5);
  glVertex3d( 0.5, 0.5,-0.5); glVertex3d( 0.5,-0.5,-0.5);
 
  glColor4d(1.0, 0.0, 1.0, 1.0); // violet (magenta=rouge+bleu)
  glVertex3d( 0.5, 0.5, 0.5); glVertex3d( 0.5, 0.5,-0.5);
  glVertex3d(-0.5, 0.5,-0.5); glVertex3d(-0.5, 0.5, 0.5);
 
  glColor4d(1.0, 0.0, 0.0, 1.0); // rouge
  glVertex3d(-0.5,-0.5,-0.5); glVertex3d( 0.5,-0.5,-0.5);
  glVertex3d( 0.5,-0.5, 0.5); glVertex3d(-0.5,-0.5, 0.5);
 
  glColor4d(1.0, 1.0, 0.0, 1.0); // jaune (=rouge+vert)
  glVertex3d( 0.5, 0.5, 0.5); glVertex3d( 0.5,-0.5, 0.5);
  glVertex3d( 0.5,-0.5,-0.5); glVertex3d( 0.5, 0.5,-0.5);
 
  glColor4d(1.0, 1.0, 1.0, 1.0); // blanc (=rouge+vert+bleu)
  glVertex3d(-0.5,-0.5,-0.5); glVertex3d(-0.5,-0.5, 0.5);
  glVertex3d(-0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5,-0.5);
  glEnd();

Il ne faut bien sûr pas oublier de placer la «caméra» (i.e. le point de vue) avant, par exemple :

  // fixe le point de vue 
  gluLookAt(5.0, 0.0, 0.0,  // position (x, y, z) de l'oeil
            0.0, 0.0, 0.0,  // point visé (x', y', z')
            0.0, 0.0, 1.0); /* vecteur representant la direction verticale 
                                (non parallèle à la direction de visée) */

NOTE : Vous pouvez compiler (et exécuter) à ce stade. Le cube devrait se dessiner...
...mais il ne peut pas encore bouger.

Pour faire tourner le cube, il faut déclarer deux variables représentant les angles de rotation par rapport aux axes y et z (coordonnées sphériques de la caméra en fait). Dans cet exemple, ces deux variables seront globales ; c'est de la mauvaise programmation, mais cela permet ici de simplifier l'exemple. A vous de faire mieux.

NOTE : Le mécanisme permettant de faire tourner le cube sera détaillé dans le quatrième exemple. Le but ici est de comprendre le fonctionnement des évènements.

double theta(35.0);
double phi(20.0);

Pour faire tourner le cube, il suffit d'incrémenter la valeur d'un de ces angles lors de l'appui sur une touche. Cela se fait dans la fonction evenements() en supposant (approche modulaire) qu'il existe deux fonctions permettant de faire tourner la caméra d'un angle alpha autour d'un des axes précisés plus haut (on tourne ici par exemple de 2 degrés) :  :

bool evenements()
{
  SDL_Event monEvenement;   
 
  // On demande à la SDL quel évènement a eu lieu  
  SDL_PollEvent(&monEvenement); 
 
  if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT
    return false;                     // On renvoit false (arrêt)
 
  else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier
    switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée
    case SDLK_ESCAPE:            // Si c'est "Esc"
    case SDLK_q:                 // Si c'est "q"
      return false;                 // On renvoie false (arrêt)
 
    case SDLK_UP:                // Si c'est la flèche du haut
      RotateTheta( 2.0);        // On incrémente theta
      break;
 
    case SDLK_DOWN:                //idem pour les autres
      RotateTheta(-2.0);
      break;
 
    case SDLK_RIGHT:
      RotatePhi( -2.0);
      break;
 
    case SDLK_LEFT:
      RotatePhi( 2.0);
      break;
     }
 
   return true; /* Pour tous les autres évènements, le programme doit
                 * continuer (on renvoit donc true).                  */
}

Reste donc à écrire ces fonctions Rotate :

void RotateTheta(double deg)
{
  theta += deg;
  while (theta < -180.0) { theta += 360.0; }
  while (theta >  180.0) { theta -= 360.0; }
}
 
void RotatePhi(double deg)
{
  phi += deg;
  while (phi <   0.0) { phi += 360.0; }
  while (phi > 360.0) { phi -= 360.0; }
}

(J'y ai ici inclus la normalisation des angles, entre 0 et 360 degrés pour phi et entre -180 et +180 pour theta.)

Pour finir, il n'y a qu'à ajouter la ligne

SDL_EnableKeyRepeat(50, 50);

juste avant la boucle principale pour que la SDL puisse gérer le cas des touches qui restent enfoncées.

Voilà pour ce second exemple ! Le code complet est :

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
 
// Quelques prototypes de fonctions (voir plus loin) ---------------------------
 
void dessiner();
void initOpenGL();
bool evenements();
void RotateTheta(double angle);
void RotatePhi(double angle);
 
// Variables globales ----------------------------------------------------------
 
double theta(35.0);
double phi(20.0);
 
// Fonction main ---------------------------------------------------------------
 
int main()
{
  SDL_Init(SDL_INIT_VIDEO);
  SDL_SetVideoMode(800, 600, 32, SDL_OPENGL);
  SDL_WM_SetCaption("Cube tournant", 0);
 
  initOpenGL();                // Initialise OpenGL
 
  SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées
 
  bool continuer(true);        // bool pour savoir si on doit continuer
 
  // On crée une boucle qui va tourner tant que continuer est vrai
  do {
 
    /* Dans laquelle on teste les évènements et voir si le programme 
     * doit continuer                                                */
    continuer=evenements();
 
    // Et on dessine la scène
    dessiner();
 
  } while(continuer);
 
  SDL_Quit();                //On quitte la SDL
  return 0;
}
 
// Fonctions -------------------------------------------------------------------
 
void initOpenGL()   // S'occupe d'initialiser quelques trucs pour OpenGL
{
  // active la gestion de la profondeur
  glEnable(GL_DEPTH_TEST);    
 
  // fixe la perspective
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65.0, 4./3., 1.0, 1000.0);
 
  // fixe la couleur du fond à noir
  glClearColor(0.0, 0.0, 0.0, 1.0);
}
 
void dessiner()  //Dessine la scène (un cube)
{
  // commence par effacer l'ancienne image
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  /* part du système de coordonnées de base 
   * (dessin à l'origine : matrice identité) */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 
  // fixe le point de vue 
  gluLookAt(5.0, 0.0, 0.0,  // position (x, y, z) de l'oeil
            0.0, 0.0, 0.0,  // point visé (x', y', z')
            0.0, 0.0, 1.0); /* vecteur representant la direction verticale 
                                (non parallèle à la direction de visée) */
 
   // effectue la rotation
   glRotated(theta, 0.0, 1.0, 0.0);        //(angle, axe)
   glRotated(phi,   0.0, 0.0, 1.0);
 
  // dessine les 6 faces du cube
  glBegin(GL_QUADS);
  glColor4d(0.0, 0.0, 1.0, 1.0); // bleu
  glVertex3d( 0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5, 0.5);
  glVertex3d(-0.5,-0.5, 0.5); glVertex3d( 0.5,-0.5, 0.5);
 
  glColor4d(0.0, 1.0, 1.0, 1.0); // turquoise (cyan=bleu+vert)
  glVertex3d(-0.5,-0.5,-0.5); glVertex3d(-0.5, 0.5,-0.5);
  glVertex3d( 0.5, 0.5,-0.5); glVertex3d( 0.5,-0.5,-0.5);
 
  glColor4d(1.0, 0.0, 1.0, 1.0); // violet (magenta=rouge+bleu)
  glVertex3d( 0.5, 0.5, 0.5); glVertex3d( 0.5, 0.5,-0.5);
  glVertex3d(-0.5, 0.5,-0.5); glVertex3d(-0.5, 0.5, 0.5);
 
  glColor4d(1.0, 0.0, 0.0, 1.0); // rouge
  glVertex3d(-0.5,-0.5,-0.5); glVertex3d( 0.5,-0.5,-0.5);
  glVertex3d( 0.5,-0.5, 0.5); glVertex3d(-0.5,-0.5, 0.5);
 
  glColor4d(1.0, 1.0, 0.0, 1.0); // jaune (=rouge+vert)
  glVertex3d( 0.5, 0.5, 0.5); glVertex3d( 0.5,-0.5, 0.5);
  glVertex3d( 0.5,-0.5,-0.5); glVertex3d( 0.5, 0.5,-0.5);
 
  glColor4d(1.0, 1.0, 1.0, 1.0); // blanc (=rouge+vert+bleu)
  glVertex3d(-0.5,-0.5,-0.5); glVertex3d(-0.5,-0.5, 0.5);
  glVertex3d(-0.5, 0.5, 0.5); glVertex3d(-0.5, 0.5,-0.5);
  glEnd();
 
  // Finalement, on envoie le dessin à l'écran
  glFlush();
   SDL_GL_SwapBuffers();
}
 
bool evenements()
{
  SDL_Event monEvenement;   
 
  // On demande à la SDL quel évènement a eu lieu  
  SDL_PollEvent(&monEvenement); 
 
  if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT
    return false;                     // On renvoit false (arrêt)
 
  else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier
    switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée
    case SDLK_ESCAPE:            // Si c'est "Esc"
    case SDLK_q:                 // Si c'est "q"
      return false;                 // On renvoie false (arrêt)
 
    case SDLK_UP:                // Si c'est la flèche du haut
      RotateTheta( 2.0);        // On incrémente theta
      break;
 
    case SDLK_DOWN:                //idem pour les autres
      RotateTheta(-2.0);
      break;
 
    case SDLK_RIGHT:
      RotatePhi( -2.0);
      break;
 
    case SDLK_LEFT:
      RotatePhi( 2.0);
      break;
     }
 
   return true; /* Pour tous les autres évènements, le programme doit
                 * continuer (on renvoit donc true).                  */
}
 
void RotateTheta(double deg)
{
  theta += deg;
  while (theta < -180.0) { theta += 360.0; }
  while (theta >  180.0) { theta -= 360.0; }
}
 
void RotatePhi(double deg)
{
  phi += deg;
  while (phi <   0.0) { phi += 360.0; }
  while (phi > 360.0) { phi -= 360.0; }
}

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 <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
 
// Quelques prototypes de fonctions (voir plus loin) ---------------------------
 
void dessiner();
void initOpenGL();
bool evenements();
void RotateTheta(double angle);
void RotatePhi(double angle);
 
// Variables globales ----------------------------------------------------------
 
double theta(35.0);
double phi(20.0);
 
// Fonction main ---------------------------------------------------------------
 
int main()
{
  SDL_Init(SDL_INIT_VIDEO);
  SDL_SetVideoMode(800, 600, 32, SDL_OPENGL);
  SDL_WM_SetCaption("Exemple 3", 0);
 
  initOpenGL();                // Initialise OpenGL
 
  SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées
 
  bool continuer(true);        // bool pour savoir si on doit continuer
 
  // On crée une boucle qui va tourner tant que continuer est vrai
  do {
 
    /* Dans laquelle on teste les évènements et voir si le programme 
     * doit continuer                                                */
    continuer=evenements();
 
    // Et on dessine la scène
    dessiner();
 
  } while(continuer);
 
  SDL_Quit();                //On quitte la SDL
  return 0;
}
 
// Fonctions -------------------------------------------------------------------
 
void initOpenGL()   // S'occupe d'initialiser quelques trucs pour OpenGL
{
  // active la gestion de la profondeur
  glEnable(GL_DEPTH_TEST);    
 
  // fixe la perspective
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65.0, 4./3., 1.0, 1000.0);
 
  // fixe la couleur du fond à noir
  glClearColor(0.0, 0.0, 0.0, 1.0);
}
 
void dessiner()  //Dessine la scène (2 sphères)
{
  GLUquadric* sphere(gluNewQuadric());  // Cree le quadrique pour les spheres
 
  // commence par effacer l'ancienne image
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  /* part du système de coordonnées de base 
   * (dessin à l'origine : matrice identité) */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 
  // fixe le point de vue 
  gluLookAt(5.0, 0.0, 0.0,  // position (x, y, z) de l'oeil
            0.0, 0.0, 0.0,  // point visé (x', y', z')
            0.0, 0.0, 1.0); /* vecteur representant la direction verticale 
                                (non parallèle à la direction de visée) */
 
   // effectue la rotation
   glRotated(theta, 0.0, 1.0, 0.0);        //(angle, axe)
   glRotated(phi,   0.0, 0.0, 1.0);
 
  // dessin de la première sphère
  glPushMatrix();                // Sauvegarder l'endroit où l'on se trouve
  glTranslated(-1.0, 0.0, 0.5);  /* Se positionner à l'endroit où l'on veut
				  * dessiner                                */
  glColor4d(0.0, 0.0, 1.0, 1.0); // Choisir la couleur (bleu ici)
  gluSphere(sphere, 1.0, 30, 30);// Dessiner une sphère
  glPopMatrix();                 // Revenir à l'ancienne position
 
  // dessin de la seconde sphère
  glPushMatrix();
  glTranslated(0.0, 0.0, 0.0); 
  glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici)
  gluSphere(sphere, 1.5, 50, 50);
  glPopMatrix();
 
  // Finalement, on envoie le dessin à l'écran
  glFlush();
   SDL_GL_SwapBuffers();
 
   // On détruit le quadrique 
   gluDeleteQuadric(sphere);
}
 
bool evenements()
{
  SDL_Event monEvenement;   
 
  // On demande à la SDL quel évènement a eu lieu  
  SDL_PollEvent(&monEvenement); 
 
  if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT
    return false;                     // On renvoit false (arrêt)
 
  else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier
    switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée
    case SDLK_ESCAPE:            // Si c'est "Esc"
    case SDLK_q:                 // Si c'est "q"
      return false;                 // On renvoie false (arrêt)
 
    case SDLK_UP:                // Si c'est la flèche du haut
      RotateTheta( 2.0);        // On incrémente theta
      break;
 
    case SDLK_DOWN:                //idem pour les autres
      RotateTheta(-2.0);
      break;
 
    case SDLK_RIGHT:
      RotatePhi( -2.0);
      break;
 
    case SDLK_LEFT:
      RotatePhi( 2.0);
      break;
     }
 
   return true; /* Pour tous les autres évènements, le programme doit
                 * continuer (on renvoit donc true).                  */
}
 
void RotateTheta(double deg)
{
  theta += deg;
  while (theta < -180.0) { theta += 360.0; }
  while (theta >  180.0) { theta -= 360.0; }
}
 
void RotatePhi(double deg)
{
  phi += deg;
  while (phi <   0.0) { phi += 360.0; }
  while (phi > 360.0) { phi -= 360.0; }
}

NOTE : Vous remarquerez que les deux sphères s'entrecoupent. C'est normal au vu des positions et des rayons que l'on a donné. Vous pouvez ainsi constater qu'OpenGL s'occupe de gérer l'affichage des choses qui sont devant et derrière de manière correcte. Ce n'est pas à vous de le faire.

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 à notre fichier  :

double theta(35.0);
double phi(20.0);
double  r(5.0);

L'utilisation de cette caméra se fait ensuite comme précédemment par son positionnement avant de dessiner les objets :

  // place la caméra
  gluLookAt(r,   0.0, 0.0,
            0.0, 0.0, 0.0,
            0.0, 0.0, 1.0);
  glRotated(theta, 0.0, 1.0, 0.0); 
  glRotated(phi,   0.0, 0.0, 1.0);

Il faut maintenant penser à la gestion de cette «caméra» via les touches du clavier. Cela se fait de façon similaire à ce que nous avions fait à l'exemple 2. Il suffit d'étendre les différents cas :

    case SDLK_PAGEUP:
      deplace(-1.0);    // On se rapproche
      break;
 
    case SDLK_PAGEDOWN:
      deplace(1.0);     // On s'éloigne
      break;
 
    case SDLK_HOME:
      r     =  5.0;     // On revient à un point fixé
      theta = 35.0;
      phi   = 20.0;
      break;

avec :

void deplace(double dr)
{
  r += dr;
  if (r < 1.0) r = 1.0;
  else if (r > 1000.0) r = 1000.0;
}

NOTE : Il y a une singularité au moment où la distance atteint 0. Le programmeur consciencieux devra donc éviter cela.

Pour finir (et par esthétisme), j'ai ajouté une troisième sphère ; ce qui donne pour le code complet de ce quatrième exemple (les principales différences avec l'exemple précédent sont indiquées en gras) :

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
 
// Quelques prototypes de fonctions (voir plus loin) ---------------------------
 
void dessiner();
void initOpenGL();
bool evenements();
void RotateTheta(double angle);
void RotatePhi(double angle);
 
// Variables globales ----------------------------------------------------------
 
double theta(35.0);
double phi(20.0);
double  r(5.0);
 
// Fonction main ---------------------------------------------------------------
 
int main()
{
  SDL_Init(SDL_INIT_VIDEO);
  SDL_SetVideoMode(800, 600, 32, SDL_OPENGL);
  SDL_WM_SetCaption("Exemple 4", 0);
 
  initOpenGL();                // Initialise OpenGL
 
  SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées
 
  bool continuer(true);        // bool pour savoir si on doit continuer
 
  // On crée une boucle qui va tourner tant que continuer est vrai
  do {
 
    /* Dans laquelle on teste les évènements et voir si le programme 
     * doit continuer                                                */
    continuer=evenements();
 
    // Et on dessine la scène
    dessiner();
 
  } while(continuer);
 
  SDL_Quit();                //On quitte la SDL
  return 0;
}
 
// Fonctions -------------------------------------------------------------------
 
void initOpenGL()   // S'occupe d'initialiser quelques trucs pour OpenGL
{
  // active la gestion de la profondeur
  glEnable(GL_DEPTH_TEST);    
 
  // fixe la perspective
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65.0, 4./3., 1.0, 1000.0);
 
  // fixe la couleur du fond à noir
  glClearColor(0.0, 0.0, 0.0, 1.0);
}
 
void dessiner()  // Dessine la scène (3 sphères)
{
  GLUquadric* sphere(gluNewQuadric());  // Cree le quadrique pour les spheres
 
  // commence par effacer l'ancienne image
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  /* part du système de coordonnées de base 
   * (dessin à l'origine : matrice identité) */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 
  // place la caméra
  gluLookAt(r,   0.0, 0.0,
            0.0, 0.0, 0.0,
            0.0, 0.0, 1.0);
  glRotated(theta, 0.0, 1.0, 0.0); 
  glRotated(phi,   0.0, 0.0, 1.0);
 
  // dessin de la première sphère
  glPushMatrix();                // Sauvegarder l'endroit où l'on se trouve
  glTranslated(-1.0, 0.0, 0.5);  /* Se positionner à l'endroit où l'on veut
				  * dessiner                                */
  glColor4d(0.0, 0.0, 1.0, 1.0); // Choisir la couleur (bleu ici)
  gluSphere(sphere, 1.0, 30, 30);// Dessiner une sphère
  glPopMatrix();                 // Revenir à l'ancienne position
 
  // dessin de la seconde sphère
  glPushMatrix();
  glTranslated(0.0, 0.0, 0.0); 
  glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici)
  gluSphere(sphere, 1.5, 50, 50);
  glPopMatrix();
 
  // dessin de la troisième sphère
  glPushMatrix();
  glTranslated(1.0, 0.0, 0.5);
  glColor4d(0.0, 0.8, 0.0, 1.0);  //vert
  gluSphere(sphere, 1.0, 50, 50);
  glPopMatrix();
 
  // Finalement, on envoie le dessin à l'écran
  glFlush();
   SDL_GL_SwapBuffers();
 
   // On détruit le quadrique 
   gluDeleteQuadric(sphere);
}
 
void deplace(double dr)
{
  r += dr;
  if (r < 1.0) r = 1.0;
  else if (r > 1000.0) r = 1000.0;
}
 
bool evenements()
{
  SDL_Event monEvenement;   
 
  // On demande à la SDL quel évènement a eu lieu  
  SDL_PollEvent(&monEvenement); 
 
  if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT
    return false;                     // On renvoit false (arrêt)
 
  else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier
    switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée
    case SDLK_ESCAPE:            // Si c'est "Esc"
    case SDLK_q:                 // Si c'est "q"
      return false;                 // On renvoie false (arrêt)
 
    case SDLK_UP:                // Si c'est la flèche du haut
      RotateTheta( 2.0);        // On incrémente theta
      break;
 
    case SDLK_DOWN:                //idem pour les autres
      RotateTheta(-2.0);
      break;
 
    case SDLK_RIGHT:
      RotatePhi( -2.0);
      break;
 
    case SDLK_LEFT:
      RotatePhi( 2.0);
      break;
 
    case SDLK_PAGEUP:
      deplace(-1.0);    // On se rapproche
      break;
 
    case SDLK_PAGEDOWN:
      deplace(1.0);     // On s'éloigne
      break;
 
    case SDLK_HOME:
      r     =  5.0;     // On revient à un point fixé
      theta = 35.0;
      phi   = 20.0;
      break;
     }
 
   return true; /* Pour tous les autres évènements, le programme doit
                 * continuer (on renvoit donc true).                  */
}
 
void RotateTheta(double deg)
{
  theta += deg;
  while (theta < -180.0) { theta += 360.0; }
  while (theta >  180.0) { theta -= 360.0; }
}
 
void RotatePhi(double deg)
{
  phi += deg;
  while (phi <   0.0) { phi += 360.0; }
  while (phi > 360.0) { phi -= 360.0; }
}

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

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 SDL, comme dans beaucoup d'autres bibliothèques similaires, le programme est «averti» qu'il doit procéder à l'une ou l'autre étape : c'est ce que l'on appelle de la «programmation événementielle» (i.e. «à base d'«évènements»).

Enfin, lorsqu'une certaine condition d'arrêt est atteinte (e.g. un certain délai dépassé, une précision suffisante ou un évènement particulier [e.g. clavier]), on arrête simplement le programme.

Passons maintenant à un exemple.

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é de façon globale au programme (dans une bonne conception d'un projet plus large, il faudrait bien sûr faire autrement !).

En l'état, le programme compile et fait un joli dessin, mais pas grand chose ne bouge. C'est normal nous n'avons pas encore ajouté dépendance temporelle à la variable position.

Pour se faire, on peut par exemple incrémenter l'angle de 1 degré à chaque tour de la boucle principale (dans le main()). Cela fonctionne, mais tout cela se passe sans «temps réel» (au sens ou les étapes du programme ne sont pas liées au temps physique se déroulant). Il faut pour cela définir un timer (et un pas de temps) qui va faire évoluer le système à chaque pas de temps :

   int dt(20);  // Pas de temps en milliseconde
...
    SDL_TimerID monTimer(0);  // Timer

Pour utiliser les «timers», il faut de plus dire à la SDL qu'on en a besoin. Cela se fait en modifiant la ligne d'initialisation de la SDL comme suit :

    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);

Il faut maintenant écrire la fonction qui sera appelée par le timer. Cette fonction doit obligatoirement avoir le prototype suivant :

Uint32 fonction(Uint32, void*)

Uint32 est un type déclaré dans la SDL, équivalent aux int (peut importe, de toute manière vous n'utiliserez pas les paramètres de cette fonction).

De plus, cette fonction doit impérativement renvoyer autre chose que 0.

Dans notre cas, on peut donc écrire :

Uint32 evolution(Uint32, void*)
{
  /* On gère ici l'évolution du système.
     Dans le cas simple de cet exemple : la position (angulaire)
     augemente de 1 degré tous les pas de temps (i.e. 1 degré par 
     20 ms)                                                       */
 
  position += 1.0;
  while (position > 360.0) position -= 360.0;
 
  return 1;
}

Il ne reste plus alors qu'à créer un timer et à l'associer à cette fonction

  /* On cree un timer, de pas de temps dt, et qui doit appeler la fonction
   * evolution                                                              */
  monTimer = SDL_AddTimer(dt, evolution, 0);

Le timer démarre automatiquement et va, toutes les 20ms (dt), appeler la fonction évolution.

NOTE : Vous pouvez compiler (et exécuter) à ce stade. La boule rouge devrait se dessiner...
...et tourner. Notez que l'on peut toujours, en même temps, utiliser les évènements clavier pour déplacer le point de vue.

Je vous propose pour finir sur ce sujet de rajouter une touche pour faire une pause dans la simulation. Comment faire ?

Il suffit pour cela de suspendre le «timer» à chaque fois qu'une pause a été demandée (et de le reprendre lors de la prochaine frappe au clavier). Il faut bien sûr associer, dans la fonction de gestion des évènements clavier, une touche (par exemple la barre espace ici) à cette action :

    case SDLK_SPACE:
      if (monTimer != 0) { // Si le timer tourne...
        SDL_RemoveTimer(monTimer);  // ...on l'enleve
        monTimer = 0;
      } else { // Sinon...
        monTimer = SDL_AddTimer(dt, evolution, 0); // ...on le remet
      }
      break;

Voici donc le code complet de ce cinquième exemple. Les principales différences avec l'exemple précédent sont indiquées en gras.

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
 
// Quelques prototypes de fonctions (voir plus loin) ---------------------------
 
void dessiner();
void initOpenGL();
bool evenements();
void RotateTheta(double angle);
void RotatePhi(double angle);
 
// Variables globales ----------------------------------------------------------
 
double theta(35.0);
double phi(20.0);
double  r(5.0);
 
// Pour la simulation temps réel -----------------------------------------------
 
Uint32 evolution(Uint32, void*);
 
double position(0.0);    // La position (angle) de la petite sphère qui tourne
unsigned int dt(20);     // Le pas de temps, en milliseconde
SDL_TimerID monTimer(0); // Timer
 
 
// Fonction main ---------------------------------------------------------------
 
int main()
{
  //On initialise la SDL, avec "timer" cette fois
  SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
 
  SDL_SetVideoMode(800, 600, 32, SDL_OPENGL);
  SDL_WM_SetCaption("Simulation temps reel", 0);
 
  initOpenGL();                // Initialise OpenGL
 
  SDL_EnableKeyRepeat(50,50); // autorise de laisser les touches enfoncées
 
  bool continuer(true);        // bool pour savoir si on doit continuer
 
  /* On cree un timer, de pas de temps dt, et qui doit appeler la fonction
   * evolution                                                              */
  monTimer = SDL_AddTimer(dt, evolution, 0);
 
  // On crée une boucle qui va tourner tant que continuer est vrai
  do {
 
    /* Dans laquelle on teste les évènements et voir si le programme 
     * doit continuer                                                */
    continuer=evenements();
 
    // Et on dessine la scène
    dessiner();
 
  } while(continuer);
 
  SDL_Quit();                //On quitte la SDL
  return 0;
}
 
// Fonctions -------------------------------------------------------------------
 
Uint32 evolution(Uint32, void*)
{
  /* On gère ici l'évolution du système.
     Dans le cas simple de cet exemple : la position (angulaire)
     augemente de 1 degré tous les pas de temps (i.e. 1 degré par 
     20 ms)                                                       */
 
  position += 1.0;
  while (position > 360.0) position -= 360.0;
 
  return 1;
}
 
void initOpenGL()   // S'occupe d'initialiser quelques trucs pour OpenGL
{
  // active la gestion de la profondeur
  glEnable(GL_DEPTH_TEST);    
 
  // fixe la perspective
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65.0, 4./3., 1.0, 1000.0);
 
  // fixe la couleur du fond à noir
  glClearColor(0.0, 0.0, 0.0, 1.0);
}
 
void dessiner()  // Dessine la scène (3 sphères)
{
  GLUquadric* sphere(gluNewQuadric());  // Crée le quadrique pour les spheres
  // commence par effacer l'ancienne image
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  /* part du système de coordonnées de base 
   * (dessin à l'origine : matrice identité) */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 
  // place la caméra
  gluLookAt(r,   0.0, 0.0,
            0.0, 0.0, 0.0,
            0.0, 0.0, 1.0);
  glRotated(theta, 0.0, 1.0, 0.0); 
  glRotated(phi,   0.0, 0.0, 1.0);
 
  // dessin de la première sphère
  glPushMatrix();                // Sauvegarder l'endroit où l'on se trouve
  glTranslated(-1.0, 0.0, 0.5);  /* Se positionner à l'endroit où l'on veut
				  * dessiner                                */
  glColor4d(0.0, 0.0, 1.0, 1.0); // Choisir la couleur (bleu ici)
  gluSphere(sphere, 1.0, 30, 30);// Dessiner une sphère
  glPopMatrix();                 // Revenir à l'ancienne position
 
  // dessin de la seconde sphère
  glPushMatrix();
  glTranslated(0.0, 0.0, 0.0); 
  glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici)
  gluSphere(sphere, 1.5, 50, 50);
  glPopMatrix();
 
  // dessin de la troisième sphère
  glPushMatrix();
  glTranslated(1.0, 0.0, 0.5);
  glColor4d(0.0, 0.8, 0.0, 1.0);  //vert
  gluSphere(sphere, 1.0, 50, 50);
  glPopMatrix();
 
  // Dessin de la petite sphère qui tourne
  glPushMatrix();
  glRotated(position, 0.0, 0.0, 1.0); /* on la fait tourner de "position" 
                                       * autour de Oz...                   */
  glTranslated(2.0, 0.0, 0.0);        /* ...sur un cercle de rayon 2.0     */
  glColor4d(1.0, 0.0, 0.0, 1.0); // couleur rouge
  gluSphere(sphere, 0.1, 30, 30);
  glPopMatrix();
 
  // Finalement, on envoie le dessin à l'écran
  glFlush();
   SDL_GL_SwapBuffers();
 
   // On détruit le quadrique 
   gluDeleteQuadric(sphere);
}
 
void deplace(double dr)
{
  r += dr;
  if (r < 1.0) r = 1.0;
  else if (r > 1000.0) r = 1000.0;
}
 
bool evenements()
{
  SDL_Event monEvenement;   
 
  // On demande à la SDL quel évènement a eu lieu  
  SDL_PollEvent(&monEvenement); 
 
  if (monEvenement.type == SDL_QUIT) // Si l'événement est de type SDL_QUIT
    return false;                     // On renvoit false (arrêt)
 
  else if (monEvenement.type == SDL_KEYDOWN) // Si c'est une touche du clavier
    switch(monEvenement.key.keysym.sym) { // Regarde quelle touche a été appuyée
    case SDLK_ESCAPE:            // Si c'est "Esc"
    case SDLK_q:                 // Si c'est "q"
      return false;                 // On renvoie false (arrêt)
 
    case SDLK_UP:                // Si c'est la flèche du haut
      RotateTheta( 2.0);        // On incrémente theta
      break;
 
    case SDLK_DOWN:                //idem pour les autres
      RotateTheta(-2.0);
      break;
 
    case SDLK_RIGHT:
      RotatePhi( -2.0);
      break;
 
    case SDLK_LEFT:
      RotatePhi( 2.0);
      break;
 
    case SDLK_PAGEUP:
      deplace(-1.0);    // On se rapproche
      break;
 
    case SDLK_PAGEDOWN:
      deplace(1.0);     // On s'éloigne
      break;
 
    case SDLK_HOME:
      r     =  5.0;     // On revient à un point fixé
      theta = 35.0;
      phi   = 20.0;
      break;
 
    case SDLK_SPACE:
      if (monTimer != 0) { // Si le timer tourne...
        SDL_RemoveTimer(monTimer);  // ...on l'enleve
        monTimer = 0;
      } else { // Sinon...
        monTimer = SDL_AddTimer(dt, evolution, 0); // ...on le remet
      }
      break;
     }
 
   return true; /* Pour tous les autres évènements, le programme doit
                 * continuer (on renvoit donc true).                  */
}
 
void RotateTheta(double deg)
{
  theta += deg;
  while (theta < -180.0) { theta += 360.0; }
  while (theta >  180.0) { theta -= 360.0; }
}
 
void RotatePhi(double deg)
{
  phi += deg;
  while (phi <   0.0) { phi += 360.0; }
  while (phi > 360.0) { phi -= 360.0; }
}

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


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