From 3b7f0869a2ac59811d3cf83a391871f33b793783 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Tue, 21 Oct 2025 17:36:34 +0900 Subject: [PATCH] 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