diff --git a/mods/shadow_mod/res/shadow.wgsl b/mods/shadow_mod/res/shadow.wgsl index 83065b1257..fe69b5bc3a 100644 --- a/mods/shadow_mod/res/shadow.wgsl +++ b/mods/shadow_mod/res/shadow.wgsl @@ -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; @@ -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(textureDimensions(scene_depth)); let texel = clamp(vec2(uv * vec2f(size)), vec2(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 { diff --git a/mods/shadow_mod/src/mod.cpp b/mods/shadow_mod/src/mod.cpp index df57eadcbd..0eec4fecef 100644 --- a/mods/shadow_mod/src/mod.cpp +++ b/mods/shadow_mod/src/mod.cpp @@ -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(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(std::clamp(get_int_option(g_cvarEdgeFadeWidth, 32), 0, 128)); uniforms.strength = mapPass.fade * static_cast(std::clamp(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 = {};