From 890b2da1089df6cbf11bf28a10d840b81bfd0f79 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 11 Apr 2024 15:35:18 +0200 Subject: [PATCH] Add **shadows caching for static geometry** --- Source/Engine/Renderer/ShadowsPass.cpp | 480 +++++++++++++++++++++---- Source/Engine/Renderer/ShadowsPass.h | 5 +- Source/Shaders/Quad.shader | 13 + 3 files changed, 433 insertions(+), 65 deletions(-) diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index a99a3fce3..32ec246aa 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -20,6 +20,7 @@ #define SHADOWS_MAX_TILES 6 #define SHADOWS_MIN_RESOLUTION 16 +#define SHADOWS_BASE_LIGHT_RESOLUTION(atlasResolution) atlasResolution / MAX_CSM_CASCADES // Allow to store 4 CSM cascades in a single row in all cases #define NormalOffsetScaleTweak 100.0f #define LocalLightNearPlane 10.0f @@ -61,12 +62,14 @@ uint16 QuantizeResolution(float input) struct ShadowAtlasLightTile { ShadowsAtlasRectTile* RectTile; + ShadowsAtlasRectTile* StaticRectTile; Matrix WorldToShadow; float FramesToUpdate; // Amount of frames (with fraction) until the next shadow update can happen bool SkipUpdate; + bool HasStaticGeometry; Viewport CachedViewport; // The viewport used the last time to render shadow to the atlas - void Free(ShadowsCustomBuffer* buffer) + void FreeDynamic(ShadowsCustomBuffer* buffer) { if (RectTile) { @@ -75,6 +78,28 @@ struct ShadowAtlasLightTile } } + void FreeStatic(ShadowsCustomBuffer* buffer) + { + if (StaticRectTile) + { + StaticRectTile->Free((ShadowsCustomBuffer*)nullptr); + StaticRectTile = nullptr; + } + } + + void Free(ShadowsCustomBuffer* buffer) + { + FreeDynamic(buffer); + FreeStatic(buffer); + } + + void ClearDynamic() + { + RectTile = nullptr; + FramesToUpdate = 0; + SkipUpdate = false; + } + void SetWorldToShadow(const Matrix& shadowViewProjection) { // Transform Clip Space [-1,+1]^2 to UV Space [0,1]^2 (saves MAD instruction in shader) @@ -92,9 +117,11 @@ struct ShadowAtlasLightTile // State for shadow cache sed to invalidate any prerendered shadow depths struct ShadowAtlasLightCache { - bool Valid; + bool StaticValid; + bool DynamicValid; float ShadowsUpdateRate; float ShadowsUpdateRateAtDistance; + float OuterConeAngle; Float3 Position; float Radius; Float3 Direction; @@ -103,15 +130,16 @@ struct ShadowAtlasLightCache void Set(const RenderView& view, const RenderLightData& light, const Float4& cascadeSplits = Float4::Zero) { - Valid = true; + StaticValid = true; + DynamicValid = true; Distance = light.ShadowsDistance; ShadowsUpdateRate = light.ShadowsUpdateRate; ShadowsUpdateRateAtDistance = light.ShadowsUpdateRateAtDistance; + Direction = light.Direction; if (light.IsDirectionalLight) { // Sun Position = view.Position; - Direction = light.Direction; CascadeSplits = cascadeSplits; } else @@ -120,6 +148,8 @@ struct ShadowAtlasLightCache const auto& localLight = (const RenderLocalLightData&)light; Position = light.Position; Radius = localLight.Radius; + if (light.IsSpotLight) + OuterConeAngle = ((const RenderSpotLightData&)light).OuterConeAngle; } } }; @@ -127,12 +157,37 @@ struct ShadowAtlasLightCache // State for light's shadows rendering struct ShadowAtlasLight { + // Static shadow map is created in 2 passes: + // - once to check if any static objects are in-use per tile (ShadowAtlasLightTile::HasStaticGeometry) + // - then to render those objects into the shadow map. + // When any static objects gets modified in the light range the second step is repeated. + // When light is changed then both steps are repeated. + enum StaticStates + { + // Not using static shadow map at all. + Unused, + // Static objects are rendered separately to dynamic objects to check if light projections need to allocate static shadow map. + WaitForGeometryCheck, + // Static objects will be rendered into static shadow map. + UpdateStaticShadow, + // Static objects are up-to-date and can be copied from static shadow map. + CopyStaticShadow, + // None of the tiles has static geometry nearby. + NoStaticGeometry, + // One of the tiles failed to insert into static atlas so fallback to default dynamic logic. + FailedToInsertTiles, + }; + uint64 LastFrameUsed; int32 ContextIndex; int32 ContextCount; uint16 Resolution; + uint16 StaticResolution; uint8 TilesNeeded; uint8 TilesCount; + bool HasStaticShadowContext; + StaticStates StaticState; + BoundingSphere Bounds; float Sharpness, Fade, NormalOffsetScale, Bias, FadeDistance, Distance, TileBorder; Float4 CascadeSplits; ShadowAtlasLightTile Tiles[SHADOWS_MAX_TILES]; @@ -145,6 +200,16 @@ struct ShadowAtlasLight POD_COPYABLE(ShadowAtlasLight); + bool HasStaticGeometry() const + { + for (auto& tile : Tiles) + { + if (tile.HasStaticGeometry) + return true; + } + return false; + } + float CalculateUpdateRateInv(const RenderLightData& light, float distanceFromView, bool& freezeUpdate) const { const float shadowsUpdateRate = light.ShadowsUpdateRate; @@ -159,24 +224,24 @@ struct ShadowAtlasLight void ValidateCache(const RenderView& view, const RenderLightData& light) { - if (!Cache.Valid) + if (!Cache.StaticValid || !Cache.DynamicValid) return; if (!Math::NearEqual(Cache.Distance, light.ShadowsDistance) || !Math::NearEqual(Cache.ShadowsUpdateRate, light.ShadowsUpdateRate) || - !Math::NearEqual(Cache.ShadowsUpdateRateAtDistance, light.ShadowsUpdateRateAtDistance)) + !Math::NearEqual(Cache.ShadowsUpdateRateAtDistance, light.ShadowsUpdateRateAtDistance) || + Float3::Dot(Cache.Direction, light.Direction) < 0.999999f) { // Invalidate - Cache.Valid = false; + Cache.StaticValid = false; } if (light.IsDirectionalLight) { // Sun - if (Float3::Dot(Cache.Direction, light.Direction) < 0.999999f || - !Float3::NearEqual(Cache.Position, view.Position, 1.0f) || + if (!Float3::NearEqual(Cache.Position, view.Position, 1.0f) || !Float4::NearEqual(Cache.CascadeSplits, CascadeSplits)) { // Invalidate - Cache.Valid = false; + Cache.StaticValid = false; } } else @@ -187,33 +252,43 @@ struct ShadowAtlasLight !Math::NearEqual(Cache.Radius, localLight.Radius)) { // Invalidate - Cache.Valid = false; + Cache.StaticValid = false; + } + if (light.IsSpotLight && !Math::NearEqual(Cache.OuterConeAngle, ((const RenderSpotLightData&)light).OuterConeAngle)) + { + // Invalidate + Cache.StaticValid = false; } } - for (int32 i = 0; i < TilesCount && Cache.Valid; i++) + Cache.DynamicValid &= Cache.StaticValid; + for (int32 i = 0; i < TilesCount && !Cache.DynamicValid; i++) { auto& tile = Tiles[i]; if (tile.CachedViewport != Viewport(tile.RectTile->X, tile.RectTile->Y, tile.RectTile->Width, tile.RectTile->Height)) { // Invalidate - Cache.Valid = false; + Cache.DynamicValid = false; } } } }; -class ShadowsCustomBuffer : public RenderBuffers::CustomBuffer +class ShadowsCustomBuffer : public RenderBuffers::CustomBuffer, public ISceneRenderingListener { public: int32 MaxShadowsQuality = 0; int32 Resolution = 0; int32 AtlasPixelsUsed = 0; + bool EnableStaticShadows = true; mutable bool ClearShadowMapAtlas = true; + mutable bool ClearStaticShadowMapAtlas = false; Vector3 ViewOrigin; GPUTexture* ShadowMapAtlas = nullptr; + GPUTexture* StaticShadowMapAtlas = nullptr; DynamicTypedBuffer ShadowsBuffer; GPUBufferView* ShadowsBufferView = nullptr; ShadowsAtlasRectTile* AtlasTiles = nullptr; // TODO: optimize with a single allocation for atlas tiles + ShadowsAtlasRectTile* StaticAtlasTiles = nullptr; // TODO: optimize with a single allocation for atlas tiles Dictionary Lights; ShadowsCustomBuffer() @@ -222,41 +297,106 @@ public: ShadowMapAtlas = GPUDevice::Instance->CreateTexture(TEXT("Shadow Map Atlas")); } - void ClearTiles() + void ClearDynamic() { ClearShadowMapAtlas = true; AtlasPixelsUsed = 0; - SAFE_DELETE(AtlasTiles); for (auto it = Lights.Begin(); it.IsNotEnd(); ++it) { auto& atlasLight = it->Value; - Platform::MemoryClear(atlasLight.Tiles, sizeof(atlasLight.Tiles)); - Platform::MemoryClear(&atlasLight.Cache, sizeof(atlasLight.Cache)); + atlasLight.Cache.DynamicValid = false; + for (int32 i = 0; i < atlasLight.TilesCount; i++) + atlasLight.Tiles[i].ClearDynamic(); } + SAFE_DELETE(AtlasTiles); } void Reset() { Lights.Clear(); - ClearTiles(); + SAFE_DELETE(StaticAtlasTiles); + ClearDynamic(); ViewOrigin = Vector3::Zero; } + void InitStaticAtlas() + { + if (StaticAtlasTiles) + return; + const int32 atlasResolution = Resolution * 2; + StaticAtlasTiles = New(0, 0, atlasResolution, atlasResolution); + if (!StaticShadowMapAtlas) + StaticShadowMapAtlas = GPUDevice::Instance->CreateTexture(TEXT("Static Shadow Map Atlas")); + auto desc = ShadowMapAtlas->GetDescription(); + desc.Width = desc.Height = atlasResolution; + if (StaticShadowMapAtlas->Init(desc)) + { + LOG(Fatal, "Failed to setup shadow map of size {0}x{1} and format {2}", desc.Width, desc.Height, ScriptingEnum::ToString(desc.Format)); + return; + } + ClearStaticShadowMapAtlas = true; + } + + void DirtyStaticBounds(const BoundingSphere& bounds) + { + // TODO: use octree to improve bounds-testing + // TODO: build list of modified bounds and dirty them in batch on next frame start (ideally in async within shadows setup job) + for (auto& e : Lights) + { + auto& atlasLight = e.Value; + if (atlasLight.StaticState == ShadowAtlasLight::CopyStaticShadow && atlasLight.Bounds.Intersects(bounds)) + { + // Invalidate static shadow + atlasLight.Cache.StaticValid = false; + } + } + } + ~ShadowsCustomBuffer() { Reset(); SAFE_DELETE_GPU_RESOURCE(ShadowMapAtlas); + SAFE_DELETE_GPU_RESOURCE(StaticShadowMapAtlas); + } + + // [ISceneRenderingListener] + void OnSceneRenderingAddActor(Actor* a) override + { + if (a->HasStaticFlag(StaticFlags::Shadow)) + DirtyStaticBounds(a->GetSphere()); + } + + void OnSceneRenderingUpdateActor(Actor* a, const BoundingSphere& prevBounds) override + { + // Dirty static objects to redraw when changed (eg. material modification) + if (a->HasStaticFlag(StaticFlags::Shadow)) + { + DirtyStaticBounds(prevBounds); + DirtyStaticBounds(a->GetSphere()); + } + } + + void OnSceneRenderingRemoveActor(Actor* a) override + { + if (a->HasStaticFlag(StaticFlags::Shadow)) + DirtyStaticBounds(a->GetSphere()); + } + + void OnSceneRenderingClear(SceneRendering* scene) override + { } }; void ShadowsAtlasRectTile::OnInsert(ShadowsCustomBuffer* buffer) { - buffer->AtlasPixelsUsed += (int32)Width * (int32)Height; + if (buffer) + buffer->AtlasPixelsUsed += (int32)Width * (int32)Height; } void ShadowsAtlasRectTile::OnFree(ShadowsCustomBuffer* buffer) { - buffer->AtlasPixelsUsed -= (int32)Width * (int32)Height; + if (buffer) + buffer->AtlasPixelsUsed -= (int32)Width * (int32)Height; } String ShadowsPass::ToString() const @@ -353,11 +493,23 @@ bool ShadowsPass::setupResources() if (_psDepthClear->Init(psDesc)) return true; } + if (_psDepthCopy == nullptr) + { + psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; + psDesc.PS = GPUDevice::Instance->QuadShader->GetPS("PS_DepthCopy"); + psDesc.DepthEnable = true; + psDesc.DepthWriteEnable = true; + psDesc.DepthFunc = ComparisonFunc::Always; + psDesc.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::None; + _psDepthCopy = GPUDevice::Instance->CreatePipelineState(); + if (_psDepthCopy->Init(psDesc)) + return true; + } return false; } -void ShadowsPass::SetupRenderContext(RenderContext& renderContext, RenderContext& shadowContext) +void ShadowsPass::SetupRenderContext(RenderContext& renderContext, RenderContext& shadowContext, ShadowAtlasLight* atlasLight, RenderContext* dynamicContext) { const auto& view = renderContext.View; @@ -366,14 +518,31 @@ void ShadowsPass::SetupRenderContext(RenderContext& renderContext, RenderContext // Prepare properties auto& shadowView = shadowContext.View; - shadowView.Flags = view.Flags; - shadowView.StaticFlagsMask = view.StaticFlagsMask; - shadowView.RenderLayersMask = view.RenderLayersMask; - shadowView.IsOfflinePass = view.IsOfflinePass; - shadowView.ModelLODBias = view.ModelLODBias; - shadowView.ModelLODDistanceFactor = view.ModelLODDistanceFactor; - shadowView.Pass = DrawPass::Depth; - shadowView.Origin = view.Origin; + if (dynamicContext) + { + // Duplicate dynamic view but with static only geometry + shadowView = dynamicContext->View; + shadowView.StaticFlagsMask = StaticFlags::Shadow; + shadowView.StaticFlagsCompare = StaticFlags::Shadow; + } + else + { + shadowView.Flags = view.Flags; + shadowView.StaticFlagsMask = view.StaticFlagsMask; + shadowView.StaticFlagsCompare = view.StaticFlagsCompare; + shadowView.RenderLayersMask = view.RenderLayersMask; + shadowView.IsOfflinePass = view.IsOfflinePass; + shadowView.ModelLODBias = view.ModelLODBias; + shadowView.ModelLODDistanceFactor = view.ModelLODDistanceFactor; + shadowView.Pass = DrawPass::Depth; + shadowView.Origin = view.Origin; + if (atlasLight && atlasLight->StaticState != ShadowAtlasLight::Unused && atlasLight->StaticState != ShadowAtlasLight::FailedToInsertTiles) + { + // Draw only dynamic geometry + shadowView.StaticFlagsMask = StaticFlags::Shadow; + shadowView.StaticFlagsCompare = StaticFlags::None; + } + } shadowContext.List = RenderList::GetFromPool(); shadowContext.Buffers = renderContext.Buffers; shadowContext.Task = renderContext.Task; @@ -389,11 +558,14 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render atlasLight.Bias = light.ShadowsDepthBias; atlasLight.FadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f); atlasLight.Distance = Math::Min(renderContext.View.Far, light.ShadowsDistance); + atlasLight.Bounds.Center = light.Position + renderContext.View.Position; + atlasLight.Bounds.Radius = 0.0f; } bool ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLocalLightData& light, ShadowAtlasLight& atlasLight) { SetupLight(shadows, renderContext, renderContextBatch, (RenderLightData&)light, atlasLight); + atlasLight.Bounds.Radius = light.Radius; // Fade shadow on distance const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f); @@ -404,11 +576,83 @@ bool ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render // Update cached state (invalidate it if the light changed) atlasLight.ValidateCache(renderContext.View, light); + // Update static shadow logic + atlasLight.HasStaticShadowContext = shadows.EnableStaticShadows && EnumHasAllFlags(light.StaticFlags, StaticFlags::Shadow); + if (!atlasLight.HasStaticShadowContext) + atlasLight.StaticState = ShadowAtlasLight::Unused; + switch (atlasLight.StaticState) + { + case ShadowAtlasLight::Unused: + if (atlasLight.HasStaticShadowContext) + atlasLight.StaticState = ShadowAtlasLight::WaitForGeometryCheck; + break; + case ShadowAtlasLight::WaitForGeometryCheck: + if (atlasLight.HasStaticGeometry()) + { + // Calculate static resolution for the light based on the world-bounds, not view-dependant + shadows.InitStaticAtlas(); + const int32 baseLightResolution = SHADOWS_BASE_LIGHT_RESOLUTION(shadows.Resolution); + int32 staticResolution = Math::RoundToInt(Math::Saturate(light.Radius / 1000.0f) * baseLightResolution); + if (!Math::IsPowerOfTwo(staticResolution)) + staticResolution = Math::RoundUpToPowerOf2(staticResolution); + atlasLight.StaticResolution = staticResolution; + + // Allocate static shadow map slot for all used tiles + for (int32 tileIndex = 0; tileIndex < atlasLight.TilesCount; tileIndex++) + { + auto& tile = atlasLight.Tiles[tileIndex]; + if (tile.StaticRectTile == nullptr) + { + tile.StaticRectTile = shadows.StaticAtlasTiles->Insert(atlasLight.StaticResolution, atlasLight.StaticResolution, 0, (ShadowsCustomBuffer*)nullptr); + if (!tile.StaticRectTile) + { + // Failed to insert tile to switch back to the default rendering + atlasLight.StaticState = ShadowAtlasLight::FailedToInsertTiles; + for (int32 i = 0; i < tileIndex; i++) + atlasLight.Tiles[i].FreeStatic(&shadows); + break; + } + } + } + if (atlasLight.StaticState == ShadowAtlasLight::WaitForGeometryCheck) + { + // Now we know the tiles with static geometry and we can render those + atlasLight.StaticState = ShadowAtlasLight::UpdateStaticShadow; + } + } + else + { + // Not using static geometry for this light shadows + atlasLight.StaticState = ShadowAtlasLight::NoStaticGeometry; + } + break; + case ShadowAtlasLight::CopyStaticShadow: + // Light was modified so update the static shadows + if (!atlasLight.Cache.StaticValid && atlasLight.HasStaticShadowContext) + atlasLight.StaticState = ShadowAtlasLight::UpdateStaticShadow; + break; + } + switch (atlasLight.StaticState) + { + case ShadowAtlasLight::CopyStaticShadow: + case ShadowAtlasLight::NoStaticGeometry: + case ShadowAtlasLight::FailedToInsertTiles: + // Skip collecting static draws + atlasLight.HasStaticShadowContext = false; + break; + } + if (atlasLight.HasStaticShadowContext) + { + // If rendering finds any static draws then it's set to true + for (auto& tile : atlasLight.Tiles) + tile.HasStaticGeometry = false; + } + // Calculate update rate based on the distance to the view bool freezeUpdate; const float updateRateInv = atlasLight.CalculateUpdateRateInv(light, dstLightToView, freezeUpdate); float& framesToUpdate = atlasLight.Tiles[0].FramesToUpdate; // Use the first tile for all local light projections to be in sync - if ((framesToUpdate > 0.0f || freezeUpdate) && atlasLight.Cache.Valid) + if ((framesToUpdate > 0.0f || freezeUpdate) && atlasLight.Cache.DynamicValid && !atlasLight.HasStaticShadowContext) { // Light state matches the cached state and the update rate allows us to reuse the cached shadow map so skip update if (!freezeUpdate) @@ -512,7 +756,7 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render bool freezeUpdate; const float updateRateInv = atlasLight.CalculateUpdateRateInv(light, dstToCascade, freezeUpdate); auto& tile = atlasLight.Tiles[cascadeIndex]; - if ((tile.FramesToUpdate > 0.0f || freezeUpdate) && atlasLight.Cache.Valid) + if ((tile.FramesToUpdate > 0.0f || freezeUpdate) && atlasLight.Cache.DynamicValid) { // Light state matches the cached state and the update rate allows us to reuse the cached shadow map so skip update if (!freezeUpdate) @@ -639,14 +883,16 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render Matrix borderScaleMatrix; Matrix::Scaling(borderScale, borderScale, 1.0f, borderScaleMatrix); - // Render depth to all 6 faces of the cube map atlasLight.ContextIndex = renderContextBatch.Contexts.Count(); - atlasLight.ContextCount = 6; + atlasLight.ContextCount = atlasLight.HasStaticShadowContext ? 12 : 6; renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount); + + // Render depth to all 6 faces of the cube map + int32 contextIndex = 0; for (int32 faceIndex = 0; faceIndex < 6; faceIndex++) { - auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + faceIndex]; - SetupRenderContext(renderContext, shadowContext); + auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + contextIndex++]; + SetupRenderContext(renderContext, shadowContext, &atlasLight); shadowContext.View.SetUpCube(LocalLightNearPlane, light.Radius, light.Position); // Apply border to the projection matrix @@ -658,6 +904,13 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render const auto shadowMapsSize = (float)atlasLight.Resolution; shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &renderContext.View); atlasLight.Tiles[faceIndex].SetWorldToShadow(shadowContext.View.ViewProjection()); + + // Draw static geometry separately to be cached + if (atlasLight.HasStaticShadowContext) + { + auto& shadowContextStatic = renderContextBatch.Contexts[atlasLight.ContextIndex + contextIndex++]; + SetupRenderContext(renderContext, shadowContextStatic, &atlasLight, &shadowContext); + } } } @@ -666,16 +919,23 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render if (SetupLight(shadows, renderContext, renderContextBatch, (RenderLocalLightData&)light, atlasLight)) return; - // Render depth to a single projection atlasLight.ContextIndex = renderContextBatch.Contexts.Count(); - atlasLight.ContextCount = 1; + atlasLight.ContextCount = atlasLight.HasStaticShadowContext ? 2 : 1; renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount); + + // Render depth to a single projection auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex]; - SetupRenderContext(renderContext, shadowContext); + SetupRenderContext(renderContext, shadowContext, &atlasLight); shadowContext.View.SetProjector(LocalLightNearPlane, light.Radius, light.Position, light.Direction, light.UpVector, light.OuterConeAngle * 2.0f); - const auto shadowMapsSize = (float)atlasLight.Resolution; - shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &renderContext.View); + shadowContext.View.PrepareCache(shadowContext, atlasLight.Resolution, atlasLight.Resolution, Float2::Zero, &renderContext.View); atlasLight.Tiles[0].SetWorldToShadow(shadowContext.View.ViewProjection()); + + // Draw static geometry separately to be cached + if (atlasLight.HasStaticShadowContext) + { + auto& shadowContextStatic = renderContextBatch.Contexts[atlasLight.ContextIndex + 1]; + SetupRenderContext(renderContext, shadowContextStatic, &atlasLight, &shadowContext); + } } void ShadowsPass::Dispose() @@ -690,6 +950,7 @@ void ShadowsPass::Dispose() _shader = nullptr; _sphereModel = nullptr; SAFE_DELETE_GPU_RESOURCE(_psDepthClear); + SAFE_DELETE_GPU_RESOURCE(_psDepthCopy); } void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch& renderContextBatch) @@ -724,6 +985,7 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch& const auto currentFrame = Engine::FrameCount; shadows.LastFrameUsed = currentFrame; shadows.MaxShadowsQuality = Math::Clamp(Math::Min((int32)Graphics::ShadowsQuality, (int32)renderContext.View.MaxShadowsQuality), 0, (int32)Quality::MAX - 1); + shadows.EnableStaticShadows = !renderContext.View.IsOfflinePass && !renderContext.View.IsSingleFrame; int32 atlasResolution; switch (Graphics::ShadowMapsQuality) { @@ -742,7 +1004,6 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch& default: return; } - const int32 baseLightResolution = atlasResolution / MAX_CSM_CASCADES; // Allow to store 4 CSM cascades in a single row in all cases if (shadows.Resolution != atlasResolution) { shadows.Reset(); @@ -765,6 +1026,7 @@ void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch& shadows.AtlasTiles = New(0, 0, atlasResolution, atlasResolution); // Update/add lights + const int32 baseLightResolution = SHADOWS_BASE_LIGHT_RESOLUTION(atlasResolution); for (const RenderLightData* light : shadowedLights) { auto& atlasLight = shadows.Lights[light->ID]; @@ -846,8 +1108,9 @@ RETRY_ATLAS_SETUP: continue; // Remove existing tiles + atlasLight.Cache.DynamicValid = false; for (ShadowAtlasLightTile& tile : atlasLight.Tiles) - tile.Free(&shadows); + tile.FreeDynamic(&shadows); } // Insert tiles into the atlas (already sorted to favor the first ones) @@ -866,7 +1129,7 @@ RETRY_ATLAS_SETUP: { // Free any previous tiles that were added for (int32 i = 0; i < tileIndex; i++) - atlasLight.Tiles[i].Free(&shadows); + atlasLight.Tiles[i].FreeDynamic(&shadows); failedToInsert = true; break; } @@ -887,7 +1150,7 @@ RETRY_ATLAS_SETUP: } // Rebuild atlas - shadows.ClearTiles(); + shadows.ClearDynamic(); shadows.AtlasTiles = New(0, 0, atlasResolution, atlasResolution); goto RETRY_ATLAS_SETUP; } @@ -901,7 +1164,9 @@ RETRY_ATLAS_SETUP: { // Invalidate cache when whole atlas will be cleared if (shadows.ClearShadowMapAtlas) - atlasLight.Cache.Valid = false; + atlasLight.Cache.DynamicValid = false; + if (shadows.ClearStaticShadowMapAtlas) + atlasLight.Cache.StaticValid = false; light->HasShadow = true; atlasLight.TilesCount = atlasLight.TilesNeeded; @@ -913,6 +1178,12 @@ RETRY_ATLAS_SETUP: SetupLight(shadows, renderContext, renderContextBatch, *(RenderDirectionalLightData*)light, atlasLight); } } + if (shadows.StaticAtlasTiles) + { + // Register for active scenes changes to invalidate static shadows + for (SceneRendering* scene : renderContext.List->Scenes) + shadows.ListenSceneRendering(scene); + } #undef IS_LIGHT_TILE_REUSABLE @@ -970,41 +1241,110 @@ void ShadowsPass::RenderShadowMaps(RenderContextBatch& renderContextBatch) const ShadowsCustomBuffer& shadows = *shadowsPtr; GPUContext* context = GPUDevice::Instance->GetMainContext(); context->ResetSR(); - context->SetRenderTarget(shadows.ShadowMapAtlas->View(), (GPUTextureView*)nullptr); - GPUConstantBuffer* quadShaderCB; - if (shadows.ClearShadowMapAtlas) + GPUConstantBuffer* quadShaderCB = GPUDevice::Instance->QuadShader->GetCB(0); + QuadShaderData quadShaderData; + + // Update static shadows + if (shadows.StaticShadowMapAtlas) { - context->ClearDepth(shadows.ShadowMapAtlas->View()); - } - else - { - QuadShaderData quadShaderData; - quadShaderData.Color = Float4::One; // Color.r is used by PS_DepthClear in Quad shader to clear depth - quadShaderCB = GPUDevice::Instance->QuadShader->GetCB(0); - context->UpdateCB(quadShaderCB, &quadShaderData); + PROFILE_GPU_CPU("Static"); + if (shadows.ClearStaticShadowMapAtlas) + context->ClearDepth(shadows.StaticShadowMapAtlas->View()); + bool renderedAny = false; + for (auto& e : shadows.Lights) + { + ShadowAtlasLight& atlasLight = e.Value; + if (atlasLight.StaticState != ShadowAtlasLight::UpdateStaticShadow || !atlasLight.HasStaticShadowContext) + continue; + int32 contextIndex = 0; + for (int32 tileIndex = 0; tileIndex < atlasLight.TilesCount; tileIndex++) + { + ShadowAtlasLightTile& tile = atlasLight.Tiles[tileIndex]; + if (!tile.RectTile) + break; + if (!tile.StaticRectTile) + continue; + if (!renderedAny) + { + renderedAny = true; + context->SetRenderTarget(shadows.StaticShadowMapAtlas->View(), (GPUTextureView*)nullptr); + } + + // Set viewport for tile + context->SetViewportAndScissors(Viewport(tile.StaticRectTile->X, tile.StaticRectTile->Y, tile.StaticRectTile->Width, tile.StaticRectTile->Height)); + if (!shadows.ClearStaticShadowMapAtlas) + { + // Color.r is used by PS_DepthClear in Quad shader to clear depth + quadShaderData.Color = Float4::One; + context->UpdateCB(quadShaderCB, &quadShaderData); + context->BindCB(0, quadShaderCB); + + // Clear tile depth + context->SetState(_psDepthClear); + context->DrawFullscreenTriangle(); + } + + // Draw objects depth + contextIndex++; // Skip dynamic context + auto& shadowContextStatic = renderContextBatch.Contexts[atlasLight.ContextIndex + contextIndex++]; + if (!shadowContextStatic.List->DrawCallsLists[(int32)DrawCallsListType::Depth].IsEmpty() || !shadowContextStatic.List->ShadowDepthDrawCallsList.IsEmpty()) + { + shadowContextStatic.List->ExecuteDrawCalls(shadowContextStatic, DrawCallsListType::Depth); + shadowContextStatic.List->ExecuteDrawCalls(shadowContextStatic, shadowContextStatic.List->ShadowDepthDrawCallsList, renderContext.List->DrawCalls, nullptr); + tile.HasStaticGeometry = true; + } + } + + // Go into copying shadow for the next draw + atlasLight.StaticState = ShadowAtlasLight::CopyStaticShadow; + } + shadows.ClearStaticShadowMapAtlas = false; + if (renderedAny) + { + context->ResetSR(); + context->ResetRenderTarget(); + } } // Render depth to all shadow map tiles + if (shadows.ClearShadowMapAtlas) + context->ClearDepth(shadows.ShadowMapAtlas->View()); + context->SetRenderTarget(shadows.ShadowMapAtlas->View(), (GPUTextureView*)nullptr); for (auto& e : shadows.Lights) { - const ShadowAtlasLight& atlasLight = e.Value; + ShadowAtlasLight& atlasLight = e.Value; int32 contextIndex = 0; for (int32 tileIndex = 0; tileIndex < atlasLight.TilesCount; tileIndex++) { - const ShadowAtlasLightTile& tile = atlasLight.Tiles[tileIndex]; + ShadowAtlasLightTile& tile = atlasLight.Tiles[tileIndex]; if (!tile.RectTile) break; if (tile.SkipUpdate) continue; // Set viewport for tile - ASSERT_LOW_LAYER(tile.CachedViewport == Viewport(tile.RectTile->X, tile.RectTile->Y, tile.RectTile->Width, tile.RectTile->Height)); context->SetViewportAndScissors(tile.CachedViewport); - - if (!shadows.ClearShadowMapAtlas) + if (tile.StaticRectTile && atlasLight.StaticState == ShadowAtlasLight::CopyStaticShadow) { - // Clear tile depth + // Color.xyzw is used by PS_DepthCopy in Quad shader to scale input texture UVs + const float staticAtlasResolutionInv = 1.0f / shadows.StaticShadowMapAtlas->Width(); + quadShaderData.Color = Float4(tile.StaticRectTile->Width, tile.StaticRectTile->Height, tile.StaticRectTile->X, tile.StaticRectTile->Y) * staticAtlasResolutionInv; + context->UpdateCB(quadShaderCB, &quadShaderData); context->BindCB(0, quadShaderCB); + + // Copy tile depth + context->BindSR(0, shadows.StaticShadowMapAtlas->View()); + context->SetState(_psDepthCopy); + context->DrawFullscreenTriangle(); + } + else if (!shadows.ClearShadowMapAtlas) + { + // Color.r is used by PS_DepthClear in Quad shader to clear depth + quadShaderData.Color = Float4::One; + context->UpdateCB(quadShaderCB, &quadShaderData); + context->BindCB(0, quadShaderCB); + + // Clear tile depth context->SetState(_psDepthClear); context->DrawFullscreenTriangle(); } @@ -1013,6 +1353,20 @@ void ShadowsPass::RenderShadowMaps(RenderContextBatch& renderContextBatch) auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + contextIndex++]; shadowContext.List->ExecuteDrawCalls(shadowContext, DrawCallsListType::Depth); shadowContext.List->ExecuteDrawCalls(shadowContext, shadowContext.List->ShadowDepthDrawCallsList, renderContext.List->DrawCalls, nullptr); + if (atlasLight.HasStaticShadowContext) + { + auto& shadowContextStatic = renderContextBatch.Contexts[atlasLight.ContextIndex + contextIndex++]; + if (!shadowContextStatic.List->DrawCallsLists[(int32)DrawCallsListType::Depth].IsEmpty() || !shadowContextStatic.List->ShadowDepthDrawCallsList.IsEmpty()) + { + if (atlasLight.StaticState != ShadowAtlasLight::CopyStaticShadow) + { + // Draw static objects directly to the shadow map + shadowContextStatic.List->ExecuteDrawCalls(shadowContextStatic, DrawCallsListType::Depth); + shadowContextStatic.List->ExecuteDrawCalls(shadowContextStatic, shadowContextStatic.List->ShadowDepthDrawCallsList, renderContext.List->DrawCalls, nullptr); + } + tile.HasStaticGeometry = true; + } + } } } diff --git a/Source/Engine/Renderer/ShadowsPass.h b/Source/Engine/Renderer/ShadowsPass.h index 5e9421d2c..df647aac6 100644 --- a/Source/Engine/Renderer/ShadowsPass.h +++ b/Source/Engine/Renderer/ShadowsPass.h @@ -18,6 +18,7 @@ private: AssetReference _shader; AssetReference _sphereModel; GPUPipelineState* _psDepthClear = nullptr; + GPUPipelineState* _psDepthCopy = nullptr; GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowDir; GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowPoint; GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowSpot; @@ -51,8 +52,8 @@ public: static void GetShadowAtlas(const RenderBuffers* renderBuffers, GPUTexture*& shadowMapAtlas, GPUBufferView*& shadowsBuffer); private: - static void SetupRenderContext(RenderContext& renderContext, RenderContext& shadowContext); - static void SetupLight(class ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLightData& light, struct ShadowAtlasLight& atlasLight); + static void SetupRenderContext(RenderContext& renderContext, RenderContext& shadowContext, struct ShadowAtlasLight* atlasLight = nullptr, RenderContext* dynamicContext = nullptr); + static void SetupLight(class ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLightData& light, ShadowAtlasLight& atlasLight); static bool SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderLocalLightData& light, ShadowAtlasLight& atlasLight); static void SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderDirectionalLightData& light, ShadowAtlasLight& atlasLight); static void SetupLight(ShadowsCustomBuffer& shadows, RenderContext& renderContext, RenderContextBatch& renderContextBatch, RenderPointLightData& light, ShadowAtlasLight& atlasLight); diff --git a/Source/Shaders/Quad.shader b/Source/Shaders/Quad.shader index da6fce92c..dcb401bc9 100644 --- a/Source/Shaders/Quad.shader +++ b/Source/Shaders/Quad.shader @@ -69,3 +69,16 @@ float PS_DepthClear(Quad_VS2PS input) : SV_Depth { return Color.r; } + +#ifdef _PS_DepthCopy + +Texture2D Source : register(t0); + +// Pixel Shader for copying depth buffer +META_PS(true, FEATURE_LEVEL_ES2) +float PS_DepthCopy(Quad_VS2PS input) : SV_Depth +{ + return Source.SampleLevel(SamplerPointClamp, input.TexCoord * Color.xy + Color.zw, 0).r; +} + +#endif