Animer des objets 3D avec OpenGL


Sommaire

[modifier] Objectif

Notre objectif est ici d'animer une scène 3D consituée de plusieurs"clones" d'un élément de base que nous avons rencontré au cours du tutoriel précédent, à savoir le cube à face colorée.

En fait nous allons reprendre la scène du tutoriel précédent, à savoir un cube coloré placé au centre de la scène, et faire tourner autour de ce dernier 2 cubes satellites, copies conformes du premier mais à échelle réduite, et en faisant tourner un des 2 satellites sur lui-même.

Figure 1 : Notre objectif, un manège de cubes :)


Figure 2 : Représentation schématique de la scène

[modifier] Définir les cubes : les CallListes

[modifier] Principe

Avant de s'intéresser aux mouvements de nos cubes, mettons en place la scène. D'abord, il nous faut créer 3 cubes, sur le modèle du cube de tutoriel précédent.

Rappelons que le cube original (le cube du centre) se définit de la façon suivante :

 glColor3d(...); // couleur de la 1ere face
 glVertex3i(...); // définition de la face
 ...
 glColor3d(...); // couleur de la 2e face
 glVertex3i(...); // définition de la face
 ...

Ici, nous avons besoin de définir 3 cubes. Nous n'allons pourtant pas écrire 3 fois la procédure de création d'un cube, car dans ce cas le code deviendrait vite ingérable. Afin de nous simplifier la tâche, OpenGL met à notre disposition les CallListes. Leur principe est simple : on attribue à une suite d'opérations un identificateur, qui nous permettra de rappeler la liste d'opérations sans avoir à tout réécrire.

[modifier] Mise en oeuvre

Grâce aux CallListes, nous pouvons regrouper nos 3 définitions de cubes en une seule. En fait, nous allons regrouper ce qui concerne la création d'un cube au sein d'une fonction Creer_Cube().

Dans un premier temps, on doit réserver un nombre d'identficateurs qui nous permettront d'accéder aux listes grâce à GLuint glGenLists(GLsizei range) : range est le nombre d'id. que l'on souhaite réserver, et la valeur de retour de la fonction correspond au premier id. que l'on peut utiliser.

 int id_cube; // à déclarer en variable globale
 id_cube=glGenLists(1); // ici, on a besoin que d'une liste

Ensuite, on déclare la liste grâce à glNewList(GLuint list, GLenum mode)list est l'id. de la liste et mode le mode de compilation (GL_COMPILE crée la liste, et GL_COMPILE_AND_EXECUTE la crée et l'affiche). En fin de liste, on appelle glEndList() :

 glNewList (id_cube,GL_COMPILE);
 glBegin(GL_QUADS);
 glColor3d(1,0,0);
 glVertex3i(1,1,1);
 ...
 glEnd();
 glEndList();

La fonction Creer_Cube() sera appelée à l'initialisation, donc dans la fonction Init().

Dans la suite du programme, il suffira d'appeler glCallList(id_cube)pour faire appel à la liste de création d'un cube.

[modifier] Mettre en place les objets

On dispose donc à ce stade du développement d'une fonction de création d'un cube aux faces colorées. On peut maintenant s'intéresser à la création de la scène.

[modifier] Créer le cube central 1

Le cube 1 est fixe, et centré sur l'origine. C'est le cube du tutoriel précédent, donc aucune difficulté pour le créer.

Figure 3 : Position du cube central 1
 glCallList(id_cube);

[modifier] Créer les cubes satellites : les transformations

[modifier] Principe

D'une manière générale, les transformations sont des opérations qui modifient la matrice courante (MODELVIEW ou PROJECTION par exemple). Elles sont basées sur la multiplication de matrices, c'est-à-dire que la nouvelle matrice courante M' s'obtient par M'=M*Toù M est la matrice courante avant transformation, et T la matrice de la transformation. Parmi les transformations les plus utilisées, la translation (glTranslate), la rotation (glRotate) ou encore l'homothétie (glScale).

De part sa définition, une transformation effectue un changement de repère. Le nouveau repère, (O',x',y',z'), s'obtient en appliquant la transformation au repère d'origine (O,x,y,z). En OpenGL, toutes les opérations qui seront effectuées après une transformation s'appliqueront au nouveau repère. Par exemple, si on fait subir une translation à notre repère de base, en modifiant la matrice MODELVIEW, les coordonnées de tous les objets seront définies dans ce nouveau repère.

Cependant, OpenGL apporte la solution à ce qui aurait pu devenir un problème. En effet, à tout moment, il nous est possible de copier la matrice courante dans une pile de matrices, grâce à glPushMatrix(), et de la rappeler grâce à glPopMatrix(). Ceci permet facilement de revenir dans un repère initial (avant la transformation).

Figure 4 : Transformation et matrices

[modifier] Positionnement des cubes

