Master Informatique 2017-2018
M3DS



TP 04 : Conception hiérarchique Changements de repères

1  Prise en main

Question 1. Reprenez carPlane.zip sur le portail :

2  Conception hiérarchique

Dans cette partie on trace un objet par conception hiérarchique. Le but est de visualiser une voiture (simple) en utilisant uniquement les primitives géométriques suivantes qui sont offertes par le squelette. Ces primitives sont définies dans leur repère local comme suit :

drawTorus() vous donne un tore de grande section 1 (petite section est 0.2)
drawCube() vous donne un cube centré d’arête 2
drawCylinder() vous donne un cylindre d’axe z, de hauteur 1, et de rayon 1 (origine à la base du cylindre)

Les questions qui suivent permettront d’obtenir progressivement (aux nuances artistiques près) :

La voiture sera décomposée hiérarchiquement (elle est constituée d’une carrosserie, de 2 essieux, chaque essieu étant constitué de 2 roues, et chaque roue constituée d’un pneu et d’une jante) : les différents éléments seront positionnés les uns par rapport aux autres en modifiant p3d::modelviewMatrix. Les méthodes utiles sur cette matrice p3d::modelviewMatrix (de classe Matrix4) sont :

.push() : mémorise dans la pile .pop() : récupère la valeur sur la pile et dépile .rotate(angle,ax,ay,az) : cumule la matrice (i.e. {\bf modifie} le repère) avec une rotation autour de l'axe (ax,ay,az) (l'angle est en degré) .translate(x,y,z) : cumule la matrice avec la translation de direction (x,y,z) .scale(x,y,z) : cumule la matrice avec un changement d'échelle (permet notamment de tracer les primitives à la dimension souhaitée).

Vous n’avez pas à vous préoccupez des aspects OpenGL dans la suite : les VBO/VAO des primitives cube, cylindre et tore sont déjà initialisés par le squelette. Un drawCube (ou drawCylinder ou drawTorus) active un shader en lui passant la matrice p3d::modelviewMatrix et effectue le tracé OpenGL. Ce shader assure également un éclairage diffus et la variable p3d::diffuseColor lui est passée : pour changer la couleur courante de tracé il suffira de faire un p3d::diffuseColor=Vector3(rouge,vert,bleu) (avant le tracé drawCube ou drawCylinder ou drawTorus).

La notion importante à acquérir pour ce tp est la décomposition hiérarchique et la manipulation des repères.

On travaille dans la classe Car. La méthode Car::draw correspond à tracer une voiture dans son repère local. Constatez que cette méthode est constituée, pour le moment, uniquement du tracé d’un cube (il s’agit du cube que vous voyez à l’écran).

Remarque : la voiture est tracée depuis GLApplication::draw() : elle appelle Car::drawWorld() (positionne la voiture dans le repère du monde; aucun positionnement pour le moment) qui, elle même, appelle Car::draw() pour tracer dans le repère local.

Question 2. Faire la carrosserie dans Car::drawBody() avec 2 cubes (donc tracés par drawCube()) positionnés l’un par rapport à l’autre pour obtenir quelque chose qui ressemble à la figure suivante. Il faut "bien sûr" modifier p3d::modelviewMatrix pour positionner les 2 cubes et leur donner la bonne dimension (avec p3d::modelviewMatrix.scale(...)). Veillez à ne pas faire d’effet de bord sur le repère courant (i.e. par push et pop sur p3d::modelviewMatrix).

Pour tester : remplacez le drawCube() par drawBody() dans Car::draw().

Remarque : vous pouvez contrôler le point de vue en faisant click gauche+glisser (tourne la caméra autour du repère du monde).

Question 3. Faire drawRim() avec 4 diamètres (par exemple). Chaque diamètre est composé d’un cylindre "allongé" (attention : l’origine du cylindre est sur sa base, et non sur son centre). Testez (appelez drawRim() au lieu de drawBody()).

Question 4. Faire drawWheel() (un drawTorus() pour le pneu, et drawRim() pour la jante; c’est drawWheel() qui doit positionner la jante avant l’appel à drawRim() : i.e. drawRim doit rester intact). Testez

Question 5. Faire drawAxle() (avec, par exemple, un drawCylinder() pour l’essieu et tracer les 2 roues par rapport au centre de l’essieu; n’oubliez pas, encore une fois, de respecter la conception hiérarchique : c’est drawAxle() qui positionne les roues).

Question 6. Et la voiture complête que vous finaliserez dans Car::draw() : c’est-à-dire le tracé de 2 essieux et d’une carrosserie. Testez.

3  Animation

Question 7. On souhaite faire tourner les roues (roulement des roues) de la voiture lorsqu’on "accélère" (appui sur la touche "z").

Le squelette se charge déjà d’affecter le membre Car::_rotateWheel avec l’angle de rotation des roues souhaité (l’appui sur "z" provoque l’appel à Car::accelerate qui fixe l’accélération; la méthode Car::move(), qui est appelée avant chaque tracé, permet de calculer un angle de rotation des roues à partir de cette accélération : la vitesse de rotation augmente ou diminue selon l’appui sur "z")

