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
il colore col quale dovrà essere renderizzato a video il pixel in oggetto. Usando lo shader appena creato ogni nostro modello tridimensionale verrà visualizzato solamente usando il colore bianco (1.0, 1.0, 1.0, 1.0)
. Lo stesso identico approccio contraddistingue anche un Vertex Shader; la variabile da valorizzare in quel caso però si chiama 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, crea il contesto WebGL sul
<canvas>
scelto, inizializza gli shaders e crea i buffer necessari a contenere le informazioni sui modelli 3D della scena; - la seconda fase, che invece possiamo chiamare di disegno, ha il compito di effettuare continuamente il refresh della scena 3D gestendo in questo modo il movimento degli oggetti in essa contenuti, le luci e la posizione dell'inquadratura.
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
, che si comporta tingendo di bianco ogni pixel per il quale viene invocato, e shader-vs che invece decide dove posizionare il vertice;- aVertexPosition sulla base di due parametri di tipo matrice
4x4
che vengono settati durante la
fase di disegno in base alla posizione del punto di vista e a quella dell'oggetto sulla scena.
Il metodo webGLStart(), invocato al load della pagina, è contenuto all'interno del file hello_triangle.js
, che si fa' carico dell'intera logica della demo e che di seguito è stato diviso in sezioni per poter essere più facilmente illustrato:
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()
inizializza il corretto contesto sul canvas di riferimento e successivamente invoca le due funzioni della fase di setup (initShaders
e initBuffers
). Fatto questo imposta il colore di sfondo della scena 3D a nero e lancia la funzione drawScene
ogni 15
millisecondi.
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
) che poi viene agganciato al contesto 3D. Le ultime righe di questo listato servono a notificare le variabili che verranno valorizzate nello shader durante la fase di disegno: aVertexPosition
per il vertice corrente e uPMatrix
e uMVMatrix
per le matrici illustrate precedentemente.
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)
che compongono i vertici della figura che si intende disegnare; essendo un triangolo avremo quindi 9 valori da specificare: tre per ognuno dei suoi tre vertici (se avessimo voluto disegnare un cubo il numero di valori da inserire sarebbe cresciuto a 8 vertici * 3 = 24). Questi punti sono poi memorizzati all'interno di un buffer che verrà utilizzato durante la fase di disegno.
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 i occupa di gestire le operazioni necessarie per disegnare la scena sul canvas designato; nelle prime due righe del listato la scena viene resettata, poi il buffer contenente le informazioni a riguardo dei vertici del triangolo viene caricato nel contesto 3D e si provvede ad impostare i parametri richiesti dallo shader. In questa demo, per non eccedere nella lunghezza dell'esempio, le due matrici necessarie allo shader sono state gestite in modo statico attraverso due array, in applicazioni reali è molto comune modificare questi valori ad ogni iterazione di disegno, a fronte di variazioni del punto di vista o della posizione dell'oggetto sulla scena.
A questo punto la funzione drawArrays disegna i vertici presenti nel buffer invocando per ognuno di essi lo shader; è importante notare come l'oggetto gl
per eseguire questa funzione utilizzi le informazioni accumulate attraverso le precedenti.
Visualizzando la pagina index.html
in un browser abilitato alle estensioni WebGL, si dovrebbe poter ammirare un triangolo bianco su sfondo nero, a prova della corretta implementazione delle istruzioni WebGL.
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.