このコードラボではARウェブアプリケーションのサンプルを構築していきます。このアプリケーションはJavaScriptを使用して、まるで現実世界に存在するかのように3Dモデルを描画します。

ここではWebXR Device API(旧WebVR API)を使用します。このAPIはまだ開発中で、拡張現実(Augmented Reality、AR)と仮想現実(Virtual Reality、VR)の両方に必要な機能を足し合わせたものになっています。今回はその中でもChromeで開発されているWebXR Device APIの実験的なAR拡張に注目します。

拡張現実とはなにか?

拡張現実(AR)とは通常はコンピューターが生成したグラフィックスと現実世界を合成することを表すために使用する用語です。スマートフォンを使用する拡張現実の場合にはライブカメラ映像上のそれらしい位置にコンピューターグラフィックスを表示することを意味します。スマートフォンが空間内を移動するのに合わせてそれらしく見せ続けるには、AR利用可能なデバイスは移動している空間を把握できなければいけません。そのために必要となる機能には物体表面を検知することや、環境内の光源を推定することなど含まれます。さらに3D空間内でのデバイスの姿勢(位置と向き)も把握できる必要があります。

セルフィーのフィルタやARベースのゲームのようなアプリの中でARを使うことはすでによく行われていて、拡張現実の利用は広がり続けています。GoogleやAppleの拡張現実プラットフォームであるARCoreARKitがリリースされてまだ一年も経っていませんが、すでに数億のARを利用可能なスマートフォンが存在しています。今や数百万の人々の手の中に必要なテクノロジがあり、フラグを有効にするだけでWebXR Device APIにAR拡張を追加する実験的な初期提案を利用できます。

何を作成するのか

このコードラボでは、拡張現実を使用して現実世界に3Dモデルを配置するウェブアプリケーションを作成します。このアプリケーションは以下のようなことを行います。

  1. デバイスのセンサーを使用して現実空間内での位置と向きを取得し、追跡します
  2. ライブカメラビューの上に3Dモデルを合成して表示します
  3. オブジェクトを配置するために現実空間内の物体表面のヒットテストを実行します

何を学ぶか

このコードラボは仮想現実APIに集中します。適切なリポジトリコードを予め提供することで、無関係な概念やコードの説明は省略します。

必要なもの

以下に必要なものの一覧をまとめました。それぞれの詳細についてはすぐ後の節で説明します。

AR機能が利用できるChromeを取得する

本コードラボ執筆時点では、最初のAR機能はバージョン69.0.3453.2以上のChrome Canaryビルドでしか実装されていません。Settings -> About Chromeを開くと、使用しているChromeのバージョンを確認できます。

ChromeのAR機能を有効にする

このコードラボを作成している時点では、AR機能の初期実装を使用するにはwebxrフラグとwebxr-hit-testフラグを有効にしなければいけません。ChromeでWebXR拡張現実機能のサポートを有効にするには以下のように設定してください。

  1. 手元のAndroidフォンがARCoreをサポートしていることを確認します。
  2. Chromeのバージョンが69.0.3453.2以上であることを確認します。
  3. URLバーにchrome://flagsと入力します。
  4. Search flags入力フィールドにwebxrと入力します。
  5. WebXR Device API#webxr)フラグをEnabledにします。
  1. WebXR Hit Test#webxr-hit-test)フラグをEnabledにします。
  2. フラグの変更を有効にするためにRELAUNCH NOWをタップします。

コードラボの最初のステップを始めるには開発用のARデバイスで下のリンクを開いてください。ページに「Your browser does not have AR features」というメッセージが表示されている場合は、Chrome CanaryのバージョンとWebXRの2つのフラグを再度確認してください。それらを変更した場合はブラウザの再起動が必要になります。

TRY IT

コードをダウンロードする

下のリンクをクリックしてコードラボのコードをすべてワークステーションにダウンロードしてください。

ソースコードのダウンロード

ダウンロードしたzipファイルを展開します。必要な全リソースとコードラボのステップごとに分けられたいくつかのディレクトリが含まれたフォルダ(ar-with-webxr-master)が展開されます。step-05フォルダとstep-06フォルダにはコードラボのステップ5とステップ6の最終的な成果があり、finalフォルダも同様にコードラボの最終成果があります。これらは参考のために使用してください。すべてのコーディング作業はworkというディレクトリ内で行います。

ウェブサーバーをインストールして確認する

