Faculté Informatique & Communications
Cours d'informatique

EPFL
EPFL

Tutoriel GLUT

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

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

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

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

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

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

Dessin 3D

Quelques concepts de base

Boucle principale

La première chose qu'il faut bien comprendre quand on utilise OpenGL, c'est qu'il ne s'agit pas d'un programme de dessin séquentiel («dessine ceci», puis «dessine cela», etc.), mais de «programmation évènementielle» (i.e. «à base d'«évènements» où tout se passe dans un boucle infinie qui ne fait qu'attendre des évènements (clic de souris, touche au clavier, mais aussi redimensionnement de la fenêtre ou tout simplement «dessin») et appelle les fonctions correspondant à ces évènements.

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

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

Puis enfin (et seulement en fin !) on lance la fameuse boucle infinie. Ce n'est qu'une fois cette boucle lancée que l'on pourra voir quelque chose.

NOTE : Cette boucle étant une boucle infinie, il faudra bien sûr penser à associer à un évènement particulier le fait de quitter le programme. Nous reviendrons sur ce point plus tard, dans notre second exemple.

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 ou de glut, la librairie ne l'exécute pas telle-quelle, mais l'applique à la «matrice courante».

Mais qu'est-ce que «la matrice courante» ?

C'est la matrice («affine», i.e. 4x4 pour un espace 3D) représentant la composition des opérations géométriques ayant été effectuées jusque là. Tous les objets et opérations graphiques sont en fait représentés par des matrices 4x4.

ATTENTION ! Cela suppose aussi que l'on utilise l'ordre de composition des fonctions : appliquer f, puis appliquer g, c'est en fait faire g(f(x)). Ceci implique que si vous faites une translation puis une rotation séquentiellement dans le code, et bien à l'écran, c'est comme s'il y avait d'abord eu une rotation, puis seulement ensuite une translation ! Faites donc attention à l'ordre de vos commandes graphiques.

Pour éviter les confusions entre plusieurs objets graphiques, il y a deux commandes très utiles : glPushMatrix() et glPopMatrix().

Ces commandes sauvent et restaurent les matrices courantes dans une pile. En pratique, ça veut dire que quand vous voulez faire des transformations, vous faites d'abord un glPushMatrix(), ensuite vous dessinez, et enfin vous faites un glPopMatrix().

Quelques commandes

Voici quelques commandes de base :

glPushMatrix() sauve la matrice courante
glPopMatrix() restaure la matrice courante
glTranslated(double x, double y, double z ) translate de (x,y,z)
glRotated(double alpha, double x, double y, double z ) effectue une rotation d'angle alpha autour de l'axe (x,y,z)
glScaled(double rx, double ry, double rz ) effectue une affinité de rapport (rx, ry rz)
glutSolidSphere(double rayon, 30, 30 ) construit une sphère
glutSolidCube(double cote) construit un cube
glutSolidTorus( double rayon_interieur, double rayon_exterieur, 30, 30 ) dessine un tore

Pour compiler

Pour compiler (ou plus exactement faire l'édition de liens («linker») d') un programme avec la bibliothèque glut, il est nécessaire de donner au compilateur des directives dans ce sens.

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

-L/usr/X11R6/lib/ -lglut -lGLU -lGL -lXmu -lXi

lors de l'édition de liens.

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

LDFLAGS  = -L/usr/X11R6/lib/
LDLIBS   = -lglut -lGLU -lGL -lXmu -lXi

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.

Comme dit plus haut, la première chose à faire est de définir les fonctions à associer à des évènements. Les deux fonctions de base qu'il faut au moins définir sont la fonction de dessin et celle du redimensionnement de la fenêtre.

Commençons par la première (dessin) :

/* Fonction effectuant le dessin.
 */
void dessine()
{
  // mode de dessin : couleur et 3D ("Z-buffer")
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // part du système de coordonnées de base (matrice identité)
  glLoadIdentity();

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

  /* défini la couleur (rouge, vert, bleu) et la transparence.
   * Ici : bleu pur pas du tout transparent                     */ 
  glColor4d(0.0, 0.0, 1.0, 1.0);

  // dessine en "fil de fer" une sphère de rayon 1 
  glutWireSphere(1.0, 30, 30);

  // ~ affiche
  glutSwapBuffers();
}

La fonction de redimensionnement se fait ensuite de la façon suivante :

/* Fonction utilisée lors du "re-dessin" de la fenêtre graphique,
 * en particulier lors de son redimensionnement.
 * "largeur" et "hauteur" sont les (nouvelles) tailles de la fenêtre.
 */
void reshape(int largeur, int hauteur) {

  // Evite une division par zéro
  if (hauteur <= 0) hauteur = 1;

  double ratio(largeur);
  ratio /= hauteur; // ratio est un double : évite la division entière

  // Reset the coordinate system before modifying
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  // Set the new viewport size
  glViewport(0, 0, largeur, hauteur);

  // Set the correct perspective.
  gluPerspective(45.0, ratio, 1.0, 1000.0);

  // Choose the projection matrix to be the matrix 
  // manipulated by the following calls
  glMatrixMode(GL_MODELVIEW);
  // Set the projection matrix to be the identity matrix
  glLoadIdentity();
  gluLookAt(1.0, 1.0, 5.0,  // position (x, y, z) de l'oeil
            0.0, 0.0, 0.0,  // point visé
            0.0, 1.0, 0.0); /* verticale de la vue 
                                (non parallèle à la direction de visée) */
}

On peut maintenant créer le programme principal.

Il faut d'abord inclure le header de glut (lequel inclut OpenGl puisque glut utilise OpenGL) :


#include <GL/glut.h>

Ensuite, dans le main(), on va :

Il reste un dernier petit «truc». Parfois, en fonction de l'état de la fenêtre au départ (par rapport aux autres fenêtres de votre environnement) il se peut que le dessin ne soit pas effectué (i.e. en fait que l'évènement «affichage» n'ait pas encore eu lieu). Pour éviter ce petit désagrément, il faut ajouter la fonction suivante :

