Salve a tutti,

Questo articolo tratterà uno degli argomenti più ostici ed interessanti del mondo 3D: l’illuminazione.
Per capire l’importanza di un tale argomento, prendiamo una nostro precedente esempio ,lesson04b.html , e facciamo un piccolo cambiamento: mettiamo lo stesso colore ad ogni faccia del cubo.
Facciamo partire lo script e noteremo un layout simile a quello mostrato dalla seguente foto:

La cosa più evidente è l’impossibilità di capire la forma del nostro oggetto: se non sapessimo a priori di trovarci di fronte ad un cubo 3D, qualcuno potrebbe giustamente pensare che sia un esagono un pò storto rosso bidimensionale. Eppure nella realtà di tutti i giorni i nostri occhi riuscirebbero a distinguere le linee di un cubo completamente pulito e monocolore.
Tutto questo è possibile grazie alla luce.
Fino ad ora pensavamo che il colore di un oggetto fosse dato solo dalla rappresentazione RGBA fissa: il colore e l’intensità di un pixel è data da molteplici fattori tra cui il tipo di luce che lo investe (luce bianca o colorata, oppure se è una fonte accecante come Sole o tenua come una candela,… ), dal tipo di reazione del materiale se investito da una fonte luminosa od anche dall’inclinazione dei raggi su di essi .
Insomma se vogliamo maggiore realismo, dobbiamo gestire anche le cosiddette sorgenti luminose ( coloro che generano luce).

L’argomento illuminazione è estremamente vasto ma possiamo lo stesso catalogare le luci in questo modo, partendo dalla più semplice e arrivando a quella più complessa:

Nome Direzione Luce Luce Riflessa Output
Luce Ambiente Non proviene da una direzione in particolare Il raggio riflesso non ha una direzione particolare Tutti i punti dell’oggetto hanno la stessa intensità e colore
Luce Diffusa proviene da una direzione precisa ( sorgente luminosa) Il raggio riflesso non ha una direzione particolare L’intensità di un punto è proporzionale all’angolo del raggio di luce che lo colpisce, i punti più vicini alla sorgente saranno più chiari quelli lontani o coperti saranno più scuri
Luce Speculare proviene da una direzione precisa ( sorgente luminosa) Il raggio riflesso ha una direzione particolare( ad esempio si immagini uno specchio). Questo comportamento è proporzionale alle caratteristiche del materiale dell’oggetto Crea la lucentezza tipica della superficie liscie tipo metalli

Ovviamente una luce realista dovrebbe contenere tutte e tre queste componenti, ma ,almeno in questa lezione tratteremo solo i primi due casi, considerati i più semplici.

Già da questa introduzione si capisce che la questione dell’illuminazione complica non poco le cose, anche solo guardando i calcoli che bisogna fare: per tale ragione, opereremo un ‘altra semplificazione; considereremo solo l caso di luce diffusa con raggi paralleli.
Il motivo è molto semplice: Nel caso di sorgenti luminose piuttosto vicine all’oggeto ad esempi, i raggi che colpirebbero il nostro cubo avranno tutti un’inclinazione differente. Poichè la quantità di luce è proporzionale a tale valore, sarebbe piuttosto rognoso elaborare tale valore ad ogni spostamento del cubo per ogni pixel.
Per tale ragione, se ipotizziamo di prendere la nostra sorgente luminosa e di mandarla molto lontano dal nostro oggetti, i raggi lentamente si avvicininerebbero tutti seconda una certa direzione il cui angolo di incidenza sarà uguale per ogni vertice ( un bel risparmio di tempo :) ).
Un esempio semplice di tale fenomeno è il Sole: esso è a distanza non infinita da noi eppure si può approssimativamente dire che i raggi solari siano paralleli gli uni agli altri ( almeno localmente, ciò è piuttosto vero ).
Ora, l’angolo che a noi interesserà sarà quello compreso tra il raggio luminoso che colpisce un vertice del cubo e la normale del piano ( per normale si intende quel vettore tridimensionale perpendicolare ad una superficie; poichè tutti i nostri vertici giacciono su una superficie quadrata regolare, la normale avrà la medesima direzione in ogni punto della faccia del cubo, e poichè siamo nel caso in cui la direzione del raggio luminoso è sempre la stessa, pure l’angolo di incidenza sarà il medesimo per ogni vertice di una tale faccia).
Mostriamo un veloce esempio:

Come vedere l’angolo ang sarà uguale per tutti i vertici sulla faccia superiore.

Riassumendo quindi i passi fondamentali:
-introduco per ogni vertice le coordinate della normale relativa
-definisco in maniera statica (per questo esempio semplificato) la direzione della luce
-calcolo angolo tra di essi e modifico propozionalmente il colore di quel dato pixel

Per ovviare al terzo punto ci toccherà modificare la gestione degli Shader (vertex shader per il calcolo, il pixel shader solo per ricavare il valore tramite interpolazione lineare).

Iniziamo a vedere un pò di codice:

var cubover;
var cuboTexture;
var cuboindex;
var cubonormale;
function initBuffers() {


cubonormale= gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubonormale);
var vertexNormals = [

// Front face

0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,
0.0, 0.0, 1.0,

// Back face

0.0, 0.0, -1.0,
0.0, 0.0, -1.0,
0.0, 0.0, -1.0,
0.0, 0.0, -1.0,

// Top face

0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,
0.0, 1.0, 0.0,

// Bottom face

0.0, -1.0, 0.0,
0.0, -1.0, 0.0,
0.0, -1.0, 0.0,
0.0, -1.0, 0.0,

// Right face

1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 0.0, 0.0,

// Left face

-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0,
-1.0, 0.0, 0.0,
];

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW);
cubonormale.itemSize = 3;
cubonormale.numItems = 24;


gl.uniform3f( shaderProgram.ambientColorUniform,1.0,1.0,1.0);

/*set direction of Directionale Light*/
var light_direction = [1.0,0.4,0.4 ];
var light_direction_transformed = vec3.create();
vec3.normalize(light_direction, light_direction_transformed);
gl.uniform3fv(shaderProgram.lightingDirectionUniform, light_direction_transformed);

/*set the color of Directional Light*/

gl.uniform3f(shaderProgram.directionalColorUniform,1.0,1.0,1.0);



}

Notiamo che in questa funzione definiamo per ogni vertice la relativa normale; ovviamente, possiamo notare che ogni vertice che appartiene alla medesima faccia hanno la stessa normale ( siamo infatti nel caso in cui la sorgente luminosa è lontanissima per cui i raggi luminosi sono praticamente paralleli).
Nella seconda parte dello script abbiamo definito tre componenti fondamentali:

-Colore Luce Ambientale ( la direzione non serve)
-direzione da cui proviene la luce sorgente (Luce Diffusa)
-colore da cui proviene la luce sorgente (Luce Diffusa)

Questi valori vengono poi inseriti dentro degli attributi per poter poi essere utilizzati dagli shader per i loro calcoli. Il calcolo della direzione da cui proviene la luce è leggermente più complesso degli altri: questo è dovuto al fatto che per semplificarci poi i calcoli, abbiamo bisogno di normalizzare tale vettore ( cioè che la sua lunghezza sia pari a 1).

ora guardiamo le modifiche da effettuare sugli shader:

Funzione: initshader

var shaderProgram;
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.vertexNormalAttribute = gl.getAttribLocation(shaderProgram, “aVertexNormal”);
gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute);
shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, “aTextureCoord”);
gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);
shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, “uPMatrix”);
shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, “uMVMatrix”);
shaderProgram.nMatrixUniform = gl.getUniformLocation(shaderProgram, “uNMatrix”);
shaderProgram.samplerUniform = gl.getUniformLocation(shaderProgram, “Texture”);

shaderProgram.ambientColorUniform = gl.getUniformLocation(shaderProgram, “AmbientColor”);
shaderProgram.lightingDirectionUniform = gl.getUniformLocation(shaderProgram, “LightingDirection”);
shaderProgram.directionalColorUniform = gl.getUniformLocation(shaderProgram, “LightingColor”);


}

