Advertisement
  1. Game Development
  2. Shaders

Una guida per principianti alla programmazione degli Shaders Grafici: Parte 2

Scroll to top
Read Time: 12 min
This post is part of a series called A Beginner's Guide to Coding Graphics Shaders.
A Beginner's Guide to Coding Graphics Shaders
A Beginner's Guide to Coding Graphics Shaders: Part 3

() translation by (you can also view the original English article)

Lo ShaderToy, che abbiamo usato nel precedente tutorial di questa serie, è ottimo per piccoli test ed esperimenti ma è comunque abbastanza limitato. Ad esempio, tra le tante cose, non è possibile controllare quali dati vengono inviati allo shader. Avendo un proprio ambiente di sviluppo in cui fosse possibile eseguire gli shader significherebbe poter fare ogni sorta di effetto potendolo anche applicare ai tuoi progetti!

Useremo quindi, come framework per girare i nostri shader, Three.js. WebGL è l'API JavaScript che ci permette il rendering degli shader; con Three.js renderemo questo lavoro più facile.

Se non siete interessati al JavaScript o alla piattaforma web, non preoccupatevi: non ci concentreremo sulle specifiche del rendering via web (se volete comunque approfondire il framework, controllate questo tutorial). Impostare shaders nel browser è il modo più rapido per iniziare e diventare abili con questo processo vi permetterà di impostare ed utilizzare facilmente gli shaders su qualsiasi piattaforma vogliate.

Il Setup

Questa sezione vi guiderà verso la creazione di shader a livello locale. È possibile seguire il processo senza bisogno di scaricare nulla con questo CodePen pre-costruito:

Potete sempre farne un fork ed editarlo su CodePen.

Ciao Three.js!

Three.js è un framework JavaScript che si prende cura di un sacco di codice interno per WebGL che ci servirà per il rendering dei nostri shader. Il metodo più semplice per iniziare è usare la versione ospitata su CDN.

Ecco un esempio HTML scaricabile che contiene una minima scena Threejs.

Provate a salvarlo sul disco e ad aprirlo nel vostro browser. Dovreste vedere una schermata nera. Non essendo molto eccitante proviamo ad aggiungere un cubo, giusto per vedere se tutto funziona.

Per creare un cubo ci serve prima definire la sua geometria, poi il suo materiale e quindi aggiungerlo alla scena. Aggiungete questo pezzo di codice sotto, dove dice Add your code here:

1
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
2
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00} );//We make it green

3
var cube = new THREE.Mesh( geometry, material );
4
//Add it to the screen

5
scene.add( cube );
6
cube.position.z = -3;//Shift the cube back so we can see it

Non andremo troppo nel dettaglio in tutto questo codice, siamo più interessati alla parte dello shader. Ma se tutto è andato bene, si dovrebbe vedere un cubo verde nel centro dello schermo:

Già che ci siamo, facciamo ruotare. La funzione render gira ad ogni frame. Possiamo accedere rotazione del cubo attraverso cube.rotation.x (o .y o .z). Provate ad incrementarlo in modo che la funzione di  rendering sia simile a questa:

1
function render() {
2
  cube.rotation.y += 0.02;
3
	
4
	requestAnimationFrame( render );
5
	renderer.render( scene, camera );
6
}

Sfida: Sapreste farlo ruotare lungo un asse diverso? Cosa ne dite di ruotare lungo due assi allo stesso tempo?

Ora abbiamo tutto impostato, aggiungiamo alcuni shader!

Aggiungere Shaders

A questo punto, possiamo cominciare a pensare al processo di creazione degli shaders. Indipendentemente dalla piattaforma con cui si intende utilizzare gli shaders, sarà molto probabile trovarsi in una situazione simile: avete tutto impostato, avete gli oggetti disegnati sullo schermo, ora come si fa si accede alla GPU?

Fase 1: Caricamento del codice GLSL

Stiamo usando JavaScript per costruire questa scena. In altre situazioni potremmo utilizzare C ++ o Lua o qualsiasi altro linguaggio. Gli Shaders, a prescindere, sono scritti in una speciale LInguaggio Shading. Il linguaggio di shading di OpenGL è GLSL (OpenGL Shading Language). Visto che stiamo usando WebGL, che è basato su OpenGL, useremo GLSL.

