ADD: CSM half-working

This commit is contained in:
2025-10-12 00:20:12 +09:00
parent b2fdcf5310
commit 26b7db9030
16 changed files with 297 additions and 105 deletions

View File

@@ -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;

View File

@@ -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{

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -1,10 +1,10 @@
#include <core/vk_descriptors.h>
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);

View File

@@ -7,7 +7,7 @@ struct DescriptorLayoutBuilder
{
std::vector<VkDescriptorSetLayoutBinding> bindings;
void add_binding(uint32_t binding, VkDescriptorType type);
void add_binding(uint32_t binding, VkDescriptorType type, uint32_t count = 1);
void clear();

View File

@@ -12,6 +12,8 @@
#include <chrono>
#include <thread>
#include <span>
#include <array>
#include "render/vk_pipelines.h"
#include <iostream>
@@ -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<RGImageHandle, kShadowCascadeCount> 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<ShadowPass>())
{
shadow->register_graph(_renderGraph.get(), hShadow, shadowExtent);
shadow->register_graph(_renderGraph.get(), std::span<RGImageHandle>(hShadowCascades.data(), hShadowCascades.size()), shadowExtent);
}
if (auto *geometry = _renderPassManager->getPass<GeometryPass>())
{
@@ -339,7 +347,8 @@ void VulkanEngine::draw()
}
if (auto *lighting = _renderPassManager->getPass<LightingPass>())
{
lighting->register_graph(_renderGraph.get(), hDraw, hGBufferPosition, hGBufferNormal, hGBufferAlbedo, hShadow);
lighting->register_graph(_renderGraph.get(), hDraw, hGBufferPosition, hGBufferNormal, hGBufferAlbedo,
std::span<RGImageHandle>(hShadowCascades.data(), hShadowCascades.size()));
}
if (auto *transparent = _renderPassManager->getPass<TransparentPass>())
{

View File

@@ -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;
}
}

View File

@@ -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;
};

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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 <array>
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<RGImageHandle> 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<RGImageHandle> 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<uint32_t>(kShadowCascadeCount, static_cast<uint32_t>(shadowCascades.size()));
std::array<VkDescriptorImageInfo, kShadowCascadeCount> 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);

View File

@@ -1,6 +1,7 @@
#pragma once
#include "vk_renderpass.h"
#include <render/rg_types.h>
#include <span>
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<RGImageHandle> 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<RGImageHandle> shadowCascades);
DeletionQueue _deletionQueue;
};

View File

@@ -1,6 +1,7 @@
#include "vk_renderpass_shadow.h"
#include <unordered_set>
#include <string>
#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<uint32_t>(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<RGImageHandle> 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<VkBuffer> indexSet;
std::unordered_set<VkBuffer> vertexSet;
auto collect = [&](const std::vector<RenderObject> &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<VkBuffer> indexSet;
std::unordered_set<VkBuffer> vertexSet;
auto collect = [&](const std::vector<RenderObject> &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);
}
}

View File

@@ -2,14 +2,13 @@
#include "vk_renderpass.h"
#include <render/rg_types.h>
#include <span>
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<RGImageHandle> 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;
};

View File

@@ -8,6 +8,9 @@
#include <glm/gtc/matrix_transform.hpp>
#include "glm/gtx/norm.inl"
#include "glm/gtx/compatibility.hpp"
#include <algorithm>
#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<far, then explicitly flip Z to reversed-Z
glm::mat4 projLight = glm::orthoRH_ZO(-orthoRange, orthoRange, -orthoRange, orthoRange,
nearDist, farDist);
mat4 invView = inverse(view);
sceneData.lightViewProj = projLight * viewLight;
auto buildCascade = [&](float nearD, float farD) -> 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();