Pour positionner les cubes dans la scène, nous allons utiliser les 2 autres transformations "de base", à savoir la rotation et la translation. Les fonctions correspondantes sont glRotated (GLdouble angle, GLdouble x, GLdouble y, GLdouble z), qui effectue une rotation d'angle angle(en degrés) autour du vecteur (x,y,z), et glTranslated (GLdouble x, GLdouble y, GLdouble z), qui effectue une translation de vecteur (x,y,z).

Intéressons nous d'abord au cube satellite 2. Sa trajectoire est un cercle de centre l'origine du repère, et de rayon fixé.

Figure 6 : Position du cube 2

Une des difficultés ici est de respecter le bon ordre des transformations : il faut ici effectuer la rotation puis la translation. Penser en terme de changement de repère est à mon sens une des meilleures solutions pour ne pas faire d'erreur. Si vous en avez compris le principe, vous devriez être capable de deviner quel serait le mouvement du cube si on inversait l'ordre des opérations :)

Pour le dernier cube, l'idée est la même si ce n'est qu'ici, la translation est effectuée dans l'autre sens, et que l'on doit effectuer une rotation, après la translation, pour produire le mouvement d'auto-rotation.

Figure 7 : Position du cube 3

[modifier] Taille des cubes

Intéressons nous maintenant à la taille des cubes. Les 2 cubes sont des répliques du cube original, mais à échelle réduite (1/2). Il va donc falloir appliquer une homothétie.Pour cela, on utilise glScaled(GLdouble x, GLdouble y, GLdouble z) (glScalef si on utilise des variables de type float), où x,y, et z représentent les facteurs d'étirement suivant les 3 axes.

x=y=z=1 (invariance) x=2 y=z=1 y=2 x=z=1 z=2 x=y=1 x=y=z=2

Figure 5 : Exemples d'utilisation de glScale


Dans notre cas, nous voulons un cube 2 fois moins grand que le cube original, nous allons donc utiliser un coefficient de 0.5 sur les 3 axes.

[modifier] Code source

Transformons ce que nous venons d'énoncer en code, en supposant que nous avons défini une variable angle globalement. Voici donc la portion de code, à placer dans la fonction Display(), qui crée et positionne les 3 cubes :

 glCallList(id_cube); // cube 1
 glPushMatrix();
 glRotated(angle,0,1,0);
 glTranslatef(0,0,-5);
 glScaled(0.5,0.5,0.5);
 glCallList(id_cube); // cube 2
 glPopMatrix();
 glPushMatrix();
 glRotated(angle,0,1,0);
 glTranslatef(0,0,5);
 glRotated(angle,0,1,0);
 glScaled(0.5,0.5,0.5);
 glCallList(id_cube); // cube 3
 glPopMatrix();

[modifier] Animer la scène

[modifier] Modifier les variables

En fait, vous l'aurez compris, animer la scène consiste à faire varier l'angle de rotation, donc la variable angle. Le seul point important est de savoir quand faire cette modification. En fait, c'est simple, on va procéder à la mise à jour de la valeur angle, chaque fois... qu'il ne se passe rien d'autre. Le callback Idle est conçu pour cela :

// Déclarations globales
void Idle();
int angle=0;
// Activation du callback dans le main()
glutIdleFunc(Idle);
// Définition de la fonction Idle
void Idle()
{
angle=(angle+5)%360; // l'opérateur % prend l'entier de la division
glutPostRedisplay(); // force le réaffichage de la scène
}

[modifier] Utiliser le double buffering

Nous n'en avons pas encore fini avec notre animation :) En effet, si on teste le programme à ce stade du développement, même si le mouvement des cubes est correct, l'animation produit un effet très désagréable de scintillement, qu'il faut absolument annuler. En fait, cet effet, appeléeffet de déchirement, est causé par le redessinement de la scène pendant qu'elle s'affiche.

Nous allons donc résoudre ce problème grâce à ce qui est une des bases de l'animation, à savoir le double buffering. Jusqu'àlors, nous n'utilisions qu'un seul buffer pour travailler sur notre scène et l'afficher. Dorénavant, nous allons travailler avec 2 buffers : le color buffer ne sera plus destiné qu'à être affiché, et nous travaillerons uniquement sur le back buffer. L'animation sera alors produite par la permutation des 2 buffers.

Pour utiliser le mode double buffering, il faut l'indiquer à GLUT par l'argument GL_DOUBLE de glutInitDisplayMode dans le main() :

glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);

Pour finir, il faut permuter les 2 buffers une fois que l'on a fini de paramètrer la scène (à la fin de la fonction Display() ). Ceci est effectué par la fonction glutSwapBuffers() qui, de plus, appelle implicitement glFlush() que l'on a plus besoin d'appeler maintenant.

glutSwapBuffers();

Ce document a été publié sur la version 3 du G.C.N. par Le-Gritche.

  • Auteur Original : Le-Gritche
  • Date de publication : 18 janvier 2002