Gli shader sono piccoli programmi eseguiti a livello di GPU, ampiamente utilizzati nella grafica computerizzata e nei videogiochi. Prendono come input le coordinate dei pixel e restituiscono un colore come output. Gli shader sono progettati per lavorare su più pixel contemporaneamente grazie alla parallelizzazione. Esistono diversi “dialetti” per gestire gli shader: uno dei più conosciuti e utilizzati è GLSL (OpenGL Shading Language), ampiamente supportato dai browser Web.
Cos’è GLSL e perché è utile per lavorare con gli shader
GLSL è un linguaggio utilizzato principalmente per scrivere shader grafici; è un dialetto specifico del linguaggio di programmazione C, progettato per operare sulla GPU. Grazie a GLSL si possono eseguire operazioni grafiche come la creazione di effetti visivi, la manipolazione delle texture, la definizione dell’illuminazione, l’applicazione di materiali e molte altre operazioni aggiuntive. La vasta gamma di tipi di dati, funzioni e strutture disponibili per lo sviluppatore permettono di scrivere shader sofisticati per creare effetti visivi complessi e dettagliati.
Il funzionamento di base degli shader
Le coordinate dei pixel negli shader sono normalizzate tra 0 e 1, con (0, 0) nell’angolo in basso a sinistra e (1, 1) nell’angolo in alto a destra. Queste coordinate sono comunemente indicate come “st” o “uv”. Gli shader possono intervenire su queste coordinate per generare vari effetti grafici.
Un esempio classico di shader, peraltro molto semplice, di shader è un gradiente in cui il componente rosso aumenta da sinistra a destra e il componente verde cresce dal basso verso l’alto. Il gradiente può essere creato in GLSL in questo modo:
precision highp float;
uniform vec2 resolution;
void main() {
// Coordinata del pixel normalizzata (da 0 a 1)
vec2 st = gl_FragCoord.xy / resolution.xy;// Rosso da sinistra a destra, verde dal basso all’alto
gl_FragColor = vec4(st.x, st.y, 0.0, 1.0); // RGBA
}
Per provare il codice GLSL nel browser, suggeriamo di utilizzare lo strumento chiamato GLSL Sandbox: basta fare clic sul pulsante New shader e incollare il codice visto in precedenza. L’effetto gradiente generato mediante lo shader è visualizzato come sfondo del codice.
Nel dettaglio, precision highp float
specifica la precisione in virgola mobile utilizzata all’interno dello shader. La riga uniform vec2 resolution
rappresenta la risoluzione del rendering target, ovvero le dimensioni della finestra o del canvas in cui lo shader è eseguito e visualizzato il suo risultato grafico. Questo valore è fornito esternamente al momento dell’esecuzione dello shader e può essere utilizzato per normalizzare le coordinate del pixel.
La variabile gl_FragCoord.xy
contiene le coordinate (x, y) del pixel attuale. Dividendo le coordinate per resolution.xy
, si normalizzano le coordinate del pixel nel range da 0 a 1 lungo l’asse x e y. Con st.x
si rappresenta la posizione orizzontale normalizzata del pixel (da sinistra a destra), mentre st.y
rappresenta la posizione verticale normalizzata del pixel (dal basso verso l’alto).
L’ultima riga assegna il colore al pixel attuale: gl_FragColor
è una variabile predefinita in GLSL che rappresenta il colore del pixel corrente. Con vec4()
si crea un vettore a 4 componenti (RGBA) dove st.x
rappresenta il rosso, st.y
il verde, `0.0` il blu e `1.0` l’opacità. Nel caso di questo shader, il rosso varia da sinistra a destra e il verde dal basso verso l’alto, creando così un gradiente.
Usare gli shader per creare forme complesse
Gli shader possono anche essere utilizzati per creare forme complesse combinando le distanze dai punti delle forme che si desiderano ottenere. Queste distanze possono essere calcolate utilizzando funzioni come distance()
che stabilisce appunto la distanza tra un punto e il centro di una forma. Inoltre, possono essere utilizzate funzioni come step()
per posizionare delle transizioni nette creando così forme piuttosto articolate.
La funzione step()
è una funzione che consente di effettuare una transizione netta tra due valori, producendo un output basato su un confronto di soglia. Nella programmazione degli shader, è utile per creare effetti di transizione nitidi o per definire le aree in cui un valore supera una determinata soglia.
Come si comportano le funzioni step() e distance()
Nel contesto dello shader preso in esame poco sopra, la funzione step()
è sfruttata per ottenere un bordo netto per una forma, come ad esempio un cerchio. Considerando la distanza di ciascun pixel dal centro del cerchio, si calcola la distanza usando la funzione distance()
che misura la distanza tra due punti nel piano.
Il risultato di distance()
è passato come input alla funzione step()
. Questa funzione utilizza due parametri: un valore soglia (threshold) e un valore da confrontare (value). Se il valore è maggiore della soglia step()
restituisce 1, altrimenti 0. Questo significa che si crea un cambio netto da 0 a 1 quando la distanza supera il valore della soglia. Nello shader preso in esame, se la distanza d
supera 0,25, il valore di s
diventa 1, altrimenti è 0.
Aggiunta dell’antialiasing, del movimento e dell’interattività
Questo approccio viene usato per generare un bordo netto o un contorno definito per la forma, ma può causare effetti di aliasing o “effetto sega” intorno ai bordi della figura a causa della transizione brusca tra 0 e 1. Per mitigare questo problema, è possibile ricorrere a un’altra funzione, smoothstep()
, che consente una transizione più graduale tra i valori, aiutando a ottenere bordi più morbidi e meno artefatti visivi.
Inoltre, è possibile utilizzare le funzioni trigonometriche come sin()
e cos()
per controllare il movimento delle forme in uno shader e creare effetti visivamente interessanti.
Si può anche aggiungere interattività agli shader consentendo all’utente di controllare i parametri come la posizione delle forme. Ad esempio, è possibile utilizzare le coordinate del mouse come input nel proprio shader e regolare i parametri in tempo reale.
Il meccanismo descritto può apparire impegnativo ma le funzioni a disposizione consentono di modellare con precisione e realizzare creatività davvero impressionanti. Provate a visitare Shadertoy per rendervi conto di ciò che è possibile creare, alla fine, con poche linee di codice.
Per approfondire, suggeriamo la lettura dell’articolo “A Journey Into Shaders” di Antoine Mayerowitz che guida alla scoperta di ulteriori caratteristiche degli shader.
Per richiamare uno shader da una pagina Web, è necessario utilizzare WebGL, una tecnologia che consente di utilizzare grafica 3D e 2D ad alte prestazioni all’interno del browser. Three.js è una libreria JavaScript per la creazione di grafica 3D che utilizza WebGL come backend ai fini del rendering. Include funzionalità per scrivere e utilizzare shader GLSL all’interno delle sue strutture di rendering.
Credit immagine in apertura: iStock.com/Ross Tomei