Lag en splash med dynamiske 2D vann effekter

Sploosh! I denne opplæringen vil jeg vise deg hvordan du kan bruke enkle matte-, fysikk- og partikkeleffekter for å simulere flotte 2D-vannbølger og -dråper.

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


Endelig resultatforhåndsvisning

Hvis du har XNA, kan du laste ned kildefilene og kompilere demoen selv. Ellers kan du se demo videoen nedenfor:

Det er to mest uavhengige deler til vann simuleringen. Først gjør vi bølgene ved hjelp av en vårmodell. For det andre bruker vi partikkeleffekter for å legge til sprut.


Gjør bølgene

For å lage bølgene, vil vi modellere overflaten av vannet som en serie vertikale fjærer, som vist i dette diagrammet:

Dette vil tillate bølgene å bobbe seg opp og ned. Vi vil da få vannpartikler til å trekke på deres nærliggende partikler for å tillate bølgene å spre seg.

Springs og Hooke's Law

En flott ting om fjærer er at de er enkle å simulere. Fjærer har en viss naturlig lengde; Hvis du strekker eller komprimerer en vår, vil den prøve å gå tilbake til den naturlige lengden.

Kraften fra en vår er gitt av Hooke's Law:

\ [
F = -kx
\]

F er kraften produsert av våren, k er våren konstant, og x er vårens forskyvning fra sin naturlige lengde. Det negative tegnet indikerer at kraften er i motsatt retning som fjæren er forskjøvet av; Hvis du presser våren ned, vil den skyve opp igjen, og omvendt.

Våren konstant, k, bestemmer stivheten til våren.

For å simulere fjærer må vi finne ut hvordan man beveger partikler rundt, basert på Hooke's Law. For å gjøre dette trenger vi et par flere formler fra fysikk. Først, Newtons andre lov om bevegelse:

\ [
F = ma
\]

Her, F er kraft, m er masse og en er akselerasjon. Dette betyr at jo sterkere en kraft skyver på en gjenstand, og jo lettere objektet er, desto mer akselererer det.

Kombinere disse to formlene og omarrangere gir oss:

\ [
a = - \ frac k m x
\]

Dette gir oss akselerasjonen for partiklene våre. Vi antar at alle partiklene våre vil ha samme masse, så vi kan kombinere k / m inn i en enkelt konstant.

For å bestemme posisjon fra akselerasjon, må vi gjøre numerisk integrasjon. Vi skal bruke den enkleste formen for numerisk integrasjon - hver ramme gjør vi bare følgende:

Posisjon + = Hastighet; Hastighet + = akselerasjon;

Dette kalles Euler-metoden. Det er ikke den mest nøyaktige typen numerisk integrasjon, men den er rask, enkel og tilstrekkelig for våre formål.

Når vi legger alt sammen, vil våre vannflatepartikler gjøre følgende hver ramme:

offentlig flyteposisjon, hastighet; offentlig ugyldig oppdatering () const float k = 0.025f; // juster denne verdien til din smak float x = Høyde - TargetHeight; flyt akselerasjon = -k * x; Posisjon + = Hastighet; Hastighet + = akselerasjon; 

Her, TargetHeight er den naturlige posisjonen til toppen av våren når den ikke strekes eller komprimeres. Du bør sette denne verdien til hvor du vil at overflaten av vannet skal være. For demoen satte jeg det til halvveis ned på skjermen, med 240 piksler.

Spenning og fuktighet

Jeg nevnte tidligere at våren konstant, k, styrer stivheten til våren. Du kan justere denne verdien for å endre vannets egenskaper. En lav fjærkonstant vil gjøre fjærene løs. Dette betyr at en kraft vil forårsake store bølger som svinger sakte. Omvendt vil en høy fjærkonstant øke spenningen i våren. Styrker vil skape små bølger som oscillerer raskt. En høy fjærkonstant vil gjøre vannet ser mer ut som jiggling Jello.

