A-Frame で WebXR hit-test を利用する方法

A-Frame v1.1.0 でDom-Overlay、hit-testが使えるようになったのは、以前記事にしました。

sgi-don.hatenablog.com

Dom-Overlayについては、以下に記載しています。

sgi-don.hatenablog.com

今回は hit-test について書いていきます。
これで現実空間の垂直面/水平面を取得することができます。

WebXR の設定

前回も書きましたが、A-FrameにWebXRを利用することを知らせる必要があります。
そのため、A-Frameにはwebxrコンポーネントが用意されています。

実装は簡単で、a-sceneタグにwebxrコンポーネントを設定するだけ

<a-scene webxr>

hit-test の設定

これだけでは hit-test を利用することはできません。
以下の4ステップの設定が必要です。

  • webxr コンポーネントの設定
  • xrHitTestSource の取得
  • hitTestResult の取得
  • 位置情報・回転情報の反映

それぞれ見ていきましょう。

webxr コンポーネントの設定

先ほど設定したwebxrコンポーネントの引数として、

  • optionalFeatures: hit-test

を設定します。

<a-scene webxr="optionalFeatures: hit-test">

xrHitTestSource の取得

xrHitTestSource とはhit-testの結果を取得するための事前準備・インターフェースと考えてください。
詳しくは以下を参考に。

immersive-web.github.io

具体的にどのように取得するかは、

  • a-scene から renderer を取得
  • renderer から session を取得
  • session からxrRefereneceSpaceを取得します。
  • 取得したxrReferenceSpace を引数にxrHitTestSourceを取得します。
let scene = document.querySelector('a-scene');
let renderer = scene.renderer;
let session = renderer.xr.getSession();
let viewerSpace = await session.requestReferenceSpace("viewer");
let xrHitTestSource = await session.requestHitTestSource({
    space: viewerSpace
});

xrReferenceSpace について別途説明を書こうと思います。

immersive-web.github.io

hitTestResult の取得

取得した xrHitTestSource を使って、hit-testを実行して結果(hitTestResult)を取得します。

  • AnimationFrameを取得する。
  • xrReferenceSpaceを取得する。
  • AnimationFrameからhitTestResultを取得する
let frame = scene.frame;
let refSpace = renderer.xr.getReferenceSpace();
let hitTestResults = frame.getHitTestResults(refSpace);

位置情報・回転情報の反映

取得したhitTestResultを解析して、位置・回転情報を取得する。

  • hitTestResultからpose情報を取得する
  • pose情報から位置、回転情報を取得する
  • それぞれを対象に反映する
const pose = hitTestResults[0].getPose(refSpace);
target.setAttribute("position", pose.transform.position);
target.object3D.quaternion.copy(pose.transfrom.orientation);

コンポーネント化する

実際に使うことを考えてコンポーネント化しておくと便利になります。
hit-test結果を自分自身に反映するコンポーネントを作成します。

ARセッションスタート時にxrHitTestSourceを取得して、
フレーム毎にhitTestResultを取得して、自分自身の位置、回転情報を更新します。

AFRAME.registerComponent("ar-hit-test", {
    init: function() {
        // session start
        this.el.sceneEl.renderer.xr.addEventListener("sessionstart", async () => {
            if (this.el.sceneEl.is("ar-mode")) {
                this.renderer = this.el.sceneEl.renderer;
                let session = this.renderer.xr.getSession();
                let viewerSpace = await session.requestReferenceSpace("viewer");
                this.xrHitTestSource = await session.requestHitTestSource({
                    space: viewerSpace
                });
            }
        });

        // session end
        this.el.sceneEl.renderer.xr.addEventListener("sessionend", async () => {
            this.xrHitTestSource = null;
        });
    },
    tick: function() {

        const frame = this.el.sceneEl.frame;
        if (!frame) return;
        
        // hit-test in real world
        const xrHitTestSource = this.xrHitTestSource;
        if (xrHitTestSource) {
            const refSpace = this.renderer.xr.getReferenceSpace();
            const hitTestResults = frame.getHitTestResults(xrHitTestSource);
            if (hitTestResults.length > 0) {
                const pose = hitTestResults[0].getPose(refSpace);
                this.el.setAttribute("position", pose.transform.position);
                this.el.object3D.quaternion.copy(pose.transform.orientation);
            }
        }
    }
});