From 85d93fbd672ea7b3ab341a29b45c179f75a05782 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Sun, 26 Oct 2025 06:57:13 +0900 Subject: [PATCH] ADD: Clipmap shadow better quality --- shaders/deferred_lighting.frag | 70 +++++++++++++++++++++++++---- src/core/config.h | 27 +++++++++-- src/render/vk_renderpass_shadow.cpp | 5 ++- src/scene/vk_scene.cpp | 25 ++++++----- 4 files changed, 100 insertions(+), 27 deletions(-) diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 0060746..8fafac4 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -10,6 +10,16 @@ layout(set=1, binding=1) uniform sampler2D normalTex; layout(set=1, binding=2) uniform sampler2D albedoTex; layout(set=2, binding=0) uniform sampler2D shadowTex[4]; +// Tunables for shadow quality and blending +// Border smoothing width in light-space NDC (0..1). Larger = wider cross-fade. +const float SHADOW_BORDER_SMOOTH_NDC = 0.08; +// Base PCF radius in texels for cascade 0; higher cascades scale this up slightly. +const float SHADOW_PCF_BASE_RADIUS = 1.35; +// Additional per-cascade radius scale for coarser cascades (0..1 factor added across levels) +const float SHADOW_PCF_CASCADE_GAIN = 2.0; // extra radius at far end +// Receiver normal-based offset to reduce acne (in world units) +const float SHADOW_NORMAL_OFFSET = 0.0025; + const float PI = 3.14159265359; float hash12(vec2 p) @@ -29,24 +39,54 @@ vec2(0.0281, -0.2468), vec2(-0.2104, 0.0573), vec2(0.1197, 0.0779), vec2(-0.0905, -0.1203) ); -// Clipmap selection: choose the smallest level whose light-space XY NDC contains the point. -uint selectCascadeIndex(vec3 worldPos) +// Compute primary cascade and an optional neighbor for cross-fade near borders +struct CascadeMix { uint i0; uint i1; float w1; }; + +CascadeMix computeCascadeMix(vec3 worldPos) { + uint primary = 3u; + vec3 ndcP = vec3(0); for (uint i = 0u; i < 4u; ++i) { vec4 lclip = sceneData.lightViewProjCascades[i] * vec4(worldPos, 1.0); vec3 ndc = lclip.xyz / max(lclip.w, 1e-6); if (abs(ndc.x) <= 1.0 && abs(ndc.y) <= 1.0 && ndc.z >= 0.0 && ndc.z <= 1.0) { - return i; + primary = i; + ndcP = ndc; + break; } } - return 3u; + + CascadeMix cm; cm.i0 = primary; cm.i1 = primary; cm.w1 = 0.0; + + if (primary < 3u) + { + float edge = max(abs(ndcP.x), abs(ndcP.y)); // 0..1, 1 at border + // start blending when we are within S of the border + float t = clamp((edge - (1.0 - SHADOW_BORDER_SMOOTH_NDC)) / max(SHADOW_BORDER_SMOOTH_NDC, 1e-4), 0.0, 1.0); + float w = smoothstep(0.0, 1.0, t); + + if (w > 0.0) + { + // Only blend if neighbor actually covers the point + uint neighbor = primary + 1u; + vec4 lclipN = sceneData.lightViewProjCascades[neighbor] * vec4(worldPos, 1.0); + vec3 ndcN = lclipN.xyz / max(lclipN.w, 1e-6); + bool insideN = (abs(ndcN.x) <= 1.0 && abs(ndcN.y) <= 1.0 && ndcN.z >= 0.0 && ndcN.z <= 1.0); + if (insideN) + { + cm.i1 = neighbor; + cm.w1 = w; + } + } + } + + return cm; } -float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) +float sampleCascadeShadow(uint ci, vec3 worldPos, vec3 N, vec3 L) { - uint ci = selectCascadeIndex(worldPos); mat4 lightMat = sceneData.lightViewProjCascades[ci]; vec4 lclip = lightMat * vec4(worldPos, 1.0); @@ -69,9 +109,8 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) ivec2 dim = textureSize(shadowTex[ci], 0); vec2 texelSize = 1.0 / vec2(dim); - float baseRadius = 1.25; - - float radius = mix(baseRadius, baseRadius * 3.0, float(ci) / 3.0); + float baseRadius = SHADOW_PCF_BASE_RADIUS; + float radius = mix(baseRadius, baseRadius + SHADOW_PCF_CASCADE_GAIN, float(ci) / 3.0); float ang = hash12(suv * 4096.0) * 6.2831853; vec2 r = vec2(cos(ang), sin(ang)); @@ -100,6 +139,19 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) return visibility; } +float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) +{ + vec3 wp = worldPos + N * SHADOW_NORMAL_OFFSET * (0.5 + 0.5 * (1.0 - max(dot(N, L), 0.0))); + + CascadeMix cm = computeCascadeMix(wp); + float v0 = sampleCascadeShadow(cm.i0, wp, N, L); + if (cm.w1 <= 0.0) + return v0; + + float v1 = sampleCascadeShadow(cm.i1, wp, N, L); + return mix(v0, v1, clamp(cm.w1, 0.0, 1.0)); +} + vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); diff --git a/src/core/config.h b/src/core/config.h index 6aab173..62a18de 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -13,10 +13,29 @@ inline constexpr int kShadowCascadeCount = 4; inline constexpr float kShadowCSMFar = 800.0f; // Shadow map resolution used for stabilization (texel snapping). Must match actual image size. inline constexpr float kShadowMapResolution = 2048.0f; +// Extra XY expansion for cascade footprint (safety against FOV/aspect changes) +inline constexpr float kShadowCascadeRadiusScale = 1.1f; +// Additive XY margin in world units beyond the scaled half-size +inline constexpr float kShadowCascadeRadiusMargin = 10.0f; // Clipmap shadow configuration (used when cascades operate in clipmap mode) // Base coverage radius of level 0 around the camera (world units). Each level doubles the radius. -inline constexpr float kShadowClipBaseRadius = 20.0f; -// Pullback distance of the light eye from the clipmap center along the light direction (world units) -inline constexpr float kShadowClipLightPullback = 160.0f; +inline constexpr float kShadowClipBaseRadius = 30.0f; +// When using dynamic pullback, compute it from the covered XY range of each level. +// pullback = max(kShadowClipPullbackMin, cover * kShadowClipPullbackFactor) +inline constexpr float kShadowClipPullbackFactor = 1.5f; // fraction of XY half-size behind center +inline constexpr float kShadowClipForwardFactor = 1.5f; // fraction of XY half-size in front of center for zFar +inline constexpr float kShadowClipPullbackMin = 10.0f; // lower bound on pullback so near levels don’t collapse // Additional Z padding for the orthographic frustum along light direction -inline constexpr float kShadowClipZPadding = 80.0f; +inline constexpr float kShadowClipZPadding = 40.0f; + +// Shadow quality & filtering +// Soft cross-fade band between cascades in light-space NDC (0..1) +inline constexpr float kShadowBorderSmoothNDC = 0.08f; +// Base PCF radius in texels for cascade 0; higher cascades scale up slightly +inline constexpr float kShadowPCFBaseRadius = 1.35f; +// Additional radius added by the farthest cascade (0..+) +inline constexpr float kShadowPCFCascadeGain = 2.0f; + +// Raster depth-bias parameters for shadow map rendering (tuned conservatively) +inline constexpr float kShadowDepthBiasConstant = 1.25f; +inline constexpr float kShadowDepthBiasSlope = 1.5f; diff --git a/src/render/vk_renderpass_shadow.cpp b/src/render/vk_renderpass_shadow.cpp index 4309261..0173a28 100644 --- a/src/render/vk_renderpass_shadow.cpp +++ b/src/render/vk_renderpass_shadow.cpp @@ -17,6 +17,7 @@ #include "core/asset_manager.h" #include "render/vk_pipelines.h" #include "core/vk_types.h" +#include "core/config.h" void ShadowPass::init(EngineContext *context) { @@ -51,8 +52,8 @@ void ShadowPass::init(EngineContext *context) // Static depth bias to help with surface acne (tune later) b._rasterizer.depthBiasEnable = VK_TRUE; - b._rasterizer.depthBiasConstantFactor = 2.0f; - b._rasterizer.depthBiasSlopeFactor = 2.0f; + b._rasterizer.depthBiasConstantFactor = kShadowDepthBiasConstant; + b._rasterizer.depthBiasSlopeFactor = kShadowDepthBiasSlope; b._rasterizer.depthBiasClamp = 0.0f; }; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 830e5e2..d8f7ae9 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -113,6 +113,7 @@ void SceneManager::update_scene() { const glm::mat4 invView = glm::inverse(view); const glm::vec3 camPos = glm::vec3(invView[3]); + const glm::vec3 camFwd = -glm::vec3(invView[2]); glm::vec3 L = glm::normalize(-glm::vec3(sceneData.sunlightDirection)); if (glm::length(L) < 1e-5f) L = glm::vec3(0.0f, -1.0f, 0.0f); @@ -126,37 +127,37 @@ void SceneManager::update_scene() return kShadowClipBaseRadius * powf(2.0f, float(level)); }; - // Keep a copy of level radii in cascadeSplitsView for debug/visualization sceneData.cascadeSplitsView = glm::vec4( level_radius(0), level_radius(1), level_radius(2), level_radius(3)); for (int ci = 0; ci < kShadowCascadeCount; ++ci) { const float radius = level_radius(ci); + const float cover = radius * kShadowCascadeRadiusScale + kShadowCascadeRadiusMargin; - // Compute camera coordinates in light's orthonormal basis (world -> light XY) - const float u = glm::dot(camPos, right); - const float v = glm::dot(camPos, up); + const float ahead = radius * 0.5; + const float fu = glm::dot(camFwd, right); + const float fv = glm::dot(camFwd, up); - // Texel size in light-space at this level - const float texel = (2.0f * radius) / float(kShadowMapResolution); + const float u = glm::dot(camPos, right) + fu * ahead; + const float v = glm::dot(camPos, up) + fv * ahead; + + const float texel = (2.0f * cover) / float(kShadowMapResolution); const float uSnapped = floorf(u / texel) * texel; const float vSnapped = floorf(v / texel) * texel; const float du = uSnapped - u; const float dv = vSnapped - v; - // World-space snapped center of this clip level const glm::vec3 center = camPos + right * du + up * dv; - // Build light view matrix looking at the snapped center - const glm::vec3 eye = center - L * kShadowClipLightPullback; + const float pullback = glm::max(kShadowClipPullbackMin, cover * kShadowClipPullbackFactor); + const glm::vec3 eye = center - L * pullback; const glm::mat4 V = glm::lookAtRH(eye, center, up); - // Conservative Z range along light direction const float zNear = 0.1f; - const float zFar = kShadowClipLightPullback + kShadowClipZPadding; + const float zFar = pullback + cover * kShadowClipForwardFactor + kShadowClipZPadding; - const glm::mat4 P = glm::orthoRH_ZO(-radius, radius, -radius, radius, zNear, zFar); + const glm::mat4 P = glm::orthoRH_ZO(-cover, cover, -cover, cover, zNear, zFar); const glm::mat4 lightVP = P * V; sceneData.lightViewProjCascades[ci] = lightVP;