ADD: CSM with base shadow map
This commit is contained in:
@@ -8,7 +8,8 @@ layout(location=0) out vec4 outColor;
|
||||
layout(set=1, binding=0) uniform sampler2D posTex;
|
||||
layout(set=1, binding=1) uniform sampler2D normalTex;
|
||||
layout(set=1, binding=2) uniform sampler2D albedoTex;
|
||||
layout(set=2, binding=0) uniform sampler2D shadowTex;
|
||||
// Mixed near + CSM: shadowTex[0] is the near/simple map, 1..N-1 are cascades
|
||||
layout(set=2, binding=0) uniform sampler2D shadowTex[4];
|
||||
|
||||
const float PI = 3.14159265359;
|
||||
|
||||
@@ -29,14 +30,29 @@ const vec2 POISSON_16[16] = vec2[16](
|
||||
vec2(0.1197, 0.0779), vec2(-0.0905, -0.1203)
|
||||
);
|
||||
|
||||
uint selectCascadeIndex(vec3 worldPos)
|
||||
{
|
||||
// Compute view-space positive depth
|
||||
vec4 vpos = sceneData.view * vec4(worldPos, 1.0);
|
||||
float depthVS = -vpos.z;
|
||||
// Near/simple map covers [0, sceneData.cascadeSplitsView.x)
|
||||
if (depthVS < sceneData.cascadeSplitsView.x) return 0u;
|
||||
if (depthVS < sceneData.cascadeSplitsView.y) return 1u;
|
||||
if (depthVS < sceneData.cascadeSplitsView.z) return 2u;
|
||||
return 3u; // last cascade extends to w
|
||||
}
|
||||
|
||||
float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
|
||||
{
|
||||
vec4 lclip = sceneData.lightViewProj * vec4(worldPos, 1.0);
|
||||
uint ci = selectCascadeIndex(worldPos);
|
||||
mat4 lightMat = sceneData.lightViewProjCascades[ci];
|
||||
|
||||
vec4 lclip = lightMat * vec4(worldPos, 1.0);
|
||||
vec3 ndc = lclip.xyz / lclip.w;
|
||||
vec2 suv = ndc.xy * 0.5 + 0.5;
|
||||
|
||||
if (any(lessThan(suv, vec2(0.0))) || any(greaterThan(suv, vec2(1.0))))
|
||||
return 1.0;
|
||||
return 1.0;
|
||||
|
||||
float current = clamp(ndc.z, 0.0, 1.0);
|
||||
|
||||
@@ -48,19 +64,20 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
|
||||
float ddz = max(abs(dzdx), abs(dzdy));
|
||||
float bias = slopeBias + ddz * 0.75;
|
||||
|
||||
ivec2 dim = textureSize(shadowTex, 0);
|
||||
ivec2 dim = textureSize(shadowTex[ci], 0);
|
||||
vec2 texelSize = 1.0 / vec2(dim);
|
||||
|
||||
float baseRadius = 1.25;
|
||||
float radius = mix(baseRadius, baseRadius * 4.0, current);
|
||||
// Slightly increase filter for farther cascades
|
||||
float radius = mix(baseRadius, baseRadius * 3.0, float(ci) / 3.0);
|
||||
|
||||
float ang = hash12(suv * 4096.0) * 6.2831853;
|
||||
vec2 r = vec2(cos(ang), sin(ang));
|
||||
mat2 rot = mat2(r.x, -r.y, r.y, r.x);
|
||||
|
||||
const int TAP_COUNT = 16;
|
||||
float occluded = 0.0;
|
||||
float wsum = 0.0;
|
||||
float visible = 0.0;
|
||||
float wsum = 0.0;
|
||||
|
||||
for (int i = 0; i < TAP_COUNT; ++i)
|
||||
{
|
||||
@@ -70,16 +87,16 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
|
||||
float pr = length(pu);
|
||||
float w = 1.0 - smoothstep(0.0, 0.65, pr);
|
||||
|
||||
float mapD = texture(shadowTex, suv + off).r;
|
||||
float mapD = texture(shadowTex[ci], suv + off).r;
|
||||
// Reversed-Z friendly compare: visible when current <= map depth
|
||||
float vis = step(mapD, current + bias);
|
||||
|
||||
float occ = step(current + bias, mapD);
|
||||
|
||||
occluded += occ * w;
|
||||
wsum += w;
|
||||
visible += vis * w;
|
||||
wsum += w;
|
||||
}
|
||||
|
||||
float shadow = (wsum > 0.0) ? (occluded / wsum) : 0.0;
|
||||
return 1.0 - shadow;
|
||||
float visibility = (wsum > 0.0) ? (visible / wsum) : 1.0;
|
||||
return visibility;
|
||||
}
|
||||
|
||||
vec3 fresnelSchlick(float cosTheta, vec3 F0)
|
||||
|
||||
@@ -3,10 +3,16 @@ layout(set = 0, binding = 0) uniform SceneData{
|
||||
mat4 view;
|
||||
mat4 proj;
|
||||
mat4 viewproj;
|
||||
// Legacy single shadow matrix (used for near range in mixed mode)
|
||||
mat4 lightViewProj;
|
||||
vec4 ambientColor;
|
||||
vec4 sunlightDirection; //w for sun power
|
||||
vec4 sunlightColor;
|
||||
|
||||
// Cascaded shadow matrices (0 = near/simple map, 1..N-1 = CSM)
|
||||
mat4 lightViewProjCascades[4];
|
||||
// View-space split distances for selecting cascades (x,y,z,w)
|
||||
vec4 cascadeSplitsView;
|
||||
} sceneData;
|
||||
|
||||
layout(set = 1, binding = 0) uniform GLTFMaterialData{
|
||||
|
||||
@@ -18,12 +18,16 @@ layout(buffer_reference, std430) readonly buffer VertexBuffer{
|
||||
layout(push_constant) uniform PushConsts {
|
||||
mat4 render_matrix;
|
||||
VertexBuffer vertexBuffer;
|
||||
uint cascadeIndex; // which cascade this pass renders
|
||||
// pad to 16-byte boundary implicitly
|
||||
} PC;
|
||||
|
||||
void main()
|
||||
{
|
||||
Vertex v = PC.vertexBuffer.vertices[gl_VertexIndex];
|
||||
vec4 worldPos = PC.render_matrix * vec4(v.position, 1.0);
|
||||
gl_Position = sceneData.lightViewProj * worldPos;
|
||||
// Use cascaded matrix; cascade 0 is the legacy near/simple map
|
||||
uint ci = min(PC.cascadeIndex, uint(3));
|
||||
gl_Position = sceneData.lightViewProjCascades[ci] * worldPos;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ inline constexpr bool kUseValidationLayers = true;
|
||||
// Shadow mapping configuration
|
||||
inline constexpr int kShadowCascadeCount = 4;
|
||||
// Maximum shadow distance for CSM in view-space units
|
||||
inline constexpr float kShadowCSMFar = 50.0f;
|
||||
inline constexpr float kShadowCSMFar = 400.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.15f;
|
||||
inline constexpr float kShadowCascadeRadiusScale = 1.25f;
|
||||
// Additive XY margin in world units (light-space) beyond scaled radius
|
||||
inline constexpr float kShadowCascadeRadiusMargin = 10.0f;
|
||||
inline constexpr float kShadowCascadeRadiusMargin = 20.0f;
|
||||
|
||||
@@ -125,7 +125,7 @@ void VulkanEngine::init()
|
||||
auto imguiPass = std::make_unique<ImGuiPass>();
|
||||
_renderPassManager->setImGuiPass(std::move(imguiPass));
|
||||
|
||||
const std::string structurePath = _assetManager->modelPath("seoul_high.glb");
|
||||
const std::string structurePath = _assetManager->modelPath("house.glb");
|
||||
const auto structureFile = _assetManager->loadGLTF(structurePath);
|
||||
|
||||
assert(structureFile.has_value());
|
||||
|
||||
@@ -104,15 +104,16 @@ void SceneManager::update_scene()
|
||||
sceneData.proj = projection;
|
||||
sceneData.viewproj = projection * view;
|
||||
|
||||
// Build a simple directional light view-projection (reversed-Z orthographic)
|
||||
// Centered around the camera for now. For the initial CSM-plumbing test,
|
||||
// duplicate this single shadow matrix across all cascades so we render
|
||||
// four identical shadow maps. This verifies the pass/descriptor wiring.
|
||||
// Mixed Near + CSM shadow setup
|
||||
// - Cascade 0: legacy/simple shadow (near range around camera)
|
||||
// - Cascades 1..N-1: cascaded shadow maps covering farther ranges up to kShadowCSMFar
|
||||
{
|
||||
const glm::vec3 camPos = glm::vec3(glm::inverse(view)[3]);
|
||||
const glm::mat4 invView = glm::inverse(view);
|
||||
const glm::vec3 camPos = glm::vec3(invView[3]);
|
||||
|
||||
// Sun direction and light basis
|
||||
glm::vec3 L = glm::normalize(-glm::vec3(sceneData.sunlightDirection));
|
||||
if (glm::length(L) < 1e-5f) L = glm::vec3(0.0f, -1.0f, 0.0f);
|
||||
|
||||
const glm::vec3 worldUp(0.0f, 1.0f, 0.0f);
|
||||
glm::vec3 right = glm::normalize(glm::cross(worldUp, L));
|
||||
glm::vec3 up = glm::normalize(glm::cross(L, right));
|
||||
@@ -122,32 +123,111 @@ void SceneManager::update_scene()
|
||||
up = glm::normalize(glm::cross(L, right));
|
||||
}
|
||||
|
||||
const float orthoRange = 40.0f; // XY half-extent
|
||||
const float nearDist = 0.1f;
|
||||
const float farDist = 200.0f;
|
||||
const glm::vec3 lightPos = camPos - L * 100.0f;
|
||||
glm::mat4 viewLight = glm::lookAtRH(lightPos, camPos, up);
|
||||
// Standard RH ZO ortho with near<far (works with our reversed-Z depth setup
|
||||
// as we clamp and bias in shader). We'll stabilize/flip later when CSM lands.
|
||||
glm::mat4 projLight = glm::orthoRH_ZO(-orthoRange, orthoRange, -orthoRange, orthoRange,
|
||||
nearDist, farDist);
|
||||
|
||||
sceneData.lightViewProj = projLight * viewLight;
|
||||
|
||||
// Fill cascade arrays with the same matrix for now so the shadow
|
||||
// pass can run four times using identical transforms.
|
||||
for (int c = 0; c < kShadowCascadeCount; ++c)
|
||||
// 0) Legacy near/simple shadow matrix (kept for cascade 0)
|
||||
{
|
||||
sceneData.lightViewProjCascades[c] = sceneData.lightViewProj;
|
||||
const float orthoRange = 30.0f; // XY half-extent around camera
|
||||
const float nearDist = 0.1f;
|
||||
const float farDist = 150.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,
|
||||
nearDist, farDist);
|
||||
const glm::mat4 lightVP = projLight * viewLight;
|
||||
sceneData.lightViewProj = lightVP; // kept for debug/compat
|
||||
sceneData.lightViewProjCascades[0] = lightVP; // cascade 0 uses the simple map
|
||||
}
|
||||
|
||||
// Provide a simple increasing split hint (view-space distances).
|
||||
// Not used yet by shaders, but helps when we switch to CSM.
|
||||
// 1) Build cascade split distances (view-space, positive forward)
|
||||
const float farView = kShadowCSMFar;
|
||||
sceneData.cascadeSplitsView = glm::vec4(0.1f * farView,
|
||||
0.3f * farView,
|
||||
0.6f * farView,
|
||||
1.0f * farView);
|
||||
// Choose a near/CSM boundary tuned for close-up detail
|
||||
const float nearSplit = 100.0;
|
||||
// Practical split scheme for remaining 3 cascades
|
||||
const float lambda = 0.6f;
|
||||
float cStart = nearSplit;
|
||||
float splits[3]{}; // end distances for cascades 1..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);
|
||||
|
||||
// 2) For cascades 1..3, compute light-space ortho matrices that bound the camera frustum slice
|
||||
auto frustum_corners_world = [&](float zn, float zf)
|
||||
{
|
||||
// camera looks along -Z in view space
|
||||
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;
|
||||
|
||||
// view-space corners
|
||||
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)
|
||||
{
|
||||
auto ws = frustum_corners_world(zNearVS, zFarVS);
|
||||
|
||||
// Light view looking toward cascade center
|
||||
glm::vec3 center(0.0f);
|
||||
for (auto &p : ws) center += p; center *= (1.0f / 8.0f);
|
||||
glm::vec3 lightPos = center - L * 200.0f;
|
||||
glm::mat4 V = glm::lookAtRH(lightPos, center, up);
|
||||
|
||||
// Project corners to light space and compute AABB
|
||||
glm::vec3 minLS(FLT_MAX), maxLS(-FLT_MAX);
|
||||
for (auto &p : ws)
|
||||
{
|
||||
glm::vec3 q = glm::vec3(V * glm::vec4(p, 1.0f));
|
||||
minLS = glm::min(minLS, q);
|
||||
maxLS = glm::max(maxLS, q);
|
||||
}
|
||||
|
||||
// Expand XY a bit to be safe/stable
|
||||
glm::vec2 halfXY = 0.5f * glm::vec2(maxLS.x - minLS.x, maxLS.y - minLS.y);
|
||||
float radius = glm::max(halfXY.x, halfXY.y) * kShadowCascadeRadiusScale + kShadowCascadeRadiusMargin;
|
||||
glm::vec2 centerXY = 0.5f * glm::vec2(maxLS.x + minLS.x, maxLS.y + minLS.y);
|
||||
|
||||
// Optional texel snapping for stability
|
||||
const float texel = (2.0f * radius) / kShadowMapResolution;
|
||||
centerXY.x = floorf(centerXY.x / texel) * texel;
|
||||
centerXY.y = floorf(centerXY.y / texel) * texel;
|
||||
|
||||
// Compose snapped view matrix by overriding translation in light space
|
||||
glm::mat4 Vsnapped = V;
|
||||
// Extract current translation in light space for center; replace x/y with snapped center
|
||||
glm::vec3 centerLS = glm::vec3(V * glm::vec4(center, 1.0f));
|
||||
glm::vec3 delta = glm::vec3(centerXY, centerLS.z) - centerLS;
|
||||
// Apply delta in light space by post-multiplying with a translation
|
||||
Vsnapped = glm::translate(glm::mat4(1.0f), -delta) * V;
|
||||
|
||||
float nearLS = minLS.z - 50.0f; // pull near/far generously around slice depth range
|
||||
float farLS = maxLS.z + 250.0f;
|
||||
glm::mat4 P = glm::orthoRH_ZO(-radius, radius, -radius, radius, std::max(0.1f, -nearLS), std::max(10.0f, farLS - nearLS + 10.0f));
|
||||
return P * Vsnapped;
|
||||
};
|
||||
|
||||
// Fill cascades 1..3
|
||||
float prev = nearSplit;
|
||||
for (int ci = 1; ci < kShadowCascadeCount; ++ci)
|
||||
{
|
||||
float end = (ci < kShadowCascadeCount - 1) ? splits[ci - 1] : farView;
|
||||
sceneData.lightViewProjCascades[ci] = build_light_matrix_for_slice(prev, end);
|
||||
prev = end;
|
||||
}
|
||||
}
|
||||
|
||||
auto end = std::chrono::system_clock::now();
|
||||
|
||||
Reference in New Issue
Block a user