Usare gli ombreggiatori di spostamento per creare un effetto sott'acqua

Nonostante la loro notorietà, la creazione di livelli d'acqua è una tradizione consolidata nella storia dei videogiochi, sia che si tratti di scuotere le meccaniche di gioco o semplicemente perché l'acqua è così bella da guardare. Esistono vari modi per produrre una sensazione subacquea, da semplici effetti visivi (come la colorazione dello schermo blu) alla meccanica (come movimento lento e gravità debole). 

Vedremo la distorsione come un modo per comunicare visivamente la presenza di acqua (immagina di essere sul bordo di una piscina e di scrutare le cose all'interno - è il tipo di effetto che vogliamo ricreare). Puoi controllare una demo del look finale qui su CodePen.

Userò Shadertoy durante il tutorial in modo da poterlo seguire nel tuo browser. Cercherò di mantenere la piattaforma abbastanza agnostica in modo da poter implementare ciò che impari qui in qualsiasi ambiente che supporti gli shader grafici. Alla fine, fornirò alcuni suggerimenti sull'implementazione e il codice JavaScript che ho usato per implementare l'esempio sopra con la libreria di giochi Phaser.

Potrebbe sembrare un po 'complicato, ma l'effetto stesso è solo un paio di righe di codice! Non è altro che diversi effetti di spostamento combinati insieme. Iniziamo da zero e vediamo esattamente cosa significa.

Rendering di un'immagine di base

Dirigiti verso Shadertoy e crea un nuovo shader. Prima di poter applicare qualsiasi distorsione, dobbiamo eseguire il rendering di un'immagine. Sappiamo dai tutorial precedenti che abbiamo solo bisogno di selezionare un'immagine in uno dei canali in basso nella pagina e mapparla sullo schermo con Texture2D:

vec2 uv = fragCoord.xy / iResolution.xy; // Ottieni la posizione normalizzata del pixel corrente fragColor = texture2D (iChannel0, uv); // Ottieni il colore del pixel corrente nella texture e impostalo sul colore sullo schermo

Ecco cosa ho scelto:

Il nostro primo dislocamento

Ora cosa succede se invece di limitarsi a rendere il pixel in posizione uv, rendiamo il pixel a uv + vec2 (0.1,0.0)?

È sempre più facile pensare in termini di ciò che accade su un singolo pixel quando si lavora con gli shader. Data qualsiasi posizione sullo schermo, invece di disegnare il colore originale nella texture, disegnerà il colore di un pixel alla sua destra. Ciò significa che, visivamente, tutto viene spostato sinistra. Provalo!

Per impostazione predefinita, Shadertoy imposta la modalità wrap su tutte le trame su ripetere. Quindi, se provi a campionare un pixel a destra del pixel più a destra, si avvolge semplicemente. Qui, l'ho cambiato morsetto (che puoi fare dall'icona dell'ingranaggio sulla scatola in cui hai selezionato la texture).

Sfida: puoi spostare l'intera immagine lentamente verso destra? Che ne dici di andare avanti e indietro? Che dire in un cerchio? 

Suggerimento: Shadertoy ti dà una variabile tempo di esecuzione chiamata iGlobalTime.

Dislocamento non uniforme

Spostare un'intera immagine non è molto eccitante e non richiede la potenza altamente parallela della GPU. Cosa succede se invece di spostare ogni posizione di una quantità fissa (come 0,1), abbiamo spostato diversi pixel di diverse quantità?

Abbiamo bisogno di una variabile che sia in qualche modo unica per ogni pixel. Qualsiasi variabile dichiarata o uniforme da te inserita non varierà tra i pixel. Fortunatamente, abbiamo già qualcosa che varia in questo modo: il pixel stesso X e y. Prova questo:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = uv.x; // Sposta y per il pixel corrente x fragColor = texture2D (iChannel0, uv);

Stiamo compensando verticalmente ogni pixel con il suo valore x. I pixel più a sinistra otterranno il minor offset (0) mentre il più a destra otterrà il massimo offset (1).

Ora abbiamo un valore che varia attraverso l'immagine da 0 a 1. Lo stiamo usando per spingere verso il basso i pixel, così otteniamo questa inclinazione. Ora per la tua prossima sfida!

Sfida: puoi usarlo per creare un'onda? (Come nella foto sotto)

Suggerimento: la variabile di offset va da 0 a 1. Si desidera che passi periodicamente da -1 a 1. La funzione coseno / seno è una scelta perfetta per questo.

Aggiungere tempo

Se hai capito l'effetto dell'onda, prova a farlo oscillare avanti e indietro moltiplicando per la nostra variabile temporale! Ecco il mio tentativo fino ad ora:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25.) * 0,06 * cos (iGlobalTime); fragColor = texture2D (iChannel0, uv);

Mi moltiplico uv.x da qualche grande numero (25) per controllare la frequenza dell'onda. Poi lo ridimensiono moltiplicando per 0,06, quindi questa è l'ampiezza massima. Infine, mi moltiplico per il coseno del tempo, per farlo periodicamente andare avanti e indietro.

Nota: se vuoi veramente confermare che la nostra distorsione sta seguendo un'onda sinusoidale, cambia da 0.06 a 1.0 e guardala al massimo!

