UnityのVR SamplesをモバイルVRで活用する

#はじめに

Unity TechnologiesさんはVR開発初心者向けに、参考になるようなテクニックを集めたVR Samplesという無料アセットを配布しています。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/30e6ed94-348d-0bdd-63e1-2f3ae3a6f6c0.png)

このアセットそのものは、Oculus Riftの使用を前提にしている感じですが、ちょっと変更を加えることで、モバイル (Cardboardなど)でも色々使えそうです。そのいくつかを紹介します。以下の解説を読む前に、アセットストアからVR Samplesをダウンロードして、新規プロジェクトにインポートしておいてください。対象Versionは1.1.1です。

#モバイル対応の準備

## Google VR SDK for Unityを導入する

現時点のUnity（Unity5.4）ではモバイルVRにネイティブ対応していないので、Google VR SDK for Unityのような、モバイル向けのプラグインが必要です。Google VR SDK for Unityは[こちらからダウンロード](https://developers.google.com/vr/unity/download)できます。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/a9d5f211-165b-1c33-3c95-5d685c28f2af.png)

以下は Google VR SDK for Unity v1.0.0での作業です。

ダウンロードしたら、UnityPackageをインポートしてください。

次に、\Assets\GoogleVR\Legacy\Scripts\Internal\VRDevicesにある EditorDevice.csというスクリプトを開き、

```csharp:EditorDevice.cs

    public override void UpdateState() {
      Quaternion rot;
      if (GvrViewer.Instance.UseUnityRemoteInput && RemoteCommunicating) {
```
のif文を

```csharp:EditorDevice.cs
    public override void UpdateState() {
      Quaternion rot;
     if (RemoteCommunicating) {
```
に変更してください。

次に、VRカメラを配置したいシーン（たとえば、Assets\VRSampleScenes\ScenesのMainMenuシーン）を開き、Assets/GoogleVR/PrefabsにあるGVRViewerMain

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/46a81cbd-3931-fdba-01c1-3fa250d289a6.png)

をヒエラルキーに配置してください。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/75998819-10df-3ebb-95d3-f248969a4d1d.png)

次に、iPhoneあるいはAndroidにUnity Remote5アプリをインストールして、USBで接続してください。この際、対応する機種のビルド用のモジュールがUnityにインストールされていることをBuild Settingで確認してください。iPhoneの場合、使用しているPCにiTunesをインストールしている必要もあります。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/88c1e432-137f-c7e6-e9c7-7e85768d7a14.png)

次に、EditorメニューのProject Settings/Editorを開き、Unity Remoteとして使う機種を選んでください。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/af8a9a6f-756e-baa5-b61c-35fdd3079f2a.png)

また、EditorメニューのProject Settings/Playerを開き、Other SettingsのVirtual Reality Supportedを**アンチェック**してください。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/3e5fbb5a-3669-b57c-2453-ceb0c1dc0a51.png)

これをしていないと、Oculus RiftをつないでいるPCでは誤動作してしまいます。

最後に、スマホ上でUnity Remote5を起動して、UnityでPlayボタンを押すと、スマホをHMDスクリーンとしてシーンを見ることができます。スマホの画面でのタッチ入力も可能です。

![GVR4.gif](https://qiita-image-store.s3.amazonaws.com/0/58503/77174eb9-8b44-fa14-bdcc-5d03445294f4.gif)



このGif画像ではなぜか背景に動かないゲーム画面が映ってますが、実際のPCやスマホ上ではそこは黒くなってますので、ご心配なく。

もし、ハコスコのような単眼のゴーグルを考えているなら、GVRViewerMainのGVR ViewerコンポーネントのVR Mode Enabledをアンチェックしてください。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/af91d791-ab5c-52a2-bbae-9fb5bcdd6f67.png)

## 視線だけで入力できるようにする

