Verplaatsingsshaders gebruiken om een ​​onderwatereffect te maken

Ondanks hun bekendheid is het creëren van waterstanden een aloude traditie in de geschiedenis van videogames, of dat nu is om de spelmechanica op te schudden of gewoon omdat water zo mooi is om naar te kijken. Er zijn verschillende manieren om een ​​onderwatergevoel te produceren, van eenvoudige beelden (zoals het blauw verkleuren van het scherm) tot mechanica (zoals langzame beweging en zwakke zwaartekracht). 

We gaan kijken naar vervorming als een manier om de aanwezigheid van water visueel te communiceren (stel je voor dat je aan de rand van een plas staat en naar dingen binnenin kijkt - dat is het soort effect dat we willen recreëren). Je kunt hier een demo van de definitieve look bekijken op CodePen.

Ik gebruik Shadertoy tijdens de tutorial, zodat je deze kunt volgen in je browser. Ik zal proberen het redelijk platformonafhankelijk te houden, zodat je kunt implementeren wat je hier leert in elke omgeving die grafische shaders ondersteunt. Aan het einde geef ik een aantal implementatietips en de JavaScript-code die ik heb gebruikt om het bovenstaande voorbeeld met de Phaser-gamebibliotheek te implementeren.

Het ziet er misschien een beetje ingewikkeld uit, maar het effect zelf is slechts een paar regels code! Het is niets meer dan verschillende verplaatsingseffecten samengebracht. We beginnen helemaal opnieuw en zien precies wat dat betekent.

Een basisafbeelding weergeven

Ga naar Shadertoy en maak een nieuwe shader. Voordat we enige vervorming kunnen toepassen, moeten we een afbeelding renderen. We weten uit eerdere tutorials dat we alleen een afbeelding moeten selecteren in een van de onderste kanalen op de pagina en dit moeten koppelen aan het scherm met texture2D:

vec2 uv = fragCoord.xy / iResolution.xy; // Haal de genormaliseerde positie van de huidige pixel op fragColor = texture2D (iChannel0, uv); // Haal de kleur van de huidige pixel in de textuur en stel deze in op de kleur op het scherm

Dit is wat ik heb uitgekozen:

Onze eerste verplaatsing

Wat gebeurt er als in plaats van alleen de pixel op positie wordt weergegeven uv, we geven de pixel weer UV + vec2 (0.1,0.0)?

Het is altijd het gemakkelijkst om te denken in termen van wat er gebeurt op één enkele pixel bij het werken met shaders. Gegeven elke positie op het scherm, zal het de kleur van een pixel naar rechts trekken in plaats van de oorspronkelijke kleur in de textuur te tekenen. Dat betekent dat alles visueel wordt verschoven links. Probeer het!

Standaard stelt Shadertoy de wrap-modus voor alle texturen in op herhaling. Dus als u een pixel aan de rechterkant van de meest rechtse pixel probeert te samplen, wordt deze eenvoudigweg omwikkeld. Hier heb ik het veranderd klem (wat je kunt doen via het tandwielpictogram op het vak waar je de textuur hebt geselecteerd).

Uitdaging: kun je het hele beeld langzaam naar rechts laten bewegen? Hoe zit het met heen en weer bewegen? Hoe zit het in een cirkel? 

Hint: Shadertoy geeft je een variabele in de looptijd genoemd iGlobalTime.

Niet-uniforme verplaatsing

Het verplaatsen van een hele afbeelding is niet erg spannend en vereist niet de zeer parallelle kracht van de GPU. Wat als we in plaats van elke positie met een vaste hoeveelheid (zoals 0.1) te verplaatsen, we verschillende pixels met verschillende bedragen hebben verplaatst?

We hebben een variabele nodig die op de een of andere manier uniek is voor elke pixel. Elke variabele die u declareert of die u doorgeeft, varieert niet tussen de pixels. Gelukkig hebben we al iets dat op deze manier varieert: de pixel zelf X en Y. Probeer dit:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = uv.x; // Verplaats de y naar de x van de huidige pixel x fragColor = texture2D (iChannel0, uv);

We compenseren elke pixel verticaal met de x-waarde. De meest linkse pixels krijgen de minste offset (0) terwijl de meest rechtse pixel de maximale offset krijgt (1).

Nu hebben we een waarde die varieert in de afbeelding van 0 tot 1. We gebruiken dit om de pixels naar beneden te duwen, dus we krijgen deze inslag. Nu voor je volgende uitdaging!

Uitdaging: kun je dit gebruiken om een ​​golf te maken? (Zoals hieronder afgebeeld)

Hint: uw offset-variabele gaat van 0 tot 1. U wilt dat deze in plaats daarvan periodiek van -1 naar 1 gaat. De cosinus / sinusfunctie is daarvoor een perfecte keuze.

Tijd toevoegen

Als je het golfeffect hebt uitgezocht, probeer het heen en weer te bewegen door te vermenigvuldigen met onze tijdvariabele! Hier is mijn poging tot nu toe:

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

Ik vermenigvuldig uv.x door een groot aantal (25) om de frequentie van de golf te regelen. Ik schaal het vervolgens door te vermenigvuldigen met 0,06, dus dat is de maximale amplitude. Ten slotte vermenigvuldig ik me met de cosinus van de tijd, zodat het periodiek heen en weer gaat.

