Faites des vagues avec des effets d’eau dynamiques en 2D

Sploosh! Dans ce didacticiel, je vais vous montrer comment utiliser des effets mathématiques, physiques et de particules simples pour simuler de superbes vagues et gouttelettes d'eau 2D..

Remarque: Bien que ce tutoriel ait été écrit en C # et XNA, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


Aperçu du résultat final

Si vous avez XNA, vous pouvez télécharger les fichiers source et compiler vous-même la démo. Sinon, regardez la vidéo de démonstration ci-dessous:

La simulation de l’eau comprend deux parties essentiellement indépendantes. Premièrement, nous allons faire les vagues en utilisant un modèle de printemps. Deuxièmement, nous allons utiliser des effets de particules pour ajouter des éclaboussures.


Faire les vagues

Pour créer les vagues, modélisons la surface de l’eau sous la forme d’une série de sources verticales, comme indiqué dans ce diagramme:

Cela permettra aux vagues de balancer de haut en bas. Nous allons ensuite faire en sorte que les particules d’eau tirent sur leurs particules voisines pour permettre aux vagues de se propager..

Loi de Springs et Hooke

Une grande chose au sujet des ressorts, c'est qu'ils sont faciles à simuler. Les ressorts ont une certaine longueur naturelle; si vous étirez ou comprimez un ressort, il essaiera de revenir à cette longueur naturelle.

La force fournie par un ressort est donnée par la loi de Hooke:

\ [
F = -kx
\]

F est la force produite par le printemps, k est le printemps constant, et X est le déplacement du ressort de sa longueur naturelle. Le signe négatif indique que la force est dans la direction opposée à laquelle le ressort est déplacé; si vous poussez le ressort vers le bas, il repoussera, et vice versa.

La constante de printemps, k, détermine la raideur du ressort.

Pour simuler des sources, nous devons déterminer comment déplacer des particules en fonction de la loi de Hooke. Pour ce faire, nous avons besoin de quelques formules supplémentaires issues de la physique. Premièrement, la deuxième loi du mouvement de Newton:

\ [
F = ma
\]

Ici, F est la force, m est la masse et une est l'accélération. Cela signifie que plus une force est forte sur un objet et plus l'objet est léger, plus elle accélère..

La combinaison de ces deux formules et la réorganisation nous donne:

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

Cela nous donne l’accélération de nos particules. Nous supposerons que toutes nos particules auront la même masse, de sorte que nous pouvons combiner k / m en une seule constante.

Pour déterminer la position à partir de l'accélération, nous devons effectuer une intégration numérique. Nous allons utiliser la forme d’intégration numérique la plus simple. Pour chaque cadre, nous nous contentons de faire ce qui suit:

Position + = vitesse; Vélocité + = Accélération;

Ceci s'appelle la méthode d'Euler. Ce n'est pas le type d'intégration numérique le plus précis, mais c'est rapide, simple et adapté à nos besoins..

En réunissant le tout, les particules de la surface de l’eau effectueront les opérations suivantes pour chaque image:

flotteur public Position, Velocity; void public Update () const float k = 0.025f; // ajuste cette valeur à votre convenance float x = Height - TargetHeight; accélération de flottement = -k * x; Position + = vitesse; Vélocité + = accélération; 

Ici, TargetHeight est la position naturelle du sommet du printemps quand elle n’est ni étirée ni comprimée. Vous devez définir cette valeur à l'emplacement souhaité pour la surface de l'eau. Pour la démo, je l'ai réglé sur la moitié de l'écran, à 240 pixels.

Tension et amortissement

J'ai mentionné plus tôt que la constante de printemps, k, contrôle la raideur du ressort. Vous pouvez ajuster cette valeur pour modifier les propriétés de l'eau. Une constante de ressort faible fera lâcher les ressorts. Cela signifie qu'une force provoquera de grosses vagues qui oscillent lentement. Inversement, une constante de ressort élevée augmentera la tension au printemps. Les forces vont créer de petites vagues qui oscillent rapidement. Une constante de source élevée fera que l'eau ressemble davantage à une agitation Jello.

