Bruk Displacement Shaders til å lage en undervanns-effekt

Til tross for sin berømmelse er å skape vannstand en tradisjon i videospillets historie, enten det er å skape spillmekanikken eller bare fordi vann er så vakkert å se på. Det er ulike måter å produsere en undervannsfølelse på, fra enkle visualer (som tinting skjermen blå) til mekanikk (som langsom bevegelse og svak tyngdekraft). 

Vi skal se på forvrengning som en måte å visuelt kommunisere tilstedeværelsen av vann (forestille at du står ved kanten av et basseng og peering på ting inni - det er den typen effekt vi ønsker å gjenskape). Du kan sjekke ut en demonstrasjon av det endelige utseendet her på CodePen.

Jeg bruker Shadertoy gjennom hele opplæringen slik at du kan følge med rett i nettleseren din. Jeg vil prøve å holde det ganske plattform agnostisk, slik at du kan implementere det du lærer her på ethvert miljø som støtter grafikkskyggere. På slutten gir jeg noen implementerings tips samt JavaScript-koden jeg pleide å implementere eksemplet ovenfor med Phaser-spillbiblioteket.

Det kan se litt komplisert ut, men selve effekten er bare et par linjer med kode! Det er ikke noe mer enn forskjellige forskyvningseffekter samlet sammen. Vi starter fra bunnen av og ser nøyaktig hva det betyr.

Gir et grunnleggende bilde

Gå over til Shadertoy og skape en ny shader. Før vi kan bruke forvrengning, må vi gjøre et bilde. Vi vet fra tidligere opplæringsprogrammer at vi bare trenger å velge et bilde i en av de nederste kanalene på siden, og kartlegge den til skjermen med texture2D:

vec2 uv = fragCoord.xy / iResolution.xy; // Få den nåværende pikselens normaliserte posisjon fragColor = texture2D (iChannel0, uv); // Få den nåværende pixelens farge i tekstur, og sett den til fargen på skjermen

Her er det jeg plukket:

Vår første forskyvning

Nå skjer det hvis i stedet for bare å gi piksel på plass uv, vi gjør piksel på uv + vec2 (0.1,0.0)?

Det er alltid lettest å tenke på hva som skjer på en enkelt piksel når du arbeider med shaders. Gitt noen posisjon på skjermen, i stedet for å tegne den opprinnelige fargen i tekstur, kommer den til å tegne fargen på en piksel til høyre. Det betyr visuelt at alt blir forskjøvet venstre. Prøv det!

Som standard setter Shadertoy innpakningsmodus på alle teksturer til gjenta. Så hvis du prøver å prøve en piksel på høyre side av høyre piksel, vil den bare vikle rundt. Her forandret jeg det til klemme (som du kan gjøre fra tannhjulikonet på boksen der du valgte tekstur).

Utfordring: Kan du få hele bildet til å bevege seg sakte til høyre? Hva med å flytte frem og tilbake? Hva med i en sirkel? 

Hint: Shadertoy gir deg en kjøretidsvariabel som kalles iGlobalTime.

Ikke-enhetlig forskyvning

Flytte et helt bilde er ikke veldig spennende, og krever ikke den høye parallelle kraften til GPU. Hva om i stedet for å forskyve hver posisjon med en fast mengde (for eksempel 0,1), forskyver vi forskjellige piksler med forskjellige mengder?

Vi trenger en variabel som er unik for hver piksel. Enhver variabel du erklærer eller ensartet du passerer inn, vil ikke variere mellom piksler. Heldigvis har vi allerede noe som varierer slik: pikselets eget x og y. Prøv dette:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = uv.x; // Flytt yen med den nåværende pixelens x fragColor = texture2D (iChannel0, uv);

Vi justerer vertikalt hver piksel med sin x-verdi. De venstre pikslene vil få minst offset (0) mens den høyeste vil få maksimal offset (1).

Nå har vi en verdi som varierer over bildet fra 0 til 1. Vi bruker dette til å skyve pikslene ned, så vi får denne skråningen. Nå for din neste utfordring!

Utfordring: Kan du bruke dette til å lage en bølge? (Som vist nedenfor)

Tips: Din forskyvningsvariabel går fra 0 til 1. Du vil at den regelmessig skal gå fra -1 til 1 i stedet. Cosinus / sinus-funksjonen er et perfekt valg for det.

Legge til tid

Hvis du fant ut bølgeffekten, prøv å gjøre den vrikke frem og tilbake ved å multiplisere med vår tidsvariabel! Her er mitt forsøk på det så langt:

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

Jeg multipliserer uv.x med noen store tall (25) for å kontrollere frekvensen av bølgen. Jeg skaler den så ned ved å multiplisere med 0,06, så det er maksimal amplitude. Til slutt multipliserer jeg med tidens cosinus, for å få det til å jevne seg regelmessig frem og tilbake.

