Cómo GWT desbloquea la realidad aumentada en su navegador
Publicado: 2022-03-11En nuestra publicación anterior sobre el kit de herramientas web de GWT, discutimos las fortalezas y características de GWT, que, para recordar la idea general, nos permite transpilar el código fuente de Java a JavaScript y combinar las bibliotecas de Java y JavaScript sin problemas. Notamos que el JavaScript generado por GWT está dramáticamente optimizado.
En la publicación de hoy, nos gustaría profundizar un poco más y ver el kit de herramientas de GWT en acción. Demostraremos cómo podemos aprovechar GWT para construir una aplicación peculiar: una aplicación web de realidad aumentada (AR) que se ejecuta en tiempo real, completamente en JavaScript, en el navegador.
En este artículo, nos centraremos en cómo GWT nos brinda la capacidad de interactuar fácilmente con muchas API de JavaScript, como WebRTC y WebGL, y nos permite aprovechar una gran biblioteca de Java, NyARToolkit, que nunca tuvo la intención de usarse en el navegador. Mostraremos cómo GWT nos permitió a mi equipo y a mí en Jooink juntar todas estas piezas para crear nuestro proyecto favorito, Picshare , una aplicación AR basada en marcadores que puedes probar en tu navegador ahora mismo.
Esta publicación no será un recorrido completo sobre cómo crear la aplicación, sino que mostrará el uso de GWT para superar desafíos aparentemente abrumadores con facilidad.
Resumen del proyecto: de la realidad a la realidad aumentada
Picshare utiliza realidad aumentada basada en marcadores. Este tipo de aplicación AR busca un marcador en la escena: un patrón geométrico específico y fácilmente reconocible, como este. El marcador proporciona información sobre la posición y la orientación del objeto marcado, lo que permite que el software proyecte paisajes 3D adicionales en la imagen de forma realista. Los pasos básicos en este proceso son:
- Acceda a la cámara: cuando se trata de aplicaciones de escritorio nativas, el sistema operativo proporciona acceso de E/S a gran parte del hardware del dispositivo. No es lo mismo cuando tratamos con aplicaciones web. Los navegadores se crearon para ser una especie de "caja de arena" para el código JavaScript descargado de la red, y originalmente no estaban destinados a permitir que los sitios web interactuaran con la mayoría del hardware del dispositivo. WebRTC rompe esta barrera utilizando las funciones de captura de medios de HTML5, lo que permite que el navegador acceda, entre otras cosas, a la cámara del dispositivo y su transmisión.
- Analice la transmisión de video: tenemos la transmisión de video... ¿ahora qué? Tenemos que analizar cada fotograma para detectar marcadores y calcular la posición del marcador en el mundo 3D reconstruido. Esta tarea compleja es el negocio de NyARToolkit.
- Aumentar el video: finalmente, queremos mostrar el video original con objetos 3D sintéticos agregados. Usamos WebGL para dibujar la escena final aumentada en la página web.
Aprovechando las API de HTML5 con GWT
El uso de API de JavaScript como WebGL y WebRTC permite interacciones inesperadas e inusuales entre el navegador y el usuario.
Por ejemplo, WebGL permite gráficos acelerados por hardware y, con la ayuda de la especificación de matriz escrita, permite que el motor de JavaScript ejecute procesamiento de números con un rendimiento casi nativo. De manera similar, con WebRTC, el navegador puede acceder a transmisiones de video (y otros datos) directamente desde el hardware de la computadora.
WebGL y WebRTC son bibliotecas de JavaScript que deben integrarse en el navegador web. La mayoría de los navegadores HTML5 modernos vienen con soporte al menos parcial para ambas API (como puede ver aquí y aquí). Pero, ¿cómo podemos aprovechar estas herramientas en GWT, que está escrito en Java? Como se discutió en la publicación anterior, la capa de interoperabilidad de GWT, JsInterop (lanzada oficialmente en GWT 2.8) hace que esto sea pan comido.
Usar JsInterop con GWT 2.8 es tan fácil como agregar -generateJsInteropExports
como argumento al compilador. Las anotaciones disponibles se definen en el paquete jsinterop.annotations
, incluido en gwt-user.jar
.
WebRTC
Como ejemplo, con un trabajo de codificación mínimo, usar getUserMedia
de WebRTC en Chrome con GWT se vuelve tan simple como escribir:
Navigator.webkitGetUserMedia( configs, stream -> video.setSrc( URL.createObjectURL(stream) ), e -> Window.alert("Error: " + e) );
Donde la clase Navigator
se puede definir de la siguiente manera:
@JsType(namespace = JsPackage.GLOBAL, isNative = true, name="navigator") final static class Navigator { public static native void webkitGetUserMedia( Configs configs, SuccessCallback success, ErrorCallback error); }
Es de interés la definición de las interfaces SuccessCallback
y ErrorCallback
, ambas implementadas por la expresión lambda anterior y definidas en Java mediante la anotación @JsFunction
:
@JsFunction public interface SuccessCallback { public void onMediaSuccess(MediaStream stream); } @JsFunction public interface ErrorCallback { public void onError(DomException error); }
Finalmente, la definición de la URL
de la clase es casi idéntica a la de Navigator
y, de manera similar, la clase Configs
se puede definir mediante:
@JsType(namespace = JsPackage.GLOBAL, isNative = true, name="Object") public static class Configs { @JsProperty public native void setVideo(boolean getVideo); }
La implementación real de todas estas funcionalidades tiene lugar en el motor JavaScript del navegador.
Puede encontrar el código anterior en GitHub aquí.
En este ejemplo, en aras de la simplicidad, se usa la API obsoleta navigator.getUserMedia()
porque es la única que funciona sin polirrelleno en la versión estable actual de Chrome. En una aplicación de producción, podemos usar adapter.js para acceder a la transmisión a través de la nueva API navigator.mediaDevices.getUserMedia()
, uniformemente en todos los navegadores, pero esto está más allá del alcance de la presente discusión.
WebGL
El uso de WebGL de GWT no es muy diferente en comparación con el uso de WebRTC, pero es un poco más tedioso debido a la complejidad intrínseca del estándar OpenGL.
Nuestro enfoque aquí refleja el seguido en la sección anterior. El resultado de la envoltura se puede ver en la implementación de GWT WebGL utilizada en Picshare , que se puede encontrar aquí, y un ejemplo de los resultados producidos por GWT se puede encontrar aquí.
Habilitar WebGL por sí solo no nos da la capacidad de gráficos 3D. Como escribe Gregg Tavares:
Lo que mucha gente no sabe es que WebGL es en realidad una API 2D, no una API 3D.
La aritmética 3D debe ser realizada por algún otro código y transformada a la imagen 2D para WebGL. Hay algunas buenas bibliotecas GWT para gráficos 3D WebGL. Mi favorito es Parallax, pero para la primera versión de Picshare seguimos un camino más de "hágalo usted mismo", escribiendo una pequeña biblioteca para renderizar mallas 3D simples. La biblioteca nos permite definir una cámara en perspectiva y administrar una escena de objetos. Siéntase libre de comprobarlo, aquí.
Compilación de bibliotecas Java de terceros con GWT
NyARToolkit es un puerto de Java puro de ARToolKit, una biblioteca de software para crear aplicaciones de realidad aumentada. El puerto fue escrito por los desarrolladores japoneses en Nyatla. Aunque el ARToolKit original y la versión de Nyatla han divergido un poco desde el puerto original, el NyARToolkit todavía se mantiene y mejora activamente.
La realidad aumentada basada en marcadores es un campo especializado y requiere competencia en visión por computadora, procesamiento de imágenes digitales y matemáticas, como se muestra aquí:
Reproducido de la documentación de ARToolKit.
Reproducido de la documentación de ARToolKit.
Todos los algoritmos utilizados por el kit de herramientas están documentados y se entienden bien, pero reescribirlos desde cero es un proceso largo y propenso a errores, por lo que es preferible utilizar un kit de herramientas existente y probado, como ARToolKit. Desafortunadamente, cuando se orienta a la web, no existe tal cosa disponible. Los kits de herramientas más poderosos y avanzados no tienen implementación en JavaScript, un lenguaje que se usa principalmente para manipular documentos y datos HTML. Aquí es donde GWT demuestra su fuerza invaluable, permitiéndonos simplemente transpilar NyARToolkit a JavaScript y usarlo en una aplicación web con muy poca molestia.
Compilando con GWT
Dado que un proyecto GWT es esencialmente un proyecto Java, usar NyARToolkit es solo una cuestión de importar los archivos de origen en su ruta de origen. Sin embargo, tenga en cuenta que dado que la transpilación del código GWT a JavaScript se realiza en el nivel del código fuente, necesita las fuentes de NyARToolkit, y no solo un JAR con las clases compiladas.
La biblioteca utilizada por Picshare se puede encontrar aquí. Depende solo de los paquetes que se encuentran dentro de lib/src
y lib/src.markersystem
de la compilación de NyARToolkit archivada aquí. Debemos copiar e importar estos paquetes en nuestro proyecto GWT.

