Concludiamo con questa puntata il lavoro illustrato per tutta la serie. In questo articolo ci occuperemo di mostrare i passi necessari per realizzare l’applicazione HTML5/JavaScript in grado di funzionare sul browser Google Chrome, interamente sul client e senza l’uso di alcun plugin.
Un’applicazione client side
Riprendendo le fila del lavoro, quello che ci proponiamo di ottenere è un’applicazione puramente JavaScript/HTML5: essa verrà eseguita da un browser, interamente sul client e senza l’utilizzo di alcun plugin.
Come abbiamo visto nelle puntate precedenti della serie, vogliamo realizzare una applicaione che ci permetta di catturare lo stream proveniente dalla webcam, lo elabori in tempo reale riconoscendo un marker predefinito, ad esempio una immagine (figura 1) e ne calcoli la posizione e l’orientamento rispetto alla telecamera dandoci così abbastanza informazione per poterlo sostituire nello stream video, senza ritardi apprezzabili, con un oggetto sintetico: nella demo abbiamo usato un semplice cubo, ma usare altre geometrie o altri oggetti multimediali è davvero un gioco da ragazzi.
Figura 1 – Il marker viene visualizzato sullo schermo di un cellulare (funziona solo con Chrome) [6].
Il nostro approccio per la realizzazione del programma è stato sin dall’inizio quello di non cercare di scrivere direttamente in JavaScript tutta l’applicazione ma di progettarla e implementarla in Java utilizzando poi il compilatore GWT per generare il codice JavaScript ottimizzato; questo ci permette peraltro anche di fare uso della libreria Java NyARToolkit come descritto nelle precedenti puntate.
Figura 2 – Il marker.
Il piano operativo
Il piano è semplice, e ricapitola un po’ quanto abbiamo visto nelle puntate precedenti della serie.
GWT ed Elemental
Dovremo ricordare quanto imparato nella prima puntata [0] per creare un progetto GWT che usi Elemental, popoli la UI con un video e lo connetta allo stream proveniente dalla webcam (che ci lascia anche la possibiltà di creare qualche snapshot casomai volessimo salvare le nostre immagini).
Visualizzare un oggetto 3D con WebGL
Dal secondo [1] e, soprattuto, dal terzo articolo [2] potremo prendere il codice per visualizzare con WebGL un oggetto 3D (sotto forma di liste di vertici, facce e poco altro) data la matrice di proiezione prospettica e quella di “posizionamento”.
GWT-NyARToolkit
Come visto nella parte pubblicata a luglio [3], potremo usare NyARToolkit (o meglio GWT-NyARToolkit [4]) per individuare un dato “marker” (in tempo reale) nei fotogrammi provenienti dalla webcam.
Determinare la posizione del marker
Infine, e questa è l’unica cosa che non abbiamo ancora visto, potremo determinare la “posizione” del marker nella scena e sovrapporlo allo stream video.
Il codice dell’applicazione
Iniziamo esattamente dal codice di [0], scaricandolo per comodità dal repository SVN su GoogleCode [5]. La prima modifica di cui ci dobbiamo occupare è quella di passare lo stream video ad ARToolKit. Come in [3] avremo bisogno di un Canvas (HTML5 ) dal quale estrarre il pixel array e sul quale, inevitabilmente, copiare il video.
Il codice di [3] non è il più versatile che possiamo pensare ma riusciamo ad ottenere il risultato desiderato sostituendo la classe ElementalVideoWidget (linea 111 di ElementalGetUserMediaDemo.java) con una nuova classe che chiameremo AugmentedElementalVideoWidget.
AugmentedElementalVideoWidget
Il costruttore, che prima creava un video element, adesso dovrà creare sia il video element (ne abbiamo bisogno per “ospitare” il media stream) che ben due canvas element: il primo servirà come “buffer’ sul quale copieremo i fotogrammi per poi estrarne la ImageData come in [3], il secondo servirà invece per mostrare i fotogrammi “aumentati” (vale a dire quelli sui quali inseriremo gli elementi, oggetti, aggiuntivi) e dovrà quindi avere il supporto webgl. Tutte queste operazioni verranno fatte attraverso un opportuno RepeatingCommand eseguito dallo Scheduler.
public AugmentedElementalVideoWidget() { videoElement = Browser.getDocument().createVideoElement() ; canvasElement = Browser.getDocument().createCanvasElement(); canvasElement_buffer = Browser.getDocument().createCanvasElement(); ctx_buffer = (CanvasRenderingContext2D) canvasElement.getContext("2d"); ctx_wgl = (WebGLRenderingContext) canvasElement.getContext("webgl"); setElement(ElementalUtils.castElementToElement(canvasElement)); initAR(); intiWebGL(); Scheduler.get().scheduleFixedDelay(updateCanvasCommand, 1); }
Va osservato che AugmentedElementalVideoWidget si inizializza (setElement) utilizzando il canvas webgl e non il video (come accadeva nella versione originale) e quindi non sarà il video originale a essere visualizzato dal browser ma il canvas, sul quale avremo aggiunto i dettagli augmented.
initAR
La creazione e il setup degli oggetti necessari ad ARToolkit viene demandata al metodo initAR, mentre il setup necessario per l’utilizzo di WebGL (shareds, buffers etc.) viene fatto in initWebGL.
Sostanzialmente in initAR dobbiamo copiare parte delle inizializzazioni che facevamo in [3] nell’EntryPoint (TrivialSample.java):
private void initAR() { try { config = new NyARMarkerSystemConfig(320,240); nyar=new NyARMarkerSystem(config); nyar.setProjectionMatrixClipping(1.0e-4, 1.0e4); i_sensor = new NyARSensor(new NyARIntSize(320,240)); //creare a marker int i_patt_resolution = 16; int i_patt_edge_percentage = 25; double i_marker_size =2; NyARCode arCode=new NyARCode(i_patt_resolution,i_patt_resolution); loadFromARToolKitFormString( Data.INSTANCE.patt_hiro().getText(),arCode); marker_id = nyar.addARMarker(arCode, i_patt_edge_percentage, i_marker_size); } catch(Exception e) { … } }
Qui si vede che abbiamo bisogno di copiare da TrivialSample.java anche il metodo loadFromARToolKitFormString (assieme alla classe Data e al file hiro.patt che definisce il marker, che comunque sono riportati nel codice d’esempio allegato, scaricabile dal menu in alto a destra).
RepeatingCommand
Inizializzato il toolkit e caricato il marker, a questo punto resterà al RepeatingCommand updateCanvasCommand il compito di fare la detection.
private RepeatingCommand updateCanvasCommand = new RepeatingCommand() { @Override public boolean execute() { if(AugmentedElementalVideoWidget.this.isAttached()) { int w = videoElement.getVideoWidth(); int h = videoElement.getVideoHeight(); canvasElement.setWidth(w); canvasElement.setHeight(h); ctx.drawImage(videoElement, 0, 0); ImageData capt = ctx.getImageData(0, 0, w, h); //elemental vs 'std' weirdness com.google.gwt.canvas.dom.client.ImageData gCapt = createImageDataFromImageData(capt); ImageDataRaster input = new ImageDataRaster(gCapt); try { i_sensor.update(input); nyar.update(i_sensor); showResult(); } catch(Exception e) { ... } } return !readyToDie; };
Ancora una volta questo metodo viene direttamente dal codice descritto in [3] con solo qualche complicazione dovuta al fatto che adesso è necessario gestire l’inserimento del widget nel DOM, la sua rimozione e la relativa rimozione del command dallo scheduler.
createImageDataFromImageData
Il metodo createImageDataFromImageData merita un commento a parte: come al solito, usando Elemental ogni tanto ci troviamo nella situazione di dover fare qualche equilibrismo quando i medesimi oggetti JavaScript (ImageData in questo caso) sono supportati sia da GWT “standard’ che da Elemental. Fortunatamente, essendo com.google.gwt.canvas.dom.client.ImageData un JSO, è sufficiente usare un banale metodo JSNI per effettuare il “cast”:
private static final native com.google.gwt.canvas.dom.client.ImageData createImageDataFromImageData(ImageData capt) /*-{ return capt; }-*/;
La componente WebGL
La “fusione” dei codici già visti è quasi alla fine a questo punto: manca di aggiungere la parte WebGL (di cui si occupa il metodo showResult) e sarà tutto.
Il codice WebGL dovrà in questa demo occuparsi innanzi tutto di copiare il video come “background” della nostra scena, poi dovrà procedere a disegnare un oggetto sulla posizione individuata del marker.
Realizzare il background richiede un “program” (se non ricordate cos’è, rileggete [1] e [2]) a parte che disegni un rettangolo sul piano “lontano” della scena e applichi su di esso il video come texture.
vertexShader
precision mediump float; attribute vec2 a_position; attribute vec2 a_texCoord; uniform vec2 u_resolution; varying vec2 v_texCoord; void main() { // convert the rectangle from pixels to 0.0 to 1.0 vec2 zeroToOne = a_position / u_resolution; // convert from 0->1 to 0->2 vec2 zeroToTwo = zeroToOne * 2.0; // convert from 0->2 to -1->+1 (clipspace) vec2 clipSpace = zeroToTwo - 1.0; gl_Position = vec4(clipSpace * vec2(1, -1), 1, 1); // pass the texCoord to the fragment shader // The GPU will interpolate this value between points. v_texCoord = a_texCoord; }
Data la descrizione del rettangolo [0,u_resolution.x][0,u_resolution.y] (che verrà passato allo shader sotto forma di due triangoli, come al solito) questo vertex shader ne restituisce i vertici trasformati in modo tale che coprano il rettangolo [-1:1][-1:1] sul piano lontano {z=1}, ribaltando, peraltro, la coordinata z in modo che la texture non appaia ribaltata.
fragmentShader
precision mediump float; // our texture uniform sampler2D u_image; // the texCoords passed in from the vertex shader. varying vec2 v_texCoord; void main() { gl_FragColor = texture2D(u_image, v_texCoord); }
Utilizzando le coordinate alla texture (varying, interpolate quindi dal sistema) che il vertex shader inizializza per ogni vertice (v_texCoord = a_texCoord) e il sampler2D (che dovremo popolare con il video) semplicemente assegna ad ogni frammento il corrispondente colore sulla texture.
Il codice Java che utilizza questi shader non è dissimile da quello visto nelle parti precedenti e quindi omettiamo di commentarlo (lo trovate nel codice allegato, AugmentedVideoWidget.java, righe 340-390, inizializzazione, e righe 465-490, drawBackround) ma in buona sostanza tutto quello che c’è da fare è creare un oggetto di topo WebGLTexture
WebGLTexture bg_texture = ctx_wgl.createTexture();
farne il “binding”
ctx_wgl.bindTexture(WebGLRenderingContext.TEXTURE_2D, bg_texture);
e popolarlo con il contenuto del canvasElement_buffer
ctx_wgl.texImage2D(WebGLRenderingContext.TEXTURE_2D, 0, WebGLRenderingContext.RGBA, WebGLRenderingContext.RGBA, WebGLRenderingContext.UNSIGNED_BYTE, canvasElement_buffer);
Come argomento di texImage2D avremmo potuto utilizzare anche direttamente il videoElement invece che il canvas ma così facendo siamo sicuri di non incorrere in problemi di delay tra la scena “sintetica” e quella “da cam” visualizzata.
Infine, disegnare la shape (il cubo di [2]) è un lavoro di copia e incolla da Elemental3DSample.java, (allegato appunto a [2]) e di inizializzazione delle matrici di proiezione e di “posizionamento”, rispettivamente perspectiveMatrix e modelViewMatrix.
Matrice di proiezione
La matrice di proiezione andrà adesso inizializzata coerentemente con quella usata da NyARToolkit (non siamo liberi ri sceglierne una, dobbiamo usare la medesima usata dal toolkit per effettuare la detection) e quindi potremo semplicemente usare nyar.getFrustum().getMatrix(), opportunamente copiata in un double[].
NyARDoubleMatrix44 pm = nyar.getFrustum().getMatrix(); double[] perspectiveMatrix = new double[] { pm.m00, pm.m10, pm.m20, pm.m30, pm.m01, pm.m11, pm.m21, pm.m31, pm.m02, pm.m12, pm.m22, pm.m32, pm.m03, pm.m13, pm.m23, pm.m33};
mentre per collocare la shape sul marker utilizzaremo la il metodo nyar.getMarkerMatrix(marker_id), da chiamare solo quando nyar.isExistMarker(marker_id) è true, opportunamente trasformata per tener conto del fatto che NyARToolkit utilizza matrici organizzate in maniera differente dal nostro vertex shader (si veda il metodo toCameraViewRH(…)).
Fatte anche queste modifiche (trovate il codice completo nei metodi initWebGL e showResults) il programma è completo e possiamo finalmente compilarlo (devmode con Elemental non funziona) ed eseguirlo, naturalmente non dimenticate di scaricare [4] e di preparare una stampa del marker; il risultato dovrebbe essere identico a quello di figura 3 (utilizzare Chrome).
Figura 3 – Il risultato del codice, utilizzando l’apposito marker.
Conclusioni
Come crediamo di aver dimostrato in questa serie di articoli, le applicazioni “in browser” possono oggi fare cose notevoli sia dal punto di vista delle prestazioni (la detection nella nostra demo richiede solo qualche millisecondo) sia da quello dell’accesso a “componenti” del computer in cui il browser viene eseguito (l’acceleratore grafico con WebGL, e la webcam con WebRTC ne sono un esempio) rendendo quindi possibile la realizzazione di applicazioni “client” sofisticate e complesse, operazione nella quale GWT risulta a nostro parere insostituibile.
Si dice di solito che il web è un bersaglio mobile e che restare aggiornati con la tecnologia è spesso molto difficile. Mai come con questa serie di articoli ce ne siamo resi conto: se infatti quando abbiamo iniziato (e sono passati solamente 4 mesi) solo Chrome supportava tutte le features HTML5 di cui avevamo bisogno, adesso anche Firefox (la current stable) supporta sia WebGL che WebRTC. La tentazione di aggiornare tutti i codici che vi stavamo mostrando è stata tanta ma poi abbiamo dovuto convenire che ci sarebbe stato troppo lavoro da fare: la release di GWT 2.5.1 ha bisogno di un paio di patch per permetterci di usare esattamente il medesimo codice sia su Firefox che su Chrome [7] [8] ed è anche necessario “esportare” il supporto WebGL fuori da elemental [9]. Ma così facendo si potrebbe riuscire a ottenere un compilato abbastanza generico da funzionare su Chrome, Firefox e Safari [10] per quanto riguarda WebGL e la detection dei marker, ma purtroppo ancora dobbiamo rinunciare a Safari in [11] in quanto non supporta (ancora ?) getUserMedia.
Internet Explorer naturalmente richiede un discorso a parte: la versione 10 dovrebbe supportate tutte le tecnologie richieste, ma non abbiamo una macchina di test sulla quale funzioni WebGL (le macchine virtuali e l’accelerazione grafica non vanno d’accordo) e sulla quale testarlo, quindi non possiamo esserne certi.
Riferimenti
[0] Alberto Mancini – Francesca Tosi, “Un’applicazione di realtà aumentata con GWT – I parte: WebRTC e la libreria Elemental”, MokaByte 183, aprile 2013
https://www.mokabyte.it/cms/article.run?articleId=43S-1FG-6TX-XR7_7f000001_26089272_eb26acb8
[1] Alberto Mancini – Francesca Tosi, “Un’applicazione di realtà aumentata con GWT – II parte: Introduciamo WebGL”, MokaByte 184, maggio 2013
https://www.mokabyte.it/cms/article.run?articleId=RTD-7CG-XKR-1EL_7f000001_26089272_8553f419
[2] Alberto Mancini – Francesca Tosi, “Un’applicazione di realtà aumentata con GWT – III parte: WebGL e oggetti 3D”, MokaByte 185, giugno 2013
https://www.mokabyte.it/cms/article.run?articleId=AB6-NGQ-PKG-ORG_7f000001_26089272_29c5cb84
[3] Alberto Mancini – Francesca Tosi, “Un’applicazione di realtà aumentata con GWT – IV parte: Utilizziamo NyARToolKit”, MokaByte 186, luglio/agosto 2013
https://www.mokabyte.it/cms/article.run?articleId=5MB-KG2-VV4-VEO_7f000001_11885319_cfbef366
[4] GWT-NyARToolkit
[5] Demo di ElementalgetUserMedia
https://code.google.com/p/elemental-getusermedia-demo/source/checkout
[6] Demo di ElementalAR
http://www.jooink.com/experiments/ElementalARDemo
[7] La patch che aggiunge LoadedMetadataEvent
https://gwt-review.googlesource.com/#/c/3756/
[8] La patch che aggiunge drawImage(VideoElement, …) overloads
https://gwt-review.googlesource.com/#/c/3421/
[9] GWT WebGL bindings
https://code.google.com/p/gwt-webgl/
[10] GWT NyARToolKit Sample
http://www.jooink.com/experiments/GWT_NyARToolKit_Sample/
[11] Share Pictures su Jooink