void idle(void)
{
  glutPostRedisplay();
  glutIdleFunc(0);
}

et l'associer à la gestion de l'évènement «idle» :

  glutIdleFunc(idle);

Voici donc pour finir le programme complet de ce premier exemple :

/* Premier exemple simple d'utilisation de la bibliothèque glut.
 * A interrompre en tapant "Control c".
 */

#include <GL/glut.h>

/* Fonction utilisée lors du "re-dessin" de la fenêtre graphique,
 * en particulier lors de son redimensionnement.
 * "largeur" et "hauteur" sont les (nouvelles) tailles de la fenêtre.
 */
void reshape(int largeur, int hauteur) {

  // Evite une division par zéro
  if (hauteur <= 0) hauteur = 1;

  double ratio(largeur);
  ratio /= hauteur; // ratio est un double : évite la division entière

  // Reset the coordinate system before modifying
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
        
  // Set the new viewport size
  glViewport(0, 0, largeur, hauteur);

  // Set the correct perspective.
  gluPerspective(45.0, ratio, 1.0, 1000.0);

  // Choose the projection matrix to be the matrix 
  // manipulated by the following calls
  glMatrixMode(GL_MODELVIEW);
  // Set the projection matrix to be the identity matrix
  glLoadIdentity();
  gluLookAt(1.0, 1.0, 5.0,  // position (x, y, z) de l'oeil
            0.0, 0.0, 0.0,  // point visé
            0.0, 1.0, 0.0); /* verticale de la vue 
                                (non parallèle à la direction de visée) */
}

/* Fonction effectuant le dessin.
 */
void dessine()
{         
  // mode de dessin : couleur et 3D ("Z-buffer")
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // part du système de coordonnées de base (matrice identité)
  glLoadIdentity();

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

  /* défini la couleur (rouge, vert, bleu) et la transparence.
   * Ici : bleu pur pas du tout transparent                     */ 
  glColor4d(0.0, 0.0, 1.0, 1.0);

  // dessine en "fil de fer" une sphère de rayon 1 
  glutWireSphere(1.0, 30, 30);

  // ~ affiche
  glutSwapBuffers();
}

/* "truc" pour avoir le dessin correct dès le début
 */
void idle(void)
{
  glutPostRedisplay();
  glutIdleFunc(0);
}

/* PROGRAMME PRINCIPAL
 */
int main(int argc, char* argv[])
{
  // initialisation de glut
  glutInit(&argc, argv);

  // initialisation du mode de dessin
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);

  // initialisation de la fenêtre (en donnant sa taille)
  glutInitWindowSize(800, 600);

  // réalisation de cette fenêtre (et en lui donnant un titre)
  glutCreateWindow("First try");

  // association de la fonction de dessin à l'évènement "affichage" (display) 
  glutDisplayFunc(dessine);

  // "truc" pour avoir le dessin correct dès le début
  glutIdleFunc(idle);

  // association de la fonction de redimensionnement à l'évènement "reshape"
  glutReshapeFunc(reshape);

  // entrée dans la boucle graphique
  glutMainLoop();

  // ne sert à rien ici (si ce n'est à éviter un warning du compilateur)
  return 0;
}

NOTE : Nous n'avons pas encore associé le fait de quitter le programme à un évènement. Donc, pour arrêter ce programme, vous devez le faire depuis le terminal où vous l'avez lancé en tapant (comme pour arrêter n'importe quelle autre programme) les touches «Control» et «c» en même temps. Vous pouvez aussi plus simplement fermer la fenêtre à l'aide du bouton correspondant.

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 à faire quitter le programme si la touche «Q» ou la touche «Echappement» sont pressées.

Vous pouvez télécharger ici le code de ce second exemple.

Comme toujours, avant de traiter un évènement particulier, il faut prévoir la fonction correspondante (à noter que les prototypes de ces fonctions sont imposés par la bibliothèque glut. Pour plus de détails, voir la documentation de référence donnée dans la bibliographie). Ici il s'agit simplement de quitter le programme si «Q», «q» ou «Echappement» ont été reçu comme argument :

/* Fonction pour gérer les évènements clavier.
 * Reçoit comme arguments le caractère correspondant à la touche tapée,
 * ainsi que les coordonnées de la souris à ce moment là (non utilisées
 * dans cet exemple).
 */
void clavier (unsigned char touche, int souris_x, int souris_y) {

  switch (touche) {
                
  case 27 : // touche "ESCAPE" ("d'échappement")
  case 'q':
  case 'Q':
    // déruit la fenêtre
    glutDestroyWindow(glutGetWindow());

    // et quitte violemment du programme
    exit(0);

    // pas utile ici, mais pour éviter des questions... ;-)
    break;
  }
}

Pour finir cet exemple, il suffit simplement d'associer (dans le main()) à la gestion des évènements clavier la fonction nouvellement créée :


  glutKeyboardFunc(clavier);

Voici le code complet de ce second exemple. Les différences avec le premier exemple sont indiquées en gras.

/* Second exemple simple d'utilisation de la bibliothèque glut :

 * ajout de la gestion d'un évènement clavier. Ici : taper 'Q' pour quitter.
 */

#include <GL/glut.h>
#include <cstdlib>   // pour la fonction exit()

/* Fonction utilisée lors du "re-dessin" de la fenêtre graphique,
 * en particulier lors de son redimensionnement.
 * "largeur" et "hauteur" sont les (nouvelles) tailles de la fenêtre.
 */
void reshape(int largeur, int hauteur) {
  if (hauteur <= 0) hauteur = 1;
  double ratio(largeur);
  ratio /= hauteur;

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glViewport(0, 0, largeur, hauteur);
  gluPerspective(45.0, ratio, 1.0, 1000.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(1.0, 1.0, 5.0,
            0.0, 0.0, 0.0,
            0.0, 1.0, 0.0);
}

/* Fonction effectuant le dessin.
 */
void dessine()
{         
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();
  gluLookAt(1.0, 1.0, 5.0,
            0.0, 0.0, 0.0,
            0.0, 1.0, 0.0);
  glColor4d(0.0, 0.0, 1.0, 1.0);
  glutWireSphere(1.0, 30, 30);
  glutSwapBuffers();
}

/* "truc" pour avoir le dessin correct dès le début
 */
void idle(void)
{
  glutPostRedisplay();
  glutIdleFunc(0);
}

/* Fonction pour gérer les évènements clavier.
 * Reçoit comme arguments le caractère correspondant à la touche tapée,
 * ainsi que les coordonnées de la souris à ce moment là (non utilisées
 * dans cet exemple).
 */
void clavier (unsigned char touche, int souris_x, int souris_y) {

  switch (touche) {
                
  case 27 : // touche "ESCAPE" ("d'échappement")
  case 'q':
  case 'Q':
    // déruit la fenêtre
    glutDestroyWindow(glutGetWindow());

    // et quitte violemment du programme
    exit(0);

    // pas utile ici, mais pour éviter des questions... ;-)
    break;
  }
}


/* PROGRAMME PRINCIPAL
 */
int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowSize(800, 600);
  glutCreateWindow("Appuyer sur Q pour quitter");
  glutDisplayFunc(dessine);
  glutIdleFunc(idle);
  glutReshapeFunc(reshape);

  // association de la fonction de gestion de clavier à l'évènement "clavier"
  glutKeyboardFunc(clavier);

  glutMainLoop();
  return 0;
}

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.

Vous pouvez télécharger ici le code de ce troisième exemple.

Ce qu'il faut faire avant de dessiner un objet c'est donc de sauvegarder la matrice courante. On peut ensuite se translater au point où l'on souhaite dessiner. Ce qui donne :

  glPushMatrix();                // penser à empiler l'état courant
  glColor4d(0.0, 0.0, 1.0, 1.0); // choisi la couleur (bleu ici)
  glTranslated(0.0, 0.0, 0.0);   // se positionne au centre de la sphère
  glutSolidSphere(1.0, 30, 30);  // dessine une sphère pleine
  glPopMatrix();                 // restituer l'état courant précédemment empilé

Et pour la seconde sphère :

  glPushMatrix();
  glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici)
  glTranslated(1.0, 0.0, -0.5); 
  glutSolidSphere(1.5, 50, 50);
  glPopMatrix();

