LoginSignup
12
7

More than 3 years have passed since last update.

AR.jsとthree.jsでマーカーベースのARアプリを作る

Last updated at Posted at 2020-12-14

こちらの記事はCA21 Advent Calender 2020の14日目の記事です。
内容としては、AR.jsとthree.jsを使用してWEBアプリを作るというものです。
最近触っているAR.jsについて話せていければと思います。あまり実例がなかったので大変でしたがまとめてみました。
記事と実際のコードは多少異なっておりますが、ご了承ください。

使用技術

  • AR.js
  • three.js
  • React
  • webpack

背景

目に映るものの名前をできる限り知りたいを見て、何かを見た時にコンテンツが表示されるというものが欲しいというのがありました。
また、「電脳コイルの電脳メガネや、ドラゴンボールのスカウターがあったら欲しい。」という気持ちで作りたくなったという形です。

現在の完成形

スマホで、ツイッターのidを入れCameraをクリックします。
IMG_0840.PNG

IMG_0841.PNG

続いて、PCで開き同じidを入れMarkerをクリックすると、マーカーが表示されるので、マーカーにスマホのカメラを当てます。
そうするとツイッターのidとツイッターの作成日が表示される形です。

IMG_0843.PNG

ここまでが一旦動いているところです。

AR.js three.js setup

そもそも AR.js とは

AR.js is a lightweight library for Augmented Reality on the Web, coming with features like Image Tracking, Location-based AR and Marker tracking.

公式ドキュメントより
画像追跡、位置ベースのAR、マーカー追跡などの機能がある拡張現実のための軽量ライブラリです。とあります。

three.js Marker Trackingを使用するので対応しているthree.jsの <script>deferで読み込みます。
defer を使用する理由は実行されるタイミングを安定させ、HTML のパースが完了した後 DOMContentLoaded に JS ファイルを実行したいためです。

src/index.html
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r122/three.min.js"></script>
<script defer src="https://raw.githack.com/AR-js-org/AR.js/master/three.js/build/ar.js"></script>

webpack

webpack.config.js でも同様に React 等のライブラリを defer で読み込みたいため、
html-webpack-pluginscript-ext-html-webpack-plugin を使用します。

webpack.config.js
module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(ROOT_PATH, 'src', 'index.html'),
    }),
    new ScriptExtHtmlWebpackPlugin({
      defaultAttribute: 'defer',
    }),
  ]
  ...
}

type

three.js の 型を global に定義します。
THREEx の型はここで省きます。

src/types/global.d.ts
import type Three from 'three';
import type THREEx from './THREEx';

declare global {
  const THREE: typeof Three;
  const THREEx: THREEx;

  interface Window {
    THREE?: typeof THREE;
    THREEx?: typeof THREEx;
  }
}

threex-artoolkit

ドキュメントにあるthreex-artoolkitを使用します。
threex-artoolkitは three.jsAR.js を簡単に使用するための拡張機能です。

THREEx.ArToolkitSource

位置追跡を行うために解析されるものを選択します。
webcamを使用するため、 sourceType に記載します。
他にも video, imageを選択できます。

const arToolkitSource = new THREEx.ArToolkitSource({
    sourceType: 'webcam',
    displayWidth: window.innerWidth,
    displayHeight: window.innerHeight,
  });

THREEx.ArToolkitContext

It is the main engine. It will actually find the marker position in the image source.

cameraParametersUrl にはカメラパラメータのURLを追記するようです。
examples にもあるように、detectionMode も同様に記載します。

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

THREEx.ArMarkerControls

こちらはマーカーの位置を制御するものになっているようです。
patternの種類とパスを与えます。
カメラをArMarkerControlsの第2引数にする場合はchangeMatrixModeになり、new THREE.Group()を使用する場合はmodelViewMatrixになります。

const group = new THREE.Group();

new THREEx.ArMarkerControls(arToolkitContext, group, {
  type: 'pattern',
  patternUrl: 'data/patt.hiro',
  changeMatrixMode: 'modelViewMatrix',
});

これで threex-artoolkit についての設定は完了です。
あとは init()して動かしていきます。


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

arToolkitSource.init(() => {
  setTimeout(() => {
    // onResizeは後述します。
    onResize();
  }, 1000);
});

three.js

threex-artoolkit の設定が終われば、普通に three.js を書けば良いです。
Scene, Camera, Renderer を書いていきます。

SampleComponent.tsx

export const SampleComponent = () => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const scene = new THREE.Scene();
  const group = new THREE.Group();
  const perspectiveCamera = new THREE.PerspectiveCamera();

  scene.add(perspectiveCamera);
  scene.add(group);

  const webGLRenderer = useSampleWebGLRenderer(canvasRef);

  ...

  return <canvas id="canvas" ref={canvasRef} />
}
useSampleWebGLRenderer.tsx
export const useSampleWebGLRenderer = (canvas: React.MutableRefObject<HTMLCanvasElement | null>) => {
  const [webGLRenderer, setWebGLRenderer] = React.useState<THREE.WebGLRenderer | null>(null);

  React.useEffect(() => {
    if (!canvas.current) return;
    const webGLRenderer = new THREE.WebGLRenderer({
      canvas: canvas.current,
      antialias: true,
      alpha: true,
    });
    webGLRenderer.setPixelRatio(window.devicePixelRatio);
    webGLRenderer.setClearColor(new THREE.Color(), 0);
    webGLRenderer.setSize(window.innerWidth, window.innerHeight);
    webGLRenderer.domElement.style.position = 'absolute';
    webGLRenderer.domElement.style.top = '0px';
    webGLRenderer.domElement.style.left = '0px';
  }, [canvas]);

  return webGLRenderer;
}

