From 3b7f0869a2ac59811d3cf83a391871f33b793783 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Tue, 21 Oct 2025 17:36:34 +0900 Subject: [PATCH 1/7] ADD: CSM base code --- src/core/config.h | 11 +++ src/core/vk_descriptors.cpp | 4 +- src/core/vk_descriptors.h | 2 +- src/core/vk_engine.cpp | 15 +++- src/core/vk_sampler_manager.cpp | 16 ++++ src/core/vk_sampler_manager.h | 3 + src/core/vk_types.h | 3 + src/render/rg_graph.cpp | 11 ++- src/render/vk_renderpass_lighting.cpp | 44 +++++++---- src/render/vk_renderpass_lighting.h | 5 +- src/render/vk_renderpass_shadow.cpp | 107 +++++++++++++++----------- src/render/vk_renderpass_shadow.h | 12 +-- src/scene/vk_scene.cpp | 23 +++++- 13 files changed, 179 insertions(+), 77 deletions(-) diff --git a/src/core/config.h b/src/core/config.h index c1d256d..14719ae 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -6,3 +6,14 @@ inline constexpr bool kUseValidationLayers = false; #else inline constexpr bool kUseValidationLayers = true; #endif + +// Shadow mapping configuration +inline constexpr int kShadowCascadeCount = 4; +// Maximum shadow distance for CSM in view-space units +inline constexpr float kShadowCSMFar = 50.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; +// Additive XY margin in world units (light-space) beyond scaled radius +inline constexpr float kShadowCascadeRadiusMargin = 10.0f; diff --git a/src/core/vk_descriptors.cpp b/src/core/vk_descriptors.cpp index 19c49ee..1da8856 100644 --- a/src/core/vk_descriptors.cpp +++ b/src/core/vk_descriptors.cpp @@ -1,10 +1,10 @@ #include -void DescriptorLayoutBuilder::add_binding(uint32_t binding, VkDescriptorType type) +void DescriptorLayoutBuilder::add_binding(uint32_t binding, VkDescriptorType type, uint32_t count) { VkDescriptorSetLayoutBinding newbind{}; newbind.binding = binding; - newbind.descriptorCount = 1; + newbind.descriptorCount = count; newbind.descriptorType = type; bindings.push_back(newbind); diff --git a/src/core/vk_descriptors.h b/src/core/vk_descriptors.h index 474a840..ad0bbe2 100644 --- a/src/core/vk_descriptors.h +++ b/src/core/vk_descriptors.h @@ -7,7 +7,7 @@ struct DescriptorLayoutBuilder { std::vector bindings; - void add_binding(uint32_t binding, VkDescriptorType type); + void add_binding(uint32_t binding, VkDescriptorType type, uint32_t count = 1); void clear(); diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 32f6eb1..67ed08d 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -16,6 +16,8 @@ #include "render/vk_pipelines.h" #include #include + +#include "config.h" #include "render/primitives.h" #include "vk_mem_alloc.h" @@ -315,8 +317,14 @@ void VulkanEngine::draw() 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}; - RGImageHandle hShadow = _renderGraph->create_depth_image("shadow.depth", shadowExtent, VK_FORMAT_D32_SFLOAT); + std::array hShadowCascades{}; + for (int i = 0; i < kShadowCascadeCount; ++i) + { + std::string name = std::string("shadow.cascade.") + std::to_string(i); + hShadowCascades[i] = _renderGraph->create_depth_image(name.c_str(), shadowExtent, VK_FORMAT_D32_SFLOAT); + } _resourceManager->register_upload_pass(*_renderGraph, get_current_frame()); @@ -331,7 +339,7 @@ void VulkanEngine::draw() } if (auto *shadow = _renderPassManager->getPass()) { - shadow->register_graph(_renderGraph.get(), hShadow, shadowExtent); + shadow->register_graph(_renderGraph.get(), std::span(hShadowCascades.data(), hShadowCascades.size()), shadowExtent); } if (auto *geometry = _renderPassManager->getPass()) { @@ -339,7 +347,8 @@ void VulkanEngine::draw() } if (auto *lighting = _renderPassManager->getPass()) { - lighting->register_graph(_renderGraph.get(), hDraw, hGBufferPosition, hGBufferNormal, hGBufferAlbedo, hShadow); + lighting->register_graph(_renderGraph.get(), hDraw, hGBufferPosition, hGBufferNormal, hGBufferAlbedo, + std::span(hShadowCascades.data(), hShadowCascades.size())); } if (auto *transparent = _renderPassManager->getPass()) { diff --git a/src/core/vk_sampler_manager.cpp b/src/core/vk_sampler_manager.cpp index 59c1fe4..25800f0 100644 --- a/src/core/vk_sampler_manager.cpp +++ b/src/core/vk_sampler_manager.cpp @@ -28,6 +28,16 @@ void SamplerManager::init(DeviceManager *deviceManager) sampl.magFilter = VK_FILTER_LINEAR; sampl.minFilter = VK_FILTER_LINEAR; vkCreateSampler(_deviceManager->device(), &sampl, nullptr, &_defaultSamplerLinear); + + // Shadow linear clamp sampler (border=white) + VkSamplerCreateInfo sh = sampl; + sh.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + sh.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + sh.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + sh.compareEnable = VK_FALSE; // manual PCF + sh.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + vkCreateSampler(_deviceManager->device(), &sh, nullptr, &_shadowLinearClamp); + } void SamplerManager::cleanup() @@ -44,4 +54,10 @@ void SamplerManager::cleanup() vkDestroySampler(_deviceManager->device(), _defaultSamplerLinear, nullptr); _defaultSamplerLinear = VK_NULL_HANDLE; } + + if (_shadowLinearClamp) + { + vkDestroySampler(_deviceManager->device(), _shadowLinearClamp, nullptr); + _shadowLinearClamp = VK_NULL_HANDLE; + } } diff --git a/src/core/vk_sampler_manager.h b/src/core/vk_sampler_manager.h index 718f189..5c3897f 100644 --- a/src/core/vk_sampler_manager.h +++ b/src/core/vk_sampler_manager.h @@ -13,10 +13,13 @@ public: VkSampler defaultLinear() const { return _defaultSamplerLinear; } VkSampler defaultNearest() const { return _defaultSamplerNearest; } + VkSampler shadowLinearClamp() const { return _shadowLinearClamp; } + private: DeviceManager *_deviceManager = nullptr; VkSampler _defaultSamplerLinear = VK_NULL_HANDLE; VkSampler _defaultSamplerNearest = VK_NULL_HANDLE; + VkSampler _shadowLinearClamp = VK_NULL_HANDLE; }; diff --git a/src/core/vk_types.h b/src/core/vk_types.h index a82a384..39e9d5c 100644 --- a/src/core/vk_types.h +++ b/src/core/vk_types.h @@ -75,6 +75,9 @@ struct GPUSceneData { glm::vec4 ambientColor; glm::vec4 sunlightDirection; // w for sun power glm::vec4 sunlightColor; + + glm::mat4 lightViewProjCascades[4]; + glm::vec4 cascadeSplitsView; }; enum class MaterialPass :uint8_t { diff --git a/src/render/rg_graph.cpp b/src/render/rg_graph.cpp index e0693d1..0338e35 100644 --- a/src/render/rg_graph.cpp +++ b/src/render/rg_graph.cpp @@ -676,7 +676,16 @@ 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; + 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; diff --git a/src/render/vk_renderpass_lighting.cpp b/src/render/vk_renderpass_lighting.cpp index 82c5142..11af5fc 100644 --- a/src/render/vk_renderpass_lighting.cpp +++ b/src/render/vk_renderpass_lighting.cpp @@ -10,11 +10,13 @@ #include "core/vk_pipeline_manager.h" #include "core/asset_manager.h" #include "core/vk_descriptors.h" +#include "core/config.h" #include "vk_mem_alloc.h" #include "vk_sampler_manager.h" #include "vk_swapchain.h" #include "render/rg_graph.h" +#include void LightingPass::init(EngineContext *context) { @@ -43,10 +45,10 @@ void LightingPass::init(EngineContext *context) writer.update_set(_context->getDevice()->device(), _gBufferInputDescriptorSet); } - // Shadow map descriptor layout (set = 2, updated per-frame) + // Shadow map descriptor layout (set = 2, updated per-frame). Use array of cascades { DescriptorLayoutBuilder builder; - builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, kShadowCascadeCount); _shadowDescriptorLayout = builder.build(_context->getDevice()->device(), VK_SHADER_STAGE_FRAGMENT_BIT); } @@ -95,9 +97,9 @@ void LightingPass::register_graph(RenderGraph *graph, RGImageHandle gbufferPosition, RGImageHandle gbufferNormal, RGImageHandle gbufferAlbedo, - RGImageHandle shadowDepth) + std::span shadowCascades) { - if (!graph || !drawHandle.valid() || !gbufferPosition.valid() || !gbufferNormal.valid() || !gbufferAlbedo.valid() || !shadowDepth.valid()) + if (!graph || !drawHandle.valid() || !gbufferPosition.valid() || !gbufferNormal.valid() || !gbufferAlbedo.valid()) { return; } @@ -105,18 +107,21 @@ void LightingPass::register_graph(RenderGraph *graph, graph->add_pass( "Lighting", RGPassType::Graphics, - [drawHandle, gbufferPosition, gbufferNormal, gbufferAlbedo, shadowDepth](RGPassBuilder &builder, EngineContext *) + [drawHandle, gbufferPosition, gbufferNormal, gbufferAlbedo, shadowCascades](RGPassBuilder &builder, EngineContext *) { builder.read(gbufferPosition, RGImageUsage::SampledFragment); builder.read(gbufferNormal, RGImageUsage::SampledFragment); builder.read(gbufferAlbedo, RGImageUsage::SampledFragment); - builder.read(shadowDepth, RGImageUsage::SampledFragment); + for (size_t i = 0; i < shadowCascades.size(); ++i) + { + if (shadowCascades[i].valid()) builder.read(shadowCascades[i], RGImageUsage::SampledFragment); + } builder.write_color(drawHandle); }, - [this, drawHandle, shadowDepth](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) + [this, drawHandle, shadowCascades](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) { - draw_lighting(cmd, ctx, res, drawHandle, shadowDepth); + draw_lighting(cmd, ctx, res, drawHandle, shadowCascades); }); } @@ -124,7 +129,7 @@ void LightingPass::draw_lighting(VkCommandBuffer cmd, EngineContext *context, const RGPassResources &resources, RGImageHandle drawHandle, - RGImageHandle shadowDepth) + std::span shadowCascades) { EngineContext *ctxLocal = context ? context : _context; if (!ctxLocal || !ctxLocal->currentFrame) return; @@ -173,12 +178,21 @@ void LightingPass::draw_lighting(VkCommandBuffer cmd, VkDescriptorSet shadowSet = ctxLocal->currentFrame->_frameDescriptors.allocate( deviceManager->device(), _shadowDescriptorLayout); { - VkImageView shadowView = resources.image_view(shadowDepth); - DescriptorWriter writer2; - writer2.write_image(0, shadowView, ctxLocal->getSamplers()->defaultLinear(), - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); - writer2.update_set(deviceManager->device(), shadowSet); + const uint32_t cascadeCount = std::min(kShadowCascadeCount, static_cast(shadowCascades.size())); + std::array infos{}; + for (uint32_t i = 0; i < cascadeCount; ++i) + { + infos[i].sampler = ctxLocal->getSamplers()->shadowLinearClamp(); + infos[i].imageView = resources.image_view(shadowCascades[i]); + infos[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + VkWriteDescriptorSet write{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = shadowSet; + write.dstBinding = 0; + write.descriptorCount = cascadeCount; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = infos.data(); + vkUpdateDescriptorSets(deviceManager->device(), 1, &write, 0, nullptr); } vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 2, 1, &shadowSet, 0, nullptr); diff --git a/src/render/vk_renderpass_lighting.h b/src/render/vk_renderpass_lighting.h index c7ab854..ff1e29d 100644 --- a/src/render/vk_renderpass_lighting.h +++ b/src/render/vk_renderpass_lighting.h @@ -17,8 +17,7 @@ public: RGImageHandle drawHandle, RGImageHandle gbufferPosition, RGImageHandle gbufferNormal, - RGImageHandle gbufferAlbedo, - RGImageHandle shadowDepth); + RGImageHandle gbufferAlbedo, std::span shadowCascades); private: EngineContext *_context = nullptr; @@ -34,7 +33,7 @@ private: EngineContext *context, const class RGPassResources &resources, RGImageHandle drawHandle, - RGImageHandle shadowDepth); + std::span shadowCascades); DeletionQueue _deletionQueue; }; diff --git a/src/render/vk_renderpass_shadow.cpp b/src/render/vk_renderpass_shadow.cpp index cb75144..d5f68ca 100644 --- a/src/render/vk_renderpass_shadow.cpp +++ b/src/render/vk_renderpass_shadow.cpp @@ -1,6 +1,7 @@ #include "vk_renderpass_shadow.h" #include +#include #include "core/engine_context.h" #include "render/rg_graph.h" @@ -24,9 +25,13 @@ void ShadowPass::init(EngineContext *context) if (!_context || !_context->pipelines) return; // Build a depth-only graphics pipeline for shadow map rendering + // Keep push constants matching current shader layout for now VkPushConstantRange pc{}; pc.offset = 0; - pc.size = sizeof(GPUDrawPushConstants); + // Push constants layout in shadow.vert is mat4 + device address + uint, rounded to 16 bytes + const uint32_t pcRaw = static_cast(sizeof(GPUDrawPushConstants) + sizeof(uint32_t)); + const uint32_t pcAligned = (pcRaw + 15u) & ~15u; // 16-byte alignment to match std430 expectations + pc.size = pcAligned; pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; GraphicsPipelineCreateInfo info{}; @@ -40,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(); - // Reverse-Z depth test & depth-only pipeline + // Reverse-Z depth test for shadow maps (clear=0.0, GREATER_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 (will tune later) + // 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; @@ -65,53 +70,61 @@ void ShadowPass::execute(VkCommandBuffer) // Shadow rendering is done via the RenderGraph registration. } -void ShadowPass::register_graph(RenderGraph *graph, RGImageHandle shadowDepth, VkExtent2D extent) +void ShadowPass::register_graph(RenderGraph *graph, std::span cascades, VkExtent2D extent) { - if (!graph || !shadowDepth.valid()) return; + if (!graph || cascades.empty()) return; - graph->add_pass( - "ShadowMap", - RGPassType::Graphics, - [shadowDepth](RGPassBuilder &builder, EngineContext *ctx) - { - // Reverse-Z depth clear to 0.0 - VkClearValue clear{}; clear.depthStencil = {0.f, 0}; - builder.write_depth(shadowDepth, true, clear); + for (uint32_t i = 0; i < cascades.size(); ++i) + { + RGImageHandle shadowDepth = cascades[i]; + if (!shadowDepth.valid()) continue; - // Ensure index/vertex buffers are tracked as reads (like Geometry) - if (ctx) + std::string passName = std::string("ShadowMap[") + std::to_string(i) + "]"; + graph->add_pass( + passName.c_str(), + RGPassType::Graphics, + [shadowDepth](RGPassBuilder &builder, EngineContext *ctx) { - const DrawContext &dc = ctx->getMainDrawContext(); - std::unordered_set indexSet; - std::unordered_set vertexSet; - auto collect = [&](const std::vector &v) - { - for (const auto &r : v) - { - if (r.indexBuffer) indexSet.insert(r.indexBuffer); - if (r.vertexBuffer) vertexSet.insert(r.vertexBuffer); - } - }; - collect(dc.OpaqueSurfaces); - // Transparent surfaces are ignored for shadow map in this simple pass + // Reverse-Z depth clear to 0.0 + VkClearValue clear{}; clear.depthStencil = {0.f, 0}; + builder.write_depth(shadowDepth, true, clear); - for (VkBuffer b : indexSet) - builder.read_buffer(b, RGBufferUsage::IndexRead, 0, "shadow.index"); - for (VkBuffer b : vertexSet) - builder.read_buffer(b, RGBufferUsage::StorageRead, 0, "shadow.vertex"); - } - }, - [this, shadowDepth, extent](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) - { - draw_shadow(cmd, ctx, res, shadowDepth, extent); - }); + // Ensure index/vertex buffers are tracked as reads (like Geometry) + if (ctx) + { + const DrawContext &dc = ctx->getMainDrawContext(); + std::unordered_set indexSet; + std::unordered_set vertexSet; + auto collect = [&](const std::vector &v) + { + for (const auto &r : v) + { + if (r.indexBuffer) indexSet.insert(r.indexBuffer); + if (r.vertexBuffer) vertexSet.insert(r.vertexBuffer); + } + }; + collect(dc.OpaqueSurfaces); + // Ignore transparent for shadow map + + for (VkBuffer b : indexSet) + builder.read_buffer(b, RGBufferUsage::IndexRead, 0, "shadow.index"); + for (VkBuffer b : vertexSet) + builder.read_buffer(b, RGBufferUsage::StorageRead, 0, "shadow.vertex"); + } + }, + [this, shadowDepth, extent, i](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) + { + draw_shadow(cmd, ctx, res, shadowDepth, extent, i); + }); + } } void ShadowPass::draw_shadow(VkCommandBuffer cmd, EngineContext *context, const RGPassResources &/*resources*/, RGImageHandle /*shadowDepth*/, - VkExtent2D extent) const + VkExtent2D extent, + uint32_t cascadeIndex) const { EngineContext *ctxLocal = context ? context : _context; if (!ctxLocal || !ctxLocal->currentFrame) return; @@ -166,6 +179,12 @@ void ShadowPass::draw_shadow(VkCommandBuffer cmd, const DrawContext &dc = ctxLocal->getMainDrawContext(); VkBuffer lastIndexBuffer = VK_NULL_HANDLE; + + struct ShadowPC + { + GPUDrawPushConstants draw; + uint32_t cascadeIndex; + }; for (const auto &r : dc.OpaqueSurfaces) { if (r.indexBuffer != lastIndexBuffer) @@ -174,11 +193,11 @@ void ShadowPass::draw_shadow(VkCommandBuffer cmd, vkCmdBindIndexBuffer(cmd, r.indexBuffer, 0, VK_INDEX_TYPE_UINT32); } - GPUDrawPushConstants pc{}; - pc.worldMatrix = r.transform; - pc.vertexBuffer = r.vertexBufferAddress; - vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &pc); - + ShadowPC spc{}; + spc.draw.worldMatrix = r.transform; + spc.draw.vertexBuffer = r.vertexBufferAddress; + spc.cascadeIndex = cascadeIndex; + vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(ShadowPC), &spc); vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0); } } diff --git a/src/render/vk_renderpass_shadow.h b/src/render/vk_renderpass_shadow.h index af75bad..6158921 100644 --- a/src/render/vk_renderpass_shadow.h +++ b/src/render/vk_renderpass_shadow.h @@ -2,14 +2,13 @@ #include "vk_renderpass.h" #include +#include class RenderGraph; class EngineContext; class RGPassResources; -// Depth-only directional shadow map pass (skeleton) -// - Writes a depth image using reversed-Z (clear=0) -// - Draw function will be filled in a later step +// Depth-only directional shadow map pass (CSM-ready API) class ShadowPass : public IRenderPass { public: @@ -19,8 +18,8 @@ public: const char *getName() const override { return "ShadowMap"; } - // Register the depth-only pass into the render graph - void register_graph(RenderGraph *graph, RGImageHandle shadowDepth, VkExtent2D extent); + // Register N cascades; one graphics pass per cascade. + void register_graph(RenderGraph *graph, std::span cascades, VkExtent2D extent); private: EngineContext *_context = nullptr; @@ -29,5 +28,6 @@ private: EngineContext *context, const RGPassResources &resources, RGImageHandle shadowDepth, - VkExtent2D extent) const; + VkExtent2D extent, + uint32_t cascadeIndex) const; }; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 5328942..bb7cbc8 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 @@ -104,7 +105,9 @@ void SceneManager::update_scene() sceneData.viewproj = projection * view; // Build a simple directional light view-projection (reversed-Z orthographic) - // Centered around the camera for now (non-cascaded, non-stabilized) + // 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. { const glm::vec3 camPos = glm::vec3(glm::inverse(view)[3]); glm::vec3 L = glm::normalize(-glm::vec3(sceneData.sunlightDirection)); @@ -124,11 +127,27 @@ void SceneManager::update_scene() 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 Date: Tue, 21 Oct 2025 21:24:35 +0900 Subject: [PATCH 2/7] ADD: CSM with base shadow map --- shaders/deferred_lighting.frag | 45 +++++++---- shaders/input_structures.glsl | 6 ++ shaders/shadow.vert | 6 +- src/core/config.h | 6 +- src/core/vk_engine.cpp | 2 +- src/scene/vk_scene.cpp | 136 ++++++++++++++++++++++++++------- 6 files changed, 154 insertions(+), 47 deletions(-) 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(); From 4c41c93d449ed67c08625929ba505f7ff29ce122 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Wed, 22 Oct 2025 00:53:36 +0900 Subject: [PATCH 3/7] ADD: CSM with base shadow map 2 --- shaders/deferred_lighting.frag | 1 - src/core/config.h | 2 +- src/core/vk_engine.cpp | 2 +- src/render/vk_renderpass_shadow.cpp | 3 +- src/scene/vk_scene.cpp | 104 ++++++++++++++-------------- 5 files changed, 55 insertions(+), 57 deletions(-) diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 81ca90b..470e805 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -88,7 +88,6 @@ 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; - // Reversed-Z friendly compare: visible when current <= map depth float vis = step(mapD, current + bias); visible += vis * w; diff --git a/src/core/config.h b/src/core/config.h index b60d9cd..71fb9b1 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -10,7 +10,7 @@ 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 = 400.0f; +inline constexpr float kShadowCSMFar = 200.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) diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index bce246b..67ed08d 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("house.glb"); + const std::string structurePath = _assetManager->modelPath("seoul_high.glb"); const auto structureFile = _assetManager->loadGLTF(structurePath); assert(structureFile.has_value()); diff --git a/src/render/vk_renderpass_shadow.cpp b/src/render/vk_renderpass_shadow.cpp index d5f68ca..4309261 100644 --- a/src/render/vk_renderpass_shadow.cpp +++ b/src/render/vk_renderpass_shadow.cpp @@ -45,7 +45,7 @@ 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(); - // Reverse-Z depth test for shadow maps (clear=0.0, GREATER_OR_EQUAL) + b.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL); b.set_depth_format(VK_FORMAT_D32_SFLOAT); @@ -85,7 +85,6 @@ void ShadowPass::register_graph(RenderGraph *graph, std::span cas RGPassType::Graphics, [shadowDepth](RGPassBuilder &builder, EngineContext *ctx) { - // Reverse-Z depth clear to 0.0 VkClearValue clear{}; clear.depthStencil = {0.f, 0}; builder.write_depth(shadowDepth, true, clear); diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index b1e96e0..6f6d83c 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -107,44 +107,43 @@ void SceneManager::update_scene() // 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 + + // ---- Mixed Near + CSM shadow setup (fixed) ---- { const glm::mat4 invView = glm::inverse(view); const glm::vec3 camPos = glm::vec3(invView[3]); - // Sun direction and light basis + // Sun direction and light basis (robust) 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)); - if (glm::length2(right) < 1e-6f) - { - right = glm::vec3(1, 0, 0); - up = glm::normalize(glm::cross(L, right)); - } + 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)); - // 0) Legacy near/simple shadow matrix (kept for cascade 0) + // 0) Legacy near/simple shadow (cascade 0 그대로) { - const float orthoRange = 30.0f; // XY half-extent around camera + const float orthoRange = 20.0f; const float nearDist = 0.1f; - const float farDist = 150.0f; - const glm::vec3 lightPos = camPos - L * 50.0f; + const float farDist = 200.0f; + const glm::vec3 lightPos = camPos - L * 80.0f; const glm::mat4 viewLight = glm::lookAtRH(lightPos, camPos, up); + + // ⚠️ API에 맞게 ZO/NO를 고르세요 (Vulkan/D3D: ZO, OpenGL 기본: NO) 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 + sceneData.lightViewProj = lightVP; + sceneData.lightViewProjCascades[0] = lightVP; } - // 1) Build cascade split distances (view-space, positive forward) + // 1) Cascade split distances (뷰공간 +Z를 "전방 거리"로 사용) const float farView = kShadowCSMFar; - // 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 + const float nearSplit = 5.0f; // 0번(레거시)와 CSM 경계 + const float lambda = 0.7f; // practical split + float splits[3]{}; for (int i = 1; i <= 3; ++i) { float si = float(i) / 3.0f; @@ -154,73 +153,73 @@ void SceneManager::update_scene() } 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 + // 2) 뷰공간 슬라이스 [zn, zf]의 월드 코너 계산 + auto frustum_corners_world = [&](float zn, float zf) { + // 카메라는 뷰공간 -Z를 바라봄. 우리는 "전방거리"를 양수로 넣고 z는 -zn, -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; - // 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 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 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); + for (auto &p: ws) center += p; + center *= (1.0f / 8.0f); + const float lightPullback = 30.0f; // 충분히 뒤로 빼서 안정화 + glm::mat4 V = glm::lookAtRH(center - L * lightPullback, center, up); - // Project corners to light space and compute AABB + // 라이트 공간으로 투영 후 AABB glm::vec3 minLS(FLT_MAX), maxLS(-FLT_MAX); - for (auto &p : ws) + 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; + // XY 반경/센터, 살짝 여유 + 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); - // Optional texel snapping for stability - const float texel = (2.0f * radius) / kShadowMapResolution; + // Texel snapping (안정화) + const float texel = (2.0f * radius) / float(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; + // 스냅된 XY 센터를 반영하도록 라이트 뷰를 라이트공간에서 평행이동 + glm::mat4 Vsnapped = glm::translate(glm::mat4(1.0f), + -glm::vec3(centerXY.x, centerXY.y, 0.0f)) * V; + + // 깊이 범위(표준 Z, reversed-Z 안 씀) + // lookAtRH는 -Z 쪽을 앞(카메라 전방)으로 둔다: 가까운 점 z는 덜 음수(값이 큰 쪽), 먼 점은 더 음수(값이 작은 쪽) + const float zPad = 50.0f; // 슬라이스 앞뒤 여유 + float zNear = glm::max(0.1f, -maxLS.z - zPad); // "가까움": -z(덜음수) → 양수 거리 + float zFar = -minLS.z + zPad; // "멀리": -z(더음수) → 더 큰 양수 + + // ⚠️ API에 맞게 ZO/NO를 선택 + glm::mat4 P = glm::orthoRH_ZO(-radius, radius, -radius, radius, zNear, zFar); - 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 + // 3) Cascades 1..3 채우기 float prev = nearSplit; for (int ci = 1; ci < kShadowCascadeCount; ++ci) { @@ -230,6 +229,7 @@ void SceneManager::update_scene() } } + auto end = std::chrono::system_clock::now(); auto elapsed = std::chrono::duration_cast(end - start); stats.scene_update_time = elapsed.count() / 1000.f; From 7edcfe1a206d97a0be109d5d6ad886d529ddcc69 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Wed, 22 Oct 2025 10:23:43 +0900 Subject: [PATCH 4/7] ADD: CSM with normal shadow --- src/core/config.h | 6 +++--- src/scene/vk_scene.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/config.h b/src/core/config.h index 71fb9b1..d7d01dc 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 = 200.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.25f; +inline constexpr float kShadowCascadeRadiusScale = 2.5f; // Additive XY margin in world units (light-space) beyond scaled radius -inline constexpr float kShadowCascadeRadiusMargin = 20.0f; +inline constexpr float kShadowCascadeRadiusMargin = 40.0f; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 6f6d83c..b680ebc 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -124,10 +124,10 @@ void SceneManager::update_scene() // 0) Legacy near/simple shadow (cascade 0 그대로) { - const float orthoRange = 20.0f; + const float orthoRange = 10.0f; const float nearDist = 0.1f; const float farDist = 200.0f; - const glm::vec3 lightPos = camPos - L * 80.0f; + const glm::vec3 lightPos = camPos - L * 180.0f; const glm::mat4 viewLight = glm::lookAtRH(lightPos, camPos, up); // ⚠️ API에 맞게 ZO/NO를 고르세요 (Vulkan/D3D: ZO, OpenGL 기본: NO) @@ -179,7 +179,7 @@ void SceneManager::update_scene() glm::vec3 center(0.0f); for (auto &p: ws) center += p; center *= (1.0f / 8.0f); - const float lightPullback = 30.0f; // 충분히 뒤로 빼서 안정화 + const float lightPullback = 50.0f; // 충분히 뒤로 빼서 안정화 glm::mat4 V = glm::lookAtRH(center - L * lightPullback, center, up); // 라이트 공간으로 투영 후 AABB From fd4fe52744e51ae3b25d4de4e501c861f5d7650e Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Wed, 22 Oct 2025 22:26:50 +0900 Subject: [PATCH 5/7] ADD: stabilized CSM 2 --- src/scene/vk_scene.cpp | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index b680ebc..7933910 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -113,24 +113,19 @@ void SceneManager::update_scene() const glm::mat4 invView = glm::inverse(view); const glm::vec3 camPos = glm::vec3(invView[3]); - // Sun direction and light basis (robust) 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)); - - // 0) Legacy near/simple shadow (cascade 0 그대로) - { + 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 * 180.0f; + const glm::vec3 lightPos = camPos - L * 50.0f; const glm::mat4 viewLight = glm::lookAtRH(lightPos, camPos, up); - // ⚠️ API에 맞게 ZO/NO를 고르세요 (Vulkan/D3D: ZO, OpenGL 기본: NO) const glm::mat4 projLight = glm::orthoRH_ZO(-orthoRange, orthoRange, -orthoRange, orthoRange, nearDist, farDist); @@ -139,10 +134,9 @@ void SceneManager::update_scene() sceneData.lightViewProjCascades[0] = lightVP; } - // 1) Cascade split distances (뷰공간 +Z를 "전방 거리"로 사용) const float farView = kShadowCSMFar; - const float nearSplit = 5.0f; // 0번(레거시)와 CSM 경계 - const float lambda = 0.7f; // practical split + const float nearSplit = 5.0f; + const float lambda = 1.0f; float splits[3]{}; for (int i = 1; i <= 3; ++i) { @@ -153,9 +147,7 @@ void SceneManager::update_scene() } sceneData.cascadeSplitsView = glm::vec4(nearSplit, splits[0], splits[1], farView); - // 2) 뷰공간 슬라이스 [zn, zf]의 월드 코너 계산 auto frustum_corners_world = [&](float zn, float zf) { - // 카메라는 뷰공간 -Z를 바라봄. 우리는 "전방거리"를 양수로 넣고 z는 -zn, -zf. const float tanHalfFov = tanf(fov * 0.5f); const float yN = tanHalfFov * zn; const float xN = yN * aspect; @@ -175,14 +167,12 @@ void SceneManager::update_scene() auto build_light_matrix_for_slice = [&](float zNearVS, float zFarVS) { auto ws = frustum_corners_world(zNearVS, zFarVS); - // 라이트 뷰: 슬라이스 센터를 본다 glm::vec3 center(0.0f); for (auto &p: ws) center += p; center *= (1.0f / 8.0f); - const float lightPullback = 50.0f; // 충분히 뒤로 빼서 안정화 + const float lightPullback = 20.0f; glm::mat4 V = glm::lookAtRH(center - L * lightPullback, center, up); - // 라이트 공간으로 투영 후 AABB glm::vec3 minLS(FLT_MAX), maxLS(-FLT_MAX); for (auto &p: ws) { @@ -191,35 +181,28 @@ void SceneManager::update_scene() maxLS = glm::max(maxLS, q); } - // XY 반경/센터, 살짝 여유 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 snapping (안정화) const float texel = (2.0f * radius) / float(kShadowMapResolution); centerXY.x = floorf(centerXY.x / texel) * texel; centerXY.y = floorf(centerXY.y / texel) * texel; - // 스냅된 XY 센터를 반영하도록 라이트 뷰를 라이트공간에서 평행이동 glm::mat4 Vsnapped = glm::translate(glm::mat4(1.0f), -glm::vec3(centerXY.x, centerXY.y, 0.0f)) * V; - // 깊이 범위(표준 Z, reversed-Z 안 씀) - // lookAtRH는 -Z 쪽을 앞(카메라 전방)으로 둔다: 가까운 점 z는 덜 음수(값이 큰 쪽), 먼 점은 더 음수(값이 작은 쪽) - const float zPad = 50.0f; // 슬라이스 앞뒤 여유 - float zNear = glm::max(0.1f, -maxLS.z - zPad); // "가까움": -z(덜음수) → 양수 거리 - float zFar = -minLS.z + zPad; // "멀리": -z(더음수) → 더 큰 양수 + const float zPad = 50.0f; + float zNear = glm::max(0.1f, -maxLS.z - zPad); + float zFar = -minLS.z + zPad; - // ⚠️ API에 맞게 ZO/NO를 선택 glm::mat4 P = glm::orthoRH_ZO(-radius, radius, -radius, radius, zNear, zFar); return P * Vsnapped; }; - // 3) Cascades 1..3 채우기 float prev = nearSplit; for (int ci = 1; ci < kShadowCascadeCount; ++ci) { @@ -229,7 +212,6 @@ void SceneManager::update_scene() } } - auto end = std::chrono::system_clock::now(); auto elapsed = std::chrono::duration_cast(end - start); stats.scene_update_time = elapsed.count() / 1000.f; From e03ea1715893cb98c52c92028b1607b8fd12187f Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Wed, 22 Oct 2025 23:30:30 +0900 Subject: [PATCH 6/7] ADD: Clipmap shadow --- shaders/deferred_lighting.frag | 19 +++--- src/core/config.h | 8 +++ src/scene/vk_scene.cpp | 118 ++++++++++----------------------- 3 files changed, 54 insertions(+), 91 deletions(-) diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 470e805..9b57fce 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -30,16 +30,19 @@ const vec2 POISSON_16[16] = vec2[16]( 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 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 + 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) diff --git a/src/core/config.h b/src/core/config.h index d7d01dc..474d8ac 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -17,3 +17,11 @@ inline constexpr float kShadowMapResolution = 2048.0f; inline constexpr float kShadowCascadeRadiusScale = 2.5f; // Additive XY margin in world units (light-space) beyond scaled radius 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; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 7933910..7bf8f20 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -104,11 +104,9 @@ void SceneManager::update_scene() sceneData.proj = projection; sceneData.viewproj = projection * view; - // 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 - - // ---- Mixed Near + CSM shadow setup (fixed) ---- + // 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. { const glm::mat4 invView = glm::inverse(view); const glm::vec3 camPos = glm::vec3(invView[3]); @@ -119,96 +117,50 @@ void SceneManager::update_scene() 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 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); + glm::vec3 up = glm::normalize(glm::cross(right, L)); - const glm::mat4 projLight = glm::orthoRH_ZO(-orthoRange, orthoRange, -orthoRange, orthoRange, - nearDist, farDist); - - 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 ws{}; - for (int i = 0; i < 8; ++i) - ws[i] = glm::vec3(invView * glm::vec4(vs[i], 1.0f)); - return ws; + auto level_radius = [](int level) { + return kShadowClipBaseRadius * powf(2.0f, float(level)); }; - auto build_light_matrix_for_slice = [&](float zNearVS, float zFarVS) { - auto ws = frustum_corners_world(zNearVS, zFarVS); + // 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)); - glm::vec3 center(0.0f); - for (auto &p: ws) center += p; - center *= (1.0f / 8.0f); - const float lightPullback = 20.0f; - glm::mat4 V = glm::lookAtRH(center - L * lightPullback, center, up); + for (int ci = 0; ci < kShadowCascadeCount; ++ci) + { + const float radius = level_radius(ci); - 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); - } - - 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); + // 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); - centerXY.x = floorf(centerXY.x / texel) * texel; - centerXY.y = floorf(centerXY.y / texel) * texel; + const float uSnapped = floorf(u / 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), - -glm::vec3(centerXY.x, centerXY.y, 0.0f)) * V; + // World-space snapped center of this clip level + const glm::vec3 center = camPos + right * du + up * dv; - const float zPad = 50.0f; - float zNear = glm::max(0.1f, -maxLS.z - zPad); - float zFar = -minLS.z + zPad; + // 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); - 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; - 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; + sceneData.lightViewProjCascades[ci] = lightVP; + if (ci == 0) + { + sceneData.lightViewProj = lightVP; + } } } From 077d9c3861546d2acf71e0b4d06be95811bd3052 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Thu, 23 Oct 2025 00:23:19 +0900 Subject: [PATCH 7/7] ADD: Clipmap shadow Works! --- src/core/config.h | 4 ++-- src/core/vk_engine.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/config.h b/src/core/config.h index 474d8ac..2542387 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -20,8 +20,8 @@ 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; +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 = 80.0f; +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 67ed08d..8a0cbd6 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("police_office.glb"); const auto structureFile = _assetManager->loadGLTF(structurePath); assert(structureFile.has_value());