ADD: Clipmap shadow better quality
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
|
||||
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 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user