WebXR Device APIを使う 第1回 ARを実装する

WebXR Device APIはデバイスの「状態」を取得するためのAPI。WebXRのXRとは、virtual reality(VR)やaugmented reality(AR)などの技術の総称です。このAPIでどんなことができるのか紹介します。

発行

著者 小山田 晃浩 フロントエンド・エンジニア
WebXR Device APIを使う シリーズの記事一覧

WebXR Device APIとは

WebXR Device API*は、デバイスの位置、姿勢、現実の風景などをJavaScriptで取得する機能です。取得した情報はVRとARどちらにも利用できます。WebXR Device APIのXRとは、virtual reality(VR)や augmented reality(AR)などの技術の総称なのです。

*注:従来の仕様

VRに特化していた仕様である、WebVR 1.1は廃止され、WebXR Device APIの一部として統合されました。

WebXR Device APIは「デバイスの状態を取り出す」ことに特化しているため、仕様自体のボリュームは大きくありません。表現(出力)には、WebGLを使うことになります。WebXR Device APIは状態を取得するところまでが責務で、その状態を使って何をするかは、また別の技術に任されているということなのです。

WebXR Device API は2019年2月にW3C仕様草案になっており、スマートフォン、デスクトップ、ヘッドマウントディスプレイなどのさまざまなデバイス上のブラウザで利用されることが考慮されています。

【ワンポイント】さらに深い仕様の理解のために

仕様の内容はKhronosのOpenXRとも共通点が多く見受けられます。

筆者はWebXR Device APIの仕様を読んでみて、曖昧な部分があれば、OpenXRでの同様の機能の説明を読んでみると、より理解が深まりました。

ブラウザによるWebXR Device API実装

WebXR Device APIの基本機能は、2019年12月リリースのChrome 79で対応されました。近々リリース予定のChrome 81ではARやヒットテスト機能など、さらに多くの機能に対応します。

2020年3月初旬現在では、ChromeとChromiumベースのMicrosoft Edgeが対応しています。

FirefoxでもWebXR Device APIへの対応が進められており、近いうちに正式対応しそうです。

一方で、Safariは、WebKitGoalsfor2020によると、2020年での対応予定にWebXR Device API対応は含まれていません*。Safariが未対応であることは、Web開発者にとってネガティブな要素ではありますが、ソフトウェアを通して限定的にXRの対応をする方法があります。これは次回以降で解説します。

*注:Safariの対応

実装自体はIgaliaが始めてはいます。

とはいえ、現状それがSafariに登載される気配はありません。

WebXR Device APIの特殊性

通常ならここでブラウザ対応の話は終わりになるのですが、WebXR Device APIの対応には、デバイスそのものの機能(たとえば、ジャイロセンサーの有無など)も関連します。前述したように、デバイスの状態を取得するのがWebXR Device APIの責務ですが、そもそもデバイスが検知できない「状態」を把握することはできません。

デバイスがどの程度状態をトラッキングができるかを、自由度、DoF(degrees of freedom)と呼びます。DoFの種類には、3DoFと6DoFの2つがあります。

3DoF(Three Degrees of Freedom)デバイスは、デバイスの回転角度をトラッキングできます。

6DoF(Six Degrees of Freedom)デバイスは、デバイスの回転角度に加えて、平行移動位置をトラッキングできます。ユーザーの位置を取得するために、カメラやLEDマーカーなどの外部装置を利用する場合もあります。

つまり、同じエンジンのブラウザであっても、デバイス自体に備わっているセンサーなどの機能によってWebXR Device APIのサポート状況は異なるわけです。たとえば、デスクトップのChromeと、モバイルのChromeでは、WebXR Device APIを通して利用できる機能が異なります。

WebXR Device APIの機能

それでは具体的にWebXR Device APIの仕様と実装方法を解説します。

まず、すべての機能は、navigator オブジェクトの下に新設されたxrにあります。navigator.xrの主要なメソッドは、次の2つです。

  • isSessionSupported():モードがサポートされているか否かをbooleanで返す。デバイスとブラウザの両方の機能を照合する役割。
  • requestSession( XRSessionMode ):モードに対応したセッションを開始する。

