Smooth fadeout for dynamic shadows (#2203)

* Implement smooth fadeout of dynamic shadows

* Add edge fade out configuration for dynamic shadows
This commit is contained in:
Giorgio Mendieta
2026-07-13 00:11:41 +02:00
committed by GitHub
parent 6a79bf1e79
commit da38a1d2d3
2 changed files with 24 additions and 3 deletions
+12 -1
View File
@@ -21,6 +21,7 @@ struct Uniforms {
bias: f32, // shadow-map depth bias (reversed-depth units)
size: vec2f, // shadow map size in texels
inv_size: vec2f,
edge_fade_width: f32,
strength: f32, // final darkening amount, horizon fade baked in
pcf_taps: f32, // 0 = single tap, 1 = 3x3, 2 = 5x5
contact_enabled: f32,
@@ -28,7 +29,6 @@ struct Uniforms {
contact_length: f32, // view-space march distance
debug_mode: u32, // 0 = composite; nonzero modes are diagnostic views
_pad0: f32,
_pad1: f32,
}
@group(0) @binding(0) var scene_depth: texture_2d<f32>;
@@ -92,6 +92,16 @@ fn sample_shadow_pcf(light_uv: vec2f, receiver: f32) -> f32 {
return sum / count;
}
// Softly fades shadows out over a small band near the shadow-map edge so receivers do not
// disappear abruptly when they leave the light's coverage area.
fn shadow_edge_fade(light_uv: vec2f) -> f32 {
let edge_texels = uniforms.edge_fade_width;
let edge_uv = edge_texels * max(uniforms.inv_size.x, uniforms.inv_size.y);
let distance_to_edge = min(min(light_uv.x, 1.0 - light_uv.x), min(light_uv.y, 1.0 - light_uv.y));
// Avoid division by zero when the fade width is zero (no fade).
return select(1.0, saturate(distance_to_edge / edge_uv), edge_uv > 0.0);
}
fn scene_depth_at(uv: vec2f) -> f32 {
let size = vec2<i32>(textureDimensions(scene_depth));
let texel = clamp(vec2<i32>(uv * vec2f(size)), vec2<i32>(0i), size - 1i);
@@ -240,6 +250,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
var occlusion = 0.0;
if in_shadow_bounds {
occlusion = sample_shadow_pcf(light_uv, receiver);
occlusion *= shadow_edge_fade(light_uv);
}
if uniforms.debug_mode == 3u {
+12 -2
View File
@@ -57,6 +57,7 @@ ConfigVarHandle g_cvarStrength = 0;
ConfigVarHandle g_cvarPcf = 0;
ConfigVarHandle g_cvarBias = 0;
ConfigVarHandle g_cvarBoxRadius = 0;
ConfigVarHandle g_cvarEdgeFadeWidth = 0;
ConfigVarHandle g_cvarContactShadows = 0;
ConfigVarHandle g_cvarDebugView = 0;
@@ -107,6 +108,7 @@ struct ShadowUniforms {
float bias;
float size[2];
float inv_size[2];
float edge_fade_width;
float strength;
float pcf_taps;
float contact_enabled;
@@ -114,7 +116,6 @@ struct ShadowUniforms {
float contact_length;
uint32_t debug_mode;
float _pad0;
float _pad1;
};
static_assert(sizeof(ShadowUniforms) % 16 == 0);
@@ -792,6 +793,8 @@ void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
uniforms.size[1] = static_cast<float>(mapPass.mapSize);
uniforms.inv_size[0] = 1.0f / uniforms.size[0];
uniforms.inv_size[1] = 1.0f / uniforms.size[1];
uniforms.edge_fade_width =
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarEdgeFadeWidth, 32), 0, 128));
uniforms.strength =
mapPass.fade *
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarStrength, 45), 0, 100)) /
@@ -867,6 +870,8 @@ ModResult build_controls_tab(
"dynamic shadows. This can be expensive.");
add_number(left, "Coverage", g_cvarBoxRadius, 1000, 20000, 500, nullptr,
"Radius of the shadowed area around the camera, in world units. Smaller is sharper.");
add_number(left, "Fade Out", g_cvarEdgeFadeWidth, 0, 128, 32, " texels",
"Fade out shadows gradually near the edge of the coverage area.");
svc_ui->pane_add_section(mod_ctx, left, "Appearance");
add_number(left, "Strength", g_cvarStrength, 0, 100, 5, "%", "How dark shadowed areas become.");
@@ -992,6 +997,10 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
if (result != MOD_OK) {
return result;
}
result = register_int_option("edgeFadeWidth", 32, g_cvarEdgeFadeWidth, error);
if (result != MOD_OK) {
return result;
}
result = register_bool_option("contactShadows", true, g_cvarContactShadows, error);
if (result != MOD_OK) {
return result;
@@ -1087,7 +1096,8 @@ MOD_EXPORT ModResult mod_shutdown(ModError*) {
}
g_cvarEnabled = g_cvarMapSize = g_cvarNoFrustumClipping = 0;
g_cvarStrength = 0;
g_cvarPcf = g_cvarBias = g_cvarBoxRadius = g_cvarContactShadows = g_cvarDebugView = 0;
g_cvarPcf = g_cvarBias = g_cvarBoxRadius = g_cvarEdgeFadeWidth = g_cvarContactShadows =
g_cvarDebugView = 0;
g_drawType = g_sceneBeginHook = g_sceneAfterTerrainHook = g_frameBeforeHudHook = 0;
g_controlsWindow = 0;
g_mapPass = {};