なおしのこれまで、これから

学んだこと・感じたこと・やりたいこと

【Vive Pro Eye】アイトラッキングで取得した瞳の位置をモデルに反映させる

f:id:vxd-naoshi-19961205-maro:20220219203040g:plain

始めに

半年前ぐらいにVive Pro Eyeを触りましてちょっとしか遊んでなかったので、今回は取得した情報をモデルに反映させてみました。

今回使用するモデルはこちらです。

booth.pm

Vive Pro Eyeについては過去記事を参考にして下さい。SDKについて簡単にまとめています。

過去記事リンク



Vive Pro Eyeから瞳の座標を取得する

Vive Pro Eyeから瞳の座標を取得するためには SRanipal_Eye_v2.GetPupilPosition を使います。 position には瞳の座標が -1 ~ 1 の範囲で代入されます。(xは右、yは上方向が1に値する)

using UnityEngine;
using ViveSR.anipal.Eye;

public class GetPupilPositionSample : MonoBehaviour
{
    void Update()
    {
        var leftPupilPosition = GetPupilPosition(EyeIndex.LEFT);
        var rightPupilPosition = GetPupilPosition(EyeIndex.RIGHT);

        Debug.Log($"Left Pupil Position:{leftPupilPosition}");
        Debug.Log($"right Pupil Position:{rightPupilPosition}");
    }
    
    private Vector2 GetPupilPosition(EyeIndex eyeIndex)
    {
        SRanipal_Eye_v2.GetPupilPosition(eyeIndex, out var position);
        return position;
    }
}


また、SRanipal_Eye_v2 を使用するにはUnityのHierarchyに SRanipal_Eye_Framework コンポーネントを追加して、Enable Eye VersionVersion 2 に設定する必要があります。

f:id:vxd-naoshi-19961205-maro:20220219141731p:plain



モデルの瞳を動かす

モデルの瞳の構造とその問題

今回使用するモデルの瞳は眼球のように球体ではなく、瞳の絵が白目に張ってある状態でした。

f:id:vxd-naoshi-19961205-maro:20220219143107p:plain


なので、瞳のオブジェクトを回転させても瞳の絵が回転するだけになりました。

f:id:vxd-naoshi-19961205-maro:20220219143452g:plain


また、瞳のオブジェクトを平行移動させた場合は瞳の絵が目からはみ出てしまう問題がありました。

f:id:vxd-naoshi-19961205-maro:20220219143917p:plain



任意の中心点を元に瞳を回転させる

考えてみれば当たり前ですが、現実の人の瞳は眼球の中心点を元に回転しています。この考えに基づいて、モデルの瞳のオブジェクトも同様に中心点を決めて回転させます。

以下モデルの瞳のオブジェクト回転させるサンプルです。

MaplePupilRotateCheck

using UnityEngine;

public class MaplePupilRotateCheck : MonoBehaviour
{
    [SerializeField] private Transform _leftEyePupil;
    [SerializeField] private Transform _rightEyePupil;

    [SerializeField] private Transform _leftEyeCenter;
    [SerializeField] private Transform _rightEyeCenter;

    [SerializeField] private float _angleX;
    [SerializeField] private float _angleY;

    private Vector3 _originLeftEyePupilPosition;
    private Quaternion _originLeftEyePupilRotation;
    private Vector3 _originRightEyePupilPosition;
    private Quaternion _originRightEyePupilRotation;

    private Vector3 _leftDistance;
    private Vector3 _rightDistance;
    
    void Start()
    {
        _originLeftEyePupilPosition = _leftEyePupil.localPosition;
        _originLeftEyePupilRotation = _leftEyePupil.localRotation;
        _originRightEyePupilPosition = _rightEyePupil.localPosition;
        _originRightEyePupilRotation = _rightEyePupil.localRotation;
        
        Reset();
    }

    void Update()
    {
        // 実行中に中心点を移動させたとき用のリセット処理
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Reset();
            return;
        }

        var localRotation = Quaternion.AngleAxis(_angleX, Vector3.right) * Quaternion.AngleAxis(_angleY, Vector3.up);
        
        _leftEyePupil.localRotation = localRotation;
        _rightEyePupil.localRotation = localRotation;

        _leftEyePupil.localPosition = localRotation * _leftDistance + _leftEyeCenter.localPosition;
        _rightEyePupil.localPosition = localRotation * _rightDistance + _rightEyeCenter.localPosition;
    }

    private void Reset()
    {
        _angleX = 0;
        _angleY = 0;
        _leftEyePupil.localPosition = _originLeftEyePupilPosition;
        _leftEyePupil.localRotation = _originLeftEyePupilRotation;
        _rightEyePupil.localPosition = _originRightEyePupilPosition;
        _rightEyePupil.localRotation = _originRightEyePupilRotation;
        _leftDistance = _leftEyePupil.localPosition - _leftEyeCenter.localPosition;
        _rightDistance = _rightEyePupil.localPosition - _rightEyeCenter.localPosition;
    }
}


上記のサンプルの実行結果は次のようになります。上手く中心点を設定することで、瞳がはみ出ることなく動かせています。

f:id:vxd-naoshi-19961205-maro:20220219155143g:plain


解説

初期化時に中心点から瞳までのベクトルを求めます。

この値は回転させるときに使用します。

        _leftDistance = _leftEyePupil.localPosition - _leftEyeCenter.localPosition;
        _rightDistance = _rightEyePupil.localPosition - _rightEyeCenter.localPosition;