Opmerking: als je echt wilt bevestigen dat onze vervorming een sinusgolf volgt, verander dat dan van 0,06 naar een 1,0 en kijk je naar het maximum!

Uitdaging: Kun je uitvinden hoe je het sneller kunt laten wiebelen??

Hint: Het is hetzelfde concept dat we gebruikten om de frequentie van de golf ruimtelijk te verhogen.

Terwijl je bezig bent, is een ander ding dat je kunt proberen hetzelfde toe te passen uv.x zo goed, dus het verstoort zowel de x als de y (en misschien schakelt het cos voor sin's uit).

Nu dit is wiebelt in een golfbeweging, maar er is iets mis. Dat is niet helemaal hoe water zich gedraagt ​​...

Een andere manier om tijd toe te voegen

Water moet er uitzien alsof het stroomt. Wat we nu hebben, gaat gewoon heen en weer. Laten we onze vergelijking opnieuw bekijken:

Onze frequentie verandert niet, wat goed is voor nu, maar we willen ook niet dat onze amplitude verandert. We willen dat de golf dezelfde vorm behoudt, maar dan verhuizing over het scherm.

Om te zien waar in onze vergelijking we willen compenseren, denk na over wat bepaalt waar de golf begint en eindigt. uv.x is de afhankelijke variabele in die zin. Waar dan ook uv.x is pi / 2, er zal geen verplaatsing zijn (sinds cos (pi / 2) = 0), en waar uv.x is in de buurt pi / 2, dat is maximale verplaatsing.

Laten we onze vergelijking een beetje aanpassen:

Nu zijn zowel onze amplitude als onze frequentie gefixeerd, en het enige dat varieert, is de positie van de golf zelf. Met dat beetje theorie uit de weg, tijd voor een uitdaging!

Uitdaging: implementeer deze nieuwe vergelijking en pas de coëfficiënten aan om een ​​mooie golvende beweging te krijgen.

Alles samenvoegen

Hier is mijn code voor wat we tot nu toe hebben:

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

Dit is in wezen het hart van het effect. We kunnen echter dingen blijven aanpassen om het er nog beter uit te laten zien. Er is bijvoorbeeld geen reden om de golf te variëren met alleen de x- of y-coördinaat. Je kunt beide veranderen, dus varieert het diagonaal! Hier is een voorbeeld:

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

Het leek een beetje repetitief dus schakelde ik de tweede cos voor een zonde om dat te repareren. Terwijl we bezig zijn, kunnen we ook proberen de amplitude een beetje te variëren:

zweven X = uv.x * 25. + iGlobalTime; zweven Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0.01 * cos (Y); uv.x + = sin (X-Y) * 0.01 * sin (Y);

En dat is ongeveer voor zover ik heb gekregen, maar je kunt altijd meer functies samenstellen en combineren om verschillende resultaten te krijgen!

Het toepassen op een gedeelte van het scherm

Het laatste dat ik in de arcering wil vermelden, is dat je in de meeste gevallen waarschijnlijk het effect op slechts een deel van het scherm moet toepassen in plaats van op het hele ding. Een eenvoudige manier om dat te doen is door een masker in te geven. Dit zou een afbeelding zijn die in kaart brengt welke delen van het scherm beïnvloed moeten worden. Degenen die transparant (of wit) zijn, kunnen niet worden beïnvloed en de ondoorzichtige (of zwarte) pixels kunnen het volledige effect hebben.

In Shadertoy kun je geen willekeurige afbeeldingen uploaden, maar je kunt deze renderen naar een afzonderlijke buffer en die doorgeven als een structuur. Hier is een Shadertoy-link waar ik het effect hierboven toepas op slechts de onderste helft van het scherm.

Het masker dat u invoert, hoeft geen statische afbeelding te zijn. Het kan een volledig dynamisch ding zijn; Zolang je het in realtime kunt weergeven en doorgeven aan de arcering, kan je water naadloos over het scherm bewegen of vloeien.

Implementeren in JavaScript

Ik heb Phaser.js gebruikt om deze arcering te implementeren. Je kunt de bron in deze live CodePen bekijken of een lokale kopie van deze repository downloaden.

Je kunt zien hoe ik de afbeeldingen manueel als uniform door geef, en ik moet ook de tijdvariabele zelf bijwerken.

Het grootste implementatiedetail om over na te denken, is waarop deze shader moet worden toegepast. Zowel in het Shadertoy-voorbeeld als in mijn JavaScript-voorbeeld heb ik slechts één afbeelding ter wereld. In een game heb je waarschijnlijk veel meer.

Met Phaser kunt u shaders toepassen op afzonderlijke objecten, maar u kunt het ook toepassen op het World-object, dat een stuk efficiënter is. Evenzo kan het op een ander platform een ​​goed idee zijn om al uw objecten op een buffer te plaatsen en die door de watershader te leiden, in plaats van deze toe te passen op elk afzonderlijk object. Op die manier functioneert het als een nabewerkingseffect.

Conclusie

Ik hoop dat je door het samenstellen van deze shader helemaal opnieuw een goed inzicht hebt gekregen in hoe veel complexe effecten worden opgebouwd door al deze verschillende kleine verplaatsingen in lagen te zetten!

Als laatste uitdaging is hier een soort waterrimpelingshader die steunt op dezelfde soort verplaatsingsideeën die we hebben gezien. Je zou kunnen proberen het uit elkaar te halen, de lagen uit te vouwen en erachter te komen wat elk stuk doet!