なにをするの?
今回作るものとしてはMagicLeapのジェスチャ( 8種類 )を組み合わせてカスタムのジェスチャを作成するというもの
— 松本隆介 (@matsumotokaka11) December 3, 2020[サンプル動画( twitterがおしゃかポンになったときの予備 )](https://www.youtube.com/watch?v=3loqi-mq3yo)
プロジェクトはこちらのリポジトリにあります
下準備
本記事は基本的なビルドまでの手順はなされていることを前提として記事を書いています
ビルド時に利用する認証ファイルとかZeroIterationの設定の仕方とか
開発環境
- Lumin SDK : 0.24.1
- MagicLeap Unity Package : 0.24.1 ( 少し古いけど Lumin SDKと合わせました、 0.24.2はUnity2020以降で利用できます )
- MagicLeap Tool-Kit : MagicLeapToolKitのリポジトリ最新版
- UniRx : 7.1.0
- UniTask : 2.0.26
ProjectSettingsのXR Plug-in Management の MagicLeapの項目にチェックを入れる
今回はハンドジェスチャを利用するので ProjectSettings > MagicLeap > ManifestSettings
より GestureConfig, GestureSubscribe を有効化
シーンの構築
CameraRigの作成
MagicLeap > Core > Assets > Prefabs
からMain Cameraをシーンに配置し、一旦そのMain Cameraオブジェクトを任意のフォルダにプレハブとして保存する( 名前を CameraRig に変更 )
HandControllerの作成
CameraRigオブジェクトの下にHandControllerオブジェクトを作成し、 MagicLeap-Tools > Code > Input > Hands > HandInput
をアタッチします
このままだと手が描画されておらず確認しづらいのでサンプルにあるHandVisualizerを利用してテストします
HandControllerオブジェクトの下に MagicLeap > Examples > Assets > Prefabs > HandVisualizer
オブジェクトを配置し、HandVisualizerコンポーネントのCenterの項目にはnullを指定します( Centerが設定されている場合は球で手の各関節を表示する機能がONになり見た目上邪魔になるため )
HandVisualizserオブジェクトの子オブジェクトにLHand, RHand オブジェクトを作成し、HandInputシーンから [VISUALIZERS]オブジェクトの中のKeyPointVisualizers 以下のオブジェクトを複製、Right~ はRHandオブジェクトの子オブジェクトに Left~はLHandオブジェクトの子オブジェクトにします
上の手順を終えたものがこちら、なおCenterオブジェクトはHandVisualizerオブジェクトを複製する際についてきたものなので削除しても構いません
RHand, LHand それぞれにHandSkeletonVisualizerコンポーネント、AxisVisualizerコンポーネントをアタッチ、BoneColorはお好きな色を設定
ここまでの動作確認
なぜかGameウィンドウに描画されていなかったのでSceneViewの方のスクショ
スクリプトの作成
MagicLeapが提供しているデフォルトのジェスチャ( 8種類 )を組み合わせてカスタムジェスチャを作成するスクリプトを作ります
スクリプト全容( せっかちな人はこれをコピペ )
using System;
using MagicLeapTools;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR.MagicLeap;
using Debug = UnityEngine.Debug;
using Cysharp.Threading.Tasks;
using UniRx;
namespace AdventCalendar
{
/// <summary>
/// ハンドコントローラ.
/// </summary>
public class HandController : MonoBehaviour
{
struct KeyInfo
{
public HandPose pose;
public float time;
}
public enum HandPose
{
LFinger,
RFinger,
LFist,
RFist,
LPinch,
RPinch,
LThumb,
RThumb,
LL,
RL,
LOpenHand,
ROpenHand,
LOk,
ROk,
LC,
RC,
LNoPose,
RNoPose,
LNoHand,
RNoHand
}
private HandPose handPose;
private ManagedHand rHand;
private ManagedHand lHand;
[SerializeField] private GameObject handVisualizer;
[field: SerializeField] public bool IsGestureLogOutput { get; set; } = false;
private async void Start()
{
await Setup();
SwitchHandVisualize();
}
private async UniTask Setup()
{
await UniTask.WaitUntil(() => HandInput.Ready);
rHand = HandInput.Right;
lHand = HandInput.Left;
rHand.Gesture.OnKeyPoseChanged += OnHandGesturePoseChanged;
lHand.Gesture.OnKeyPoseChanged += OnHandGesturePoseChanged;
// 確認用にジェスチャをDebug.Logに出力.
this.ObserveEveryValueChanged(_ => handPose).Subscribe(e =>
{
if (IsGestureLogOutput)
Debug.Log($"Key {e}");
});
}
private void SwitchHandVisualize()
{
#if UNITY_EDITOR
handVisualizer.SetActive(true);
#elif !UNITY_EDITOR || UNITY_LUMIN
handVisualizer.SetActive(false);
#endif
}
private void OnHandGesturePoseChanged(
ManagedHand hand,
MLHandTracking.HandKeyPose pose)
{
bool isLeft = hand.Hand.Type == MLHandTracking.HandType.Left;
switch (pose)
{
case MLHandTracking.HandKeyPose.C: handPose = isLeft ? HandPose.LC : HandPose.RC; break;
case MLHandTracking.HandKeyPose.Finger: handPose = isLeft ? HandPose.LFinger : HandPose.RFinger; break;
case MLHandTracking.HandKeyPose.Fist: handPose = isLeft ? HandPose.LFist : HandPose.RFist; break;
case MLHandTracking.HandKeyPose.L: handPose = isLeft ? HandPose.LL : HandPose.RL; break;
case MLHandTracking.HandKeyPose.Ok: handPose = isLeft ? HandPose.LOk : HandPose.ROk; break;
case MLHandTracking.HandKeyPose.Pinch: handPose = isLeft ? HandPose.LPinch : HandPose.RPinch; break;
case MLHandTracking.HandKeyPose.Thumb: handPose = isLeft ? HandPose.LThumb : HandPose.RThumb; break;
case MLHandTracking.HandKeyPose.NoHand: handPose = isLeft ? HandPose.LNoHand : HandPose.RNoHand; break;
case MLHandTracking.HandKeyPose.NoPose: handPose = isLeft ? HandPose.LNoPose : HandPose.RNoPose; break;
case MLHandTracking.HandKeyPose.OpenHand: handPose = isLeft ? HandPose.LOpenHand : HandPose.ROpenHand; break;
}
}
/// <summary>
/// ジェスチャコマンドのオブザーバー作成.
/// </summary>
/// <param name="time"></param>
/// <param name="poseA"></param>
/// <param name="poseB"></param>
/// <returns></returns>
private IObservable<KeyInfo> CreateGestureCommandObserver(
float time,
HandPose poseA,
HandPose poseB)
{
// 指定したキーの判定を通知するObserverを返す.
IObservable<KeyInfo> GetInputObserver(HandPose pose)
{
return this.ObserveEveryValueChanged(_ => handPose)
.Where(k => k == pose)
.Select(k => new KeyInfo{pose = k, time = Time.realtimeSinceStartup});
}
var observer = GetInputObserver(poseA);
observer = observer.Merge(GetInputObserver(poseB))
.Buffer(2, 1)
.Where(b => b[1].time - b[0].time < time)
.Where(b => b[0].pose == poseA && b[1].pose == poseB)
.Select(b => b[1]);
return observer;
}
/// <summary>
/// カスタムジェスチャを登録し、登録したカスタムジェスチャが発火されたら実行する.
/// </summary>
/// <param name="time"></param>
/// <param name="poseA"></param>
/// <param name="poseB"></param>
/// <param name="callback"></param>
public void RegisterCustomGesture(
float time,
HandPose poseA,
HandPose poseB,
UnityAction callback,
Func<bool> option = null)
{
CreateGestureCommandObserver(time, poseA, poseB)
.Subscribe(e => callback?.Invoke())
.AddTo(this);
}
}
}
スクリプトの解説
先ずカスタムのジェスチャを作成するためにハンドポーズを宣言します、MagicLeapのデフォルトのHandPoseは MLHandTracking.cs
内に定義されています、このままでも使えますが使い勝手が悪い為一旦カスタムのジェスチャを定義してそちらに変換するようにします。
public enum HandPose
{
LFinger,
RFinger,
LFist,
RFist,
LPinch,
RPinch,
LThumb,
RThumb,
LL,
RL,
LOpenHand,
ROpenHand,
LOk,
ROk,
LC,
RC,
LNoPose,
RNoPose,
LNoHand,
RNoHand
}
取得したハンドジェスチャをカスタムのジェスチャに変換する処理
private void OnHandGesturePoseChanged(
ManagedHand hand,
MLHandTracking.HandKeyPose pose)
{
bool isLeft = hand.Hand.Type == MLHandTracking.HandType.Left;
switch (pose)
{
case MLHandTracking.HandKeyPose.C: handPose = isLeft ? HandPose.LC : HandPose.RC; break;
case MLHandTracking.HandKeyPose.Finger: handPose = isLeft ? HandPose.LFinger : HandPose.RFinger; break;
case MLHandTracking.HandKeyPose.Fist: handPose = isLeft ? HandPose.LFist : HandPose.RFist; break;
case MLHandTracking.HandKeyPose.L: handPose = isLeft ? HandPose.LL : HandPose.RL; break;
case MLHandTracking.HandKeyPose.Ok: handPose = isLeft ? HandPose.LOk : HandPose.ROk; break;
case MLHandTracking.HandKeyPose.Pinch: handPose = isLeft ? HandPose.LPinch : HandPose.RPinch; break;
case MLHandTracking.HandKeyPose.Thumb: handPose = isLeft ? HandPose.LThumb : HandPose.RThumb; break;
case MLHandTracking.HandKeyPose.NoHand: handPose = isLeft ? HandPose.LNoHand : HandPose.RNoHand; break;
case MLHandTracking.HandKeyPose.NoPose: handPose = isLeft ? HandPose.LNoPose : HandPose.RNoPose; break;
case MLHandTracking.HandKeyPose.OpenHand: handPose = isLeft ? HandPose.LOpenHand : HandPose.ROpenHand; break;
}
}
カスタムジェスチャ購読用のObserverを作成する処理
指定時間以内に 1番目のジェスチャ
-> 2番目のジェスチャ
と連続で呼ばれたイベントを購読するオブザーバーを作成し、返り値で返します
private IObservable<KeyInfo> CreateGestureCommandObserver(
float time,
HandPose poseA,
HandPose poseB)
{
// 指定したキーの判定を通知するObserverを返す.
IObservable<KeyInfo> GetInputObserver(HandPose pose)
{
return this.ObserveEveryValueChanged(_ => handPose)
.Where(k => k == pose)
.Select(k => new KeyInfo{pose = k, time = Time.realtimeSinceStartup});
}
var observer = GetInputObserver(poseA);
observer = observer.Merge(GetInputObserver(poseB))
.Buffer(2, 1)
.Where(b => b[1].time - b[0].time < time)
.Where(b => b[0].pose == poseA && b[1].pose == poseB)
.Select(b => b[1]);
return observer;
}
動作確認
サンプル用スクリプト
Sample.cs
using UnityEngine;
namespace AdventCalendar
{
public class Sample : MonoBehaviour
{
[SerializeField] private Transform camera;
[SerializeField] private HandController handController;
[SerializeField] private GameObject objA;
[SerializeField] private GameObject objB;
[SerializeField] private GameObject objC;
private void Start()
{
// 1秒以内に右手グー( RFist ) -> 右手パー( ROpenHand )で発火 SpawnObjA()を処理する.
handController.RegisterCustomGesture(1f, HandController.HandPose.RFist, HandController.HandPose.ROpenHand, SpawnObjA);
// 1秒以内に左手グー( LFist ) -> 左手パー( LOpenHand )で発火 SpawnObjB()を処理する.
handController.RegisterCustomGesture(1f, HandController.HandPose.LFist, HandController.HandPose.LOpenHand, SpawnObjB);
// 1秒以内に左手OK ( LOk ) -> 右手OK ( ROk )で発火 SpawnObjC()を発火する.
handController.RegisterCustomGesture(1f, HandController.HandPose.LOk, HandController.HandPose.ROk, SpawnObjC);
}
private void SpawnObjA()
{
GameObject obj = Instantiate(objA);
obj.transform.position = camera.position + (camera.forward * 0.5f);
}
private void SpawnObjB()
{
GameObject obj = Instantiate(objB);
obj.transform.position = camera.position + (camera.forward * 0.5f);
}
private void SpawnObjC()
{
GameObject obj = Instantiate(objC);
obj.transform.position = camera.position + (camera.forward * 0.5f);
}
}
}
SpawnObj.cs
テスト用のオブジェクト、生成されてから5秒で破棄するだけの処理
using System.Collections;
using UnityEngine;
namespace AdventCalendar
{
/// <summary>
/// スポーンしたオブジェクト用スクリプト、5秒で破棄する.
/// </summary>
public class SpawnObj : MonoBehaviour
{
private void Start()
{
StartCoroutine(AutoDeath());
}
private IEnumerator AutoDeath()
{
yield return new WaitForSeconds(5f);
Destroy(gameObject);
}
}
}
シーン上に適当なオブジェクト ObjA ~ ObjC を作成しプレハブにし、それぞれにSpawnObjコンポーネントをアタッチ
動作確認用に Sampleオブジェクトを作成し Sampleコンポーネントをアタッチ、シーン上に配置します、オブジェクトの参照周りは以下の画像の通り
Sample.csのStart()にてカスタムジェスチャを登録し、イベントが発火されたら登録したオブジェクトを生成するようにしています
今回のサンプルでは
- 右Fist -> 右OpenHand で ObjA の生成
- 左Fist -> 左OpenHand で ObjB の生成
- 左OK -> 右OK で ObjC の生成
を行います
private void Start()
{
handController.RegisterCustomGesture(1f, HandController.HandPose.RFist, HandController.HandPose.ROpenHand, SpawnObjA);
handController.RegisterCustomGesture(1f, HandController.HandPose.LFist, HandController.HandPose.LOpenHand, SpawnObjB);
handController.RegisterCustomGesture(1f, HandController.HandPose.LOk, HandController.HandPose.ROk, SpawnObjC);
}