Comment GWT déverrouille la réalité augmentée dans votre navigateur

Publié: 2022-03-11

Dans notre précédent article sur le GWT Web Toolkit, nous avons évoqué les points forts et les caractéristiques de GWT, qui, pour rappeler l'idée générale, permet de transpiler le code source Java en JavaScript et de mélanger les bibliothèques Java et JavaScript de manière transparente. Nous avons noté que le JavaScript généré par GWT est considérablement optimisé.

Dans l'article d'aujourd'hui, nous aimerions aller un peu plus loin et voir la boîte à outils GWT en action. Nous montrerons comment nous pouvons tirer parti de GWT pour créer une application particulière : une application Web de réalité augmentée (AR) qui s'exécute en temps réel, entièrement en JavaScript, dans le navigateur.

La réalité augmentée dans le navigateur ? C'est plus facile que vous ne le pensez.

Dans cet article, nous nous concentrerons sur la façon dont GWT nous donne la possibilité d'interagir facilement avec de nombreuses API JavaScript, telles que WebRTC et WebGL, et nous permet d'exploiter une grande bibliothèque Java, NyARToolkit, jamais destinée à être utilisée dans le navigateur. Nous montrerons comment GWT a permis à mon équipe et à moi de Jooink de rassembler toutes ces pièces pour créer notre projet favori, Picshare , une application AR basée sur des marqueurs que vous pouvez essayer dès maintenant dans votre navigateur.

Cet article ne sera pas une présentation complète de la façon de créer l'application, mais présentera plutôt l'utilisation de GWT pour surmonter facilement des défis apparemment écrasants.

Présentation du projet : de la réalité à la réalité augmentée

Pipeline pour la réalité augmentée basée sur des marqueurs dans le navigateur avec GWT, avec WebRTC, WebGL et ARToolKit.

Picshare utilise la réalité augmentée basée sur des marqueurs. Ce type d'application AR recherche dans la scène un marqueur : un motif géométrique spécifique et facilement reconnaissable, comme celui-ci. Le marqueur fournit des informations sur la position et l'orientation de l'objet marqué, permettant au logiciel de projeter des paysages 3D supplémentaires dans l'image de manière réaliste. Les étapes de base de ce processus sont les suivantes :

  • Accéder à la caméra : lorsqu'il s'agit d'applications de bureau natives, le système d'exploitation fournit un accès E/S à une grande partie du matériel de l'appareil. Il n'en va pas de même lorsqu'il s'agit d'applications Web. Les navigateurs ont été conçus pour être une sorte de "bac à sable" pour le code JavaScript téléchargé sur le net, et n'étaient pas destinés à permettre aux sites Web d'interagir avec la plupart des appareils. WebRTC franchit cette barrière en utilisant les fonctionnalités de capture multimédia de HTML5, permettant au navigateur d'accéder, entre autres, à la caméra de l'appareil et à son flux.
  • Analysez le flux vidéo : nous avons le flux vidéo… et maintenant ? Nous devons analyser chaque image pour détecter les marqueurs et calculer la position du marqueur dans le monde 3D reconstruit. Cette tâche complexe est l'affaire de NyARToolkit.
  • Augmenter la vidéo : Enfin, nous souhaitons afficher la vidéo originale avec des objets 3D synthétiques ajoutés. Nous utilisons WebGL pour dessiner la scène finale augmentée sur la page Web.

Tirer parti des API HTML5 avec GWT

L'utilisation d'API JavaScript comme WebGL et WebRTC permet des interactions inattendues et inhabituelles entre le navigateur et l'utilisateur.

Par exemple, WebGL permet des graphiques accélérés par le matériel et, avec l'aide de la spécification de tableau typé, permet au moteur JavaScript d'exécuter des calculs numériques avec des performances presque natives. De même, avec WebRTC, le navigateur peut accéder aux flux vidéo (et autres données) directement à partir du matériel informatique.

WebGL et WebRTC sont deux bibliothèques JavaScript qui doivent être intégrées au navigateur Web. La plupart des navigateurs HTML5 modernes prennent en charge au moins partiellement les deux API (comme vous pouvez le voir ici et ici). Mais comment exploiter ces outils dans GWT, qui est écrit en Java ? Comme indiqué dans le post précédent, la couche d'interopérabilité de GWT, JsInterop (publiée officiellement dans GWT 2.8) en fait un jeu d'enfant.

