Come GWT sblocca la realtà aumentata nel tuo browser

Pubblicato: 2022-03-11

Nel nostro precedente post sul GWT Web Toolkit, abbiamo discusso i punti di forza e le caratteristiche di GWT, che, per richiamare l'idea generale, ci consente di trasporre il codice sorgente Java in JavaScript e di mescolare le librerie Java e JavaScript senza soluzione di continuità. Abbiamo notato che il JavaScript generato da GWT è notevolmente ottimizzato.

Nel post di oggi, vorremmo approfondire un po' e vedere il GWT Toolkit in azione. Dimostreremo come possiamo sfruttare GWT per costruire un'applicazione particolare: un'applicazione web di realtà aumentata (AR) che viene eseguita in tempo reale, completamente in JavaScript, nel browser.

Realtà aumentata nel browser? È più facile di quanto pensi.

In questo articolo, ci concentreremo su come GWT ci offre la possibilità di interagire facilmente con molte API JavaScript, come WebRTC e WebGL, e ci consente di sfruttare una grande libreria Java, NyARToolkit, mai pensata per essere utilizzata nel browser. Mostreremo come GWT ha permesso a me e al mio team di Jooink di mettere insieme tutti questi pezzi per creare il nostro progetto preferito, Picshare , un'applicazione AR basata su marker che puoi provare subito nel tuo browser.

Questo post non sarà una guida dettagliata su come creare l'applicazione, ma mostrerà piuttosto l'uso di GWT per superare facilmente sfide apparentemente schiaccianti.

Panoramica del progetto: dalla realtà alla realtà aumentata

Pipeline per la realtà aumentata basata su marker nel browser con GWT, con WebRTC, WebGL e ARToolKit.

Picshare utilizza la realtà aumentata basata su marker. Questo tipo di applicazione AR ricerca nella scena un marker : un motivo geometrico specifico e facilmente riconoscibile, come questo. L'indicatore fornisce informazioni sulla posizione e sull'orientamento dell'oggetto contrassegnato, consentendo al software di proiettare scenari 3D aggiuntivi nell'immagine in modo realistico. I passaggi fondamentali di questo processo sono:

  • Accesso alla fotocamera: quando si tratta di applicazioni desktop native, il sistema operativo fornisce l'accesso I/O a gran parte dell'hardware del dispositivo. Non è lo stesso quando ci occupiamo di applicazioni web. I browser sono stati progettati per essere una sorta di "sandbox" per il codice JavaScript scaricato dalla rete e originariamente non erano concepiti per consentire ai siti Web di interagire con la maggior parte dell'hardware del dispositivo. WebRTC supera questa barriera utilizzando le funzionalità di acquisizione multimediale di HTML5, consentendo al browser di accedere, tra le altre cose, alla fotocamera del dispositivo e al suo flusso.
  • Analizza il flusso video: abbiamo il flusso video... e adesso? Dobbiamo analizzare ogni fotogramma per rilevare i marcatori e calcolare la posizione del marcatore nel mondo 3D ricostruito. Questo compito complesso è l'attività di NyARToolkit.
  • Aumenta il video: infine, vogliamo visualizzare il video originale con l'aggiunta di oggetti 3D sintetici. Usiamo WebGL per disegnare la scena finale aumentata sulla pagina web.

Sfruttare le API di HTML5 con GWT

L'uso di API JavaScript come WebGL e WebRTC consente interazioni inaspettate e insolite tra il browser e l'utente.

Ad esempio, WebGL consente la grafica con accelerazione hardware e, con l'aiuto della specifica dell'array tipizzato, consente al motore JavaScript di eseguire il crunching dei numeri con prestazioni quasi native. Allo stesso modo, con WebRTC, il browser è in grado di accedere ai flussi video (e altri dati) direttamente dall'hardware del computer.

