JSARToolKit を使用して拡張現実アプリケーションを作成する

Ilmari Heikkinen

はじめに

この記事では、JSARToolKit ライブラリと WebRTC getUserMedia API を使用して、ウェブ上で拡張現実アプリケーションを実行する方法について説明します。レンダリングにはパフォーマンスの向上を理由に、WebGL を使用しています。この記事の最終結果は、ウェブカメラの動画内の拡張現実マーカーの上に 3D モデルを配置するデモ アプリケーションです。

JSARToolKit は JavaScript 用の拡張現実ライブラリです。これは GPL の下でリリースされたオープンソース ライブラリであり、私は Mozilla Remixing Reality デモ用に作成した Flash FLARToolKit の直接ポートです。FLARToolKit 自体は Java NyARToolKit の移植版であり、C ARToolKit の移植版です。長い道のりでしたが、たどり着きました。

JSARToolKit は canvas 要素に対して機能します。キャンバスから画像を読み取る必要があるため、画像はページと同じオリジンから取得するか、CORS を使用して同一オリジン ポリシーを回避する必要があります。簡単に言うと、テクスチャとして使用する画像要素または動画要素の crossOrigin プロパティを '' または 'anonymous' に設定します。

解析のためにキャンバスを JSARToolKit に渡すと、JSARToolKit は画像内で見つかった AR マーカーのリストと、対応する変換行列を返します。マーカーの上に 3D オブジェクトを描画するには、使用する 3D レンダリング ライブラリに変換行列を渡し、その行列を使用してオブジェクトが変換されるようにします。次に、WebGL シーンに動画フレームを描画し、その上にオブジェクトを描画すれば完成です。

JSARToolKit を使用して動画を分析するには、動画をキャンバスに描画してから、キャンバスを JSARToolKit に渡します。これをフレームごとに行うと、動画の AR トラッキングが可能になります。JSARToolKit は最新の JavaScript エンジンでも高速で、640x480 の動画フレームでもリアルタイムでこの処理を行えます。ただし、動画のフレームが大きいほど、処理に時間がかかります。動画のフレームサイズは 320x240 が適していますが、小さなマーカーや複数のマーカーを使用する場合は 640x480 をおすすめします。

デモ

ウェブカメラのデモを見るには、ブラウザで WebRTC を有効にする必要があります(Chrome の場合は about:flags に移動し、MediaStream を有効にしてください)。また、以下の AR マーカーを印刷する必要もあります。スマートフォンやタブレットでマーカーの画像を開き、ウェブカメラに表示してみることもできます。

AR マーカー。
AR マーカー。

JSARToolKit のセットアップ

JSARToolKit API は Java によく似た API なので、少し変形して使う必要があります。基本的な考え方は、ラスター オブジェクトで動作する検出オブジェクトを用意することです。検出機能とラスターの間には、ラスター座標をカメラ座標に変換するカメラ パラメータ オブジェクトがあります。検出されたマーカーを検出器から取得するには、マーカーを反復処理し、その変換行列をコードにコピーします。

まず、ラスター オブジェクト、カメラ パラメータ オブジェクト、検出オブジェクトを作成します。

// Create a RGB raster object for the 2D canvas.
// JSARToolKit uses raster objects to read image data.
// Note that you need to set canvas.changed = true on every frame.
var raster = new NyARRgbRaster_Canvas2D(canvas);

// FLARParam is the thing used by FLARToolKit to set camera parameters.
// Here we create a FLARParam for images with 320x240 pixel dimensions.
var param = new FLARParam(320, 240);

// The FLARMultiIdMarkerDetector is the actual detection engine for marker detection.
// It detects multiple ID markers. ID markers are special markers that encode a number.
var detector = new FLARMultiIdMarkerDetector(param, 120);

// For tracking video set continue mode to true. In continue mode, the detector
// tracks markers across multiple frames.
detector.setContinueMode(true);

// Copy the camera perspective matrix from the FLARParam to the WebGL library camera matrix.
// The second and third parameters determine the zNear and zFar planes for the perspective matrix.
param.copyCameraMatrix(display.camera.perspectiveMatrix, 10, 10000);

getUserMedia を使用してウェブカメラにアクセスする

次に、WebRTC API を介してウェブカメラの動画を取得する video 要素を作成します。事前録画された動画の場合は、動画のソース属性を動画の URL に設定するだけです。静止画像からマーカー検出を行う場合も、同様の方法で image 要素を使用できます。

