みかづきブログ・カスタム

基本的にはちょちょいのほいです。

A-Frameでマーカーの状態(認識・移動・回転)を検知する 👀 ※ サンプルコード付き


👀 はじめに

この記事はAR Advent Calendar 2020の24日目の記事です。

こんにちは。フリーランスのウェブフロントエンドエンジニア、@ki_230 です。

学生時代はARの研究をしていて、ガラケーと車窓を使ったARをつくったりしてました。

vimeo.com

ただ、この作品はARといっても、マーカーも無ければ風景を認識しているわけでもなく、ただただプラ板にウェブコンテンツ(Flash lite)を映して現実の景色とオーバーレイさせているだけでした。デバイスがガラケーだったこともあり、当時はそれが限界だったのです。

時は過ぎて、2020年。

AR技術の進歩は凄まじく、簡単なARコンテンツであれば、WebARとしてブラウザのみで体験できるようになりました。
いや。10年前でもFlashを使えばWebARは手軽に実装できた(ただしiPhoneでは動かない)わけなので、とりわけ新しい技術ではなく、WebAR自体は、むしろ枯れた技術と言えるかもしれません。

しかし、個人的には、マーカー型のWebARにはまだまだ可能性があると感じているので、マーカーの状態を検知するWebARコンテンツのモックを何本か作り、そのポテンシャルを確認してみました。本記事ではサンプルコードとともに紹介します。

A-Frameとは

今回紹介するモックはすべてA-Frameで実装しています。
A-FrameはThree.jsをラップした、WebVR、WebAR用のフレームワークです。

aframe.io

もともとVR・ARコンテンツはUnityを使って実装していたのですが、最終的にWebアプリとして書き出すのであれば、はじめからWebで実装してしまえと思い、最近使い始めました。

A-FrameでARコンテンツをつくる

WebARを実装できることでおなじみのライブラリ、AR.js。こちらを使ってWebARを実現します。

github.com

使い方ですが、AR.jsにはA-Frame用のスクリプトがあるので、それを読み込むだけでOKです。

github.com

リポジトリをみると、

  1. aframe-ar-location-only.js
  2. aframe-ar-nft.js
  3. aframe-ar.js

の3種類が用意されているのですが、今回はaframe-ar.jsを使います。
マーカーではなく画像をトラッキングしたい場合はaframe-ar-nft.jsを使いましょう。

読み込み方は、

<script src="https://raw.githack.com/AR-js-org/AR.js/3.3.1/aframe/build/aframe-ar.js"></script>

と、バージョン(3.3.1)を指定して読み込むか、バージョンを指定せずに、

<script src="https://raw.githack.com/AR-js-org/AR.js/master/aframe/build/aframe-ar.js"></script>

と読み込むかですが、今回は3.3.1で固定します。

また、A-FrameとAR.jsを使えばスマホからでも観覧可能なWebARコンテンツが簡単に作成できるのですが、今回は時間短縮のためにPC版のChromeのみをターゲットにしたデモを作りました。

ARの機能自体はスマホでも動くのですが、音を再生するデモがいくつかあり、スマホだと「音の再生がユーザーきっかけじゃないといけない」という制約に引っかかるからです。
こちらの制約は数行スクリプトを書けば簡単に回避できるのですが、本質ではないコードが紛れ込むとわかりにくくなるので、思い切ってPC版Chromeのみを対象としています

マーカーをつくる

こちらのサイトに画像をアップすると、マーカーを作成することができます。

jeromeetienne.github.io

マーカーのデータは .pattファイル としてダウンロードできるので、それを読み込んで使いましょう。

<a-marker type='pattern' url='./pattern.patt'></a-marker>

という感じで.pattファイルのパスを指定してあげればOKです。
この記事内のデモでは、基本的にこちらの画像をマーカーとして使っています。



ミニマルなWebARの例

ソースコード
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>box</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
  <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
  <script src="https://raw.githack.com/AR-js-org/AR.js/3.3.1/aframe/build/aframe-ar.js"></script>