WebGL e WebRTC sono entrambe librerie JavaScript che devono essere integrate nel browser web. La maggior parte dei moderni browser HTML5 viene fornita con un supporto almeno parziale per entrambe le API (come puoi vedere qui e qui). Ma come possiamo sfruttare questi strumenti in GWT, che è scritto in Java? Come discusso nel post precedente, il livello di interoperabilità di GWT, JsInterop (rilasciato ufficialmente in GWT 2.8) lo rende un gioco da ragazzi.

Usare JsInterop con GWT 2.8 è facile come aggiungere -generateJsInteropExports come argomento al compilatore. Le annotazioni disponibili sono definite nel pacchetto jsinterop.annotations , in bundle in gwt-user.jar .

WebRTC

Ad esempio, con un lavoro di codifica minimo, l'utilizzo di getUserMedia di WebRTC su Chrome con GWT diventa semplice come scrivere:

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

Dove la classe Navigator può essere definita come segue:

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

Di interesse è la definizione delle interfacce SuccessCallback ed ErrorCallback , entrambe implementate dall'espressione lambda sopra e definite in Java tramite l'annotazione @JsFunction :

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

Infine, la definizione URL della classe è quasi identica a quella di Navigator e, allo stesso modo, la classe Configs può essere definita da:

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

L'effettiva implementazione di tutte queste funzionalità avviene nel motore JavaScript del browser.

Puoi trovare il codice sopra su GitHub qui.

In questo esempio, per motivi di semplicità, viene utilizzata l'API navigator.getUserMedia() deprecata perché è l'unica che funziona senza polyfill nell'attuale versione stabile di Chrome. In un'app di produzione, possiamo utilizzare adapter.js per accedere allo stream tramite la più recente API navigator.mediaDevices.getUserMedia() , in modo uniforme in tutti i browser, ma questo va oltre lo scopo della presente discussione.

WebGL

L'utilizzo di WebGL da GWT non è molto diverso rispetto all'utilizzo di WebRTC, ma è un po' più noioso a causa della complessità intrinseca dello standard OpenGL.

Il nostro approccio qui rispecchia quello seguito nella sezione precedente. Il risultato del wrapping può essere visto nell'implementazione GWT WebGL utilizzata in Picshare , che può essere trovata qui, e un esempio dei risultati prodotti da GWT può essere trovato qui.

L'abilitazione di WebGL di per sé in realtà non ci dà capacità di grafica 3D. Come scrive Gregg Tavares:

Quello che molte persone non sanno è che WebGL è in realtà un'API 2D, non un'API 3D.

L'aritmetica 3D deve essere eseguita da un altro codice e trasformata nell'immagine 2D per WebGL. Ci sono alcune buone librerie GWT per la grafica 3D WebGL. Il mio preferito è Parallax, ma per la prima versione di Picshare abbiamo seguito un percorso più “fai da te”, scrivendo una piccola libreria per il rendering di semplici mesh 3D. La libreria ci consente di definire una telecamera prospettica e gestire una scena di oggetti. Sentiti libero di dare un'occhiata, qui.

Compilazione di librerie Java di terze parti con GWT

NyARToolkit è un port in puro Java di ARToolKit, una libreria software per la creazione di applicazioni di realtà aumentata. Il port è stato scritto dagli sviluppatori giapponesi di Nyatla. Sebbene l'ARToolKit originale e la versione Nyatla siano leggermente divergenti rispetto al port originale, NyARToolkit è ancora attivamente mantenuto e migliorato.

L'AR basata su marcatori è un campo specializzato e richiede competenza in visione artificiale, elaborazione di immagini digitali e matematica, come è evidente qui:

Analisi delle immagini in realtà aumentata basata su marker con ARToolKit.

Riprodotto dalla documentazione di ARToolKit.

Pipeline di realtà aumentata basata su marker con ARToolKit.

Riprodotto dalla documentazione di ARToolKit.

