Un'applicazione di realtà aumentata con GWT

III parte: WebGL e oggetti 3Ddi e

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

http://glsl.heroku.com/

Condividi

Pubblicato nel numero
185 giugno 2013
Sviluppatore web e mobile, freelance, teacher, sysadmin. Attualmente lavoro come Java Developer e Architect per l'azienda di cui sono co-fondatore: K-TEQ Srls (http://www.k-teq.com) #Java #GWT #JavaScript #HTML5 #Flink #MachineLearning https://www.linkedin.com/in/abmancini/
Freelance, Web & Mobile Developer and Architect, with a passion for fine tuned details. Co-founder at K-TEQ Srls (http://www.k-teq.com). GDG-Firenze Lead and founder. Intel Software Innovator. #Java #GWT #StreamProcessing #MachineLearning https://www.linkedin.com/in/francescatosij/
Articoli nella stessa serie
Ti potrebbe interessare anche