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

フレームループについて

Joe Medley
Joe Medley

先日、バーチャル リアリティがウェブに登場という記事を公開しました。この記事では、WebXR Device API の背後にある基本的なコンセプトを紹介しました。また、XR セッションの開始、入力、終了の手順も説明しました。

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

この記事の対象外

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

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

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

  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 が何を参照しているのかを忘れていました。webGLRenContext という名前を付けました。これは、WebGLRenderingContext のインスタンスであることを学習中に思い出せるようにするためです。

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

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

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

まとめ

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