Update で指定されたx軸とy軸の回転から回転量 localRotation を求めて瞳をローカル空間で回転させます。

        var localRotation = Quaternion.AngleAxis(_angleX, Vector3.right) * Quaternion.AngleAxis(_angleY, Vector3.up);
        
        _leftEyePupil.localRotation = localRotation;
        _rightEyePupil.localRotation = localRotation;


次に瞳の座標を設定します。初期化時に求めた中心点から瞳までのベクトルに先程求めた回転量を掛け合わせて、中心点の座標に加算します。

こうすることで、中心点を元に瞳を回転させられます。

        _leftEyePupil.localPosition = localRotation * _leftDistance + _leftEyeCenter.localPosition;
        _rightEyePupil.localPosition = localRotation * _rightDistance + _rightEyeCenter.localPosition;



※ Transform.RotateAroundを使わなかった理由

任意のオブジェクトを中心に回転させるメソッドとして Transform.RotateAround があります。

Transform-RotateAround - Unity スクリプトリファレンス

こちらは回転量を指定して現在の座標から回転させるメソッドになります。(上記のプログラムでは元の座標から回転させる

Vive Pro Eyeで取得した瞳の座標から Transform.RotateAround を使用するには前フレームとの誤差を取得して回転量を求める等かなり面倒くさい方法をしなければならなかったため、使用しませんでした。



Vive Pro Eyeと組み合わせる

後はVive Pro Eyeと瞳を動かすプログラムを組み合わせます。

以下サンプルです。

MaplePupilRotate

using UnityEngine;

public class MaplePupilRotate : MonoBehaviour
{
    [SerializeField] private Transform _leftPupil;
    [SerializeField] private Transform _rightPupil;
    
    [SerializeField] private Transform _leftEyeCenter;
    [SerializeField] private Transform _rightEyeCenter;

    [SerializeField] private float _maxAngleX;
    [SerializeField] private float _maxAngleY;

    private Vector3 _leftEyeCenterLocalPosition;
    private Vector3 _rightEyeCenterLocalPosition;

    private Vector3 _distanceFromLeftCenterToPupil;
    private Vector3 _distanceFromRightCenterToPupil;

    public void Awake()
    {
        _leftEyeCenterLocalPosition = _leftEyeCenter.localPosition;
        _rightEyeCenterLocalPosition = _rightEyeCenter.localPosition;

        _distanceFromLeftCenterToPupil = _leftPupil.localPosition - _leftEyeCenterLocalPosition;
        _distanceFromRightCenterToPupil = _rightPupil.localPosition - _rightEyeCenterLocalPosition;
    }

    public void SetLeftPupilPosition(Vector2 pupilPosition)
    {
        var localRotation = CalculateEyeRotation(pupilPosition);
        _leftPupil.localRotation = localRotation;
        _leftPupil.localPosition = localRotation * _distanceFromLeftCenterToPupil + _leftEyeCenterLocalPosition;
    }

    public void SetRightPupilPosition(Vector2 pupilPosition)
    {
        var localRotation = CalculateEyeRotation(pupilPosition);
        _rightPupil.localRotation = localRotation;
        _rightPupil.localPosition = localRotation * _distanceFromRightCenterToPupil + _rightEyeCenterLocalPosition;
    }

    private Quaternion CalculateEyeRotation(Vector2 pupilPosition)
    {
        // MEMO: pupilPositionは瞳の座標、計算では瞳の回転角度なので x, y が逆になっている
        var angleX = pupilPosition.y * _maxAngleX;
        var angleY = pupilPosition.x * _maxAngleY;

        return Quaternion.AngleAxis(angleX, Vector3.left) * Quaternion.AngleAxis(angleY, Vector3.up);
    }
}

MaplePupilSample

using UnityEngine;
using ViveSR.anipal.Eye;

public class MaplePupilSample : MonoBehaviour
{
    [SerializeField] private MaplePupilRotate _maplePupilRotate;
    
    void Update()
    {
        var leftPupilPosition = GetPupilPosition(EyeIndex.LEFT);
        var rightPupilPosition = GetPupilPosition(EyeIndex.RIGHT);
        
        _maplePupilRotate.SetLeftPupilPosition(leftPupilPosition);
        _maplePupilRotate.SetRightPupilPosition(rightPupilPosition);
    }
    
    private Vector2 GetPupilPosition(EyeIndex eyeIndex)
    {
        SRanipal_Eye_v2.GetPupilPosition(eyeIndex, out var position);
        return position;
    }
}



実行結果

上記のサンプル以外にまばたきするプログラムも動かしています。

f:id:vxd-naoshi-19961205-maro:20220219203040g:plain


また、シェイプキーでしいたけ目にしたり目を小さくしても問題なく動きました。

f:id:vxd-naoshi-19961205-maro:20220219203659g:plain


最後にウィンクをしてみました。(苦手なので許してください)瞳がぶれたりしないか心配でしたが、問題なさそうです。

f:id:vxd-naoshi-19961205-maro:20220219204214g:plain



最後に

アバターに瞬きや目線を加えるだけで、かなり生きてる感が増して楽しかったです。

今後はフェイシャルトラッカーにも挑戦してみる予定です。

また、丁度VRCでもOSCを使えばVive Pro Eyeやフェイシャルトラッカーの情報も送信できるみたいなので機会があれば触ってみようと思います。