Opprett en glødende, flytende lavvann ved hjelp av Bézier-kurver og shaders

Mesteparten av tiden, ved hjelp av konvensjonelle grafiske teknikker, er den riktige veien å gå. Noen ganger kan imidlertid eksperiment og kreativitet på grunnleggende nivåer av en effekt være gunstig for spillets stil, slik at det skiller seg ut mer. I denne opplæringen skal jeg vise deg hvordan du lager en animert 2D lava-elv ved hjelp av Bézier-kurver, tilpasset strukturert geometri og vertex shaders.

Merk: Selv om denne opplæringen er skrevet med AS3 og Flash, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.


Endelig resultatforhåndsvisning

Klikk på Pluss-tegnet for å åpne flere alternativer: Du kan justere tykkelsen og hastigheten til elven, og dra kontrollpunkter og plasseringspunkter rundt.

Ingen Flash? Sjekk ut YouTube-videoen i stedet:


Setup

Demo implementeringen ovenfor bruker AS3 og Flash med Starling Framework for GPU akselerert rendering og Feathers bibliotek for UI elementer. I vår første scene skal vi legge et bakgrunnsbilde og et forgrunnsrockbilde. Senere kommer vi til å legge til en elv, setter den inn mellom de to lagene.


Geometry

Elver er dannet av komplekse naturlige prosesser for interaksjon mellom en væskemasse og bakken under den. Det ville være upraktisk å gjøre en fysisk korrekt simulering for et spill. Vi vil bare ha den rette visuelle representasjonen, og for å gjøre det, skal vi bruke en forenklet modell av en elv.

Modellering av elva som en kurve er en av løsningene vi kan bruke, noe som gjør oss i stand til å ha god kontroll og oppnå et meanderende utseende. Jeg valgte å bruke kvadratiske Bézier kurver for å holde ting enkelt.

Bézier-kurver er parametriske kurver som ofte brukes i datagrafikk; i kvadratiske Bézier-kurver, går kurven gjennom to spesifiserte punkter, og dens form bestemmes av det tredje punktet, som vanligvis kalles et kontrollpunkt.

Som vist ovenfor går kurven gjennom posisjonspunkter mens kontrollpunktet styrer kurset det tar. For eksempel definerer styrpunktet direkte mellom stillingspunktene en rett linje, mens andre verdier for kontrollpunktet "tiltrekker seg" kurven for å gå nær det punktet.

Denne typen kurve er definert ved hjelp av følgende matematiske formel:

[latex] Stor B (t) = (1 - t) ^ 2 P_0 + (2t - 2t ^ 2) C + t ^ 2 P_1 [/ latex]

På t = 0 er vi på begynnelsen av kurven vår; ved t = 1 er vi på slutten.

Teknisk skal vi bruke flere Bézier kurver hvor slutten av en er starten på den andre, danner en kjede.

Nå må vi løse problemet med å faktisk vise elven vår. Kurver har ingen tykkelse, så vi skal bygge en geometrisk primitiv rundt den.

Først trenger vi en måte å ta kurve på og konvertere den til linjesegmenter. For å gjøre dette tar vi våre poeng og plugger dem i den matematiske definisjonen av kurven. Den ryddige tingen om dette er at vi enkelt kan legge til en parameter for å kontrollere kvaliteten på denne operasjonen.

Her er koden for å generere poengene fra definisjonen av kurven:

 // Beregn punkt fra kvadratisk Bezier uttrykk privat funksjon quadraticBezier (P0: Punkt, P1: Punkt, C: Punkt, t: Nummer): Punkt var x = (1 - t) * (1 - t) * P0.x + (2 - 2 * t) * t * Cx + t * t * P1.x; var y = (1 - t) * (1 - t) * P0.y + (2 - 2 * t) * t * C.y + t * t * P1.y; returnere nytt punkt (x, y); 

Og her er hvordan du konverterer kurven til linjesegmenter:

 // Dette er en metode som bruker en liste med noder // Hver node er definert som: posisjon, kontroll offentlig funksjon convertToPoints (kvalitet: Nummer = 10): Vector. var poeng: Vector. = Ny Vector. (); var presisjon: tall = 1 / kvalitet; // Pass gjennom alle noder for å generere linjesegmenter for (var i: int = 0; i < _nodes.length - 1; i++)  var current:CurveNode = _nodes[i]; var next:CurveNode = _nodes[i + 1]; // Sample Bezier curve between two nodes // Number of steps is determined by quality parameter for (var step:Number = 0; step < 1; step += precision)  var newPoint:Point = quadraticBezier(current.position, next.position, current.control, step); points.push(newPoint);   return points; 