Un mot d'avertissement: ne réglez pas la constante du ressort trop haut. Les ressorts très rigides exercent des forces très fortes qui changent considérablement en très peu de temps. Cela ne fonctionne pas bien avec l'intégration numérique, qui simule les ressorts comme une série de sauts discrets à intervalles de temps réguliers. Un ressort très rigide peut même avoir une période d'oscillation plus courte que votre pas de temps. Pire encore, la méthode d'intégration d'Euler tend à gagner de l'énergie à mesure que la simulation devient moins précise, ce qui provoque l'explosion de ressorts raides..

Il y a un problème avec notre modèle de printemps jusqu'à présent. Une fois qu'un ressort commence à osciller, il ne s'arrêtera jamais. Pour résoudre cela, nous devons appliquer quelques amortissement. L'idée est d'appliquer une force dans la direction opposée à celle de notre ressort afin de la ralentir. Cela nécessite un petit ajustement à notre formule de printemps:

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

Ici, v est la vitesse et est le facteur d'amortissement - une autre constante que vous pouvez modifier pour ajuster la sensation de l’eau. Il devrait être assez petit si vous voulez que vos vagues oscillent. La démo utilise un facteur d'atténuation de 0,025. Un facteur d'amortissement élevé donnera à l'eau une apparence épaisse, semblable à de la mélasse, tandis qu'une valeur faible permettra aux vagues d'osciller pendant une longue période..

Faire se propager les vagues

Maintenant que nous pouvons fabriquer une source, utilisons-la pour modéliser l'eau. Comme le montre le premier diagramme, nous modélisons l'eau à l'aide d'une série de sources parallèles et verticales. Bien sûr, si les sources sont toutes indépendantes, les vagues ne se disperseront jamais comme de vraies vagues..

Je vais d'abord montrer le code, puis l'examiner:

pour (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 * (ressorts [i] .Hauteur - ressorts [i - 1] .Hauteur]; ressorts [i - 1] .Vitesse + = leftDeltas [i];  si je < 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) ressorts [i - 1] .Hauteur + = leftDeltas [i]; si je < springs.Length - 1) springs[i + 1].Height += rightDeltas[i];  

Ce code serait appelé chaque image de votre Mettre à jour() méthode. Ici, les ressorts est un tableau de ressorts, disposés de gauche à droite. gaucheDeltas est un ensemble de flotteurs qui stocke la différence de hauteur entre chaque ressort et son voisin de gauche. rightDeltas est l'équivalent pour les bons voisins. Nous stockons toutes ces différences de hauteur dans les tableaux car les deux derniers si les déclarations modifient les hauteurs des ressorts. Nous devons mesurer les différences de hauteur avant de modifier les hauteurs..

Le code commence par exécuter la loi de Hooke à chaque printemps, comme décrit précédemment. Il examine ensuite la différence de hauteur entre chaque printemps et ses voisins, et chaque printemps tire ses ressorts voisins vers lui en modifiant les positions et les vitesses des voisins. L'étape d'extraction de voisin est répétée huit fois pour permettre aux ondes de se propager plus rapidement.

Il y a une autre valeur tweakable appelée ici Propager. Il contrôle la vitesse à laquelle les vagues se propagent. Il peut prendre des valeurs comprises entre 0 et 0,5, des valeurs plus grandes accélérant l’étalement des vagues..

Pour que les vagues bougent, nous allons ajouter une méthode simple appelée Éclaboussure().

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

Chaque fois que vous voulez faire des vagues, appelez Éclaboussure(). le indice paramètre détermine à quel printemps le splash doit provenir, et le la vitesse paramètre détermine la taille des vagues.

Le rendu

Nous allons utiliser le XNA PrimitiveBatch classe de XNA PrimitivesSample. le PrimitiveBatch class nous aide à dessiner des lignes et des triangles directement avec le GPU. Vous l'utilisez comme ça:

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

Une chose à noter est que, par défaut, vous devez spécifier les sommets du triangle dans le sens des aiguilles d'une montre. Si vous les ajoutez dans le sens anti-horaire, le triangle sera éliminé et vous ne le verrez pas..

Il n'est pas nécessaire d'avoir un ressort pour chaque pixel de largeur. Dans la démo, j'ai utilisé 201 sources réparties sur une fenêtre de 800 pixels de large. Cela donne exactement 4 pixels entre chaque printemps, le premier ressort étant à 0 et le dernier à 800 pixels. Vous pourriez probablement utiliser encore moins de sources tout en conservant un aspect lisse.