Modifiez le tracé effectué dans Car::draw() pour que les roues tournent sur leur axe de rotation suivant cet angle _rotateWheel (sans modifier le positionnement hiérarchique, drawWheel ne doit pas être modifié : soit c’est drawAxle qui positionne les 2 roues en tenant compte de l’angle, soit c’est Car::draw qui tourne tout l’axe). Testez : les roues doivent rouler quand vous appuyez sur ’z’/’s’ (réglez éventuellement l’incrémentation et la décrémentation de _rotateWheel dans Car::move pour avoir une vitesse des roues qui vous semble "convenable").

Question 8. Intégrez enfin la prise en compte de l’attribut _steering (braquage des roues) qui doit être interprété comme la rotation de tout l’essieu avant par rapport à son centre. Testez : l’essieu avant doit tourner selon l’appui des touches ’q’/’d’ (inutile de vous préoccuper des intersections éventuelles des roues avec la voiture). Remarque : comme vous pouvez le constater dans Car::move() l’angle _steering est diminué selon la vitesse et l’essieu tendra donc à se remettre "droit" tout seul : cela est fait pour "simuler" très empiriquement l’adhérence des roues sur le sol.

4  Faire rouler la voiture

Nous avons à présent une voiture, tracée dans son repère local : nous avons directement manipuler la modelview pour traduire les différents positionnements.

Pour positionner la voiture dans la scène (repère World), nous allons manipuler 2 attributs de la classe Car :

class Voiture { Vector3 _position; // la position de l'origine de la voiture (par rapport au repère du monde) Quaternion _orientation; // et son orientation (par rapport au repère du monde) ...

Pour tracer la voiture dans le monde, on affecte la modelview lors du tracé Car::drawWorld en tenant compte de ces 2 champs _position et _orientation.

Question 9. Faites le dans Car::drawWorld (les surcharges existantes de .translate et .rotate vous permettent de passer directement un Quaternion à p3d::modelviewMatrix.rotate(a_Quaternion) et un Vector3 pour p3d::modelviewMatrix.translate(a_Vector3)). Testez : la voiture doit tourner sur elle-même lorsque vous appuyez sur les touches ’z’ et ’q’/’d’ (la rotation de la voiture n’a pas une vitesse constante car elle dépend du braquage des roues, qui lui même dépend de la vitesse; l’attribut _orientation est mis à jour en fonction du braquage dans Car::move). Le champ _position semble inutile (i.e. la voiture tourne sur place) car ce champ n’est pas modifié par le squelette (sujet des questions suivantes).

Question 10. Il reste à faire avancer la voiture. Il suffit de modifier _position dans Car::move (pour le moment seule l’orientation est gérée): la difficulté est qu’elle doit avancer dans sa direction arrière/avant. Autrement dit, il suffit de faire un _position = _position+uneDirection avec uneDirection exprimée dans le repère du monde (_position est en effet exprimée dans le repère du monde). On connait la direction arrière/avant de la voiture dans son repère local (quelle est elle ?). Il reste donc à faire un changement de repère à uneDirection.

Affectez correctement _position dans Car::move (remarque : la classe Quaternion offre l’opérateur * pour les transformations : a=q*b transforme le Vector3 b par la rotation représentée par le Quaternion q). Une fois fait, pour régler l’amplitude de uneDirection vous pouvez utiliser la vitesse _velocity avec éventuellement un coefficient multiplicateur pour régler la vitesse de déplacement de la voiture.

Testez la voiture doit avancer correctement (on peut éventuellement régler les coefficients dans Car::move pour obtenir un "meilleur" résultat)

5  Avion

Cliquez sur le bouton "Follow Plane" (ou sur la touche ’g’ du clavier) : la caméra est repositionnée pour avoir l’avion face à vous (remarque : le placement de la caméra est simplement une modification de p3d::modelviewMatrix que vous n’avez pas à gérer pour ce tp). L’avion est géré dans la classe Airplane. L’avion est positionné par rapport à la scène avec un attribut _position et son orientation est controlée par angles d’Euler (3 attributs : _angleX, _angleY, _angleZ) : vous pouvez le vérifier dans Airplane::drawWorld() (ordre des axes de rotations : Y-X-Z).

Question 11. Ces angles sont modifiés au clavier simplement en les incrémentant ou en les décrémentant (’q’/’d’ pour le roll, ’z’/’s’ pour le pitch et ’a’/’e’ pour le yaw) : Testez. Mettez l’avion dans une orientation quelconque en jouant aléatoirement avec toutes ces touches. Essayer alors de prévoir comment va tourner l’avion en appuyant sur ’z’/’s’ (tangage). Constatez alors que ’z’/’s’ peut ne plus correspondre au tangage de l’avion (l’interaction de l’avion devient alors difficilement intuitive).

Constatez également le gimbal-lock : mettez l’avion à la verticale, puis essayez d’interagir sur les 2 autres axes (’q’/’d’ et ’a’/’e’); ils correspondent tous les deux au même axe de rotation (l’avion tourne autour de la verticale : impossible alors de faire un lacet par rapport à l’avion).

Question 12. Comprenez la composition des rotations par angles d’Euler effectuées dans le Airplane::drawWorld et l’effet qu’elles provoquent sur l’interaction.

Question 13. Pour avoir une interaction un peu plus intuitive, il serait préférable que les touches correspondent à une rotation autour d’un axe local de l’avion (par exemple ’z’/’s’ doit faire un tangage de l’avion quelque soit son orientation). Autrement dit, il faut composer la rotation souhaitée avec l’orientation courante de l’avion. Difficile d’obtenir cette propriété directement avec les angles d’Euler (essayez !), et il est préférable de représenter l’orientation de l’avion par un quaternion (ou par une matrice de rotation) :

