Hacer un chapoteo con efectos dinámicos de agua 2D

Sploosh! En este tutorial, te mostraré cómo puedes usar matemática simple, física y efectos de partículas para simular ondas y gotas de agua en 2D de gran apariencia.

Nota: Aunque este tutorial está escrito con C # y XNA, deberías poder usar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos.


Vista previa del resultado final

Si tiene XNA, puede descargar los archivos de origen y compilar la demostración por sí mismo. De lo contrario, echa un vistazo al siguiente video de demostración:

Hay dos partes en su mayoría independientes a la simulación de agua. Primero, haremos las olas usando un modelo de resorte. Segundo, usaremos efectos de partículas para agregar salpicaduras..


Haciendo las olas

Para hacer las olas, modelaremos la superficie del agua como una serie de manantiales verticales, como se muestra en este diagrama:

Esto permitirá que las olas suban y bajen. Luego haremos que las partículas de agua atraigan a sus partículas vecinas para permitir que las ondas se propaguen..

Muelles y ley de Hooke

Una gran cosa acerca de los resortes es que son fáciles de simular. Los manantiales tienen una cierta longitud natural; Si estiras o comprimes un resorte, intentará volver a esa longitud natural..

La fuerza proporcionada por un resorte está dada por la Ley de Hooke:

\ [
F = -kx
\]

F Es la fuerza producida por el resorte., k es la constante de primavera, y X Es el desplazamiento de la primavera de su longitud natural. El signo negativo indica que la fuerza está en la dirección opuesta a la que se desplaza el resorte; Si empuja el resorte hacia abajo, empujará hacia arriba y viceversa..

La constante de primavera, k, Determina la rigidez de la primavera..

Para simular los resortes, debemos averiguar cómo mover las partículas en función de la Ley de Hooke. Para hacer esto, necesitamos un par de fórmulas más de la física. Primero, la segunda ley de movimiento de Newton:

\ [
F = ma
\]

aquí, F es fuerza, metro es masa y una es la aceleracion Esto significa que una fuerza más fuerte empuja un objeto, y cuanto más ligero es el objeto, más acelera..

Combinar estas dos fórmulas y reorganizar nos da:

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

Esto nos da la aceleración de nuestras partículas. Asumiremos que todas nuestras partículas tendrán la misma masa, por lo que podemos combinar k / m en una sola constante.

Para determinar la posición de la aceleración, necesitamos hacer una integración numérica. Vamos a utilizar la forma más simple de integración numérica: en cada fotograma simplemente hacemos lo siguiente:

Posición + = Velocidad; Velocidad + = aceleración;

Esto se llama el método de Euler. No es el tipo de integración numérica más preciso, pero es rápido, simple y adecuado para nuestros propósitos.

Juntándolo todo, nuestras partículas de la superficie del agua harán lo siguiente en cada cuadro:

Posición flotante pública, velocidad; Public void Update () const float k = 0.025f; // ajusta este valor a tu gusto float x = Height - TargetHeight; aceleración de flotación = -k * x; Posición + = Velocidad; Velocidad + = aceleración; 

aquí, TargetHeight es la posición natural de la parte superior del resorte cuando no está estirado ni comprimido. Debe establecer este valor en el lugar donde desea que esté la superficie del agua. Para la demostración, lo puse en la mitad de la pantalla, a 240 píxeles..

Tensión y humedecimiento

Mencioné anteriormente que la constante de primavera, k, Controla la rigidez de la primavera. Puedes ajustar este valor para cambiar las propiedades del agua. Una baja constante de resorte hará que los resortes se suelten. Esto significa que una fuerza causará ondas grandes que oscilan lentamente. Por el contrario, una alta constante de resorte aumentará la tensión en el resorte. Las fuerzas crearán pequeñas olas que oscilarán rápidamente. Una alta constante de manantial hará que el agua se vea más como jalea de gelatina.

Una advertencia: no establezca la constante de resorte demasiado alta. Los resortes muy rígidos aplican fuerzas muy fuertes que cambian enormemente en muy poco tiempo. Esto no funciona bien con la integración numérica, que simula los resortes como una serie de saltos discretos en intervalos de tiempo regulares. Un resorte muy rígido puede incluso tener un período de oscilación más corto que el paso de tiempo. Peor aún, el método de integración de Euler tiende a ganar energía a medida que la simulación se vuelve menos precisa, lo que hace que los resortes rígidos exploten..

