Il nostro percorso per creare una applicazione di realtà aumentata con Chrome continua con questo articolo: abbiamo visto come rendere la webcam accessibile alla applicazione, è ora di introdurre WebGL, l’API che permette grafica ad alte prestazioni dall’interno del browser.
Se nella prima puntata del nostro percorso per sperimentare realtà aumentata con Chrome [0] ci siamo occupati di guadagnare l’accesso alla webcam dall’interno di una applicazione web, in questo e nell’articolo seguente ci dovremo occupare di imparare a utilizzare WebGL [1], la API che ci permetterà di fare grafica ad alte prestazioni dall’interno del browser.
WebGL
Citando quasi alla lettera la definizione che troviamo su Khronos [1] possiamo dire che WebGL è uno standard web, cross-platform, royalty-free per l’accesso a una API grafica a basso livello, basata su OpenGL ES 2.0, esposta attraverso un nuovo rendering context dell’elemento canvas di HTML5.
Come funziona WebGL
Per usare WebGL dobbiamo dunque creare un canvas ed inserirlo nel DOM:
int w = 640; int h = 480; final CanvasElement canvasElement = Browser.getDocument().createCanvasElement(); canvasElement.setWidth(w); canvasElement.setHeight(h); Browser.getDocument().getBody().appendChild(canvasElement);
Dal canvasElement dobbiamo poi prendere il rendering context webgl:
WebGLRenderingContext ctx3D = null; ctx3D = (WebGLRenderingContext)canvas.getContext("experimental-webgl"); if (ctx3D == null) ctx3D = (WebGLRenderingContext)canvas.getContext("webgl");
Qui occorre prestare attenzione al fatto che alcuni browser utilizzano la stringa “webgl” mentre altri quella “experimental-webgl” per indicare il rendering context webgl.
Se entrambi questi metodi ritornano un oggetto null, dovremo concludere che WebGL non è supportato dal browser. Le verifiche sono omesse per semplicità dal testo dell’articolo, ma sono riportate nel codice d’esempio che potete scaricare dal menu “Allegati” a destra.
Una volta ottenuto il rendering context possiamo iniziare a disegnare… beh, quasi.
Web GL: potente ma non immediato
Purtroppo, continuando a esplorare WebGL si arriva alla conclusione che si tratta di una low-level API dedicata alla grafica ad alte prestazioni, ma non certo di uso immediato.
Una trattazione accurata di WebGL è decisamente al di fuori dei nostri obiettivi, ma alcuni concetti ci sono necessari e quindi cercheremo di introdurli, sebbene in forma semplificata descrivendo un esempio minimalistico [6] basato sulla libreria “to the metal” di GWT: Elemental. Per una visione d’insieme di Web GL, si consiglia di leggere la guida di OpenGL ES 2.0 [4] e la specifica [5].
Come dicevamo sopra WebGL è una libreria a basso livello e il suo target non è quello di disegnare sul canvas ma piuttosto quello di darci gli strumenti (e le guidelines) per utilizzare in maniera efficiente l’acceleratore grafico. Questo significa che, prima di riuscire a scrivere qualcosa sul canvas dobbiamo cercare di capire quale sia la filosofia generale d’uso di questo sistema
Le premesse e i concetti dai quali non si può prescindere sono
- primitive
- pipeline, shaders e GLSL
- buffer e chiamate
Di seguito vediamo questi elementi.
Gli elementi fondamentali di Web GL
Primitive
Quanto dovrà essere processato dall’acceleratore grafico e che quindi comporrà l’output della nostra eleborazione dovrà essere specificato per mezzo di primitive grafiche.
WebGL mette a nostra disposizione per la costruzione di superifici sostanzialmente un’unica primitiva: i triangoli (si veda [7] per una concisa trattazione delle possibili opzioni). I nostri algoritmi dovranno essere quindi organizzati in modo da prevedere solamente l’utilizzo di triangoli; per esempio, un rettangolo dovrà essere visto come unione di due triangoli: il rettangolo axis-aligned di vertici (x1,y1) e (x2, y2) dovrà essere scomposto nei due triangoli (x1,y1), (x2,y1), (x1,y2) e (x1,y2), (x2,y1), (x2,y2) che dovranno poi essere passati alla pipeline grafica di opengl, il programma che si occuperà di processare e alla fine disegnare i triangoli.
Figura 1 – Un esempio di costruzione di rettangolo a partire dalla primitiva grafica triangolo.
Pipeline, shader e GLSL
Al fine di permettere allo sviluppatore di avere a disposizione il massimo della flessibilità nell’utilizzo dell’hardware grafico, OpenGL non specifica come le primitive verranno “scritte” sul rendering context (e quindi sul canvas) ma prevede che vengano sviluppati dei programmi scritti in un vero e proprio linguaggio (GLSL [8]) che verranno compilati ed eseguiti direttamente nell’hardware grafico per effettuare il rendering delle primitive.
Ogni programma OpenGL deve prevedere almeno due subroutines (shader) dette rispettivamente vertex shader e fragment shader e corrispondenti a due diverse fasi della rendering pipeline [10]. Il primo, per vertex, verrà eseguito per ogni vertice che passeremo al program; il secondo, per fragment, verrà eseguito subito dopo la rasterizzazione ([9]) su ogni frammento generato.
Di seguito riportiamo un esempio di Vertex Shader:
attribute vec2 a_position; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 zeroToTwo = zeroToOne * 2.0; vec2 clipSpace = zeroToTwo - 1.0; gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); }
E qui abbiamo un esempio di Fragment Shader:
precision mediump float; uniform vec4 u_color; void main() {" + gl_FragColor = u_color; }
Come si vede, gli shader sono scritti in un linguaggio che ricorda vagamente il C, il cui nome è GLSL (GL Shading Language) che è stato appositamente sviluppato per rendere agevole la scrittura delle operazioni che solitamente servono in una pipeline grafica; infatti GLSL ha come tipi primitivi, oltre a numeri in virgola mobile (float), vettori di 2, 3 e 4 elementi e matrici (fino a dimensione 4 x 4) e una completa gamma di operazioni tra matrici e vettori.
Il compito del Vertex Shader è in generale quello di processare i vertici che compongono le nostre primitive e trasformarli in modo che siano pronti per la rasterizzazione e quindi la visualizzazione. Il vertex shader ritorna il valore del vertice in output scrivendo nella variabile “implicita” gl_Position (vec4). Solitamente il vertex shader viene utilizzato per applicare ai vertici trasformazioni, come nel nostro caso, in cui viene utilizzato per trasformare le coordinate da “spazio dei pixel” a clipSpace, cioè [-1,1] x [-1,1]. Le coordinate, nel caso della grafica 3D, sono espresse sotto forma di matrici 4 x 4 dette proiezioni (ma di questo parleremo in una prossima puntata).
Il compito del Fragment Shader è invece quello di scrivere la variabile “implicita” gl_FragColor (vec4) che rappresenta il colore di ogni singolo frammento derivante dalla rasterizzazione delle primitive.
Le variabili contrassegnate dalle keyword attribute e uniform (e varying che non compare nell’esempio) sono i parametri che saranno passati al programma GLSL.
I primi (attribute) sono quelli che variano per ogni vertice (vale a dire le coordinate) mentre gli uniform sono le costanti che resteranno immutate per un intera esecuzione; i varying, infine, saranno variabili del fragment shader che assumeranno valori interpolati (triangolo per triangolo) per ogni frammento.
Buffers e chiamate
Al fine di massimizzare le performance dell’esecuzione degli shader, e di minimizzare le operazioni di trasferimento dati con l’hardware grafico, WebGL prevede che il programma chiamante crei e popoli dei buffer con le primitive e gli attributi necessari per l’esecuzione degli shaders e poi li passi al programma opengl in un solo colpo.
Tradurre il tutto in codice
Ora che abbiamo visto gli elementi base di Web GL, riportiamo il codice che implementa quanto appena detto, attraverso diversi passaggi.
Costruire gli shader
Gli shader sono costruiti come segue:
WebGLShader vs = createShader(WebGLRenderingContext.VERTEX_SHADER, vertexShader, ctx3D); WebGLShader fs = createShader(WebGLRenderingContext.FRAGMENT_SHADER, fragmentShader, ctx3D);
Qui, le variabili String vertexShader e fragmentShader sono inizializzate con gli shader riportati sopra. Tecnicamente la funzione createShader non è parte di WebGL ma è uno helper che abbiamo definito noi per rendere più leggibile il codice e consta di quattro passi:
//creazione dell'oggetto shader WebGLShader shader = ctx.createShader(type); //impostazione del sorgente ctx.shaderSource(shader,code ); //compilazione del sorgente ctx.compileShader(shader); //verifica dell'esito della compilazione testShaderStatus(ctx,shader);
Per la verità, l’ultima delle operazioni non si riesce a farla direttamente usando Elemental, ma dobbiamo farla ricorrendo a codice nativo a causa di complicazioni nella tipatura del metodo getShaderParameter del rendering context webgl. In ogni caso, il sorgente del metodo è riportato nel codice d’esempio scaricabile dal menu “Allegati” in alto a destra.
Una volta creati gli shader, possiamo infine linkarli in un unico programma:
WebGLProgram program = createAndUseProgram(Arrays.asList(vs,fs), ctx3D);
anche questa volta facendo uso di un helper non dissimile dal precedente.
Popolare i buffer
Creato il programma possiamo assegnare gli uniform:
//vertex WebGLUniformLocation resolutionLocation = ctx3D.getUniformLocation(program, "u_resolution"); ctx3D.uniform2f(resolutionLocation, w,h); //fragment WebGLUniformLocation colorLocation = ctx3D.getUniformLocation(program, "u_color"); ctx3D.uniform4f(colorLocation, (float)Math.random(), (float)Math.random(), (float)Math.random(), 1);
E poi si assegnano gli attributi:
int positionLocation = ctx3D.getAttribLocation(program, "a_position"); ctx3D.enableVertexAttribArray(positionLocation); ctx3D.vertexAttribPointer(positionLocation, 2, WebGLRenderingContext.FLOAT, false, 0, 0);
Questi dovranno essere popolati con l’array dei vertici ai triangoli
Float32Array rect = createRectangleVertices(rx, ry, rw, rh); WebGLBuffer vertexPositionBuffer = ctx3D.createBuffer(); ctx3D.bindBuffer(WebGLRenderingContext.ARRAY_BUFFER, vertexPositionBuffer); ctx3D.bufferData(WebGLRenderingContext.ARRAY_BUFFER, (ArrayBuffer) rect, WebGLRenderingContext.STATIC_DRAW);
dove createRectangleVertices è semplicemente il metodo che ritorna l’array di vertici ai triangoli.
Eseguire i programmi
Infine, eseguiremo una chiamata con la quale otterremo l’esecuzione del programma, che utilizzerà 6 vertici, da interpretarsi come vertici di triangoli, a partire dalla posizione “0” di vertexPositionBuffer.
ctx3D.drawArrays(WebGLRenderingContext.TRIANGLES, 0, 6);
Conclusioni
Ci rendiamo ovviamente conto che il codice così realizzato è estremamente complesso per disegnare un rettangolo; ma l’aspetto importante è che esso si presta facilmente a generalizzazioni. Vedremo infatti che integrare lo stream video proveniente dalla webcam (visto nell’articolo precedente) come texture del nostro rettangolo richiede solo poche modifiche; e, sulla base di quanto esposto in questo articolo, anche l’inserimento di oggetti 3D è relativamente semplice. Ma vedremo tutto ciò nella prossima puntata.
Riferimenti
[0] Il primo articolo della serie
https://www.mokabyte.it/cms/article.run?articleId=43S-1FG-6TX-XR7_7f000001_26089272_eb26acb8
[1] La definizione di Web GL
[2] WebGL Fundamentals
http://games.greggman.com/game/webgl-fundamentals/
[3] Proiezioni tridimensionali
http://en.wikipedia.org/wiki/3D_projection
[4] Specifiche OpenGL ES (novembre 2010)
http://www.khronos.org/registry/gles/specs/2.0/es_full_spec_2.0.25.pdf
[5] Specifiche WebGL (marzo 2013)
https://www.khronos.org/registry/webgl/specs/latest/
[6] Esempio WebGL/Elemental
http://jooink.blogspot.it/2012/10/webgl-tinysample.html
[7] Different drawArrays modes in WebGL
http://www.khronos.org/message_boards/showthread.php/7292-Different-drawArrays-modes-in-WebGL
[8] Il linguaggio OpenGL Shading Language
http://www.opengl.org/documentation/glsl/
[9] Rasterizzazione
http://en.wikipedia.org/wiki/Rasterisation
[10] Panoramica sulla Rendering Pipeline di OpenGL
http://www.opengl.org/wiki/Rendering_Pipeline_Overview