Utilisation des données de regard
Il est temps de la dernière pièce du puzzle : utiliser les données du regard réelles fournies par untraqueur oculaire intégré dans un casques VR.
Réutiliser ou reconstruire
Encore une fois, nous pouvons réutiliser nos précédents projets VR Unity pour gagner du temps (si votre projet précédent était configuré pour un casque Pico).
Commencez par créer un dossier Scrips dans Assets, ainsi qu'un dossier Resources, qui contient l'échantillon audio pop.mp3 que nous avons utilisé précédemment.
Créez un objet _Plane_3D comme sol de la scène — nommez-le Floor
.
Configuration du suivi du regard
Pour utiliser l'oculométrie avec un casque Pico 4 (spécifiquement) nous devons activer la fonctionnalité dans le composant "PXR_Manager" qui devrait se trouver sur l'objet "XR Origin (XR Rig)". S'il n'existe pas ajoutez le en cliquant "Add Component" tout en bas de l'inspecteur. Configuré ainsi le composant.
Créez un nouveau script nommé "EyeTrackingManager" que vous remplirez avec le code ci-dessous.
Ce script récupère les données oculaires (position et direction) et les transformes pour être relative au monde globale, et non plus à la caméra.
using UnityEngine;
using Unity.XR.PXR;
public class EyeTrackingManager : MonoBehaviour
{
public Transform Origin;
private Vector3 combineEyeGazeVector;
private Vector3 combineEyeGazeOrigin;
private Matrix4x4 originPoseMatrix;
public Vector3 combineEyeGazeVectorInWorldSpace;
public Vector3 combineEyeGazeOriginInWorldSpace;
private Collider lastHit;
void Start()
{
combineEyeGazeVector = Vector3.zero;
combineEyeGazeOrigin = Vector3.zero;
}
void Update()
{
originPoseMatrix = Origin.localToWorldMatrix;
PXR_EyeTracking.GetCombineEyeGazeVector(out combineEyeGazeVector);
PXR_EyeTracking.GetCombineEyeGazePoint(out combineEyeGazeOrigin);
//Translate Eye Gaze point and vector to world space
combineEyeGazeOriginInWorldSpace = originPoseMatrix.MultiplyPoint(combineEyeGazeOrigin);
combineEyeGazeVectorInWorldSpace = originPoseMatrix.MultiplyVector(combineEyeGazeVector);
}
Assigner la "Main Camera" à la variable Origin
dans l'inspecteur.
Ces données (combineEyeGazeOriginInWorldSpace et combineEyeGazeVectorInWorldSpace ) sont mises à jour à chaque frame car le code en question est appelé dans Update
. Elles sont aussi disponible au reste de votre code pour mettre en place des interactions ou pour être sauvegardées dans un fichier.
Attachez le script à l'objet Floor afin qu'il s'exécute dans votre scène.
Visualisation du regard
Nous allons maintenant créer des cubes qui changeront de couleur et de taille lorsque qu'ils sont regardés.
Cela sera fait de manière similaire à notre Cube Factory : de nouveaux cubes seront créés et équipés des fonctionnalités nécessaires.
Animation des cubes
Nous ne voulons pas que les cubes disparaissent simplement pour l'instant comme nous l'avons fait avec la cube factory, mais plutôt qu'ils changent de couleur (soudainement, puis progressivement).
Créez un nouveau script dans le dossier Scripts nommé AnimateOnCollide.cs
:
using System.Collections;
using UnityEngine;
public class AnimateOnGaze : MonoBehaviour
{
public bool isColliding;
public Color[] colors;
private Material _material;
private float _animationDuration = .25f;
private Vector3 scaleStart = new Vector3(.15f, .15f, .15f);
private Vector3 scaleEnd = new Vector3(.35f, .35f, .35f);
public void OnGazeEnter()
{
StartCoroutine(Animate());
}
public void OnGazeExit()
{
StopAllCoroutines();
transform.localScale = scaleStart;
}
private IEnumerator Animate()
{
float animationTime = _animationDuration;
// Interpolate between color one and two
while (animationTime >= 0)
{
animationTime -= Time.deltaTime;
_material.color = Color.Lerp(colors[1], colors[0],1 - animationTime / _animationDuration );
transform.localScale = Vector3.Lerp(scaleStart, scaleEnd, 1 - animationTime / _animationDuration);
yield return null; // Wait for next frame
}
}
}
L'IEnumerator Animate()
comptera à rebours à partir de la durée donnée dans _animationDuration
tout en "lerpant", ou interpolant entre les deux premières couleurs de son tableau colors
, et les assignera au matériau de l'objet. Similairement les cubes grossiront progressivement passant d'une taille de départ à une de fin plus grande.
A ce stade les fonctions _OnGazeEnter_
et _OnGazeExit_
ne seront jamais appelée. Nous allons déclencher ces évènements manuellement lorsque le regard se portera sur un cube. Ajoutez ce qui suit à la fin de la fonction Update
du script "EyeTrackingManager" :
RaycastHit hit;
if (Physics.Raycast(combineEyeGazeOriginInWorldSpace,combineEyeGazeVectorInWorldSpace, out hit, Mathf.Infinity))
{
if (hit.collider.name == "Collidable")
{
print($"Gaze hit");
if (!lastHit)
{
hit.collider.gameObject.SendMessage("OnGazeEnter", new Collider(), SendMessageOptions.DontRequireReceiver);
} else if (lastHit != hit.collider)
{
hit.collider.gameObject.SendMessage("OnGazeEnter", new Collider(), SendMessageOptions.DontRequireReceiver);
lastHit.gameObject.SendMessage("OnGazeExit", new Collider(), SendMessageOptions.DontRequireReceiver);
}
lastHit = hit.collider;
return;
}
}
if (lastHit != null)
{
lastHit.gameObject.SendMessage("OnGazeExit", new Collider(), SendMessageOptions.DontRequireReceiver);
lastHit = null;
}
Nous utilisons la méthode du RayCast pour savoir si un objet croise le regard. Cette méthode revient à placer un laser à la position de l'oeil et dans la direction du regard, le rayon de ce laser peut s'étendre à l'infini et la fonction Physics.Raycast
nous retourne l'objet (Collider
) intersecté par le regard si intersection il y a.
Création de cubes pour la visualisation du regard
Notre sol sera maintenant une sorte de cube factory. Créez un nouveau script attacher au GameOjbect "Floor" appelé ProtocolVisualiseGaze
:
using System.Collections;
using UnityEngine;
using Random = UnityEngine.Random;
public class ProtocolVisualiseGaze : MonoBehaviour
{
private static void CreateInteractiveObject(Vector3 position, Quaternion rotation, Color col1)
{
GameObject cubeGo = GameObject.CreatePrimitive(PrimitiveType.Cube);
cubeGo.name = "Collidable";
Transform cubeTrans = cubeGo.transform;
cubeTrans.position = position;
cubeTrans.rotation = rotation;
cubeTrans.localScale *= .15f;
AnimateOnGaze cubeCollChk = cubeGo.AddComponent<AnimateOnGaze>();
cubeCollChk.colors = new[]
{
col1,
new Color (1f-col1.r, 1f-col1.g, 1f-col1.b)
};
}
}
CreateInteractiveCube()
crée des cubes avec une position
, rotation
et couleur
données, et les équipe du composant AnimateOnGaze
. Il leur donne le nom "CollidableCube", et assigne à leur composant AnimateOnGaze
deux couleurs : une passée à la fonction CreateInteractiveCube()
via le paramètre col1
, et son opposée via la commande new Color (1f-col1.r, 1f-col1.g, 1f-col1.b)
.
Ajoutez maintenant un IEnumerator Start()
à ce script pour exécuter la génération de cubes avec le code ci-dessous :
IEnumerator Start()
{
// Create floating cubes in a square formation around the room's origin
Vector2[] moveVec = new[]
{
new Vector2(0,-1),
new Vector2(1,0),
new Vector2(0,1),
new Vector2(-1,0),
};
Vector3 startPos = new Vector3(1.8f, 1.6f, 1.8f);
for (int iBorder = 0; iBorder < 4; iBorder++)
{
float tmpVal = startPos.x;
startPos.x = -startPos.z;
startPos.z = tmpVal;
for (int iCube = 0; iCube < 4; iCube++)
{
Vector3 position = startPos;
position.x += moveVec[iBorder].x * (3.6f/4f * iCube);
position.z += moveVec[iBorder].y * (3.6f/4f * iCube);
CreateInteractiveObject(position, Random.rotation, Random.ColorHSV());
yield return new WaitForSeconds(.1f);
}
}
}
Bien que cette fonction semble longue, elle est plutôt simple dans ce qu'elle fait : elle crée simplement des cubes autour du centre de la pièce agencé le long d'un carré.
Enregistrez le code et essayez-le. Voyez si vous pouvez activer les cubes simplement en les regardant.
If looks could destroy
Au lieu d'animer nos cubes en les regardant, détruisons-les comme nous l'avons fait avec la sortie excédentaire de notre Cube Factory.
Créez un nouveau script appelé DestroyOnCollide
dans notre dossier de scripts :
using System.Collections;
using UnityEngine;
public class DestroyOnCollide : MonoBehaviour
{
private AudioSource _audioSource;
private void Start()
{
_audioSource = gameObject.AddComponent<AudioSource>();
_audioSource.playOnAwake = false;
_audioSource.clip = Resources.Load<AudioClip>("pop");
}
private void OnGazeEnter()
{
StartCoroutine(PlayAudioThenDestroy());
}
private IEnumerator PlayAudioThenDestroy()
{
print($"Destoyed {name}");
// Hide object
Destroy(GetComponent<MeshRenderer>());
// Delete collider component to prevent calling this coroutine twice
Destroy(GetComponent<Rigidbody>());
// Play pop sound
_audioSource.Play();
yield return new WaitUntil(() => !_audioSource.isPlaying);
// Actually destroy the object now
Destroy(gameObject);
}
}
Sa structure ne devrait plus être nouvelle à ce stade.
Retournez à notre script ProtocolVisualiseGaze
et modifiez sa fonction CreateInteractiveCube()
pour qu'elle ressemble à ceci :
private static void CreateInteractiveCube(Vector3 position, Quaternion rotation, Color col1)
{
GameObject cubeGo = GameObject.CreatePrimitive(PrimitiveType.Cube);
cubeGo.name = "CollidableCube";
Transform cubeTrans = cubeGo.transform;
cubeTrans.position = position;
cubeTrans.rotation = rotation;
cubeTrans.localScale *= .15f;
cubeGo.AddComponent<DestroyOnCollide>();
cubeGo.GetComponent<MeshRenderer>().material.color = col1;
}
Enregistrez le code, lancez le jeu, et faites des pop !
Challenge :
Créez une séquence d'évènement basée sur l'oculométrie :
- Le participant se trouve dans une scène contenant seulement une petite sphère flottant à niveau d'yeux,
- L'utilisateur doit regarder cette cible (la petite sphère) pendant 500ms pour déclencher l'apparition d'une scène complexe (la sphère disparaît),
- La scène complexe contient des éléments provenant du Furniture Kit utilisé au tout début de notre atelier,
- Donnez 20s à votre participant pour observer la scène puis cachez la de nouveau,
- Affichez sur un panneau les trois objets les plus regardés ainsi que leur durée d'observation respective.
Bonne chance.