Quindi, come e dove possiamo scrivere il nostro codice GLSL? La regola generale è che si carichi il nostro codice GLSL come una stringa. È quindi possibile inviarlo al parser (che analizza il sorgente e lo interpreta) per poi essere eseguito dalla GPU.

In JavaScript, è possibile fare questo semplicemente inserendo tutto il codice inline dentro ad una variabile in questo modo:

1
var shaderCode = "All your shader code here;"

Così funziona, ma non è molto conveniente dal momento che Javascript non ha un metodo facile per creare stringhe multi linea. La maggior parte delle persone tende a scrivere il codice dello shader in un file di testo dandogli una estensione .glsl o .frag (abbreviazione di frammento di shader), che poi basta caricare.

Questo metodo è valido, noi invece per i fini di questo tutorial, scriveremo il codice del nostro shader all'interno di un nuovo tag <script> così da caricarlo direttamente nel JavaScript.

Crete un nuovo tag <script> all'interno del codice HTML che assomiglia a questo:

1
<script id="fragShader" type="shader-code">
2
3
</script>

Diamogli l'ID fragShader così potremo accederci più tardi. Il tipo di shader-code è in realtà un tipo di script fasullo che non esiste. (Potremmo dargli qualsiasi nome, avrebbe comunque funzionato). Il motivo per cui facciamo questo è che così il codice non verrà eseguito e non verrà neanche visualizzato nel codice HTML.

Ora lanciamo uno shader molto basilare che restituisce solo bianco.

1
<script id="fragShader" type="shader-code">
2
void main() {
3
    gl_FragColor = vec4(1.0,1.0,1.0,1.0);
4
}
5
</script>

(I componenti di vec4 in questo caso corrispondono al valore rgba, come spiegato nel tutorial precedente.)

Alla fine dobbiamo caricarlo con questo codice. Possiamo farlo con una semplice riga di JavaScript che cerca l'elemento HTML corrispondente e ne copia il testo all'interno:

1
var shaderCode = document.getElementById("fragShader").innerHTML;

Questo dovrebbe andare sotto il codice del cubo.

Ricordate: solo ciò che è stato caricato come stringa verrà analizzato come codice GLSL valido (cioè, void main () {...}. Tutto il resto è solo codice HTML liscio.)

Potete sempre farne un fork ed editarlo su CodePen.

Fase 2: Applicazione dello Shader

Il metodo per applicare lo shader potrebbe essere diverso a seconda della piattaforma che state utilizzando e a come si interfaccia con la GPU. Non è mai una fase troppo complicata, comunque, una ricerca sommaria su Google ci mostra come creare un oggetto e applicarci uno shader con Three.js.

Abbiamo bisogno di creare un materiale speciale e fornirgli il nostro codice shader. Creeremo un piano come oggetto per lo shader (ma potremmo benissimo usare il cubo). Questo è tutto quello che dobbiamo fare:

1
//Create an object to apply the shaders to

2
var material = new THREE.ShaderMaterial({fragmentShader:shaderCode})
3
var geometry = new THREE.PlaneGeometry( 10, 10 );
4
var sprite = new THREE.Mesh( geometry,material );
5
scene.add( sprite );
6
sprite.position.z = -1;//Move it back so we can see it

A questo punto, dovremmo vedere uno schermo bianco:

Potete sempre farne un fork ed editarlo su CodePen.


Se modifichiamo il sorgente nello shader con qualsiasi altro colore e aggiorniamo, si dovrebbe vedere il nuovo colore!

Sfida: Si può impostare una porzione dello schermo di rosso, e un'altra porzione di blu? (Se vi bloccate, il passo successivo dovrebbe darvi un suggerimento!)

Fase 3: Invio dei dati

A questo punto possiamo fare quello che vogliamo con il nostro shader, ma in realtà non c'è molto che possiamo fare. Abbiamo solo la posizione interna del pixel gl_FragCoord con cui lavorare, e se vi ricordate, non è normalizzata. Abbiamo bisogno di avere almeno le dimensioni dello schermo.

Per inviare dati al nostro shader dobbiamo farlo con la così detta variabile uniform. Per farlo, creiamo un oggetto chiamato uniforms e aggiungiamogli le nostre variabili. Ecco la sintassi per l'invio della risoluzione:

