/
redgpu
/
ezEngine
Обзор
Документация
Войти
/
redgpu
/
ezEngine
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
Code/Engine/RendererCore/Lights/Implementation/ShadowPool.cpp
1 032 строки
38 KB
C-Core
Render Pipeline Blackboards (#1974)
06 июл 2026, 13:55
Не верифицирован
06 июл 2026, 13:55
5715bdc
Код
Авторство
О чём код?
#include <RendererCore/RendererCorePCH.h> #include <Core/GameApplication/GameApplicationBase.h> #include <Core/Graphics/Camera.h> #include <Core/Utils/Blackboard.h> #include <Foundation/Configuration/CVar.h> #include <Foundation/Configuration/Startup.h> #include <Foundation/Profiling/Profiling.h> #include <RendererCore/Debug/DebugRenderer.h> #include <RendererCore/Lights/DirectionalLightComponent.h> #include <RendererCore/Lights/Implementation/ShadowPool.h> #include <RendererCore/Lights/PointLightComponent.h> #include <RendererCore/Lights/SpotLightComponent.h> #include <RendererCore/Pipeline/View.h> #include <RendererCore/RenderGraph/RenderGraph.h> #include <RendererCore/RenderGraph/RenderGraphManager.h> #include <RendererCore/RenderWorld/RenderWorld.h> #include <RendererCore/Textures/DynamicTextureAtlas.h> #include <RendererCore/Utils/CoreRenderProfile.h> #include <RendererFoundation/CommandEncoder/CommandEncoder.h> #include <RendererFoundation/Device/Device.h> #include <RendererFoundation/Resources/Texture.h> #include <RendererFoundation/Shader/ShaderUtils.h> #include <Shaders/Common/LightData.h> // clang-format off EZ_BEGIN_SUBSYSTEM_DECLARATION(RendererCore, ShadowPool) BEGIN_SUBSYSTEM_DEPENDENCIES "Foundation", "Core", "RenderWorld" END_SUBSYSTEM_DEPENDENCIES ON_HIGHLEVELSYSTEMS_STARTUP { ezShadowPool::OnEngineStartup(); } ON_HIGHLEVELSYSTEMS_SHUTDOWN { ezShadowPool::OnEngineShutdown(); } EZ_END_SUBSYSTEM_DECLARATION; // clang-format on #if EZ_ENABLED(EZ_COMPILE_FOR_DEVELOPMENT) ezCVarBool cvar_RenderingShadowsShowAtlasTexture("Rendering.Shadows.ShowAtlasTexture", false, ezCVarFlags::Default, "Display the shadow atlas texture"); ezCVarBool cvar_RenderingShadowsVisCascadeBounds("Rendering.Shadows.VisCascadeBounds", false, ezCVarFlags::Default, "Visualizes the bounding volumes of shadow cascades"); #endif ezCVarFloat cvar_RenderingShadowsScaleMappingExponent("Rendering.Shadows.ScaleMappingExponent", 1.5f, ezCVarFlags::Default, "Determines how fast the shadow map size is reduced with screen space size"); /// NOTE: The default values for these are defined in ezCoreRenderProfileConfig /// but they can also be overwritten in custom game states at startup. EZ_RENDERERCORE_DLL ezCVarInt cvar_RenderingShadowsAtlasSize("Rendering.Shadows.AtlasSize", 4096, ezCVarFlags::RequiresDelayedSync, "The size of the shadow atlas texture."); EZ_RENDERERCORE_DLL ezCVarInt cvar_RenderingShadowsMaxShadowMapSize("Rendering.Shadows.MaxShadowMapSize", 1024, ezCVarFlags::RequiresDelayedSync, "The max shadow map size used."); EZ_RENDERERCORE_DLL ezCVarInt cvar_RenderingShadowsMinShadowMapSize("Rendering.Shadows.MinShadowMapSize", 64, ezCVarFlags::RequiresDelayedSync, "The min shadow map size used."); static ezUInt32 s_uiLastConfigModification = 0; static float s_fMinRelativeShadowMapSize = 0.0f; struct ShadowView { ezViewHandle m_hView; ezCamera m_Camera; ezCamera m_CullingCamera; }; struct ShadowData { ezHybridArray<ezViewHandle, 6> m_Views; ezUInt32 m_uiType; float m_fShadowMapScale; float m_fPenumbraSize; float m_fSlopeBias; float m_fConstantBias; float m_fFadeOutStart; float m_fMinRange; float m_fActualRange; ezUInt32 m_uiPackedDataOffset; // in 16 bytes steps }; struct LightAndRefView { EZ_DECLARE_POD_TYPE(); const ezLightComponent* m_pLight; const ezView* m_pReferenceView; }; struct SortedShadowData { EZ_DECLARE_POD_TYPE(); ezUInt32 m_uiIndex; float m_fShadowMapScale; EZ_ALWAYS_INLINE bool operator<(const SortedShadowData& other) const { if (m_fShadowMapScale > other.m_fShadowMapScale) // we want to sort descending (higher scale first) return true; return m_uiIndex < other.m_uiIndex; } }; static ezDynamicArray<SortedShadowData> s_SortedShadowData; static float ShadowMapScaleFromScreenSpaceSize(float fScreenSpaceSize) { const float fClampedShadowMapScale = ezMath::Clamp(ezMath::Pow(fScreenSpaceSize * 0.5f, cvar_RenderingShadowsScaleMappingExponent), s_fMinRelativeShadowMapSize, 1.0f); return fClampedShadowMapScale; } static float AddSafeBorder(ezAngle fov, float fPenumbraSize) { float fHalfHeight = ezMath::Tan(fov * 0.5f); float fNewFov = ezMath::ATan(fHalfHeight + fPenumbraSize).GetDegree() * 2.0f; return fNewFov; } ezTagSet s_ExcludeTagsWhiteList; static void CopyExcludeTagsOnWhiteList(const ezTagSet& referenceTags, ezTagSet& out_targetTags) { out_targetTags.Clear(); out_targetTags.SetByName("EditorHidden"); for (auto& tag : referenceTags) { if (s_ExcludeTagsWhiteList.IsSet(tag)) { out_targetTags.Set(tag); } } } // must not be in anonymous namespace template <> struct ezHashHelper<LightAndRefView> { EZ_ALWAYS_INLINE static ezUInt32 Hash(LightAndRefView value) { return ezHashingUtils::xxHash32(&value.m_pLight, sizeof(LightAndRefView)); } EZ_ALWAYS_INLINE static bool Equal(const LightAndRefView& a, const LightAndRefView& b) { return a.m_pLight == b.m_pLight && a.m_pReferenceView == b.m_pReferenceView; } }; ////////////////////////////////////////////////////////////////////////// struct ezShadowPool::Data { Data() { Clear(); } ~Data() { for (auto& shadowView : m_ShadowViews) { ezRenderWorld::DeleteView(shadowView.m_hView); } m_TextureAtlas.Deinitialize(); ezGALDevice::GetDefaultDevice()->DestroyBuffer(m_hShadowDataBuffer); } enum { MAX_SHADOW_DATA = 1024 }; void CreateShadowAtlasTexture() { if (m_TextureAtlas.IsInitialized() == false) { // use the current CVar values to initialize the values ezUInt32 uiAtlas = cvar_RenderingShadowsAtlasSize; ezUInt32 uiMax = cvar_RenderingShadowsMaxShadowMapSize; ezUInt32 uiMin = cvar_RenderingShadowsMinShadowMapSize; // if the platform profile has changed, use it to reset the defaults if (s_uiLastConfigModification != ezGameApplicationBase::GetGameApplicationBaseInstance()->GetPlatformProfile().GetLastModificationCounter()) { s_uiLastConfigModification = ezGameApplicationBase::GetGameApplicationBaseInstance()->GetPlatformProfile().GetLastModificationCounter(); const auto* pConfig = ezGameApplicationBase::GetGameApplicationBaseInstance()->GetPlatformProfile().GetTypeConfig<ezCoreRenderProfileConfig>(); uiAtlas = pConfig->m_uiShadowAtlasTextureSize; uiMax = pConfig->m_uiMaxShadowMapSize; uiMin = pConfig->m_uiMinShadowMapSize; } // if the CVars were modified recently (e.g. during game startup), use those values to override the default if (cvar_RenderingShadowsAtlasSize.HasDelayedSyncValueChanged()) uiAtlas = cvar_RenderingShadowsAtlasSize.GetValue(ezCVarValue::DelayedSync); if (cvar_RenderingShadowsMaxShadowMapSize.HasDelayedSyncValueChanged()) uiMax = cvar_RenderingShadowsMaxShadowMapSize.GetValue(ezCVarValue::DelayedSync); if (cvar_RenderingShadowsMinShadowMapSize.HasDelayedSyncValueChanged()) uiMin = cvar_RenderingShadowsMinShadowMapSize.GetValue(ezCVarValue::DelayedSync); // make sure the values are valid uiMax = ezMath::Clamp(ezMath::PowerOfTwo_Floor(uiMax), 64u, 2048u); uiMin = ezMath::Clamp(ezMath::PowerOfTwo_Floor(uiMin), 8u, 512u); uiMax = ezMath::Max(uiMin, uiMax); uiMin = ezMath::Min(uiMin, uiMax); uiAtlas = ezMath::Clamp(static_cast<ezUInt32>(ezMath::RoundToMultiple(double(uiAtlas), uiMax)), uiMax, 8192u); // write back the clamped values, so that everyone sees the valid values cvar_RenderingShadowsAtlasSize = uiAtlas; cvar_RenderingShadowsMaxShadowMapSize = uiMax; cvar_RenderingShadowsMinShadowMapSize = uiMin; // apply the new values cvar_RenderingShadowsAtlasSize.SetToDelayedSyncValue(); cvar_RenderingShadowsMaxShadowMapSize.SetToDelayedSyncValue(); cvar_RenderingShadowsMinShadowMapSize.SetToDelayedSyncValue(); ezGALTextureCreationDescription desc; desc.SetAsRenderTarget(cvar_RenderingShadowsAtlasSize, cvar_RenderingShadowsAtlasSize, ezGALResourceFormat::D16); m_TextureAtlas.Initialize(desc).AssertSuccess("Failed to initialize shadow atlas"); s_fMinRelativeShadowMapSize = (cvar_RenderingShadowsMinShadowMapSize - 1.0f) / cvar_RenderingShadowsMaxShadowMapSize; } } void CreateShadowDataBuffer() { if (m_hShadowDataBuffer.IsInvalidated()) { ezGALBufferCreationDescription desc; desc.m_uiStructSize = sizeof(ezVec4); desc.m_uiTotalSize = desc.m_uiStructSize * MAX_SHADOW_DATA; desc.m_BufferFlags = ezGALBufferUsageFlags::StructuredBuffer | ezGALBufferUsageFlags::ShaderResource; desc.m_ResourceAccess.m_bImmutable = false; m_hShadowDataBuffer = ezGALDevice::GetDefaultDevice()->CreateBuffer(desc); } } ezViewHandle CreateShadowView() { CreateShadowAtlasTexture(); CreateShadowDataBuffer(); ezView* pView = nullptr; ezViewHandle hView = ezRenderWorld::CreateView("Unknown", pView); pView->SetCameraUsageHint(ezCameraUsageHint::Shadow); ezGALRenderTargets renderTargets; renderTargets.m_hDSTarget = m_TextureAtlas.GetTexture(); pView->SetRenderTargets(renderTargets); EZ_ASSERT_DEV(m_ShadowViewsMutex.IsLocked(), "m_ShadowViewsMutex must be locked at this point."); m_ShadowViewsMutex.Unlock(); // if the resource gets loaded in the call below, his could lead to a deadlock // ShadowMapRenderPipeline.ezRenderPipelineAsset pView->SetRenderPipelineResource(ezResourceManager::LoadResource<ezRenderPipelineResource>("{ 4f4d9f16-3d47-4c67-b821-a778f11dcaf5 }")); m_ShadowViewsMutex.Lock(); // Set viewport size to something valid, this will be changed to the proper location in the atlas texture in OnEndExtraction before // rendering. pView->SetViewport(ezRectFloat(0.0f, 0.0f, 1024.0f, 1024.0f)); pView->m_IncludeTags.SetByName("CastShadow"); pView->m_ExcludeTags.SetByName("EditorHidden"); pView->SetBlackboard(ezBlackboard::Create("ShadowViewBlackboard")); return hView; } ShadowView& GetShadowView(ezView*& out_pView) { EZ_LOCK(m_ShadowViewsMutex); if (m_uiUsedViews == m_ShadowViews.GetCount()) { m_ShadowViews.ExpandAndGetRef().m_hView = CreateShadowView(); } auto& shadowView = m_ShadowViews[m_uiUsedViews]; if (ezRenderWorld::TryGetView(shadowView.m_hView, out_pView)) { out_pView->SetCamera(&shadowView.m_Camera); out_pView->SetCullingCamera(nullptr); out_pView->SetLodCamera(nullptr); } m_uiUsedViews++; return shadowView; } bool GetDataForExtraction(const ezLightComponent* pLight, const ezView* pReferenceView, float fShadowMapScale, ezUInt32 uiPackedDataSizeInBytes, ShadowData*& out_pData) { EZ_LOCK(m_ShadowDataMutex); LightAndRefView key = {pLight, pReferenceView}; ezUInt32 uiDataIndex = ezInvalidIndex; if (m_LightToShadowDataTable.TryGetValue(key, uiDataIndex)) { out_pData = &m_ShadowData[uiDataIndex]; out_pData->m_fShadowMapScale = ezMath::Max(out_pData->m_fShadowMapScale, fShadowMapScale); return true; } m_ShadowData.EnsureCount(m_uiUsedShadowData + 1); out_pData = &m_ShadowData[m_uiUsedShadowData]; out_pData->m_fShadowMapScale = fShadowMapScale; out_pData->m_fPenumbraSize = pLight->GetPenumbraSize(); out_pData->m_fSlopeBias = pLight->GetSlopeBias() * 100.0f; // map from user friendly range to real range out_pData->m_fConstantBias = pLight->GetConstantBias() / 100.0f; // map from user friendly range to real range out_pData->m_fFadeOutStart = 1.0f; out_pData->m_fMinRange = 1.0f; out_pData->m_fActualRange = 1.0f; out_pData->m_uiPackedDataOffset = m_uiUsedPackedShadowData; m_LightToShadowDataTable.Insert(key, m_uiUsedShadowData); ++m_uiUsedShadowData; m_uiUsedPackedShadowData += uiPackedDataSizeInBytes / sizeof(ezVec4); return false; } void Clear() { m_uiUsedViews = 0; m_uiUsedShadowData = 0; m_LightToShadowDataTable.Clear(); m_uiUsedPackedShadowData = 0; } ezMutex m_ShadowViewsMutex; ezDeque<ShadowView> m_ShadowViews; ezUInt32 m_uiUsedViews = 0; ezMutex m_ShadowDataMutex; ezDeque<ShadowData> m_ShadowData; ezUInt32 m_uiUsedShadowData = 0; ezHashTable<LightAndRefView, ezUInt32> m_LightToShadowDataTable; ezDynamicArray<ezVec4, ezAlignedAllocatorWrapper> m_PackedShadowData[2]; ezUInt32 m_uiUsedPackedShadowData = 0; // in 16 bytes steps (sizeof(ezVec4)) ezDynamicTextureAtlas m_TextureAtlas; ezGALBufferHandle m_hShadowDataBuffer; ezSharedPtr<ezRenderGraph> m_pRenderGraph; }; ////////////////////////////////////////////////////////////////////////// ezShadowPool::Data* ezShadowPool::s_pData = nullptr; // static ezUInt32 ezShadowPool::AddDirectionalLight(const ezDirectionalLightComponent* pDirLight, const ezView* pReferenceView) { EZ_ASSERT_DEBUG(pDirLight->GetCastShadows(), "Implementation error"); // No shadows in orthographic views if (pReferenceView->GetCullingCamera()->IsOrthographic()) { return ezInvalidIndex; } float fMaxReferenceSize = ezMath::Max(pReferenceView->GetViewport().width, pReferenceView->GetViewport().height); float fShadowMapScale = ezMath::Clamp(fMaxReferenceSize / cvar_RenderingShadowsMaxShadowMapSize, s_fMinRelativeShadowMapSize, 10.0f); ShadowData* pData = nullptr; if (s_pData->GetDataForExtraction(pDirLight, pReferenceView, fShadowMapScale, sizeof(ezDirShadowData), pData)) { return pData->m_uiPackedDataOffset; } ezUInt32 uiNumCascades = ezMath::Min(pDirLight->GetNumCascades(), 4u); const ezCamera* pReferenceCamera = pReferenceView->GetCullingCamera(); pData->m_uiType = LIGHT_TYPE_DIR; pData->m_fFadeOutStart = pDirLight->GetFadeOutStart(); pData->m_fMinRange = pDirLight->GetMinShadowRange(); pData->m_Views.SetCount(uiNumCascades); // determine cascade ranges float fNearPlane = pReferenceCamera->GetNearPlane(); float fShadowRange = pDirLight->GetMinShadowRange(); float fSplitModeWeight = pDirLight->GetSplitModeWeight(); float fCascadeRanges[4]; for (ezUInt32 i = 0; i < uiNumCascades; ++i) { float f = float(i + 1) / uiNumCascades; float logDistance = fNearPlane * ezMath::Pow(fShadowRange / fNearPlane, f); float linearDistance = fNearPlane + (fShadowRange - fNearPlane) * f; fCascadeRanges[i] = ezMath::Lerp(linearDistance, logDistance, fSplitModeWeight); } const ezStringView viewNames[4] = {"-C0"_ezsv, "-C1"_ezsv, "-C2"_ezsv, "-C3"_ezsv}; ezStringBuilder tmp; const ezGameObject* pOwner = pDirLight->GetOwner(); const ezVec3 vLightDirForwards = pOwner->GetGlobalDirForwards(); const ezVec3 vLightDirUp = pOwner->GetGlobalDirUp(); float fAspectRatio = pReferenceView->GetViewport().width / pReferenceView->GetViewport().height; float fCascadeStart = 0.0f; float fCascadeEnd = 0.0f; float fTanFovX = ezMath::Tan(pReferenceCamera->GetFovX(fAspectRatio) * 0.5f); float fTanFovY = ezMath::Tan(pReferenceCamera->GetFovY(fAspectRatio) * 0.5f); ezVec3 corner = ezVec3(fTanFovX, fTanFovY, 1.0f); float fNearPlaneOffset = pDirLight->GetNearPlaneOffset(); for (ezUInt32 i = 0; i < uiNumCascades; ++i) { ezView* pView = nullptr; ShadowView& shadowView = s_pData->GetShadowView(pView); pData->m_Views[i] = shadowView.m_hView; // Setup view { if (pOwner->GetName().IsEmpty()) { tmp.Set("Dir", viewNames[i]); } else { tmp.Set(pOwner->GetName(), viewNames[i]); } pView->SetName(tmp); pView->SetWorld(const_cast<ezWorld*>(pDirLight->GetWorld())); pView->SetCullingCamera(&shadowView.m_CullingCamera); pView->SetLodCamera(pReferenceCamera); pView->GetBlackboard()->SetEntryValue(ezMakeHashedString("ShadowDepth.RenderTransparentObjects"), pDirLight->GetTransparentShadows()); CopyExcludeTagsOnWhiteList(pReferenceView->m_ExcludeTags, pView->m_ExcludeTags); } // Setup camera { fCascadeStart = fCascadeEnd; // Make sure that the last cascade always covers the whole range so effects like e.g. particles can get away with // sampling only the last cascade. if (i == uiNumCascades - 1) { fCascadeStart = 0.0f; } fCascadeEnd = fCascadeRanges[i]; ezVec3 startCorner = corner * fCascadeStart; ezVec3 endCorner = corner * fCascadeEnd; pData->m_fActualRange = endCorner.GetLength(); // Find the enclosing sphere for the frustum: // The sphere center must be on the view's center ray and should be equally far away from the corner points. // x = distance from camera origin to sphere center // d1^2 = sc.x^2 + sc.y^2 + (x - sc.z)^2 // d2^2 = ec.x^2 + ec.y^2 + (x - ec.z)^2 // d1 == d2 and solve for x: float x = (endCorner.Dot(endCorner) - startCorner.Dot(startCorner)) / (2.0f * (endCorner.z - startCorner.z)); x = ezMath::Min(x, fCascadeEnd); ezVec3 center = pReferenceCamera->GetPosition() + pReferenceCamera->GetDirForwards() * x; // prevent too large values // sometimes this can happen when imported data is badly scaled and thus way too large // then adding dirForwards result in no change and we run into other asserts later center.x = ezMath::Clamp(center.x, -1000000.0f, +1000000.0f); center.y = ezMath::Clamp(center.y, -1000000.0f, +1000000.0f); center.z = ezMath::Clamp(center.z, -1000000.0f, +1000000.0f); endCorner.z -= x; const float radius = endCorner.GetLength(); const float fCameraToCenterDistance = radius + fNearPlaneOffset; const ezVec3 shadowCameraPos = center - vLightDirForwards * fCameraToCenterDistance; const float fFarPlane = radius + fCameraToCenterDistance; ezCamera& camera = shadowView.m_Camera; camera.LookAt(shadowCameraPos, center, vLightDirUp); camera.SetCameraMode(ezCameraMode::OrthoFixedWidth, radius * 2.0f, 0.0f, fFarPlane); // stabilize const ezMat4 worldToLightMatrix = pView->GetViewMatrix(ezCameraEye::Left); const float texelInWorld = (2.0f * radius) / cvar_RenderingShadowsMaxShadowMapSize; ezVec3 offset = worldToLightMatrix.TransformPosition(ezVec3::MakeZero()); offset.x -= ezMath::Floor(offset.x / texelInWorld) * texelInWorld; offset.y -= ezMath::Floor(offset.y / texelInWorld) * texelInWorld; camera.MoveLocally(0.0f, offset.x, offset.y); // culling camera with pulled back near plane ezCamera& cullingCamera = shadowView.m_CullingCamera; cullingCamera = camera; cullingCamera.SetCameraMode(ezCameraMode::OrthoFixedWidth, radius * 2.0f, -pReferenceCamera->GetFarPlane(), fFarPlane); #if EZ_ENABLED(EZ_COMPILE_FOR_DEVELOPMENT) if (cvar_RenderingShadowsVisCascadeBounds) { ezDebugRenderer::DrawLineSphere(pReferenceView->GetHandle(), ezBoundingSphere::MakeFromCenterAndRadius(center, radius), ezColorScheme::LightUI(ezColorScheme::Orange)); ezDebugRenderer::DrawLineSphere(pReferenceView->GetHandle(), ezBoundingSphere::MakeFromCenterAndRadius(ezVec3(0, 0, fCameraToCenterDistance), radius), ezColorScheme::LightUI(ezColorScheme::Red), ezTransform::MakeFromMat4(camera.GetViewMatrix().GetInverse())); } #endif } ezRenderWorld::AddViewDependency(*pReferenceView, GetShadowAtlasTexture(), ezGALResourceState::ShaderResource); ezRenderWorld::AddViewToRender(shadowView.m_hView); } return pData->m_uiPackedDataOffset; } // static ezUInt32 ezShadowPool::AddPointLight(const ezPointLightComponent* pPointLight, float fScreenSpaceSize, const ezView* pReferenceView) { EZ_ASSERT_DEBUG(pPointLight->GetCastShadows(), "Implementation error"); // point lights use a lot of atlas space thus we half the scale const float fShadowMapScale = ShadowMapScaleFromScreenSpaceSize(fScreenSpaceSize) * 0.5f; ShadowData* pData = nullptr; if (s_pData->GetDataForExtraction(pPointLight, nullptr, fShadowMapScale, sizeof(ezPointShadowData), pData)) { return pData->m_uiPackedDataOffset; } pData->m_uiType = LIGHT_TYPE_POINT; pData->m_Views.SetCount(6); ezVec3 faceDirs[6] = { ezVec3(1.0f, 0.0f, 0.0f), ezVec3(-1.0f, 0.0f, 0.0f), ezVec3(0.0f, 1.0f, 0.0f), ezVec3(0.0f, -1.0f, 0.0f), ezVec3(0.0f, 0.0f, 1.0f), ezVec3(0.0f, 0.0f, -1.0f), }; const ezStringView viewNames[6] = { "+X"_ezsv, "-X"_ezsv, "+Y"_ezsv, "-Y"_ezsv, "+Z"_ezsv, "-Z"_ezsv, }; const ezGameObject* pOwner = pPointLight->GetOwner(); ezVec3 vPosition = pOwner->GetGlobalPosition(); ezVec3 vUp = ezVec3(0.0f, 0.0f, 1.0f); float fPenumbraSize = ezMath::Max(pPointLight->GetPenumbraSize(), (0.5f / cvar_RenderingShadowsMinShadowMapSize)); // at least one texel for hardware pcf float fFov = AddSafeBorder(ezAngle::MakeFromDegree(90.0f), fPenumbraSize); ///\todo expose somewhere float fNearPlane = 0.1f; float fFarPlane = pPointLight->GetEffectiveRange(); ezStringBuilder tmp; for (ezUInt32 i = 0; i < 6; ++i) { ezView* pView = nullptr; ShadowView& shadowView = s_pData->GetShadowView(pView); pData->m_Views[i] = shadowView.m_hView; // Setup view { if (pOwner->GetName().IsEmpty()) { tmp.Set("Point", viewNames[i]); } else { tmp.Set(pOwner->GetName(), viewNames[i]); } pView->SetName(tmp); pView->SetWorld(const_cast<ezWorld*>(pPointLight->GetWorld())); pView->GetBlackboard()->SetEntryValue(ezMakeHashedString("ShadowDepth.RenderTransparentObjects"), pPointLight->GetTransparentShadows()); CopyExcludeTagsOnWhiteList(pReferenceView->m_ExcludeTags, pView->m_ExcludeTags); } // Setup camera { ezVec3 vForward = faceDirs[i]; ezCamera& camera = shadowView.m_Camera; camera.LookAt(vPosition, vPosition + vForward, vUp); camera.SetCameraMode(ezCameraMode::PerspectiveFixedFovX, fFov, fNearPlane, fFarPlane); } ezRenderWorld::AddViewDependency(*pReferenceView, GetShadowAtlasTexture(), ezGALResourceState::ShaderResource); ezRenderWorld::AddViewToRender(shadowView.m_hView); } return pData->m_uiPackedDataOffset; } // static ezUInt32 ezShadowPool::AddSpotLight(const ezSpotLightComponent* pSpotLight, float fScreenSpaceSize, const ezView* pReferenceView) { EZ_ASSERT_DEBUG(pSpotLight->GetCastShadows(), "Implementation error"); const float fShadowMapScale = ShadowMapScaleFromScreenSpaceSize(fScreenSpaceSize); ShadowData* pData = nullptr; if (s_pData->GetDataForExtraction(pSpotLight, nullptr, fShadowMapScale, sizeof(ezSpotShadowData), pData)) { return pData->m_uiPackedDataOffset; } pData->m_uiType = LIGHT_TYPE_SPOT; pData->m_Views.SetCount(1); ezView* pView = nullptr; ShadowView& shadowView = s_pData->GetShadowView(pView); pData->m_Views[0] = shadowView.m_hView; const ezGameObject* pOwner = pSpotLight->GetOwner(); // Setup view { if (pOwner->GetName().IsEmpty()) { pView->SetName("Spot"_ezsv); } else { pView->SetName(pOwner->GetName()); } pView->SetWorld(const_cast<ezWorld*>(pSpotLight->GetWorld())); pView->GetBlackboard()->SetEntryValue(ezMakeHashedString("ShadowDepth.RenderTransparentObjects"), pSpotLight->GetTransparentShadows()); CopyExcludeTagsOnWhiteList(pReferenceView->m_ExcludeTags, pView->m_ExcludeTags); } // Setup camera { const ezGameObject* pOwner = pSpotLight->GetOwner(); ezVec3 vPosition = pOwner->GetGlobalPosition(); ezVec3 vForward = pOwner->GetGlobalDirForwards(); ezVec3 vUp = pOwner->GetGlobalDirUp(); float fFov = AddSafeBorder(pSpotLight->GetOuterSpotAngle(), pSpotLight->GetPenumbraSize()); float fNearPlane = 0.1f; ///\todo expose somewhere float fFarPlane = pSpotLight->GetEffectiveRange(); ezCamera& camera = shadowView.m_Camera; camera.LookAt(vPosition, vPosition + vForward, vUp); camera.SetCameraMode(ezCameraMode::PerspectiveFixedFovX, fFov, fNearPlane, fFarPlane); } ezRenderWorld::AddViewDependency(*pReferenceView, GetShadowAtlasTexture(), ezGALResourceState::ShaderResource); ezRenderWorld::AddViewToRender(shadowView.m_hView); return pData->m_uiPackedDataOffset; } // static ezGALTextureHandle ezShadowPool::GetShadowAtlasTexture() { return s_pData->m_TextureAtlas.GetTexture(); } // static ezGALBufferHandle ezShadowPool::GetShadowDataBuffer() { return s_pData->m_hShadowDataBuffer; } // static void ezShadowPool::AddExcludeTagToWhiteList(const ezTag& tag) { s_ExcludeTagsWhiteList.Set(tag); } // static void ezShadowPool::OnEngineStartup() { s_pData = EZ_DEFAULT_NEW(ezShadowPool::Data); ezRenderWorld::GetExtractionEvent().AddEventHandler(OnExtractionEvent); ezRenderWorld::GetRenderEvent().AddEventHandler(OnRenderEvent); } // static void ezShadowPool::OnEngineShutdown() { ezRenderWorld::GetExtractionEvent().RemoveEventHandler(OnExtractionEvent); ezRenderWorld::GetRenderEvent().RemoveEventHandler(OnRenderEvent); EZ_DEFAULT_DELETE(s_pData); } // static void ezShadowPool::OnExtractionEvent(const ezRenderWorldExtractionEvent& e) { if (e.m_Type != ezRenderWorldExtractionEvent::Type::EndExtraction) return; EZ_PROFILE_SCOPE("Shadow Pool Update"); ezUInt32 uiDataIndex = ezRenderWorld::GetDataIndexForExtraction(); auto& packedShadowData = s_pData->m_PackedShadowData[uiDataIndex]; packedShadowData.SetCountUninitialized(s_pData->m_uiUsedPackedShadowData); if (s_pData->m_uiUsedShadowData == 0) return; // Sort by shadow map scale s_SortedShadowData.Clear(); for (ezUInt32 uiShadowDataIndex = 0; uiShadowDataIndex < s_pData->m_uiUsedShadowData; ++uiShadowDataIndex) { auto& shadowData = s_pData->m_ShadowData[uiShadowDataIndex]; auto& sorted = s_SortedShadowData.ExpandAndGetRef(); sorted.m_uiIndex = uiShadowDataIndex; sorted.m_fShadowMapScale = shadowData.m_fShadowMapScale; } s_SortedShadowData.Sort(); // Prepare atlas s_pData->m_TextureAtlas.Clear(); float fAtlasInvWidth = 1.0f / cvar_RenderingShadowsAtlasSize; float fAtlasInvHeight = 1.0f / cvar_RenderingShadowsAtlasSize; for (auto& sorted : s_SortedShadowData) { ezUInt32 uiShadowDataIndex = sorted.m_uiIndex; auto& shadowData = s_pData->m_ShadowData[uiShadowDataIndex]; ezUInt32 uiMaxShadowMapSize = cvar_RenderingShadowsMaxShadowMapSize; const ezUInt32 uiShadowMapSize = ezMath::PowerOfTwo_Ceil((ezUInt32)(uiMaxShadowMapSize * ezMath::Saturate(shadowData.m_fShadowMapScale))); ezTempHybridArray<ezView*, 8> shadowViews; ezTempHybridArray<ezRectU16, 8> atlasRects; // Fill atlas for (ezUInt32 uiViewIndex = 0; uiViewIndex < shadowData.m_Views.GetCount(); ++uiViewIndex) { ezView* pShadowView = nullptr; ezRenderWorld::TryGetView(shadowData.m_Views[uiViewIndex], pShadowView); shadowViews.PushBack(pShadowView); EZ_ASSERT_DEV(pShadowView != nullptr, "Implementation error"); ezRectU16 atlasRect = ezRectU16::MakeZero(); auto allocationId = s_pData->m_TextureAtlas.Allocate(uiShadowMapSize, uiShadowMapSize, pShadowView->GetName(), &atlasRect); #if EZ_ENABLED(EZ_COMPILE_FOR_DEVELOPMENT) if (allocationId.IsInvalidated()) { static ezUInt8 s_uiWarnCounter = 0; if (s_uiWarnCounter == 0) { ezLog::Warning("Shadow Pool is full. Not enough space for a {0}x{0} shadow map. The light will have no shadow.", uiShadowMapSize); } ++s_uiWarnCounter; // rely on integer overflow to reach 0 again } #endif atlasRects.PushBack(atlasRect); pShadowView->SetViewport(ezRectFloat((float)atlasRect.x, (float)atlasRect.y, (float)atlasRect.width, (float)atlasRect.height)); } // Fill shadow data if (shadowData.m_uiType == LIGHT_TYPE_DIR) { ezUInt32 uiNumCascades = shadowData.m_Views.GetCount(); ezUInt32 uiMatrixIndex = GET_WORLD_TO_LIGHT_MATRIX_INDEX(shadowData.m_uiPackedDataOffset, 0); ezMat4& worldToLightMatrix = *reinterpret_cast<ezMat4*>(&packedShadowData[uiMatrixIndex]); worldToLightMatrix = shadowViews[0]->GetViewProjectionMatrix(ezCameraEye::Left); for (ezUInt32 uiViewIndex = 0; uiViewIndex < uiNumCascades; ++uiViewIndex) { if (uiViewIndex >= 1) { ezMat4 cascadeToWorldMatrix = shadowViews[uiViewIndex]->GetInverseViewProjectionMatrix(ezCameraEye::Left); ezVec3 cascadeCorner = cascadeToWorldMatrix.TransformPosition(ezVec3(0.0f)); cascadeCorner = worldToLightMatrix.TransformPosition(cascadeCorner); ezVec3 otherCorner = cascadeToWorldMatrix.TransformPosition(ezVec3(1.0f)); otherCorner = worldToLightMatrix.TransformPosition(otherCorner); ezUInt32 uiCascadeScaleIndex = GET_CASCADE_SCALE_INDEX(shadowData.m_uiPackedDataOffset, uiViewIndex - 1); ezUInt32 uiCascadeOffsetIndex = GET_CASCADE_OFFSET_INDEX(shadowData.m_uiPackedDataOffset, uiViewIndex - 1); ezVec4& cascadeScale = packedShadowData[uiCascadeScaleIndex]; ezVec4& cascadeOffset = packedShadowData[uiCascadeOffsetIndex]; cascadeScale = ezVec3(1.0f).CompDiv(otherCorner - cascadeCorner).GetAsVec4(1.0f); cascadeOffset = cascadeCorner.GetAsVec4(0.0f).CompMul(-cascadeScale); } ezUInt32 uiAtlasScaleOffsetIndex = GET_ATLAS_SCALE_OFFSET_INDEX(shadowData.m_uiPackedDataOffset, uiViewIndex); ezVec4& atlasScaleOffset = packedShadowData[uiAtlasScaleOffsetIndex]; const ezRectU16& atlasRect = atlasRects[uiViewIndex]; if (atlasRect.HasNonZeroArea()) { ezVec2 scale = ezVec2(atlasRect.width * fAtlasInvWidth, atlasRect.height * fAtlasInvHeight); ezVec2 offset = ezVec2(atlasRect.x * fAtlasInvWidth, atlasRect.y * fAtlasInvHeight); // combine with tex scale offset atlasScaleOffset.x = scale.x * 0.5f; atlasScaleOffset.y = scale.y * -0.5f; atlasScaleOffset.z = offset.x + scale.x * 0.5f; atlasScaleOffset.w = offset.y + scale.y * 0.5f; } else { atlasScaleOffset.Set(1.0f, 1.0f, 0.0f, 0.0f); } } const ezCamera* pFirstCascadeCamera = shadowViews[0]->GetCamera(); const ezCamera* pLastCascadeCamera = shadowViews[uiNumCascades - 1]->GetCamera(); float cascadeSize = pFirstCascadeCamera->GetFovOrDim(); float texelSize = 1.0f / uiShadowMapSize; float penumbraSize = ezMath::Max(shadowData.m_fPenumbraSize / cascadeSize, texelSize); float goodPenumbraSize = 8.0f / uiShadowMapSize; float relativeShadowSize = uiShadowMapSize * fAtlasInvHeight; // params { // tweak values to keep the default values consistent with spot and point lights float slopeBias = shadowData.m_fSlopeBias * ezMath::Max(penumbraSize, goodPenumbraSize); float constantBias = shadowData.m_fConstantBias * 0.2f; ezUInt32 uilastCascadeIndex = uiNumCascades - 1; ezUInt32 uiParamsIndex = GET_SHADOW_PARAMS_INDEX(shadowData.m_uiPackedDataOffset); ezVec4& shadowParams = packedShadowData[uiParamsIndex]; shadowParams.x = slopeBias; shadowParams.y = constantBias; shadowParams.z = penumbraSize * relativeShadowSize; shadowParams.w = *reinterpret_cast<float*>(&uilastCascadeIndex); } // params2 { float ditherMultiplier = 0.2f / cascadeSize; float zRange = cascadeSize / pFirstCascadeCamera->GetFarPlane(); float actualPenumbraSize = shadowData.m_fPenumbraSize / pLastCascadeCamera->GetFovOrDim(); float penumbraSizeIncrement = ezMath::Max(goodPenumbraSize - actualPenumbraSize, 0.0f) / shadowData.m_fMinRange; ezUInt32 uiParams2Index = GET_SHADOW_PARAMS2_INDEX(shadowData.m_uiPackedDataOffset); ezVec4& shadowParams2 = packedShadowData[uiParams2Index]; shadowParams2.x = 1.0f - ezMath::Max(penumbraSize, goodPenumbraSize); shadowParams2.y = ditherMultiplier; shadowParams2.z = ditherMultiplier * zRange; shadowParams2.w = penumbraSizeIncrement * relativeShadowSize; } // fadeout { float fadeOutRange = 1.0f - shadowData.m_fFadeOutStart; float xyScale = -1.0f / fadeOutRange; float xyOffset = -xyScale; ezUInt32 xyScaleOffset = ezShaderUtils::PackFloat16intoUint(xyScale, xyOffset); float zFadeOutRange = fadeOutRange * pLastCascadeCamera->GetFovOrDim() / pLastCascadeCamera->GetFarPlane(); float zScale = -1.0f / zFadeOutRange; float zOffset = -zScale; ezUInt32 zScaleOffset = ezShaderUtils::PackFloat16intoUint(zScale, zOffset); float distanceFadeOutRange = fadeOutRange * shadowData.m_fActualRange; float distanceScale = -1.0f / distanceFadeOutRange; float distanceOffset = -distanceScale * shadowData.m_fActualRange; ezUInt32 uiFadeOutIndex = GET_FADE_OUT_PARAMS_INDEX(shadowData.m_uiPackedDataOffset); ezVec4& fadeOutParams = packedShadowData[uiFadeOutIndex]; fadeOutParams.x = *reinterpret_cast<float*>(&xyScaleOffset); fadeOutParams.y = *reinterpret_cast<float*>(&zScaleOffset); fadeOutParams.z = distanceScale; fadeOutParams.w = distanceOffset; } } else // spot or point light { ezMat4 texMatrix; texMatrix.SetIdentity(); texMatrix.SetDiagonal(ezVec4(0.5f, -0.5f, 1.0f, 1.0f)); texMatrix.SetTranslationVector(ezVec3(0.5f, 0.5f, 0.0f)); ezAngle fov = ezAngle::MakeZero(); float fRange = 0.0f; for (ezUInt32 uiViewIndex = 0; uiViewIndex < shadowData.m_Views.GetCount(); ++uiViewIndex) { ezView* pShadowView = shadowViews[uiViewIndex]; EZ_ASSERT_DEV(pShadowView != nullptr, "Implementation error"); ezUInt32 uiMatrixIndex = GET_WORLD_TO_LIGHT_MATRIX_INDEX(shadowData.m_uiPackedDataOffset, uiViewIndex); ezMat4& worldToLightMatrix = *reinterpret_cast<ezMat4*>(&packedShadowData[uiMatrixIndex]); const ezRectU16& atlasRect = atlasRects[uiViewIndex]; if (atlasRect.HasNonZeroArea()) { ezVec2 scale = ezVec2(atlasRect.width * fAtlasInvWidth, atlasRect.height * fAtlasInvHeight); ezVec2 offset = ezVec2(atlasRect.x * fAtlasInvWidth, atlasRect.y * fAtlasInvHeight); ezMat4 atlasMatrix; atlasMatrix.SetIdentity(); atlasMatrix.SetDiagonal(ezVec4(scale.x, scale.y, 1.0f, 1.0f)); atlasMatrix.SetTranslationVector(offset.GetAsVec3(0.0f)); fov = pShadowView->GetCamera()->GetFovY(1.0f); fRange = pShadowView->GetCamera()->GetFarPlane(); const ezMat4& viewProjection = pShadowView->GetViewProjectionMatrix(ezCameraEye::Left); worldToLightMatrix = atlasMatrix * texMatrix * viewProjection; } else { worldToLightMatrix.SetIdentity(); } } const float screenHeight = ezMath::Tan(fov * 0.5f) * 20.0f; // screen height in world-space at 10m distance const float texelSize = 1.0f / uiShadowMapSize; const float penumbraSize = ezMath::Max(shadowData.m_fPenumbraSize / screenHeight, texelSize); const float relativeShadowSize = uiShadowMapSize * fAtlasInvHeight; // empirical tweak factors const float fovFactor = 0.15f * ezMath::Pow(5.5f, fov.GetRadian()); const float rangeFactor = ezMath::Max(0.018f * fRange + 0.0098f * fRange * fRange, 0.1f); const float slopeBias = shadowData.m_fSlopeBias * penumbraSize * fovFactor * rangeFactor; const float constantBias = shadowData.m_fConstantBias * cvar_RenderingShadowsMaxShadowMapSize / uiShadowMapSize; ezUInt32 uiParamsIndex = GET_SHADOW_PARAMS_INDEX(shadowData.m_uiPackedDataOffset); ezVec4& shadowParams = packedShadowData[uiParamsIndex]; shadowParams.x = slopeBias; shadowParams.y = constantBias; shadowParams.z = penumbraSize * relativeShadowSize; shadowParams.w = 0.0f; } } #if EZ_ENABLED(EZ_COMPILE_FOR_DEVELOPMENT) if (cvar_RenderingShadowsShowAtlasTexture) { ezDebugRendererContext debugContext(ezWorld::GetWorld(0)); float viewWidth = 1920; float viewHeight = 1080; if (const ezView* pView = ezRenderWorld::GetViewByUsageHint(ezCameraUsageHint::MainView, ezCameraUsageHint::EditorView)) { debugContext = ezDebugRendererContext(pView->GetHandle()); viewWidth = pView->GetViewport().width; viewHeight = pView->GetViewport().height; } s_pData->m_TextureAtlas.DebugDraw(debugContext, viewWidth, viewHeight); } #endif s_pData->Clear(); } // static void ezShadowPool::OnRenderEvent(const ezRenderWorldRenderEvent& e) { if (e.m_Type != ezRenderWorldRenderEvent::Type::BeginRender) return; if (s_pData->m_TextureAtlas.IsInitialized() == false || s_pData->m_hShadowDataBuffer.IsInvalidated()) return; if (cvar_RenderingShadowsAtlasSize.HasDelayedSyncValueChanged() || cvar_RenderingShadowsMinShadowMapSize.HasDelayedSyncValueChanged() || cvar_RenderingShadowsMaxShadowMapSize.HasDelayedSyncValueChanged() || s_uiLastConfigModification != ezGameApplicationBase::GetGameApplicationBaseInstance()->GetPlatformProfile().GetLastModificationCounter()) { OnEngineShutdown(); OnEngineStartup(); } ezGALDevice* pDevice = ezGALDevice::GetDefaultDevice(); if (s_pData->m_pRenderGraph == nullptr) s_pData->m_pRenderGraph = ezRenderGraphManager::CreateRenderGraph("ShadowPool", ezRenderGraphPhase::PreRender); auto rtv = pDevice->GetDefaultRenderTargetView(s_pData->m_TextureAtlas.GetTexture()); if (rtv.IsInvalidated() == false) { s_pData->m_pRenderGraph->Reset(); ezRenderGraphTextureHandle hShadowAtlas = s_pData->m_pRenderGraph->ImportTexture(s_pData->m_TextureAtlas.GetTexture()); { auto pass = s_pData->m_pRenderGraph->AddGraphicsPass("Shadow Atlas"); pass.AddDepthStencilTarget(hShadowAtlas); pass.SetClearDepth(); pass.HasSideEffects(); pass.SetExecuteCallback([](const ezRenderGraphContext& ctx) { ezUInt32 uiDataIndex = ezRenderWorld::GetDataIndexForRendering(); auto& packedShadowData = s_pData->m_PackedShadowData[uiDataIndex]; if (!packedShadowData.IsEmpty()) { EZ_PROFILE_SCOPE("Shadow Data Buffer Update"); ctx.GetCommandEncoder()->UpdateBuffer(s_pData->m_hShadowDataBuffer, 0, packedShadowData.GetByteArrayPtr(), ezGALUpdateMode::AheadOfTime); } }); } ezRenderGraphManager::EnqueueRenderGraph(s_pData->m_pRenderGraph); } } EZ_STATICLINK_FILE(RendererCore, RendererCore_Lights_Implementation_ShadowPool);