UnityのWebGLを一番シンプルだと思う実装でWebVRに対応させてみた。(アセット追加)

UnityのWebGLはWebVRにはまだ対応していないようです。
そこで、WebVRに対応させてみました。
それには、JS側の実装はもちろんのこと、Unity側にも実装を加えなければなりません。

#Unity側の実装(C#)
まずは、Unity側の実装を解説します。カメラの親オブジェクトにアタッチしたスクリプトファイルに実装することにします。

##カメラの配置
WebVRではノーマルモード時は１つのカメラ、VRモード時にSideBySideのステレオカメラに切り替えてレンダリングを行っています。これと同じことをUnity側で実装しなければなりません。
ノーマルモード時のカメラと、VRモード時ステレオカメラの計３つのカメラを作成します。親のEmpty GameObjectを用意し、この配下に３つのカメラを配置します。ステレオカメラは、位置を左右にずらしてPosition(のX)を設定し、それぞれ左と右の半分づつに表示されるようにViewport Rectを設定します。

![set.png](https://qiita-image-store.s3.amazonaws.com/0/17554/0ff61976-e5b5-57ed-0ad2-48a4d2376211.png)

##getVRDevices()を呼び出す
前は、postRunに設定する方法で解説してたのですが、どうもインスタンスが生成される前に呼び出されるらしく、SendMessageでエラーが発生しました。そこで、インスタンスが生成され手から実行されるよう、ExternalCallを使用して呼び出します。

```csharp:Unity_MonoBehaviour
    void Start()
    {
        Application.ExternalCall("getVRDevices");
    }
```


##トラッキングデータをJSから受け取る
WebVRのヘッドトラッキングおよびポジショントラッキングのデータをJSから受け取ります。
JSから渡す方法として、SendMessage()を利用します。
SendMessage()は、string/int/float型の値を１回の関数実行で１つの値しか渡せません。
ですので、冗長となりますが複数回実行することで渡すことにします。
受け取る値ごとにそれぞれ関数を用意することにしました。


```csharp:Unity_MonoBehaviour
    Vector3 webVREuler = new Vector3();
    Vector3 webVRPosition = new Vector3();

    void euler_x(float val)
    {
        webVREuler.x = val;
    }

    void euler_y(float val)
    {
        webVREuler.y = val;
    }

    void euler_z(float val)
    {
        webVREuler.z = val;
    }

    void position_x(float val)
    {
        webVRPosition.x = val;
    }

    void position_y(float val)
    {
        webVRPosition.y = val;
    }

    void position_z(float val)
    {
        webVRPosition.z = val;
    }
```

##座標系の違い
Unityは左手系の座標を使用しています。それに対して、WebVRのトラッキングデータはWebGLで使用されるのが目的なため右手系となっています。そのため変換が必要となるのですが、positionの場合はZの座標値を反転させるだけでいいのですが、orientationは単純にはいきません。この辺知識不足ですのでググって変換コードを見つけました。
[このページの一番下に書かれているコード:stackoverflow](http://stackoverflow.com/questions/29378998/convert-euler-angles-between-systems-with-different-coordinate-axiss-unity-and)


##モード切替の通知を受け取る
モード切替をJSから受け取る必要があります。これも受け取る関数を用意し、モードによってカメラを切り替えるように実装します。

```csharp:Unity_MonoBehaviour

    void changeMode(string mode)
    {
        switch (mode)
        {
            case "normal":
                cameraMain.GetComponent<Camera>().enabled = true;
                cameraL.GetComponent<Camera>().enabled = false;
                cameraR.GetComponent<Camera>().enabled = false;
                break;
            case "vr":
                cameraMain.GetComponent<Camera>().enabled = false;
                cameraL.GetComponent<Camera>().enabled = true;
                cameraR.GetComponent<Camera>().enabled = true;
                break;
        }
    }
```

##トラッキングデータを反映
JSから送られてきたトラッキングデータをカメラの親であるCameraSetにUpdateイベントで反映させます。positionの値はたぶんメートルを基準にした値となっているのだと思います。ポジショントラッキングがすぐに実感できるようちょっと酔うかもしれませんが、倍率を高くして(x10)反映させています。

```csharp:Unity_MonoBehaviour
    myTransform = this.transform;

    void Update()
    {
        var unityEuler = ConvertWebVREulerToUnity(webVREuler);
        unityEuler.x = -unityEuler.x;
        unityEuler.z = -unityEuler.z;
        myTransform.rotation = Quaternion.Euler(unityEuler);
        var pos = webVRPosition * 10;
        pos.z *= -1;
        myTransform.position = pos;
    }
```

#JS側の実装
次にJS側の実装の解説です。ここでは、DefaultテンプレートでWebGLビルドしたindex.htmlを改造する方法で解説を行います。

##JSコードの実装位置
以降で解説するすべてのJSコードの実装位置ですが、index.htmlにはベタにコードが記述されているscriptタグが２つありますが、上の// connect to canvasというコメントで始まる方のこのコメントの前に実装します。

##フルスクリーンボタンをWebVRボタンに変更
Defaultテンプレートにはフルスクリーンボタンがありますが、これをWebVRボタンに変更します。
インラインで実装されているonclickのコードをonclick="webvr_click()"に変更し、イベントハンドラの関数を実装します。

```js:WebVRボタンクリックイベントハンドラ
    var canvas = document.getElementById('canvas');

    function webvr_click() {
      if(!vrHMD) {
        alert('VRデバイスが取得できませんでした。');
        return;
      }
      if (canvas.mozRequestFullScreen) {
        canvas.mozRequestFullScreen({ vrDisplay: vrHMD });
      } else if (canvas.webkitRequestFullscreen) {
        canvas.webkitRequestFullscreen({ vrDisplay: vrHMD });
      }
    }
```

##リセットセンサーボタンの追加
これは、JSではなくほぼHTMLでの実装となります。
配置位置を考えるのが面倒なので、いっそのこと左上隅に配置するようにします。
これはscriptタグ内ではまずいのでbodyタグ配下の適当な場所に配置します。

```html:HTML
 <div style="position:absolute; cursor:pointer; left:0; top:0; font-family:Meiryo; font-weight:bold; padding:5px; height:30px; line-height:30px; margin-bottom:10px; background:rgb(30,174,211); color:white;" onclick="vrSensor && vrSensor.resetSensor()">リセットセンサー</div>
```

##モード切替をUnityに通知する
WebVRボタンのクリックイベントだけではVRモード解除が判断できないため、fullscreenchangeイベントハンドラを設定し、ここでどのモードになったか判断し、Unityに通知します。

```js:JS
    var fullscreenchange = canvas.mozRequestFullScreen ? 'mozfullscreenchange' : 'webkitfullscreenchange';

    document.addEventListener(fullscreenchange, function (event) {
      if (document.mozFullScreenElement || document.webkitFullscreenElement) {
        SendMessage('CameraSet', 'changeMode', 'vr');
      } else {
        SendMessage('CameraSet', 'changeMode', 'normal');
      }
    }, false);
```

##VRデバイス取得
VRデバイスを取得します。すぐには実行されないようにするため関数(getVRDevices)にして実装します。呼び出すタイミングは後述します。
HMDデバイスおよびセンサーデバイス両方が取得できたら、トラッキングデータの取得を開始します(getVRSensorState())。

```js:JS
    var vrHMD, vrSensor;

    function getVRDevices() {
      if (navigator.getVRDevices) {
        navigator.getVRDevices().then(function (devices) {
          for (var i = 0; i < devices.length; i++) {
            if (devices[i] instanceof HMDVRDevice) {
              vrHMD = devices[i];
              break;
            }
          }
          for (var i = 0; i < devices.length; i++) {
            if (devices[i] instanceof PositionSensorVRDevice &&
              vrHMD.hardwareUnitId == devices[i].hardwareUnitId) {
              vrSensor = devices[i];
              getVRSensorState();
              break;
            }
          }
        });
      }
    }
```

##トラッキングデータを取得し、Unityに送る
毎フレーム取得するため、requestAnimationFrameを使ってループし、取得したトラッキングデータをUnityに送ります。
(ただ、quaternionからeulerを取得するのにthree.jsを利用しているのですが、three.jsを利用するのはここ１か所のみで、これだけのために利用するのはちょっと気が引けるのですが私は術を持ち合わせていないため仕方なく利用します)

```js:JS
    function getVRSensorState() {
      requestAnimationFrame(getVRSensorState);
      var state = vrSensor.getState();
      var euler = new THREE.Euler().setFromQuaternion(state.orientation);
      SendMessage('CameraSet', 'euler_x', euler.x);
      SendMessage('CameraSet', 'euler_y', euler.y);
      SendMessage('CameraSet', 'euler_z', euler.z);
      if (state.position != null) {
        SendMessage('CameraSet', 'position_x', state.position.x);
        SendMessage('CameraSet', 'position_y', state.position.y);
        SendMessage('CameraSet', 'position_z', state.position.z);
      }
    }
```

#これで、WebVRに対応させることができます。
もともと実装してあるものを後からWebVRに対応させるにはこの解説の通りの実装では難しいところが出て来ると思います。その場合は適宜、方法を変えて実装してください。
今回は省きましたが、このほかにもeyeTranslation(左右の目の中心からの距離)やrecommendedFieldOfView(左右の目のFOV)の値をWebVRのデバイスから取得することができるのでこれをUnity側に反映させるともっとよくなると思います。

#アセットにしてみました
[github](https://github.com/gtk2k/Unity-WebVR-Test-Assets)にアセットを公開しました。これでサクッとWebVRを試せると思います。

#サンプル
実際にWebVRに対応させた [サンプルのページ](https://googledrive.com/host/0B88pXCjq3N8QfkZXQXpPZVlBeU5ZNk9mN0VQVHQ4RklpbUUyQ09ZTnZCcTIwWXoyTktEZU0) を用意しました。
動作の確認にはWebVRビルド版のChromeまたはNightly版のFirefoxを用意します。
両者ともにWebVRがデフォルトでは有効になっていません。DLからWebVRを有効にするまでを簡単に説明します。

##WebVRビルド版のChrome
WebVRビルド版Chromeは [こちら](https://drive.google.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ#list) から、最新のビルドをDLし実行します。
デフォルトではWebVRが有効になっていません。chrome://flagsにアクセスし、"Enable WebVR"という項目がありますので"有効にする"リンクをクリックして再起動することでWebVRが有効になります。
ChromeはOculus Riftのダイレクトモード(Direct HMD Access from Apps)および拡張モード(Extend Desktop to the HMD)両方のディスプレイモードに対応しています。

##Nightly版のFirefox
[こちら](http://mozvr.com/downloads/) のページにアクセスして、まず"Firefox Nightly"をクリックしてインストールします。インストールが終わったら起動し、Firefoxから再度先ほどのページにアクセスし、"WebVR Enabler Add-on"をクリックしてアドオンをインストールします。続いて、右上のメニューボタンをクリックし"New e10s Wndow"をクリックします。新たにウィンドウが起動しますので
このウィンドウでWebVRコンテンツにアクセスします。ただし、現在のところFirefox拡張モードにしか対応していません。

以上

2015/04/28 修正
初出時Firefoxではサンプルページが動作しないと書きましたが、マシン再起動して再度試したらFirefoxでも動作しました。私のマシン環境によるのかもしれませんが、初回アクセス時は特にそうですが何度か試すと時々Oculus Riftに映像が出ない現象が発生しまます。

2015/04/30 修正
getVRDevice()呼び出しタイミング(JS実行タイミング)をpostRunからUnityのStartイベントに修正