WebGL per HTML5

[Lezione 2] - Componenti del WebGL

15/02/2017


Questa lezione racchiude in se una serie di componenti fondamentali del WebGL.

TIPI PRIMITIVI

In WebGL ogni poligono complesso è composto da una serie di triangoli, linee e punti. Questi prendono il nome di tipi primitivi ed ogni elemento viene processato in parallelo dalla GPU del nostro computer attraverso una serie di step che prende il nome di pipeline di rendering.

Esistono tre tipi primitivi: punti, linee e triangoli. Questi possono essere utilizzati in sette modi differenti:
1) POINTS: non sono altro che coordinate spaziali x, y e z (o vertici) e vengono renderizzati come singoli punti nello spazio;
2) LINES: sono rappresentate da una coppia di vertici (o punti);
3) LINE_STRIP: sono una serie di linee collegate tra loro dove il vertice finale di una linea rappresenta il vertice di inizio della linea successiva;
4) LINE_LOOP: funziona come LINE_STRIP solamente che l'ultima linea va a chiudere la figura geometrica collegando l'ultimo vertice con il primo. Per esempio, con quattro vertici si può rappresentare un quadrato;
5) TRIANGLES: rappresentato da una o più triplette di punti nello spazio o vertici che, collegandosi tra di loro, formano uno o più triangoli;
6) TRIANGLE_STRIP: utilizza gli ultimi due vertici del primo triangolo come vertici del triangolo successivo. Per esempio, 5 vertici creano tre triangoli collegati tra di loro;
7) TRIANGLE_FAN: simile a TRIANGLE_STRIP ma con la differenza che tutta la figura geometrica formata dai triangoli si chiude in se stessa facendo combaciare il primo vertice come punto comune a tutti i triangoli;




SHADERS

In WebGL è possibile settare direttamente il posizionamento ed il colore del vertice all'interno della nostra scena che verrà rappresentata nel canvas. Questo è reso possibile grazie alla presenza di due shaders (uno shader è rappresentato da un set di istruzioni che spiegano alla GPU [Graphics Processing Unit] come renderizzare la figura geometrica) che vengono eseguiti direttamente sulla GPU del nostro computer lasciando la nostra CPU libera per altre mansioni.

Vediamo quali sono questi due shaders:
Vertex Shader (shader-vs): ha il compito di restituire al sistema, per ogni tripletta di coordinate (x, y, z) che identificano un vertice, una coppia di coordinate che identifichino lo stesso vertice sul piano bidimensionale del canvas. Tutto ciò è realizzato attraverso la valorizzazione di gl_Position (vettore di 4 valori di tipo float - 4D float vector);
Fragment Shader (shader-fs): ha il compito, sulla base di quanto restituito dal Vertex Shader, di assegnare il colore ad ogni vertice che verrà posizionato sempre sul canvas, le texture e la luce attraverso la valorizzazione di gl_FragColor (vettore di 4 valori di tipo float - 4D float vector).

In base a quanto descritto risulta chiaro che i due shaders saranno richiamati per ogni vertice da noi definito.
Gli shaders vengono definiti nel WebGL, come vedremo in seguito, attraverso il linguaggio GLSL (derivato dal linguaggio C) studiato appositamente per questo compito.

Vediamo quindi come definirli all'interno della nostra pagina web:

<!doctype html>
<html>
    <head>
        <title>webgl</title>
        <style>
            body{ background-color: white; }
            canvas{ background-color: black; }
        </style>
        <script id="shader-vs" type="x-shader/x-vertex">
                attribute vec3 aVertexPosition; // posizione (x,y,z) del vertice
                void main(void) {
                    gl_Position = vec4(aVertexPosition, 1.0);
                }
        </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>
        ...
        ...
        ...
    </head>
    <body onLoad="initWebGL()" >
        <canvas id="my-canvas" width="600" height="400">
             Il tuo browser non supporta l'elemento canvas di HTML5
        </canvas>
    </body>
</html>


BUFFERS, VBO, IBO e struttura principale

Il WebGL ha diversi tipi di buffers (blocchi di memoria dove le informazioni vengono scritte e lette) che assolvono diversi compiti:
Color buffer: utilizzato per conservare le informazioni riguardanti i colori (valori red, green, blue) e, in alcuni casi, quello che viene chiamato valore alpha che determina la trasparenza/opacità;
Depth buffer: utilizzato per conservare le informazioni riguardanti la componente di profondità (asse z);
Stencil buffer: utilizzato per delineare l'area di renderizzazione (visualizzazione a video) del disegno tridimensionale che andremo a creare.

