Files
UnrealEngine/Engine/Source/Runtime/Renderer/Private/Shadows/ShadowSceneRenderer.cpp
2025-05-18 13:04:45 +08:00

1188 lines
48 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ShadowSceneRenderer.h"
#include "ShadowScene.h"
#include "DeferredShadingRenderer.h"
#include "ScenePrivate.h"
#include "ShadowRendering.h"
#include "VirtualShadowMaps/VirtualShadowMapCacheManager.h"
#include "VirtualShadowMaps/VirtualShadowMapProjection.h"
#include "VirtualShadowMaps/VirtualShadowMapClipmap.h"
#include "SceneCulling/SceneCulling.h"
#include "SceneCulling/SceneCullingRenderer.h"
#include "Rendering/NaniteStreamingManager.h"
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
#include "DynamicPrimitiveDrawing.h"
#endif
CSV_DECLARE_CATEGORY_EXTERN(VSM);
extern TAutoConsoleVariable<int32> CVarNaniteShadowsUpdateStreaming;
extern TAutoConsoleVariable<int32> CVarVsmUseFarShadowRules;
TAutoConsoleVariable<int32> CVarVSMMaterialVisibility(
TEXT("r.Shadow.Virtual.Nanite.MaterialVisibility"),
0,
TEXT("Enable Nanite CPU-side visibility filtering of draw commands, depends on r.Nanite.MaterialVisibility being enabled."),
ECVF_RenderThreadSafe
);
TAutoConsoleVariable<int32> CVarMaxDistantLightsPerFrame(
TEXT("r.Shadow.Virtual.MaxDistantUpdatePerFrame"),
1,
TEXT("Maximum number of distant lights to update each frame. Invalidated lights that were missed may be updated in a later frame (round-robin)."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarDistantLightMode(
TEXT("r.Shadow.Virtual.DistantLightMode"),
1,
TEXT("Control whether distant light mode is enabled for local lights.\n0 == Off, \n1 == On (default), \n2 == Force All.\n")
TEXT("When on, lights with a pixel footprint below the threshold are marked as distant. Updates to distant lights are throttled (force-cached), they use simpler page-table logic and the memory cost is lower."),
ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<float> CVarDistantLightForceCacheFootprintFraction(
TEXT("r.Shadow.Virtual.DistantLightForceCacheFootprintFraction"),
0.0f,
TEXT("Fraction of footprint size below which start force-caching lights that are invalidated (i.e., are moving or re-added)\n")
TEXT(" Larger values may improve performance but may also produce more visible artifacts\n")
TEXT(" The base footprint is based on the page size.\n")
TEXT(" 0.0 == Never force-cache (default), 1.0 == Always force-cache."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<bool> CVarUseConservativeDistantLightThreshold(
TEXT("r.Shadow.Virtual.UseConservativeDistantLightThreshold"),
false,
TEXT("Base the distant light cutoff on the minimum mip level instead of the shadow resolution calculated through the old path.\n")
TEXT(" This fixes problems around the use of an inscribed sphere."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<float> CVarNaniteShadowsLODBias(
TEXT("r.Shadow.NaniteLODBias"),
1.0f,
TEXT("LOD bias for nanite geometry in shadows. 0 = full detail. >0 = reduced detail."),
ECVF_Scalability | ECVF_RenderThreadSafe);
TAutoConsoleVariable<int32> CVarVirtualShadowOnePassProjection(
TEXT("r.Shadow.Virtual.OnePassProjection"),
1,
TEXT("Projects all local light virtual shadow maps in a single pass for better performance."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<float> CVarResolutionLodBiasLocal(
TEXT("r.Shadow.Virtual.ResolutionLodBiasLocal"),
0.0f,
TEXT("Bias applied to LOD calculations for local lights. -1.0 doubles resolution, 1.0 halves it and so on."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<float> CVarResolutionLodBiasLocalMoving(
TEXT("r.Shadow.Virtual.ResolutionLodBiasLocalMoving"),
1.0f,
TEXT("Bias applied to LOD calculations for local lights that are moving. -1.0 doubles resolution, 1.0 halves it and so on.\n")
TEXT("The bias transitions smoothly back to ResolutionLodBiasLocal as the light transitions to non-moving, see 'r.Shadow.Scene.LightActiveFrameCount'."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarVirtualShadowMapFirstPersonClipmapFirstLevel(
TEXT( "r.FirstPerson.Shadow.Virtual.Clipmap.FirstLevel" ),
8,
TEXT( "First level of the virtual clipmap. Lower values allow higher resolution shadows closer to the camera, but may increase page count." ),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarVirtualShadowMapFirstPersonClipmapLastLevel(
TEXT( "r.FirstPerson.Shadow.Virtual.Clipmap.LastLevel" ),
18,
TEXT( "Last level of the virtual clipmap. Indirectly determines radius the clipmap can cover. Each extra level doubles the maximum range, but may increase page count." ),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarVirtualShadowMapNaniteAllowMultipassViews(
TEXT( "r.Shadow.Virtual.Nanite.AllowMultipassViews" ),
1,
TEXT( "When enabled, allows multiple Nanite passes if the view count might exceed Nanite limits.\n" )
TEXT( "This has some performance overhead and is generally not required since views are aggressively culled on the GPU, but can maintain correct rendering in some extreme cases." ),
ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarForceInvalidateLocalVSM(
TEXT("r.Shadow.Virtual.Cache.ForceInvalidateLocal"),
0,
TEXT("Forces local light VSMs (including distant lights) to always invalidate every frame. Generally only used for debugging."),
ECVF_RenderThreadSafe);
extern TAutoConsoleVariable<int32> CVarMarkPixelPagesMipModeLocal;
extern float GMinScreenRadiusForShadowCaster;
bool IsVSMOnePassProjectionEnabled(const FEngineShowFlags& ShowFlags)
{
return CVarVirtualShadowOnePassProjection.GetValueOnAnyThread()
// Debug outputs from projection pass do not support one pass projection
&& (ShowFlags.VisualizeVirtualShadowMap == 0);
}
DECLARE_DWORD_COUNTER_STAT(TEXT("VSM Total Raster Bins"), STAT_VSMNaniteBasePassTotalRasterBins, STATGROUP_ShadowRendering);
DECLARE_DWORD_COUNTER_STAT(TEXT("VSM Visible Raster Bins"), STAT_VSMNaniteBasePassVisibleRasterBins, STATGROUP_ShadowRendering);
DECLARE_DWORD_COUNTER_STAT(TEXT("VSM Total Shading Bins"), STAT_VSMNaniteBasePassTotalShadingBins, STATGROUP_ShadowRendering);
DECLARE_DWORD_COUNTER_STAT(TEXT("VSM Visible Shading Bins"), STAT_VSMNaniteBasePassVisibleShadingBins, STATGROUP_ShadowRendering);
DECLARE_DWORD_COUNTER_STAT(TEXT("Distant Light Count"), STAT_DistantLightCount, STATGROUP_ShadowRendering);
DECLARE_DWORD_COUNTER_STAT(TEXT("Distant Cached Count"), STAT_DistantCachedCount, STATGROUP_ShadowRendering);
DECLARE_DWORD_COUNTER_STAT(TEXT("VSM Light Projections (Directional)"), STAT_VSMDirectionalProjectionFull, STATGROUP_ShadowRendering);
DECLARE_DWORD_COUNTER_STAT(TEXT("VSM Light Projections (Local Full)"), STAT_VSMLocalProjectionFull, STATGROUP_ShadowRendering);
DECLARE_DWORD_COUNTER_STAT(TEXT("VSM Light Projections (Local One Pass Copy)"), STAT_VSMLocalProjectionOnePassCopy, STATGROUP_ShadowRendering);
FShadowSceneRenderer::FShadowSceneRenderer(FDeferredShadingSceneRenderer& InSceneRenderer, FShadowScene& InShadowScene)
: ISceneExtensionRenderer(InSceneRenderer)
, SceneRenderer(InSceneRenderer)
, Scene(InShadowScene.Scene)
, ShadowScene(InShadowScene)
, VirtualShadowMapArray(InSceneRenderer.VirtualShadowMapArray)
, bUseConservativeDistantLightThreshold(CVarUseConservativeDistantLightThreshold.GetValueOnAnyThread())
, DistantLightMode(CVarDistantLightMode.GetValueOnAnyThread())
{
}
float FShadowSceneRenderer::ComputeNaniteShadowsLODScaleFactor()
{
return FMath::Pow(2.0f, -CVarNaniteShadowsLODBias.GetValueOnRenderThread()) * Nanite::GStreamingManager.GetQualityScaleFactor();
}
namespace
{
struct FHeapPair
{
int32 Age;
TSharedPtr<FVirtualShadowMapPerLightCacheEntry> CacheEntry;
// Order for a min-heap, we always want to replace the least-old item
bool operator <(const FHeapPair& Other) const { return Age < Other.Age; }
};
}
void FShadowSceneRenderer::PreInitViews(FRDGBuilder& GraphBuilder)
{
// Clear the frame setups to indicate that nothing is allocated for this frame
CommonSetups.SetNumZeroed(ShadowScene.LightsCommonData.GetMaxIndex());
// Allocate space for each directional light in the scene, one for each view.
bool bIsStereo = SceneRenderer.IsRenderingStereo();
const int32 ShadowNumViews = bIsStereo ? 1 : SceneRenderer.Views.Num();
DirectionalLights.Reserve(ShadowScene.DirectionalLights.Num() * ShadowNumViews);
// pre-allocate indexes and setups for each directional light, strided by the view count.
for (const auto& DirectionalLight : ShadowScene.DirectionalLights)
{
FLightCommonFrameSetup& CommonSetup = CommonSetups[DirectionalLight.LightId];
CommonSetup.bIsDirectional = true;
CommonSetup.SetupIndex = DirectionalLights.Num();
if (bIsStereo)
{
// only set up one for both
DirectionalLights.Emplace(DirectionalLight.LightId, 3u);
check(SceneRenderer.Views.Num() == 2)
}
else
{
for (int32 ViewIndex = 0; ViewIndex < ShadowNumViews; ++ViewIndex)
{
DirectionalLights.Emplace(DirectionalLight.LightId, 1u << ViewIndex);
}
}
}
bNeedVSMOnePassProjection = false;
bNeedMegaLightsProjection = false;
ViewDatas.Reserve(SceneRenderer.Views.Num());
for (const FViewInfo& View : SceneRenderer.Views)
{
const FVector2f ViewSize = FVector2f(View.ViewRect.Size());
FVector2f RadiusClipXY = FVector2f(2.0f) / ViewSize;
const FMatrix &ViewToClip = View.ViewMatrices.GetProjectionMatrix();
// TODO: is RadiusXY always symmetrical?
FVector2f ProjScaleXY = FVector2f(static_cast<float>(ViewToClip.M[0][0]), static_cast<float>(ViewToClip.M[1][1]));
FVector2f RadiusXY = RadiusClipXY / ProjScaleXY;
float MinRadiusXY = FMath::Min(RadiusXY.X, RadiusXY.Y);
float ClipToViewSizeScale = ViewToClip.M[2][3] * MinRadiusXY;
float ClipToViewSizeBias = ViewToClip.M[3][3] * MinRadiusXY;
ViewDatas.Emplace(FViewData{ ClipToViewSizeScale, ClipToViewSizeBias });
}
// Kick off shadow scene updates.
ShadowScene.UpdateForRenderedFrame(GraphBuilder);
// Priority queue of distant lights to update.
const int32 MaxToUpdate = CVarMaxDistantLightsPerFrame.GetValueOnRenderThread() < 0 ? INT32_MAX : CVarMaxDistantLightsPerFrame.GetValueOnRenderThread();
if (MaxToUpdate == 0 || !VirtualShadowMapArray.IsEnabled() || !VirtualShadowMapArray.CacheManager->IsCacheEnabled())
{
return;
}
RendererSetupTask = GraphBuilder.AddSetupTask([this, MaxToUpdate]()
{
FVirtualShadowMapArrayCacheManager& CacheManager = *VirtualShadowMapArray.CacheManager;
TArray<FHeapPair> DistantLightUpdateQueue;
int32 SceneFrameNumber = int32(Scene.GetFrameNumber());
for (auto It = CacheManager.CreateConstEntryIterator(); It; ++It)
{
TSharedPtr<FVirtualShadowMapPerLightCacheEntry> PerLightCacheEntry = It.Value();
if (PerLightCacheEntry->IsFullyCached())
{
int32 Age = SceneFrameNumber - int32(PerLightCacheEntry->GetLastScheduledFrameNumber());
if (DistantLightUpdateQueue.Num() < MaxToUpdate)
{
DistantLightUpdateQueue.HeapPush(FHeapPair{ Age, PerLightCacheEntry});
}
else
{
// Queue is full, but we found an older item
if (DistantLightUpdateQueue.HeapTop().Age < Age)
{
// Replace heap top and restore heap property.
DistantLightUpdateQueue[0] = FHeapPair{ Age, PerLightCacheEntry };
AlgoImpl::HeapSiftDown(DistantLightUpdateQueue.GetData(), 0, DistantLightUpdateQueue.Num(), FIdentityFunctor(), TLess<FHeapPair>());
}
}
}
}
for (const FHeapPair &HeapPair : DistantLightUpdateQueue)
{
// Mark frame it was scheduled, this is picked up later in AddLocalLightShadow to trigger invalidation
HeapPair.CacheEntry->Current.ScheduledFrameNumber = SceneFrameNumber;
}
});
}
UE::Renderer::Private::IShadowInvalidatingInstances *FShadowSceneRenderer::GetInvalidatingInstancesInterface(const FSceneView *SceneView)
{
// No need to collect invalidations if there is nothing to invalidate.
FVirtualShadowMapArrayCacheManager* CacheManager = Scene.GetVirtualShadowMapCache();
if (CacheManager && CacheManager->IsCacheDataAvailable())
{
// TODO: Make use of the SceneView parameter to register invalidations for view-dependent shadows appropriately.
return CacheManager->GetInvalidatingInstancesInterface();
}
return nullptr;
}
static float GetResolutionLODBiasLocal(float LightMobilityFactor, float LightLODBias)
{
return FVirtualShadowMapArray::InterpolateResolutionBias(
CVarResolutionLodBiasLocal.GetValueOnRenderThread(),
CVarResolutionLodBiasLocalMoving.GetValueOnRenderThread(),
LightMobilityFactor) + LightLODBias;
}
void FShadowSceneRenderer::UpdateLocalLightProjectionShaderDataMatrices(
const FProjectedShadowInfo* ProjectedShadowInfo,
int32 MapIndex,
FVirtualShadowMapProjectionShaderData* OutProjectionShaderData) const
{
const FViewMatrices ViewMatrices = ProjectedShadowInfo->GetShadowDepthRenderingViewMatrices(MapIndex, true);
const FDFVector3 PreViewTranslation(ProjectedShadowInfo->PreShadowTranslation);
OutProjectionShaderData->ShadowViewToClipMatrix = FMatrix44f(ViewMatrices.GetProjectionMatrix());
OutProjectionShaderData->TranslatedWorldToShadowUVMatrix = FMatrix44f(CalcTranslatedWorldToShadowUVMatrix( ViewMatrices.GetTranslatedViewMatrix(), ViewMatrices.GetProjectionMatrix() ));
OutProjectionShaderData->TranslatedWorldToShadowUVNormalMatrix = FMatrix44f(CalcTranslatedWorldToShadowUVNormalMatrix( ViewMatrices.GetTranslatedViewMatrix(), ViewMatrices.GetProjectionMatrix() ));
OutProjectionShaderData->PreViewTranslationHigh = PreViewTranslation.High;
OutProjectionShaderData->PreViewTranslationLow = PreViewTranslation.Low;
OutProjectionShaderData->LightDirection = FVector3f(0, 0, 0); // Unused for local lights
OutProjectionShaderData->ClipmapLevel_ClipmapLevelCountRemaining = -1; // Not a clipmap
}
FShadowSceneRenderer::FDirectionalLightShadowFrameSetup* FShadowSceneRenderer::FindDirectional(const FLightSceneInfo::FPersistentId LightId, int32 ViewIndex)
{
// may alternatively be indexed through CommonSetups[LightId].SetupIndex but it is probably more efficient to do a linear search.
for (FDirectionalLightShadowFrameSetup& Setup : DirectionalLights)
{
if (LightId == Setup.LightId && (Setup.ViewMask & (1u << ViewIndex)) != 0u)
{
return &Setup;
}
}
check(false);
return nullptr;
}
/**
* Calculate the radius in world-space units of a single pixel at a given depth.
*/
static float GetWorldSpacePixelFootprint(float ViewSpaceDepth, float ClipToViewSizeScale, float ClipToViewSizeBias)
{
return ViewSpaceDepth * ClipToViewSizeScale + ClipToViewSizeBias;
}
/**
* Compute the lowest (highest res) mip level that might be marked by any pixels inside the light influence radius for a given scene primary view.
*/
static uint32 GetConservativeMipLevelLocal(const FViewInfo& View, float ClipToViewSizeScale, float ClipToViewSizeBias, const FVector& LightOrigin, float LightRadius, float WorldToShadowFootprintScale, float ResolutionLodBias, float GlobalResolutionLodBias, uint32 MipModeLocal)
{
// Note: not just a rotation, full world-space DP.
FVector ViewSpaceOrigin = View.GetShadowViewMatrices().GetViewMatrix().TransformPosition(LightOrigin);
// Remove radius to arrive at minimum possible z-distance in view space, from primary view.
float RadiusWorld = GetWorldSpacePixelFootprint(FMath::Max(0.0f, float(ViewSpaceOrigin.Z) - LightRadius), ClipToViewSizeScale, ClipToViewSizeBias);
// Radius is the max possible shadow view space Z, which would require the max res.
float ShadowFootprint = RadiusWorld * WorldToShadowFootprintScale / LightRadius;
return UE::HLSL::GetMipLevelLocal(ShadowFootprint, MipModeLocal, ResolutionLodBias, GlobalResolutionLodBias);
}
TSharedPtr<FVirtualShadowMapPerLightCacheEntry> FShadowSceneRenderer::AddLocalLightShadow(const FWholeSceneProjectedShadowInitializer& ProjectedShadowInitializer, FProjectedShadowInfo* ProjectedShadowInfo, FLightSceneInfo* LightSceneInfo, float MaxScreenRadius)
{
FVirtualShadowMapArrayCacheManager* CacheManager = VirtualShadowMapArray.CacheManager;
FLightSceneInfo::FPersistentId LightId = LightSceneInfo->Id;
FLightCommonFrameSetup& CommonSetup = CommonSetups[LightId];
// prevent double allocation
check(!CommonSetup.bHasVirtualShadowMap);
CommonSetup.bHasVirtualShadowMap = true;
// link from ID to the allocated local shadow slot
CommonSetup.SetupIndex = uint32(LocalLights.Num());
FLocalLightShadowFrameSetup& LocalLightSetup = LocalLights.AddDefaulted_GetRef();
LocalLightSetup.ProjectedShadowInfo = ProjectedShadowInfo;
LocalLightSetup.LightSceneInfo = LightSceneInfo;
const FLightSceneProxy* LightSceneProxy = ProjectedShadowInfo->GetLightSceneInfo().Proxy;
const float ResolutionLODBiasLocal = GetResolutionLODBiasLocal(ShadowScene.GetLightMobilityFactor(LightId), LightSceneProxy->GetVSMResolutionLodBias());
// Compute conservative mip level estimate based on radius of the bounding sphere.
// TODO: can probably do better by finding closest point on cone for certain scenarios? Not as important as it might seem as the worst case is for a narrow cone, but then the narrow FOV limits the required resolution.
const FVector2f ShadowViewSize = FVector2f(FVirtualShadowMap::VirtualMaxResolutionXY, FVirtualShadowMap::VirtualMaxResolutionXY);
const FMatrix& ShadowViewToClip = ProjectedShadowInfo->bOnePassPointLightShadow ? ProjectedShadowInfo->OnePassShadowFaceProjectionMatrix : ProjectedShadowInfo->ViewToClipOuter;
float ShadowProjScale = ShadowViewToClip.M[0][0]; // always symmetrical
const float WorldToShadowFootprintScale = ShadowProjScale * ShadowViewSize.X;
// TODO: this (min distance calc) is duplicated in more places, consolidate.
int32 ClosestCullingViewIndex = 0;
uint32 MinMipLevel = FVirtualShadowMap::MaxMipLevels;
double MinDistanceSq = DBL_MAX;
for (int32 ViewIndex = 0; ViewIndex < SceneRenderer.Views.Num(); ++ViewIndex)
{
const FViewInfo& View = SceneRenderer.Views[ViewIndex];
const FViewData& ViewData = ViewDatas[ViewIndex];
FVector TestOrigin = View.GetShadowViewMatrices().GetViewOrigin();
double TestDistanceSq = (TestOrigin + ProjectedShadowInfo->PreShadowTranslation).SquaredLength();
if (TestDistanceSq < MinDistanceSq)
{
ClosestCullingViewIndex = ViewIndex;
MinDistanceSq = TestDistanceSq;
}
MinMipLevel = FMath::Min(MinMipLevel, GetConservativeMipLevelLocal(
View,
ViewData.ClipToViewSizeScale,
ViewData.ClipToViewSizeBias,
LightSceneProxy->GetOrigin(),
LightSceneProxy->GetRadius(),
WorldToShadowFootprintScale,
ResolutionLODBiasLocal,
CacheManager->GetGlobalResolutionLodBias(),
CVarMarkPixelPagesMipModeLocal.GetValueOnRenderThread()
));
}
bool bIsDistantLight = DistantLightMode == 2;
bool bShouldForceTimeSliceDistantUpdate = false;
if (DistantLightMode == 1)
{
if (bUseConservativeDistantLightThreshold)
{
// use distant light only if we are sure that there's only one mip level.
bIsDistantLight = MinMipLevel == (FVirtualShadowMap::MaxMipLevels - 1);
bShouldForceTimeSliceDistantUpdate = false;// TODO: (bIsDistantLight && MaxScreenRadius <= BiasedFootprintThreshold * DistantLightForceCacheFootprintFraction); ??
}
else
{
// Single page res, at this point we force the VSM to be single page
const float BiasedFootprintThreshold = float(FVirtualShadowMap::PageSize) * FMath::Exp2(ResolutionLODBiasLocal - LightSceneProxy->GetVSMResolutionLodBias());
bIsDistantLight = MaxScreenRadius <= BiasedFootprintThreshold;
const float DistantLightForceCacheFootprintFraction = FMath::Clamp(CVarDistantLightForceCacheFootprintFraction.GetValueOnRenderThread(), 0.0f, 1.0f);
bShouldForceTimeSliceDistantUpdate = (bIsDistantLight && MaxScreenRadius <= BiasedFootprintThreshold * DistantLightForceCacheFootprintFraction);
}
}
const int32 NumMaps = ProjectedShadowInitializer.bOnePassPointLightShadow ? 6 : 1;
TSharedPtr<FVirtualShadowMapPerLightCacheEntry> PerLightCacheEntry = CacheManager->FindCreateLightCacheEntry(LightId, 0, NumMaps);
LocalLightSetup.PerLightCacheEntry = PerLightCacheEntry;
PerLightCacheEntry->UpdateLocal(
ProjectedShadowInitializer,
LightSceneProxy->GetOrigin(),
LightSceneProxy->GetRadius(),
bIsDistantLight,
!CacheManager->IsCacheEnabled() || (CVarForceInvalidateLocalVSM.GetValueOnRenderThread() != 0),
!bShouldForceTimeSliceDistantUpdate,
IsVirtualShadowMapLocalReceiverMaskEnabled());
if (bIsDistantLight && PerLightCacheEntry->Prev.ScheduledFrameNumber == Scene.GetFrameNumber())
{
PerLightCacheEntry->Invalidate();
}
// Update info on the ProjectionShadowInfo; eventually this should all move into local data structures here
const int32 VirtualShadowMapId = VirtualShadowMapArray.Allocate(bIsDistantLight, NumMaps);
LocalLightSetup.VirtualShadowMapId = VirtualShadowMapId;
ProjectedShadowInfo->VirtualShadowMapId = VirtualShadowMapId;
ProjectedShadowInfo->VirtualShadowMapPerLightCacheEntry = PerLightCacheEntry;
ProjectedShadowInfo->bShouldRenderVSM = !PerLightCacheEntry->IsFullyCached();
{
uint32 PackedCullingViewId = FVirtualShadowMapProjectionShaderData::PackCullingViewId(SceneRenderer.Views[ClosestCullingViewIndex].SceneRendererPrimaryViewId, SceneRenderer.Views[ClosestCullingViewIndex].PersistentViewId);
uint32 Flags = PerLightCacheEntry->IsUncached() ? VSM_PROJ_FLAG_UNCACHED : 0U;
Flags |= PerLightCacheEntry->ShouldUseReceiverMask() ? VSM_PROJ_FLAG_USE_RECEIVER_MASK : 0U;
const FLightSceneProxy* Proxy = ProjectedShadowInfo->GetLightSceneInfo().Proxy;
// For now just tie this to whether anything has invalidated the light (including movement)
// This is slightly over-conservative but catches the important cases
const bool bUpdateMatrices = PerLightCacheEntry->IsInvalidated();
for (int32 Index = 0; Index < NumMaps; ++Index)
{
const int32 FaceVirtualShadowMapId = VirtualShadowMapId + Index;
FVirtualShadowMapCacheEntry& VirtualSmCacheEntry = PerLightCacheEntry->ShadowMapEntries[Index];
VirtualSmCacheEntry.Update(VirtualShadowMapArray, *PerLightCacheEntry, FaceVirtualShadowMapId);
FVirtualShadowMapProjectionShaderData& ProjectionData = VirtualSmCacheEntry.ProjectionData;
if (bUpdateMatrices)
{
UpdateLocalLightProjectionShaderDataMatrices(ProjectedShadowInfo, Index, &ProjectionData);
}
// TODO: All of this is per-light data; splitting this out to a separate structure could help
ProjectionData.LightType = Proxy->GetLightType();
ProjectionData.LightSourceRadius = Proxy->GetSourceRadius();
ProjectionData.LightRadius = Proxy->GetRadius();
ProjectionData.TexelDitherScale = Proxy->GetVSMTexelDitherScale();
ProjectionData.ResolutionLodBias = ResolutionLODBiasLocal;
ProjectionData.Flags = Flags;
ProjectionData.MinMipLevel = MinMipLevel;
ProjectionData.PackedCullingViewId = PackedCullingViewId;
}
}
// TODO: This is remarkably slow and shouldn't really need to be evaluated multiple times
FLightOcclusionType OcclusionType = GetLightOcclusionType(*LightSceneInfo->Proxy, SceneRenderer.ViewFamily);
// Depending on which type of projection we're going to use, mark that we need to associated path for later
if (OcclusionType == FLightOcclusionType::Shadowmap)
{
bNeedVSMOnePassProjection = true;
}
else if (OcclusionType == FLightOcclusionType::MegaLightsVSM)
{
bNeedMegaLightsProjection = true;
}
else
{
// ??? Should not get into this path with other projection types
check(false);
}
return PerLightCacheEntry;
}
void FShadowSceneRenderer::AddDirectionalLightShadow(FLightSceneInfo& LightSceneInfo, FViewInfo& View, float MaxNonFarCascadeDistance, TArray<FProjectedShadowInfo*, SceneRenderingAllocator>& OutShadowInfosThatNeedCulling)
{
const int32 ViewIndex = View.SceneRendererPrimaryViewId;
FLightSceneInfo::FPersistentId LightId = LightSceneInfo.Id;
FVisibleLightInfo& VisibleLightInfo = SceneRenderer.VisibleLightInfos[LightId];
FLightCommonFrameSetup& CommonSetup = CommonSetups[LightId];
check(CommonSetup.bIsDirectional);
CommonSetup.bHasVirtualShadowMap = true;
// Helper function to create a projected shadow info. This is needed to:
// * Get the matrices included in the shadow rendering pass setup, driving nanite VSM rendering (which VisibleLightInfo.AllProjectedShadows is appended to)
auto AddLegacySetup = [&](const TSharedPtr<FVirtualShadowMapClipmap>& Clipmap, bool bQueueforNonNaniteCulling)
{
FProjectedShadowInfo* ProjectedShadowInfo = SceneRenderer.Allocator.Create<FProjectedShadowInfo>();
ProjectedShadowInfo->SetupClipmapProjection(&LightSceneInfo, &View, Clipmap, CVarVsmUseFarShadowRules.GetValueOnRenderThread() != 0 ? MaxNonFarCascadeDistance : -1.0f);
// This is needed to get it into the line for ending up in the SortedShadowsForShadowDepthPass.VirtualShadowMapShadows which is what drives the shadow rendering.
VisibleLightInfo.AllProjectedShadows.Add(ProjectedShadowInfo);
ProjectedShadowInfo->VirtualShadowMapPerLightCacheEntry = Clipmap->GetCacheEntry();
if (bQueueforNonNaniteCulling)
{
OutShadowInfosThatNeedCulling.Emplace(ProjectedShadowInfo);
}
return ProjectedShadowInfo;
};
// a secondary view should not allocate shadow for itself!
check(View.GetPrimaryView() == &View);
FDirectionalLightShadowFrameSetup& Setup = DirectionalLights[CommonSetup.SetupIndex + ViewIndex];
check(Setup.LightId == LightId);
check(Setup.ViewMask & (1u << ViewIndex));
// Set up regular clipmap
FDirectionalLightShadowFrameSetup::FClipmapInfo& RegularInfo = Setup.ClipmapInfos[EVirtualShadowTypeId::Regular];
{
FVirtualShadowMapClipmapConfig Config = FVirtualShadowMapClipmapConfig::GetGlobal();
TSharedPtr<FVirtualShadowMapClipmap> VirtualShadowMapClipmap = TSharedPtr<FVirtualShadowMapClipmap>(new FVirtualShadowMapClipmap(
VirtualShadowMapArray,
LightSceneInfo,
View.ViewMatrices,
View.ViewRect.Size(),
&View,
ShadowScene.GetLightMobilityFactor(LightSceneInfo.Id),
Config));
// NOTE: only contains "regular" clipmaps, the alternate types are internal to the system and needs to be queried for.
VisibleLightInfo.VirtualShadowMapClipmaps.Add(VirtualShadowMapClipmap);
// NOTE: If there are multiple camera views this will simply be associated with "one of them"
VisibleLightInfo.VirtualShadowMapId = VirtualShadowMapClipmap->GetVirtualShadowMapId();
RegularInfo.ProjectedShadowInfo = AddLegacySetup(VirtualShadowMapClipmap, true);
RegularInfo.Clipmap = VirtualShadowMapClipmap;
}
if (!ShadowScene.FirstPersonWorldSpacePrimitives.IsEmpty())
{
FDirectionalLightShadowFrameSetup::FClipmapInfo& FPInfo = Setup.ClipmapInfos[EVirtualShadowTypeId::FirstPerson];
// Clone the setup from the regular VSM clipmap
FVirtualShadowMapClipmapConfig Config = FVirtualShadowMapClipmapConfig::GetGlobal();
Config.ShadowTypeId = EVirtualShadowTypeId::FirstPerson;
Config.bForceInvalidate = true;
Config.FirstCoarseLevel = -1;
Config.LastCoarseLevel = -1;
Config.FirstLevel = CVarVirtualShadowMapFirstPersonClipmapFirstLevel.GetValueOnRenderThread();
Config.LastLevel = CVarVirtualShadowMapFirstPersonClipmapLastLevel.GetValueOnRenderThread();
Config.bIsFirstPersonShadow = true;
FPInfo.Clipmap = TSharedPtr<FVirtualShadowMapClipmap>(new FVirtualShadowMapClipmap(
VirtualShadowMapArray,
LightSceneInfo,
View.ViewMatrices,
View.ViewRect.Size(),
&View,
1.0f,// mobility factor as if moving - this VSM has no persistence, though perhaps it should for HZB?
Config
));
FPInfo.ProjectedShadowInfo = AddLegacySetup(FPInfo.Clipmap, false);
for (FPrimitiveSceneInfo* PrimitiveSceneInfo : ShadowScene.FirstPersonWorldSpacePrimitives)
{
// _invert_ the shadow relevance because we want to render the thing despite being owner no see, and NOT anyting that is the opposite (owned by other views).
FPInfo.ProjectedShadowInfo->AddSubjectPrimitive(PrimitiveSceneInfo, SceneRenderer.Views, false, true);
}
}
FLightOcclusionType OcclusionType = GetLightOcclusionType(*LightSceneInfo.Proxy, SceneRenderer.ViewFamily);
// Mark that we need MegaLights projection pass if this light uses it
// We don't need to explicitly mark VSM projection here since directional lights don't go through one pass projection,
// and thus will naturally go down the path that will render into the screen shadow mask.
if (OcclusionType == FLightOcclusionType::MegaLightsVSM)
{
bNeedMegaLightsProjection = true;
}
}
void FShadowSceneRenderer::PostInitDynamicShadowsSetup()
{
// Dispatch async Nanite culling job if appropriate
if (CVarVSMMaterialVisibility.GetValueOnRenderThread() != 0)
{
TArray<FConvexVolume, SceneRenderingAllocator> NaniteCullingViewsVolumes;
// If we have a clipmap that can't be culled, it'd be a complete waste of time to cull the local lights.
bool bUnboundedClipmap = false;
for (const FDirectionalLightShadowFrameSetup& DirectionalLightShadowFrameSetup : DirectionalLights)
{
FProjectedShadowInfo* ProjectedShadowInfo = DirectionalLightShadowFrameSetup.ClipmapInfos[EVirtualShadowTypeId::Regular].ProjectedShadowInfo;
if (!bUnboundedClipmap && ProjectedShadowInfo && ProjectedShadowInfo->bShouldRenderVSM)
{
const bool bIsCached = !ProjectedShadowInfo->VirtualShadowMapPerLightCacheEntry->IsUncached();
// We can only do this culling if the light is both uncached & it is using the accurate bounds (i.e., r.Shadow.Virtual.Clipmap.UseConservativeCulling is turned off).
if (!bIsCached && !ProjectedShadowInfo->CascadeSettings.ShadowBoundsAccurate.Planes.IsEmpty())
{
NaniteCullingViewsVolumes.Add(ProjectedShadowInfo->CascadeSettings.ShadowBoundsAccurate);
}
else
{
bUnboundedClipmap = true;
}
}
}
if (!bUnboundedClipmap)
{
for (const FLocalLightShadowFrameSetup& LocalLightShadowFrameSetup : LocalLights)
{
FProjectedShadowInfo* ProjectedShadowInfo = LocalLightShadowFrameSetup.ProjectedShadowInfo;
if (ProjectedShadowInfo->bShouldRenderVSM)
{
FConvexVolume WorldSpaceCasterOuterFrustum = ProjectedShadowInfo->CasterOuterFrustum;
for (FPlane& Plane : WorldSpaceCasterOuterFrustum.Planes)
{
Plane = Plane.TranslateBy(-ProjectedShadowInfo->PreShadowTranslation);
}
WorldSpaceCasterOuterFrustum.Init();
NaniteCullingViewsVolumes.Add(WorldSpaceCasterOuterFrustum);
}
}
if (!NaniteCullingViewsVolumes.IsEmpty())
{
NaniteVisibilityQuery = Scene.NaniteVisibility[ENaniteMeshPass::BasePass].BeginVisibilityQuery(
SceneRenderer.Allocator,
Scene,
NaniteCullingViewsVolumes,
&Scene.NaniteRasterPipelines[ENaniteMeshPass::BasePass],
&Scene.NaniteShadingPipelines[ENaniteMeshPass::BasePass]
);
}
}
}
}
void FShadowSceneRenderer::RenderVirtualShadowMaps(FRDGBuilder& GraphBuilder, bool bNaniteEnabled, bool bUpdateNaniteStreaming)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FShadowSceneRenderer::RenderVirtualShadowMaps);
// Always process an existing query if it exists
if (NaniteVisibilityQuery != nullptr)
{
#if STATS
GraphBuilder.AddSetupTask([Query = NaniteVisibilityQuery]
{
const FNaniteVisibilityResults& VisibilityResults = *Nanite::GetVisibilityResults(Query);
uint32 TotalRasterBins = 0;
uint32 VisibleRasterBins = 0;
VisibilityResults.GetRasterBinStats(VisibleRasterBins, TotalRasterBins);
uint32 TotalShadingBins = 0;
uint32 VisibleShadingBins = 0;
VisibilityResults.GetShadingBinStats(VisibleShadingBins, TotalShadingBins);
SET_DWORD_STAT(STAT_VSMNaniteBasePassTotalRasterBins, TotalRasterBins);
SET_DWORD_STAT(STAT_VSMNaniteBasePassVisibleRasterBins, VisibleRasterBins);
SET_DWORD_STAT(STAT_VSMNaniteBasePassTotalShadingBins, TotalShadingBins);
SET_DWORD_STAT(STAT_VSMNaniteBasePassVisibleShadingBins, VisibleShadingBins);
}, Nanite::GetVisibilityTask(NaniteVisibilityQuery));
#endif
}
if (VirtualShadowMapArray.GetNumShadowMaps() == 0)
{
return;
}
if (bNaniteEnabled && NaniteRenderPasses.Num() > 0)
{
VirtualShadowMapArray.RenderVirtualShadowMapsNanite(
GraphBuilder,
SceneRenderer,
bUpdateNaniteStreaming,
NaniteVisibilityQuery,
NaniteRenderPasses);
}
if (UseNonNaniteVirtualShadowMaps(SceneRenderer.ShaderPlatform, SceneRenderer.FeatureLevel))
{
const TArray<FProjectedShadowInfo*, SceneRenderingAllocator>& VirtualShadowMapShadows = SceneRenderer.SortedShadowsForShadowDepthPass.VirtualShadowMapShadows;
VirtualShadowMapArray.RenderVirtualShadowMapsNonNanite(GraphBuilder, SceneRenderer.GetSceneUniforms(), VirtualShadowMapShadows, SceneRenderer.Views);
}
VirtualShadowMapArray.PostRender(GraphBuilder);
}
void FShadowSceneRenderer::DispatchVirtualShadowMapViewAndCullingSetup(FRDGBuilder& GraphBuilder, TConstArrayView<FProjectedShadowInfo*> VirtualShadowMapShadows)
{
// Unconditionally update GPU physical pages (on all GPUs) with new VSM IDs/addresses
VirtualShadowMapArray.UpdatePhysicalPageAddresses(GraphBuilder);
if (!VirtualShadowMapShadows.IsEmpty() && UseNanite(SceneRenderer.ShaderPlatform))
{
CreateNaniteRenderPasses(GraphBuilder, SceneRenderer.Views, VirtualShadowMapShadows);
// Dispatch collected queries
for (FNaniteVirtualShadowMapRenderPass& RenderPass : NaniteRenderPasses)
{
RenderPass.SceneInstanceCullingQuery->Dispatch(GraphBuilder);
}
}
}
void FShadowSceneRenderer::PostSetupDebugRender()
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
if ((SceneRenderer.ViewFamily.EngineShowFlags.DebugDrawDistantVirtualSMLights) && VirtualShadowMapArray.IsEnabled())
{
int32 NumFullyCached = 0;
int32 NumDistant = 0;
for (FViewInfo& View : SceneRenderer.Views)
{
FViewElementPDI DebugPDI(&View, nullptr, nullptr);
for (const FLocalLightShadowFrameSetup& LightSetup : LocalLights)
{
FLinearColor Color = FLinearColor(FColor::Blue);
if (LightSetup.PerLightCacheEntry && LightSetup.PerLightCacheEntry->bIsDistantLight)
{
++NumDistant;
int32 FramesSinceLastRender = int32(Scene.GetFrameNumber()) - int32(LightSetup.PerLightCacheEntry->GetLastScheduledFrameNumber());
float Fade = FMath::Min(0.8f, float(FramesSinceLastRender) / float(LocalLights.Num()));
if (LightSetup.PerLightCacheEntry->IsFullyCached())
{
++NumFullyCached;
Color = FMath::Lerp(FLinearColor(FColor::Green), FLinearColor(FColor::Red), Fade);
}
else
{
Color = FLinearColor(FColor::Purple);
}
}
Color.A = 1.0f;
if (LightSetup.LightSceneInfo->Proxy->GetLightType() == LightType_Spot)
{
FTransform TransformNoScale = FTransform(LightSetup.LightSceneInfo->Proxy->GetLightToWorld());
TransformNoScale.RemoveScaling();
DrawWireSphereCappedCone(&DebugPDI, TransformNoScale, LightSetup.LightSceneInfo->Proxy->GetRadius(), FMath::RadiansToDegrees(LightSetup.LightSceneInfo->Proxy->GetOuterConeAngle()), 16, 4, 8, Color, SDPG_World);
}
else
{
DrawWireSphereAutoSides(&DebugPDI, -LightSetup.ProjectedShadowInfo->PreShadowTranslation, Color, LightSetup.LightSceneInfo->Proxy->GetRadius(), SDPG_World);
}
}
}
SET_DWORD_STAT(STAT_DistantLightCount, NumDistant);
SET_DWORD_STAT(STAT_DistantCachedCount, NumFullyCached);
}
#endif
}
void FShadowSceneRenderer::RenderVirtualShadowMaps(
FRDGBuilder& GraphBuilder,
bool bNaniteEnabled,
const FSingleLayerWaterPrePassResult* SingleLayerWaterPrePassResult,
const FFrontLayerTranslucencyData& FrontLayerTranslucencyData,
const Froxel::FRenderer& FroxelRenderer)
{
if (!VirtualShadowMapArray.IsEnabled())
{
return;
}
VirtualShadowMapArray.BuildPageAllocations(GraphBuilder,
SceneRenderer.GetActiveSceneTextures(),
SceneRenderer.Views,
SceneRenderer,
SceneRenderer.VisibleLightInfos,
SingleLayerWaterPrePassResult,
FrontLayerTranslucencyData,
FroxelRenderer,
AreAnyLocalLightsPreset()
);
RenderVirtualShadowMaps(GraphBuilder, bNaniteEnabled, CVarNaniteShadowsUpdateStreaming.GetValueOnRenderThread() != 0);
}
void FShadowSceneRenderer::RenderVirtualShadowMapProjectionMaskBits(
FRDGBuilder& GraphBuilder,
FMinimalSceneTextures& SceneTextures)
{
bShouldUseVirtualShadowMapOnePassProjection =
VirtualShadowMapArray.IsAllocated() &&
IsVSMOnePassProjectionEnabled(SceneRenderer.ViewFamily.EngineShowFlags) &&
bNeedVSMOnePassProjection;
if (bShouldUseVirtualShadowMapOnePassProjection)
{
RDG_EVENT_SCOPE(GraphBuilder, "VirtualShadowMapProjectionMaskBits");
VirtualShadowMapMaskBits = CreateVirtualShadowMapMaskBits(GraphBuilder, SceneTextures, VirtualShadowMapArray, TEXT("Shadow.Virtual.MaskBits"));
VirtualShadowMapMaskBitsHairStrands = CreateVirtualShadowMapMaskBits(GraphBuilder, SceneTextures, VirtualShadowMapArray, TEXT("Shadow.Virtual.MaskBits(HairStrands)"));
for (int32 ViewIndex = 0; ViewIndex < SceneRenderer.Views.Num(); ++ViewIndex)
{
RDG_EVENT_SCOPE_CONDITIONAL(GraphBuilder, SceneRenderer.Views.Num() > 1, "View%d", ViewIndex);
const FViewInfo& View = SceneRenderer.Views[ViewIndex];
RenderVirtualShadowMapProjectionOnePass(
GraphBuilder,
SceneTextures,
View, ViewIndex,
VirtualShadowMapArray,
EVirtualShadowMapProjectionInputType::GBuffer,
VirtualShadowMapMaskBits);
if (HairStrands::HasViewHairStrandsData(View))
{
// Shadow bits
RenderVirtualShadowMapProjectionOnePass(
GraphBuilder,
SceneTextures,
View, ViewIndex,
VirtualShadowMapArray,
EVirtualShadowMapProjectionInputType::HairStrands,
VirtualShadowMapMaskBitsHairStrands);
// Transmittance bits
HairTransmittanceMaskBits = RenderHairStrandsOnePassTransmittanceMask(GraphBuilder, View, ViewIndex, VirtualShadowMapMaskBitsHairStrands, VirtualShadowMapArray).TransmittanceMask;
}
}
}
else
{
VirtualShadowMapMaskBits = nullptr;//Dummy;
VirtualShadowMapMaskBitsHairStrands = nullptr;//Dummy;
HairTransmittanceMaskBits = nullptr; //Dummy
}
}
void FShadowSceneRenderer::RenderVirtualShadowMapProjection(
FRDGBuilder& GraphBuilder,
const FMinimalSceneTextures& SceneTextures,
FLightSceneInfo::FPersistentId LightId,
const FViewInfo& View, int32 ViewIndex,
const FIntRect ScissorRect,
EVirtualShadowMapProjectionInputType InputType,
bool bModulateRGB,
FTiledVSMProjection* TiledVSMProjection,
FRDGTextureRef OutputShadowMaskTexture)
{
FShadowSceneRenderer::FDirectionalLightShadowFrameSetup* DirSetup = FindDirectional(LightId, ViewIndex);
if (DirSetup != nullptr)
{
::RenderVirtualShadowMapProjection(
GraphBuilder,
SceneTextures,
View, ViewIndex,
VirtualShadowMapArray,
ScissorRect,
EVirtualShadowMapProjectionInputType::GBuffer,
DirSetup->ClipmapInfos[EVirtualShadowTypeId::Regular].Clipmap,
true, // bModulateRGB
TiledVSMProjection,
OutputShadowMaskTexture,
DirSetup->ClipmapInfos[EVirtualShadowTypeId::FirstPerson].Clipmap);
}
}
TArray<int32, SceneRenderingAllocator> FShadowSceneRenderer::GatherClipmapIds(int32 ViewIndex) const
{
TArray<int32, SceneRenderingAllocator> Result;
Result.Reserve(DirectionalLights.Num() * EVirtualShadowTypeId::Max );
for (const FDirectionalLightShadowFrameSetup& DirSetup : DirectionalLights)
{
if (DirSetup.ViewMask & (1u << ViewIndex))
{
for (const auto& Info: DirSetup.ClipmapInfos)
{
if (Info.Clipmap)
{
Result.Emplace(Info.Clipmap->GetVirtualShadowMapId());
}
}
}
}
return Result;
}
void FShadowSceneRenderer::ApplyVirtualShadowMapProjectionForLight(
FRDGBuilder& GraphBuilder,
const FMinimalSceneTextures& SceneTextures,
const FLightSceneInfo* LightSceneInfo,
const EVirtualShadowMapProjectionInputType InputType,
FRDGTextureRef OutputScreenShadowMaskTexture)
{
if (!VirtualShadowMapArray.HasAnyShadowData())
{
return;
}
FLightSceneInfo::FPersistentId LightId = LightSceneInfo->Id;
const FLightCommonFrameSetup& CommonSetup = CommonSetups[LightId];
// No VSM set up this frame
if (!CommonSetup.bHasVirtualShadowMap)
{
return;
}
const FVisibleLightInfo& VisibleLightInfo = SceneRenderer.VisibleLightInfos[LightId];
if (!VisibleLightInfo.HasVirtualShadowMap())
{
return;
}
// Some lights can elide the screen shadow mask entirely, in which case they will be sampled directly in the lighting shader
if (!OutputScreenShadowMaskTexture)
{
return;
}
for (int32 ViewIndex = 0; ViewIndex < SceneRenderer.Views.Num(); ++ViewIndex)
{
RDG_EVENT_SCOPE_CONDITIONAL(GraphBuilder, SceneRenderer.Views.Num() > 1, "View%d", ViewIndex);
FViewInfo& View = SceneRenderer.Views[ViewIndex];
FIntRect ScissorRect;
if (!LightSceneInfo->Proxy->GetScissorRect(ScissorRect, View, View.ViewRect))
{
ScissorRect = View.ViewRect;
}
if (ScissorRect.Area() > 0)
{
if (InputType == EVirtualShadowMapProjectionInputType::HairStrands && !HairStrands::HasViewHairStrandsData(View))
{
continue;
}
if (CommonSetup.bIsDirectional)
{
// remap to use the primary view index for stereo rendering.
int32 ShadowViewIndex = View.GetPrimaryView()->SceneRendererPrimaryViewId;
const FDirectionalLightShadowFrameSetup& DirSetup = DirectionalLights[CommonSetup.SetupIndex + ShadowViewIndex];
INC_DWORD_STAT(STAT_VSMDirectionalProjectionFull);
// Project directional light virtual shadow map
::RenderVirtualShadowMapProjection(
GraphBuilder,
SceneTextures,
View, ViewIndex,
VirtualShadowMapArray,
ScissorRect,
InputType,
DirSetup.ClipmapInfos[EVirtualShadowTypeId::Regular].Clipmap,
false, // bModulateRGB
nullptr, // TiledVSMProjection
OutputScreenShadowMaskTexture,
DirSetup.ClipmapInfos[EVirtualShadowTypeId::FirstPerson].Clipmap);
}
else
{
const FLocalLightShadowFrameSetup& LocalLightSetup = LocalLights[CommonSetup.SetupIndex];
if (bShouldUseVirtualShadowMapOnePassProjection)
{
INC_DWORD_STAT(STAT_VSMLocalProjectionOnePassCopy);
// Copy local light from one pass projection output
CompositeVirtualShadowMapFromMaskBits(
GraphBuilder,
SceneTextures,
View, ViewIndex,
ScissorRect,
VirtualShadowMapArray,
InputType,
LocalLightSetup.VirtualShadowMapId,
InputType == EVirtualShadowMapProjectionInputType::HairStrands ? VirtualShadowMapMaskBitsHairStrands : VirtualShadowMapMaskBits,
OutputScreenShadowMaskTexture);
}
else
{
INC_DWORD_STAT(STAT_VSMLocalProjectionFull);
// Project local light virtual shadow map
::RenderVirtualShadowMapProjection(
GraphBuilder,
SceneTextures,
View, ViewIndex,
VirtualShadowMapArray,
ScissorRect,
InputType,
*LightSceneInfo,
LocalLightSetup.VirtualShadowMapId,
OutputScreenShadowMaskTexture);
}
}
}
}
}
struct FVSMRenderViewInfo
{
FCullingVolume CullingVolume;
uint32 NumPrimaryViews = 0u;
uint32 MaxCullingViews = 0u;
};
static FVSMRenderViewInfo GetRenderViewInfo(const FProjectedShadowInfo* ProjectedShadowInfo)
{
FVSMRenderViewInfo Info;
Info.CullingVolume.WorldToVolumeTranslation = ProjectedShadowInfo->PreShadowTranslation;
uint32 NumPrimaryViews = 0;
if (ProjectedShadowInfo->VirtualShadowMapClipmap)
{
Info.NumPrimaryViews = ProjectedShadowInfo->VirtualShadowMapClipmap->GetLevelCount();
Info.MaxCullingViews = Info.NumPrimaryViews;
const bool bIsCached = ProjectedShadowInfo->VirtualShadowMapClipmap->GetCacheEntry() && !ProjectedShadowInfo->VirtualShadowMapClipmap->GetCacheEntry()->IsUncached();
// We can only do this culling if the light is both uncached & it is using the accurate bounds (i.e., r.Shadow.Virtual.Clipmap.UseConservativeCulling is turned off).
if (!bIsCached && !ProjectedShadowInfo->CascadeSettings.ShadowBoundsAccurate.Planes.IsEmpty())
{
Info.CullingVolume.ConvexVolume = ProjectedShadowInfo->CascadeSettings.ShadowBoundsAccurate;
// ShadowBoundsAccurate is in world-space
Info.CullingVolume.WorldToVolumeTranslation = FVector3d::ZeroVector;
}
else
{
Info.CullingVolume.Sphere = ProjectedShadowInfo->VirtualShadowMapClipmap->GetBoundingSphere();
Info.CullingVolume.ConvexVolume = ProjectedShadowInfo->VirtualShadowMapClipmap->GetViewFrustumBounds();
}
}
else
{
Info.NumPrimaryViews = ProjectedShadowInfo->bOnePassPointLightShadow ? 6u : 1u;
Info.CullingVolume.Sphere = ProjectedShadowInfo->GetLightSceneInfo().Proxy->GetBoundingSphere();
Info.CullingVolume.ConvexVolume = ProjectedShadowInfo->CasterOuterFrustum;
uint32 MinMipLevel = ProjectedShadowInfo->VirtualShadowMapPerLightCacheEntry->ShadowMapEntries[0].ProjectionData.MinMipLevel;
Info.MaxCullingViews = Info.NumPrimaryViews * (FVirtualShadowMap::MaxMipLevels - MinMipLevel);
}
return Info;
}
void FShadowSceneRenderer::CreateNaniteViewsForPass(
FRDGBuilder& GraphBuilder,
const FVirtualShadowMapArray& VirtualShadowMapArray,
TConstArrayView<FViewInfo> Views,
float ShadowsLODScaleFactor,
FNaniteVirtualShadowMapRenderPass& InOutRenderPass)
{
InOutRenderPass.VirtualShadowMapViews = Nanite::FPackedViewArray::CreateWithSetupTask(
GraphBuilder,
InOutRenderPass.TotalPrimaryViews,
[VirtualShadowMapArray, Views, InOutRenderPass, ShadowsLODScaleFactor] (Nanite::FPackedViewArray::ArrayType& VirtualShadowViews)
{
TRACE_CPUPROFILER_EVENT_SCOPE(AddNaniteRenderViews);
const bool bUseHzbOcclusion = VirtualShadowMapArray.UseHzbOcclusion();
for (FProjectedShadowInfo* Shadow : InOutRenderPass.Shadows)
{
check(Shadow->bShouldRenderVSM);
VirtualShadowMapArray.AddRenderViews(
Shadow,
Views,
ShadowsLODScaleFactor,
bUseHzbOcclusion,
bUseHzbOcclusion,
VirtualShadowViews);
}
});
}
void FShadowSceneRenderer::CreateNaniteRenderPasses(
FRDGBuilder& GraphBuilder,
TConstArrayView<FViewInfo> Views,
TConstArrayView<FProjectedShadowInfo*> Shadows)
{
// NOTE: We need to assume the worst case in terms of max mip views because of the way we pack the array
// In practice almost all view sets will have the max # of mips unless there are no local lights anyways
static constexpr uint32 MaxViews = NANITE_MAX_VIEWS_PER_CULL_RASTERIZE_PASS;
static constexpr uint32 MaxPrimaryViews = MaxViews / FVirtualShadowMap::MaxMipLevels;
// Don't want to run this more than once in a given frame.
check(NaniteRenderPasses.IsEmpty());
FSceneCullingRenderer* SceneCullingRenderer = SceneRenderer.GetSceneExtensionsRenderers().GetRendererPtr<FSceneCullingRenderer>();
if (ensure(SceneCullingRenderer))
{
const bool bAllowMultipass = CVarVirtualShadowMapNaniteAllowMultipassViews.GetValueOnRenderThread() != 0;
FNaniteVirtualShadowMapRenderPass RenderPass;
auto Flush = [this](FNaniteVirtualShadowMapRenderPass& NaniteRenderPass)
{
if (NaniteRenderPass.MaxCullingViews > 0)
{
// Flush any previous render pass
check(NaniteRenderPass.TotalPrimaryViews > 0);
check(NaniteRenderPass.SceneInstanceCullingQuery);
NaniteRenderPasses.Add(NaniteRenderPass);
NaniteRenderPass = FNaniteVirtualShadowMapRenderPass();
}
};
for (FProjectedShadowInfo* ProjectedShadowInfo : Shadows)
{
// First person VSM clipmaps currently do not support Nanite primitives because Nanite does not yet support filtering for such primitives
// so that they'll end up in the correct clipmap. Without this check, all Nanite primitives will end being rendered into the first person clipmap,
// when we actually only want primitives tagged as FirstPersonWorldSpaceRepresentation to be rendered into it.
if (ProjectedShadowInfo->VirtualShadowMapClipmap && ProjectedShadowInfo->VirtualShadowMapClipmap->IsFirstPersonShadow())
{
continue;
}
if (ProjectedShadowInfo->bShouldRenderVSM)
{
FVSMRenderViewInfo Info = GetRenderViewInfo(ProjectedShadowInfo);
// Space for the new views in the current pass?
if (bAllowMultipass && (RenderPass.MaxCullingViews + Info.MaxCullingViews) > MaxViews)
{
Flush(RenderPass);
}
RenderPass.Shadows.Add(ProjectedShadowInfo);
// Add a shadow thingo to be culled, need to know the primary view ranges.
if (!RenderPass.SceneInstanceCullingQuery)
{
RenderPass.SceneInstanceCullingQuery = SceneCullingRenderer->CreateInstanceQuery(GraphBuilder);
}
RenderPass.SceneInstanceCullingQuery->Add(RenderPass.TotalPrimaryViews, Info.NumPrimaryViews, Info.MaxCullingViews, Info.CullingVolume);
RenderPass.MaxCullingViews += Info.MaxCullingViews;
RenderPass.TotalPrimaryViews += Info.NumPrimaryViews;
}
}
Flush(RenderPass);
}
const float ShadowsLODScaleFactor = ComputeNaniteShadowsLODScaleFactor();
for (FNaniteVirtualShadowMapRenderPass& RenderPass : NaniteRenderPasses)
{
CreateNaniteViewsForPass(GraphBuilder, VirtualShadowMapArray, Views, ShadowsLODScaleFactor, RenderPass);
}
}