バーチャル リアリティがウェブに登場(パート 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 はほぼ同じ見方を持ち、両者の間に「リンクを提供する」と述べています。そのため、他のプレーヤーにアクセスできます。

一般に、WebGL オブジェクトは、2D および 3D グラフィックをレンダリングするための状態情報を保存します。

WebGLFramebuffer

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

XRViewport

ビューポートは、WebGLFramebuffer 内で長方形領域の座標と寸法を指定します。

WebGLRenderingContext

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

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

XRWebGLLayer と WebGLRenderingContext の関係
XRWebGLLayerWebGLRenderingContext の関係

試合

プレーヤーを特定できたところで、プレイするゲームについて見てみましょう。フレームごとに最初からやり直すゲームです。フレームは、基盤となるハードウェアに応じたレートで発生するフレームループの一部であることを思い出してください。VR アプリの場合、1 秒あたりのフレーム数は 60 ~ 144 の任意の値に設定できます。Android 向け AR は 30 fps で動作します。コードで特定のフレームレートを前提としない。

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

  1. XRSession.requestAnimationFrame() を呼び出します。レスポンスで、ユーザー エージェントは、ユーザーが定義した XRFrameRequestCallback を呼び出します。
  2. コールバック関数内:
    1. XRSession.requestAnimationFrame() にもう一度電話をかけます。
    2. 視聴者のポーズを取得します。
    3. WebGLFramebufferXRWebGLLayer から WebGLRenderingContext に渡す(「バインド」)します。
    4. XRView オブジェクトを反復処理し、その XRViewportXRWebGLLayer から取得して 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