mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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:
parent
67e63653a4
commit
03a62526e1
11 changed files with 1306 additions and 115 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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;
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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<CharacterPreview*> activePreviews_;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<VkPipelineColorBlendAttachmentState> colorBlendAttachments_;
|
||||
VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
|
||||
VkRenderPass renderPass_ = VK_NULL_HANDLE;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <memory>
|
||||
#include <optional>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#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 {
|
||||
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<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,
|
||||
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<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; }
|
||||
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<VkFramebuffer> water1xFramebuffers;
|
||||
|
||||
std::vector<WaterSurface> surfaces;
|
||||
bool renderingEnabled = true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<GPUPerFrameData*>(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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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<VkDescriptorPoolSize, 2> 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<uint32_t>(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<VkWriteDescriptorSet, 2> 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<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);
|
||||
|
||||
// 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<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);
|
||||
|
||||
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 {
|
||||
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<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 wowee
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue