Använda Displacement Shaders för att skapa en undervattenseffekt

Trots sin anmärkningsvärt är skapandet av vattennivån en tradition i videospelets historia, oavsett om det är att skaka upp spelmekaniken eller bara för att vatten är så vackert att titta på. Det finns olika sätt att producera en undervatten känsla, från enkla visuella (som att tona skärmen blå) till mekanik (som långsam rörelse och svag gravitation). 

Vi ska titta på förvrängning som ett sätt att visuellt kommunicera närvaron av vatten (föreställ dig att du står vid kanten av en pool och peering på saker inuti - det är den typ av effekt vi vill återskapa). Du kan kolla in en demo av det sista utseendet här på CodePen.

Jag använder Shadertoy under hela handledningen så att du kan följa med i raden i din webbläsare. Jag ska försöka hålla den ganska plattformen agnostisk så att du kan genomföra det du lär dig här på vilken miljö som helst som stöder grafikskärare. I slutet kommer jag att ge några implementerings tips samt JavaScript-koden som jag brukade implementera exemplet ovan med Phaser-spelbiblioteket.

Det kan se lite komplicerat ut, men effekten är bara ett par rader kod! Det är inget mer än olika förskjutningseffekter sammansatta. Vi börjar från början och ser exakt vad det betyder.

Återgivning av en grundläggande bild

Gå över till Shadertoy och skapa en ny shader. Innan vi kan tillämpa någon snedvridning måste vi göra en bild. Vi vet från tidigare handledning att vi bara behöver välja en bild i en av de nedre kanalerna på sidan och kartlägga den på skärmen med texture2D:

vec2 uv = fragCoord.xy / iResolution.xy; // Hämta den aktuella pixelens normaliserade position fragColor = texture2D (iChannel0, uv); // Hämta den nuvarande pixelens färg i texturen och ställ in den på färgen på skärmen

Här är vad jag plockade:

Vår första förskjutning

Nu vad händer om istället för att bara göra pixeln på plats uv, vi gör pixeln på uv + vec2 (0.1,0.0)?

Det är alltid lättast att tänka på vad som händer på en enda pixel när man arbetar med shaders. Med tanke på vilken position som helst på skärmen, istället för att dra den ursprungliga färgen i texturen kommer den att dra en pixels färg till höger. Det betyder visuellt att allt blir förskjutet vänster. Försök!

Som standard anger Shadertoy omslaget på alla texturer till upprepa. Så om du försöker prova en pixel till höger om höger pixel, kommer den helt enkelt att slingras runt. Här bytte jag det till klämma (som du kan göra från kugghjulsikonen på rutan där du valde texturen).

Utmaning: Kan du göra hela bilden långsamt till höger? Vad sägs om att flytta fram och tillbaka? Vad sägs om i en cirkel? 

Hint: Shadertoy ger dig en körtid variabel som heter iGlobalTime.

Icke-enhetlig förskjutning

Att flytta en hel bild är inte särskilt spännande och kräver inte den höga parallella kraften hos GPU. Vad händer om istället för att förskjuta varje position med en bestämd mängd (som 0,1), förskjutna vi olika pixlar med olika mängder?

Vi behöver en variabel som på något sätt är unik för alla pixlar. Varje variabel du förklarar eller enhetlig du passerar in varierar inte mellan pixlar. Lyckligtvis har vi redan något som varierar så här: pixelns egna x och y. Prova detta:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = uv.x; // Flytta y med nuvarande pixelens x fragColor = texture2D (iChannel0, uv);

Vi förskjuter vertikalt varje pixel med dess x-värde. De vänstra pixlarna kommer att få minst offset (0) medan den högsta kan få maximal offset (1).

Nu har vi ett värde som varierar över bilden från 0 till 1. Vi använder det här för att trycka upp pixlarna, så vi får det här snedstället. Nu för din nästa utmaning!

Utmaning: Kan du använda detta för att skapa en våg? (Enligt bilden nedan)

Tips: Din förskjutningsvariabel går från 0 till 1. Du vill att den regelbundet ska gå från -1 till 1 istället. Cosinus / sinusfunktionen är ett perfekt val för det.

Lägger till tid

Om du tänkte ut vågseffekten, försök att göra det vinkla fram och tillbaka genom att multiplicera med vår tidsvariabel! Här är mitt försök till det hittills:

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

Jag multiplicerar uv.x med ett stort antal (25) för att styra vågens frekvens. Jag skala sedan ner den genom att multiplicera med 0,06, så det är maximal amplitud. Slutligen multiplicerar jag med tidens cosinus, för att få det periodiskt att vända fram och tillbaka.

