ADD: Clipmap shadow

This commit is contained in:
2025-10-22 23:30:30 +09:00
parent fd4fe52744
commit e03ea17158
3 changed files with 54 additions and 91 deletions

View File

@@ -30,16 +30,19 @@ const vec2 POISSON_16[16] = vec2[16](
vec2(0.1197, 0.0779), vec2(-0.0905, -0.1203) 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) uint selectCascadeIndex(vec3 worldPos)
{ {
// Compute view-space positive depth for (uint i = 0u; i < 4u; ++i)
vec4 vpos = sceneData.view * vec4(worldPos, 1.0); {
float depthVS = -vpos.z; vec4 lclip = sceneData.lightViewProjCascades[i] * vec4(worldPos, 1.0);
// Near/simple map covers [0, sceneData.cascadeSplitsView.x) vec3 ndc = lclip.xyz / max(lclip.w, 1e-6);
if (depthVS < sceneData.cascadeSplitsView.x) return 0u; if (abs(ndc.x) <= 1.0 && abs(ndc.y) <= 1.0 && ndc.z >= 0.0 && ndc.z <= 1.0)
if (depthVS < sceneData.cascadeSplitsView.y) return 1u; {
if (depthVS < sceneData.cascadeSplitsView.z) return 2u; return i;
return 3u; // last cascade extends to w }
}
return 3u; // fallback to farthest level
} }
float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)

View File

@@ -17,3 +17,11 @@ inline constexpr float kShadowMapResolution = 2048.0f;
inline constexpr float kShadowCascadeRadiusScale = 2.5f; inline constexpr float kShadowCascadeRadiusScale = 2.5f;
// Additive XY margin in world units (light-space) beyond scaled radius // Additive XY margin in world units (light-space) beyond scaled radius
inline constexpr float kShadowCascadeRadiusMargin = 40.0f; inline constexpr float kShadowCascadeRadiusMargin = 40.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 = 40.0f;
// Pullback distance of the light eye from the clipmap center along the light direction (world units)
inline constexpr float kShadowClipLightPullback = 80.0f;
// Additional Z padding for the orthographic frustum along light direction
inline constexpr float kShadowClipZPadding = 80.0f;

View File

