diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 87e1931..03bfe94 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[MAX_CASCADES]; +// 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,22 +30,32 @@ 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) +{ + 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; + } + } + return 3u; // fallback to farthest level +} + float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) { - // Choose cascade based on view-space depth - float viewDepth = - (sceneData.view * vec4(worldPos, 1.0)).z;// positive - int ci = 0; - if (viewDepth > sceneData.cascadeSplitsView.x) ci = 1; - if (viewDepth > sceneData.cascadeSplitsView.y) ci = 2; - if (viewDepth > sceneData.cascadeSplitsView.z) ci = 3; - ci = clamp(ci, 0, MAX_CASCADES-1); + uint ci = selectCascadeIndex(worldPos); + mat4 lightMat = sceneData.lightViewProjCascades[ci]; - vec4 lclip = sceneData.lightViewProjCascades[ci] * vec4(worldPos, 1.0); + 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); @@ -60,15 +71,16 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) 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) { @@ -79,19 +91,14 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) float w = 1.0 - smoothstep(0.0, 0.65, pr); float mapD = texture(shadowTex[ci], suv + off).r; + float vis = step(mapD, current + bias); - // Forward-Z depth shadow map: - // - Occluded when current + bias > mapD - // - Unoccluded otherwise - // Use step(edge, x): returns 1 when x >= edge. Make occ=1 for occluded. - float occ = step(mapD + bias, current); - - 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 50c5be2..97f9f03 100644 --- a/shaders/input_structures.glsl +++ b/shaders/input_structures.glsl @@ -6,13 +6,16 @@ layout(set = 0, binding = 0) uniform SceneData{ mat4 view; mat4 proj; mat4 viewproj; - mat4 lightViewProj; // legacy single-shadow for fallback + // Legacy single shadow matrix (used for near range in mixed mode) + mat4 lightViewProj; vec4 ambientColor; vec4 sunlightDirection; //w for sun power vec4 sunlightColor; - // CSM data - mat4 lightViewProjCascades[MAX_CASCADES]; - vec4 cascadeSplitsView; // positive view-space distances of far plane per cascade + + // 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 5d9cae2..f1dedb6 100644 --- a/shaders/shadow.vert +++ b/shaders/shadow.vert @@ -18,14 +18,16 @@ layout(buffer_reference, std430) readonly buffer VertexBuffer{ layout(push_constant) uniform PushConsts { mat4 render_matrix; VertexBuffer vertexBuffer; - uint cascadeIndex; + 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); - uint ci = min(PC.cascadeIndex, uint(MAX_CASCADES-1)); + // 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 4faeabc..2542387 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -10,10 +10,18 @@ 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 = 4096.0f; +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 = 2.5f; // Additive XY margin in world units (light-space) beyond scaled radius -inline constexpr float kShadowCascadeRadiusMargin = 10.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 = 20.0f; +// Pullback distance of the light eye from the clipmap center along the light direction (world units) +inline constexpr float kShadowClipLightPullback = 160.0f; +// Additional Z padding for the orthographic frustum along light direction +inline constexpr float kShadowClipZPadding = 80.0f; diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 9c55b82..3ee5630 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -18,6 +18,8 @@ #include "render/vk_pipelines.h" #include #include + +#include "config.h" #include "render/primitives.h" #include "vk_mem_alloc.h" @@ -126,7 +128,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("police_office.glb"); const auto structureFile = _assetManager->loadGLTF(structurePath); assert(structureFile.has_value()); @@ -317,6 +319,7 @@ void VulkanEngine::draw() RGImageHandle hGBufferAlbedo = _renderGraph->import_gbuffer_albedo(); RGImageHandle hSwapchain = _renderGraph->import_swapchain_image(swapchainImageIndex); + // Create a transient shadow depth target (fixed resolution for now) // Create transient depth targets for cascaded shadow maps const VkExtent2D shadowExtent{2048, 2048}; std::array hShadowCascades{}; diff --git a/src/core/vk_sampler_manager.cpp b/src/core/vk_sampler_manager.cpp index 4a4b8cd..25800f0 100644 --- a/src/core/vk_sampler_manager.cpp +++ b/src/core/vk_sampler_manager.cpp @@ -35,11 +35,9 @@ void SamplerManager::init(DeviceManager *deviceManager) sh.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; sh.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; sh.compareEnable = VK_FALSE; // manual PCF - // Depth shadow maps are single-level; keep base LOD only and avoid mip filtering. - sh.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; - sh.maxLod = 0.0f; sh.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; vkCreateSampler(_deviceManager->device(), &sh, nullptr, &_shadowLinearClamp); + } void SamplerManager::cleanup() @@ -56,6 +54,7 @@ void SamplerManager::cleanup() vkDestroySampler(_deviceManager->device(), _defaultSamplerLinear, nullptr); _defaultSamplerLinear = VK_NULL_HANDLE; } + if (_shadowLinearClamp) { vkDestroySampler(_deviceManager->device(), _shadowLinearClamp, nullptr); diff --git a/src/core/vk_sampler_manager.h b/src/core/vk_sampler_manager.h index f6daddc..6534687 100644 --- a/src/core/vk_sampler_manager.h +++ b/src/core/vk_sampler_manager.h @@ -15,6 +15,7 @@ public: VkSampler defaultNearest() const { return _defaultSamplerNearest; } VkSampler shadowLinearClamp() const { return _shadowLinearClamp; } + private: DeviceManager *_deviceManager = nullptr; VkSampler _defaultSamplerLinear = VK_NULL_HANDLE; diff --git a/src/render/rg_graph.cpp b/src/render/rg_graph.cpp index f78ebb6..0338e35 100644 --- a/src/render/rg_graph.cpp +++ b/src/render/rg_graph.cpp @@ -676,15 +676,17 @@ void RenderGraph::execute(VkCommandBuffer cmd) if (rec && rec->imageView != VK_NULL_HANDLE) { depthInfo = vkinit::depth_attachment_info(rec->imageView, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); - if (p.depthAttachment.clearOnLoad) - { - depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - depthInfo.clearValue = p.depthAttachment.clear; - } - else - { - depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; - } + if (p.depthAttachment.clearOnLoad) + { + depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + depthInfo.clearValue = p.depthAttachment.clear; + } + else + { + depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + } + if (p.depthAttachment.clearOnLoad) depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + else depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; if (!p.depthAttachment.store) depthInfo.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; hasDepth = true; if (rec->extent.width && rec->extent.height) chosenExtent = clamp_min(chosenExtent, rec->extent); diff --git a/src/render/vk_renderpass_lighting.h b/src/render/vk_renderpass_lighting.h index e7a1182..bd96a73 100644 --- a/src/render/vk_renderpass_lighting.h +++ b/src/render/vk_renderpass_lighting.h @@ -19,8 +19,7 @@ public: RGImageHandle drawHandle, RGImageHandle gbufferPosition, RGImageHandle gbufferNormal, - RGImageHandle gbufferAlbedo, - std::span shadowCascades); + RGImageHandle gbufferAlbedo, std::span shadowCascades); private: EngineContext *_context = nullptr; diff --git a/src/render/vk_renderpass_shadow.cpp b/src/render/vk_renderpass_shadow.cpp index cc0cf95..4309261 100644 --- a/src/render/vk_renderpass_shadow.cpp +++ b/src/render/vk_renderpass_shadow.cpp @@ -45,11 +45,11 @@ void ShadowPass::init(EngineContext *context) b.set_cull_mode(VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE); b.set_multisampling_none(); b.disable_blending(); - // Forward-Z for shadow maps only (engine uses reversed-Z elsewhere) - // We clear depth to 1.0 and use LESS_OR_EQUAL so the nearest depth wins. - b.enable_depthtest(true, VK_COMPARE_OP_LESS_OR_EQUAL); + + b.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL); b.set_depth_format(VK_FORMAT_D32_SFLOAT); + // 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; @@ -85,8 +85,7 @@ void ShadowPass::register_graph(RenderGraph *graph, std::span cas RGPassType::Graphics, [shadowDepth](RGPassBuilder &builder, EngineContext *ctx) { - // Forward-Z in shadow pass: clear depth to 1.0 (far) - VkClearValue clear{}; clear.depthStencil = {1.f, 0}; + VkClearValue clear{}; clear.depthStencil = {0.f, 0}; builder.write_depth(shadowDepth, true, clear); // Ensure index/vertex buffers are tracked as reads (like Geometry) diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index c47b60d..830e5e2 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -4,6 +4,7 @@ #include "vk_swapchain.h" #include "core/engine_context.h" +#include "core/config.h" #include "glm/gtx/transform.hpp" #include @@ -106,123 +107,64 @@ void SceneManager::update_scene() sceneData.proj = projection; sceneData.viewproj = projection * view; - // Build cascaded directional light view-projection matrices + // Clipmap shadow setup (directional). Each level i covers a square region + // around the camera in the light's XY plane with radius R_i = R0 * 2^i. + // The region center is snapped to the light-space texel grid for stability. { - using namespace glm; - const vec3 camPos = vec3(inverse(view)[3]); - // Use light-ray direction (from light to scene). - // Shaders compute per-fragment L as -sunlightDirection (vector to light). - vec3 L = normalize(vec3(sceneData.sunlightDirection)); - if (!glm::all(glm::isfinite(L)) || glm::length2(L) < 1e-10f) - L = glm::vec3(0.0f, -1.0f, 0.0f); + const glm::mat4 invView = glm::inverse(view); + const glm::vec3 camPos = glm::vec3(invView[3]); - const glm::vec3 worldUp(0, 1, 0), altUp(0, 0, 1); - glm::vec3 upPick = (std::abs(glm::dot(worldUp, L)) > 0.99f) ? altUp : worldUp; - glm::vec3 right = glm::normalize(glm::cross(upPick, L)); - glm::vec3 up = glm::normalize(glm::cross(L, right)); + 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::cross(L, worldUp); + if (glm::length2(right) < 1e-6f) right = glm::vec3(1, 0, 0); + right = glm::normalize(right); + glm::vec3 up = glm::normalize(glm::cross(right, L)); - const float csmFar = kShadowCSMFar; - const float lambda = 0.5f; - const int cascades = kShadowCascadeCount; - - float splits[4] = {0, 0, 0, 0}; - for (int i = 1; i <= cascades; ++i) - { - float p = (float) i / (float) cascades; - float logd = nearPlane * std::pow(csmFar / nearPlane, p); - float lind = nearPlane + (csmFar - nearPlane) * p; - float d = glm::mix(lind, logd, lambda); - if (i - 1 < 4) splits[i - 1] = d; - } - sceneData.cascadeSplitsView = vec4(splits[0], splits[1], splits[2], splits[3]); - - mat4 invView = inverse(view); - - float baseWorldTexel = 0.0f; - - auto buildCascade = [&](int idx, float nearD, float farD) -> mat4 { - float tanHalf = tanf(fov * 0.5f); - float yf = tanHalf * farD; - float xf = yf * aspect; - float rStable = 1.05f * sqrtf(xf * xf + yf * yf); - - float texelWorld; - if (idx == 0) - { - baseWorldTexel = (2.0f * rStable) / kShadowMapResolution; - baseWorldTexel = powf(2.0f, ceilf(log2f(baseWorldTexel))); - texelWorld = baseWorldTexel; - } - else - { - texelWorld = baseWorldTexel * (1 << idx); - rStable = 0.5f * texelWorld * kShadowMapResolution; - } - - vec3 cornersV[8]; { - float tanHalfFov = tanf(fov * 0.5f); - float yn = tanHalfFov * nearD, xn = yn * aspect; - float yf = tanHalfFov * farD, xf = yf * aspect; - cornersV[0] = {-xn, -yn, -nearD}; - cornersV[1] = {xn, -yn, -nearD}; - cornersV[2] = {xn, yn, -nearD}; - cornersV[3] = {-xn, yn, -nearD}; - cornersV[4] = {-xf, -yf, -farD}; - cornersV[5] = {xf, -yf, -farD}; - cornersV[6] = {xf, yf, -farD}; - cornersV[7] = {-xf, yf, -farD}; - } - mat4 invView = inverse(view); - vec3 cornersW[8], centerWS(0); - for (int i = 0; i < 8; ++i) - { - cornersW[i] = vec3(invView * vec4(cornersV[i], 1)); - centerWS += cornersW[i]; - } - centerWS *= 1.0f / 8.0f; - - float lightDist = rStable + 50.0f; - vec3 lightPos = centerWS - L * lightDist; - mat4 viewLight = lookAtRH(lightPos, centerWS, up); - - vec2 centerLS = vec2(viewLight * vec4(centerWS, 1)); - vec2 snapped = floor(centerLS / texelWorld) * texelWorld; - vec2 deltaLS = snapped - centerLS; - vec3 shiftWS = right * deltaLS.x + up * deltaLS.y; - vec3 centerSnapped = centerWS + shiftWS; - - lightPos = centerSnapped - L * lightDist; - viewLight = lookAtRH(lightPos, centerSnapped, up); - - float radius = ceil(rStable / texelWorld) * texelWorld; - vec2 cLS = vec2(viewLight * vec4(centerSnapped, 1)); - float left = cLS.x - radius, rightE = cLS.x + radius; - float bottom = cLS.y - radius, top = cLS.y + radius; - - float minZ = 1e9f, maxZ = -1e9f; - for (int i = 0; i < 8; ++i) - { - vec3 p = vec3(viewLight * vec4(cornersW[i], 1)); - minZ = std::min(minZ, p.z); - maxZ = std::max(maxZ, p.z); - } - float sliceLen = farD - nearD; - float zPad = std::max(10.0f, 0.2f * sliceLen); - float casterExtrude = 100.0f; - float nearLS = 0.01f; - float farLS = -minZ + zPad + casterExtrude; - - mat4 projLight = orthoRH_ZO(left, rightE, bottom, top, nearLS, farLS); - return projLight * viewLight; + auto level_radius = [](int level) { + return kShadowClipBaseRadius * powf(2.0f, float(level)); }; - for (int i = 0; i < cascades; ++i) + // 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) { - float nearD = (i == 0) ? nearPlane : splits[i - 1]; - float farD = splits[i]; - sceneData.lightViewProjCascades[i] = buildCascade(i, nearD, farD); + const float radius = level_radius(ci); + + // 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); + + // Texel size in light-space at this level + const float texel = (2.0f * radius) / 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 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 glm::mat4 P = glm::orthoRH_ZO(-radius, radius, -radius, radius, zNear, zFar); + const glm::mat4 lightVP = P * V; + + sceneData.lightViewProjCascades[ci] = lightVP; + if (ci == 0) + { + sceneData.lightViewProj = lightVP; + } } - sceneData.lightViewProj = sceneData.lightViewProjCascades[0]; } auto end = std::chrono::system_clock::now();