Análise de Shader de Reflexão de Água 2D no Godot Engine

September 15, 2024

Neste artigo, focaremos exclusivamente no funcionamento de um shader que simula um efeito de água no Godot Engine.

A função fragment() é definida como segue:

void fragment() {
  vec2 noise_uv = vec2(SCREEN_UV.x + TIME * 0.025, SCREEN_UV.y);
  float noise_val = texture(noise, noise_uv).r * texture(noise2, SCREEN_UV).r + 0.2;

  float wave = horizon;
  wave += sin(UV.x * wave_frequency + TIME * wave_speed) * wave_magnitude;
  wave += sin(TIME * tides_speed) * tides_magnitude;
  wave -= texture(noise2, SCREEN_UV).r * 0.05 * noise_wave;

  vec2 offset = vec2(0.0, (wave + (SCREEN_UV.y - wave)) - SCREEN_UV.y);
  offset += noise_val * 0.1 - 0.05;

  vec2 col_uv = SCREEN_UV;
  col_uv.y += offset.y * step(wave, SCREEN_UV.y);

  vec4 col = texture(SCREEN_TEXTURE, col_uv);
  col = mix(col, water_color, step(wave, SCREEN_UV.y) * 0.5);
  col = mix(col, shine_color, step(wave, SCREEN_UV.y) * step(noise_val + abs(SCREEN_UV.x - shine_position), shine_width) * shine_intensity);

  vec2 reflection_uv = vec2(col_uv.x, 2.0 * horizon - col_uv.y); // Reflexão vertical em relação ao horizonte
  vec4 reflection_color = texture(SCREEN_TEXTURE, reflection_uv);

  if (SCREEN_UV.y > horizon) {
    col = mix(col, reflection_color, 0.3); // Ajuste do fator de mistura conforme necessário
  }

  COLOR = col;
}

Efeito de Água no Godot Engine

Cálculo do Ruído Temporal e Espacial

Iniciamos definindo noise_uv, que é um vetor de coordenadas de textura utilizado para amostrar a primeira textura de ruído (noise). As coordenadas no eixo X são deslocadas ao longo do tempo (TIME * 0.025), introduzindo uma variação temporal que simula o movimento da água. Em seguida, calculamos noise_val, que é o produto dos componentes red (.r) das texturas noise e noise2 nas respectivas coordenadas. Essa multiplicação aumenta a complexidade do ruído, resultando em padrões mais orgânicos. Adicionamos um offset constante de 0.2 para evitar valores muito baixos que poderiam reduzir a intensidade do efeito.

vec2 noise_uv = vec2(SCREEN_UV.x + TIME * 0.025, SCREEN_UV.y);
float noise_val = texture(noise, noise_uv).r * texture(noise2, SCREEN_UV).r + 0.2;

Cálculo da Altura da Onda

A variável wave é inicialmente definida como a posição do horizonte (horizon), representando a linha de base da água. Adicionamos uma componente senoide que varia ao longo do eixo X e do tempo para simular ondas que se movem horizontalmente:

wave += sin(UV.x * wave_frequency + TIME * wave_speed) * wave_magnitude;

Aqui, UV.x é a coordenada de textura no eixo X, wave_frequency controla a frequência espacial das ondas, e wave_speed controla a velocidade com que as ondas se deslocam ao longo do tempo. Em seguida, adicionamos uma variação lenta e global no nível da água para simular o efeito das marés:

wave += sin(TIME * tides_speed) * tides_magnitude;

TIME é uma variável global que representa o tempo em segundos, tides_speed controla a velocidade das marés, e tides_magnitude ajusta a amplitude das marés. Por fim, introduzimos variações aleatórias na altura das ondas usando uma textura de ruído:

wave -= texture(noise2, SCREEN_UV).r * 0.05 * noise_wave;

Isso torna a superfície da água menos uniforme, adicionando detalhes que aumentam o realismo visual.

Cálculo do Deslocamento Vertical

Calculamos o deslocamento vertical necessário para os pixels abaixo da superfície da água:

vec2 offset = vec2(0.0, (wave + (SCREEN_UV.y - wave)) - SCREEN_UV.y);
offset += noise_val * 0.1 - 0.05;

Essa expressão ajusta a posição vertical dos pixels, criando a ilusão de que a imagem abaixo da água está sendo distorcida pelo movimento das ondas. Adicionamos o valor do ruído para introduzir pequenas variações adicionais:

