Francesca Tosi, Alberto Mancini

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.
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.ELEMENTARRAYBUFFER,

indexesBuffer);

ctx3d.bufferData(WebGLRenderingContext.ELEMENTARRAYBUFFER,

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.ELEMENTARRAYBUFFER, indexesBuffer);

ctx3d.clear(WebGLRenderingContext.COLORBUFFERBIT \|

WebGLRenderingContext.DEPTHBUFFERBIT);

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.
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.

<span class="mark"><u>http://www.jooink.com/experiments/Elemental3DSample/</u></span>

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/>

WebGL Fundamentals

  • \
  • [1\] <http://games.greggman.com/game/webgl-fundamentals/>)

La voce di Wikipedia sulle proiezioni tridimensionali

  • \
  • [2\] <http://en.wikipedia.org/wiki/3D_projection>

La voce di Wikipedia sull’ombreggiatura nel 3D

  • \
  • [3\] <http://en.wikipedia.org/wiki/Shading>

GWT - Developer's Guide: Client Bundle

  • \
  • [4\] <https://developers.google.com/web-toolkit/doc/latest/DevGuideClientBundle>

GLSL Sandbox: esempi di shaders con codice

  • \
  • [5\] <http://glsl.heroku.com/>