Cómo crear apps de Realidad Aumentada usando Processing para Android.
Google ofrece un entorno de trabajo para crear aplicaciones de Realidad Aumentada (RA) para Android, llamado ARCore. Experiencias de RA en teléfonos inteligentes generalmente involucran el dibujo de contenido digital 3D superpuesto al mundo físico, tal como se lo vé desde la perspectiva de la cámara del teléfono, de tal manera que los contenidos digitales aparentan ser parte del espacio real de manera convincente. Esto requiere el uso de algoritmos bastante complejos para reconocer objetos en el entorno físico, como paredes, muebles, incluso personas, y determinar la pose (posición y orientación) del teléfono en relación con el espacio en tiempo real. Afortunadamente para nosotros, ARCore maneja todos estos cálculos automáticamente y proporciona una API para acceder a las características reconocidas en el espacio físico y adjuntarles contenidos digitales desde nuestro código. Si bien es posible acceder a ARCore directamente desde Processing, el modo Android incluye una biblioteca de RA que hace que sea más fácil usar ARCore en nuestros bosquejos de Processing y aplicar la API de dibujo de Processing para crear contenido en RA.
AR es un campo en rápido crecimiento, gracias a entornos como ARCore en Android (y ARKit en iOS) y al rápido ritmo de mejora técnica de los teléfonos inteligentes. La integración de los espacios físicos con contenidos digitales está abriendo muchas nuevas posibilidades para experiencias "físicas" mediadas a través de los teléfonos inteligentes. Los Experimentos AR de Google y la lista Awesome ARKit de aplicaciones RA para iOS son buenos recursos para encontrar proyectos que hagan un uso creativo de RA.
Con el fin de garantizar una buena experiencia de RA, Google certifica los dispositivos móblies compatibles con ARCore. La lista oficial de dispositivos compatibles está disponible aquí. Los dispositivos en esa lista también deberían funcionar sin problemas con Processing para crear proyectos de RA.
El primer paso es seleccionar la opción AR en el menú de Android para asegurarse de nuestro bosquejo sea compilado como una aplicación AR:
Mientras que prácticamente cualquier bosquejo 3D de Processing se puede visualizar en RV simplemente importando la biblioteca de realidad virtual y utilizando el motor de renderizado VR, el uso de RA es un poco más complejo y require algunos pasos más. En primer lugar, necesitamos agregar un objeto ARTracker a nuestro boceto, y llamar a su función start()
para iniciar el seguimiento en RA. Las otras dos clases en la biblioteca AR son ARTrackble y ARAnchor, que representan respectivamente una superficie que se puede seguir o "trackear" en el espacio físico (como pueden ser una mesa o una pared) y un punto en el espacio que permanece fijo en relación con una superficie trackeable dada. Estos conceptos se irán aclarando a medida que avancemos en el tutorial. Por el momento, un esqueleto básico para un boceto de RA en Processing está provisto en el siguiente código, donde sólamente iniciamos el seguimiento e imprimimos en la consola los objetos trackeables detectados en cada cuadro:
import processing.ar.*;
ARTracker tracker;
void setup() {
fullScreen(AR);
tracker = new ARTracker(this);
tracker.start();
noStroke();
}
void draw() {
lights();
for (int i = 0; i < tracker.count(); i++) {
ARTrackable t = tracker.get(i);
println("Trackable", i, t);
}
}
Los objetos trackeables en Processing están limitados a superficies planas, aunque el objeto trackable de ARCore subyacenbte puede representar otro tipo de características en el espacio físico, tales como nubes de puntos. Cada objeto trackeable contiene información básica acerca de la entidad física que representa, incluido el tamaño y el estado actual (ya sea que se lo esté trackeando, pausado o detenido). Para dibujar un plano trackeable, primero debemos aplicar la transformación que convierte a los coordinates globales de Processing en el sistema del trackeable, de esta manera, es fácil representar puntos relativos al plano trackeable que abarca los ejes X y Z, independientemente de su orientación con respeto al sistema de coordenadas de Processing. Podemos obtener el tamaño del trackeable a lo largo de cada eje con las funciones lengthX() y lengthZ (), y llamar a la función transform() antes de hacer cualquier dibujo:
import processing.ar.*;
ARTracker tracker;
void setup() {
fullScreen(AR);
tracker = new ARTracker(this);
tracker.start();
noStroke();
}
void draw() {
lights();
for (int i = 0; i < tracker.count(); i++) {
ARTrackable t = tracker.get(i);
pushMatrix();
t.transform();
float lx = t.lengthX();
float lz = t.lengthZ();
fill(255, 100);
beginShape(QUADS);
vertex(-lx/2, 0, -lz/2);
vertex(-lx/2, 0, +lz/2);
vertex(+lx/2, 0, +lz/2);
vertex(+lx/2, 0, -lz/2);
endShape();
popMatrix();
}
}
El bosquejo anterior debería dibujar todos los planos trackeables que el teléfono detecta mientras nos movemos en el espacio:
También es fácil determinar si el usuario está seleccionando un trackable usando la pantalla táctil, todo lo que tenemos que hacer es llamar al isSelected(x, y)
con las coordenadas (x, y) del puntero de toque:
...
t.transform();
float lx = t.lengthX();
float lz = t.lengthZ();
if (mousePressed && t.isSelected(mouseX, mouseY)) {
fill(255, 0, 0, 100);
} else {
fill(255, 100);
}
beginShape(QUADS);
vertex(-lx/2, 0, -lz/2);
...
Si agregamos este código a nuestro bosquejo previos deberíamos obtener la siguiente interacción al tocar un plano en la imágen de la cámara:
Una vez que tenemos superficies rastreables en nuestra escena de RA, podemos unir anclas a las mismas. Anchors o anclas en ARCore son esencialmente posiciones en el espacio que se fijan en relación con el trackeable al que están unidas y nos permiten hacer que los objetos virtuales permanezcan en su lugar en la escena. Los objetos de anclaje en Processing se crean con las coordenadas relativas que tendrán en relación con su trackeable pariente. Extendiendo nuestro código anterior, podríamos agregar nuevos anclajes exactamente cuando se detecte un trackeable por primera vez. Ese evento puede manejarse agregando la función trackableEvent()
a nuestro código, que recibirá el trackable detectado como argumento. Necesitamos hacer un seguimiento de todos los anclajes en nuestra escena manualmente, por ejemplo almacenándolos en una lista y eliminándolos cuando ya no se están rastreando. El siguiente código hace todo eso:
import processing.ar.*;
ARTracker tracker;
ArrayList<ARAnchor> trackAnchors = new ArrayList<ARAnchor>();
void setup() {
fullScreen(AR);
tracker = new ARTracker(this);
tracker.start();
noStroke();
}
void draw() {
lights();
drawAnchors();
drawTrackables();
}
void trackableEvent(ARTrackable t) {
if (trackAnchors.size() < 10 && t.isFloorPlane()) {
trackAnchors.add(new ARAnchor(t, 0, 0, 0));
}
}
void drawAnchors() {
for (ARAnchor anchor : trackAnchors) {
if (anchor.isTracking()) drawSphere(anchor, 0.05);
if (anchor.isStopped()) anchor.dispose();
}
tracker.clearAnchors(trackAnchors);
}
void drawTrackables() {
for (int i = 0; i < tracker.count(); i++) {
ARTrackable t = tracker.get(i);
pushMatrix();
t.transform();
float lx = t.lengthX();
float lz = t.lengthZ();
if (mousePressed && t.isSelected(mouseX, mouseY)) {
fill(255, 0, 0, 100);
} else {
fill(255, 100);
}
drawPlane(lx, lz);
popMatrix();
}
}
void drawSphere(ARAnchor anchor, float r) {
anchor.attach();
fill(#CF79F5);
sphere(r);
anchor.detach();
}
void drawPlane(float lx, float lz) {
beginShape(QUADS);
vertex(-lx/2, 0, -lz/2);
vertex(-lx/2, 0, +lz/2);
vertex(+lx/2, 0, +lz/2);
vertex(+lx/2, 0, -lz/2);
endShape();
}
En el código anterior, creamos un nuevo anclaje en el punto (0, 0, 0) en relación con el trackeable que se ha detectado, pero sólamente si representa una superficie horizontal (es decir, un piso), lo cual podemos determinar llamando la función isFloorPlane()
. Una función similar, isWallPlane()
, nos permite saber si el trackable es un plano vertical (es decir, una pared). Es importante observar que las dimensiones de los objetos en RA son bastante pequeñas en comparación con un bosquejo de 3D típico (por ejemplo, el radio de las esferas que se dibujan en las posiciones de anclaje es solamente 0.05).
También es importante que desechemos un ancla una vez que ya no sea rastreada con la suguiente línea de código: if (anchor.isStopped ()) anchor.dispose ();
. De esta manera nos asegurarmos de que no desperdiciar recursos con anclas que ya no están activas (las anclas que simplemente están en pausa no deben desecharse ya que pueden rastrearse nuevamente más tarde, pero las detenidas no se reiniciarán de nuevo). También deberíamos eliminar los anclajes desechados de nuestra lista de matrices, podemos hacerlo manualmente desde nuestro bosquejo o usar la función de utilidad clearAnchors
provista en la clase ARTracker. El resultado de nuestro último código debería ser el siguiente:
Una interacción típica en A es desplazar un objeto (virtual) a lo largo de un plano trackeable, por ejemplo, con el fin de encontrar su mejor ubicación en el espacio físico de manera interactiva. La biblioteca AR en Processing ofrece una funcionalidad que facilita la implementación de dichas interacciones. La clase ARTracker provee una función get()
que toma una posición táctil (x, y) como argumento y devuelve el trackeable que está siendo tocado por el puntero táctil. De esta forma, podemos crear un nuevo ancla para ese trackeable tocado, y el ancla se colocará exactamente en la posición del trackeable que se encuentra en la intersección con el puntero táctil. A medida que el puntero se mueve, debemos desechar el ancla en la posición anterior y crear otra en la nueva posición. Esto se hace con el siguiente código (para ser agregado a la versión previa de nuesto bosquejo):
import processing.ar.*;
ARTracker tracker;
ArrayList<ARAnchor> trackAnchors = new ArrayList<ARAnchor>();
ARAnchor touchAnchor;
PShape obj;
void setup() {
fullScreen(AR);
obj = loadShape("model.obj");
tracker = new ARTracker(this);
tracker.start();
noStroke();
}
void draw() {
lights();
drawObject(touchAnchor);
drawAnchors();
drawTrackables();
}
void mousePressed() {
if (touchAnchor != null) touchAnchor.dispose();
ARTrackable hit = tracker.get(mouseX, mouseY);
if (hit != null) touchAnchor = new ARAnchor(hit);
else touchAnchor = null;
}
...
void drawObject(ARAnchor anchor) {
if (anchor != null) {
anchor.attach();
shape(obj);
anchor.detach();
}
}
En este ejemplo utilizamos un modelo OBJ cargado desde la la carpeta de datos del bosquejo para dibujar en la ubicación del anclaje de toque. Si todo es correcto en el código, el resultado sería similar al siguiente:
El código del bosquejo completo está disponible aquí.