Tutti gli algoritmi utilizzati dal toolkit sono documentati e ben compresi, ma riscriverli da zero è un processo lungo e soggetto a errori, quindi è preferibile utilizzare un toolkit esistente e collaudato, come ARToolKit. Sfortunatamente, quando si prende di mira il Web, non è disponibile una cosa del genere. I toolkit più potenti e avanzati non hanno implementazione in JavaScript, un linguaggio utilizzato principalmente per manipolare documenti e dati HTML. È qui che GWT dimostra la sua inestimabile forza, consentendoci di trasporre semplicemente NyARToolkit in JavaScript e utilizzarlo in un'applicazione Web con pochissimi problemi.

Compilazione con GWT

Poiché un progetto GWT è essenzialmente un progetto Java, l'utilizzo di NyARToolkit è solo questione di importare i file di origine nel percorso di origine. Tuttavia, si noti che poiché la traspilazione del codice GWT in JavaScript viene eseguita a livello di codice sorgente, sono necessari i sorgenti di NyARToolkit e non solo un JAR con le classi compilate.

La libreria utilizzata da Picshare può essere trovata qui. Dipende solo dai pacchetti trovati all'interno di lib/src e lib/src.markersystem dalla build NyARToolkit archiviata qui. Dobbiamo copiare e importare questi pacchetti nel nostro progetto GWT.

Dovremmo mantenere questi pacchetti di terze parti separati dalla nostra implementazione, ma per procedere con la "GWT-ization" di NyARToolkit dobbiamo fornire un file di configurazione XML che informi il compilatore GWT dove cercare i sorgenti. Nel pacchetto jp.nyatla.nyartoolkit , aggiungiamo il file NyARToolkit.gwt.xml

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

Ora, nel nostro pacchetto principale, com.jooink.gwt.nyartoolkit , creiamo il file di configurazione principale, GWT_NyARToolKit.gwt.xml e istruiamo il compilatore a includere il sorgente di Nyatla nel percorso classe ereditando dal suo file XML:

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

Abbastanza facile, in realtà. Nella maggior parte dei casi, questo sarebbe tutto ciò che serve, ma sfortunatamente non abbiamo ancora finito. Se proviamo a compilare o eseguire tramite la modalità Super Dev in questa fase, riscontriamo un errore che afferma, sorprendentemente:

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

La ragione di ciò è che NyARToolkit (ovvero una libreria Java destinata ai progetti Java) utilizza classi di JRE che non sono supportate da Emulated JRE di GWT. Ne abbiamo discusso brevemente nel post precedente.

In questo caso, il problema riguarda InputStream e le relative classi IO. In effetti, non abbiamo nemmeno bisogno di usare la maggior parte di queste classi, ma dobbiamo fornire qualche implementazione al compilatore. Bene, potremmo investire un sacco di tempo nella rimozione manuale di questi riferimenti dalla fonte NyARToolkit, ma sarebbe pazzesco. GWT ci offre una soluzione migliore: fornire le nostre implementazioni delle classi non supportate tramite il tag XML <super-source> .

<super-source>

Come descritto nella documentazione ufficiale:

Il <super-source> indica al compilatore di eseguire nuovamente il root di un percorso di origine. Ciò è utile nei casi in cui si desidera riutilizzare un'API Java esistente per un progetto GWT, ma la fonte originale non è disponibile o non è traducibile. Un motivo comune per questo è emulare parte del JRE non implementato da GWT.

Quindi <super-source> è esattamente ciò di cui abbiamo bisogno.

Possiamo creare una directory jre nel progetto GWT, dove possiamo mettere le nostre implementazioni per le classi che ci stanno causando problemi:

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

Tutti questi, tranne java.lang.reflect.Array , sono davvero inutilizzati, quindi abbiamo solo bisogno di implementazioni stupide. Ad esempio, il nostro FileInputStream legge come segue:

 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'istruzione Window.alert nel costruttore è utile durante lo sviluppo. Sebbene dobbiamo essere in grado di compilare la classe, vogliamo assicurarci di non usarla mai effettivamente, quindi questo ci avviserà nel caso in cui la classe venga utilizzata inavvertitamente.

