tobii eye tracker 4cを使ってVRMモデルにアイトラッキングとヘッドトラッキングを実装する

# はじめに
[ViveProEye](https://www.vive.com/jp/pro-eye/)が発表され、[みゅみゅ教授](https://twitter.com/miyumiyuna5)によるアイトラッキングのあるバーチャルキャスト実験放送があり、アイトラッキングの素敵っぷりをバリバリに感じ取りました。
どうにかしてアイトラッキングがしたい、と思って探してたところ`tobii eye tracker 4c`という素敵デバイスを見つけたのを購入して試してみた結果、なかなかいい感じだったのでそのレビューや実装手法を本記事に記していきます。

記事内の僕の作成したスクリプトのライセンスは[CC0](https://creativecommons.jp/sciencecommons/aboutcc0/)とします。

本記事でこんな感じのことができます↓
![ezgif-1-5014f62dce48.gif](https://qiita-image-store.s3.amazonaws.com/0/140271/af424849-1a70-f505-0c7b-7ed1ca270144.gif)



# tobii eye tracker 4c
![Dy3VdwVUwAExZwk.jpg](https://qiita-image-store.s3.amazonaws.com/0/140271/3b95e7be-e974-ecef-e699-1ad2e087d330.jpeg)
[tobii eye tracker 4c](https://gaming.tobii.com/product/tobii-eye-tracker-4c/)とはTobii社が開発した視線入力装置で、アイトラッキングとヘッドトラッキングを同時に行うことができます。公式ではPCゲーム用デバイスと説明されており、ゲームでの利用を想定しているようです。実際、FPSゲームなどのゲーム実況で使われていることが多いみたいですね。ですが、バッチリ開発向けにSDKやAPIが用意されているので、エンジニアの皆さんにも安心です。
[公式サイト](https://gaming.tobii.com/product/tobii-eye-tracker-4c/)でも購入できますが、[Amazon](https://www.amazon.co.jp/exec/obidos/ASIN/B01MAWPMXQ/ref=nosim?tag=maftracking213201-22&linkCode=ure&creative=6339)でも購入でき、日本国内でも簡単に入手することができます。お値段も2万程度で性能の割には価格が非常に抑えられていていいですね。一日ですぐ届くのもありがたいです。

動画参考:[【BF1】FPSプロゲーマーの目線の動き方を可視化してみた 【実況】](https://www.youtube.com/watch?v=2qrhzoo84bA)
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/2qrhzoo84bA/0.jpg)](http://www.youtube.com/watch?v=2qrhzoo84bA)

### スペック(公式サイトの紹介を日本語化して一部抜粋)
|  |  |
|---|---|
|大きさ  |17 x 15 x 335 mm (0.66 x 0.6 x 13.1 in)  |
|最大画面サイズ  |16：9の縦横比で27インチ（21：9の縦横比で30インチ）  |
|動作距離  |20 - 37 "/ 50 - 95 cm  |
|OSの互換性  |Windows 7、8.1、10（64ビットのみ）  |
|CPU負荷  |1％（Core i7）  |
|Illuminator  |近赤外線（NIR 850nm)  |
|動作推奨スペック  |2.0 GHz, Intel i5 or i7, 8 GB RAM  |

Vの開発や現場で使うことを想定してスペックをレビューしておきます。

**大きさ**
座って配信する場合には問題なさそうですが、フルトラッキングで動き回る場合は難しそうですね。ヘルメットみたいなものを被って顔前に置く手法も考えられますが、それにしては大きすぎます。座ってのゲームや雑談配信、上半身のみで解決するシステムに組み込むことを想定した方が良いです。

**最大画面サイズ**
モニターの下部に張り付けて使用することを前提としているため、こういう記載があります。別に無視しても動作はするので、視線の値変動を取得したいだけなら気にしないで大丈夫そうです。ただし、無視すると「モニターのどこを見てるか」という情報はもちろん崩れます。ちなみに僕は適当にお菓子の箱(ガーナチョコ)の上に乗っけて使ったり雑に机の上に置いたまま使ったりしました。

**動作距離**
思ったより距離を取らないと動いてくれないなという感じです。フェイストラッキングのために別途にカメラなどで顔を撮影をしている場合は、顔とカメラの位置配置でちょっと苦労しそうです。僕はフェイストラッキング用のカメラが遠くなり、顔が小さく(特徴点の値変動も小さくなる)なってしまって少し苦労しました。また、前屈みになってモニターに近づくとトラッキングが外れることが多かったので少し不便に感じました。

**OSの互換性**
大丈夫そう。この対象に洩れてるPCはぜひバージョンアップしよう。

**CPU負荷**
負荷は軽いですね。UnityでVtuberシステムに組み込んでも大幅にフレームレートが落ちることはないと思います。

**Illuminator**
赤外線が用いられているので要注意です。LeapMotionやViveなどに干渉して精度を落とす可能性があります。

#VRMモデルにアイトラッキングとヘッドトラッキングを実装
Unityを用いてVRMモデルにtobii eye tracker 4cで取得したアイトラッキングとヘッドトラッキングデータを適応させ、現実の動きとモデルの動きをリアルタイムに同期させます。
##動作環境
- Windows 10 Home
- Unity2017.4.12f1
- tobii eye tracker 4c
- Tobii Eye Tracking Core Software v2.13.4
- TobiiUnitySDKForDesktop_4.0.3
- PCスペック
   - GPU   :GTX1080
   - CPU   :Corei7 8700K
   - メモリ:16GB

##セットアップ
#### 1.Tobii Eye Tracking Core Softwareをインストール
[こちら](https://gaming.tobii.com/getstarted/)からTobii EYETRACKINGを選択し、`Tobii Eye Tracking Core Software`をDLしてインストールします。正常に完了すれば、これでtobii eye tracker 4cをPCが認識できるようになります。
![tobii.PNG](https://qiita-image-store.s3.amazonaws.com/0/140271/b1601d4b-56ff-3b18-aa2c-5d6a7c065ca1.png)


#### 2. TobiiUnitySDKForDesktopを入手
[こちら](https://github.com/Tobii/UnitySDK/releases)から`TobiiUnitySDKForDesktop_4.0.3.unitypackage`をダウンロードします。

#### 3.Unityに`TobiiUnitySDKForDesktop_4.0.3.unitypackage`をインポート
先ほどDLした`TobiiUnitySDKForDesktop_4.0.3.unitypackage`をUnityにインポートします。インポートするとライセンスの同意を求められるので、ライセンスに目を通した上で同意して進みましょう。対話的利用（アイトラッキングデータをユーザー入力として利用するソフトウェア）によるアプリケーションの開発においては特別なライセンスは必要ありませんが、分析用途(アイトラッキングデータを保存または転送する)には別途ライセンスが必要なので注意しましょう。Vtuber的な利用は許諾の範囲だと考えられますが、収益に関する記述は見つからなかったため、活動に利用される方は[公式フォーラム](https://developer.tobii.com/community-forums/)などから確認を取った方が良いと思います。
![UnityTobii.PNG](https://qiita-image-store.s3.amazonaws.com/0/140271/65e30145-3925-78b8-4e6e-e6c0b82ce1cc.png)

#### 4.VRMモデルを配置
[こちら](https://github.com/dwango/UniVRM/releases)からUniVRMのunitypackageを入手し、Unityにインポートします。モデルは何でもいいですが、[VroidHub](https://hub.vroid.com/)から入手するのが楽だと思います。UniVRMが正常にインポートされていれば、vrmファイルを読み込むだけで、prefabが自動で生成されるはずです。

これでセットアップは終わりました。次項で実装していきます。

## 実装
セットアップができたプロジェクトのシーンにVRMモデル配置しておきましょう。モデルは[千駄ヶ谷 渋ちゃん](https://hub.vroid.com/characters/5860098757548846785/models/6567311261748429976)をお借りしました。
![VroidSet.PNG](https://qiita-image-store.s3.amazonaws.com/0/140271/d6d9e8cc-c7db-07bb-813a-9da652bbe1e0.png)

###ヘッドトラッキング
TobiiAPIに頭の向きを取得できるGetHeadPose()メソッドが用意されているので、これを使うだけでほぼ終わりです。
TobiiUnitySDKForDesktopのサンプルシーン`UserPresenceAndHeadPose`にある`HeadMovement.cs`を参考に以下のようなスクリプトを作成しました。

```c#:HeadMovementV.cs
using UnityEngine;
using Tobii.Gaming;

namespace TobiiEyeTracking{
	public class HeadMovementV : MonoBehaviour
	{
		public Transform Neck;
		public float Responsiveness = 10f;

		void Update()
		{
			var headPose = TobiiAPI.GetHeadPose();
			if (headPose.IsRecent())
			{
				Neck.transform.localRotation = Quaternion.Lerp(Neck.transform.localRotation, headPose.Rotation, Time.unscaledDeltaTime * Responsiveness);
			}
		}
	}
}
```
GetHeadPose()メソッドで取得した値をLerp()を使って徐々に適応させることで滑らかな動きを実現しています。

モデルに上記スクリプトをアタッチし、Neckにモデルの首にあたるオブジェクトのTransformをセットすればヘッドトラッキングのセットアップ完了です。
![HeadTrack.PNG](https://qiita-image-store.s3.amazonaws.com/0/140271/9538a0b7-6f58-4112-20af-e5ef86d6e321.png)

**実行結果**
![ezgif-1-6c69cbc26dca.gif](https://qiita-image-store.s3.amazonaws.com/0/140271/bcc0da32-332a-667b-c21d-88349f4f3650.gif)

###アイトラッキング
VRMには視線追従を行ってくれる`VRMLookAtHead`と`VRMLookAtBoneApplyer`コンポーネントがあるのでこちらを活用します。`VRMLookAtHead`のTargetに注視したいオブジェクトをセットするとそのオブジェクトを視線追従し続けるようになります。何でもいいので注視用オブジェクト(例ではCube)を作成し、顔前正面に配置して、そのCubeを`VRMLookAtHead`のTargetにセットします。

![図1.png](https://qiita-image-store.s3.amazonaws.com/0/140271/ad0caed4-582a-4c84-96da-881298e12d86.png)

これでVRMモデルがこの注視用オブジェクトを視線追従してくれるようになりました。次に、この注視用オブジェクトを視線情報に応じて動かします。先ほどの操作でVRMモデルは注視用オブジェクトの動きを目で追うようになったので、この注視用オブジェクトに視線情報と同じ動きをさせることができればアイトラッキング成功となるわけですね。

視線情報の取得にはTobiiAPIに視線の座標位置を取得できるGetGazePoint()メソッドが用意されているので、そちらを用いて以下のようなスクリプトを作成しました。

```c#:LookTargetMovement.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Tobii.Gaming;

namespace TobiiEyeTracking{
	public enum UpdateType{
		None,
		Update,
		LateUpdate,
	}

	public class LookTargetMovement : MonoBehaviour {
		public UpdateType UpdateType = UpdateType.Update;
		public GameObject LookTarget;
		private Vector3 StartPos;
		public float Renge_X;
		public float Renge_Y;
		public bool Invert_X;
		public bool Invert_Y;
		public float FilterSmoothingFactor = 0.15f;
		private bool _hasHistoricPoint;
		private Vector3 _historicPoint;

		void Start () {
			StartPos = LookTarget.transform.position;
		}

		void Update () {
			if (UpdateType == UpdateType.Update){
				GazePoint gazePoint = TobiiAPI.GetGazePoint();
				Renge_X = Mathf.Abs(Renge_X)*(Invert_X ? 1 : -1);
				Renge_Y = Mathf.Abs(Renge_Y)*(Invert_Y ? 1 : -1);

				Vector3 gazePointInWorld = new Vector3(StartPos.x+((gazePoint.Viewport.x-0.5f)*Renge_X),StartPos.y+((gazePoint.Viewport.y-0.5f)*Renge_Y),StartPos.z);
				LookTarget.transform.position = Smoothify(gazePointInWorld);
			}
		}
		
		void LateUpdate () {
			if (UpdateType == UpdateType.LateUpdate){
				GazePoint gazePoint = TobiiAPI.GetGazePoint();
				Renge_X = Mathf.Abs(Renge_X)*(Invert_X ? 1 : -1);
				Renge_Y = Mathf.Abs(Renge_Y)*(Invert_Y ? 1 : -1);

				Vector3 gazePointInWorld = new Vector3(StartPos.x+((gazePoint.Viewport.x-0.5f)*Renge_X),StartPos.y+((gazePoint.Viewport.y-0.5f)*Renge_Y),StartPos.z);
				LookTarget.transform.position = Smoothify(gazePointInWorld);
			}
		}

		private Vector3 Smoothify(Vector3 point)
		{
			
			if (!_hasHistoricPoint)
			{
				_historicPoint = point;
				_hasHistoricPoint = true;
			}

			var smoothedPoint = new Vector3(
				point.x * (1.0f - FilterSmoothingFactor) + _historicPoint.x * FilterSmoothingFactor,
				point.y * (1.0f - FilterSmoothingFactor) + _historicPoint.y * FilterSmoothingFactor,
				point.z);

			_historicPoint = smoothedPoint;

			return smoothedPoint;
		}
	}
}
```
GazePoint.Viewportにより、ビューポート座標で視線を取得しています。TobiiAPiにはビューポート座標の他にもスクリーン座標で取得するGazePoint.Screen、GUI座標で取得するGazePoint.GUIも用意されています。今回は、解像度に依存したくないのとピクセルで返ってくると計算が複雑になるのでGazePoint.Viewportを用いました(0~1で返ってきて計算が楽)。

次に、GazePoint.Viewportで取得した視線の座標位置を元に、注視用オブジェクトの座標を指定範囲内(Renge_X,Renge_Y)で求めています。そのまま値を反映させると急に眼が動いて怖く見えてしまうので、前回の座標と加減算をして平滑化処理を行っています。

下図のようにスクリプトをアタッチし、パラメータを調整しました。また、注視用オブジェクトのMeshを非表示にして画面内に映らないようにしています。
![Target.PNG](https://qiita-image-store.s3.amazonaws.com/0/140271/89574c8d-4546-72db-c59a-5416cfe769e3.png)


**実行結果**
結果gifでは視線を分かりやすくするために、Tobii公式から提供されているTobiiGhostというアプリで視線を可視化しています。`VRMLookAtBoneApplyer`コンポーネントで目の可動域などの設定ができるので、白目をよく向いてしまう場合はそちらのパラメータを調整しましょう。
![ezgif-1-d7ca9d084638.gif](https://qiita-image-store.s3.amazonaws.com/0/140271/6c825eee-ed0e-dea3-cdb5-f80d6cef97db.gif)

# 雑記Q&A
わざわざ項目作って書くほどでもない情報を雑多に書き記していきます。

Q,眼鏡でも大丈夫ですか。
A.大丈夫でした。あまり精度の低下は感じませんでした。記事内のGifもかなりプリズムの強い眼鏡を付けたままやっているものです。

Q.TobiiSDKのサンプルに用意されているGazePlotter.cs(視線情報に応じてパーティクルを動かしてくれる)で注視用オブジェクトを動かせば良かったのでは。
A.視線情報をスクリーン座標で取った後Camera.ScreenToWorldPoint()メソッドでワールド座標に変換しており、座標がカメラに依存してしまっています。そのため、VRMモデルが正面を向いていない場合、VRMモデルが正面にいない場合、カメラが複数ある場合などを想定したときに不都合だったので使いませんでした。

Q.何で公式の説明通りにモニター下部にtobii eye tracker 4cを設置しないのですか
A.検証という意味合いも強いですが、僕のモニターの下部が斜めになっていて、綺麗に張り付いてくれなかったためです。また、起動していると赤外線が目立って普段作業には使いたくなかったため、移動可能なところに設置したかったからです。
画像はバレンタインの売れ残りで手に入れた半額ガーナ空箱。めちゃくちゃ有能。抜群の安定性と哀しみを持つ。
![20190221_021740.jpg](https://qiita-image-store.s3.amazonaws.com/0/140271/298162a0-fd54-ff8e-8e38-6d2afa90b91e.jpeg)

Q.処理負荷についてもうちょっと詳しく
A.本記事で作成した千駄ヶ谷 渋モデルとCubeを配置し、千駄ヶ谷 渋モデルにデフォルトでアタッチされているスクリプトと`HeadMovementV.cs`と`LookTargetMovement.cs`が動作しているシーンのフレームレートが79~88fpsくらいでした(動作PCスペックについては動作環境項参照)。同じシーンで`HeadMovementV.cs`と`LookTargetMovement.cs`を非アクティブにし、tobii eye tracker 4cをUSBからぶっこ抜いたシーンのフレームレートが80~90fpsくらいでした。僕の環境だと1~3fpsくらい落とすというところでしょうか。
Plofilerで確認してもあまり負荷をかけているようには見えませんね。
![Overview.PNG](https://qiita-image-store.s3.amazonaws.com/0/140271/178d3965-2c40-1bcf-f282-bf0e9acd4101.png)



#おわりに
自分が趣味で拡張して遊んでいるVtuberシステムに組み込んでもみましたが、とてもプレゼンスが上がって良かったです。見ている場所が分かるので、アイデア次第で面白い演出を入れることもできそうです。また、処理が非常に軽く、実装もお手軽なのもとても良いですね。OpenCVとDlibによる画像処理は処理が非常に重く、学習データのライセンス問題の解決が大変でした。
ViveProEyeの発売が楽しみですね！！
<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">アイトラッキング実装できた実験で惨敗する僕 <a href="https://t.co/lBKSNsaBBL">pic.twitter.com/lBKSNsaBBL</a></p>&mdash; かいき (@kaikiofkaiki) <a href="https://twitter.com/kaikiofkaiki/status/1097265089743220736?ref_src=twsrc%5Etfw">February 17, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>