ハコスコをはじめとする簡易VRゴーグルの多くは入力装置を持たないため、視線マーカーを一定時間ボタンなどに向けるとボタンを押したことになる、というシステムが一般的です。しかし、VR Samplesはジョイスティックなどでボタンを押さないと入力できないため、改造が必要です。ここでは、以下のスクリプトを使います。

```csharp:GazeInput.cs

using UnityEngine;
using VRStandardAssets.Utils;

public class GazeInput : MonoBehaviour
{

    private VRInput vrInput;

    void Start()
    {

        var vrObjects = FindObjectsOfType<VRInteractiveItem>();　//シーン中のVRInteractiveItemをすべて見つける。

        foreach (var vrObject in vrObjects)
        {
            vrObject.OnOver += OnOver; //GazeInputのOnOver()をサブスクライブ
            vrObject.OnOut += OnOut;　 //GazeInputのOnOut()をサブスクライブ
        }

        vrInput = FindObjectsOfType<VRInput>()[0];//シーン中のVRInputを見つける。一つのはずだが、複数なら最初のを。

    }

    void OnOver()
    {
        vrInput.DoOnDown();
    }

    void OnOut()
    {
        vrInput.DoOnUp();
    }
}
```

スクリプト中に出てくるVRInteractiveItemはVR Sample付属の、視線の対象になるオブジェクト（MainMenuシーンのMenuItemFlyerなど）にアタッチするコンポーネントです。こちらにはOnOver, OnOutというevent用のAction形のpublic変数があり、視線マーカー（VR SamplesではGUI Reticle）がオブジェクトに重なるとOnOverが、その状態からマーカーがオブジェクトの外に出るとOnOutが実行されます。

GazeInput.csでは、GazeInput.csで定義したOnOver(), OnOut()メソッドをVRInteractiveItemの対応するEventであるOnOver,OnOutにサブスクライブして、それぞれマーカーがオブジェクトに重なったり、外に出たときに実行されるようにします。

さて、GazeInput.csのOnOver(), OnOut()の中身ですが、VRInputのOnDown,OnUpをそれぞれ実行できるようにします。VRInputはVR Sample付属の、Main Cameraにアタッチされるコンポーネントでキーやマウスなどの入力処理に使われます。OnDownはマウス左ボタンを押下すると、OnUpはボタンを離すと実行される、event用のAction形の変数です。ただし、[eventはそれを宣言しているクラスの外からは実行できないため](http://stackoverflow.com/questions/17426369/c-sharp-invoke-event-outside-of-declaring-class)、GazeInputから直接実行できません。そこで、VRInput.csに以下のメソッドを追加します

```csharp:VRInput.cs

        public void DoOnDown()
        {
            if (OnDown != null)
                OnDown();
        }

        public void DoOnUp()
        {
            if (OnUp != null)
                OnUp();
        }

```

これで、GazeInput側のOnOver,OnOutメソッドからVRInputのOnDown,OnUpにサブスクライブされた命令が実行できます。

更に細かい修正点として、SelectionRadial.csのHandleUp()というメソッドで以下のように、if (m_IsSelectionRadialActive)の部分をコメントアウトします。

```csharp:SelectionRadial.cs
        private void HandleUp()
        {

//            if (m_IsSelectionRadialActive)
//            {
                if (m_SelectionFillRoutine != null)
                {
                    StopCoroutine(m_SelectionFillRoutine);
                }

                m_Selection.fillAmount = 0f;
//            }
        }
```
これはオブジェクトから視線を外すときに視線マーカーのタイムカウンタをリセットするのですが、視線を外すとm_IsSelectionRadialActiveもfalseになってしまうため、もとのままだと条件式の中の命令が実行されないためです。

最後に、GazeInput.csをシーン内のゲームオブジェクトにアタッチして実行すると、以下のようになります。

![GVR5.gif](https://qiita-image-store.s3.amazonaws.com/0/58503/d84802d7-e4a6-1ff5-8d8d-c51343053cbb.gif)

ボタンに視線マーカーが入るといきなりカウントダウンが始まってしまうので、難しいと感じる場合は適宜待ち時間を入れると良いでしょう。ちなみに、VR Samplesではダブルクリックなどが必要なところもありますが、それは今回は実装していません。

# VR Samplesの便利なスクリプト

VR Samplesには、ほかのプロジェクトでも使える便利なスクリプトがいくつかあります。私が使っているものを紹介します。

## VR Camera Fade

MainMenuシーンなどのMain Cameraにアタッチされてます。カメラの前にImageを表示させて画面を白<->黒と変えます。

自分のプロジェクトで使う際は、Main Cameraの下にCanvas/Imageオブジェクトが必要です。CanvasのRender ModeはWorld Spaceにしましょう。 そして、ImageをCanvasいっぱいに広げ、位置を微調整し、Main Cameraの前が真っ白になるようにします。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/4186ffa5-a868-7dd3-5298-22d461f9209d.png)

次に、VRCameraFade.csをMain Cameraにアタッチし、Fade Imageに上記のImageを割り当てます。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/a575a396-6bfe-56ff-a861-47c1efedbb64.png)

あとは、下記のようなスクリプトを使って、VRCameraFadeのBeginFadeIn, BeginFadeOutメソッドを呼び出せばフェードイン、アウトができます。

```csharp:VRFadeTest.cs

using UnityEngine;
using System.Collections;
using VRStandardAssets.Utils;

public class VRFadeTest : MonoBehaviour
{

    VRCameraFade VRCameraFade;

    void Start()
    {
        VRCameraFade = FindObjectOfType<VRCameraFade>();
        Invoke("FadeIn",1);
        Invoke("FadeOut",5);
    }

    public void FadeIn()
    {
        StartCoroutine(FadeInCoroutine());
    }
    public void FadeOut()
    {
        StartCoroutine(FadeOutCoroutine());
    }

    private IEnumerator FadeInCoroutine()
    {
        yield return StartCoroutine(VRCameraFade.BeginFadeIn(true));
        Debug.Log("FadeIn Finished");
    }

    private IEnumerator FadeOutCoroutine()
    {
        yield return StartCoroutine(VRCameraFade.BeginFadeOut(true));
        Debug.Log("FadeOut Finished");
    }
}


```

画面のフェードイン、アウトに加え、音のフェードイン、アウトも設定できます。その場合は、Audio Mixer Snapshotの設定が必要です。AssetメニューからCreate/Audio MixerでAudiow Mixerを作ります。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/6c6af71f-24d7-295c-8e9b-01f8ada2fb97.png)

これを、Audio SourceコンポーネントのOutputとして設定します。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/473b4254-6783-a383-1508-73c7e85da51a.png)

次に、Audio Mixerウィンドウを開き、Snapshotsの右の＋ボタンを押して、スナップショットを設定します。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/57a5efa6-b265-efbf-3d11-01042868a50c.png)

フェード前後の音を、とりあえずMax,Minとし、それぞれの音量を決めます

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/1ef2dbce-890c-4377-a132-23c1e18c674d.png)

これらスナップショットを、VRFadeTest.csのDefault Snapshot, Faded Snapshotに割り当てれば、画面のフェードに合わせて音が変化します。BeginFadeIn(true)、BeginFadeIn(true)というように、trueを引数としてメソッドを呼び出せば、音のフェードが有効になります。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/399cfaf9-688b-cbca-4a83-9513f0c5d2df.png)

![GVR7.gif](https://qiita-image-store.s3.amazonaws.com/0/58503/b3381690-461d-a284-4beb-d8c3e15e6111.gif)


## 視線マーカー
VR Samplesのサンプルシーンでは、Main cameraの子オブジェクトであるVR CameraUI, GUIReticle, UISelectionBar, Backgroundという一連のオブジェクトで視線マーカーを表示させています。バラバラに設定すると大変なので、Assets\VRSampleScenes\PrefabsにあるMain Cameraプレハブをそのまま持ってきましょう。Main Cameraには不要なコンポーネントがいくつかあるので、とりあえず、以下のものだけにして、後は削除してください。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/1f5bd1ed-103c-95eb-5a37-b5cff6d0c10d.png)

