#version 450 layout(set = 0, binding = 0) uniform PerFrame { mat4 view; mat4 projection; mat4 lightSpaceMatrix; vec4 lightDir; vec4 lightColor; vec4 ambientColor; vec4 viewPos; vec4 fogColor; vec4 fogParams; vec4 shadowParams; }; layout(push_constant) uniform Push { mat4 model; float waveAmp; float waveFreq; float waveSpeed; float liquidBasicType; // 0=water, 1=ocean, 2=magma, 3=slime } push; layout(location = 0) in vec3 aPos; layout(location = 1) in vec2 aTexCoord; layout(location = 0) out vec3 FragPos; layout(location = 1) out vec3 Normal; layout(location = 2) out vec2 TexCoord; layout(location = 3) out float WaveOffset; layout(location = 4) out vec2 ScreenUV; // --- Gerstner wave --- // Coordinate system: X,Y = horizontal plane, Z = up (height) // displacement.xy = horizontal, displacement.z = vertical struct GerstnerResult { vec3 displacement; vec3 tangent; // along X vec3 binormal; // along Y float waveHeight; // raw wave height for foam }; GerstnerResult evaluateGerstnerWaves(vec2 pos, float time, float amp, float freq, float spd, float basicType) { GerstnerResult r; r.displacement = vec3(0.0); r.tangent = vec3(1.0, 0.0, 0.0); r.binormal = vec3(0.0, 1.0, 0.0); r.waveHeight = 0.0; // Magma/slime: simple slow undulation if (basicType >= 1.5) { float wave = sin(pos.x * freq * 0.5 + time * spd * 0.3) * 0.4 + sin(pos.y * freq * 0.3 + time * spd * 0.5) * 0.3; r.displacement.z = wave * amp * 0.5; float dx = cos(pos.x * freq * 0.5 + time * spd * 0.3) * freq * 0.5 * amp * 0.5 * 0.4; float dy = cos(pos.y * freq * 0.3 + time * spd * 0.5) * freq * 0.3 * amp * 0.5 * 0.3; r.tangent = vec3(1.0, 0.0, dx); r.binormal = vec3(0.0, 1.0, dy); r.waveHeight = wave; return r; } // 6 wave directions for more chaotic, natural-looking water // Spread across many angles to avoid visible patterns vec2 dirs[6] = vec2[6]( normalize(vec2(0.86, 0.51)), normalize(vec2(-0.47, 0.88)), normalize(vec2(0.32, -0.95)), normalize(vec2(-0.93, -0.37)), normalize(vec2(0.67, -0.29)), normalize(vec2(-0.15, 0.74)) ); float amps[6]; float freqs[6]; float spds_arr[6]; float steepness[6]; if (basicType > 0.5) { // Ocean: broader range of wave scales for realistic chop amps[0] = amp * 1.0; amps[1] = amp * 0.55; amps[2] = amp * 0.30; amps[3] = amp * 0.18; amps[4] = amp * 0.10; amps[5] = amp * 0.06; freqs[0] = freq * 0.7; freqs[1] = freq * 1.3; freqs[2] = freq * 2.1; freqs[3] = freq * 3.4; freqs[4] = freq * 5.0; freqs[5] = freq * 7.5; spds_arr[0] = spd * 0.8; spds_arr[1] = spd * 1.0; spds_arr[2] = spd * 1.3; spds_arr[3] = spd * 1.6; spds_arr[4] = spd * 2.0; spds_arr[5] = spd * 2.5; steepness[0] = 0.35; steepness[1] = 0.30; steepness[2] = 0.25; steepness[3] = 0.20; steepness[4] = 0.15; steepness[5] = 0.10; } else { // Inland water: gentle but multi-scale ripples amps[0] = amp * 0.5; amps[1] = amp * 0.25; amps[2] = amp * 0.15; amps[3] = amp * 0.08; amps[4] = amp * 0.05; amps[5] = amp * 0.03; freqs[0] = freq * 1.0; freqs[1] = freq * 1.8; freqs[2] = freq * 3.0; freqs[3] = freq * 4.5; freqs[4] = freq * 7.0; freqs[5] = freq * 10.0; spds_arr[0] = spd * 0.6; spds_arr[1] = spd * 0.9; spds_arr[2] = spd * 1.2; spds_arr[3] = spd * 1.5; spds_arr[4] = spd * 1.9; spds_arr[5] = spd * 2.3; steepness[0] = 0.20; steepness[1] = 0.18; steepness[2] = 0.15; steepness[3] = 0.12; steepness[4] = 0.10; steepness[5] = 0.08; } float totalWave = 0.0; for (int i = 0; i < 6; i++) { float w = freqs[i]; float A = amps[i]; float phi = spds_arr[i] * w; // phase speed float Q = steepness[i] / (w * A * 6.0); Q = clamp(Q, 0.0, 1.0); float phase = w * dot(dirs[i], pos) + phi * time; float s = sin(phase); float c = cos(phase); // Gerstner displacement: xy = horizontal, z = vertical (up) r.displacement.x += Q * A * dirs[i].x * c; r.displacement.y += Q * A * dirs[i].y * c; r.displacement.z += A * s; // Tangent/binormal accumulation for analytical normal float WA = w * A; r.tangent.x -= Q * dirs[i].x * dirs[i].x * WA * s; r.tangent.y -= Q * dirs[i].x * dirs[i].y * WA * s; r.tangent.z += dirs[i].x * WA * c; r.binormal.x -= Q * dirs[i].x * dirs[i].y * WA * s; r.binormal.y -= Q * dirs[i].y * dirs[i].y * WA * s; r.binormal.z += dirs[i].y * WA * c; totalWave += A * s; } r.waveHeight = totalWave; return r; } void main() { float time = fogParams.z; vec4 worldPos = push.model * vec4(aPos, 1.0); // Evaluate Gerstner waves using X,Y horizontal plane GerstnerResult waves = evaluateGerstnerWaves( vec2(worldPos.x, worldPos.y), time, push.waveAmp, push.waveFreq, push.waveSpeed, push.liquidBasicType ); // Apply displacement: xy = horizontal, z = vertical (up) worldPos.x += waves.displacement.x; worldPos.y += waves.displacement.y; worldPos.z += waves.displacement.z; WaveOffset = waves.waveHeight; // raw wave height for fragment shader foam // Player interaction ripples — concentric waves emanating from player position vec2 playerPos = vec2(shadowParams.z, shadowParams.w); float rippleStrength = fogParams.w; float d = length(worldPos.xy - playerPos); float ripple = rippleStrength * 0.12 * exp(-d * 0.12) * sin(d * 2.5 - time * 6.0); worldPos.z += ripple; // Analytical normal from Gerstner tangent/binormal (cross product gives Z-up normal) Normal = normalize(cross(waves.binormal, waves.tangent)); FragPos = worldPos.xyz; TexCoord = aTexCoord; vec4 clipPos = projection * view * worldPos; gl_Position = clipPos; vec2 ndc = clipPos.xy / max(clipPos.w, 1e-5); ScreenUV = ndc * 0.5 + 0.5; }