Add player water ripples and separate 1x water pass for MSAA compatibility

Player interaction ripples: vertex shader adds radial damped-sine displacement
centered on player position, fragment shader adds matching normal perturbation
for specular highlights. Player XY packed into shadowParams.zw, ripple strength
into fogParams.w. Separate 1x render pass for water when MSAA is active to
avoid MSAA-induced darkening — water renders after main pass resolves, using
the resolved swapchain image and depth resolve target. Water 1x framebuffers
rebuilt on swapchain recreate (window resize).
This commit is contained in:
Kelsi 2026-02-22 22:34:48 -08:00
parent 67e63653a4
commit 03a62526e1
11 changed files with 1306 additions and 115 deletions

View file

@ -18,6 +18,7 @@ layout(push_constant) uniform Push {
float waveAmp; float waveAmp;
float waveFreq; float waveFreq;
float waveSpeed; float waveSpeed;
float liquidBasicType;
} push; } push;
layout(set = 1, binding = 0) uniform WaterMaterial { 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 = 0) uniform sampler2D SceneColor;
layout(set = 2, binding = 1) uniform sampler2D SceneDepth; 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 = 0) in vec3 FragPos;
layout(location = 1) in vec3 Normal; layout(location = 1) in vec3 Normal;
@ -38,85 +43,286 @@ layout(location = 4) in vec2 ScreenUV;
layout(location = 0) out vec4 outColor; layout(location = 0) out vec4 outColor;
// ============================================================
// Dual-scroll detail normals (multi-octave ripple overlay)
// ============================================================
vec3 dualScrollWaveNormal(vec2 p, float time) { vec3 dualScrollWaveNormal(vec2 p, float time) {
// Two independently scrolling octaves (normal-map style layering).
vec2 d1 = normalize(vec2(0.86, 0.51)); vec2 d1 = normalize(vec2(0.86, 0.51));
vec2 d2 = normalize(vec2(-0.47, 0.88)); vec2 d2 = normalize(vec2(-0.47, 0.88));
float f1 = 0.19; vec2 d3 = normalize(vec2(0.32, -0.95));
float f2 = 0.43; float f1 = 0.19, f2 = 0.43, f3 = 0.72;
float s1 = 0.95; float s1 = 0.95, s2 = 1.73, s3 = 2.40;
float s2 = 1.73; float a1 = 0.22, a2 = 0.10, a3 = 0.05;
float a1 = 0.26;
float a2 = 0.12;
vec2 p1 = p + d1 * (time * s1 * 4.0); vec2 p1 = p + d1 * (time * s1 * 4.0);
vec2 p2 = p + d2 * (time * s2 * 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 c1 = cos(dot(p1, d1) * f1);
float ph2 = dot(p2, d2) * f2; float c2 = cos(dot(p2, d2) * f2);
float c3 = cos(dot(p3, d3) * f3);
float c1 = cos(ph1); float dHx = c1 * d1.x * f1 * a1 + c2 * d2.x * f2 * a2 + c3 * d3.x * f3 * a3;
float c2 = cos(ph2); 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; return normalize(vec3(-dHx, -dHy, 1.0));
float dHz = c1 * d1.y * f1 * a1 + c2 * d2.y * f2 * a2; }
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() { void main() {
float time = fogParams.z; float time = fogParams.z;
float basicType = push.liquidBasicType;
vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(SceneColor, 0));
// --- Normal computation ---
vec3 meshNorm = normalize(Normal); vec3 meshNorm = normalize(Normal);
vec3 waveNorm = dualScrollWaveNormal(FragPos.xz, time); vec3 detailNorm = dualScrollWaveNormal(FragPos.xy, time);
vec3 norm = normalize(mix(meshNorm, waveNorm, 0.82)); 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 viewDir = normalize(viewPos.xyz - FragPos);
vec3 ldir = normalize(-lightDir.xyz); 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); float dist = length(viewPos.xyz - FragPos);
// Beer-Lambert style approximation from view distance. // --- Schlick Fresnel ---
float opticalDepth = 1.0 - exp(-dist * 0.0035); const vec3 F0 = vec3(0.02);
vec3 litTransmission = waterColor.rgb * (ambientColor.rgb * 0.85 + diff * lightColor.rgb * 0.55); float fresnel = F0.x + (1.0 - F0.x) * pow(1.0 - NdotV, 5.0);
vec3 absorbed = mix(litTransmission, waterColor.rgb * 0.52, opticalDepth);
absorbed += vec3(crest);
// Schlick Fresnel with water-like F0. // ============================================================
const float F0 = 0.02; // Refraction (screen-space from scene history)
float fresnel = F0 + (1.0 - F0) * pow(1.0 - ndotv, 5.0); // ============================================================
vec2 refractOffset = norm.xz * (0.012 + 0.02 * fresnel); vec2 refractOffset = norm.xy * (0.02 + 0.03 * fresnel);
vec2 refractUV = clamp(ScreenUV + refractOffset, vec2(0.001), vec2(0.999)); vec2 refractUV = clamp(screenUV + refractOffset, vec2(0.001), vec2(0.999));
vec3 sceneRefract = texture(SceneColor, refractUV).rgb; vec3 sceneRefract = texture(SceneColor, refractUV).rgb;
float sceneDepth = texture(SceneDepth, refractUV).r; float sceneDepth = texture(SceneDepth, refractUV).r;
float waterDepth = clamp((sceneDepth - gl_FragCoord.z) * 180.0, 0.0, 1.0);
float depthBlend = waterDepth; float near = 0.05;
// Fallback when sampled depth does not provide meaningful separation. float far = 30000.0;
if (sceneDepth <= gl_FragCoord.z + 1e-4) { float sceneLinDepth = linearizeDepth(sceneDepth, near, far);
depthBlend = 0.45 + opticalDepth * 0.40; 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 absorbed = exp(-absorptionCoeff * depthDiff);
vec3 refractedTint = mix(sceneRefract, absorbed, depthBlend);
vec3 specular = spec * lightColor.rgb * (0.45 + 0.75 * fresnel) vec3 shallowColor = waterColor.rgb * 1.2;
+ sparkle * lightColor.rgb * 0.30; vec3 deepColor = waterColor.rgb * vec3(0.3, 0.5, 0.7);
// Add a clear surface reflection lobe at grazing angles. float depthFade = 1.0 - exp(-depthDiff * 0.15);
vec3 envReflect = mix(fogColor.rgb, lightColor.rgb, 0.38) * vec3(0.75, 0.86, 1.0); vec3 waterBody = mix(shallowColor, deepColor, depthFade);
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);
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 *= 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); float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
color = mix(fogColor.rgb, color, fogFactor); color = mix(fogColor.rgb, color, fogFactor);

Binary file not shown.

View file

@ -18,6 +18,7 @@ layout(push_constant) uniform Push {
float waveAmp; float waveAmp;
float waveFreq; float waveFreq;
float waveSpeed; float waveSpeed;
float liquidBasicType; // 0=water, 1=ocean, 2=magma, 3=slime
} push; } push;
layout(location = 0) in vec3 aPos; layout(location = 0) in vec3 aPos;
@ -29,32 +30,132 @@ layout(location = 2) out vec2 TexCoord;
layout(location = 3) out float WaveOffset; layout(location = 3) out float WaveOffset;
layout(location = 4) out vec2 ScreenUV; layout(location = 4) out vec2 ScreenUV;
float hashGrid(vec2 p) { // --- Gerstner wave ---
return fract(sin(dot(floor(p), vec2(127.1, 311.7))) * 43758.5453); // 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() { void main() {
float time = fogParams.z; float time = fogParams.z;
vec4 worldPos = push.model * vec4(aPos, 1.0); 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 // Evaluate Gerstner waves using X,Y horizontal plane
+ sin(py * push.waveFreq * 0.7 + time * push.waveSpeed * 1.3) * 0.3 GerstnerResult waves = evaluateGerstnerWaves(
+ sin((px + py) * push.waveFreq * 0.5 + time * push.waveSpeed * 0.7) * 0.1; 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 // Apply displacement: xy = horizontal, z = vertical (up)
+ sin(py * push.waveFreq * 0.8 + time * push.waveSpeed * 1.1 + hashGrid(vec2(py, px) * 0.01) * 6.28) * 0.5; 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); // Player interaction ripples — concentric waves emanating from player position
worldPos.y += wave * push.waveAmp; vec2 playerPos = vec2(shadowParams.z, shadowParams.w);
WaveOffset = wave; 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; // Analytical normal from Gerstner tangent/binormal (cross product gives Z-up normal)
float dz = cos(py * push.waveFreq * 0.7 + time * push.waveSpeed * 1.3) * push.waveFreq * 0.7 * push.waveAmp; Normal = normalize(cross(waves.binormal, waves.tangent));
Normal = normalize(vec3(-dx, 1.0, -dz));
FragPos = worldPos.xyz; FragPos = worldPos.xyz;
TexCoord = aTexCoord; TexCoord = aTexCoord;

Binary file not shown.

View file

@ -386,9 +386,17 @@ private:
GPUPerFrameData currentFrameData{}; GPUPerFrameData currentFrameData{};
float globalTime = 0.0f; 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(); bool createPerFrameResources();
void destroyPerFrameResources(); void destroyPerFrameResources();
void updatePerFrameUBO(); void updatePerFrameUBO();
void setupWater1xPass();
void renderReflectionPass();
// Active character previews for off-screen rendering // Active character previews for off-screen rendering
std::vector<CharacterPreview*> activePreviews_; std::vector<CharacterPreview*> activePreviews_;

View file

@ -85,6 +85,8 @@ public:
return (depthResolveImage == VK_NULL_HANDLE) && (msaaSamples_ > VK_SAMPLE_COUNT_1_BIT); return (depthResolveImage == VK_NULL_HANDLE) && (msaaSamples_ > VK_SAMPLE_COUNT_1_BIT);
} }
VkFormat getDepthFormat() const { return depthFormat; } 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 // UI texture upload: creates a Vulkan texture from RGBA data and returns
// a VkDescriptorSet suitable for use as ImTextureID. // a VkDescriptorSet suitable for use as ImTextureID.