Sfida: puoi capire come renderlo più veloce?

Suggerimento: è lo stesso concetto usato per aumentare la frequenza dell'onda spazialmente.

Mentre ci sei, un'altra cosa che puoi provare è applicare la stessa cosa uv.x pure, quindi distorce sia la x che la y (e forse cambia i cos per i sin).

Ora questo è dimenando in un moto ondoso, ma qualcosa non funziona. Non è proprio come si comporta l'acqua ...

Un modo diverso per aggiungere tempo

L'acqua ha bisogno di sembrare come se scorre. Quello che abbiamo adesso sta andando avanti e indietro. Esaminiamo nuovamente la nostra equazione:

La nostra frequenza non sta cambiando, il che è positivo per ora, ma non vogliamo neanche che la nostra ampiezza cambi. Vogliamo che l'onda mantenga la stessa forma, ma a mossa attraverso lo schermo.

Per vedere dove vogliamo compensare la nostra equazione, pensiamo a cosa determina dove inizia e finisce l'onda. uv.x è la variabile dipendente in questo senso. Dovunque uv.x è pi / 2, non ci sarà spostamento (poiché cos (pi / 2) = 0), e dove uv.x è in giro pi / 2, quello sarà il dislocamento massimo.

Analizziamo un po 'la nostra equazione:

Ora sia la nostra ampiezza sia la frequenza sono fisse, e l'unica cosa che varia sarà la posizione dell'onda stessa. Con quel pezzo di teoria tolto, tempo per una sfida!

Sfida: implementa questa nuova equazione e modifica i coefficienti per ottenere un bel movimento ondulato.

Mettere tutto insieme

Ecco il mio codice per quello che abbiamo ottenuto finora:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25. + iGlobalTime) * 0,01; uv.x + = cos (uv.y * 25. + iGlobalTime) * 0,01; fragColor = texture2D (iChannel0, uv);

Ora questo è essenzialmente il cuore dell'effetto. Tuttavia, possiamo continuare a perfezionare le cose per renderlo ancora più bello. Ad esempio, non c'è motivo per cui devi variare l'onda solo con la coordinata x o y. Puoi cambiare entrambi, quindi varia in diagonale! Ecco un esempio:

float X = uv.x * 25. + iGlobalTime; float Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0,01; uv.x + = sin (X-Y) * 0,01;

Sembrava un po 'ripetitivo quindi ho cambiato il secondo cos per un peccato per sistemarlo. Mentre ci siamo, possiamo anche provare a variare leggermente l'ampiezza:

float X = uv.x * 25. + iGlobalTime; float Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0,01 * cos (Y); uv.x + = sin (X-Y) * 0,01 * sin (Y);

E questo è quanto ho ottenuto, ma puoi sempre combinare e combinare più funzioni per ottenere risultati diversi!

Applicandolo a una sezione dello schermo

L'ultima cosa che voglio menzionare nello shader è che nella maggior parte dei casi, probabilmente avrai bisogno di applicare l'effetto a solo una parte dello schermo invece dell'intera cosa. Un modo semplice per farlo è passare una maschera. Questa sarebbe un'immagine che mappa quali aree dello schermo dovrebbero essere influenzate. Quelli che sono trasparenti (o bianchi) non possono essere modificati e i pixel opachi (o neri) possono avere l'effetto completo.

In Shadertoy, non puoi caricare immagini arbitrarie, ma puoi eseguire il rendering in un buffer separato e passarlo come trama. Ecco un link Shadertoy in cui applico l'effetto sopra alla metà inferiore dello schermo.

La maschera che si passa non ha bisogno di essere un'immagine statica. Può essere una cosa completamente dinamica; finché puoi renderlo in tempo reale e passarlo allo shader, l'acqua può muoversi o fluire attraverso lo schermo senza problemi.

Implementandolo in JavaScript

Ho usato Phaser.js per implementare questo shader. Puoi controllare la fonte in questo codice Code in tempo reale o scaricare una copia locale da questo repository.

Puoi vedere come faccio a passare le immagini manualmente come uniformi e devo anche aggiornare la variabile temporale.

Il più grande dettaglio di implementazione a cui pensare è come applicare questo shader a. Sia nell'esempio di Shadertoy che nel mio esempio di JavaScript, ho solo un'immagine al mondo. In un gioco, probabilmente ne avrai molto di più.

Phaser ti consente di applicare shader a singoli oggetti, ma puoi anche applicarlo all'oggetto world, che è molto più efficiente. Allo stesso modo, potrebbe essere una buona idea su un'altra piattaforma per il rendering di tutti gli oggetti su un buffer e passarlo attraverso lo shader, invece di applicarlo a ogni singolo oggetto. In questo modo funziona come un effetto di post-elaborazione.

Conclusione

Spero che passare da zero a comporre questo shader ci abbia dato una buona idea di come molti effetti complessi siano costruiti sovrapponendo tutti questi piccoli spostamenti!

Come ultima sfida, ecco una sorta di shader di ripple dell'acqua che si basa sullo stesso tipo di idee di spostamento che abbiamo visto. Potresti provare a smontarlo, dispiegare gli strati e capire cosa fa ogni pezzo!