Oculus Goでコントローラ操作(3DオブジェクトとuGUI)

# はじめに

![download (1).gif](https://qiita-image-store.s3.amazonaws.com/0/140270/1aa83aa5-6a97-51cb-a8d8-6409f9869d09.gif)

上の動画のように、Oculusのコントローラでモノを選択するという、 **一見当たり前** のことをやろうとしたら
Unity初心者 & VR初心者ゆえなのか **とんでもなく手こずりました** 。 
手順を覚えられそうにないので、記事にしたためています。
今回の実装方法が果たして最善最楽の方法なのかよくわからないので、ご存知の方いらっしゃったら教えてください。

ちなみに、[Easy Controller Selection](https://developer.oculus.com/blog/easy-controller-selection/)という記事にあるUnityパッケージを使わないで、 [Oculus Integration](https://assetstore.unity.com/packages/tools/integration/oculus-integration-82022?aid=1101lGoY&utm_source=aff)だけを使いました。(Easy Controller Selectionのパッケージを入れると、名前が同じスクリプトなどが発生して色々面倒だったり、Oculus Integrationだけを使っている人の記事を参考しにくいなどのデメリットが見えたので)

### 動作確認環境

* HMD: Oculus Go
* Unityバージョン: 2018.2.11f11
* Oculus Integrationバージョン: 1.30.1 

### 主な参考資料

* [Use GearVrController with Selection Ray](https://forums.oculusvr.com/developer/discussion/comment/570581/#Comment_570581)
* [Unity’s UI System in VR](https://developer.oculus.com/blog/unitys-ui-system-in-vr/)
* [Oculus Go 開発メモ - uGUI編](https://qiita.com/JunSuzukiJapan/items/bead777e8dfeaa49ea30)

# 実装上必要になる知識

## コントローラから何かを操作する(ための当たり判定をする)にはRaycasterを使う

[Unity’s UI System in VR](https://developer.oculus.com/blog/unitys-ui-system-in-vr/)が参考になります。

* UnityにおけるRaycasterとは
  * ある直線上にColliderを持つ物体が存在するかどうかを判定してくれる。
  * 非VRゲームでは、例えばクリックされた対象を特定するために使われる。
  * VRでは
      * ユーザの視点を起点とする→視線操作の当たり判定に使う
      * コントローラを起点とする→コントローラ操作の当たり判定に使う
* UnityのRaycasterには２種類ある
  * [GraphicRaycaster](https://docs.unity3d.com/ja/current/ScriptReference/UI.GraphicRaycaster.html): GUI用
  * [PhysicsRaycaster](https://docs.unity3d.com/ja/current/ScriptReference/EventSystems.PhysicsRaycaster.html): 3D物理コンポーネント用
* Oculus Integrationでは以下のRaycasterが提供されている
  * [OVRRaycaster](https://developer.oculus.com/reference/unity/1.30/class_o_v_r_raycaster/): GraphicRaycasterに相当するもの
  * [OVRPhysicsRaycaster](https://developer.oculus.com/reference/unity/1.30/class_unity_engine_event_systems_o_v_r_physics_raycaster/): PhysicsRaycasterに相当するもの (今回は使わなかった)

## Oculusアプリにするには特別なカメラや入力手段が必要
* OVRCameraRigをカメラ・コントローラトラッキングに使う
  * OVRCameraRigというプレハブが頭や手の動きをトラッキングしてくれる
* OVRInputModuleを持つEventSystemがコントローラ入力を処理する
  * EventSystemは入力やRaycastを処理して、イベントを送る役割を持つ。シーンに1つしか置けない。
  * デフォルトではクリックなどの入力を処理するStandaloneInputModuleコンポーネントがくっついている。
  * Oculusコントローラからの入力を受け付けるにはStandaloneInputModuleの代用であるOVRInputModuleを使用する。

## その他

* レーザーを描画したい場合はLine Rendererが便利
  * Line Rendererは指定した２点間に線を描画してくれる

# 手順

## 事前準備

* Oculus Integrationをインポートしておく
* UnityでVR用開発ができるように設定しておく

## 3Dオブジェクト・uGUI共通

### カメラの入れ替え & コントローラの表示
<img width="224" alt="Screen Shot 2018-10-31 at 14.11.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/140270/6b90646f-da06-b565-6811-732989389440.png">

* シーンのデフォルトカメラ(MainCamera)を削除
* シーンにOculus/VR/PrefabsのOVRCameraRigを配置
* OVRCameraRig/TrackingSpace/RightHandAnchor以下にOculus/VR/PrefabsのTrackedRemoteを配置 (上の画像の感じになっていればOK)


### EventSystemの入れ替え

* シーンにEventSystemを追加
* EventSystemからStandaloneInputModuleコンポーネントを削除
* EventSystemのOculus/VR/Scripts/UtilのOVRInputModuleを追加
* OVRInputModuleのRay TransformにOVRCameraRig/TrackingSpace/RightHandAnchorを設定
* 以下のようになっていればOK   
<img width="421" alt="Screen Shot 2018-10-31 at 14.15.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/140270/333c88ef-4920-96c7-2435-092f60b9f5de.png">

### コントローラのトリガーが押されたら何かする

* 以下のGetDownメソッドでトリガーが押されているか判定可能

```c#
if (OVRInput.GetDown(OVRInput.Button.PrimaryIndexTrigger))
{
    // do something
}
```

ここまで設定が間違っていなければ、コントローラのボタン入力系イベントには対応できるようになっています。他のイベントは[Oculus Goのコントローラ入力の取得まとめ](https://qiita.com/rykgy/items/2b783b969c874ef4cc64)を参照してください


## 3Dオブジェクト用

OVRPhysicsRaycasterを利用したイベント発火方法がわからなかったので、以下のように自作スクリプト内でPhysicsRaycasterを利用しました

### コントローラを起点とした当たり判定の追加 & レーザーの表示

* 適当なEmptyObject(Laserと名付けた)をシーンに配置 
* LaserにLine Rendererコンポーネントを追加
* Line Rendererコンポーネントの設定値を適宜変更(以下は参考程度に)

<img width="408" alt="Screen Shot 2018-10-31 at 14.49.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/140270/c7fccf79-970f-30d7-e461-e8599d955369.png">

* Laserに新しいスクリプト(ControllerLaserRendererと名付けた)を追加
* スクリプトにレーザー描画のコードを実装する(以下は例)
  * rightHandAnchorにはOVRCameraRig/TrackingSpace/RightHandAnchorを指定する
  * lineRendererにはLaser自身を指定する

```c#:ControllerLaserRenderer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ControllerLaserRenderer : MonoBehaviour {
    public Transform rightHandAnchor = null;
    public LineRenderer lineRenderer = null;
    public float maxRayDistance = 500.0f;

    void Update ()
    {
        // 右手のコントローラの位置と向いている方向からRayを作成
        Ray laserPointer = new Ray(rightHandAnchor.position, rightHandAnchor.forward);

        // 作成したRay上にColliderがあるか判定
        RaycastHit hit;
        if (Physics.Raycast(laserPointer, out hit, maxRayDistance))
        {
            // Colliderがあれば、衝突箇所までレーザーを描画
            renderLaserToHit(laserPointer, hit);
        } else {
            // Colliderがなければ、最大長のレーザーを描画
            renderLaserFullLength(laserPointer);
        }
    }

    private void renderLaserToHit(Ray ray, RaycastHit hit)
    {
        renderLaser(ray.origin, hit.point);
    }

    private void renderLaserFullLength(Ray ray)
    {
        renderLaser(ray.origin, ray.origin + ray.direction * maxRayDistance);
    }

    private void renderLaser(Vector3 from, Vector3 to)
    {
        // Line Rendererの1点目と2点目の位置を指定する
        lineRenderer.SetPosition(0, from);
        lineRenderer.SetPosition(1, to);
    }

}

```

### Rayがゲームオブジェクトに当たったら何かする

Raycast時のRaycastHitから衝突したGameObjectが取得できるので、それに対して好きな方法でメソッド呼び出しすればOK

* Colliderのついているオブジェクトをシーンに配置する
* そのオブジェクトに適当なスクリプトを設定する (以下はRayが当たった時だけマテリアルを変える例)

```c#:Hoverable.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Hoverable : MonoBehaviour {

    public Material normal;
    public Material hovered;

    private Renderer rend;

    private void Start()
    {
        rend = GetComponent<Renderer>();
        rend.material = normal;
    }

    public void OnPointerEnter()
    {
        rend.material = hovered;
    }

    public void OnPointerExit()
    {
        rend.material = normal;
    }
}
```

* Raycast時に当たったオブジェクトに対してメソッドを呼び出しを行う
  * RaycastHitからゲームオブジェクトを取得
  * ゲームオブジェクトに対してSendMessage

```c#
GameObject pointedObject = hit.collider.gameObject;
pointedObject.transform.SendMessage("OnPointerEnter");
```


## uGUI用

[Oculus Go 開発メモ - uGUI編](https://qiita.com/JunSuzukiJapan/items/bead777e8dfeaa49ea30)を参考にさせていただきました

### SampleシーンのOVRGazePointerをPrefab化する

OVRGazePointerを自作したければ、[OculusGoでEventSystemを使ってオブジェクトをクリックする方法(not uGUI)](https://qiita.com/culage/items/c7e27d72025276ddf557#%E4%BB%98%E9%8C%B21-%E8%87%AA%E5%88%86%E3%81%A7ovrgazepointer%E3%82%92%E4%BD%9C%E3%82%8B%E6%96%B9%E6%B3%95)を参照してください。(私はめんどくさがってPrefab化しました。)

* Oculus/VR/Scenes/UIを開く
* OVRGazePointerをPrefabにする

### (作業していたシーンに戻って)Canvasの設定

* Canvasをシーンに配置
* CanvasのRender ModeをWorld Spaceにする
  * Render Modeとは?という方は[【Unity】uGUIのCanvasとRenderModeについて](https://tech.pjin.jp/blog/2017/03/02/unity_ugui_canvas_rendermode/)を参照してください
* CanvasのEventCameraをOVRCameraRig/TrackingSpace/CenterEyeAnchorに設定
* CanvasのGraphicRaycasterコンポーネントを削除
* CanvasにOculus/VR/Scripts/UtilのOVRRaycasterを追加
* 以下のようになればOK

<img width="486" alt="Screen Shot 2018-11-02 at 8.31.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/140270/44c83c05-4e6b-34cd-81fd-3ae20c444e2d.png">

### OVRGazePointerの設定

* Prefab化(または自作)したOVRGazePointerをシーンに配置
* Ray TransformにOVRCameraRig/TrackingSpace/RightHandAnchorを設定

<img width="486" alt="Screen Shot 2018-11-02 at 8.33.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/140270/c2c5eee8-c615-e619-fca9-81d013f2a38d.png">



### RayがGUI部品に当たったら何かする設定

* CanvasにGUI部品を設置
* ボタンならばOnClick()に呼びたいメソッドを設定

# 最後までわからなかったこと

* OVRPhysicsRaycasterを使用してコントローラ操作
  * OVRCameraRigに取り付けて使用するらしいが、RightHandAnchorのRayを指定する方法がわからず断念
* OVRGazePointerを使用しないで動かす方法
  * OVRInputModuleの実装内でOVRGazePointerを参照しているところを消せば動くかもしれないが、そもそもOVR系スクリプト同士がどう関連し合っているのかわからないのであまり手を出したくない。
  * OVRRaycasterを使わず、GraphicRaycasterを使う方法を模索する方が良いのかもしれない。