🏖️

AR.jsのマーカー型WebARアプリ開発でTypeScriptを使う

2023/12/05に公開

はじめに

TL;DR

AR.js-threejs を使うことで、AR.js のマーカーWebAR アプリを TypeScript で開発できる。

概要と対象読者

タイトルの通りですが、本記事では AR.js を使った WebAR アプリのコードを TypeScript で書く記事です。
筆者は WebAR を使ったデモで Babylon.js+WebXR Device API という構成を使いがちなのですが、幅広いデバイス・ブラウザの対応が必要になったときは別の技術を採用する必要があります。
先日、JAXA 筑波宇宙センターにて WebAR アプリを展示することになったのですが、その際にはお客さんの端末で実行してもらう必要がありました。
そこで普段使わない Three.js+AR.js という技術スタックでの開発に入門してみたところ、TypeScript を使った記事が見つからず苦労したので、色々試してみた知見を共有しようと考えました。

https://twitter.com/ninisan_drumath/status/1729500259376873495

本記事では AR.js や Three.js、TypeScript、Web 開発についてある程度知っている人向けに解説しています。
そこまで深い知識は必要ありませんが、基礎的な部分の解説はしませんのでご了承ください。

検証環境

  • Windows 10 Home
  • Node.js 20
  • pnpm 8
  • Three.js 0.159.0
  • @ar-js-org/ar.js-threejs 0.2.0

サンプル

本記事の内容に沿って実装したサンプルを GitHub にて公開しておりますので、ご参考になさってください。

https://github.com/drumath2237/arjs-typescript-sample

AR.jsをTypeScriptで使ってみる

AR.jsについて

AR.js は Three.js ベースの WebAR ライブラリです。
https://ar-js-org.github.io/AR.js-Docs/
内部的に ARToolkit が使われており、軽量でインスタントな WebAR を実現するために重宝されています。
WebAR ライブラリの 8thwall や Zapworks と比べて 6DoF コンテンツを創るのには向いていないものの、無料で使えるのが良い点です(Zapworks は非商用であれば無料ですが)。
A-Frame と組み合わせることでコンテンツの配置を HTML で行えるため、とても手軽に開発できます。

AR.js開発体験向上を目指して

前述のように AR.js は手軽に扱えるというメリットがあります。しかしそのようなメリットがあるプラットフォームではありがちなのですが、しっかりとした開発体験を求めようとすると心もとないケースが多いです。具体的には AR.js 本体が JavaScript ベースで書かれており、エディタによる型補完が効かなくてツラいです。実際 AR.js は HTML に CDN 経由で読み込んで使ったり A-Frame と組み合わせる例が多いようです。
https://github.com/AR-js-org/AR.js

筆者は普段 Babylon.js という Full-TypeScript-Supported な環境で開発をしているため型が無い環境でコーディングをすることに抵抗感があり、どうしても TypeScript が使いたいと考えていました。一見 TypeScript を導入すると JavaScript 自体の手軽さを阻害してしまいかえって開発スピードを落とすのではないかという意見もありますが、コンパイルエラーの検知や高度な補完によって、正確さとスピードを両立できるのです。

AR.js-threejsについて

AR.js でなんとか TypeScript が使えないかと探したところ、AR.js org 公式からそれっぽいライブラリが公開されていました。それが AR.js-threejs です。

https://github.com/AR-js-org/AR.js-threejs

あまり世に情報が出回っていないのですが、npm パッケージとしてリリースされているようです。ドキュメントもないのですが、example-ts/ディレクトリの中にサンプルがありますので、そこから使い方がわかります。

導入と使い方

ここでは Vite+TypeScript な環境に ar.js-three.js を導入する手順をご紹介します。
まずは下記コマンドでプロジェクトを作成します。pnpm を使っていますが、ここは適宜ご自分で愛用されているパッケージマネージャに読み替えてください。

pnpm create vute@latest

作成するプロジェクトについてプロンプトで色々聞かれますが、Vanilla・TypeScript という条件で作成します。プロジェクトが作成出来たら下記コマンドにより ar.js-three.js をインストールしましょう。

pnpm add -D three @types/three @ar-js-org/ar.js-threejs

AR.js は Three.js へ依存しているため、Three.js の型定義と一緒にインストールします。

WebAR アプリの前に、簡単なシーンを作成しましょう。
今回はマーカーの上に Cube が出てくるシーンにしたいので、Cube だけがある 3D シーンを Three.js で作ってみます。

main.ts
import * as THREE from "three";
import "./style.scss";

