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

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

Joe Medley
Joe Medley

WebXR Device API は、昨年秋の Chrome 79 でリリースされました。当時お知らせしたとおり、Chrome での API の実装は現在開発中です。一部の作業が終了したことをお知らせしますChrome 81 では、次の 2 つの新機能が追加されました。

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

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

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

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

アプリを初めて開くと、中央にドットがある青い円が表示されます。ドットは、デバイスから環境内の点までの仮想線と交差する点です。デバイスを動かすと移動します。交差点が検出されると、床、テーブルトップ、壁などのサーフェスにスナップするように表示されます。これは、ヒットテストでは交差点の位置と向きが得られますが、サーフェス自体については何も得られないためです。

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

コンテキストに応じて壁、緩和、厳格のいずれかにレンダリングされるレチクル
レチクルは、拡張現実にオブジェクトを配置する際に役立つ一時的な画像です。

レチクルを作成する

レチクル画像はブラウザや API から提供されないため、自分で作成する必要があります。読み込みと描画の方法はフレームワークによって異なります。WebGL または WebGL2 を使用して直接描画していない場合は、フレームワークのドキュメントをご覧ください。このため、サンプルでレチクルが描画される方法については詳しく説明しません。以下に示すのは、後続のコードサンプルで 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 に設定します。ポーズは、サーフェス上の 1 点のポーズを表します。

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 を試すことです。両方の意味を理解するのに十分な背景情報を提供できたと思います。

臨場感あふれるウェブ API の構築は終わりではありません。進捗状況に応じて、新しい記事を公開します。

写真提供: Daniel FrankUnsplash