Fai un tuffo con effetti d'acqua 2D dinamici

Sploosh! In questo tutorial, ti mostrerò come utilizzare semplici effetti matematici, fisici e di particelle per simulare onde e gocce d'acqua 2D di grande impatto.

Nota: Sebbene questo tutorial sia scritto usando C # e XNA, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Anteprima del risultato finale

Se hai XNA, puoi scaricare i file sorgente e compilare tu stesso la demo. Altrimenti, guarda il video dimostrativo qui sotto:

Ci sono due parti per lo più indipendenti alla simulazione dell'acqua. Innanzitutto, faremo le onde usando un modello a molla. Secondo, useremo effetti particellari per aggiungere schizzi.


Fare le onde

Per creare le onde, modelleremo la superficie dell'acqua come una serie di molle verticali, come mostrato in questo diagramma:

Ciò consentirà alle onde di salire e scendere. Faremo in modo che le particelle d'acqua attraggano le particelle vicine per consentire alle onde di diffondersi.

La legge di Springs e Hooke

Una cosa grandiosa delle molle è che sono facili da simulare. Le sorgenti hanno una certa lunghezza naturale; se allunghi o comprimi una molla, proverà a ritornare a quella lunghezza naturale.

La forza fornita da una molla è data dalla legge di Hooke:

\ [
F = -kx
\]

F è la forza prodotta dalla molla, K è la costante di primavera, e X è lo spostamento della molla dalla sua lunghezza naturale. Il segno negativo indica che la forza è nella direzione opposta a cui è spostata la molla; se spingi la molla verso il basso, si spingerà indietro, e viceversa.

La costante di primavera, K, determina la rigidità della molla.

Per simulare le molle, dobbiamo capire come spostare le particelle in base alla legge di Hooke. Per fare ciò, abbiamo bisogno di un paio di altre formule dalla fisica. Innanzitutto, la seconda legge del moto di Newton:

\ [
F = ma
\]

Qui, F è forza, m è di massa e un è l'accelerazione. Ciò significa che una forza più forte spinge un oggetto e più è leggero l'oggetto, più esso accelera.

Combinando queste due formule e riorganizzando ci dà:

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

Questo ci dà l'accelerazione per le nostre particelle. Supponiamo che tutte le nostre particelle abbiano la stessa massa, quindi possiamo combinarle k / m in una singola costante.

Per determinare la posizione dall'accelerazione, dobbiamo fare integrazione numerica. Useremo la forma più semplice di integrazione numerica: ogni frame facciamo semplicemente quanto segue:

Posizione + = Velocità; Velocity + = Accelerazione;

Questo è chiamato il metodo Eulero. Non è il tipo più preciso di integrazione numerica, ma è veloce, semplice e adeguato ai nostri scopi.

Mettendo tutto insieme, le nostre particelle di superficie dell'acqua faranno quanto segue ogni fotogramma:

public float Position, Velocity; public void Update () const float k = 0.025f; // regola questo valore a tuo piacimento float x = Altezza - TargetHeight; accelerazione mobile = -k * x; Posizione + = Velocità; Velocity + = accelerazione; 

Qui, TargetHeight è la posizione naturale della parte superiore della molla quando non è né tesa né compressa. Dovresti impostare questo valore nel punto in cui desideri che la superficie dell'acqua sia. Per la demo, l'ho impostato a metà dello schermo, a 240 pixel.

Tensione e smorzamento

Ho menzionato in precedenza che la costante di primavera, K, controlla la rigidità della molla. È possibile regolare questo valore per modificare le proprietà dell'acqua. Una bassa costante di primavera farà sciogliere le molle. Ciò significa che una forza causerà grandi onde che oscillano lentamente. Viceversa, un'alta costante della molla aumenterà la tensione in primavera. Le forze creeranno piccole onde che oscillano rapidamente. Un'alta costante di primavera renderà l'acqua più simile a Jello jiggling.

