From 4a479364147d36a0f207402dd2ad9ee7492ee8cf Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Tue, 28 Oct 2025 13:38:32 +0900 Subject: [PATCH] ADD: Receiver plane depth bias --- shaders/deferred_lighting.frag | 53 +++++++++++++++++++++++++++++----- src/core/config.h | 2 +- src/scene/vk_scene.cpp | 2 +- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 8fafac4..f2b739d 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -19,6 +19,10 @@ const float SHADOW_PCF_BASE_RADIUS = 1.35; 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; +// Scale for receiver-plane depth bias term (tweak if over/under biased) +const float SHADOW_RPDB_SCALE = 1.0; +// Minimum clamp to keep a tiny bias even on perpendicular receivers +const float SHADOW_MIN_BIAS = 1e-5; const float PI = 3.14159265359; @@ -85,6 +89,35 @@ CascadeMix computeCascadeMix(vec3 worldPos) return cm; } +// Compute receiver-plane depth gradient dz/duv using derivatives of shadow NDC +// Reference: Akenine-Möller et al., "Receiver Plane Depth Bias" (PCF-friendly) +vec2 receiverPlaneDepthGradient(vec3 ndc, vec3 dndc_dx, vec3 dndc_dy) +{ + // Convert XY to shadow map UV derivatives (ndc -> uv: u = 0.5*x + 0.5) + vec2 duv_dx = 0.5 * dndc_dx.xy; + vec2 duv_dy = 0.5 * dndc_dy.xy; + + // Build Jacobian J = [du/dx du/dy; dv/dx dv/dy] (column-major) + mat2 J = mat2(duv_dx.x, duv_dy.x, + duv_dx.y, duv_dy.y); + + // Depth derivatives w.r.t screen pixels + vec2 dz_dxdy = vec2(dndc_dx.z, dndc_dy.z); + + // Invert J to obtain dz/du and dz/dv. Guard against near-singular Jacobian. + float det = J[0][0] * J[1][1] - J[1][0] * J[0][1]; + if (abs(det) < 1e-8) + { + // Degenerate mapping; return zero gradient so only slope/const bias applies + return vec2(0.0); + } + + // Manual inverse for stability/perf on some drivers + mat2 invJ = (1.0 / det) * mat2( J[1][1], -J[0][1], + -J[1][0], J[0][0]); + return invJ * dz_dxdy; // (dz/du, dz/dv) +} + float sampleCascadeShadow(uint ci, vec3 worldPos, vec3 N, vec3 L) { mat4 lightMat = sceneData.lightViewProjCascades[ci]; @@ -98,13 +131,14 @@ float sampleCascadeShadow(uint ci, vec3 worldPos, vec3 N, vec3 L) float current = clamp(ndc.z, 0.0, 1.0); + // Slope-based tiny baseline bias (cheap safety net) float NoL = max(dot(N, L), 0.0); - float slopeBias = max(0.0006 * (1.0 - NoL), 0.0001); + float slopeBias = max(0.0006 * (1.0 - NoL), SHADOW_MIN_BIAS); - float dzdx = dFdx(current); - float dzdy = dFdy(current); - float ddz = max(abs(dzdx), abs(dzdy)); - float bias = slopeBias + ddz * 0.75; + // Receiver-plane depth gradient in shadow UV space + vec3 dndc_dx = dFdx(ndc); + vec3 dndc_dy = dFdy(ndc); + vec2 dz_duv = receiverPlaneDepthGradient(ndc, dndc_dx, dndc_dy); ivec2 dim = textureSize(shadowTex[ci], 0); vec2 texelSize = 1.0 / vec2(dim); @@ -123,13 +157,18 @@ float sampleCascadeShadow(uint ci, vec3 worldPos, vec3 N, vec3 L) for (int i = 0; i < TAP_COUNT; ++i) { vec2 pu = rot * POISSON_16[i]; - vec2 off = pu * radius * texelSize; + vec2 off = pu * radius * texelSize; // uv-space offset of this tap float pr = length(pu); float w = 1.0 - smoothstep(0.0, 0.65, pr); float mapD = texture(shadowTex[ci], suv + off).r; - float vis = step(mapD, current + bias); + + // Receiver-plane depth bias: conservative depth delta over this tap's offset + // Approximate |Δz| ≈ |dz/du|*|Δu| + |dz/dv|*|Δv| + float rpdb = dot(abs(dz_duv), abs(off)) * SHADOW_RPDB_SCALE; + + float vis = step(mapD, current + slopeBias + rpdb); visible += vis * w; wsum += w; diff --git a/src/core/config.h b/src/core/config.h index 8d2c1bc..f62d2a1 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -19,7 +19,7 @@ inline constexpr float kShadowCascadeRadiusScale = 1.1f; 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 = 30.0f; +inline constexpr float kShadowClipBaseRadius = 20.0f; // When using dynamic pullback, compute it from the covered XY range of each level. // pullback = max(kShadowClipPullbackMin, cover * kShadowClipPullbackFactor) inline constexpr float kShadowClipPullbackFactor = 2.5f; // fraction of XY half-size behind center diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 2c10f45..a109cfd 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -157,7 +157,7 @@ void SceneManager::update_scene() const glm::vec3 eye = center - L * pullback; const glm::mat4 V = glm::lookAtRH(eye, center, up); - const float zNear = 0.1f; + const float zNear = 0.2f; const float zFar = pullback + cover * kShadowClipForwardFactor + kShadowClipZPadding; const glm::mat4 P = glm::orthoRH_ZO(-cover, cover, -cover, cover, zNear, zFar);