NOTE : j'ai ici introduit des sphères «solides» (i.e. colorées) plutôt qu'en «fil de fer» comme précédemment. Pour gérer correctement les parties cachées, il faut ajouter dans le main() :

  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

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


/* Troisième exemple d'utilisation de la bibliothèque glut :
 * deux sphères avec gestion des parties cachées.
 * Pour plus de commentaires, voir les deux exemples précédents.
 */

#include <GL/glut.h>
#include <cstdlib> 

void reshape(int largeur, int hauteur) {

  if (hauteur <= 0) hauteur = 1;
  double ratio(largeur);
  ratio /= hauteur;
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glViewport(0, 0, largeur, hauteur);
  gluPerspective(45.0, ratio, 1.0, 1000.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(0.0, 0.0, 5.0,
            0.0, 0.0, 0.0,
            0.0, 1.0, 0.0);
}

void dessine()
{         
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glLoadIdentity();
  gluLookAt(0.0, 0.0, 5.0,
            0.0, 0.0, 0.0,
            0.0, 1.0, 0.0);

  // dessin de la première sphère
  glPushMatrix();                // penser à empiler l'état courant
  glColor4d(0.0, 0.0, 1.0, 1.0); // choisi la couleur (bleu ici)
  glTranslated(0.0, 0.0, 0.0);   // se positionne au centre de la sphère
  glutSolidSphere(1.0, 30, 30);  // dessine une sphère pleine
  glPopMatrix();                 // restituer l'état courant précédemment empilé

  // dessin de la seconde sphère
  glPushMatrix();
  glColor4d(0.0, 1.0, 1.0, 1.0); // choisi la couleur (turquoise ici)
  glTranslated(1.0,0.0,-0.5); 
  glutSolidSphere(1.5, 50, 50);
  glPopMatrix();

  glutSwapBuffers();
}

void idle(void)
{
  glutPostRedisplay();
  glutIdleFunc(0);
}

void clavier (unsigned char touche, int souris_x, int souris_y) {
  switch (touche) {
  case 27 :
  case 'q':
  case 'Q':
    glutDestroyWindow(glutGetWindow());
    exit(0);
    break;
  }
}

int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowSize(800, 600);
  glutCreateWindow("Appuyer sur Q pour quitter");
  glutDisplayFunc(dessine);
  glutIdleFunc(idle);
  glutKeyboardFunc(clavier);
  glutReshapeFunc(reshape);

  // Permet la gestion des parties cachées
  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);


  glutMainLoop();
  return 0;
}

Quatrième exemple : gestion du point de vue

On va maintenant combiner la gestion des touches au clavier avec le positionnement du point de vue. Plus précisément, je vous propose ici de déplacer la «caméra» (i.e. le point de vue) à l'aide des touches «flèches», «page up» et «page down» du clavier.

Vous pouvez télécharger ici le code de ce quatrième exemple.

La première chose à faire est donc de prévoir les variables nécessaires pour stocker la «caméra». Nous allons pour simplifier utiliser ici une variable globale de type structure regroupant toutes les données nécessaires. Dans un programme plus conséquent il faudrait bien sûr :

Comme nous l'avons vu dans les exemples précédents, une «caméra» nécessite trois vecteurs 3D : un pour la position, un pour le point visé et un pour indiquer la dimension verticale de la caméra (i.e. pour fixer la rotation autour de la direction de visée). Il est donc préférable que cette verticale soit perpendiculaire à la direction de visée.

De façon simpliste (à améliorer), on définit alors la structure :

struct Camera {
  // position de l'oeil
  double x;
  double y;
  double z;

  // point visé
  double dx;
  double dy;
  double dz;

  // verticale (non parallèle à la direction de visée)
  double ux;
  double uy;
  double uz;
};

