Oculus Questのコントローラー制御を覚える

# はじめに

![controller.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/5daa6d8e-3ade-176b-b9b9-c17e1c76349d.png)


Oculus Questを本格的に触りだして早1～2カ月経った。いまだにコントローラーの扱いが分からない。
サンプルはあるのだが、いまいちどこで何やってるか分からない。
そこで、備忘録も兼ねて今の時点で情報を取りまとめる。

Oculus Quest用の環境構築は[Unity＋Oculus Quest 開発メモ](https://framesynthesis.jp/tech/unity/oculusquest/)と言う素晴らしいまとめがあるので、一礼して参照していただきたい。

尚、本記事の構成としては

 + OVRInputを使ってみる
 + OVRCameraRigを学ぶ
 + OVRPlayerControllerを使う
 + DistanceGrabを学ぶ

# はじめてのOVRInput

おそらくサンプルコードから始めてすんなり受け入れられなかった方は、OVRInputが何かから始めたほうが良い。
OVRInputをざっくりいうと、<strong>Oculus Questのセンサー値を扱うためのAPI群</strong>だ。
サンプルを見てもさっぱりなので基本中の基本、OVRInputから始める。

[OVRInput](https://developer.oculus.com/documentation/unity/unity-ovrinput/?locale=ja_JP)


使うだけなら単純で、下記2点を組み込めばいい。

 + シーンにOVRManagerを導入する
 + メインスクリプトにOVRInput.Update()とOVRInput.FixedUpdate()の呼び出しを記述

まずはここまでやってみる。

 + 新規シーンを作成
   + 名前: FirstOVRInput
 + MainCameraを削除し、OVRCameraRigを追加
   + Assets->Oculus->VR->Prefab->OVRCameraRig
 + 新規GameObjectを作成しスクリプトを追加
   + 名前: MainController
   + スクリプトの名前: FirstOVRInputMainController

こんな状態。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/2a3f823f-e261-b514-ecba-71cebf49ffb8.png)

ここまで来たらいったん実行する。何もない空間で、コントローラーのアイコンすら出ない状態になっていれば成功だ。
前述のOVRManager導入とか、OVRInput.Update()の呼び出しはどうした？と、思われるかもしれない。
実は、OVRCameraRigの中でOVRManagerが導入されており、OVRManager内でOVRInput.Update()を呼び出している。
なので、OVRCameraRigを追加するだけでOVRInputを使う準備ができていると言うわけ。

# コントローラーを表示する

存在するはずの手が存在しないと、何をどう操作してよいか全くわからない。と言うわけで、一旦コントローラーの位置にアイコンを出現させる。
もう少し具体的に書くと、<strong>OVRInputからコントローラーの位置を取得して、取得した位置にオブジェクトを配置する</strong>だ。
一気にコード書かなきゃ感が出てきたが、安心してほしい。OVRCameraRigは内部で、<strong>トラッキングスペース、目、コントローラーの情報をオブジェクトに割り当てる</strong>動作を行っている。
その証拠に、OVRCameraRigは下記の様な改装になっているはずだ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/953c0808-ed94-e545-f894-7977b8ca5d84.png)


オブジェクトへの割り当ては、下記ようなコードが組んである(抜粋)。OVRInputからコントローラーの位置情報を取得してHandAnchorの位置を更新しているのが分かると思う。

```csharp:OVRCameraRig.cs
//Need this for controller offset because if we're on OpenVR, we want to set the local poses as specified by Unity, but if we're not, OVRInput local position is the right anchor
if (OVRManager.loadedXRDevice == OVRManager.XRDevice.OpenVR)
{
	Vector3 leftPos = Vector3.zero;
	Vector3 rightPos = Vector3.zero;
	Quaternion leftQuat = Quaternion.identity;
	Quaternion rightQuat = Quaternion.identity;

	if (OVRNodeStateProperties.GetNodeStatePropertyVector3(Node.LeftHand, NodeStatePropertyType.Position, OVRPlugin.Node.HandLeft, OVRPlugin.Step.Render, out leftPos))
		leftHandAnchor.localPosition = leftPos;
	if (OVRNodeStateProperties.GetNodeStatePropertyVector3(Node.RightHand, NodeStatePropertyType.Position, OVRPlugin.Node.HandRight, OVRPlugin.Step.Render, out rightPos))
		rightHandAnchor.localPosition = rightPos;
	if (OVRNodeStateProperties.GetNodeStatePropertyQuaternion(Node.LeftHand, NodeStatePropertyType.Orientation, OVRPlugin.Node.HandLeft, OVRPlugin.Step.Render, out leftQuat))
		leftHandAnchor.localRotation = leftQuat;
	if (OVRNodeStateProperties.GetNodeStatePropertyQuaternion(Node.RightHand, NodeStatePropertyType.Orientation, OVRPlugin.Node.HandRight, OVRPlugin.Step.Render, out rightQuat))
		rightHandAnchor.localRotation = rightQuat;

}
else
{
	leftHandAnchor.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.LTouch);
	rightHandAnchor.localPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch);
	leftHandAnchor.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.LTouch);
	rightHandAnchor.localRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch);
}

```

