7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AR.jsとthree.jsでマーカーをタップできるようにする

Posted at

AR.jsとthree.jsを使ったMarker Based ARでマーカーをタップできるようにしてみます。

作成したサンプルはマーカーを認識すると、「Tap Marker!」という文字が表示されます。映像からはわからないですが、マーカーをタップするとタップ位置にランダムな高さのキューブを作成します。マーカー外をタップした場合は何も起きません。
arjs-threejs-tap-marker.gif

コード全文は以下のようになっています。AR.jsをthree.jsで使用する基本的な方法は過去に記事を書いたので、そちらを参照してください。このサンプルを実行するにはdataというディレクトリを作成して、その中にcamera_para.datpatt.hirohelvetiker_bold.typeface.jsonを配置する必要があります。

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <title>Tappable Marker 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 style='margin: 0px; overflow: hidden;'>
    <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();
      const camera = new THREE.PerspectiveCamera();
      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 marker = new THREE.Group();
      scene.add(marker);

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

      const markerPlane = new THREE.Mesh(
        new THREE.PlaneBufferGeometry(1, 1),
        new THREE.MeshBasicMaterial({
          colorWrite: false,
          depthWrite: false,
        })
      );
      markerPlane.rotation.x = -0.5 * Math.PI;
      marker.add(markerPlane);

      const raycaster = new THREE.Raycaster();
      renderer.domElement.addEventListener('click', (event) => {
        const element = event.currentTarget;
        const x = event.clientX - element.offsetLeft;
        const y = event.clientY - element.offsetTop;
        const w = element.offsetWidth;
        const h = element.offsetHeight;
        const mouse = new THREE.Vector2((x / w) * 2 - 1, -(y / h) * 2 + 1);
        raycaster.setFromCamera(mouse, camera);

        const intersects = raycaster.intersectObject(markerPlane);
        if (intersects.length !== 0) {
          const intersect = intersects[0];
          const height = 0.1 + Math.random() * 0.4;
          const cube = new THREE.Mesh(
            new THREE.BoxBufferGeometry(0.15, height, 0.15),
            new THREE.MeshNormalMaterial()
          );
          cube.position.copy(marker.worldToLocal(intersect.point));
          cube.position.y += 0.5 * height;
          marker.add(cube);
        }
      });

      const loader = new THREE.FontLoader();
      loader.load('data/helvetiker_bold.typeface.json', (font) => {
        const textGeom = new THREE.TextBufferGeometry('Tap Marker!', {
            font: font,
            size: 0.2,
            height: 0.04,
          });
        textGeom.center();
        const text = new THREE.Mesh(
          textGeom,
          new THREE.MeshNormalMaterial()
        );
        text.position.set(0, 0.75, 0);
        marker.add(text);
      });

      requestAnimationFrame(function animate(){
        requestAnimationFrame(animate);
        if (arToolkitSource.ready) {
          arToolkitContext.update(arToolkitSource.domElement);
        }
        renderer.render(scene, camera);
      });
    </script>
  </body>
</html>

サンプルコードについて、マーカーのタップに関連する箇所を説明していきます。基本的な考え方としてはマーカーと同じ位置・大きさに見えない平面を作成して、その平面とタップ位置との交差判定をTHREE.Raycasterで行うという流れになります。

まずカメラです。AR.jsではカメラの射影行列をAR.js側が設定するため基底クラスのTHREE.Cameraを使用すればいいのですが、THREE.Raycasterのサポートチェックを回避するためにTHREE.PerspectiveCameraを使用します。

const camera = new THREE.PerspectiveCamera();

マーカーと同じ位置・大きさに見えない平面を追加します。colorWritedepthWritefalseにすることで色も深度も描画されなくなります。

const markerPlane = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(1, 1),
  new THREE.MeshBasicMaterial({
    colorWrite: false,
    depthWrite: false,
  })
);
markerPlane.rotation.x = -0.5 * Math.PI;
marker.add(markerPlane);

THREE.Raycasterを生成して、canvasがタップされたら先程作成した平面とタップ位置の交差判定を行います。交差している場合にはキューブを追加します。intersect.pointには交差位置がワールド座標系で含まれているので、ローカル座標系に変換してからmarkerに追加しています。THREE.Raycasterを使った交差判定については以下の記事を参考にしました。
Three.jsでオブジェクトとの交差を調べる - ICS MEDIA

const raycaster = new THREE.Raycaster();
renderer.domElement.addEventListener('click', (event) => {
  const element = event.currentTarget;
  const x = event.clientX - element.offsetLeft;
  const y = event.clientY - element.offsetTop;
  const w = element.offsetWidth;
  const h = element.offsetHeight;
  const mouse = new THREE.Vector2((x / w) * 2 - 1, -(y / h) * 2 + 1);
  raycaster.setFromCamera(mouse, camera);

  const intersects = raycaster.intersectObject(markerPlane);
  if (intersects.length !== 0) {
    const intersect = intersects[0];
    const height = 0.1 + Math.random() * 0.4;
    const cube = new THREE.Mesh(
      new THREE.BoxBufferGeometry(0.15, height, 0.15),
      new THREE.MeshNormalMaterial()
    );
    cube.position.copy(marker.worldToLocal(intersect.point));
    cube.position.y += 0.5 * height;
    marker.add(cube);
  }
});
7
6
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
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?