L'idea di portare il 3D in tempo reale all'interno di una pagina Web non è nuova, anzi, fin dagli esordi del www abbiamo assistito ad un susseguirsi di implementazioni pionieristiche volte a perseguire proprio questo obiettivo: applet Java, VRML, la prima versione di O3D e molte altre.
Tra queste tecnologie le uniche in grado di superare lo stato sperimentale e di trovare collocazione all'interno di applicazioni di largo consumo sono state Flash e alcune librerie JavaScript molto elaborate. Anche in questi (rari) casi si è però quasi sempre trattato di apportare piccoli accorgimenti tridimensionali ad un'interfaccia progettata in modo strettamente bidimensionale.
Con l'arrivo delle nuove specifiche HTML5 ci sono però buone chance di approdare a breve ad uno standard condiviso per l'utilizzo di 3D realtime nel browser; una delle sub-specifiche previste all'interno di questa nuova versione del famoso linguaggio di markup è infatti specificatamente dedicata a questo compito e nasce dalla collaborazione dei principali player del settore come Apple, Google e Mozilla.
La tecnologia in questione si chiama WebGL e si basa sulla scelta del tag HTML <canvas> come base sulla quale costruire ed animare modelli tridimensionali usando delle API Javascript derivate dalle specifiche OPENGL ES 2.0.
Questo articolo si pone l'obiettivo di introdurre le WebGL a livello operativo e di illustrare una panoramica dei framework e degli strumenti di sviluppo ad oggi disponibili; per poter provare gli esempi e le demo che verranno presentate è necessario munirsi della versione 'sperimentale' di uno qualunque fra i più noti browser in circolazione; tuttavia suggerisco di utilizzare Chromium in quanto le diverse implementazioni di questa tecnologia differiscono ancora leggermente tra loro e gli esempi proposti sono stati costruiti usando questo browser.
Dettagli sulla procedura di installazione possono essere recuperati su questo sito.
Shaders e tutto il resto
Ci sono due aspetti che rendono ostico il mondo 3D per chi, come l'autore, possiede un background da sviluppatore web: la terminologia e la filosofia con la quale sono state scritte le API. Partiamo da quest'ultimo problema; come già accennato le WebGL nascono come implementazione JavaScript delle ben più note OPENGL ES 2.0, tali API sono di basso livello e funzionano intuitivamente come una sequenza di comandi utilizzati per pilotare le azioni del motore 3D. Un esempio di questo approccio è nel seguente listato WebGL che disegna sullo schermo un singolo triangolo:
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, ...);
gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, new Float32Array ...);
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, new Float32Array ...);
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
Al di là del significato delle istruzioni è interessante notare come il tutto si riassuma nel mandare messaggi all'oggetto gl
; tale istanza identifica un contesto WebGL costruito su di un <canvas>
specifico usando la sintassi:
gl = document.getElementById("a_canvas_id").getContext("experimental-webgl");
Passiamo ora alla terminologia: è chiaro che non è possibile elencare e definire l'intero glossario che interessa il mondo legato alla programmazione 3D realtime, però credo che sia importante evidenziare un paio concetti che torneranno utili a breve.
Shader
La prima parola chiave è shader: uno shader è rappresentato da un set di istruzioni che spiegano alla GPU come comportarsi durante la fase di rendering, cioè di visualizzazione a video, di una scena 3D. Le specifiche WebGL richiedono che lo sviluppatore provveda a fornire alla GPU due shaders chiamati rispettivamente Vertex Shader e Fragment Shader.
Il compito imperativo del Vertex Shader è quello di restituire, per ogni tripletta di coordinate che identificano un vertice nello spazio (se stessi disegnando un cubo il Vertex Shader verrebbe interpellato 8 volte), una coppia di coordinate che identifichino lo stesso vertice sul piano bidimensionale del <canvas>
. Questa operazione deve chiaramente tenere conto di quale sia il punto di vista e l'angolazione dalla quale si sta osservando la scena. A questo compito essenziale si aggiungono alcuni calcoli facoltativi, legati alla gestione delle luci ed al posizionamento delle textures.
Il compito del Fragment Shader è invece quello di definire il colore di ognuno dei pixel del <canvas>
sui quali giace la rappresentazione bidimensionale della scena che è stata calcolata con l'ausilio delle informazioni del Vertex Shader.
WebGL utilizza un dialetto del C chiamato GLSL per definire gli shaders. Questo linguaggio, appositamente studiato per questo compito, è basato su una serie di convenzioni e può essere facilmente integrato in una pagina web. Un semplicissimo Fragment Shader ad esempio ha questo aspetto:
<script id="shader-fs" type="x-shader/x-fragment">
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // vec4 è un vettore di 4 elementi RGBA
}
</script>
GLSL si aspetta nella variabile gl_FragColor
(1.0, 1.0, 1.0, 1.0)
gl_Position
Con questi concetti in mente possiamo accingerci a stendere il nostro primo script WebGL.
Hello Triangle!
Uno script WebGL si può dividere sommariamente in 2 fasi:
- la prima, che possiamo chiamare di setup
<canvas>
- la seconda fase, che invece possiamo chiamare di disegno
Nell'applicazione di prova che stiamo per creare mostreremo a video un singolo triangolo bianco; tale risultato non rientra esattamente nelle massima espressione possibile della grafica tridimensionale, ma è quanto basta per poter sperimentare tutti gli aspetti principali delle specifiche WebGL senza diventare troppo prolissi.
Iniziamo scrivendo un file index.html
(demo):
<html>
<head>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.4.3.min.js"></script>
<script type="text/javascript" src="hello_triangle.js"></script>
<script id="shader-fs" type="x-shader/x-fragment">
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition; // posizione (x,y,z) del vertice
// Imprime al vertice le modifiche dovute al movimento dell'oggetto sulla scena
uniform mat4 uMVMatrix;
// Imprime al vertice le modifiche dovute al punto di vista
uniform mat4 uPMatrix;
void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>
</head>
<body onload="webGLStart();">
<canvas id="mycanvas" style="border: none;" width="500" height="500"></canvas>
</body>
</html>
Già in questo piccolo file possiamo evidenziare un numero di elementi interessanti, come le definizioni dei due shaders:
shader-fs
- aVertexPosition
4x4
fase di disegno in base alla posizione del punto di vista e a quella dell'oggetto sulla scena.
Il metodo webGLStart()
hello_triangle.js
var gl;
var shaderProgram;
var triangleVertexPositionBuffer;
function webGLStart() {
var canvas = document.getElementById("mycanvas");
gl = canvas.getContext("experimental-webgl");
gl.viewportWidth = canvas.width;
gl.viewportHeight = canvas.height;
initShaders();
initBuffers();
gl.clearColor(0.0, 0.0, 0.0, 1.0); // imposto lo sfondo a nero
gl.clearDepth(1.0);
setInterval(drawScene, 15);
}
La funzione webGLStart()
initShaders
initBuffers
drawScene
15
function initShaders() {
shaderProgram = gl.createProgram();
$.each([[gl.FRAGMENT_SHADER,"#shader-fs"],
[gl.VERTEX_SHADER,"#shader-vs"]],
function (ind,val) {
var shader = gl.createShader(val[0]);
gl.shaderSource(shader, $(val[1]).text());
gl.compileShader(shader);
gl.attachShader(shaderProgram, shader);
});
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}
In questa porzione di codice vengono istanziati e compilati i due shaders all'interno di un contenitore preposto (shaderProgram
aVertexPosition
uPMatrix
uMVMatrix
function initBuffers() {
triangleVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
var vertices = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
triangleVertexPositionBuffer.itemSize = 3;
triangleVertexPositionBuffer.numItems = 3;
}
Qui vengono dichiarate le coordinate dei punti (x,y,z)
function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
gl.vertexAttribPointer(
shaderProgram.vertexPositionAttribute,
triangleVertexPositionBuffer.itemSize,
gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false,
new Float32Array([ 1,0,0,0,
0,1,0,0,
0,0,0,-1,
0,0,0,0]));
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false,
new Float32Array([ 1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,-2,0]));
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
}
La funzione drawScene
A questo punto la funzione drawArrays
gl
Visualizzando la pagina index.html

