Salve a tutti!
Finalmente è finito il primo ciclo delle lezioni sul webgl, il livello base. A questo punto possiediamo tutte le conoscenze base per realizzare la nostra prima ambientazione 3D.

Obiettivo: creare una stanza 3D con texture dove il giocatore potrà spostare la telecamera sia con la tastiera che con le frecce

 

(una sorta di sparatutto in prima persona molto primitivo).

Mostreremo due esempio: il primo, che tratteremo in questa lezione, sarà una versione base ma funzionante, nella prossima arricchiremo il nostro gioco per rendere il tutto ancora più realistico.

In questo primo approccio non metteremo alcuna illuminazione, per cui possiamo partire dal codice dell’articolo 8 per sviluppare l’applicazione webgl.

La lezione di oggi utilizza in parte il codice e le scelte progettuali documentate nel seguente articolo che tratta il medesimo argomento. Approfitto della parentesi per invitarvi a dare un occhio a questo blog sul WEBGL, davvero ben curato.

-function InitBuffers()
Al solito in questa funzione istanziamo i buffer riempiendo gli array delle posizioni dei vertici e delle loro coordinate Texture.

function initBuffers() {
var str=””;
str += “\n”;
str += “NUMPOLLIES 24\n”;
str += “\n”;
str += “// A1\n”;
str += “\n”;
str += “-2.0 1.0 -2.0 0.0 1.0\n”;
str += “-2.0 0.0 -2.0 0.0 0.0\n”;
str += “-0.5 0.0 -2.0 1.5 0.0\n”;
str += “-2.0 1.0 -2.0 0.0 1.0\n”;
str += “-0.5 1.0 -2.0 1.5 1.0\n”;
str += “-0.5 0.0 -2.0 1.5 0.0\n”;
str += “\n”;
str += “// A2\n”;
str += “\n”;
str += ” 2.0 1.0 -2.0 2.0 1.0\n”;
str += ” 2.0 0.0 -2.0 2.0 0.0\n”;
str += ” 0.5 0.0 -2.0 0.5 0.0\n”;

handleLoadedgeneral(str,”wall”);
str=””;
str += “\n”;
str += “NUMPOLLIES 6\n”;
str += “\n”;
str += “// Floor 1\n”;
str += “-3.0 0.0 -3.0 0.0 6.0\n”;
str += “-3.0 0.0 3.0 0.0 0.0\n”;
str += ” 3.0 0.0 3.0 6.0 0.0\n”;
str += “\n”;
str += “-3.0 0.0 -3.0 0.0 6.0\n”;
str += ” 3.0 0.0 -3.0 6.0 6.0\n”;
str += ” 3.0 0.0 3.0 6.0 0.0\n”;
handleLoadedgeneral(str,”floor”);
str=””;
str += “\n”;
str += “NUMPOLLIES 6\n”;
str += “\n”;
str += “// Ceiling 1\n”;
str += “-3.0 1.0 -3.0 0.0 6.0\n”;
str += “-3.0 1.0 3.0 0.0 0.0\n”;
str += ” 3.0 1.0 3.0 6.0 0.0\n”;
str += “-3.0 1.0 -3.0 0.0 6.0\n”;
str += ” 3.0 1.0 -3.0 6.0 6.0\n”;
str += ” 3.0 1.0 3.0 6.0 0.0\n”;
str += “\n”;
handleLoadedgeneral(str,”ceiling”);
}

var worldVertexPositionBuffer = null;
var worldVertexTextureCoordBuffer = null;
var floorVertexPositionBuffer = null;
var floorVertexTextureCoordBuffer = null;
var ceilinceilinggVertexPositionBuffer = null;
var ceilingVertexTextureCoordBuffer = null;

function handleLoadedgeneral(data,indexelement) {
var lines = data.split(“\n”);
var vertexCount = 0;
var vertexPositions = [];
var vertexTextureCoords = [];
for (var i in lines) {
var vals = lines[i].replace(/^\s+/, “”).split(/\s+/);
if (vals.length == 5 && vals[0] != “//”) {
// It is a line describing a vertex; get X, Y and Z first
vertexPositions.push(parseFloat(vals[0]));
vertexPositions.push(parseFloat(vals[1]));
vertexPositions.push(parseFloat(vals[2]));

// And then the texture coords
vertexTextureCoords.push(parseFloat(vals[3]));
vertexTextureCoords.push(parseFloat(vals[4]));

vertexCount += 1;
}
}

switch(indexelement)
{ case “ceiling”:
ceilingVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, ceilingVertexPositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPositions), gl.STATIC_DRAW);
ceilingVertexPositionBuffer.itemSize = 3;
ceilingVertexPositionBuffer.numItems = vertexCount;

ceilingVertexTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, ceilingVertexTextureCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexTextureCoords), gl.STATIC_DRAW);
ceilingVertexTextureCoordBuffer.itemSize = 2;
ceilingVertexTextureCoordBuffer.numItems = vertexCount;
break;

case “floor”:
floorVertexPositionBuffer = gl.createBuffer();
… ( stessa cosa con floorVertexPositionBuffer e floorVertexTextureCoordBuffer)

break;

case “wall”:
worldVertexPositionBuffer = gl.createBuffer();

…( stessa cosa con worldVertexPositionBuffer e worldVertexTextureCoordBuffer)
break;
}
}

Come possiamo notare, l’operazione di lettura ed inserimento nei buffer viene in realtà eseguita dalla funziona handleLoadedgeneral(), alla quale viene passato una enorme stringa che contiene tutti le posizioni e le coordinate texture dei nostri pulsanti; è buona prassi inserire tali elementi in un file txt eppoi utilizzare ajax per leggere ma questa soluzione viene bloccata nel caso la nostra pagina html non sia su un web server ( insomma, in locale non funziona bene, bisogna uploadarla su un sito o creare un web server sul proprio pc ).

Nella lezione prossima mostreremo questa nuova modalità.
Inoltre possiamo notare la creazione di 6 Buffer distinti, 3 per le posizioni e tre per le texture. Questo è stato fatto perchè il nostro obiettivo è quello di assegnare al pavimento e al soffitto una texture differente da quelle delle mura e per fare ciò una possibile soluzione è dividere le strutture dati che dovremmo riempire. Per tale ragione, nella funzione DrawScene saremmo però costretti a chiamare più volte la drawArrays per disegnare il contenuto dei buffer.

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

floorTexture.image.src = “floor.jpg”;

ceilingTexture= gl.createTexture();
ceilingTexture.image = new Image();
ceilingTexture.image.onload = function() {
LoadedTexture(ceilingTexture)
}

ceilingTexture.image.src = “ceiling.jpg”;

wallTexture = gl.createTexture();
wallTexture.image = new Image();
wallTexture.image.onload = function () {
LoadedTexture(wallTexture)
}

wallTexture.image.src = “wall.jpg”;
}

 

Qua non facciamo altro che caricare le tre texture che ci serviranno, una per il Muro ( wallTexture), una per il pavimento ( floorTexture) ed una per il soffitto (ceilingTexture).

 

DESIGN:
Prima di parlare delle funzioni javascript per catturare gli spostamenti, parliamo di cme vogliamo modellare la nostra applicazione: l’InitBuffer e la relativa drawArray disegneranno il nostro mondo 3D ma come funzionerà la nostra telecamera? Come si sposterà in questo spazio tridimensionale?

Per rispondere a questa domanda, prenderemo come spunto un qualunque videogioco sparatutto: usando le frecce direzionali ( o i pulsanti w-a-s-d), la telecamera si sposterà dentro questa stanza come se camminasse, mentre il mouse ci servirà per direzionare lo sguardo del nostro personaggio senza però farli muovere un passo. Per limitare lo spazio di manovra, impediremo alla nostra telecamera di cambiare posizione sull’asse delle ordinate, cioè gli “inibieremo” la possibilità di volare,saltare o abbassarsi e quindi cambiare l’altezza da cui osserva.
Ora dobbiamo pensare ad un modo matematico per schematizzare lo spostamento di questa telecamera: ovviamente la telecamera dovrà possedere delle coordinate spaziali ( quindi ben vengano le variabili PosX,PosY,PosZ).

A ciò però bisogna anche aggiungere le informazioni per identificare la direzione in cui sta guardando il soggetto: la soluzione più semplice è utilizzare due angoli :
-il yaw: è l’angolo orizzontale -> si modifica cliccando sulla freccia destra o sinistra oppure muovendo il mouse nelle medesime direzioni.
-il pitch: è l’angolo verticale-> si modifica muovendo il mouse in alto o in basso.

