使用 JSARToolKit 編寫擴增實境應用程式

Ilmari Heikkinen

簡介

本文說明如何搭配使用 JSARToolKit 程式庫與 WebRTC getUserMedia API,在網路上執行擴增實境應用程式。我使用 WebGL 來提升轉譯效能。本文將製作一個試用版應用程式,在網路攝影機影片中,將 3D 模型置於擴增實境標記上方。

JSARToolKit 是 JavaScript 的擴增實境程式庫。這是在 GPL 下發布的開放原始碼程式庫,也是我為 Mozilla Remixing Reality 試用版所製作的 Flash FLARToolKit 直接連接埠。FLARToolKit 本身是 Java NyARToolKit 的充電站,這是 C ARToolKit 的連接埠。這趟旅程很長,但我們堅持到底。

JSARToolKit 會在畫布元素上運作。由於圖片需要從畫布上閱讀圖片,因此圖片必須來自與頁面相同的來源,或者是使用 CORS 來因應相同來源的政策。簡單來說,請將要做為紋理的圖片或影片元素上的 crossOrigin 屬性設為 '''anonymous'

當您將畫布傳遞至 JSARToolKit 進行分析時,JSARToolKit 會傳回圖片中找到的 AR 標記清單,以及對應的轉換矩陣。如要在標記上繪製 3D 物件,您可以將轉換矩陣傳送至目前使用的 3D 轉譯程式庫,藉此利用矩陣轉換物件。然後,在 WebGL 場景中繪製影片影格,然後在上面繪製物件,即可開始使用。

若要使用 JSARToolKit 分析影片,請在畫布上繪製影片,然後將畫布傳送給 JSARToolKit。你必須為每個影格重複上述步驟,才能使用影片 AR 追蹤功能。在新型 JavaScript 引擎中,JSARToolKit 速度夠快,即使在 640x480 的影片影格中也能即時執行這項操作。不過,影片畫面越大,處理時間就越長。理想的影片影格大小為 320x240,但如果您預期使用小型標記或多個標記,建議使用 640x480。

操作示範

如要觀看網路攝影機示範影片,你必須在瀏覽器中啟用 WebRTC (如果是使用 Chrome,請前往 about:flags 並啟用 MediaStream)。您還需要列印下方的 AR 標記。此外,你也可以試著在手機或平板電腦上開啟標記圖片,然後展示在網路攝影機中。

AR 標記。
AR 標記。

設定 JSARToolKit

JSARToolKit API 相當類似 Java,因此您必須稍加改變才能使用。基本概念是,您可以具有針對光柵物件運作的偵測工具物件。在偵測器和光柵之間是 Camera 參數物件,可將光柵座標轉換成相機座標。如要從偵測工具中取得偵測到的標記,您需要逐一逐一處理,並將標記的轉換矩陣複製到您的程式碼。

第一步是建立光柵物件、相機參數物件和偵測工具物件。

// 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 取得網路攝影機影片。如果是預錄影片,只要將影片的來源屬性設為影片網址即可。如果您是從靜態圖片偵測標記,也可以以類似的方式使用圖片元素。

由於 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 後,我們即可開始提供圖片來偵測 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 矩陣 (也就是 16 個元素,其中包含最後四個元素中平移欄的 16 個元素 FloatArrays)。它的運作方式是魔術 (閱讀:我不知道 ARToolKit 矩陣是怎麼設定的,反轉 Y 軸就是猜測。)總之,這部分反轉式的 Vodoo 可以讓 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 視角矩陣的攝影機,以及帶有標記矩陣的物件做為轉換作業。我將逐步引導您完成以下程式碼的整合作業。

// 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 可能會有點麻煩,但可以確定。如果我自己在示範模式中做到這一點,我不確定歡迎加入修補程式 :)

參考資料