バーチャル リアリティがウェブに登場(パート 2)

フレームループの詳細

Joe Medley
Joe Medley

最近、WebXR Device API の基本コンセプトを紹介する記事「バーチャル リアリティがウェブに登場」を公開しました。また、XR セッションのリクエスト、開始、終了の手順も説明しました。

この記事では、ユーザー エージェントが制御する無限ループで、コンテンツが画面に繰り返し描画されるフレームループについて説明します。コンテンツはフレームと呼ばれる個別のブロックで描画されます。連続したフレームによって、動きがあるように見えます。

WebXR アプリのフレームループ中にコンテンツをレンダリングする唯一の方法は、WebGL と WebGL2 です。幸い、多くのフレームワークは、WebGL と WebGL2 の上に抽象化レイヤを提供しています。このようなフレームワークには、three.jsbabylonjsPlayCanvas などがあります。A-FrameReact 360 は、WebXR とのやり取り用に設計されています。

この記事は、WebGL やフレームワークのチュートリアルではありません。Immersive Web Working Group の Immersive VR Session サンプル(デモソース)を使用して、フレームループの基本について説明します。WebGL またはいずれかのフレームワークを詳しく知りたい場合は、インターネットで記事のリストを確認できます。

プレーヤーとゲーム

フレームループを理解しようとすると、細かい部分で迷ってしまいます。多くのオブジェクトが使用されており、そのうちの一部は他のオブジェクトの参照プロパティでのみ名前が付けられています。混乱しないように、オブジェクト(ここでは「プレーヤー」と呼びます)について説明します。次に、それらの相互作用について説明します。これを「ゲーム」と呼びます。

プレーヤー

XRViewerPose

ポーズとは、3D 空間における物体の位置と向きです。視聴者と入力デバイスの両方にポーズがありますが、ここでは視聴者のポーズに注目します。ビューアと入力デバイスの両方の姿勢には、位置をベクトルとして、向きを原点に対するクォータニオンとして記述する transform 属性があります。オリジンは、XRSession.requestReferenceSpace() を呼び出すときにリクエストされた参照空間タイプに基づいて指定されます。

参照空間については、説明に少し時間がかかります。詳しくは、拡張現実をご覧ください。この記事のベースとして使用しているサンプルでは、'local' 参照空間を使用しています。つまり、床が明確に定義されていないセッション作成時の視聴者の位置が原点になり、その正確な位置はプラットフォームによって異なる場合があります。

XRView

ビューは、仮想シーンを撮影するカメラに対応しています。ビューには、ベクトルとしての位置と向きを記述する transform 属性もあります。これらは、ベクトル/クォータニオンのペアと同等のマトリックスの両方で提供されます。コードに最も適した表現を選択して使用できます。各ビューは、デバイスが視聴者に画像を表示するために使用するディスプレイまたはディスプレイの一部に対応します。XRView オブジェクトは、XRViewerPose オブジェクトから配列で返されます。配列内のビューの数は異なります。モバイル デバイスでは、AR シーンには 1 つのビューがあり、デバイスの画面を覆う場合もあれば、覆わない場合もあります。ヘッドセットには通常、左右の目に 1 つずつ、2 つのビューがあります。

XRWebGLLayer

レイヤは、ビットマップ イメージのソースと、デバイスでそれらの画像をレンダリングする方法の説明を提供します。この説明では、このプレーヤーの機能が十分に説明されていません。デバイスと WebGLRenderingContext の間の仲介者と考えるようになりました。MDN もほぼ同じ見解で、2 つの間に「リンクを提供」すると述べています。そのため、他のプレーヤーにアクセスできます。

一般に、WebGL オブジェクトは 2D グラフィックと 3D グラフィックのレンダリング用の状態情報を格納します。

WebGLFramebuffer

フレームバッファは、WebGLRenderingContext に画像データを提供します。XRWebGLLayer から取得したら、現在の WebGLRenderingContext に渡すだけです。bindFramebuffer() を呼び出す場合(後述)を除き、このオブジェクトに直接アクセスすることはありません。XRWebGLLayer から WebGLRenderingContext に渡すだけです。

