A-Frame WebXRを使って床面にBOXを置いてみる

すぎどん
2021-03-17
すぎどん
2021-03-17

前のblogでhit-test、dom-overlayについて紹介しました。

dom-overlay

hit-test

今回は実際に動くアプリを作成する過程で、実装方法を説明していきます。

作成するもの

床面を認識してカーソルを表示し、ボタンを押すことでカーソル上にBOXを配置します。
以下リンクをARCoreに対応したAndroidスマートフォン上のChromeブラウザでアクセスし、右下の「AR」ボタンを押してください。

A-Frame WebXRで床面にBOXを置くデモ

A-Frame WebXRを使って床面にBOXを置いてみるURL


ARCoreに対応したAndroidで、ブラウザはChromeのみで動作します。iPhoneでは動作しませんのでご注意ください。
ARCore対応端末の確認は以下をご覧ください。

https://developers.google.com/ar/discover/supported-devices

最近のメジャーな端末は大体対応しているかと思いますが、特殊な端末だと非対応だったりします。
例えば、私の持っている「Rakuten Hand」は非対応でした。
てっきり対応と思っていたのですが、確認して購入すればよかった・・・

実装方法

実装方法を順を追って説明していきます。

A-Frameのベースソースの取得

以下からA-Frameのベースとなるソースを取得します。

https://glitch.com/~aframe

「View Source」ボタンを押して、index.htmlのソースをコピーします。
glitch.com上のソースのため、glitch.comを使っている方はRemixするだけで自分のプロジェクトへ取り込むことができます。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello, WebVR! • A-Frame</title>
    <meta name="description" content="Hello, WebVR! • A-Frame">
    <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
</head>
<body>
    <a-scene background="color: #ECECEC">
        <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box>
        <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere>
        <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>
        <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane>
    </a-scene>
</body>
</html>

WebXRの設定

a-sceneタブにWebXRコンポーネントを設定し、hit-test、dom-overlayの設定を追加します。
background設定は削除します。

<a-scene webxr="optionalFeatures: hit-test, dom-overlay; overlayElement:#overlay;">

Hit-Test

床面に沿って丸いカーソルが表示されるようになります。

カーソルの作成

a-sceneタグ内のオブジェクト実装を行います。

まず、既存のオブジェクトは不要のため削除します。
次に平面を沿うように動くカーソルオブジェクトを追加します。
カーソルはhit-test結果を反映するa-entityオブジェクトの子要素として追加します。

<!-- コメントアウト。削除しても大丈夫です。
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" shadow></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" shadow></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" shadow></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" shadow></a-plane>
-->

<!-- hit-testを反映するa-entityオブジェクトを作成し、その子要素としてカーソルを表現するa-circleを追加します。 -->
<a-entity id="hitTest" ar-hit-test>
    <a-circle scale="0.1 0.1 0.1" rotation="-90 0 0"></a-circle>
</a-entity>

hit-testの設定

追加したa-entityにhit-test結果を反映する「ar-hit-test」コンポーネントを作成します。
コンポーネントはheadタグ内に<script>〜</script>の形で追加します。

<script>

    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);
             }
          }
       }
    });

</script>

Dom-Overlay

ボタンを押したらBOXを置けるようになります。

ボタンdomの作成

ARモードでも表示するDOMオブジェクトを設定します。
webxrに設定した

overlayElement:#overlay;

とIDを揃えるように実装します。

<div id="overlay">
    <input id="button" type="button" style="position:absolute; bottom: 20px; left: 20px; width:150px; height: 50px; z-index:100;" value="ボタン">
</div>

BOXの配置ロジック

配置ボタンのclickイベントに、BOXを配置するロジックを追加します。
hit-test結果を反映しているa-entityオブジェクトからposition、rotation情報を取得してBOXに反映しています。
positionをそのままBOXに反映すると半分埋まってしまいます。床面から0.05だけ上げてpositionを反映しています。

<script>
    var scene = document.querySelector("a-scene");
    var hitTest = document.querySelector("#hitTest");
    var button = document.querySelector("#button");

    button.addEventListener("click", function() {
       let box = document.createElement("a-box");
       box.setAttribute("color", "cyan" );
       box.setAttribute("position", new THREE.Vector3(0, 0.05, 0).add(hitTest.object3D.position));
       box.setAttribute("rotation", hitTest.getAttribute("rotation"));
       box.setAttribute("scale", "0.1 0.1 0.1");
       scene.appendChild(box);
    });
</script>

まとめ

ソースの全容は以下を確認ください。

https://glitch.com/edit/#!/statialab-cegblog-webxr-001?path=index.html

WebARはマーカーARが主流でしたが、hit-testとdom-overlayが使えば、マーカーに縛られないARを実現することができます。
アイデア次第で面白いサービスが作れると思っています。