…ammettetelo…quasi non ci credete…ebbene sì…siamo ritornati a proporvi i nuovi appuntamenti del corso sulla tecnologia che cambierà il web come oggi lo conosciamo per portarlo sulla 3° dimensione…ritorna il corso su WebGL!

Nell’ultimo incontro ci eravamo occupati della preparazione dell’ambiente che ospiterà il nostro spazio 3D, realizzando una semplice pagina web, che per mezzo del Tag <canvas> di HTML 5 ed qualche funzione javascript ci consentirà di disegnare in questa lezione il nostro primo triangolo ed il nostro primo quadrato!

“Tutto qui ?” direte voi…beh, bisogna partire dal basso per poter realizzare strutture complesse, e come abbiamo detto nelle lezioni precedenti il triangolo è la figura geometrica che sta alla base delle mesh poligonali.

Ma procediamo subito…

La prima integrazione che andremo a fare al codice presentato nella precedente lezione riguarda la funzione avvioWebGL().

Le integrazioni consentono di richiamare le due funzioni di inizializzazione dei Buffers e degli Shaders (che descriveremo dettagliatamente tra poco), e di eseguire il disegno delle figure geometriche:

//Funzione richiamata al caricamento del body che consente<br />    function avvioWebGL() <br />    {<br />      //Creo un oggetto con il canvas prelevato dal DOM della pagina<br />      canvas = document.getElementById("glcanvas");<br />      //Richiamo la funzione per l’inizializzazione di WebGL<br />      inizializzoWebGL(canvas);      <br />     <br />      //########### ECCO QUI IL CODICE DI QUESTA LEZIONE ######################<br />      //Inizializzazione dei Buffers<br />      inizializzaBuffers();<br />      //Inizializzazione degli Shaders<br />      inizializzaShaders();<br />      //#################################  <br />    <br />      //Se WebGL è supportato e quindi è stato creato il contesto adeguato nel canvas<br />      if (gl) {<br />        // Setta il colore neutro per il buffer del colore<br />        gl.clearColor(0.0, 0.0, 0.0, 1.0); <br />        // Abilita il test della profondita<br />        gl.enable(gl.DEPTH_TEST);  <br />    <br />        //########### ECCO QUI IL CODICE DI QUESTA LEZIONE ######################<br />        disegnaScena();<br />        // E’ stata rimossa la riga:<br />        //alert("WebGL inizializzato. Un quadrato nero che al prossimo appuntamento si riempirà."); <br />        //#################################<br />      }<br />    }<br />

Analiziamo le integrazioni e le funzioni relative…

inizializzaBuffers()

La funzione inizializzaBuffers() consente di creare e popolare i Buffer dove saranno memorizzate l’elenco delle coordinate dei punti che comporranno il nostro Triangolo (per mezzo della variabile globale triangleVertexPositionBuffer) ed il nostro Quadrato (per mezzo della variabile globale squareVertexPositionBuffer).

Ma diamo un’occhiata al codice:

//Variabili Globali dei Buffers<br />    var triangleVertexPositionBuffer;<br />    var squareVertexPositionBuffer;<br />    <br />    //Funzione di inizializzazione dei buffers del triangolo e del quadrato<br />    function inizializzaBuffers()<br />    {<br />      //Creo per mezzo del Context il buffer<br />      triangleVertexPositionBuffer = gl.createBuffer();<br />      //Indico di quale tipologia è il buffer <br />      gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);<br />      //Array dei punti del Triangolo<br />      var vertices = [<br />      0.0, 1.0, 0.0,<br />      -1.0, -1.0, 0.0,<br />      1.0, -1.0, 0.0<br />      ];<br />      //Passo l’array appena creato come dati del triangolo<br />      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);<br />      //Indico il numero di vertici del triangolo e di quanti elementi è composto ogni singolo vertice <br />      triangleVertexPositionBuffer.itemSize = 3;<br />      triangleVertexPositionBuffer.numItems = 3;<br />    <br />      //Creo per mezzo del Context il buffer<br />      squareVertexPositionBuffer = gl.createBuffer();<br />      //Indico di quala tipologia è il buffer<br />      gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);<br />      //Array dei punti del Quadrato<br />      vertices = [<br />      1.0, 1.0, 0.0,<br />      -1.0, 1.0, 0.0,<br />      1.0, -1.0, 0.0,<br />      -1.0, -1.0, 0.0<br />      ];<br />      //Passo l’array appena creato come dati del quadrato<br />      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);<br />      //Indico il numero di vertici del quadrato e di quanti elementi è composto ogni singolo vertice <br />      squareVertexPositionBuffer.itemSize = 3;<br />      squareVertexPositionBuffer.numItems = 4;<br />    } 