@@ -104,11 +104,9 @@ void SceneManager::update_scene()
sceneData.proj = projection; sceneData.proj = projection;
sceneData.viewproj = projection * view; sceneData.viewproj = projection * view;
// Mixed Near + CSM shadow setup // Clipmap shadow setup (directional). Each level i covers a square region
// - Cascade 0: legacy/simple shadow (near range around camera) // around the camera in the light's XY plane with radius R_i = R0 * 2^i.
// - Cascades 1..N-1: cascaded shadow maps covering farther ranges up to kShadowCSMFar // The region center is snapped to the light-space texel grid for stability.
// ---- Mixed Near + CSM shadow setup (fixed) ----
{ {
const glm::mat4 invView = glm::inverse(view); const glm::mat4 invView = glm::inverse(view);
const glm::vec3 camPos = glm::vec3(invView[3]); const glm::vec3 camPos = glm::vec3(invView[3]);
@@ -119,96 +117,50 @@ void SceneManager::update_scene()
glm::vec3 right = glm::cross(L, worldUp); glm::vec3 right = glm::cross(L, worldUp);
if (glm::length2(right) < 1e-6f) right = glm::vec3(1, 0, 0); if (glm::length2(right) < 1e-6f) right = glm::vec3(1, 0, 0);
right = glm::normalize(right); right = glm::normalize(right);
glm::vec3 up = glm::normalize(glm::cross(right, L)); { glm::vec3 up = glm::normalize(glm::cross(right, L));
const float orthoRange = 10.0f;
const float nearDist = 0.1f;
const float farDist = 200.0f;
const glm::vec3 lightPos = camPos - L * 50.0f;
const glm::mat4 viewLight = glm::lookAtRH(lightPos, camPos, up);
const glm::mat4 projLight = glm::orthoRH_ZO(-orthoRange, orthoRange, -orthoRange, orthoRange, auto level_radius = [](int level) {
nearDist, farDist); return kShadowClipBaseRadius * powf(2.0f, float(level));
const glm::mat4 lightVP = projLight * viewLight;
sceneData.lightViewProj = lightVP;
sceneData.lightViewProjCascades[0] = lightVP;
}
const float farView = kShadowCSMFar;
const float nearSplit = 5.0f;
const float lambda = 1.0f;
float splits[3]{};
for (int i = 1; i <= 3; ++i)
{
float si = float(i) / 3.0f;
float logd = nearSplit * powf(farView / nearSplit, si);
float lind = glm::mix(nearSplit, farView, si);
splits[i - 1] = glm::mix(lind, logd, lambda);
}
sceneData.cascadeSplitsView = glm::vec4(nearSplit, splits[0], splits[1], farView);
auto frustum_corners_world = [&](float zn, float zf) {
const float tanHalfFov = tanf(fov * 0.5f);
const float yN = tanHalfFov * zn;
const float xN = yN * aspect;
const float yF = tanHalfFov * zf;
const float xF = yF * aspect;
glm::vec3 vs[8] = {
{-xN, -yN, -zn}, {+xN, -yN, -zn}, {+xN, +yN, -zn}, {-xN, +yN, -zn},
{-xF, -yF, -zf}, {+xF, -yF, -zf}, {+xF, +yF, -zf}, {-xF, +yF, -zf}
};
std::array<glm::vec3, 8> ws{};
for (int i = 0; i < 8; ++i)
ws[i] = glm::vec3(invView * glm::vec4(vs[i], 1.0f));
return ws;
}; };
auto build_light_matrix_for_slice = [&](float zNearVS, float zFarVS) { // Keep a copy of level radii in cascadeSplitsView for debug/visualization
auto ws = frustum_corners_world(zNearVS, zFarVS); sceneData.cascadeSplitsView = glm::vec4(
level_radius(0), level_radius(1), level_radius(2), level_radius(3));
glm::vec3 center(0.0f); for (int ci = 0; ci < kShadowCascadeCount; ++ci)
for (auto &p: ws) center += p; {
center *= (1.0f / 8.0f); const float radius = level_radius(ci);
const float lightPullback = 20.0f;
glm::mat4 V = glm::lookAtRH(center - L * lightPullback, center, up);
glm::vec3 minLS(FLT_MAX), maxLS(-FLT_MAX); // Compute camera coordinates in light's orthonormal basis (world -> light XY)
for (auto &p: ws) const float u = glm::dot(camPos, right);
{ const float v = glm::dot(camPos, up);
glm::vec3 q = glm::vec3(V * glm::vec4(p, 1.0f));
minLS = glm::min(minLS, q);
maxLS = glm::max(maxLS, q);
}
glm::vec2 extXY = glm::vec2(maxLS.x - minLS.x, maxLS.y - minLS.y);
float radius = 0.5f * glm::max(extXY.x, extXY.y);
radius = radius * kShadowCascadeRadiusScale + kShadowCascadeRadiusMargin;
glm::vec2 centerXY = 0.5f * glm::vec2(maxLS.x + minLS.x, maxLS.y + minLS.y);
// Texel size in light-space at this level
const float texel = (2.0f * radius) / float(kShadowMapResolution); const float texel = (2.0f * radius) / float(kShadowMapResolution);
centerXY.x = floorf(centerXY.x / texel) * texel; const float uSnapped = floorf(u / texel) * texel;
centerXY.y = floorf(centerXY.y / texel) * texel; const float vSnapped = floorf(v / texel) * texel;
const float du = uSnapped - u;
const float dv = vSnapped - v;
glm::mat4 Vsnapped = glm::translate(glm::mat4(1.0f), // World-space snapped center of this clip level
-glm::vec3(centerXY.x, centerXY.y, 0.0f)) * V; const glm::vec3 center = camPos + right * du + up * dv;
const float zPad = 50.0f; // Build light view matrix looking at the snapped center
float zNear = glm::max(0.1f, -maxLS.z - zPad); const glm::vec3 eye = center - L * kShadowClipLightPullback;
float zFar = -minLS.z + zPad; const glm::mat4 V = glm::lookAtRH(eye, center, up);
glm::mat4 P = glm::orthoRH_ZO(-radius, radius, -radius, radius, zNear, zFar); // Conservative Z range along light direction
const float zNear = 0.1f;
const float zFar = kShadowClipLightPullback + kShadowClipZPadding;
return P * Vsnapped; const glm::mat4 P = glm::orthoRH_ZO(-radius, radius, -radius, radius, zNear, zFar);
}; const glm::mat4 lightVP = P * V;
float prev = nearSplit; sceneData.lightViewProjCascades[ci] = lightVP;
for (int ci = 1; ci < kShadowCascadeCount; ++ci) if (ci == 0)
{ {
float end = (ci < kShadowCascadeCount - 1) ? splits[ci - 1] : farView; sceneData.lightViewProj = lightVP;
sceneData.lightViewProjCascades[ci] = build_light_matrix_for_slice(prev, end); }
prev = end;
} }
} }