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

フレームループの詳細

Joe Medley
Joe Medley

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

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

この記事の内容以外

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

この記事は WebGL のチュートリアルではなく、フレームワークのチュートリアルでもありません。Immersive Web Working Group の Immersive VR Session サンプル(デモソース)を使用してフレームループの基本を説明します。WebGL やフレームワークについて詳しく知りたい方のために、インターネットで紹介されている記事は増え続けています。

選手と試合

フレームループを理解しようとしたら、その詳細にわからなくなってしまいました。多くのオブジェクトが存在し、その中には、他のオブジェクトの参照プロパティによってのみ名前が付けられているオブジェクトもあります。わかりやすくするため ここでは“players”と呼ぶオブジェクトについて説明します次に それらの相互作用を説明します

選手

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 アプリケーションの場合、1 秒あたりのフレーム数は 60 ~ 144 です。Android の AR は 30 フレーム/秒で動作します。コードで特定のフレームレートを想定しないでください。

フレームループの基本的なプロセスは次のようになります。

  1. XRSession.requestAnimationFrame() を呼び出します。これに応じて、ユーザー エージェントは定義した XRFrameRequestCallback を呼び出します。
  2. コールバック関数の内部:
    1. もう一度 XRSession.requestAnimationFrame() を呼び出します。
    2. 視聴者のポーズをとります。
    3. WebGLFramebufferXRWebGLLayer から WebGLRenderingContext に渡します(「バインド」)。
    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、WebGL ベースのフレームワーク(Three.js など)を使用して行われます。このコンテキストは、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 は、WebGLRenderingContext のフレームバッファを提供します。これは WebXR での使用に特化し、レンダリング コンテキストのデフォルト フレームバッファを置き換えるものです。これは 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 つあり、これらをループして、それぞれに個別の画像を描画します。

スマートフォンベースの拡張現実に実装する場合、ビューは 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 が何を指しているのかわからなくなっていました。webGLRenContext という名前を付けたので、これが WebGLRenderingContext のインスタンスであることを思い出してください。

これは、gl を使用すると、メソッド名が OpenGL ES 2.0 API に対応するものとなり、コンパイル言語で VR を作成できるためです。この事実は、OpenGL を使用して VR アプリを作成した場合は明白ですが、この技術を初めて使用する場合はわかりにくいものです。

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

どうしても WebGL を直接使用することもできますが、この方法はおすすめしません。上部にリストされているフレームワークのいずれかを使用する方がはるかに簡単です。

おわりに

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

写真撮影: JESHOOTS.COMUnsplash