Bakulog

獏の夢日記的な何か。

視線トラッキングデバイスでプロ生ちゃんを動かす

本記事はプロ生ちゃん Advent Calendar 2017の17日目の記事です。

 

Aimer, ce n'est pas se regarder l'un l'autre, c'est regarder ensemble dans la même direction. (愛とはお互いに見つめ合うことではなく、いっしょに同じ方向を見つめることである。) -視線トラッキング技術に期待を寄せるサン・テグジュペリ-

 

もくじ

  1. 視線トラッキングバイスって何
  2. プロ生ちゃんが視線を追いかけるようにしてみる
  3. 目の開閉を反映しようとして失敗したので読者向け課題とします
  4. まとめ

1. 視線トラッキングバイスって何

名前の通り、ユーザーがどこを見ているのかを追跡するデバイスです。 「どこを見ているのか」というと曖昧ですが、具体例としては

  • デスクトップ画面座標のX=100, Y=200
  • いまGUIの○○ボタンを注目しています

みたいなレベルで視線の情報を落としてくれます。 使いどころが分かってれば便利なデバイスです。

製品は主にTobii社から色々なものが出ていますが、本記事ではその中でも、Tobii Eye Tracker 4Cという、一般向けに売られている低価格なデバイスを使います。

視線トラッキングバイスにも設置型とかメガネ型とか色々あるんですが、Tobii Eye Tracker 4Cはディスプレイに貼り付けて使う、Webカメラみたいなタイプです。

2. プロ生ちゃんが視線を追いかけるようにしてみる

ここからはWindows上でUnityを使用する前提で、プロ生ちゃんを動かすまでの手順と結果をざっくり紹介します。

注意として、手元ではUnity 2017 2.0f3を使用し、ランタイムバージョンを.NET 4.6 Equivalentにしています。下記スクリプトは(割とどうでもいいところで)C# 6.0の機能を使っているので、再現の際にはランタイムバージョンを引き上げるか、スクリプトをちょっと手直しして使ってください。

まずUnity向けSDKの情報があるので拾ってきます。 ここにはデモシーンやデモ用スクリプトがいくつかあるのですが、その中にGazePlotter.csという便利そうなスクリプトがあります。このスクリプトがしている主な処理は以下2つです。

  • ユーザーが見ているデスクトップの座標を、Unityのカメラ座標に変換する
  • 変換した座標を使ってUnity上のオブジェクトをその位置へ動かす

キャラクターの制御にも使えそうなのでパクr…改造し、次のようにします。スクリプト名はAdjustGazeTarget.csとしています。

using UnityEngine;
using Tobii.Gaming;

public class AdjustGazeTarget : MonoBehaviour
{
    [Tooltip("Distance from screen to visualization plane in the World.")]
    public float VisualizationDistance = 1f;
    [Range(0.1f, 1.0f), Tooltip("How heavy filtering to apply to gaze point bubble movements. 0.1f is most responsive, 1.0f is least responsive.")]
    public float FilterSmoothingFactor = 0.15f;

    public bool HasValidPosition { get; private set; } = false;

    private GazePoint _lastGazePoint = GazePoint.Invalid;

    private bool _hasPreviousGazePoint;
    private Vector3 _previousGazePoint;

    void Update()
    {
        var gazePoint = TobiiAPI.GetGazePoint();

        if (gazePoint.IsRecent()
            && gazePoint.Timestamp > (_lastGazePoint.Timestamp + float.Epsilon))
        {
            Vector3 gazePointInWorld = ProjectToPlaneInWorld(gazePoint);
            transform.position = Smoothify(gazePointInWorld);

            _lastGazePoint = gazePoint;
        }

        HasValidPosition = gazePoint.IsValid;
    }

    private Vector3 ProjectToPlaneInWorld(GazePoint gazePoint)
    {
        Vector3 gazeOnScreen = gazePoint.Screen;
        gazeOnScreen += (transform.forward * VisualizationDistance);
        return Camera.main.ScreenToWorldPoint(gazeOnScreen);
    }

    private Vector3 ProjectMousePositionToPlaneInWorld()
    {
        var mousePosition = 
            new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0) +
            (transform.forward * VisualizationDistance);

        return Camera.main.ScreenToWorldPoint(mousePosition);
    }

    private Vector3 Smoothify(Vector3 point)
    {
        if (!_hasPreviousGazePoint)
        {
            _previousGazePoint = point;
            _hasPreviousGazePoint = true;
        }

        _previousGazePoint = Vector3.Lerp(_previousGazePoint, point, FilterSmoothingFactor);

        return _previousGazePoint;
    }
}

コレを適当な空のGameObjectにアタッチします。設定例はこんな感じ。

次に、プロ生ちゃんを召喚し、プロ生ちゃんの顔が視線先を向くように別のスクリプトを用意します。こちらはLookToGazeTarget.csとしています1

