Ondulação do mar
Um trecho de mar aberto com cristas pontudas e vales largos, feito movendo cada vértice de um plano liso em círculo com três ondas de Gerstner.
- 01ObjetivoDistorcer
- 02TécnicaDeslocamento de vértices
- 03ExecuçãoOndulação do mar
Parâmetros
Copie para a sua plataforma
- GLSLCopiar
- GodotCopiar
Cadeia de nós
Montada passo a passo, no estilo do GLSL, com os nomes de nós dele.
Montada passo a passo, no estilo do Godot, com os nomes de nós dele.
Este card ainda não tem versão para Unreal Engine 5, então os nós mostram os nomes do GLSL.
Este card ainda não tem versão para Unity, então os nós mostram os nomes do GLSL.
Este card ainda não tem versão para Blender, então os nós mostram os nomes do GLSL.
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep PosiçãoNormalVisãoTempo01 Altura da superfície deslocada
dotDotProductdotdotdot Entrada02 Luz na nova normal
powPowpowpowpow Entrada03 Reflexo do sol
powFresnelpowpowpow Entrada04 Céu nos ângulos rasantes
mixMixmixmixmix Entrada05 Cor da água
mixMixmixmixmix Entrada06 Espuma e reflexo
Como funciona
O plano da prévia é liso: 65 por 65 vértices na altura 0. Toda onda que você vê é feita no
shader de vérticeO programa da GPU que roda uma vez para cada vértice de uma malha. Ele posiciona o vértice na tela escrevendo gl_Position e repassa valores para o shader de fragmento. Shader de vértice O programa da GPU que roda uma vez para cada vértice de uma malha. Ele posiciona o vértice na tela escrevendo gl_Position e repassa valores para o shader de fragmento.
A entrada de vértice: três ondas de Gerstner
Uma onda senoidal simples move cada ponto para cima e para baixo: Onda de Gerstner Um modelo de onda de água em que cada ponto da superfície anda em círculo, em vez de só subir e descer. As cristas ficam pontudas e os vales largos, como num mar de verdade.y = A * sin(k * x - w * t). A água não faz isso.
Numa onda de GerstnerUm modelo de onda de água em que cada ponto da superfície anda em círculo, em vez de só subir e descer. As cristas ficam pontudas e os vales largos, como num mar de verdade.P = (x + d.x·A·cos θ, A·sin θ, z + d.y·A·cos θ), θ = k·(d·xz) − ωt, cada ponto da superfície percorre um pequeno círculo enquanto a
onda passa: para a frente na crista, para trás no vale. O movimento horizontal junta os pontos nas cristas e os espalha
nos vales, então as cristas ficam pontudas e os vales largos. Essa única mudança é quase tudo o que faz uma ondulação
parecer água.
A função gerstner calcula uma onda na posição de repouso de um vértice:
k = 2π / spantransforma o comprimento de onda num número de onda, e a fase ék * dot(direction, rest) - w * t.- A velocidade vem da física de águas profundas:
w = sqrt(9.8 * k). Ondas longas correm mais que as curtas, então as três ondas deslizam umas sobre as outras e o padrão nunca se repete exatamente. - A amplitude é
steepness / k. A inclinação diz o quanto a onda é empurrada em direção a um laço: em 1, a crista vira uma ponta; acima disso, os círculos se cruzam e a superfície dobra através de si mesma. As três ondas dividemp_steepness(metade, três décimos, um quinto), então a soma fica em 1 ou menos no topo do controle.
swell soma três ondas vindas de direções diferentes, com comprimentos em proporções (1, 0.61, 0.37) que não dividem
umas às outras, e move o vértice pela soma.
Recalculando a normal
Mover os vértices não basta. A normalUm vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções. Normal Um vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções. Vetor Um grupo de dois a quatro números tratado como um só valor (vec2, vec3, vec4). O mesmo tipo guarda uma posição, uma direção, uma cor ou uma UV.
Para uma onda de Gerstner essas derivadas têm forma fechada, então a função soma a inclinação de cada onda em alongX
e alongZ conforme avança, sem avaliações extras. normalize(cross(alongZ, alongX)) dá então uma normal que aponta
para cima, para fora da água. A ordem importa: cruzar ao contrário dá uma normal apontando para baixo, e o mar fica
escuro.
A entrada de fragmento: colorindo a água
1. Altura da superfície deslocada. crest é alto onde o vértice foi erguido perto da altura total da onda
principal. A entrada de fragmento vê a posição movida, então a cor acompanha as ondas. A prévia mostra linhas brancas
nas cristas.
2. Luz na nova normal. max(dot(normal, sun), 0.0) é o termo de Lambert. Com a normal recalculada, as faces das
ondas viradas para o sol clareiam e as costas escurecem; com a normal plana isso seria um valor só.
3. Reflexo do sol. O vetor médio entre o sol e a direção da câmera, num produto escalar com a normal e elevado a 120, dá um brilho pequeno e nítido que dança pelas encostas viradas ao mesmo tempo para o sol e para a câmera.
4. Céu nos ângulos rasantes. Efeito Fresnel Superfícies refletem mais luz vistas em ângulo rasante do que de frente. Shaders costumam imitar isso com o produto escalar entre a normal e a direção de visão.pow(1.0 - dot(normal, view), 4.0) é um termo de FresnelSuperfícies refletem mais luz vistas em ângulo rasante do que de frente. Shaders costumam imitar isso com o produto escalar entre a normal e a direção de visão.F = F0 + (1 − F0)(1 − cos θ)^5
5 e 6. Cor da água, espuma e reflexo. A cor funda, iluminada pelo termo de Lambert, é misturada em direção ao céu pelo termo de Fresnel, as cristas são misturadas em direção à espuma, e o reflexo é somado por cima.
Custo e notas de produção
A entrada de vértice custa cerca de 40 operações por onda, 130 para as três mais a normal, uma vez por vértice: cerca de 550.000 operações por quadro neste plano de 4.225 vértices, o que uma GPU nem percebe. A entrada de fragmento custa cerca de 45 operações por pixel. Nenhuma leitura de textura, uma passada.
- A densidade de vértices é o limite de verdade. Uma onda precisa de pelo menos seis ou oito vértices por comprimento de onda, ou as cristas viram segmentos retos e as pontas são cortadas. Aqui o plano tem cerca de 32 vértices por unidade, então a onda mais curta (0.37 do controle de comprimento de onda) começa a quebrar abaixo de um comprimento de cerca de 0.6. Num jogo, use um plano denso perto da câmera e com menos vértices longe dela, ou tesselação.
- Tudo o que precisa da superfície precisa desta conta. A passada de sombra e o depth prepass rodam a mesma função de vértice sozinhos. A flutuação de um barco roda na CPU e precisa avaliar as mesmas três ondas lá. Ondas de Gerstner movem os pontos para os lados, então achar a altura sobre um dado x e z leva algumas iterações.
- Três ondas parecem um padrão num mar grande. Some mais, cada uma com inclinação menor (mantendo a soma em 1 ou menos), varie as direções dentro de uns 60 graus em volta do vento, ou passe para um espectro.
- Espaço de mundo. As ondas são calculadas a partir da posição no mundo, então malhas de água vizinhas se encaixam sem emenda. O plano da prévia não tem transformação, então o espaço do objeto é o do mundo; numa engine, mantenha o deslocamento em espaço de mundo, como faz o shader exportado.
Monte no GLSL
O botão de copiar dá dois shaders para um programa: um shader de vértice que chama swell() e projeta o vértice
movido, e um shader de fragmento que chama effect(). Os dois incluem o efeito inteiro, então declaram os mesmos
uniforms.
- Monte um plano no plano XZ virado para +Y, subdividido numa grade, por exemplo 128 por 128 células em 4 por 4
unidades, com as posições no atributo de location 0, as normais (todas
0, 1, 0) no 1 e as UVs no 2. - Compile o shader de vértice e o de fragmento a partir do código copiado e ligue-os num programa só.
- A cada quadro, ponha em
u_viewProjectiona projeção da câmera vezes a matriz de visão, emu_cameraa posição da câmera no mundo e emu_timeos segundos desde o início. Defina os uniformsp_uma vez, ou quando um controle mudar; as cores vão em linear. - Desenhe a grade com o teste de profundidade ligado. O shader de fragmento escreve cor opaca, codificada em sRGB.
A direção da luz e a cor do céu estão dentro de effect(). Para iluminar a água com o sol da sua cena, troque a
constante sun por um uniform, e passe a cor do horizonte do seu céu como horizon.
Monte no Godot
O shader é um shader spatial com render_mode world_vertex_coords, então o vertex() move o plano em espaço de mundo e malhas de água vizinhas se encaixam. Ele é unshaded, como a prévia; o sol e o céu dele estão em effect().
O shader exportado move os vértices no vertex() e os colore no fragment(), os dois a partir do mesmo código do
efeito, então um único ShaderMaterial faz o cartão inteiro.
- Adicione um
MeshInstance3De dê a ele umPlaneMeshde tamanho 4 por 4, com Subdivide Width e Subdivide Depth em 127. - Ponha em Material Override um novo
ShaderMaterial, e no Shader dele um novoShader. - Cole o código copiado no shader. As ondas começam a se mexer na hora: o
TIMEdo Godot alimentai.time. - Em Geometry, aumente o Extra Cull Margin para cerca de 1, para as cristas perto da borda da tela não sumirem.
- Ajuste os parâmetros em Shader Parameters: os mesmos nomes deste cartão, sem o prefixo
p_.
As passadas de sombra e de profundidade do Godot rodam o mesmo vertex(), então a sombra que a água projeta
acompanha as ondas, e, depois que você tira o unshaded (veja as armadilhas), as sombras que caem sobre ela também.
A física não: um RigidBody3D boiando precisa das três ondas calculadas em GDScript na posição dele.
Um VisualShader consegue fazer a parte de vértice com nós Expression, mas as três ondas e as tangentes delas formam
um grafo grande; o shader em texto é a forma prática. Na parte de fragmento, os nós são DotProduct e Max para a
luz, Pow para o reflexo, Fresnel para o céu e Mix para juntar tudo.
Ainda não há tutorial de Unreal Engine 5 para este card.
Ainda não há tutorial de Unity para este card.
Ainda não há tutorial de Blender para este card.
Código
#version 300 es// Vertex displacement → Ocean swell, vertex shader, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform mat4 u_viewProjection;uniform vec3 u_camera;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_wavelength; // Wavelength: 1.2 (0.3 to 3.0)uniform float p_steepness; // Steepness: 0.7 (0.0 to 1.0)uniform float p_speed; // Speed: 0.4 (0.0 to 2.0)uniform vec3 p_deep; // Deep water: vec3(0.03, 0.2, 0.32), sRGB; upload it decoded to linearuniform vec3 p_horizon; // Sky reflection: vec3(0.62, 0.78, 0.88), sRGB; upload it decoded to linearuniform vec3 p_foam; // Foam: vec3(0.92, 0.96, 1.0), sRGB; upload it decoded to linearlayout(location = 0) in vec3 a_position;layout(location = 1) in vec3 a_normal;layout(location = 2) in vec2 a_uv;out vec2 v_uv;out vec3 v_position;out vec3 v_normal;out vec3 v_view;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// One Gerstner wave at the rest position `rest`: returns how far it moves the point and adds its slope to the two// tangents of the surface, along x and along z. The point moves on a circle, so the crests come out sharper than a// sine's and the troughs flatter.vec3 gerstner(vec2 rest, vec2 direction, float span, float q, float t, inout vec3 alongX, inout vec3 alongZ) { vec2 d = normalize(direction); float k = 6.2831853 / span; // Deep-water waves: speed grows with wavelength (w^2 = g * k), so long swells outrun short chop float phase = k * dot(d, rest) - sqrt(9.8 * k) * t; float a = q / k; float s = q * sin(phase); float c = q * cos(phase); alongX += vec3(-d.x * d.x * s, d.x * c, -d.x * d.y * s); alongZ += vec3(-d.x * d.y * s, d.y * c, -d.y * d.y * s); return vec3(d.x * a * cos(phase), a * sin(phase), d.y * a * cos(phase));}Inputs swell(Inputs i) { vec2 rest = i.position.xz; float t = i.time * p_speed; vec3 alongX = vec3(1.0, 0.0, 0.0); vec3 alongZ = vec3(0.0, 0.0, 1.0); // Three waves from different directions, sharing the steepness slider so their sum never passes 1, which // keeps the crests from folding over into loops vec3 moved = gerstner(rest, vec2(1.0, 0.35), p_wavelength, p_steepness * 0.5, t, alongX, alongZ); moved += gerstner(rest, vec2(0.6, -0.8), p_wavelength * 0.61, p_steepness * 0.3, t, alongX, alongZ); moved += gerstner(rest, vec2(-0.3, 1.0), p_wavelength * 0.37, p_steepness * 0.2, t, alongX, alongZ); i.position += moved; i.normal = normalize(cross(alongZ, alongX)); return i;}vec4 effect(Inputs i) { vec3 sun = normalize(vec3(-0.5, 0.6, -0.6)); // Foam from 30% to 100% of the main wave's amplitude (steepness / k); // the smaller waves stacked on top lift some crests higher float peak = p_steepness * 0.5 * p_wavelength / 6.2831853; float crest = smoothstep(0.3 * peak, peak, i.position.y); float diffuse = max(dot(i.normal, sun), 0.0); float shine = pow(max(dot(i.normal, normalize(sun + i.view)), 0.0), 120.0); float fresnel = pow(1.0 - max(dot(i.normal, i.view), 0.0), 4.0); vec3 water = mix(p_deep * (0.4 + 0.6 * diffuse), p_horizon, fresnel); vec3 color = mix(water, p_foam, crest * 0.7) + shine; return vec4(color, 1.0);}void main() { Inputs i; i.uv = a_uv; // The test meshes have no model transform: object space is world space i.position = a_position; i.normal = normalize(a_normal); i.view = normalize(u_camera - a_position); i.time = u_time; // No pixel yet in a vertex shader i.fragCoord = vec2(0.0); i.resolution = u_resolution; i = swell(i); v_uv = a_uv; v_position = i.position; v_normal = i.normal; // From the moved surface toward the camera; the fragment shader normalizes it v_view = u_camera - i.position; gl_Position = u_viewProjection * vec4(i.position, 1.0);}#version 300 es// Vertex displacement → Ocean swell, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_wavelength; // Wavelength: 1.2 (0.3 to 3.0)uniform float p_steepness; // Steepness: 0.7 (0.0 to 1.0)uniform float p_speed; // Speed: 0.4 (0.0 to 2.0)uniform vec3 p_deep; // Deep water: vec3(0.03, 0.2, 0.32), sRGB; upload it decoded to linearuniform vec3 p_horizon; // Sky reflection: vec3(0.62, 0.78, 0.88), sRGB; upload it decoded to linearuniform vec3 p_foam; // Foam: vec3(0.92, 0.96, 1.0), sRGB; upload it decoded to linearin vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// One Gerstner wave at the rest position `rest`: returns how far it moves the point and adds its slope to the two// tangents of the surface, along x and along z. The point moves on a circle, so the crests come out sharper than a// sine's and the troughs flatter.vec3 gerstner(vec2 rest, vec2 direction, float span, float q, float t, inout vec3 alongX, inout vec3 alongZ) { vec2 d = normalize(direction); float k = 6.2831853 / span; // Deep-water waves: speed grows with wavelength (w^2 = g * k), so long swells outrun short chop float phase = k * dot(d, rest) - sqrt(9.8 * k) * t; float a = q / k; float s = q * sin(phase); float c = q * cos(phase); alongX += vec3(-d.x * d.x * s, d.x * c, -d.x * d.y * s); alongZ += vec3(-d.x * d.y * s, d.y * c, -d.y * d.y * s); return vec3(d.x * a * cos(phase), a * sin(phase), d.y * a * cos(phase));}Inputs swell(Inputs i) { vec2 rest = i.position.xz; float t = i.time * p_speed; vec3 alongX = vec3(1.0, 0.0, 0.0); vec3 alongZ = vec3(0.0, 0.0, 1.0); // Three waves from different directions, sharing the steepness slider so their sum never passes 1, which // keeps the crests from folding over into loops vec3 moved = gerstner(rest, vec2(1.0, 0.35), p_wavelength, p_steepness * 0.5, t, alongX, alongZ); moved += gerstner(rest, vec2(0.6, -0.8), p_wavelength * 0.61, p_steepness * 0.3, t, alongX, alongZ); moved += gerstner(rest, vec2(-0.3, 1.0), p_wavelength * 0.37, p_steepness * 0.2, t, alongX, alongZ); i.position += moved; i.normal = normalize(cross(alongZ, alongX)); return i;}vec4 effect(Inputs i) { vec3 sun = normalize(vec3(-0.5, 0.6, -0.6)); // Foam from 30% to 100% of the main wave's amplitude (steepness / k); // the smaller waves stacked on top lift some crests higher float peak = p_steepness * 0.5 * p_wavelength / 6.2831853; float crest = smoothstep(0.3 * peak, peak, i.position.y); float diffuse = max(dot(i.normal, sun), 0.0); float shine = pow(max(dot(i.normal, normalize(sun + i.view)), 0.0), 120.0); float fresnel = pow(1.0 - max(dot(i.normal, i.view), 0.0), 4.0); vec3 water = mix(p_deep * (0.4 + 0.6 * diffuse), p_horizon, fresnel); vec3 color = mix(water, p_foam, crest * 0.7) + shine; return vec4(color, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Vertex displacement → Ocean swell, Godot 4. From Creative Center, MIT license.shader_type spatial;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch it// world_vertex_coords: vertex() sees VERTEX and NORMAL in world space, as the effect's vertex entry expectsrender_mode unshaded, world_vertex_coords;uniform float wavelength : hint_range(0.3, 3.0, 0.05) = 1.2;uniform float steepness : hint_range(0.0, 1.0, 0.01) = 0.7;uniform float speed : hint_range(0.0, 2.0, 0.05) = 0.4;uniform vec3 deep : source_color = vec3(0.03, 0.2, 0.32);uniform vec3 horizon : source_color = vec3(0.62, 0.78, 0.88);uniform vec3 foam : source_color = vec3(0.92, 0.96, 1.0);struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// One Gerstner wave at the rest position `rest`: returns how far it moves the point and adds its slope to the two// tangents of the surface, along x and along z. The point moves on a circle, so the crests come out sharper than a// sine's and the troughs flatter.vec3 gerstner(vec2 rest, vec2 direction, float span, float q, float t, inout vec3 alongX, inout vec3 alongZ) { vec2 d = normalize(direction); float k = 6.2831853 / span; // Deep-water waves: speed grows with wavelength (w^2 = g * k), so long swells outrun short chop float phase = k * dot(d, rest) - sqrt(9.8 * k) * t; float a = q / k; float s = q * sin(phase); float c = q * cos(phase); alongX += vec3(-d.x * d.x * s, d.x * c, -d.x * d.y * s); alongZ += vec3(-d.x * d.y * s, d.y * c, -d.y * d.y * s); return vec3(d.x * a * cos(phase), a * sin(phase), d.y * a * cos(phase));}Inputs swell(Inputs i) { vec2 rest = i.position.xz; float t = i.time * speed; vec3 alongX = vec3(1.0, 0.0, 0.0); vec3 alongZ = vec3(0.0, 0.0, 1.0); // Three waves from different directions, sharing the steepness slider so their sum never passes 1, which // keeps the crests from folding over into loops vec3 moved = gerstner(rest, vec2(1.0, 0.35), wavelength, steepness * 0.5, t, alongX, alongZ); moved += gerstner(rest, vec2(0.6, -0.8), wavelength * 0.61, steepness * 0.3, t, alongX, alongZ); moved += gerstner(rest, vec2(-0.3, 1.0), wavelength * 0.37, steepness * 0.2, t, alongX, alongZ); i.position += moved; i.normal = normalize(cross(alongZ, alongX)); return i;}vec4 effect(Inputs i) { vec3 sun = normalize(vec3(-0.5, 0.6, -0.6)); // Foam from 30% to 100% of the main wave's amplitude (steepness / k); // the smaller waves stacked on top lift some crests higher float peak = steepness * 0.5 * wavelength / 6.2831853; float crest = smoothstep(0.3 * peak, peak, i.position.y); float diffuse = max(dot(i.normal, sun), 0.0); float shine = pow(max(dot(i.normal, normalize(sun + i.view)), 0.0), 120.0); float fresnel = pow(1.0 - max(dot(i.normal, i.view), 0.0), 4.0); vec3 water = mix(deep * (0.4 + 0.6 * diffuse), horizon, fresnel); vec3 color = mix(water, foam, crest * 0.7) + shine; return vec4(color, 1.0);}void vertex() { Inputs i; // Godot's UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); i.position = VERTEX; i.normal = normalize(NORMAL); i.view = normalize(CAMERA_POSITION_WORLD - VERTEX); i.time = TIME; // No pixel yet in vertex() i.fragCoord = vec2(0.0); i.resolution = VIEWPORT_SIZE; i = swell(i); VERTEX = i.position; NORMAL = i.normal;}void fragment() { Inputs i; // Godot's UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // VERTEX, NORMAL and VIEW are in view space here; the effect works in world space i.position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; i.normal = normalize((INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); i.view = normalize((INV_VIEW_MATRIX * vec4(VIEW, 0.0)).xyz); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = VIEWPORT_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); ALBEDO = frag_out.rgb;}Ainda não há código de Unreal Engine 5 para este card.
Ainda não há código de Unity para este card.
Ainda não há código de Blender para este card.
Leitura complementar
- Effective Water Simulation from Physical Models (GPU Gems, chapter 1) (abre em uma nova aba)Livro
- Trochoidal wave (abre em uma nova aba)Referência
- Dispersion (water waves) (abre em uma nova aba)Referência
- cross (abre em uma nova aba)Referência
- normalize (abre em uma nova aba)Referência
- Your first 3D shader (abre em uma nova aba)Documentação