Representación en tiempo real de agua cáustica

En este artículo, presentaré mi intento de generalizar el cálculo de cáusticos en tiempo real utilizando WebGL y ThreeJS. El hecho de que se trate de un intento es importante, ya que encontrar una solución que funcione en todos los casos y proporcione 60 fps es difícil, si no imposible. Pero verás que con la ayuda de mi método puedes lograr resultados bastante decentes.



¿Qué es cáustico?



Las cáusticas son patrones de luz que ocurren cuando la luz se refracta y refleja desde una superficie, en nuestro caso, en el borde del agua y el aire.



Debido a que la reflexión y la refracción ocurren en las ondas de agua, el agua actúa aquí como una lente dinámica, creando estos patrones de luz.





En este post nos centraremos en las cáusticas provocadas por la refracción de la luz, que es lo que suele ocurrir bajo el agua.



Para lograr 60 fps estables, necesitamos calcularlo en una tarjeta gráfica (GPU), por lo que solo calcularemos cáusticos con sombreadores escritos en GLSL.



Para calcularlo, necesitamos:



  • calcular los rayos refractados en la superficie del agua (en GLSL esto es fácil, porque hay una función incorporada para esto )
  • calcular, utilizando el algoritmo de intersección, los puntos en los que estos rayos chocan con el entorno
  • calcular el brillo cáustico comprobando los puntos de convergencia de los rayos




Demostración de agua bien conocida en WebGL



Siempre me ha sorprendido esta demostración de Evan Wallace que muestra cáusticos de agua visualmente realistas en WebGL: madebyevan.com/webgl-water





Recomiendo leer su artículo de Medium , que explica cómo calcular cáusticos en tiempo real utilizando la malla frontal ligera y las funciones GLSL PD . Su implementación es extremadamente rápida y luce muy bien, pero tiene algunos inconvenientes: solo funciona con una piscina de cubos y una bola de piscina esférica . Si coloca un tiburón bajo el agua, la demostración no funcionará: está codificado en los sombreadores que hay una bola esférica debajo del agua.



Colocó una esfera debajo del agua porque calcular la intersección entre un rayo de luz refractado y una esfera es una tarea fácil que usa matemáticas muy simples.



Todo esto es bueno para una demostración, pero quería crear una solución más general. para calcular cáusticos para que cualquier malla no estructurada, como un tiburón, pueda estar en la piscina.





Ahora pasemos a mi técnica. Para este artículo, asumiré que ya conoce los conceptos básicos del renderizado 3D con rasterización y que está familiarizado con cómo el sombreador de vértices y el sombreador de fragmentos funcionan juntos para representar primitivas (triángulos) en la pantalla.



Trabajar con restricciones GLSL



En los sombreadores escritos en GLSL (OpenGL Shading Language), solo podemos tener acceso a una cantidad limitada de información sobre la escena, por ejemplo:



  • Atributos del vértice actualmente dibujado (posición: vector 3D, normal: vector 3D, etc.). Podemos pasar nuestros atributos de GPU, pero deben ser del tipo GLSL incorporado.
  • Uniforme , es decir, constantes para toda la malla actualmente renderizada en el marco actual. Pueden ser texturas, matriz de proyección de la cámara, dirección de iluminación, etc. Deben tener un tipo incorporado: int, float, sampler2D para texturas, vec2, vec3, vec4, mat3, mat4.


Sin embargo, no hay forma de acceder a las mallas presentes en la escena.



Es por eso que la demostración de webgl-water solo se puede hacer con una simple escena en 3D. Es más fácil calcular la intersección de un rayo refractado y una forma muy simple que se puede representar usando uniforme. En el caso de una esfera, se puede especificar por posición (vector 3D) y radio (flotante), por lo que esta información se puede pasar a los sombreadores usando uniforme , y el cálculo de intersecciones requiere matemáticas muy simples, realizadas fácil y rápidamente en el sombreador.



Algunas técnicas de trazado de rayos realizadas en sombreadores renderizan mallas en texturas, pero en 2020 esta solución no es aplicable para renderizado en tiempo real en WebGL. Hay que recordar que para obtener un resultado decente debemos calcular 60 imágenes por segundo con muchos rayos. Si calculamos las cáusticas usando 256x256 = 65536 rayos, entonces cada segundo tenemos que hacer una cantidad significativa de cálculos de intersección (que también depende del número de mallas en la escena).



Necesitamos encontrar una manera de representar el entorno submarino de manera uniforme y calcular la intersección manteniendo la velocidad suficiente.



Crear un mapa del entorno



Cuando se requiere calcular sombras dinámicas, el mapeo de sombras es una técnica bien conocida . Se usa a menudo en videojuegos, se ve bien y es rápido de ejecutar.



El mapeo de sombras es una técnica de dos pasos:



  • Primero, la escena 3D se renderiza en términos de fuente de luz. Esta textura no contiene los colores de los fragmentos, sino la profundidad de los fragmentos (la distancia entre la fuente de luz y el fragmento). Esta textura se llama mapa de sombras.
  • Luego, el mapa de sombras se utiliza al renderizar la escena 3D. Al dibujar un fragmento en la pantalla, sabemos si hay otro fragmento entre la fuente de luz y el fragmento actual. Si es así, entonces sabemos que el fragmento actual está en la sombra y necesitamos dibujarlo un poco más oscuro.


Puede leer más sobre el mapeo de sombras en este excelente tutorial de OpenGL: www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping .



También puede ver un ejemplo interactivo en ThreeJS (presione T para mostrar el mapa de sombras en la esquina inferior izquierda): threejs.org/examples/?q=shadowm#webgl_shadowmap .



En la mayoría de los casos, esta técnica funciona bien. Puede funcionar con cualquier malla no estructurada de la escena.



Al principio pensé que podría usar un enfoque similar para los cáusticos del agua, es decir, primero convertir el entorno submarino en una textura y luego usar esta textura para calcular la intersección entre los rayos y el entorno.... En lugar de representar solo las profundidades de los fragmentos, también represento la posición de los fragmentos en el mapa del entorno.



Aquí está el resultado de crear un mapa de entorno:





Mapa de entorno: la posición XYZ se almacena en los canales RGB, la profundidad en el canal alfa



Cómo calcular la intersección de un rayo y sus alrededores



Ahora que tengo un mapa del entorno submarino, necesito calcular la intersección entre los rayos refractados y el entorno.



El algoritmo funciona de la siguiente manera:



  • Etapa 1: comienza en el punto de intersección entre el rayo de luz y la superficie del agua
  • Etapa 2: cálculo de la refracción usando la función de refracción
  • Etapa 3: pasar de la posición actual en la dirección del rayo refractado, un píxel en la textura del mapa del entorno.
  • Paso 4: Compare la profundidad del ambiente registrada (almacenada en el píxel actual de la textura del ambiente) con la profundidad actual. Si la profundidad del entorno es mayor que la profundidad actual, entonces debemos seguir adelante, por lo que aplicamos el paso 3 nuevamente . Si la profundidad del entorno es menor que la profundidad actual, significa que el rayo chocó con el entorno en la posición leída en la textura del entorno y encontramos una intersección con el entorno.




La profundidad actual es menor que la profundidad del entorno: debes seguir adelante





La profundidad actual es mayor que la profundidad circundante: encontramos una intersección



Textura cáustica



Después de encontrar la intersección, podemos calcular la luminancia cáustica (y la textura de luminancia cáustica) utilizando la técnica descrita por Evan Wallace en su artículo . La textura resultante se parece a esto:





Textura de luminancia cáustica (tenga en cuenta que el efecto cáustico es menos importante en el tiburón porque está más cerca de la superficie del agua, lo que reduce la convergencia de los rayos de luz)



Esta textura contiene información sobre la intensidad de la luz para cada punto en el espacio 3D. Al renderizar la escena terminada, podemos leer esta intensidad de luz de la textura cáustica y obtener el siguiente resultado:







Se puede encontrar una implementación de esta técnica en el repositorio de Github: github.com/martinRenou/threejs-caustics . ¡Dale una estrella si te gustó!



Si desea ver los resultados del cálculo de cáusticos, puede ejecutar la demostración: martinrenou.github.io/threejs-caustics .



Acerca de este algoritmo de intersección



Esta decisión depende en gran medida de la resolución de la textura del entorno . Cuanto mayor sea la textura, mejor será la precisión del algoritmo, pero más tiempo llevará encontrar una solución (antes de encontrarla, debe contar y comparar más píxeles).



Además, leer la textura en los sombreadores es aceptable siempre que no lo hagas muchas veces; aquí creamos un bucle que continúa leyendo nuevos píxeles de la textura, lo cual no se recomienda.



Además, los bucles while no están permitidos en WebGL.(y por una buena razón), por lo que necesitamos implementar un algoritmo en un bucle for que el compilador pueda expandir. Esto significa que necesitamos una condición de terminación de ciclo conocida en tiempo de compilación, generalmente el valor de "iteración máxima", lo que nos obliga a dejar de buscar una solución si no la hemos encontrado dentro del número máximo de intentos. Esta limitación conduce a resultados cáusticos incorrectos si la refracción es demasiado importante.



Nuestra técnica no es tan rápida como el enfoque simplificado sugerido por Evan Wallace, pero es mucho más flexible que el enfoque de trazado de rayos completo y también se puede utilizar para renderizado en tiempo real. Sin embargo, la velocidad aún depende de algunas condiciones: la dirección de la luz, el brillo de las refracciones y la resolución de la textura ambiental.



Cerrando la revisión de la demostración



En este artículo analizamos el cálculo de la cáustica del agua, pero se utilizaron otras técnicas en la demostración.



Al renderizar la superficie del agua, usamos una textura de caja de cielo y mapas de cubos para obtener reflejos. También aplicamos refracción a la superficie del agua usando refracción simple en el espacio de la pantalla (ver este artículo sobre reflejos y refracciones en el espacio de la pantalla), esta técnica es físicamente incorrecta, pero visualmente convincente y rápida. También agregamos aberración cromática para mayor realismo.



Tenemos más ideas para mejorar aún más la metodología, que incluyen:



  • Aberración cromática en cáusticos: ahora estamos aplicando aberración cromática a la superficie del agua, pero este efecto también debería ser visible en cáusticos subacuáticos.
  • Dispersión de luz en el volumen de agua.
  • Como aconsejaron Martin Gerard y Alan Wolf en Twitter , podemos mejorar el rendimiento con mapas de entornos jerárquicos (que se utilizarán como árboles cuádruples para encontrar intersecciones). También aconsejaron renderizar mapas del entorno en términos de rayos refractados (suponiendo que sean perfectamente planos), lo que hará que el rendimiento sea independiente del ángulo de incidencia de la iluminación.


Expresiones de gratitud



Este trabajo sobre visualización realista del agua en tiempo real se llevó a cabo en QuantStack y fue financiado por ERDC .



All Articles