虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Three.js + WebXR で平面に画像を AR 表示してみた🌟

こんにちは、虎の穴ラボの後藤です。

現実空間の平面を検出し、画像を配置する Web アプリを作成しました!
好きな画像を自由に AR 表示できます!

DEMO

※ マーカーが表示された後、画面タップで配置できます

URLhttps://img2ar-main-public.vercel.app/
※ Android 14 + Chrome 125 で動くことを確認しています。iOS では恐らく動きません :(

リポジトリhttps://github.com/tenugui-taro/img2ar-main__public

解説用リポジトリhttps://github.com/tenugui-taro/img2ar-vite__public

目次

Web 技術で AR コンテンツを作る!

AR コンテンツ開発と聞くと、特殊な知識や複雑な開発環境が必要だと考えるかもしれません。
しかし、WebXR を使えば、普段の Web 開発と同じ技術スタックで AR コンテンツを作成できます!

developer.mozilla.org

WebXR Device API はまだ実験的な機能で、ブラウザ対応状況は下記の通りです。

"WebXR" | Can I use... Support tables for HTML5, CSS3, etc

Step by Step の実装解説

ここからは、実際の開発準備から各ステップの詳細な実装解説に進みます。平面検出、画像表示、UI 実装といったポイントを中心に解説します。詳細なコードは以下のリポジトリにあります。

解説用リポジトリhttps://github.com/tenugui-taro/img2ar-vite__public

使用技術

  • WebGL:ブラウザ内で 3D グラフィックスを描画するための JavaScript API
  • WebXR:Web ブラウザで VR および AR 体験を実現するために必要な機能を提供
  • Three.js:WebGL を簡単に扱うための JavaScript ライブラリ

Step 0:開発準備

まずはPCで動作確認するために WebXRエミュレーター拡張機能をインストールします!

chromewebstore.google.com

インストールした後は、開発者コンソールから「WebXR」を選択し「Samsung Galaxy S8+ (AR)」を設定しておきます。

開発環境について、解説用リポジトリでは Vite + React + TypeScript 構成にしています(この辺りはお好みでどうぞ

$ npm create vite@latest
✔ Project name: … img2ar-vite
✔ Select a framework: › React
✔ Select a variant: › TypeScript

Three.js を扱うために必要な下記ライブラリをインストールします。

$ npm i three @types/three

これで開発の準備が整いました。次のステップでは、立方体を AR 表示する実装を進めていきます。

Step 1:立方体を AR 表示

まずはシンプルな例として、AR セッション中にタップ操作で立方体を表示する実装を行います。

コードsrc/components/HelloAR/index.tsx

DEMO

基本は Three.js お馴染みのシーン、レンダラー、カメラ、光源などを用意します。
加えて、下記のような AR 開発特有の実装をしています。

  • レンダラーの XR 有効化
  • アニメーションループの設定
  • AR セッションの開始
  • コントローラー設定

それでは、それぞれの実装について詳しく見ていきましょう。

レンダラーの XR 有効化

レンダラーの WebXR フラグを有効化することで、WebXR 機能を使うことができるようになります。

WebXRManager.enabled

  /* Renderer */
  const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true,
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.xr.enabled = true; // WebXR フラグを有効化

アニメーションループの設定

AR セッションではrequestAnimationFrameではなく、AR 用のアニメーションループメソッドrenderer.setAnimationLoopを用いて設定します。

WebGLRenderer.setAnimationLoop

  // フレームごとに実行されるアニメーション
  animate();

  function animate() {
    renderer.setAnimationLoop(render); // アニメーションループの設定
  }

  async function render() {
    // 立方体の回転
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    // レンダリング
    renderer.render(scene, camera);
  }

AR セッションの開始

AR セッションを開始するには、requestSessionを用います。また、併せてレンダラーに AR セッションを設定しています。

XRSystem: requestSession()

  const SESSION_OPTIONS: ARButtonSessionInit = {
    domOverlay: { root: document.body },
  };

  async function startARSession() {
    if (navigator.xr) {
      session = await navigator.xr.requestSession(
        "immersive-ar",
        SESSION_OPTIONS
      );
      if (session == null) {
        throw new Error("Failed to start AR session");
      }
      renderer.xr.setReferenceSpaceType("local");
      renderer.xr.setSession(session);
    } else {
      throw new Error("WebXR is not supported");
    }
  }

ここでは AR セッションの開始処理を独自に実装していますが、Three.js に用意されている ARButton を用いると、AR サポート確認や AR セッション管理が楽に実装できます!

使い方や特徴は下記を参照ください。

『AR檸檬🍋』WebXRでこっそりレモン🍋を置く【クソアプリ】 #three.js - Qiita

コントローラー設定

コントローラーを設定し、AR セッション内でタップ操作した時に実行したい処理を設定できるようにします。

WebXRManager.getController

  /* Controller */
  const controller = renderer.xr.getController(0);
  controller.addEventListener("select", onSelect);
  scene.add(controller);

  function onSelect() {
    cube.position.set(0, 0, -0.3).applyMatrix4(controller.matrixWorld);
  }

Step 1 では以上のような実装を行い、立方体を AR 表示できるようにしました。次のステップでは、平面検出を行い、より現実感のあるオブジェクト配置を目指します!

Step 2:平面検出

平面を検出して立方体を平面上に配置できるようにします!

コードsrc/components/HitTest/index.tsx

DEMO ※ 立方体とレティクルのサイズは大きくしています

実装としては、平面検出 -> 検出座標の取得 -> オブジェクト配置 を行っています。

平面検出機能を有効化する

navigator.xr.requestSessionのオプションでhit-testを指定することで平面検出を有効化します。

  const SESSION_OPTIONS: ARButtonSessionInit = {
    domOverlay: { root: document.body },
    requiredFeatures: ["hit-test"],
  };

  async function startARSession() {
    if (navigator.xr) {
      session = await navigator.xr.requestSession(
        "immersive-ar",
        SESSION_OPTIONS
      );
      if (session == null) {
        throw new Error("Failed to start AR session");
      }
      renderer.xr.setReferenceSpaceType("local");
      renderer.xr.setSession(session);
    } else {
      throw new Error("WebXR is not supported");
    }
  }

平面検出し座標を取得する

アニメーションループの処理内で、フレームごとに平面検出を行っています。 また、検出座標を取得しレティクルを移動させる処理も併せて行います。

  let hitTestSource: XRHitTestSource | null = null;
  let hitTestSourceRequested = false;

  async function render(timestamp: number, frame: XRFrame) {
    if (frame) {
      const referenceSpace = renderer.xr.getReferenceSpace();
      if (!referenceSpace) return;

      const session = renderer.xr.getSession();
      if (!session) return;

      if (hitTestSourceRequested === false) {
        session.requestReferenceSpace("viewer").then((referenceSpace) => {
          session.requestHitTestSource!({ space: referenceSpace })!.then(
            function (source) {
              hitTestSource = source;
            }
          );
        });

        session.addEventListener("end", function () {
          hitTestSourceRequested = false;
          hitTestSource = null;
        });

        hitTestSourceRequested = true;
      }

      if (hitTestSource) {
        const hitTestResults = frame.getHitTestResults(hitTestSource);

        if (hitTestResults.length) {
          const hit = hitTestResults[0];
          reticle.visible = true;

          reticle.matrix.fromArray(
            hit.getPose(referenceSpace)!.transform.matrix
          );
        } else {
          reticle.visible = false;
        }
      }
    }
    renderer.render(scene, camera);
  }

タップ操作で立方体を配置

タップ操作時にレティクルの座標へ立方体を配置することで、平面上に立方体を配置することができます

  /* Controller */
  const controller = renderer.xr.getController(0);
  controller.addEventListener("select", onSelect);
  scene.add(controller);

  function onSelect() {
    if (reticle.visible) {
      cube.position.setFromMatrixPosition(reticle.matrix);
    }
  }

これで、平面検出を行い、その上に物体を配置することができました。次は画像を平面上に配置するステップに進みます。

Step 3:画像を配置する

プレーンのテクスチャに画像を設定することで、画像が配置されているように見せます

コードsrc/components/ImageTexture/index.tsx

DEMO

レンダラーの透過設定

png 画像の透過を保つために、レンダラーにalpha: trueを指定しておきます。

  /* Renderer */
  const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true,
  });

