Aller au contenu

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.

EyeTrackingManager.cs
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:

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" :

EyeTrackingManager.cs
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 :

ProtocolVisualiseGaze.cs
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 :

ProtocolVisualiseGaze.cs
    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 :

DestroyOnCollide.cs
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 :

ProtocolVisualiseGaze.cs
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.