自由に好きなウェブサーバーを使って構いませんが、何も用意がない人のためにChrome Web Serverの使い方を説明しておきます。まだワークステーションにインストールしていなければ、Chrome Web Storeからインストールできます。

Web Server for Chromeをインストール

Web Server for Chromeアプリをインストールしたら、chrome://appsを開き、ウェブサーバーアイコンをクリックしてください。

次のようなダイアログが表示され、ローカルウェブサーバーを設定できます。

  1. choose folderボタンをクリックして、ar-with-webxr-masterフォルダを選択します。これでウェブサーバーのダイアログ(Web Server URL(s)セクション)で強調されているURLを通じて作成中のサイトを表示できます。
  2. Optionsの下のAutomatically show index.htmlがチェックされていることを確認します。
  3. Web Server: STARTED/STOPPEDというラベルのあるトグルスイッチを左右にスライドするとサーバを停止/再開できます。

  1. 少なくともWeb Server URL(s)が表示されることを確認します。

次にARデバイスでlocalhost:8887を表示することでワークステーションの同じポートにアクセスできるようにARデバイスを設定します。

  1. 開発用ワークステーションで、chrome://inspectを開き、Port forwarding...ボタンをクリックします。

Port forwarding settingsダイアログを使用して、ポート8887localhost:8887にフォワードするように設定します。Enable port forwardingがチェックされていることを確認してください。

接続を確認します。

  1. ワークステーションとARデバイスをUSBケーブルで接続します。
  2. ARデバイスのChrome CanaryのURLバーでhttp://localhost:8887と入力します。ARデバイスはこのリクエストを開発用ワークステーションのウェブサーバーにフォワードするはずです。
  3. ARデバイスで、workディレクトリをタップしてwork/index.htmlページを読み込みます。

ENTER AUGMENTED REALITYボタンのあるページが表示されるはずです...

...が、もしUnsupported Browserエラーページが表示されたら、前に戻ってchrome://flagsでChrome Canaryのバージョンを確認し、Chromeを再起動してください。

  1. ウェブサーバーとARデバイスが接続されたら、ENTER AUGMENTED REALITYボタンをクリックします。もしかするとARCoreのインストール用プロンプトが表示されるかもしれません。
  2. ARアプリケーションを初めて動かす場合は、カメラのパーミッションが求められるでしょう。

すべてが順調に進めば、複数の直方体のあるシーンがカメラフィード上に重ねて表示されているはずです。カメラによる空間の解析が進むにつれてシーン認識が改善されます。つまり少しカメラを動かすと物体がより安定します。

歴史

WebGLはウェブ上での3Dコンテンツの描画を可能にする強力なグラフィックライブラリですが、ウェブからVRデバイスにアクセスするにはデバイス検出やリフレッシュレートの同期、位置の把握などが必要です。ウェブ開発者がウェブでのVRアプリケーション構築を試すことができるように、実験的にWebVR 1.1 APIがブラウザに実装されました。これはVRヘッドセット用に適切な変形が施された立体視シーンをウェブに描画するフレームワークでした。DaydreamGearVRが利用できるモバイルデバイスとOculus RiftHTC Viveのようなルームスケールシステムなどが複数のブラウザでサポートされていたプラットフォームです。

利用される業界やユースケースが増えるにつれ、ウェブでARをサポートする必要性が増し、ARとVRに関係する技術はお互いに似通っているため、その両方を含める形でWebXR Device APIが作られました。まだ開発中で変更の可能性もありますが、WebXR Divice APIの核となる部分は十分に安定していて、すべてのメジャーなブラウザベンダーで担当者により開発が進められています。このAPIはVR体験とARプロポーザルの初期実装をサポートしています。AR機能についてはこれからプロトタイプを作成し動作を確認していきます。

実装

