TP Interaction et Controle d'Application


L'objectif du TP est de vous montrer quelques unes des problématiques liées à l'interaction en réalité virtuelle et plus spécifiquement la gestion des élements de controle d'application, à savoir les menus et l'interaction avec ces menus.

Préambule

Nous repartirons pour cette séance du programme réalisée au TP précédent, contenant une première technique pour attraper et déplacer des objets (drag and drop de la scène entière). Ajoutez à présent à votre programme la méthode suivante :

void Application::drawRWB()
{
   	glPushAttrib(GL_ALL_ATTRIB_BITS);
   	glPushMatrix();

   	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
   	glBegin(GL_QUADS);
   		glColor3f(0.5, 1.0, 0.5);
   		// Horizontal Screen
   		glVertex3f(-0.9, -0.91, 1.23); glVertex3f(0.9,-0.91,1.23);
   		glVertex3f(0.9,-0.63,-0.075); glVertex3f(-0.9,-0.63,-0.075);
   		// Vertical Screen
   		glVertex3f(-0.9,-0.63,-0.075); glVertex3f(0.9,-0.63,-0.075);
   		glVertex3f(0.9,0.72,-0.075); glVertex3f(-0.9,0.72,-0.075);
   	glEnd();

   	glPopMatrix();
   	glPopAttrib();
}

Cette méthode sera appelée directement dans la méthode draw de votre programme lorsque vous le testerez sur station, pour représenter dans l'espace virtuel le cadre du workbench et faciliter le placement des éléments graphiques. Elle ne sera utile que dans la simulation sur station de travail et vous pourrez retirer (ou mettre en commentaire) son appel lorsque vous passerez sur le workbench pour tester votre application.

Etudiez et commentez la signification des valeurs numériques utilisées dans les glVertex.

Rayon virtuel

Nous désirons à présent et pour toute la suite etre capable de saisir plusieurs objets différents et les déplacer indépendamment les uns des autres. La technique du TP précédent déplace l'ensemble de la scène et de ses objets. Il nous faut ajuster cette technique pour sélectionner (identifier) et déplacer indépendamment chaque objet dans la scène.

La technique du rayon virtuel, très utilisée en RV, consiste à visualiser la direction pointée par le périphérique d'interaction (le flystick) par un rayon virtuel. Il est représenté graphiquement comme une simple ligne, de très grande longueur. Son placement est relatif au flystick et sa position doit être mise a jour régulièrement à chaque frame (dans la méthode preFrame).

Une fois que le rayon virtuel suit correctement les mouvements et les orientations du flystick, il faut ensuite être capable de détecter l'intersection du rayon virtuel avec les objets de la scène. Créez dans votre scène deux boites cubiques (chacune possédant sa matrice de positionnement dans le repère du monde) et ajoutez le code nécessaire pour détecter l'intersection du rayon avec les éléments de la scène :

bool Application::intersectCube(gmtl::Vec3f origin, gmtl::Vec3f dir, gmtl::Matrix44f cubeMat)
{
	gmtl::Ray ray(origin, dir);
	gmtl::Tri tri[12];
	float u,v,t;
	bool res = false;
	
	// 8 sommets
	gmtl::Vec4f A, B, C, D, E, F, G, H;
	gmtl::Vec3f a, b, c, d, e, f, g, h;
	
	A.set(0.0f, 0.0f, 0.0f, 1.0f); 
	B.set(1.0f, 0.0f, 0.0f, 1.0f);
	C.set(1.0f, 1.0f, 0.0f, 1.0f);
	D.set(0.0f, 1.0f, 0.0f, 1.0f);
	E.set(0.0f, 0.0f, 1.0f, 1.0f); 
	F.set(1.0f, 0.0f, 1.0f, 1.0f);
	G.set(1.0f, 1.0f, 1.0f, 1.0f);
	H.set(0.0f, 1.0f, 1.0f, 1.0f);

	A = cubeMat * A;
	B = cubeMat * B;
	C = cubeMat * C;
	D = cubeMat * D;
	E = cubeMat * E;
	F = cubeMat * F;
	G = cubeMat * G;
	H = cubeMat * H;
	
	a.set(A[0],A[1],A[2]);
	b.set(B[0],B[1],B[2]);
	c.set(C[0],C[1],C[2]);
	d.set(D[0],D[1],D[2]);
	e.set(E[0],E[1],E[2]);
	f.set(F[0],F[1],F[2]);
	g.set(G[0],G[1],G[2]);
	h.set(H[0],H[1],H[2]);

	// 2 triangles par face
	tri[0].set(a,b,c);
	tri[1].set(c,d,a);
	
	tri[2].set(e,f,g);
	tri[3].set(g,h,e);
	
	tri[4].set(f,b,c);
	tri[5].set(c,g,f);
	
	tri[6].set(a,e,h);
	tri[7].set(h,d,a);
	
	tri[8].set(h,g,c);
	tri[9].set(c,d,h);
	
	tri[10].set(a,b,f);
	tri[11].set(f,e,a);

	res = intersect(tri[0],ray,u,v,t) ||
		  intersect(tri[1],ray,u,v,t) ||
		  intersect(tri[2],ray,u,v,t) ||
		  intersect(tri[3],ray,u,v,t) ||
		  intersect(tri[4],ray,u,v,t) ||
		  intersect(tri[5],ray,u,v,t) ||
		  intersect(tri[6],ray,u,v,t) ||
		  intersect(tri[7],ray,u,v,t) ||
		  intersect(tri[8],ray,u,v,t) ||
		  intersect(tri[9],ray,u,v,t) ||
		  intersect(tri[10],ray,u,v,t) ||
		  intersect(tri[11],ray,u,v,t);
	
	return res;
}