Una parola di avvertimento: non impostare la costante della molla troppo alta. Molle molto rigide applicano forze molto forti che cambiano notevolmente in un tempo molto ridotto. Questo non funziona bene con l'integrazione numerica, che simula le molle come una serie di salti discreti a intervalli di tempo regolari. Una molla molto rigida può anche avere un periodo di oscillazione più breve del tempo. Ancora peggio, il metodo di integrazione di Eulero tende a guadagnare energia mentre la simulazione diventa meno accurata, facendo esplodere le molle rigide.

C'è un problema con il nostro modello di primavera finora. Quando una molla inizia ad oscillare, non si fermerà mai. Per risolvere questo dobbiamo applicare alcuni smorzamento. L'idea è di applicare una forza nella direzione opposta che la nostra molla sta muovendo per rallentarla. Ciò richiede un piccolo adeguamento alla nostra formula di primavera:

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

Qui, v è velocità e d è il fattore di smorzamento - un'altra costante che puoi modificare per regolare la sensazione dell'acqua. Dovrebbe essere piuttosto piccolo se vuoi che le tue onde oscillino. La demo utilizza un fattore di smorzamento di 0,025. Un elevato fattore di smorzamento farà apparire l'acqua densa come una melassa, mentre un valore basso consentirà alle onde di oscillare a lungo.

Rendere le onde propagarsi

Ora che possiamo fare una molla, usiamoli per modellare l'acqua. Come mostrato nel primo diagramma, modelliamo l'acqua usando una serie di molle verticali parallele. Naturalmente, se le sorgenti sono tutte indipendenti, le onde non si espanderanno mai come fanno le onde vere.

Prima mostrerò il codice e poi lo esaminerò:

per (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 * (springs [i] .Hight - springs [i - 1] .Hight); springs [i - 1] .Speed ​​+ = leftDeltas [i];  se io < 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) springs [i - 1] .Hight + = leftDeltas [i]; se io < springs.Length - 1) springs[i + 1].Height += rightDeltas[i];  

Questo codice verrebbe chiamato ogni frame dal tuo Aggiornare() metodo. Qui, molle è una serie di molle, disposte da sinistra a destra. leftDeltas è una serie di galleggianti che memorizza la differenza di altezza tra ciascuna molla e il suo vicino di sinistra. rightDeltas è l'equivalente per i vicini giusti. Conserviamo tutte queste differenze di altezza negli array perché gli ultimi due Se le dichiarazioni modificano le altezze delle molle. Dobbiamo misurare le differenze di altezza prima che venga modificata una qualsiasi delle altezze.

Il codice inizia eseguendo la legge di Hooke ogni primavera come descritto in precedenza. Poi guarda la differenza di altezza tra ogni molla e i suoi vicini, e ogni molla tira le sue vicine molle verso se stessa alterando le posizioni e le velocità dei vicini. La fase di avvicinamento dei vicini viene ripetuta otto volte per consentire alle onde di propagarsi più rapidamente.

C'è un altro valore modificabile qui chiamato Diffusione. Controlla la velocità di propagazione delle onde. Può assumere valori compresi tra 0 e 0,5, con valori maggiori che rendono le onde più veloci.

Per avviare le onde in movimento, aggiungeremo un semplice metodo chiamato Splash ().

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

Ogni volta che vuoi fare le onde, chiama Splash (). Il indice il parametro determina in quale molla deve essere generato lo splash e il velocità parametro determina quanto saranno grandi le onde.

Rendering

Useremo XNA PrimitiveBatch classe dal XNA PrimitivesSample. Il PrimitiveBatch la classe ci aiuta a disegnare linee e triangoli direttamente con la GPU. Lo usi in questo modo:

// in LoadContent () primitiveBatch = new PrimitiveBatch (GraphicsDevice); // in Draw () primitiveBatch.Begin (PrimitiveType.TriangleList); foreach (Triangolo triangolare in triangoli su Draw) primitiveBatch.AddVertex (triangle.Point1, Color.Red); primitiveBatch.AddVertex (triangle.Point2, Color.Red); primitiveBatch.AddVertex (triangle.Point3, Color.Red);  primitiveBatch.End ();

