Uso de sombreadores de desplazamiento para crear un efecto bajo el agua

A pesar de su notoriedad, la creación de niveles de agua es una tradición consagrada en la historia de los videojuegos, ya sea para agitar la mecánica del juego o simplemente porque el agua es tan hermosa de ver. Hay varias formas de producir una sensación bajo el agua, desde imágenes simples (como teñir la pantalla azul) hasta mecánicos (como movimiento lento y gravedad débil). 

Vamos a ver la distorsión como una forma de comunicar visualmente la presencia de agua (imagina que estás parado en el borde de una piscina y observando cosas dentro, ese es el tipo de efecto que queremos recrear). Puede ver una demostración del aspecto final aquí en CodePen.

Usaré Shadertoy a lo largo del tutorial para que pueda seguirlo directamente en su navegador. Trataré de mantener la plataforma bastante independiente para que pueda implementar lo que aprende aquí en cualquier entorno que admita sombreadores gráficos. Al final, proporcionaré algunos consejos de implementación, así como el código JavaScript que utilicé para implementar el ejemplo anterior con la biblioteca de juegos Phaser..

Puede parecer un poco complicado, ¡pero el efecto en sí es solo un par de líneas de código! No es más que diferentes efectos de desplazamiento compuestos juntos. Empezaremos desde cero y veremos exactamente lo que eso significa..

Renderizando una imagen básica

Dirígete a Shadertoy y crea un nuevo sombreador. Antes de que podamos aplicar cualquier distorsión, necesitamos renderizar una imagen. Sabemos por tutoriales anteriores que solo necesitamos seleccionar una imagen en uno de los canales inferiores de la página y mapearla en la pantalla con textura2D:

vec2 uv = fragCoord.xy / iResolution.xy; // Obtener la posición normalizada del píxel actual fragColor = textura2D (iChannel0, uv); // Obtenga el color del píxel actual en la textura y ajústelo al color en la pantalla

Esto es lo que elegí:

Nuestro primer desplazamiento

Ahora, ¿qué sucede si en lugar de simplemente representar el píxel en la posición? uv, renderizamos el pixel a uv + vec2 (0.1.0.0)?

Siempre es más fácil pensar en lo que sucede en un solo píxel cuando se trabaja con sombreadores. Dada cualquier posición en la pantalla, en lugar de dibujar el color original en la textura, se dibujará el color de un píxel a su derecha. Eso significa, visualmente, que todo cambia izquierda. Intentalo!

Por defecto, Shadertoy establece el modo de ajuste en todas las texturas para repetir. Entonces, si intentas muestrear un píxel a la derecha del píxel más a la derecha, simplemente se ajustará. Aquí, lo cambié a abrazadera (que puede hacer desde el icono de engranaje en el cuadro donde seleccionó la textura).

Desafío: ¿Puedes hacer que toda la imagen se mueva lentamente hacia la derecha? ¿Qué hay de moverse de un lado a otro? Que tal en un circulo? 

Pista: Shadertoy te da una variable de tiempo de ejecución llamada iGlobalTime.

Desplazamiento no uniforme

Mover una imagen completa no es muy emocionante y no requiere la potencia altamente paralela de la GPU. ¿Qué pasa si, en lugar de desplazar cada posición en una cantidad fija (como 0.1), desplazamos píxeles diferentes en cantidades diferentes??

Necesitamos una variable que de alguna manera sea única para cada píxel. Cualquier variable que declare o uniforme que pase no variará entre los píxeles. Por suerte, ya tenemos algo que varía de esta manera: el píxel propio X y y. Prueba esto:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = uv.x; // Mueve la y por el píxel actual x fragColor = textura2D (iChannel0, uv);

Estamos compensando verticalmente cada píxel por su valor de x. Los píxeles de la izquierda obtendrán el menor desplazamiento (0) mientras que los más a la derecha obtendrán el máximo desplazamiento (1).

Ahora tenemos un valor que varía en la imagen de 0 a 1. Estamos usando esto para empujar los píxeles hacia abajo, por lo que tenemos esta inclinación. Ahora para tu próximo reto!

Desafío: ¿Puedes usar esto para crear una ola? (Como se muestra abajo)

Sugerencia: su variable de compensación va de 0 a 1. En su lugar, desea que vaya periódicamente de -1 a 1. La función coseno / seno es una opción perfecta para eso.

Añadiendo tiempo

Si descubrió el efecto de onda, ¡intente hacer que se mueva hacia adelante y hacia atrás multiplicando por nuestra variable de tiempo! Aquí está mi intento de que hasta ahora:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25.) * 0.06 * cos (iGlobalTime); fragColor = texture2D (iChannel0, uv);

Yo multiplico uv.x por un número grande (25) para controlar la frecuencia de la onda. Luego lo reduzco multiplicando por 0.06, así que esa es la amplitud máxima. Finalmente, lo multiplico por el coseno del tiempo, para que periódicamente se mueva de un lado a otro..

Nota: Si realmente desea confirmar que nuestra distorsión está siguiendo una onda sinusoidal, cambie ese 0.06 a un 1.0 y obsérvelo en su máximo!

Desafío: ¿Puedes imaginarte cómo hacer que se mueva más rápido??