WebRTC と getUserMedia はまだ新しいテクノロジーであるため、機能検出を行う必要があります。詳しくは、Eric Bidelman 氏による HTML5 での音声と動画のキャプチャをご覧ください。

var video = document.createElement('video');
video.width = 320;
video.height = 240;

var getUserMedia = function(t, onsuccess, onerror) {
  if (navigator.getUserMedia) {
    return navigator.getUserMedia(t, onsuccess, onerror);
  } else if (navigator.webkitGetUserMedia) {
    return navigator.webkitGetUserMedia(t, onsuccess, onerror);
  } else if (navigator.mozGetUserMedia) {
    return navigator.mozGetUserMedia(t, onsuccess, onerror);
  } else if (navigator.msGetUserMedia) {
    return navigator.msGetUserMedia(t, onsuccess, onerror);
  } else {
    onerror(new Error("No getUserMedia implementation found."));
  }
};

var URL = window.URL || window.webkitURL;
var createObjectURL = URL.createObjectURL || webkitURL.createObjectURL;
if (!createObjectURL) {
  throw new Error("URL.createObjectURL not found.");
}

getUserMedia({'video': true},
  function(stream) {
    var url = createObjectURL(stream);
    video.src = url;
  },
  function(error) {
    alert("Couldn't access webcam.");
  }
);

マーカーの検出

検出機能が正常に動作することを確認したら、AR 行列を検出するための画像のフィードを開始できます。まず、ラスター オブジェクトのキャンバスに画像を描画し、次にラスター オブジェクトに対して検出機能を実行します。検出器は、画像内で見つかったマーカーの数を返します。

// Draw the video frame to the raster canvas, scaled to 320x240.
canvas.getContext('2d').drawImage(video, 0, 0, 320, 240);

// Tell the raster object that the underlying canvas has changed.
canvas.changed = true;

// Do marker detection by using the detector object on the raster object.
// The threshold parameter determines the threshold value
// for turning the video frame into a 1-bit black-and-white image.
//
var markerCount = detector.detectMarkerLite(raster, threshold);

最後のステップでは、検出されたマーカーに対して反復処理を行い、その変換行列を取得します。変換行列を使用して、マーカーの上に 3D オブジェクトを配置します。

// Create a NyARTransMatResult object for getting the marker translation matrices.
var resultMat = new NyARTransMatResult();

var markers = {};

// Go through the detected markers and get their IDs and transformation matrices.
for (var idx = 0; idx < markerCount; idx++) {
  // Get the ID marker data for the current marker.
  // ID markers are special kind of markers that encode a number.
  // The bytes for the number are in the ID marker data.
  var id = detector.getIdMarkerData(idx);

  // Read bytes from the id packet.
  var currId = -1;
  // This code handles only 32-bit numbers or shorter.
  if (id.packetLength <= 4) {
    currId = 0;
    for (var i = 0; i &lt; id.packetLength; i++) {
      currId = (currId << 8) | id.getPacketData(i);
    }
  }

  // If this is a new id, let's start tracking it.
  if (markers[currId] == null) {
    markers[currId] = {};
  }
  // Get the transformation matrix for the detected marker.
  detector.getTransformMatrix(idx, resultMat);

  // Copy the result matrix into our marker tracker object.
  markers[currId].transform = Object.asCopy(resultMat);
}

行列マッピング

JSARToolKit 行列を glMatrix 行列(最後の 4 つの要素に翻訳列がある 16 要素の FloatArrays の行列)にコピーするためのコードを次に示します。これは魔法のように機能します(読む: ARToolKit の行列がどのように設定されているのかがわかりません。Y 軸が反転しているように見えます)。いずれにせよ、この符号を反転したブードゥー教では、JSARToolKit 行列が glMatrix と同じように機能します。

Three.js などの別のライブラリでライブラリを使用するには、ARToolKit の行列をライブラリの行列形式に変換する関数を記述する必要があります。また、FLARParam.copyCameraMatrix メソッドにフックする必要もあります。copyCameraMatrix メソッドは、FLARParam 透視マトリックスを glMatrix スタイルのマトリックスに書き込みます。

