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:
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.
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
vargeometry=newTHREE.BoxGeometry(1,1,1);
2
varmaterial=newTHREE.MeshBasicMaterial({color:0x00ff00});//We make it green
3
varcube=newTHREE.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
functionrender(){
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 (OpenGLShading 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
varshaderCode="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
voidmain(){
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:
Ricordate: solo ciò che è stato caricato come stringa verrà analizzato come codice GLSL valido (cioè, void main () {...}. Tutto il resto è solo codice HTML liscio.)
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:
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:
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ì:
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
uniformvec2resolution;//Uniform variables must be declared here first
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
functionrender(){
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!
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
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
uniformvec2resolution;
2
uniformsampler2Dtexture;
3
voidmain(){
4
vec2pos=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:
(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:
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!
Currently a student at St. Olaf College in Northfield, Minnesota. Originally from Egypt. I've been making games for over 7 years. I'm super passionate about building tools or crafting experiences that make life a little better. I enjoy teaching, and am usually far more eloquent on paper.