using UnityEngine;

public class LookToGazeTarget : MonoBehaviour
{
    public Transform Head;
    public Transform Target;
    public AdjustGazeTarget GazeTargetAdjuster;
    public Vector3 RotationOffset;
    public Vector3 PositionOffset;
    public float Responsiveness = 10f;

    private bool _hasCurrentRotation = false;
    private Quaternion _currentRotation = Quaternion.identity;

    private Quaternion _rotationOffset;

    private void Start()
    {
        _rotationOffset = Quaternion.Euler(RotationOffset);
    }

    void LateUpdate()
    {
        if (Head == null || Target == null || GazeTargetAdjuster == null)
        {
            return;
        }

        if (!GazeTargetAdjuster.HasValidPosition)
        {
            //Eye Tracker未接続の場合など
            return;
        }

        var targetRotation = _rotationOffset * Quaternion.FromToRotation(Head.forward, Target.position - Head.position - PositionOffset);

        if (!_hasCurrentRotation)
        {
            _currentRotation = targetRotation;
            _hasCurrentRotation = true;
        }

        _currentRotation = Quaternion.Lerp(
            _currentRotation,
           targetRotation,
            Time.unscaledDeltaTime * Responsiveness
            );

        Head.transform.localRotation = _currentRotation;
    }
}

これはプロ生ちゃんにアタッチし、先ほどのAdjustGazeTarget.csをアタッチしたオブジェクトとか、動かす首のボーンとかを設定します。

以上の設定が完了した状態で動かすとプロ生ちゃんが視線の方向に向いてくれます。動画中に表示されている白い円は、ユーザーが実際に画面上で注視している場所を示しています。

https://twitter.com/baku_dreameater/status/942205792706367488

ひとまず動いてくれているのが分かります。

なお、ちょっと見栄えが悪いですが、これは首だけ回して瞳をちっとも動かしてないせいです。 実は瞳の制御も少し試したんですが、計算がうまく行かず白目とかアヘ顔になってしまったのでお蔵入りにしてます。

瞳まできちんと制御できた場合に何が起こるか等について知りたければ、例えばPlayAnimakerを作成されているMuRoさんをフォローして情報を追いかけることをオススメします。

3. 目の開閉を反映しようとして失敗したので読者課題とします

Unityでどうやるのか分かってないんですが、.NET向けのSDKでは目の開閉が取得できます。以下に例を示します。

  1. (Unityではなく).NETのコンソールアプリケーションを作成
  2. NuGetパッケージマネージャで以下2つを取得
    • Tobii.Interaction
    • Tobii.StreamEngine
  3. コードを書いて実行

コードはこんな感じ。

using System;
using Tobii.Interaction;

namespace CheckEyeState
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var host = new Host())
            {
                host.Streams
                    .CreateEyePositionStream()
                    .Subscribe(OnEyePositionData);
                Console.ReadLine();
            }
        }

        private static void OnEyePositionData(StreamData<EyePositionData> streamData)
        {
            Console.WriteLine(
                $"L:{streamData.Data.HasLeftEyePosition}, " + 
                $"R:{streamData.Data.HasRightEyePosition}, " + 
                $"T:{streamData.Data.Timestamp}"
                );
        }
    }
}

これを実行すると、目の開閉がboolで拾えるのが確認できます。

この情報をUnity上で取得するなり.NETアプリケーションからUnityに投げつけるなりすればプロ生ちゃんに瞬きさせることができます…が、時間の都合で実装が間に合わなかったので、読者の課題とします。

なお、プロ生ちゃんの目をUnityで開閉する手はいくつかありますが、通常のモデルをいじる方法とか、MMDモデルを持ち込んでモーフ制御するとか、いくつか選択肢はあるので好きなものを使ってください。参考として、だいぶ前ですがオノッチさんがこういう事をされていたりします。

https://twitter.com/onotchi_/status/562211158531125248

4. まとめ

まあ、「やりました」というくらいで、とりあえずプロ生ちゃんが動かせて満足したという話でした。ガジェットの話題提供として参考になれば幸いです。

ひとつ言い訳をしておくと、今回の用途では視線トラッキングバイスとしての本領はあんまり発揮できていません。キャラの見た目に反映するだけならOpen Poseとか、普通のWebカメラで出来る方法の方が汎用性が高いので、そっちから検討すべきでしょう。

視線トラッキングバイスの良いところは以下の記事でやってるような、実際にUIコントロールを触るケースです。もし興味がありましたらこちらも是非どうぞ。

プロ生ちゃん Advent Calendar 2017の次の人は(…あれ、居ない…??)

12/18追記: 18日目の記事はプロ生ちゃんと NFCです。


  1. このスクリプトも参考元があったはずなんですが忘れてしまいました…。