puis la variable globale (que l'on initialise) :

Camera ma_camera = { 0.0, 0.0, 5.0,
                     0.0, 0.0, 0.0,
                     0.0, 1.0, 0.0  };

L'utilisation de cette «caméra» se fait ensuite dans les fonctions reshape et dessine comme précédemment (on ne fait que remplacer les constantes par des variables) :

  gluLookAt(ma_camera.x , ma_camera.y , ma_camera.z ,
            ma_camera.dx, ma_camera.dy, ma_camera.dz,
            ma_camera.ux, ma_camera.uy, ma_camera.uz );

Il faut maintenant penser à la gestion de cette «caméra» via les touches «flèches», «page up» et «page down» du clavier. Cela se fait de façon similaire à la gestion des touches pour stopper le programme, sauf que les touches en question sont considérées comme «spéciales» et requierent de ce fait une gestion séparée des touches usuelles. On utilise pour cela une autre fonction associée à l'évènement «Special» (sous entendu «special keyboard») au lieu de l'évènement «Keyboard» usuel.

Voici un exemple code correspondant (lequel fait avancer/reculer la caméra sur «flêche avant»/«flêche arrière», et tourner la caméra sur elle-même avec les autres touches. Il n'est bien sûr par nécessaire qu'elle soit aussi compliquée. J'y ai de plus inclu quelques calculs usuels de géometrie dans l'espace, mais un tel code devrait sûrement se trouver ailleurs dans un programme plus ambitieux) :

void clavier_special (int touche, int souris_x, int souris_y)
{
  /* déplace la caméra sans changer le point de vue */

  // pas (arbitraire) de déplacement
  const double step(0.1);

  // quelques calculs préliminaires qui seraient peut être stockés ailleurs
  // 1) la direction de visee (vecteur unitaire)
  double visee_x(ma_camera.dx - ma_camera.x);
  double visee_y(ma_camera.dy - ma_camera.y);
  double visee_z(ma_camera.dz - ma_camera.z);
  const double distance(sqrt(visee_x*visee_x+visee_y*visee_y+visee_z*visee_z));
  visee_x /= distance;
  visee_y /= distance;
  visee_z /= distance;
  // 2) la verticale (vecteur unitaire perpendiculaire a la visee)
  double scal(ma_camera.ux * visee_x + 
	      ma_camera.uy * visee_y + 
	      ma_camera.uz * visee_z);
  if (std::abs(scal) > 1e-6) {
    ma_camera.ux -= scal * visee_x;
    ma_camera.uy -= scal * visee_y;
    ma_camera.uz -= scal * visee_z;
  }
  double norme(ma_camera.ux * ma_camera.ux +
	       ma_camera.uy * ma_camera.uy + 
	       ma_camera.uz * ma_camera.uz);
  if (norme != 1.0) {
    norme = sqrt(norme);
    ma_camera.ux /= norme;
    ma_camera.uy /= norme;
    ma_camera.uz /= norme;
  }
  // 3) produit vectoriel entre la direction de visee et la verticale
  double const vx(visee_y *  ma_camera.uz -  visee_z * ma_camera.uy);
  double const vy(visee_z *  ma_camera.ux -  visee_x * ma_camera.uz);
  double const vz(visee_x *  ma_camera.uy -  visee_y * ma_camera.ux);


  /* déplacement */
  switch (touche) {
		
  case GLUT_KEY_UP :
    // déplacement avant
    ma_camera.x += step * visee_x;
    ma_camera.y += step * visee_y;
    ma_camera.z += step * visee_z;

    ma_camera.dx += step * visee_x;
    ma_camera.dy += step * visee_y;
    ma_camera.dz += step * visee_z;
    break;
			
  case GLUT_KEY_DOWN :
    // déplacement arrière
    ma_camera.x -= step * visee_x;
    ma_camera.y -= step * visee_y;
    ma_camera.z -= step * visee_z;

    ma_camera.dx -= step * visee_x;
    ma_camera.dy -= step * visee_y;
    ma_camera.dz -= step * visee_z;
    break;

  case GLUT_KEY_LEFT :
    /* rotation d'un angle "step" autour de la verticale :
       le point de visée est déplacé de sorte à garder la
       même distance à l'oeil */
    ma_camera.dx = ma_camera.x + distance * 
      // rotation de step autours de ma_camera.u
      // cf http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToMatrix/
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step)             ) * visee_x +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_y +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_y +
       ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step)             ) * visee_z
      );

    break;
		 
  case GLUT_KEY_RIGHT :
    /* rotation d'un angle "-step" autour de la verticale */
    ma_camera.dx = ma_camera.x + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step)             ) * visee_x +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_y +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_y +
       ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step)             ) * visee_z
      );

    break;
			
  case GLUT_KEY_PAGE_UP:
    /* rotation d'un angle "step" autour de visee^verticale :
       le point de visée est déplacé de sorte à garder la
       même distance à l'oeil */
    ma_camera.dx = ma_camera.x + distance * 
      // rotation de step autours de v
      ( 
       ((1.0-cos(step))*vx*vx + cos(step)             ) * visee_x +
       ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_y +
       ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_x +
       ((1.0-cos(step))*vy*vy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_x +
       ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_y +
       ((1.0-cos(step))*vz*vz + cos(step)             ) * visee_z
      );

    break;

  case GLUT_KEY_PAGE_DOWN:
    /* rotation d'un angle "-step" autour de visee^verticale*/
    ma_camera.dx = ma_camera.x + distance * 
      ( 
       ((1.0-cos(step))*vx*vx + cos(step)             ) * visee_x +
       ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_y +
       ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_x +
       ((1.0-cos(step))*vy*vy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_x +
       ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_y +
       ((1.0-cos(step))*vz*vz + cos(step)             ) * visee_z
      );

    break;
  }

  glutPostRedisplay();
}

et son association à l'évènement correspondant (à mettre dans le main()) :

glutSpecialFunc(clavier_special);

Pour finir (et par esthétisme), j'ai ajouté une troisième sphère.

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