Nous voulons dessiner des trapèzes minces et hauts qui s’étendent du bas de l’écran jusqu’à la surface de l’eau et relient les sources, comme indiqué dans ce schéma:

Comme les cartes graphiques ne dessinent pas de trapèzes directement, nous devons dessiner chaque trapèze sous forme de deux triangles. Pour rendre l’apparence un peu plus agréable, nous assombrirons également l’eau en coloriant les sommets inférieurs en bleu foncé. Le GPU interpolera automatiquement les couleurs entre les sommets.

primitiveBatch.Begin (PrimitiveType.TriangleList); Couleur midnightBlue = new Color (0, 15, 40) * 0.9f; Couleur lightBlue = new Couleur (0.2f, 0.5f, 1f) * 0.8f; var viewport = GraphicsDevice.Viewport; float bottom = viewport.Height; // étire les positions x des ressorts pour occuper toute la fenêtre float scale = viewport.Width / (springs.Length - 1f); // veillez à utiliser la division float pour (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();

Voici le résultat:


Faire les éclaboussures

Les vagues sont plutôt belles, mais j'aimerais voir des éclaboussures lorsque la roche heurte l'eau. Les effets de particules sont parfaits pour cela.

Effets de particules

Un effet de particule utilise un grand nombre de petites particules pour produire un effet visuel. Ils sont parfois utilisés pour des choses comme la fumée ou des étincelles. Nous allons utiliser des particules pour les gouttelettes d'eau dans les éclaboussures.

La première chose dont nous avons besoin est notre classe de particules:

classe Particle public Vector2 Position; public Vector2 Velocity; float Orientation; Particule publique (position Vector2, vitesse Vector2, orientation du flottement) Position = position; Vélocité = vélocité; Orientation = orientation; 

Cette classe ne contient que les propriétés qu'une particule peut avoir. Ensuite, nous créons une liste de particules.

liste particules = nouvelle liste();

Chaque image, nous devons mettre à jour et dessiner les particules.

void UpdateParticle (particule de particule) const float Gravity = 0.3f; particule.Vélocité.Y + = Gravité; particule.Position + = particule.Vitesse; particule.Orientation = GetAngle (particule.Vélocité);  float privé GetAngle (vecteur Vector2) return (float) Math.Atan2 (vector.Y, vector.X);  public void Update () foreach (particule var dans des particules) UpdateParticle (particule); // supprime les particules hors écran ou sous l'eau particules = particules.Où (x => x.Position.X> = 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList(); 

Nous mettons à jour les particules pour qu'elles tombent sous la gravité et définissons l'orientation de la particule pour qu'elle corresponde à la direction dans laquelle elle se dirige. l'assigner à des particules. Ensuite, nous attirons les particules.

void DrawParticle (particule de particule) Vector2 origin = nouveau Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, particule.Position, null, Couleur.Couleur blanche, particule.Orientation, origine, 0.6f, 0, 0);  public void Draw () foreach (particule var dans des particules) DrawParticle (particule); 

Ci-dessous la texture que j'ai utilisée pour les particules.

Maintenant, chaque fois que nous créons une éclaboussure, nous faisons un tas de particules.

CreateSplashParticles (float xPosition, vitesse de flottement) float y = GetHeight (xPosition); si (vitesse> 60) pour (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));   

Vous pouvez appeler cette méthode à partir du Éclaboussure() méthode que nous utilisons pour faire des vagues. Le paramètre de vitesse est la vitesse à laquelle la roche frappe l'eau. Nous ferons de plus grosses éclaboussures si la pierre bouge plus vite.

GetRandomVector2 (40) renvoie un vecteur avec une direction aléatoire et une longueur aléatoire comprise entre 0 et 40. Nous voulons ajouter un peu de hasard aux positions pour que les particules n'apparaissent pas toutes en un seul point.. DePolar () renvoie un Vecteur2 avec une direction et une longueur données.

Voici le résultat:

Utiliser des Metaballs en Particules