ここまでくれば話は早い。LeftControllerAnchor, RightControllerAnchorにそれっぽいPrefab(OVRControllerPrefab)をぶちこんであげればよい。ついでに床も追加しておく(影が確認できるから)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/b89168d2-5ac3-01f3-5431-23925ff62b3d.png)

この状態で実行するとコントローラーが表示され、実際のコントローラーに合わせて動くはずだ。

# もう少しOVRCameraRigを追う

OVRCameraRigとコントローラーの関係が分かったところで、もう少しだけOVRCameraRigをお勉強しておく。
前項までで、<strong>OVRCameraRigはOVRInputをQuest的な感じに提供してくれるもの</strong>という事は何となく理解していただけたと思う。だって、コントローラーのプレファブを追加するだけでコントローラの動きに合わせて表示してくれる。便利すぎる。便利だ。ただ、これだけでは次項で登場するOVRPlayerControllerの影が薄くなってしまう。今一度OVRCameraRigを見てみよう。

OVRCameraRigプレファブの階層

 + OVRCameraRig
   + TrackingSpace
     + LeftEyeAnchor
     + CenterEyeAnchor
     + RightEyeAnchor
     + TrackerAnchor
     + LeftHandAnchor
          + LeftControllerAnchor
     + RightHandAnchor
          + RightControllerAnchor

LeftControllerAnchor,RightControllerAnchorはすでに使ったので何となく理解できると思う。問題はほかのオブジェクトだ。

| オブジェクト | 概要 | 
| :----- | :----- |
| LeftEyeAnchor | 左目のカメラ。 |
| CenterEyeAnchor | 中央のカメラ(謎)。OVRCameraRigのuse per eye camerasで無効になる。 |
| RightEyeAnchor | 右目のカメラ。 |
| TrackerAnchor | Quest自体の位置情報を取得するためのセンサー。 | 
| LeftHandAnchor | 左手、左手コントローラー。 |
| RightHandAnchor | 右手、右手コントローラー。 |

カメラやセンサーは(俺は)Questでは扱わないので、Oculus Questアプリで意識すればよいのは、すでに出てきたコントローラーぐらいだ。
階層に持っているオブジェクトが分かったところで、軽くスクリプトも見ておこう。

| Script | 概要 |
| :----- | :----- |
| OVR Camera Rig | OVRInputをいい感じにオブジェクトと連動させる |
| OVR Manager | OculusQuest自体の情報(CPU温度,オーディオ,手の認識など)を取得・設定する |
| OVR Headset Emulator | キーボード,マウスでの移動をエミュレート |

ここまで頭に入れておくと、<strong>コントローラーの位置を取得するとかで悩んだらOVRCameraRigのOVR Camera Rigスクリプトを追えばいい</strong>と言うのが何となく理解できたと思う。

# ようやくOVRPlayerController

OVRCameraRigが卵焼きならOVRPlayerControllerは幕の内弁当だ(個人的な意見)。最初っからOVRPlayerControllerを追った場合、どこで何をやってるか非常に謎だ。
が、OVRCameraRigの動きを把握した今ならOVRPlayerControllerは怖くない。

 + シーンからOVRCameraRigを削除する
 + Assets->Oculus->VR->Prefab->OVRPlayerControllerを追加する
 + OVRPlayerController.OVRCameraRig.TrackingSpace.LeftHandAnchor.LeftControllerAnchorにOVRControllerPrefabを追加する
   + RightControllerAnchorも同様。 
 + OVRPlayerControllerの前にオブジェクトを追加 
 + 床を削除

ここまで来たらいったん実行してみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/fc91b503-5f79-b820-5b43-6e53998ff2fa.png)


違いが分かっただろうか。おそらく、追加した立方体が上方にすっ飛ぶはずだ。正確にはプレイヤーが落ちて行ってる。
この動きで分かる通り、OVRPlayerControllerなるものは、Questにゲーム内のキャラクターとしての動きを提供する。歩いていて何かにぶつかったりするにはこのOVRPlayerControllerが必要と言うわけだ。
次のステップに進む前にOVRPlayerControllerが持っている情報を見ておく。

