Un'applicazione di realtà aumentata con GWT
Nel precedente articolo abbiamo imparato a costruire un primo programma che disegna un rettangolo su un canvas utilizzando WebGL (e la libreria Elemental di GWT); in questa puntata ci occuperemo di imparare a visualizzare oggetti 3D sul canvas.
Dal bi- al tridimensionale
Se effettuiamo una accurata ricerca nella specifica su Khronos [0] arriviamo a una prima "sorprendente" conclusione: diversamente da quanto ci saremmo aspettati, WebGL non fornisce alcun supporto diretto per la grafica tridimensionale: "WebGL is a 2D library" [1].
Fortunatamente però WebGL, con la sua pipeline e i suoi shaders, mette a disposizione tutti gli strumenti con cui possiamo fornire al sistema le matrici di proiezione [2] e quindi basterà passare le matrici al vertex-shader (sotto forma di uniforms), e l'acceleratore grafico le potrà applicare per noi a tutti i vertici.

Figura 1 - Il principio della proiezione prospettica, su cui è basata la riproduzione "tridimensionale" di immagini su schermi bidimensionali.
Analogamente potremo scrivere un fragment-shader che calcoli il colore di ogni fragment (pixel), ad esempio sulla base di qualche modello di shading and lighting [3].
Organizzare il codice
Visto che gli shaders di questo programma di esempio iniziano a essere non banalissimi cerchiamo di organizzare il codice in modo da non doverli mantenere come String nel codice Java. A questo fine risultano davvero molto comodi i ClientBundle di GWT [4].
L'interfaccia
Definendo l'interfaccia
public interface Shaders extends ClientBundle {
public static Shaders INSTANCE = GWT.create(Shaders.class);
@Source("vertexShader.txt")
public TextResource vertexShader();
@Source("fragmentShader.txt")
public TextResource fragmentShader();
}
possiamo mettere il codice degli shaders in file separati (che a tempo di compilazione verranno incorporati nel codice JavaScript) e utilizzarli nel nostro codice Java facendo riferimento, per esempio, a Shaders.INSTANCE.vertexShader().getText().
VertexShader
Il file vertexShader.txt dovrà contenere un programma simile a quello che segue:
attribute highp vec3 aVertexNormal;
attribute highp vec3 aVertexPosition;
uniform highp mat4 uNormalMatrix;
uniform highp mat4 uMVMatrix;
uniform highp mat4 uPMatrix;
uniform highp vec3 uAmbientLight;
uniform highp vec3 uLightColor;
uniform highp vec3 uLightDirection;
varying highp vec3 vLighting;
void main(void) {
gl_Position = uPMatrix*uMVMatrix*vec4(aVertexPosition, 1.0);
highp vec4 transformedNormal =
normalize(uNormalMatrix * vec4(aVertexNormal, 0.0));
highp float directional =
max(dot(transformedNormal.xyz, uLightDirection), 0.0);
vLighting = uAmbientLight + (uLightColor * directional);
}
Qui si istruisce la pipeline a calcolare gl_Position come il risultato dell'applicazione della matrice ottenuta moltiplicando le 2 matrici 4x4 (passate sotto forma di uniforms) uPMatrix e uMVMatrix al vettore (attribute) aVertexPosition al quale viene aggiunta la quarta componente (di suo è un vettore di 3 elementi, vec3) uguale a 1.0.
Il vertex-shader questa volta deve comunicare al fragment-shader anche un altro vettore che sarà necessario per il calcolo del colore: vLighting.Questo vettore (vec3) è contrassegnato nell'intestazione come varying e quindi WebGL si occuperà di passarlo al fragment shader dopo la rasterizzazione e la necessaria interpolazione. È bene osservare che per il calcolo di vLighting lo shader ha bisogno di conoscere la normale al vertice che, di conseguenza, andrà fornita al momento dell'esecuzione.
fragmentShader
Il file fragmentShader.txt è in questo esempio piuttosto semplice in quanto tutto il lavoro è stato fatto dal vertex shader (che ha calcolato vLighting):
precision mediump float;
varying highp vec3 vLighting;
uniform vec4 uColor;
void main(void) {
vec4 texelColor = uColor;
gl_FragColor = vec4(texelColor.rgb * vLighting, texelColor.a);
}
Gli shader utilizzati in questo esempio non sono da considerarsi particolarmente evoluti ma sono i più semplici da utilizzare per ottenere una visualizzazione 3D comunque decente. Per risultati più accattivanti, la rete è piena di shader GLSL più o meno pronti per l'uso; alcuni esempi interessanti possono essere trovati su [5].
Il codice dell'esempio
Una volta preparati gli shader possiamo procedere a costruire il nostro esempio
CanvasElement canvas =
Browser.getDocument().createCanvasElement();
canvas.setWidth(640);
canvas.setHeight(480);
Browser.getDocument().getBody().appendChild(canvas);
String ctxString = "experimental-webgl";
WebGLRenderingContext ctx3d =
(WebGLRenderingContext)canvas.getContext(ctxString);
ctx3d.viewport(0, 0, 640, 480);
Configurariamo il context per fare depth-test:
ctx3d.enable(WebGLRenderingContext.DEPTH_TEST);
ctx3d.depthFunc(WebGLRenderingContext.LEQUAL);
Come di consueto gli shaders andranno compilati e linkati in un program:
WebGLShader vs =
createShader(WebGLRenderingContext.VERTEX_SHADER,
Shaders.INSTANCE.vertexShader().getText(), ctx3d);
WebGLShader fs =
createShader(WebGLRenderingContext.FRAGMENT_SHADER,
Shaders.INSTANCE.fragmentShader().getText(), ctx3d);
WebGLProgram program =
createAndUseProgram(Arrays.asList(vs,fs), ctx3d);
Andranno poi "catturati" dal program i riferimenti agli uniforms:
WebGLUniformLocation pMatrixUniform =
ctx3d.getUniformLocation(program, "uPMatrix");
WebGLUniformLocation mvMatrixUniform =
ctx3d.getUniformLocation(program, "uMVMatrix");
WebGLUniformLocation nmMatrixUniform =
ctx3d.getUniformLocation(program, "uNormalMatrix");
WebGLUniformLocation colorUniform =
ctx3d.getUniformLocation(program, "uColor");
WebGLUniformLocation ambientColorUniform =
ctx3d.getUniformLocation(program, "uAmbientLight");
WebGLUniformLocation lightColorUniform =
ctx3d.getUniformLocation(program, "uLightColor");
WebGLUniformLocation lightDirectionUniform =
ctx3d.getUniformLocation(program, "uLightDirection");
E andranno catturati i riferimenti agli attributi:
int vertexPositionAttribute =
ctx3d.getAttribLocation(program, "aVertexPosition");
int vertexNormalAttribute =
ctx3d.getAttribLocation(program, "aVertexNormal");
Quindi vanno preparati i buffers:
WebGLCube cube = new WebGLCube();
WebGLBuffer verticesPositionsBuffer = ctx3d.createBuffer();
ctx3d.bindBuffer(
WebGLRenderingContext.ARRAY_BUFFER,
verticesPositionsBuffer);
ctx3d.bufferData(WebGLRenderingContext.ARRAY_BUFFER,
createFloat32Array(cube.getVerticesArray()),
WebGLRenderingContext.STATIC_DRAW);
ctx3d.vertexAttribPointer(vertexPositionAttribute, 3,
WebGLRenderingContext.FLOAT, false, 0, 0);
ctx3d.enableVertexAttribArray(vertexPositionAttribute);
//nota: le operazioni sui buffer sono riferite al buffer
//che è in binding al momento
WebGLBuffer verticesNormalsBuffer = ctx3d.createBuffer();
ctx3d.bindBuffer(WebGLRenderingContext.ARRAY_BUFFER,
verticesNormalsBuffer);
ctx3d.bufferData(WebGLRenderingContext.ARRAY_BUFFER,
createFloat32Array(cube.getNormalsArray()),
WebGLRenderingContext.STATIC_DRAW);
ctx3d.vertexAttribPointer(vertexNormalAttribute, 3,
WebGLRenderingContext.FLOAT, false, 0, 0);
ctx3d.enableVertexAttribArray(vertexNormalAttribute);
WebGLBuffer indexesBuffer = ctx3d.createBuffer();
ctx3d.bindBuffer(WebGLRenderingContext.ELEMENT_ARRAY_BUFFER,
indexesBuffer);
ctx3d.bufferData(WebGLRenderingContext.ELEMENT_ARRAY_BUFFER,
createUint16Array(cube.getIndexesArray()),
WebGLRenderingContext.STATIC_DRAW);
//non si tratta di un ARRAY_BUFFER attributo, quindi non serve
//il materiale concernente l'attributo
int numIndicies = cube.getNumIndices();
Nel codice sopra riportato abbiamo utilizzato l'oggetto WebGLCube che contiene la descrizione "geometrica" dell'oggetto da visualizzare:
public class WebGLCube {
private static final double[] vertices = {
// Front face
-1.0, -1.0, 2.0,
... tutti i vertici
};
private static final double[] normals = {
// Front face
0.0, 0.0, 1.0,
... tutte le normali ai vertici
};
private static final int[] triangles = {
0, 1, 2, 0, 2, 3,
// Front face
... gli indici, nell'array vertices, dei vertici
che compongono ogni singola faccia,
descritta come l'unione di 2 triangoli
};
E abbiamo anche inserito qualche metodo comodo per scrivere il nostro codice:
private static native
JsArrayOfNumber fromDoubleArray(double[] a) /*-{
return a;
}-*/;
private static native
JsArrayOfInt fromIntArray(int[] a) /*-{
return a;
}-*/;
public JsArrayOfNumber getVerticesArray() {
return fromDoubleArray(vertices);
}
public JsArrayOfInt getIndexesArray() {
return fromIntArray(triangles);
}
public int getNumIndices() {
return triangles.length;
}
public JsArrayOfNumber getNormalsArray() {
return fromDoubleArray(normals);
}
}
A questo punto possiamo assegnare valori agli uniforms e agli attributi:
ctx3d.uniformMatrix4fv(pMatrixUniform, false,
createArrayOfFloat32(perspectiveMatrix));
ctx3d.uniformMatrix4fv(mvMatrixUniform, false,
createArrayOfFloat32(modelViewMatrix));
ctx3d.uniformMatrix4fv(nmMatrixUniform, false,
createArrayOfFloat32(normalTransformMatrix);
ctx3d.uniform4f(colorUniform, .8f,0f,0f,1f);
ctx3d.uniform3f(ambientColorUniform, .2f, .2f, .2f);
ctx3d.uniform3f(lightColorUniform, 0.8f, 0.8f, 0.8f);
ctx3d.uniform3f(lightDirectionUniform, 0.0f,
1.0f/(float)Math.sqrt(2.0),
1.0f/(float)Math.sqrt(2.0));
E adesso possiamo disegnare :
// il buffer è stato lasciato precedentemente in binding
// per cui non ce ne sarebbe assoluta necessità:
// ma lasciamo queste righe di codice come promemoria
// ctx3d.bindBuffer(
// WebGLRenderingContext.ELEMENT_ARRAY_BUFFER, indexesBuffer);
ctx3d.clear(WebGLRenderingContext.COLOR_BUFFER_BIT |
WebGLRenderingContext.DEPTH_BUFFER_BIT);
ctx3d.drawElements(WebGLRenderingContext.TRIANGLES, numIndicies,
WebGLRenderingContext.UNSIGNED_SHORT, 0);

Figura 2 - Il risultato del nostro lavoro: seguite il link riportato poco sotto, in "Conclusioni" per vedere l'effetto tridimensionale nel vostro browser.
Conclusioni
Come avete visto, è stato necessario operare alcune scelte e realizzare un codice forse un po' macchinoso, ma alla fin fine non complesso e facilmente estendibile. Oltrettutto, questo è codice performante, dato che tutte le operazioni "computazionalmente intensive" vengono eseguite direttamente dall'hardware grafico.
Al link seguente potete visualizzare il risultato.
http://www.jooink.com/experiments/Elemental3DSample/
Nel codice sopra riportato abbiamo omesso le definizione delle matrici di proiezione e trasformazione in quanto laboriose e poco significative se non dopo una lettura di [2]. Esse sono comunque riportate nel codice di esempio allegato (menu in alto a destra).
Riferimenti
[0] Le ultime specifiche di WebGL
https://www.khronos.org/registry/webgl/specs/1.0/
[1] WebGL Fundamentals
http://games.greggman.com/game/webgl-fundamentals/
[2] La voce di Wikipedia sulle proiezioni tridimensionali
http://en.wikipedia.org/wiki/3D_projection
[3] La voce di Wikipedia sull'ombreggiatura nel 3D
http://en.wikipedia.org/wiki/Shading
[4] GWT - Developer's Guide: Client Bundle
https://developers.google.com/web-toolkit/doc/latest/DevGuideClientBundle
[5] GLSL Sandbox: esempi di shaders con codice
Condividi
- Un'applicazione di realtà aumentata con GWT
I parte: WebRTC e la libreria Elemental - Un'applicazione di realtà aumentata con GWT
II parte: Introduciamo WebGL - Un'applicazione di realtà aumentata con GWT
IV parte: Utilizziamo NyARToolKit - Un'applicazione di realtà aumentata con GWT
V parte: Creare l'applicazione HTML5/JavaScript
- AJAX e framework
Sviluppare Applicazioni AJAX con il framework Java Google Web Toolkit - Il Web 2.0
III parte: AJAX - Processing e visualizzazione
II parte: Le primitive grafiche - La programmazione RIA con GWT/GXT
III parte: Semplificare lo sviluppo di applicazioni complesse grazie al MVC - Spring Roo: un RAD per Java EE
I parte: Introduzione a Spring Roo