Slorp! In deze zelfstudie laat ik je zien hoe je eenvoudige wiskunde, natuurkunde en partikeleffecten kunt gebruiken om groot uitziende 2D-watergolven en -druppeltjes te simuleren.
Notitie: Hoewel deze tutorial geschreven is met C # en XNA, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.
Als je XNA hebt, kun je de bronbestanden downloaden en de demo zelf samenstellen. Bekijk anders de demovideo hieronder:
Er zijn twee meestal onafhankelijke delen van de watersimulatie. Eerst maken we de golven met behulp van een veermodel. Ten tweede zullen we deeltjeseffecten gebruiken om spatten toe te voegen.
Om de golven te maken, modelleren we het oppervlak van het water als een reeks verticale veren, zoals weergegeven in dit diagram:
Hierdoor kunnen de golven op en neer golven. We zullen dan waterdeeltjes trekken op hun naburige deeltjes om de golven te laten verspreiden.
Een groot voordeel van Springs is dat ze eenvoudig te simuleren zijn. Veren hebben een bepaalde natuurlijke lengte; als je een veer uitrekt of samendrukt, zal hij proberen terug te keren naar die natuurlijke lengte.
De kracht geleverd door een veer wordt gegeven door de Wet van Hooke:
\ [
F = -kx
\]
F
is de kracht geproduceerd door de veer, k
is de lente constant, en X
is de verplaatsing van de veer van zijn natuurlijke lengte. Het negatieve teken geeft aan dat de kracht zich in de tegenovergestelde richting bevindt waarnaar de veer wordt verplaatst; als je de veer omlaag duwt, wordt deze terug omhoog gedrukt en omgekeerd.
De veerconstante, k
, bepaalt de stijfheid van de veer.
Om bronnen te simuleren, moeten we erachter komen hoe deeltjes verplaatst kunnen worden op basis van de Wet van Hooke. Om dit te doen, hebben we nog een paar formules uit de natuurkunde nodig. Ten eerste, de tweede bewegingswet van Newton:
\ [
F = ma
\]
Hier, F
is kracht, m
is massa en een
is versnelling. Dit betekent dat hoe sterker een kracht op een voorwerp drukt, en hoe lichter het object is, hoe meer het versnelt.
Door deze twee formules te combineren en opnieuw te rangschikken, hebben we:
\ [
a = - \ frac k m x
\]
Dit geeft ons de versnelling voor onze deeltjes. We nemen aan dat al onze deeltjes dezelfde massa hebben, dus we kunnen combineren k / m
in een enkele constante.
Om de positie van versnelling te bepalen, moeten we numerieke integratie doen. We gaan de eenvoudigste vorm van numerieke integratie gebruiken - elk frame doen we eenvoudigweg het volgende:
Positie + = snelheid; Velocity + = versnelling;
Dit wordt de Euler-methode genoemd. Het is niet het meest nauwkeurige type numerieke integratie, maar het is snel, eenvoudig en adequaat voor onze doeleinden.
Alles bij elkaar genomen, zullen onze wateroppervlakdeeltjes elk frame het volgende doen:
openbare zweefpositie, snelheid; public void Update () const float k = 0.025f; // pas deze waarde aan naar wens float x = Hoogte - TargetHeight; vlotteracceleratie = -k * x; Positie + = snelheid; Velocity + = versnelling;
Hier, TargetHeight
is de natuurlijke positie van de bovenkant van de veer wanneer deze niet uitgerekt of samengedrukt is. U moet deze waarde instellen op de plaats waar u het wateroppervlak wilt hebben. Voor de demo heb ik het halverwege het scherm ingesteld op 240 pixels.
Ik noemde eerder dat de lente constant is, k
, regelt de stijfheid van de veer. U kunt deze waarde aanpassen om de eigenschappen van het water te wijzigen. Een lage veerconstante maakt de veren los. Dit betekent dat een kracht grote golven veroorzaakt die langzaam oscilleren. Omgekeerd zal een hoge veerconstante de spanning in de veer verhogen. Krachten zullen kleine golven creëren die snel oscilleren. Een hoge veerconstante zorgt ervoor dat het water er meer uitziet als Jello.
Een woord van waarschuwing: stel de veer niet constant te hoog in. Zeer stijve veren hebben zeer sterke krachten die in een zeer korte tijd enorm veranderen. Dit werkt niet goed met numerieke integratie, die de veren simuleert als een reeks discrete sprongen op regelmatige tijdsintervallen. Een zeer stijve veer kan zelfs een oscillatieperiode hebben die korter is dan uw tijdstap. Erger nog, de Euler-integratiemethode heeft de neiging om energie te krijgen als de simulatie minder nauwkeurig wordt, waardoor stijve veren exploderen.
Er is tot nu toe een probleem met ons lentemodel. Zodra een veer begint te oscilleren, zal deze nooit stoppen. Om dit op te lossen, moeten we wat toepassen demping. Het idee is om een kracht in de tegenovergestelde richting uit te oefenen die onze veer beweegt om het te vertragen. Dit vereist een kleine aanpassing aan onze voorjaarsformule:
\ [
a = - \ frac k m x - dv
\]
Hier, v
is snelheid en d
is de dempende factor - een andere constante die je kunt aanpassen om het gevoel van het water aan te passen. Het moet vrij klein zijn als je wilt dat je golven oscilleren. De demo gebruikt een dempingsfactor van 0,025. Een hoge dempingsfactor zorgt ervoor dat het water er dik uitziet als melasse, terwijl een lage waarde de golven langdurig laat oscilleren..
Nu we een veer kunnen maken, laten we ze gebruiken om water te modelleren. Zoals in het eerste diagram wordt getoond, modelleren we het water met behulp van een reeks parallelle, verticale veren. Natuurlijk, als de veren allemaal onafhankelijk zijn, zullen de golven zich nooit verspreiden zoals echte golven doen.
Ik zal eerst de code laten zien en er dan overheen gaan:
voor (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 * (veren [i]. Hoogte - veren [i - 1]. Hoogte); veren [i - 1]. Snelheid + = linksDeltas [i]; als ik < 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) veren [i - 1]. Hoogte + = links Delta's [i]; als ik < springs.Length - 1) springs[i + 1].Height += rightDeltas[i];
Deze code zou elk frame van jouw worden genoemd Bijwerken()
methode. Hier, springs
is een reeks veren, die van links naar rechts zijn aangelegd. leftDeltas
is een reeks drijvers die het hoogteverschil tussen elke veer en de linkerbuur opslaat. rightDeltas
is het equivalent voor de juiste buren. We slaan al deze hoogteverschillen in arrays op, omdat de laatste twee als
uitspraken wijzigen de hoogten van de veren. We moeten de hoogteverschillen meten voordat een van de hoogten wordt gewijzigd.
De code begint met het uitvoeren van Hooke's Law op elke veer zoals eerder beschreven. Vervolgens wordt gekeken naar het hoogteverschil tussen elke veer en zijn buren en elke veer trekt de aangrenzende veren naar zich toe door de posities en snelheden van de buren te wijzigen. De buurtrekkingsstap wordt acht keer herhaald om de golven sneller te laten voortplanten.
Hier wordt nog een tweakable waarde genoemd Verspreiding
. Het bepaalt hoe snel de golven zich verspreiden. Het kan waarden tussen 0 en 0,5 aannemen, met grotere waarden waardoor de golven sneller worden verspreid.
Om de golven te laten bewegen, gaan we een eenvoudige methode toevoegen genaamd Plons()
.
public void Splash (int index, float speed) if (index> = 0 && index < springs.Length) springs[i].Speed = speed;
Elke keer dat je golven wilt maken, bel Plons()
. De inhoudsopgave
parameter bepaalt tegen welke veer de splash zou moeten ontstaan, en de snelheid
parameter bepaalt hoe groot de golven zullen zijn.
We zullen de XNA gebruiken PrimitiveBatch
klasse van de XNA Primitives Sample. De PrimitiveBatch
klasse helpt ons lijnen en driehoeken rechtstreeks met de GPU te tekenen. Je gebruikt het als volgt:
// in LoadContent () primitiveBatch = new PrimitiveBatch (GraphicsDevice); // in Draw () primitiveBatch.Begin (PrimitiveType.TriangleList); foreach (Triangle triangle in trianglesToDraw) primitiveBatch.AddVertex (triangle.Point1, Color.Red); primitiveBatch.AddVertex (triangle.Point2, Color.Red); primitiveBatch.AddVertex (triangle.Point3, Color.Red); primitiveBatch.End ();
Een ding om op te merken is dat u standaard de hoekpunten in de richting van de wijzers van de klok moet opgeven. Als je ze in een tegengestelde volgorde toevoegt, wordt de driehoek geselecteerd en zie je deze niet meer.
Het is niet nodig om een veer te hebben voor elke pixel met breedte. In de demo gebruikte ik 201 veren verspreid over een raam met 800 pixels breed. Dat geeft precies 4 pixels tussen elke veer, met de eerste veer op 0 en de laatste op 800 pixels. Je kunt waarschijnlijk nog minder veren gebruiken en het water er nog steeds glad uit laten zien.
Wat we willen doen is dunne, hoge trapezoïden tekenen die zich van de onderkant van het scherm naar het wateroppervlak uitstrekken en de veren verbinden, zoals weergegeven in dit diagram:
Omdat grafische kaarten geen trapezoïden rechtstreeks tekenen, moeten we elke trapezium als twee driehoeken tekenen. Om het er iets leuker uit te laten zien, maken we het water ook donkerder naarmate het dieper wordt door de onderste hoekpunten donkerblauw in te kleuren. De GPU interpoleert automatisch kleuren tussen de hoekpunten.
primitiveBatch.Begin (PrimitiveType.TriangleList); Kleur middernachtBlauw = nieuw Kleur (0, 15, 40) * 0.9f; Kleur lichtblauw = nieuw Kleur (0.2f, 0.5f, 1f) * 0.8f; var viewport = GraphicsDevice.Viewport; zwevende bodem = kijkvenster. Hoogte; // strek de x-posities van de veren om het hele venster te nemen float scale = viewport.Width / (springs.Length - 1f); // gebruik float division voor (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();
Hier is het resultaat:
De golven zien er goed uit, maar ik zou graag een plons zien als de rots het water raakt. Effecten met deeltjes zijn hier perfect voor.
Een deeltjeseffect gebruikt een groot aantal kleine deeltjes om een visueel effect te produceren. Ze worden soms gebruikt voor zaken als rook of vonken. We gaan deeltjes gebruiken voor de waterdruppels in de spatten.
Het eerste dat we nodig hebben is onze deeltjesklasse:
class Particle public Vector2 Position; openbare Vector2 Velocity; openbare zwevende oriëntatie; openbaar deeltje (Vector2-positie, Vector2-snelheid, zweefstand) Positie = positie; Velocity = velocity; Oriëntatie = oriëntatie;
Deze klasse bevat alleen de eigenschappen die een deeltje kan hebben. Vervolgens maken we een lijst met deeltjes.
Lijstparticles = nieuwe lijst ();
Voor elk frame moeten we de deeltjes bijwerken en tekenen.
void UpdateParticle (Particle particle) const float Gravity = 0.3f; particle.Velocity.Y + = Gravity; particle.Position + = particle.Velocity; particle.Orientation = GetAngle (particle.Velocity); private float GetAngle (Vector2 vector) return (float) Math.Atan2 (vector.Y, vector.X); public void Update () foreach (var deeltje in deeltjes) UpdateParticle (particle); // verwijder deeltjes die buiten het scherm of onder waterdeeltjes = deeltjes zijn.Where (x => x.Position.X> = 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList();
We werken de deeltjes bij om onder invloed van de zwaartekracht te vallen en stellen de oriëntatie van het deeltje in op de richting waarin het binnenkomt. We ontdoen zich dan van deeltjes die buiten het scherm of onder water zijn door alle deeltjes die we willen bewaren in een nieuwe lijst te kopiëren en toewijzen aan deeltjes. Vervolgens tekenen we de deeltjes.
void DrawParticle (Particle particle) Vector2 origin = new Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, particle.Position, null, Color.White, particle.Orientation, origin, 0.6f, 0, 0); public void Draw () foreach (var particle in particles) DrawParticle (particle);
Hieronder is de textuur die ik voor de deeltjes heb gebruikt.
Als we nu een plons maken, maken we een hoop deeltjes.
private void CreateSplashParticles (float xPosition, float speed) float y = GetHeight (xPosition); if (snelheid> 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));
Je kunt deze methode vanuit de Plons()
methode die we gebruiken om golven te maken. De parametersnelheid is hoe snel de rots het water raakt. We zullen grotere spatten maken als de steen sneller beweegt.
GetRandomVector2 (40)
retourneert een vector met een willekeurige richting en een willekeurige lengte tussen 0 en 40. We willen een beetje willekeur toevoegen aan de posities, zodat de deeltjes niet allemaal op één punt verschijnen. FromPolar ()
geeft a terug Vector2
met een gegeven richting en lengte.
Hier is het resultaat:
Onze spatten zien er redelijk uit, en sommige geweldige games, zoals World of Goo, hebben spatten met deeltjeseffect die veel op de onze lijken. Ik ga je echter een techniek laten zien om de spatten er vloeibaarder uit te laten zien. De techniek gebruikt metaballs, organisch uitziende blobs waarvan ik eerder een tutorial heb geschreven. Als u geïnteresseerd bent in de details over metaballs en hoe ze werken, leest u die zelfstudie. Als je gewoon wilt weten hoe je ze op onze spatten kunt toepassen, blijf dan lezen.
Metaballs zien er vloeibaar uit in de manier waarop ze samensmelten, waardoor ze een goede match zijn voor onze vloeibare spatten. Om de metaballs te maken, moeten we nieuwe klassenvariabelen toevoegen:
RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;
Die we zo initialiseren:
var view = GraphicsDevice.Viewport; metaballTarget = new RenderTarget2D (GraphicsDevice, view.Width, view.Height); alphaTest = nieuwe AlphaTestEffect (GraphicsDevice); alphaTest.ReferenceAlpha = 175; alphaTest.Projection = Matrix.CreateTranslation (-0.5f, -0.5f, 0) * Matrix.CreateOrthographicOffCenter (0, view.Width, view.Height, 0, 0, 1);
Vervolgens tekenen we de metaballs:
GraphicsDevice.SetRenderTarget (metaballTarget); GraphicsDevice.Clear (Color.Transparent); Kleur lichtblauw = nieuw Kleur (0.2f, 0.5f, 1f); spriteBatch.Begin (0, BlendState.Additive); foreach (var particle in particles) Vector2 origin = new Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, particle.Position, null, lightBlue, particle.Orientation, origin, 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 (); // teken golven en andere dingen
Het metaball-effect hangt af van het hebben van een deeltjestextuur die vervaagt naarmate je verder uit het centrum komt. Dit is wat ik heb gebruikt, ingesteld op een zwarte achtergrond om het zichtbaar te maken:
Hier is hoe het eruit ziet:
De waterdruppeltjes smelten nu samen wanneer ze dichtbij zijn. Ze smelten echter niet samen met het oppervlak van het water. We kunnen dit oplossen door een verloop toe te voegen aan het wateroppervlak waardoor het geleidelijk vervaagt en het weergeeft aan ons metaball renderdoel.
Voeg de volgende code toe aan de bovenstaande methode vóór de regel GraphicsDevice.SetRendertarget (null)
:
primitiveBatch.Begin (PrimitiveType.TriangleList); const float dikte = 20; float scale = GraphicsDevice.Viewport.Width / (springs.Length - 1f); voor (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();
Nu zullen de deeltjes samensmelten met het wateroppervlak.
De waterdeeltjes zien er een beetje plat uit, en het zou leuk zijn om ze wat schaduw te geven. Idealiter zou je dit in een arcering doen. Om deze tutorial eenvoudig te houden, gebruiken we echter een snelle en eenvoudige truc: we gaan eenvoudig de deeltjes drie keer tekenen met verschillende kleuren en verschuivingen, zoals geïllustreerd in het onderstaande schema..
Om dit te doen, willen we de metaball-deeltjes vastleggen in een nieuw renderdoel. Vervolgens tekenen we dat renderdoel één keer voor elke tint.
Eerst een nieuw verklaren RenderTarget2D
net zoals we deden voor de metaballs:
particlesTarget = new RenderTarget2D (GraphicsDevice, view.Width, view.Height);
Dan in plaats van tekenen metaballsTarget
direct naar de backbuffer, we willen het tekenen particlesTarget
. Ga hiervoor naar de methode waarbij we de metaballs tekenen en deze regels eenvoudig wijzigen:
GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue);
… naar:
GraphicsDevice.SetRenderTarget (particlesTarget); device.Clear (Color.Transparent);
Gebruik vervolgens de volgende code om de deeltjes drie keer te tekenen met verschillende tinten en verschuivingen:
Kleur lichtblauw = nieuw Kleur (0.2f, 0.5f, 1f); GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue); spriteBatch.Begin (); spriteBatch.Draw (particlesTarget, -Vector2.One, nieuwe kleur (0.8f, 0.8f, 1f)); spriteBatch.Draw (particlesTarget, Vector2.One, new Color (0f, 0f, 0.2f)); spriteBatch.Draw (particlesTarget, Vector2.Zero, lightBlue); spriteBatch.End (); // teken golven en andere dingen
Dat is het voor standaard 2D-water. Voor de demo heb ik een steen toegevoegd die je in het water kunt laten vallen. Ik teken het water met wat transparantie bovenop de rots om het er uit te laten zien alsof het onder water is, en laat het langzamer worden als het onder water staat vanwege de waterbestendigheid.
Om de demo een beetje leuker te laten lijken, ging ik naar opengameart.org en vond een afbeelding voor de achtergrond van rock en hemel. Je kunt de rots en lucht vinden op http://opengameart.org/content/rocks en opengameart.org/content/sky-backdrop respectievelijk.