次に画面のリサイズをする処理を書きます。

const onResize = () => {
  arToolkitSource.onResizeElement();
  if (!webGLRenderer) return;
  arToolkitSource.copyElementSizeTo(webGLRenderer.domElement);
  if (arToolkitContext.arController !== null) {
    arToolkitSource.copyElementSizeTo(arToolkitContext.arController.canvas);
  }
};

あとは、表示させたいテキストを表示させていきます。

const useSampleTextLoader = (group: THREE.Group, text: string, y: number) => {
  React.useEffect(() => {
    const loader = new THREE.FontLoader();
    loader.load(typeface, (font) => {
      const textGeometry = new THREE.TextBufferGeometry(text, {
        font,
        size: 0.2,
        height: 0.04,
      });
      textGeometry.center();
      const textMesh = new THREE.Mesh(textGeometry, new THREE.MeshNormalMaterial());
      textMesh.position.set(0, y, 0);
      group.add(textMesh);
    });
  }, [group, text, y]);
};

useSampleTextLoader(group, 'sample', 0.75);

ブラウザからマーカーを生成する

AR.js のリポジトリの examples にある threex-arpatternfile.js を使ってブラウザから表示させるためのマーカーを生成します。
RawGitを使用してCDNとして読み込むため、htmlthreex-arpatternfile.jsを追記します。
* RawGitはおすすめはしないのですが、ここでは使用しております。

<script defer src="https://rawcdn.githack.com/AR-js-org/AR.js/a5619a021e6ff40427ff8f9c62169e99a390f56b/three.js/examples/marker-training/threex-arpatternfile.js"></script>

次に、THREEx.ArPatternFile.buildFullMarker, THREEx.ArPatternFile.encodeImageURL を使用して
マーカーのURLを生成します。

  const useSampleUserPattern = () => {
  const { user, setUser } = useUserContext();
  const [marker, setMarker] = React.useState<string | null>(null);
  const [patternUrl, setPatternUrl] = React.useState<string | null>(null);

  const {
    params: { screenName },
  } = React.useRouteMatch<{ screenName: string }>();

  const userPattern = () => {
    (async () => {
      const currentUser = user ? user : await getUser(screenName);
      const iconURL = currentUser.profile_image_url_https;
      const imgDataRes = await fetch(iconURL.replace('_normal', ''));
      const imgData = await imgDataRes.blob();
      const imgLocalURL = URL.createObjectURL(imgData);
      await new Promise((resolve) => {
        THREEx.ArPatternFile.buildFullMarker(imgLocalURL, 0.5, 512, 'black', (markerUrl) => {
          resolve(markerUrl);
          setMarker(markerUrl);
        });
      });

      THREEx.ArPatternFile.encodeImageURL(imgLocalURL, (pattern) => {
        const patternBlob = new Blob([pattern], { type: 'text/plain' });
        setPatternUrl(URL.createObjectURL(patternBlob));
      });
    })();
  };
  ...
}

getUser(screenName) はTwitterのAPIを叩いており、user情報を取得しています。iconURLは画像の解像度を調節するためにreplace()してます。

patternUrl を生成して new THREEx.ArMarkerControlspatternUrl へ渡せば大丈夫です。

marker

<img src={marker} alt="marker" />

のようにJSXの部分にかけば表示されます。
これでユーザーごとにマーカーの画像が生成できるようになりましたのでここまでとします。

まとめ

最近はAR.jsを触っていたので、簡単にまとめてみました。
Node.jsでTwitter APIを書いてて使用しているライブラリはこちらです。
今回はやっていることがシンプルなので、Twitter APIの方は省きました。
書き終わってみれば意外にも簡単に見えますが、実際コードを書いているときはかなりしんどいものがありました。(型とか)
表示させるテキストは日本語に対応できていないですし、コードも一旦動けばいいというような形で書いてましたので、まだまだ改善が必要だと個人的には感じております。

参考になった記事を載せておきます。
https://qiita.com/aa_debdeb/items/4edf6a2e053e02305ef5
https://qiita.com/aa_debdeb/items/c88338c90a0adb061002

大分コードを省いているので少し分かりづらいかもしれないのですが、最後まで読んでいただいた皆さん、ありがとうございました!是非他のアドベントカレンダーにある記事もご覧ください!

PR

内定先のCyberAgentでは22卒のエンジニア採用を行っています!
https://www.cyberagent.co.jp/careers/news/detail/id=25511

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7