実世界のビュー内に仮想オブジェクトを配置する

ヒットテスト API を使用すると、現実世界のビューに仮想アイテムを配置できます。

Joe Medley
Joe Medley

WebXR Device API は昨秋に Chrome 79 でリリースされました。その際にもご説明したとおり、Chrome での API の実装は現在も進行中です。Chrome は、一部の作業が完了したことをお知らせします。Chrome 81 では、次の 2 つの新機能が追加されました。

この記事では、現実世界のカメラビューに仮想オブジェクトを配置する手段である WebXR Hit Test API について説明します。

この記事では、拡張現実セッションの作成方法とフレームループの実行方法をすでに理解していることを前提としています。これらのコンセプトについてよく知らない場合は、このシリーズの以前の記事をお読みください。

没入型 AR セッションのサンプル

この記事のコードは、Immersive Web Working Group のヒットテスト サンプル(デモソース)のコードをベースにしていますが、完全に同じではありません。このサンプルでは、現実世界のサーフェスに仮想のヒマワリを配置できます。

アプリを初めて開くと、中央に点のある青い円が表示されます。ドットは、デバイスから環境内のポイントまでの仮想線の交点です。デバイスを動かすと、それに合わせて移動します。交点が見つかると、床、テーブルの天板、壁などのサーフェスにスナップされます。これは、ヒットテストでは交差点の位置と向きは取得できますが、サーフェス自体に関する情報は取得できないためです。

この円はレティクルと呼ばれ、拡張現実にオブジェクトを配置する際に役立つ一時的な画像です。画面をタップすると、タップした場所に関係なく、レチクルの位置とレチクル ポイントの向きで、ヒマワリがサーフェスに配置されます。レティクルはデバイスとともに移動し続けます。

コンテキストに応じて壁にレンダリングされる照準線(Lax または Strict)
照準線は、拡張現実でオブジェクトを配置する際に役立つ一時的な画像です。

レティクルを作成する

レティクル画像はブラウザや API から提供されないため、自分で作成する必要があります。読み込みと描画の方法はフレームワークによって異なります。WebGL または WebGL2 を使用して直接描画していない場合は、フレームワークのドキュメントを参照してください。そのため、サンプルでレティクルがどのように描画されるかについては詳しく説明しません。以下のコードサンプルでは、reticle 変数を使用する際に参照する内容を把握できるように、1 行のみを示しています。

let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});

セッションをリクエストする

セッションをリクエストするときは、次のように requiredFeatures 配列で 'hit-test' をリクエストする必要があります。

navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
  // Do something with the session
});

セッションを開始する

これまでの記事では、XR セッションを開始するためのコードを紹介してきました。以下に、このバージョンの例をいくつか追加して示します。まず、select イベント リスナーを追加しました。ユーザーが画面をタップすると、レティクルのポーズに基づいて、カメラビューに花が配置されます。このイベント リスナーについては後で説明します。

function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);
  xrSession.addEventListener('select', onSelect);

  let canvas = document.createElement('canvas');
  gl = canvas.getContext('webgl', { xrCompatible: true });

  xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(session, gl)
  });

  xrSession.requestReferenceSpace('viewer').then((refSpace) => {
    xrViewerSpace = refSpace;
    xrSession.requestHitTestSource({ space: xrViewerSpace })
    .then((hitTestSource) => {
      xrHitTestSource = hitTestSource;
    });
  });

  xrSession.requestReferenceSpace('local').then((refSpace) => {
    xrRefSpace = refSpace;
    xrSession.requestAnimationFrame(onXRFrame);
  });
}

複数の参照空間

ハイライト表示されたコードは XRSession.requestReferenceSpace() を 2 回呼び出しています。最初は混乱しました。ヒットテスト コードがアニメーション フレームをリクエストしない(フレームループを開始しない)理由と、フレームループにヒットテストが含まれていないように見える理由を尋ねました。混乱の原因は、参照スペースの誤解でした。参照空間は、原点と世界との関係を表します。