E le freccie in alto ed in basso? Esse effettivamente sono i due input che permettono alla telecamera di cambiare posizione; cliccando essi infatti la telecamera si sposterà nella direzione in cui si sta guardando, cambiando perciò i valori di PosX e PosZ.

Nella funzione Start() setto le funzioni che ci serviranno per gli input del giocatore:

document.onkeydown =KeyboardDown;
document.onkeyup = KeyboardUp;
document.onmousemove = MouseMove;

MOUSE:

var oldX = null;
var oldY = null;
var newX=null;
var newY=null;

function MouseMove(event) {
ntickstop=0;

newX = event.clientX;
newY = event.clientY;
if(newX!=null && oldX!=null){

if(newX-oldX>0) yawRate = 0.1;
if(newX-oldX<0) yawRate = -0.1; if(newX-oldX==0) yawRate=0; } if(newY!=null && oldY!=null){ if(newY-oldY>0) pitchRate = 0.1;
if(newY-oldY<0) pitchRate = -0.1;
if(newY-oldY==0) pitchRate=0;

}
oldX = newX;
oldY = newY;
}

Il funzionamento del Mouse è piuttosto semplice: calcolando quello che è lo spostamento del mouse ( nuove coordinate del mouse-vecchie coordinate), viene aumentato o diminuito gli angoli di pitch e way, a seconda dell’asse in cui si sposta ( aumenta way se guardo verso destra,diminuisce verso sinistra).
wayRate e pitchRate rappresentano la velocità negativa o positiva con cui devono diminuire gli angoli.
Al momento noterete anche l’inizializzazione di una variabile,ntickstop, il cui funzionamento verrà spiegato successivamente.

KEYBOARD:

var currentPressedKey= {};
function KeyboardDown(event) {
currentPressedKey[event.keyCode] = true;
}

function KeyboardUp(event) {

currentPressedKey[event.keyCode] = false;
pitchRate = 0;
yawRate = 0;
speed = 0;
}

var speed=0.0;
var yawRate=0;
var pitchRate=0;
function KeyPressed()

{

if (currentPressedKey[33]) {
// Page Up
pitchRate = -0.1;
} else if (currentPressedKey[34]) {
// Page Down
pitchRate = 0.1;
}

if (currentPressedKey[37] || currentPressedKey[65]) {
// Left cursor key or A
yawRate = -0.1;
} else if (currentPressedKey[39] || currentPressedKey[68]) {
// Right cursor key or D
yawRate = 0.1;
}

if (currentPressedKey[38] || currentPressedKey[87]) {
// Up cursor key or W
speed = -0.003;
} else if (currentPressedKey[40] || currentPressedKey[83]) {
// Down cursor key
speed = 0.003;
}

}

Il codice con cui si capisce quale pulsante è stato cliccato è leggermente diverso da quello usato fino adesso nelle nostre lezioni: questo perchè noi vogliamo gestire anche il caso in cui schiacciamo più di un pulsante alla volta. Con il codice precedente infatti ci salvavamo l’identificativo del pulsante toccato, mentre in questo caso l’informazione ricavata da una pression su di un tasto è salvata su un array di variabili booleane: grazie a questo trucchetto sarà infatti possibile al nostro soggetto di spostarsi in avanti e contemporaneamente ruotare.

Possiamo notare come lo spostamento in avanti ed indietro è dettato dalla variabile speed ( unica variabile che permette di cambiare posizione alla telecamera), inoltre è fondamentale settare la funzione KeyboardUp ( funzione che parte quando rilasciamo un pulsante): tale codice infatti permette di azzerare ogni movimento nel caso in cui il pulsante viene rilasciato.

Animazione:

Manca ancora una funzione, precisamente quella che calcola , a seconda degli input letti, le variazioni alle variabili di stato, cioè la posizione della telecamera ( PosX,PosY,PosZ) e la sua direzione ( yaw, pitch):

 

