Pisanie aplikacji rzeczywistości rozszerzonej za pomocą JSARToolKit

Ilmari Heikkinen

Wprowadzenie

W tym artykule opisano korzystanie z biblioteki JSARToolKit z interfejsem getUserMedia API WebRTC w przypadku aplikacji rozszerzonej rzeczywistości w internecie. Do renderowania używam WebGL, ponieważ zapewnia on większą wydajność. Efektem końcowym tego artykułu jest aplikacja demonstracyjna, która umieszcza model 3D na wierzchu znacznika rzeczywistości rozszerzonej w filmie z kamery internetowej.

JSARToolKit to biblioteka rzeczywistości rozszerzonej dla JavaScriptu. Jest to biblioteka open source wydana na licencji GPL i bezpośredni port FLARToolKit, który został stworzony dla demo Remixing Reality w Mozilla. FLARToolKit to port biblioteki NyARToolKit w Javie, która jest portem biblioteki ARToolKit w C. Długa droga, ale jesteśmy.

JSARToolKit działa na elementach kanwy. Ponieważ musi odczytać obraz z płóta, musi on pochodzić z tego samego źródła co strona lub użyć CORS, aby obejść zasadę tego samego pochodzenia. Krótko mówiąc, ustaw właściwość crossOrigin elementu obrazu lub filmu, którego chcesz użyć jako tekstury, na '' lub 'anonymous'.

Gdy przekażesz kanwę do analizy do JSARToolKit, JSARToolKit zwróci listę znaczników AR znalezionych na obrazie oraz odpowiadające im macierze przekształceń. Aby narysować obiekt 3D na wierzchu znacznika, przekazujesz macierz przekształcenia do dowolnej używanej przez siebie biblioteki do renderowania 3D, aby obiekt został przekształcony za pomocą tej macierzy. Następnie narysuj ramkę wideo w scenie WebGL i na niej obiekt.

Aby przeanalizować film za pomocą pakietu JSARToolKit, narysuj go na płótnie, a potem prześlij płótno do pakietu JSARToolKit. Wykonaj to w przypadku każdego klatki, aby uzyskać śledzenie AR w filmie. JSARToolKit jest wystarczająco szybki na nowoczesnych silnikach JavaScriptu, aby wykonywać te operacje w czasie rzeczywistym nawet w przypadku klatek wideo o rozdzielczości 640 × 480. Im większy jest jednak kadr filmu, tym dłużej trwa jego przetwarzanie. Dobry rozmiar ramki filmu to 320 x 240, ale jeśli zamierzasz używać małych lub wielu znaczników, lepiej jest użyć 640 x 480.

Prezentacja

Aby wyświetlić demonstrację kamery internetowej, musisz włączyć WebRTC w przeglądarce (w Chrome otwórz about:flags i włącz MediaStream). Musisz też wydrukować poniższy znacznik AR. Możesz też otworzyć obraz znacznika na telefonie lub tablecie i pokazać go przed kamerą internetową.

znacznik AR;
Oznaczenie AR.

Konfigurowanie JSARToolKit

Interfejs JSARToolKit API jest podobny do Javy, więc aby z niego korzystać, musisz się trochę nagimnastykować. Zasada jest taka, że masz obiekt detektora, który działa na obiekcie rastrowym. Pomiędzy detektorem a rasterem znajduje się obiekt parametrów aparatu, który przekształca współrzędne rastera na współrzędne aparatu. Aby uzyskać wykryte znaczniki z detektora, należy je przejrzeć i skopiować ich macierze przekształcenia do kodu.

Pierwszym krokiem jest utworzenie obiektu rastrowego, obiektu parametrów aparatu i obiektu detektora.

// 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);

Uzyskiwanie dostępu do kamery internetowej za pomocą metody getUserMedia

Następnie utwórz element wideo, który pobiera obraz z kamery internetowej za pomocą interfejsów API WebRTC. W przypadku nagranych wcześniej filmów wystarczy ustawić atrybut źródła filmu na adres URL filmu. Jeśli chcesz wykrywać znaczniki na podstawie statycznych obrazów, możesz użyć elementu obrazu w praktycznie taki sam sposób.

WebRTC i getUserMedia to wciąż nowe, rozwijające się technologie, dlatego musisz je wykrywać. Więcej informacji znajdziesz w artykule Erica Bidelmana Nagrywanie dźwięku i wideo w 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.");
  }
);

Wykrywanie znaczników

Gdy detektor działa prawidłowo, możemy zacząć podawać mu obrazy, aby wykrywał macierze AR. Najpierw narysuj obraz na obszarze roboczym obiektu rastrowego, a potem uruchom detektor na tym obiekcie. Detektor zwraca liczbę znaczników znalezionych na obrazie.

// 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);

Ostatnim krokiem jest przejście przez wykryte znaczniki i uzyskanie ich macierzy przekształcenia. Matryc transformacji używa się do umieszczania obiektów 3D na wierzchu znaczników.

// 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);
}

Mapowanie macierzy

Oto kod do kopiowania macierzy JSARToolKit do macierzy glMatrix (które są 16-elementowymi FloatArrays z kolumną translation w 4 ostatnich elementach). Działa to jak za dotknięciem magicznej różdżki (czyli nie wiem, jak skonfigurować macierze ARToolKit). Odwrócona oś Y. W każdym razie ta odrobina czarów z odwróceniem znaku sprawia, że macierz JSARToolKit działa tak samo jak glMatrix.

Aby używać biblioteki z inną biblioteką, np. Three.js, musisz napisać funkcję, która konwertuje macierze ARToolKit do formatu macierzy biblioteki. Musisz też zaimplementować metodę FLARParam.copyCameraMatrix. Metoda copyCameraMatrix zapisuje matrycę perspektywy FLARParam w formie matrycy 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;
}

Integracja z three.js

Three.js to popularny silnik 3D w JavaScript. Pokażę Ci, jak używać danych wyjściowych JSARToolKit w Three.js. Potrzebne są 3 elementy: kwadrat na pełnym ekranie z narysowanym obrazem wideo, kamera z macierzą perspektywy FLARParam i obiekt z macierzą znacznika jako jego transformacja. Poniżej pokażę Ci, jak przeprowadzić integrację za pomocą kodu.

// 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);
}

Podsumowanie

W tym artykule omówiliśmy podstawy JSARToolKit. Teraz możesz tworzyć własne aplikacje rzeczywistości rozszerzonej korzystające z kamery internetowej i zaimplementowane w języku JavaScript.

Integracja JSARToolKit z Three.js jest nieco kłopotliwa, ale na pewno możliwa. Nie jestem w 100% pewien, czy robię to prawidłowo podczas demonstracji, więc daj mi znać, jeśli znasz lepszy sposób na przeprowadzenie integracji. Zapraszamy do zgłaszania poprawek :)

Pliki referencyjne