| Script | 概要 |
| :----- | :----- |
| OVR Player Controller | ジャンプや移動の加速度など、一般的にキャラクターが保持するであろう情報を提供する |
| OVR Scene Simple Controller | ESCでゲーム中断などの一般的なゲームシーンのキー操作を提供する |
| OVR Debug Info | SpaceキーでFPSなどのデバッグ情報を表示する |

もうお腹いっぱいな位高機能。<strong>ゲーム内での移動などはOVRPlayerControllerのOVR Player Controllerスクリプトを追えばいい</strong>が何となく分かれば大丈夫。

# Oculus.SampleFramework.Usage.DistanceGrabを理解する

OVRPlayerControllerが幕の内弁当なら、DistanceGrabは花見セットだ(個人的な意見)。
このDistanceGrab、コントローラーを使って何かしたいときのサンプルとして提供されている。
が、最初っからこれを追いかけるとドツボにはまる。ある程度中身知ってないとうまく流用できない。
原初にして最強。
この最難関のOculusサンプルを理解すればコントローラーの情報を扱えるようになったと言っても過言じゃないハズ。

前項までで下記の事が分かっている。

 + OVRInputでQuestの情報(コントローラーなど)を取得する
 + OVRCameraRigでコントローラーの情報をオブジェクトに反映してくれる
 + OVRPlayerControllerがキャラクタの情報を管理してくれる

DistanceGrubのシーンを開くと下記の様になっている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/d6876e9e-8b46-555f-758c-52f03eee6a02.png)
<font color="dark_pink">ついでにちょこちょこ動かしてみて動きを確認しておく</font>

もう、オブジェクトが多くて泣きそうになる。が、今までの情報を理解していればそこまで難しい事は無い。ただ一つ気づいていただきたいのは、前項までのようにOVRCameraRig配下に手のオブジェクトが無いという事だ。
ざっとオブジェクトの紹介をしておく。

| object | 概要 | 
| :----- | :----- |
| OVRPlayerController | 前述。割愛。 |
| Environment#Static | 動かせないオブジェクト |
| Environment#Dynamic | 動かせるオブジェクトと手(重要) |
| CanvasWithDebug | デバッグ情報表示用 |
| DistanceGrabberSampleScript | オブジェクトを投げられるかどうかのデバッグ情報設定 |
| Occluder | 見えるけど掴めないオブジェクト |

はい。これだけあって、OVRPlayerControllerを追わなくていい事に感謝だ。
Environment#Dynamic配下のオブジェクトを見てみよう。

| オブジェクト | 概要 | 
| :----- | :----- |
| WoodBlocks | つかめるブロック達 |
| Billiards | つかめるビリヤードの玉達 |
| DistanceGrabHandLeft | 左手(重要) |
| DistanceGrabHandRight | 右手(重要) |
| DistanceGrabOculusWaterBottlePf | 水とか入れるボトル。つかめる。 |
| DistanceGrabSoccerBallPf | つかめるサッカーボール。 |
| DistanceGrabCubeCrosshair | つかめる十字線の入った箱。 |

DistanceGrabHandLeft(Right)とつかまれる側の何かしらを追ってしまえば勝ったも同然だ。

## つかむ側

DistanceGrabHandLeftをじっくり見てゆこう。まずはオブジェクトの階層から。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42445/54c9a4e9-3ed5-46c2-51c6-a07d6e293489.png)

<font color="dark_pink">いきなり難しい感じ出してくるよね</font>

| object | description |
| :----- | :------ |
| DistanceGrabHandLeft | 物つかんだりする制御。重要。 |
| gripTrans | 割愛。|
| GrabVolumeCone | 割愛。 |
| GrabVolumeBig | 割愛。 |
| GrabVolumeSmall | 割愛。 |
| l_hand_skeletal_lowres | 手のメッシュを構成する。アニメーターあり |
| hands:hands_geom | レンダラーの親オブジェクト |
| hands:Lhand | 手の描画。メッシュレンダラー。 |
| hands:l_hand_world | 指などの各部分の親オブジェクト |
| hands:b_l_hand | 指などの各部分 |

スクリプトはこんな感じ

| object | script | description |
| :----- | :------ | :------ |
| DistanceGrabHandLeft | Hand | ThumsUpなどのアニメーションを制御する。そこまで重要じゃない。 |
| DistanceGrabHandLeft | DistanceGrabber | OVRGrabberを継承している。実際にモノを掴んだりするスクリプト。重要。 |