WebXR Device APIの最初の実装は、フラグ(#webxr)の設定とオリジントライアルが必要となりますが、Chrome 67で有効になりました。AR機能の実験的な初期実装は#webxr-hit-testフラグを有効にしたChrome 69以上で利用できます。本記事を執筆している時点で、WebVRを実装しているすべてのブラウザが、将来的にWebXR Device APIをサポートすることを宣言しています。

将来

ブラウザが現在利用できるシーン認識機能は「ヒットテスト」だけです。この機能を使用すると、例えばユーザーがスクリーンをタップした位置に応じてデバイスからレイを飛ばして、現実世界での衝突点を取得し、その座標を仮想シーンをオーバーレイするための情報として利用できます。

将来的にはシーン認識機能が拡充され、光源の推定やサーフェス、メッシュ、特徴点クラウドなどの検出が提供されるようになるでしょう。

CSSによるスタイル設定と、カメラの位置と同期した複数の直方体のあるシーンを描画するという基本的なAR機能を実装したJavaScriptを組み込んだHTMLページがすでに用意されています。これを使用することでコードラボで前準備がほとんど必要なくなり、AR機能の実装だけに集中することができます。

HTMLページ

AR体験は既存のウェブ技術を使用した従来のウェブページ上に組み込まれます。今回の開発ではフルスクリーン描画されるcanvasを使用するため、使用するHTMLファイルはそれほど複雑なものにはなりません。グラフィックライブラリが使用する<canvas>をフルスクリーンで表示するようにCSSを設定します。HTMLページはスクリプトも読み込みます。

AR機能を起動するにはユーザーの操作が必要です。そのためいくつかのMaterial Design Lite要素を使用して、「Start AR」ボタンとブラウザがサポートしていないことを示すメッセージを表示します。

すでにworkディレクトリにあるindex.htmlファイルの内容は次のようになっています。(これは実際の内容の一部だけを抜き出したものです。ファイルにそのままコピーして使用しないでください

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Building an augmented reality application with the WebXR Device API</title>
    <link rel="stylesheet" type="text/css" href="../shared/app.css" />
    <link rel="stylesheet" type="text/css" href="../third_party/mdl/material.min.css" />
  </head>
  <body>
    <div id="enter-ar-info" class="demo-card mdl-card mdl-shadow--4dp">
      <!-- デモのためのMaterial Design要素 -->  
      <!-- ... -->
    </div>
    <script src="../third_party/three.js/three.js"></script>
    <script src="../shared/utils.js"></script>
    <script src="app.js"></script>
  </body>
</html>



JavaScriptコードの主要部分を確認する

アプリは3D JavaScriptライブラリであるthree.js、いくつかのユーティリティ、WebXRとアプリ独自のコードを保持するapp.jsという3つのJavaScriptファイルで構成されています。それではアプリのボイラープレートコードを見ていきましょう。

workディレクトリにはアプリのコード(app.js)も含まれています。そこでは次のとおりAppクラスが定義されています。

class App {
  constructor() {
    ...
  }

  async init() {
    ...
  }

  async onEnterAR() {
    ...
  }

  onNoXRDevice() {
    ...
  }

  async onSessionStarted(session) {
   ...
  }

  onXRFrame(time, frame) {
    ...
  }
};

window.app = new App();

アプリをインスタンス化した後で、Chrome DevToolsを使用してデバッグできるようにwindow.appに保持します。

コンストラクタはARを使用するために非同期関数であるthis.init()を呼び出してXRSessionを開始します。この関数はまずWebXR Device APIのエントリポイントnavigator.xrと、AR機能XRSession.prototype.requestHitTest(Chromeのwebxr-hit-testフラグで有効化される)が存在するかどうかを確認します。

すべてがうまくいけば、ユーザーがボタンをクリックしたときにXRセッションを作成するクリックリスナーを「Enter Augmented Reality」ボタンにバインドします。

class App {
  ...
  async init() {
    if (navigator.xr && XRSession.prototype.requestHitTest) {
      try {
        this.device = await navigator.xr.requestDevice();
      } catch (e) {
        this.onNoXRDevice();
        return;
      }
    } else {
      this.onNoXRDevice();
      return;
    }

    document.querySelector('#enter-ar').addEventListener('click', this.onEnterAR);
  }
}

XRDeviceが見つかると、this.deviceとして保持します。デバイスとやりとりするにはそのXRSessionが必要になります。XRDeviceは複数のXRSessionを持つことができ、それぞれのセッションを通してデバイスの姿勢、ユーザーの環境、デバイスに描画するためのハンドルが取得できます。

ページに表示するにはセッションの出力が必要です。それにはWebGLコンテンツを描画するためにWebGLRenderingContextを作成するのと同様に、XRPresentationContextを作成しなければいけません。

class App {
  ...
  async onEnterAR() {
    const outputCanvas = document.createElement('canvas');
    const ctx = outputCanvas.getContext('xrpresent');
    
    try {
      const session = await this.device.requestSession({
        outputContext: ctx,
        environmentIntegration: true
      });
      document.body.appendChild(outputCanvas);
      this.onSessionStarted(session);
    } catch (e) {
      this.onNoXRDevice();
    }
  }
}

canvasのgetContext('xrpresent')を呼び出すと、XRPresentationContextを返します。このコンテキストはXRデバイスに表示するためのものです。その後でXRデバイスのrequestSession()に出力表示用のコンテキストと、AR機能を有効にするためのenvironmentIntegrationフラグを渡してセッションを要求し、プロミスの実行完了を待ちます(await)。

XRSessionが手に入れば、three.jsを使用して描画する準備は完了です。アニメーションループを開始しましょう。three.jsのWebGLRendererを作成します。このオブジェクトには2つめのcanvasが保持されています。alphapreserveDrawingBufferをtrueに設定することとautoClearを無効にすることに気をつけてください。このオブジェクトを通じてWebGLRenderingContextは使用し、非同期に対応するXRデバイスを設定します。コンテキストがデバイスに対応していることを確認するとXRWebGLayerを作成してXRSessionのbaseLayerに設定できます。これはthis.init()で作成されたcanvas上にライブカメラフィードと合成して表示するシーンを、このコンテキストを通して描画するようにセッションに対して指示するものです。

three.jsシーンを描画するには、描画を処理するWebGLRenderer、描画されるオブジェクトを含むシーン、シーンを描画するための視点を指示するカメラという3つの要素が必要です。今回はDemoUtils.createCubeScene()を使用して事前に空間上に浮かぶ大量の立方体が配置されえいるシーンを使用します。three.jsやWebGLをこれまで使ったことがなかったとしても心配は不要です!描画に問題があった場合は、自身のコードとサンプルを比較してください。

レンダリングループを開始する前に、このデバイスは位置も追跡するということを指定するために(対照的にDaydreamやGearVRを使用する場合はVR体験なので向きだけを追跡します)、'eye-level'を渡してXRFrameOfReferenceを取得する必要があります。基準系(Frame of Reference)が手に入ると、window.requestAnimationFrameと同様にして、XRSessionのrequestAnimationFrameを使用してレンダリングループを開始できます。

class App {
  ...
  async onSessionStarted(session) {
    this.session = session;

    document.body.classList.add('ar');
    
    this.renderer = new THREE.WebGLRenderer({
      alpha: true,
      preserveDrawingBuffer: true,
    });
    this.renderer.autoClear = false;

    this.gl = this.renderer.getContext();
    
    await this.gl.setCompatibleXRDevice(this.session.device);
  
    this.session.baseLayer = new XRWebGLLayer(this.session, this.gl);

    this.scene = DemoUtils.createCubeScene();

    this.camera = new THREE.PerspectiveCamera();
    this.camera.matrixAutoUpdate = false;

    this.frameOfRef = await this.session.requestFrameOfReference('eye-level');
    this.session.requestAnimationFrame(this.onXRFrame);
  }
}

タイムスタンプとXRPresentationFrameを引数としてthis.onXRFrameが毎フレーム呼び出されます。フレームオブジェクトからは、空間内での位置と向きを表すオブジェクトであるXRDevicePoseと、現在のデバイス上に適切に表示するためにシーンを描画しなければいけないすべての視点を表すオブジェクトであるXRViewの配列を取得できます。

描画する前にまず現在の姿勢を取得し、session.requestAnimationFrame(this.onXRFrame)を呼び出して次のフレームのアニメーションをキューに登録しなければいけません。立体視VRには(それぞれの目のために)ビューが2つありますが、今回はAR体験をフルスクリーンに表示するのでビューはひとつしかありません。描画するにはそれぞれのビューをループして、ビューから取得できる投影行列とカメラの姿勢から取得できるビュー行列を使用してカメラを準備する必要があります。これにより仮想カメラの姿勢が推定されるデバイスの物理的な姿勢に同期されます。その後で、与えられた仮想カメラを使用してシーンを描画するようにレンダラーに対して指示できます。

class App {
  ...
  onXRFrame(time, frame) {
    const session = frame.session;
    const pose = frame.getDevicePose(this.frameOfRef);

    session.requestAnimationFrame(this.onXRFrame);

    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.session.baseLayer.framebuffer);

    if (pose) {
      for (let view of frame.views) {
        const viewport = session.baseLayer.getViewport(view);
        this.renderer.setSize(viewport.width, viewport.height);

        this.camera.projectionMatrix.fromArray(view.projectionMatrix);
        const viewMatrix = new THREE.Matrix4().fromArray(pose.getViewMatrix(view));
        this.camera.matrix.getInverse(viewMatrix);
        this.camera.updateMatrixWorld(true);

        this.renderer.clearDepth();

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

これがすべてです!XRDeviceを取得して、XRSessionを作成し、仮想カメラの姿勢を推定されるデバイスの物理的な姿勢で更新してから各フレームでシーンを描画するというコードの全体に目を通し終わりました。

試してみる

コードを一通り見終わりました。それでは実際にボイラープレートが動くところを見てみましょう!すでにセットアップの最中に一度表示しましたが、コードを見終わってから見直すとどうでしょう。デバイスを動かすことで視界も移動する宙に浮いた直方体とカメラフィードが見えるはずです。動き回ればそれだけトラッキングが正確になります。何がどうなっているのかいろいろと試してみましょう。

TRY IT

アプリの実行になにか問題がある場合は、「はじめに」「準備する」を確認してください。

ここまでで、ライブビデオフィードを表示し、デバイスの姿勢を使用してカメラの位置と向きを設定して3Dの直方体をビデオフィード上に表示できることが確認できました。遂にヒットテストを使用して現実世界とのやり取りを始めるときです。ここでは現実世界の物体表面(例えば地面など)を検出してそこに直方体を配置していきたいと思います。

「ヒットテスト」とはなにか?


ヒットテストは一般には空間内のある一点からある方向に直線を伸ばし、対象のオブジェクトのいずれかと交わるかどうかを確かめる処理のことを言います。今回の場合は、ARデバイスの画面をタップしたときに、指からデバイスの中に向かってレイが発せられて、デバイスのカメラに写っているとおりに物理世界の中を真っすぐ進んでいくと考えてください。

WebXR Device APIは内部的なAR機能と周囲の認識結果に基づいてこのレイが現実世界のいずれかのオブジェクトと交差するかどうかを知らせてくれます。少し地面に向けたデバイスを通して周辺を眺めながらスマートフォンをタップするとレイが地面にヒットし、衝突が発生した位置がレスポンスとして得られるはずです。

シーンの準備

stabilizationというIDを持つdivをindex.htmlに追加しましょう。ここではユーザーのstabilizationステータスを表すアニメーションを表示してデバイスを動かすように促します。この要素は<body>のクラス属性によって制御され、ARが始まったときに表示されて、レチクルが物体表面を見つけると表示されなくなります。

  <div id="stabilization"></div>
  <script src="../third_party/three.js/three.js"></script>
  ...
</body>
</html>

まずは浮遊する直方体をapp.jsから取り除きましょう。onSessionStarted関数の中のDemoUtils.createCubeScene()をまっさらなnew THREE.Scene()と交換してください。

  // this.scene = DemoUtils.createCubeScene();
  this.scene = new THREE.Scene();

シーンを作成した後すぐに、衝突が発生したときに配置するオブジェクトを作成する必要があります。three.jsでは表示対象のオブジェクトはジオメトリとマテリアルを持つTHREE.Meshオブジェクトで表されます。以下のコードではジオメトリとマテリアルを作成し、直方体の座標を原点が底面に来るように調整しています。今はまだメッシュを作成して座標変換し、その結果をthis.modelとして保持するだけです。

  const geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5);
  const material = new THREE.MeshNormalMaterial();
  geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.25, 0));
  this.model = new THREE.Mesh(geometry, material);

画面の中心を追跡して継続的にヒットテストを行い、デバイスが環境をどのように認識しているかについて、仮想的なフィードバックをユーザーに返すReticleというthree.jsオブジェクトを用意しました(ソースコードはshared/utils.jsにあります)。onSessionStarted関数の中の基準系を取得した直後にコンストラクタを追加します。onXRFrame関数では、レチクルのupdateメソッドを呼び出し、Reticleが物体表面を検出したらbodyに'stabilized'クラスを追加します。これによりユーザーにデバイスを動かすように促すstabilizationヘルパーアニメーションが表示されなくなります。

class App {
  ...
  onSessionStarted(session) {
    ...
    this.reticle = new Reticle(this.session, this.camera);
    this.scene.add(this.reticle);
    
    this.frameOfRef = await this.session.requestFrameOfReference('eye-level');
    this.session.requestAnimationFrame(this.onXRFrame);
    ...
  }
  onXRFrame(time, frame) {
    let session = frame.session;
    let pose = frame.getDevicePose(this.frameOfRef);

    this.reticle.update(this.frameOfRef);
    if (this.reticle.visible && !this.stabilized) {
      this.stabilized = true;
      document.body.classList.add('stabilized');
    }
    ...
  }
}

次はユーザーが画面をタップしたときにだけヒットテストを実行するようにしましょう。それにはイベントハンドラが必要です。

class App {
  constructor() {
    ...
    this.onClick = this.onClick.bind(this);
  }
  
  onSessionStarted(session) {
    ...
    this.reticle = new Reticle(this.session, this.camera);
    this.scene.add(this.reticle);

    window.addEventListener('click', this.onClick);
  }

  onClick(e) {
    console.log('click!');
  }
}

ここでアプリを実行してみてください。デバイスでChrome DevToolsを使用しているなら、クリックするとコンソールに文字列が表示されることがわかるでしょう。Chrome DevToolsを設定したければ、Get Started with Remote Debugging Android Devicesを参照してください。

ヒットテストの実行

一体のモデルとタップにバインドされたイベントハンドラが用意でき、ついに実際にヒットテストを実行できます。XRSessionのAPIには原点と向き、そして先ほど作成したXRFrameOfReferenceが必要です。今回の原点はWebXR座標系におけるデバイスの座標で、向きはデバイスの背面に向かうベクトルになります。three.jsにはある点からベクトルを投影する便利な関数があるので、その関数を使用しましょう。このおかげで行列やベクトルに馴染みがなくても物怖じする必要はありません。必要な数学的操作を扱うためにthree.jsのRaycasterを利用して、原点とベクトルをTHREE.Vector3として取得します。setFromCameraのx座標とy座標はスクリーン空間での座標で、この関数は-1から1の間に収まるように正規化されたデバイス座標を受け取ります。今回は単に画面の中心にオブジェクトを配置したいだけなので、xとyの値はちょうど0になります。

requestHitTestXRHitResultの配列に解決されるPromiseを返します。XRHitResultには衝突が発生した位置の配列が保持されています。デバイスからまっすぐに伸ばした直線が壁や床に当たると、hitの位置はその直線と壁面または床面が交差する位置になります。hitが見つかると、最初のhit(最も近いhit)を取り出して、THREE.Matrix4に変換します。それから、先ほど作成した直方体を衝突が発生した場所に配置し、さらに直方体をシーンに追加すると、その直方体を画面に描画できます。onClickの中でawaitを使用しなければいけないので、onClickはasync関数にしなければいけません。

class App {
  ...
  async onClick(e) {
    const x = 0;
    const y = 0;
   
    this.raycaster = this.raycaster || new THREE.Raycaster();
    this.raycaster.setFromCamera({ x, y }, this.camera);
    const ray = this.raycaster.ray;
    
    const origin = new Float32Array(ray.origin.toArray());
    const direction = new Float32Array(ray.direction.toArray());
    const hits = await this.session.requestHitTest(origin,
                                                   direction,
                                                   this.frameOfRef);

    if (hits.length) {
      const hit = hits[0];
      const hitMatrix = new THREE.Matrix4().fromArray(hit.hitMatrix);

      this.model.position.setFromMatrixPosition(hitMatrix);

      this.scene.add(this.model);
    }
  }
}

これでタップに反応して、デバイスの背面方向にレイを飛ばし、交差が見つかると衝突点に直方体を配置するイベントハンドラが得られました。Chrome for Androidによって支えられている内部的なARプラットフォームは水平な表面と垂直な表面を検出できます。周辺を見て回り、どこに新しく直方体を配置できるか確認してみましょう!

試してみる

TRY IT

これで現実世界の物体表面に立方体を配置するアプリが手に入りました。モバイルデバイスで現実世界の中に何かを描画できるのは非常に楽しいことですが、派手な赤色の直方体だけではあまり感動的ではありません。アーティストが登録した3DアセットのコレクションであるPolyから取ってきたモデルをシーンに読み込んでみましょう。それらのモデルの多くはCC-BYライセンス下にあり、シーンの中で修正して利用できます。ここではNaomi ChenのArctic FoxモデルCC-BY)を使用します。

使用するモデルの属性情報を追加しましょう。#infoというIDを持つdivを作成して、属性情報を何行か追加します。

  <div id="stabilization"></div>
  <div id="info">
    <span>
      <a href="https://poly.google.com/view/dK08uQ8-Zm9">Model</a> by
      <a href="https://poly.google.com/user/f8cGQY15_-g">Naomi Chen</a>
      <a href="https://creativecommons.org/licenses/by/2.0/">CC-BY</a>
    </span><br />
  </div>
  <script src="../third_party/three.js/three.js"></script>
  ...
</body>
</html>

OBJファイルとMTLファイルを読み込む

まずはじめに、three.jsのOBJLoaderを使用してモデルを読み込み、MTLLoaderを使用してマテリアルを読み込みます。これらのローダーはthree.jsではなく別のファイルとして提供されていますが、third_party/three.jsディレクトリにコピーを用意しています。index.htmlに以下を追加しましょう。three.jsよりも後、そしてアプリのコードよりは前に読み込まれるようにしてください!

<html>
...
<body>
  ...
  <script src="../third_party/three.js/three.js"></script>
  <script src="../third_party/three.js/OBJLoader.js"></script>
  <script src="../third_party/three.js/MTLLoader.js"></script>
  <script src="../shared/utils.js"></script>
  <script src="app.js"></script>
</body>
</html>

ホッキョクギツネのモデルとテクスチャはすでにassets/ディレクトリにあります。app.jsを開き、ファイルの一番上にそれらの場所を示す定数を追加してください。

const MODEL_OBJ_URL = '../assets/ArcticFox_Posed.obj';
const MODEL_MTL_URL = '../assets/ArcticFox_Posed.mtl';
const MODEL_SCALE = 0.1;

次に、onSessionStarted関数に戻ります。シーンにライトを追加し、モデルを読み込まなければいけません。事前にいくつかのライトが設定済みであるTHREE.Sceneを作成するDemoUtils.createLitScene()を使用してください。その次にまた別のユーティリティDemoUtils.loadModel()を通して、先ほど取り込んだOBJLoaderとMTLLoaderを使用します。この関数は読み込みが完了したときにthree.jsオブジェクトのモデルとして解決されるPromiseを返します。しかし結果はawaitせずに、モデルの読み込み中も描画が続けられるようにしましょう。

モデルの読み込みが完了すると、そのモデルをthis.modelに保持します。最後にモデルは様々な形状やサイズがあり得るので、モデルを適切に縮小する必要があります。今回のホッキョクギツネに関しては0.1がちょうどいい値です。他のモデルを読み込んだときは、様々なスケールを試してください。モデルが想像の1000倍も大きかったり小さかったりするせいでまったく見えないということもよくあります!

async onSessionStarted(session) {
  ...
  // this.scene = new THREE.Scene();
  this.scene = DemoUtils.createLitScene();
  
  DemoUtils.fixFramebuffer(this);
  
  // 直方体モデルはもう必要ない。
  // ごめんね、さようなら!
  /*
  const geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5);
  const material = new THREE.MeshBasicMaterial();
  geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0.25, 0));
  this.model = new THREE.Mesh(geometry, material);
  */

  DemoUtils.loadModel(MODEL_OBJ_URL, MODEL_MTL_URL).then(model => {
    this.model = model;
    this.model.scale.set(MODEL_SCALE, MODEL_SCALE, MODEL_SCALE);
  });

  ...
}

新しいモデルを配置する

これでほぼ完成です!この時点で、ヒットテスト関数でthis.modelオブジェクトが配置されます。このオブジェクトは今では単なる直方体ではなくホッキョクギツネです。しかしまだいくつか改善できる点があります。モデルは非同期に読み込まれるので、onClickハンドラでモデルが読み込まれる前に配置しようとすることがあります。はじめに簡単な確認を追加して、this.modelがまだ読み込まれていないければすぐに処理を終わりましょう。その後で、もしhitがあれば、キツネを私たちの方を向くように回転させたいと思います。モデルの位置と私たちの位置を元に角度を取得し、私たちの方に向くようにキツネをY軸回転する数学的な処理を行うユーティリティを用意しているので、モデルを配置したあとでその関数を呼び出します。

async onClick(e) {
  if (!this.model) {
    return;
  }
  ...

  if (hits.length) {
    const hit = hits[0];

    const hitMatrix = new THREE.Matrix4().fromArray(hit.hitMatrix);

    this.model.position.setFromMatrixPosition(hitMatrix);

    DemoUtils.lookAtOnY(this.model, this.camera);

    this.scene.add(this.model);
  }
}

試してみる

これでレチクルが安定してからARデバイスをタップするとホッキョクギツネのモデルが物体表面に表示されるようになったはずです!ARデバイスで下のリンクをクリックして、このステップの完成版を試してください。

TRY IT

模式化されたローポリモデルを使用していますが、そのようなデジタルオブジェクトでも適切なライティングと影を与えれば十分なリアルさが得られ、シーンに溶け込んで見えます。ライティングと影は、どのライトから影を生じるのか、どのマテリアルがそれを受けて影を描画するのか、どのメッシュが影を生じさせるのかを指定することで、three.jsで扱うことができます。幸い、シーンには影を生じるライトと影だけが描画される平らな表面がすでに用意されています。影が有効になった最終的な作業結果はfinal/app.jsで確認できます。

まずはじめに、three.jsのWebGLRendererで影を有効にしなければいけません。レンダラーを作成した後で、shadowMapに以下のような値を設定します。

async onSessionStarted(session) {
  ...
  this.renderer = new THREE.WebGLRenderer(...);
  ...
  this.renderer.shadowMap.enabled = true;
  this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  ...
}

小さなユーティリティ関数、DemoUtils.fixFramebuffer()を呼び出してthree.jsのフレームバッファの問題に対処する必要があります。興味があればソースコードを見るとより詳細な情報が得られますが、とりあえずはWebXRで影を使うときに生じる問題が修正されるということだけを知っておけばいいでしょう。次に、キツネのメッシュが影を発するように設定しなければいけません。モデルのスケールを設定する直前で、子メッシュを操作して、castShadowブーリアン変数をtrueに設定します。

async onSessionStarted(session) {
  ...

  // WebXRで影を使用する場合は、このヘルパー関数を
  // 呼び出すことを忘れないように!
  DemoUtils.fixFramebuffer(this);

  DemoUtils.loadModel(MODEL_OBJ_URL, MODEL_MTL_URL).then(model => {
    this.model = model;

    // このモデルに含まれるすべてのメッシュが影を発するように設定
    this.model.children.forEach(mesh => mesh.castShadow = true);

    this.model.scale.set(MODEL_SCALE, MODEL_SCALE, MODEL_SCALE);
  });
  ...
}

最後に重要なことですが、現在のシーンにはshadowMeshという影だけを描画する平らな水平面オブジェクトが含まれています。はじめはこの表面のY座標は10000ユニットに設定されています。ヒットテストで表面が見つかったときに、キツネの影が現実世界の地面の上に描画されるようにshadowMeshを現実世界の表面と同じ高さにしましょう。ヒットテスト関数の中でthis.modelの座標を設定した後に、シーンの子オブジェクトの中からshadowMeshを見つけ、shadowMeshのY座標をモデルのY座標と揃えます。

async onClick(e) {
  ...
  if (hits.length) {
    ...
    const shadowMesh = this.scene.children.find(c => c.name === 'shadowMesh');
    shadowMesh.position.y = this.model.position.y;
    this.scene.add(this.model);
  }
}

試してみる

前回のステップと非常によく似ていますが、今度はキツネの近くの地面の上に柔らかな影が表示されているはずです。

TRY IT

ホッキョクギツネのモデルをPolyから読み込んで現実世界の表面に配置し、影を表示させることができました。WebXRのrequestHitTest機能は最初に実装されたARシーン認識コンポーネントですが、将来的にはシーンの光源やサーフェス、メッシュ、深度、ポイントクラウドなどを測定する機能が利用可能になるでしょう。これらの機能を利用すると、さらにうまくデジタルオブジェクトを現実世界と統合できるようになります。例えば、光源推定によりモデルの影が現実世界と統合され、深度データによりデジタルオブジェクトの前に何かがあるときにオブジェクトを物陰に隠すことができるようになります。さらにWebXR Device APIではAnchorの仕様の策定が進められています。これにより現実世界のオブジェクトを追跡し、シーン内のより正確な位置にオブジェクトを配置できるようになります。

追加課題: Polyから独自のモデルを選択する

Polyから別のモデルを見つけて、シーンに読み込みましょう。モデルが見えない場合はスケールをいろいろと変更してみましょう!

リソース