Utiliser JsInterop avec GWT 2.8 est aussi simple que d'ajouter -generateJsInteropExports comme argument au compilateur. Les annotations disponibles sont définies dans le package jsinterop.annotations , regroupées dans gwt-user.jar .

WebRTC

Par exemple, avec un travail de codage minimal, utiliser getUserMedia de WebRTC sur Chrome avec GWT devient aussi simple que d'écrire :

 Navigator.webkitGetUserMedia( configs, stream -> video.setSrc( URL.createObjectURL(stream) ), e -> Window.alert("Error: " + e) );

Où la classe Navigator peut être définie comme suit :

 @JsType(namespace = JsPackage.GLOBAL, isNative = true, name="navigator") final static class Navigator { public static native void webkitGetUserMedia( Configs configs, SuccessCallback success, ErrorCallback error); }

La définition des interfaces SuccessCallback et ErrorCallback , toutes deux implémentées par l'expression lambda ci-dessus et définies en Java au moyen de l'annotation @JsFunction , est intéressante :

 @JsFunction public interface SuccessCallback { public void onMediaSuccess(MediaStream stream); } @JsFunction public interface ErrorCallback { public void onError(DomException error); }

Enfin, la définition de l' URL de la classe est quasiment identique à celle de Navigator , et de même, la classe Configs peut être définie par :

 @JsType(namespace = JsPackage.GLOBAL, isNative = true, name="Object") public static class Configs { @JsProperty public native void setVideo(boolean getVideo); }

L'implémentation proprement dite de toutes ces fonctionnalités s'effectue dans le moteur JavaScript du navigateur.

Vous pouvez trouver le code ci-dessus sur GitHub ici.

Dans cet exemple, par souci de simplicité, l'API obsolète navigator.getUserMedia() est utilisée car c'est la seule qui fonctionne sans polyfilling sur la version stable actuelle de Chrome. Dans une application de production, nous pouvons utiliser adapter.js pour accéder au flux via la nouvelle API navigator.mediaDevices.getUserMedia() , uniformément dans tous les navigateurs, mais cela dépasse le cadre de la présente discussion.

WebGL

L'utilisation de WebGL à partir de GWT n'est pas très différente de celle de WebRTC, mais c'est un peu plus fastidieux en raison de la complexité intrinsèque de la norme OpenGL.

Notre approche ici reflète celle suivie dans la section précédente. Le résultat de l'habillage peut être vu dans l'implémentation GWT WebGL utilisée dans Picshare , qui peut être trouvée ici, et un exemple des résultats produits par GWT peut être trouvé ici.

L'activation de WebGL en soi ne nous donne pas réellement de capacité graphique 3D. Comme l'écrit Gregg Tavares :

Ce que beaucoup de gens ne savent pas, c'est que WebGL est en fait une API 2D, pas une API 3D.

L'arithmétique 3D doit être effectuée par un autre code et transformée en image 2D pour WebGL. Il existe de bonnes bibliothèques GWT pour les graphiques 3D WebGL. Mon préféré est Parallax, mais pour la première version de Picshare, nous avons suivi un chemin plus "bricolage", en écrivant une petite bibliothèque pour rendre des maillages 3D simples. La bibliothèque permet de définir une caméra en perspective et de gérer une scène d'objets. N'hésitez pas à le consulter, ici.

Compilation de bibliothèques Java tierces avec GWT

NyARToolkit est un port Java pur d'ARToolKit, une bibliothèque de logiciels pour la création d'applications de réalité augmentée. Le port a été écrit par les développeurs japonais de Nyatla. Bien que l'ARToolKit original et la version Nyatla aient quelque peu divergé depuis le port d'origine, le NyARToolkit est toujours activement maintenu et amélioré.

La RA basée sur des marqueurs est un domaine spécialisé et nécessite des compétences en vision par ordinateur, en traitement d'images numériques et en mathématiques, comme on le voit ici :

Analyse d'images en réalité augmentée basée sur des marqueurs avec ARToolKit.

Reproduit à partir de la documentation ARToolKit.

Pipeline de réalité augmentée basé sur des marqueurs avec ARToolKit.

Reproduit à partir de la documentation ARToolKit.

