Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Shaders
Gamedevelopment

Creando Agua de Caricatura para la Web: Parte 2

by
Difficulty:AdvancedLength:LongLanguages:
This post is part of a series called Creating Toon Water for the Web.
Creating Toon Water for the Web: Part 1
Creating Toon Water for the Web: Part 3

Spanish (Español) translation by RRGG (you can also view the original English article)

Bienvenidos a esta serie de tres partes sobre la creación de agua toon estilizada en PlayCanvas utilizando sombreadores de vértices. En la Parte 1, cubrimos la configuración de nuestro medio ambiente y la superficie del agua. Esta parte cubrirá la aplicación de flotabilidad a los objetos, la adición de líneas de agua a la superficie y la creación de líneas de espuma con el amortiguador de profundidad alrededor de los bordes de los objetos que se cruzan con la superficie.

Hice algunos pequeños cambios en mi escena para que se vea un poco mejor. Puedes personalizar tu escena como quieras, pero lo que hice fue:

  • Se agregó el faro y los modelos de pulpo.
  • Se agregó un plano de tierra con color # FFA457.
  • Se agregó un color claro para la cámara de # 6CC8FF.
  • Se agregó un color ambiental a la escena de # FFC480 (puede encontrarlo en la configuración de escenas).

A continuación se muestra mi punto de partida.

The scene now includes an octopus and a ligthouse

Flotabilidad

La forma más directa de crear flotabilidad es simplemente crear una secuencia de comandos que empujará los objetos hacia arriba y hacia abajo. Cree un nuevo script llamado Buoyancy.js y configure su inicialización en:

Ahora, en la actualización, incrementamos el tiempo y rotamos el objeto:

¡Aplica este script a tu bote y míralo subir y bajar en el agua! Puede aplicar esta secuencia de comandos a varios objetos (incluida la cámara; pruébelo).

Texturizando la superficie

En este momento, la única forma en que puedes ver las olas es mirando los bordes de la superficie del agua. Agregar una textura ayuda a que el movimiento en la superficie sea más visible y es una forma económica de simular reflejos y cáusticos.

Puede intentar encontrar alguna textura cáustica o hacer la suya propia. Aquí hay uno que dibujé en Gimp que puedes usar libremente. Cualquier textura funcionará siempre que se pueda embaldosar sin problemas.

Una vez que haya encontrado la textura que le gusta, arrástrela a la ventana de activos de su proyecto. Necesitamos hacer referencia a esta textura en nuestra secuencia de comandos Water.js, así que cree un atributo para ella:

Y luego asignarlo en el editor:

The water texture is added to the water script

Ahora tenemos que pasarlo a nuestro sombreador. Vaya a Water.js y establezca un nuevo parámetro en la función CreateWaterMaterial:

Ahora ve a Water.frag y declara nuestro nuevo uniforme:

Casi estámos allí. Para representar la textura en el plano, necesitamos saber dónde está cada píxel a lo largo de la malla. Lo que significa que necesitamos pasar algunos datos del sombreador de vértices al sombreador de fragmentos.

Variables Variantes

Una variable variable le permite pasar datos del sombreador de vértices al sombreador de fragmentos. Este es el tercer tipo de variable especial que puede tener en un sombreado (los otros dos son uniformes y atributos). Se define para cada vértice y es accesible para cada píxel. Como hay muchos más píxeles que vértices, el valor se interpola entre los vértices (de ahí viene el nombre "variable"; varía de los valores que le das).

Para probar esto, declare una nueva variable en Water.vert como variable:

Y luego configúralo en gl_Position después de haber sido computado:

Ahora regrese a Water.frag y declare la misma variable. No hay forma de obtener algún resultado de depuración dentro de un sombreado, pero podemos usar color para depurar visualmente. Aquí hay una manera de hacer esto:

El plano ahora debe verse en blanco y negro, donde la línea que los separa es donde ScreenPosition.x es 0. Los valores de color solo van de 0 a 1, pero los valores en ScreenPosition pueden estar fuera de este rango. Se bloquean automáticamente, por lo que si ves negro, podría ser 0 o negativo.

Lo que acabamos de hacer pasa la posición de pantalla de cada vértice a cada píxel. Puede ver que la línea que separa los lados blanco y negro siempre estará en el centro de la pantalla, independientemente de dónde se encuentre realmente la superficie en el mundo.

Reto # 1: Crea una nueva variable variable para pasar la posición mundial en lugar de la posición de la pantalla. Visualízalo de la misma manera que lo hicimos arriba. Si el color no cambia con la cámara, entonces has hecho esto correctamente.

Usando UVs

Los UV son las coordenadas 2D para cada vértice a lo largo de la malla, normalizados de 0 a 1. Esto es exactamente lo que necesitamos para muestrear la textura en el plano correctamente, y ya debe estar configurada desde la parte anterior.

Declare un nuevo atributo en Water.vert (este nombre proviene de la definición del sombreador en Water.js):

Y todo lo que tenemos que hacer es pasarlo al sombreador de fragmentos, de modo que simplemente cree una variación y establézcala en el atributo:

Ahora declaramos las mismas variaciones en el sombreador de fragmentos. Para verificar que funcione, podemos visualizarlo como antes, de modo que Water.frag ahora se vea así:

Y debería ver un degradado, confirmando que tenemos un valor de 0 en un extremo y 1 en el otro. Ahora, para realmente muestrear nuestra textura, todo lo que tenemos que hacer es:

Y deberías ver la textura en la superficie:

Caustics texture is applied to the water surface

Estilizando la Textura

En lugar de simplemente configurar la textura como nuestro nuevo color, vamos a combinarlo con el azul que teníamos:

Esto funciona porque el color de la textura es negro (0) en todas partes a excepción de las líneas de agua. Al agregarlo, no cambiamos el color azul original a excepción de los lugares donde hay líneas, donde se vuelve más brillante.

Sin embargo, ésta no es la única manera de combinar los colores.

Reto # 2: ¿Puedes combinar los colores de una manera para obtener el efecto más sutil que se muestra a continuación?
Water lines applied to the surface with a more subtle color

Moviendo la Textura

Como efecto final, queremos que las líneas se muevan a lo largo de la superficie para que no parezcan tan estáticas. Para hacer esto, usamos el hecho de que cualquier valor dado a la función texture2D fuera del rango de 0 a 1 se ajustará (de modo que 1.5 y 2.5 se convertirán en 0.5). De modo que podemos incrementar nuestra posición en la variable de tiempo uniforme que ya configuramos y multiplicar la posición para aumentar o disminuir la densidad de las líneas en nuestra superficie, haciendo que nuestro shader de fragmentación final se vea así:

Líneas de Espuma y Buffer de Profundidad

Hacer líneas de espuma alrededor de objetos en el agua hace que sea mucho más fácil ver cómo se sumergen los objetos y dónde cortan la superficie. También hace que nuestra agua se vea mucho más creíble. Para hacer esto, necesitamos de alguna manera averiguar dónde están los bordes en cada objeto, y hacer esto de manera eficiente.

El Truco

Lo que queremos es poder decir, dado un píxel en la superficie del agua, si está cerca de un objeto. Si es así, podemos colorearlo como espuma. No hay una manera directa de hacer esto (que yo sepa). Entonces, para resolver esto, vamos a utilizar una técnica útil para resolver problemas: inventemos un ejemplo del que sepamos la respuesta y veamos si podemos generalizarlo.

Considera la vista a continuación.

Lighthouse in water

¿Cuáles píxeles deberían ser parte de la espuma? Sabemos que se debería ver algo así:

Lighthouse in water with foam

Así que pensemos en dos píxeles específicos. Marqué dos con estrellas a continuación. El negro está en la espuma. El rojo no es. ¿Cómo podemos distinguirlos dentro de un sombreador?

Lighthouse in water with two marked pixels

Lo que sabemos es que a pesar de que esos dos píxeles están muy juntos en el espacio de la pantalla (ambos se muestran directamente sobre el cuerpo del faro), en realidad están muy separados en el espacio mundial. Podemos verificar esto mirando la misma escena desde un ángulo diferente, como se muestra a continuación.

Viewing the lighthouse from above

Observe que la estrella roja no está en la parte superior del cuerpo del faro como apareció, pero la estrella negra en realidad sí lo está. Podemos diferenciarlos usando la distancia a la cámara, comúnmente conocida como "profundidad", donde una profundidad de 1 significa que está muy cerca de la cámara y una profundidad de 0 significa que está muy lejos. Pero no se trata solo de la distancia o profundidad absoluta del mundo a la cámara. Es la profundidad en comparación con el píxel detrás.

Mira hacia atrás a la primera vista. Digamos que el cuerpo del faro tiene un valor de profundidad de 0.5. La profundidad de la estrella negra sería muy cercana a 0.5. Entonces, y el píxel detrás de él tienen valores de profundidad similares. La estrella roja, por otro lado, tendría una profundidad mucho mayor, porque estaría más cerca de la cámara, digamos 0.7. Y, sin embargo, el píxel detrás de él, todavía en el faro, tiene un valor de profundidad de 0,5, por lo que hay una gran diferencia allí.

Este es el truco. Cuando la profundidad del píxel en la superficie del agua está lo suficientemente cerca de la profundidad del píxel sobre el que se dibuja, estamos bastante cerca del borde de algo, y podemos renderizarlo como espuma.

Entonces necesitamos más información de la disponible en cualquier pixel dado. De alguna manera, necesitamos saber la profundidad del píxel sobre el que se va a dibujar. Aquí es donde entra el búfer de profundidad

El Buffer de Profundidad

Puede pensar en un búfer, o en un framebuffer, como un objetivo de renderizado fuera de la pantalla o una textura. Desearía mostrar fuera de la pantalla cuando intente leer datos, una técnica que emplea este efecto de humo.

El búfer de profundidad es un objetivo de renderizado especial que contiene información sobre los valores de profundidad en cada píxel. Recuerde que el valor en gl_Position calculado en el sombreado de vértices era un valor de espacio de pantalla, pero también tenía una tercera coordenada, un valor Z. Este valor Z se usa para calcular la profundidad que se escribe en el buffer de profundidad.

