Faceți o stropire cu efecte dinamice de apă 2D

Sploosh! În acest tutorial, vă voi arăta cum puteți folosi simturi de matematică simplă, fizică și particule pentru a simula valuri de apă 2D și picături.

Notă: Deși acest tutorial este scris folosind C # și XNA, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.


Rezultatul final al rezultatelor

Dacă aveți XNA, puteți să descărcați fișierele sursă și să le compilați singur. În caz contrar, verificați videoclipul demo de mai jos:

Există două părți principale independente ale simulării apei. În primul rând, vom face valurile folosind un model de primăvară. În al doilea rând, vom folosi efecte de particule pentru a adăuga stropi.


Efectuarea valurilor

Pentru a face valurile, vom modela suprafața apei ca o serie de arcuri verticale, după cum se arată în această diagramă:

Acest lucru va permite valurilor să bob și în sus. Apoi vom face ca particulele de apă să tragă particulele vecine pentru a permite extinderii valurilor.

Springs și Legea lui Hooke

Un lucru minunat despre izvoare este că sunt ușor de simulat. Izvoarele au o anumită lungime naturală; dacă întindeți sau comprimați un arc, acesta va încerca să se întoarcă la acea lungime naturală.

Forța furnizată de un izvor este dată de Legea lui Hooke:

\ [
F = -kx
\]

F este forța produsă de primăvară, k este constanta de primavara, si X este deplasarea izvorului din lungimea sa naturală. Semnul negativ indică faptul că forța este în direcția opusă la care arcul este deplasat; dacă împingeți arcul în jos, va împinge înapoi și invers.

Constanta arcului, k, determină rigiditatea arcului.

Pentru a simula izvoarele, trebuie să ne dăm seama cum se pot deplasa particule pe baza Legii lui Hooke. Pentru a face acest lucru, avem nevoie de mai multe formule din fizică. În primul rând, a doua lege a mișcării de la Newton:

\ [
F = ma
\]

Aici, F este forta, m este masa și A este accelerația. Aceasta înseamnă că o forță mai puternică împinge un obiect, iar cu cât este mai ușor obiectul, cu atât accelerează mai mult.

Combinarea acestor două formule și rearanjarea ne dă:

\ [
a = frac k m x
\]

Acest lucru ne dă accelerația pentru particulele noastre. Vom presupune că toate particulele noastre vor avea aceeași masă, astfel încât să putem combina k / m într-o singură constantă.

Pentru a determina poziția de la accelerare, trebuie să facem integrare numerică. Vom folosi cea mai simplă formă de integrare numerică - fiecare cadru facem pur și simplu următoarele:

Poziția + = viteza; Viteza + = Accelerare;

Aceasta se numește metoda Euler. Nu este cel mai precis tip de integrare numerică, dar este rapid, simplu și adecvat scopurilor noastre.

Punandu-le impreuna, particulele de suprafata a apei vor face fiecare rama:

Poziția plutitoare publică, Viteza; public void Actualizare () const float k = 0.025f; // ajustați această valoare pentru floatul dvs. preferat x = Height - TargetHeight; flotarea accelerației = -k * x; Poziția + = viteza; Viteza + = accelerare; 

Aici, TargetHeight este poziția naturală a vârfului arcului când nu este nici întinsă, nici comprimată. Ar trebui să setați această valoare acolo unde doriți să fie suprafața apei. Pentru demo, l-am setat la jumătatea ecranului, la 240 de pixeli.

Tensiune și atenuare

Am menționat mai devreme că constantă de primăvară, k, controlează rigiditatea arcului. Puteți ajusta această valoare pentru a schimba proprietățile apei. O constantă scăzută a arcului va face să izbucnească izvoarele. Aceasta înseamnă că o forță va cauza valuri mari care oscilează lent. Dimpotrivă, o constantă mare a arcului va crește tensiunea în arc. Forțele vor crea valuri mici care oscilează rapid. O constantă de primăvară mare va face ca apa să arate mai mult ca Jello.

Un avertisment: nu setați constanta arcului prea mare. Izvoarele foarte rigide aplică forțe foarte puternice care se schimbă foarte mult într-o perioadă foarte mică de timp. Aceasta nu joacă bine cu integrarea numerică, care simulează izvoarele ca o serie de salturi discrete la intervale regulate de timp. Un arc foarte rigid poate avea chiar o perioadă de oscilație care este mai scurtă decât pasul tău de timp. Chiar și mai rău, metoda de integrare Euler tinde să câștige energie deoarece simularea devine mai puțin precisă, provocând izbucniri rigide pentru a exploda.