Una cosa da notare è che, per impostazione predefinita, è necessario specificare i vertici del triangolo in senso orario. Se li aggiungi in senso antiorario, il triangolo verrà abbattuto e non lo vedrai.

Non è necessario avere una molla per ogni pixel di larghezza. Nella demo ho usato 201 molle sparse su una finestra larga 800 pixel. Ciò fornisce esattamente 4 pixel tra ogni molla, con la prima molla a 0 e l'ultima a 800 pixel. Probabilmente potresti usare ancora meno molle e avere comunque l'acqua liscia.

Quello che vogliamo fare è disegnare trapezi alti sottili e alti che si estendono dal fondo dello schermo alla superficie dell'acqua e collegano le molle, come mostrato in questo diagramma:

Dato che le schede grafiche non disegnano direttamente i trapezi, dobbiamo disegnare ogni trapezio come due triangoli. Per renderlo un po 'più bello, renderemo l'acqua più scura man mano che diventa più profonda colorando i vertici inferiori blu scuro. La GPU interpola automaticamente i colori tra i vertici.

primitiveBatch.Begin (PrimitiveType.TriangleList); Colore midnight Blue = new Color (0, 15, 40) * 0.9f; Colore lightBlue = new Color (0.2f, 0.5f, 1f) * 0.8f; var viewport = GraphicsDevice.Viewport; float bottom = viewport.Height; // allunga le posizioni x delle molle per occupare l'intera finestra float scale = viewport.Width / (springs.Length - 1f); // assicurati di usare la divisione float per (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();

Ecco il risultato:


Fare gli spruzzi

Le onde sembrano belle, ma mi piacerebbe vedere uno schianto quando la roccia colpisce l'acqua. Gli effetti particellari sono perfetti per questo.

Effetti particellari

Un effetto particellare usa un gran numero di piccole particelle per produrre un qualche effetto visivo. A volte vengono utilizzati per cose come fumo o scintille. Utilizzeremo particelle per le gocce d'acqua negli spruzzi.

La prima cosa di cui abbiamo bisogno è la nostra classe di particelle:

classe Particle public Vector2 Position; velocità Vector2 pubblica; orientamento del galleggiante pubblico; Particella pubblica (posizione Vector2, velocità Vector2, orientamento flottante) Posizione = posizione; Velocità = velocità; Orientamento = orientamento; 

Questa classe contiene solo le proprietà che una particella può avere. Successivamente, creiamo un elenco di particelle.

Elenco particelle = nuova lista();

Ogni frame, dobbiamo aggiornare e disegnare le particelle.

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 particle in particles) UpdateParticle (particle); // cancella le particelle fuori dallo schermo o sott'acqua particelle = particelle. Dove (x => x.Posizione.X> = 0 && x.Posizione.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList(); 

Aggiorniamo le particelle per cadere sotto la gravità e impostare l'orientamento della particella in modo che corrisponda alla direzione in cui sta andando. Quindi eliminiamo tutte le particelle fuori dallo schermo o sott'acqua copiando tutte le particelle che vogliamo tenere in una nuova lista e assegnandolo alle particelle. Quindi disegniamo le particelle.

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

Sotto c'è la texture che ho usato per le particelle.

Ora, ogni volta che creiamo uno splash, facciamo un mucchio di particelle.

private void CreateSplashParticles (float xPosition, float speed) float y = GetHeight (xPosition); if (speed> 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));   

Puoi chiamare questo metodo dal Splash () metodo che usiamo per fare le onde. La velocità dei parametri è la velocità con cui la roccia colpisce l'acqua. Faremo grandi schizzi se la roccia si muove più velocemente.

GetRandomVector2 (40) restituisce un vettore con una direzione casuale e una lunghezza casuale tra 0 e 40. Vogliamo aggiungere un po 'di casualità alle posizioni in modo che le particelle non appaiano tutte in un singolo punto. FromPolar () restituisce a Vector2 con una data direzione e lunghezza.

Ecco il risultato:

Utilizzo di metaball come particelle