プレハブの中のGUIReticleのImageにチェックを入れ、UISelectionBarのゲームオブジェクト名の横はアンチェックしましょう。

![image](https://qiita-image-store.s3.amazonaws.com/0/58503/0f1145c6-413c-aa71-51b1-a50ba686b84d.png)

これで実行すると、対象物を見ていない時は点(GUIReticle)、見ているときはタイムカウンタ用の円(UISelectionBar)が出てきます。

![GVR8.gif](https://qiita-image-store.s3.amazonaws.com/0/58503/120c4320-ab0b-6e8b-e55f-e44fff89c1a4.gif)

この視線マーカーにゲームオブジェクトを反応させるには、ゲームオブジェクト側にコライダー、VRInteractiveItemコンポーネントと「見られたとき」の処理を行うためのスクリプトをアタッチする必要があります。例えば、以下のようなものです。

```csharp:SimpleButton.cs
using UnityEngine;
using VRStandardAssets.Utils;

public class SimpleButton : MonoBehaviour
{
    //Modified from MenuButton.cs

    [SerializeField]
    private SelectionRadial selectionRadial;       
    [SerializeField]
    private VRInteractiveItem vrInteractiveItem;       

    private bool gazeOver;   

    private void OnEnable()
    {
        vrInteractiveItem.OnOver += HandleOver;
        vrInteractiveItem.OnOut += HandleOut;
        selectionRadial.OnSelectionComplete += HandleSelectionComplete;
    }

    private void OnDisable()
    {
        vrInteractiveItem.OnOver -= HandleOver;
        vrInteractiveItem.OnOut -= HandleOut;
        selectionRadial.OnSelectionComplete -= HandleSelectionComplete;
    }

    private void HandleOver()
    {
        selectionRadial.Show();
        gazeOver = true;
    }

    private void HandleOut()
    {
        selectionRadial.Hide();
        gazeOver = false;
    }

    private void HandleSelectionComplete()
    {
        if (gazeOver)
            ActivateButton();
    }

    private void ActivateButton()
    {
        Debug.Log("Activate");
    }
}
```

このスクリプトはVR SamplesのMenuButton.csを簡略にしたものです。ポイントは、

* VRIntearactiveItemのOnOver, OnOutに、視線マーカーが乗った時(OnOver)、外れたとき(OnOut)の処理を登録する。
* SelectionRadialのOnSelectionCompleteに、視線マーカーのタイムカウンタがFullになった時の処理を登録する。

ということです。SelectionRadialはVR SamplesのサンプルシーンでMain Cameraにアタッチされているコンポーネントで、視線マーカーのタイムカウンタの処理を行います。ここでは、SimpleButton.csのHandleOver(), HandleOut()メソッドで、視線がオブジェクトに乗った時はタイムカウンタの円を表示(SelectionRadial.Show())し、外れたら消す処理を付けています(SelectionRadial.Hide())。これがないと、タイムカウンタの円は表示されません。

SelectionRadialでタイムカウントをし、一定の時間に達するとselectionRadial.OnSelectionCompleteに登録されている命令が実行されます。SimpleButton.csからはHandleSelectionCompleteメソッドを登録してるので、タイムカウントが終われば、このメソッドが実行されます。

こちらのスクリプトをCubeにアタッチすれば、先ほどの説明の動画のように、Cubeに視線が当たるとタイムカウンタの円が表示されます。

#続きます

他にもVR Samplesには役立つスクリプトがいろいろありますが、長くなったので一旦終了です。もし、もっと良いやり方、新しい活用をご存知でしたら、お知らせいただければ幸いです。