diff --git a/assets/shaders/water.frag.glsl b/assets/shaders/water.frag.glsl index acf037f1..acc0566e 100644 --- a/assets/shaders/water.frag.glsl +++ b/assets/shaders/water.frag.glsl @@ -18,6 +18,7 @@ layout(push_constant) uniform Push { float waveAmp; float waveFreq; float waveSpeed; + float liquidBasicType; } push; layout(set = 1, binding = 0) uniform WaterMaterial { @@ -29,6 +30,10 @@ layout(set = 1, binding = 0) uniform WaterMaterial { layout(set = 2, binding = 0) uniform sampler2D SceneColor; layout(set = 2, binding = 1) uniform sampler2D SceneDepth; +layout(set = 2, binding = 2) uniform sampler2D ReflectionColor; +layout(set = 2, binding = 3) uniform ReflectionData { + mat4 reflViewProj; +}; layout(location = 0) in vec3 FragPos; layout(location = 1) in vec3 Normal; @@ -38,85 +43,286 @@ layout(location = 4) in vec2 ScreenUV; layout(location = 0) out vec4 outColor; +// ============================================================ +// Dual-scroll detail normals (multi-octave ripple overlay) +// ============================================================ vec3 dualScrollWaveNormal(vec2 p, float time) { - // Two independently scrolling octaves (normal-map style layering). vec2 d1 = normalize(vec2(0.86, 0.51)); vec2 d2 = normalize(vec2(-0.47, 0.88)); - float f1 = 0.19; - float f2 = 0.43; - float s1 = 0.95; - float s2 = 1.73; - float a1 = 0.26; - float a2 = 0.12; + vec2 d3 = normalize(vec2(0.32, -0.95)); + float f1 = 0.19, f2 = 0.43, f3 = 0.72; + float s1 = 0.95, s2 = 1.73, s3 = 2.40; + float a1 = 0.22, a2 = 0.10, a3 = 0.05; vec2 p1 = p + d1 * (time * s1 * 4.0); vec2 p2 = p + d2 * (time * s2 * 4.0); + vec2 p3 = p + d3 * (time * s3 * 4.0); - float ph1 = dot(p1, d1) * f1; - float ph2 = dot(p2, d2) * f2; + float c1 = cos(dot(p1, d1) * f1); + float c2 = cos(dot(p2, d2) * f2); + float c3 = cos(dot(p3, d3) * f3); - float c1 = cos(ph1); - float c2 = cos(ph2); + float dHx = c1 * d1.x * f1 * a1 + c2 * d2.x * f2 * a2 + c3 * d3.x * f3 * a3; + float dHy = c1 * d1.y * f1 * a1 + c2 * d2.y * f2 * a2 + c3 * d3.y * f3 * a3; - float dHx = c1 * d1.x * f1 * a1 + c2 * d2.x * f2 * a2; - float dHz = c1 * d1.y * f1 * a1 + c2 * d2.y * f2 * a2; + return normalize(vec3(-dHx, -dHy, 1.0)); +} - return normalize(vec3(-dHx, 1.0, -dHz)); +// ============================================================ +// GGX/Cook-Torrance BRDF +// ============================================================ +float DistributionGGX(vec3 N, vec3 H, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH * NdotH; + float denom = NdotH2 * (a2 - 1.0) + 1.0; + return a2 / (3.14159265 * denom * denom + 1e-7); +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; + float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + float ggx2 = NdotL / (NdotL * (1.0 - k) + k); + return ggx1 * ggx2; +} + +vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { + return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} + +// ============================================================ +// Linearize depth +// ============================================================ +float linearizeDepth(float d, float near, float far) { + return near * far / (far - d * (far - near)); +} + +// ============================================================ +// Noise functions for foam +// ============================================================ +float hash21(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +float hash22x(vec2 p) { + return fract(sin(dot(p, vec2(269.5, 183.3))) * 43758.5453); +} + +float noiseValue(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + float a = hash21(i); + float b = hash21(i + vec2(1.0, 0.0)); + float c = hash21(i + vec2(0.0, 1.0)); + float d = hash21(i + vec2(1.0, 1.0)); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +float fbmNoise(vec2 p, float time) { + float v = 0.0; + v += noiseValue(p * 3.0 + time * 0.3) * 0.5; + v += noiseValue(p * 6.0 - time * 0.5) * 0.25; + v += noiseValue(p * 12.0 + time * 0.7) * 0.125; + return v; +} + +// Voronoi-like cellular noise for foam particles +float cellularFoam(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + float minDist = 1.0; + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 neighbor = vec2(float(x), float(y)); + vec2 point = vec2(hash21(i + neighbor), hash22x(i + neighbor)); + float d = length(neighbor + point - f); + minDist = min(minDist, d); + } + } + return minDist; } void main() { float time = fogParams.z; + float basicType = push.liquidBasicType; + + vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(SceneColor, 0)); + + // --- Normal computation --- vec3 meshNorm = normalize(Normal); - vec3 waveNorm = dualScrollWaveNormal(FragPos.xz, time); - vec3 norm = normalize(mix(meshNorm, waveNorm, 0.82)); + vec3 detailNorm = dualScrollWaveNormal(FragPos.xy, time); + vec3 norm = normalize(mix(meshNorm, detailNorm, 0.55)); + + // Player interaction ripple normal perturbation + vec2 playerPos = vec2(shadowParams.z, shadowParams.w); + float rippleStrength = fogParams.w; + float d = length(FragPos.xy - playerPos); + float rippleEnv = rippleStrength * exp(-d * 0.12); + if (rippleEnv > 0.001) { + vec2 radialDir = (FragPos.xy - playerPos) / max(d, 0.01); + float dHdr = rippleEnv * 0.12 * (-0.12 * sin(d * 2.5 - time * 6.0) + 2.5 * cos(d * 2.5 - time * 6.0)); + norm = normalize(norm + vec3(-radialDir * dHdr, 0.0)); + } vec3 viewDir = normalize(viewPos.xyz - FragPos); vec3 ldir = normalize(-lightDir.xyz); - float ndotv = max(dot(norm, viewDir), 0.0); + float NdotV = max(dot(norm, viewDir), 0.001); + float NdotL = max(dot(norm, ldir), 0.0); - float diff = max(dot(norm, ldir), 0.0); - vec3 halfDir = normalize(ldir + viewDir); - float spec = pow(max(dot(norm, halfDir), 0.0), 96.0); - float sparkle = sin(FragPos.x * 20.0 + time * 3.0) * sin(FragPos.z * 20.0 + time * 2.5); - sparkle = max(0.0, sparkle) * shimmerStrength; - - float crest = smoothstep(0.3, 1.0, WaveOffset) * 0.15; float dist = length(viewPos.xyz - FragPos); - // Beer-Lambert style approximation from view distance. - float opticalDepth = 1.0 - exp(-dist * 0.0035); - vec3 litTransmission = waterColor.rgb * (ambientColor.rgb * 0.85 + diff * lightColor.rgb * 0.55); - vec3 absorbed = mix(litTransmission, waterColor.rgb * 0.52, opticalDepth); - absorbed += vec3(crest); + // --- Schlick Fresnel --- + const vec3 F0 = vec3(0.02); + float fresnel = F0.x + (1.0 - F0.x) * pow(1.0 - NdotV, 5.0); - // Schlick Fresnel with water-like F0. - const float F0 = 0.02; - float fresnel = F0 + (1.0 - F0) * pow(1.0 - ndotv, 5.0); - vec2 refractOffset = norm.xz * (0.012 + 0.02 * fresnel); - vec2 refractUV = clamp(ScreenUV + refractOffset, vec2(0.001), vec2(0.999)); + // ============================================================ + // Refraction (screen-space from scene history) + // ============================================================ + vec2 refractOffset = norm.xy * (0.02 + 0.03 * fresnel); + vec2 refractUV = clamp(screenUV + refractOffset, vec2(0.001), vec2(0.999)); vec3 sceneRefract = texture(SceneColor, refractUV).rgb; float sceneDepth = texture(SceneDepth, refractUV).r; - float waterDepth = clamp((sceneDepth - gl_FragCoord.z) * 180.0, 0.0, 1.0); - float depthBlend = waterDepth; - // Fallback when sampled depth does not provide meaningful separation. - if (sceneDepth <= gl_FragCoord.z + 1e-4) { - depthBlend = 0.45 + opticalDepth * 0.40; + + float near = 0.05; + float far = 30000.0; + float sceneLinDepth = linearizeDepth(sceneDepth, near, far); + float waterLinDepth = linearizeDepth(gl_FragCoord.z, near, far); + float depthDiff = max(sceneLinDepth - waterLinDepth, 0.0); + + // ============================================================ + // Beer-Lambert absorption + // ============================================================ + vec3 absorptionCoeff = vec3(0.46, 0.09, 0.06); + if (basicType > 0.5 && basicType < 1.5) { + absorptionCoeff = vec3(0.35, 0.06, 0.04); } - depthBlend = clamp(depthBlend, 0.28, 1.0); - vec3 refractedTint = mix(sceneRefract, absorbed, depthBlend); + vec3 absorbed = exp(-absorptionCoeff * depthDiff); - vec3 specular = spec * lightColor.rgb * (0.45 + 0.75 * fresnel) - + sparkle * lightColor.rgb * 0.30; - // Add a clear surface reflection lobe at grazing angles. - vec3 envReflect = mix(fogColor.rgb, lightColor.rgb, 0.38) * vec3(0.75, 0.86, 1.0); - vec3 reflection = envReflect * (0.45 + 0.55 * fresnel) + specular; - float reflectWeight = clamp(fresnel * 1.15, 0.0, 0.92); - vec3 color = mix(refractedTint, reflection, reflectWeight); + vec3 shallowColor = waterColor.rgb * 1.2; + vec3 deepColor = waterColor.rgb * vec3(0.3, 0.5, 0.7); + float depthFade = 1.0 - exp(-depthDiff * 0.15); + vec3 waterBody = mix(shallowColor, deepColor, depthFade); - float alpha = mix(waterAlpha * 1.05, min(1.0, waterAlpha * 1.30), fresnel) * alphaScale; + vec3 refractedColor = mix(sceneRefract * absorbed, waterBody, depthFade * 0.7); + + if (depthDiff < 0.01) { + float opticalDepth = 1.0 - exp(-dist * 0.004); + refractedColor = mix(sceneRefract, waterBody, opticalDepth * 0.6); + } + + vec3 litBase = waterBody * (ambientColor.rgb * 0.7 + NdotL * lightColor.rgb * 0.5); + refractedColor = mix(refractedColor, litBase, clamp(depthFade * 0.3, 0.0, 0.5)); + + // ============================================================ + // Planar reflection — subtle, not mirror-like + // ============================================================ + // reflWeight starts at 0; only contributes where we have valid reflection data + float reflAmount = 0.0; + vec3 envReflect = vec3(0.0); + + vec4 reflClip = reflViewProj * vec4(FragPos, 1.0); + if (reflClip.w > 0.1) { + vec2 reflUV = reflClip.xy / reflClip.w * 0.5 + 0.5; + reflUV.y = 1.0 - reflUV.y; + reflUV += norm.xy * 0.015; + + // Wide fade so there's no visible boundary — fully gone well inside the edge + float edgeFade = smoothstep(0.0, 0.15, reflUV.x) * smoothstep(1.0, 0.85, reflUV.x) + * smoothstep(0.0, 0.15, reflUV.y) * smoothstep(1.0, 0.85, reflUV.y); + + reflUV = clamp(reflUV, vec2(0.002), vec2(0.998)); + vec3 texReflect = texture(ReflectionColor, reflUV).rgb; + + float reflBrightness = dot(texReflect, vec3(0.299, 0.587, 0.114)); + float reflValidity = smoothstep(0.002, 0.05, reflBrightness) * edgeFade; + + envReflect = texReflect * 0.5; + reflAmount = reflValidity * 0.4; + } + + // ============================================================ + // GGX Specular + // ============================================================ + float roughness = 0.18; + vec3 halfDir = normalize(ldir + viewDir); + float D = DistributionGGX(norm, halfDir, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + vec3 F = fresnelSchlickRoughness(max(dot(halfDir, viewDir), 0.0), F0, roughness); + vec3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.001) * lightColor.rgb * NdotL; + specular = min(specular, vec3(2.0)); + + // Noise-based sparkle + float sparkleNoise = fbmNoise(FragPos.xy * 4.0 + time * 0.5, time * 1.5); + float sparkle = pow(max(sparkleNoise - 0.55, 0.0) / 0.45, 3.0) * shimmerStrength * 0.10; + specular += sparkle * lightColor.rgb; + + // ============================================================ + // Subsurface scattering + // ============================================================ + float sssBase = pow(max(dot(viewDir, -ldir), 0.0), 4.0); + float sss = sssBase * max(0.0, WaveOffset * 3.0) * 0.25; + vec3 sssColor = vec3(0.05, 0.55, 0.35) * sss * lightColor.rgb; + + // ============================================================ + // Combine — reflection only where valid, no dark fallback + // ============================================================ + // reflAmount is 0 where no valid reflection data exists — no dark arc + float reflectWeight = clamp(fresnel * reflAmount, 0.0, 0.30); + vec3 color = mix(refractedColor, envReflect, reflectWeight); + color += specular + sssColor; + + float crest = smoothstep(0.5, 1.0, WaveOffset) * 0.04; + color += vec3(crest); + + // ============================================================ + // Shoreline foam — scattered particles, not smooth bands + // ============================================================ + if (basicType < 1.5 && depthDiff > 0.01) { + float foamDepthMask = 1.0 - smoothstep(0.0, 1.8, depthDiff); + + // Fine scattered particles + float cells1 = cellularFoam(FragPos.xy * 14.0 + time * vec2(0.15, 0.08)); + float foam1 = (1.0 - smoothstep(0.0, 0.10, cells1)) * 0.5; + + // Tiny spray dots + float cells2 = cellularFoam(FragPos.xy * 30.0 + time * vec2(-0.12, 0.22)); + float foam2 = (1.0 - smoothstep(0.0, 0.06, cells2)) * 0.35; + + // Micro specks + float cells3 = cellularFoam(FragPos.xy * 55.0 + time * vec2(0.25, -0.1)); + float foam3 = (1.0 - smoothstep(0.0, 0.04, cells3)) * 0.2; + + // Noise breakup for clumping + float noiseMask = noiseValue(FragPos.xy * 3.0 + time * 0.15); + float foam = (foam1 + foam2 + foam3) * foamDepthMask * smoothstep(0.3, 0.6, noiseMask); + + foam *= smoothstep(0.0, 0.1, depthDiff); + color = mix(color, vec3(0.92, 0.95, 0.98), clamp(foam, 0.0, 0.45)); + } + + // ============================================================ + // Wave crest foam (ocean only) — particle-based + // ============================================================ + if (basicType > 0.5 && basicType < 1.5) { + float crestMask = smoothstep(0.5, 1.0, WaveOffset); + float crestCells = cellularFoam(FragPos.xy * 6.0 + time * vec2(0.12, 0.08)); + float crestFoam = (1.0 - smoothstep(0.0, 0.18, crestCells)) * crestMask; + float crestNoise = noiseValue(FragPos.xy * 3.0 - time * 0.3); + crestFoam *= smoothstep(0.3, 0.6, crestNoise); + color = mix(color, vec3(0.92, 0.95, 0.98), crestFoam * 0.35); + } + + // ============================================================ + // Alpha and fog + // ============================================================ + float baseAlpha = mix(waterAlpha, min(1.0, waterAlpha * 1.5), depthFade); + float alpha = mix(baseAlpha, min(1.0, baseAlpha * 1.3), fresnel) * alphaScale; alpha *= smoothstep(1600.0, 350.0, dist); - alpha = clamp(alpha, 0.50, 1.0); + alpha = clamp(alpha, 0.15, 0.92); float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0); color = mix(fogColor.rgb, color, fogFactor); diff --git a/assets/shaders/water.frag.spv b/assets/shaders/water.frag.spv index 496a10a9..f094c68e 100644 Binary files a/assets/shaders/water.frag.spv and b/assets/shaders/water.frag.spv differ diff --git a/assets/shaders/water.vert.glsl b/assets/shaders/water.vert.glsl index 7547f891..6eb0e796 100644 --- a/assets/shaders/water.vert.glsl +++ b/assets/shaders/water.vert.glsl @@ -18,6 +18,7 @@ layout(push_constant) uniform Push { float waveAmp; float waveFreq; float waveSpeed; + float liquidBasicType; // 0=water, 1=ocean, 2=magma, 3=slime } push; layout(location = 0) in vec3 aPos; @@ -29,32 +30,132 @@ layout(location = 2) out vec2 TexCoord; layout(location = 3) out float WaveOffset; layout(location = 4) out vec2 ScreenUV; -float hashGrid(vec2 p) { - return fract(sin(dot(floor(p), vec2(127.1, 311.7))) * 43758.5453); +// --- 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); - float px = worldPos.x; - float py = worldPos.z; - float dist = length(worldPos.xyz - viewPos.xyz); - float blend = smoothstep(150.0, 400.0, dist); - float seamless = sin(px * push.waveFreq + time * push.waveSpeed) * 0.6 - + sin(py * push.waveFreq * 0.7 + time * push.waveSpeed * 1.3) * 0.3 - + sin((px + py) * push.waveFreq * 0.5 + time * push.waveSpeed * 0.7) * 0.1; + // 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 + ); - float gridWave = sin(px * push.waveFreq + time * push.waveSpeed + hashGrid(vec2(px, py) * 0.01) * 6.28) * 0.5 - + sin(py * push.waveFreq * 0.8 + time * push.waveSpeed * 1.1 + hashGrid(vec2(py, px) * 0.01) * 6.28) * 0.5; + // 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 - float wave = mix(seamless, gridWave, blend); - worldPos.y += wave * push.waveAmp; - WaveOffset = wave; + // 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; - float dx = cos(px * push.waveFreq + time * push.waveSpeed) * push.waveFreq * push.waveAmp; - float dz = cos(py * push.waveFreq * 0.7 + time * push.waveSpeed * 1.3) * push.waveFreq * 0.7 * push.waveAmp; - Normal = normalize(vec3(-dx, 1.0, -dz)); + // Analytical normal from Gerstner tangent/binormal (cross product gives Z-up normal) + Normal = normalize(cross(waves.binormal, waves.tangent)); FragPos = worldPos.xyz; TexCoord = aTexCoord; diff --git a/assets/shaders/water.vert.spv b/assets/shaders/water.vert.spv index 4325d2c5..3f585319 100644 Binary files a/assets/shaders/water.vert.spv and b/assets/shaders/water.vert.spv differ diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 7447ecb8..667adc19 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -386,9 +386,17 @@ private: GPUPerFrameData currentFrameData{}; float globalTime = 0.0f; + // Per-frame reflection UBO (mirrors camera for planar reflections) + VkBuffer reflPerFrameUBO = VK_NULL_HANDLE; + VmaAllocation reflPerFrameUBOAlloc = VK_NULL_HANDLE; + void* reflPerFrameUBOMapped = nullptr; + VkDescriptorSet reflPerFrameDescSet = VK_NULL_HANDLE; + bool createPerFrameResources(); void destroyPerFrameResources(); void updatePerFrameUBO(); + void setupWater1xPass(); + void renderReflectionPass(); // Active character previews for off-screen rendering std::vector activePreviews_; diff --git a/include/rendering/vk_context.hpp b/include/rendering/vk_context.hpp index 2496c43e..4f405073 100644 --- a/include/rendering/vk_context.hpp +++ b/include/rendering/vk_context.hpp @@ -85,6 +85,8 @@ public: return (depthResolveImage == VK_NULL_HANDLE) && (msaaSamples_ > VK_SAMPLE_COUNT_1_BIT); } VkFormat getDepthFormat() const { return depthFormat; } + VkImageView getDepthResolveImageView() const { return depthResolveImageView; } + VkImageView getDepthImageView() const { return depthImageView; } // UI texture upload: creates a Vulkan texture from RGBA data and returns // a VkDescriptorSet suitable for use as ImTextureID. diff --git a/include/rendering/vk_pipeline.hpp b/include/rendering/vk_pipeline.hpp index 0f85dee3..86929b72 100644 --- a/include/rendering/vk_pipeline.hpp +++ b/include/rendering/vk_pipeline.hpp @@ -64,6 +64,7 @@ public: // Multisampling PipelineBuilder& setMultisample(VkSampleCountFlagBits samples); + PipelineBuilder& setAlphaToCoverage(bool enable); // Pipeline layout PipelineBuilder& setLayout(VkPipelineLayout layout); @@ -80,6 +81,7 @@ public: // Common blend states static VkPipelineColorBlendAttachmentState blendDisabled(); static VkPipelineColorBlendAttachmentState blendAlpha(); + static VkPipelineColorBlendAttachmentState blendPremultiplied(); static VkPipelineColorBlendAttachmentState blendAdditive(); private: @@ -98,6 +100,7 @@ private: float depthBiasConstant_ = 0.0f; float depthBiasSlope_ = 0.0f; VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT; + bool alphaToCoverage_ = false; std::vector colorBlendAttachments_; VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE; VkRenderPass renderPass_ = VK_NULL_HANDLE; diff --git a/include/rendering/water_renderer.hpp b/include/rendering/water_renderer.hpp index e4f5e68e..12b04347 100644 --- a/include/rendering/water_renderer.hpp +++ b/include/rendering/water_renderer.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +62,8 @@ struct WaterSurface { }; /** - * Water renderer (Vulkan) + * Water renderer (Vulkan) with planar reflections, Gerstner waves, + * GGX specular, shoreline foam, and subsurface scattering. */ class WaterRenderer { public: @@ -81,13 +83,44 @@ public: void recreatePipelines(); - void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time); + // Separate 1x pass for MSAA mode — water rendered after MSAA resolve + bool createWater1xPass(VkFormat colorFormat, VkFormat depthFormat); + void createWater1xFramebuffers(const std::vector& swapViews, + VkImageView depthView, VkExtent2D extent); + void destroyWater1xResources(); + bool beginWater1xPass(VkCommandBuffer cmd, uint32_t imageIndex, VkExtent2D extent); + void endWater1xPass(VkCommandBuffer cmd); + bool hasWater1xPass() const { return water1xRenderPass != VK_NULL_HANDLE; } + VkRenderPass getWater1xRenderPass() const { return water1xRenderPass; } + + void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera, float time, bool use1x = false); void captureSceneHistory(VkCommandBuffer cmd, VkImage srcColorImage, VkImage srcDepthImage, VkExtent2D srcExtent, bool srcDepthIsMsaa); + // --- Planar reflection pass --- + // Call sequence: beginReflectionPass → [render scene] → endReflectionPass + bool beginReflectionPass(VkCommandBuffer cmd); + void endReflectionPass(VkCommandBuffer cmd); + + // Get the dominant water height near a position (for reflection plane) + std::optional getDominantWaterHeight(const glm::vec3& cameraPos) const; + + // Compute reflected view matrix for a given water height + static glm::mat4 computeReflectedView(const Camera& camera, float waterHeight); + // Compute oblique clip projection to clip below-water geometry in reflection + static glm::mat4 computeObliqueProjection(const glm::mat4& proj, const glm::mat4& view, float waterHeight); + + // Update the reflection UBO with reflected viewProj matrix + void updateReflectionUBO(const glm::mat4& reflViewProj); + + VkRenderPass getReflectionRenderPass() const { return reflectionRenderPass; } + VkExtent2D getReflectionExtent() const { return {REFLECTION_WIDTH, REFLECTION_HEIGHT}; } + bool hasReflectionPass() const { return reflectionRenderPass != VK_NULL_HANDLE; } + bool hasSurfaces() const { return !surfaces.empty(); } + void setEnabled(bool enabled) { renderingEnabled = enabled; } bool isEnabled() const { return renderingEnabled; } @@ -108,6 +141,10 @@ private: void createSceneHistoryResources(VkExtent2D extent, VkFormat colorFormat, VkFormat depthFormat); void destroySceneHistoryResources(); + // Reflection pass resources + void createReflectionResources(); + void destroyReflectionResources(); + VkContext* vkCtx = nullptr; // Pipeline @@ -131,6 +168,30 @@ private: VkExtent2D sceneHistoryExtent = {0, 0}; bool sceneHistoryReady = false; + // Planar reflection resources + static constexpr uint32_t REFLECTION_WIDTH = 512; + static constexpr uint32_t REFLECTION_HEIGHT = 512; + VkRenderPass reflectionRenderPass = VK_NULL_HANDLE; + VkFramebuffer reflectionFramebuffer = VK_NULL_HANDLE; + VkImage reflectionColorImage = VK_NULL_HANDLE; + VmaAllocation reflectionColorAlloc = VK_NULL_HANDLE; + VkImageView reflectionColorView = VK_NULL_HANDLE; + VkImage reflectionDepthImage = VK_NULL_HANDLE; + VmaAllocation reflectionDepthAlloc = VK_NULL_HANDLE; + VkImageView reflectionDepthView = VK_NULL_HANDLE; + VkSampler reflectionSampler = VK_NULL_HANDLE; + VkImageLayout reflectionColorLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + // Reflection UBO (mat4 reflViewProj) + ::VkBuffer reflectionUBO = VK_NULL_HANDLE; + VmaAllocation reflectionUBOAlloc = VK_NULL_HANDLE; + void* reflectionUBOMapped = nullptr; + + // Separate 1x water pass (used when MSAA is active) + VkRenderPass water1xRenderPass = VK_NULL_HANDLE; + VkPipeline water1xPipeline = VK_NULL_HANDLE; + std::vector water1xFramebuffers; + std::vector surfaces; bool renderingEnabled = true; }; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 80b6fd87..3db966a3 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -394,16 +394,16 @@ bool Renderer::createPerFrameResources() { return false; } - // --- Create descriptor pool for both UBO and combined image sampler --- + // --- Create descriptor pool for UBO + image sampler (normal frames + reflection) --- VkDescriptorPoolSize poolSizes[2]{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - poolSizes[0].descriptorCount = MAX_FRAMES; + poolSizes[0].descriptorCount = MAX_FRAMES + 1; // +1 for reflection perFrame UBO poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSizes[1].descriptorCount = MAX_FRAMES; + poolSizes[1].descriptorCount = MAX_FRAMES + 1; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - poolInfo.maxSets = MAX_FRAMES; + poolInfo.maxSets = MAX_FRAMES + 1; // +1 for reflection descriptor set poolInfo.poolSizeCount = 2; poolInfo.pPoolSizes = poolSizes; @@ -472,6 +472,63 @@ bool Renderer::createPerFrameResources() { vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); } + // --- Create reflection per-frame UBO and descriptor set --- + { + VkBufferCreateInfo bufInfo{}; + bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufInfo.size = sizeof(GPUPerFrameData); + bufInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + + VmaAllocationCreateInfo allocInfo{}; + allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VmaAllocationInfo mapInfo{}; + if (vmaCreateBuffer(vkCtx->getAllocator(), &bufInfo, &allocInfo, + &reflPerFrameUBO, &reflPerFrameUBOAlloc, &mapInfo) != VK_SUCCESS) { + LOG_ERROR("Failed to create reflection per-frame UBO"); + return false; + } + reflPerFrameUBOMapped = mapInfo.pMappedData; + + VkDescriptorSetAllocateInfo setAlloc{}; + setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + setAlloc.descriptorPool = sceneDescriptorPool; + setAlloc.descriptorSetCount = 1; + setAlloc.pSetLayouts = &perFrameSetLayout; + + if (vkAllocateDescriptorSets(device, &setAlloc, &reflPerFrameDescSet) != VK_SUCCESS) { + LOG_ERROR("Failed to allocate reflection per-frame descriptor set"); + return false; + } + + VkDescriptorBufferInfo descBuf{}; + descBuf.buffer = reflPerFrameUBO; + descBuf.offset = 0; + descBuf.range = sizeof(GPUPerFrameData); + + VkDescriptorImageInfo shadowImgInfo{}; + shadowImgInfo.sampler = shadowSampler; + shadowImgInfo.imageView = shadowDepthView; + shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkWriteDescriptorSet writes[2]{}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = reflPerFrameDescSet; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[0].pBufferInfo = &descBuf; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = reflPerFrameDescSet; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[1].pImageInfo = &shadowImgInfo; + + vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); + } + LOG_INFO("Per-frame Vulkan resources created (shadow map ", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")"); return true; } @@ -487,6 +544,11 @@ void Renderer::destroyPerFrameResources() { perFrameUBOs[i] = VK_NULL_HANDLE; } } + if (reflPerFrameUBO) { + vmaDestroyBuffer(vkCtx->getAllocator(), reflPerFrameUBO, reflPerFrameUBOAlloc); + reflPerFrameUBO = VK_NULL_HANDLE; + reflPerFrameUBOMapped = nullptr; + } if (sceneDescriptorPool) { vkDestroyDescriptorPool(device, sceneDescriptorPool, nullptr); sceneDescriptorPool = VK_NULL_HANDLE; @@ -526,6 +588,17 @@ void Renderer::updatePerFrameUBO() { currentFrameData.shadowParams = glm::vec4(shadowsEnabled ? 1.0f : 0.0f, 0.5f, 0.0f, 0.0f); + // Player water ripple data: pack player XY into shadowParams.zw, ripple strength into fogParams.w + if (cameraController) { + currentFrameData.shadowParams.z = characterPosition.x; + currentFrameData.shadowParams.w = characterPosition.y; + bool inWater = cameraController->isSwimming(); + bool moving = cameraController->isMoving(); + currentFrameData.fogParams.w = (inWater && moving) ? 1.0f : 0.0f; + } else { + currentFrameData.fogParams.w = 0.0f; + } + // Copy to current frame's mapped UBO uint32_t frame = vkCtx->getCurrentFrame(); std::memcpy(perFrameUBOMapped[frame], ¤tFrameData, sizeof(GPUPerFrameData)); @@ -777,7 +850,15 @@ void Renderer::applyMsaaChange() { // Recreate all sub-renderer pipelines (they embed sample count from render pass) if (terrainRenderer) terrainRenderer->recreatePipelines(); - if (waterRenderer) waterRenderer->recreatePipelines(); + if (waterRenderer) { + waterRenderer->recreatePipelines(); + if (vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT) { + waterRenderer->destroyWater1xResources(); + setupWater1xPass(); + } else { + waterRenderer->destroyWater1xResources(); + } + } if (wmoRenderer) wmoRenderer->recreatePipelines(); if (m2Renderer) m2Renderer->recreatePipelines(); if (characterRenderer) characterRenderer->recreatePipelines(); @@ -833,6 +914,15 @@ void Renderer::beginFrame() { // Handle swapchain recreation if needed if (vkCtx->isSwapchainDirty()) { vkCtx->recreateSwapchain(window->getWidth(), window->getHeight()); + // Rebuild water 1x framebuffers (they reference swapchain image views) + if (waterRenderer && waterRenderer->hasWater1xPass() + && vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT) { + VkImageView depthView = vkCtx->getDepthResolveImageView(); + if (depthView) { + waterRenderer->createWater1xFramebuffers( + vkCtx->getSwapchainImageViews(), depthView, vkCtx->getSwapchainExtent()); + } + } } // Acquire swapchain image and begin command buffer @@ -870,6 +960,9 @@ void Renderer::beginFrame() { renderShadowPass(); } + // Water reflection pre-pass (renders scene from mirrored camera into 512x512 texture) + renderReflectionPass(); + // --- Begin main render pass (clear color + depth) --- VkRenderPassBeginInfo rpInfo{}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; @@ -909,7 +1002,7 @@ void Renderer::beginFrame() { void Renderer::endFrame() { if (!vkCtx || currentCmd == VK_NULL_HANDLE) return; - // Record ImGui draw commands into the command buffer + // ImGui always renders in the main pass (its pipeline matches the main render pass) ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), currentCmd); vkCmdEndRenderPass(currentCmd); @@ -923,6 +1016,18 @@ void Renderer::endFrame() { vkCtx->isDepthCopySourceMsaa()); } + // Render water in separate 1x pass after MSAA resolve + scene capture + bool waterDeferred = waterRenderer && waterRenderer->hasWater1xPass() + && vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT; + if (waterDeferred && camera) { + VkExtent2D ext = vkCtx->getSwapchainExtent(); + uint32_t frame = vkCtx->getCurrentFrame(); + if (waterRenderer->beginWater1xPass(currentCmd, currentImageIndex, ext)) { + waterRenderer->render(currentCmd, perFrameDescSets[frame], *camera, globalTime, true); + waterRenderer->endWater1xPass(currentCmd); + } + } + // Submit and present vkCtx->endFrame(currentCmd, currentImageIndex); currentCmd = VK_NULL_HANDLE; @@ -3140,7 +3245,10 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { } // Water (transparent, after all opaques) - if (waterRenderer && camera) { + // When MSAA is on and 1x pass is available, water renders after main pass ends + bool waterDeferred = waterRenderer && waterRenderer->hasWater1xPass() + && vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT; + if (waterRenderer && camera && !waterDeferred) { waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime); } @@ -3244,6 +3352,8 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (!waterRenderer->initialize(vkCtx, perFrameSetLayout)) { LOG_ERROR("Failed to initialize water renderer"); waterRenderer.reset(); + } else if (vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT) { + setupWater1xPass(); } } @@ -3688,6 +3798,87 @@ glm::mat4 Renderer::computeLightSpaceMatrix() { return lightProj * lightView; } +void Renderer::setupWater1xPass() { + if (!waterRenderer || !vkCtx) return; + VkImageView depthView = vkCtx->getDepthResolveImageView(); + if (!depthView) { + LOG_WARNING("No depth resolve image available - cannot create 1x water pass"); + return; + } + + waterRenderer->createWater1xPass(vkCtx->getSwapchainFormat(), vkCtx->getDepthFormat()); + waterRenderer->createWater1xFramebuffers( + vkCtx->getSwapchainImageViews(), depthView, vkCtx->getSwapchainExtent()); +} + +void Renderer::renderReflectionPass() { + if (!waterRenderer || !camera || !waterRenderer->hasReflectionPass() || !waterRenderer->hasSurfaces()) return; + if (currentCmd == VK_NULL_HANDLE || !reflPerFrameUBOMapped) return; + + // Reflection pass uses 1x MSAA. Scene pipelines must be render-pass-compatible, + // which requires matching sample counts. Only render scene into reflection when MSAA is off. + bool canRenderScene = (vkCtx->getMsaaSamples() == VK_SAMPLE_COUNT_1_BIT); + + // Find dominant water height near camera + auto waterH = waterRenderer->getDominantWaterHeight(camera->getPosition()); + if (!waterH) return; + + float waterHeight = *waterH; + + // Skip reflection if camera is underwater (Z is up) + if (camera->getPosition().z < waterHeight + 0.5f) return; + + // Compute reflected view and oblique projection + glm::mat4 reflView = WaterRenderer::computeReflectedView(*camera, waterHeight); + glm::mat4 reflProj = WaterRenderer::computeObliqueProjection( + camera->getProjectionMatrix(), reflView, waterHeight); + + // Update water renderer's reflection UBO with the reflected viewProj + waterRenderer->updateReflectionUBO(reflProj * reflView); + + // Fill the reflection per-frame UBO (same as normal but with reflected matrices) + GPUPerFrameData reflData = currentFrameData; + reflData.view = reflView; + reflData.projection = reflProj; + // Reflected camera position (Z is up) + glm::vec3 reflPos = camera->getPosition(); + reflPos.z = 2.0f * waterHeight - reflPos.z; + reflData.viewPos = glm::vec4(reflPos, 1.0f); + std::memcpy(reflPerFrameUBOMapped, &reflData, sizeof(GPUPerFrameData)); + + // Begin reflection render pass (clears to black; scene rendered if pipeline-compatible) + if (!waterRenderer->beginReflectionPass(currentCmd)) return; + + if (canRenderScene) { + // Render scene into reflection texture (sky + terrain + WMO only for perf) + if (skySystem) { + rendering::SkyParams skyParams; + skyParams.timeOfDay = (skySystem->getSkybox()) ? skySystem->getSkybox()->getTimeOfDay() : 12.0f; + if (lightingManager) { + const auto& lp = lightingManager->getLightingParams(); + skyParams.directionalDir = lp.directionalDir; + skyParams.sunColor = lp.diffuseColor; + skyParams.skyTopColor = lp.skyTopColor; + skyParams.skyMiddleColor = lp.skyMiddleColor; + skyParams.skyBand1Color = lp.skyBand1Color; + skyParams.skyBand2Color = lp.skyBand2Color; + skyParams.cloudDensity = lp.cloudDensity; + skyParams.fogDensity = lp.fogDensity; + skyParams.horizonGlow = lp.horizonGlow; + } + skySystem->render(currentCmd, reflPerFrameDescSet, *camera, skyParams); + } + if (terrainRenderer && terrainEnabled) { + terrainRenderer->render(currentCmd, reflPerFrameDescSet, *camera); + } + if (wmoRenderer) { + wmoRenderer->render(currentCmd, reflPerFrameDescSet, *camera); + } + } + + waterRenderer->endReflectionPass(currentCmd); +} + void Renderer::renderShadowPass() { if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return; if (currentCmd == VK_NULL_HANDLE) return; @@ -3698,7 +3889,8 @@ void Renderer::renderShadowPass() { auto* ubo = reinterpret_cast(perFrameUBOMapped[frame]); if (ubo) { ubo->lightSpaceMatrix = lightSpaceMatrix; - ubo->shadowParams = glm::vec4(shadowsEnabled ? 1.0f : 0.0f, 0.8f, 0.0f, 0.0f); + ubo->shadowParams.x = shadowsEnabled ? 1.0f : 0.0f; + ubo->shadowParams.y = 0.8f; } // Barrier 1: transition shadow map into writable depth layout. diff --git a/src/rendering/vk_pipeline.cpp b/src/rendering/vk_pipeline.cpp index 5002bf69..4e565b07 100644 --- a/src/rendering/vk_pipeline.cpp +++ b/src/rendering/vk_pipeline.cpp @@ -90,6 +90,11 @@ PipelineBuilder& PipelineBuilder::setMultisample(VkSampleCountFlagBits samples) return *this; } +PipelineBuilder& PipelineBuilder::setAlphaToCoverage(bool enable) { + alphaToCoverage_ = enable; + return *this; +} + PipelineBuilder& PipelineBuilder::setLayout(VkPipelineLayout layout) { pipelineLayout_ = layout; return *this; @@ -145,6 +150,7 @@ VkPipeline PipelineBuilder::build(VkDevice device) const { multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisampling.sampleShadingEnable = VK_FALSE; multisampling.rasterizationSamples = msaaSamples_; + multisampling.alphaToCoverageEnable = alphaToCoverage_ ? VK_TRUE : VK_FALSE; // Depth/stencil VkPipelineDepthStencilStateCreateInfo depthStencil{}; @@ -218,6 +224,20 @@ VkPipelineColorBlendAttachmentState PipelineBuilder::blendAlpha() { return state; } +VkPipelineColorBlendAttachmentState PipelineBuilder::blendPremultiplied() { + VkPipelineColorBlendAttachmentState state{}; + state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blendEnable = VK_TRUE; + state.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + state.colorBlendOp = VK_BLEND_OP_ADD; + state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + state.alphaBlendOp = VK_BLEND_OP_ADD; + return state; +} + VkPipelineColorBlendAttachmentState PipelineBuilder::blendAdditive() { VkPipelineColorBlendAttachmentState state{}; state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index 4dc82f87..62d42907 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -33,7 +33,12 @@ struct WaterPushConstants { float waveAmp; float waveFreq; float waveSpeed; - float _pad; + float liquidBasicType; // 0=water, 1=ocean, 2=magma, 3=slime +}; + +// Matches set 2 binding 3 in water.frag.glsl +struct ReflectionUBOData { + glm::mat4 reflViewProj; }; WaterRenderer::WaterRenderer() = default; @@ -79,7 +84,7 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay return false; } - // --- Scene history descriptor set layout (set 2) --- + // --- Scene history + reflection descriptor set layout (set 2) --- VkDescriptorSetLayoutBinding sceneColorBinding{}; sceneColorBinding.binding = 0; sceneColorBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; @@ -92,20 +97,36 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay sceneDepthBinding.descriptorCount = 1; sceneDepthBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - sceneSetLayout = createDescriptorSetLayout(device, {sceneColorBinding, sceneDepthBinding}); + VkDescriptorSetLayoutBinding reflColorBinding{}; + reflColorBinding.binding = 2; + reflColorBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + reflColorBinding.descriptorCount = 1; + reflColorBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + VkDescriptorSetLayoutBinding reflUBOBinding{}; + reflUBOBinding.binding = 3; + reflUBOBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + reflUBOBinding.descriptorCount = 1; + reflUBOBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + sceneSetLayout = createDescriptorSetLayout(device, + {sceneColorBinding, sceneDepthBinding, reflColorBinding, reflUBOBinding}); if (!sceneSetLayout) { LOG_ERROR("WaterRenderer: failed to create scene set layout"); return false; } - VkDescriptorPoolSize scenePoolSize{}; - scenePoolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - scenePoolSize.descriptorCount = 2; + // Pool needs 3 combined image samplers + 1 uniform buffer + std::array scenePoolSizes{}; + scenePoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + scenePoolSizes[0].descriptorCount = 3; + scenePoolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + scenePoolSizes[1].descriptorCount = 1; VkDescriptorPoolCreateInfo scenePoolInfo{}; scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; scenePoolInfo.maxSets = 1; - scenePoolInfo.poolSizeCount = 1; - scenePoolInfo.pPoolSizes = &scenePoolSize; + scenePoolInfo.poolSizeCount = static_cast(scenePoolSizes.size()); + scenePoolInfo.pPoolSizes = scenePoolSizes.data(); if (vkCreateDescriptorPool(device, &scenePoolInfo, nullptr, &sceneDescPool) != VK_SUCCESS) { LOG_ERROR("WaterRenderer: failed to create scene descriptor pool"); return false; @@ -113,7 +134,7 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay // --- Pipeline layout --- VkPushConstantRange pushRange{}; - pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; pushRange.offset = 0; pushRange.size = sizeof(WaterPushConstants); @@ -124,6 +145,10 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay return false; } + // Create reflection resources FIRST so reflectionUBO exists when + // createSceneHistoryResources writes descriptor binding 3 + createReflectionResources(); + createSceneHistoryResources(vkCtx->getSwapchainExtent(), vkCtx->getSwapchainFormat(), vkCtx->getDepthFormat()); @@ -184,6 +209,8 @@ void WaterRenderer::recreatePipelines() { if (!vkCtx) return; VkDevice device = vkCtx->getDevice(); + destroyReflectionResources(); + createReflectionResources(); createSceneHistoryResources(vkCtx->getSwapchainExtent(), vkCtx->getSwapchainFormat(), vkCtx->getDepthFormat()); @@ -245,6 +272,8 @@ void WaterRenderer::shutdown() { VkDevice device = vkCtx->getDevice(); vkDeviceWaitIdle(device); + destroyWater1xResources(); + destroyReflectionResources(); destroySceneHistoryResources(); if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; } if (pipelineLayout) { vkDestroyPipelineLayout(device, pipelineLayout, nullptr); pipelineLayout = VK_NULL_HANDLE; } @@ -378,19 +407,59 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo depthInfo.imageView = sceneDepthView; depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - std::array writes{}; - writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[0].dstSet = sceneSet; - writes[0].dstBinding = 0; - writes[0].descriptorCount = 1; - writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - writes[0].pImageInfo = &colorInfo; - writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[1].dstSet = sceneSet; - writes[1].dstBinding = 1; - writes[1].descriptorCount = 1; - writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - writes[1].pImageInfo = &depthInfo; + // Reflection color texture (binding 2) — use scene color as placeholder until reflection is created + VkDescriptorImageInfo reflColorInfo{}; + reflColorInfo.sampler = sceneColorSampler; + reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sceneColorView; + reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + // Reflection UBO (binding 3) + VkDescriptorBufferInfo reflUBOInfo{}; + reflUBOInfo.buffer = reflectionUBO; + reflUBOInfo.offset = 0; + reflUBOInfo.range = sizeof(ReflectionUBOData); + + // Write bindings 0,1 always; write 2,3 only if reflection resources exist + std::vector writes; + + VkWriteDescriptorSet w0{}; + w0.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w0.dstSet = sceneSet; + w0.dstBinding = 0; + w0.descriptorCount = 1; + w0.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + w0.pImageInfo = &colorInfo; + writes.push_back(w0); + + VkWriteDescriptorSet w1{}; + w1.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w1.dstSet = sceneSet; + w1.dstBinding = 1; + w1.descriptorCount = 1; + w1.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + w1.pImageInfo = &depthInfo; + writes.push_back(w1); + + VkWriteDescriptorSet w2{}; + w2.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w2.dstSet = sceneSet; + w2.dstBinding = 2; + w2.descriptorCount = 1; + w2.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + w2.pImageInfo = &reflColorInfo; + writes.push_back(w2); + + if (reflectionUBO) { + VkWriteDescriptorSet w3{}; + w3.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w3.dstSet = sceneSet; + w3.dstBinding = 3; + w3.descriptorCount = 1; + w3.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + w3.pBufferInfo = &reflUBOInfo; + writes.push_back(w3); + } + vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); // Initialize history images to shader-read layout so first frame samples are defined. @@ -683,11 +752,12 @@ void WaterRenderer::clear() { // ============================================================== void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, - const Camera& /*camera*/, float /*time*/) { - if (!renderingEnabled || surfaces.empty() || !waterPipeline) return; + const Camera& /*camera*/, float /*time*/, bool use1x) { + VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline; + if (!renderingEnabled || surfaces.empty() || !pipeline) return; if (!sceneSet) return; - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &perFrameSet, 0, nullptr); @@ -699,17 +769,20 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, if (!surface.materialSet) continue; bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5); - float waveAmp = canalProfile ? 0.04f : 0.06f; - float waveFreq = canalProfile ? 0.30f : 0.22f; - float waveSpeed = canalProfile ? 1.20f : 2.00f; + uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4); + float waveAmp = canalProfile ? 0.10f : (basicType == 1 ? 0.35f : 0.18f); + float waveFreq = canalProfile ? 0.35f : (basicType == 1 ? 0.20f : 0.30f); + float waveSpeed = canalProfile ? 1.00f : (basicType == 1 ? 1.20f : 1.40f); WaterPushConstants push{}; push.model = glm::mat4(1.0f); push.waveAmp = waveAmp; push.waveFreq = waveFreq; push.waveSpeed = waveSpeed; + push.liquidBasicType = static_cast(basicType); - vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, + vkCmdPushConstants(cmd, pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(WaterPushConstants), &push); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, @@ -1101,23 +1174,548 @@ std::optional WaterRenderer::getWaterTypeAt(float glX, float glY) cons glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const { uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) { - case 0: return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); - case 1: return glm::vec4(0.06f, 0.18f, 0.34f, 1.0f); - case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); - case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); - default: return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); + case 0: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f); // inland: blue-green + case 1: return glm::vec4(0.04f, 0.14f, 0.30f, 1.0f); // ocean: deep blue + case 2: return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f); // magma + case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); // slime + default: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f); } } float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const { uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); switch (basicType) { - case 1: return 0.68f; - case 2: return 0.72f; - case 3: return 0.62f; - default: return 0.38f; + case 1: return 0.72f; // ocean + case 2: return 0.75f; // magma + case 3: return 0.65f; // slime + default: return 0.48f; // inland water } } +// ============================================================== +// Planar reflection resources +// ============================================================== + +void WaterRenderer::createReflectionResources() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + // --- Reflection color image --- + VkImageCreateInfo colorImgCI{}; + colorImgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + colorImgCI.imageType = VK_IMAGE_TYPE_2D; + colorImgCI.format = vkCtx->getSwapchainFormat(); + colorImgCI.extent = {REFLECTION_WIDTH, REFLECTION_HEIGHT, 1}; + colorImgCI.mipLevels = 1; + colorImgCI.arrayLayers = 1; + colorImgCI.samples = VK_SAMPLE_COUNT_1_BIT; + colorImgCI.tiling = VK_IMAGE_TILING_OPTIMAL; + colorImgCI.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + colorImgCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY; + + if (vmaCreateImage(allocator, &colorImgCI, &allocCI, + &reflectionColorImage, &reflectionColorAlloc, nullptr) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create reflection color image"); + return; + } + + VkImageViewCreateInfo colorViewCI{}; + colorViewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + colorViewCI.image = reflectionColorImage; + colorViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D; + colorViewCI.format = vkCtx->getSwapchainFormat(); + colorViewCI.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + if (vkCreateImageView(device, &colorViewCI, nullptr, &reflectionColorView) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create reflection color view"); + return; + } + + // --- Reflection depth image --- + VkImageCreateInfo depthImgCI{}; + depthImgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + depthImgCI.imageType = VK_IMAGE_TYPE_2D; + depthImgCI.format = vkCtx->getDepthFormat(); + depthImgCI.extent = {REFLECTION_WIDTH, REFLECTION_HEIGHT, 1}; + depthImgCI.mipLevels = 1; + depthImgCI.arrayLayers = 1; + depthImgCI.samples = VK_SAMPLE_COUNT_1_BIT; + depthImgCI.tiling = VK_IMAGE_TILING_OPTIMAL; + depthImgCI.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + depthImgCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + if (vmaCreateImage(allocator, &depthImgCI, &allocCI, + &reflectionDepthImage, &reflectionDepthAlloc, nullptr) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create reflection depth image"); + return; + } + + VkImageViewCreateInfo depthViewCI{}; + depthViewCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + depthViewCI.image = reflectionDepthImage; + depthViewCI.viewType = VK_IMAGE_VIEW_TYPE_2D; + depthViewCI.format = vkCtx->getDepthFormat(); + depthViewCI.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + if (vkCreateImageView(device, &depthViewCI, nullptr, &reflectionDepthView) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create reflection depth view"); + return; + } + + // --- Reflection sampler --- + VkSamplerCreateInfo sampCI{}; + sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sampCI.magFilter = VK_FILTER_LINEAR; + sampCI.minFilter = VK_FILTER_LINEAR; + sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + if (vkCreateSampler(device, &sampCI, nullptr, &reflectionSampler) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create reflection sampler"); + return; + } + + // --- Reflection render pass --- + VkAttachmentDescription colorAttach{}; + colorAttach.format = vkCtx->getSwapchainFormat(); + colorAttach.samples = VK_SAMPLE_COUNT_1_BIT; + colorAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + colorAttach.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + colorAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + colorAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + colorAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + colorAttach.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkAttachmentDescription depthAttach{}; + depthAttach.format = vkCtx->getDepthFormat(); + depthAttach.samples = VK_SAMPLE_COUNT_1_BIT; + depthAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + depthAttach.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + depthAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + depthAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + depthAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + depthAttach.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = &depthRef; + + VkSubpassDependency dep{}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dep.srcAccessMask = 0; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + std::array attachments = {colorAttach, depthAttach}; + VkRenderPassCreateInfo rpCI{}; + rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpCI.attachmentCount = static_cast(attachments.size()); + rpCI.pAttachments = attachments.data(); + rpCI.subpassCount = 1; + rpCI.pSubpasses = &subpass; + rpCI.dependencyCount = 1; + rpCI.pDependencies = &dep; + + if (vkCreateRenderPass(device, &rpCI, nullptr, &reflectionRenderPass) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create reflection render pass"); + return; + } + + // --- Reflection framebuffer --- + std::array fbAttach = {reflectionColorView, reflectionDepthView}; + VkFramebufferCreateInfo fbCI{}; + fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbCI.renderPass = reflectionRenderPass; + fbCI.attachmentCount = static_cast(fbAttach.size()); + fbCI.pAttachments = fbAttach.data(); + fbCI.width = REFLECTION_WIDTH; + fbCI.height = REFLECTION_HEIGHT; + fbCI.layers = 1; + + if (vkCreateFramebuffer(device, &fbCI, nullptr, &reflectionFramebuffer) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create reflection framebuffer"); + return; + } + + // --- Reflection UBO --- + VkBufferCreateInfo bufCI{}; + bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufCI.size = sizeof(ReflectionUBOData); + bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + + VmaAllocationCreateInfo uboAllocCI{}; + uboAllocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + uboAllocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VmaAllocationInfo mapInfo{}; + if (vmaCreateBuffer(allocator, &bufCI, &uboAllocCI, + &reflectionUBO, &reflectionUBOAlloc, &mapInfo) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create reflection UBO"); + return; + } + reflectionUBOMapped = mapInfo.pMappedData; + + // Initialize with identity + ReflectionUBOData initData{}; + initData.reflViewProj = glm::mat4(1.0f); + if (reflectionUBOMapped) { + std::memcpy(reflectionUBOMapped, &initData, sizeof(initData)); + } + + // Transition reflection color image to shader-read so first frame doesn't read undefined + vkCtx->immediateSubmit([&](VkCommandBuffer cmd) { + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = reflectionColorImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + }); + reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + LOG_INFO("Water reflection resources created (", REFLECTION_WIDTH, "x", REFLECTION_HEIGHT, ")"); +} + +void WaterRenderer::destroyReflectionResources() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + VmaAllocator allocator = vkCtx->getAllocator(); + + if (reflectionFramebuffer) { vkDestroyFramebuffer(device, reflectionFramebuffer, nullptr); reflectionFramebuffer = VK_NULL_HANDLE; } + if (reflectionRenderPass) { vkDestroyRenderPass(device, reflectionRenderPass, nullptr); reflectionRenderPass = VK_NULL_HANDLE; } + if (reflectionColorView) { vkDestroyImageView(device, reflectionColorView, nullptr); reflectionColorView = VK_NULL_HANDLE; } + if (reflectionDepthView) { vkDestroyImageView(device, reflectionDepthView, nullptr); reflectionDepthView = VK_NULL_HANDLE; } + if (reflectionColorImage) { vmaDestroyImage(allocator, reflectionColorImage, reflectionColorAlloc); reflectionColorImage = VK_NULL_HANDLE; } + if (reflectionDepthImage) { vmaDestroyImage(allocator, reflectionDepthImage, reflectionDepthAlloc); reflectionDepthImage = VK_NULL_HANDLE; } + if (reflectionSampler) { vkDestroySampler(device, reflectionSampler, nullptr); reflectionSampler = VK_NULL_HANDLE; } + if (reflectionUBO) { + AllocatedBuffer ab{}; ab.buffer = reflectionUBO; ab.allocation = reflectionUBOAlloc; + destroyBuffer(allocator, ab); + reflectionUBO = VK_NULL_HANDLE; + reflectionUBOMapped = nullptr; + } + reflectionColorLayout = VK_IMAGE_LAYOUT_UNDEFINED; +} + +// ============================================================== +// Reflection pass begin/end +// ============================================================== + +bool WaterRenderer::beginReflectionPass(VkCommandBuffer cmd) { + if (!reflectionRenderPass || !reflectionFramebuffer || !cmd) return false; + + VkRenderPassBeginInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpInfo.renderPass = reflectionRenderPass; + rpInfo.framebuffer = reflectionFramebuffer; + rpInfo.renderArea = {{0, 0}, {REFLECTION_WIDTH, REFLECTION_HEIGHT}}; + + VkClearValue clears[2]{}; + clears[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + clears[1].depthStencil = {1.0f, 0}; + rpInfo.clearValueCount = 2; + rpInfo.pClearValues = clears; + + vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); + + VkViewport vp{0, 0, static_cast(REFLECTION_WIDTH), static_cast(REFLECTION_HEIGHT), 0.0f, 1.0f}; + vkCmdSetViewport(cmd, 0, 1, &vp); + VkRect2D sc{{0, 0}, {REFLECTION_WIDTH, REFLECTION_HEIGHT}}; + vkCmdSetScissor(cmd, 0, 1, &sc); + + return true; +} + +void WaterRenderer::endReflectionPass(VkCommandBuffer cmd) { + if (!cmd) return; + vkCmdEndRenderPass(cmd); + reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + // Update scene descriptor set with the freshly rendered reflection texture + if (sceneSet && reflectionColorView && reflectionSampler) { + VkDescriptorImageInfo reflInfo{}; + reflInfo.sampler = reflectionSampler; + reflInfo.imageView = reflectionColorView; + reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = sceneSet; + write.dstBinding = 2; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &reflInfo; + vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr); + } +} + +void WaterRenderer::updateReflectionUBO(const glm::mat4& reflViewProj) { + if (!reflectionUBOMapped) return; + ReflectionUBOData data{}; + data.reflViewProj = reflViewProj; + std::memcpy(reflectionUBOMapped, &data, sizeof(data)); +} + +// ============================================================== +// Mirror camera computations +// ============================================================== + +std::optional WaterRenderer::getDominantWaterHeight(const glm::vec3& cameraPos) const { + if (surfaces.empty()) return std::nullopt; + + // Find the water surface closest to the camera (XY distance) + float bestDist = std::numeric_limits::max(); + float bestHeight = 0.0f; + bool found = false; + + for (const auto& surface : surfaces) { + // Skip magma/slime — only reflect water/ocean + uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4); + if (basicType >= 2) continue; + + // Compute center of surface in world space + glm::vec3 center = surface.origin + + surface.stepX * (static_cast(surface.width) * 0.5f) + + surface.stepY * (static_cast(surface.height) * 0.5f); + + float dx = cameraPos.x - center.x; + float dy = cameraPos.y - center.y; + float dist = dx * dx + dy * dy; + if (dist < bestDist) { + bestDist = dist; + bestHeight = surface.minHeight; + found = true; + } + } + + if (!found) return std::nullopt; + return bestHeight; +} + +glm::mat4 WaterRenderer::computeReflectedView(const Camera& camera, float waterHeight) { + // In this engine, Z is up. Water height is stored in the Z component. + // Mirror camera position across Z = waterHeight plane. + + glm::vec3 camPos = camera.getPosition(); + glm::vec3 reflPos = camPos; + reflPos.z = 2.0f * waterHeight - camPos.z; + + // Get camera forward and reflect the Z component + glm::vec3 forward = camera.getForward(); + forward.z = -forward.z; + glm::vec3 reflTarget = reflPos + forward; + + glm::vec3 up(0.0f, 0.0f, 1.0f); + return glm::lookAt(reflPos, reflTarget, up); +} + +glm::mat4 WaterRenderer::computeObliqueProjection(const glm::mat4& proj, const glm::mat4& view, + float waterHeight) { + // Clip plane: everything below waterHeight in world space + // Z is up, so the clip plane normal is (0, 0, 1) + glm::vec4 clipPlaneWorld(0.0f, 0.0f, 1.0f, -waterHeight); + glm::vec4 clipPlaneView = glm::transpose(glm::inverse(view)) * clipPlaneWorld; + + // Lengyel's oblique near-plane projection matrix modification + glm::mat4 result = proj; + glm::vec4 q; + q.x = (glm::sign(clipPlaneView.x) + result[2][0]) / result[0][0]; + q.y = (glm::sign(clipPlaneView.y) + result[2][1]) / result[1][1]; + q.z = -1.0f; + q.w = (1.0f + result[2][2]) / result[3][2]; + + glm::vec4 c = clipPlaneView * (2.0f / glm::dot(clipPlaneView, q)); + result[0][2] = c.x; + result[1][2] = c.y; + result[2][2] = c.z + 1.0f; + result[3][2] = c.w; + + return result; +} + +// ============================================================== +// Separate 1x water pass (used when MSAA is active) +// ============================================================== + +bool WaterRenderer::createWater1xPass(VkFormat colorFormat, VkFormat depthFormat) { + if (!vkCtx) return false; + VkDevice device = vkCtx->getDevice(); + + VkAttachmentDescription attachments[2]{}; + // Color: load existing resolved content, store after water draw + attachments[0].format = colorFormat; + attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + // Depth: load resolved depth for depth testing + attachments[1].format = depthFormat; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference colorRef{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkAttachmentReference depthRef{1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &colorRef; + subpass.pDepthStencilAttachment = &depthRef; + + VkSubpassDependency dep{}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT; + + VkRenderPassCreateInfo rpCI{}; + rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + rpCI.attachmentCount = 2; + rpCI.pAttachments = attachments; + rpCI.subpassCount = 1; + rpCI.pSubpasses = &subpass; + rpCI.dependencyCount = 1; + rpCI.pDependencies = &dep; + + if (vkCreateRenderPass(device, &rpCI, nullptr, &water1xRenderPass) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create 1x water render pass"); + return false; + } + + // Build 1x water pipeline against this render pass + VkShaderModule vertShader, fragShader; + if (!vertShader.loadFromFile(device, "assets/shaders/water.vert.spv") || + !fragShader.loadFromFile(device, "assets/shaders/water.frag.spv")) { + LOG_ERROR("WaterRenderer: failed to load shaders for 1x pipeline"); + return false; + } + + VkVertexInputBindingDescription vertBinding{}; + vertBinding.binding = 0; + vertBinding.stride = 8 * sizeof(float); + vertBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector vertAttribs = { + { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, + { 1, 0, VK_FORMAT_R32G32_SFLOAT, 6 * sizeof(float) }, + }; + + water1xPipeline = PipelineBuilder() + .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({ vertBinding }, vertAttribs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(PipelineBuilder::blendAlpha()) + .setMultisample(VK_SAMPLE_COUNT_1_BIT) + .setLayout(pipelineLayout) + .setRenderPass(water1xRenderPass) + .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) + .build(device); + + vertShader.destroy(); + fragShader.destroy(); + + if (!water1xPipeline) { + LOG_ERROR("WaterRenderer: failed to create 1x water pipeline"); + return false; + } + + LOG_INFO("WaterRenderer: created 1x water pass and pipeline"); + return true; +} + +void WaterRenderer::createWater1xFramebuffers(const std::vector& swapViews, + VkImageView depthView, VkExtent2D extent) { + if (!vkCtx || !water1xRenderPass || !depthView) return; + VkDevice device = vkCtx->getDevice(); + + // Destroy old framebuffers + for (auto fb : water1xFramebuffers) { + if (fb) vkDestroyFramebuffer(device, fb, nullptr); + } + water1xFramebuffers.clear(); + + water1xFramebuffers.resize(swapViews.size()); + for (size_t i = 0; i < swapViews.size(); i++) { + VkImageView views[2] = { swapViews[i], depthView }; + VkFramebufferCreateInfo fbCI{}; + fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbCI.renderPass = water1xRenderPass; + fbCI.attachmentCount = 2; + fbCI.pAttachments = views; + fbCI.width = extent.width; + fbCI.height = extent.height; + fbCI.layers = 1; + if (vkCreateFramebuffer(device, &fbCI, nullptr, &water1xFramebuffers[i]) != VK_SUCCESS) { + LOG_ERROR("WaterRenderer: failed to create 1x framebuffer ", i); + } + } +} + +void WaterRenderer::destroyWater1xResources() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + for (auto fb : water1xFramebuffers) { + if (fb) vkDestroyFramebuffer(device, fb, nullptr); + } + water1xFramebuffers.clear(); + if (water1xPipeline) { vkDestroyPipeline(device, water1xPipeline, nullptr); water1xPipeline = VK_NULL_HANDLE; } + if (water1xRenderPass) { vkDestroyRenderPass(device, water1xRenderPass, nullptr); water1xRenderPass = VK_NULL_HANDLE; } +} + +bool WaterRenderer::beginWater1xPass(VkCommandBuffer cmd, uint32_t imageIndex, VkExtent2D extent) { + if (!water1xRenderPass || imageIndex >= water1xFramebuffers.size() || !water1xFramebuffers[imageIndex]) + return false; + + VkRenderPassBeginInfo rpBI{}; + rpBI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpBI.renderPass = water1xRenderPass; + rpBI.framebuffer = water1xFramebuffers[imageIndex]; + rpBI.renderArea = {{0, 0}, extent}; + rpBI.clearValueCount = 0; + rpBI.pClearValues = nullptr; + vkCmdBeginRenderPass(cmd, &rpBI, VK_SUBPASS_CONTENTS_INLINE); + + VkViewport vp{0, 0, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + vkCmdSetViewport(cmd, 0, 1, &vp); + VkRect2D sc{{0, 0}, extent}; + vkCmdSetScissor(cmd, 0, 1, &sc); + + return true; +} + +void WaterRenderer::endWater1xPass(VkCommandBuffer cmd) { + vkCmdEndRenderPass(cmd); +} + } // namespace rendering } // namespace wowee