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

608 lines
29 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
VirtualShadowMapClipmap.cpp
=============================================================================*/
#include "VirtualShadowMapClipmap.h"
#include "CoreMinimal.h"
#include "HAL/IConsoleManager.h"
#include "LightSceneInfo.h"
#include "LightSceneProxy.h"
#include "RendererModule.h"
#include "ScenePrivate.h"
#include "SceneRendering.h"
#include "VirtualShadowMapArray.h"
#include "VirtualShadowMapCacheManager.h"
#include "VirtualShadowMapDefinitions.h"
#include "CollisionQueryParams.h"
#include "Engine/HitResult.h"
#include "Engine/World.h"
static TAutoConsoleVariable<int32> CVarForceInvalidateDirectionalVSM(
TEXT("r.Shadow.Virtual.Cache.ForceInvalidateDirectional"),
0,
TEXT("Forces the clipmap to always invalidate, useful to emulate a moving sun to avoid misrepresenting cache performance."),
ECVF_RenderThreadSafe);
static TAutoConsoleVariable<float> CVarVirtualShadowMapResolutionLodBiasDirectional(
TEXT( "r.Shadow.Virtual.ResolutionLodBiasDirectional" ),
-0.5f,
TEXT( "Bias applied to LOD calculations for directional lights. -1.0 doubles resolution, 1.0 halves it and so on." ),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<float> CVarVirtualShadowMapResolutionLodBiasDirectionalMoving(
TEXT( "r.Shadow.Virtual.ResolutionLodBiasDirectionalMoving" ),
0.5f,
TEXT( "Bias applied to LOD calculations for directional lights that are moving. -1.0 doubles resolution, 1.0 halves it and so on.\n" )
TEXT( "The bias transitions smoothly back to ResolutionLodBiasDirectional as the light transitions to non-moving, see 'r.Shadow.Scene.LightActiveFrameCount'." ),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarVirtualShadowMapClipmapFirstLevel(
TEXT( "r.Shadow.Virtual.Clipmap.FirstLevel" ),
6,
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> CVarVirtualShadowMapClipmapLastLevel(
TEXT( "r.Shadow.Virtual.Clipmap.LastLevel" ),
22,
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
);
TAutoConsoleVariable<int32> CVarVirtualShadowMapClipmapFirstCoarseLevel(
TEXT("r.Shadow.Virtual.Clipmap.FirstCoarseLevel"),
15,
TEXT("First level of the clipmap to mark coarse pages for. Lower values allow higher resolution coarse pages near the camera but increase total page counts."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
TAutoConsoleVariable<int32> CVarVirtualShadowMapClipmapLastCoarseLevel(
TEXT("r.Shadow.Virtual.Clipmap.LastCoarseLevel"),
18,
TEXT("Last level of the clipmap to mark coarse pages for. Higher values provide dense clipmap data for a longer radius but increase total page counts."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
TAutoConsoleVariable<float> CVarVirtualShadowMapClipmapZRangeScale(
TEXT("r.Shadow.Virtual.Clipmap.ZRangeScale"),
1000.0f,
TEXT("Scale of the clipmap level depth range relative to the radius. Affects z-near/z-far of the shadow map. Should generally be at least 10 or it will result in excessive cache invalidations. Values that are too large cause depth imprecisions and shadow flickering."),
ECVF_RenderThreadSafe
);
TAutoConsoleVariable<int32> CVarVirtualShadowMapClipmapMinCameraViewportWidth(
TEXT("r.Shadow.Virtual.Clipmap.MinCameraViewportWidth"),
0,
TEXT("If greater than zero, clamps the camera viewport dimensions used to adjust the clipmap resolution.\n")
TEXT("This can be useful to avoid dynamic resolution indirectly dropping the shadow resolution far too low."),
ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarClipmapWPODisableDistance(
TEXT("r.Shadow.Virtual.Clipmap.WPODisableDistance"),
1,
TEXT("When enabled, disables WPO animation in clipmap levels based on a primitive's WPO disable distance and r.Shadow.Virtual.Clipmap.WPODisableDistance.LodBias setting."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarClipmapWPODisableDistanceLodBias(
TEXT("r.Shadow.Virtual.Clipmap.WPODisableDistance.LodBias"),
3,
TEXT("The number of clipmap levels further than the distance that an instance would be animated to allow shadow animation.\n")
TEXT("Typically 2-4 works well but may need to be adjusted for very low light angles with significant WPO movement."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<bool> CVarOrthoVSMEstimateClipmapLevels(
TEXT("r.Ortho.VSM.EstimateClipmapLevels"),
true,
TEXT("Enable/Disable calculating the FirstLevel VSM based on the current camera OrthoWidth"),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<int32> CVarOrthoVSMClipmapLODBias(
TEXT("r.Ortho.VSM.ClipmapLODBias"),
0,
TEXT("LOD setting for adjusting the VSM first level from it's OrthoWidth based value."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<bool> CVarOrthoVSMProjectViewOrigin(
TEXT("r.Ortho.VSM.ProjectViewOrigin"),
true,
TEXT("Enable/Disable moving the WorldOrigin of the VSM clipmaps to focus around the ViewTarget (if present)"),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<bool> CVarOrthoVSMRayCastViewOrigin(
TEXT("r.Ortho.VSM.RayCastViewOrigin"),
true,
TEXT("Enable/Disable whether the ViewOrigin should be estimated with a raycast if the ViewTarget is not present (i.e. standalone camera)"),
ECVF_Scalability | ECVF_RenderThreadSafe
);
TAutoConsoleVariable<int32> CVarMarkCoarsePagesDirectional(
TEXT("r.Shadow.Virtual.MarkCoarsePagesDirectional"),
1,
TEXT("Marks coarse pages in directional light virtual shadow maps so that low resolution data is available everywhere.")
TEXT("Ability to disable is primarily for profiling and debugging."),
ECVF_Scalability | ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<bool> CVarVSMClipmapCullDynamicTightly(
TEXT("r.Shadow.Virtual.Clipmap.CullDynamicTightly"),
true,
TEXT("When enabled(default) the far culling plane for uncached clipmpap levels is set to the size of the clipmap level.\n")
TEXT(" Currently, this is only used when r.Shadow.Virtual.Cache.ForceInvalidateDirectional is enabled, as cached levels need a larger range."),
ECVF_RenderThreadSafe
);
static TAutoConsoleVariable<bool> CVarVSMUseReceiverMaskDirectional(
TEXT("r.Shadow.Virtual.UseReceiverMaskDirectional"),
false,
TEXT("Use receiver page masks with directional lights. This enables much more effective culling especially at lower resolutions."),
ECVF_RenderThreadSafe
);
bool IsVirtualShadowMapDirectionalReceiverMaskEnabled()
{
return CVarVSMUseReceiverMaskDirectional.GetValueOnRenderThread() != 0;
}
// "Virtual" clipmap level to clipmap radius
// NOTE: This is the radius of around the clipmap origin that this level must cover
// The actual clipmap dimensions will be larger due to snapping and other accomodations
float FVirtualShadowMapClipmap::GetLevelRadius(float AbsoluteLevel)
{
// NOTE: Virtual clipmap indices can be negative (although not commonly)
// Clipmap level rounds *down*, so radius needs to cover out to 2^(Level+1), where it flips
return FMath::Pow(2.0f, AbsoluteLevel + 1.0f);
}
FVirtualShadowMapClipmapConfig FVirtualShadowMapClipmapConfig::GetGlobal()
{
bool bMarkCoarsePagesDirectional = CVarMarkCoarsePagesDirectional.GetValueOnRenderThread() != 0;
return FVirtualShadowMapClipmapConfig
{
.FirstLevel = CVarVirtualShadowMapClipmapFirstLevel.GetValueOnRenderThread(),
.LastLevel = CVarVirtualShadowMapClipmapLastLevel.GetValueOnRenderThread(),
.FirstCoarseLevel = bMarkCoarsePagesDirectional ? CVarVirtualShadowMapClipmapFirstCoarseLevel.GetValueOnRenderThread() : -1,
.LastCoarseLevel = bMarkCoarsePagesDirectional ? CVarVirtualShadowMapClipmapLastCoarseLevel.GetValueOnRenderThread() : -1,
.ResolutionLodBias = CVarVirtualShadowMapResolutionLodBiasDirectional.GetValueOnRenderThread(),
.ResolutionLodBiasMoving = CVarVirtualShadowMapResolutionLodBiasDirectionalMoving.GetValueOnRenderThread(),
.bForceInvalidate = CVarForceInvalidateDirectionalVSM.GetValueOnRenderThread() != 0,
.bCullDynamicTightly = CVarVSMClipmapCullDynamicTightly.GetValueOnRenderThread(),
.bUseReceiverMask = IsVirtualShadowMapDirectionalReceiverMaskEnabled()
};
}
FVirtualShadowMapClipmap::FVirtualShadowMapClipmap(
FVirtualShadowMapArray& VirtualShadowMapArray,
const FLightSceneInfo& InLightSceneInfo,
const FViewMatrices& CameraViewMatrices,
FIntPoint CameraViewRectSize,
const FViewInfo* InDependentView,
float LightMobilityFactor,
const FVirtualShadowMapClipmapConfig& InConfig)
: Config(InConfig),
LightSceneInfo(InLightSceneInfo),
DependentView(InDependentView)
{
FVirtualShadowMapArrayCacheManager* VirtualShadowMapArrayCacheManager = VirtualShadowMapArray.CacheManager;
LightDirection = LightSceneInfo.Proxy->GetDirection().GetSafeNormal();
const FMatrix WorldToLightRotationMatrix = FInverseRotationMatrix(LightDirection.Rotation());
const FMatrix FaceMatrix(
FPlane( 0, 0, 1, 0 ),
FPlane( 0, 1, 0, 0 ),
FPlane(-1, 0, 0, 0 ),
FPlane( 0, 0, 0, 1 ));
WorldToLightViewRotationMatrix = WorldToLightRotationMatrix * FaceMatrix;
// Pure rotation matrix
FMatrix ViewToWorldRotationMatrix = WorldToLightViewRotationMatrix.GetTransposed();
// Optionally clamp camera viewport to avoid excessively low resolution shadows with dynamic resolution
const int32 MinCameraViewportWidth = CVarVirtualShadowMapClipmapMinCameraViewportWidth.GetValueOnRenderThread();
bool bIsOrthographicCamera = !CameraViewMatrices.IsPerspectiveProjection();
int32 CameraViewportWidth = CameraViewRectSize.X;
if (bIsOrthographicCamera)
{
/**
* Orthographic cameras have uniform depth, so basing the LodScale on the width alone can cause issues when selecting the clipmap area to resolve
* at larger scale views. Instead we use the OrthoWidth. This gives a larger area for the shadows to be drawn to and ensures shadows further away/in
* the corners of the view rect have the correct LOD resolution. We default to the viewport as a minimum.
*/
CameraViewportWidth = FMath::Max(FMath::CeilToInt(CameraViewMatrices.GetInvProjectionMatrix().M[0][0] * 2.0f), CameraViewportWidth);
}
CameraViewportWidth = FMath::Max(MinCameraViewportWidth, CameraViewportWidth);
// NOTE: Rotational (roll) invariance of the directional light depends on square pixels so we just base everything on the camera X scales/resolution
// NOTE: 0.5 because we double the size of the clipmap region below to handle snapping
float LodScale = 0.5f / CameraViewMatrices.GetProjectionScale().X;
LodScale *= float(FVirtualShadowMap::VirtualMaxResolutionXY) / float(CameraViewportWidth);
// For now we adjust resolution by just biasing the page we look up in. This is wasteful in terms of page table vs.
// just resizing the virtual shadow maps for each clipmap, but convenient for now. This means we need to additionally bias
// which levels are present.
ResolutionLodBias = FVirtualShadowMapArray::InterpolateResolutionBias(Config.ResolutionLodBias, Config.ResolutionLodBiasMoving, LightMobilityFactor) + FMath::Log2(LodScale);
ResolutionLodBias += GetLightSceneInfo().Proxy->GetVSMResolutionLodBias();
// Clamp negative absolute resolution biases as they would exceed the maximum resolution/ranges allocated
ResolutionLodBias = FMath::Max(0.0f, ResolutionLodBias);
WorldOrigin = CameraViewMatrices.GetViewOrigin();
CameraToViewTarget = FVector::ZeroVector;
if (bIsOrthographicCamera && CVarOrthoVSMProjectViewOrigin.GetValueOnRenderThread())
{
/**
* If enabled, use the ViewTarget location as the WorldOrigin location, this helps with scaling VSMs in Ortho
* as the clipmaps emanate more evenly from the focus of the view.
* A ViewTarget is not always necessarily present, but there isn't really an alternative way to estimate the best
* WorldOrigin for this effect to work well right now without the ViewTarget set.
*/
CameraToViewTarget = CameraViewMatrices.GetCameraToViewTarget();
if (CameraToViewTarget.Length() == 0.0f && CVarOrthoVSMRayCastViewOrigin.GetValueOnRenderThread()
&& DependentView && DependentView->Family && DependentView->Family->Scene)
{
if (UWorld* World = DependentView->Family->Scene->GetWorld())
{
FVector ViewForward = CameraViewMatrices.GetViewMatrix().GetColumn(2);
FCollisionObjectQueryParams ObjectParams = FCollisionObjectQueryParams(FCollisionObjectQueryParams::InitType::AllObjects);
FCollisionQueryParams CollisionParams(FName(TEXT("OrthoCamera_VSMTrace")), true);
if (DependentView->ViewActor.IsSet())
{
CollisionParams.AddIgnoredSourceObject(DependentView->ViewActor.ActorUniqueId);
}
FHitResult Hit(ForceInit);
if (World->LineTraceSingleByObjectType(
Hit, //result
WorldOrigin, //start
WorldOrigin + ViewForward * FMath::Abs(CameraViewMatrices.GetInvProjectionMatrix().M[2][2]), //end
ObjectParams,//collision channel
CollisionParams
))
{
CameraToViewTarget = ViewForward * Hit.Distance;
}
}
}
WorldOrigin += CameraToViewTarget;
}
FirstLevel = Config.FirstLevel;
int32 LastLevel = Config.LastLevel;
if (bIsOrthographicCamera && CVarOrthoVSMEstimateClipmapLevels.GetValueOnRenderThread())
{
/**
* For Ortho projections, this branch bases the first level VSM on the set OrthoWidth. This reduces the number of clipmaps generated
* and also scales the precision of the clipmaps depending on the scene.
*
* To be on the safe side, we output -1 FirstLevel compared to what the full OrthoWidth would output. The InvProjectionMatrix outputs
* half the OrthoWidth in the [0][0] position, and as we are using Log2, we can just use that raw value, rather than multiplying it then
* subtracting a level.
*/
int32 OrthoFirstLevel = FMath::FloorToInt(FMath::Log2(static_cast<float>(CameraViewMatrices.GetInvProjectionMatrix().M[0][0])));
if (OrthoFirstLevel > FirstLevel)
{
//Only apply the ortho level if it above the desired minimum first level.
FirstLevel = OrthoFirstLevel;
}
//Allow manual correction using the Ortho only FirstLevel bias.
FirstLevel = FMath::Max(FirstLevel + CVarOrthoVSMClipmapLODBias.GetValueOnRenderThread(), 0);
}
LastLevel = FMath::Max(FirstLevel, LastLevel);
int32 LevelCount = LastLevel - FirstLevel + 1;
// Per-clipmap projection data
LevelData.Empty();
LevelData.AddDefaulted(LevelCount);
VirtualShadowMapId = VirtualShadowMapArray.Allocate(false, LevelCount);
// Enable various fast paths if we are not storing cache data
bool bUncached = Config.bForceInvalidate || !VirtualShadowMapArrayCacheManager->IsCacheEnabled();
// TODO: We need a light/cache entry for every light/VSM now, but scene captures may not have persistent view state for indexing
// This is likely to all change as we refactor the multiple cache managers stuff anyways; for now this is hopefully safe since they do have separate copies
const uint32 UniqueViewKey = InDependentView->ViewState ? InDependentView->ViewState->GetViewKey() : 0U;
PerLightCacheEntry = VirtualShadowMapArrayCacheManager->FindCreateLightCacheEntry(LightSceneInfo.Id, UniqueViewKey, LevelCount, Config.ShadowTypeId);
PerLightCacheEntry->UpdateClipmap(LightDirection, FirstLevel, bUncached, Config.bUseReceiverMask);
const int RadiusLn = GetLevelRadius(static_cast<float>(LastLevel));
const int RadiiPerLevel = 4;
FVector SnappedOriginLn = WorldToLightViewRotationMatrix.TransformPosition(WorldOrigin);
{
FInt64Point OriginSnapUnitsLn(
FMath::RoundToInt64(SnappedOriginLn.X / RadiusLn),
FMath::RoundToInt64(SnappedOriginLn.Y / RadiusLn));
SnappedOriginLn.X = OriginSnapUnitsLn.X * RadiusLn;
SnappedOriginLn.Y = OriginSnapUnitsLn.Y * RadiusLn;
}
// We expand the depth range of the clipmap level to allow for camera movement without having to invalidate cached shadow data
// (See VirtualShadowMapCacheManager::UpdateClipmap for invalidation logic.)
// This also better accomodates SMRT where we want to avoid stepping outside of the Z bounds of a given clipmap
// NOTE: It's tempting to use a single global Z range for the entire clipmap (which avoids some SMRT overhead too)
// but this can cause precision issues with cached pages very near the camera.
const double ViewRadiusZScale = CVarVirtualShadowMapClipmapZRangeScale.GetValueOnRenderThread();
const FVector ViewCenter = WorldToLightViewRotationMatrix.TransformPosition(WorldOrigin);
for (int32 Index = 0; Index < LevelCount; ++Index)
{
FLevelData& Level = LevelData[Index];
const int32 AbsoluteLevel = Index + FirstLevel; // Absolute (virtual) level index
const double RawLevelRadius = GetLevelRadius(static_cast<float>(AbsoluteLevel));
double HalfLevelDim = 2.0 * RawLevelRadius;
double SnapSize = RawLevelRadius;
FVector SnappedViewCenter = ViewCenter;
FVector2D CenterSnapUnits(
FMath::RoundToDouble(ViewCenter.X / SnapSize),
FMath::RoundToDouble(ViewCenter.Y / SnapSize));
SnappedViewCenter.X = CenterSnapUnits.X * SnapSize;
SnappedViewCenter.Y = CenterSnapUnits.Y * SnapSize;
FInt64Point CornerOffset;
CornerOffset.X = -(int64_t)CenterSnapUnits.X + (RadiiPerLevel/2);
CornerOffset.Y = (int64_t)CenterSnapUnits.Y + (RadiiPerLevel/2);
const FVector SnappedWorldCenter = ViewToWorldRotationMatrix.TransformPosition(SnappedViewCenter);
Level.WorldCenter = SnappedWorldCenter;
Level.CornerOffset = CornerOffset;
// A relative corner offset is used for LWC reasons.
// The reference point is WorldOrigin snapped to a grid of GetLevelRadius(LastLevel),
// because points on this grid are guaranteed to also be present on lower levels,
// therefore allowing the offsets to represented as factors of level radii without precision loss.
const FInt64Point SnappedPageOriginLi(-SnappedViewCenter.X, SnappedViewCenter.Y);
const FInt64Point SnappedPageOriginLn(-SnappedOriginLn.X, SnappedOriginLn.Y);
const FInt64Point RelativeCornerOffset = SnappedPageOriginLi - SnappedPageOriginLn + ((RadiiPerLevel / 2) * (int64_t)SnapSize);
Level.RelativeCornerOffset = FIntPoint(
static_cast<int32>(RelativeCornerOffset.X / SnapSize),
static_cast<int32>(RelativeCornerOffset.Y / SnapSize));
// Check if we have a cache entry for this level
// If we do and it covers our required depth range, we can use cached pages. Otherwise we need to invalidate.
FVirtualShadowMapCacheEntry* ClipmapLevelEntry = &PerLightCacheEntry->ShadowMapEntries[Index];
double ViewRadiusZ = RawLevelRadius * ViewRadiusZScale;
double ViewCenterDeltaZ = 0.0f;
// We snap to half the size of the VSM at each level
check((FVirtualShadowMap::Level0DimPagesXY & 1) == 0);
FInt64Point PageOffset(CornerOffset * (FVirtualShadowMap::Level0DimPagesXY >> 2));
// This is the "WPO distance disable" threshold at which we allow WPO animation into this clipmap
// See VirtualShadowMapIsWPOAllowed in VirtualShadowMappageCacheCommon.ush
// NOTE: We use the ResolutionLodBias here because it is desirable for the shadow WPO distance to not
// vary a ton at different scalability settings. In particular, we do not want it to get *closer* to the
// caster at higher quality settings, as it otherwise would.
// We cannot however easily incorportate the *global* GPU resolution bias as this
// decision needs to be constant, otherwise we'd have to track all these variables for invalidations.
// As it is, if these variables change we can do full cache invalidation as they are not expected to be
// changing on the fly in a game.
// We quantize the result to a powers of two (similar to the clipmaps) to avoid continuous invalidation in
// cases like window resizes and similar.
if (CVarClipmapWPODisableDistance.GetValueOnRenderThread() > 0)
{
const int32 WPODisableDistanceLodBias = CVarClipmapWPODisableDistanceLodBias.GetValueOnRenderThread();
double WPOThresholdCombinedLevel = FMath::CeilToDouble(static_cast<double>(AbsoluteLevel - WPODisableDistanceLodBias) - ResolutionLodBias);
// NOTE: Squared
Level.WPODistanceDisableThresholdSquared = FMath::Pow(2.0, 2.0 * WPOThresholdCombinedLevel);
}
else
{
Level.WPODistanceDisableThresholdSquared = 0.0;
}
ClipmapLevelEntry->UpdateClipmapLevel(
VirtualShadowMapArray,
*PerLightCacheEntry,
GetVirtualShadowMapId(Index),
PageOffset,
RawLevelRadius,
ViewCenter.Z,
ViewRadiusZ,
Level.WPODistanceDisableThresholdSquared);
// Update min/max Z based on the cached page (if present and valid)
// We need to ensure we use a consistent depth range as the camera moves for each level
ViewCenterDeltaZ = ViewCenter.Z - ClipmapLevelEntry->Clipmap.ViewCenterZ;
ViewRadiusZ = ClipmapLevelEntry->Clipmap.ViewRadiusZ;
Level.DynamicDepthCullRange.X = 0.0f;
Level.DynamicDepthCullRange.Y = FLT_MAX;
// TODO: Maybe we remove this path if we end up swapping things to uncached dynamically
// This path changes the depth range and could have visual effects in some edge cases
if (Config.bForceInvalidate && Config.bCullDynamicTightly)
{
// Far plane in the clipmap should be just the end of the visible range
float CullingEndDistance = GetLevelRadius(static_cast<float>(AbsoluteLevel) - ResolutionLodBias);
const double ViewRangeZ = CullingEndDistance + ViewRadiusZ;
const double ZScale = 1.0 / ViewRangeZ;
const double ZOffset = ViewRadiusZ;
Level.ViewToClip = FReversedZOrthoMatrix(HalfLevelDim, HalfLevelDim, ZScale, ZOffset);
}
else
{
// NOTE: These values are all in regular ranges after being offset
const double ZScale = 0.5 / ViewRadiusZ;
const double ZOffset = ViewRadiusZ + ViewCenterDeltaZ;
Level.ViewToClip = FReversedZOrthoMatrix(HalfLevelDim, HalfLevelDim, ZScale, ZOffset);
// With receiver mask enabled, dynamic is uncached and thus can be culled tightly
if (Config.bUseReceiverMask && Config.bCullDynamicTightly)
{
// We subtract the LodBias here because we know that we'll only ever render objects to a fraction of the
// clipmap range based on the biasing.
float CullingEndDistance = GetLevelRadius(static_cast<float>(AbsoluteLevel) - ResolutionLodBias);
Level.DynamicDepthCullRange.X = Level.ViewToClip.TransformPosition(FVector(0.0, 0.0, CullingEndDistance)).Z;
}
}
// Update the entry with the new projection data
ClipmapLevelEntry->ProjectionData = ComputeProjectionShaderData(Index);
}
ComputeBoundingVolumes(WorldOrigin);
}
const FVirtualShadowMapProjectionShaderData& FVirtualShadowMapClipmap::GetProjectionShaderData(int32 ClipmapIndex) const
{
return PerLightCacheEntry->ShadowMapEntries[ClipmapIndex].ProjectionData;
}
void FVirtualShadowMapClipmap::ComputeBoundingVolumes(const FVector ViewOrigin)
{
// We don't really do much CPU culling with clipmaps. After various testing the fact that we are culling
// a single frustum that goes out and basically the entire map, and we have to extrude towards (and away!) from
// the light, and dilate to cover full pages at every clipmap level (to avoid culling something that will go
// into a page that then gets cached with incomplete geometry), in many situations there is effectively no
// culling that happens. For instance, as soon as the camera looks vaguely towards or away from the light direction,
// the extruded frustum effectively covers the whole world.
// Thus we don't spend a lot of time trying to optimize for the easy cases and instead just pick an extremely
// conservative frustum.
ViewFrustumBounds = FConvexVolume();
BoundingSphere = FSphere(ViewOrigin, GetMaxRadius());
}
float FVirtualShadowMapClipmap::GetMaxRadius() const
{
return GetLevelRadius(static_cast<float>(GetClipmapLevel(GetLevelCount() - 1)));
}
FViewMatrices FVirtualShadowMapClipmap::GetViewMatrices(int32 ClipmapIndex) const
{
check(ClipmapIndex >= 0 && ClipmapIndex < LevelData.Num());
const FLevelData& Level = LevelData[ClipmapIndex];
FViewMatrices::FMinimalInitializer Initializer;
// NOTE: Be careful here! There's special logic in FViewMatrices around ViewOrigin for ortho projections we need to bypass...
// There's also the fact that some of this data is going to be "wrong", due to the "overridden" matrix thing that shadows do
Initializer.ViewOrigin = Level.WorldCenter;
Initializer.ViewRotationMatrix = WorldToLightViewRotationMatrix;
Initializer.ProjectionMatrix = Level.ViewToClip;
// TODO: This is probably unused in the shadows/nanite path, but coupling here is not ideal
Initializer.ConstrainedViewRect = FIntRect(0, 0, FVirtualShadowMap::VirtualMaxResolutionXY, FVirtualShadowMap::VirtualMaxResolutionXY);
return FViewMatrices(Initializer);
}
int32 PackClipmapLevelAndCount(int32 ClipmapLevel, int32 ClipmapLevelCountRemaining)
{
check(ClipmapLevel + VSM_PACKED_CLIP_LEVEL_BIAS >= 0);
check(ClipmapLevelCountRemaining >= 0);
return ((ClipmapLevel + VSM_PACKED_CLIP_LEVEL_BIAS) << 16u) | ClipmapLevelCountRemaining;
}
FVirtualShadowMapProjectionShaderData FVirtualShadowMapClipmap::ComputeProjectionShaderData(int32 ClipmapIndex) const
{
check(ClipmapIndex >= 0 && ClipmapIndex < LevelData.Num());
const FLevelData& Level = LevelData[ClipmapIndex];
const FVector PreViewTranslation = GetPreViewTranslation(ClipmapIndex);
const FDFVector3 PreViewTranslationDF(PreViewTranslation);
// WorldOrigin should be near the Level.WorldCenter, so we can make it relative
// NOTE: We need to negate so that it's not opposite though
const FVector3f NegativeClipmapWorldOriginOffset(-(WorldOrigin + PreViewTranslation));
// NOTE: Some shader logic (projection, etc) assumes some of these parameters are constant across all levels in a clipmap
FVirtualShadowMapProjectionShaderData Data;
Data.LightDirection = FVector3f(-LightDirection); // Negative to be consistent with FLightShaderParameters/GetDeferredLightParameters
Data.ShadowViewToClipMatrix = FMatrix44f(Level.ViewToClip);
Data.TranslatedWorldToShadowUVMatrix = FMatrix44f(CalcTranslatedWorldToShadowUVMatrix(WorldToLightViewRotationMatrix, Level.ViewToClip));
Data.TranslatedWorldToShadowUVNormalMatrix = FMatrix44f(CalcTranslatedWorldToShadowUVNormalMatrix(WorldToLightViewRotationMatrix, Level.ViewToClip));
Data.PreViewTranslationHigh = PreViewTranslationDF.High;
Data.PreViewTranslationLow = PreViewTranslationDF.Low;
Data.LightType = ELightComponentType::LightType_Directional;
Data.NegativeClipmapWorldOriginLWCOffset = NegativeClipmapWorldOriginOffset;
int32 ClipmapLevel = FirstLevel + ClipmapIndex;
int32 ClipmapLevelCountRemaining = LevelData.Num() - ClipmapIndex;
Data.ClipmapLevel_ClipmapLevelCountRemaining = PackClipmapLevelAndCount(FirstLevel + ClipmapIndex, ClipmapLevelCountRemaining);
Data.ResolutionLodBias = ResolutionLodBias;
Data.ClipmapCornerRelativeOffset = Level.RelativeCornerOffset;
Data.ClipmapLevelWPODistanceDisableThresholdSquared = static_cast<float>(Level.WPODistanceDisableThresholdSquared);
Data.LightSourceRadius = GetLightSceneInfo().Proxy->GetSourceRadius();
Data.Flags = PerLightCacheEntry->IsUncached() ? VSM_PROJ_FLAG_UNCACHED : 0U;
Data.PackedCullingViewId = FVirtualShadowMapProjectionShaderData::PackCullingViewId(DependentView->SceneRendererPrimaryViewId, DependentView->PersistentViewId);
if (Config.FirstCoarseLevel >= 0)
{
if ((ClipmapLevel >= Config.FirstCoarseLevel && ClipmapLevel <= Config.LastCoarseLevel)
// Always mark coarse pages in the last level for clouds/skyatmosphere
|| ClipmapLevelCountRemaining == 1)
{
Data.Flags |= VSM_PROJ_FLAG_IS_COARSE_CLIP_LEVEL;
}
}
if (Config.bIsFirstPersonShadow)
{
Data.Flags |= VSM_PROJ_FLAG_IS_FIRST_PERSON_SHADOW;
}
if (Config.bUseReceiverMask)
{
Data.Flags |= VSM_PROJ_FLAG_USE_RECEIVER_MASK;
}
Data.TexelDitherScale = GetLightSceneInfo().Proxy->GetVSMTexelDitherScale();
return Data;
}
static inline void LazyInitAndSetBitArray(TBitArray<>& BitArray, int32 Index, bool Value, int32 MaxNum)
{
if (BitArray.IsEmpty())
{
BitArray.Init(false, MaxNum);
}
BitArray[Index] = Value;
}
void FVirtualShadowMapClipmap::OnPrimitiveRendered(const FPrimitiveSceneInfo* PrimitiveSceneInfo)
{
if (PerLightCacheEntry.IsValid())
{
FPersistentPrimitiveIndex PersistentPrimitiveId = PrimitiveSceneInfo->GetPersistentIndex();
const int32 RenderedPrimitivesMaxNum = PerLightCacheEntry->RenderedPrimitives.Num();
check(PersistentPrimitiveId.IsValid());
check(PersistentPrimitiveId.Index < RenderedPrimitivesMaxNum);
// Check previous-frame state to detect transition from hidden->visible
const bool bPrimitiveRevealed = !PerLightCacheEntry->RenderedPrimitives[PersistentPrimitiveId.Index];
// update current frame-state.
LazyInitAndSetBitArray(RenderedPrimitives, PersistentPrimitiveId.Index, true, RenderedPrimitivesMaxNum);
// update cached state (this is checked & cleared whenever a primitive is invalidating the VSM).
PerLightCacheEntry->OnPrimitiveRendered(PrimitiveSceneInfo, bPrimitiveRevealed);
}
}
void FVirtualShadowMapClipmap::UpdateCachedFrameData()
{
if (PerLightCacheEntry.IsValid() && !RenderedPrimitives.IsEmpty())
{
PerLightCacheEntry->RenderedPrimitives = MoveTemp(RenderedPrimitives);
}
}