Tous les algorithmes utilisés par la boîte à outils sont documentés et bien compris, mais les réécrire à partir de zéro est un processus long et sujet aux erreurs, il est donc préférable d'utiliser une boîte à outils existante et éprouvée, comme ARToolKit. Malheureusement, lorsque vous ciblez le Web, rien de tel n'est disponible. Les boîtes à outils avancées les plus puissantes n'ont pas d'implémentation en JavaScript, un langage principalement utilisé pour manipuler des documents et des données HTML. C'est là que GWT prouve sa force inestimable, nous permettant de transpiler simplement NyARToolkit en JavaScript et de l'utiliser dans une application Web avec très peu de tracas.

Compiler avec GWT

Puisqu'un projet GWT est essentiellement un projet Java, l'utilisation de NyARToolkit consiste simplement à importer les fichiers source dans votre chemin source. Cependant, notez que puisque la transpilation du code GWT en JavaScript se fait au niveau du code source, vous avez besoin des sources de NyARToolkit, et pas seulement d'un JAR avec les classes compilées.

La bibliothèque utilisée par Picshare peut être trouvée ici. Il dépend uniquement des packages trouvés dans lib/src et lib/src.markersystem à partir de la construction de NyARToolkit archivée ici. Nous devons copier et importer ces packages dans notre projet GWT.

Nous devons garder ces packages tiers séparés de notre propre implémentation, mais pour procéder à la "GWT-isation" de NyARToolkit, nous devons fournir un fichier de configuration XML qui informe le compilateur GWT où rechercher les sources. Dans le package jp.nyatla.nyartoolkit , nous ajoutons le fichier NyARToolkit.gwt.xml

 <module> <source path="core" /> <source path="detector" /> <source path="nyidmarker" /> <source path="processor" /> <source path="psarplaycard" /> <source path="markersystem" /> </module>

Maintenant, dans notre package principal, com.jooink.gwt.nyartoolkit , nous créons le fichier de configuration principal, GWT_NyARToolKit.gwt.xml , et demandons au compilateur d'inclure la source de Nyatla dans le classpath en héritant de son fichier XML :

 <inherits name='jp.nyatla.nyartoolkit.NyARToolkit'/>

Assez facile, en fait. Dans la plupart des cas, ce serait tout ce qu'il faudrait, mais malheureusement nous n'avons pas encore fini. Si nous essayons de compiler ou d'exécuter via le mode Super Dev à ce stade, nous rencontrons une erreur indiquant, de manière assez surprenante :

 No source code is available for type java.io.InputStream; did you forget to inherit a required module?

La raison en est que NyARToolkit (c'est-à-dire une bibliothèque Java destinée aux projets Java) utilise des classes du JRE qui ne sont pas prises en charge par le JRE émulé du GWT. Nous en avons parlé brièvement dans le post précédent.

Dans ce cas, le problème concerne InputStream et les classes d'E/S associées. En l'occurrence, nous n'avons même pas besoin d'utiliser la plupart de ces classes, mais nous devons fournir une implémentation au compilateur. Eh bien, nous pourrions investir une tonne de temps pour supprimer manuellement ces références de la source NyARToolkit, mais ce serait fou. GWT nous offre une meilleure solution : fournissez nos propres implémentations des classes non prises en charge via la balise XML <super-source> .

<super-source>

Comme décrit dans la documentation officielle :

La <super-source> demande au compilateur de re-raciner un chemin source. Ceci est utile dans les cas où vous souhaitez réutiliser une API Java existante pour un projet GWT, mais la source d'origine n'est pas disponible ou ne peut pas être traduite. Une raison courante à cela est d'émuler une partie du JRE non implémentée par GWT.

Donc <super-source> est exactement ce dont nous avons besoin.

Nous pouvons créer un répertoire jre dans le projet GWT, où nous pouvons mettre nos implémentations pour les classes qui nous posent problème :

 java.io.FileInputStream java.io.InputStream java.io.InputStreamReader java.io.StreamTokenizer java.lang.reflect.Array java.nio.ByteBuffer java.nio.ByteOrder

Tous ces éléments, à l'exception de java.lang.reflect.Array , sont vraiment inutilisés, nous n'avons donc besoin que d'implémentations stupides. Par exemple, notre FileInputStream se lit comme suit :

 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; } }

