O que é cáustico?
Cáusticas são padrões de luz que ocorrem quando a luz é refratada e refletida de uma superfície, em nosso caso, na fronteira entre a água e o ar.
Como a reflexão e a refração ocorrem nas ondas da água, a água atua como uma lente dinâmica aqui, criando esses padrões de luz.

Neste post, vamos nos concentrar nos cáusticos causados pela refração da luz, que é o que normalmente acontece debaixo d'água.
Para atingir 60fps estáveis, precisamos calculá-lo em uma placa de vídeo (GPU), portanto, calcularemos apenas as cáusticas com shaders escritos em GLSL.
Para calculá-lo, precisamos:
- calcular os raios refratados na superfície da água (em GLSL isso é fácil, porque há uma função embutida para isso )
- calcular, usando o algoritmo de interseção, os pontos em que esses raios colidem com o ambiente
- calcule o brilho cáustico verificando os pontos de convergência dos raios

Demonstração de água bem conhecida em WebGL
Sempre fiquei surpreso com esta demonstração de Evan Wallace que mostra cáusticos visualmente realistas da água no WebGL: madebyevan.com/webgl-water

Recomendo a leitura de seu artigo do Medium , que explica como calcular cáusticas em tempo real usando a malha frontal de luz e as funções GLSL PD . A sua implementação é extremamente rápida e tem um aspecto muito bonito, mas tem algumas desvantagens: só funciona com uma piscina cúbica e uma bola de bilhar esférica . Se você colocar um tubarão sob a água, a demonstração não funcionará: está codificado nos shaders que há uma bola esférica sob a água.
Ele colocou uma esfera sob a água porque calcular a interseção entre um raio de luz refratado e uma esfera é uma tarefa fácil que usa matemática muito simples.
Tudo isso é bom para uma demonstração, mas eu queria criar uma solução mais geral. para calcular cáusticas para que qualquer malha não estruturada, como um tubarão, possa estar na piscina.

Agora vamos passar para minha técnica. Para este artigo, assumirei que você já conhece os fundamentos da renderização 3D com rasterização e que está familiarizado com como o sombreador de vértice e o sombreador de fragmento trabalham juntos para renderizar primitivos (triângulos) na tela.
Trabalho com restrições GLSL
Em shaders escritos em GLSL (OpenGL Shading Language), podemos acessar apenas uma quantidade limitada de informações sobre a cena, por exemplo:
- Atributos do vértice desenhado atualmente (posição: vetor 3D, normal: vetor 3D, etc.). Podemos passar nossos atributos de GPU, mas eles devem ser do tipo GLSL integrado.
- Uniforme , isto é, constantes para toda a malha renderizada atualmente no quadro atual. Podem ser texturas, matriz de projeção de câmera, direção de iluminação, etc. Eles devem ter um tipo embutido: int, float, sampler2D para texturas, vec2, vec3, vec4, mat3, mat4.
Porém, não há como acessar as malhas presentes na cena.
É por isso que a demonstração do webgl-water só pode ser feita com uma cena 3D simples. É mais fácil calcular a interseção de um raio refratado e uma forma muito simples que pode ser representada usando uniforme. No caso de uma esfera, ela pode ser especificada por posição (vetor 3D) e raio (flutuação), de modo que essa informação pode ser passada para shaders usando uniformes , e o cálculo de interseções requer matemática muito simples, fácil e rapidamente realizada no shader.
Algumas técnicas de ray tracing realizadas em shaders renderizam malhas em texturas, mas em 2020 esta solução não é aplicável para renderização em tempo real no WebGL. É preciso lembrar que para obter um resultado decente, devemos calcular 60 imagens por segundo com muitos raios. Se calcularmos as cáusticas usando 256x256 = 65536 raios, então a cada segundo temos que fazer uma quantidade significativa de cálculos de interseção (que também depende do número de malhas na cena).
Precisamos encontrar uma maneira de representar o ambiente subaquático de maneira uniforme e calcular a interseção, mantendo a velocidade suficiente.
Criação de um mapa do ambiente
Quando o cálculo de sombras dinâmicas é necessário, o mapeamento de sombras é uma técnica bem conhecida . É freqüentemente usado em videogames, tem boa aparência e é rápido de executar.
O mapeamento de sombra é uma técnica de duas passagens:
- Primeiro, a cena 3D é renderizada em termos da fonte de luz. Essa textura não contém as cores dos fragmentos, mas a profundidade dos fragmentos (a distância entre a fonte de luz e o fragmento). Essa textura é chamada de mapa de sombra.
- O mapa de sombra é então usado ao renderizar a cena 3D. Ao desenhar um fragmento na tela, sabemos se existe outro fragmento entre a fonte de luz e o fragmento atual. Nesse caso, sabemos que o fragmento atual está na sombra e precisamos desenhá-lo um pouco mais escuro.
Você pode ler mais sobre o mapeamento de sombra neste excelente tutorial OpenGL: www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping .
Você também pode assistir a um exemplo interativo no ThreeJS (pressione T para exibir o mapa de sombra no canto esquerdo inferior): threejs.org/examples/?q=shadowm#webgl_shadowmap .
Na maioria dos casos, essa técnica funciona bem. Ele pode funcionar com qualquer malha não estruturada na cena.
A princípio, pensei que poderia usar uma abordagem semelhante para cáusticos de água, ou seja, primeiro renderizar o ambiente subaquático em uma textura e, em seguida, usar essa textura para calcular a interseção entre os raios e o ambiente.... Em vez de renderizar apenas as profundezas dos fragmentos, também renderizo a posição dos fragmentos no mapa do ambiente.
Aqui está o resultado da criação de um mapa do ambiente:

