From 26b7db9030e24c40e96a41eeed6b55a8ddd7486b Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Sun, 12 Oct 2025 00:20:12 +0900 Subject: [PATCH] ADD: CSM half-working --- shaders/deferred_lighting.frag | 17 +++- shaders/input_structures.glsl | 8 +- shaders/shadow.vert | 4 +- src/core/config.h | 11 +++ src/core/vk_descriptors.cpp | 4 +- src/core/vk_descriptors.h | 2 +- src/core/vk_engine.cpp | 17 +++- src/core/vk_sampler_manager.cpp | 14 +++ src/core/vk_sampler_manager.h | 3 +- src/core/vk_types.h | 6 +- src/render/rg_graph.cpp | 11 ++- src/render/vk_renderpass_lighting.cpp | 44 ++++++--- src/render/vk_renderpass_lighting.h | 8 +- src/render/vk_renderpass_shadow.cpp | 107 +++++++++++--------- src/render/vk_renderpass_shadow.h | 12 +-- src/scene/vk_scene.cpp | 134 ++++++++++++++++++++++---- 16 files changed, 297 insertions(+), 105 deletions(-) diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 5466368..f6523ee 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -8,7 +8,7 @@ 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; +layout(set=2, binding=0) uniform sampler2D shadowTex[MAX_CASCADES]; const float PI = 3.14159265359; @@ -31,7 +31,15 @@ const vec2 POISSON_16[16] = vec2[16]( float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) { - vec4 lclip = sceneData.lightViewProj * vec4(worldPos, 1.0); + // 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); + + vec4 lclip = sceneData.lightViewProjCascades[ci] * vec4(worldPos, 1.0); vec3 ndc = lclip.xyz / lclip.w; vec2 suv = ndc.xy * 0.5 + 0.5; @@ -48,7 +56,7 @@ 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; @@ -70,8 +78,9 @@ 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; + // Standard depth shadow map: occluded when current > mapD + bias float occ = step(current + bias, mapD); occluded += occ * w; diff --git a/shaders/input_structures.glsl b/shaders/input_structures.glsl index e095044..50c5be2 100644 --- a/shaders/input_structures.glsl +++ b/shaders/input_structures.glsl @@ -1,12 +1,18 @@ +// Maximum number of shadow cascades supported in shaders +#define MAX_CASCADES 4 + layout(set = 0, binding = 0) uniform SceneData{ mat4 view; mat4 proj; mat4 viewproj; - mat4 lightViewProj; + mat4 lightViewProj; // legacy single-shadow for fallback 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 } sceneData; layout(set = 1, binding = 0) uniform GLTFMaterialData{ diff --git a/shaders/shadow.vert b/shaders/shadow.vert index 8a0f5e2..5d9cae2 100644 --- a/shaders/shadow.vert +++ b/shaders/shadow.vert @@ -18,12 +18,14 @@ layout(buffer_reference, std430) readonly buffer VertexBuffer{ layout(push_constant) uniform PushConsts { mat4 render_matrix; VertexBuffer vertexBuffer; + uint cascadeIndex; } 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; + uint ci = min(PC.cascadeIndex, uint(MAX_CASCADES-1)); + gl_Position = sceneData.lightViewProjCascades[ci] * worldPos; } 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..9c55b82 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include #include "render/vk_pipelines.h" #include @@ -31,6 +33,7 @@ #include "vk_resource.h" #include "engine_context.h" #include "core/vk_pipeline_manager.h" +#include "core/config.h" VulkanEngine *loadedEngine = nullptr; @@ -314,9 +317,14 @@ 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}; - 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..0d1adef 100644 --- a/src/core/vk_sampler_manager.cpp +++ b/src/core/vk_sampler_manager.cpp @@ -28,6 +28,15 @@ 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 +53,9 @@ 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..f6daddc 100644 --- a/src/core/vk_sampler_manager.h +++ b/src/core/vk_sampler_manager.h @@ -13,10 +13,11 @@ 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..faa3de1 100644 --- a/src/core/vk_types.h +++ b/src/core/vk_types.h @@ -71,10 +71,14 @@ struct GPUSceneData { glm::mat4 view; glm::mat4 proj; glm::mat4 viewproj; - glm::mat4 lightViewProj; + glm::mat4 lightViewProj; // legacy single-shadow; kept for transition glm::vec4 ambientColor; glm::vec4 sunlightDirection; // w for sun power glm::vec4 sunlightColor; + + // CSM data (unused by current shaders until wired) + 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..f78ebb6 100644 --- a/src/render/rg_graph.cpp +++ b/src/render/rg_graph.cpp @@ -676,8 +676,15 @@ 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; - 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.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.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..e7a1182 100644 --- a/src/render/vk_renderpass_lighting.h +++ b/src/render/vk_renderpass_lighting.h @@ -1,6 +1,7 @@ #pragma once #include "vk_renderpass.h" #include +#include class LightingPass : public IRenderPass { @@ -13,19 +14,20 @@ public: const char *getName() const override { return "Lighting"; } + // Register lighting; consumes GBuffer + CSM cascades. void register_graph(class RenderGraph *graph, RGImageHandle drawHandle, RGImageHandle gbufferPosition, RGImageHandle gbufferNormal, RGImageHandle gbufferAlbedo, - RGImageHandle shadowDepth); + std::span shadowCascades); private: EngineContext *_context = nullptr; VkDescriptorSetLayout _gBufferInputDescriptorLayout = VK_NULL_HANDLE; VkDescriptorSet _gBufferInputDescriptorSet = VK_NULL_HANDLE; - VkDescriptorSetLayout _shadowDescriptorLayout = VK_NULL_HANDLE; // set=2 + VkDescriptorSetLayout _shadowDescriptorLayout = VK_NULL_HANDLE; // set=2 (array) VkPipelineLayout _pipelineLayout = VK_NULL_HANDLE; VkPipeline _pipeline = VK_NULL_HANDLE; @@ -34,7 +36,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..ad2238c 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -8,6 +8,9 @@ #include #include "glm/gtx/norm.inl" +#include "glm/gtx/compatibility.hpp" +#include +#include "core/config.h" void SceneManager::init(EngineContext *context) { @@ -103,32 +106,123 @@ 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 (non-cascaded, non-stabilized) + // Build cascaded directional light view-projection matrices { - const glm::vec3 camPos = glm::vec3(glm::inverse(view)[3]); - glm::vec3 L = glm::normalize(-glm::vec3(sceneData.sunlightDirection)); - if (glm::length(L) < 1e-5f) L = glm::vec3(0.0f, -1.0f, 0.0f); + using namespace glm; + const vec3 camPos = vec3(inverse(view)[3]); + 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::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) + 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)); + + const float csmFar = kShadowCSMFar; // configurable shadow distance + const float lambda = 0.8f; // split weighting + const int cascades = kShadowCascadeCount; + + float splits[4] = {0, 0, 0, 0}; + for (int i = 1; i <= cascades; ++i) { - right = glm::vec3(1, 0, 0); - up = glm::normalize(glm::cross(L, right)); + 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]); - 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 mat4 { + // Frustum in view-space (RH, forward -Z) + float tanHalfFov = tanf(fov * 0.5f); + float yn = tanHalfFov * nearD; + float xn = yn * aspect; + float yf = tanHalfFov * farD; + float xf = yf * aspect; + + vec3 cornersV[8] = { + {-xn, -yn, -nearD}, {xn, -yn, -nearD}, {xn, yn, -nearD}, {-xn, yn, -nearD}, + {-xf, -yf, -farD}, {xf, -yf, -farD}, {xf, yf, -farD}, {-xf, yf, -farD} + }; + vec3 cornersW[8]; + vec3 centerWS(0.0f); + for (int i = 0; i < 8; ++i) + { + vec3 w = vec3(invView * vec4(cornersV[i], 1.0f)); + cornersW[i] = w; + centerWS += w; + } + centerWS *= (1.0f / 8.0f); + + // Initial light view + const float lightDist = 100.0f; + vec3 lightPos = centerWS - L * lightDist; + mat4 viewLight = lookAtRH(lightPos, centerWS, up); + + // Compute symmetric bounds around center in light space + vec2 centerLS = vec2(viewLight * vec4(centerWS, 1.0f)); + float minZ = 1e9f, maxZ = -1e9f; + float radius = 0.0f; + for (int i = 0; i < 8; ++i) + { + vec3 p = vec3(viewLight * vec4(cornersW[i], 1.0f)); + minZ = std::min(minZ, p.z); + maxZ = std::max(maxZ, p.z); + radius = std::max(radius, glm::length(vec2(p.x, p.y) - centerLS)); + } + + // Pad extents + radius *= 1.05f; + float sliceLen = farD - nearD; + float zPad = std::max(50.0f, 0.2f * sliceLen); + // Two-sided along light direction: include casters between light and slice + float nearLS = 0.01f; + float farLS = -minZ + zPad; + + // Stabilize by snapping to shadow texel grid + float texelSize = (2.0f * radius) / kShadowMapResolution; + vec2 snapped = floor(centerLS / texelSize) * texelSize; + vec2 deltaLS = snapped - centerLS; + vec3 shiftWS = right * deltaLS.x + up * deltaLS.y; + vec3 centerSnapped = centerWS + shiftWS; + vec3 lightPosSnapped = centerSnapped - L * lightDist; + viewLight = lookAtRH(lightPosSnapped, centerSnapped, up); + + // Recompute z-range with snapped view + centerLS = vec2(viewLight * vec4(centerSnapped, 1.0f)); + minZ = 1e9f; maxZ = -1e9f; radius = 0.0f; + for (int i = 0; i < 8; ++i) + { + vec3 p = vec3(viewLight * vec4(cornersW[i], 1.0f)); + minZ = std::min(minZ, p.z); + maxZ = std::max(maxZ, p.z); + radius = std::max(radius, glm::length(vec2(p.x, p.y) - centerLS)); + } + // Keep near plane close to the light to include forward casters + nearLS = 0.01f; + farLS = -minZ + zPad; + + float left = centerLS.x - radius; + float rightE = centerLS.x + radius; + float bottom = centerLS.y - radius; + float top = centerLS.y + radius; + + mat4 projLight = orthoRH_ZO(left, rightE, bottom, top, nearLS, farLS); + return projLight * viewLight; + }; + + for (int i = 0; i < cascades; ++i) + { + float nearD = (i == 0) ? nearPlane : splits[i - 1]; + float farD = splits[i]; + sceneData.lightViewProjCascades[i] = buildCascade(nearD, farD); + } + // For legacy paths, keep first cascade in single matrix + sceneData.lightViewProj = sceneData.lightViewProjCascades[0]; } auto end = std::chrono::system_clock::now();