Troppo complicato! ...e se usassimo un framework?
Disegnare un triangolo bianco e statico è costato 60
righe di codice, escluso il file index.html
: decisamente troppo! È abbastanza impensabile riuscire a gestire la complessità di una mappa di Quake II con delle API di questo livello. È proprio per questo che, intorno alle WebGL, si stanno già affacciando diversi framework che promettono di migliorare sensibilmente l'impatto con questa tecnologia. Un elenco completo è disponibile sulla Wiki ufficiale.
Tra i vari disponibili spicca SceneJS per la sua filosofia di implementazione che consente di descrivere con una sintassi simil-JSON l'intera scena, comprensiva di luci, camere e modelli.
Anche X3DOM è interessante e sembra essere il miglior candidato per l'utilizzo di WebGL in applicazioni all-pourpose soprattutto in virtù del suo approccio che consente di descrivere la scena usando un linguaggio di markup.
Se invece l'obiettivo è la produzione di un videogioco allora la scelta dovrebbe focalizzarsi su CopperLicht che dispone anche di un editor per lo sviluppo delle mappe.
Conclusioni
È importante sapere cosa succede dietro le quinte quando si utilizza una tecnologia; il più delle volte conoscere i meccanismi che la governano aiuta a risolvere in fretta problemi che altrimenti sarebbero difficilmente sanabili. WebGL non fa differenza e può essere un peccato di superficialità utilizzare uno dei framework elencati, senza prima essersi acclimatati un po' con le API standard.
Detto questo le API WebGL si rivelano decisamente troppo complicate e laboriose per consentire il loro utilizzo direttamente all'interno di un progetto che sia più complesso di una demo. A questo si aggiunge una curva di apprendimento particolarmente ripida che configura quindi l'uso di un framework come scelta preferenziale, soprattutto per chi proviene da un ambiente di sviluppo Web e non ha esperienza con tecnologie di questo tipo.
Nonostante queste difficoltà non è improbabile che le WebGL divengano parte integrante dei siti e dei portali Web del futuro, trovando anche posto nel panorama dello sviluppo videoludico, magari in campo mobile, soprattutto in virtù della loro attinenza con le OPENGL ES2.0.