L'instruction Window.alert dans le constructeur est utile pendant le développement. Bien que nous devions pouvoir compiler la classe, nous voulons nous assurer que nous ne l'utilisons jamais réellement, cela nous alertera donc au cas où la classe serait utilisée par inadvertance.

java.lang.reflect.Array est en fait utilisé par le code dont nous avons besoin, donc une implémentation pas complètement stupide est requise. Voici notre code :

 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; } }

Maintenant, si nous plaçons <super-source path="jre"/> dans le fichier de module GWT_NyARToolkit.gwt.xml , nous pouvons compiler et utiliser NyARToolkit en toute sécurité dans notre projet !

Tout coller avec GWT

Nous sommes maintenant en position d'avoir :

  • WebRTC, une technologie capable de prendre un flux de la webcam et de l'afficher dans une <video> .
  • WebGL, une technologie capable de manipuler des graphiques accélérés par le matériel dans un HTML <canvas> .
  • NyARToolkit, une bibliothèque Java capable de prendre une image (sous la forme d'un tableau de pixels), de rechercher un marqueur et, s'il est trouvé, de nous donner une matrice de transformation qui définit entièrement la position du marqueur dans l'espace 3D.

Le défi consiste maintenant à intégrer toutes ces technologies ensemble.

Projection d'un espace 3D sur la caméra.

Nous n'entrerons pas dans les détails sur la façon d'accomplir cela, mais l'idée de base est d'utiliser l'imagerie vidéo comme arrière-plan de notre scène (une texture appliquée au plan "lointain" dans l'image ci-dessus) et de construire une structure de données 3D nous permettant de projeter cette image dans l'espace en utilisant les résultats de NyARToolkit.

Cette construction nous donne la bonne structure pour interagir avec la bibliothèque de NyARToolkit pour la reconnaissance des marqueurs et dessiner le modèle 3D au-dessus de la scène de la caméra.

Rendre le flux de caméra utilisable est un peu délicat. Les données vidéo ne peuvent être dessinées que sur un élément <video> . L'élément HTML5 <video> est opaque et ne nous permet pas d'extraire directement les données de l'image, nous sommes donc obligés de copier la vidéo sur un intermédiaire <canvas> , d'extraire les données de l'image, de les transformer en un tableau de pixels, et enfin poussez-le vers la méthode Sensor.update() de NyARToolkit. Ensuite, NyARToolkit peut faire le travail d'identification du marqueur dans l'image et renvoyer une matrice de transformation correspondant à sa position dans notre espace 3D.

Avec ces éléments, nous pouvons colocaliser un objet synthétique exactement sur le marqueur, en 3D, dans le flux vidéo en direct ! Grâce aux hautes performances de GWT, nous disposons de nombreuses ressources de calcul, nous pouvons donc même appliquer certains effets vidéo sur le canevas, tels que le sépia ou le flou, avant de l'utiliser comme arrière-plan pour la scène WebGL.

Le code abrégé suivant décrit le cœur du processus :

 // 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 ... } ...

Avec cette technique, nous pouvons générer des résultats comme celui-ci :

Résultats de l'application de réalité augmentée dans le navigateur Picshare.

Résultats de l'application de réalité augmentée dans le navigateur Picshare avec plusieurs marqueurs.

C'est le processus que nous avons utilisé pour créer Picshare , où vous êtes invité à imprimer un marqueur ou à l'afficher sur votre mobile, et à jouer avec la réalité augmentée basée sur des marqueurs dans votre navigateur. Prendre plaisir!

Remarques finales

Picshare est un projet à long terme pour nous chez Jooink. La première implémentation remonte à quelques années, et même alors elle était assez rapide pour être impressionnante. Sur ce lien, vous pouvez voir l'une de nos expériences précédentes, compilée en 2012 et jamais touchée. Notez que dans l'exemple, il n'y a qu'un seul <video> . Les deux autres fenêtres sont des éléments <canvas> affichant les résultats du traitement.

GWT était assez puissant même en 2012. Avec la sortie de GWT 2.8, nous avons acquis une couche d'interopérabilité bien améliorée avec JsInterop, améliorant encore les performances. De plus, pour la célébration de beaucoup, nous avons également obtenu un bien meilleur environnement de développement et de débogage, le Super Dev Mode. Oh oui, et la prise en charge de Java 8.

Nous attendons avec impatience GWT 3.0 !