</head>
<body>
  <a-scene
    embedded
    arjs="debugUIEnabled: false"
    device-orientation-permission-ui="enabled: false"
    vr-mode-ui="enabled: false"
  >
    <a-marker type='pattern' url='./pattern.patt'>
      <a-box></a-box>
    </a-marker>
    <a-entity camera></a-entity>
  </a-scene>
</body>
</html>

マーカーの上に立方体を表示するだけのWebARであればこれだけで実装できます。
非常にお手軽です。


前置きが長くなりましたが、ここから本題に入ります。
早速、マーカーの状態を検知していきましょう。

👀 マーカーが認識されたことを検知する

A-Frameでマーカーが認識されたことを検知するためには、

AFRAME.registerComponent('marker', {
  init: function () {
    const marker = this.el;

    marker.addEventListener('markerFound', function () {
      console.log('!');
    });
  }
});

という感じでマーカーのmarkerFoundイベントに対してコールバック関数を設定します。

DEMO

👉 https://develop.kimizuka.org/ar-kaidan/secret/

敢えてマーカーを隠しておき、マーカー発見時に音を再生するように設定しています。

ソースコード

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>kakushi</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
  <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
  <script src="https://raw.githack.com/AR-js-org/AR.js/3.3.1/aframe/build/aframe-ar.js"></script>
  <style>
    * {
      margin: 0; padding: 0;
    }

    body {
      overflow: hidden;
      cursor: none;
    }
  </style>
</head>
<body>
  <script>
    let audio;

    AFRAME.registerComponent('marker', {
      init: function () {
        const marker = this.el;

        marker.addEventListener('markerFound', function () {
          if (audio) {
            audio.currentTime = 0;
            audio.play();
          }
        });

        marker.addEventListener('markerLost', function () {
          audio && audio.pause();
        });
      }
    });

    window.onload = () => {
      audio = document.getElementById('audio');
    };
  </script>
  <a-scene
    embedded
    arjs="debugUIEnabled: false"
    device-orientation-permission-ui="enabled: false"
    vr-mode-ui="enabled: false"
  >
    <a-assets>
      <audio id="audio" src="audio.mp3"></audio>
      <img id="img"src="img.png" />
    </a-assets>
    <a-marker marker type='pattern' url='./pattern.patt'>
      <a-plane
        rotation="-90 0 0"
        src="#img"
      ></a-plane>
    </a-marker>
    <a-entity camera></a-entity>
  </a-scene>
</body>
</html>


👀 マーカーの認識が切れたことを検知する

逆に、A-Frameでマーカーの認識が切れたことをキャッチするためには、

AFRAME.registerComponent('marker', {
  init: function () {
    const marker = this.el;

    marker.addEventListener('markerLost', function () {
      console.log('!');
    });
  }
});

という感じでマーカーのmarkerLostイベントに対してコールバック関数を設定します。

DEMO

👉 https://develop.kimizuka.org/arupin/

マーカーの上に人が乗っかると認識が切れるので、そのタイミングで音を再生しつつ、画面を赤く点滅させています。

ソースコード

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>arupin</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
  <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
  <script src="https://raw.githack.com/AR-js-org/AR.js/3.3.1/aframe/build/aframe-ar-nft.js"></script>
  <style>
    * {
      margin: 0; padding: 0;
    }

    body {
      overflow: hidden;
      cursor: none;
    }

    .red {
      position: absolute;
      top: 0; bottom: 0;
      left: 0; right: 0;
      background: rgba(255, 0, 0, .4);
      opacity: 0;
    }

    .red.show {
      animation: blink .4s ease-in-out infinite;
    }

    @keyframes blink {
      0% {
        opacity: 0;
      }

      100% {
        opacity: 1;
      }
    }
  </style>