  1. Changez le Airplane::drawWorld pour tenir compte de l’attribut Quaternion _orientation (déjà déclaré) à la place des 3 rotates selon les axes x,y,z.
  2. Changez le Airplane::pitchDown (qui est appelé lors de l’appui sur ’z’) pour remplacer _angleX+=_increment par :
    _orientation.rotate(_increment,Vector3(1,0,0)); // on cumule l'orientation courante avec une rotation (et non plus l'angle autour de X).
  3. De manière analogue, changez également Airplane::pitchDown, rollLeft, rollRight, yawLeft, yawRight.

Testez (constatez et comprenez la différence d’interactivité obtenue : chaque modification de l’orientation est maintenant faite par rapport à l’orientation courante de l’avion). Vérifiez également que le gimbal-lock n’existe plus. Assurez vous de comprendre l’ensemble.

Question 14. Dans Airplane::move on veut que l’avion avance de la valeur _velocity (cet attribut _velocity est modifié avec la molette de la souris) dans sa direction arrière/avant. Il faut donc modifier correctement _position (c’est la même chose que pour la voiture, sauf que l’axe arrière/avant local n’est pas nécessairement le même). Testez

Question 15. On souhaite que la caméra soit toujours placée à l’arrière de l’avion. Rendez vous dans GLApplication::updateCamera() (dans le case Camera_Follow_Plane). Pour le moment la caméra est bien placée en tenant compte de la position de l’avion, mais elle n’est pas placée à l’arrière de l’avion de manière fixe). Réalisez ce positionnement : vous pouvez utiliser _camera.position(Vector3), _camera.orientation(Quaternion). Pour transformer une direction par une rotation, il suffit de faire a=q*bq est un quaternion représentant la rotation, b un Vector3 et a le Vector3 transformé. Pour modifier un quaternion q, il suffit de faire q.rotate(angle,ax,ay,az). La position et l’orientation de l’avion (par rapport au monde) sont obtenues par _airplane.orientation() (Quaternion) et _airplane.position() (Vector3). Il faut donc affecter correctement la position/orientation de la caméra en fonction de la position/orientation de l’avion.

Testez :

6  Voiture/Avion

Question 16. Faire de même pour la voiture (dans GLApplication::updateCamera, dans le case Camera_Follow_Car) pour que la caméra suive toujours la voiture (raisonnement identique à celui pour l’avion : _car.position() et _car.orientation() vous donne la position et l’orientation de la voiture). Pour tester il faut cliquer sur "Follow Car" (le squelette affecte alors la variable nécessaire pour être dans le cas Camera_Follow_Car).

Question 17. On souhaite faire une transition plus douce entre les positions des caméras lorsqu’on clique successivement sur les boutons "Follow Car" (derrière la voiture) et "Follow Plane" (derrière l’avion). Nous allons effectuer une interpolation linéaire entre les positions et orientations de ces 2 caméras. On dispose de 2 variables _cameraStart et _cameraStop. La _cameraStart est affectée, par le squelette, lors du click (cf GLApplication::leftPanel) : elle prend simplement la valeur de la caméra courante (i.e. _camera). Il reste à affecter _cameraStop à la position/orientation à atteindre. On peut le faire, par exemple, dans GLApplication::updateCamera : au lieu d’affecter _camera (question précédente), on affecte _cameraStop (donc _cameraStop sera, soit à "derrière l’avion", soit à "derrière la voiture").

Il reste à affecter _camera (la caméra courante) avec une interpolation linéaire entre _cameraStart et _cameraStop en position et en orientation : une valeur _lambda évolue déjà dans le squelette entre 0 (au moment du _cameraStart) et 1 (cf début de GLApplication::updateCamera()).

Réalisez cette interpolation dans updateCamera (l’interpolation sera constamment faites le _lambda restant fixe une fois qu’il atteint 1; vous pouvez directement multiplié un Vector3 ou un Quaternion par un réel; par exemple q1=q2*_lambda). Testez :


Ce document a été traduit de LATEX par HEVEA