I nostri splash sembrano abbastanza decenti, e alcuni grandi giochi, come World of Goo, hanno degli schizzi di particelle che assomigliano molto ai nostri. Tuttavia, ti mostrerò una tecnica per rendere gli schizzi più liquidi. La tecnica sta usando metaball, blob dall'aspetto organico che ho scritto in precedenza su un tutorial. Se sei interessato ai dettagli sui metaball e su come funzionano, leggi questo tutorial. Se vuoi solo sapere come applicarli ai nostri spruzzi, continua a leggere.

Le metaballe hanno un aspetto simile al liquido nel modo in cui si fondono, rendendole un ottimo abbinamento per i nostri schizzi di liquidi. Per realizzare i metaball, dovremo aggiungere nuove variabili di classe:

RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;

Che inizializziamo in questo modo:

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

Quindi disegniamo i metaball:

GraphicsDevice.SetRenderTarget (metaballTarget); GraphicsDevice.Clear (Color.Transparent); Colore lightBlue = new Color (0.2f, 0.5f, 1f); spriteBatch.Begin (0, BlendState.Additive); foreach (particella var in particelle) origine Vector2 = nuova 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 (); // disegna onde e altre cose

L'effetto metaball dipende dall'avere una texture particellare che svanisce man mano che ti allontani dal centro. Ecco cosa ho usato, impostato su uno sfondo nero per renderlo visibile:

Ecco come appare:

Le gocce d'acqua ora si fondono quando sono vicine. Tuttavia, non si fondono con la superficie dell'acqua. Possiamo aggiustarlo aggiungendo un gradiente alla superficie dell'acqua che lo fa sfumare gradualmente e renderlo al nostro obiettivo di rendering di metaball.

Aggiungi il seguente codice al metodo precedente prima della linea GraphicsDevice.SetRendertarget (null):

primitiveBatch.Begin (PrimitiveType.TriangleList); const float thickness = 20; float scale = GraphicsDevice.Viewport.Width / (springs.Length - 1f); per (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();

Ora le particelle si fondono con la superficie dell'acqua.

Aggiungere l'effetto di smussatura

Le particelle d'acqua sembrano un po 'piatte, e sarebbe bello dar loro qualche sfumatura. Idealmente, lo faresti con uno shader. Tuttavia, al fine di mantenere questo tutorial semplice, utilizzeremo un trucco facile e veloce: stiamo semplicemente disegnando le particelle tre volte con diverse colorazioni e offset, come illustrato nello schema seguente.

Per fare questo, vogliamo catturare le particelle metaball in un nuovo bersaglio di rendering. Quindi disegneremo quell'obiettivo di rendering una volta per ciascuna tinta.

In primo luogo, dichiara un nuovo RenderTarget2D proprio come abbiamo fatto per le metaball:

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

Quindi, invece di disegnare metaballsTarget direttamente al backbuffer, vogliamo disegnarlo particlesTarget. Per fare questo, vai al metodo in cui disegniamo i metaball e semplicemente cambia queste linee:

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

… a:

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

Quindi utilizzare il seguente codice per disegnare le particelle tre volte con diverse tinte e offset:

Colore lightBlue = new Color (0.2f, 0.5f, 1f); GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue); spriteBatch.Begin (); spriteBatch.Draw (particlesTarget, -Vector2.One, new Color (0.8f, 0.8f, 1f)); spriteBatch.Draw (particlesTarget, Vector2.One, new Color (0f, 0f, 0.2f)); spriteBatch.Draw (particlesTarget, Vector2.Zero, lightBlue); spriteBatch.End (); // disegna onde e altre cose

Conclusione

Questo è tutto per l'acqua 2D di base. Per la demo, ho aggiunto una roccia che puoi far cadere nell'acqua. Io disegno l'acqua con una certa trasparenza sulla parte superiore della roccia per farlo sembrare sott'acqua, e farlo rallentare quando è sott'acqua a causa della resistenza all'acqua.

Per rendere la demo un po 'più carina, sono andato su opengameart.org e ho trovato un'immagine per il rock e lo sfondo del cielo. Puoi trovare il rock e il cielo su http://opengameart.org/content/rocks e rispettivamente opengameart.org/content/sky-backdrop.