diff --git a/assets/shaders/layout/default_pass.hlslh b/assets/shaders/layout/default_pass.hlslh index 9563aaaa..ab84c49b 100644 --- a/assets/shaders/layout/default_pass.hlslh +++ b/assets/shaders/layout/default_pass.hlslh @@ -13,4 +13,6 @@ [[vk::binding(9, 0)]] SamplerState PrefilteredMapSampler : register(s4, space0); [[vk::binding(10, 0)]] Texture2D HizBuffer : register(t5, space0); -[[vk::binding(11, 0)]] SamplerState HizBufferSampler : register(s5, space0); \ No newline at end of file +[[vk::binding(11, 0)]] SamplerState HizBufferSampler : register(s5, space0); + +#include "layout/tile_shadow.hlslh" \ No newline at end of file diff --git a/assets/shaders/layout/tile_shadow.hlslh b/assets/shaders/layout/tile_shadow.hlslh new file mode 100644 index 00000000..b3198530 --- /dev/null +++ b/assets/shaders/layout/tile_shadow.hlslh @@ -0,0 +1,128 @@ +// Tile-based shadow map shader bindings and helper functions. +// Included by default_pass.hlslh – do not include this file directly. + +#ifndef MAX_SHADOW_LIGHTS +#define MAX_SHADOW_LIGHTS 4 +#endif + +#ifndef TILE_SIZE +#define TILE_SIZE 16 +#endif + +// Maximum tile counts that match the CPU-side constants in RenderBuiltinLayout.h +#define MAX_SHADOW_TILES_X 128 +#define MAX_SHADOW_TILES_Y 72 +#define MAX_SHADOW_TILES (MAX_SHADOW_TILES_X * MAX_SHADOW_TILES_Y) + +// ── Per-light shadow data ───────────────────────────────────────────────────── +struct ShadowLightData +{ + float4x4 LightViewProj; // Light's combined view-projection matrix + float4 PosRadius; // xyz = world position, w = radius (0 for directional) + int LightType; // 0 = directional, 1 = spot, 2 = point + float3 Pad; +}; + +// ── Tile shadow info UBO (binding 12) ──────────────────────────────────────── +[[vk::binding(12, 0)]] cbuffer TileShadowInfo : register(b2, space0) +{ + ShadowLightData ShadowLights[MAX_SHADOW_LIGHTS]; + uint ShadowLightCount; // Number of active shadow lights this frame + uint TileCountX; // (screenW + TILE_SIZE - 1) / TILE_SIZE + uint TileCountY; // (screenH + TILE_SIZE - 1) / TILE_SIZE + float ShadowPad; +} + +// ── Per-tile light bitmask UBO (binding 13) ─────────────────────────────────── +// Bit N of each element is 1 when shadow light N affects that tile. +[[vk::binding(13, 0)]] cbuffer TileLightBitmask : register(b3, space0) +{ + uint TileBitmasks[MAX_SHADOW_TILES]; +} + +// ── Shadow atlas Texture2DArray (binding 14) ────────────────────────────────── +// Layer index equals the shadow light index (0..ShadowLightCount-1). +[[vk::binding(14, 0)]] Texture2DArray ShadowAtlas : register(t6, space0); +[[vk::binding(15, 0)]] SamplerState ShadowAtlasSampler : register(s6, space0); + +// ── Bias matrix to convert from NDC [-1,1] to UV [0,1] ─────────────────────── +static const float4x4 TileBiasMat = float4x4( + 0.5, 0.0, 0.0, 0.5, + 0.0, 0.5, 0.0, 0.5, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0); + +// Returns the per-tile bitmask for the fragment at 'screenPos' (SV_Position). +uint GetTileShadowBitmask(float2 screenPos) +{ + uint tileX = (uint)screenPos.x / TILE_SIZE; + uint tileY = (uint)screenPos.y / TILE_SIZE; + uint tileIdx = tileY * TileCountX + tileX; + tileIdx = min(tileIdx, (uint)(MAX_SHADOW_TILES - 1)); + return TileBitmasks[tileIdx]; +} + +// Shadowed fragment attenuation: fragments in shadow receive this fraction of direct light. +static const float TILE_SHADOW_ATTENUATION = 0.1; + +// PCF shadow sample from the atlas for one shadow light. +// Returns 1.0 (fully lit) or < 1.0 (in shadow). +float SampleTileShadowPCF(uint lightIdx, float3 worldPos) +{ + ShadowLightData light = ShadowLights[lightIdx]; + float4 sc = mul(TileBiasMat, mul(light.LightViewProj, float4(worldPos, 1.0))); + sc.xyz /= sc.w; + + // Discard fragments outside the shadow frustum + if (sc.z <= 0.0 || sc.z >= 1.0 || + sc.x <= 0.0 || sc.x >= 1.0 || + sc.y <= 0.0 || sc.y >= 1.0) + { + return 1.0; + } + + float bias = 0.005; + float shadow = 0.0; + float count = 0.0; + + uint atlasW, atlasH, atlasLayers; + ShadowAtlas.GetDimensions(atlasW, atlasH, atlasLayers); + float dx = 1.0 / (float)atlasW; + float dy = 1.0 / (float)atlasH; + + [unroll] + for (int x = -1; x <= 1; ++x) + { + [unroll] + for (int y = -1; y <= 1; ++y) + { + float2 uv = sc.xy + float2(dx * x, dy * y); + float depth = ShadowAtlas.Sample(ShadowAtlasSampler, + float3(uv, (float)lightIdx)).r; + shadow += (depth + bias < sc.z) ? TILE_SHADOW_ATTENUATION : 1.0; + count += 1.0; + } + } + return shadow / count; +} + +// Computes the combined tile-shadow factor for 'worldPos'. +// 'screenPos' is SV_Position (pixel coordinates). +// Returns a value in [0,1]: 1 = fully lit, < 1 = shadowed. +float ComputeTileShadow(float2 screenPos, float3 worldPos) +{ + if (ShadowLightCount == 0u) return 1.0; + + uint bitmask = GetTileShadowBitmask(screenPos); + if (bitmask == 0u) return 1.0; + + float shadow = 1.0; + for (uint i = 0u; i < ShadowLightCount; ++i) + { + if ((bitmask & (1u << i)) != 0u) + { + shadow = min(shadow, SampleTileShadowPCF(i, worldPos)); + } + } + return shadow; +} diff --git a/assets/shaders/standard_pbr.hlsl b/assets/shaders/standard_pbr.hlsl index 083ffd56..90ea3245 100644 --- a/assets/shaders/standard_pbr.hlsl +++ b/assets/shaders/standard_pbr.hlsl @@ -11,6 +11,7 @@ #pragma option({"key": "ENABLE_IBL", "default": 0, "type": "Batch"}) #pragma option({"key": "ENABLE_SHADOW", "default": 0, "type": "Pass"}) +#pragma option({"key": "ENABLE_TILE_SHADOW", "default": 0, "type": "Pass"}) #include "vertex/standard.hlslh" @@ -378,6 +379,8 @@ float4 FSMain(VSOutput input) : SV_TARGET float4 fragPosLightSpace = mul(biasMat, mul(LightMatrix, float4(input.WorldPos, 1.0))); float4 shadowCoord = fragPosLightSpace / fragPosLightSpace.w; float shadow = FilterPCF(shadowCoord); +#elif ENABLE_TILE_SHADOW + float shadow = ComputeTileShadow(input.Pos.xy, input.WorldPos); #else float shadow = 1.0; #endif diff --git a/engine/render/adaptor/include/render/adaptor/pipeline/DefaultForwardPipeline.h b/engine/render/adaptor/include/render/adaptor/pipeline/DefaultForwardPipeline.h index 16ff4b52..7c3bc6bb 100644 --- a/engine/render/adaptor/include/render/adaptor/pipeline/DefaultForwardPipeline.h +++ b/engine/render/adaptor/include/render/adaptor/pipeline/DefaultForwardPipeline.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -40,10 +41,12 @@ namespace sky { rhi::ImagePtr hizDepth; rhi::SamplerPtr pointSampler; + rhi::SamplerPtr shadowAtlasSampler; std::unique_ptr depth; std::unique_ptr hiz; std::unique_ptr shadowMap; + std::unique_ptr tileShadow; std::unique_ptr forward; std::unique_ptr postProcess; std::unique_ptr present; diff --git a/engine/render/adaptor/include/render/adaptor/pipeline/DefaultPassConstants.h b/engine/render/adaptor/include/render/adaptor/pipeline/DefaultPassConstants.h index 2b1db4b4..3357e6db 100644 --- a/engine/render/adaptor/include/render/adaptor/pipeline/DefaultPassConstants.h +++ b/engine/render/adaptor/include/render/adaptor/pipeline/DefaultPassConstants.h @@ -20,4 +20,9 @@ namespace sky { static constexpr std::string_view SWAP_CHAIN = "SwapChain"; + // Tile-based shadow map resources + static constexpr std::string_view TILE_SHADOW_ATLAS = "ShadowAtlas"; + static constexpr std::string_view TILE_SHADOW_INFO = "TileShadowInfo"; + static constexpr std::string_view TILE_LIGHT_BITMASK = "TileLightBitmask"; + } // namespace sky \ No newline at end of file diff --git a/engine/render/adaptor/include/render/adaptor/pipeline/TileShadowPass.h b/engine/render/adaptor/include/render/adaptor/pipeline/TileShadowPass.h new file mode 100644 index 00000000..f4cd01ec --- /dev/null +++ b/engine/render/adaptor/include/render/adaptor/pipeline/TileShadowPass.h @@ -0,0 +1,112 @@ +// +// Created by blues on 2024/9/6. +// + +#pragma once + +#include +#include +#include +#include +#include + +namespace sky { + class RenderScenePipeline; + class SceneView; + + /** + * @brief Raster pass that renders one shadow-casting light into a single layer of the shadow atlas. + * + * Each ShadowSlotPass is managed by TileShadowPass. The atlas layer image view is + * imported into the render graph by TileShadowPass before this pass runs. + */ + class ShadowSlotPass : public RasterPass { + public: + explicit ShadowSlotPass(uint32_t index, uint32_t shadowMapSize); + ~ShadowSlotPass() override = default; + + void SetLayout(const RDResourceLayoutPtr &layout_); + void SetShadowView(SceneView *view); + private: + void Setup(rdg::RenderGraph &rdg, RenderScene &scene) override; + void SetupSubPass(rdg::RasterSubPassBuilder &builder, RenderScene &scene) override; + + uint32_t slotIndex; + Name layerName; // Imported atlas-layer resource in the render graph + Name viewUBOName; // Shadow-view UBO resource in the render graph + Name sceneViewName; // Name under which the SceneView is registered + SceneView *shadowSceneView = nullptr; + }; + + /** + * @brief Tile-based shadow map manager. + * + * Owns a persistent Texture2DArray shadow atlas (one layer per active shadow light). + * Maintains: + * - ShadowSlotPass instances to render each light's shadow map. + * - A TileShadowPassInfo UBO with per-light view-projection matrices. + * - A per-tile bitmask UBO that records which shadow lights affect each screen tile. + * + * The bitmask is computed on the CPU each frame and is used by the fragment shader to + * skip lights that do not affect a given tile. + * + * Usage in the pipeline (inside DefaultForwardPipeline::Collect): + * @code + * tileShadow->Setup(rdg, *scene, width, height); + * tileShadow->AddPasses(*this); + * @endcode + */ + class TileShadowPass { + public: + static constexpr uint32_t MAX_LIGHTS = TILE_SHADOW_MAX_LIGHTS; + static constexpr uint32_t MAP_SIZE = 1024; + static constexpr uint32_t TILE_SIZE = TILE_SHADOW_TILE_SIZE; + + TileShadowPass(); + ~TileShadowPass() = default; + + void SetLayout(const RDResourceLayoutPtr &layout_); + + /** + * @brief Called from DefaultForwardPipeline::Collect. + * Imports persistent GPU resources and updates CPU-side data. + */ + void Setup(rdg::RenderGraph &rdg, RenderScene &scene, uint32_t screenW, uint32_t screenH); + + /** + * @brief Registers active ShadowSlotPass instances with the pipeline. + * Must be called after Setup(). + */ + void AddPasses(RenderScenePipeline &pipeline); + + private: + void EnsureAtlas(); + void UpdateLightData(RenderScene &scene); + void BuildTileBitmask(const RenderScene &scene, uint32_t screenW, uint32_t screenH); + + // Slot passes (one per shadow light) + std::array, MAX_LIGHTS> slotPasses; + + // Shadow scene views (created lazily, owned by RenderScene) + std::array shadowViews{}; + + // Persistent GPU shadow atlas + rhi::ImagePtr atlasImage; + rhi::ImageViewPtr atlasFullView; // Full Texture2DArray view for shader sampling + std::array atlasLayerViews; // Per-layer views for rendering + + // Per-frame CPU data stored in UBOs + RDUniformBufferPtr shadowInfoUBO; // TileShadowPassInfo + RDUniformBufferPtr tileBitmaskUBO; // uint32_t[TILE_SHADOW_MAX_TILES] + + // CPU-side copy so we can read back values without extra UBO mapping + TileShadowPassInfo shadowInfo{}; + uint32_t activeLights = 0; + + // Whether the atlas image has been rendered to at least once (reserved for future use) + // bool atlasInitialized = false; + + RDResourceLayoutPtr layout; + }; + +} // namespace sky diff --git a/engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp b/engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp index cccfcfc7..620de989 100644 --- a/engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp +++ b/engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp @@ -55,6 +55,15 @@ namespace sky { rhi::DescriptorType::SAMPLED_IMAGE, 1, 10, stageFlags, "HizBuffer"); desc.bindings.emplace_back( rhi::DescriptorType::SAMPLER, 1, 11, stageFlags, "HizBufferSampler"); + // Tile-based shadow map bindings + desc.bindings.emplace_back( + rhi::DescriptorType::UNIFORM_BUFFER, 1, 12, stageFlags, "TileShadowInfo"); + desc.bindings.emplace_back( + rhi::DescriptorType::UNIFORM_BUFFER, 1, 13, stageFlags, "TileLightBitmask"); + desc.bindings.emplace_back( + rhi::DescriptorType::SAMPLED_IMAGE, 1, 14, stageFlags, "ShadowAtlas"); + desc.bindings.emplace_back( + rhi::DescriptorType::SAMPLER, 1, 15, stageFlags, "ShadowAtlasSampler"); defaultRasterLayout = new ResourceGroupLayout(); defaultRasterLayout->SetRHILayout(RHI::Get()->GetDevice()->CreateDescriptorSetLayout(desc)); @@ -70,6 +79,10 @@ namespace sky { defaultRasterLayout->AddNameHandler(Name("PrefilteredMapSampler"), {9}); defaultRasterLayout->AddNameHandler(Name("HizBuffer"), {10}); defaultRasterLayout->AddNameHandler(Name("HizBufferSampler"), {11}); + defaultRasterLayout->AddNameHandler(Name("TileShadowInfo"), {12, sizeof(TileShadowPassInfo)}); + defaultRasterLayout->AddNameHandler(Name("TileLightBitmask"), {13, TILE_SHADOW_MAX_TILES * sizeof(uint32_t)}); + defaultRasterLayout->AddNameHandler(Name("ShadowAtlas"), {14}); + defaultRasterLayout->AddNameHandler(Name("ShadowAtlasSampler"), {15}); defaultGlobal = new UniformBuffer(); defaultGlobal->Init(sizeof(ShaderPassInfo)); @@ -83,6 +96,9 @@ namespace sky { shadowMap = std::make_unique(4096, 4096); shadowMap->SetLayout(defaultRasterLayout); + tileShadow = std::make_unique(); + tileShadow->SetLayout(defaultRasterLayout); + brdfLut = std::make_unique(brdfTech); postProcess = std::make_unique(postTech); present = std::make_unique(output->GetSwapChain()); @@ -155,6 +171,18 @@ namespace sky { rg.ImportUBO(fwdPassInfoName, defaultGlobal); rg.ImportSampler(Name("PointSampler"), pointSampler); + + // Shadow atlas sampler (comparison sampler for PCF) + if (!shadowAtlasSampler) { + rhi::Sampler::Descriptor shadowSamplerDesc = {}; + shadowSamplerDesc.minFilter = rhi::Filter::LINEAR; + shadowSamplerDesc.magFilter = rhi::Filter::LINEAR; + shadowSamplerDesc.addressModeU = rhi::WrapMode::CLAMP_TO_BORDER; + shadowSamplerDesc.addressModeV = rhi::WrapMode::CLAMP_TO_BORDER; + shadowSamplerDesc.addressModeW = rhi::WrapMode::CLAMP_TO_BORDER; + shadowAtlasSampler = RHI::Get()->GetDevice()->CreateSampler(shadowSamplerDesc); + } + rg.ImportSampler(Name("ShadowAtlasSampler"), shadowAtlasSampler); } void DefaultForwardPipeline::Collect(rdg::RenderGraph &rdg) @@ -162,14 +190,22 @@ namespace sky { const auto renderWidth = output->GetWidth(); const auto renderHeight = output->GetHeight(); + // Tile-based shadow map: build shadow matrices + import atlas resources first + // so that SetupGlobal can read the up-to-date LightMatrix. + tileShadow->Setup(rdg, *scene, renderWidth, renderHeight); + SetupGlobal(rdg, renderWidth, renderHeight); SetupScreenExternalImages(rdg, renderWidth, renderHeight); + // Legacy single-light shadow map is disabled in favour of tile shadows shadowMap->SetEnable(false); AddPass(brdfLut.get()); AddPass(shadowMap.get()); + // Add active tile shadow slot passes (render shadow maps into atlas layers) + tileShadow->AddPasses(*this); + forward->Resize(renderWidth, renderHeight); AddPass(forward.get()); diff --git a/engine/render/adaptor/src/pipeline/ForwardMSAAPass.cpp b/engine/render/adaptor/src/pipeline/ForwardMSAAPass.cpp index 85ba633b..d81f1201 100644 --- a/engine/render/adaptor/src/pipeline/ForwardMSAAPass.cpp +++ b/engine/render/adaptor/src/pipeline/ForwardMSAAPass.cpp @@ -96,6 +96,24 @@ namespace sky { rdg::ComputeView{Name("PrefilteredMap"), rdg::ComputeType::SRV, stageFlags} }); + // Tile-based shadow map resources + computeResources.emplace_back(ComputeResource{ + Name("TileShadowInfo"), + rdg::ComputeView{Name("TileShadowInfo"), rdg::ComputeType::CBV, stageFlags} + }); + + computeResources.emplace_back(ComputeResource{ + Name("TileLightBitmask"), + rdg::ComputeView{Name("TileLightBitmask"), rdg::ComputeType::CBV, stageFlags} + }); + + computeResources.emplace_back(ComputeResource{ + Name("ShadowAtlas"), + rdg::ComputeView{Name("ShadowAtlas"), rdg::ComputeType::SRV, stageFlags} + }); + + samplers.emplace_back(SamplerResource{Name("ShadowAtlasSampler"), Name("ShadowAtlasSampler")}); + // computeResources.emplace_back(ComputeResource{ // Name("HizBuffer"), // rdg::ComputeView{Name("HizBuffer"), rdg::ComputeType::SRV, stageFlags} diff --git a/engine/render/adaptor/src/pipeline/TileShadowPass.cpp b/engine/render/adaptor/src/pipeline/TileShadowPass.cpp new file mode 100644 index 00000000..65d02198 --- /dev/null +++ b/engine/render/adaptor/src/pipeline/TileShadowPass.cpp @@ -0,0 +1,364 @@ +// +// Created by blues on 2024/9/6. +// + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace sky { + + // ─── ShadowSlotPass ─────────────────────────────────────────────────────── + + ShadowSlotPass::ShadowSlotPass(uint32_t index, uint32_t shadowMapSize) + : RasterPass(Name(("TileShadow_" + std::to_string(index)).c_str())) + , slotIndex(index) + { + width = shadowMapSize; + height = shadowMapSize; + + const std::string id = std::to_string(index); + layerName = Name(("TileShadowLayer_" + id).c_str()); + viewUBOName = Name(("TileShadowViewUBO_" + id).c_str()); + sceneViewName = Name(("TileShadowSceneView_" + id).c_str()); + + auto stageFlags = rhi::ShaderStageFlagBit::VS | rhi::ShaderStageFlagBit::FS | + rhi::ShaderStageFlagBit::TAS | rhi::ShaderStageFlagBit::MS; + + // Bind the same pass-info UBO that all other passes use (contains light matrix etc.) + computeResources.emplace_back(ComputeResource{ + Name("FWD_PassInfo"), + rdg::ComputeView{Name("passInfo"), rdg::ComputeType::CBV, stageFlags} + }); + + // Bind the per-light shadow view UBO + computeResources.emplace_back(ComputeResource{ + viewUBOName, + rdg::ComputeView{Name("viewInfo"), rdg::ComputeType::CBV, stageFlags} + }); + } + + void ShadowSlotPass::SetLayout(const RDResourceLayoutPtr &layout_) + { + layout = layout_; + } + + void ShadowSlotPass::SetShadowView(SceneView *view) + { + shadowSceneView = view; + } + + void ShadowSlotPass::Setup(rdg::RenderGraph &rdg, RenderScene &scene) + { + if (shadowSceneView == nullptr) { + return; // No active light for this slot – skip rendering + } + + // Use the atlas layer view that TileShadowPass imported this frame + depthStencil = Attachment{ + rdg::RasterAttachment{layerName, rhi::LoadOp::CLEAR, rhi::StoreOp::STORE}, + rhi::ClearValue(1.f, 0) + }; + + // Make the shadow-view UBO available in the render graph + rdg.resourceGraph.ImportUBO(viewUBOName, shadowSceneView->GetUBO()); + + RasterPass::Setup(rdg, scene); + } + + void ShadowSlotPass::SetupSubPass(rdg::RasterSubPassBuilder &builder, RenderScene &/*scene*/) + { + builder.SetViewMask(0); + builder.AddQueue(Name("queue1")) + .SetRasterID(Name("Shadow")) + .SetSceneView(sceneViewName) + .SetLayout(layout); + } + + // ─── TileShadowPass ─────────────────────────────────────────────────────── + + TileShadowPass::TileShadowPass() + { + for (uint32_t i = 0; i < MAX_LIGHTS; ++i) { + slotPasses[i] = std::make_unique(i, MAP_SIZE); + } + + shadowInfoUBO = new UniformBuffer(); + shadowInfoUBO->Init(sizeof(TileShadowPassInfo)); + + tileBitmaskUBO = new UniformBuffer(); + tileBitmaskUBO->Init(TILE_SHADOW_MAX_TILES * sizeof(uint32_t)); + + // Zero-initialise CPU shadow info + std::memset(&shadowInfo, 0, sizeof(shadowInfo)); + } + + void TileShadowPass::SetLayout(const RDResourceLayoutPtr &layout_) + { + layout = layout_; + for (auto &pass : slotPasses) { + pass->SetLayout(layout_); + } + } + + // ── EnsureAtlas ────────────────────────────────────────────────────────── + + void TileShadowPass::EnsureAtlas() + { + if (atlasImage) { + return; + } + + rhi::Image::Descriptor desc = {}; + desc.imageType = rhi::ImageType::IMAGE_2D; + desc.format = rhi::PixelFormat::D32; + desc.extent = {MAP_SIZE, MAP_SIZE, 1}; + desc.mipLevels = 1; + desc.arrayLayers = MAX_LIGHTS; + desc.samples = rhi::SampleCount::X1; + desc.usage = rhi::ImageUsageFlagBit::DEPTH_STENCIL | rhi::ImageUsageFlagBit::SAMPLED; + desc.memory = rhi::MemoryType::GPU_ONLY; + + atlasImage = RHI::Get()->GetDevice()->CreateImage(desc); + + // Full array view for shader sampling + { + rhi::ImageViewDesc viewDesc = {}; + viewDesc.viewType = rhi::ImageViewType::VIEW_2D_ARRAY; + viewDesc.subRange = {0, 1, 0, MAX_LIGHTS, rhi::AspectFlagBit::DEPTH_BIT}; + atlasFullView = atlasImage->CreateView(viewDesc); + } + + // One view per layer for rendering + for (uint32_t i = 0; i < MAX_LIGHTS; ++i) { + rhi::ImageViewDesc layerDesc = {}; + layerDesc.viewType = rhi::ImageViewType::VIEW_2D; + layerDesc.subRange = {0, 1, i, 1, rhi::AspectFlagBit::DEPTH_BIT}; + atlasLayerViews[i] = atlasImage->CreateView(layerDesc); + } + } + + // ── UpdateLightData ────────────────────────────────────────────────────── + + void TileShadowPass::UpdateLightData(RenderScene &scene) + { + auto *lf = GetFeatureProcessor(&scene); + + activeLights = 0; + std::memset(&shadowInfo, 0, sizeof(shadowInfo)); + + if (lf == nullptr) { + shadowInfoUBO->WriteT(0, shadowInfo); + return; + } + + // Slot 0: main directional light + auto *mainLight = lf->GetMainLight(); + if (mainLight != nullptr) { + const uint32_t idx = activeLights; + + if (shadowViews[idx] == nullptr) { + shadowViews[idx] = scene.CreateSceneView(1); + scene.AttachSceneView( + shadowViews[idx], + Name(("TileShadowSceneView_" + std::to_string(idx)).c_str())); + } + + mainLight->BuildMatrix(*shadowViews[idx]); + shadowViews[idx]->Update(); + + ShadowLightData &ld = shadowInfo.lights[idx]; + ld.lightViewProj = mainLight->GetMatrix(); + ld.posRadius = Vector4(0.f, 0.f, 0.f, 0.f); + ld.lightType = 0; // directional + std::memset(ld.pad, 0, sizeof(ld.pad)); + + slotPasses[idx]->SetShadowView(shadowViews[idx]); + ++activeLights; + } + + // Slots 1–3: spot lights from the feature processor's light list + const auto &lights = lf->GetLights(); + for (auto it = lights.begin(); it != lights.end() && activeLights < MAX_LIGHTS; ++it) { + auto *spotLight = dynamic_cast(it->get()); + if (spotLight == nullptr) { + continue; + } + + const uint32_t idx = activeLights; + + if (shadowViews[idx] == nullptr) { + shadowViews[idx] = scene.CreateSceneView(1); + scene.AttachSceneView( + shadowViews[idx], + Name(("TileShadowSceneView_" + std::to_string(idx)).c_str())); + } + + // Build spot-light shadow matrix + spotLight->BuildShadowMatrix(*shadowViews[idx]); + shadowViews[idx]->Update(); + + ShadowLightData &ld = shadowInfo.lights[idx]; + ld.lightViewProj = shadowViews[idx]->GetViewProject(); + ld.lightType = 1; // spot + + LightInfo li{}; + spotLight->Collect(li); + ld.posRadius = Vector4(li.position.x, li.position.y, li.position.z, + spotLight->GetRange()); + std::memset(ld.pad, 0, sizeof(ld.pad)); + + slotPasses[idx]->SetShadowView(shadowViews[idx]); + ++activeLights; + } + + // Disable inactive slots + for (uint32_t i = activeLights; i < MAX_LIGHTS; ++i) { + slotPasses[i]->SetShadowView(nullptr); + } + + shadowInfo.lightCount = activeLights; + shadowInfoUBO->WriteT(0, shadowInfo); + } + + // ── BuildTileBitmask ───────────────────────────────────────────────────── + + void TileShadowPass::BuildTileBitmask(const RenderScene &scene, uint32_t screenW, uint32_t screenH) + { + uint32_t tileCountX = (screenW + TILE_SIZE - 1) / TILE_SIZE; + uint32_t tileCountY = (screenH + TILE_SIZE - 1) / TILE_SIZE; + uint32_t totalTiles = std::min(tileCountX * tileCountY, TILE_SHADOW_MAX_TILES); + + // Write tile counts back into the info UBO + shadowInfo.tileCountX = tileCountX; + shadowInfo.tileCountY = tileCountY; + shadowInfoUBO->WriteT(0, shadowInfo); + + // Build per-tile bitmask (bit N = shadow light N affects tile) + std::vector bitmasks(TILE_SHADOW_MAX_TILES, 0u); + + if (activeLights == 0) { + tileBitmaskUBO->Write(0, + reinterpret_cast(bitmasks.data()), + static_cast(TILE_SHADOW_MAX_TILES * sizeof(uint32_t))); + return; + } + + auto *cameraView = scene.GetSceneView(Name("MainCamera")); + const Matrix4 *viewProjPtr = (cameraView != nullptr) ? &cameraView->GetViewProject() : nullptr; + + for (uint32_t li = 0; li < activeLights; ++li) { + const ShadowLightData &ld = shadowInfo.lights[li]; + + if (ld.lightType == 0) { + // Directional light: affects every tile + for (uint32_t t = 0; t < totalTiles; ++t) { + bitmasks[t] |= (1u << li); + } + } else if (viewProjPtr != nullptr) { + // Spot/point light: project bounding sphere into screen space + const Vector3 worldPos(ld.posRadius.x, ld.posRadius.y, ld.posRadius.z); + const float radius = ld.posRadius.w; + + Vector4 clipPos = (*viewProjPtr) * Vector4(worldPos.x, worldPos.y, worldPos.z, 1.f); + if (clipPos.w <= 0.f) { + continue; + } + + float ndcX = clipPos.x / clipPos.w; + float ndcY = clipPos.y / clipPos.w; + // Approximate screen-space radius + float screenRadius = radius / clipPos.w * static_cast(screenW) * 0.5f; + + float sx = (ndcX * 0.5f + 0.5f) * static_cast(screenW); + float sy = (ndcY * 0.5f + 0.5f) * static_cast(screenH); + + int minTX = static_cast((sx - screenRadius) / static_cast(TILE_SIZE)); + int maxTX = static_cast((sx + screenRadius) / static_cast(TILE_SIZE)); + int minTY = static_cast((sy - screenRadius) / static_cast(TILE_SIZE)); + int maxTY = static_cast((sy + screenRadius) / static_cast(TILE_SIZE)); + + minTX = std::max(0, minTX); + maxTX = std::min(static_cast(tileCountX) - 1, maxTX); + minTY = std::max(0, minTY); + maxTY = std::min(static_cast(tileCountY) - 1, maxTY); + + for (int ty = minTY; ty <= maxTY; ++ty) { + for (int tx = minTX; tx <= maxTX; ++tx) { + uint32_t tileIdx = static_cast(ty) * tileCountX + + static_cast(tx); + if (tileIdx < TILE_SHADOW_MAX_TILES) { + bitmasks[tileIdx] |= (1u << li); + } + } + } + } + } + + tileBitmaskUBO->Write(0, + reinterpret_cast(bitmasks.data()), + static_cast(TILE_SHADOW_MAX_TILES * sizeof(uint32_t))); + } + + // ── Setup ───────────────────────────────────────────────────────────────── + + void TileShadowPass::Setup(rdg::RenderGraph &rdg, RenderScene &scene, + uint32_t screenW, uint32_t screenH) + { + EnsureAtlas(); + UpdateLightData(scene); + BuildTileBitmask(scene, screenW, screenH); + + auto &rsg = rdg.resourceGraph; + + // Import per-layer views so each ShadowSlotPass can write to its layer. + // Use NONE as the initial state each frame – the render graph will generate + // the required barrier from the previous state. + for (uint32_t i = 0; i < MAX_LIGHTS; ++i) { + rsg.ImportImageView( + Name(("TileShadowLayer_" + std::to_string(i)).c_str()), + atlasImage, atlasLayerViews[i], + rhi::AccessFlagBit::NONE); + } + + // Import full array view for shader sampling. + // After the slot passes run, the atlas layers will be in DEPTH_STENCIL_WRITE + // state; the render graph will transition them to FRAGMENT_SRV as needed. + rsg.ImportImageView(Name(TILE_SHADOW_ATLAS.data()), atlasImage, atlasFullView, + rhi::AccessFlagBit::NONE); + + // Import UBOs + rsg.ImportUBO(Name(TILE_SHADOW_INFO.data()), shadowInfoUBO); + rsg.ImportUBO(Name(TILE_LIGHT_BITMASK.data()), tileBitmaskUBO); + + // Register shadow scene views with the render graph + for (uint32_t i = 0; i < activeLights; ++i) { + if (shadowViews[i] != nullptr) { + rdg.AddSceneView( + Name(("TileShadowSceneView_" + std::to_string(i)).c_str()), + shadowViews[i]); + } + } + } + + // ── AddPasses ───────────────────────────────────────────────────────────── + + void TileShadowPass::AddPasses(RenderScenePipeline &pipeline) + { + for (uint32_t i = 0; i < activeLights; ++i) { + pipeline.AddPass(slotPasses[i].get()); + } + } + +} // namespace sky diff --git a/engine/render/core/include/render/RenderBuiltinLayout.h b/engine/render/core/include/render/RenderBuiltinLayout.h index 75a2e07c..c50c3ab6 100644 --- a/engine/render/core/include/render/RenderBuiltinLayout.h +++ b/engine/render/core/include/render/RenderBuiltinLayout.h @@ -7,6 +7,31 @@ #include namespace sky { + + // Tile-based shadow map constants + static constexpr uint32_t TILE_SHADOW_MAX_LIGHTS = 4; + static constexpr uint32_t TILE_SHADOW_TILE_SIZE = 16; + static constexpr uint32_t TILE_SHADOW_MAX_TILES_X = 128; // 128 * 16 = 2048px max width + static constexpr uint32_t TILE_SHADOW_MAX_TILES_Y = 72; // 72 * 16 = 1152px max height + static constexpr uint32_t TILE_SHADOW_MAX_TILES = TILE_SHADOW_MAX_TILES_X * TILE_SHADOW_MAX_TILES_Y; + + // Per-shadow-light data uploaded to the GPU + struct ShadowLightData { + Matrix4 lightViewProj; // Light's view-projection matrix + Vector4 posRadius; // xyz = light position, w = radius (0 for directional) + int32_t lightType; // 0 = directional, 1 = spot, 2 = point + float pad[3]; + }; + + // Uploaded once per frame to describe all active shadow lights and tile counts + struct TileShadowPassInfo { + ShadowLightData lights[TILE_SHADOW_MAX_LIGHTS]; // One entry per active shadow light + uint32_t lightCount; // Number of active shadow lights + uint32_t tileCountX; // Number of tiles in X direction + uint32_t tileCountY; // Number of tiles in Y direction + float pad; + }; + struct SceneViewInfo { Matrix4 world; Matrix4 view; diff --git a/engine/render/core/include/render/light/LightBase.h b/engine/render/core/include/render/light/LightBase.h index e6213073..8afc8499 100644 --- a/engine/render/core/include/render/light/LightBase.h +++ b/engine/render/core/include/render/light/LightBase.h @@ -110,12 +110,21 @@ namespace sky { SpotLight() = default; ~SpotLight() override = default; + void SetPosition(const Vector3 &pos) { position = pos; } + void SetDirection(const Vector3 &dir) { direction = dir; } + void SetAngle(float a) { angle = a; } + void SetRange(float r) { range = r; } + + float GetRange() const { return range; } + void Collect(LightInfo &info) override; + void BuildShadowMatrix(SceneView &view) const; private: Vector3 position; Vector3 direction; float angle = 1.f; + float range = 10.f; }; class DirectLight : public Light { diff --git a/engine/render/core/include/render/light/LightFeatureProcessor.h b/engine/render/core/include/render/light/LightFeatureProcessor.h index 852b208d..4a2b4906 100644 --- a/engine/render/core/include/render/light/LightFeatureProcessor.h +++ b/engine/render/core/include/render/light/LightFeatureProcessor.h @@ -44,6 +44,8 @@ namespace sky { void AddLight(Light *light); void RemoveLight(Light *light); + + const std::vector &GetLights() const { return lights; } private: void GatherLightInfo(); diff --git a/engine/render/core/src/light/LightBase.cpp b/engine/render/core/src/light/LightBase.cpp index 0d24d1e2..886644b6 100644 --- a/engine/render/core/src/light/LightBase.cpp +++ b/engine/render/core/src/light/LightBase.cpp @@ -21,6 +21,42 @@ namespace sky { info.direction = ToVec4(direction); } + void SpotLight::BuildShadowMatrix(SceneView &view) const + { + Matrix4 p = Matrix4::Identity(); + p[1][1] = RHI::Get()->GetDevice()->GetConstants().flipY ? -1.f : 1.f; + + // Build a look-at world matrix for the spot light. + // SceneView::SetMatrix expects a world (TRS) matrix – the engine computes + // the view matrix as its inverse internally. + Vector3 fwd = direction; + fwd.Normalize(); + + const float kDotY = fwd.y; // fwd · (0,1,0) + Vector3 worldUp = (std::abs(kDotY) > 0.99f) + ? Vector3(1.f, 0.f, 0.f) + : Vector3(0.f, 1.f, 0.f); + + Vector3 right = fwd.Cross(worldUp); + right.Normalize(); + Vector3 up = right.Cross(fwd); + up.Normalize(); + + // Row-major world matrix (translation in row 3): + // row0 = right, row1 = up, row2 = fwd, row3 = position + Matrix4 worldMat = Matrix4::Identity(); + worldMat[0][0] = right.x; worldMat[0][1] = right.y; worldMat[0][2] = right.z; + worldMat[1][0] = up.x; worldMat[1][1] = up.y; worldMat[1][2] = up.z; + worldMat[2][0] = fwd.x; worldMat[2][1] = fwd.y; worldMat[2][2] = fwd.z; + worldMat[3][0] = position.x; + worldMat[3][1] = position.y; + worldMat[3][2] = position.z; + worldMat[3][3] = 1.f; + + view.SetMatrix(worldMat); + view.SetPerspective(0.1f, range, angle * 2.f, 1.f); + } + void DirectLight::Collect(LightInfo &info) { info.color = color;