Il depth buffer e lo stencil buffer lavorano insieme per eliminare dall'elaborazione tutte le immagini appartenenti al nostro mondo 3D che risultano nascoste da altri oggetti o non visibili in quanto non attualmente inquadrati all'interno del nostro canvas. Per default non tutti i buffer appena descritti risultano abilitati, infatti lo stencil buffer per default risulta essere disabilitato.

Per passare i dati ai buffer vengono creati i Vertex Buffer Objects (VBO) che conterranno nel loro interno dati come la posizione, il colore, le coordinate delle texture ed altre informazioni (vedremo in seguito quali) per essere poi inviati agli shaders e quindi alla GPU del nostro computer. Per realizzare questa operazione faremo uso di una semplice array javascript, le quali saranno valorizzate con i valori delle triplette (x,y,z) che rappresentano i nostri vertici.

Verranno in seguito creati anche gli Index Buffer Objects (IBO) che conterranno nel loro interno label numeriche corrispondenti ai vertici passati nel VBO. Queste saranno utili per la connessione dei vertici in modo da creare una superfice.

L'esecuzione dello script che andremo a scrivere di seguito viene distinto in due fasi che chiameremo:
- inizializzazione o SETUP: questa fase viene eseguita una sola volta e comprende l'inizializzazione degli shader (initShaders()) e la creazione dei buffer (setupBuffers()) necessari ad accogliere i dati (coordinate x,y e z dei vertici);
- disegno o DRAWING: questa fase viene invece eseguita continuamente (ciclicamente o in loop) ma solamente dopo aver eseguito la fase precedente (setupWebGL() e drawScene()) ed effettua il refresh continuo della scena in modo da gestire il movimento degli oggetti, la posizione della camera (o punto di vista) e le luci.

Nell'esempio seguente disattiveremo la gestione dell'esecuzione continua (//function animLoop()) in quanto non viene attualmente gestito nessun movimento. La scena sarà quindi disegnata una sola volta.
<!doctype html>
<html>
    <head>
        <title>webgl</title>
        <style>
            body{ background-color: gray; }
            canvas{ background-color: black; }
            .parent{ width:100%; }
            .child{ width: 50%; margin: 0 auto; }
        </style>
        <script id="shader-vs" type="x-shader/x-vertex">
                attribute vec3 aVertexPosition; // posizione (x,y,z) del vertice
                void main(void) {
                    //gl_PointSize = 3.0;
                    gl_Position = vec4(aVertexPosition, 1.0);
                }
        </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>
            var gl = null,
                canvas = null,
                glProgram = null,
                fragmentShader = null,
                vertexShader = null;
        
            var vertexPositionAttribute = null,
                trianglesVerticeBuffer = null;

            function initWebGL()
            {
                canvas = document.getElementById("my-canvas");
                try{
                    gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
                } catch(e) {
                }
			
                if(gl)
                {
		    initShaders();
		    setupBuffers();
		    //(function animLoop(){
                        setupWebGL();
                        drawScene();
                    //})();
                }else{
		    alert("ERRORE: Il tuo Browser non supporta WebGL");
	        }
            }

            function initShaders()
            {
                var fs_source = document.getElementById('shader-fs').innerHTML,
                    vs_source = document.getElementById('shader-vs').innerHTML;
                
                vertexShader = makeShader(vs_source, gl.VERTEX_SHADER);
                fragmentShader = makeShader(fs_source, gl.FRAGMENT_SHADER);

                glProgram = gl.createProgram();

                gl.attachShader(glProgram, vertexShader);
                gl.attachShader(glProgram, fragmentShader);

                gl.linkProgram(glProgram);

                if (!gl.getProgramParameter(glProgram, gl.LINK_STATUS)) {
                    alert("Impossibile inizializzare lo shader program.");
                }

                gl.useProgram(glProgram);
            }

            function makeShader(src, type)
            {
                 //compile the vertex shader
                 var shader = gl.createShader(type);
                 gl.shaderSource(shader, src);
                 gl.compileShader(shader);
                 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                     alert("Errore di compilazione dello shader: " + gl.getShaderInfoLog(shader));
                 }
                 return shader;
            }

            function setupBuffers()
            {
                //VERTICI
                var triangleVertices = [
                    //triangolo sinistro
                    -0.5, 0.5, 0.0, //vertice 0
                     0.0, 0.0, 0.0, //vertice 1
                    -0.5, -0.5, 0.0, //vertice 2
                    //triangolo destro
                    0.5, 0.5, 0.0, //vertice 3
                    0.0, 0.5, 0.0, //vertice 4
                    0.5, -0.2, 0.0 //vertice 5
                ];
                trianglesVerticeBuffer = gl.createBuffer();
                gl.bindBuffer(gl.ARRAY_BUFFER, trianglesVerticeBuffer);
                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangleVertices), gl.STATIC_DRAW);
            }

            function setupWebGL()
            {
                gl.clearColor(0.0, 0.0, 0.0, 1.0);
                gl.clear(gl.COLOR_BUFFER_BIT);
            }

            function drawScene()
            {
                //VERTICI
                vertexPositionAttribute = gl.getAttribLocation(glProgram, "aVertexPosition");
                gl.bindBuffer(gl.ARRAY_BUFFER, trianglesVerticeBuffer);
                gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
                gl.enableVertexAttribArray(vertexPositionAttribute);

                gl.drawArrays(gl.TRIANGLES, 0, 6); //visualizza tutti e due i triangoli
                //gl.drawArrays(gl.TRIANGLES, 0, 3); //visualizza solo il primo triangolo
		//gl.drawArrays(gl.TRIANGLES, 3, 3); //visualizza solo il secondo triangolo
                //gl.drawArrays(gl.LINES, 0, 6);
                //gl.drawArrays(gl.POINTS, 0, 6);
            }
        </script>
    </head>
    <body onLoad="initWebGL()" >
        <div class="parent">
            <div class="child">
                <canvas id="my-canvas" width="600" height="400">
                    Il tuo browser non supporta l'elemento canvas di HTML5
                </canvas>
            </div>
        </div>
    </body>