Vi kan nå ta en vilkårlig kurve og konvertere den til et tilpasset antall linjesegmenter - jo flere segmenter, jo høyere er kvaliteten:

For å komme til geometrien skal vi generere to nye kurver basert på den opprinnelige. Deres posisjon og kontrollpunkter vil bli flyttet med en normal vektorforskyvningsverdi, som vi kan tenke på som tykkelsen. Den første kurven vil bli flyttet i negativ retning, mens andre flyttes i positiv retning.

Vi bruker nå funksjonen som ble definert tidligere for å lage linjesegmenter som danner kurver. Dette vil danne en grense rundt den opprinnelige kurven.

Hvordan gjør vi dette i kode? Vi må beregne normaler for posisjons- og kontrollpunkter, multiplisere dem med forskyvningen og legge dem til de opprinnelige verdiene. For stillingspunktene må vi interpolere normaler dannet av linjer til tilstøtende kontrollpunkter.

 // Iterere gjennom alle poeng for (var i: int = 0; i < _nodes.length; i++)  var normal:Point; var surface:Point; // Normal formed by position points if (i == 0)  // First point - take normal from first line segment normal = lineNormal(_nodes[i].position, _nodes[i].control); surface = lineNormal(_nodes[i].position, _nodes[i + 1].position);  else if (i + 1 == _nodes.length)  // Last point - take normal from last line segment normal = lineNormal(_nodes[i - 1].control, _nodes[i].position); surface = lineNormal(_nodes[i - 1].position, _nodes[i].position);  else  // Middle point - take 2 normals from segments // adjecent to the point, and interpolate them normal = lineNormal(_nodes[i].position, _nodes[i].control); normal = normal.add( lineSegmentNormal(_nodes[i - 1].control, _nodes[i].position)); normal.normalize(1); // This causes a slight visual issue for thicker rivers // It can be avoided by adding more nodes surface = lineNormal(_nodes[i].position, _nodes[i + 1].position);  // Add offsets to the original node, forming a new one. nodesWithOffset.add( _nodes[i].position.x + normal.x * offset, _nodes[i].position.y + normal.y * offset, _nodes[i].control.x + surfaceNormal.x * offset, _nodes[i].control.y + surfaceNormal.y * offset ); 

Du kan allerede se at vi kan bruke disse punktene til å definere små firesidige polygoner - "quads". Implementeringen vår bruker et tilpasset Starling DisplayObject, som gir våre geometriske data direkte til GPU.

Et problem, avhengig av implementeringen, er at vi ikke kan sende quads direkte; I stedet må vi sende trekanter. Men det er lett nok å plukke ut to trekanter med fire poeng:

Resultat:


teksture

Ren geometrisk stil er morsom, og det kan også være en god stil for noen eksperimentelle spill. Men for å gjøre elven vår ser veldig bra ut, kan vi gjøre med noen flere detaljer. Bruke en tekstur er en god ide. Som fører oss til problemet med å vise det på tilpasset geometri opprettet tidligere.

Vi må legge til ytterligere informasjon i våre hjørner; stillinger alene vil ikke gjøre lenger. Hvert toppunkt kan lagre flere parametere etter vår smak, og for å støtte teksturkartlegging må vi definere teksturkoordinater.

Tekstur koordinater er i tekstur plass, og kart pixel verdier av bildet til verdens posisjoner av poeng. For hver piksel som vises på skjermen, beregner vi interpolerte teksturkoordinater og bruker dem til å lete opp pikselverdier for posisjoner i tekstur. Verdiene 0 og 1 i teksturområdet samsvarer med teksturkanter; hvis verdier forlater dette området har vi et par alternativer:

  • Gjenta - Gjenta uendelig tekstur.
  • Klemme - klipp av tekstur utenfor grensene for intervallet [0, 1].

De som kjenner litt om tekstur kartlegging er sikkert klar over mulige kompleksiteter av teknikken. Jeg har gode nyheter for deg! Denne måten å representere elver er enkelt kartlagt til en tekstur.

Fra sidene er teksturhøyde kartlagt i sin helhet, mens lengden på elva er segmentert i mindre biter av teksturområdet, passende dimensjonert til teksturbredde.

Nå for å implementere det i koden:

 // _texture er en Starling tekstur var avstand: Nummer = 0; // Iterere gjennom alle poeng for (var i: int = 0; i < _points.length; i++)  if (i > 0) // Avstand i tekstur plass for nåværende linjesegment avstand + = Point.distance (lastPoint, _points [i]) / _texture.width;  // Tilordne teksturkoordinater til geometri _vertexData.setTexCoords (vertexId ++, avstand, 0); _vertexData.setTexCoords (vertexId ++, avstand, 1); 

Nå ser det mye mer ut som en elv:


