【Unity(C#)】OculusQuest(Oculus Integration)で手のアニメーションを制限する方法

##指の動きを制限したい
今までViveしか触ったことがなかったため、
OculusTouchコントローラーで人差し指と親指が動かせると知った時には感動しました。

しかし、過去にViveで遊んできたコンテンツ、作成してきたコンテンツの中で
「ここで人差し指とか親指が動かせたらな～」と思ったことは さほどありませんでした。

実際にOculusQuestで開発を進めていると、
**指が動くとかえって邪魔になる**という場面にすら遭遇しました。


なので、開発に応じて**簡単に指の動きの制限を切り替えられる機能は無いのか？**　と思い調べました。

着地点は下記実装です。
①コブシを握る
②握ったコブシを開く
③必要に応じて指を動かせるようにする

##Hand

`Hand`クラスの中に指のアニメーションのオンオフ切り替え機能がありました。
Prefabは`Oculus`-`SampleFramework`-`Core`-`CustomHands`-`CustomHandLeft(Right)`にあります。
Inspectorはこんな感じです。

![CustomHand.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/604d5041-1db6-2fec-7b29-9a7cd4d71050.png)

この中で今回のアニメーションの切り替えに関係しているプロパティがあります。
`Hand`の中の`Default Grab Pose`です。
初期状態ではPrefabの中の`HandPoseDefaultPf`がアタッチされています。
`HandPoseDefaultPf`のInspectorはこうです。
![Pose.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/88b4b80e-bb9c-78b1-e668-24d003b68390.png)
この**Allow Pointing**、**Allow Thumbs Up**が人差し指、親指の制御を担っています。
今回は使いませんが、**Pose ID**も便利な機能を持っていて、物に応じてつかんだ時のポーズを設定できます。


##コード
少し`Hand`メソッドの中を見ていきます。

```C#:Hand
       private void UpdateAnimStates()
        {
            bool grabbing = m_grabber.grabbedObject != null;
            HandPose grabPose = m_defaultGrabPose;
            if (grabbing)
            {
                HandPose customPose = m_grabber.grabbedObject.GetComponent<HandPose>();
                if (customPose != null) grabPose = customPose;
            }
            // Pose
            HandPoseId handPoseId = grabPose.PoseId;
            m_animator.SetInteger(m_animParamIndexPose, (int)handPoseId);

            // Flex
            // blend between open hand and fully closed fist
            float flex = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, m_controller);
            m_animator.SetFloat(m_animParamIndexFlex, flex);

            // Point
            bool canPoint = !grabbing || grabPose.AllowPointing;
            float point = canPoint ? m_pointBlend : 0.0f;
            m_animator.SetLayerWeight(m_animLayerIndexPoint, point);

            // Thumbs up
            bool canThumbsUp = !grabbing || grabPose.AllowThumbsUp;
            float thumbsUp = canThumbsUp ? m_thumbsUpBlend : 0.0f;
            m_animator.SetLayerWeight(m_animLayerIndexThumb, thumbsUp);

            float pinch = OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger, m_controller);
            m_animator.SetFloat("Pinch", pinch);
        }
```


下記の箇所で掴んだ物に応じたアニメーション制御を可能にしています。
掴んだ物にHandPoseをアタッチして`Pose Id`を設定することで可能です。


```C#:物ごとにポーズを設定が可能
        HandPose grabPose = m_defaultGrabPose;
        if (grabbing)
        {
            HandPose customPose = m_grabber.grabbedObject.GetComponent<HandPose>();
            if (customPose != null) grabPose = customPose
        }
```
今回の目的は、
①コブシを握る
②握ったコブシを開く
③必要に応じて指を動かせるようにする
だけに制限することなので使いませんが非常に便利かと思います。



修正する箇所は下記の**canPoint**と**canThumbsUp**です。

```C#
          // Point
          //bool canPoint = !grabbing || grabPose.AllowPointing;
            bool canPoint = grabPose.AllowPointing && !grabbing;
            float point = canPoint ? m_pointBlend : 0.0f;
            m_animator.SetLayerWeight(m_animLayerIndexPoint, point);

          // Thumbs up
          //bool canThumbsUp = !grabbing || grabPose.AllowThumbsUp;
            bool canThumbsUp = grabPose.AllowThumbsUp && !grabbing;
            float thumbsUp = canThumbsUp ? m_thumbsUpBlend : 0.0f;
            m_animator.SetLayerWeight(m_animLayerIndexThumb, thumbsUp);

```

短絡評価[^1]を使用しているのでややこしくみえますが、
改良前のコード(コメントアウトしてある箇所)は、先に**grabbing(物をつかんでいるか)**の評価を行っているので、
`canPoint`と`canThumbsUp`のTrue、Falseに関わらず指が動いてしまいます。
つまり、改良前のコード(コメントアウトしてある箇所)は**物をつかんだ時に指が動くかどうか**のみを制限しているということです。

改良後のコードでは根本的に指を動かすことを制限するために、判定を逆にしています。
これにより、
①コブシを握る
②握ったコブシを開く
③必要に応じて指を動かせるようにする
という制限を与える事が可能になりました。

##より厳密に
制限を加えることができましたが、まだ厳密には制限しきれていません。
画像のように、つまむ動作に関しては反映されてしまいます。
![OkHand.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/dd91aa6f-f487-7249-9e69-24cfdbfdee20.png)

これはアニメーターの設定がゴリゴリになっていることが原因です。
![AnimeBlend.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/92579d6d-4004-5a00-3b69-3542b391a0dd.png)

ほとんど使わないのでこうします。
![SimpleBelnd.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/279a378b-9cfa-553d-24ba-a67b99c36221.png)

右手がアニメーションを整理する前、左手が整理した後です。
見ても何も伝わらないと思いますのでアニメーターの設定変更と最後のコードのコピペをして試してみてほしいです。
![Hand.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/3dba09e1-9651-205f-4893-7c875bb4943b.gif)

##最終的なコード
いらないと思ったものはガンガン削除しました。
一応、別のScriptとして作成しました。

```C#:HandAction
using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using UnityEngine.SceneManagement;
#endif

namespace OVRTouchSample
{
    [RequireComponent(typeof(OVRGrabber))]
    [RequireComponent(typeof(HandPose))]
    public class HandAction : MonoBehaviour
    {
        public const string ANIM_LAYER_NAME_POINT = "Point Layer";
        public const string ANIM_LAYER_NAME_THUMB = "Thumb Layer";
        public const string ANIM_PARAM_NAME_FLEX = "Flex";
        public const float THRESH_COLLISION_FLEX = 0.9f;

        public const float INPUT_RATE_CHANGE = 20.0f;

        [SerializeField]
        private OVRInput.Controller m_controller;
        [SerializeField]
        private Animator m_animator = null;

        private Collider[] m_colliders = null;
        private bool m_collisionEnabled = true;
        private OVRGrabber m_grabber;
        private HandPose grabPose;

        private int m_animLayerIndexThumb = -1;
        private int m_animLayerIndexPoint = -1;
        private int m_animParamIndexFlex = -1;
       
        private bool m_isPointing = false;
        private bool m_isGivingThumbsUp = false;
        private float m_pointBlend = 0.0f;
        private float m_thumbsUpBlend = 0.0f;

        private void Start()
        {
            m_grabber = GetComponent<OVRGrabber>();
            grabPose = this.gameObject.GetComponent<HandPose>();

            // Collision starts disabled. We'll enable it for certain cases such as making a fist.
            m_colliders = this.GetComponentsInChildren<Collider>().Where(childCollider => !childCollider.isTrigger).ToArray();
            
            // Get animator layer indices by name, for later use switching between hand visuals
            m_animLayerIndexPoint = m_animator.GetLayerIndex(ANIM_LAYER_NAME_POINT);
            m_animLayerIndexThumb = m_animator.GetLayerIndex(ANIM_LAYER_NAME_THUMB);
            m_animParamIndexFlex = Animator.StringToHash(ANIM_PARAM_NAME_FLEX);

#if UNITY_EDITOR
            OVRPlugin.SendEvent("custom_hand", (SceneManager.GetActiveScene().name == "CustomHands").ToString(), "sample_framework");
#endif
        }

        private void Update()
        {
            UpdateCapTouchStates();

            m_pointBlend = InputValueRateChange(m_isPointing, m_pointBlend);
            m_thumbsUpBlend = InputValueRateChange(m_isGivingThumbsUp, m_thumbsUpBlend);

            float flex = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, m_controller);

            bool collisionEnabled = m_grabber.grabbedObject == null && flex >= THRESH_COLLISION_FLEX;
            
            UpdateAnimStates();
        }

        // Just checking the state of the index and thumb cap touch sensors, but with a little bit of
        // debouncing.
        private void UpdateCapTouchStates()
        {
            m_isPointing = !OVRInput.Get(OVRInput.NearTouch.PrimaryIndexTrigger, m_controller);
            m_isGivingThumbsUp = !OVRInput.Get(OVRInput.NearTouch.PrimaryThumbButtons, m_controller);
        }

        private float InputValueRateChange(bool isDown, float value)
        {
            float rateDelta = Time.deltaTime * INPUT_RATE_CHANGE;
            float sign = isDown ? 1.0f : -1.0f;
            return Mathf.Clamp01(value + rateDelta * sign);
        }

        private void UpdateAnimStates()
        {
            bool grabbing = m_grabber.grabbedObject != null;
   
            // Flex
            float flex = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, m_controller);
            m_animator.SetFloat(m_animParamIndexFlex, flex);

            // Point
            bool canPoint = grabPose.AllowPointing&&!grabbing;
            float point = canPoint ? m_pointBlend : 0.0f;
            m_animator.SetLayerWeight(m_animLayerIndexPoint, point);

            // Thumbs up
            bool canThumbsUp =  grabPose.AllowThumbsUp && !grabbing;
            float thumbsUp = canThumbsUp ? m_thumbsUpBlend : 0.0f;
            m_animator.SetLayerWeight(m_animLayerIndexThumb, thumbsUp);

        }
    }
}
```

なんかOculusの権利関係ややこしそうなので、
コピーライティング書いときます。

```
Copyright © Facebook Technologies, LLC and
its affiliates. All rights reserved.
```

[^1]: [参考リンク](https://ufcpp.net/study/csharp/st_operator.html)