Există o problemă cu modelul nostru de primăvară până acum. Odată ce un arc începe să oscileze, nu se va opri niciodată. Pentru a rezolva acest lucru trebuie să aplicăm unele umezire. Ideea este de a aplica o forță în direcția opusă pe care arcul nostru o face în mișcare pentru ao încetini. Acest lucru necesită o mică adaptare la formula noastră de primăvară:

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

Aici, v este viteza și d este factor de umezire - o altă constanță puteți să ajustați pentru a regla simțul apei. Ar trebui să fie destul de mic dacă doriți ca undele dvs. să oscileze. Demo-ul folosește un factor de atenuare de 0,025. Un factor de atenuare ridicat va face apa să arate groasă ca melasa, în timp ce o valoare mică va permite valurilor să oscileze pentru o lungă perioadă de timp.

Efectuarea propagării valurilor

Acum, că putem face un izvor, să le folosim pentru a modela apa. Așa cum se arată în prima diagramă, modelăm apa folosind o serie de izvoare verticale paralele. Desigur, dacă izvoarele sunt toate independente, valurile nu se vor întinde niciodată ca valurile reale.

Voi arăta mai întâi codul și apoi îl voi trece peste el:

pentru (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 * (izvoarele [i] .Height - izvoare [i - 1]. Înălțime); arcuri [i - 1]. Viteza + = leftDeltas [i];  dacă eu < 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) izvoare [i - 1]. Hee + = leftDeltas [i]; dacă eu < springs.Length - 1) springs[i + 1].Height += rightDeltas[i];  

Acest cod ar fi numit fiecare cadru de la dvs. Actualizați() metodă. Aici, arcuri este o gamă de izvoare, întinse de la stânga la dreapta. leftDeltas este o serie de flotoare care stochează diferența de înălțime dintre fiecare primăvară și vecina stângă. rightDeltas este echivalentul pentru vecinii potriviți. Stocăm toate aceste diferențe de înălțime în matrice deoarece ultimele două dacă declarațiile modifică înălțimile izvoarelor. Trebuie să măsuram diferențele de înălțime înainte de modificarea oricăror înălțimi.

Codul începe prin executarea Legii lui Hooke în fiecare primăvară, așa cum este descris mai devreme. Apoi, se uită la diferența de înălțime dintre fiecare primăvară și vecinii săi, iar fiecare izvor trage izvoarele învecinate spre sine, modificând pozițiile și vitezele vecinilor. Treapta vecinătătoare se repetă de opt ori pentru a permite propagarea mai rapidă a valurilor.

Există o altă valoare tweakable numită aici răspândire. Controlează cât de repede se răspândesc valurile. Pot avea valori cuprinse între 0 și 0,5, cu valori mai mari, ceea ce face ca undele să se răspândească mai repede.

Pentru a porni în mișcare valurile, vom adăuga o metodă simplă numită Stropi().

public void Splash (index int, viteza flotorului) if (index> = 0 && index < springs.Length) springs[i].Speed = speed; 

Oriunde vrei să faci valuri, sună-te Stropi(). index parametrul determină la care primă ar trebui să provină stropirea și viteză parametrul determină cât de mari vor fi undele.

Redare

Vom folosi XNA PrimitiveBatch clasă din exemplul XNA Primitives. PrimitiveBatch clasa ne ajută să trasăm linii și triunghiuri direct cu GPU-ul. O folosiți așa:

// în LoadContent () primitiveBatch = nou PrimitiveBatch (GraphicsDevice); // în Draw () primitiveBatch.Begin (PrimitiveType.TriangleList); foreach (triunghi triunghi în triunghiuriToDraw) primitiveBatch.AddVertex (triunghi.Point1, Color.Red); primitiveBatch.AddVertex (triunghi.Point2, Color.Red); primitiveBatch.AddVertex (triunghi.Point3, Color.Red);  primitiveBatch.End ();

Un lucru de reținut este că, în mod implicit, trebuie să specificați vârfurile triunghiului în ordinea acelor de ceasornic. Dacă le adăugați într-o ordine în sens contrar acelor de ceasornic, triunghiul va fi distrus și nu îl veți vedea.

Nu este necesar să ai un arc pentru fiecare pixel de lățime. În demo am folosit 201 de izvoare răspândite pe o fereastră de 800 de pixeli. Aceasta oferă exact 4 pixeli între fiecare arc, cu primul arc la 0 și ultimul la 800 de pixeli. Ați putea folosi, probabil, chiar și mai puține izvoare și încă apă să arate netedă.