El propósito del buffer de profundidad es dibujar nuestra escena correctamente, sin la necesidad de ordenar los objetos de atrás hacia adelante. Cada píxel que está por dibujarse consulta primero el búfer de profundidad. Si su valor de profundidad es mayor que el valor en el búfer, se dibuja y su propio valor sobrescribe el del búfer. De lo contrario, se descarta (porque significa que otro objeto está frente a él).

De hecho, puede desactivar la escritura en el búfer de profundidad para ver cómo se verían las cosas sin él. Puedes probar esto en Water.js:

Verá cómo el agua siempre se renderizará en la parte superior, incluso si está detrás de objetos opacos.

Visualizando el Buffer de Profundidad

Agreguemos una forma de visualizar el búfer de profundidad para depuración. Cree un nuevo script llamado DepthVisualize.js. Adjunte esto a su cámara.

Todo lo que tenemos que hacer para acceder al buffer de profundidad en PlayCanvas es decir:

Esto inyectará automáticamente un uniforme en todos nuestros sombreadores que podemos usar al declararlo como:

A continuación se muestra un script de muestra que solicita el mapa de profundidad y lo representa en la parte superior de nuestra escena. Está configurado para la recarga en caliente.

Intenta copiar eso y comenta / descomenta la línea this.app.scene.drawCalls.push (this.command); para alternar la representación de profundidad. Debería verse algo así como la imagen de abajo.

Boat and lighthouse scene rendered as a depth map
Reto # 3: La superficie del agua no se dibuja en el buffer de profundidad. El motor PlayCanvas lo hace intencionalmente. ¿Puedes averiguar por qué? ¿Qué tiene de especial el material de agua? Para decirlo de otra manera, de acuerdo con nuestras reglas de verificación de profundidad, ¿qué pasaría si los píxeles de agua escribieran en el buffer de profundidad?

Sugerencia: Hay una línea que puede cambiar en Water.js que hará que el agua se escriba en el búfer de profundidad.

Otra cosa para notar es que multiplico el valor de profundidad por 30 en el sombreador incrustado en la función de inicialización. Esto es solo para poder verlo con claridad, porque de lo contrario el rango de valores es demasiado pequeño para verse como sombras de color.

Implementando el Truco

El motor PlayCanvas incluye un conjunto de funciones de ayuda para trabajar con valores de profundidad, pero al momento de escribir no se lanzan a producción, así que vamos a configurarlas nosotros mismos.

Defina los siguientes uniformes para Water.frag:

Defina estas funciones auxiliares por encima de la función principal:

Pase alguna información sobre la cámara al sombreador en Water.js. Pon esto donde pasas otros uniformes como uTime:

Finalmente, necesitamos la posición mundial para cada píxel en nuestro frag shader. Necesitamos obtener esto del sombreador de vértices. Así que defina una variable en Water.frag:

Defina los mismos que varían en Water.vert. Luego ajústelo a la posición distorsionada en el sombreador de vértices, para que el código completo se vea así:

Implementando Realmente el Truco

Ahora finalmente estamos listos para implementar la técnica descrita al principio de esta sección. Queremos comparar la profundidad del píxel con el que estamos a la profundidad del píxel detrás de él. El píxel en el que estamos proviene de la posición mundial, y el píxel detrás proviene de la posición de la pantalla. Así que toma estas dos profundidades:

Reto # 4: Uno de estos valores nunca será mayor que el otro (asumiendo depthTest = true). ¿Puedes deducir cuál?

Sabemos que la espuma va a ser donde la distancia entre estos dos valores es pequeña. Así que vamos a representar esa diferencia en cada píxel. Coloque esto en la parte inferior de su sombreador (y asegúrese de que la secuencia de comandos de visualización de profundidad de la sección anterior esté desactivada):

Que debería verse algo como esto:

A rendering of the depth difference at each pixel

¡Que selecciona correctamente los bordes de cualquier objeto sumergido en agua en tiempo real! Por supuesto, puede escalar esta diferencia que estamos renderizando para que la espuma se vea más gruesa / delgada.

Ahora hay muchas formas en que puede combinar esta salida con el color de la superficie del agua para obtener líneas de espuma de aspecto agradable. Puede mantenerlo como un degradado, usarlo para muestrear desde otra textura, o establecerlo en un color específico si la diferencia es menor o igual a algún umbral.

Mi aspecto favorito es configurarlo en un color similar al de las líneas de agua estáticas, por lo que mi función principal final se ve así:

Resumen

Creamos flotabilidad en los objetos que flotan en el agua, dimos a nuestra superficie una textura móvil para simular cáusticos, y vimos cómo podíamos usar el amortiguador de profundidad para crear líneas de espuma dinámicas.

Para finalizar, la parte siguiente y final presentará los efectos posteriores al proceso y cómo usarlos para crear el efecto de distorsión subacuática.

Código Fuente

Aquí puede encontrar el proyecto PlayCanvas alojado terminado. Un puerto Three.js también está disponible en este repositorio.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.