diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 5466368..81ca90b 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -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) diff --git a/shaders/input_structures.glsl b/shaders/input_structures.glsl index e095044..35857b1 100644 --- a/shaders/input_structures.glsl +++ b/shaders/input_structures.glsl @@ -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{ diff --git a/shaders/shadow.vert b/shaders/shadow.vert index 8a0f5e2..f1dedb6 100644 --- a/shaders/shadow.vert +++ b/shaders/shadow.vert @@ -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; } diff --git a/src/core/config.h b/src/core/config.h index 14719ae..b60d9cd 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -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; diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 67ed08d..bce246b 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -125,7 +125,7 @@ void VulkanEngine::init() auto imguiPass = std::make_unique(); _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()); diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index bb7cbc8..b1e96e0 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -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 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();