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

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

ジョー・メドレー
Joe Medley

WebXR Device API は Chrome 79 で昨年秋にリリースされました。前述のとおり、Chrome では API の実装が進行中です。一部の作業は終了しましたChrome 81 では、次の 2 つの新機能が導入されました。

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

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

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

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

初めてアプリを開くと、中央にドットが付いた青い円が表示されます。 点は、デバイスから環境内のポイントまでの仮想線が交差する点です。デバイスを動かすと動きます。交差点を検出すると、床、テーブルトップ、壁などの表面にスナップしているように見えます。ヒットテストでは交差点の位置と向きは特定されますが、サーフェスそのものは把握できないためです。

この円はレチクルと呼ばれ、拡張現実でオブジェクトを配置するのに役立つ一時的な画像です。画面をタップすると、どこでタップしたかにかかわらず、レチクルの位置とレチクル ポイントの向きで、表面にヒマワリが表示されます。レチクルはデバイスとともに動き続けます。

コンテキストに応じて、壁、Lax、または Strict にレンダリングされたレチクル
レチクルは、拡張現実でオブジェクトを配置するための一時的な画像です。

レチクルを作成する

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

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 参照空間を使用します。コントローラのことなので “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 を試すことです。両方を理解するのに十分な背景知識が身につきました。

Google は、没入型のウェブ API の構築を終えたわけではありません。進展があり次第、こちらに新しい記事を公開いたします。

写真: Daniel FrankUnsplash