animasjon

Vår elv ser nå mye mer ut som en ekte, med et stort unntak: det står stille!

Ok, så må vi animere den. Det første du kanskje tenker på, er å bruke sprite ark animasjon. Og det kan godt fungere, men for å holde mer fleksibilitet og spare litt på teksturminnet, gjør vi noe mer interessant.

I stedet for å endre tekstur, kan vi endre måten tekstur kartlegger til geometrien. Vi gjør dette ved å endre tekstur koordinater for våre hjørner. Dette vil bare fungere for fliser med tekstur som er satt til gjenta.

En enkel måte å implementere dette på er å endre tekstur koordinatene på CPU og sende resultatene til GPU hver ramme. Det er vanligvis en god måte å starte en implementering denne typen teknikk, siden feilsøking er mye lettere. Vi skal imidlertid dykke rett inn på den beste måten vi kan oppnå dette: animerende teksturkoordinater ved hjelp av vertex shaders.

Fra erfaring kan jeg fortelle at folk noen ganger er skremt av shaders, sannsynligvis på grunn av deres tilknytning til de avanserte grafiske effektene av blockbuster-spill. Sannheten blir fortalt at konseptet bak dem er ekstremt enkelt, og hvis du kan skrive et program, kan du skrive en skygge - det er alt de er, små programmer som kjører på GPU. Vi skal bruke en vertex shader for å animere elven vår, det finnes flere andre typer shaders, men vi kan gjøre uten dem.

Som navnet antyder, behandler vertex shaders vertices. De kjører for hvert toppunkt, og tar som input-attributter: posisjon, teksturkoordinater og farge.

Målet vårt er å kompensere X-verdien av elvens teksturkoordinat for å simulere strømning. Vi holder en strømteller og øker den hver ramme etter tid delta. Vi kan spesifisere en ekstra parameter for hastigheten på animasjonen. Offset verdi skal sendes til shader som en uniform (konstant) verdi, en måte å gi shader program med mer informasjon enn bare vertices. Denne verdien er vanligvis en firekomponentvektor; Vi skal bare bruke X-komponenten til å lagre verdien, mens du stiller inn Y, Z og W til 0.

 // Texture offset ved indeks 5, som vi senere refererer til i skygge context.setProgramConstantsFromVector (Context3DProgramType.VERTEX, 5, ny [-_textureOffset, 0, 0, 0], 1);

Denne implementeringen bruker AGAL shader-språket. Det kan være litt vanskelig å forstå, som det er en samling som språk. Du kan lære mer om det her.

Vertex shader:

 m44 op, va0, vc0 // Beregn vertex verdensposisjon mul v0, va1, vc4 // Beregn vertex farge // Legg vertex tekstur koordinat (va2) og vår tekstur offset konstant (vc5): legg til v1, va2, vc5

Animasjon i aksjon:


Hvorfor stoppe her?

Vi er ganske mye gjort, bortsett fra at elven vår fortsatt ser unaturlig ut. Sletten kuttet mellom bakgrunn og elv er en ekte øye. For å løse dette kan du bruke et ekstra lag av elven, litt tykkere, og en spesiell tekstur som ville overlappe elvene og dekke den stygge overgangen.

Og siden demoen representerer elv av smeltet lava, kan vi muligens ikke gå uten litt glød! Lag en annen forekomst av flodgeometri, bruk nå en glødtekstur og sett blandingsmodus for å legge til. For enda morsommere, legg til glatt animasjon av glød alfa-verdien.

Endelig demo:

Selvfølgelig kan du gjøre mye mer enn bare elver ved hjelp av denne typen effekt. Jeg har sett den brukt til spøkelsespartikkeleffekter, fosser eller til og med for animerende kjeder. Det er mye plass for ytterligere forbedringer, ytelsesvis endelig versjon fra oven kan gjøres ved hjelp av en tegneanrop hvis teksturer slås sammen til et atlas. Lang elver bør deles i flere deler og kastes. En viktig utvidelse vil være å implementere forking av kurve noder for å aktivere flere elvebaner og igjen simulere bifurcation.

Jeg bruker denne teknikken i vårt nyeste spill, og jeg er veldig fornøyd med hva vi kan gjøre med det. Vi bruker den til elver og veier (uten animasjon, selvsagt). Jeg tenker på å bruke en lignende effekt for innsjøer.


Konklusjon

Jeg håper jeg ga deg ideer om hvordan du tenker utenom vanlige grafiske teknikker, for eksempel å bruke sprite ark eller fliser sett for å oppnå effekter som dette. Det krever litt mer arbeid, litt matte, og litt GPU programmeringskunnskap, men i retur får du mye større fleksibilitet.