Et ord med advarsel: Ikke sett våren konstant for høyt. Meget stive fjærer bruker svært sterke krefter som forandrer seg sterkt i svært liten tid. Dette spiller ikke bra med numerisk integrasjon, noe som simulerer fjærene som en serie diskrete hopp med vanlige tidsintervaller. En veldig stiv kilde kan til og med ha en svingningsperiode som er kortere enn tidstrinnet. Enda verre har Euler-integrasjonsmetoden en tendens til å få energi ettersom simuleringen blir mindre nøyaktig, noe som forårsaker at stive fjærer eksploderer.

Det er et problem med vårens vårmodell så langt. Når en vår begynner å svinge, vil den aldri stoppe. For å løse dette må vi søke på noen demping. Tanken er å bruke en kraft i motsatt retning som våren beveger seg for å senke den ned. Dette krever en liten justering av vårformelen vår:

\ [
a = - \ frac k m x - dv
\]

Her, v er hastighet og d er den dempningsfaktor - En annen konstant du kan justere for å justere følelsen av vannet. Det bør være ganske lite hvis du vil at bølgene dine skal oscillere. Demoen bruker en dempningsfaktor på 0,025. En høy dempningsfaktor gjør at vannet ser tykt ut som melasse, mens en lav verdi vil tillate bølgene å svinge i lang tid.

Gjør bølgene forplantet

Nå som vi kan lage en vår, la oss bruke dem til å modellere vann. Som vist i det første diagrammet modellerer vi vannet med en serie parallelle vertikale fjærer. Selvfølgelig, hvis fjærene er alle uavhengige, vil bølgene aldri spre seg ut som ekte bølger gjør.

Jeg vil vise koden først, og deretter gå over den:

for (int i = 0; i < springs.Length; i++) springs[i].Update(Dampening, Tension); float[] leftDeltas = new float[springs.Length]; float[] rightDeltas = new float[springs.Length]; // do some passes where springs pull on their neighbours for (int j = 0; j < 8; j++)  for (int i = 0; i < springs.Length; i++)  if (i > 0) leftDeltas [i] = Spread * (fjærer [i] .Høyfjærer [i - 1] .Høyde); fjærer [i - 1] .Speed ​​+ = leftDeltas [i];  hvis jeg < springs.Length - 1)  rightDeltas[i] = Spread * (springs[i].Height - springs [i + 1].Height); springs[i + 1].Speed += rightDeltas[i];   for (int i = 0; i < springs.Length; i++)  if (i > 0) fjærer [i - 1] .Height + = leftDeltas [i]; hvis jeg < springs.Length - 1) springs[i + 1].Height += rightDeltas[i];  

Denne koden vil bli kalt hver ramme fra din Oppdater() metode. Her, fjærer er en rekke fjærer, lagt ut fra venstre til høyre. leftDeltas er en rekke flyter som lagrer forskjellen i høyde mellom hver vår og den venstre naboen. rightDeltas er ekvivalent for de rette naboene. Vi lagrer alle disse høydeforskjellene i arrays fordi de to siste hvis utsagn modifiserer fjærens høyder. Vi måler høydeforskjellene før noen av høyder er endret.

Koden starter ved å løpe Hooke's Law på hver vår som beskrevet tidligere. Det ser da på høydeforskjellen mellom hver vår og naboene, og hver vår trekker sine nærkilder til seg selv ved å endre naboens posisjoner og hastigheter. Nabolagetrekkstrinnet gjentas åtte ganger for å tillate bølgene å formere seg raskere.

Det er en ytterligere tweakable verdi her kalt Spredt. Det styrer hvor fort bølgene sprer seg. Det kan ta verdier mellom 0 og 0,5, med større verdier som gjør at bølgene sprer seg raskere.

For å starte bølgene som beveger seg, skal vi legge til en enkel metode som heter Sprut().

offentlig tomrom Splash (int indeks, flythastighet) if (index> = 0 && index < springs.Length) springs[i].Speed = speed; 

