[Oculus] WebVR APIについて調べてみた


[2015.06.03 追記]
[WebVRのドラフト](http://mozvr.github.io/webvr-spec/webvr.html)はGithub上にあるようです。


-------------------------------------------

先日、[FirefoxのNightlyビルドでWebVR APIが実装された](http://news.mynavi.jp/news/2015/01/22/068/)、というニュースがありました。
Oculusを広めたい自分としては色々と興味がそそられるAPIです。

ちなみに、 **Firefox Nightlyビルドで試したところ、Oculusを接続してからFirefoxを起動しないと認識されないようです。** なので、認識されない場合はFirefox自体を再起動することで認識されました。

※ [ChromiumのWebVRを実装したビルド](https://drive.google.com/folderview?id=0BzudLt22BqGRbW9WTHMtOWMzNjQ&usp=sharing#list)もあります。

## 今回作ったもの

せっかくなので、過去に配布されていたジプシー・デンジャーのモデルデータを使ってみました。

![WebVR demo](https://qiita-image-store.s3.amazonaws.com/0/5616/e8c48364-4482-07f0-b001-cf2f6b0c6814.png "WebVR demo")

動画はこちら↓
[![Oculus動画](https://qiita-image-store.s3.amazonaws.com/0/5616/0c0c9a5f-9933-8a1c-cc36-b83db227dd91.jpeg "Oculus動画")](https://www.youtube.com/watch?v=sZvQ_RslvuE&feature=youtu.be)
https://www.youtube.com/watch?v=sZvQ_RslvuE&feature=youtu.be

上記デモは[Githubに公開](https://github.com/edom18/WebVR-demo/)しています。（[実際の動作デモ](http://edom18.github.io/WebVR-demo/)はこちら）

*※ ちなみに海の表現はThree.jsのexampleから拝借しました。VRRendererをそのまま利用すると海の反射がおかしなことになったのでちょっとだけ手を入れています。*


## Firefox NightlyへのWebVRはAdd-onとして提供されている

最初、VR Deviceを取得しようとしてそのAPI（`navigator.getVRDevice`）がなくて焦りました。
が、少し調べてみると、どうやらAdd-onという形で提供されているようです。
（[WEB VR LANDS IN FIREFOX NIGHTLY](http://mozvr.com/posts/webvr-lands-in-nightly/)を参考にしました。[Add-onはこちら](http://mozvr.com/downloads/webvr-oculus-addon-0.4.4.xpi)です。（直接DLされます））

さらにAdd-onを追加しただけではダメで、Add-on追加後、Fileメニューから「New Non-e10s Window」を選択して専用のウィンドウを立ち上げる必要があります。


## Getting Started

New Non-e10s Windowを立ち上げたらいよいよWebVRが利用できるようになります。
こちらの「[Getting started with the WebVR API](http://tyrovr.com/2014/06/29/three-vr-renderer-tutorial.html)」に、ごく簡単な[デモ](http://tyrovr.com/lab/webvr/renderer/)とそのコードが紹介されていました。

デモではThree.jsを組み合わせて実装されています。
Three.jsについての説明は今回は割愛します。デモで使われているWebVR APIとその表示方法に絞って書こうと思います。


## VR Deviceの取得

OculusなどのVR Deviceを取得するには以下のように取得するメソッドを実行します。

```js:getVR
if (navigator.getVRDevices) {
	navigator.getVRDevices().then(vrDeviceCallback);
}
else {
	console.log('WebVR is not supported!');
}
```

*参考にした記事は`mozGetVRDevices`もチェックして実行していましたが、現在のNightlyビルドでは`moz`のプレフィクスはなくなっていました。*

見てもらうと分かりますが、さりげなく`Promise`が使われています。
デバイスの取得が非同期で行われるためでしょう。
そしてそのコールバックでデバイス取得の処理を行います。

```js:vrDeviceCallback
function vrDeviceCallback(vrdevs) {
    for (var i = 0; i < vrdevs.length; ++i) {
        if (vrdevs[i] instanceof HMDVRDevice) {
            vrHMD = vrdevs[i];
            break;
        }
    }
    for (var i = 0; i < vrdevs.length; ++i) {
        if (vrdevs[i] instanceof PositionSensorVRDevice &&
            vrdevs[i].hardwareUnitId == vrHMD.hardwareUnitId) {
            vrHMDSensor = vrdevs[i];
            break;
        }
    }
    initScene();
    initRenderer();
    render();
}
```

最初の`for`ループがHMDの本体を取得するループです。
次の`for`ループは、HMDのセンサーを取得しています。
本体と同じ`hardwreUnitId`を持つものを取得しているのが分かると思います。


## シーンのセットアップ

デバイスが取得できたら、そのままシーンのセットアップを行います。
といっても、ここは普通にThree.jsでのシーンのセットアップとまったく同じです。

```js:setup-scene
function initScene() {
    camera = new THREE.PerspectiveCamera(60, 1280 / 800, 0.001, 10);
    camera.position.z = 2;
    scene = new THREE.Scene();
    var geometry = new THREE.IcosahedronGeometry(1, 1);
    var material = new THREE.MeshNormalMaterial();
    mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
}
```


## レンダラーのセットアップ

続いてレンダラーのセットアップです。
こちらも基本的にはThree.jsのレンダラーのセットアップそのままです。
が、一点だけ異なるのはVR用のレンダラーを用意することです。

```js:renderer-setup
function initRenderer() {
    renderCanvas = document.getElementById("render-canvas");
    renderer = new THREE.WebGLRenderer({
        canvas: renderCanvas,
    });
    renderer.setClearColor(0x555555);
    renderer.setSize(1280, 800, false);
    vrrenderer = new THREE.VRRenderer(renderer, vrHMD);
}
```


## レンダリング

セットアップが終わったらレンダリングを行います。
レンダリングは`vrrenderer`が担当します。
レンダリング時にカメラの位置と回転を、VR Deviceからの値でupdateすれば、OculusのVR体験が実現できる、というわけです。

```js:render
function render() {
    requestAnimationFrame(render);
    mesh.rotation.y += 0.01;
    var state = vrHMDSensor.getState();
    camera.quaternion.set(state.orientation.x, 
                          state.orientation.y, 
                          state.orientation.z, 
                          state.orientation.w);
    vrrenderer.render(scene, camera);
}
```

## 歪み補正

ちなみに、Oculusはレンズを使って歪ませて視野を確保していますが、そのために、画面に出力される内容を、Oculusのレンズに合わせて歪み補正をする必要があります。

その補正を担当してくれているのが`THREE.VRRenderer`というわけです。

補正の詳しい計算については、上記のVRRendererの実装を見てもらうと分かります。
というか、ぶっちゃけなにをしているかよく分かりませんｗ
*（ただ、通常のprojection matrixに似た計算を行っているので、Oculusの歪みを補正するSkew的な処理を入れてるのかも？）*
HMDから得られるFOVから、色々計算していい感じに補正しているようです。


### VRRenderer

デモで使われているThree.jsのプラグインという形で提供されている`THREE.VRRenderer`ですが、コード自体はそれほど多くありません。

大きくやっていることを箇条書すると、

**初期化時**

- HMDの目線位置を取得
- HMDの左右のカメラの`FieldOfView`を取得

**update時**

- 渡されたカメラを2つに複製
- 複製したカメラをそれぞれ左半分、右半分のレンダリングに使う
- 複製したカメラの`projectionMatrix`を、歪み補正をしたものに差し替え
- カメラの位置を目線位置から算出して再計算

という流れでレンダリングを行っているようです。
これを実行することで、Oculusのコンテンツでよく見る、ふたつに画面分割されたものが表示されます。


### HMDのトラッキング

最後に、update時にはHMDの状態をトラッキングし、それをカメラに反映させます。
具体的には以下のコードです。

```js:head-traking
var state = vrHMDSensor.getState();
camera.quaternion.set(state.orientation.x, 
                      state.orientation.y, 
                      state.orientation.z, 
                      state.orientation.w);
```

最初に取得したVR Sensorから現在の状態を取得し、カメラに適用します。


## フルスクリーンモード

上記までで、OculusからのデータをThree.jsの世界に反映することができました。
最後に実装するのはフルスクリーンモードです。特に、モニタとリンクさせている場合はフルスクリーンにしないと正常に見えません。

```js:full-screen
window.addEventListener("keypress", function(e) {
    if (e.charCode == 'f'.charCodeAt(0)) {
        if (renderCanvas.mozRequestFullScreen) {
            renderCanvas.mozRequestFullScreen({
                vrDisplay: vrHMD
            });
        } else if (renderCanvas.webkitRequestFullscreen) {
            renderCanvas.webkitRequestFullscreen({
                vrDisplay: vrHMD,
            });
        }
    }
}, false);
```

デモでは`F`キーを押すことでフルスクリーンになるように実装されています。
ここで行っていることは、フルスクリーン用のAPIに対して、`{ vrDisplay: vrHMD }`として、`vrDisplay`に、デバイス取得で取得したHMDのインスタンスを渡すことです。

これで完成です。