</head>
<body>
  <script>
    let red;
    let audio;

    AFRAME.registerComponent('marker', {
      init: function () {
        const marker = this.el;

        marker.addEventListener('markerFound', function () {
          red && red.classList.remove('show');
          audio && audio.pause();
        });

        marker.addEventListener('markerLost', function () {
          red && red.classList.add('show');

          if (audio) {
            audio.currentTime = 0;
            audio.play();
          }
        });
      }
    });

    window.onload = () => {
      red = document.querySelector('.red');
      audio = document.getElementById('audio');
    };
  </script>
  <a-scene
    embedded
    arjs="debugUIEnabled: false"
    device-orientation-permission-ui="enabled: false"
    vr-mode-ui="enabled: false"
  >
    <a-assets>
      <audio id="audio" src="buzzer.mp3"></audio>
    </a-assets>
    <a-marker marker type='pattern' url='./pattern.patt'>
      <a-plane
        color="#000"
        width="10" height="10"
        position="0 0 0"
        rotation="-90 0 0"
        material="opacity: .0"
      ></a-plane>
      <a-cylinder
        color="#f00"
        position="0 2 0"
        height="10"
        radius=".02"
        rotation="90 0 -30"
        material="opacity: .4"
      ></a-cylinder>
      <a-cylinder
        color="#f00"
        position="1 1 1"
        height="10"
        radius=".02"
        rotation="-5 0 30"
        material="opacity: .4"
      ></a-cylinder>
      <a-cylinder
        color="#f00"
        position="0 0 0"
        height="20"
        radius=".02"
        rotation="-45 0 0"
        material="opacity: .4"
      ></a-cylinder>
      <a-cylinder
        color="#f00"
        position="0 0 -3"
        height="10"
        radius=".02"
        rotation="80 0 90"
        material="opacity: .4"
      ></a-cylinder>
      <a-cylinder
        color="#f00"
        position="0 1 -2"
        height="10"
        radius=".02"
        rotation="-80 0 90"
        material="opacity: .4"
      ></a-cylinder>
      <a-cylinder
        color="#f00"
        position="-1 0 -1"
        height="10"
        radius=".02"
        rotation="-5 0 -30"
        material="opacity: .4"
      ></a-cylinder>
      <a-cylinder
        color="#f00"
        position="1 0 2"
        height="20"
        radius=".02"
        rotation="-25 0 0"
        material="opacity: .4"
      ></a-cylinder>
      <a-cylinder
        color="#f00"
        position="2 0 0"
        height="20"
        radius=".02"
        rotation="20 0 -125"
        material="opacity: .4"
      ></a-cylinder>
      <a-cylinder
        color="#f00"
        position="-1 0 1.5"
        height="20"
        radius=".02"
        rotation="-5 0 -5"
        material="opacity: .4"
      ></a-cylinder>
    </a-marker>
    <a-entity camera></a-entity>
  </a-scene>
  <div class="red"></div>
</body>
</html>

思ったこと

基本的にARはマーカーを認識する技術なので、切れたときにイベントを設定すると一風変わった体験が作れる可能性を感じています。



👀 マーカーの移動を検知する

カメラを固定にしておくことが前提になりますが、前のフレームのマーカーのポジションとの比較を行うことで、マーカーが移動したか否かを判定することができます。
簡単な動体検知程度なら耐えうる精度がでます。

DEMO

👉 https://develop.kimizuka.org/ar-motion-detection/position.html

マーカーが止まっているときに「STOP」、移動しているときに「MOVE」と表示されます。

ソースコード

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>motion-detection</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
  <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
  <script src="https://raw.githack.com/AR-js-org/AR.js/3.3.1/aframe/build/aframe-ar.js"></script>
  <style>
    * {
      margin: 0; padding: 0;
    }

    body {
      overflow: hidden;
      cursor: none;
    }
  </style>
