Introduction
Cet article explique comment utiliser la bibliothèque JSARToolKit avec l'API getUserMedia de WebRTC pour créer des applications de réalité augmentée sur le Web. Pour le rendu, j'utilise WebGL en raison des performances améliorées qu'il offre. Le résultat final de cet article est une application de démonstration qui place un modèle 3D au-dessus d'un repère de réalité augmentée dans une vidéo de webcam.
JSARToolKit est une bibliothèque de réalité augmentée pour JavaScript. Il s'agit d'une bibliothèque Open Source publiée sous la licence GPL et d'un port direct du FLARToolKit Flash que j'ai créé pour la démo Remixing Reality de Mozilla. FLARToolKit est lui-même un port du NyARToolKit Java, qui est un port du ARToolKit C. Un long chemin parcouru, mais nous y sommes.
JSARToolKit fonctionne sur les éléments de canevas. Comme il doit lire l'image hors du canevas, elle doit provenir de la même origine que la page ou utiliser CORS pour contourner la règle de même origine. En résumé, définissez la propriété crossOrigin
sur l'élément image ou vidéo que vous souhaitez utiliser comme texture sur ''
ou 'anonymous'
.
Lorsque vous transmettez un canevas à JSARToolKit pour analyse, JSARToolKit renvoie une liste des repères RA détectés dans l'image et les matrices de transformation correspondantes. Pour dessiner un objet 3D au-dessus d'un repère, vous transmettez la matrice de transformation à la bibliothèque de rendu 3D que vous utilisez afin que votre objet soit transformé à l'aide de la matrice. Ensuite, dessinez le cadre vidéo dans votre scène WebGL et dessinez l'objet par-dessus.
Pour analyser une vidéo à l'aide de JSARToolKit, dessinez la vidéo sur un canevas, puis transmettez le canevas à JSARToolKit. Répétez l'opération pour chaque image, et vous obtiendrez le suivi de la RA vidéo. JSARToolKit est suffisamment rapide sur les moteurs JavaScript modernes pour effectuer cette opération en temps réel, même sur des images vidéo de 640 x 480. Toutefois, plus le frame vidéo est grand, plus le traitement prend du temps. Une bonne taille de frame vidéo est de 320 x 240 pixels, mais si vous prévoyez d'utiliser de petits repères ou plusieurs repères, une taille de 640 x 480 pixels est préférable.
Démo
Pour afficher la démonstration de la webcam, vous devez activer WebRTC dans votre navigateur (dans Chrome, accédez à about:flags et activez MediaStream). Vous devez également imprimer le repère RA ci-dessous. Vous pouvez également essayer d'ouvrir l'image du repère sur votre téléphone ou votre tablette et de la montrer à la webcam.
Configurer JSARToolKit
L'API JSARToolKit est assez semblable à Java. Vous devrez donc vous adapter pour l'utiliser. L'idée de base est qu'un objet détecteur fonctionne sur un objet raster. Entre le détecteur et le raster se trouve un objet de paramètres de l'appareil photo qui transforme les coordonnées du raster en coordonnées de l'appareil photo. Pour obtenir les repères détectés à partir du détecteur, vous devez les itérer et copier leurs matrices de transformation dans votre code.
La première étape consiste à créer l'objet raster, l'objet de paramètres de l'appareil photo et l'objet du détecteur.
// 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);
Utiliser getUserMedia pour accéder à la webcam
Je vais ensuite créer un élément vidéo qui récupère la vidéo de la webcam via les API WebRTC. Pour les vidéos préenregistrées, il vous suffit de définir l'attribut source de la vidéo sur l'URL de la vidéo. Si vous effectuez une détection de repères à partir d'images fixes, vous pouvez utiliser un élément image de la même manière.
Comme WebRTC et getUserMedia sont encore de nouvelles technologies émergentes, vous devez les détecter. Pour en savoir plus, consultez l'article d'Eric Bidelman sur la capture d'audio et de vidéo en 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.");
}
);
Détecter des repères
Une fois le détecteur opérationnel, nous pouvons commencer à lui fournir des images pour détecter les matrices de RA. Commencez par dessiner l'image sur le canevas de l'objet raster, puis exécutez le détecteur sur l'objet raster. Le détecteur renvoie le nombre de repères détectés dans l'image.
// 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);
La dernière étape consiste à itérer sur les repères détectés et à obtenir leurs matrices de transformation. Vous utilisez les matrices de transformation pour placer des objets 3D au-dessus des repères.
// 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 < 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);
}
Mappage matriciel
Voici le code permettant de copier les matrices JSARToolKit sur les matrices glMatrix (qui sont des FloatArrays à 16 éléments avec la colonne de translation dans les quatre derniers éléments). Cela fonctionne par magie (lire: je ne sais pas comment les matrices ARToolKit sont configurées. Je pense que l'axe Y est inversé.) Quoi qu'il en soit, ce petit tour de magie consistant à inverser le signe fait fonctionner une matrice JSARToolKit de la même manière qu'une matrice glMatrix.
Pour utiliser la bibliothèque avec une autre bibliothèque, telle que Three.js, vous devez écrire une fonction qui convertit les matrices ARToolKit au format de matrice de la bibliothèque. Vous devez également vous connecter à la méthode FLARParam.copyCameraMatrix. La méthode copyCameraMatrix écrit la matrice de perspective FLARParam dans une matrice de style 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;
}
Intégration de Three.js
Three.js est un moteur JavaScript 3D populaire. Je vais vous expliquer comment utiliser la sortie de JSARToolKit dans Three.js. Vous avez besoin de trois éléments: un quad plein écran sur lequel est dessinée l'image vidéo, une caméra avec la matrice de perspective FLARParam et un objet avec la matrice de repère comme transformation. Je vais vous expliquer l'intégration dans le code ci-dessous.
// 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<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);
}
Résumé
Dans cet article, nous avons vu les principes de base de JSARToolKit. Vous êtes maintenant prêt à créer vos propres applications de réalité augmentée avec une webcam à l'aide de JavaScript.
L'intégration de JSARToolKit à Three.js est un peu compliquée, mais c'est tout à fait possible. Je ne suis pas sûr à 100% de faire les choses correctement dans ma démonstration. Veuillez m'indiquer si vous connaissez une meilleure façon d'effectuer l'intégration. Les correctifs sont les bienvenus :)