Merk: Hvis du virkelig vil bekrefte at vår forvrengning følger en sinusbølge, endrer du den 0,06 til en 1,0 og ser den på sitt maksimum!

Utfordring: Kan du finne ut hvordan du gjør det vri raskere?

Tips: Det er det samme konseptet vi brukte til å øke frekvensen av bølgen romlig.

Mens du er på det, kan du også gjøre det samme for andre ting du kan prøve uv.x også, så det forvrenger både på x og y (og kanskje bytter ut cos'en for syndens).

Nå dette er wiggling i en bølge bevegelse, men noe er av. Det er ikke helt hvordan vann oppfører seg ...

En annen måte å legge til tid

Vann trenger å se ut som om det flyter. Det vi har akkurat nå, går bare frem og tilbake. La oss undersøke vår ligning igjen:

Frekvensen vår endrer seg ikke, noe som er bra for nå, men vi ønsker ikke at amplitude skal endres. Vi ønsker at bølgen skal være i samme form, men til bevege seg over skjermen.

For å se hvor i vår ligning vi vil kompensere, tenk på hva som bestemmer hvor bølgen starter og slutter. uv.x er den avhengige variabelen i den forstand. Hvor enn uv.x er pi / 2, det vil ikke være noen forskyvning (siden cos (pi / 2) = 0), og hvor uv.x er rundt pi / 2, Det vil være maksimal forskyvning.

La oss justere vår likning litt:

Nå er både vår amplitude og frekvens fast, og det eneste som varierer vil være bølgens posisjon. Med den lille teorien ut av veien, tid til en utfordring!

Utfordring: Implementer denne nye ligningen og juster koeffisientene for å få en fin bølgete bevegelse.

Sette alt sammen

Her er koden min for hva vi har så langt:

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);

Nå er dette i hovedsak hjertet av effekten. Vi kan imidlertid fortsette å tilpasse ting slik at det ser enda bedre ut. For eksempel er det ingen grunn til at du må variere bølgen ved bare x- eller y-koordinaten. Du kan bytte begge deler, så det varierer diagonalt! Her er et eksempel:

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

Det så litt repeterende så jeg byttet den andre cos for en synd for å fikse det. Mens vi er i det, kan vi også prøve å variere amplitude litt:

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

Og det handler om så langt jeg har fått, men du kan alltid kombinere og kombinere flere funksjoner for å få forskjellige resultater!

Bruk den til en del av skjermen

Det siste jeg vil nevne i skyggen er at i de fleste tilfeller vil du sannsynligvis trenge å bruke effekten til bare en del av skjermen i stedet for hele greia. En enkel måte å gjøre det på er å passere i en maske. Dette er et bilde som kartlegger hvilke områder av skjermen som skal påvirkes. De som er gjennomsiktige (eller hvite) kan være upåvirket, og de ugjennomsiktige (eller svarte) pikslene kan ha full effekt.

I Shadertoy kan du ikke laste opp vilkårlig bilder, men du kan gjengi til en separat buffer og sende den inn som en tekstur. Her er en Shadertoy-kobling hvor jeg bruker effekten ovenfor til bare nederste halvdel av skjermen.

Masken du passerer inn trenger ikke å være et statisk bilde. Det kan være en helt dynamisk ting; så lenge du kan gjengi det i sanntid og sende det til skyggeren, kan vannet bevege deg eller strømme gjennom hele skjermen sømløst.

Implementere det i JavaScript

Jeg brukte Phaser.js til å implementere denne shader. Du kan sjekke kilden i denne live CodePen, eller laste ned en lokal kopi fra dette depotet.

Du kan se hvordan jeg passerer bildene manuelt som uniformer, og jeg må også oppdatere tidsvariabelen selv.

Den største gjennomføringsdetaljene å tenke på, er hva du skal bruke denne skyggen på. I både Shadertoy-eksemplet og mitt JavaScript-eksempel har jeg bare ett bilde i verden. I et spill vil du sannsynligvis ha mye mer.

Phaser lar deg bruke shaders til individuelle objekter, men du kan også bruke den på verdensobjektet, noe som er mye mer effektivt. På samme måte kan det være en god ide på en annen plattform å gjengi alle objekter på noe buffer, og passere det gjennom vannskyggen, i stedet for å bruke det på hvert enkelt objekt. På den måten fungerer det som en etterbehandlingseffekt.

Konklusjon

Jeg håper å gå gjennom komponere denne shader fra grunnen ga deg litt godt innblikk i hvordan mange komplekse effekter er bygget ved lagring alle disse forskjellige små forskyvninger!

Som en siste utfordring, er det en slags rippelskygge som er avhengig av de samme forflytningsideer som vi så. Du kan prøve å ta det fra hverandre, utfolde lagene, og finne ut hva hvert stykke gjør!