この記事は編集レビューを必要としています。ぜひご協力ください。
草案
このページは完成していません。
WebVR API はウェブ開発者のツールキットへのすばらしい追加機能で、Oculus Rift のようなバーチャルリアリティハードウェアへのアクセスが可能となります。そして出力された動きや向きはウェブアプリの描画更新に変換されます。しかし VR アプリを開発はどのようにやればいいのでしょうか? この記事では、それに関する基礎的な解説を行います。
注記: WebVR は現在実験的な段階にあります(最新の仕様はこちらにあります); 今の段階でもっとも正常に動作するのは Firefox Nightly/Developer Edition で、一部の機能は Google Chrome でも動作します。詳細は Brandon Jonesの Bringing VR to Chrome を参照してください。
始めるには
WebVRを始めるには,VRハードウェアのマニュアルに従ったセットアップと、WebVR environment setup に示されているコンピュータへの設定が必要になります、スムースな動作には専用GPUが推奨されます。
Firefox Nightly (または Developer Edition) のインストールと合わせて WebVR Enabler Add-on も必要となります。
いちど環境が設定できたら、テストのために私たちの MozVR projects を開いて、[Enter VR] ボタンをクリックすることを試してください。
注記: より深い情報のために,WebVR environment setup をチェックしてください。
注記: モバイルデバイスを HMD として用いるような安価な選択肢もあります。この場合,位置センサは利用できませんので、代わりに deviceorientation API を用いて擬似的な向きデータを使う必要があるかもしれません。
簡単なデモ
WebVR のデモは MozVR team repo や MDN webvr-tests repo にたくさんありますが、この記事では、主にpositionsensorvrdevice について (動作しているデモ) を例に解説します。
これは簡単な 2.5D のデモで,HTML5 Canvas にレンダリングされた Firefox ロゴが右目と左目のビューに表示されるものです.VR HMDでデモを見ているときにキャンバスをクリックすると、デモはフルスクリーンになり、Firefox ロゴに近づけるようになります。あなたが動くと頭の動きに合わせて上下左右や回転してリアルに動きます。
あなたが WebVR のコードがどう動いているかを簡単に確認できるように、デモは意図的にシンプルになるよう保持されています。API は十分シンプルであるため,単純な DOM ベースインターフェイスでも複雑な WebGL シーンでも、好きなアプリに WebVR 制御の移動を簡単に適用できます。
アプリはどう動く?
このセクションでは、アプリを動作させるために必要なコードの変更箇所を通じて、基礎的なレベルで何が必要かを知ることができます。
VRデバイスへのアクセス
最初にコンピュータに接続中のVRハードウェアへのプログラム的な参照を取得します。それには接続中の全 VR デバイスの配列へと解決できるプロミスを返す Navigator.getVRDevices
を使います。
返される可能性のあるオブジェクトが2種類あります:
PositionSensorVRDevice
: 位置センサカメラ。HMDVRDevice
: VRヘッドマウントディスプレイ。
vrdevice demo で基本的なデバイス情報を表示するための非常に簡単なコードを見ることができます。
本当に欲しいものはデバイスのペアを取得するものです (将来のマルチプレイヤVRゲームでは複数のペアになるかもですが)。WebVR 仕様からもってきた(そして positionsensorvrdevice デモでも使っている)次のコードはかなりよく使うトリックです:
var gHMD, gPositionSensor; navigator.getVRDevices().then(function(devices) { for (var i = 0; i < devices.length; ++i) { if (devices[i] instanceof HMDVRDevice) { gHMD = devices[i]; break; } } if (gHMD) { for (var i = 0; i < devices.length; ++i) { if (devices[i] instanceof PositionSensorVRDevice && devices[i].hardwareUnitId === gHMD.hardwareUnitId) { gPositionSensor = devices[i]; break; } } } });
最初に見つかった HMDVRDevice
のインスタンスを取得し、それを gHMD
変数へ保存します.次に見つかった PositionSensorVRDevice
のインスタンスを取得して gPositionSensor
変数に代入していますが,それは先ほど取得した gHMD
オブジェクトの VRDevice.hardWareUnitId
プロパティが一致するものだけを対象にしています。同一のハードウェアは複数のデバイスとして取得されますが、それらはハードウェアユニットIDを共有しています — これは取得した 2 つのデバイスの参照がマッチングしているかをチェックする方法です。
アプリの初期化
シーンを描画する <canvas>
要素を次のように作成し、配置します:
var myCanvas = document.createElement('canvas'); var ctx = myCanvas.getContext('2d'); var body = document.querySelector('body'); body.appendChild(myCanvas);
次に、新しい image を作成し、アプリの main loop であるdraw()を実行する前に
image が ロードされているかをチェックするために
load
イベントを使います:
var image = new Image(); image.src = 'firefox.png'; image.onload = draw;
メインループ
draw()
は次のように実装します:
function draw() { WIDTH = window.innerWidth; HEIGHT = window.innerHeight; lCtrOffset = WIDTH*0.25; rCtrOffset = WIDTH*0.25; myCanvas.width = WIDTH; myCanvas.height = HEIGHT; setView(); drawImages(); drawCrosshairs(); requestAnimationFrame(draw); }
window の WIDTH
と HEIGHT
は各フレームでリサンプリングされ,次の設定に使われます:
- 左右の目のビュー中心からの相対的に描画される画像を維持するのに使われる左右のオフセット値です。半分の幅のシーンのコピーを描画するので、各コピーの中心はそれぞれ、エッジの端から端までのキャンバス全体幅のちょうど1/4の幅になります。
- キャンバスの width と height。
これによってブラウザウィンドウがリサイズされたとしても、シーンが正しくリサイズされます。
次にメインループの中で3つの関数を実行しています:
setView()
は,VR ハードウェアから位置と向きの情報を受け取り,シーン内の更新された画像位置の描画に使用する準備をします。drawImages()
はシーンを更新された位置で実際に描画します。drawCrosshairs()
は常にシーンの中央にある十字線を描画します。
これらの詳細は、後ほど解説します。
ループの最後に requestAnimationFrame(draw) を実行し、draw()
ループが連続して呼び出されるようにします。
位置と向き情報の受取り
では setView()
関数の詳細を見ていきましょう。コードの各部分を順に追って、そこで何をしているかを説明します:
function setView() { var posState = gPositionSensor.getState();
位置センサへの参照を使って PositionSensorVRDevice.getState
を呼び出します。このメソッドは、あなたが知りたい現在のHMDの状態のすべてを返します — VRPositionState
オブジェクトへのアクセスを通じて — 位置、向き、そして速度/ 加速度や角速度 / 角加速度のようなより高度な情報を含んでいます。
if(posState.hasPosition) { posPara.textContent = 'Position: x' + roundToTwo(posState.position.x) + " y" + roundToTwo(posState.position.y) + " z" + roundToTwo(posState.position.z); xPos = -posState.position.x * WIDTH * 2; yPos = posState.position.y * HEIGHT * 2; if(-posState.position.z > 0.01) { zPos = -posState.position.z; } else { zPos = 0.01; } }
HMDのスイッチがOFFにされたり位置センサを向いていなかったりした場合など、アプリがエラーになったり停止したりしないように、 VRPositionState.hasPosition
を使って HMD の正常な位置情報が利用可能かを確認する方法をチェックします。
そして通知を目的として、アプリのUI内のパラグラフへ現在の位置情報を出力します。読みやすくするために、カスタム関数を使って小数点以下 2 桁に丸めています。
最後に VRPositionState.position
に格納されている位置情報に関して、xPos
、 yPos
、zPos
変数に代入します。zPos
の値を 0.01 以上にするのに if ... else
ブロックが利用されていることに気付くでしょう — このアプリは0以下になると例外を投げていました。
if(posState.hasOrientation) { orientPara.textContent = 'Orientation: x' + roundToTwo(posState.orientation.x) + " y" + roundToTwo(posState.orientation.y) + " z" + roundToTwo(posState.orientation.z); xOrient = posState.orientation.x * WIDTH; yOrient = -posState.orientation.y * HEIGHT * 2; zOrient = posState.orientation.z * 180; }
次に同じような処理をして、HMDの向きに応じてシーンの更新処理をします — VRPositionState.hasOrientation
を使って有効な向きデータかをチェックして,向きのデータを通知用のUIに表示し、xOrient
、yOrient
、zOrient
の値を VRPositionState.orientation
に格納されている値から設定します.
timePara.textContent = 'Timestamp: ' + Math.floor(posState.timeStamp); }
最後に VRPositionState.timeStamp
に格納されている現在のタイムスタンプを通知 UI に出力します。この値は位置データが更新済みか、どんな順序で更新が発生したかを判断するのに役立ちます。
シーンの更新
setView()
で取得された xPos
、yPos
、zPos
、xOrient
、yOrient
、zOrient
の値は、drawImages()
で行われるシーン病がの更新のための変更値として使用されます。どうやっているかを見ていきますが、左目のビューの描画コードだけをウォークスルーしていきます。右目については、右にオーバーシフトしている以外はほぼ同じです:
function drawImages() { ctx.fillStyle = 'white'; ctx.fillRect(0,0,WIDTH,HEIGHT);
最初に次のフレームが描画される前にシーンをクリアするため、白い fillRect()
を描画します。
ctx.save(); ctx.beginPath(); ctx.translate(WIDTH/4,HEIGHT/2); ctx.rect(-(WIDTH/4),-(HEIGHT/2),WIDTH/2,HEIGHT);
次に左目のビューを別の画像として扱って右目のビューに影響を与えないコードにするので、 save()
でコンテキスト状態を保存します。
そして pathを開始し
, canvasを変換します
、これによって原点を左目の中心(全体の1/4幅で半分の高さ)に移動させます。回転を正しく動かすためにもこれは必要です。回転はcanvasの原点が中心となります。そして左目のビュー全体を覆うように rect()
を描画します。
rect()
はマイナスの 1/4 幅,マイナスの1/2 高さから描画し始めていることに注意してください。これは原点が既に移動しているためです。
ctx.clip();
canvas を clip()
します。rect()
が描画された直後にこれを呼ぶので、キャンバス上に対して行うことは rect() の内側に制限され
、restore()
が呼び出されるまですべてのオーバーフローは隠蔽されます(後述)。これは左ビュー全体が右ビューから独立したままであることを保証します。
ctx.rotate(zOrient * Math.PI / 180);
頭の回転と同じようにシーンを回転させるために、zOrientの値に従った回転が画像に適用します。
ctx.drawImage(image,-(WIDTH/4)+lCtrOffset-((image.width)/(2*(1/zPos)))+xPos-yOrient,-((image.height)/(2*(1/zPos)))+yPos+xOrient,image.width*zPos,image.height*zPos);
実際に画像を描画しましょう! この少し厄介なコードを、ここでは引数ごとに分解してみましょう:
image
: 描画する画像-
-(WIDTH/4)+lCtrOffset-((image.width)/(2*(1/zPos)))+xPos-yOrient
: 画像原点の水平座標。前に行った平行移動を打ち消すためにWIDTH/4
を引きます.そして中心に戻すために左中心オフセットを加えて,画像幅をzPos
の逆数の2倍で割ったものを引きます— 描画する画像が小さい(大きい)ほど減算値が小さい(大きい)くなり,画像中心が保持されます.最後に,HMD の水平方向の動きや回転にあわせて画像位置を更新するためにxPos
を加えて,yOrient
を引きます(y軸周りの回転が画像を水平方向に移動します)。 -((image.height)/(2*(1/zPos)))+yPos+xOrient
: 画像原点の垂直方向の座標です。これはIn this case the "HEIGHT/2の減算"と"右中心オフセットの追加"は、ちょうどお互いにキャンセルされるので、計算式から取り除きます。計算式の残りは上と同じように、zPos の逆数の 2 倍で画像幅を割ったものを減算することによる画像中心を保持と、yPos
とxOrient
による描画位置の修正です。image.width*zPos
: 画像を描画する幅; 近いものほど大きく描画されるようにzPos
で修正します。image.height*zPos
: 画像を描画する高さ; 近いものほど大きく描画されるようにzPos
で修正します。
ctx.strokeStyle = "black"; ctx.stroke();
左目ビューの周囲に黒い stroke()
を描画します。これはビューの分離をちょっとだけわかりやすくする手助けとなります。
ctx.restore();
右目ビューの描画の実施に移行するため、キャンバスの復元を restore()
で行います。
... }
注記: ここである種のチートをしていて,2D キャンバスを使って3Dシーンを擬似的に表現しています。学習目的の場合、物事を簡単にすることができます。WEBテクノロジで作成された任意のアプリで、ビューレンダリングを修正するために上述した位置と向きのデータを使うことができます。例えば 3Dpositionorientation デモでは、Three.js を使って作成されたWebGLシーンのビューを制御するために上述の方法と非常によく似たコードを使っています。
注記: drawCrosshairs() のコード
は drawImages()と比較して
非常にシンプルですので、もし興味があるなら自分自身で勉強することをおすすめします!
フルスクリーン表示
VRエフェクトはアプリを フルスクリーンモード で実行すると非常に効果的です。ディスプレイのダブルクリックやボタンの押下のような、特定のイベントが発生した時に <canvas>
要素をフルスクリーンにするための一般的な設定を説明します。
シンプルさを保つために、ここではキャンバスのクリック時に fullScreen()
関数を実行します:
myCanvas.addEventListener('click',fullScreen,false);
fullScreen()
関数は、できるだけ互換性を保つために、ブラウザによって異なるキャンバスに実装されている requestFullscreen()
メソッドのバージョンをチェックして、見つかった適切な関数を呼び出します:
function fullScreen() { if (myCanvas.requestFullscreen) { myCanvas.requestFullscreen(); } else if (myCanvas.msRequestFullscreen) { myCanvas.msRequestFullscreen(); } else if (myCanvas.mozRequestFullScreen) { myCanvas.mozRequestFullScreen(); } else if (myCanvas.webkitRequestFullscreen) { myCanvas.webkitRequestFullscreen(); } }
FOV とデバイスのキャリブレーション
現在のデモではあまり考えませんでしたが,商用アプリでは,ユーザが持っているVRハードウェアを正しく動作させるためにユーザキャリブレーションをする必要があるでしょう.WebVR API はそれを手助けする多くの機能があります。
HMDの位置と姿勢をリセットするために PositionSensorVRDevice.resetSensor
メソッドを利用できます。実行すると、現在のヘッドセットの位置/向きが 0 にセットされます。実行前に,ヘッドセットが検知可能な位置にあることを保証する必要があります。positionsensorvrdevice demo*** では、[Reset Sensor] ボタンでそれを実行することができます:
<button>Reset Sensor</button>
document.querySelector('button').onclick = function() { gPositionSensor.resetSensor(); }
他にもヘッドセットの視野角 (FOV) を、シーン内で上,右,下,左方向に見える範囲がどの程度かキャリブレーションします。それぞれの目のパラメータを別々に返す HMDVRDevice.getEyeParameters
メソッドを呼ぶと、両目それぞれの情報を個別に受け取ることができます。なお左目用パラメータで 1 回、右目用で 1 回の計 2 回の呼出しが必要です。それぞれの目用に VREyeParameters
オブジェクトを返します。
一例として、 VREyeParameters.currentFieldOfView
を用いて片目分の現在のFOV を受け取れます。これは次の 4 つのプロパティを持つ VRFieldOfView
オブジェクトを返します:
upDegrees
: FOVの上方向へ広がる角度の値.rightDegrees
: FOVの右方向へ広がる角度の値.downDegrees
: FOVの下方向へ広がる角度の値.leftDegrees
: FOVの左方向へ広がる角度の値.
FOVは眼を頂点としたピラミッド形になっています.
あなたのアプリに適切なFOVをユーザが持っているかをチェックし,もしそうでないなら HMDVRDevice.setFieldOfView
メソッドを使って新しいFOVを設定します.これを扱う簡単な関数は次のような感じです:
function setCustomFOV(up,right,down,left) { var testFOV = new VRFieldOfView(up,right,down,left); gHMD.setFieldOfView(testFOV,testFOV,0.01,10000.0); }
この関数は引数として4つの角度を受け取り、VRFieldOfView() コンストラクタを用いて新しい VRFieldOfView
オブジェクトを作成します。これを setFieldOfView()
の最初の2つの引数(左目と右目のFOV)として渡します。第 3、4 引数は,FOV のオブジェクト可視領域を定義する眼からの最短、最大距離を示す zNear
と zFar
です.