3つのモード

現在、WebXRには3つのモードがあります。モードは仕様内にenum XRSessionModeとして定義されています。

  • "inline":HTML ページ中に埋め込まれる、視差を必要としない、単独のビューポートを持つHTML要素。「マジックウインドウ」とも呼ばれる。視点はマウスやタッチで操作する。普通のWebGLコンテンツとあまり変わらない。
  • "immersive-vr":デバイスの画面全体でコンテンツを表示する。さらに、デバイスの角度や位置を視点に利用できる。現実の風景を反映しない。
  • "immersive-ar":デバイスの画面全体でコンテンツを表示する。さらに、デバイスの角度や位置を視点に利用できる。加えて、AR独自の機能や、カメラから取り込んだ現実の風景も利用できる。

指定したモードでセッションをつくる

次のコードは、指定したモードのXRSessionをつくる例です。手順は以下のようになります。

  1. 指定したモードが利用可能かどうかチェック
  2. モードが利用できなればボタンを無効化
  3. モードが利用できればボタンを有効化したままユーザーのクリックを待つ
  4. クリックがあれば起動

注意点としては、XRセッションは、必ずユーザーの操作がなければ起動できない点です。たとえば、ユーザーの関与なしに、Webページを開いた直後に自動でXRセッションを起動するといったことはできません。

ボタンを用意し、ユーザーのクリックを受け付けることでXRセッションを作成することができます。

任意のモードでセッションをつくる

( async () => {

  const $button = document.getElementById( 'startButton' );
  // 任意のモードが利用可能かを調べる
  const isInlineSupported = navigator.xr && await navigator.xr.isSessionSupported( 'inline' );

  // モードが利用できなければ起動ボタンを無効化
  $button.disabled = ! isInlineSupported;

  const onButtonClicked = async () => {

    const xrSession = await navigator.xr.requestSession( 'inline' );
    console.log( xrSession );

  }

  $button.addEventListener( 'click', onButtonClicked );

} )();

上記のコードを実行した結果は次の図のようになります。「click to start」ボタンを押すと、コンソールにXRセッションが表示できているのがわかります。

XRSessionの機能