</html>
Diamo ora una breve spiegazione di quanto scritto nel codice precedente.
La prima operazione che verrà eseguita, una volta che il nostro browser avrà caricato la pagina web, sarà quella di eseguire la funzione initWebGL() presente all'interno dell'evento onLoad del body HTML. Questa funzione contiene tutte le chiamate alle sub-funzioni che svolgeranno le operazioni di configurazione iniziale dell'ambiente ed esecuzione dell'intera animazione 3D.

Iniziamo quindi ad analizzare le sub-funzioni che verranno chiamate in sequenza. La prima funzione chiamata, dopo aver creato una variabile canvas che conterrà il riferimento al canvas HTML e creato il contesto attraverso la variabile gl, sarà initShaders(). Questa funzione ha il compito di creare gli shaders VERTEX e FRAGMENT, collegare il sorgente agli shaders attraverso l'istruzione document.getElementById('shader-XX').innerHTML, compilarli, creare un programma e infine linkare gli shaders al programma sempre facendo riferimento alla variabile gl.

La funzione successiva sarà setupBuffers(). In questa verranno inizialmente definiti, con una array javascript, 6 vertici (x,y,z) che identificheranno i due triangoli che andremo a disegnare. Chiamando la funzione gl.createBuffer() creeremo un nuovo VBO, precedentemente descritto, e passeremo i vertici al VBO. La differenza tra VBO ed IBO viene specificata durante l'operazione di BIND del buffer con il parametro ARRAY_BUFFER per ottenere un VBO o ELEMENT_ARRAY_BUFFER per ottenere un IBO.
Mentre l'operazione di bindBuffer() prepara il buffer ad ospitare i dati, l'operazione di bufferData() esegue l'operazione di passaggio dei dati (vertici).

La funzione successiva sarà setupWebGL(). In questa funzione viene deciso il colore con il quale verrà inizializzato il contesto.

Infine sarà richiamata la funzione drawScene() nella quale viene prima di tutto associato il vettore aVertexPosition dello shader VERTEX alla variabile vertexPositionAttribute, attraverso la funzione gl.getAttribLocation, in modo da poter passare i valori posizionati nel buffer trianglesVerticeBuffer allo stesso shader VERTEX. Attraverso la funzione gl.vertexAttribPointer comunichiamo che ogni vertice sarà costituito da tre numeri (x, y, z). Dopodiché verrà chiamata la funzione drawArrays alla quale passeremo il TIPO PRIMITIVO che vogliamo disegnare, il vertice iniziale e il numero totale di vertici precedentemente passati.

Le righe commentate possono essere provate, una dopo l'altra, commentando la rispettiva chiamata alla funzione gl.drawArrays con l'elemento primitivo TRIANGLES. Per provare l'impostazione POINTS bisognerà togliere il commento all'istruzione gl_PointSize presente nel vertex shader in modo da regolare la dimensione dei punti.


< lezione precedente      lezione successiva >