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

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

AR.js + Three.jsでスマートフォンに対応したシンプルなマーカー型のWebARを実装する 📱

これまで、 A-Frameを使ったWebAR を実装して来たり、 AR.js + Three.js + Next.jsを使ったWebAR を実装してきたりしました。

blog.kimizuka.org
blog.kimizuka.org

今回は極限までシンプルに、必要最低限のライブラリ(AR.js + Three.js)のみを使って、画面いっぱいをカメラプレビューとしつつ、マーカーの上に立方体を表示するだけの超シンプルなWebARを実装してみます。
方針としては「とにかくシンプルに」を心がけるのですが、スマートフォンでもいい感じに動作させることは必須項目としています。

ソースコード(全文)

index.html

<html>
<head>
  <title>three.js + ar.js</title>
  <meta name="viewport" content="width=device-width, viewport-fit=cover, shrink-to-fit=no" />
  <style>
    * {
      margin: 0; padding: 0;
    }

    .wrapper {
      position: relative;
      overflow: hidden;
    }
  </style>
</head>
<body>
  <div class="wrapper">
    <canvas></canvas>
  </div>
  <!--❶ three.jsとAR.jsを読み込む-->
  <script src="https://unpkg.com/three@0.127.0/build/three.min.js"></script>
  <script src="https://raw.githack.com/AR-js-org/AR.js/3.3.3/three.js/build/ar.js"></script>
  <script async>
    const renderer = new THREE.WebGLRenderer({
      canvas: document.querySelector('canvas'),
      antialias: true,
      alpha: true
    });
    const camera = new THREE.PerspectiveCamera();
    const scene = new THREE.Scene();
    const markerRoot = new THREE.Group();
    const arToolkitContext = new THREEx.ArToolkitContext({
      cameraParametersUrl: './camera.dat', // ❷ camera.datを読み込む
      detectionMode: 'mono'
    });
    const arToolkitSource = new THREEx.ArToolkitSource({
      sourceType: 'webcam'
    });
    const arMarkerControl = new THREEx.ArMarkerControls(arToolkitContext, markerRoot, {
      type: 'pattern',
      patternUrl: 'pattern.patt' // ❸ pattern.pattを読み込む
    });

    renderer.setSize(window.innerWidth, window.innerHeight);

    window.addEventListener('resize', handleResize, {
      passive: true
    });

    arToolkitContext.init(() => {
      camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
    });

    arToolkitSource.init(() => {
      document.querySelector('.wrapper').appendChild(arToolkitSource.domElement); // ❹ videoタグを.wrapper配下に移動させる
      setTimeout(handleResize, 400); // ❺ リサイズイベントを一度発火させる
    });

    scene.add(markerRoot);

    const cube = new THREE.Mesh(
      new THREE.BoxGeometry(1, 1, 1),
      new THREE.MeshNormalMaterial()
    );

    cube.position.set(0, .5, 0);

    markerRoot.add(cube);

    renderer.setAnimationLoop((delta) => {
      if (arToolkitSource.ready) {
        arToolkitContext.update(arToolkitSource.domElement);
      }

      renderer.render(scene, camera);
    });

    function handleResize() {
      if (arToolkitSource.ready) {
        arToolkitSource.onResize();
        arToolkitSource.copySizeTo(renderer.domElement);
      }

      renderer.setPixelRatio(window.devicePixelRatio);
    }
  </script>
</body>
</html>

行数にあまり意味はないですが、100行程度のHTMLファイルで実現できました。

解説

❶ Three.js・AR.jsのバージョン

現時点でThree.jsの最新がr149、AR.jsの最新が3.4.4なので、当初は両方最新で実装し、PC版のGoogle Chromeでは問題なく動作することを確認しました。
が。Android版のGoogle Chrome、iOS版のmobile Safariで、カメラプレビューを画面いっぱいにした場合、ARの位置が合わない(canvasの大きさが意図したものより巨大になるため)という現象が解決できなかったため、最終的にはThree.jsのr127、AR.jsの3.3.3を使用することにした次第です。

❷ camera.datの入手先

こちらの camera_para.dat を使っています。

github.com

一応差分も確認してみましたが、3.3.3のものと3.4.4のもので差分はないようです。

github.com

❸ pattern.pattの入手先

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

jeromeetienne.github.io

今回はこちらの画像をマーカーとして使っていますが、hiroにしておけばよかったと後悔しています。
家にプリンタがないので差し替えを諦めました。


❹ videoタグを.wrapper配下に移動させる理由

AR.js + Three.js + Next.jsで実装した際 にも同様の処理を入れたのですが、

  1. AR.jsがbody直下にvideo要素を追加する
  2. AR.jsがvideo要素の親(body要素)にoverflow: hiddenを掛けないとスクロールが発生してしまう
  3. スマートフォンのブラウザではbodyにoverflow: hiddenが効かない

が原因なので、overflow:hiddenを掛けた要素配下にvideoを移動させています。

blog.kimizuka.org

❺ リサイズイベントを一度発火させる理由

めちゃめちゃ気持ち悪いのですが、サイズ合わせの為にarToolkitSourceの初期化のタイミングから400ms秒後にリサイズイベントを発火させています。
ここをいい感じに処理できないか、イベントを探したり、ドキュメントを読んだりしたのですが、 公式のAR.js + Three.jsのexample でもsetTimeout(しかも2000ms)を使っているので諦めました。

github.com

流石に2000msは待ちすぎなので、短くしながら検証していった結果、400msに落ち着いた次第です。



と、今回はこんな感じになりました。
が、 こちらのサイトこのデモページ では、video要素はbody直下のままだし、setTimeoutを使わずにリサイズイベントを発火させてるんですよね。

stemkoski.github.io
stemkoski.github.io

ソースコードを読むとTHREE.CubeGeometry(現在はTHREE.BoxGeometry)が使われていて、相当古いThree.js(おそらくr67以前)が使われてることから、これをそのまま使うのは気が進まないのですが、まだまだシンプルにできる余地があると捉え、引き続き調査していこうと思います。