画像のテクスチャ設定

ジオメトリーにはプレーンを選択し、画像を読み込んでプレーンのマテリアルに設定しています。
裏側から見た時も表示するため、マテリアルにはside: THREE.DoubleSideを設定しています。

  // プレーンを作成
  const pGeometry = new THREE.PlaneGeometry(0.15, 0.15);
  pGeometry.translate(0, 0.1, 0);

  // テクスチャーを読み込み
  const loader = new THREE.TextureLoader();
  const texture = loader.load("/images/1_Introduction.png");
  if (!texture) return;
  texture.colorSpace = THREE.SRGBColorSpace;

  // マテリアルにテクスチャーを設定
  const imageMaterial = new THREE.MeshBasicMaterial({
    map: texture,
    transparent: true,
    opacity: 1.0,
    side: THREE.DoubleSide,
    depthWrite: false,
  });
  const mesh = new THREE.Mesh(pGeometry, imageMaterial);
  scene.add(mesh);

ここまでで、テクスチャを設定したプレーンが配置されるようになりました。次に、最後のステップとして、AR セッション中に React で作成した UI を重ねて表示する方法を説明します。

Step 4:React による UI 実装

AR セッション中にも React で作成した UI を重ねて表示できるように設定します。
これにより、3D レンダリングは Three.js、複雑な UI は React と役割を分けてそれぞれの長所を活かすことができます。

コードsrc/components/Overlay/index.tsx

※ WebXR エミュレーター拡張機能だと UI が確認できないため、デプロイするなどして実機でご確認ください

オーバーレイの設定

navigator.xr.requestSessionのオプションでdom-overlayを指定することでオーバーレイ機能を有効化します

  const SESSION_OPTIONS: ARButtonSessionInit = {
    domOverlay: { root: document.body },
    requiredFeatures: ["hit-test"],
    optionalFeatures: ["dom-overlay"],
  };

  async function startARSession() {
    if (navigator.xr) {
      session = await navigator.xr.requestSession(
        "immersive-ar",
        SESSION_OPTIONS
      );
      renderer.xr.setReferenceSpaceType("local");
      renderer.xr.setSession(session);
    } else {
      throw new Error("WebXR is not supported");
    }
  }

これで、AR セッション中にも React で作成した UI が表示されるようになります!

振り返り

WebXR を活用することで、アプリのインストールなしにブラウザ上で簡単に AR 体験を提供できました。また、Three.js を使用することで、手軽に WebGL の実装が行えました。

WebXR Device API はまだ実験的な機能ではありますが、今回の開発を通じてその可能性を十分に感じることができました。今後も WebXR の動向に注目していきたいと思います。

参考