Når som helst du vil lage bølger, ring Sprut(). De index parameteren bestemmer ved hvilken vår splash skal oppstå, og hastighet parameteren bestemmer hvor stor bølgene vil være.

Gjengivelse

Vi bruker XNA PrimitiveBatch klasse fra XNA PrimitivesSample. De PrimitiveBatch klassen hjelper oss å tegne linjer og trekanter direkte med GPU. Du bruker det slik:

// i LoadContent () primitiveBatch = ny PrimitiveBatch (GraphicsDevice); // i Draw () primitiveBatch.Begin (PrimitiveType.TriangleList); foreach (Triangle triangle i trianglesToDraw) primitiveBatch.AddVertex (triangle.Point1, Color.Red); primitiveBatch.AddVertex (triangle.Point2, Color.Red); primitiveBatch.AddVertex (triangle.Point3, Color.Red);  primitiveBatch.End ();

En ting å merke seg er at du som standard må angi trekantpunktene i en urvisende rekkefølge. Hvis du legger til dem i en mot klokka rekkefølge, vil triangelet bli slettet og du vil ikke se det.

Det er ikke nødvendig å ha en fjær for hver piksel med bredde. I demoen brukte jeg 201 fjærer spredt over et 800 piksler stort vindu. Det gir nøyaktig 4 piksler mellom hver vår, med første vår på 0 og sist ved 800 piksler. Du kan nok bruke enda færre fjærer og fortsatt få vannet til å se jevnt ut.

Det vi ønsker å gjøre er å tegne tynne, lange trapezoer som strekker seg fra bunnen av skjermen til vannoverflaten og koble fjærene, som vist i dette diagrammet:

Siden grafikkort ikke trekker trapeser direkte, må vi tegne hver trapes som to trekanter. For å gjøre det ser litt finere ut, vil vi også gjøre vannet mørkere etter hvert som det blir dypere ved å fargelegge de nederste vertikaler mørkblå. GPU'en vil automatisk interpolere farger mellom kryssene.

primitiveBatch.Begin (PrimitiveType.TriangleList); Farge midnattBlue = Ny Farge (0, 15, 40) * 0,9f; Farge lightBlue = ny Farge (0.2f, 0.5f, 1f) * 0.8f; var viewport = GraphicsDevice.Viewport; float bottom = viewport.Height; // strekk fjærens x posisjoner for å ta opp hele vinduet float skala = viewport.Width / (springs.Length - 1f); // vær sikker på å bruke float divisjon for (int i = 1; i < springs.Length; i++)  // create the four corners of our triangle. Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); Vector2 p2 = new Vector2(i * scale, springs[i].Height); Vector2 p3 = new Vector2(p2.X, bottom); Vector2 p4 = new Vector2(p1.X, bottom); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.AddVertex(p3, midnightBlue); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p3, midnightBlue); primitiveBatch.AddVertex(p4, midnightBlue);  primitiveBatch.End();

Her er resultatet:


Gjør sprutene

Bølgene ser ganske bra ut, men jeg vil gjerne se et sprut når steinen treffer vannet. Partikkel effekter er perfekt for dette.

Partikkeleffekter

En partikkel-effekt bruker et stort antall små partikler for å gi noen visuell effekt. De brukes noen ganger for ting som røyk eller gnister. Vi skal bruke partikler til vanndråpene i sprutene.

Det første vi trenger er vår partikkel klasse:

klasse partikkel offentlig vektor2 posisjon; offentlig Vector2 Velocity; offentlig float Orientering; offentlig partikkel (vektor2 posisjon, vektor2 hastighet, flytretning) posisjon = posisjon; Hastighet = hastighet; Orientering = orientering; 

Denne klassen har bare egenskapene en partikkel kan ha. Deretter oppretter vi en liste over partikler.

Liste partikler = ny liste();

Hver ramme, vi må oppdatere og tegne partiklene.