offset += noise_val * 0.1 - 0.05;

Isso simula perturbações na superfície da água causadas por efeitos como ondulações e correntes.

Ajuste das Coordenadas de Textura para Amostragem

Definimos col_uv como as coordenadas de textura que serão usadas para amostrar a textura da tela (SCREEN_TEXTURE):

vec2 col_uv = SCREEN_UV;

Aplicamos o deslocamento vertical calculado anteriormente, mas apenas aos pixels que estão abaixo da superfície da água. Utilizamos a função step para garantir que o deslocamento seja aplicado condicionalmente:

col_uv.y += offset.y * step(wave, SCREEN_UV.y);

A função step(wave, SCREEN_UV.y) retorna 0.0 se SCREEN_UV.y < wave (acima da água) e 1.0 caso contrário (abaixo da água).

Amostragem e Mistura de Cores

Amostramos a textura da tela nas coordenadas ajustadas, o que cria o efeito de distorção da imagem subaquática:

vec4 col = texture(SCREEN_TEXTURE, col_uv);

Misturamos essa cor com a cor definida para a água (water_color), novamente aplicando a mistura apenas abaixo da superfície:

col = mix(col, water_color, step(wave, SCREEN_UV.y) * 0.5);

O fator de mistura é 0.5, o que significa que a cor resultante será uma média entre a cor amostrada e a cor da água. Em seguida, adicionamos o brilho especular para simular o reflexo de uma fonte de luz na superfície da água:

col = mix(
  col,
  shine_color,
  step(wave, SCREEN_UV.y) * step(noise_val + abs(SCREEN_UV.x - shine_position), shine_width) * shine_intensity
);

Aqui, utilizamos uma combinação de funções step para determinar se o pixel atual está dentro da região onde o brilho deve ser aplicado. O brilho é controlado por parâmetros como shine_position, shine_width e shine_intensity, permitindo ajustar sua posição, largura e intensidade.

Cálculo da Reflexão

Calculamos as coordenadas para a reflexão do ambiente na água espelhando a coordenada Y em relação ao horizonte:

vec2 reflection_uv = vec2(col_uv.x, 2.0 * horizon - col_uv.y);

Em seguida, amostramos a textura da tela nessas coordenadas refletidas:

vec4 reflection_color = texture(SCREEN_TEXTURE, reflection_uv);

Mistura da Reflexão

Aplicamos a reflexão apenas aos pixels abaixo do horizonte:

if (SCREEN_UV.y > horizon) {
  col = mix(col, reflection_color, 0.3);
}

O fator de mistura é 0.3, o que significa que a cor refletida contribui com 30% para a cor final. Isso adiciona o efeito de reflexão do ambiente na superfície da água, aumentando o realismo visual.

Saída da Cor Final

Por fim, definimos a cor final do fragmento:

COLOR = col;

A variável COLOR é uma variável reservada que determina a cor final do pixel renderizado.

Análise Técnica Detalhada

A função combina diversas técnicas avançadas de sombreamento para simular um efeito de água realista. Utiliza funções seno para criar ondas dinâmicas, ajustadas por parâmetros como wave_frequency e wave_speed. As texturas de ruído adicionam variações aleatórias, tornando a superfície da água mais orgânica. O deslocamento vertical é aplicado condicionalmente, garantindo que apenas os pixels abaixo da superfície sejam afetados. A cor final é resultado de múltiplas misturas condicionais, considerando fatores como profundidade, posição e valores de ruído. O cálculo da reflexão do ambiente e a adição do brilho especular contribuem significativamente para o realismo do efeito.

Experimentação e Ajustes Avançados

Para personalizar o efeito de acordo com as necessidades específicas do projeto, é possível ajustar os parâmetros uniformes. Alterando wave_frequency e wave_magnitude, é possível simular diferentes condições aquáticas, como águas calmas ou agitadas. Modificando tides_magnitude e tides_speed, é possível controlar as marés. Experimentar com diferentes texturas de ruído pode resultar em padrões únicos na superfície da água. Ajustando shine_position, shine_intensity e shine_width, é possível simular diferentes posições e intensidades de fontes de luz. O fator de mistura na reflexão também pode ser ajustado para aumentar ou diminuir a influência da cena refletida na água.

Referências