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 4×4 (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