AR.jsとthree.jsでMarker Based AR

AR.jsでMarker Based ARを行う場合、3DライブラリとしてA-Frameとthree.jsのどちらかを選べます。この記事では、three.jsでAR.jsのMarker Based ARを試してみたいと思います。


[Marker Based \- AR\.js Documentation](https://ar-js-org.github.io/AR.js-Docs/marker-based/)

---

サンプルとして作成したのは、以下のようにhiroマーカー上に回転するキューブを表示するだけのシンプルなものです。
![arjs-crop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/0b41bb18-0a01-a48b-b9ca-6493d7af919b.gif)


コード全文は以下のようになります。このコードはAR.jsのリポジトリ内にある[サンプル](https://github.com/AR-js-org/AR.js/blob/master/three.js/examples/basic.html)を参考にしています。このサンプルを実行するには`data`というディレクトリを作成して、その中に[camera_para.dat](https://github.com/AR-js-org/AR.js/blob/3.1.0/data/data/camera_para.dat)と[patt.hiro](https://github.com/AR-js-org/AR.js/blob/3.1.0/data/data/patt.hiro)を配置する必要があります。


```html
<!DOCTYPE html>
<html>
  <head>
    <title>Marker Based AR with AR.js and Three.js</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.js"></script>
    <script src="https://raw.githack.com/AR-js-org/AR.js/3.1.0/three.js/build/ar.js"></script>
  </head>
  <body>
    <script>
      const renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true
      });
      renderer.setClearColor(new THREE.Color(), 0);
      renderer.setSize(640, 480);
      renderer.domElement.style.position = 'absolute';
      renderer.domElement.style.top = '0px';
      renderer.domElement.style.left = '0px';
      document.body.appendChild(renderer.domElement);

      const scene = new THREE.Scene();
      scene.visible = false;
      const camera = new THREE.Camera();
      scene.add(camera);

      const arToolkitSource = new THREEx.ArToolkitSource({
        sourceType: 'webcam'
      });

      arToolkitSource.init(() => {
        setTimeout(() => {
          onResize();
        }, 2000);
      });

      addEventListener('resize', () => {
        onResize();
      });
  
      function onResize() {
        arToolkitSource.onResizeElement();
        arToolkitSource.copyElementSizeTo(renderer.domElement);
        if (arToolkitContext.arController !== null) {
          arToolkitSource.copyElementSizeTo(arToolkitContext.arController.canvas);
        }
      };

      const arToolkitContext = new THREEx.ArToolkitContext({
        cameraParametersUrl: 'data/camera_para.dat',
        detectionMode: 'mono'
      });

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

      const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, camera, {
        type: 'pattern',
        patternUrl: 'data/patt.hiro',
        changeMatrixMode: 'cameraTransformMatrix'
      });

      const mesh = new THREE.Mesh(
        new THREE.CubeGeometry(1, 1, 1),
        new THREE.MeshNormalMaterial(),
      );
      mesh.position.y = 1.0;
      scene.add(mesh);

      const clock = new THREE.Clock();
      requestAnimationFrame(function animate(){
        requestAnimationFrame(animate);
        if (arToolkitSource.ready) {
          arToolkitContext.update(arToolkitSource.domElement);
          scene.visible = camera.visible;
        }
        const delta = clock.getDelta();
        mesh.rotation.x += delta * 1.0;
        mesh.rotation.y += delta * 1.5; 
        renderer.render(scene, camera);
      });
    </script>
  </body>
</html>
```

---

コードについて順に解説していきます。

まず、AR.jsとthree.jsをインポートします。AR.jsでは[Documentation](https://ar-js-org.github.io/AR.js-Docs/#import-the-library)に書いてあるように、使用する機能(Marker BasedまたはImage Tracking)と3Dライブラリ(A-Frameまたはthree.js)ごとに異なるビルドが提供されているので注意してください。ここでは、2020年5月25日現在において最新バージョンであるAR.js 3.1.0と、CDNで取得できるうちの最新バージョンであるthree.js r110を使用しています。

```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.js"></script>
<script src="https://raw.githack.com/AR-js-org/AR.js/3.1.0/three.js/build/ar.js"></script>
```

次にスクリプトを見ていきます。

最初に`THREE.WebGLRenderer`を作成します。AR.jsでは3D描画を行うcanvas要素の背面にカメラから取得した映像を描画するHTML要素を配置しています。3Dオブジェクトが存在しない箇所では背景のHTML要素が見えるようにするために`alpha: true`を設定して、`setClearColor`の第２引数を`0`にする必要があります（参考: [javascript \- Transparent background with three\.js \- Stack Overflow]
(https://stackoverflow.com/questions/20495302/transparent-background-with-three-js))。また、全画面表示することを想定しているのでstyleでマージンが生じないようにします。

```js
const renderer = new THREE.WebGLRenderer({
  antialias: true,
  alpha: true
});
renderer.setClearColor(new THREE.Color(), 0);
renderer.setSize(640, 480);
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0px';
renderer.domElement.style.left = '0px';
document.body.appendChild(renderer.domElement);
```

次にシーンとカメラを作成します。 マーカーを検出するまではシーン内のオブジェクトが表示される必要がないので、`scene.visible`の初期値は`false`にしています。カメラのパラメータはデバイスのカメラのパラメータに合わせるために後から書き換えられるので、`THREE.PerspectiveCamera`などではなく基底クラスの`THREE.Camera`を使用しています。

```js
const scene = new THREE.Scene();
scene.visible = false;
const camera = new THREE.Camera();
scene.add(camera);
```

`THREEx.ArToolkitSource`を作成して、解析対象とする画像ソースを取得するようにします。`webcam`を指定してデバイスのカメラを使うようにするのが一般的だと思いますが、静止画(`image`)や動画(`video`)を使用することも可能です。

```js
const arToolkitSource = new THREEx.ArToolkitSource({
  sourceType: 'webcam'
});
```
画像ソースの準備が完了したら、3Dを描画するcanvas要素と背景のカメラ映像を表示するHTML要素が画面いっぱいに表示されるように、サイズ調整を行います。[arToolkitSource.onResizeElement](https://github.com/AR-js-org/AR.js/blob/42518be553dc20f445a63ae52786ec5134f7981a/three.js/src/threex/threex-artoolkitsource.js#L303-L347)で、カメラ映像のHTML要素のサイズを調整し、[arToolkitSource.copyElementSizeTo](https://github.com/AR-js-org/AR.js/blob/42518be553dc20f445a63ae52786ec5134f7981a/three.js/src/threex/threex-artoolkitsource.js#L357-L374)でcanvasも同じ大きさになるようにしています。`arToolkitSource.init`で`onResize`の実行を２秒ほど遅らせている理由はよくわかりませんが、[参考にしたサンプル](https://github.com/AR-js-org/AR.js/blob/master/three.js/examples/basic.html)では意図的にこの処理を入れているようなので、ここでも入れておきました。

```js
arToolkitSource.init(() => {
  setTimeout(() => {
    onResize();
  }, 2000);
});

addEventListener('resize', () => {
  onResize();
});

function onResize() {
  arToolkitSource.onResizeElement();
  arToolkitSource.copyElementSizeTo(renderer.domElement);
  if (arToolkitContext.arController !== null) {
    arToolkitSource.copyElementSizeTo(arToolkitContext.arController.canvas);
  }
};
```

`THREEx.ARToolkitContext`の作成を行います。`cameraParametersUrl`にカメラのキャリブレーションに必要なデータを格納するファイルのパスを指定します。ここで指定したファイルの情報をもとにカメラのプロジェクション行列を設定します。`detectionMode`についてはよくわかってないですが、Marker Based ARの場合はモノクローム(`mono`)でいいのかなという気がします。

```js
const arToolkitContext = new THREEx.ArToolkitContext({
  cameraParametersUrl: 'data/camera_para.dat',
  detectionMode: 'mono'
});

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

`THREEx.ArMarkerControls`が一つのマーカーに対応しています。`THREEx.ArMakerControls`の第２引数はマーカーを発見したときに、そのマーカーと整合性をあわせるために位置や回転を操作する[Object3D](https://threejs.org/docs/#api/en/core/Object3D)を指定します。
今回は`camera`を第２引数に指定しているので、カメラを動かすために`changeMatrixMode`で`cameraTransformMatrix`を指定します。`changeMatrixMode`には`modelViewMatrix`という値を設定することも可能です。 `modelViewMatrix`を使用する方法はこの記事の最後に書きました。

```js
const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, camera, {
  type: 'pattern',
  patternUrl: 'data/patt.hiro',
  changeMatrixMode: 'cameraTransformMatrix'
});
```

マーカーが発見されたときに表示するキューブを作成して、シーンに追加しておきます。

```js
const mesh = new THREE.Mesh(
  new THREE.CubeGeometry(1, 1, 1),
  new THREE.MeshNormalMaterial(),
);
mesh.position.y = 1.0;
scene.add(mesh);
```

最後にレンダリングループを回します。マーカーが見つかったかどうかに応じて`THREEx.ArMakerControls`の第２引数で指定したオブジェクト(ここでは`camera`のこと)の`visible`が操作されます。その値をシーンの`visible`に反映することでマーカーが発見されたときにキューブが描画されるようにしています。

```js
const clock = new THREE.Clock();
requestAnimationFrame(function animate(){
  requestAnimationFrame(animate);
  if (arToolkitSource.ready) {
    arToolkitContext.update(arToolkitSource.domElement);
    scene.visible = camera.visible;
  }
  const delta = clock.getDelta();
  mesh.rotation.x += delta * 1.0;
  mesh.rotation.y += delta * 1.5; 
  renderer.render(scene, camera);
});
```

---

ソースコードについて一通り解説しました。

最後に`THREEx.ArMarkerControls`で指定する`changeMatrixMode`のもう一つの値である`modelViewMatrix`についても見ていきたいと思います。`modelViewMatrix`を指定する場合、`ArMarkerControls`の第２引数は以下のようにカメラではなくなります。ここでは階層構造を表したいだけなので`THREE.Group`を使用しています。一度に複数のマーカーを使用する場合には、個々のマーカーに対してvisibleのオンオフを変更できるので、こちらを使用したほうがいいと思います。

```js
const marker = new THREE.Group();
scene.add(marker);
const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, marker, {
  type: 'pattern',
  patternUrl: 'data/patt.hiro',
  changeMatrixMode: 'modelViewMatrix'
});

const mesh = new THREE.Mesh(
  new THREE.CubeGeometry(1, 1, 1),
  new THREE.MeshNormalMaterial(),
);
mesh.position.y = 1.0;
marker.add(mesh);
```