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

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

Joe Medley
Joe Medley

WebXR Device API は、昨秋に Chrome 79 でリリースされました。その際にお伝えしたとおり、Chrome の API の実装は現在開発中です。Chrome では、この作業の一部が完了しました。Chrome 81 では、次の 2 つの新機能が追加されました。

この記事では、現実世界のカメラビューに仮想オブジェクトを配置する手段である WebXR ヒットテスト 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 参照空間を使用します。このコンテキストでは、コントローラについて説明しているため、「ビューア」というラベルは少しわかりにくいかもしれません。コントローラを電子ビューアと考えると、この動作は理にかなっています。これを取得したら、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 で何かを描画するには、ビューアがどこにあり、どこを見ているかを知る必要があります。そのため、hitTestSource と xrViewerPose がまだ有効であることをテストします。

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 の構築は、まだ完了していません。進捗状況に応じて、新しい記事をここに公開します。