全集中・Magic Leapの呼吸 肆ノ型 手入力 "かすたむ じぇすちゃ"

# なにをするの？
今回作るものとしてはMagicLeapのジェスチャ( 8種類 )を組み合わせてカスタムのジェスチャを作成するというもの

<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/z3AGTZsCjt">pic.twitter.com/z3AGTZsCjt</a></p>&mdash; 松本隆介 (@matsumotokaka11) <a href="https://twitter.com/matsumotokaka11/status/1334487983144148993?ref_src=twsrc%5Etfw">December 3, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
[サンプル動画( twitterがおしゃかポンになったときの予備 )](https://www.youtube.com/watch?v=3loqi-mq3yo)

プロジェクトは[こちらのリポジトリ](https://github.com/RyusukeMatsumoto7C9-B-2/magic-leap-advent-calendar-2020)にあります


# 下準備


本記事は基本的なビルドまでの手順はなされていることを前提として記事を書いています
ビルド時に利用する認証ファイルとかZeroIterationの設定の仕方とか


### 開発環境
- Lumin SDK : 0.24.1
- MagicLeap Unity Package : 0.24.1 ( 少し古いけど Lumin SDKと合わせました、 0.24.2はUnity2020以降で利用できます )
- MagicLeap Tool-Kit : [MagicLeapToolKitのリポジトリ最新版](https://github.com/magicleap/Magic-Leap-Toolkit-Unity)
- UniRx : 7.1.0
- UniTask : 2.0.26


ProjectSettingsのXR Plug-in Management の MagicLeapの項目にチェックを入れる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/44becf1b-6c01-a0f3-67ca-e881be66be76.png)

今回はハンドジェスチャを利用するので ```ProjectSettings > MagicLeap > ManifestSettings``` より GestureConfig, GestureSubscribe を有効化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/e38a78f5-8550-df06-aa95-2184ad2ba8bf.png)

# シーンの構築

**CameraRigの作成**
```MagicLeap > Core > Assets > Prefabs``` からMain Cameraをシーンに配置し、一旦そのMain Cameraオブジェクトを任意のフォルダにプレハブとして保存する( 名前を CameraRig に変更 )
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/d918f4b8-3b11-a520-6fcf-3fa08a95157a.png)

**HandControllerの作成**
CameraRigオブジェクトの下にHandControllerオブジェクトを作成し、 ```MagicLeap-Tools > Code > Input > Hands > HandInput``` をアタッチします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/3e562bf4-833d-2979-c6dd-83c998a61930.png)

---

このままだと手が描画されておらず確認しづらいのでサンプルにあるHandVisualizerを利用してテストします

HandControllerオブジェクトの下に ```MagicLeap > Examples > Assets > Prefabs > HandVisualizer``` オブジェクトを配置し、HandVisualizerコンポーネントのCenterの項目にはnullを指定します( Centerが設定されている場合は球で手の各関節を表示する機能がONになり見た目上邪魔になるため )
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/e0153f59-b102-f675-ed08-7d3a844333b7.png)

HandVisualizserオブジェクトの子オブジェクトにLHand, RHand オブジェクトを作成し、HandInputシーンから [VISUALIZERS]オブジェクトの中のKeyPointVisualizers 以下のオブジェクトを複製、Right~ はRHandオブジェクトの子オブジェクトに Left~はLHandオブジェクトの子オブジェクトにします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/7903eefd-1bcb-5378-17ec-de43139c1bc3.png)

上の手順を終えたものがこちら、なおCenterオブジェクトはHandVisualizerオブジェクトを複製する際についてきたものなので削除しても構いません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/7c75ba14-1e3e-22dc-ee12-f151edc5f57a.png)

RHand, LHand それぞれにHandSkeletonVisualizerコンポーネント、AxisVisualizerコンポーネントをアタッチ、BoneColorはお好きな色を設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/e9e9fabd-05b4-3553-e6c0-81bcbc7d619a.png)

## ここまでの動作確認
なぜかGameウィンドウに描画されていなかったのでSceneViewの方のスクショ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/fb1c75e7-ac7a-61a1-6373-63ec0841c32c.png)

ZeroIterationでの実機上でのスクショ
![ML_20201128_19.14.58.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/fe0bd17f-a69b-0e41-0d0a-2ea87850a65c.jpeg)


# スクリプトの作成

MagicLeapが提供しているデフォルトのジェスチャ( 8種類 )を組み合わせてカスタムジェスチャを作成するスクリプトを作ります
![InteractionModel_InputMethods_Gesture_Example_Main.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/78d7600b-3a1a-6a8a-c4f0-0e7cd15bc12f.png)

### スクリプト全容( せっかちな人はこれをコピペ )
```csharp
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``` 内に定義されています、このままでも使えますが使い勝手が悪い為一旦カスタムのジェスチャを定義してそちらに変換するようにします。


```csharp

        public enum HandPose
        {
            LFinger,
            RFinger,
            LFist,
            RFist,
            LPinch,
            RPinch,
            LThumb,
            RThumb,
            LL,
            RL,
            LOpenHand,
            ROpenHand,
            LOk,
            ROk,
            LC,
            RC,
            LNoPose,
            RNoPose,
            LNoHand,
            RNoHand
        }

```

---

**取得したハンドジェスチャをカスタムのジェスチャに変換する処理**

```csharp
        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番目のジェスチャ``` と連続で呼ばれたイベントを購読するオブザーバーを作成し、返り値で返します

```csharp
   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**

```csharp
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秒で破棄するだけの処理

```csharp

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コンポーネントをアタッチ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/bbddc8a7-0e0d-f0fb-61bf-a57e698d9ba6.png)


動作確認用に Sampleオブジェクトを作成し Sampleコンポーネントをアタッチ、シーン上に配置します、オブジェクトの参照周りは以下の画像の通り
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212999/25d1721e-0ed3-cc99-d844-3f1ce61dce24.png)


Sample.csのStart()にてカスタムジェスチャを登録し、イベントが発火されたら登録したオブジェクトを生成するようにしています
今回のサンプルでは

- 右Fist -> 右OpenHand で ObjA の生成
- 左Fist -> 左OpenHand で ObjB の生成
- 左OK -> 右OK で ObjC の生成

を行います

```csharp

        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);
        }

```