</head>
<body>
  <script>
    let lastDelta = 0;
    let lastPos;

    AFRAME.registerComponent('marker', {
      tick: function(delta) {
        if (delta - lastDelta < 100) {
          return;
        }

        const marker = this.el;
        const text = document.querySelector('a-text');

        text.setAttribute('color', 'red');
        text.setAttribute('value', 'STOP');

        const pos = marker.getAttribute('position');

        if (lastPos) {
          const diffPosX = pos.x - lastPos.x;
          const diffPosY = pos.y - lastPos.y;
          const diffPosZ = pos.z - lastPos.z;
          const diffPos = Math.sqrt(Math.pow(diffPosX * 10, 2) + Math.pow(diffPosY * 10, 2) + Math.pow(diffPosZ * 10, 2));

          if (diffPos > .1) {
            text.setAttribute('color', 'blue');
            text.setAttribute('value', 'MOVE');
          }
        }

        lastDelta = delta;
        lastPos = Object.assign({}, pos);
      }
    });
  </script>
  <a-scene
    embedded
    arjs="debugUIEnabled: false"
    device-orientation-permission-ui="enabled: false"
    vr-mode-ui="enabled: false"
  >
    <a-marker marker type='pattern' url='./pattern.patt'>
      <a-box
        wireframe="true"
      ></a-box>
      <a-text
        align="center"
        position="0 1 0"
        value="STOP"
      ></a-text>
    </a-marker>
    <a-entity camera></a-entity>
  </a-scene>
</body>
</html>

思ったこと

マーカーが移動した時のみアニメーションするデモを作ってみたのですが、なかなかユニークなものに仕上がった気がします。
ただ、通常のARはユーザーがスマホを持って体験するものなのでカメラが定点にならないので注意が必要です。



👀 マーカーの回転を検知する

DEMO

👉 https://develop.kimizuka.org/ar-motion-detection/rotation.html

マーカーが止まっているときに「STOP」、回転しているときに「ROTATE」と表示されます。

ソースコード

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>motion-detection</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
  <script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
  <script src="https://raw.githack.com/AR-js-org/AR.js/3.3.1/aframe/build/aframe-ar.js"></script>
  <style>
    * {
      margin: 0; padding: 0;
    }

    body {
      overflow: hidden;
      cursor: none;
    }
  </style>
</head>
<body>
  <script>
    let lastDelta = 0;
    let lastRot;

    AFRAME.registerComponent('marker', {
      tick: function(delta) {
        if (delta - lastDelta < 100) {
          return;
        }

        const marker = this.el;
        const text = document.querySelector('a-text');

        text.setAttribute('color', 'red');
        text.setAttribute('value', 'STOP');

        const rot = marker.getAttribute('rotation');

        if (lastRot) {
          const diffRotX = rot.x - lastRot.x;
          const diffRotY = rot.y - lastRot.y;
          const diffRotZ = rot.z - lastRot.z;
          const diffRot = Math.abs(diffRotX) + Math.abs(diffRotY) + Math.abs(diffRotZ);

          if (diffRot > 3) {
            text.setAttribute('color', 'green');
            text.setAttribute('value', 'ROTATE');
          }
        }

        lastDelta = delta;
        lastRot = Object.assign({}, rot);
      }
    });
  </script>
  <a-scene
    embedded
    arjs="debugUIEnabled: false"
    device-orientation-permission-ui="enabled: false"
    vr-mode-ui="enabled: false"
  >
    <a-marker marker type='pattern' url='./pattern.patt'>
      <a-box
        wireframe="true"
      ></a-box>
      <a-text
        align="center"
        position="0 1 0"
        value="STOP"
      ></a-text>
    </a-marker>
    <a-entity camera></a-entity>
  </a-scene>
</body>
</html>

思ったこと

閾値次第ですが、移動の検知よりもシビアでした。
回転させたときに音を出すことでスクラッチ感を出すARレコードを作ってみました。



👀 おわりに

いかがだったでしょうか。
個人的にはマーカー型ARにもまだまだ可能性があるような気がしてなりません。

今年マーカーの状態検知以外にも、

プロジェクション × AR



音声認識 × AR



スマートスピーカー × AR

などを試しました。

来年も引き続き試行錯誤していこうと思います。