void UpdateParticle (Partikkelpartikkel) const float Gravity = 0.3f; partikkel.Velocity.Y + = Gravity; partikkel.Posisjon + = partikkel.Velocity; particle.Orientation = GetAngle (partikkel.Velocity);  privat float GetAngle (Vector2 vektor) return (float) Math.Atan2 (vector.Y, vector.X);  offentlig ugyldig oppdatering () foreach (var partikkel i partikler) UpdateParticle (partikkel); // slette partikler som er utenfor skjermen eller under vannpartikler = partikler. Hvor (x => x.Position.X> = 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList(); 

Vi oppdaterer partiklene for å falle under tyngdekraften og setter partikkelenes orientering for å matche retningen den går inn i. Vi blir så kvitt partikler som er på skjermen eller under vann ved å kopiere alle partiklene vi ønsker å holde inn i en ny liste og tildele det til partikler. Deretter tegner vi partiklene.

void DrawParticle (Particle particle) Vector2 opprinnelse = ny Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, particle.Position, null, Color.White, particle.Orientation, opprinnelse, 0,6f, 0, 0);  offentlig ugyldig tegning () foreach (var partikkel i partikler) DrawParticle (partikkel); 

Nedenfor er tekstur jeg brukte for partiklene.

Nå, når vi lager et sprut, lager vi en masse partikler.

private tomrom CreateSplashParticles (float xPosition, flythastighet) float y = GetHeight (xPosition); hvis (hastighet> 60) for (int i = 0; i < speed / 8; i++)  Vector2 pos = new Vector2(xPosition, y) + GetRandomVector2(40); Vector2 vel = FromPolar(MathHelper.ToRadians(GetRandomFloat(-150, -30)), GetRandomFloat(0, 0.5f * (float)Math.Sqrt(speed))); particles.Add(new Particle(pos, velocity, 0));   

Du kan ringe denne metoden fra Sprut() Metode vi bruker til å lage bølger. Parameterhastigheten er hvor fort fjellet treffer vannet. Vi får større sprut hvis klippen beveger seg raskere.

GetRandomVector2 (40) returnerer en vektor med en tilfeldig retning og en tilfeldig lengde mellom 0 og 40. Vi vil legge til litt tilfeldig plassering slik at partiklene ikke alle vises på et enkelt punkt. FromPolar () returnerer a Vektor2 med en gitt retning og lengde.

Her er resultatet:

Bruke Metaballs som partikler

Våre sprut ser ganske anstendig ut, og noen flotte spill, som World of Goo, har partikkel effekt sprut som ser ut som våre. Imidlertid skal jeg vise deg en teknikk for å få sprutene til å se mer flytende ut. Teknikken bruker metaballer, organisk utseende blobs som jeg har skrevet en opplæring om før. Hvis du er interessert i detaljene om metaballs og hvordan de fungerer, les den opplæringen. Hvis du bare vil vite hvordan du bruker dem til våre sprut, fortsett å lese.

Metaballs ser likformig ut i måten de smelter sammen, noe som gjør dem til en god kamp for våre flytende sprut. For å lage metaballs må vi legge til nye klassevariabler:

RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;

Som vi initialiserer slik:

var view = GraphicsDevice.Viewport; metaballTarget = ny RenderTarget2D (GraphicsDevice, view.Width, view.Height); alphaTest = ny AlphaTestEffect (GraphicsDevice); alfaTest.ReferenceAlpha = 175; alphaTest.Projection = Matrix.CreateTranslation (-0.5f, -0.5f, 0) * Matrix.CreateOrthographicOffCenter (0, view.Width, view.Hight, 0, 0, 1);

Så tegner vi metaballene:

GraphicsDevice.SetRenderTarget (metaballTarget); GraphicsDevice.Clear (Color.Transparent); Farge lightBlue = ny Farge (0.2f, 0.5f, 1f); spriteBatch.Begin (0, BlendState.Additive); foreach (var partikkel i partikler) Vector2 opprinnelse = ny Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, particle.Position, null, lightBlue, particle.Orientation, opprinnelse, 2f, 0, 0);  spriteBatch.End (); GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue); spriteBatch.Begin (0, null, null, null, null, alphaTest); spriteBatch.Draw (metaballTarget, Vector2.Zero, Color.White); spriteBatch.End (); // tegne bølger og andre ting