Obs! Om du verkligen vill bekräfta att vår snedvridning följer en sinusvåg, ändra den 0,06 till en 1,0 och se den på sitt maximala!

Utmaning: Kan du räkna ut hur man gör det wiggle snabbare?

Hint: Det är samma koncept som vi brukade öka vågens frekvens spatalt.

Medan du är på det, kan du också göra en sak som du kan försöka uv.x också, så det snedvrider både x och y (och kanske byter cos för syndens).

Nu här är vinklar i en vågrörelse, men någonting är avstängt. Det är inte riktigt hur vatten beter sig ...

Ett annat sätt att lägga till tid

Vatten behöver se ut som om det strömmar. Det vi har just nu går bara fram och tillbaka. Låt oss undersöka vår ekvation igen:

Vår frekvens förändras inte, vilket är bra för nu, men vi vill inte att vår amplitude ska förändras heller. Vi vill att vågen ska hålla samma form, men till flytta över skärmen.

För att se var i vår ekvation vi vill kompensera, tänk på vad som bestämmer var vågen börjar och slutar. uv.x är den beroende variabeln i den meningen. Vart som helst uv.x är pi / 2, det blir ingen förskjutning (eftersom cos (pi / 2) = 0) och var uv.x är runt pi / 2, det kommer att vara maximal förskjutning.

Låt oss justera vår ekvation lite:

Nu är både vår amplitude och frekvens fixerad, och det enda som varierar kommer att vara vågens position. Med den där teorin ur vägen, dags för en utmaning!

Utmaning: Implementera den här nya ekvationen och tweak koefficienterna för att få en bra vågig rörelse.

Få alltid att falla på plats

Här är min kod för vad vi har hittills:

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

Nu är detta i grunden hjärtat av effekten. Vi kan dock hålla tweaking saker för att få det att se ännu bättre ut. Det finns till exempel ingen anledning att du måste variera vågen med bara x- eller y-koordinaten. Du kan ändra båda, så det varierar diagonalt! Här är ett exempel:

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;

Det såg lite repetitivt så jag bytte den andra cos för en synd att fixa det. Medan vi är i det, kan vi också försöka variera amplituden lite:

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

Och det handlar om så långt jag har fått, men du kan alltid kombinera och kombinera fler funktioner för att få olika resultat!

Applicera den på ett avsnitt på skärmen

Det sista jag vill nämna i skuggan är att i de flesta fall kommer du förmodligen att behöva applicera effekten till bara en del av skärmen istället för hela saken. Ett enkelt sätt att göra det är att passera i en mask. Detta skulle vara en bild som kartlägger vilka områden av skärmen som ska påverkas. De som är transparenta (eller vita) kan inte påverkas, och de opaka pixlarna (eller svarta) kan få full effekt.

I Shadertoy kan du inte ladda upp godtyckliga bilder, men du kan göra till en separat buffert och överföra det som en textur. Här är en Shadertoy-länk där jag applicerar effekten ovan till bara den nedre halvan av skärmen.

Masken du passerar in behöver inte vara en statisk bild. Det kan vara en helt dynamisk sak; så länge du kan göra det i realtid och skicka det till skuggaren, kan ditt vatten röra sig eller flyta hela skärmen sömlöst.

Implementera det i JavaScript

Jag använde Phaser.js för att implementera denna shader. Du kan kolla källan i den här live CodePen, eller hämta en lokal kopia från det här förvaret.

Du kan se hur jag skickar in bilderna manuellt som uniformer, och jag måste också uppdatera tidsvariabeln själv.

Den största implementeringsdetalj att tänka på är vad man ska tillämpa denna skuggning på. I både Shadertoy-exemplet och mitt JavaScript-exempel har jag bara en bild i världen. I ett spel kommer du förmodligen att ha mycket mer.

Phaser låter dig applicera shaders på enskilda objekt, men du kan också applicera det på världsobjektet, vilket är mycket mer effektivt. På samma sätt kan det vara en bra idé på en annan plattform att göra alla dina föremål till en viss buffert och skicka det genom vattenskärmen, istället för att applicera det på varje enskilt objekt. På så sätt fungerar det som en efterbehandlingseffekt.

Slutsats

Jag hoppas att jag gick igenom komponeringen av denna skuggning från början och gav dig en bra inblick i hur många komplexa effekter som byggs genom att laga alla dessa olika små förskjutningar!

Som en sista utmaning, här är en form av vattenkrypningsskuggare som bygger på samma slags förskjutningsideer som vi såg. Du kan försöka ta det ifrån varandra, expandera lagren och ta reda på vad varje bit gör!