Ce code s'appuie sur la fonction gmtl suivante : intersect qui détecte l'intersection d'un triangle et d'un rayon.

Une fois que le rayon détecte correctement les intersections avec les objets de la scène, ajoutez les instructions nécessaires pour permettre de saisir et déplacer un objet sélectionné lorsque l'utilisateur clique sur un bouton du flystick et tant qu'il le maintient.

Pour cela vous devez sauvegarder la matrice de positionnement et d'orientation du rayon virtuel au moment du clic initial dans le repère du monde (le repère des trackers) : M(w<-f). W pour world (repere du monde) et f pour Flystick (le joystick tenu en main d'ou part le rayon virtuel). Appelons d'autre part M(w<-o) la matrice de positionnement/orientation de l'objet dans le repère du monde.

L'objet saisi par le rayon est positionné/orienté dans le repère local du rayon/flystick par la matrice : M(f<-o) = M(f<-w) * M(w<-o) = M(w<-f)^(-1) * M(w<-o). L'inversion est réalisée par la fonction gmtl::makeInvert. Cette matrice M(f<-o) est invariante tant que l'on aggripe l'objet ! L'objet est solidaire du rayon pendant la manipulation.

A chacune des frames suivantes (tant que le clic est maintenu),

Menu 2D flottant fixe

Il s'agit ici de réaliser un petit menu simple prenant l'aspect d'une fenetre 2D "flottante" avec quelques boutons dessus. Le menu aura l'apparence d'une boite rectangulaire dotée une faible épaisseur et sera placé dans l'espace 3D au niveau de l'écran du bas du workbench, à portée de main. Deux boutons de forme rectangulaire eux aussi seront disposés sur la barre de menu. Cf le schéma ci-dessous pour l'apparence à reproduire :

Le menu restera fixe dans cette première version. Ajoutez les instructions nécessaires à votre programme pour que l'intersection du rayon virtuel manipulé par l'utilisateur avec les boutons du menu déclenchent un changement de couleur des boutons (pour les "illuminer" quand on passe dessus).

De plus, lorsque l'utilisateur "clique" (ie appuie sur un des boutons du flystick), si le rayon intersecte un bouton, l'action correspondante doit etre déclenchée. Testez en changeant par exemple la couleur d'un cube dans la scène en fonction du bouton choisi (bleu ou rouge).

Menu 2D flottant mobile

Parfois un menu fixe peut etre obstrué par des éléments manipulés dans l'environnement virtuel. Pour palier à ce problème d'inaccessibilité, une premiere solution consiste à permettre le déplacement du menu lui meme.

Faites en sorte à présent que le socle du menu puisse etre déplacé lorsqu'il est sélectionné par le rayon (en drag and drop comme précédemment. N'oubliez pas que lorsque le socle bouge... les boutons aussi).

Menu 2D flottant attaché à la main non dominante

Une technique alternative pour combattre les problèmes d'occlusion, toujours en donnant le choix à l'utilisateur de placer le menu ou il le souhaite, consiste à afficher le menu par rapport au corps de l'utilisateur et plus spécifiquement ici par rapport à sa main non dominante.

Nous allons à présent modifier votre programme pour que la position du menu suive la position de la main non dominante de l'utilisateur. Notez que la sélection des boutons se fera toujours avec la main dominante qui controle le rayon et que l'interaction devient bi-manuelle. Nous allons donc utiliser les gants de données, qui vont remplacer le flystick.

Pour utiliser les gants vous devez définir

dans la classe simpleApp :

 
   gadget::PositionInterface  mRightHand, mLeftHand;
   gadget::AnalogInterface  mRightThumb, mRightIndex, mRightMiddle;
   gadget::AnalogInterface  mLeftThumb, mLeftIndex, mLeftMiddle;
dans la méthode init :

    mRightHand.init("VJRightHand");
    mRightIndex.init("RightIndexFlex");
    //etc...
dans la méthode preFrame
	gmtl::Vec3f rightGlovePos(mRightHand->getData(1.0)[0][3], mRightHand->getData(1.0)[1][3], mRightHand->getData(1.0)[2][3]); // pour recupérer les valeurs de tracker des gants
	
	mLeftIndex.getProxy()->getData() renvoie une valeur entre 0 et 1 de flexion du doigt qui peut être testé pour "cliquer" en fermant la main

Sélection au rayon vs Sélection main libre

Dans la majorité des applications de réalité virtuelle la sélection et la manipulation des objets passe par une des deux grandes techniques de sélection/manipulation d'objet : le rayon virtuel ou l'interaction directe avec la main. Le rayon virtuel permet de s'abstraire du problème de l'ajout de la profondeur à la tâche classique de sélection. Regardons à présent la sélection directe avec la main.

En repartant de votre programme actuel, changer l'interaction pour que l'utilisateur sélectionne les objets/boutons/menus en plongeant la main à l'intérieur (il faut écrire une fonction pour détecter si un pointeur se situe "dans" un volume cubique). "Cliquer" équivaudra à refermer le poing.

Comparer vos impressions avec les deux méthodes.