/* Quatrième exemple d'utilisation de la bibliothèque glut :
 * trois sphères avec déplacement de la caméra.
 * Pour plus de commentaires, voir les exemples précédents.
 */

#include <GL/glut.h>
#include <cstdlib>
#include <cmath>     // pour sin() et cos()


// une simple structure pour représenter la "caméra" (i.e. le point de vue)
struct Camera {
  // position de l'oeil
  double x;
  double y;
  double z;

  // point visé
  double dx;
  double dy;
  double dz;

  // verticale (non parallèle à la direction de visée)
  double ux;
  double uy;
  double uz;
};


/* Une variable globale (!) pour le point de vue.
 * (Le mieux serait bien sûr de faire proprement une classe avec tout ça.)
 */
Camera ma_camera = { 0.0, 0.0, 5.0,
                     0.0, 0.0, 0.0,
                     0.0, 1.0, 0.0 };

void reshape(int largeur, int hauteur) {

  if (hauteur <= 0) hauteur = 1;
  double ratio(largeur);
  ratio /= hauteur;

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glViewport(0, 0, largeur, hauteur);
  gluPerspective(45.0, ratio, 1.0, 1000.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(ma_camera.x, ma_camera.y, ma_camera.z,
            ma_camera.dx, ma_camera.dy, ma_camera.dz,
            ma_camera.ux, ma_camera.uy, ma_camera.uz);
}

void dessine()
{         
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glLoadIdentity();
  gluLookAt(ma_camera.x, ma_camera.y, ma_camera.z,
            ma_camera.dx, ma_camera.dy, ma_camera.dz,
            ma_camera.ux, ma_camera.uy, ma_camera.uz);

  // dessine une première sphère
  glPushMatrix();
  glColor4d(0.0, 0.0, 1.0, 1.0);
  glTranslated(-1.0, 0.0, 0.5);
  glutSolidSphere(1.0, 30, 30);
  glPopMatrix();

  // dessin de la seconde sphère
  glPushMatrix();
  glColor4d(0.0, 1.0, 1.0, 1.0);
  glTranslated(0.0, 0.0, 0.0);
  glutSolidSphere(1.5, 50, 50);
  glPopMatrix();


  // dessin de la troisième sphère
  glPushMatrix();
  glColor4d(0.0, 0.0, 1.0, 1.0);
  glTranslated(1.0, 0.0, 0.5);
  glutSolidSphere(1.0, 50, 50);
  glPopMatrix();

  glutSwapBuffers();
}

void idle(void)
{
  glutPostRedisplay();
  glutIdleFunc(0);
}

void clavier (unsigned char touche, int souris_x, int souris_y) {

  switch (touche) {
  case 27 :
  case 'q':
  case 'Q':
    glutDestroyWindow(glutGetWindow());
    exit(0);
    break;
  }
}

void clavier_special (int touche, int souris_x, int souris_y)
{
  /* déplace la caméra sans changer le point de vue */

  // pas (arbitraire) de déplacement
  const double step(0.1);

  // quelques calculs préliminaires qui seraient peut être stockés ailleurs
  // 1) la direction de visee (vecteur unitaire)
  double visee_x(ma_camera.dx - ma_camera.x);
  double visee_y(ma_camera.dy - ma_camera.y);
  double visee_z(ma_camera.dz - ma_camera.z);
  const double distance(sqrt(visee_x*visee_x+visee_y*visee_y+visee_z*visee_z));
  visee_x /= distance;
  visee_y /= distance;
  visee_z /= distance;
  // 2) la verticale (vecteur unitaire perpendiculaire a la visee)
  double scal(ma_camera.ux * visee_x + 
	      ma_camera.uy * visee_y + 
	      ma_camera.uz * visee_z);
  if (std::abs(scal) > 1e-6) {
    ma_camera.ux -= scal * visee_x;
    ma_camera.uy -= scal * visee_y;
    ma_camera.uz -= scal * visee_z;
  }
  double norme(ma_camera.ux * ma_camera.ux +
	       ma_camera.uy * ma_camera.uy + 
	       ma_camera.uz * ma_camera.uz);
  if (norme != 1.0) {
    norme = sqrt(norme);
    ma_camera.ux /= norme;
    ma_camera.uy /= norme;
    ma_camera.uz /= norme;
  }
  // 3) produit vectoriel entre la direction de visee et la verticale
  double const vx(visee_y *  ma_camera.uz -  visee_z * ma_camera.uy);
  double const vy(visee_z *  ma_camera.ux -  visee_x * ma_camera.uz);
  double const vz(visee_x *  ma_camera.uy -  visee_y * ma_camera.ux);


  /* déplacement */
  switch (touche) {
		
  case GLUT_KEY_UP :
    // déplacement avant
    ma_camera.x += step * visee_x;
    ma_camera.y += step * visee_y;
    ma_camera.z += step * visee_z;

    ma_camera.dx += step * visee_x;
    ma_camera.dy += step * visee_y;
    ma_camera.dz += step * visee_z;
    break;
			
  case GLUT_KEY_DOWN :
    // déplacement arrière
    ma_camera.x -= step * visee_x;
    ma_camera.y -= step * visee_y;
    ma_camera.z -= step * visee_z;

    ma_camera.dx -= step * visee_x;
    ma_camera.dy -= step * visee_y;
    ma_camera.dz -= step * visee_z;
    break;

  case GLUT_KEY_LEFT :
    /* rotation d'un angle "step" autour de la verticale :
       le point de visée est déplacé de sorte à garder la
       même distance à l'oeil */
    ma_camera.dx = ma_camera.x + distance * 
      // rotation de step autours de ma_camera.u
      // cf http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToMatrix/
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step)             ) * visee_x +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_y +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_y +
       ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step)             ) * visee_z
      );

    break;
		 
  case GLUT_KEY_RIGHT :
    /* rotation d'un angle "-step" autour de la verticale */
    ma_camera.dx = ma_camera.x + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step)             ) * visee_x +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_y +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_y +
       ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step)             ) * visee_z
      );

    break;
			
  case GLUT_KEY_PAGE_UP:
    /* rotation d'un angle "step" autour de visee^verticale :
       le point de visée est déplacé de sorte à garder la
       même distance à l'oeil */
    ma_camera.dx = ma_camera.x + distance * 
      // rotation de step autours de v
      ( 
       ((1.0-cos(step))*vx*vx + cos(step)             ) * visee_x +
       ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_y +
       ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_x +
       ((1.0-cos(step))*vy*vy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_x +
       ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_y +
       ((1.0-cos(step))*vz*vz + cos(step)             ) * visee_z
      );

    break;

  case GLUT_KEY_PAGE_DOWN:
    /* rotation d'un angle "-step" autour de visee^verticale*/
    ma_camera.dx = ma_camera.x + distance * 
      ( 
       ((1.0-cos(step))*vx*vx + cos(step)             ) * visee_x +
       ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_y +
       ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_x +
       ((1.0-cos(step))*vy*vy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_x +
       ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_y +
       ((1.0-cos(step))*vz*vz + cos(step)             ) * visee_z
      );

    break;
  }

  glutPostRedisplay();
}