var lastTime=0;
function animate() {
ntickstop++;
var timeNow = new Date().getTime();
if (lastTime != 0) {
var elapsed = timeNow – lastTime;

if (speed != 0) {
PosX -= Math.sin(degToRad(yaw)) * speed * elapsed;
PosZ-= Math.cos(degToRad(yaw)) * speed * elapsed;

}

yaw += yawRate * elapsed;
if(pitch+pitchRate * elapsed<90 && pitch+pitchRate * elapsed>-90)
pitch += pitchRate * elapsed;

if(newX>990) yawRate=0.1;
else if(newX<10) yawRate=-0.1; else if(newY>590) pitchRate = 0.1;
else if(newY<10) pitchRate = -0.1; else if(ntickstop>5) {
pitchRate=0;
yawRate=0;
ntickstop=0;
oldX = newX;
oldY = newY;
}

}
lastTime = timeNow;
}

Si può notare che questa funzione esegue molte operazioni:
-calcola l’intervallo di tempo elapsed tra una chiamata animate e la precedente ( utile per calcolare di quanto si è spostato)
– se la velocità è diversa da 0 ( ho cliccato perciò preccia su o freccia giù), calcola lo spostamento lungo la direzione in cui sta guardando la telecamera usando la trigonometria ( cambiano perciò i valori di PosX e PoxZ).
-modifica i valori dei due angoli della direzione a seconda del valore di yawRate e pitchRate ( da notare una minuscola finezza: poichè il pitch rappresenta l’angolo verticale, si suppone che un essere umano non riesca e girare verticalmente a 360, al massimo riesce a guardarsi i piedi e viceversa al massimo riesce ad alzare lo sguardo fino allo zenith).
-gestione dei confini:
vengono controllati le coordinate X ed Y del curso del mouse: se il cursore si trova piuttosto vicino alla cornice del nostro canvas, la telecamera si continuerà a muoversi nella direzione del bordo fino a che tale cursore non ritornerà nella zona centrale, anche se il mouse non si muove: tale trucco escogitato server perchè la nostra zona di gioco non è infinita e non sarebbe possibile ruotare se il mouse si scontrasse con il limite.
-incremento ntickstop e reset:
in pratica ogni 5 chiamate alla funzione animate, il webGL blocca tutti i movimenti: tale pezza serve per bloccare il movimento nel caso si stia usando solo il mouse. Se per esempio decidiamo di spostare il mouse per qualche secondo e dopo lo teniamo fermo, la funzione MouseMove(event), che viene lanciata ad ogni VARIAZIONE della posizione del cursore non verrebbe chiamata e quindi la telecamera rimarrebbe in movimento perchè non rileverebbe tale arresto.
Tale contatore viene resettato a 0 ogni volta che rileva un movimento del mouse ( tale soluzione perciò introduce un ‘inerzia nell’arresto del movimento ma è accettabile).

DRAWING:
ultima funzione da analizzare è la DrawScene(), più corposa del solito perchè deve disegnare tre oggetti distinti:

var yaw=0.0;
var pitch=0.0;

var PosX=0.0;
var PosY=0.5;
var PosZ=0.0;

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();

mvRotate(pitch,[1,0,0]);
mvRotate(yaw,[0,1,0]);
mvTranslate([-PosX, -PosY, PosZ]);

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, wallTexture);
gl.uniform1i(shaderProgram.samplerUniform, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexTextureCoordBuffer);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, worldVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, worldVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
setMatrixUniforms();
gl.drawArrays(gl.TRIANGLES, 0, worldVertexPositionBuffer.numItems);

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, floorTexture);
gl.uniform1i(shaderProgram.samplerUniform, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, floorVertexTextureCoordBuffer);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute,

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

floorVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
setMatrixUniforms();
gl.drawArrays(gl.TRIANGLES, 0, floorVertexPositionBuffer.numItems);

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D,ceilingTexture);
gl.uniform1i(shaderProgram.samplerUniform, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, ceilingVertexTextureCoordBuffer);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute,
ceilingVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER,ceilingVertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
ceilingVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
setMatrixUniforms();
gl.drawArrays(gl.TRIANGLES, 0, ceilingVertexPositionBuffer.numItems);

}

La prima cosa che notiamo è l’inizializzazione della variabile di stato PosY, la quale non cambierà mai durante l’applicazione come precedentemente spiegato.
La prima parte della funzione non fa altro che spostare e disegnare la telecamera nel punto prefissato PosX,posY,PosZ e ruotarla nella direzione corrente.
Nella seconda parte viene effettivamente disegnata la stanza, caricando ogni volta la Testure corrente da disegnare ed il buffer contenti le posizioni.
Qua sotto riportiamo uno screen della nostra applicazione:

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

Grazie per l’attenzione,

Andrea