XRViewport

ビューポートは、WebGLFramebuffer 内の長方形領域の座標とサイズを提供します。

WebGLRenderingContext

レンダリング コンテキストは、キャンバス(描画対象のスペース)へのプログラムによるアクセス ポイントです。これを行うには、WebGLFramebuffer と XRViewport の両方が必要です。

XRWebGLLayerWebGLRenderingContext の関係に注目してください。1 つは視聴者のデバイスに対応し、もう 1 つはウェブページに対応します。WebGLFramebufferXRViewport は前者から後者に渡されます。

XRWebGLLayer と WebGLRenderingContext の関係
XRWebGLLayerWebGLRenderingContext の関係

ゲーム

プレイヤーの属性がわかったところで、次は彼らがプレイするゲームについて見てみましょう。フレームごとに最初からやり直すゲームです。フレームは、基盤となるハードウェアに依存するレートで発生するフレームループの一部であることを思い出してください。VR アプリケーションの場合、フレームレートは 60 ~ 144 の範囲で指定できます。Android 向け AR は 30 fps で動作します。コードで特定のフレームレートを前提としない。

フレームループの基本的なプロセスは次のとおりです。

  1. XRSession.requestAnimationFrame() を呼び出します。ユーザー エージェントは、ユーザーが定義した XRFrameRequestCallback を呼び出します。
  2. コールバック関数内:
    1. XRSession.requestAnimationFrame() をもう一度呼び出します。
    2. 視聴者のポーズを取得します。
    3. XRWebGLLayer から WebGLRenderingContextWebGLFramebuffer を渡します(「バインド」します)。
    4. XRView オブジェクトを反復処理し、XRWebGLLayer から XRViewport を取得して WebGLRenderingContext に渡します。
    5. フレームバッファに何かを描画します。

ステップ 1 と 2a は前回の記事で説明したので、ステップ 2b から始めます。

視聴者のポーズを取得する

言うまでもないかもしれませんが、AR や VR で何かを描画するには、視聴者の位置と視線の向きを知る必要があります。ビューアの位置と向きは、XRViewerPose オブジェクトによって提供されます。現在のアニメーション フレームで XRFrame.getViewerPose() を呼び出して、視聴者のポーズを取得します。セッションの設定時に取得した参照空間を渡します。このオブジェクトから返される値は、現在のセッションに入ったときにリクエストした参照空間を基準としています。ポーズをリクエストするときに、現在の参照空間を渡す必要があることを思い出してください。

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
   
// Render based on the pose.
 
}
}

ユーザーの全体的な位置を表す視聴者のポーズが 1 つあります。つまり、視聴者の頭またはスマートフォンの場合はスマートフォンのカメラです。ポーズは、視聴者の位置をアプリに伝えます。実際の画像レンダリングでは XRView オブジェクトが使用されます。これについては後で説明します。

次に進む前に、システムがトラッキングを失ったり、プライバシー上の理由でポーズをブロックしたりした場合に備えて、視聴者のポーズが返されたかどうかをテストします。トラッキングとは、XR デバイスが、デバイス自体や入力デバイスが環境に対してどこにあるかを把握する機能です。トラッキングが失われる原因はいくつかあり、トラッキングに使用した方法によって異なります。たとえば、ヘッドセットやスマートフォンのカメラがトラッキングに使用されている場合、暗い場所や光がまったくない場所では、またはカメラが覆われている場合は、デバイスの位置を特定できなくなることがあります。

プライバシー上の理由でポーズをブロックする例として、ヘッドセットに権限プロンプトなどのセキュリティ ダイアログが表示されている場合、この処理中にブラウザがアプリへのポーズの提供を停止することがあります。ただし、すでに XRSession.requestAnimationFrame() を呼び出しているので、システムが復元できればフレームループは続行されます。そうでない場合、ユーザー エージェントはセッションを終了し、end イベント ハンドラを呼び出します。