Hay un problema con nuestro modelo de primavera hasta ahora. Una vez que un resorte comienza a oscilar, nunca se detendrá. Para resolver esto debemos aplicar algunos humedecer. La idea es aplicar una fuerza en la dirección opuesta a la que se mueve nuestro resorte para reducir la velocidad. Esto requiere un pequeño ajuste a nuestra fórmula de primavera:

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

aquí, v es la velocidad y re es el factor de amortiguación - Otra constante que puedes modificar para ajustar la sensación del agua. Debería ser bastante pequeño si quieres que tus ondas oscilen. La demo utiliza un factor de amortiguación de 0.025. Un alto factor de amortiguación hará que el agua se vea espesa como la melaza, mientras que un valor bajo permitirá que las olas oscilen durante mucho tiempo..

Haciendo que las ondas se propaguen

Ahora que podemos hacer un manantial, usémoslo para modelar el agua. Como se muestra en el primer diagrama, estamos modelando el agua usando una serie de manantiales verticales paralelos. Por supuesto, si los resortes son todos independientes, las olas nunca se extenderán como las olas reales..

Primero mostraré el código y luego lo revisaré:

para (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 * (resortes [i] .Hight - resortes [i - 1] .Hight); resortes [i - 1]. Velocidad + = leftDeltas [i];  si yo < 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) resortes [i - 1]. Altura + = izquierdaDeltas [i]; si yo < springs.Length - 1) springs[i + 1].Height += rightDeltas[i];  

Este código se llamaría cada fotograma de su Actualizar() método. aquí, muelles Es una serie de resortes, dispuestos de izquierda a derecha.. izquierdaDeltas es una matriz de flotadores que almacena la diferencia de altura entre cada resorte y su vecino izquierdo. rightDeltas Es el equivalente para los vecinos correctos. Almacenamos todas estas diferencias de altura en matrices porque las dos últimas Si Las declaraciones modifican las alturas de los manantiales. Tenemos que medir las diferencias de altura antes de modificar cualquiera de las alturas..

El código comienza ejecutando la Ley de Hooke en cada primavera como se describió anteriormente. Luego observa la diferencia de altura entre cada resorte y sus vecinos, y cada resorte tira de sus resortes vecinos hacia sí mismo al alterar las posiciones y velocidades de los vecinos. El paso del tirón del vecino se repite ocho veces para permitir que las ondas se propaguen más rápido.

Hay un valor más tweakable aquí llamado Untado. Controla qué tan rápido se propagan las olas. Puede tomar valores entre 0 y 0.5, con valores más grandes que hacen que las ondas se extiendan más rápido.

Para comenzar a mover las olas, vamos a agregar un método simple llamado Chapoteo().

Splash del vacío público (índice int, velocidad de flotación) if (index> = 0 && index < springs.Length) springs[i].Speed = speed; 

Cuando quieras hacer olas, llama. Chapoteo(). los índice el parámetro determina en qué primavera debe originarse la salpicadura, y la velocidad parámetro determina qué tan grandes serán las olas.

Representación

Estaremos usando el XNA Lote primitivo clase del XNA PrimitivesSample. los Lote primitivo La clase nos ayuda a dibujar líneas y triángulos directamente con la GPU. Lo usas así:

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

Una cosa a tener en cuenta es que, de forma predeterminada, debe especificar los vértices de triángulos en el orden de las agujas del reloj. Si los agrega en el sentido contrario a las agujas del reloj, el triángulo se eliminará y no lo verá.

No es necesario tener un resorte para cada píxel de ancho. En la demostración utilicé 201 resortes distribuidos en una ventana de 800 píxeles de ancho. Eso da exactamente 4 píxeles entre cada resorte, con el primer resorte en 0 y el último en 800 píxeles. Probablemente podrías usar incluso menos manantiales y aún así hacer que el agua se vea suave.

Lo que queremos hacer es dibujar trapecios delgados y altos que se extiendan desde la parte inferior de la pantalla hasta la superficie del agua y conecten los resortes, como se muestra en este diagrama:

Como las tarjetas gráficas no dibujan trapezoides directamente, tenemos que dibujar cada trapecio como dos triángulos. Para que se vea un poco más agradable, también haremos que el agua se oscurezca a medida que se profundiza al colorear los vértices inferiores de color azul oscuro. La GPU interpolará automáticamente los colores entre los vértices.

primitiveBatch.Begin (PrimitiveType.TriangleList); Color midnightBlue = nuevo Color (0, 15, 40) * 0.9f; Color lightBlue = new Color (0.2f, 0.5f, 1f) * 0.8f; var viewport = GraphicsDevice.Viewport; float bottom = viewport.Height; // estirar las posiciones x de los resortes para ocupar toda la ventana float scale = viewport.Width / (springs.Length - 1f); // asegúrese de usar la división flotante para (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();

Aquí está el resultado:


Haciendo las salpicaduras

Las olas se ven bastante bien, pero me gustaría ver un chapoteo cuando la roca golpea el agua. Los efectos de partículas son perfectos para esto..

Efectos de partículas

Un efecto de partículas utiliza una gran cantidad de partículas pequeñas para producir algún efecto visual. A veces se usan para cosas como humo o chispas. Vamos a utilizar partículas para las gotas de agua en las salpicaduras..

Lo primero que necesitamos es nuestra clase de partículas:

clase Partícula public Vector2 Position; pública Vector2 Velocity; flotación pública Orientación; Partícula pública (posición Vector2, velocidad Vector2, orientación flotante) Posición = posición; Velocidad = velocidad; Orientación = orientación; 

Esta clase solo tiene las propiedades que una partícula puede tener. A continuación, creamos una lista de partículas..

Lista partículas = nueva lista();

Cada cuadro, debemos actualizar y dibujar las partículas..

void UpdateParticle (Partícula partícula) const float Gravity = 0.3f; partícula.Velocidad.Y + = Gravedad; Partícula.Posición + = Partícula.Velocidad; partícula.orientación = GetAngle (partícula.Velocidad);  private float GetAngle (Vector2 vector) return (float) Math.Atan2 (vector.Y, vector.X);  Public void Update () foreach (var partícula en partículas) UpdateParticle (partícula); // eliminar partículas que están fuera de la pantalla o debajo del agua partículas = partículas.Donde (x => x.Position.X> = 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList(); 

Actualizamos las partículas para que caigan bajo la gravedad y configuramos la orientación de la partícula para que coincida con la dirección en la que va. Luego, nos deshacemos de las partículas que están fuera de la pantalla o debajo del agua al copiar todas las partículas que queremos mantener en una nueva lista y asignándolo a partículas. A continuación dibujamos las partículas..

void DrawParticle (Partícula partícula) Vector2 origen = nuevo Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, partícula.Posición, nula, Color.Blanco, partícula.Orientación, origen, 0.6f, 0, 0);  public void Draw () foreach (var partícula en partículas) DrawParticle (partícula); 

A continuación se muestra la textura que utilicé para las partículas..

Ahora, cada vez que creamos una salpicadura, hacemos un montón de partículas..

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

Puedes llamar a este método desde el Chapoteo() Método que utilizamos para hacer olas. La velocidad del parámetro es qué tan rápido la roca golpea el agua. Haremos salpicaduras más grandes si la roca se mueve más rápido..

GetRandomVector2 (40) devuelve un vector con una dirección aleatoria y una longitud aleatoria entre 0 y 40. Queremos agregar un poco de aleatoriedad a las posiciones para que las partículas no aparezcan todas en un solo punto. DesdePolar () devuelve un Vector2 con una dirección y longitud dadas.

Aquí está el resultado:

Usando Metaballs como Partículas

Nuestras salpicaduras parecen bastante decentes, y algunos juegos geniales, como World of Goo, tienen salpicaduras de efecto de partículas que se parecen mucho a las nuestras. Sin embargo, te mostraré una técnica para hacer que las salpicaduras se vean más líquidas. La técnica es el uso de metaballs, burbujas de aspecto orgánico sobre las que he escrito un tutorial. Si está interesado en los detalles sobre los metaballs y cómo funcionan, lea ese tutorial. Si solo quieres saber cómo aplicarlos a nuestras salpicaduras, sigue leyendo..