java.lang.reflect.Array è effettivamente utilizzato dal codice di cui abbiamo bisogno, quindi è necessaria un'implementazione non completamente stupida. Questo è il nostro codice:

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

Ora se inseriamo <super-source path="jre"/> nel file del modulo GWT_NyARToolkit.gwt.xml , possiamo tranquillamente compilare e utilizzare NyARToolkit nel nostro progetto!

Incollando il tutto insieme a GWT

Ora siamo nella posizione di avere:

  • WebRTC, una tecnologia in grado di prelevare uno stream dalla webcam e visualizzarlo in un tag <video> .
  • WebGL, una tecnologia in grado di manipolare la grafica con accelerazione hardware in un HTML <canvas> .
  • NyARToolkit, una libreria Java in grado di acquisire un'immagine (come un array di pixel), cercare un marker e, se trovato, fornirci una matrice di trasformazione che definisce completamente la posizione del marker nello spazio 3D.

La sfida ora è integrare tutte queste tecnologie insieme.

Proiezione di uno spazio 3D sulla fotocamera.

Non approfondiremo come farlo, ma l'idea di base è quella di utilizzare le immagini video come sfondo della nostra scena (una texture applicata al piano "lontano" nell'immagine sopra) e costruire una struttura dati 3D permettendoci di proiettare questa immagine nello spazio usando i risultati di NyARToolkit.

Questa costruzione ci offre la struttura giusta per interagire con la libreria di NyARToolkit per il riconoscimento dei marker e disegnare il modello 3D sopra la scena della telecamera.

Rendere utilizzabile il flusso della videocamera è un po' complicato. I dati video possono essere disegnati solo su un elemento <video> . L'elemento HTML5 <video> è opaco e non ci consente di estrarre direttamente i dati dell'immagine, quindi siamo costretti a copiare il video su un <canvas> intermedio, estrarre i dati dell'immagine, trasformarli in un array di pixel e infine invialo al metodo Sensor.update() di NyARToolkit. Quindi NyARToolkit può fare il lavoro di identificare il marker nell'immagine e restituire una matrice di trasformazione corrispondente alla sua posizione nel nostro spazio 3D.

Con questi elementi possiamo collocare un oggetto sintetico esattamente sopra il marker, in 3D, nel flusso video live! Grazie alle elevate prestazioni di GWT, abbiamo molte risorse di calcolo, quindi possiamo persino applicare alcuni effetti video sulla tela, come seppia o sfocatura, prima di usarla come sfondo per la scena WebGL.

Il seguente codice abbreviato descrive il nucleo del processo:

 // 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 questa tecnica possiamo generare risultati come questo:

Risultati dell'applicazione di realtà aumentata nel browser Picshare.

Risultati dell'applicazione di realtà aumentata nel browser Picshare con più marcatori.

Questo è il processo che abbiamo utilizzato per creare Picshare , in cui sei invitato a stampare un marker o visualizzarlo sul tuo telefonino e giocare con l'AR basata su marker nel tuo browser. Divertiti!

Osservazioni finali

Picshare è un progetto a lungo termine per noi di Jooink. La prima implementazione risale a qualche anno fa, e anche allora era abbastanza veloce da essere impressionante. A questo link potete vedere uno dei nostri precedenti esperimenti, compilato nel 2012 e mai toccato. Nota che nell'esempio c'è solo un <video> . Le altre due finestre sono elementi <canvas> che mostrano i risultati dell'elaborazione.

GWT era abbastanza potente anche nel 2012. Con il rilascio di GWT 2.8 abbiamo ottenuto un livello di interoperabilità molto migliorato con JsInterop, aumentando ulteriormente le prestazioni. Inoltre, per festeggiare molti, abbiamo anche ottenuto un ambiente di sviluppo e debug molto migliore, la modalità Super Dev. Oh sì, e supporto per Java 8.

Non vediamo l'ora di GWT 3.0!