Introdução
Este artigo aborda o uso da biblioteca JSARToolKit com a API getUserMedia do WebRTC para criar aplicativos de realidade aumentada na Web. Para renderização, estou usando o WebGL devido ao aumento de desempenho que ele oferece. O resultado final deste artigo é um aplicativo de demonstração que coloca um modelo 3D sobre um marcador de realidade aumentada em um vídeo de webcam.
O JSARToolKit é uma biblioteca de realidade aumentada para JavaScript. É uma biblioteca de código aberto lançada sob a GPL e uma porta direta do FLARToolKit do Flash que eu fiz para a demonstração Remixing Reality do Mozilla. O FLARToolKit é uma porta do NyARToolKit Java, que é uma porta do ARToolKit C. Um longo caminho, mas cá estamos nós.
O JSARToolKit opera em elementos de tela. Como a imagem precisa ser lida fora da tela, ela precisa vir da mesma origem da página ou usar CORS para contornar a política de mesma origem. Resumindo, defina a propriedade crossOrigin
no elemento de imagem ou vídeo que você quer usar como uma textura para ''
ou 'anonymous'
.
Quando você transmite uma tela para análise ao JSARToolKit, ele retorna uma lista de marcadores de RA encontrados na imagem e as matrizes de transformação correspondentes. Para desenhar um objeto 3D sobre um marcador, transmita a matriz de transformação para a biblioteca de renderização 3D que você está usando para que o objeto seja transformado usando a matriz. Em seguida, renderize o frame de vídeo na sua cena do WebGL e o objeto em cima dele.
Para analisar o vídeo usando o JSARToolKit, desenhe o vídeo em uma tela e transmita a tela para o JSARToolKit. Faça isso para cada frame e você terá o rastreamento de RA em vídeo. O JSARToolKit é rápido o suficiente em mecanismos modernos de JavaScript para fazer isso em tempo real, mesmo em frames de vídeo de 640x480. No entanto, quanto maior o frame do vídeo, mais tempo ele leva para ser processado. Um bom tamanho de frame de vídeo é 320 x 240, mas se você pretende usar marcadores pequenos ou vários marcadores, 640 x 480 é preferível.
Demonstração
Para conferir a demonstração da webcam, você precisa ativar o WebRTC no navegador. No Chrome, acesse about:flags e ative o MediaStream. Você também precisa imprimir o marcador de RA abaixo. Você também pode abrir a imagem do marcador no smartphone ou tablet e mostrar para a webcam.
Como configurar o JSARToolKit
A API JSARToolKit é bastante semelhante ao Java, então você terá que fazer algumas contorções para usá-la. A ideia básica é que você tem um objeto detector que opera em um objeto raster. Entre o detector e o raster, há um objeto de parâmetro da câmera que transforma as coordenadas do raster em coordenadas da câmera. Para receber os marcadores detectados do detector, você itera sobre eles e copia as matrizes de transformação para o código.
A primeira etapa é criar o objeto raster, o objeto de parâmetro da câmera e o objeto de detector.
// 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);
Como usar getUserMedia para acessar a webcam
Em seguida, vou criar um elemento de vídeo que recebe vídeo da webcam pelas APIs WebRTC. Para vídeos pré-gravados, basta definir o atributo de origem do vídeo como o URL dele. Se você estiver fazendo a detecção de marcadores de imagens estáticas, poderá usar um elemento de imagem da mesma forma.
Como o WebRTC e o getUserMedia ainda são tecnologias emergentes, é necessário detectar o recurso. Para mais detalhes, consulte o artigo de Eric Bidelman sobre Como capturar áudio e vídeo no 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.");
}
);
Como detectar marcadores
Depois que o detector estiver funcionando corretamente, vamos começar a alimentar imagens para detectar matrizes de RA. Primeiro, desenhe a imagem na tela do objeto raster e, em seguida, execute o detector no objeto raster. O detector retorna o número de marcadores encontrados na imagem.
// 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);
A última etapa é iterar pelos marcadores detectados e receber as matrizes de transformação. Você usa as matrizes de transformação para colocar objetos 3D sobre os marcadores.
// 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);
}
Mapeamento de matriz
Confira o código para copiar matrizes do JSARToolKit para matrizes do glMatrix (que são FloatArrays de 16 elementos com a coluna de tradução nos quatro últimos elementos). Funciona por magia (leia-se: eu não sei como as matrizes do ARToolKit são configuradas. O eixo Y invertido é minha suposição.) De qualquer forma, esse pouco de magia de inversão de sinal faz com que uma matriz JSARToolKit funcione da mesma forma que uma glMatrix.
Para usar a biblioteca com outra, como a Three.js, você precisa escrever uma função que converta as matrizes do ARToolKit para o formato de matriz da biblioteca. Você também precisa se conectar ao método FLARParam.copyCameraMatrix. O método copyCameraMatrix grava a matriz de perspectiva FLARParam em uma matriz do tipo 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;
}
Integração do three.js
O Three.js é um mecanismo 3D JavaScript conhecido. Vou mostrar como usar a saída do JSARToolKit no Three.js. Você precisa de três coisas: um quadrângulo de tela cheia com a imagem do vídeo desenhada nele, uma câmera com a matriz de perspectiva FLARParam e um objeto com a matriz de marcador como transformação. Vou explicar a integração no código abaixo.
// 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);
}
Resumo
Neste artigo, abordamos os princípios básicos do JSARToolKit. Agora você está pronto para criar seus próprios aplicativos de realidade aumentada com webcam usando JavaScript.
A integração do JSARToolKit com o Three.js é um pouco complicada, mas é possível. Não tenho 100% de certeza se estou fazendo isso certo na minha demonstração. Informe se você conhece uma maneira melhor de fazer a integração. Patches são bem-vindos :)