Metaballs se ve como un líquido en la forma en que se fusionan, por lo que son un buen partido para nuestras salpicaduras de líquidos. Para hacer los metaballs, necesitaremos agregar nuevas variables de clase:

RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;

Que inicializamos como tal:

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

Luego dibujamos los metaballs:

GraphicsDevice.SetRenderTarget (metaballTarget); GraphicsDevice.Clear (Color.Transparent); Color lightBlue = new Color (0.2f, 0.5f, 1f); spriteBatch.Begin (0, BlendState.Additive); foreach (var partícula en partículas) Vector2 origen = nuevo Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, partícula.Posición, nulo, lightBlue, partícula.Orientación, origen, 2f, 0, 0);  spriteBatch.End (); GraphicsDevice.SetRenderTarget (null); dispositivo.Clear (Color.CornflowerBlue); spriteBatch.Begin (0, null, null, null, null, alphaTest); spriteBatch.Draw (metaballTarget, Vector2.Zero, Color.White); spriteBatch.End (); // dibujar olas y otras cosas

El efecto metaball depende de tener una textura de partículas que se desvanece a medida que te alejas del centro. Esto es lo que usé, establecido sobre un fondo negro para hacerlo visible:

Esto es lo que parece:

Las gotas de agua ahora se fusionan cuando están cerca. Sin embargo, no se fusionan con la superficie del agua. Podemos solucionar esto agregando un gradiente a la superficie del agua que lo haga desaparecer gradualmente y convirtiéndolo en nuestro objetivo de procesamiento metaball.

Agregue el siguiente código al método anterior antes de la línea GraphicsDevice.SetRendertarget (null):

primitiveBatch.Begin (PrimitiveType.TriangleList); espesor de flotación constante = 20; escala de flotación = GraphicsDevice.Viewport.Width / (springs.Length - 1f); para (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();

Ahora las partículas se fusionarán con la superficie del agua..

Añadiendo el efecto de biselado

Las partículas de agua se ven un poco planas, y sería bueno darles un poco de sombra. Idealmente, harías esto en un shader. Sin embargo, por el simple hecho de mantener este tutorial simple, usaremos un truco rápido y sencillo: simplemente dibujaremos las partículas tres veces con diferentes matices y compensaciones, como se ilustra en el diagrama a continuación..

Para hacer esto, queremos capturar las partículas metaball en un nuevo objetivo de procesamiento. Luego dibujaremos ese objetivo de renderizado una vez para cada tinte..

Primero, declara un nuevo. RenderTarget2D Al igual que hicimos para los metaballs:

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

Entonces, en lugar de dibujar metaballsTarget directamente al backbuffer, queremos dibujarlo en las partículas se dirigen. Para hacer esto, vaya al método donde dibujamos los metaballs y simplemente cambie estas líneas:

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

… a:

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

Luego use el siguiente código para dibujar las partículas tres veces con diferentes matices y compensaciones:

Color lightBlue = new Color (0.2f, 0.5f, 1f); GraphicsDevice.SetRenderTarget (null); dispositivo.Clear (Color.CornflowerBlue); spriteBatch.Begin (); spriteBatch.Draw (cellsTarget, -Vector2.One, nuevo Color (0.8f, 0.8f, 1f)); spriteBatch.Draw (cellsTarget, Vector2.One, nuevo Color (0f, 0f, 0.2f)); spriteBatch.Draw (modulesTarget, Vector2.Zero, lightBlue); spriteBatch.End (); // dibujar olas y otras cosas

Conclusión

Eso es todo para agua 2D básica. Para la demostración, agregué una roca que puedes tirar al agua. Extraigo el agua con algo de transparencia sobre la roca para que se vea como si estuviera bajo el agua, y la hago más lenta cuando está bajo el agua debido a la resistencia al agua..

Para hacer que la demostración se vea un poco mejor, fui a opengameart.org y encontré una imagen para la roca y un fondo de cielo. Puede encontrar la roca y el cielo en http://opengameart.org/content/rocks y opengameart.org/content/sky-backdrop respectivamente.