IMA5 2019/2020 P18 : Différence entre versions
(→Partie web) |
(→Partie web) |
||
Ligne 127 : | Ligne 127 : | ||
}</nowiki> | }</nowiki> | ||
− | Maintenant, la fonction d' | + | Maintenant, la fonction d'envoi de configuration crée le json ci-dessous: |
<nowiki>{ | <nowiki>{ | ||
"ButtonA":{"x":---,"y":1,"z":---}, | "ButtonA":{"x":---,"y":1,"z":---}, | ||
Ligne 140 : | Ligne 140 : | ||
"oculusControllerOn": false | "oculusControllerOn": false | ||
}</nowiki> | }</nowiki> | ||
− | Sur Unity, les axes y et z sont inversés, c'est pour cela que la première fenêtre enverra les données en x et z dans le json, et la seconde en y. Cependant, les positions qui été enregistrées sont relatives aux pixels de la page web, et ne sont pas adapter au Quest qui prend des positions métriques (si x=1 alors la touche est | + | Elle récupère enfaite la position relative de chaque image sur la fenêtre. Sur Unity, les axes y et z sont inversés, c'est pour cela que la première fenêtre enverra les données en x et z dans le json, et la seconde en y. Cependant, les positions qui été enregistrées sont relatives aux pixels de la page web, et ne sont pas adapter au Quest qui prend des positions métriques (si x=1 alors la touche est placée à 1m à droite du joueur), en semaine 3 on établira rapidement l'équation pour les convertir. |
=====Partie Unity===== | =====Partie Unity===== |
Version du 18 février 2020 à 10:46
Sommaire
Présentation générale
Sujet : Virtual Reality Old Gaming
Etudiant : Ibrahim Ben Dhiab et Fabien Di Natale
Encadrant : Laurent Grisoni & Valentin Beauchamp
Description
L'équipe de recherche MINT, dans le cadre d'un projet européen intitulé VR4REHAB, travaille sur un dispositif combinant réalité virtuelle et "serious gaming", et qui a pour but d'aider à la rééducation des personnes handicapées. L'utilisateur pourra jouer à des jeux rétros tel que Tetris ou encore Bomberman, et bien d'autre. Pour cela, ils ont installé le système d'exploitation RetroPie sur une Raspberry Pi qui permet d'émuler des jeux rétros, la Raspberry envoie ensuite le flux vidéo du jeu par wifi au casque VR.
Objectifs
Nous souhaitons:
faire l'adaptation nécessaire à l'allègement de la forme physique du dispositif, ou nous souhaitons passer à une version n'utilisant plus le portable, mais une émulation sur carte Rapsberry. Le travail de proposition d'architecture a déjà été fait pour ce point.- faire en sorte d'étendre le système pour avoir une appli de configuration permettant de choisir la console et le jeu. Une partie de ces éléments de configuration pourront être utilisables dans le casque de RV par le malade, l'autre partie par le médecin, via une application tierce.
- éventuellement et en fonction de l'avancement du projet, mettre en place quelques scénarios d'interaction spécifiques, déjà déterminés, dans le dispositif interactif utile à l'avancée du projet européen.
Préparation du projet
Cahier des charges
Le projet a été mis en place suite à un besoin des hôpitaux et cliniques de rendre la rééducation moins compliquée et plus attrayante. Nous utilisons donc de vieux jeux vidéo en réalité virtuelle afin d'amener les personnes à effectuer des mouvements comme si elle faisait des exercices physiques tout en s'amusant, rendant ainsi le processus de rééducation plus agréable. De plus, les pathologies chez les personnes handicapées sont très variées, notre dispositif doit ainsi être polyvalent et facilement configurable par les médecins ou le personnel médical qui serait amené à faire les rééducations.
Ainsi, nous allons nous concentrer sur la partie polyvalence et configuration du système. Nous avons pour objectif de rendre le système le plus polyvalent afin qu'il puisse convenir au plus de personnes possibles et facilement utilisable par l'équipe médicale.
Nous devons :
- Améliorer la page web afin de proposer la personnalisation des paramètres suivant:
- Choisir le nombre de manettes (type NES) utilisables par le patient
- Interface de repositionnement libre des touches dans l'espace virtuel
- Ajout de script C# en concordance avec le serveur web
- Décoder correctement les configurations envoyées par le serveur web de la Raspberry
- Implémenter une fonction pour déplacer toutes les touches séparément de la manette de base
- Implémenter une fonction pour que la manette et l'écran de jeu se déplace
- Implémenter une fonction pour effectuer des actions avec des mouvements, donc sans manette virtuelle
Choix techniques : matériel et logiciel
Le choix matériel et logiciel avait déjà été décidé par l'équipe MINT. Le casque Oculus Quest est idéal pour ce projet car il est portable et les applications tournent directement dessus, sans devoir passer par un ordinateur, et a un coût abordable. La Raspberry Pi a été choisie car moins intrusive comparé à un ordinateur, et bien moins cher.
Ce projet est réalisé à l'aide de Unity (scripts en C#) pour programmé le casque Oculus Quest, couplé à des serveurs webs et TCP sur une Raspberry Pi, donnant l'accès à une page web de configuration pour l'utilisateur et renvoyant la configuration au Quest.
Calendrier prévisionnel
Réalisation du Projet
Semaine 1
Valentin Beauchamp, l'ingénieur de recherche qui travaille sur le projet, et qui nous supervisera tout au long des 6 semaines, nous a introduit au projet et à son fonctionnement.
Nous nous sommes familiarisés avec le Quest, son interface et son fonctionnement.
- Télécharger le projet à partir du git
- Suivre les instructions des read.me
- Installation de Unity, de certain assets et paramétrage d'Unity;
- Installation et configuration de la Raspberry Pi
Ce sujet est divisé en deux parties:
- Partie web (Raspberry Pi)
- Partie Unity (Quest)
En fin de première semaine, nous avons pu découvrir la structure du projet sur la Raspberry et le Quest. Plusieurs serveurs tournent sur la Raspberry à partir de deux scripts JS, et ont pour but de permettre à l'utilisateur de choisir une configuration sur une page web et de communiquer cette config au casque. Pour l'instant, il y a deux pages web qui permettent de:
- Contrôler le jeu dans le casque à distance à l'aide de différents dispositifs de contrôle (Gamepad, Touchpad ...)
- Configurer la scène virtuelle avec différents paramètres, tel que le nombre de Gamepad (1 à 3), ou activer/désactiver les manettes Oculus.
Semaine 2
Prise en main du projet avec un apprentissage de node.js et des langages javascript et C# qui sont nouveaux pour nous. Découverte de l'interface Unity, est composé de scènes et de scripts permettant de changer ces scènes.
La scène Unity
Nous en avons une seule scène composée des objets suivants :
- L'objet statique créant la pièce de jeu, c'est l'environnement que nous pouvons retrouver autour de nous lorsque nous avons le casque de réalité virtuelle devant les yeux. Cet objet est statique et n'est pas relié à des scripts ou autre, d'où son nom.
- L'objet quad qui est un affichage lié à 4 scripts, cet objet est l'endroit où l'image produit par la Raspberry va apparaître, en d'autre mot, ça sera là où l'on peut voir le jeu comme une télévision. Les 4 scripts permettent la connexion et les échanges avec la Raspberry, nous avons les scripts TCP config, TCP Commande, TCP screen et Vibration manager. TCP config permet de récupérer la configuration envoyée par la Raspberry et lancer toutes les fonctions nécessaires afin de reconfigurer le Quest selon la configuration reçue.
- L'objet Event System permet la liaison entre la scène et les interactions extérieurs comme des entrées clavier, souris ou des entrées spéciales. Cet objet est fait pour faciliter la gestion des différents objets de la scène et la communication entre ces objets. L'Event System gère quel objet est sélectionné, quelle gamme d'entrées sont considérés et les différentes mise à jour de la scène.
- L'objet Canvas permet l'affichage des erreurs, cet objet a été ajouté afin de pouvoir afficher du texte en jeu. Il est positionné devant le joueur mais ne peut être activé seulement à partir de Unity, quand cet objet est activé cela signifie que l'on est en DEBUG mode. Nous pouvons ainsi voir tous les logs que le programme envoie et ainsi détecter les erreurs plus facilement.
- L'objet OVRPlayerControler est l'objet le plus complexe, nous ne rentrerons pas dans les détails mais cet objet va gérer toute la partie casque, manette lié au casque Oculus et suivi des mains du joueur. Il possède une partie de suivi de position du casque permettant de positionner le joueur dans l'espace de la scène, pour cela il a des sous objets fixe qui lui servent de repère dans l'espace. Il possède ensuite d'autres sous objets fixe afin de pouvoir suivre le mouvement des manettes ou des mains à l'aide des caméras qu'il possède.
- L'objet GameControl regroupe les manettes NES de base que l'on utilise dans la configuration par défaut. Il y a trois sous objets, soit trois manettes. Ces trois manettes ont une seule différence leur positionnement, elles sont positionnées de telle sorte que lorsque les trois manettes sont activées on puisse avoir accès à tous les boutons sans conflit entre eux. L'objet utilise un script Game Controle Button permettant la modification du nombre de manettes affichées et donnant la possibilité de changer les boutons actifs à intervalle de temps régulier.
- L'objet PadLibre est l'objet créé pour notre fonctionnalité, c'est une manette NES comme les sous objets du GameControl mais sans le design autour donnant une impression de boutons volants et nous permettant ainsi de déplacer n'importe où les boutons dans l'espace.
Les scripts utilisés
- ActiveControllerCollider permet l'activation des collisions des mains avec les autres objets de la scène ayant leur collision activée (Partie du projet où l'on veut utiliser les mains au lieu des manettes de l'Oculus)
- ChangeScale permet d'agrandir ou réduire l'objet sur un input particulier
- ClickOnButtonMenu est un script pas utilisé mais fait dans le but de pouvoir viser avec la manette de l'Oculus et sélectionner certain boutons, ici les boutons du menu
- CollisionScript fait toutes les commandes nécessaires lors de la rencontre entre la manette et les boutons, ce script rend le bouton un petit peu plus foncé et fait vibrer la manette lors de la collision afin de pouvoir voir que l'on touche le bouton. De plus il change des variables afin que le programme sache quel bouton est appuyé et puisse l'envoyer à la raspberry.
- ControllerXbox permet de détecter les entrées du contrôleur xbox afficher sur le site web, les transforme en commande et les envoie à la raspberry.
- GameControleButton a pour but de gérer l'objet GameControl et les trois manettes qui vont avec, il permet d'activer de zéro à trois manettes tout en les positionnant du mieux possible afin qu'elles soit le plus centré possible. De plus lorsque plusieurs manettes sont activées le script possède un système de changement de bouton actif afin que les boutons actifs change en permanence, par défaut c'est toutes les 30 secondes.
- Handcollider est un script s'occupant de la partie du projet sans les manettes, il permet de créer les collisions entre les mains du joueur et les différents objets de la scène.
- HandInteraction est un script s'occupant de la partie du projet sans les manettes, il permet de transformer certain mouvement de doigt ou position de main en commande comme ouvrir le menu ou autre
- Lz4DecoderStream permet de coder et décoder les messages que nous envoyons et recevons de la Raspberry.
- OculusController permet de détecter les entrées du contrôleur de l'Oculus, les transforme en commandes et les envoie à la Raspberry.
- QuitGame permet de fermer l'application proprement.
- rotationScreen est un script créé dans le but de faire tourner l'écran par la suite mais pas utilisé pour le moment.
- TCPCommande permettant de faire la liaison entre les collisions entre les manettes et les boutons et les commandes envoyé à la Raspberry pour manipuler le jeu.
- TCPConfig permet la configuration de notre scène (de l'environnement de jeu), celui-ci lit les messages en attendant une configuration dans un thread, une fois que cette configuration est arrivée il décortique l'information et lance les fonctions nécessaires et change les variables nécessaires afin d'appliquer la nouvelle configuration reçue.
- TCPScreen permet de recevoir les messages en provenance de la Raspberry, les décode à l'aide du script Lz4DecoderStream et les affiches ensuite. La réception des messages est fait dans un thread comme toutes les réceptions de message.
- VibrationManager permet de faire vibrer les manettes lorsque la fonction est appelée.
Brainstorming
Réunion avec des membres de MINT afin d'apporter des idées de fonctionnalités à implémenter pendant notre PFE (ont été ajoutées dans le cahier des charges).
Au cours de cette réunion, nous avons pu récolter des idées de fonctionnalités à implémenter. Nous avons pu en ressortir deux d'entre elles. La première consiste à déplacer librement les différents boutons composant la manette NES afin de pouvoir changer la difficulté de l'exercice en temps réel et pouvoir adapter l'exercice au plus de patients possibles. La seconde idée a pour but de faire marcher le patient, pour ceci nous avons pensé à faire bouger l'écran afin que le patient soit obligé de se déplacer pour pouvoir continuer à voir l'écran correctement. Cette fonctionnalité pose plusieurs problèmes, si nous faisons bouger l'écran et que le patient bouge, nous devons également faire bouger la manette NES matérialisée dans le casque. Cependant, faire bouger la manette NES sans aucune concordance avec les déplacements du patient rendrait l'utilisation de la manette très difficile. Nous avons donc décidé d'accrocher la manette NES à la caméra et ainsi toujours l'avoir devant les yeux. Le second problème vient du déplacement, si le patient se déplace avec le casque sur les yeux, il ne pourra pas voir les obstacles qui l'entoure. Pour cela, la seule solution est un bon paramétrage du guardian (zone de jeu, en dehors de cette zone de jeu le casque affiche l'entourage de l'utilisateur grâce aux caméras de l'Oculus) ou une surveillance des médecins. Pour ce second problème, nous pouvons limiter les risques d'accident en définissant les déplacements de l'écran et toujours le faire revenir à sa position initiale et donc faire des aller-retour. A la fin de cette réunion nous avons pu parler de ce qui était faisable dans notre temps imparti et avec l'équipe nous en avons déduit que la seconde fonctionnalité était trop longue à réaliser en intégralité avec le peu de temps que nous disposions et au vu de nos connaissances des différents langages de programmation et du projet. Cependant il serait bien que l'on en étudie la faisabilité pour les personnes qui continuerons de travailler sur le projet. Nous avons ainsi été assigner à la première fonctionnalité qui est la plus intéressante en termes d'interêt.
La première fonctionnalité
Suite au brainstorming, notre tuteur nous a lancé sur notre première fonctionnalité qui a pour avantage de pouvoir convenir au plus de patients possibles et surtout à la majorité des pathologies nécessitant une rééducation du haut du corps. Cette fonctionnalité est en réalité une configuration particulière et très polyvalente, le but est de délier les boutons de la manette NES de leur support afin de pouvoir les placer tout autour du joueur. Ainsi, le but est de pouvoir changer le placement de tous les boutons quand on le souhaite et pouvoir les placer là où on le souhaite à l'aide de la page web.
Partie web
Il nous a été conseillé d'utiliser PixiJS pour créer une interface de manipulation d'images, en effet, PixiJS est un moteur de rendu 2D WebGL, et permet de créer des images dynamiques sur une page web. On a ainsi décidé de créer deux fenêtres ou canvas, une permettra de placer les boutons sur les axes x et y de l'espace virtuel, et l'autre permettra de les déplacer sur l'axe z à l'aide d'un fonctionnement par drag and drop. Pour cela, on s'est inspiré d'un exemple intitulé Dragging fourni sur le site de PixiJS. Il fallait tout d'abord installer le module Pixi sur la Raspberry, on a pu utiliser npm qui est le gestionnaire de paquets officiel de Node.js.
Lorsqu'un paquet est téléchargé, une dépendance est créée dans un fichier package.json, ce fichier permet enfaite de faciliter le partage du projet, en effet, comme on utilise GIT, on ne souhaite pas y enregistrer le dossier node_modules qui contient tous les paquets installés par npm, ainsi, seul le ficher package.json y est enregistré, et à partir de ce fichier, avec la commande npm install
, toutes les dépendances présentes dedans vont être installées.
Une fois Pixi installé, on a débuté la mise en place de la première fenêtre, un objet est créé pour chaques boutons, pour le fond d'écran, et pour l'image du Quest (point de référence). Tous les boutons ont été paramétrés pour être déplacables. Il faut maintenant modifier la fonction qui stocke la configuration dans le fichier json nommé headsetConfig.json. Cette configuration est envoyée par l'appui du bouton Send config sur la page web qu'on peut voir ci-dessous:
La page web utilise Bootstrap, principalement pour avoir une interface adaptée à une utilisation par téléphone. Initialement, le json était construit de cette manière:
{ "gamepadOn": false, "oculusControllerOn": false }
Maintenant, la fonction d'envoi de configuration crée le json ci-dessous:
{ "ButtonA":{"x":---,"y":1,"z":---}, "ButtonB":{"x":---,"y":1,"z":---}, "ButtonUp":{"x":---,"y":1,"z":---}, "ButtonDown":{"x":---,"y":1,"z":---}, "ButtonRight":{"x":---,"y":1,"z":---}, "ButtonLeft":{"x":---,"y":1,"z":---}, "ButtonStart":{"x":---,"y":1,"z":---}, "ButtonSelect":{"x":---,"y":1,"z":---}, "gamepadOn": false, "oculusControllerOn": false }
Elle récupère enfaite la position relative de chaque image sur la fenêtre. Sur Unity, les axes y et z sont inversés, c'est pour cela que la première fenêtre enverra les données en x et z dans le json, et la seconde en y. Cependant, les positions qui été enregistrées sont relatives aux pixels de la page web, et ne sont pas adapter au Quest qui prend des positions métriques (si x=1 alors la touche est placée à 1m à droite du joueur), en semaine 3 on établira rapidement l'équation pour les convertir.
Partie Unity
Du côté du casque de réalité virtuel nous devons créer un nouvel objet manette NES couplé à un nouveau script afin de placer les boutons en fonction de la configuration que nous recevons. Pour commencer nous avons dû nous entendre sur la construction du message json permettant de transférer la configuration entre la Raspberry et le casque vu plus tôt. Notre choix de créer un nouvel objet indépendant de tous les autres objets de la scène a pour but de garder le projet le plus propre possible. Comme nous sommes en coopération avec l'équipe mint nous avons préféré modifier le moins possible les fichiers de base afin de pouvoir mieux identifier ce que l'on a fait et permettre à l'équipe de pouvoir utiliser facilement les fonctionnalités que nous allons rajouter. Dans ce projet nous sommes nouveaux et ne connaissons pas toutes les subtilités de celui-ci, il est donc plus prudent de faire tout à part et ajouter notre travail au projet une fois que tout fonctionne et aura été validé par notre tuteur.
Une fois cela définit nous avons dû apprendre comment manipuler un fichier json en C#, récupérer les informations que l'on souhaite. Cela nous a pris quelque temps car avec le programme déjà en place nous avons pu voir comment récupérer les éléments basiques d'un fichier json et les traiter. Cependant le fait que nous aillons choisi d'avoir mis des objets json pour chaque bouton cela impliqué une nouvelle méthode de lecture car celle utilisée dans ce projet ne fonctionner pas. Nous avons remarqué que lors de l'appel à la fonction affichage, l'affichage se fait correctement mais lorsque nous essayons de manipuler les objets le compilateur nous produit des erreurs. Nous avons donc choisi de reproduire une partie de la fonction d'affichage, la partie cast nous permettant alors de manipuler les sous objets comme des strings pour pouvoir enfin récupérer les données qui nous ont été envoyées. Nous envoyons ensuite la configuration des boutons dans le casque. Afin que tous cela fonctionne nous avons dû modifier le fichier de configuration qui est l'une des pièces maîtresse du projet. Nous avons fait en sorte que cette fonction ne soit pas la prioritaire et que la fonction prioritaire soit la manette NES totalement prédéfinie.
La modification du fichier de configuration n'a été faite qu'une fois le test fini c'est à dire que pour tester notre script nous l'avons rendu complétement indépendant avec une mise à jour lors d'une entrée clavier sur un fichier qui était directement stocké sur l'ordinateur. Le fait de travailler sous Unity avec un matériel de ce type et une dépendance à la Raspberry nous a obligé à nous adapter et découvrir de nouvelles fonctionnalités du logiciel Unity. Ainsi, après avoir testé tout cela au fur et à mesure grâce à Unity, nous avons pu tester cela avec la configuration venant directement de la Raspberry. (Ne pas oublier d'enlever tous ce qui a permis de faire les tests sur le simulateur de Unity et ajouter les liaisons avec le fichier de configuration.)
Semaine 3
Le première fonctionnalité (suite)
La phase de test et de debug
Pour cette troisième semaine, nous reprenons notre travaille en cours, maintenant que les parties programmation web et programmation Unity pour les boutons sont fini nous pouvons tester le tout. Lors de ce test nous avons découvert un nouveau type de bug qui entraînait la fin de l'update du côté de l'oculus. Dans un projet Unity tous les scripts qui sont liés à un objet Unity possède une fonction Start qui se lance lors du lancement du projet et une fonction Update permettant la mise à jour des images, elle est donc appelée à chaque image formée par l'Oculus. Nous faisons tourner l'Oculus sur une base de 60 images par seconde ce qui produit un appelle de la fonction Update 120 fois par seconde car l'oculus fonctionne avec deux écrans, un par oeil.
Ainsi lorsque le programme s'arrête pendant l'update, le jeu continuera de fonctionner et ignorera seulement la fin de la fonction Update et la rappellera au prochain rafraîchissement. De plus lorsque nous transférons le projet Unity sur le casque de réalité virtuel celui-ci devient indépendant donc nous n'avons aucune façon conventionnelle de trouver les erreurs. Nous avons alors dû créer un objet dans la scène unity affichant des messages, pour aller avec cela nous avons dû créer des logs afin de trouver pourquoi l'Update ne se faisait pas.
Nous avons enfin découvert notre problème, notre lecture de message et donc de fichier json était fait sans aucune protection. C'est à dire qu'au moindre problème de lecture de message, de formation de message ou de formation du fichier de configuration json. Pour que notre fonction Update fonctionne ce message devait être parfait et toutes les options devait être renseigné sinon la lecture du json formé à partir du message provoqué une exception. Nous avons ainsi ajouté des protections pour toutes les lectures d'options implémenté et utilisé. Pour cela nous avons dû choisir une configuration par défaut, la configuration choisie est la configuration simple d'une manette formé sans aucune modification. Pour cela nous avons tout d'abord initialiser toutes les options afin de répondre à la configuration par défaut. Ensuite pour chaque option nous vérifions que l'option est présente dans le json avant d'essayer de la lire. Une fois lu nous mettons la priorité sur la configuration par défaut en cas de conflit entre les différentes options. Nous faisons la même chose pour la lecture des boutons si l'option padLibre est activé, tous les boutons ont une place prédéfinie qu'ils prennent en cas de manque d'information ou de problème de lecture.
Structure finale des scripts
Finalement pour cette fonctionnalité nous avons créé un seul script permettant de gérer le nouvel objet que nous avons créé. Ce script reste en relation avec les scripts principaux du projet, comme le script TCPConfig que nous avons modifié pour qu'il puisse continuer de fonctionner en cas d'erreur dans le message envoyé mais aussi les scripts de collision, de vibration et de commande. Ainsi notre script nommé ButtonChange comporte les deux fonctions de base qui sont start() et update avec start qui est lancé au lancement du programme et update qui se lance à chaque frame. Ces deux fonctions sont utilisés uniquement lors des tests dans le simulateur d'Unity. Nous avons ensuite les deux fonctions suivantes : ChangeButtonPosition et getKey. La fonction getKey permet d’interagir avec les boutons et donc envoyer les bonnes instructions au jeu. Nous arrivons enfin à notre fonction ChangeButtonPosition qui prend en entré le dictionnaire fait directement à partir du json reçu en message. Cette fonction commence par placer notre support invisible afin de pouvoir mieux se repérer dans l'espace par la suite. Elle continue par vérifier que l'emplacement de tous les boutons sont bien présent dans la configuration en créant deux liste, une pour les boutons présents et une seconde pour les boutons manquants. Une fois cela fait nous arrivons dans le coeur de la fonction, nous commençons avec une boucle sur tous les boutons que l'objet possède, nous vérifions qu'ils sont bien présents dans le json lu. Si ils sont présents nous les activons puis nous récupérons le sous-fichier json grâce à la méthode d'affichage expliqué plus haut. Nous continuons par vérifier puis récupérer leur paramètres avant de les assigner aux boutons. Les paramètres absents sont remplacés par les paramètres par défaut stockés dans le tableau base_pos. Si ils ne sont pas présents dans le fichier de configuration nous les configurons directement par rapport au tableau base_pos.
Partie web
On a considérée la zone de placement des touches avec les distances ci-contre:
- Axe x: -1.8 à 1.8m
- Axe y: -0.8 à 0.8m
- Axe z: -0.9 à 0.9m
On a obtient ainsi, par exemple, la conversion pour l'axe x ci-dessous:
((coordonnees.x-(app.screen.width/2))/(app.screen.width/2))*1.8
De cette manière, le Quest reçoit des coordonnéees adéquates, et peut positionné les touches sans problème. Maintenant que la première fenêtre à bien été mise en place, la seconde est réalisée de la même manière, seulement cette fois, les touches peuvent seulement être translatées sur l'axe y, et pas dans le plan commme sur la première fenêtre. On obtient ainsi les fenêtres suivantes:
On a également ajouté une checkbox similaire aux autres checkbox, pour activer/désactiver le placement libre. On avait un problème pour manipuler les fenêtres sur la page html, on a donc du créer un style CSS #frame et y lier les fenêtres, et ainsi on peut facilement les manipuler dans les div html.
La seconde fonctionnalité
Cette seconde fonctionnalité, comme on a pu le voir précédemment, consiste à déplacer l'écran (l'éloigner et le rapprocher) afin de faire marcher le patient tout en gardant la manette NES matérialisé juste devant le joueur. Dans un premier temps nous devons en étudier la faisabilité et le coder si le temps nous le permet.
Faisabilité
Nous devons commencer par identifier les fonctions que ce script doit avoir avant de nous lancer.
Afin de créer cette seconde fonctionnalité, le script doit être capable de faire bouger l'écran de façon dynamique, c'est à dire que l'écran ne peut pas disparaître d'un point pour réapparaître 5m plus loin. Nous devons pouvoir gérer la vitesse de mouvement de l'écran et limiter ses déplacements selon l'envie du praticien et donc pouvoir rendre cela configurable par le praticien.
Du côté du pad (la manette NES dans le jeu) nous devons faire en sorte qu’elle suive le joueur tout au long de la partie pour ainsi pouvoir laisser le joueur libre de ses mouvements.
Pour commencer nous reprenons ce que l'on a pu voir précédemment pendant la découverte du code et le développement de la première fonctionnalité et nous arrivons sur des fonctions de positionnement et de rotation avec la fonction de positionnement utilisé pour positionner les boutons et la fonction de rotation qui est utilisé pour positionner les manettes dans la fonctionnalité qui permettait de choisir le nombre de manette NES nous voulons pour jouer. Cette fonctionnalité était déjà présente à notre arrivée, celui-ci nous a beaucoup aidé dans la manipulation d'objet et dans la découverte des fonctions de base dédié aux objets Unity et à la communication entre les différents objets et script.
Ainsi avec cela nous avons pu voir que cette seconde fonctionnalité était possible mais vraiment peu pratique pour faire bouger l'écran. En ce qui concernés le fait de fixer la manette devant le joueur, la fonction pour déplacer l'objet est parfaite pour nous permettre de garder la manette devant nous, car nous ne savons pas comment le joueur va bouger, s’il va reculer ou tourner la tête ou se baisser. Elle était parfaite à condition de pouvoir avoir la position du casque à chaque frame et ainsi modifier la position de la manette à chaque frame.
Nous continuons alors par chercher comment trouver la position du joueur, en réalité dans un projet Unity pour les casques de réalité virtuelle ne matérialise pas le corps de la personne qui joue et donc n'a aucun repère sur la position de son corps. Cependant nous avons un objet caméra qui représente le casque dans la scène et donc la tête du joueur. Nous avons donc besoin de récupérer les positions de cet objet Unity et de les utiliser afin de placer le pad. Après des recherches sur la documentation Unity et le forum Unity nous avons pu trouver la fonction Camera.main.transform nous donnant l'accès à l'objet camera. Nous pouvons donc avoir accès à toutes ses variables de position comme si c'était un objet standard de la scène. Cependant nous ne pouvons pas modifier sa position ou faire toute action de ce genre, pour bouger la vision nous devons créer un objet secondaire caméra le configurer avant de l'assigner en caméra principale et ensuite l'assigner en caméra principale pour que le joueur change de point de vue. Ceci est souvent utilisé dans les jeux modernes mais ce n'est pas notre cible ici.
Maintenant que nous avons vu qu'il est possible de garder la manette fixée à la vision du joueur, les fonctions sont disponible il suffira d'un peu de mathématique pour la garder à la place souhaitée. Nous pouvons nous concentrer sur le déplacement de l'écran, c'est un objet comme les autres, nous pouvons donc le déplacer à chaque frame un tout petit peu et créer un déplacement rectiligne selon des options définie lors de la configuration. Cependant nous avons tout de même cherché sur leur documentation si il n'y avait pas quelque chose de plus facile et plus pratique à utiliser et nous avons trouvé la fonction Translate. Cette fonction permet de bouger un objet de façon rectiligne selon un vecteur défini.
Nous pouvons maintenant affirmer que la seconde fonctionnalité est faisable et pourrait être intégrée au projet sans grande difficulté.
Semaine 4
La seconde fonctionnalité (suite)
La semaine dernière nous avons pu voir que cette seconde fonctionnalité était faisable, il nous reste du temps donc nous allons pouvoir commencer son développement et l'implémenter dans le projet. Cette seconde fonctionnalité comporte deux parties, celle de la manette devant suivre le joueur et celle de l'écran devant bouger dans l'espace. Nous allons commencer par la partie la plus mathématique, le suivi de la manette.
Suivi du Pad
Pour cette première partie, nous voulons que le pad nous suive et reste devant nous, nous avons commencé en pensant que le pad devait rester face à l'écran donc nous n'avons pas fait attention aux rotations et avons fixé le pad afin qu'il reste face à l'écran. Pour cela nous avons dû commencer par récupérer la position de la caméra et simplement assigner à l'axe x la valeur de zéro afin que l'objet reste en face de la caméra, à l'axe y la valeur de -0.5 afin de garder l'objet 50cm en dessous de la caméra et ainsi avoir la manette un peu au-dessus du bassin et pour l'axe z nous avons assigné à l'objet la valeur de 0.4 qui est la distance entre le corps et le centre de l'objet pour ainsi avoir la manette devant ses yeux.
Ceci a pu être fait si facilement, car le repère utilisé est le repère de la caméra, ainsi il suffit juste de mettre à jour à chaque frame la position de l'objet afin que celui-ci bouge avec la caméra (la vision du joueur). Ceci était fait à chaque appelle de fonction Update et donc à chaque image créée pour le jeu(frame).
Ceci était bien mais trop simpliste et pas assez pratique, ce que souhaitait notre tuteur était une manette qui nous suive partout et soit toujours orienté vers le joueur. Nous avons donc continué le développement en prenant compte l'orientation du casque dans l'espace. Le défi était de toujours avoir la manette face à nous, conserver toujours la même distance et toujours l'avoir orienté vers nous.
Nous avons donc utilisé la trigonométrie en récupérant les différentes orientations de la caméra et en conservant la distance entre la position du joueur et la position de la manette. Une fois que cela a était fait nous nous sommes concentré sur la partie rotation de la manette à l'aide de la fonction rotate. Cette partie n'a pas été facile, à force de manipulation avec cette fonction, nous avons pu enfin trouver exactement comment elle fonctionnait, nous avons modifié notre code en conséquence. Pour arriver aux demandes de notre tuteur nous avons alors regardé à chaque rafraichissement d'image comment le casque avait bougé et nous bougeons la manette en conséquence, ainsi la manette est déplacée à chaque frame. Nous la voyons donc se déplacer en continue dans la vision du casque.
Mettre en mouvement l'écran
Pour cette seconde partie, nous n'avons pas fait très compliqué, nous avons créé une fonction de mouvement prenant en paramètre la vitesse et la direction que l'on voulait donner à l'écran et il nous suffit de l'appeler à chaque image. Pour éviter que l'écran ne se déplace à l'infinie nous avons ajouté une fonction de protection prenant en entré des limites ou prenant les limites par défaut. Nous nous sommes limité à des mouvements basiques de va-et-vient mais selon ce que le tuteur voudra faire dans le futur il suffira de modifier la direction afin de répondre aux différentes idées que l'équipe pourrait avoir par la suite. La fonction limite modifie déjà la direction afin de faire des aller-retour, mais nous pouvons imaginer une autre fonction ou la fonction de limite modifiant en continue la direction en fonction de certain paramètres et ainsi créer des mouvements plus complexes défini par une liste de vecteur.
Les tests
Maintenant que nous faisons bouger l'écran et que la manette nous suit nous pouvons faire les tests en condition réelle. Nous avons commencé par bien définir le guardian afin de ne pas risquer de se faire mal et nous avons lancé le programme. Nous avons très vite remarqué que la manette virtuelle était bien trop grande, quand on voulait atteindre les boutons sur les côtés on tournait la tête afin de les voir mais la manette partait avec donc très dur à manipuler.
Nous avons donc décidé de réduire le pad dans son entièreté lors de l'activation du suivi et le remettre à taille normale lors de la désactivation. Cela marchait très bien mais rendait les boutons plus petits, ce qui dérangeait notre tuteur. Nous avons alors trouvé une autre solution qui a était de déplacer les boutons vers le milieu de la manette en fonction de leur positionnement. C'est à dire que plus le bouton était sur le côté et plus il était ramené vers le centre et inversement, plus il était vers le centre moins il bougeait. Cette solution est très bonne, elle permet au joueur de cliquer sur les boutons sans bouger la tête et ainsi ne pas avoir de problèmes pour viser le bouton. Cependant nous avons laissé la première solution en commentaire afin de donner la possibilité à notre tuteur de changer de méthode ou de combiner les deux méthodes dans le futur. Sans oublier que cela pourra être utilisé par la suite dans de la nouvelle fonctionnalité ou juste si nous voulons ajouter la possibilité de changer la taille de la manette ou la taille des boutons.
Les tests étaient maintenant finis et tout marché très bien et nous avons pu voir que d'autres fonctionnalités étaient possibles et pouvairntt dériver de notre travail. Maintenant que les deux fonctionnalités sont faites nous pouvons passer à la deuxième partie du projet que l'on devait faire si l'on avait le temps.
Semaine 5
Maintenant que les deux fonctionnalités qui nous avaient été demandées de faire sont terminées nous attaquons la seconde partie du projet qui concerne une librairie créée par l'équipe MINT, la librairie Gina.
La librairie Gina
La librairie Gina a pour but de créer des événements à partir de mouvement, pour cela elle se découpe en trois blocs. Le premier bloc est le décodeur, il prend en entrée un flux d'information brut venant d'un matériel quelconque (une kinect, une sourie, le positionnement du doigt sur une tablette et d'autre) et le traduit en information utilisable par les autres blocs. Ce bloc permet d'uniformiser l'information afin de limiter le nombre de bloc suivant et surtout permettre l'utilisation de plusieurs sources d'entrées en même temps.
Le second bloc est un filtre, il va permettre de filtrer les mouvements entrant afin de supprimer les tremblements et autre défaut que peut avoir l'information afin d'avoir des mouvements les plus propres et les plus facilement exploitables possible.
Le troisième et dernier bloc permet de créer des évènements à partir des informations qu'il reçoit et ainsi interagir avec le système.
Cette librairie est très complète et très pratique, cependant pour qu'un objet soit utilisable par la librairie, elle doit posséder le premier bloc permettant de convertir l'information brute de l'objet en information exploitable par les autres blocs de la librairie. Malheureusement le bloc pour android n'était pas implémenté. Cette librairie est donc inutilisable pour notre projet sans la modifier. Sous les conseils du développeur de la librairie et de notre tuteur nous n'allons pas créer le bloc de traduction et nous allons donc utiliser les fonctions proposer directement par Oculus et Unity pour arriver à jouer juste à partir des mouvements de la manette de l'Oculus et donc supprimer la manette virtuelle.
Contrôle par mouvement
Comme la librairie Gina est trop difficilement utilisable et déconseillé vu que nous connaissons déjà Unity et ses différentes interactions avec le casque de réalité virtuel. Nous nous lançons alors dans la recherche d'algorithme pouvant répondre à de la reconnaissance de paterne et à la recherche des différentes fonctions intéressantes. Nous commençons par regarder qu'elle structure pourrait avoir notre code. Nous avons choisi d'utiliser la même structure que la librairie Gina, une fonction qui détecte et récupère les mouvements, une fonction qui traite les mouvements et une fonction qui traduit le mouvement en évènement afin d'envoyer les informations à la Raspberry (le jeu).
Récupération des données
Pour la fonction qui récupère les mouvements nous avons commencé par chercher quand et comment récupérer les mouvements afin que cela soit le plus rapide possible et le plus efficace possible. Afin d'éviter la surcharge du flux de donner et surtout pour éviter que des mouvements non souhaités soit pris en compte par le jeu. Nous avons décidé de prendre en compte les mouvements des manettes uniquement quand le joueur appui sur la gâchette. Nous avons donc dû chercher les différentes méthodes permettant de récupérer la position des manettes et l'état des boutons de la manette, en particulier l'état de la gâchette. Nous avons donc les méthodes suivantes :
- Input.GetAxis("Oculus_CrossPlatform_PrimaryIndexTrigger") nous donnant la puissance de pression sur la gâchette gauche.
- Input.GetAxis("Oculus_CrossPlatform_SecondaryIndexTrigger") nous donnant la puissance de pression sur la gâchette droite.
- OVRInput.GetLocalControllerPosition(OVRInput.Controller.LTouch) nous donnant la position de la manette gauche.
- OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch) nous donnant la position de la manette droite.
Nous avons maintenant les fonctions principales nous permettant de gérer la récupération d'informations, nous stockons alors tout ça dans une liste dont nous limitons la capacité à 60*2*2, 60 pour le nombre de rafraichissement par seconde, 2 car il y a deux écrans et le second 2 car on veut conserver l'action des deux secondes précédentes au maximum deux. Une fois que la position a était enregistré nous passons la liste à travers le filtre 1€. Ce filtre est un filtre développé par l'INRIA en collaboration avec l'université de Waterloo et amélioré au fil du temps afin de le rendre accessible dans le plus grand nombre de langage de programmation.
Traitement des données
Après avoir créé les listes et les avoir filtrés nous passons au traitement de l'information. Pour cette partie nous devons récupérer quel mouvement est fait par l'utilisateur mais aussi délimiter les mouvements et ne pas confondre une pose d'enregistrement avec un mouvement. Pour la première problématique, récupérer le mouvement fait nous avons simplement créé 6 variables (2 par axes) afin d'avoir la distance parcourue selon les trois axes et leurs directions.
Pour la seconde problématique de délimitation de mouvement nous avons tout d'abord choisi de regarder le déplacement entre les deux derniers points enregistré et considéré les points précédents comme faisant partie d'un seul et même mouvement à condition que les valeurs ne changent pas trop. Cependant ceci était beaucoup trop imprécis, le changement de vitesse de déplacement pouvait induire en erreur et le déplacement ne s’adaptait pas, c'est à dire que le corps humain arrive très peu à faire des mouvements rectilignes sans faire attention rendant alors très difficile la manipulation. Nous avons alors pensé à une autre méthode consistant à normaliser les mouvements et ainsi traiter des vecteurs et non des distances que l'on fait évoluer avec le temps. Ceci donne alors plus de sens à la limite changement, nous pouvons alors parler de pourcentage de similarité ou de changement et ainsi choisir la précision que nous voulons. Afin d'optimiser cette différentiation de mouvement, nous avons d'abord une comparaison entre le vecteur de référence et le vecteur étudié nous donnant alors un pourcentage de changement. Si ce changement est trop grand nous arrêtons la détection et passons à l'interprétation des données trouvées. Sinon nous modifions notre vecteur de base en faisant la moyenne des vecteurs du mouvement et nous ajoutons les valeurs du dernier déplacement aux 6 valeurs de référence des axes.
La troisième problématique est venue une fois que tout est codé, pendant la partie il suffisait de finir un mouvement avec le bras tendu et commencer un autre mouvement avec le bras plié pour que le jeu en déduise une action et envoie à la Raspberry l'action. Ceci entrainait plusieurs bugs du style lorsqu’un mouvement vers la gauche était fait, le mouvement suivant envoyait au minimum un mouvement vers la droite entraînant ainsi pour le jeu Tetris l'impossibilité de mettre les pièces collées au mur. Donc pour cette troisième problématique nous avons pensé à plusieurs solutions, la première serait de nettoyer les listes et ainsi reprendre comme si on avait jamais appuyé sur les gâchettes mais ceci nous oblige à avoir deux variables globales de plus pour savoir quand nettoyer les listes et ne pas les nettoyer en boucle lorsque les gâchettes ne sont pas appuyée. De plus si le matériel est en fin de vie ou si la gâchette ou l'envoie d'information bug pendant une frame, on ne veut pas perdre toutes les informations récoltées avant la perte de signal. Ainsi on arrive à la seconde solution qui est d'instaurer un déplacement maximal entre deux frames. Nous partons du principe que nous pouvons déplacer la manette à très grande vitesse et que nous avons 120 frames par seconde (soit 120 prise de mesures par seconde), nous pouvons alors établir un seuil de modification au-delà duquel le logiciel considérera le mouvement comme un nouveau mouvement. Nous avons défini cette limite de changement à 0.05 soit 5cm ce qui correspond à une vitesse supérieure à 6 mètres par seconde ou 3 mètres par seconde si on a perdu une mesure. Cette vitesse est facilement modifiable mais impossible à dépasser pendant le jeu. Nous avons ainsi supprimé le problème ce qui évite tout retour en arrière et augmente la précision de la détection.
Interprétation des données
Maintenant que l'on a nos 6 variables indiquant tous les déplacements du mouvement nous pouvons les interpréter et ainsi envoyer les commandes correspondantes au jeu (à la Raspberry). Nous avons alors les données du mouvement en cours assez détaillé, à partir de ceci nous pouvons détecter tous les mouvements de type rectiligne. Ainsi nous avons créé une fonction prenant en comptes les 6 variables et vérifie qu'il y a eu assez de mouvement sur la variable passer dans la première entrée et que le reste des variables n'est pas trop élevé. Cela signifie que nous voulons un mouvement dans la direction désignée par la première variable sans qu'il y ai eu trop de perturbation dans les autres directions. Nous avons laissé la possibilité de modifier facilement les limites et d'ajouter des fonctions d'interprétation.
Les tests
Tous ceci est le résultat de beaucoup de test nous ayant permis de régler les variables de changement afin de plus facilement délimiter les mouvements, régler les limites d'interprétation afin d'éviter de faire des gestes trop amples. De plus ces tests nous on permit de nous rendre compte du troisième problème du traitement des données. Au début nous avons pas pensé à ajouter un filtre mais avec les tests et le fait que les utilisateurs seront des personnes en rééducation donc des personnes n'ayant pas pleinement la possibilité de faire des gestes droits, l'utilisation d'un filtre était obligatoire. Cependant l'utilisation de ce filtre entraine un lissage des positions et donc rend notre protection contre les sauts c'est à dire entre la fin d'un mouvement et le début du suivant, lorsqu'on lâche la gâchette de la manette à un certain endroit et que nous bougeons la manette pour commencer un autre mouvement.
La troisième problématique est de nouveau d'actualité, nous avons donc étudié plus en profondeur le code du filtre 1€ développé par cristal afin de trouver une solution pour régler ce problème et ainsi réinitialiser le filtre lorsque le mouvement est trop grand. Cependant après plusieurs tests nous ne sommes pas arrivés à résoudre ce problème de cette façon. De plus le temps nous manque et le filtre ne comporte pas de fonction de destruction, nous supposons alors que le garbage collector fonctionne correctement sur le filtre. Avec cette supposition nous créons alors un nouveau filtre dès que la gâchette est relâchée mais ceci pourrait créer une perte de mémoire donc pas optimale, ceci aura besoin d'être modifié par la suite.
Semaine 6
Maintenant que les deux fonctionnalités sont terminées et que nous avons fini le contrôle par le mouvement des manettes nous avons effectué beaucoup de test et de combinaison afin de chercher les bugs qui pourrait survenir. On a réglé les différentes fonctionnalités afin qu'elles s'exécutent proprement et que l'on puisse changer de fonctionnalité sans bug et sans devoir relancer l'application.
Le premier était qu'il nous était impossible d'accéder à la seconde fonctionnalité ou le lancement de celui-ci était très approximatif et l'écran passait derrière le mur. Nous avons donc réussi à régler le problème qui venait de la configuration qui oublié d'activer le gamepad utilisé dans cette fonctionnalité et désactivait le mur. Une fois cela fait nous avons remarqué que nous ne pouvions plus sortir de cette fonctionnalité. Nous avons fini par remarquer une erreur dans notre configuration une nouvelle fois, cette fois-ci la partie de désactivation n'était pas atteinte à cause d'un certain enchaînement de commande. Nous avons donc réglé cela ainsi que la fonction de fin de suivi dans laquelle nous avons oublié de rétablir les positions initiales des boutons.
Suite à tous ces tests nous sommes satisfaits de notre travail et de son bon fonctionnement. La plupart de notre travaille a été fait dans des nouveaux scripts et nouveaux fichiers afin de ne pas gêner le projet et afin de rendre notre travail le plus facilement réutilisable par la suite.
Sur demande de notre tuteur nous arrêtons de développer et d'améliorer ce que l'on a fait pour combiner notre travaille avec le travaille qu'il a effectué sur le projet pendant ces 6 semaines. Cependant avec le développement nous avons pu envisager plusieurs nouvelles fonctionnalités à implémenter pour le futur du projet VR4REHAB.
Améliorations potentielles
Pour la première fonctionnalité, nous pouvons l'améliorer en ajoutant une option permettant de choisir la taille de chaque bouton en plus de leur position. De plus un script que nous n'avons pas utilisé durant notre PFE est déjà présent dans le projet. Cette option pourrait être ajoutée par la suite selon les envies du client ou de notre tuteur. Cependant ceci pourrait rendre la configuration trop lourde pour un médecin et donc faire perdre trop de temps, il faudrait donc voir avec eux.
Pour la seconde fonctionnalité, nous pouvons créer une fonction permettant de faire bouger l'écran selon un paterne prédéfini ou configuré par le médecin avec la manette. Ce qui pourrait être imaginé c'est que le médecin prenne une des manettes de l'oculus pour définir le déplacement que l'écran devra faire en appuyant sur un bouton. Le script aurait à transformer les positions en une liste de vecteur puis faire bouger l'écran soit en faisant des aller-retours ou des boucles selon si le déplacement fait par le médecin commence en un point et fini au même point ou pas. Ainsi pour cela il faudrait une fonction pour traduire les positions en une liste de vecteurs, ceci peut s'appuyer sur la normalisation et le filtre utilisé dans le script de détection de mouvement. Il faudrait ensuite une fonction prenant une liste de vecteur et une vitesse afin de déplacer l'écran, ceci peut s'appuyer sur la fonction permettant déjà de bouger l'écran avec un flag permettant de dire si on fait des aller-retours ou des boucles et une variable globale permettant de se déplacer dans la liste.
De plus nous ne savons pas si cela est possible mais pour la sécurité du patient et pouvoir permettre au médecin de lancer le patient dans son exercice et pouvoir partir voir d'autre patient nous pouvons imaginer un système de détection des obstacles et les modéliser, mais nous ne savons pas combien de temps ça pourrait prendre ou si c'est faisable.
Pour la gestion des mouvements, il y a le problème du filtre à régler pour commencer. Ensuite notre code laisse la possibilité de différencier les deux manettes grâce à une variable nommée main ce qui pourrait mener à quelques optimisations ou améliorations selon les envies du tuteur ou les demandes des médecins. De plus nous pouvons imaginer de nouvelles détections de mouvement plus complexe que de simple mouvement rectiligne que nous avons pu implémenter la semaine dernière. Pour cela nous pouvons imaginer une fonction connaissant tous les mouvements implémenter, prenant le mouvement en cours du joueur et éliminant les différentes possibilités de correspondance jusqu'à ce que la correspondance soit parfaite avec un des mouvements implémenté. Ceci permettrait d'éliminer le problème de ne pas avoir assez de mouvement implémenter pour y faire correspondre les 8 boutons de la manette NES nécessaire pour jouer. Une autre amélioration est possible avec la détection de mouvement, c'est le fait d'appuyer sur un bouton afin qu'une fois le mouvement détecter le jeu considère que l'on reste appuyer sur le bouton permettant ainsi de maintenir un bouton enfoncer. Ce qui est impossible à faire sur plus de quelques secondes avec la détection des mouvements normale car lorsque nous arrêtons de bouger notre main avec la manette nous tremblons, le joueur ne sait pas garder sa main stable, le script considère alors le début d'un nouveau mouvement et envoie la commande de fin de mouvement. De plus la détection peut créer une erreur infime mais suffisante pour que le vecteur créé ne corresponde pas au mouvement précédent. Ainsi le script considère que nous avons commencé un nouveau mouvement et enverra la commande fin de mouvement.