Ceea ce vrem să facem este să tragem trapezoizi subțiri și înalți care se extind de la partea de jos a ecranului până la suprafața apei și să conecteze arcurile, după cum se arată în această diagramă:

Din moment ce plăcile grafice nu trag direct trapezoidele, trebuie să tragem fiecare trapezoid ca două triunghiuri. Pentru a face ca aspectul să fie mai plăcut, vom face și apa mai întunecată pe măsură ce devine mai profundă prin colorarea vârfurilor de fund albastru închis. GPU-ul va interpola automat culorile între vârfuri.

primitiveBatch.Begin (PrimitiveType.TriangleList); Culoare midnightBlue = culoare nou (0, 15, 40) * 0.9f; Culoare luminăBlue = culoare nouă (0.2f, 0.5f, 1f) * 0.8f; var viewport = GraphicsDevice.Viewport; float bottom = viewport.Height; // întindeți arcurile 'x pozițiile pentru a prelua întreaga fereastră float scale = viewport.Width / (arcuri.Length - 1f); // asigurați-vă că utilizați diviziunea flotantă pentru (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();

Iată rezultatul:


Efectuarea Stropilor

Valurile arata destul de bine, dar mi-ar placea sa vad o stropi cand roca loveste apa. Efectele particulelor sunt perfecte pentru acest lucru.

Efectele particulelor

Un efect de particule utilizează un număr mare de particule mici pentru a produce un efect vizual. Sunt uneori folosite pentru lucruri precum fum sau scântei. Vom folosi particule pentru picăturile de apă în stropi.

Primul lucru de care avem nevoie este clasa noastră de particule:

Particola de clasă public Vector2 Position; Viteza Vector2 publică; public float Orientare; Particule publice (poziția Vector2, viteza Vector2, orientarea flotorului) Position = position; Viteza = viteza; Orientare = orientare; 

Această clasă deține doar proprietățile pe care le poate avea o particulă. Apoi, vom crea o listă de particule.

Listă particule = listă nouă();

Fiecare cadru trebuie să actualizăm și să desenăm particulele.

void UpdateParticle (Particle particule) const float Gravitatea = 0.3f; particle.Velocity.Y + = gravitatea; particle.Position + = particle.Velocity; particle.Orientation = GetAngle (particle.Velocity);  float privat GetAngle (vector2 vector) return (float) Math.Atan2 (vector.Y, vector.X);  public void Update () foreach (particula var în particule) UpdateParticle (particle); // ștergeți particulele care sunt în afara ecranului sau sub particule de apă = particule.În cazul în care (x => x.Position.X> = 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList(); 

Actualizăm particulele să cadă sub gravitate și fixăm orientarea particulelor astfel încât să se potrivească direcției în care se află. Apoi scăpăm de orice particule care sunt în afara ecranului sau sub apă prin copierea tuturor particulelor pe care dorim să le păstrăm într-o listă nouă și atribuindu-i particulelor. Apoi tragem particulele.

void DrawParticle (Particula particulei) Vector2 origine = nou Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, particle.Position, null, Color.White, particle.Orientation, origine, 0.6f, 0, 0);  void public Draw () foreach (particula var în particule) DrawParticle (particule); 

Mai jos este textura pe care am folosit-o pentru particule.

Acum, ori de câte ori creăm o stropire, facem o grămadă de particule.

void privat CreateSplashParticles (float xPosition, viteza flotorului) float y = GetHeight (xPosition); dacă (viteza> 60) pentru (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));   

Puteți apela această metodă din Stropi() metoda pe care o folosim pentru a face valuri. Viteza parametrilor este cât de repede se lovește roca de apă. Vom face stropi mai mari dacă roca se mișcă mai repede.

GetRandomVector2 (40) returnează un vector cu o direcție aleatorie și o lungime aleatorie între 0 și 40. Vrem să adăugăm puțină aleatorie pozițiilor astfel încât particulele să nu apară la un singur punct. FromPolar () returnează a Vector2 cu o anumită direcție și lungime.

Iată rezultatul:

Utilizarea Metaballs ca particule

Stropile noastre arată destul de decentă, iar unele jocuri minunate, cum ar fi World of Goo, au stropi cu efect de particule care arată foarte asemănător cu ale noastre. Cu toate acestea, vă voi arăta o tehnică pentru a face stropirile să pară mai lichide. Tehnica folosește metabaluri, bloburi cu aspect organic, despre care am scris deja un tutorial. Dacă sunteți interesat de detaliile despre metabluri și de modul în care funcționează, citiți acel tutorial. Dacă doriți doar să știți cum să le aplicați stropilor noștri, continuați să citiți.