int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowSize(800, 600);
  glutCreateWindow("Appuyer sur Q pour quitter");
  glutDisplayFunc(dessine);
  glutIdleFunc(idle);
  glutKeyboardFunc(clavier);
  glutSpecialFunc(clavier_special);
  glutReshapeFunc(reshape);

  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glutMainLoop();
  return 0;
}

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 que l'on lâche depuis une certaine hauteur. On pourrait, comme dans l'exercice que vous avez fait au premier semestre, calculer à l'avance le temps au bout duquel la balle touchera le sol. Mais dans une simulation physique en temps réel, on voudrait avoir la position de la balle à chaque instant, par exemple pour pouvoir l'afficher.

On doit donc pouvoir être capable de décrire à chaque instant la nouvelle position de la balle en fonction de la position précédente et du temps dt écoulé entre deux calculs. Ce temps est simplement le temps que l'ordinateur a mis pour calculer et afficher la dernière position.

Dans une simulation numérique non temps réel, cet intervalle dt est fixé à une valeur arbitraire, aussi petite que la précision de calcul voulue le nécessite (voir cours d'analyse numérique).

Dans un programme «temps réel», c'est par contre la puissance de la machine qui détermine la valeur de dt : plus la scène est complexe à animer et afficher, plus dt sera grand, et plus la simulation sera approximative et l'animation saccadée.

NOTE : La raison pour laquelle on ne fixe pas à l'avance l'intervalle dt est qu'on a a priori aucune idée du temps que prendra le calcul (et l'affichage !) d'une image et, surtout, qu'on n'a aucune garantie que ce temps restera constant : plus il y a d'éléments à prendre en compte, plus ce temps augmentera. On s'en rend bien compte dans certains jeux vidéos : lorsqu'il y a un phénomène complexe (e.g. une explosion) ou trop d'unités à gérer, c'est le nombre d'images par seconde qui diminue et non le temps qui se dilate.

Concrètement, dt est donné par un «timer», qui est tout simplement une fonction de la bibliothèque glut.

La simulation est donc une boucle qui répète en permanence plusieurs étapes, parmi lesquelles :

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

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

Passons maintenant à un exemple.

Cinquième exemple

On va ici introduire une petite évolution «temps réel» dans notre dernier exemple. Pour cela je propose de faire tourner (simplement, de façon proportionnelle au temps (i.e. mouvement uniforme)) une petite boule rouge autour des trois sphères précédemment dessinées.

Vous pouvez télécharger ici le code de ce cinquième exemple.

La première chose à faire est donc de prévoir l'élément qui va se déplacer. Il faut ici simplement dessiner une sphère, mais dont la position dépend du temps. Par exemple :

  glPushMatrix();
  glColor4d(1.0, 0.0, 0.0, 0.60); // couleur rouge, transparente à 40%
  double u1(sin(position));
  double u2(cos(position));
  glTranslated(2*u1, 0.4*u1*u2, 2*u2);
  glutSolidSphere(.1, 30, 30);
  glPopMatrix();

Il s'agit ici d'une sphère dont le centre suit la courbe paramétrique (2*sin(u), 0.4*sin(u)*cos(u), 2*cos(u)). Dans cet exemple jouet, le paramètre u est représenté par la variable globale position (il faudrait bien sûr faire autrement) :

double position(0.0);

En l'état, le programme compile et fait un joli dessin, mais pas grand chose ne bouge. Il faut pour cela ajouter une fonction à l'évènement «timer» du programme. Commençons par écrire cette fonction.

Elle doit simplement calculer la nouvelle position à chaque instant. Je choisis ici de vous donner un exemple utilisant la notion de «pas de temps» comme décrit dans l'introduction théorique (bien que pour le mouvement uniforme implémenté ici ce ne soit pas nécessaire. On pourrait bien sûr dans ce cas extrêmement simple utiliser simplement directement le temps absolu).

Ce qui nous donne :

void simulation(int id)
{
  // mémorise (localement) le nouveau temps
  unsigned int now(glutGet(GLUT_ELAPSED_TIME)); 

  // calcule la valeur de l'intervalle de temps
  unsigned int dt(now - temps);

  // stocke la valeur du "nouveau temps" au niveau global 
  // (pour la prochaine fois)
  temps = now;

  // calcule la nouvelle position (mouvement uniforme)
  position += dt / 1500.00; 

  // demande l'affichage
  glutPostRedisplay();
}

Le prototype de la fonction simulation nous est imposé par la bibliothèque et le paramètre «id» non utilisé ici peut être utilisé pour passer une valeur lors de l'appel (si nécessaire).

La variable globale temps est utilisée ici pour garder la mémoire du temps absolu au niveau du programme entier (c'est cela l'aspect «temps réel») :

unsigned int temps(0);

Il ne reste maintenant plus qu'à associer cette fonction à l'évènement «timer». Cela se fait dans le main() par :