La prima parte della funzione si occupa di creare il buffer per il triangolo, mentre la seconda per il quadrato.

La prima operazione effettuata è la creazione del buffer per mezzo della funzione createBuffer del context (creato nello scorso appuntamento).

Successivamente si definisce il buffer attivo al quale verranno collegati tutti i successivi comandi che verranno eseguiti (fino ad un nuovo binding) e si imposta la tipologia del buffer a “tipo array” per mezzo del comando bindbuffer (specificando il parametro ARRAY_BUFFER).

Dopo aver creato un array con i valori delle coordinate (vertices) popoliamo definitivamente il buffer appena “bindato” per mezzo della funzione bufferData indicando la tipologia del buffer  (ARRAY_BUFFER), la lista dei vertici (passati come un oggetto di tipo Float32Array che vedremo in futuro), e che il buffer non cambierà durante le operazione successive (per mezzo del parametro STATIC_DRAW).

Infine definiamo come sono rappresentati i vertici nell’array specificando che sono 3 vertici (per mezzo della proprietà numItems) e che ogni vertice è composto da tre elementi (per mezzo della proprietà itemSize).

 

inizializzaShaders()

La funzione inizializzaShaders consente di inizializzare gli Shaders che abbiamo descritto accuratamente nell’appuntamento Pipeline di Rendering.

Ecco il codice:

var shaderProgram;<br />    function inizializzaShaders() {<br />      //Recupero i due shaders<br />      var fragmentShader = getShader(gl, "shader-fs");<br />      var vertexShader = getShader(gl, "shader-vs");<br />      //Creo il program e collego gli shaders<br />      shaderProgram = gl.createProgram();<br />      gl.attachShader(shaderProgram, vertexShader);<br />      gl.attachShader(shaderProgram, fragmentShader);<br />      //Link tra program e context e successivo controllo di inizializzazione<br />      gl.linkProgram(shaderProgram);<br />      if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) <br />      {<br />         alert("Could not initialise shaders");<br />      }<br />      //Rendo attivo l’uso del program<br />      gl.useProgram(shaderProgram);<br />      <br />      //Associazione delle variabili del vertex shader per la “comunicazione” con lo shader stesso<br />      shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");<br />      gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);<br />      shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");<br />      shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");<br />    }

Come prima cosa viene utilizzata la funzione getShader (funzione che non descriveremo in questa lezione data la semplicità della funzione stessa) che consente di recuperare gli script relativi agli Shader che sono definiti nell'Header della pagina web per mezzo di script di tipo:

<script id=”nomeshader” type="x-shader/x-fragment"> nel caso di Fragment Shaders;

<script id=”nomeshader” type="x-shader/x-vertex"> nel caso di Vertex Shaders;

Dopo aver recuperato gli shaders viene creato un oggetto di tipo Program per mezzo del metodo createProgram dell’oggetto context, che consente di eseguire gli shaders facendo da bridge sulla scheda grafica dopo aver effettuato il collegamento degli shaders al program per mezzo del metodo attachShader.

Appena dopo è necessario linkare il program al context webgl (con il metodo linkProgram), verificare la corretta inizializzazione degli shaders (con il metodo getProgramParameter e passando come parametri il program ed il parametro LINK_STATUS) ed infine segnalare al context quale program utilizzare (o meglio rendere attivo) con il metodo useProgram.

Successivamente creiamo un attributo chiamato vertexPositionAttribute (date le possibilità offerte da javascript!) per l’oggetto program in modo da avere accesso più facilmente alle posizioni degli vertici in fase di disegno nella funzione disegnaScena(), ed indichiamo che vorremmo fornire i valori per l’attributo utilizzando una matrice grazie al metodo enableVertexAttribArray (che approfondiremo in seguito).

Infine andiamo a creare due nuovi attributi per l’oggetto program per avere accesso più facilmente alle informazioni per le Variabili Uniformi (che approfondiremo al momento opportuno).

 

Ma vediamo un attimo come sono strutturati i due Shader che abbiamo fino ad ora inizializzato sull’oggetto program.

E’ necessario specificare che gli shader sono scritti in un linguaggio speciale chiamato GLSL (linguaggio derivato dal C).

Eccovi il codice:

<script id="shader-fs" type="x-shader/x-fragment"><br />      #ifdef GL_ES<br />      precision highp float;<br />      #endif<br />    <br />      void main(void) {<br />        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);<br />      }<br />    </script><br />    <br />    <script id="shader-vs" type="x-shader/x-vertex"><br />      attribute vec3 aVertexPosition;<br />    <br />      uniform mat4 uMVMatrix;<br />      uniform mat4 uPMatrix;<br />    <br />      void main(void) {<br />        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);<br />      }<br />    </script>
 

Il Fragment Shader non fa altro (per mezzo di un costrutto obbligatorio) che specificare alla scheda grafica che il tipo di dato voluto è un numero floating-point, e successivamente indichiamo che il colore utilizzato per disegnare è il bianco (tutti gli approfondimenti del caso li rinviamo al momento in cui utilizzeremo i colori!).

Il Vertex Shader invece ci consente di far lavorare la scheda grafica direttamente sui vertici. Qui ritroviamo le due Variabili Uniformi di cui abbiamo parlato (e “collegato”) sopra, che ci consentono di accedere allo Shader dall’esterno (come abbiamo fatto da inizializzaShaders() in cui abbiamo associato i valori delle matrici del model-view e della proiezione), e troviamo anche l’attributo aVertexPosition con il quale si passano i valori del vertice dal codice javascript al codice dello Shader che eseguirà l’operazione per ogni vertice della geometria. Difatti l’operazione svolta nella routine main è la determinazione della posizione del vertice che viene ottenuta (e quindi ritornata all’esterno dello shader) dalla moltiplicazione tra la matrice di Proiezione, la matrice Model-View ed il vertice stesso come abbiamo già detto in passato.

function disegnaScena() <br />    {<br />      //Impostazione del viewport<br />      gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);<br />      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);<br />      //Impostazione della prospettiva<br />      mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);<br />      //Matrice Identità<br />      mat4.identity(mvMatrix);<br />      //Translazione all’interno dello spazio 3d<br />      mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);<br />      //Binding del buffer con i vertici del triangolo<br />      gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);<br />      //Posizione dei vertici (passando dallo shader)<br />      gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);<br />      //<br />      setMatrixUniforms();<br />      //Disegno del triangolo<br />      gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);<br />      mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);<br />      gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);<br />      gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);<br />      setMatrixUniforms();<br />      gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);<br />    } 

Prima di tutto viene eseguito il setting delle dimensioni della scena con le stesse dimensioni del <canvas> per mezzo del metodo viewport passando come parametri le dimensioni memorizzate precedentemente nell’oggetto context (più avanti con gli appuntamenti capiremo che è possibile impostare altri tipi di dimensioni della scena…ma ogni cosa al suo tempo!).

Appena dopo verrà ripulito il viewport in previsione del disegno con il metodo clear (passando come parametri COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT).

Successivamente impostiamo la prospettiva con cui si desidera visualizzare la scena.

Di default WebGL utilizza una proiezione di tipo ortogonale (spiegata nello specifico incontro), mentre noi vogliamo utilizzare una proiezione che ci consenta di osservare gli oggetti più distanti proporzionalmente più piccoli di quelli vicini alla camera, ed inoltre vogliamo impostare un margine di visibilità degli oggetti.

Con il metodo prespective della classe mv4  (fornitoci dalla libreria glMatrix, dato che WebGL non gestisce tutto ciò che riguarda il calcolo matriciale e le trasformazioni ad esso correlate) stiamo impostando che il nostro campo visivo è di 45 ° in rapporto alla larghezza e altezza della nostra tela. Inoltre non vogliamo vedere le cose che sono ad una distanza inferiore a 0,1 unità al nostro punto di vista o maggiore di 100 unità.

 

A questo punto sarà necessario spostarsi nei punti della scena dove poter disegnare le nostre geometrie.

Abbiamo detto in precedenza che le matrici possono rappresentare le translazioni, le rotazioni e altre trasformazioni geometriche  per rappresentare qualsiasi trasformazione nello spazio 3D.

Si inizia con la matrice di identità – che è la matrice che rappresenta una trasformazione “che non fa niente” e poi si moltiplica per la matrice che rappresenta la prima trasformazione, poi da quello che rappresenta la vostra seconda trasformazione, e così via.

Il metodo che ci ritorna la matrice di identità è identity della classe mat4 (fornitoci dalla libreria glMatrix, dato che WebGL non gestisce tutto ciò che riguarda il calcolo matriciale e le trasformazioni ad esso correlate).

Successivamente effettuiamo la prima translazione con il metodo translate e muoviamoci rispettivamente sugli assi x, y e z di -1.5, 0 e -7 unità.

 

Finalmente possiamo disegnare il primo triangolo!

Selezioniamo prima il buffer sul quale lavorare con il metodo bindBuffer, indicando come secondo parametro il buffer contenente i vertici del triangolo. 

L’istruzione dopo ci consente di dire a WebGL che utilizzeremo come posizione dei vertici l’attributo creato nella funzione inizializzaShader, e che il formato dei vertici è quello impostato sull’attributo itemSize del buffer.

Dopo aver reso “uniformi” le matrici Model-View e Prospettiva dell’oggetto Shader Program (per mezzo della funzione setMatrixUniforms), possiamo finalmente concludere con il disegno del triangolo tramite la riga:

 

gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

 

in cui chiediamo al context di disegnare come triangoli la serie di vertici del buffer bindato in precedenza e valutando che ogni vertice ha 3 elementi.

Il processo risulta identico per il quadrato tranne che per la riga di disegno nella quale è impostato come parametro TRIANGLE_STRIP che comunica a WebGL di disegnare in questo caso il primo triangolo, e poi utilizzare gli ultimi due punti del primo triangolo ed il primo punto del prossimo per disegnarne il successivo, e cosi via…

 

Abbiamo cosi finito questo incontro che si pone come il vero punto di inizio…e spero che finalmente siate soddisfatti di aver disegnato qualcosa sulla nostra tela…

non è molto lo sappiamo, ma come ben vedete i concetti sono particolari e complessi, ed è quindi giusto che si proceda con ordine … e soprattutto è necessario prendere questo breve incontro come un punto dal quale partire per approfondire i vari metodi utilizzati per capire bene “come funziona” il meccanismo… ma intanto scaricate da qui il codice di questo appuntamento.

E' possibile che vi troviate ancora disorientati ma la prossima volta aggiungeremo i colori alle nostre figure geometriche e faremo ancora più chiarezza sugli Shaders e su GLSL.

 

Ringraziamo come le altre volte le fonti validissime come Learning WebGL…

Alla prossima! 

 

The following two tabs change content below.

Francesco Sciuti

Freelance a Vroom Agency
Amante dello sviluppo web, della grafica 3d e della buona musica (che non guasta mai!), 'web developpa' tutto il giorno...e prova a trovare sempre il bandolo della matassa in questo allegro ma sconfinato mondo.

//life motto
if(sad() === true) {
    sad().stop();
    beAwesome();
}