Metabele arată în formă lichidă în felul în care se îmbină împreună, făcându-le o potrivire potrivită pentru stropirile noastre de lichide. Pentru a face metabelele, va trebui să adăugăm noi variabile de clasă:

RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;

Ce inițializăm așa:

var vedere = GraphicsDevice.Viewport; metaballTarget = noul RenderTarget2D (GraphicsDevice, view.Width, view.Height); alphaTest = nou AlphaTestEffect (GraphicsDevice); alphaTest.ReferenceAlpha = 175; alphaTest.Projection = Matrix.CreateTranslation (-0.5f, -0.5f, 0) * Matrix.CreateOrthographicOffCenter (0, vedere Lățime, vedere.Încălzire, 0, 0, 1);

Apoi tragem metabalurile:

GraphicsDevice.SetRenderTarget (metaballTarget); GraphicsDevice.Clear (Color.Transparent); Culoare luminăBlue = culoare nouă (0.2f, 0.5f, 1f); spriteBatch.Begin (0, BlendState.Additiv); foreach (particula var în particule) Vector2 origine = nou Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, particle.Position, null, lightBlue, particle.Orientation, origine, 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 (); // atrage valuri și alte lucruri

Efectul metaball depinde de a avea o textură de particule care se estompează pe măsură ce ajungeți mai departe din centru. Iată ce am folosit, așezat pe fundal negru pentru a fi vizibil:

Iată cum arată:

Picăturile de apă se îmbină împreună atunci când sunt aproape. Cu toate acestea, ele nu fuzioneaza cu suprafata apei. Putem rezolva acest lucru prin adăugarea unui gradient la suprafața apei, care o face să se estompeze treptat și să o transformăm în obiectivul nostru de transformare metaball.

Adăugați următorul cod la metoda de mai sus înainte de linie GraphicsDevice.SetRendertarget (null):

primitiveBatch.Begin (PrimitiveType.TriangleList); const grosimea flotorului = 20; float scară = GraphicsDevice.Viewport.Width / (springs.Length - 1f); pentru (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();

Acum particulele se vor fuziona cu suprafața apei.

Adăugarea efectului de derulare

Particulele de apă arata un pic plat și ar fi frumos să le dați niște umbre. În mod ideal, ați face acest lucru într-un shader. Cu toate acestea, pentru a menține acest tutorial simplu, vom folosi un truc rapid și ușor: pur și simplu vom desena particulele de trei ori cu nuanțe și decalări diferite, după cum se arată în diagrama de mai jos.

Pentru a face acest lucru, vrem să capturăm particulele metaball într-o nouă direcție de randare. Apoi, vom desena acea țintă pentru o dată pentru fiecare nuanță.

În primul rând, declarați un nou RenderTarget2D la fel ca și noi pentru metabalități:

particlesTarget = nou RenderTarget2D (GraphicsDevice, view.Width, view.Height);

Apoi, în loc să desenezi metaballsTarget direct la backbuffer, vrem să-l atragem particlesTarget. Pentru a face acest lucru, mergeți la metoda în care tragem metabelele și schimbați pur și simplu aceste linii:

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

… la:

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

Apoi folosiți următorul cod pentru a desena particulele de trei ori cu diferite nuanțe și decalări:

Culoare luminăBlue = culoare nouă (0.2f, 0.5f, 1f); GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue); spriteBatch.Begin (); spriteBatch.Draw (particuleTarget, -Vector2.One, culoare nouă (0.8f, 0.8f, 1f)); spriteBatch.Draw (particuleTarget, Vector2.One, culoare nouă (0f, 0f, 0.2f)); spriteBatch.Draw (particuleTarget, Vector2.Zero, lightBlue); spriteBatch.End (); // trageți valuri și alte lucruri

Concluzie

Asta e pentru apa de bază 2D. Pentru demo, am adăugat o piatră pe care o puteți arunca în apă. Am tras apa cu puțină transparență deasupra stâncii pentru a arăta că este subacvatică și încetinește când este sub apă din cauza rezistenței la apă.

Pentru a face demo-ul să arate mai bine, m-am dus la opengameart.org și am găsit o imagine pentru rock și un fundal de cer. Puteți găsi piatra și cerul la http://opengameart.org/content/rocks și opengameart.org/content/sky-backdrop respectiv.