View file

@ -64,6 +64,7 @@ public:
// Multisampling // Multisampling
PipelineBuilder& setMultisample(VkSampleCountFlagBits samples); PipelineBuilder& setMultisample(VkSampleCountFlagBits samples);
PipelineBuilder& setAlphaToCoverage(bool enable);
// Pipeline layout // Pipeline layout
PipelineBuilder& setLayout(VkPipelineLayout layout); PipelineBuilder& setLayout(VkPipelineLayout layout);
@ -80,6 +81,7 @@ public:
// Common blend states // Common blend states
static VkPipelineColorBlendAttachmentState blendDisabled(); static VkPipelineColorBlendAttachmentState blendDisabled();
static VkPipelineColorBlendAttachmentState blendAlpha(); static VkPipelineColorBlendAttachmentState blendAlpha();
static VkPipelineColorBlendAttachmentState blendPremultiplied();
static VkPipelineColorBlendAttachmentState blendAdditive(); static VkPipelineColorBlendAttachmentState blendAdditive();
private: private:
@ -98,6 +100,7 @@ private:
float depthBiasConstant_ = 0.0f; float depthBiasConstant_ = 0.0f;
float depthBiasSlope_ = 0.0f; float depthBiasSlope_ = 0.0f;
VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT;
bool alphaToCoverage_ = false;
std::vector<VkPipelineColorBlendAttachmentState> colorBlendAttachments_; std::vector<VkPipelineColorBlendAttachmentState> colorBlendAttachments_;
VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
VkRenderPass renderPass_ = VK_NULL_HANDLE; VkRenderPass renderPass_ = VK_NULL_HANDLE;

