Master Informatique 2017-2018
M3DS



TP 10 : Interaction 3D


Exercice 1. Introduction


Exercice 2. Navigation

Dans GLApplication.cpp vous disposez d’une variable pour la caméra Camera2 _camera. Une caméra est définie par (cf Camera2.h) :

Pour ce TP, vous n’avez pas à vous préoccuper des aspects OpenGL. Le squelette intègre le tracé de la scène (voir GLApplication::draw() qui appelle tous les tracés nécessaires), la gestion de la modelview (p3d::modelviewMatrix), de la projection (p3d::projectionMatrix) pour les shaders.

Pour effectuer les changements de repère nécessaires au tp, vous disposez déjà des méthodes pour passer du repère world au repère caméra, et réciproquement (voir les déclarations dans Camera2.h) :

Question 1. Dans le squelette, la mise à jour du positionnement de la caméra sera assurée par GLApplication::updateCamera (appelée avant le tracé de chaque image) : faites des _camera.translate qui conviennent pour les tests if left() (correspond à l’appui sur la touche ’q’), right (touche ’d’), forward (touche ’z’) et backward (touche ’s’).

Testez (réglez éventuellement l’amplitude choisie pour la translation).

Question 2. Pour orienter le point de vue, on va compléter le if (mouseLeft()) de la même méthode GLApplication::updateCamera (le test passe si on maintient le click gauche de la souris). Pour cela, on choisit de considérer une navigation dite "Fly" : le mouvement de la souris bas/haut correspond à orienter la tête bas/haut, et le mouvement gauche/droite à tourner le torse vers la gauche/droite. Le corps reste toujours vertical par rapport au sol (i.e. l’axe "pied/tête" correspond à l’axe (0,1,0) du monde). Pour le résultat attendu, voir la vidéo :

Pour l’angle de rotation vous utiliserez les angles deltaMouseX() et deltaMouseY() qui donne les incréments de la souris. Les translations (codées à la question précédente) devront toujours correspondre à "avancer/reculer", "gauche/droite" selon la caméra (i.e. la navigation doit être "intuitive")

Codez les _camera.rotate corrects pour modifier l’orientation dans le if (mouseLeft()) de GLApplication::updateCamera() (la principale difficulté est de déterminer précisément par rapport à quels axes, et dans quel ordre, on fait les rotations; toute approche par tatonnement risque de donner une navigation non intuitive).

Testez.