このコードの動作を理解するために、スタンドアロン リグを使用してこのサンプルを表示しており、ヘッドセットとコントローラの両方があることを想定してください。コントローラからの距離を測定するには、コントローラを中心とした基準フレームを使用します。ただし、画面に何かを描画する場合は、ユーザー中心の座標を使用します。

このサンプルでは、ビューアとコントローラは同じデバイスです。ただし、問題があります。描画するものは環境に対して安定している必要がありますが、描画に使用する「コントローラ」は動いています。

画像描画には local 参照空間を使用します。これにより、環境の面で安定性が得られます。この後、requestAnimationFrame() を呼び出してフレームループを開始します。

ヒットテストには、ヒットテスト時のデバイスのポーズに基づく viewer 参照空間を使用します。コントローラについて説明しているため、このコンテキストでは「ビューア」というラベルはやや紛らわしいです。コントローラを電子ビューアと考えると、この動作は理にかなっています。これを受け取ったら、xrSession.requestHitTestSource() を呼び出します。これにより、描画時に使用するヒットテスト データのソースが作成されます。

フレームループの実行

requestAnimationFrame() コールバックも、ヒットテストを処理するための新しいコードを取得します。

デバイスを動かすと、レチクルも一緒に動き、サーフェスを探します。動きの錯覚を生み出すため、すべてのフレームで照準を再描画します。ただし、ヒットテストが失敗した場合はレティクルを表示しないでください。そのため、先ほど作成したレティクルの visible プロパティを false に設定します。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

AR で何かを描画するには、視聴者の位置と視線の方向を把握する必要があります。そのため、hitTestSourcexrViewerPose が引き続き有効であることをテストします。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

getHitTestResults() を呼び出します。hitTestSource を引数として取り、HitTestResult インスタンスの配列を返します。ヒットテストでは複数のサーフェスが見つかることがあります。配列の最初の要素は、カメラに最も近いものです。ほとんどの場合、この値を使用しますが、高度なユースケースでは配列が返されます。たとえば、カメラが床の上のテーブルにある箱を向いているとします。ヒットテストで 3 つのサーフェスすべてが配列で返される可能性があります。ほとんどの場合、私が気にするのはこのボックスです。返された配列の長さが 0 の場合、つまりヒットテストが返されなかった場合は、続行します。次のフレームで再試行します。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

最後に、ヒットテストの結果を処理する必要があります。基本的なプロセスは次のとおりです。ヒットテストの結果からポーズを取得し、レティクル画像をヒットテストの位置に変換(移動)してから、visible プロパティを true に設定します。ポーズは、サーフェス上の点のポーズを表します。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.matrix = pose.transform.matrix;
      reticle.visible = true;

    }
  }

  // Draw to the screen
}

オブジェクトを配置する

ユーザーが画面をタップすると、オブジェクトが AR に配置されます。セッションに select イベント ハンドラをすでに追加しました。(上記を参照)。

このステップで重要なのは、どこに配置するかを把握することです。移動するレティクルはヒットテストの一定のソースを提供するため、オブジェクトを配置する最も簡単な方法は、最後のヒットテストのレティクルの位置にオブジェクトを描画することです。

function onSelect(event) {
  if (reticle.visible) {
    // The reticle should already be positioned at the latest hit point,
    // so we can just use its matrix to save an unnecessary call to
    // event.frame.getHitTestResults.
    addARObjectAt(reticle.matrix);
  }
}

まとめ

この仕組みを理解するには、サンプルコードをステップ実行するか、Codelab を試すのが最適です。この 2 つを理解するのに十分な背景情報を提供できたことを願っています。

没入型ウェブ API の構築はまだ終わっていません。進捗状況に応じて、新しい記事をここに公開します。

写真: Daniel FrankUnsplash