Qua non facciamo altro che preparare gli attributi da usare negli shader.

Fragment Shader.

<script id=”shader-fs” type=”x-shader/x-fragment”>
#ifdef GL_ES
precision highp float;
varying vec3 LightWeighting;
varying vec2 vTextureCoord;
uniform sampler2D Texture;

#endif
void main(void) {
//gl_FragColor = texture2D(Texture, vec2(vTextureCoord.s, vTextureCoord.t));
vec4 textureColor = texture2D(Texture, vec2(vTextureCoord.s, vTextureCoord.t));

gl_FragColor = vec4(textureColor.rgb * LightWeighting, textureColor.a);

}

L’unica aggiunta che viene effettuata in questo shader è l’inserimento di una nuova componente, LightWeighting: essa rappresenta il contributo sul colore che proviene dalla luce. Tale contributo verrà infatti calcolato nel Vertex Shader.

Vertex Shader

<script id=”shader-vs” type=”x-shader/x-vertex”>
attribute vec3 aVertexPosition;
attribute vec3 aVertexNormal;
attribute vec2 aTextureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
uniform mat3 uNMatrix;
varying vec2 vTextureCoord;
uniform vec3 AmbientColor;
uniform vec3 LightingDirection;
uniform vec3 LightingColor;
varying vec3 LightWeighting;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
vTextureCoord = aTextureCoord;
vec3 transformedNormal = uNMatrix * aVertexNormal;
float directionalLightWeighting = max(-dot(transformedNormal, LightingDirection), 0.0);
LightWeighting = AmbientColor + LightingDirection * directionalLightWeighting;

}

Decisamente più complesso del Fragment Shader, qua calcoleremo infatti il contributo LightWeighting della luce;analizziamo i suoi componenti:

-AmbientColor = abbiamo fissato il colore della luce ambientale a RGB=[1.0,1.0,1.0] cioè luce bianca. Questo contributo è sempre presente nel parametro LightWeighting perchè non dipende da nessun altro valore tra cui ad esempio la direzione di una sorgente .
-LightingDirection = abbiamo fissato il colore della luce ambientale a RGB=[1.0,1.0,1.0] cioè luce bianca per semplificarci la vita. Ovviamente possiamo pensare che la nostra sorgente di Luca Diffusa può variare il colore dell’illuminazione con il tempo ( pensiamo ad una discoteca).
-directionalLightWeighting= unico elemento che dipende dalla direzione,infatti lo calcoleremo utilizzando l’angolo compreso tra la direzione da cui proviene la sorgente e la normale del vettore. Tale contributò dovrà essere sempre maggiore o uguale a 0 ( per quello imponiamo il massimo tra tale valore e 0.0), inoltre, poichè i due vettori che sottendono l’angolo sono in realtà versori ( cioè hanno lunghezza pari a 1), invece che calcolare il coseno dell’angolo che ci interessa, possiamo alternativamente utilizzare un’operazione binaria matematica conosciuta come Prodotto Scalare, che ci restituirà un valore numerico. Il segno meno ci serve per ribaltare tutto su valori di angolo ammissibili.
(Per dubbi su queste ultime affermazioni, non esitate a chiedere 😉 ).
Grazie a questo calcolo, nel caso di zone d’ombra o comunque di punti sul cubo il cui angolo non è compreso nel range considerato valido, il contributo della luce diffusa varrà 0 e il contributo totale della luce sul vertice sarà uguale soltanto alla luce ambientale ( in tale modo si creano zone con più luce ed altre più scure: tale diversità ci garantirà una resa 3D decisamente più realistica).

Ora salvate il tutto e provate a fare partire lo script; questo dovrebbe essere l’output che vi troverete:

A causa di altre problematiche che riguardano l’uso delle normali ed altro, è stato necessario modificare i file js di supporto, soprattutto il file MatriUtilityFunc.js che potrete trovare al seguente link.
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