IMA5 2019/2020 P18 : Différence entre versions
(→Choix techniques : matériel et logiciel) |
|||
(59 révisions intermédiaires par 2 utilisateurs non affichées) | |||
Ligne 7 : | Ligne 7 : | ||
Encadrant : Laurent Grisoni & Valentin Beauchamp | Encadrant : Laurent Grisoni & Valentin Beauchamp | ||
==Description== | ==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 | + | 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 atteintes d’un handicap ou d’une incapacité physique. 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étro, la Raspberry envoie ensuite le flux vidéo du jeu par wifi au casque de réalité virtuelle Oculus Quest. |
==Objectifs== | ==Objectifs== | ||
Ligne 32 : | Ligne 32 : | ||
**Implémenter une fonction pour déplacer toutes les touches séparément de la manette de base | **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 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 | + | **Implémenter une fonction pour effectuer des actions avec des mouvements, donc sans manette virtuelle |
==Choix techniques : matériel et logiciel== | ==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. La Raspberry Pi a été choisie car moins intrusive comparé à un ordinateur, et bien moins cher. | + | 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 | + | |
+ | Ce projet est réalisé à l'aide de Unity (scripts en C#) pour programmer 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== | ==Calendrier prévisionnel== | ||
Ligne 43 : | Ligne 44 : | ||
==Semaine 1== | ==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. | |
[[File:OculusQuest.jpg|thumb|400px|Casque Oculus Quest]] | [[File:OculusQuest.jpg|thumb|400px|Casque Oculus Quest]] | ||
− | Nous | + | Nous nous sommes familiarisés avec le Quest, son interface et son fonctionnement. |
+ | |||
*Télécharger le projet à partir du git | *Télécharger le projet à partir du git | ||
− | *Suivre les instructions des read.me | + | *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 | + | 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 ...) | |
− | *Contrôler le jeu à l'aide de | + | *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. |
− | * | + | <div><ul> |
+ | <li style="display: inline-block;">[[File:ScreenUnityfbrassar.png|600px|thumb|Interface Unity]]</li> | ||
+ | <li style="display: inline-block;">[[File:Scene quest.png|400px|thumb|Aperçu de l'espace virtuel dans le Quest]]</li> | ||
+ | </ul></div> | ||
==Semaine 2== | ==Semaine 2== | ||
− | Prise en main du projet avec un apprentissage des langages javascript | + | 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=== | ===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 '''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 | + | *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 '''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 | + | *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. |
[[File:Gamepad.PNG|thumb|200px|Manette NES utilisé]] | [[File:Gamepad.PNG|thumb|200px|Manette NES utilisé]] | ||
− | *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 '''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 | + | *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 | + | *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.<br><br> |
===Les scripts utilisés=== | ===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 | + | *'''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 | + | *'''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 | + | *'''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. | + | *'''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. | + | *'''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. | + | *'''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. | + | *'''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 | + | *'''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. | + | *'''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 | + | *'''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. | + | *'''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. | + | *'''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. | + | *'''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 | + | *'''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. | + | *'''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. | + | *'''VibrationManager''' permet de faire vibrer les manettes lorsque la fonction est appelée. |
[[File:Gamepad.gif|center|thumb|600px|Configuration initiale: Gamepad activé]] | [[File:Gamepad.gif|center|thumb|600px|Configuration initiale: Gamepad activé]] | ||
===Brainstorming=== | ===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é assignés à 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é [https://pixijs.io/examples/#/interaction/dragging.js Dragging] fourni sur le site de [https://www.pixijs.com/ 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 <code>npm install</code>, 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: | |
− | + | [[Fichier:Config page initiale.png|center|600px|thumb|Page web initiale]] | |
− | + | 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: | |
+ | <nowiki>{ | ||
+ | "gamepadOn": false, | ||
+ | "oculusControllerOn": false | ||
+ | }</nowiki> | ||
− | ====Partie Unity==== | + | Maintenant, la fonction d'envoi de configuration crée le json ci-dessous: |
− | 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 | + | <nowiki>{ |
− | [[File:GamepadLibre.PNG|thumb|200px|PadLibre créé pour | + | "ButtonA":{"x":---,"y":1,"z":---}, |
− | 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 | + | "ButtonB":{"x":---,"y":1,"z":---}, |
− | Afin que | + | "ButtonUp":{"x":---,"y":1,"z":---}, |
− | La modification du fichier de configuration n'a | + | "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 | ||
+ | }</nowiki> | ||
+ | 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 étaient enregistrées sont relatives aux pixels de la page web, et ne sont pas adaptées 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. | ||
+ | |||
+ | [[File:GamepadLibre.PNG|thumb|200px|PadLibre créé pour cette fonctionnalité]] | ||
+ | |||
+ | 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 tout 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== | ==Semaine 3== | ||
− | ===Le | + | ===Le première fonctionnalité (suite)=== |
====La phase de test et de debug==== | ====La phase de test et de debug==== | ||
Ligne 128 : | Ligne 163 : | ||
====Structure finale des scripts==== | ====Structure finale des scripts==== | ||
− | Finalement pour | + | 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''. | 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''. | ||
[[File:Placement libre.gif|center|thumb|600px|Configuration: Placement libre activé]] | [[File:Placement libre.gif|center|thumb|600px|Configuration: Placement libre activé]] | ||
− | === | + | ====Partie web==== |
− | + | On a considéré 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 obtient ainsi, par exemple, la conversion pour l'axe x ci-dessous: | ||
+ | |||
+ | <code>((coordonnees.x-(app.screen.width/2))/(app.screen.width/2))*1.8</code> (avec app.screen.width = longueur de la fenêtre) | ||
+ | |||
+ | De cette manière, le Quest reçoit des coordonnéees adéquates, et peut positionner 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: | ||
+ | |||
+ | [[Fichier:Fenetre pageweb.png|center|600px|thumb|Fenêtres de placement libre des boutons]] | ||
+ | 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 dû 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 l'utilisateur tout en gardant la manette NES matérialisée juste devant lui. Dans un premier temps, nous devons en étudier la faisabilité et le coder si le temps nous le permet. | ||
+ | |||
====Faisabilité==== | ====Faisabilité==== | ||
+ | |||
Nous devons commencer par identifier les fonctions que ce script doit avoir avant de nous lancer. | Nous devons commencer par identifier les fonctions que ce script doit avoir avant de nous lancer. | ||
− | Afin de créer | + | 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. | + | 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 | + | |
− | Ainsi | + | 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ée pour positionner les boutons et la fonction de rotation qui est utilisée pour positionner les manettes dans l'option qui permettait de choisir le nombre de manettes NES affichées. Cette option était déjà présente à notre arrivée, elle nous a beaucoup aidé dans la manipulation d'objet et dans la découverte des fonctions de bases dédiées aux objets Unity et à la communication entre les différents objets et script. |
− | + | ||
− | + | Ainsi, nous avons pu voir que cette seconde fonctionnalité était réalisable mais vraiment peu pratique pour déplacer l'écran. En ce qui concerne le fait de fixer la manette devant le joueur, la fonction pour déplacer l'objet est idéale, 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 pouvons maintenant affirmer que | + | Nous continuons alors par chercher comment trouver la position du joueur, en réalité dans un projet Unity, les casques de réalité virtuelle ne matérialisent pas le corps de la personne qui joue et donc n'ont 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 Gamepad. 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 quelconques actions 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 et que les fonctions sont disponibles, il suffira d'un peu de mathématiques 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éfinies 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 translater un objet de façon rectiligne selon un vecteur pré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== | ==Semaine 4== | ||
− | === | + | |
− | La semaine dernière nous avons pu voir que | + | ===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==== | ====Suivi du Pad==== | ||
− | |||
− | |||
− | Ceci était | + | Pour cette première partie, nous souhaitons que le Gamepad 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. |
− | 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 | + | Cela a pu être réalisé simplement, 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 qu'il bouge avec la caméra (la vision du joueur). Ceci était effectué à chaque appel de fonction Update et donc à chaque image créée pour le jeu(frame). |
+ | |||
+ | Cette méthode était fonctionnelle 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ée 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 été 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 répondre aux demandes de notre tuteur, nous avons alors regardé à chaque rafraichissement d'image comment le casque avait bougé et nous bougions 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==== | ====Mettre en mouvement l'écran==== | ||
− | Pour cette seconde partie | + | |
+ | Pour cette seconde partie, 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'infini, nous avons ajouté une fonction de protection prenant en entrée 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 modifie en continue la direction selon certains paramètres et ainsi créer des mouvements plus complexes définis par une liste de vecteurs. | ||
====Les tests==== | ====Les tests==== | ||
− | |||
− | |||
− | Les tests étaient maintenant | + | 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 en rentrant dans des objets 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 suivait aussi ce mouvement donc c'était très embêtant d'appuyer les boutons à l'extrémité. |
+ | |||
+ | 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 fonctionnait très bien mais rendait les boutons plus petits, ce qui dérangeait notre tuteur. Nous avons alors trouvé une autre solution qui a été 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 idéale, 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 si nous voulons ajouter la possibilité de changer la taille de la manette ou la taille des boutons. | ||
+ | |||
+ | Les tests étaient maintenant terminés et tout fonctionné correctement, nous avons pu nous apercevoir que d'autres fonctionnalités étaient possibles et pouvaient dériver de notre travail. Maintenant que les deux fonctionnalités sont faites nous pouvons passer à la dernière partie du projet que l'on devait faire si on avait le temps. | ||
[[File:Suivi.gif|center|thumb|600px|Configuration: Suivi activé]] | [[File:Suivi.gif|center|thumb|600px|Configuration: Suivi activé]] | ||
+ | |||
+ | ====Partie web==== | ||
+ | |||
+ | L'activation du mode suivi nécessitée juste une checkbox comme pour l'option ''nombre gamepad'', on a donc juste ajoutée une checkbox '''Suivi ON''' pour activer/désactiver la fonctionnalité. On avait cependant un problème quant au redimensionnement des fenêtres de placement libre, lorsque la fenêtre du navigateur web ou sur téléphone été redimensionnée, la taille des canvas ne s'ajustait pas, et les éléments de la page se retrouvaient en-dessous et cachés par les canvas. | ||
+ | |||
+ | Pour résoudre ce problème, on a mis en place une fonction '''resize()''' qui est appelée une fois quand la page est chargée, puis dès que le script reconnaît l'événement de redimensionnement de la page web avec la commande <code>window.addEventListener('resize', resize)</code>. Ainsi, dès qu'un redimensionnement de la page est reconnu, les canvas sont rétrécis/agrandis, ainsi que tous leurs objets. | ||
==Semaine 5== | ==Semaine 5== | ||
− | Maintenant que les deux | + | 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=== | ||
− | 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 | + | 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 dispositif quelconque (une kinect, une souris, le positionnement du doigt sur une tablette etc...) 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 | + | |
− | 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. | + | 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 possibles. |
+ | |||
+ | 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 | + | Cette librairie est très complète et 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é, et le casque fonctionne sous Android. De plus, son principal avantage est l'utilisation "cross-plateforme", c'est à dire l'utilisation de manettes de différents constructeurs pour interagir avec un seul programme en simultané. Cette librairie est donc inutilisable pour notre projet sans la modifier, de plus notre projet n'utilise que l'Oculus. 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=== | ===Contrôle par mouvement=== | ||
− | Comme la librairie Gina est | + | Comme la librairie Gina est 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 motif et des différentes fonctions intéressantes. |
− | Nous commençons par regarder | + | Nous commençons par regarder quelle 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==== | ====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 | + | 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 et efficace possible. Afin d'éviter la surcharge du flux de données 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 appuie sur la gâchette du contrôlleur Oculus. Nous avons donc dû chercher les différentes méthodes permettant de récupérer la position des contrôlleurs et l'état des boutons du contrôlleur, 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_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. | *''Input.GetAxis("Oculus_CrossPlatform_SecondaryIndexTrigger")'' nous donnant la puissance de pression sur la gâchette droite. | ||
Ligne 186 : | Ligne 258 : | ||
*''OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch)'' nous donnant la position de la manette droite. | *''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 | + | 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 rafraichissements 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 été enregistrée, nous passons la liste à travers le filtre 1€. Ce filtre est développé par l'INRIA en collaboration avec l'université de Waterloo et a été amélioré au fil du temps afin de le rendre accessible dans le plus grand nombre de langage de programmation. |
====Traitement des données==== | ====Traitement des données==== | ||
− | Après avoir créé les listes et les avoir | + | Après avoir créé les listes et les avoir filtrées, 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 un arrêt d'enregistrement (soulagement de la gâchette) avec un mouvement. Pour la première problématique, ''récupérer le mouvement réalisé'', 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 | + | 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és et considérer 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 a du mal à faire des mouvements rectilignes sans être concentré, 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 apparue une fois que tout était 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 l'envoie à la Raspberry. Ceci entrainait plusieurs bugs, par exemple, 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ées. De plus, si le matériel est en fin de vie ou si la gâchette ou l'envoi 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 une mesure est perdue. 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==== | ====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 | + | 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ées, à partir de cela 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 passée 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étations. |
====Les tests==== | ====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' | + | 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étations 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 de traitement des données. Au début, nous n'avions 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 nécessaire. |
− | 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 | + | |
+ | 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 avons créé 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. | ||
[[File:Mouvement libre.gif|center|thumb|600px|Configuration: Mouvement libre activé]] | [[File:Mouvement libre.gif|center|thumb|600px|Configuration: Mouvement libre activé]] | ||
+ | |||
+ | ====Partie web==== | ||
+ | De même que pour l'option de suivi, il fallait juste ajouter une checkbox pour activer/désactiver l'option ''Mouvement Libre''. On a aussi ajouté un moyen d'enregistrer des configurations de placement libre des touches, en leurs attribuant des ID, comme ça l'utilisateur peut rapidement charger le placement des touches qu'il souhaite. Les ID sont stockés dans un fichier JSON '''layoutIDS.json''' qu'on peut facilement manipuler, et est appelé à chaque chargement de page pour pouvoir incrémenter un menu déroulant de sélection des IDs. Ce menu est également incrémenté dès qu'un nouvel ID est créé. Si l'utilisateur essaie de créer un ID avec un nom déjà existant, une alert s'affiche et indique que l'ID est déjà existant. Il est également impossible d'enregistrer un ID sans nom. La liste des IDs peut être réinitialisée à tout moment avec le bouton ''Reset layouts IDs list''. | ||
+ | |||
+ | On a décidé de peaufiner la page pour rendre l'interface utilisateur plus pertinente, notamment en chargeant la dernière configuration envoyée au casque dès l'ouverture de la page, pour que l'utilisateur sache la configuration actuellement dans le casque, c'est à dire que les checkbox seront cochées ou non, et le dernier layout ID affiché sera le dernier envoyé. On a aussi fait en sorte de cacher la partie ''placement libre'' lorsque la checkbox est décochée, pour être moins intrusive et rendre l'interface plus agréable. | ||
+ | |||
+ | [[File:Conf page finale.png|center|thumb|600px|Aperçu page web finale (les canvas sont en dessous du bouton Reset Layout)]] | ||
==Semaine 6== | ==Semaine 6== | ||
− | Maintenant que les deux | + | 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 tests et de combinaisons afin de chercher les bugs qui pourraient 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. |
− | |||
− | + | Un bug rendait impossible l'accés à la seconde fonctionnalité ou son lancement était approximatif et l'écran passait derrière le mur. Ce problème venait de la configuration qui oubliait 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 où nous avons oublié de rétablir les positions initiales des boutons. | |
− | Sur demande de notre tuteur nous | + | Sur demande de notre tuteur, nous avons arrêté de développer le projet pour combiner notre travail avec celui réalisé par notre tuteur ainsi que François Brassart. 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 | + | 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 à cet effet dans le projet. Cette option pourrait être ajoutée par la suite selon les envies du client ou de notre tuteur. Cependant, cela 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 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 | + | Pour la seconde fonctionnalité, nous pouvons créer une fonction permettant de faire bouger l'écran selon un motif prédéfini ou configuré par le médecin avec la manette. Ce qui pourrait être imaginé c'est que le médecin prenne un des contrôlleur du Quest 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 vecteurs 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 il faudrait une fonction pour traduire les positions en une liste de vecteurs, pour cela on 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, ici on peut s'appuyer sur la fonction permettant déjà de bouger l'écran avec un flag permettant d'indiquer si on fait des aller-retours ou des boucles et une variable globale permettant de se déplacer dans la liste. |
− | 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 | + | |
+ | 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 besoins du client. De plus, nous pouvons imaginer de nouvelles détections de mouvement plus complexes que de simples mouvements rectilignes que nous avons pu implémenter la semaine dernière. Pour cela, nous pouvons imaginer une fonction connaissant tous les mouvements implémentés, 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 mouvement implémenté. Ceci permettrait d'éliminer le problème de ne pas avoir assez de mouvements implémentés pour y faire correspondre les 8 boutons de la manette NES nécessaires 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, en effet, 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. | ||
=Documents Rendus= | =Documents Rendus= | ||
+ | |||
+ | Rapport : [[Fichier:RapportPFE_P18_Fdinatale_Ibendhiab.pdf]] | ||
+ | |||
+ | Présentation : [[Fichier:PresentationPFE_P18_Fdinatale_Ibendhiab.pdf]] |
Version actuelle datée du 19 février 2020 à 16:48
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 atteintes d’un handicap ou d’une incapacité physique. 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étro, la Raspberry envoie ensuite le flux vidéo du jeu par wifi au casque de réalité virtuelle Oculus Quest.
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 programmer 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é assignés à 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 étaient enregistrées sont relatives aux pixels de la page web, et ne sont pas adaptées 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 tout 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é 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 obtient ainsi, par exemple, la conversion pour l'axe x ci-dessous:
((coordonnees.x-(app.screen.width/2))/(app.screen.width/2))*1.8
(avec app.screen.width = longueur de la fenêtre)
De cette manière, le Quest reçoit des coordonnéees adéquates, et peut positionner 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 dû 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 l'utilisateur tout en gardant la manette NES matérialisée juste devant lui. 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ée pour positionner les boutons et la fonction de rotation qui est utilisée pour positionner les manettes dans l'option qui permettait de choisir le nombre de manettes NES affichées. Cette option était déjà présente à notre arrivée, elle nous a beaucoup aidé dans la manipulation d'objet et dans la découverte des fonctions de bases dédiées aux objets Unity et à la communication entre les différents objets et script.
Ainsi, nous avons pu voir que cette seconde fonctionnalité était réalisable mais vraiment peu pratique pour déplacer l'écran. En ce qui concerne le fait de fixer la manette devant le joueur, la fonction pour déplacer l'objet est idéale, 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, les casques de réalité virtuelle ne matérialisent pas le corps de la personne qui joue et donc n'ont 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 Gamepad. 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 quelconques actions 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 et que les fonctions sont disponibles, il suffira d'un peu de mathématiques 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éfinies 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 translater un objet de façon rectiligne selon un vecteur pré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 souhaitons que le Gamepad 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. Cela a pu être réalisé simplement, 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 qu'il bouge avec la caméra (la vision du joueur). Ceci était effectué à chaque appel de fonction Update et donc à chaque image créée pour le jeu(frame).
Cette méthode était fonctionnelle 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ée 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 été 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 répondre aux demandes de notre tuteur, nous avons alors regardé à chaque rafraichissement d'image comment le casque avait bougé et nous bougions 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 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'infini, nous avons ajouté une fonction de protection prenant en entrée 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 modifie en continue la direction selon certains paramètres et ainsi créer des mouvements plus complexes définis par une liste de vecteurs.
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 en rentrant dans des objets 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 suivait aussi ce mouvement donc c'était très embêtant d'appuyer les boutons à l'extrémité.
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 fonctionnait très bien mais rendait les boutons plus petits, ce qui dérangeait notre tuteur. Nous avons alors trouvé une autre solution qui a été 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 idéale, 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 si nous voulons ajouter la possibilité de changer la taille de la manette ou la taille des boutons.
Les tests étaient maintenant terminés et tout fonctionné correctement, nous avons pu nous apercevoir que d'autres fonctionnalités étaient possibles et pouvaient dériver de notre travail. Maintenant que les deux fonctionnalités sont faites nous pouvons passer à la dernière partie du projet que l'on devait faire si on avait le temps.
Partie web
L'activation du mode suivi nécessitée juste une checkbox comme pour l'option nombre gamepad, on a donc juste ajoutée une checkbox Suivi ON pour activer/désactiver la fonctionnalité. On avait cependant un problème quant au redimensionnement des fenêtres de placement libre, lorsque la fenêtre du navigateur web ou sur téléphone été redimensionnée, la taille des canvas ne s'ajustait pas, et les éléments de la page se retrouvaient en-dessous et cachés par les canvas.
Pour résoudre ce problème, on a mis en place une fonction resize() qui est appelée une fois quand la page est chargée, puis dès que le script reconnaît l'événement de redimensionnement de la page web avec la commande window.addEventListener('resize', resize)
. Ainsi, dès qu'un redimensionnement de la page est reconnu, les canvas sont rétrécis/agrandis, ainsi que tous leurs objets.
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 dispositif quelconque (une kinect, une souris, le positionnement du doigt sur une tablette etc...) 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 possibles.
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 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é, et le casque fonctionne sous Android. De plus, son principal avantage est l'utilisation "cross-plateforme", c'est à dire l'utilisation de manettes de différents constructeurs pour interagir avec un seul programme en simultané. Cette librairie est donc inutilisable pour notre projet sans la modifier, de plus notre projet n'utilise que l'Oculus. 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 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 motif et des différentes fonctions intéressantes. Nous commençons par regarder quelle 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 et efficace possible. Afin d'éviter la surcharge du flux de données 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 appuie sur la gâchette du contrôlleur Oculus. Nous avons donc dû chercher les différentes méthodes permettant de récupérer la position des contrôlleurs et l'état des boutons du contrôlleur, 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 rafraichissements 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 été enregistrée, nous passons la liste à travers le filtre 1€. Ce filtre est développé par l'INRIA en collaboration avec l'université de Waterloo et a été 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ées, 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 un arrêt d'enregistrement (soulagement de la gâchette) avec un mouvement. Pour la première problématique, récupérer le mouvement réalisé, 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és et considérer 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 a du mal à faire des mouvements rectilignes sans être concentré, 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 apparue une fois que tout était 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 l'envoie à la Raspberry. Ceci entrainait plusieurs bugs, par exemple, 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ées. De plus, si le matériel est en fin de vie ou si la gâchette ou l'envoi 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 une mesure est perdue. 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ées, à partir de cela 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 passée 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étations.
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étations 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 de traitement des données. Au début, nous n'avions 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 nécessaire.
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 avons créé 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.
Partie web
De même que pour l'option de suivi, il fallait juste ajouter une checkbox pour activer/désactiver l'option Mouvement Libre. On a aussi ajouté un moyen d'enregistrer des configurations de placement libre des touches, en leurs attribuant des ID, comme ça l'utilisateur peut rapidement charger le placement des touches qu'il souhaite. Les ID sont stockés dans un fichier JSON layoutIDS.json qu'on peut facilement manipuler, et est appelé à chaque chargement de page pour pouvoir incrémenter un menu déroulant de sélection des IDs. Ce menu est également incrémenté dès qu'un nouvel ID est créé. Si l'utilisateur essaie de créer un ID avec un nom déjà existant, une alert s'affiche et indique que l'ID est déjà existant. Il est également impossible d'enregistrer un ID sans nom. La liste des IDs peut être réinitialisée à tout moment avec le bouton Reset layouts IDs list.
On a décidé de peaufiner la page pour rendre l'interface utilisateur plus pertinente, notamment en chargeant la dernière configuration envoyée au casque dès l'ouverture de la page, pour que l'utilisateur sache la configuration actuellement dans le casque, c'est à dire que les checkbox seront cochées ou non, et le dernier layout ID affiché sera le dernier envoyé. On a aussi fait en sorte de cacher la partie placement libre lorsque la checkbox est décochée, pour être moins intrusive et rendre l'interface plus agréable.
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 tests et de combinaisons afin de chercher les bugs qui pourraient 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.
Un bug rendait impossible l'accés à la seconde fonctionnalité ou son lancement était approximatif et l'écran passait derrière le mur. Ce problème venait de la configuration qui oubliait 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 où nous avons oublié de rétablir les positions initiales des boutons.
Sur demande de notre tuteur, nous avons arrêté de développer le projet pour combiner notre travail avec celui réalisé par notre tuteur ainsi que François Brassart. 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 à cet effet dans le projet. Cette option pourrait être ajoutée par la suite selon les envies du client ou de notre tuteur. Cependant, cela 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 motif prédéfini ou configuré par le médecin avec la manette. Ce qui pourrait être imaginé c'est que le médecin prenne un des contrôlleur du Quest 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 vecteurs 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 il faudrait une fonction pour traduire les positions en une liste de vecteurs, pour cela on 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, ici on peut s'appuyer sur la fonction permettant déjà de bouger l'écran avec un flag permettant d'indiquer si on fait des aller-retours ou des boucles et une variable globale permettant de se déplacer dans la liste.
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 besoins du client. De plus, nous pouvons imaginer de nouvelles détections de mouvement plus complexes que de simples mouvements rectilignes que nous avons pu implémenter la semaine dernière. Pour cela, nous pouvons imaginer une fonction connaissant tous les mouvements implémentés, 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 mouvement implémenté. Ceci permettrait d'éliminer le problème de ne pas avoir assez de mouvements implémentés pour y faire correspondre les 8 boutons de la manette NES nécessaires 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, en effet, 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.
Documents Rendus
Rapport : Fichier:RapportPFE P18 Fdinatale Ibendhiab.pdf
Présentation : Fichier:PresentationPFE P18 Fdinatale Ibendhiab.pdf