Salve a tutti,
in questo articolo analizziamo la questione delle Texture.
Per dare infatti più realismo alla scena e contemporaneamente arricchirla, una pratica molto usata è quella di “incollare” sulle nostre superfici delle immagini, per esempio per realizzare la simulazione di un prato o di un cielo .
Nell’esempio che mostreremo infatti avremmo a che fare con il cubo rotante dell’esempio lesson04b dell’articolo precedente, ma stavolta, al posto della gestione dei colori , ci preoccuperemo della gestione delle texture, leggermente più complessa ma fondamentale per realizzare applicazioni graficamente appaganti.
Il nostro primo obiettivo sarà caricare l’immagine dentro una variabile globale, in modo tale da poterla richiamare nella fase di disegno. Ovviamente l’introduzione di questa funzionalità provocherà modifiche su molte delle funzioni della’applicaione: oltre che recuperare la nostra immagine infatti, dovremmo assegnare ad ogni vertice delle coordinate 2d (x;y) lungo la superficie della texture ( parallelamente anche quando gestiamo i pixel della nostra scena 2d dovremmo assegnare ad ogni porzione di spazio delle coordiante pixel della nosta texture). Il motivo di queste informazioni appare banale se ipotizziamo di dover tappezzare un poster sopra un quadretto magari più piccolo: dobbiamo decidere quale porzione della texture sarà attaccata alla superficie considerata. Altrettante informazioni da gestire saranno ad esempio la resa grafica della texture se ci allontaniamo dall’oggetto o viceversa.


Infine nella funzione di drawScene dovremmo richiamare il buffer delle texture al posto del buffer dei colori ( che sarebbe inutile )per poter disegnare.
Insomma, ogni porzione di codice in cui gestivamo i colori ( ormai obsoleto) è sostituito da uno che gestisce e texture.

Iniziamo dalla funzione Start:

function Start() {
var canvas = document.getElementById(“Mycanvas”);
initGL(canvas);
initShaders();
initBuffers();
initTexture();

setInterval(tick, 15);
}

Introduciamo ora la nuova funzioni initTexture:

var newTexture;
function initTexture() {
newTexture= gl.createTexture();
newTexture.image = new Image();
newTexture.image.onload = function() {
LoadedTexture(newTexture)
}

newTexture.image.src = “test.gif”;
}

test.gif:

Come possiamo vedere ,creiamo un riferimento alla Texture ( notiamo che usiamo una variabile globale a tutto il progetto, esattamente come con il buffer dei colori). Dopo gli assegniamo un attributo di tipo Immagine e la leghiamo alla nostra texture. Prima però di assegnare a questo attributo il percorso relativo della nostra immagine di test, agganciamo al caricamento dell’immagine una funzione javascript, che verrà richiamata solo quando l’attributo sarà caricato.

function LoadedTexture(texture) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.bindTexture(gl.TEXTURE_2D, null);
}

Primo obiettivo di questo codice è quella selezionare la texture passata alla funzione LoadedTexture come texture corrente.
La funzione gl.pixelStorei non è sempre presente: essa viene chiamata in caso di discrepanza tra il verso crescente delle nostre coordinate (x;y;z) e quelle delle texture ( dipende dal tipo dell’immagine comunque nel nostro esempio possiamo anche commentarla). Successivamente carichiamo l’immagine della nostra texture della scheda grafica con gl.texImage2D ;gl.texParameteri e’ una funzione assai importante, esso stabilisce “come vediamo” la nostra texture a seconda che siamo vicini( gl.TEXTURE_MAG_FILTER) o lontani( gl.TEXTURE_MIN_FILTER): il terzo parametro indica che filtro utilizzare ( gl.NEAREST vuol dire nessun filtro quindi si vede peggio da entrambi ma il gioco consuma poche risorse, GL_LINEAR è un filtro lineare,etc..).

Il nostro prossimo obiettivo è assegnare le cosiddette Coordinate Texture ad ogni nostro vertice: poichè nella precedente lezione abbiamo scoperto che dobbiamo definire 24 vertici, anche in questo caso ci toccherà definire un array di 24 elementi ognuno composto da 2 elementi ( dobbiamo definire una punto su un immagine in 2D, per cui abbiamo bisogno solo di una X e d una Y).Metteremo poi tutti questi elementi dentro un buffer globale che useremo più tardi.
Attenzione: il range di possibili valori che possono assumere le coordinate texture sono normalizzate tra 0.0 e 1.0 ( vedi figura).

var cubover; //vertici cubo

var cuboTexture; //texture vertici cubo

var cuboindex; //indici vertici cubo
function initBuffers() {

cuboTexture= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cuboTexture);
var cuboText= [
0.0,0.0 , 1.0,0.0,
1.0,1.0 , 0.0,1.0,
1.0,0.0 , 1.0,1.0,
0.0,1.0 , 0.0,0.0,
0.0,1.0 , 0.0,0.0,
1.0,0.0 , 1.0,1.0,
1.0,1.0 , 0.0,1.0,
0.0,0.0 , 1.0,0.0,
1.0,0.0 , 1.0,1.0,
0.0,1.0 , 0.0,0.0,
0.0,0.0 , 1.0,0.0,
1.0,1.0 , 0.0,1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cuboText), gl.STATIC_DRAW);
cuboTexture.itemSize = 2;
cuboTexture.numItems = 24;