Metaball-effekten avhenger av å ha en partikkeltekstur som fades ut når du kommer lenger fra senteret. Her er det jeg brukte, satt på en svart bakgrunn for å gjøre det synlig:

Slik ser det ut:

Vanndråpene smelter nå sammen når de er nærme. Men de smelter ikke med vannoverflaten. Vi kan fikse dette ved å legge til en gradient til vannoverflaten som gjør at den gradvis falmer ut, og gjør det til vårt metaball gjengitte mål.

Legg til følgende kode i fremgangsmåten ovenfor før linjen GraphicsDevice.SetRendertarget (null):

primitiveBatch.Begin (PrimitiveType.TriangleList); const float tykkelse = 20; float scale = GraphicsDevice.Viewport.Width / (springs.Length - 1f); for (int i = 1; i < springs.Length; i++)  Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); Vector2 p2 = new Vector2(i * scale, springs[i].Height); Vector2 p3 = new Vector2(p1.X, p1.Y - thickness); Vector2 p4 = new Vector2(p2.X, p2.Y - thickness); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p3, Color.Transparent); primitiveBatch.AddVertex(p3, Color.Transparent); primitiveBatch.AddVertex(p4, Color.Transparent); primitiveBatch.AddVertex(p2, lightBlue);  primitiveBatch.End();

Nå smelter partiklene med vannoverflaten.

Legge til Behandlingseffekten

Vannpartiklene ser litt flate ut, og det ville være fint å gi dem litt skygge. Ideelt sett ville du gjøre dette i en skygge. Men for å holde denne opplæringen enkel, skal vi bruke et raskt og enkelt triks: vi skal bare trekke partiklene tre ganger med forskjellige fargetoner og forskyvninger, som illustrert i diagrammet under.

For å gjøre dette, vil vi fange metaballpartiklene i et nytt gjengemål. Vi trekker da det gjengitte målet en gang for hver nyanse.

Først erklærer du en ny RenderTarget2D akkurat som vi gjorde for metaballene:

particlesTarget = ny RenderTarget2D (GraphicsDevice, view.Width, view.Hight);

Deretter, i stedet for å tegne metaballsTarget direkte til backbufferen, ønsker vi å tegne det på particlesTarget. For å gjøre dette, gå til metoden hvor vi tegner metaballene og bare endre disse linjene:

GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue);

… til:

GraphicsDevice.SetRenderTarget (particlesTarget); device.Clear (Color.Transparent);

Bruk deretter følgende kode for å trekke partiklene tre ganger med forskjellige toner og forskyvninger:

Farge lightBlue = ny Farge (0.2f, 0.5f, 1f); GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue); spriteBatch.Begin (); SpriteBatch.Draw (partiklerTarget, -Vector2.One, ny farge (0.8f, 0.8f, 1f)); SpriteBatchDraw (partiklerTarget, Vector2.One, ny Color (0f, 0f, 0.2f)); spriteBatch.Draw (partiklerTarget, Vector2.Zero, lightBlue); spriteBatch.End (); // tegne bølger og andre ting

Konklusjon

Det er det for grunnleggende 2D vann. For demoen la jeg til en stein du kan slippe inn i vannet. Jeg tegner vannet med litt gjennomsiktighet på toppen av fjellet slik at det ser ut som det er under vann, og gjør det sakte når det er under vann på grunn av vannmotstand.

For å få demoen til å se litt finere, gikk jeg til opengameart.org og fant et bilde for fjellet og en himmelbakgrunn. Du kan finne klippen og himmelen på http://opengameart.org/content/rocks og opengameart.org/content/sky-backdrop henholdsvis.