前述の通りxrオブジェクト自体はシンプルな内容でした。一方で、得られたXRSessionオブジェクトには、XRを実現するための機能が格納されています。よく使う代表的な機能は次のようなものです。

  • XRSpace:「デバイスが3D空間のどこにあるか」を得る。(xrSession.requestReferenceSpace()によりインスタンスを取得できる)
  • XRPose:「デバイスの姿勢」を得る。(xrSession.requestAnimationFrame() の引数として得られる)
  • XRRenderState:WebGLの描画時に利用するVFOV、far、near*、baseLayer(real world imagery)を得る(xrSession.renderState

*注:VFOV、far、near

この3つの値は描画のためにカメラが使う値です。VFOVは、vertical field of view、つまり縦方向の視野角、far はファークリッピング、nearはニアークリッピングと呼ばれます。詳しくは次の記事の「カメラを準備」を参照してください。

WebXR Device APIを使った実装の手順

WebXR Device APIは複雑な仕様に見えるかもしれませんが、手順はシンプルにまとまっています。

たとえば、ARのコンテンツを作る場合には次の手順となります。

  1. immersive-arのモードでセッションを作成
  2. カメラからの映像をXRWebGLLayerで取得
  3. WebGLで任意の3Dオブジェクトを描画
  4. WebGLのbindFramebufferでカメラからの映像(2)と3Dオブジェクト(3)を統合して表示

この手順を図に表したのが以下です。

WebXR Device API を利用したAR

では実際に、WebXR Device API を利用してARを実現してみましょう。冒頭で述べたとおり、WebXR Device APIはデバイスから状態を引き出すためのAPIに過ぎません。描画はWebGLを利用することになります。

ここでは、WebXR Device APIから取り出した「センサーを通した姿勢」や「カメラを通した現実の風景」をthree.jsの描画結果に反映するAR実装の例を用意しました。

コードの実行結果は、AndroidのモバイルChromeで以下の動画のように表示されます。

コード全体は次のリンクで確認できます。

以下のURLにて、ご自身の以下の端末で動作を試すことができます。2020年3月現在は Android Chrome 81 で確認可能です。

AR用のHTMLを用意する

まず、AR用のHTMLの構造です。描画結果を表示するためのcanvas要素と、ユーザーにセッションを起動してもらうためのボタンが置いてあります。

script要素内にコードを記述していきます。three.jsを読み込むために、script要素のtype属性をmoduleとしています。

描画用のHTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>=^.^=</title>
<style>
body {margin: 0;}
canvas{display: block;}
#startButton{font-size: 50px;}
</style>
</head>
<body>
<button type="button" id="startButton">click to start</button>
<!-- WebXR の描画結果は WebGL の canvas となる。-->
<canvas id="xrCanvas"></canvas>
<script type="module">
<!-- ここにJavaScriptが入る -->
</script>
</body>
</html>

AR用のJavaScriptを用意する

ここからは、WebXR Device API経由で取得した情報を利用して、ARコンテンツをthree.jsで描画をするためのコードを書いていきます。

これがVRであっても、基本的な処理の流れは同じです。途中説明を入れるためにコードが途切れていますが、一続きのコードになっています。

モードの設定とセッションの準備

下準備として、immersive-arモードが使えるかどうかチェックします。ユーザーがボタンをクリックすると、onEnterAR関数が実行されます。

なお、WebXR Device APIのメソッドの多くは、Promiseで結果を返します。Promiseの入れ子になるとコードが読みづらくなってしまいますので、ここではasync・awaitを利用してコードを書いていきます。

モードの設定とセッションの準備

import * as THREE from 'https://unpkg.com/three@0.113.2/build/three.module.js';

const width  = window.innerWidth;
const height = window.innerHeight;
const $button = document.getElementById( 'startButton' );

( async () => {

  // 任意のモードが利用可能かを調べる
  const isArSupported = navigator.xr && await navigator.xr.isSessionSupported( 'immersive-ar' );

  // モードが利用できなければ起動ボタンを無効化
  $button.disabled = ! isArSupported;

  // ユーザーが操作して初めて WebXR を起動できる
  $button.addEventListener( 'click', onEnterAR );

  async function onEnterAR() {

onEnterAR関数

次に、immersive-arモードでセッションをつくり、three.jsのセットアップ、並びに、3Dオブジェクトを準備します。3Dオブジェクトは0.2ユニットの立方体です。XRではWebGLの1ユニットが1メートルに対応しますので、この立方体は各辺が0.2mで表示されることになります。

初期設定と立方体の描画(onEnterAR関数)

    $button.style.display = 'none';

    // immersive-ar を渡したときは、カメラからの現実の風景を入力することができる。
    // 他にも inline や vr のモードがある
    const xrSession = await navigator.xr.requestSession( 'immersive-ar' );

    // 本来は `getContext( 'webgl' )` で gl コンテキストを引き出す際に
    // オプションとして「`xrCompatible: true`」を明示する必要がある。
    // three.js では常に「`xrCompatible: true`」のオプションがついているので
    // ここでは特別な設定をせずに XR として利用することができる
    const renderer = new THREE.WebGLRenderer( { canvas: xrCanvas } );
    renderer.autoClear = false;
    renderer.setSize( width, height );

    const gl = renderer.getContext();

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera();

    scene.add( new THREE.GridHelper( 100, 100 ) );
    // 立方体を描画する
    const box = new THREE.Mesh(
      new THREE.BoxBufferGeometry( .2, .2, .2 ),
      new THREE.MeshNormalMaterial()
    );
    scene.add( box );

カメラの画像とデバイスの姿勢、位置を取得する

次にカメラから取り込まれた風景を受けるためのベースレイヤーを設定します。ここではWebGLと互換性のある、XRWebGLLayerからベースレイヤーをつくります。作成したベースレイヤーに毎フレーム最新のカメラ入力を反映するためにxrSession.updateRenderState()に紐付けておきます。

また、ARでは現実世界のユーザーの位置を知る必要もあります。ユーザーの位置の起点(オリジン)をxrSession.requestReferenceSpace( 'local' )で取得します。これにより取得できるオブジェクトはリファレンススペースと呼ばれます。

リファレンススペースはlocal以外にもいくつかの引数をとることができますが、詳しくは次回で説明します。

カメラの映像をベースレイヤーに格納(onEnterAR関数)

    // 「デバイスのカメラから取り込まれた現実の風景」の受け先となる、ベースレイヤーを作る。
    const xrWebGLLayer = new XRWebGLLayer( xrSession, gl );
    // `updateRenderState()` を設定していない場合、`xrSession.requestAnimationFrame()` が呼ばれないので注意
    xrSession.updateRenderState( { baseLayer: xrWebGLLayer } );

    // 「デバイスの姿勢(pose)」の参照先を作る
    // これにより、デバイスの傾きや位置を取得できるようになる。
    const referenceSpace = await xrSession.requestReferenceSpace( 'local' );

レンダリングする

XRセッションから取り出したベースレイヤー、リファレンススペースを利用して3Dの描画を行います。

毎フレームの処理には、XRセッションのrequestAnimationFrame()を利用します。

通常のWebGLコンテンツなど利用するwindow.requestAnimationFrame()と違い、XRセッションのxrSession.requestAnimationFrame()は第二引数として「その瞬間のデバイスの状態」が収められたxrFrameが自動で渡されてきます。window.requestAnimationFrame()にはそのような仕様はありません。

また、XRセッションのrequestAnimationFrame()はデバイスの画面のリフレッシュレートに応じてコールバックが呼ばれます。一方、window.requestAnimationFrame()ではブラウザの内部実装でFPSが最大60ほどに抑えられています。

xrFrameから、現在の姿勢(pose)、つまり位置や角度をgetViewerPose()で取り出します。姿勢を取得する際に引数にはリファレンススペースを渡します。「姿勢」はWebGL空間の視点の位置、角度にマッピングすることができます。WebGL空間の視点は、three.jsのCameraが相当します。

また、getViewport()により視点の画角(FoV)も取得できます。取得できたデバイスの視点の位置、角度、画角を、WebGL空間の視点のプロジェクション行列と同期すれば、AR、VRになる、というわけです。

加えて、ARでは、「現実世界の風景」も必要になります。「現実世界の風景」は、すでにxrWebGLLayerとして作成していますので、これをgl.bindFramebuffer()でWebGLの描画に転写します。

レンダリングする

    xrSession.requestAnimationFrame( onDrawFrame );

    function onDrawFrame( timestamp, xrFrame ) {

      xrSession.requestAnimationFrame( onDrawFrame );
      // 姿勢を取り出す。行列(matrix)の要素が格納された配列で受け取ることができる。
      const pose = xrFrame.getViewerPose( referenceSpace );

      // xrFrame からは、現在のセッション、とベースレイヤーを取り出すこともできる。
      // xrFrame.session === xrSession;
      // xrSession.renderState.baseLayer === xrWebGLLayer;

      // 現実の風景をWebGLのフレームバッファーに転写する
      gl.bindFramebuffer( gl.FRAMEBUFFER, xrWebGLLayer.framebuffer );

      if ( ! pose ) return;

      // xrSession が両目用の場合には、2つの画面をレンダリングするために`pose.views` に2つのviewオブジェクトが格納さえています。
      // 全画面やマジックウインドウなど、1画面の場合は、pose.viewsには1つのviewオブジェクトが格納されています。
      pose.views.forEach( ( view ) => {

        const viewport = xrWebGLLayer.getViewport( view );
        renderer.setSize( viewport.width, viewport.height );

        camera.matrix.fromArray( view.transform.matrix );
        camera.projectionMatrix.fromArray( view.projectionMatrix );
        camera.updateMatrixWorld( true );

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

      } );

    }

  }

} )();

ARを生成するため、コードは長めになりますが、行われている処理や組み立てはそれほど複雑ではありません。

ここまでのまとめ

第1回目はWebXR Device APIの概要と、ARの組み立て方を中心に、大まかな処理の流れを解説しました。

次回はVRや別のモードで作成したセッションなどについて解説する予定です。