やりたいこと
First VRを使って指の形がとれるのなら、それをアバターに適用すれば指を制御できるのではないかと思って試してみました。
結果
わかりにくいと思うので、もうちょっとマシな動画はTwitterの方にアップロードしてあります。 こちら
先に感想を言うと
- そこそこの精度でキャリブレーションができる
- ただし手首の角度が変わると精度が落ちるため、激しい運動をしながらの計測は厳しそう
- 指のパターンが増えるとキャリブレーションに要する時間が増える
最初は「グーから指を1本ずつ開いていく計5パターン」で動かしていました。
ただその場合、1パターンにつきキャリブレーションに30秒ほど、5パターンキャリブレーションするのに2分強かかってしまい、デバッグがつらいので3パターンにしています。
FirstVR自体の装着は楽な部類だと思うので、簡易に指のパターンを計測したいなら選択肢としてはまぁありかな…?といったくらいです。
ただし安定した動作をさせるためには、キャリブレーションなどにコツが必要そうです。
実装
あとはつらつらと実装内容について紹介してきます。
VRMの扱い
今回はVRMの動的ロードの必要がないため、Prefab化したモデルを直接シーンに配置しています。
ポーズはユニティちゃんのものを利用しています。
アバターの指を曲げる
アバターの曲げ伸ばしは、以前 ARでVRアバターを表示するシステムを構築しよう で発表したスクリプトを利用しています。
たとえば、チョキにしたければ LateUpdate()で次のように書けば動きます。
void LateUpdate()
{
fingerController.FingerRotation(FingerController.FingerType.RightAll, 1); //いっかい全部曲げる
fingerController.FingerRotation(FingerController.FingerType.RightIndex, 0); // 人差し指を開く
fingerController.FingerRotation(FingerController.FingerType.RightMiddle, 0); // 中指を開く
}
FirstVRのキャリブレーション
UI
キャリブレーションはまず、このようなUIを用意しました。
グー、チョキ、パーごとにキャリブレーションボタンがあり、それを押すことでキャリブレーションが行われます。
実装
指のパターンを表すFingerPattern
まず、指の状態を管理するためにenumを定義します。
using System;
namespace VRMSamples
{
public enum FingerPattern
{
Zero,
Two,
Five
}
public static class FingerPatternExtension
{
public static string ToName(this FingerPattern pattern)
{
switch (pattern)
{
case FingerPattern.Zero:
return "グー";
case FingerPattern.Two:
return "チョキ";
case FingerPattern.Five:
return "パー";
default:
throw new ArgumentOutOfRangeException(nameof(pattern), pattern, null);
}
}
}
}
キャリブレーションするCalibrationManager
キャリブレーションと、現在の指の状態を管理するCalibrationManager
を作成します。
キャリブレーション周りの非同期処理については、以前書いたFirstVR キャリブレーションをasync/awatiで書き直すを参考に。
using System;
using System.Collections.Generic;
using System.Linq;
using FVRlib;
using UniRx;
using UniRx.Async;
using UnityEngine;
namespace VRMSamples
{
public class CalibrationManager : MonoBehaviour
{
// FirstVRの管理コンポーネント
[SerializeField] private FVRConnection _connection;
private FVRGestureManager _manager;
// 指のパターンとキャリブレーションデータの管理
Dictionary<FingerPattern, FVRGesture> _gestures = new Dictionary<FingerPattern, FVRGesture>();
// 初期化完了通知
private AsyncSubject<Unit> _initAsyncSubject = new AsyncSubject<Unit>();
public IObservable<Unit> Initialized => _initAsyncSubject;
/// <summary>
/// 現在、指の形状が指定の指パターンになっているか?
/// </summary>
public IReadOnlyReactiveProperty<bool> IsGestureChanged(FingerPattern target)
{
var g = _gestures[target];
return g.ObserveEveryValueChanged(x => x.held).ToReactiveProperty();
}
public bool IsGestured(FingerPattern target)
{
return _gestures[target].held;
}
void Start()
{
_manager = _connection.gestureManager;
// ジェスチャーを作って登録する
foreach (var pattern in Enum.GetValues(typeof(FingerPattern)).Cast<FingerPattern>())
{
var gesture = _manager.RegisterCustomGesture(pattern.ToName());
_gestures.Add(pattern, gesture);
}
_initAsyncSubject.OnNext(Unit.Default);
_initAsyncSubject.OnCompleted();
}
/// <summary>
/// 対象をキャリブレーションする
/// </summary>
public async UniTask CallibrateTarget(FingerPattern target, IProgress<float> progress)
{
// SetTargetDataに指定のジェスチャーを指定
var targetGeture = _gestures[target];
_manager.SetTargetData(targetGeture);
// SetNonTargetDataに、指定のジェスチャーではないものを指定
foreach (var non in Enum.GetValues(typeof(FingerPattern)).Cast<FingerPattern>()
.Where(x => x != target))
{
_manager.SetNonTargetData(_gestures[non]);
}
var startTime = Time.time;
var span = _manager.calibrationRoundLength;
// キャリブレーション完了を待機
while (targetGeture.registering)
{
await UniTask.Yield();
progress?.Report((Time.time - startTime) / span);
}
}
}
}
キャリブレーションUIのボタンなどを管理するCalibrationPresenter
CalibrationPresenter
は各キャリブレーション要素ごとに1つ貼り付けられているコンポーネントです。
using UniRx;
using UniRx.Async;
using UnityEngine;
using UnityEngine.UI;
namespace VRMSamples
{
public class CalibrationPresenter : MonoBehaviour
{
// キャリブレーションを実行するボタン
[SerializeField] private Button _calibrationButton;
// 対象としている指パターン
[SerializeField] private Text _calibrationTargetName;
// キャリブレーション状態を表示するスライダ
[SerializeField] private Slider _calibrationSlider;
// 現在の指状態を表すイメージ(赤と緑に変わるやつ)
[SerializeField] private Image _indicatorImage;
public void Setup(CalibrationManager manager, FingerPattern pattern, BoolReactiveProperty trigger)
{
// ボタンが押されたらキャリブレーションする
// キャリブレーションが終わるまで、他のボタンもまとめて無効化する
_calibrationButton.BindToOnClick(trigger,
_ =>
{
return manager
.CallibrateTarget(pattern, Progress.Create<float>(p =>
{
// キャリブレーション進行中はスライダを更新
_calibrationSlider.value = p;
}))
.ToObservable()
.ForEachAsync(__ =>
{
// キャリブレーションが終わったらスライダを0にもどす
_calibrationSlider.value = 0;
});
});
_calibrationTargetName.text = pattern.ToName();
_calibrationSlider.value = 0;
// 指の形状が指定パターンになっているならインジケータを緑色にする
manager.IsGestureChanged(pattern)
.Subscribe(x => { _indicatorImage.color = x ? Color.green : Color.red; });
}
}
}
なお、AsyncReactiveCommand
を使って、「キャリブレーション中は他のボタンも含めてまとめて無効化する」ということをしています。
こういう複雑な挙動もサクッと実現できちゃうので、AsyncReactiveCommand
、ぜひ使ってみてください。
UIを配置するCalibrationBuilder
キャリブレーションUIを配置するBuilderを用意して動的にUIを構築してます。
プロトタイピングなので、実行効率とかは考えてないです。
ちなみに、こういう「相手の初期化待ち」はasync/awaitで書くとめっちゃきれいに書けるのでおすすめです。
詳しくは下の動画/スライドを見てください。
using UniRx;
using UnityEngine;
namespace VRMSamples
{
class CalibrationBuilder : MonoBehaviour
{
[SerializeField] private GameObject caliblrationButton;
[SerializeField] private RectTransform _root;
[SerializeField] private CalibrationManager _manager;
private BoolReactiveProperty _trigger = new BoolReactiveProperty(true);
private async void Start()
{
await _manager.Initialized; // managerの初期化完了待ち
Create(FingerPattern.Zero);
Create(FingerPattern.Two);
Create(FingerPattern.Five);
}
private void Create(FingerPattern pattern)
{
var go = Instantiate(caliblrationButton, _root);
var pre = go.GetComponent<CalibrationPresenter>();
pre.Setup(_manager, pattern, _trigger);
}
}
}
指をアバターに反映するAvatarController
FirstVRから取得した指のパターンを、VRMアバターに反映します。
すごい雑に書いてありますが、ここでジェスチャーの判定優先度などを調整すればもう少し安定した動作はできるかもしれないです。
using System;
using System.Linq;
using UnityEngine;
namespace VRMSamples
{
public class AvatarController : MonoBehaviour
{
[SerializeField] private CalibrationManager _manager;
[SerializeField] private FingerController _fingerController;
private void LateUpdate()
{
foreach (var fingerPattern in Enum.GetValues(typeof(FingerPattern)).Cast<FingerPattern>())
{
if (_manager.IsGestured(fingerPattern))
{
ChangeFinger(fingerPattern);
break;
}
}
}
void ChangeFinger(FingerPattern pattern)
{
switch (pattern)
{
case FingerPattern.Zero:
_fingerController.FingerRotation(FingerController.FingerType.RightAll, 1);
break;
case FingerPattern.Two:
_fingerController.FingerRotation(FingerController.FingerType.RightAll, 1);
_fingerController.FingerRotation(FingerController.FingerType.RightIndex, 0);
_fingerController.FingerRotation(FingerController.FingerType.RightMiddle, 0);
break;
case FingerPattern.Five:
_fingerController.FingerRotation(FingerController.FingerType.All, 0);
break;
default:
throw new ArgumentOutOfRangeException(nameof(pattern), pattern, null);
}
}
}
}
まとめ
- FirstVRで指の動きをとるの楽しい
- UniTaskとかAsyncReactiveCommandがすごい便利