glutTimerFunc(1, simulation, 0);

Le premier paramètre est le nombre minimum de millisecondes à attendre avant de déclencher le «timer» (i.e. la fonction simulation). Cela contrôle en fait le nombre de fois par seconde que l'on souhaite voir s'exécuter la fonction simulation. Le dernier paramètre (0) est justement celui que cette fonction reçoit lors de son appel (le paramètre «id» dans l'exemple précédent).

Si vous testez le programme à ce stade, vous constaterez qu'il ne fonctionne pas (en ce sens que rien ne bouge). En effet, dès que le «timer» appelle sa fonction associée, celle-ci est automatiquement dissociée de l'évènement «timer». Pour une simulation en continu il est donc nécessaire de le réassocier à chaque fois. Cela se fait en ajoutant cette ligne à la fin de la fonction simulation :

   glutTimerFunc(15, simulation, 0);

NOTE : 15 correspond ici à 1000/15, soit 66, recalculs (i.e. appels de la fonction simulation) par seconde.

Vous pouvez maintenant tester le programme à ce stade : ÇA MARCHE ! La petite boule rouge tourne !

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

Il suffit pour cela de supprimer la réassociation précédente en fin de simulation à chaque fois qu'une pause a été demandée. Pour cela, on va introduire une variable booléenne (encore globale dans cette exemple jouet, mais à améliorer dans un programme plus conséquent) qui représente si oui ou non la simulation doit tourner :

static bool timer_on(false);

Ensuite on associe, dans la fonction clavier, une touche (par exemple la barre espace ici) à l'arrêt/au redémarrage de la simulation :

 case ' ':
    if (timer_on) {
      timer_on = false; // arrête la simulation si était en cours
    } else {
      // redémarre la simulation si arrêtée
      glutTimerFunc(1, timer, 0);
      timer_on = true; 
      temps = glutGet(GLUT_ELAPSED_TIME);
    }

et enfin on effectue la réassociation du «timer» dans la fonction simulation que si timer_on est à «vrai» :

  if (timer_on) glutTimerFunc(15, timer, 0);

Il ne reste plus qu'à mettre les bonnes valeurs initiales dans le main et le tour est joué :

  glutTimerFunc(1, timer, 0);
  timer_on = true; // maintenant il est "on" (on vient de le mettre)
  temps = glutGet(GLUT_ELAPSED_TIME);

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

/* Cinquième exemple d'utilisation de la bibliothèque glut :
 * on ajoute une sphère gravitant autour des trois autres
 * et la possibilité de faire une pause dans l'animation en 
 * appuyant sur la "barre d'espace".
 * Pour plus de commentaires, voir les exemples précédents.
 */
#include <GL/glut.h>
#include <cstdlib>
#include <cmath>

struct Camera {
  double x;
  double y;
  double z;
  double dx;
  double dy;
  double dz;
  double ux;
  double uy;
  double uz;
};

// tout ceci devrait être mis dans des classes appropriées
Camera ma_camera = { 0.0, 0.0, 5.0,
                     0.0, 0.0, 0.0,
                     0.0, 1.0, 0.0  };
unsigned int temps(0);
double position(0.0);
bool timer_on(false);

void reshape(int largeur, int hauteur) {

  if (hauteur <= 0) hauteur = 1;
  double ratio(largeur);
  ratio /= hauteur;

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glViewport(0, 0, largeur, hauteur);
  gluPerspective(45.0, ratio, 1.0, 1000.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(ma_camera.x, ma_camera.y, ma_camera.z,
            ma_camera.dx, ma_camera.dy, ma_camera.dz,
            ma_camera.ux, ma_camera.uy, ma_camera.uz);
}

void dessine()
{         
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glLoadIdentity();
  gluLookAt(ma_camera.x, ma_camera.y, ma_camera.z,
            ma_camera.dx, ma_camera.dy, ma_camera.dz,
            ma_camera.ux, ma_camera.uy, ma_camera.uz);

  // dessine une première sphère
  glPushMatrix();
  glColor4d(0.0, 0.0, 1.0, 1.0);
  glTranslated(-1.0, 0.0, 0.5);
  glutSolidSphere(1.0, 30, 30);
  glPopMatrix();

  // dessin de la seconde sphère
  glPushMatrix();
  glColor4d(0.0, 1.0, 1.0, 1.0);
  glTranslated(0.0, 0.0, 0.0);
  glutSolidSphere(1.5, 50, 50);
  glPopMatrix();

  // dessin de la troisième sphère
  glPushMatrix();
  glColor4d(0.0, 0.0, 1.0, 1.0);
  glTranslated(1.0, 0.0, 0.5);
  glutSolidSphere(1.0, 50, 50);
  glPopMatrix();

  // dessin de la quatrième sphère (celle qui va bouger)
  glPushMatrix();
  glColor4d(1.0, 0.0, 0.0, 0.60); // couleur rouge, transparente à 40%
  double u1(sin(position));
  double u2(cos(position));
  glTranslated(2*u1, 0.4*u1*u2, 2*u2);
  glutSolidSphere(.1, 30, 30);
  glPopMatrix();

  glutSwapBuffers();
}

/* Cette nouvelle fonction sert à faire évoluer le temps. 
 * Ici on fait simplement dépendre "position" linéairement du temps.
 * Le paramètre "id" n'est pas utilisé ici.
 */
void timer(int id)
{
  // mémorise (localement) le nouveau temps
  unsigned int now(glutGet(GLUT_ELAPSED_TIME)); 

  // calcule la valeur de l'intervalle de temps
  unsigned int dt(now - temps);

  // stocke la valeur du "nouveau temps" au niveau global 
  // (pour la prochaine fois)
  temps = now;

  // calcule la nouvelle position (mouvement uniforme)
  position += dt / 1500.00; 

  // demande l'affichage
  glutPostRedisplay();

  // si on n'a pas demandé d'arrêter la simulation, on la relance.
  // 15 correspond à environ 66 recalculs par seconde (1000/15 en fait)
  if (timer_on) glutTimerFunc(15, timer, 0);
}


void idle(void)
{
  glutPostRedisplay();
  glutIdleFunc(0);
}

void clavier (unsigned char touche, int souris_x, int souris_y) {

  switch (touche) {
  case 27 :
  case 'q':
  case 'Q':
    glutDestroyWindow(glutGetWindow());
    exit(0);
    break;

  // Pause sur la touche "espace"
  case ' ':
    if (timer_on) {
      timer_on = false; // arrête la simulation si était en cours
    } else {
      // redémarre la simulation si arrêtée
      glutTimerFunc(1, timer, 0);
      timer_on = true; 
      temps = glutGet(GLUT_ELAPSED_TIME);
    }

  }
}

void clavier_special (int touche, int souris_x, int souris_y)
{
  const double step(0.1);
  double visee_x(ma_camera.dx - ma_camera.x);
  double visee_y(ma_camera.dy - ma_camera.y);
  double visee_z(ma_camera.dz - ma_camera.z);
  const double distance(sqrt(visee_x*visee_x+visee_y*visee_y+visee_z*visee_z));
  visee_x /= distance;
  visee_y /= distance;
  visee_z /= distance;
  double scal(ma_camera.ux * visee_x + 
	      ma_camera.uy * visee_y + 
	      ma_camera.uz * visee_z);
  if (std::abs(scal) > 1e-6) {
    ma_camera.ux -= scal * visee_x;
    ma_camera.uy -= scal * visee_y;
    ma_camera.uz -= scal * visee_z;
  }
  double norme(ma_camera.ux * ma_camera.ux +
	       ma_camera.uy * ma_camera.uy + 
	       ma_camera.uz * ma_camera.uz);
  if (norme != 1.0) {
    norme = sqrt(norme);
    ma_camera.ux /= norme;
    ma_camera.uy /= norme;
    ma_camera.uz /= norme;
  }
  double const vx(visee_y *  ma_camera.uz -  visee_z * ma_camera.uy);
  double const vy(visee_z *  ma_camera.ux -  visee_x * ma_camera.uz);
  double const vz(visee_x *  ma_camera.uy -  visee_y * ma_camera.ux);

  switch (touche) {
		
  case GLUT_KEY_UP :
    ma_camera.x += step * visee_x;
    ma_camera.y += step * visee_y;
    ma_camera.z += step * visee_z;

    ma_camera.dx += step * visee_x;
    ma_camera.dy += step * visee_y;
    ma_camera.dz += step * visee_z;
    break;
			
  case GLUT_KEY_DOWN :
    ma_camera.x -= step * visee_x;
    ma_camera.y -= step * visee_y;
    ma_camera.z -= step * visee_z;

    ma_camera.dx -= step * visee_x;
    ma_camera.dy -= step * visee_y;
    ma_camera.dz -= step * visee_z;
    break;

  case GLUT_KEY_LEFT :
    ma_camera.dx = ma_camera.x + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step)             ) * visee_x +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_y +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_y +
       ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step)             ) * visee_z
      );

    break;
		 
  case GLUT_KEY_RIGHT :
    ma_camera.dx = ma_camera.x + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.ux + cos(step)             ) * visee_x +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy + sin(step)*ma_camera.uz) * visee_y +
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz - sin(step)*ma_camera.uy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uy - sin(step)*ma_camera.uz) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz + sin(step)*ma_camera.ux) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*ma_camera.ux*ma_camera.uz + sin(step)*ma_camera.uy) * visee_x +
       ((1.0-cos(step))*ma_camera.uy*ma_camera.uz - sin(step)*ma_camera.ux) * visee_y +
       ((1.0-cos(step))*ma_camera.uz*ma_camera.uz + cos(step)             ) * visee_z
      );

    break;
			
  case GLUT_KEY_PAGE_UP:
    ma_camera.dx = ma_camera.x + distance * 
      ( 
       ((1.0-cos(step))*vx*vx + cos(step)             ) * visee_x +
       ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_y +
       ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_x +
       ((1.0-cos(step))*vy*vy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_x +
       ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_y +
       ((1.0-cos(step))*vz*vz + cos(step)             ) * visee_z
      );

    break;

  case GLUT_KEY_PAGE_DOWN:
    ma_camera.dx = ma_camera.x + distance * 
      ( 
       ((1.0-cos(step))*vx*vx + cos(step)             ) * visee_x +
       ((1.0-cos(step))*vx*vy + sin(step)*vz) * visee_y +
       ((1.0-cos(step))*vx*vz - sin(step)*vy) * visee_z
      );

    ma_camera.dy = ma_camera.y + distance * 
      ( 
       ((1.0-cos(step))*vx*vy - sin(step)*vz) * visee_x +
       ((1.0-cos(step))*vy*vy + cos(step)             ) * visee_y +
       ((1.0-cos(step))*vy*vz + sin(step)*vx) * visee_z
      );

    ma_camera.dz = ma_camera.z + distance * 
      ( 
       ((1.0-cos(step))*vx*vz + sin(step)*vy) * visee_x +
       ((1.0-cos(step))*vy*vz - sin(step)*vx) * visee_y +
       ((1.0-cos(step))*vz*vz + cos(step)             ) * visee_z
      );

    break;
  }

  glutPostRedisplay();
}

int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowSize(800, 600);
  glutCreateWindow("Appuyer sur Q pour quitter");
  glutDisplayFunc(dessine);
  glutIdleFunc(idle);
  glutKeyboardFunc(clavier);
  glutSpecialFunc(clavier_special);
  glutReshapeFunc(reshape);

  // ajoute les fonctionnalités de gestion du temps (simulation)
  glutTimerFunc(1, timer, 0);
  timer_on = true;
  temps = glutGet(GLUT_ELAPSED_TIME);

  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glClearColor(0.0, 0.0, 0.0, 0.0);

  glutMainLoop();
  return 0;
}

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


Dernière mise à jour : $Date: 2008/02/25 12:35:54 $   ($Revision: 1.16 $)