Pista: es el mismo concepto que usamos para aumentar la frecuencia de la onda espacialmente.

Mientras estás en eso, otra cosa que puedes intentar es aplicar lo mismo para uv.x también, por lo que es distorsionante tanto en la x como en la y (y tal vez cambiar los cos's por los del pecado).

Ahora esto es moviéndose en un movimiento ondulatorio, pero algo está mal. Así no es como se comporta el agua ...

Una manera diferente de agregar tiempo

El agua necesita verse como si estuviera fluyendo. Lo que tenemos ahora es simplemente ir y venir. Examinemos nuestra ecuación de nuevo:

Nuestra frecuencia no está cambiando, lo que es bueno por ahora, pero tampoco queremos que cambie nuestra amplitud. Queremos que la ola mantenga la misma forma, pero para movimiento a través de la pantalla.

Para ver dónde en nuestra ecuación queremos compensar, piense qué determina dónde comienza y termina la onda. uv.x Es la variable dependiente en ese sentido. Donde quiera uv.x es pi / 2, no habrá desplazamiento (ya que cos (pi / 2) = 0), y donde uv.x esta alrededor pi / 2, eso será máximo desplazamiento.

Vamos a ajustar un poco nuestra ecuación:

Ahora tanto nuestra amplitud como nuestra frecuencia son fijas, y lo único que varía será la posición de la onda en sí. Con ese poco de teoría fuera del camino, es hora de un desafío!

Desafío: implemente esta nueva ecuación y modifique los coeficientes para obtener un movimiento ondulado agradable.

Poniendolo todo junto

Aquí está mi código para lo que tenemos hasta ahora:

vec2 uv = fragCoord.xy / iResolution.xy; uv.y + = cos (uv.x * 25. + iGlobalTime) * 0.01; uv.x + = cos (uv.y * 25. + iGlobalTime) * 0.01; fragColor = texture2D (iChannel0, uv);

Ahora, este es esencialmente el corazón del efecto. Sin embargo, podemos seguir modificando las cosas para que se vea aún mejor. Por ejemplo, no hay razón para que varíe la onda solo con la coordenada x o y. ¡Puedes cambiar ambos, por lo que varía diagonalmente! Aquí hay un ejemplo:

float X = uv.x * 25. + iGlobalTime; float Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0.01; uv.x + = sin (X-Y) * 0.01;

Parecía un poco repetitivo, así que cambié la segunda cos por un pecado para arreglar eso. Mientras estamos en ello, también podemos intentar variar un poco la amplitud:

float X = uv.x * 25. + iGlobalTime; float Y = uv.y * 25. + iGlobalTime; uv.y + = cos (X + Y) * 0.01 * cos (Y); uv.x + = sin (X-Y) * 0.01 * sin (Y);

Y eso es todo lo que he conseguido, pero siempre puedes componer y combinar más funciones para obtener diferentes resultados.!

Aplicándolo a una sección de la pantalla

Lo último que quiero mencionar en el sombreador es que en la mayoría de los casos, probablemente deba aplicar el efecto solo a una parte de la pantalla en lugar de a todo. Una forma fácil de hacerlo es pasar una máscara. Esta sería una imagen que mapea las áreas de la pantalla que deberían verse afectadas. Los que son transparentes (o blancos) no pueden verse afectados, y los píxeles opacos (o negros) pueden tener el efecto completo..

En Shadertoy, no puedes subir imágenes arbitrarias, pero puedes renderizar en un búfer separado y pasarlo como una textura. Aquí hay un enlace de Shadertoy en el que aplico el efecto anterior a la mitad inferior de la pantalla..

La máscara que se pasa no necesita ser una imagen estática. Puede ser una cosa completamente dinámica; Mientras pueda renderizarlo en tiempo real y pasarlo al sombreador, su agua puede moverse o fluir a través de la pantalla sin problemas.

Implementándolo en JavaScript

Utilicé Phaser.js para implementar este shader. Puede consultar la fuente en este CodePen en vivo o descargar una copia local desde este repositorio.

Puedes ver cómo paso las imágenes manualmente como uniformes, y también tengo que actualizar la variable de tiempo..

El mayor detalle de implementación a considerar es a qué aplicar este sombreador. Tanto en el ejemplo de Shadertoy como en mi ejemplo de JavaScript, solo tengo una imagen en el mundo. En un juego, probablemente vas a tener mucho más.

Phaser te permite aplicar sombreadores a objetos individuales, pero también puedes aplicarlo al objeto mundial, que es mucho más eficiente. De manera similar, podría ser una buena idea en otra plataforma representar todos los objetos en un búfer y pasarlo a través del sombreador de agua, en lugar de aplicarlo a cada objeto individual. De esa manera funciona como un efecto de post-procesamiento..

Conclusión

Espero que la composición de este shader desde cero te haya dado una buena idea de cómo se construyen muchos efectos complejos mediante la combinación de todos estos pequeños desplazamientos.!

Como desafío final, aquí hay una especie de sombreador de ondas de agua que se basa en el mismo tipo de ideas de desplazamiento que vimos. Podrías intentar desarmarlo, desplegar las capas y descubrir qué hace cada pieza.!