始めに
前回、前々回の記事です。
記事を書きながらなんとなくで使い方がわかったので、実際に自分でプログラムを書いてVive Pro Eyeで遊んでみようと思います。
実行環境
Windows10
Unity2020.3.7f1
Eye And Facial Tracking SDK 1.3.3.0
SRanipal Runtime 1.3.2.0
見ている個所にポインタをつける
試しに見ている方向にRayを飛ばして、そのRayが当たっている個所にSphereを配置するプログラムを作ってみました。
FocusPoint.cs
using UnityEngine;
using ViveSR.anipal.Eye;
public class FocusPoint : MonoBehaviour
{
[SerializeField]
private GameObject _lookPointerObject;
[SerializeField]
private Transform _hmdTransform;
void Update()
{
if (SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.WORKING)
return;
var gazeRay = GetGazeRay();
if (Physics.Raycast(gazeRay, out var hit))
{
_lookPointerObject.SetActive(true);
_lookPointerObject.transform.position = hit.point;
}
else
{
_lookPointerObject.SetActive(false);
}
}
private Ray GetGazeRay()
{
var eyeData = GetCombineSingleEyeData();
var origin = _hmdTransform.position;
var gazeDirection = eyeData.gaze_direction_normalized;
gazeDirection.x *= -1; // トラッキング情報は右手座標系なので反転させる
return new Ray(origin, _hmdTransform.rotation * gazeDirection);
}
private SingleEyeData GetCombineSingleEyeData()
{
SRanipal_Eye_v2.GetVerboseData(out var verboseData);
return verboseData.combined.eye_data;
}
}
このプログラムでやっていることは、両目合わせたデータから視線ベクトルを取得しHMDの座標、回転と組み合わせてRayを生成しています。
あとはそのRayが当たった場所にSphereを置いています。
private Ray GetGazeRay() { var eyeData = GetCombineSingleEyeData(); var origin = _hmdTransform.position; var gazeDirection = eyeData.gaze_direction_normalized; gazeDirection.x *= -1; // トラッキング情報は右手座標系なので反転させる return new Ray(origin, _hmdTransform.rotation * gazeDirection); }
実行してみた結果、かなり精度が高いものだと感じました。
下のgifでは距離を約3ぐらい離して、直径が1より少し小さい円を見るテストをしたものです。
見るときは円の端を上、下、右、左の順で見ています。やっている感覚としては多少ずれてはいるものの、おおよそ見ている部分にポインタが付きました。
次に距離を10ぐらい離したものです。
円にポインタをつけることは簡単でしたが、円の端を見るのに少し苦戦しました。ただし、HMDをきちんと顔に合わせれば問題なく円の端にポインタをつけることが可能でした。
簡単なゲームを作ってみる
軽く触ってみて意外と楽しかったので簡単なゲームを作ってみようと思います。
ゲーム内容は的をランダムな位置に生成してその的を見ながら瞬きをすると破壊出来るというものです。
参考程度に雑に書いたプログラムを載せておきます。
TargetGenerator.cs
using UnityEngine;
public class TargetGenerator : MonoBehaviour
{
[SerializeField] private Target _targetPrefab;
[SerializeField] private int _maxCount;
[SerializeField] private float _intervalTime;
[Space(20)]
[Header("ターゲットを置く範囲")]
[Header("X")]
[SerializeField] private float _minX;
[SerializeField] private float _maxX;
[Header("Y")]
[SerializeField] private float _minY;
[SerializeField] private float _maxY;
[Header("Z")]
[SerializeField] private float _minZ;
[SerializeField] private float _maxZ;
private int _count = 0;
private float _time = 0;
void Update()
{
if (_count == _maxCount)
return;
_time += Time.deltaTime;
if (_time < _intervalTime)
return;
CreateTarget();
_count++;
_time = 0;
}
private void CreateTarget()
{
var newTarget = Instantiate(_targetPrefab);
newTarget.transform.position = RandomPosition();
newTarget.SetTargetGenerator(this);
}
private Vector3 RandomPosition()
{
return new Vector3(
Random.Range(_minX, _maxX),
Random.Range(_minY, _maxY),
Random.Range(_minZ, _maxZ));
}
public void DecreaseCount()
{
_count--;
}
}
Target.cs
using UnityEngine;
public class Target : MonoBehaviour
{
private TargetGenerator _targetGenerator;
[SerializeField]
private GameObject _destoryEffect;
public void SetTargetGenerator(TargetGenerator targetGenerator)
{
_targetGenerator = targetGenerator;
}
public void DestroyTarget()
{
var effect = Instantiate(_destoryEffect, transform.position, Quaternion.identity);
Destroy(effect, 1f);
Destroy(gameObject);
}
private void OnDestroy()
{
_targetGenerator.DecreaseCount();
}
}
FocusPoint.cs
using UnityEngine;
using ViveSR.anipal.Eye;
public class FocusPoint : MonoBehaviour
{
[SerializeField]
private GameObject _lookPointerObject;
[SerializeField]
private Transform _hmdTransform;
private const float CloseEyeThreshold = 0.7f;
void Update()
{
if (SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.WORKING)
return;
var gazeRay = GetGazeRay();
if (Physics.Raycast(gazeRay, out var hit))
{
_lookPointerObject.SetActive(true);
_lookPointerObject.transform.position = hit.point;
if (IsCloseEye() && hit.collider.TryGetComponent<Target>(out var target))
{
target.DestroyTarget();
}
}
else
{
_lookPointerObject.SetActive(false);
}
}
private Ray GetGazeRay()
{
var eyeData = GetCombineSingleEyeData();
var origin = _hmdTransform.position;
var gazeDirection = eyeData.gaze_direction_normalized;
gazeDirection.x *= -1;
return new Ray(origin, _hmdTransform.rotation * gazeDirection);
}
private SingleEyeData GetCombineSingleEyeData()
{
SRanipal_Eye_v2.GetVerboseData(out var verboseData);
return verboseData.combined.eye_data;
}
private bool IsCloseEye()
{
SRanipal_Eye_v2.GetVerboseData(out var verboseData);
float leftOpenness = verboseData.left.eye_openness;
float rightOpenness = verboseData.right.eye_openness;
return rightOpenness < CloseEyeThreshold && leftOpenness < CloseEyeThreshold;
}
}
あとは適当にPrefabなどを設定して早速遊んでみました。
遊んでみての感想
的に標準を合わせることはとても容易でしたが、的を見たまま目を閉じることが難しかったです。
というよりも、目を少しでも閉じると瞳孔の座標がずれるので目を閉じる閾値をかなり上げた状態にしました。
慣れれば、的を淡々と破壊できるようになりました。
最後に
目を閉じるとトラッキングの精度が落ちるデメリットがありましたが、目を開けている状態であれば凝視している個所とポインタを表示している個所がおおよそ一致していることがわかりました。
また、プログラムを書いていて見ている個所にポインタを置くのも難しくはなかったので、目線で何かするのは意外と難しくないかもしれません。
この機能を今後どのように活かすかは未定ですが、何か面白そうなことが出来そうだったら試してみようと思います。