Deberíamos mantener estos paquetes de terceros separados de nuestra propia implementación, pero para continuar con la "ización de GWT" de NyARToolkit, debemos proporcionar un archivo de configuración XML que informe al compilador de GWT dónde buscar las fuentes. En el paquete jp.nyatla.nyartoolkit
, agregamos el archivo NyARToolkit.gwt.xml
<module> <source path="core" /> <source path="detector" /> <source path="nyidmarker" /> <source path="processor" /> <source path="psarplaycard" /> <source path="markersystem" /> </module>
Ahora, en nuestro paquete principal, com.jooink.gwt.nyartoolkit
, creamos el archivo de configuración principal, GWT_NyARToolKit.gwt.xml
, y le indicamos al compilador que incluya la fuente de Nyatla en el classpath heredándolo de su archivo XML:
<inherits name='jp.nyatla.nyartoolkit.NyARToolkit'/>
Bastante fácil, en realidad. En la mayoría de los casos, esto sería todo lo que se necesita, pero lamentablemente aún no hemos terminado. Si intentamos compilar o ejecutar a través del modo Super Dev en esta etapa, encontramos un error que dice, sorprendentemente:
No source code is available for type java.io.InputStream; did you forget to inherit a required module?
La razón de esto es que NyARToolkit (es decir, una biblioteca de Java diseñada para proyectos de Java) utiliza clases de JRE que no son compatibles con el JRE emulado de GWT. Hablamos de esto brevemente en el post anterior.
En este caso, el problema es con InputStream
y las clases IO relacionadas. Da la casualidad de que ni siquiera necesitamos usar la mayoría de estas clases, pero debemos proporcionar alguna implementación al compilador. Bueno, podríamos invertir mucho tiempo en eliminar manualmente estas referencias de la fuente de NyARToolkit, pero sería una locura. GWT nos ofrece una mejor solución: proporcionar nuestras propias implementaciones de las clases no admitidas a través de la etiqueta XML <super-source>
.
<super-source>
Como se describe en la documentación oficial:
La etiqueta
<super-source>
indica al compilador que vuelva a rootear una ruta de origen. Esto es útil para los casos en los que desea reutilizar una API de Java existente para un proyecto GWT, pero la fuente original no está disponible o no se puede traducir. Una razón común para esto es emular parte del JRE no implementado por GWT.
Así que <super-source>
es exactamente lo que necesitamos.
Podemos crear un directorio jre
en el proyecto GWT, donde podemos poner nuestras implementaciones para las clases que nos están causando problemas:
java.io.FileInputStream java.io.InputStream java.io.InputStreamReader java.io.StreamTokenizer java.lang.reflect.Array java.nio.ByteBuffer java.nio.ByteOrder
Todos estos, excepto java.lang.reflect.Array
, realmente no se usan, por lo que solo necesitamos implementaciones tontas. Por ejemplo, nuestro FileInputStream
dice lo siguiente:
package java.io; import java.io.InputStream; import com.google.gwt.user.client.Window; public class FileInputStream extends InputStream { public FileInputStream(String filename) { Window.alert("WARNING, FileInputStream created with filename: " + filename ); } @Override public int read() { return 0; } }
La instrucción Window.alert
en el constructor es útil durante el desarrollo. Aunque debemos poder compilar la clase, queremos asegurarnos de que nunca la usemos, por lo que esto nos alertará en caso de que la clase se use inadvertidamente.
java.lang.reflect.Array
es realmente utilizado por el código que necesitamos, por lo que se requiere una implementación no completamente tonta. Este es nuestro código:
package java.lang.reflect; import jp.nyatla.nyartoolkit.core.labeling.rlelabeling.NyARRleLabelFragmentInfo; import jp.nyatla.nyartoolkit.markersystem.utils.SquareStack; import com.google.gwt.user.client.Window; public class Array { public static <T> Object newInstance(Class<T> c, int n) { if( NyARRleLabelFragmentInfo.class.equals(c)) return new NyARRleLabelFragmentInfo[n]; else if(SquareStack.Item.class.equals(c)) return new SquareStack.Item[n]; else Window.alert("Creating array of size " + n + " of " + c.toString()); return null; } }
Ahora, si colocamos <super-source path="jre"/>
en el archivo del módulo GWT_NyARToolkit.gwt.xml
, ¡podemos compilar y usar NyARToolkit en nuestro proyecto de manera segura!
Pegarlo todo junto con GWT
Ahora estamos en la posición de tener:
- WebRTC, una tecnología capaz de tomar una transmisión de la cámara web y mostrarla en una etiqueta
<video>
. - WebGL, una tecnología capaz de manipular gráficos acelerados por hardware en un HTML
<canvas>
. - NyARToolkit, una biblioteca de Java capaz de tomar una imagen (como una matriz de píxeles), buscar un marcador y, si lo encuentra, darnos una matriz de transformación que define completamente la posición del marcador en el espacio 3D.
El desafío ahora es integrar todas estas tecnologías juntas.
No profundizaremos en cómo lograr esto, pero la idea básica es usar las imágenes de video como fondo de nuestra escena (una textura aplicada al plano "lejano" en la imagen de arriba) y construir una estructura de datos 3D permitiéndonos proyectar esta imagen en el espacio usando los resultados de NyARToolkit.
Esta construcción nos brinda la estructura adecuada para interactuar con la biblioteca de NyARToolkit para el reconocimiento de marcadores y dibujar el modelo 3D sobre la escena de la cámara.
Hacer que la transmisión de la cámara sea utilizable es un poco complicado. Los datos de video solo se pueden dibujar en un elemento <video>
. El elemento HTML5 <video>
es opaco y no nos permite extraer los datos de la imagen directamente, por lo que nos vemos obligados a copiar el video en un <canvas>
intermedio, extraer los datos de la imagen, transformarlos en una matriz de píxeles y finalmente empújelo al método Sensor.update()
de NyARToolkit. Luego, NyARToolkit puede hacer el trabajo de identificar el marcador en la imagen y devolver una matriz de transformación correspondiente a su posición en nuestro espacio 3D.
¡Con estos elementos, podemos colocar un objeto sintético exactamente sobre el marcador, en 3D, en la transmisión de video en vivo! Gracias al alto rendimiento de GWT, tenemos muchos recursos computacionales, por lo que incluso podemos aplicar algunos efectos de video en el lienzo, como sepia o desenfoque, antes de usarlo como fondo para la escena WebGL.
El siguiente código abreviado describe el núcleo del proceso:
// given a <canvas> drawing context with appropriate width and height // and a <video> where the mediastream is drawn ... // for each video frame // draw the video frame on the canvas ctx.drawImage(video, 0, 0, w, h); // extract image data from the canvas ImageData capt = ctx.getImageData(0, 0, w, h); // convert the image data in a format acceptable by NyARToolkit ImageDataRaster input = new ImageDataRaster(capt); // push the image in to a NyARSensor sensor.update(input); // update the NyARMarkerSystem with the sensor nyar.update(sensor); // the NyARMarkerSystem contains information about the marker patterns and is able to detect them. // After the call to update, all the markers are detected and we can get information for each // marker that was found. if( nyar.isExistMarker( marker_id ) ) { NyARDoubleMatrix44 m = nyar.getMarkerMatrix(marker_id); // m is now the matrix representing the pose (position and orientation) of // the marker in the scene, so we can use it to superimpose an object of // our choice ... } ...
Con esta técnica, podemos generar resultados como este:
Este es el proceso que utilizamos para crear Picshare , donde se le invita a imprimir un marcador o mostrarlo en su dispositivo móvil y jugar con AR basado en marcadores en su navegador. ¡Disfrutar!
Observaciones finales
Picshare es un proyecto favorito a largo plazo para nosotros en Jooink. La primera implementación se remonta a unos años atrás, e incluso entonces fue lo suficientemente rápido como para ser impresionante. En este enlace puedes ver uno de nuestros experimentos anteriores, compilado en 2012 y nunca tocado. Tenga en cuenta que en la muestra solo hay un <video>
. Las otras dos ventanas son elementos <canvas>
que muestran los resultados del procesamiento.
GWT era lo suficientemente potente incluso en 2012. Con el lanzamiento de GWT 2.8, hemos obtenido una capa de interoperabilidad muy mejorada con JsInterop, lo que aumenta aún más el rendimiento. Además, para celebración de muchos, también obtuvimos un entorno de desarrollo y depuración mucho mejor, el modo Super Dev. Ah, sí, y compatibilidad con Java 8.
¡Esperamos con ansias GWT 3.0!