From 98fb6b47daa8e096aede6555bddb58ec5c36a6e9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Feb 2026 04:14:27 -0800 Subject: [PATCH] Add 9-tap PCF soft shadows and normal-offset bias to all fragment shaders Replaces single-tap shadow sampling with 3x3 grid PCF (36-sample equivalent with hardware bilinear filtering) for smooth shadow edges. Adds normal-offset bias to eliminate shadow acne on oblique surfaces without peter-panning. --- assets/shaders/character.frag.glsl | 18 ++++++++++++++++-- assets/shaders/m2.frag.glsl | 18 ++++++++++++++++-- assets/shaders/terrain.frag.glsl | 19 +++++++++++++++++-- assets/shaders/wmo.frag.glsl | 18 ++++++++++++++++-- 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/assets/shaders/character.frag.glsl b/assets/shaders/character.frag.glsl index b096ce76..b28fa314 100644 --- a/assets/shaders/character.frag.glsl +++ b/assets/shaders/character.frag.glsl @@ -43,6 +43,18 @@ layout(location = 4) in vec3 Bitangent; layout(location = 0) out vec4 outColor; +const float SHADOW_TEXEL = 1.0 / 4096.0; + +float sampleShadowPCF(sampler2DShadow smap, vec3 coords) { + float shadow = 0.0; + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z)); + } + } + return shadow / 9.0; +} + // LOD factor from screen-space UV derivatives float computeLodFactor() { vec2 dx = dFdx(TexCoord); @@ -149,14 +161,16 @@ void main() { float shadow = 1.0; if (shadowParams.x > 0.5) { - vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); + float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir))); + vec3 biasedPos = FragPos + norm * normalOffset; + vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0); vec3 proj = lsPos.xyz / lsPos.w; proj.xy = proj.xy * 0.5 + 0.5; if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z >= 0.0 && proj.z <= 1.0) { float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005); - shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); + shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias)); } shadow = mix(1.0, shadow, shadowParams.y); } diff --git a/assets/shaders/m2.frag.glsl b/assets/shaders/m2.frag.glsl index d85455c1..9e12129e 100644 --- a/assets/shaders/m2.frag.glsl +++ b/assets/shaders/m2.frag.glsl @@ -37,6 +37,18 @@ layout(location = 4) in float ModelHeight; layout(location = 0) out vec4 outColor; +const float SHADOW_TEXEL = 1.0 / 4096.0; + +float sampleShadowPCF(sampler2DShadow smap, vec3 coords) { + float shadow = 0.0; + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z)); + } + } + return shadow / 9.0; +} + // 4x4 Bayer dither matrix (normalized to 0..1) float bayerDither4x4(ivec2 p) { int idx = (p.x & 3) + (p.y & 3) * 4; @@ -126,14 +138,16 @@ void main() { spec = pow(max(dot(norm, halfDir), 0.0), 32.0) * specularIntensity; if (shadowParams.x > 0.5) { - vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); + float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir))); + vec3 biasedPos = FragPos + norm * normalOffset; + vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0); vec3 proj = lsPos.xyz / lsPos.w; proj.xy = proj.xy * 0.5 + 0.5; if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z >= 0.0 && proj.z <= 1.0) { float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005); - shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); + shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias)); } shadow = mix(1.0, shadow, shadowParams.y); } diff --git a/assets/shaders/terrain.frag.glsl b/assets/shaders/terrain.frag.glsl index 21d51e38..96aa2c57 100644 --- a/assets/shaders/terrain.frag.glsl +++ b/assets/shaders/terrain.frag.glsl @@ -37,6 +37,18 @@ layout(location = 3) in vec2 LayerUV; layout(location = 0) out vec4 outColor; +const float SHADOW_TEXEL = 1.0 / 4096.0; + +float sampleShadowPCF(sampler2DShadow smap, vec3 coords) { + float shadow = 0.0; + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z)); + } + } + return shadow / 9.0; +} + float sampleAlpha(sampler2D tex, vec2 uv) { vec2 edge = min(uv, 1.0 - uv); float border = min(edge.x, edge.y); @@ -80,12 +92,15 @@ void main() { float shadow = 1.0; if (shadowParams.x > 0.5) { - vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); + vec3 ldir = normalize(-lightDir.xyz); + float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir))); + vec3 biasedPos = FragPos + norm * normalOffset; + vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0); vec3 proj = lsPos.xyz / lsPos.w; proj.xy = proj.xy * 0.5 + 0.5; if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z <= 1.0) { float bias = 0.0002; - shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); + shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias)); shadow = mix(1.0, shadow, shadowParams.y); } } diff --git a/assets/shaders/wmo.frag.glsl b/assets/shaders/wmo.frag.glsl index aaffef29..705ecd47 100644 --- a/assets/shaders/wmo.frag.glsl +++ b/assets/shaders/wmo.frag.glsl @@ -43,6 +43,18 @@ layout(location = 5) in vec3 Bitangent; layout(location = 0) out vec4 outColor; +const float SHADOW_TEXEL = 1.0 / 4096.0; + +float sampleShadowPCF(sampler2DShadow smap, vec3 coords) { + float shadow = 0.0; + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + shadow += texture(smap, vec3(coords.xy + vec2(x, y) * SHADOW_TEXEL, coords.z)); + } + } + return shadow / 9.0; +} + // LOD factor from screen-space UV derivatives float computeLodFactor() { vec2 dx = dFdx(TexCoord); @@ -155,14 +167,16 @@ void main() { float shadow = 1.0; if (shadowParams.x > 0.5) { - vec4 lsPos = lightSpaceMatrix * vec4(FragPos, 1.0); + float normalOffset = SHADOW_TEXEL * 2.0 * (1.0 - abs(dot(norm, ldir))); + vec3 biasedPos = FragPos + norm * normalOffset; + vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0); vec3 proj = lsPos.xyz / lsPos.w; proj.xy = proj.xy * 0.5 + 0.5; if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z >= 0.0 && proj.z <= 1.0) { float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005); - shadow = texture(uShadowMap, vec3(proj.xy, proj.z - bias)); + shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias)); } shadow = mix(1.0, shadow, shadowParams.y); }