Kelsidavis-WoWee/assets/shaders/water.frag.glsl
Kelsi fb4ff46fe3 Fix WMO water rendering: correct MLIQ parsing, tile masking, and depth effects
- Fix MLIQ vertex stride: each vertex is 8 bytes (4 flow + 4 height), not 4
- Use MLIQ tile flags to mask out tiles with no liquid (bridges, covered areas)
- Disable wave displacement on WMO water to prevent edge slosh artifacts
- Convert screen-space depth to vertical depth for shoreline foam and water
  transparency, preventing false shoreline effects on occluding geometry
- Add underwater blue fog overlay and scene fog shift (terrain water only)
- Add getNearestWaterHeightAt to avoid false underwater detection from
  elevated WMO water surfaces
- Tint refracted scene toward water color to mask occlusion edge artifacts
- Lower WMO water by 1 unit to match terrain water level
2026-02-23 00:18:32 -08:00

346 lines
14 KiB
GLSL

#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(push_constant) uniform Push {
mat4 model;
float waveAmp;
float waveFreq;
float waveSpeed;
float liquidBasicType;
} push;
layout(set = 1, binding = 0) uniform WaterMaterial {
vec4 waterColor;
float waterAlpha;
float shimmerStrength;
float alphaScale;
};
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;
layout(location = 2) in vec2 TexCoord;
layout(location = 3) in float WaveOffset;
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) {
vec2 d1 = normalize(vec2(0.86, 0.51));
vec2 d2 = normalize(vec2(-0.47, 0.88));
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 c1 = cos(dot(p1, d1) * f1);
float c2 = cos(dot(p2, d2) * f2);
float c3 = cos(dot(p3, d3) * f3);
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;
return normalize(vec3(-dHx, -dHy, 1.0));
}
// ============================================================
// 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 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.001);
float NdotL = max(dot(norm, ldir), 0.0);
float dist = length(viewPos.xyz - FragPos);
// --- Schlick Fresnel ---
const vec3 F0 = vec3(0.02);
float fresnel = F0.x + (1.0 - F0.x) * pow(1.0 - NdotV, 5.0);
// ============================================================
// 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 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);
// Convert screen-space depth difference to approximate vertical water depth.
// depthDiff is along the view ray; multiply by the vertical component of
// the view direction so grazing angles don't falsely trigger shoreline foam
// on occluding geometry (bridges, posts) that isn't at the waterline.
float verticalFactor = abs(viewDir.z); // 1.0 looking straight down, ~0 at grazing
float verticalDepth = depthDiff * max(verticalFactor, 0.05);
// ============================================================
// 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);
}
vec3 absorbed = exp(-absorptionCoeff * verticalDepth);
// Underwater blue fog — geometry below the waterline fades to a blue haze
// with depth, masking occlusion edge artifacts and giving a natural look.
vec3 underwaterFogColor = waterColor.rgb * 0.5 + vec3(0.04, 0.10, 0.20);
float underwaterFogFade = 1.0 - exp(-verticalDepth * 0.35);
vec3 foggedScene = mix(sceneRefract, underwaterFogColor, underwaterFogFade);
vec3 shallowColor = waterColor.rgb * 1.2;
vec3 deepColor = waterColor.rgb * vec3(0.3, 0.5, 0.7);
float depthFade = 1.0 - exp(-verticalDepth * 0.15);
vec3 waterBody = mix(shallowColor, deepColor, depthFade);
vec3 refractedColor = mix(foggedScene * absorbed, waterBody, depthFade * 0.7);
if (verticalDepth < 0.01) {
float opticalDepth = 1.0 - exp(-dist * 0.004);
refractedColor = mix(foggedScene, 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
// Only on terrain water (waveAmp > 0); WMO water (canals, indoor)
// has waveAmp == 0 and should not show shoreline interaction.
// ============================================================
if (basicType < 1.5 && verticalDepth > 0.01 && push.waveAmp > 0.0) {
float foamDepthMask = 1.0 - smoothstep(0.0, 1.8, verticalDepth);
// 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, verticalDepth);
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 && push.waveAmp > 0.0) {
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.15, 0.92);
float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
color = mix(fogColor.rgb, color, fogFactor);
outColor = vec4(color, alpha);
}