Nos éclaboussures ont l’air très correctes, et certains grands jeux, comme World of Goo, ont des éclaboussures aux effets de particules qui ressemblent beaucoup aux nôtres. Cependant, je vais vous montrer une technique pour rendre les éclaboussures plus liquides. La technique utilise des metaballs, des blobs d'aspect organique sur lesquels j'ai déjà écrit un tutoriel. Si vous souhaitez en savoir plus sur les métaballons et leur fonctionnement, lisez ce didacticiel. Si vous voulez juste savoir comment les appliquer à nos éclaboussures, continuez à lire.

Les metaballs ont une apparence liquide dans la façon dont ils fusionnent, ce qui les rend bien compatibles avec nos éclaboussures de liquide. Pour faire les metaballs, nous devrons ajouter de nouvelles variables de classe:

RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;

Ce que nous initialisons comme suit:

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

Ensuite, nous dessinons les metaballs:

GraphicsDevice.SetRenderTarget (metaballTarget); GraphicsDevice.Clear (Color.Transparent); Couleur lightBlue = nouvelle couleur (0.2f, 0.5f, 1f); spriteBatch.Begin (0, BlendState.Additive); foreach (particule var en particules) Origine du vecteur 2 = nouveau vecteur 2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, particule.Position, null, lightBlue, particule.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 (); // dessine des vagues et d'autres choses

L'effet metaball dépend de la texture des particules qui s'estompe à mesure que vous vous éloignez du centre. Voici ce que j'ai utilisé, placé sur un fond noir pour le rendre visible:

Voici à quoi ça ressemble:

Les gouttelettes d'eau fusionnent maintenant lorsqu'elles sont proches. Cependant, ils ne fusionnent pas avec la surface de l'eau. Nous pouvons résoudre ce problème en ajoutant un dégradé à la surface de l’eau qui la fait disparaître progressivement et en la rendant à notre cible de rendu des métaboules..

Ajoutez le code suivant à la méthode ci-dessus avant la ligne GraphicsDevice.SetRendertarget (null):

primitiveBatch.Begin (PrimitiveType.TriangleList); épaisseur de flottement constante = 20; float scale = GraphicsDevice.Viewport.Width / (springs.Length - 1f); pour (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();

Maintenant, les particules vont fusionner avec la surface de l'eau.

Ajout de l'effet de biseautage

Les particules d'eau semblent un peu plates et il serait bien de leur donner de l'ombre. Idéalement, vous le feriez dans un shader. Cependant, afin de garder ce tutoriel simple, nous allons utiliser un truc simple et rapide: nous allons simplement dessiner les particules trois fois avec différentes teintes et décalages, comme illustré dans le diagramme ci-dessous.

Pour ce faire, nous voulons capturer les particules de métaball dans une nouvelle cible de rendu. Nous allons ensuite dessiner cette cible de rendu une fois pour chaque teinte.

Tout d'abord, déclarer un nouveau RenderTarget2D comme nous l'avons fait pour les metaballs:

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

Ensuite, au lieu de dessiner metaballsTarget directement à la cartouche, nous voulons l’attirer sur particules cible. Pour ce faire, allez à la méthode où nous dessinons les metaballs et changeons simplement ces lignes:

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

… à:

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

Utilisez ensuite le code suivant pour dessiner les particules trois fois avec différentes teintes et décalages:

Couleur lightBlue = nouvelle couleur (0.2f, 0.5f, 1f); GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue); spriteBatch.Begin (); spriteBatch.Draw (particulesTarget, -Vector2.One, nouvelle couleur (0.8f, 0.8f, 1f)); spriteBatch.Draw (particulesTarget, Vector2.One, nouvelle couleur (0f, 0f, 0.2f)); spriteBatch.Draw (particulesTarget, Vector2.Zero, lightBlue); spriteBatch.End (); // dessine des vagues et d'autres choses

Conclusion

Voilà pour l'eau 2D de base. Pour la démo, j'ai ajouté un rocher que vous pouvez laisser tomber dans l'eau. Je tire l'eau avec un peu de transparence sur le rocher pour lui donner l'impression d'être sous l'eau, et le ralentir lorsqu'il est sous l'eau en raison de sa résistance à l'eau.

Pour rendre la démo un peu plus agréable, je suis allé sur opengameart.org et j'ai trouvé une image pour le rocher et un fond de ciel. Vous pouvez trouver le ciel et la roche à l’adresse http://opengameart.org/content/rocks et opengameart.org/content/sky-backdrop respectivement.