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;
}
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.