Introduzione agli Shaders
È finalmente giunto il momento di immergersi nel mondo degli shaders. Sono essenziali per creare tutti i tipi di effetti visivi. In questo capitolo, impareremo cosa sono gli shaders, cosa possiamo ottenere con essi e come usarli in React Three Fiber.
Preludio
Prima di iniziare, voglio menzionare che gli shaders possono richiedere del tempo prima che ci si abitui. Funzionano in modo diverso rispetto al resto del codice che abbiamo scritto finora. È un nuovo modo di pensare e creare effetti visivi. Ma non preoccuparti, ti guiderò attraverso il processo, inizieremo con le basi e ci sposteremo gradualmente verso argomenti più avanzati.
Non scoraggiarti se non capisci tutto subito. È normale. È un nuovo concetto e richiede pratica per sentirsi più a proprio agio. Ti consiglio di prenderti il tuo tempo, sperimentare e fare pratica, ma te lo prometto, ne vale la pena! Gli shaders sono incredibilmente potenti e ti daranno il controllo per creare qualsiasi effetto visivo tu possa immaginare.
Inoltre, ognuno di noi ha stili di apprendimento differenti. Alcune persone imparano meglio leggendo, altre guardando video e altre ancora facendo pratica. Per questo argomento in particolare, fare riferimento a diverse fonti può essere molto utile. Condividerò con te delle risorse alla fine di questo capitolo per consolidare la tua conoscenza e andare oltre ciò che copriamo qui.
Spero di non averti spaventato e che tu sia entusiasta di imparare sugli shaders. Iniziamo!
Cosa sono gli Shaders?
Gli Shaders sono piccoli programmi che girano sulla GPU (Graphics Processing Unit). Sono scritti in un linguaggio chiamato GLSL (OpenGL Shading Language), che è simile a C.
Vengono utilizzati per posizionare i vertici di un mesh (Vertex Shader) e per colorare ogni pixel delle facce (Fragment Shader).
In realtà, abbiamo usato gli shaders per tutto il tempo. Quando creiamo un material, stiamo utilizzando uno shader. Ad esempio, quando creiamo un MeshBasicMaterial
, stiamo utilizzando uno shader che colora il mesh con un colore unico. Quando creiamo un MeshStandardMaterial
, stiamo utilizzando uno shader che simula l'illuminazione, le ombre e i riflessi.
Vertex Shader
Un vertex shader è un programma eseguito per ciascun vertice di una geometria. La sua responsabilità principale è trasformare i vertici dallo spazio 3D (il nostro mondo 3D) allo spazio 2D (il nostro schermo o viewport). Raggiunge questa trasformazione utilizzando diverse matrici:
- View Matrix: Questa matrice rappresenta la posizione e l'orientamento della camera nella scena. Trasforma i vertici dallo spazio mondo allo spazio camera.
- Projection Matrix: Questa matrice, o prospettica o ortografica, converte i vertici dallo spazio camera alle coordinate normalizzate del dispositivo (NDC), preparandoli per la proiezione finale sullo schermo 2D.
- Model Matrix: Questa matrice include la posizione, rotazione e scala di ciascun oggetto individuale nella scena. Trasforma i vertici dallo spazio oggetto allo spazio mondo.
Inoltre, il vertex shader incorpora anche la posizione originale del vertice e qualsiasi altro attributo ad esso associato.
Per ogni vertice della geometria, verrà eseguito il vertex shader.
Infine, la posizione trasformata del vertice nello spazio 2D viene restituita tramite la variabile predefinita gl_Position
. Dopo che tutti i vertici sono stati trasformati, la GPU interpola i valori tra di essi per generare le facce della geometria, che vengono poi rasterizzate e renderizzate sullo schermo.
Shader di Frammento
Un fragment shader, noto anche come pixel shader, è un programma eseguito per ogni frammento (o pixel) generato dal processo di rasterizzazione. Il suo compito principale è determinare il colore finale di ogni pixel sullo schermo.
Per ogni frammento generato durante la rasterizzazione, il fragment shader verrà eseguito.
Il fragment shader riceve valori interpolati dallo vertex shader, come colori, coordinate delle texture, normali e qualsiasi altro attributo associato ai vertici della geometria. Questi valori interpolati sono chiamati varyings, e forniscono informazioni sulle proprietà della superficie in ciascuna posizione del frammento.
Oltre ai valori interpolati, il fragment shader può anche campionare texture e accedere a variabili uniformi, che sono costanti per tutti i frammenti. Queste variabili uniform possono rappresentare parametri come posizioni delle luci, proprietà dei materiali o qualsiasi altro dato necessario per i calcoli di shading.
Torneremo su attributi e uniform più avanti in questa lezione.
Utilizzando i dati di input, il fragment shader esegue vari calcoli per determinare il colore finale del frammento. Questo può includere calcoli di illuminazione complessi, mapping delle texture, effetti di shading o qualsiasi altro effetto visivo desiderato nella scena.
Una volta completato il calcolo del colore, il fragment shader emette il colore finale del frammento utilizzando la variabile predefinita gl_FragColor
.
Ho cercato quanto possibile di spiegare gli shader in modo semplice e ho omesso di proposito alcuni dettagli tecnici, ma capisco che possa ancora sembrare un po' astratto. Creiamo uno shader semplice per vedere come funziona in pratica.
Il Tuo Primo Shader
Eseguiamo il pacchetto di partenza. Dovresti vedere questo frame con un piano nero al centro dello schermo:
Apri il file ShaderPlane.jsx
, contiene un semplice mesh con una geometria di piano e un materiale di base. Sostituiremo questo materiale con un materiale shader personalizzato.
shaderMaterial
Per creare un materiale shader, utilizziamo la funzione shaderMaterial
dalla Drei library.
Prende 3 parametri:
uniforms
: Un oggetto contenente le variabili uniform utilizzate nello shader. Lasciamolo vuoto per ora.vertexShader
: Una stringa contenente il codice GLSL per il vertex shader.fragmentShader
: Una stringa contenente il codice GLSL per il fragment shader.
Sulla parte superiore del nostro file, dichiariamo un nuovo materiale shader chiamato MyShaderMaterial
:
import { shaderMaterial } from "@react-three/drei"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` );
Esploreremo il codice dello shader tra un momento.
Per poterlo utilizzare dichiarativamente con React Three Fiber, utilizziamo il metodo extend
:
import { extend } from "@react-three/fiber"; // ... extend({ MyShaderMaterial });
Ora possiamo sostituire <meshBasicMaterial>
con il nostro nuovo materiale shader:
import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` ); extend({ MyShaderMaterial }); export const ShaderPlane = ({ ...props }) => { return ( <mesh {...props}> <planeGeometry args={[1, 1]} /> <myShaderMaterial /> </mesh> ); };
Dovresti vedere lo stesso piano nero di prima. Non abbiamo cambiato nulla, ma ora stiamo utilizzando un materiale shader personalizzato.
Per verificare che funzioni, cambiamo il colore che stiamo restituendo nel fragment shader. Sostituisci la linea gl_FragColor
con la seguente:
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor
è una variabile predefinita che rappresenta il colore del frammento. È un vec4
(un vettore con 4 componenti) che rappresenta i canali di colore rosso, verde, blu e alpha. Ogni componente è un float compreso tra 0 e 1.
Impostando il primo componente a 1.0
, impostiamo il canale rosso al suo valore massimo, il che risulterà in un colore rosso.
Dovresti vedere un piano rosso al centro dello schermo:
Congratulazioni! Hai appena creato il tuo primo materiale shader. È semplice, ma è un inizio.
Codice Shader
Prima di procedere, impostiamo il nostro ambiente di sviluppo per scrivere shader in modo più confortevole.
Hai due opzioni per scrivere il codice shader:
- Inline: Puoi scrivere il codice shader direttamente nel file JavaScript.
- Esterna: Puoi scrivere il codice shader in un file separato con estensione
.glsl
e importarlo nel tuo file JavaScript.
Solitamente, preferisco l'approccio inline all'interno dei file di material appropriati, in questo modo il codice shader è vicino alla dichiarazione del material.
Ma per rendere la scrittura e la lettura più semplice, consiglio di usare un evidenziatore di sintassi per GLSL. Puoi utilizzare l'estensione Comment tagget templates per Visual Studio Code. Evidenzierà il codice GLSL all'interno delle template strings.
Una volta installata, per abilitare l'evidenziatore di sintassi, è necessario aggiungere il seguente commento all'inizio del codice shader:
const MyShaderMaterial = shaderMaterial( {}, /* glsl */ ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /* glsl */ ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } ` );
Dovresti vedere il codice GLSL evidenziato nelle template strings:
Lo shader dei vertici in alto ora ha l'evidenziatore di sintassi corretto ed è più facile da leggere.
Questo è tutto ciò di cui hai bisogno per il codice shader inline. Puoi comunque decidere di usare un file esterno se preferisci mantenere il codice shader separato. Vediamo come farlo.
Importa file GLSL
Per prima cosa, crea una nuova cartella chiamata shaders
nella cartella src
. All'interno di questa cartella, crea due file: myshader.vertex.glsl
e myshader.fragment.glsl
e copia il rispettivo codice shader in ciascun file.
myshader.vertex.glsl
:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
myshader.fragment.glsl
:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
Sentiti libero di utilizzare la convenzione di denominazione che preferisci e di raggruppare i tuoi shader in sottocartelle se ne hai molti.
Poi, per poter importare questi file nel nostro file JavaScript, dobbiamo installare il plugin vite-plugin-glsl come dipendenza di sviluppo:
yarn add vite-plugin-glsl --dev
Poi, nel tuo file vite.config.js
, importa il plugin e aggiungilo all'array plugins
:
import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import glsl from "vite-plugin-glsl"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), glsl()], });
Ora puoi importare i file GLSL nel tuo file JavaScript e usarli come codice shader:
import myShaderFragment from "./shaders/myshader.fragment.glsl"; import myShaderVertex from "./shaders/myshader.vertex.glsl"; const MyShaderMaterial = shaderMaterial({}, myShaderVertex, myShaderFragment);
Ora che abbiamo un modo comodo per scrivere e importare il codice shader, possiamo iniziare a esplorare le diverse parti del codice shader.
GLSL
Il codice degli shader è scritto in GLSL (OpenGL Shading Language). È un linguaggio simile a C, vediamo le basi.
Tipi
GLSL ha diversi tipi, ma i più comuni sono:
- bool: Un valore booleano (
true
ofalse
). - int: Un numero intero.
- float: Un numero a virgola mobile.
- vettori: Una collezione di numeri.
vec2
è una collezione di 2 numeri a virgola mobile (x
ey
),vec3
è una collezione di 3 numeri a virgola mobile (x
,y
ez
), evec4
è una collezione di 4 numeri a virgola mobile (x
,y
,z
ew
). Invece di utilizzarex
,y
,z
ew
, puoi anche usarer
,g
,b
ea
per i colori; sono intercambiabili. - matrici: Una collezione di vettori. Ad esempio,
mat2
è una collezione di 2 vettori,mat3
è una collezione di 3 vettori, emat4
è una collezione di 4 vettori.
Swizzling e manipolazioni
Puoi accedere ai componenti di un vettore utilizzando lo swizzling. Ad esempio, puoi creare un nuovo vettore utilizzando i componenti di un altro vettore:
vec3 a = vec3(1.0, 2.0, 3.0); vec2 b = a.xy;
In questo esempio, b
sarà un vettore con i componenti x
e y
di a
.
Puoi anche usare lo swizzling per modificare l'ordine dei componenti:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = a.zyx;
In questo esempio, b
sarà uguale a vec3(3.0, 2.0, 1.0)
.
Per creare un nuovo vettore con tutti i componenti uguali, puoi utilizzare il costruttore:
vec3 a = vec3(1.0);
In questo esempio, a
sarà uguale a vec3(1.0, 1.0, 1.0)
.
Operatori
GLSL dispone dei comuni operatori aritmetici: +
, -
, *
, /
, +=
, /=
, *=
e dei comuni operatori di confronto: ==
, !=
, >
, <
, >=
, <=
.
Devono essere usati con i tipi corretti. Ad esempio, non puoi sommare un intero a un float, devi prima convertire l'intero in un float:
int a = 1; float b = 2.0; float c = float(a) + b;
Puoi eseguire operazioni anche su vettori e matrici:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = vec3(4.0, 5.0, 6.0); vec3 c = a + b;
Che equivale a:
vec3 c = vec3(a.x + b.x, a.y + b.y, a.z + b.z);
Funzioni
Il punto di ingresso degli shader vertex e fragment è la funzione main
. È la funzione che verrà eseguita quando lo shader viene chiamato.
void main() { // Il tuo codice qui }
void è il tipo di ritorno della funzione. Significa che la funzione non ritorna nulla.
Puoi definire anche le tue funzioni:
float add(float a, float b) { return a + b; }
Puoi quindi chiamare questa funzione nella funzione main
:
void main() { float result = add(1.0, 2.0); // ... }
GLSL fornisce molte funzioni integrate per operazioni comuni come sin
, cos
, max
, min
, abs
, round
, floor
, ceil
, e molte altre utili come mix
, step
, length
, distance
e altre ancora.
Scopriremo quelle essenziali e ci eserciteremo con esse nella prossima lezione.
Loop e Condizioni
GLSL supporta i loop for
e le istruzioni if
. Funzionano in modo simile a JavaScript:
for (int i = 0; i < 10; i++) { // Il tuo codice qui } if (condition) { // Il tuo codice qui } else { // Il tuo codice qui }
Logging / Debugging
Poiché i programmi shader vengono eseguiti in parallelo per ogni vertice e frammento, non è possibile utilizzare console.log
per eseguire il debug del codice né aggiungere breakpoint. Questo rende difficile il debug degli shader.
Un modo comune per effettuare il debug degli shader è utilizzare gl_FragColor
per visualizzare i valori delle variabili.
Errori di compilazione
Se commetti un errore nel tuo codice shader, vedrai un errore di compilazione nella console. Ti indicherà la linea e il tipo di errore. Non è sempre facile da capire, ma è un buon modo per sapere dove cercare il problema.
Rimuoviamo il canale alfa da gl_FragColor
e vediamo cosa succede:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0); }
Dovresti vedere un errore di compilazione nella console:
Ci indica che gl_FragColor
si aspetta 4 componenti, ma ne abbiamo forniti solo 3.
Non dimenticare di ripristinare il canale alfa a 1.0
per rimuovere l'errore.
Uniforms
Per passare dati dal codice JavaScript allo shader utilizziamo gli uniforms. Sono costanti su tutti i vertici e i frammenti.
La projectionMatrix
, modelViewMatrix
e position
sono esempi di uniformi predefiniti che vengono automaticamente passati allo shader.
Creiamo un uniform personalizzato per passare un colore allo shader. Lo useremo per colorare il piano. Lo chiameremo uColor
. È una buona pratica usare un prefisso u
nel nome dell'uniform per chiarire nel nostro codice che si tratta di un uniform.
Prima, dichiariamolo nell'oggetto uniforms di shaderMaterial
:
import { Color } from "three"; // ... const MyShaderMaterial = shaderMaterial( { uColor: new Color("pink"), } // ... ); // ...
Successivamente, lo possiamo utilizzare nel fragment shader:
uniform vec3 uColor; void main() { gl_FragColor = vec4(uColor, 1.0); }
Dovresti vedere il piano colorato in rosa:
Qui, il colore rosa è il valore predefinito dell'uniform. Possiamo cambiarlo direttamente sul materiale:
<MyShaderMaterial uColor={"lightblue"} />
Il piano ora è colorato in azzurro.
Sia il vertex shader che il fragment shader possono accedere agli uniforms. Aggiungiamo il tempo come secondo uniform nel vertex shader per muovere il piano su e giù:
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.