Assurez vous d’avoir une navigation "intuitive" (pas de roulis par rapport au monde; la caméra avance/recule dans la direction du point de vue; vérifiez avec la vidéo. Remarque : il ne s’agit pas d’une navigation First-Person par laquelle il faudrait avancer/reculer en tenant compte uniquement de l’orientation du torse (mouvement toujours parallèle au sol).

Comprenez parfaitement l’ensemble et, notamment, la différence entre un _camera.translate(...,Coordinate_Local) et un _camera.translate(...,Coordinate_World) pour les translations effectuées sur la caméra (testez les deux possibilités si ce n’est pas clair).


Exercice 3. Curseur Souris/Monde
Pour effectuer une sélection d’objet avec un click souris (opération dite de picking), la première chose à faire est d’obtenir les coordonnées de la souris dans le repère du monde. Les coordonnées de la souris sont initialement données en coordonnées Window (coordonnées pixel récupérées par le serveur graphique). Dans le squelette on dispose de ces coordonnées Window par (mouseX(),mouseY()).

Pour savoir quel objet 3D est cliqué, nous allons déterminer tout d’abord la droite qui part de la caméra et qui passe par le pixel (mouseX(),mouseY()). On nomme souvent cette droite picking ray. Il faut exprimer ce rayon dans le repère du monde pour pouvoir ensuite effectuer les intersections nécessaires avec les objets de la scène.

Question 1. Pour exprimer la souris dans le repère du monde, il s’agit de faire l’opération inverse du tracé quand on part des coordonnées locales d’un objet (pour rappel : lors du tracé on effectue la "chaine" local->world->eye->(clip coordinates)->NDC->window coordinates sur les coordonnées d’un sommet à tracer).

Pour notre souris, connue en Window Coordinate, on l’exprime donc d’abord en NDC : il suffit d’appliquer le viewport inverse. Faites le dans Camera2::windowToNDC(x,y) ((le viewport est donné par les attributs _viewX,_viewY,_viewWidth,_viewHeight; la coordonnées z de la souris correspond à la position de l’écran, c’est à dire −1 en NDC).

Pour tester : vérifier dans la console lorsque vous cliquez avec le bouton gauche; les coordonnées affichées doivent correspondre à des coordonnées normalisées : notamment (-1,-1,-1) pour le coin bas gauche et (1,1,-1) pour le coin haut droit, (0,0,-1) pour le centre de la fenêtre.

Question 2. Une fois normalisées il suffit de passer en coordonnées Eye. Pour cela on applique l’inverse de la projection aux coordonnées de la souris une fois qu’elles sont connues en NDC : faites-le dans Camera2::windowToCamera(x,y). Ensuite, il reste à trouver les coordonnées World (il suffit d’appliquer Mworldcamera une fois qu’on connait la souris dans le repère Eye) : faites le tout dans Camera2::windowToWorld(x,y).

Pour pouvoir tester, il reste à compléter Camera2::pickingRay(x,y) qui doit donner le rayon (i.e. la droite d’origine la caméra et passant par le pixel (x,y)) dans le repère world à partir du (x,y) de la souris. Pour cela, grâce à votre Camera2::windowToWorld(x,y), on connait les coordonnées souris dans le repère du monde Pworld; l’origine de la droite lui est Aeye=(0,0,0) dans le repère Eye; on peut alors prendre comme vecteur directeur AworldPworld.

Testez : vous devez voir une sphère rouge qui correspond au click souris (cette sphère est affichée dans GLApplication::drawCursor; elle utilise la droite définie par l’attribut _pickingRay pour la positionner; _pickingRay est affecté dans GLApplication::selectObject avant chaque image en appelant votre _camera.pickingRay(x,y)).


Exercice 4. Sélection
Pour déterminer quel objet 3D est cliqué, on choisit de faire explicitement l’intersection du _pickingRay avec tous les triangles de la scène (sélection dite "fine"). C’est la classe SceneIntersection qui se charge de faire ces intersections. La méthode SceneIntersection::intersect(MeshObject3D *mesh) se charge de faire les intersections pour un objet 3D avec _pickingRay (la boucle sur tous les objets est déjà faite dans la méthode du dessous). La droite d’intersection est l’attribut _pickingRay.

On cherche l’intersection avec chaque triangle de l’objet. Chaque intersection trouvée est placée dans un tableau (membre SceneIntersection::_result). Chaque intersection (de classe Intersection) contiendra les informations nécessaires pour exploiter cette intersection par la suite (la référence de l’objet 3D intersecté, le rayon, et le lambda de l’équation du rayon P=Au correspondant à l’intersection). L’insertion de l’intersection dans ce tableau s’effectue par ordre croissant dans la direction du rayon (lambda croissant). Toute cette démarche est déjà codée dans SceneIntersection::intersect. Il reste à compléter l’intersection du rayon avec un triangle.

Question 1. Il manque l’intersection du rayon avec un triangle bool SceneIntersection::intersectTriangle(ray,s0,s1,s2,*lambdaRes) que vous devez compléter (cf les commentaires pour les aspects techniques). Pour tester, seul le gros triangle de la scène est pour l’instant dans la liste des objets à intersecter (voir la méthode GLApplication::selectObject).

Testez : lorsque vous faites un click droit les intersections doivent s’afficher à l’écran (notamment un "0" s’affiche sur le triangle, ainsi que le rayon); bougez avec le click-gauche pour s’assurer que l’intersection est bien valide quelque soit le point de vue.

Question 2. Mettez tous les objets en faisant _sceneIntersection.intersect({&_triangle,&_triceratops,&_cow},_pickingRay); dans GLApplication::selectObject et constatez que toutes les intersections apparaissent bien.

Click droit sourisaprès mouvement de la caméra


Exercice 5. Manipulation en translation
Le déplacement de l’objet sélectionné selon la souris est géré dans GLApplication::moveSelectedObject.

Question 1. Dans le bloc if (_controlMouse==Manipulation_Translation), essayez un simple mesh->translate(dx,dy,0,Coordinate_Local); (dx et dy dépendent directement des incréments de la souris; mesh correspond à l’objet intersecté lors du select). Constatez (et comprenez) que :

Question 2. De nombreux principes peuvent être adoptés pour faire la manipulation (la difficulté étant d’offrir le maximum de liberté tout en ayant une interaction intuitive). Ici on choisit de déplacer l’objet toujours dans un plan parallèle à la caméra. La vidéo montre le résultat attendu :

La translation à effectuer sur l’objet doit bien correspondre à un déplacement (dx,dy,0) mais considérés dans le repère de la caméra. Il suffit de convertir ce déplacement (dx,dy,0) du repère caméra dans le repère de l’objet avant le mesh->translate(?,?,?,Coordinate_Local) la classe Mesh disposent des même facilités que la camera : notamment vous disposez de mesh->pointTo(Coordinate_Local,...) , mesh->directionTo(Coordinate_Local,...), etc). Testez (vérifiez notamment l’indépendance du point de vue en bougeant la caméra).

Question 3. On souhaite que le point clické corresponde maintenant précisément au curseur. Comparez le résultat précédent avec le résultat attendu :

moveDirect

Pour résoudre ce problème, on peut s’appuyer sur le schéma suivant :

T correspond au mouvement de la souris sur l’écran. Il faut trouver la translation T′ à appliquer à l’objet pour que l’objet suive le curseur (i.e. P doit être déplacé en P′). La difficulté est de trouver T′ en fonction des informations qu’on possède. Le point P est mémorisé par le squelette à chaque update dans _attachPointWorld (initialement c’est l’intersection entre le premier _pickingRay et l’objet). Ensuite, pour trouver P′, on propose de procéder par intersection entre le picking ray et le plan d’interaction (i.e. le plan parallèle à l’écran passant par le point P; i.e. le plan où est représenté T′ sur le schéma)

Pour le plan d’interaction, on connait le point P dans le repère world (c’est _attachPointWorld) et sa normale est (0,0,1) dans le repère caméra. Il suffit de résoudre l’intersection droite/plan pour trouver P’ (tout exprimer dans le repère world, par exemple, avant de faire le calcul).

Une fois connue P’, la translation est PP’^→ (qu’il faut exprimer dans le repère de l’objet pour effectuer un mesh->translate(..., Coordinate_Local) ou dans le repère world avec un mesh->translate(..., Coordinate_World) ).

Testez : le click droit doit translater l’objet sélectionné parallèlement au plan de la caméra. Le point cliqué doit rester au même endroit par rapport à l’objet (revoir la vidéo du résultat attendu; notez que la sphère rouge est fixe par rapport à l’objet manipulé et désigne toujours le même point de l’objet). Vérifier également que cette manipulation est indépendante du point de vue en naviguant avec click gauche (i.e. le déplacement de l’objet est toujours parallèle au plan de la caméra quelque soit le point de vue).

Remarque : d’autres calculs peuvent aboutir au même résultat (le schéma fait penser à thales par exemple...), mais ici le principe reste général pour contraindre l’interaction sur n’importe quel plan. Par exemple, on peut envisager d’interagir pour placer les objets les uns par rapport aux autres avec un plan d’interaction parallèle au sol : testez cette possibilité (il suffit simplement de changer la normale du plan d’interaction).


Exercice 6. Manipulation en rotation
En interaction 3D, il est difficile de proposer une manipulation en orientation intuitive et générique quelque soit l’application (rotations autour des axes de l’objet, des axes de la caméra ?). Les logiciels tentent de proposer toutes les possibilités en visualisant explicitement les axes de rotation (par des cercles accompagnés de "poignées" généralement : cf par exemple https://docs.blender.org/manual/en/dev/editors/3dview/object/editing/transform/control/orientations.html). On se contente ici d’une solution simple en tournant l’objet autour de l’axe X ou l’axe Y de la caméra.

Question 1. Proposez (dans GLApplication::moveSelectedObject dans le bloc Manipulation_Orientation) une orientation selon les axes X (souris bas/haut) et Y (souris gauche/droite) de la caméra : vous pouvez utiliser mesh->rotate(angle,axe,Coordinate_???) pour tourner l’objet. Testez et critiquez brièvement votre solution dans le Readme.txt (manipulez par exemple aléatoirement la vache, puis essayez de la ré-orienter pour qu’elle regarde vers la caméra).


Ce document a été traduit de LATEX par HEVEA