Mapa de ambiente: a posição XYZ é armazenada nos canais RGB, a profundidade no canal alfa
Como calcular a interseção de um raio e arredores
Agora que tenho um mapa do ambiente subaquático, preciso calcular a interseção entre os raios refratados e o ambiente.
O algoritmo funciona da seguinte maneira:
- Etapa 1: começa no ponto de intersecção entre o raio de luz e a superfície da água
- Estágio 2: cálculo da refração usando a função refratar
- Etapa 3: vá da posição atual na direção do raio refratado, um pixel na textura do mapa do ambiente.
- Etapa 4: compare a profundidade do ambiente registrada (armazenada no pixel atual da textura do ambiente) com a profundidade atual. Se a profundidade do ambiente for maior do que a profundidade atual, precisamos seguir em frente, então aplicamos a etapa 3 novamente . Se a profundidade do ambiente for menor que a profundidade atual, significa que o raio colidiu com o ambiente na posição lida na textura do ambiente e encontramos uma interseção com o ambiente.

A profundidade atual é menor que a profundidade do ambiente: você precisa seguir em frente

A profundidade atual é maior do que a profundidade circundante: encontramos a interseção
Textura cáustica
Após encontrar a interseção, podemos calcular a luminância cáustica (e a textura da luminância cáustica) usando a técnica descrita por Evan Wallace em seu artigo . A textura resultante é semelhante a esta:

Textura de luminância cáustica (observe que o efeito cáustico é menos importante no tubarão porque está mais próximo da superfície da água, o que reduz a convergência dos raios de luz)
Esta textura contém informações sobre a intensidade da luz para cada ponto no espaço 3D. Ao renderizar a cena finalizada, podemos ler essa intensidade de luz da textura cáustica e obter o seguinte resultado:


Uma implementação dessa técnica pode ser encontrada no repositório Github: github.com/martinRenou/threejs-caustics . Dê a ela uma estrela se você gostou!
Se você quiser ver os resultados do cálculo de cáusticas, pode executar a demonstração: martinrenou.github.io/threejs-caustics .
Sobre este algoritmo de interseção
Esta decisão é altamente dependente da resolução da textura do ambiente . Quanto maior a textura, melhor é a precisão do algoritmo, mas mais tempo leva para encontrar uma solução (antes de encontrá-la, você precisa contar e comparar mais pixels).
Além disso, ler a textura em shaders é aceitável, desde que você não faça isso muitas vezes; aqui criamos um loop que continua a ler novos pixels da textura, o que não é recomendado.
Além disso, loops while não são permitidos no WebGL.(e por um bom motivo), portanto, precisamos implementar um algoritmo em um loop for que possa ser expandido pelo compilador. Isso significa que precisamos de uma condição de encerramento do loop conhecida em tempo de compilação, geralmente o valor de "iteração máxima", que nos força a parar de procurar uma solução se não a encontrarmos dentro do número máximo de tentativas. Essa limitação leva a resultados cáusticos incorretos se a refração for muito importante.
Nossa técnica não é tão rápida quanto o método simplificado sugerido por Evan Wallace, mas é muito mais flexível do que a abordagem de traçado de raio completo e também pode ser usada para renderização em tempo real. No entanto, a velocidade ainda depende de algumas condições - a direção da luz, o brilho das refrações e a resolução da textura do ambiente.
Fechando a revisão de demonstração
Neste artigo, vimos como calcular a cáustica da água, mas outras técnicas foram usadas na demonstração.
Ao renderizar a superfície da água, usamos uma textura de skybox e mapas de cubo para obter reflexos. Também aplicamos refração na superfície da água usando refração simples no espaço da tela (veja este artigo sobre reflexos e refrações no espaço da tela), esta técnica é fisicamente incorreta, mas visualmente convincente e rápida. Nós também adicionamos a aberração cromática para mais realismo.
Temos mais ideias para melhorar ainda mais a metodologia, incluindo:
- Aberração cromática em produtos cáusticos: agora estamos aplicando aberração cromática à superfície da água, mas este efeito também deve ser visível em produtos cáusticos subaquáticos.
- Espalhamento de luz no volume de água.
- Como Martin Gerard e Alan Wolfe aconselharam no Twitter , podemos melhorar o desempenho com mapas de ambiente hierárquico (que serão usados como árvores quadradas para encontrar interseções). Eles também aconselharam a renderização de mapas do ambiente em termos de raios refratados (assumindo que eles sejam perfeitamente planos), o que tornará o desempenho independente do ângulo de incidência da iluminação.
Agradecimentos
Este trabalho de visualização realista e em tempo real da água foi realizado na QuantStack e financiado pelo ERDC .