Mise en place de l'Eye Tracking avec le SDK Pico sur Unity 3D


But du projet

Dans cet article nous verrons la mise en place de l’eye tracking de Pico sur Unity 3D.

L’objectif est le suivant:

  • déplacer un papillon dans l’espace avec les yeux

Un deuxième article viendra en complément pour récupérer les positions des yeux sur un plan 2D.

Cas d’usage de l’eye tracking en XR

Les cas d’usage de cette technologie sont nombreux :

  • Interaction et navigation naturelle
  • Foveated Rendering
  • Formation et simulation
  • Recherche en neurosciences et psychologie
  • Accessibilité
  • Suivi de la santé oculaire comme chez Eyesoft
  • Réhabilitation et thérapie

Jusqu’à il n’y a pas si longtemps il fallait passer par des capteurs externes en VR tels que ceux de la société Tobii, ces capteurs sont utilisés également hors XR en complément d’un ordinateur ou d’une tablette. Mais depuis quelques années ces capteurs sont intégrés dans les casques VR professionnels tel ceux de Pico XR ou encore Meta Quest Pro. On peut être certain qu’à terme nous retrouverons cette technologie dans les casques grand public.

Une des meilleurs utilisation que j’ai pu voir en terme d’expérience utilisateur a été sur l’Apple Vision Pro, l’expérience était vraiment convaincante.

Personnellement je me suis lancé sur cette technologie pour répondre à une demande client : permettre aux personnes en situation de polyhandicap d’interagir en VR, même pour les personnes tétraplégiques.

Stack Technique

  • Unity 3D 2021 (je n’ai pas testé sur les versions ultérieures mais ça doit fonctionner)
  • Pico Unity Integration SDK (2.5.0)
  • Casque Compatible Eye Tracking chez Pico : Pico 4 Enterprise ou Neo 3 Pro Eye

Mise en place

Si vous démarrer de zéro sur ce projet, je vous recommande de lire mon article dédié à cette étape : Hello World Pico Unity XR SDK

Activation du Eye Tracking

L’activation de l’Eye Tracking se passe dans le composant PX Manager, généralement vous avez placé celui ci sur l’objet XR Origin.

Activation de l'eye tracking

Déplacement du personnage dans la scène

Pour avoir un personnage se mouvant dans toutes les dimensions de manière réaliste, j’utilise l’asset gratuit d’un papillon :

Un joli papillon

Dans la scène créer un objet vide player dans lequel vous ajouterez ensuite le prefab de votre personnage (ici le papillon).

Créer ensuite un script FollowEyes.cs que l’on attache à l’objet Player.

Ce script va gérer les mouvements du Player pour qu’il se dirige de manière fluide vers la où le regard se porte.

On expose tout d’abord trois propriétés sur notre composant qui vont permettre d’ajuster la distance et la vitesse de déplacement du personnage. Notre personnage se déplacera toujours à la même distance de nous, il évoluera donc sur le bord d’une sphère de rayon “distance”.

public class FollowEyes : MonoBehaviour
{
    [SerializeField] private float speed = 20.0f;
    [SerializeField] private float smoothTime = 0.3f;
    [SerializeField] private float distance = 600;

Nous allons maintenant activer l’EyeTracking dans notre scène, on se reporte à la documentation Pico et implémente le code suivant dans le démarrage du composant.

A noter que les fonctionnalité d’Eye tracking font parti d’un ensemble de fonctionnalités d’interaction regroupées sous l’appellation interaction pack chez Pico.

    private TrackingStateCode trackingState;

    void Start()
    {
        // Start eye tracking
        EyeTrackingStartInfo info = new EyeTrackingStartInfo();
        info.needCalibration = 1;
        info.mode = EyeTrackingMode.PXR_ETM_BOTH;
        this.trackingState = (TrackingStateCode)PXR_MotionTracking.StartEyeTracking(ref info);

        if (trackingState != TrackingStateCode.PXR_MT_SUCCESS)
            Debug.LogError("Failed to start EyeTracking");
    }

Nous allons maintenant récupérer le point combiné du regard de l’utilisateur et pour cela nous allons utiliser les méthodes :

Pour pouvoir utiliser ce point et ce vecteur nous allons devoir utiliser une matrice de transformation en fonction de la caméra courante de l’utilisateur.