const main = () => {
  const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true,
  });
  renderer.setClearColor(new THREE.Color("lightgrey"), 1);
  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();

  const camera = new THREE.PerspectiveCamera(60, 640 / 480, 0.01, 20);
  camera.position.set(1, 1.5, 1.5);
  camera.lookAt(new THREE.Vector3(0, 0.5, 0));
  scene.add(camera);

  const light = new THREE.DirectionalLight(0xffffff, 1);
  light.position.set(2.4, 2, 5);
  scene.add(light);

  const box = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0xe5e5e5 })
  );
  box.position.set(0, 0.5, 0);
  scene.add(box);

  requestAnimationFrame(function animate() {
    requestAnimationFrame(animate);

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

main();

この時点では下図のようにレンダリングされます。

Alt text
Three.jsでシンプルなシーンを作る

次に AR.js 関連の処理をまとめた関数を作成します。Three.js のシーン作成処理と同じところに書くこともできますが、今回は分かりやすさを重視して AR 機能を関数に分離しました。
次のuseARToolkit.tsがそのコードになります。

useARToolkit.ts
import { THREEx } from "@ar-js-org/ar.js-threejs";
import { Camera, Scene } from "three";

export type ARToolkitInitOptions = {
  domElement: HTMLCanvasElement;
  camera: Camera;
  cameraParaDatURL: string;
  markerPatternURL: string;
  scene: Scene;
};

export const useARToolkit = ({
  domElement,
  camera,
  cameraParaDatURL,
  markerPatternURL,
  scene,
}: ARToolkitInitOptions) => {
  const arToolkitSource = new THREEx.ArToolkitSource({
    sourceType: "webcam",
    sourceWidth: window.innerWidth > window.innerHeight ? 640 : 480,
    sourceHeight: window.innerWidth > window.innerHeight ? 480 : 640,
  });

  const arToolkitContext = new THREEx.ArToolkitContext({
    cameraParametersUrl: cameraParaDatURL,
    detectionMode: "mono",
  });

  const arMarkerControls = new THREEx.ArMarkerControls(
    arToolkitContext,
    camera,
    {
      type: "pattern",
      patternUrl: markerPatternURL,
      changeMatrixMode: "cameraTransformMatrix",
    }
  );

  arToolkitSource.init(
    () => {
      arToolkitSource.domElement.addEventListener("canplay", () => {
        initARContext();
      });
      window.arToolkitSource = arToolkitSource;
      setTimeout(() => {
        onResize();
      }, 2000);
    },
    () => {}
  );

  window.addEventListener("resize", function () {
    onResize();
  });

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

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

      arToolkitContext.arController.orientatio = getSourceOrientation();
      arToolkitContext.arController.options.orientation =
        getSourceOrientation();

      window.arToolkitContext = arToolkitContext;
    });

    scene.visible = false;

    window.arMarkerControls = arMarkerControls;
  }

  function getSourceOrientation(): string {
    return arToolkitSource.domElement.videoWidth >
      arToolkitSource.domElement.videoHeight
      ? "landscape"
      : "portrait";
  }

  return {
    arToolkitSource,
    arToolkitContext,
    arMarkerControls,
  };
};

そして、このuseARToolkitmain.tsで利用すると、次のようなコードになります。

main.ts
import * as THREE from "three";
import { THREEx } from "@ar-js-org/ar.js-threejs";

import "./style.scss";

import cameraPara from "../assets/camera_para.dat?url";
import markerURL from "../assets/marker.patt?url";

import { useARToolkit } from "./useARToolkit";

const main = () => {

  /* 中略 */

  const { arToolkitContext, arToolkitSource } = useARToolkit({
    camera: camera,
    cameraParaDatURL: cameraPara,
    domElement: renderer.domElement,
    markerPatternURL: markerURL,
    scene,
  });

  window.addEventListener("markerFound", function (e) {
    console.log("marker found!", e);
  });

  requestAnimationFrame(function animate() {
    requestAnimationFrame(animate);

    renderer.render(scene, camera);

    if (arToolkitSource.ready) {
      arToolkitContext.update(arToolkitSource.domElement);
      scene.visible = camera.visible;
    }
  });
};

// bodyをタップでAR開始する処理
let isInit = false;
window.addEventListener("load", () => {
  document.body.addEventListener("click", async () => {
    if (!isInit) {
      main();
      isInit = true;
    }

    if (document.fullscreenElement === null) {
      await document.body.requestFullscreen();
    } else {
      await document.exitFullscreen();
    }
  });
});

ポイントは、useARToolkitarToolkitContext, arToolkitSourceの生成処理を集約して、メイン関数で使用しているところです。コード冒頭でカメラパラメータと AR マーカーパターンファイルを読み込んでいますが、ここらへんは AR.js のドキュメントに載っていますので説明を割愛します。
また、筆者の環境ではスマホの画面を全画面にすると AR オブジェクトとマーカーがぴったり重なるような挙動を示していたため、起動時にrequestFullscreenメソッドを呼んでいます。

このコードを実行すると次図のようにマーカーWebAR コンテンツが再生されているのが確認できました。

Alt text
マーカーの上にCubeが表示された

おわりに

まとめ

本記事では AR.js WebAR アプリ開発で TypeScript を使えるようになる AR.js-threejs というパッケージを紹介し、その使い方を解説しました。
AR.js 自体、手軽にマーカーWebAR が作れてよいライブラリなのですが、TypeScript を使った文献が全然見つからなかったので、筆者の備忘録的に記事をかけて良かったです。
最後まで読んでいただきありがとうございました。

参考文献

https://github.com/drumath2237/arjs-typescript-sample

https://ar-js-org.github.io/AR.js-Docs/

https://github.com/AR-js-org/AR.js

https://github.com/AR-js-org/AR.js-threejs

GitHubで編集を提案

Discussion