function copyMarkerMatrix(arMat, glMat) {
  glMat[0] = arMat.m00;
  glMat[1] = -arMat.m10;
  glMat[2] = arMat.m20;
  glMat[3] = 0;
  glMat[4] = arMat.m01;
  glMat[5] = -arMat.m11;
  glMat[6] = arMat.m21;
  glMat[7] = 0;
  glMat[8] = -arMat.m02;
  glMat[9] = arMat.m12;
  glMat[10] = -arMat.m22;
  glMat[11] = 0;
  glMat[12] = arMat.m03;
  glMat[13] = -arMat.m13;
  glMat[14] = arMat.m23;
  glMat[15] = 1;
}

Three.js の統合

Three.js は、一般的な JavaScript 3D エンジンです。Three.js で JSARToolKit 出力を使用する方法を紹介します。必要なのは、動画画像が描画されるフルスクリーンのクワッド、FLARParam 遠近マトリックスが表示されたカメラ、マーカー マトリックスを変換するオブジェクトの 3 つです。以下のコードで統合の手順を説明します。

// I'm going to use a glMatrix-style matrix as an intermediary.
// So the first step is to create a function to convert a glMatrix matrix into a Three.js Matrix4.
THREE.Matrix4.prototype.setFromArray = function(m) {
  return this.set(
    m[0], m[4], m[8], m[12],
    m[1], m[5], m[9], m[13],
    m[2], m[6], m[10], m[14],
    m[3], m[7], m[11], m[15]
  );
};

// glMatrix matrices are flat arrays.
var tmp = new Float32Array(16);

// Create a camera and a marker root object for your Three.js scene.
var camera = new THREE.Camera();
scene.add(camera);

var markerRoot = new THREE.Object3D();
markerRoot.matrixAutoUpdate = false;

// Add the marker models and suchlike into your marker root object.
var cube = new THREE.Mesh(
  new THREE.CubeGeometry(100,100,100),
  new THREE.MeshBasicMaterial({color: 0xff00ff})
);
cube.position.z = -50;
markerRoot.add(cube);

// Add the marker root to your scene.
scene.add(markerRoot);

// Next we need to make the Three.js camera use the FLARParam matrix.
param.copyCameraMatrix(tmp, 10, 10000);
camera.projectionMatrix.setFromArray(tmp);


// To display the video, first create a texture from it.
var videoTex = new THREE.Texture(videoCanvas);

// Then create a plane textured with the video.
var plane = new THREE.Mesh(
  new THREE.PlaneGeometry(2, 2, 0),
  new THREE.MeshBasicMaterial({map: videoTex})
);

// The video plane shouldn't care about the z-buffer.
plane.material.depthTest = false;
plane.material.depthWrite = false;

// Create a camera and a scene for the video plane and
// add the camera and the video plane to the scene.
var videoCam = new THREE.Camera();
var videoScene = new THREE.Scene();
videoScene.add(plane);
videoScene.add(videoCam);

...

// On every frame do the following:
function tick() {
  // Draw the video frame to the canvas.
  videoCanvas.getContext('2d').drawImage(video, 0, 0);
  canvas.getContext('2d').drawImage(videoCanvas, 0, 0, canvas.width, canvas.height);

  // Tell JSARToolKit that the canvas has changed.
  canvas.changed = true;

  // Update the video texture.
  videoTex.needsUpdate = true;

  // Detect the markers in the video frame.
  var markerCount = detector.detectMarkerLite(raster, threshold);
  for (var i=0; i&lt;markerCount; i++) {
    // Get the marker matrix into the result matrix.
    detector.getTransformMatrix(i, resultMat);

    // Copy the marker matrix to the tmp matrix.
    copyMarkerMatrix(resultMat, tmp);

    // Copy the marker matrix over to your marker root object.
    markerRoot.matrix.setFromArray(tmp);
  }

  // Render the scene.
  renderer.autoClear = false;
  renderer.clear();
  renderer.render(videoScene, videoCam);
  renderer.render(scene, camera);
}

まとめ

この記事では、JSARToolKit の基本について説明しました。これで、JavaScript でウェブカメラを使用した独自の拡張現実アプリケーションを作成する準備が整いました。

JSARToolKit と Three.js を統合するのは少し面倒ですが、確実に可能です。このデモでうまくいくかどうかは 100% 確信していませんので、統合を実現するよりよい方法をご存じでしたらお知らせください。パッチも歓迎 :)

参照