Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion assets/shaders/layout/default_pass.hlslh
Original file line number Diff line number Diff line change
Expand Up @@ -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);
[[vk::binding(11, 0)]] SamplerState HizBufferSampler : register(s5, space0);

#include "layout/tile_shadow.hlslh"
128 changes: 128 additions & 0 deletions assets/shaders/layout/tile_shadow.hlslh
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions assets/shaders/standard_pbr.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <render/adaptor/pipeline/DepthPass.h>
#include <render/adaptor/pipeline/BRDFLutPass.h>
#include <render/adaptor/pipeline/ShadowMapPass.h>
#include <render/adaptor/pipeline/TileShadowPass.h>
#include <render/adaptor/pipeline/EmptyPass.h>
#include <memory>

Expand Down Expand Up @@ -40,10 +41,12 @@ namespace sky {

rhi::ImagePtr hizDepth;
rhi::SamplerPtr pointSampler;
rhi::SamplerPtr shadowAtlasSampler;

std::unique_ptr<DepthPass> depth;
std::unique_ptr<HizGenerator> hiz;
std::unique_ptr<ShadowMapPass> shadowMap;
std::unique_ptr<TileShadowPass> tileShadow;
std::unique_ptr<ForwardMSAAPass> forward;
std::unique_ptr<PostProcessingPass> postProcess;
std::unique_ptr<PresentPass> present;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
112 changes: 112 additions & 0 deletions engine/render/adaptor/include/render/adaptor/pipeline/TileShadowPass.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// Created by blues on 2024/9/6.
//

#pragma once

#include <render/renderpass/RasterPass.h>
#include <render/adaptor/pipeline/DefaultPassConstants.h>
#include <render/resource/Buffer.h>
#include <render/RenderBuiltinLayout.h>
#include <array>

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<std::unique_ptr<ShadowSlotPass>, MAX_LIGHTS> slotPasses;

// Shadow scene views (created lazily, owned by RenderScene)
std::array<SceneView *, MAX_LIGHTS> shadowViews{};

// Persistent GPU shadow atlas
rhi::ImagePtr atlasImage;
rhi::ImageViewPtr atlasFullView; // Full Texture2DArray view for shader sampling
std::array<rhi::ImageViewPtr, MAX_LIGHTS> 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
36 changes: 36 additions & 0 deletions engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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));
Expand All @@ -83,6 +96,9 @@ namespace sky {
shadowMap = std::make_unique<ShadowMapPass>(4096, 4096);
shadowMap->SetLayout(defaultRasterLayout);

tileShadow = std::make_unique<TileShadowPass>();
tileShadow->SetLayout(defaultRasterLayout);

brdfLut = std::make_unique<BRDFLutPass>(brdfTech);
postProcess = std::make_unique<PostProcessingPass>(postTech);
present = std::make_unique<PresentPass>(output->GetSwapChain());
Expand Down Expand Up @@ -155,21 +171,41 @@ 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)
{
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());

Expand Down
Loading