View file

@ -4,6 +4,7 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <cstdint> #include <cstdint>
#include <functional>
#include <vulkan/vulkan.h> #include <vulkan/vulkan.h>
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
@ -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 { class WaterRenderer {
public: public:
@ -81,13 +83,44 @@ public:
void recreatePipelines(); 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<VkImageView>& 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, void captureSceneHistory(VkCommandBuffer cmd,
VkImage srcColorImage, VkImage srcColorImage,
VkImage srcDepthImage, VkImage srcDepthImage,
VkExtent2D srcExtent, VkExtent2D srcExtent,
bool srcDepthIsMsaa); 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<float> 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; } void setEnabled(bool enabled) { renderingEnabled = enabled; }
bool isEnabled() const { return renderingEnabled; } bool isEnabled() const { return renderingEnabled; }
@ -108,6 +141,10 @@ private:
void createSceneHistoryResources(VkExtent2D extent, VkFormat colorFormat, VkFormat depthFormat); void createSceneHistoryResources(VkExtent2D extent, VkFormat colorFormat, VkFormat depthFormat);
void destroySceneHistoryResources(); void destroySceneHistoryResources();
// Reflection pass resources
void createReflectionResources();
void destroyReflectionResources();
VkContext* vkCtx = nullptr; VkContext* vkCtx = nullptr;
// Pipeline // Pipeline
@ -131,6 +168,30 @@ private:
VkExtent2D sceneHistoryExtent = {0, 0}; VkExtent2D sceneHistoryExtent = {0, 0};
bool sceneHistoryReady = false; 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<VkFramebuffer> water1xFramebuffers;
std::vector<WaterSurface> surfaces; std::vector<WaterSurface> surfaces;
bool renderingEnabled = true; bool renderingEnabled = true;
}; };