    var originOffset = matrix.MultiplyPoint(eyeOrigin);
    var directionOffset = matrix.MultiplyVector(eyeDirection);

Il ne reste plus qu’à calculer nouvelle position de notre joueur en additionnant le point d’origin, le direction et enfin notre distance (rayon de notre sphère) pour garder le joueur toujours à la même distance. La méthode Vector3.SmoothDamp permet d’obtenir un déplacement fluide et sans à-coup de notre joueur (le regard peut changer très rapidement de position)

    Vector3 targetPosition = originOffset + directionOffset * distance;
    transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime, speed);

La coroutine qui va repositionner notre player toutes les 24 centième.


    void Update()
    {
        StartCoroutine(UpdatePlayerPosition());
    }

    private IEnumerator UpdatePlayerPosition()
    {
        yield return new WaitForSeconds(1 / 24f);
        try
        {
            if (PXR_EyeTracking.GetCombineEyeGazePoint(out var p))
            {
                matrix = Matrix4x4.TRS(Camera.main.transform.position, Camera.main.transform.rotation, Vector3.one);

                Vector3 eyeDirection = Vector3.zero;
                Vector3 eyeOrigin;
                bool result = PXR_EyeTracking.GetCombineEyeGazePoint(out eyeOrigin) && PXR_EyeTracking.GetCombineEyeGazeVector(out eyeDirection);

                if (result)
                {
                    var originOffset = matrix.MultiplyPoint(eyeOrigin);
                    var directionOffset = matrix.MultiplyVector(eyeDirection);

                    Vector3 targetPosition = originOffset + directionOffset * distance;

                    transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime, speed);
                }
            }
            catch (Exception ex)
            {
                Debug.LogError(ex);
            }
        }

A ce niveau nous avons un papillon qui se déplace dans la scène en suivant notre regard.

Améliorations

Afin de rendre le contrôle plus intéressant nous allons modifier le script précédent.

Changement d’orientation du papillon

A ce stade notre papillon a toujours la même orientation ce qui selon le cas peut le faire voler en arrière. Nous allons maintenant faire en sorte que l’orientation change en fonction du sens de la marche.

Après avoir changé la position du papillon dans la coroutine, ajouter le code ci dessous :

   Vector3 direction = targetPosition - transform.position;
   if (direction != Vector3.zero)
   {
       Quaternion rotation = Quaternion.LookRotation(direction, Vector3.up);
       rotation.x = 0;
       rotation.z = 0;
       if (rotation.y > 0 && rotation.y <= 180)
           rotation.y = 0;
       else
           rotation.y = 180;
       transform.rotation = Quaternion.Lerp(transform.rotation, rotation, 0.1f);
   }

Changer la profondeur de mouvement du papillon

On souhaite maintenant que le papillon se rapproche des objets qui sont fixés par le regard.

Le but du jeu est que lorsque l’on détecte via un RayCast que le regard entre en collision avec un objet alors la distance par rapport au joueur n’est plus la distance du rayon de notre sphère par défaut mais la distance de l’objet par rapport au joueur.

Modifier le calcul de targetPosition avec le code suivant.

  // Création du rayon par rapport au vecteur de notre regard
  Ray ray = new Ray(originOffset, directionOffset);
  RaycastHit hit;

  // distance par défaut
  float distanceCoridor = distance;

  // si il y a un match alors on prend la distance de notre raycast.
  if (Physics.Raycast(ray, out hit))
      distanceCoridor = hit.distance;
  
  Vector3 targetPosition = originOffset + directionOffset * distanceCoridor;

Attention petite subtilité, le Raycast va va retourner vrai en permanence. Cela est du au papillon qui reste en permanence dans le champs de vision.

Il faut donc modifier les layers actifs sur notre papillon pour ignorer les raycast :

Ignore Raycast

Et voila c’est tout pour la partie code. Afin de rendre l’exemple plus sympa j’ai effectué les opérations suivantes :

  • changement de skybox
  • ajout d’un sol
  • ajout d’objet à fixer (pierres)
  • ajout de collider et rigibody pour éviter que le papillon traverse les objets
  • modification de l’animation de base du papillon pour retirer la partie où le papillon se pose

Si vous voulez de l’information ou plus de détail vous pouvez poser la question en commentaire.

Et voici le rendu dans le casque, le regard est d’abord porté vers le ciel puis on regarde les pierres au sol.

Ressources

Si vous souhaitez être accompagné sur ce type de projet merci de rentrer en contact avec moi via linkedin.