少し回り道

次のステップでは、セッションのセットアップ中に作成したオブジェクトが必要です。キャンバスを作成し、XR 互換の Web GL レンダリング コンテキストを作成するように指示しました。このコンテキストは canvas.getContext() を呼び出して取得しました。すべての描画は、WebGL API、WebGL2 API、または Three.js などの WebGL ベースのフレームワークを使用して行われます。このコンテキストは、XRWebGLLayer の新しいインスタンスとともに、updateRenderState() を介してセッション オブジェクトに渡されました。

let canvas = document.createElement('canvas');
// The rendering context must be based on WebGL or WebGL2
let webGLRenContext
= canvas.getContext('webgl', { xrCompatible: true });
xrSession
.updateRenderState({
    baseLayer
: new XRWebGLLayer(xrSession, webGLRenContext)
 
});

WebGLFramebuffer を渡す(「バインド」する)

XRWebGLLayer は、WebXR での使用に特化して提供され、レンダリング コンテキストのデフォルト フレームバッファに代わる WebGLRenderingContext のフレームバッファを提供します。これは WebGL では「バインディング」と呼ばれます。

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
// Iterate over the views
 
}
}

各 XRView オブジェクトを反復処理する

ポーズを取得してフレームバッファをバインドしたら、ビューポートを取得します。XRViewerPose には、ディスプレイまたはディスプレイの一部を表す XRView インターフェースの配列が含まれています。デバイスと視聴者に対して正しく配置されたコンテンツをレンダリングするために必要な情報(視野、アイ オフセット、その他の光学特性など)が含まれています。2 つの目のために描画するため、2 つのビューがあり、ループしてそれぞれに個別の画像を描画します。

スマートフォンベースの拡張現実を実装する場合、ビューは 1 つだけですが、ループは使用します。1 つのビューを反復処理するのは無意味に思えるかもしれませんが、これにより、さまざまな没入型エクスペリエンスに単一のレンダリング パスを使用できます。これは、WebXR と他の没入型システムの重要な違いです。

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
for (let xrView of xrViewerPose.views) {
     
// Pass viewports to the context
   
}
 
}
}

XRViewport オブジェクトを WebGLRenderingContext に渡す

XRView オブジェクトは、画面で検出可能なものを指します。ただし、そのビューに描画するには、デバイス固有の座標とサイズが必要です。フレームバッファと同様に、XRWebGLLayer からリクエストして WebGLRenderingContext に渡します。

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
for (let xrView of xrViewerPose.views) {
      let viewport
= glLayer.getViewport(xrView);
      webGLRenContext
.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
     
// Draw something to the framebuffer
   
}
 
}
}

webGLRenContext

この記事を書く際に、webGLRenContext オブジェクトの命名について、同僚と議論しました。サンプル スクリプトとほとんどの WebXR コードでは、この変数を gl と呼び出します。サンプルを理解しようとしたときに、gl が何を指しているかを忘れ続けました。WebGLRenderingContext のインスタンスであることを学習中に思い出せるように、webGLRenContext としています。

これは、gl を使用すると、コンパイル済み言語で VR の作成に使用される OpenGL ES 2.0 API の同等のメソッド名のようにメソッド名を設定できるためです。この事実は、OpenGL を使用して VR アプリを作成したことがあれば明らかですが、このテクノロジーを初めて使用する場合は混乱する可能性があります。

フレームバッファに何かを描画する

野心的な場合は、WebGL を直接使用することもできますが、おすすめしません。上部に記載されているフレームワークのいずれかを使用する方がはるかに簡単です。

まとめ

WebXR の更新や記事はこれで終わりではありません。MDN で、WebXR のすべてのインターフェースとメンバーのリファレンスを確認できます。インターフェース自体の今後の機能強化については、Chrome のステータスで個々の機能の最新情報をご確認ください。

写真提供: JESHOOTS.COMUnsplash