cubover= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubover);
var cuboverarr= [
// Faccia davanti
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// Faccia dietro
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
1.0, -1.0, -1.0,
// Faccia superiore
-1.0, 1.0, -1.0,
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
// Faccia inferiore
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
// Faccia a destra
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
1.0, -1.0, 1.0,
// Faccia a sinistra
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cuboverarr), gl.STATIC_DRAW);
cubover.itemSize = 3;
cubover.numItems = 24;
cuboindex= gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cuboindex);
var indexes= [
0, 1, 2, 0, 2, 3, // Faccia davanti
4, 5, 6, 4, 6, 7, // Faccia dietro
8, 9, 10, 8, 10, 11, // Faccia superiore
12, 13, 14, 12, 14, 15, // Faccia inferiore
16, 17, 18, 16, 18, 19, // Faccia a destra
20, 21, 22, 20, 22, 23 // Faccia a sinistra
];
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexes), gl.STATIC_DRAW);
cuboindex.itemSize = 1;
cuboindex.numItems = 36;
}

Adesso il nostro prossimo obiettivo è vedere come cambieranno gli Shader della nostra applicazione; nel caso del vertex shader ( una chiamata per ogni vertice definito), è molto simile a quello che utilizzavamo con i colori:

<script id=”shader-vs” type=”x-shader/x-vertex”>
attribute vec3 aVertexPosition;

uniform mat4 uMVMatrix;

uniform mat4 uPMatrix;

attribute vec2 aTextureCoord;

varying vec2 vTextureCoord;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);

vTextureCoord=aTextureCoord;

}

</script>

L’unica differenza rispetto al colore è che passiamo dal colore definito da 4 componenti ( vec4)  ad elementi, cioè le coordinate texture invece rappresentati da una sola coppia di valori ( vec2).

Il pixel shader ha qualche variazione più marcata:

<script id=”shader-fs” type=”x-shader/x-fragment”>
#ifdef GL_ES
precision highp float;

varying vec2 vTextureCoord;
uniform sampler2D Texture;

#endif
void main(void) {

gl_FragColor = texture2D(Texture, vec2(vTextureCoord.s, vTextureCoord.t));

}
</script>

Stavolta il colore che assume una particolare pixel è dato dalle Coordinate Texture rappresentate da vec2(…) ( in questo caso vTextureCoord.s corrisponderebbe alla “coordinata x” e vTextureCoord.t alla “y”). Texture è una variabile assai importante perchè rappresenta la Texture che lo shader deve usare. Ovviamente in un mondo 3D possiamo avere più di una texture, indi è necessario passare allo shader l’immagine da cui “estrapoliamo” il colore da assegnare al nostro pixel .

Ora bisogna riportare la modifica degli shader anche nella funzione che gli inizializza:

function initShaders() {
var fragmentShader = getShader(gl, “shader-fs”);
var vertexShader = getShader(gl, “shader-vs”);

shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);

if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert(“Could not initialise shaders”);
}

gl.useProgram(shaderProgram);

shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, “aVertexPosition”);
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, “aTextureCoord”);
gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);
shaderProgram.Texture= gl.getUniformLocation(shaderProgram, “Texture”);

shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, “uPMatrix”);
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, “uMVMatrix”);

}

Come con i colori, abilitiamo l’attributo che ci serve ed assegniamo allo shaderProgram un altro attributo che indicherà la texture da usare.
Infine non rimane altro che cambiare il codice della nostra funzione di disegno:

function drawScene() {
gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0);
loadIdentity();
mvTranslate([0.0, 0.0, -5.0]);
mvRotate(cubeangle,[0.5,1,0]);

gl.bindBuffer(gl.ARRAY_BUFFER, cubover);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubover.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, cuboTexture);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cuboTexture.itemSize, gl.FLOAT, false, 0, 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, newTexture);
gl.uniform1i(shaderProgram.Texture, 0);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cuboindex);
setMatrixUniforms();
gl.drawElements(gl.TRIANGLES, cuboindex.numItems, gl.UNSIGNED_SHORT, 0);
}

In questa funzione le aggiunte sono l’attivazione della nostra texture, la definizione di essa come texture corrente e la selezione della coordinate Texture da usare e l’assegnazione allo shader di quale Texture usare; da notare la funzione gl.activeTexture(gl.TEXTURE0): il WebGL infatti può gestire fino a 32 Texture possibili, partendo da Texture0 fino a Texture31. Nel nostro caso attiviamo il texture0 e lo assegniamo la variabile globale newTexture ( dove abbiamo caricato l’immagine che testeremo) come Texture corrente, infine assegniamo all variabile per lo shader la Texture il cui identificativo è 0 visto che abbiamo a disposizione solo Texture0.
Salviamo il tutto e facciamo partire lo script: se tutto è stato fatto correttamente, il nostro output dovrebbe essere così:

Per potere vedere l’esempio appena mostrato e scaricare il codice che è stato presentato nell’articolo, collegarsi al solito repository Lab .

Grazie per l’attenzione,

Andrea