1
var uniforms = {};
2
uniforms.resolution = {type:'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)};
Ogni variabile uniform deve avere un type e un value. In questo caso, si tratta di un vettore di 2 dimensioni con larghezza e altezza della finestra come sue coordinate. La tabella che segue (tratto dalla documentazione di Three.js) mostra tutti i tipi di dati che è possibile inviare e i loro identificatori:
Stringa di tipo uniform tipo GLSL Tipo JavaScript
'i', '1i'
int
Number
'f', '1f' float
Number
'v2'
vec2
THREE.Vector2
'v3'
vec3
THREE.Vector3
'c' vec3
THREE.Color
'v4' vec4
THREE.Vector4
'm3' mat3
THREE.Matrix3
'm4' mat4
THREE.Matrix4
't' sampler2D
THREE.Texture
't' samplerCube
THREE.CubeTexture
In realtà per inviarli allo shader, modificate l'istanziatore ShaderMaterial per includerlo, così:
1
var material = new THREE.ShaderMaterial({uniforms:uniforms,fragmentShader:shaderCode})

Non abbiamo ancora finito! Ora che il nostro shader sta ricevendo questa variabile abbiamo bisogno di farne qualcosa. Creiamo un gradiente come abbiamo fatto nel precedente tutorial: normalizzando le nostre coordinate e usandole per crearci il valore del nostro colore.

Modificate il codice dello shader in modo che assomigli a questo:

1
uniform vec2 resolution;//Uniform variables must be declared here first

2
void main() {
3
    //Now we can normalize our coordinate

4
	vec2 pos = gl_FragCoord.xy / resolution.xy;
5
    //And create a gradient!

6
    gl_FragColor = vec4(1.0,pos.x,pos.y,1.0);
7
}

E si dovrebbe vedere questo bel gradiente!

Potete sempre farne un fork ed editarlo su CodePen.

Se siete un pò confusi su come siamo riusciti a creare un simile gradiente con solo due righe di codice di shader, controllate la prima parte di questa serie di tutorial per un veloce ripasso delle logiche che stanno alla base.

Sfida: Potete dividere lo schermo in 4 sezioni uguali con colori diversi? Qualcosa come questo:

Fase4: Aggiornamento dei dati

E bello essere in grado di inviare dati al nostro shader, ma se avessimo bisogno di aggiornarli? Ad esempio, se si apre il precedente esempio in una nuova scheda, e se si ridimensiona la finestra, il gradiente non si aggiorna, perché sta ancora utilizzando le dimensioni della schermata iniziale.

Per aggiornare le nostre variabili, di solito, basta reinviare la variabile uniform perché sia aggiornata. Con Three.js, però, abbiamo solo bisogno di aggiornare l'oggetto uniforms nella nostra funzione render senza bisogno di inviarlo di nuovo allo shader.

Quindi, ecco come apparirà la nostra funzione di rendering, dopo aver effettuato questo cambiamento:

1
function render() {
2
	cube.rotation.y += 0.02;
3
	uniforms.resolution.value.x = window.innerWidth;
4
	uniforms.resolution.value.y = window.innerHeight;
5
6
	requestAnimationFrame( render );
7
	renderer.render( scene, camera );
8
}

Se aprite il nuovo CodePen e si ridimensionate la finestra, vedrete i colori che cambiano (anche se la dimensione iniziale della finestra rimane la stessa). Per rendersene conto è più semplice guardare i colori in ogni angolo per verificare che non cambiano.

Nota: Inviare dati alla GPU come questo metodo è generalmente costoso. Inviare un pugno di variabili per fotogramma può andar bene, ma il framerate potrebbe davvero rallentare se se ne inviassero centinaia per fotogramma. Potrebbe non sembrare uno scenario realistico, ma se avete un paio di centinaia di oggetti sullo schermo, e tutti devono avere una propria illuminazione, per esempio, tutti con caratteristiche diverse, le cose potrebbero presto andare fuori controllo. Impareremo di più su come ottimizzare i nostri shader nei prossimi articoli!

Sfida: Potete cambiare i colori al variare del tempo? (Se vi siete bloccati, guardate come abbiamo fatto nella prima parte di questa serie di tutorial.)

Fase 5: Lavorare con le Texture

Indipendentemente da come carichiamo le vostre texture o in quale formato siano, le invierete allo shader sempre allo stesso modo sulle varie piattaforme, come variabili uniform.

Una breve nota sul caricamento dei file in JavaScript: è possibile caricare immagini da un URL esterno senza troppi problemi (che è quello che faremo qui), ma se si desidera caricare l'immagine a livello locale (da disco), si incorrerà in problemi di autorizzazione, perché JavaScript non può, e non deve, di norma accedere ai file sul vostro sistema. Il modo più semplice per aggirare questo ostacolo è quello di avviare un server Python locale, che forse è più semplice di quanto non sembri.

Three.js ci fornisce un po' di funzioni pratiche per il caricamento di un'immagine come texture:

1
THREE.ImageUtils.crossOrigin = '';//Allows us to load an external image

2
var tex = THREE.ImageUtils.loadTexture( "https://tutsplus.github.io/Beginners-Guide-to-Shaders/Part2/SIPI_Jelly_Beans.jpg" );

La prima riga deve essere impostata solo la prima volta. Qua possiamo mettere un qualsiasi URL dell'immagine.

Successivamente, vogliamo aggiungere la nostra texture agli oggetti uniforms.

1
uniforms.texture = {type:'t',value:tex};

Infine, vogliamo dichiarare la nostra variabile uniform nel nostro codice shader e disegnarla allo stesso modo utilizzato nel precedente tutorial, con la funzione texture2D:

1
uniform vec2 resolution;
2
uniform sampler2D texture;
3
void main() {
4
	vec2 pos = gl_FragCoord.xy / resolution.xy;
5
    gl_FragColor = texture2D(texture,pos);
6
}

Dovremmo vedere alcune gustose caramelle di gelatina che si estendono lungo tutto il nostro schermo:

Potete sempre farne un fork ed editarlo su CodePen.

(Questa immagine è un'immagine di prova standard nel campo della computer grafica, prelevata dall'Istituto di Signal ed Image Processing della University of Southern California (da qui la sigla IPI). Sembra opportuno usarla come nostra immagine di prova per imparare gli shader grafici!)

Sfida: Potete fare in modo che la texture cambi da colore pieno a scala di grigio nel tempo? (Ancora, se siete bloccati, lo abbiamo già fatto nella prima parte di questa serie.)

Fase Bonus: Applicare Shaders su altri oggetti

Non c'è niente di speciale rispetto al piano che abbiamo creato. Avremmo potuto applicare il tutto al nostro cubo. In effetti, possiamo solo cambiare la riga della geometria del piano:

1
var geometry = new THREE.PlaneGeometry( 10, 10 );

in:

1
var geometry = new THREE.BoxGeometry( 1, 1, 1 );

Voila, caramelle di gelatina su di un cubo:

Potete sempre farne un fork ed editarlo su CodePen.

Ora potreste star pensando, "Aspetta, non mi sembra tanto corretta la proiezione della texture sul cubo!". E avreste ragione; se guardiamo indietro al nostra shader vedremo che quello abbiamo realmente davvero fatto è stato dire "mappa tutti i pixel di questa immagine sullo schermo". Il fatto è che, solo sul cubo, significa che i pixel che stanno fuori dalla sagoma vengono scartati.

Volendo applicare la texture in modo che appaia disegnata fisicamente sul cubo, dovremmo affrontare un sacco di lavoro come reinventarsi un motore 3D (che suona un pò sciocco considerando che stiamo già utilizzando un motore 3D ed infatti ci basterebbe chiedergli di disegnare la texture su ciascun lato singolarmente). Ma, essendo questa serie di tutorial incentrata più sull'utilizzo degli shader, non andremo oltre a scavare in dettagli del genere. (Se siete desiderosi di saperne di più, Udacity ha un grande corso sui fondamenti della grafica 3D)

Fase successiva

A questo punto, dovreste essere in grado di fare tutto ciò che abbiamo fatto in ShaderToy, solo che adesso abbiamo la libertà di usare qualsiasi texture che vogliamo su qualsiasi forma a piacere e auspicabilmente, su qualsiasi piattaforma scelta.

Con questa libertà, ora potremo fare cose come istituire un sistema di illuminazione, con luci e ombre che sembrino realistiche. Questo è ciò su cui ci concentreremo nella parte successiva, oltre a suggerimenti e tecniche per l'ottimizzazione degli shader!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.