כתיבת אפליקציות של מציאות רבודה באמצעות JSARToolKit

Ilmari Heikkinen

מבוא

מאמר זה עוסק בשימוש בספריית JSARToolKit עם WebRTC getUserMedia API כדי לבצע אפליקציות של מציאות רבודה באינטרנט. לצורך העיבוד אני משתמש ב-WebGL בגלל הביצועים המשופרים שהוא מציע. התוצאה הסופית של המאמר הזה היא אפליקציית הדגמה (דמו) שמציגה מודל תלת-ממדי מעל סמן של מציאות רבודה בסרטון במצלמת אינטרנט.

JSARToolKit היא ספריית מציאות רבודה עבור JavaScript. זוהי ספריית קוד פתוח שהושקה במסגרת GPL ויציאה ישירה של Flash FLARToolKit שיצרתי עבור הדגמת המציאות של רמיקס של Mozilla. FLARToolKit עצמו הוא יציאה של Java NyARToolKit, שהוא יציאה של C ARToolKit. עבר הרבה זמן, אבל סיימנו.

JSARToolKit פועל על אלמנטים של בד קנבס. מכיוון שצריך לקרוא את התמונה מחוץ לאזור העריכה, התמונה צריכה להגיע מאותו מקור כמו הדף או להשתמש ב-CORS כדי לעקוף את מדיניות המקור הזהה. בקצרה, מגדירים את המאפיין crossOrigin ברכיב התמונה או הסרטון שרוצים להשתמש בו כמרקם ל-'' או ל-'anonymous'.

כשמעבירים בד ציור אל JSARToolKit לצורך ניתוח, JSARToolKit מחזיר רשימה של סמני AR שנמצאו בתמונה ואת מטריצות הטרנספורמציה התואמות. כדי לצייר אובייקט תלת-ממדי על סמן, מעבירים את מטריצת הטרנספורמציה לכל ספריית עיבוד תלת-ממד שבה אתם משתמשים כדי שהאובייקט ישתנה באמצעות המטריצה. לאחר מכן, ציירו את מסגרת הווידאו בסצנת WebGL, ציירו את האובייקט מעליה וזהו.

כדי לנתח סרטון באמצעות JSARToolKit, משרטטים את הסרטון על קנבס ומעבירים את הקנבס ל-JSARToolKit. אפשר לעשות זאת בכל פריים, כולל מעקב AR לווידאו. JSARToolKit מהיר מספיק במנועי JavaScript מודרניים כדי לבצע זאת בזמן אמת גם במסגרות וידאו בגודל 640x480. עם זאת, ככל שמסגרת הסרטון גדולה יותר, כך תהליך העיבוד יהיה ארוך יותר. גודל טוב של מסגרת וידאו הוא 320x240, אבל אם אתם מצפים להשתמש בסמנים קטנים או בכמה סמנים, עדיף 640x480.

הדגמה (דמו)

כדי לצפות בהדגמה של מצלמת האינטרנט, עליך להפעיל את WebRTC בדפדפן שלך (ב-Chrome, עבור אל about:flags ומפעילים את MediaStream). עליך להדפיס גם את סמן ה-AR שבהמשך. תוכל גם לנסות לפתוח את תמונת הסמן בטלפון או בטאבלט ולהציג אותה למצלמת האינטרנט.

סמן AR.
סמן AR.

הגדרת JSARToolKit

ה-API JSARToolKit דומה למדי ל-Java, כך שתצטרכו לבצע כמה עיוותים כדי להשתמש בו. הרעיון הבסיסי הוא שיש לכם אובייקט של גלאי שפועל על אובייקט מסוג רסטר. בין המזהה לרסטר נמצא אובייקט פרמטר של מצלמה, שמשנה קואורדינטות של רשת נקודות לפי קואורדינטות של המצלמה. כדי לקבל את הסמנים שזוהו מהמזהה, עליכם לעבור עליהם באיטרציה ולהעתיק את מטריצות הטרנספורמציה שלהם לקוד שלכם.

השלב הראשון הוא ליצור את האובייקט של רשת נקודות, את אובייקט של פרמטר המצלמה ואת אובייקט הגלאי.

// 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 כדי לגשת למצלמת האינטרנט

בשלב הבא, אני אצור רכיב וידאו שמקבל וידאו ממצלמת אינטרנט דרך ממשקי ה-API של WebRTC. עבור סרטונים שהוקלטו מראש, פשוט הגדירו את מאפיין המקור של הסרטון לכתובת האתר של הסרטון. אם אתם מבצעים זיהוי סמנים מתמונות סטילס, תוכלו להשתמש ברכיב תמונה באופן דומה.

מכיוון ש-WebRTC ו-getUserMedia הן עדיין טכנולוגיות מתפתחות חדשות, עליך לזהות אותן. לפרטים נוספים, עיין במאמר של אריק בידלמן בנושא צילום אודיו ווידאו ב-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);

השלב האחרון הוא לחזור על הסמנים שזוהו ולקבל את מטריצות הטרנספורמציה שלהם. עליך להשתמש במטריצות הטרנספורמציה כדי להציב אובייקטים תלת ממדיים מעל הסמנים.

// 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 (שהן FloatArrays של 16 אלמנטים ועמודת התרגום בארבעת האלמנטים האחרונים). זה פועל באורח קסם (קריאה: אני לא יודע איך מוגדרות מטריצות 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 פופולרי בתלת-ממד. אני רוצה להסביר איך להשתמש בפלט JSARToolKit ב-Three.js. אתה זקוק לשלושה דברים: ריבוע במסך מלא עם תמונת הסרטון המצוירת עליה, מצלמה עם מטריצת הפרספקטיבה של 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 הוא קל ופשוט, אבל זה בהחלט אפשרי. אני לא בטוח ב-100% אם אני עושה את זה כמו שצריך בהדגמה שלי, אז אשמח לדעת אם ידוע לך על דרך טובה יותר להשיג את השילוב. תיקונים יתקבלו בברכה :)

קובצי עזר