DistanceGrabberのスクリプトを軽く追ってみると、実際に掴んだり動かしたりの記述がみられる。

```csharp:DistanceGrabber
       protected override void GrabBegin()
        {
            DistanceGrabbable closestGrabbable = m_target;
            Collider closestGrabbableCollider = m_targetCollider;

            GrabVolumeEnable(false);

            if (closestGrabbable != null)
            {
                if (closestGrabbable.isGrabbed)
                {
                    ((DistanceGrabber)closestGrabbable.grabbedBy).OffhandGrabbed(closestGrabbable);
                }

                m_grabbedObj = closestGrabbable;
                m_grabbedObj.GrabBegin(this, closestGrabbableCollider);
                SetPlayerIgnoreCollision(m_grabbedObj.gameObject, true);

                m_movingObjectToHand = true;
                m_lastPos = transform.position;
                m_lastRot = transform.rotation;

                // If it's within a certain distance respect the no-snap.
                Vector3 closestPointOnBounds = closestGrabbableCollider.ClosestPointOnBounds(m_gripTransform.position);
                if(!m_grabbedObj.snapPosition && !m_grabbedObj.snapOrientation && m_noSnapThreshhold > 0.0f && (closestPointOnBounds - m_gripTransform.position).magnitude < m_noSnapThreshhold)
                {
                    Vector3 relPos = m_grabbedObj.transform.position - transform.position;
                    m_movingObjectToHand = false;
                    relPos = Quaternion.Inverse(transform.rotation) * relPos;
                    m_grabbedObjectPosOff = relPos;
                    Quaternion relOri = Quaternion.Inverse(transform.rotation) * m_grabbedObj.transform.rotation;
                    m_grabbedObjectRotOff = relOri;
                }
                else
                {
                    // Set up offsets for grabbed object desired position relative to hand.
                    m_grabbedObjectPosOff = m_gripTransform.localPosition;
                    if (m_grabbedObj.snapOffset)
                    {
                        Vector3 snapOffset = m_grabbedObj.snapOffset.position;
                        if (m_controller == OVRInput.Controller.LTouch) snapOffset.x = -snapOffset.x;
                        m_grabbedObjectPosOff += snapOffset;
                    }

                    m_grabbedObjectRotOff = m_gripTransform.localRotation;
                    if (m_grabbedObj.snapOffset)
                    {
                        m_grabbedObjectRotOff = m_grabbedObj.snapOffset.rotation * m_grabbedObjectRotOff;
                    }
                }

            }
        }
```

ここで一通りスクリプトを追ってみて分かると思うが、コントローラーの位置を反映する部分が見つからない。
なぜかと言うと、DistanceGrabberの継承元であるOVRGrabberで制御を行っているからだ。
インスペクションにあるParent Transformがそれ。ここでマスターすべきは下記2点。

 + ものをつかむ制御を行いたいなら、OVRGrabberを継承してクラスを作る。
     + Oculus.VR.Scripts.Util.OVRGrabber
 + コントローラーの延長線上にあるオブジェクトを探すならDistanceGrabberのスクリプトが参考になる

## つかまれる側

ついでに掴まれる側も軽く追っておく。追うのはDistanceGrabOculusWaterBottlePf。
つかむ側を追った今、正直に言うとつかまれる側を追うのはちょろい。追うべきスクリプトはDistanceGrabbableのみだ。

DistanceGrabbableは100行にも満たないスクリプト。やっている事をざっくり言うと、<strong>つかめる状態になったり、つかまれた時に色をかえたりする</strong>だけだ。
処理の大部分はつかむ側が行っている。

覚えておくべきは

 + つかんで持ち歩けるオブジェクトを作りたいならOVRGrabbableを継承したスクリプトを追加する
     + Oculus.VR.Scripts.Util.OVRGrabbable

# ふりかえり

OculusQuestを買って早1年、ようやっと本腰をいれてコンテンツを作ろうと思った。
作り始めて速攻で躓いた。コントローラーの位置すら取れなかった。
Oculusの公式サイトでは<strong>OVRCameraRigを導入しましょう！</strong>と軽く書いてあるが、裏で何やってるかさっぱり分からない。

公式サンプルは下記特徴を持つので初心者の心が折れる

 + 多機能
 + 提供されているプレファブを拡張した形で作りこんである
 + シーンの構成を追っかける所から始まる

コントローラーを使ってやりたい事がある時に、どこを追えばいいかが分かってしまえば、<strong>とりあえずOVRGrabber付けたけどどうすればいい？</strong>みたいなことにはならないと思う。