View file

@ -394,16 +394,16 @@ bool Renderer::createPerFrameResources() {
return false; 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]{}; VkDescriptorPoolSize poolSizes[2]{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; 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].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = MAX_FRAMES; poolSizes[1].descriptorCount = MAX_FRAMES + 1;
VkDescriptorPoolCreateInfo poolInfo{}; VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; 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.poolSizeCount = 2;
poolInfo.pPoolSizes = poolSizes; poolInfo.pPoolSizes = poolSizes;
@ -472,6 +472,63 @@ bool Renderer::createPerFrameResources() {
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); 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, ")"); LOG_INFO("Per-frame Vulkan resources created (shadow map ", SHADOW_MAP_SIZE, "x", SHADOW_MAP_SIZE, ")");
return true; return true;
} }
@ -487,6 +544,11 @@ void Renderer::destroyPerFrameResources() {
perFrameUBOs[i] = VK_NULL_HANDLE; perFrameUBOs[i] = VK_NULL_HANDLE;
} }
} }
if (reflPerFrameUBO) {
vmaDestroyBuffer(vkCtx->getAllocator(), reflPerFrameUBO, reflPerFrameUBOAlloc);
reflPerFrameUBO = VK_NULL_HANDLE;
reflPerFrameUBOMapped = nullptr;
}
if (sceneDescriptorPool) { if (sceneDescriptorPool) {
vkDestroyDescriptorPool(device, sceneDescriptorPool, nullptr); vkDestroyDescriptorPool(device, sceneDescriptorPool, nullptr);
sceneDescriptorPool = VK_NULL_HANDLE; 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); 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 // Copy to current frame's mapped UBO
uint32_t frame = vkCtx->getCurrentFrame(); uint32_t frame = vkCtx->getCurrentFrame();
std::memcpy(perFrameUBOMapped[frame], &currentFrameData, sizeof(GPUPerFrameData)); std::memcpy(perFrameUBOMapped[frame], &currentFrameData, sizeof(GPUPerFrameData));
@ -777,7 +850,15 @@ void Renderer::applyMsaaChange() {
// Recreate all sub-renderer pipelines (they embed sample count from render pass) // Recreate all sub-renderer pipelines (they embed sample count from render pass)
if (terrainRenderer) terrainRenderer->recreatePipelines(); 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 (wmoRenderer) wmoRenderer->recreatePipelines();
if (m2Renderer) m2Renderer->recreatePipelines(); if (m2Renderer) m2Renderer->recreatePipelines();
if (characterRenderer) characterRenderer->recreatePipelines(); if (characterRenderer) characterRenderer->recreatePipelines();
@ -833,6 +914,15 @@ void Renderer::beginFrame() {
// Handle swapchain recreation if needed // Handle swapchain recreation if needed
if (vkCtx->isSwapchainDirty()) { if (vkCtx->isSwapchainDirty()) {
vkCtx->recreateSwapchain(window->getWidth(), window->getHeight()); 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 // Acquire swapchain image and begin command buffer
@ -870,6 +960,9 @@ void Renderer::beginFrame() {
renderShadowPass(); renderShadowPass();
} }
// Water reflection pre-pass (renders scene from mirrored camera into 512x512 texture)
renderReflectionPass();
// --- Begin main render pass (clear color + depth) --- // --- Begin main render pass (clear color + depth) ---
VkRenderPassBeginInfo rpInfo{}; VkRenderPassBeginInfo rpInfo{};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
@ -909,7 +1002,7 @@ void Renderer::beginFrame() {
void Renderer::endFrame() { void Renderer::endFrame() {
if (!vkCtx || currentCmd == VK_NULL_HANDLE) return; 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); ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), currentCmd);
vkCmdEndRenderPass(currentCmd); vkCmdEndRenderPass(currentCmd);
@ -923,6 +1016,18 @@ void Renderer::endFrame() {
vkCtx->isDepthCopySourceMsaa()); 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 // Submit and present
vkCtx->endFrame(currentCmd, currentImageIndex); vkCtx->endFrame(currentCmd, currentImageIndex);
currentCmd = VK_NULL_HANDLE; currentCmd = VK_NULL_HANDLE;
@ -3140,7 +3245,10 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
} }
// Water (transparent, after all opaques) // 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); waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime);
} }
@ -3244,6 +3352,8 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
if (!waterRenderer->initialize(vkCtx, perFrameSetLayout)) { if (!waterRenderer->initialize(vkCtx, perFrameSetLayout)) {
LOG_ERROR("Failed to initialize water renderer"); LOG_ERROR("Failed to initialize water renderer");
waterRenderer.reset(); waterRenderer.reset();
} else if (vkCtx->getMsaaSamples() != VK_SAMPLE_COUNT_1_BIT) {
setupWater1xPass();
} }
} }
@ -3688,6 +3798,87 @@ glm::mat4 Renderer::computeLightSpaceMatrix() {
return lightProj * lightView; 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() { void Renderer::renderShadowPass() {
if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return; if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return;
if (currentCmd == VK_NULL_HANDLE) return; if (currentCmd == VK_NULL_HANDLE) return;
@ -3698,7 +3889,8 @@ void Renderer::renderShadowPass() {
auto* ubo = reinterpret_cast<GPUPerFrameData*>(perFrameUBOMapped[frame]); auto* ubo = reinterpret_cast<GPUPerFrameData*>(perFrameUBOMapped[frame]);
if (ubo) { if (ubo) {
ubo->lightSpaceMatrix = lightSpaceMatrix; 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. // Barrier 1: transition shadow map into writable depth layout.

View file

@ -90,6 +90,11 @@ PipelineBuilder& PipelineBuilder::setMultisample(VkSampleCountFlagBits samples)
return *this; return *this;
} }
PipelineBuilder& PipelineBuilder::setAlphaToCoverage(bool enable) {
alphaToCoverage_ = enable;
return *this;
}
PipelineBuilder& PipelineBuilder::setLayout(VkPipelineLayout layout) { PipelineBuilder& PipelineBuilder::setLayout(VkPipelineLayout layout) {
pipelineLayout_ = layout; pipelineLayout_ = layout;
return *this; return *this;
@ -145,6 +150,7 @@ VkPipeline PipelineBuilder::build(VkDevice device) const {
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE; multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = msaaSamples_; multisampling.rasterizationSamples = msaaSamples_;
multisampling.alphaToCoverageEnable = alphaToCoverage_ ? VK_TRUE : VK_FALSE;
// Depth/stencil // Depth/stencil
VkPipelineDepthStencilStateCreateInfo depthStencil{}; VkPipelineDepthStencilStateCreateInfo depthStencil{};
@ -218,6 +224,20 @@ VkPipelineColorBlendAttachmentState PipelineBuilder::blendAlpha() {
return state; 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 PipelineBuilder::blendAdditive() {
VkPipelineColorBlendAttachmentState state{}; VkPipelineColorBlendAttachmentState state{};
state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |

View file

@ -33,7 +33,12 @@ struct WaterPushConstants {
float waveAmp; float waveAmp;
float waveFreq; float waveFreq;
float waveSpeed; 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; WaterRenderer::WaterRenderer() = default;
@ -79,7 +84,7 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay
return false; return false;
} }
// --- Scene history descriptor set layout (set 2) --- // --- Scene history + reflection descriptor set layout (set 2) ---
VkDescriptorSetLayoutBinding sceneColorBinding{}; VkDescriptorSetLayoutBinding sceneColorBinding{};
sceneColorBinding.binding = 0; sceneColorBinding.binding = 0;
sceneColorBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sceneColorBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
@ -92,20 +97,36 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay
sceneDepthBinding.descriptorCount = 1; sceneDepthBinding.descriptorCount = 1;
sceneDepthBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; 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) { if (!sceneSetLayout) {
LOG_ERROR("WaterRenderer: failed to create scene set layout"); LOG_ERROR("WaterRenderer: failed to create scene set layout");
return false; return false;
} }
VkDescriptorPoolSize scenePoolSize{}; // Pool needs 3 combined image samplers + 1 uniform buffer
scenePoolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; std::array<VkDescriptorPoolSize, 2> scenePoolSizes{};
scenePoolSize.descriptorCount = 2; 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{}; VkDescriptorPoolCreateInfo scenePoolInfo{};
scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; scenePoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
scenePoolInfo.maxSets = 1; scenePoolInfo.maxSets = 1;
scenePoolInfo.poolSizeCount = 1; scenePoolInfo.poolSizeCount = static_cast<uint32_t>(scenePoolSizes.size());
scenePoolInfo.pPoolSizes = &scenePoolSize; scenePoolInfo.pPoolSizes = scenePoolSizes.data();
if (vkCreateDescriptorPool(device, &scenePoolInfo, nullptr, &sceneDescPool) != VK_SUCCESS) { if (vkCreateDescriptorPool(device, &scenePoolInfo, nullptr, &sceneDescPool) != VK_SUCCESS) {
LOG_ERROR("WaterRenderer: failed to create scene descriptor pool"); LOG_ERROR("WaterRenderer: failed to create scene descriptor pool");
return false; return false;
@ -113,7 +134,7 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay
// --- Pipeline layout --- // --- Pipeline layout ---
VkPushConstantRange pushRange{}; 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.offset = 0;
pushRange.size = sizeof(WaterPushConstants); pushRange.size = sizeof(WaterPushConstants);
@ -124,6 +145,10 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay
return false; return false;
} }
// Create reflection resources FIRST so reflectionUBO exists when
// createSceneHistoryResources writes descriptor binding 3
createReflectionResources();
createSceneHistoryResources(vkCtx->getSwapchainExtent(), createSceneHistoryResources(vkCtx->getSwapchainExtent(),
vkCtx->getSwapchainFormat(), vkCtx->getSwapchainFormat(),
vkCtx->getDepthFormat()); vkCtx->getDepthFormat());
@ -184,6 +209,8 @@ void WaterRenderer::recreatePipelines() {
if (!vkCtx) return; if (!vkCtx) return;
VkDevice device = vkCtx->getDevice(); VkDevice device = vkCtx->getDevice();
destroyReflectionResources();
createReflectionResources();
createSceneHistoryResources(vkCtx->getSwapchainExtent(), createSceneHistoryResources(vkCtx->getSwapchainExtent(),
vkCtx->getSwapchainFormat(), vkCtx->getSwapchainFormat(),
vkCtx->getDepthFormat()); vkCtx->getDepthFormat());
@ -245,6 +272,8 @@ void WaterRenderer::shutdown() {
VkDevice device = vkCtx->getDevice(); VkDevice device = vkCtx->getDevice();
vkDeviceWaitIdle(device); vkDeviceWaitIdle(device);
destroyWater1xResources();
destroyReflectionResources();
destroySceneHistoryResources(); destroySceneHistoryResources();
if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; } if (waterPipeline) { vkDestroyPipeline(device, waterPipeline, nullptr); waterPipeline = VK_NULL_HANDLE; }
if (pipelineLayout) { vkDestroyPipelineLayout(device, pipelineLayout, nullptr); pipelineLayout = 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.imageView = sceneDepthView;
depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; depthInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
std::array<VkWriteDescriptorSet, 2> writes{}; // Reflection color texture (binding 2) — use scene color as placeholder until reflection is created
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; VkDescriptorImageInfo reflColorInfo{};
writes[0].dstSet = sceneSet; reflColorInfo.sampler = sceneColorSampler;
writes[0].dstBinding = 0; reflColorInfo.imageView = reflectionColorView ? reflectionColorView : sceneColorView;
writes[0].descriptorCount = 1; reflColorInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[0].pImageInfo = &colorInfo; // Reflection UBO (binding 3)
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; VkDescriptorBufferInfo reflUBOInfo{};
writes[1].dstSet = sceneSet; reflUBOInfo.buffer = reflectionUBO;
writes[1].dstBinding = 1; reflUBOInfo.offset = 0;
writes[1].descriptorCount = 1; reflUBOInfo.range = sizeof(ReflectionUBOData);
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[1].pImageInfo = &depthInfo; // Write bindings 0,1 always; write 2,3 only if reflection resources exist
std::vector<VkWriteDescriptorSet> 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<uint32_t>(writes.size()), writes.data(), 0, nullptr); vkUpdateDescriptorSets(device, static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
// Initialize history images to shader-read layout so first frame samples are defined. // 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, void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
const Camera& /*camera*/, float /*time*/) { const Camera& /*camera*/, float /*time*/, bool use1x) {
if (!renderingEnabled || surfaces.empty() || !waterPipeline) return; VkPipeline pipeline = (use1x && water1xPipeline) ? water1xPipeline : waterPipeline;
if (!renderingEnabled || surfaces.empty() || !pipeline) return;
if (!sceneSet) 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, vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
0, 1, &perFrameSet, 0, nullptr); 0, 1, &perFrameSet, 0, nullptr);
@ -699,17 +769,20 @@ void WaterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
if (!surface.materialSet) continue; if (!surface.materialSet) continue;
bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5); bool canalProfile = (surface.wmoId != 0) || (surface.liquidType == 5);
float waveAmp = canalProfile ? 0.04f : 0.06f; uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);
float waveFreq = canalProfile ? 0.30f : 0.22f; float waveAmp = canalProfile ? 0.10f : (basicType == 1 ? 0.35f : 0.18f);
float waveSpeed = canalProfile ? 1.20f : 2.00f; float waveFreq = canalProfile ? 0.35f : (basicType == 1 ? 0.20f : 0.30f);
float waveSpeed = canalProfile ? 1.00f : (basicType == 1 ? 1.20f : 1.40f);
WaterPushConstants push{}; WaterPushConstants push{};
push.model = glm::mat4(1.0f); push.model = glm::mat4(1.0f);
push.waveAmp = waveAmp; push.waveAmp = waveAmp;
push.waveFreq = waveFreq; push.waveFreq = waveFreq;
push.waveSpeed = waveSpeed; push.waveSpeed = waveSpeed;
push.liquidBasicType = static_cast<float>(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); 0, sizeof(WaterPushConstants), &push);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout,
@ -1101,23 +1174,548 @@ std::optional<uint16_t> WaterRenderer::getWaterTypeAt(float glX, float glY) cons
glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const { glm::vec4 WaterRenderer::getLiquidColor(uint16_t liquidType) const {
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
switch (basicType) { switch (basicType) {
case 0: 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.06f, 0.18f, 0.34f, 1.0f); 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); 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); case 3: return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f); // slime
default: return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); default: return glm::vec4(0.12f, 0.32f, 0.48f, 1.0f);
} }
} }
float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const { float WaterRenderer::getLiquidAlpha(uint16_t liquidType) const {
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4); uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
switch (basicType) { switch (basicType) {
case 1: return 0.68f; case 1: return 0.72f; // ocean
case 2: return 0.72f; case 2: return 0.75f; // magma
case 3: return 0.62f; case 3: return 0.65f; // slime
default: return 0.38f; 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<VkAttachmentDescription, 2> attachments = {colorAttach, depthAttach};
VkRenderPassCreateInfo rpCI{};
rpCI.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpCI.attachmentCount = static_cast<uint32_t>(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<VkImageView, 2> fbAttach = {reflectionColorView, reflectionDepthView};
VkFramebufferCreateInfo fbCI{};
fbCI.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbCI.renderPass = reflectionRenderPass;
fbCI.attachmentCount = static_cast<uint32_t>(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<float>(REFLECTION_WIDTH), static_cast<float>(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<float> 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<float>::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<float>(surface.width) * 0.5f) +
surface.stepY * (static_cast<float>(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<VkVertexInputAttributeDescription> 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<VkImageView>& 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<float>(extent.width), static_cast<float>(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 rendering
} // namespace wowee } // namespace wowee