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

620 lines
29 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
ShadowSetupMobile.cpp: Shadow setup implementation for mobile specific features.
=============================================================================*/
#include "CoreMinimal.h"
#include "Stats/Stats.h"
#include "HAL/IConsoleManager.h"
#include "Engine/World.h"
#include "EngineDefines.h"
#include "ConvexVolume.h"
#include "RendererInterface.h"
#include "Math/GenericOctree.h"
#include "LightSceneInfo.h"
#include "SceneRendering.h"
#include "DynamicPrimitiveDrawing.h"
#include "ScenePrivate.h"
#include "MeshPassProcessor.h"
#include "UObject/UObjectIterator.h"
#include "RenderCore.h"
#include "ShadowRendering.h"
#include "StaticMeshBatch.h"
static TAutoConsoleVariable<int32> CVarCsmShaderCullingDebugGfx(
TEXT("r.Mobile.Shadow.CSMShaderCullingDebugGfx"),
0,
TEXT(""),
ECVF_RenderThreadSafe);
const uint32 CSMShaderCullingMethodDefault = 1;
static TAutoConsoleVariable<int32> CVarsCsmShaderCullingMethod(
TEXT("r.Mobile.Shadow.CSMShaderCullingMethod"),
CSMShaderCullingMethodDefault,
TEXT("Method to determine which primitives will receive CSM shaders:\n")
TEXT("0 - disabled (all primitives will receive CSM)\n")
TEXT("1 - Light frustum, all primitives whose bounding box is within CSM receiving distance. (default)\n")
TEXT("2 - Combined caster bounds, all primitives whose bounds are within CSM receiving distance and the capsule of the combined bounds of all casters.\n")
TEXT("3 - Light frustum + caster bounds, all primitives whose bounds are within CSM receiving distance and capsule of at least one caster. (slowest)\n")
TEXT("4 - Cull all. Prevent primitives from receiving CSM shadows.\n")
TEXT("5 - Disable culling if mobile distance field shadowing is used for all views.\n")
TEXT("Combine with 16 to change primitive bounding test to spheres instead of box. (i.e. 18 == combined casters + sphere test)")
,ECVF_RenderThreadSafe);
static void OnCsmShaderCullingMethodChanged()
{
// Cannot do this in editors because feature levels can change
#if !WITH_EDITOR
static int32 PrevValue = CSMShaderCullingMethodDefault;
const int32 CurValue = (CVarsCsmShaderCullingMethod.GetValueOnGameThread() & 0xF);
if (CurValue != PrevValue && (CurValue == 5 || PrevValue == 5))
{
PrevValue = CurValue;
if (CurValue == 5)
{
TArray<ERHIFeatureLevel::Type> UsedFeatureLevels;
for (TObjectIterator<UWorld> It; It; ++It)
{
const UWorld* World = *It;
if (World && World->Scene)
{
UsedFeatureLevels.AddUnique(World->Scene->GetFeatureLevel());
}
}
bool bBasePassAlwaysUseCSM = UsedFeatureLevels.Num() > 0;
for (ERHIFeatureLevel::Type FeatureLevel : UsedFeatureLevels)
{
bBasePassAlwaysUseCSM = bBasePassAlwaysUseCSM && MobileBasePassAlwaysUsesCSM(GetFeatureLevelShaderPlatform(FeatureLevel));
}
const EMeshPassFlags NewFlags = bBasePassAlwaysUseCSM ? EMeshPassFlags::MainView : (EMeshPassFlags::CachedMeshCommands | EMeshPassFlags::MainView);
FPassProcessorManager::SetPassFlags(EShadingPath::Mobile, EMeshPass::MobileBasePassCSM, NewFlags);
}
else
{
FPassProcessorManager::SetPassFlags(EShadingPath::Mobile, EMeshPass::MobileBasePassCSM, EMeshPassFlags::CachedMeshCommands | EMeshPassFlags::MainView);
}
}
#endif
}
static FAutoConsoleVariableSink CVarsCsmShaderCullingMethodSink(FConsoleCommandDelegate::CreateStatic(&OnCsmShaderCullingMethodChanged));
static bool CouldStaticMeshEverReceiveCSMFromStationaryLight(ERHIFeatureLevel::Type FeatureLevel, const FPrimitiveSceneInfo* PrimitiveSceneInfo, const FStaticMeshBatch& StaticMesh)
{
// test if static shadows are allowed in the first place:
static auto* CVarMobileAllowDistanceFieldShadows = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.Mobile.AllowDistanceFieldShadows"));
const bool bMobileAllowDistanceFieldShadows = CVarMobileAllowDistanceFieldShadows->GetValueOnRenderThread() == 1;
bool bHasCSMApplicableLightInteraction = bMobileAllowDistanceFieldShadows && StaticMesh.LCI && StaticMesh.LCI->GetLightMapInteraction(FeatureLevel).GetType() == LMIT_Texture;
bool bHasCSMApplicableShadowInteraction = bHasCSMApplicableLightInteraction && StaticMesh.LCI && StaticMesh.LCI->GetShadowMapInteraction(FeatureLevel).GetType() == SMIT_Texture;
return (bHasCSMApplicableLightInteraction && bHasCSMApplicableShadowInteraction) ||
(!bHasCSMApplicableLightInteraction && PrimitiveSceneInfo->Proxy->IsMovable());
}
static bool EnableStaticMeshCSMVisibilityState(bool bMovableLight, const FPrimitiveSceneInfo* PrimitiveSceneInfo, FMobileCSMVisibilityInfo& MobileCSMVisibilityInfo, FViewInfo& View)
{
bool bFoundReceiver = false;
if (MobileCSMVisibilityInfo.MobilePrimitiveCSMReceiverVisibilityMap[PrimitiveSceneInfo->GetIndex()])
{
return bFoundReceiver;
}
MobileCSMVisibilityInfo.MobilePrimitiveCSMReceiverVisibilityMap[PrimitiveSceneInfo->GetIndex()] = true;
INC_DWORD_STAT_BY(STAT_CSMStaticPrimitiveReceivers, 1);
for (int32 MeshIndex = 0; MeshIndex < PrimitiveSceneInfo->StaticMeshes.Num(); MeshIndex++)
{
const FStaticMeshBatch& StaticMesh = PrimitiveSceneInfo->StaticMeshes[MeshIndex];
bool bHasCSMApplicableShadowInteraction = View.StaticMeshVisibilityMap[StaticMesh.Id] && StaticMesh.LCI;
bHasCSMApplicableShadowInteraction = bHasCSMApplicableShadowInteraction && StaticMesh.LCI->GetShadowMapInteraction(View.GetFeatureLevel()).GetType() == SMIT_Texture;
if (bMovableLight || CouldStaticMeshEverReceiveCSMFromStationaryLight(View.GetFeatureLevel(), PrimitiveSceneInfo, StaticMesh))
{
const FMaterialRenderProxy* MaterialRenderProxy = StaticMesh.MaterialRenderProxy;
const FMaterial& Material = MaterialRenderProxy->GetMaterialWithFallback(View.GetFeatureLevel(), MaterialRenderProxy);
if (Material.GetShadingModels().IsLit())
{
// CSM enabled list
MobileCSMVisibilityInfo.MobileCSMStaticMeshVisibilityMap[StaticMesh.Id] = MobileCSMVisibilityInfo.MobileNonCSMStaticMeshVisibilityMap[StaticMesh.Id];
// CSM excluded list
MobileCSMVisibilityInfo.MobileNonCSMStaticMeshVisibilityMap[StaticMesh.Id] = false;
INC_DWORD_STAT_BY(STAT_CSMStaticMeshReceivers, 1);
bFoundReceiver = true;
}
}
}
return
bFoundReceiver ||
// Dynamic primitives do not have static meshes
PrimitiveSceneInfo->StaticMeshes.Num() == 0;
}
template<typename TReceiverFunc>
static bool MobileDetermineStaticMeshesCSMVisibilityStateInner(
FScene* Scene,
FViewInfo& View,
const FPrimitiveSceneInfoCompact& PrimitiveSceneInfoCompact,
FProjectedShadowInfo* ProjectedShadowInfo,
TReceiverFunc IsReceiverFunc
)
{
FConvexVolume ViewVolume;
const FLightSceneInfo& LightSceneInfo = ProjectedShadowInfo->GetLightSceneInfo();
FLightSceneProxy* RESTRICT LightProxy = LightSceneInfo.Proxy;
FVector LightDir = LightProxy->GetDirection();
FPrimitiveSceneInfo* RESTRICT PrimitiveSceneInfo = PrimitiveSceneInfoCompact.PrimitiveSceneInfo;
FPrimitiveSceneProxy* RESTRICT PrimitiveProxy = PrimitiveSceneInfoCompact.Proxy;
const FBoxSphereBounds PrimitiveBounds(PrimitiveSceneInfoCompact.Bounds);
bool bFoundCSMReceiver = false;
if (PrimitiveProxy->WillEverBeLit() && PrimitiveProxy->ShouldReceiveMobileCSMShadows()
&& (PrimitiveProxy->GetLightingChannelMask() & LightProxy->GetLightingChannelMask()) != 0)
{
const FVector LightDirection = LightProxy->GetDirection();
const FVector PrimitiveToShadowCenter = ProjectedShadowInfo->ShadowBounds.Center - PrimitiveBounds.Origin;
// Project the primitive's bounds origin onto the light vector
const FVector::FReal ProjectedDistanceFromShadowOriginAlongLightDir = PrimitiveToShadowCenter | LightDirection;
// Calculate the primitive's squared distance to the cylinder's axis
const FVector::FReal PrimitiveDistanceFromCylinderAxisSq = (-LightDirection * ProjectedDistanceFromShadowOriginAlongLightDir + PrimitiveToShadowCenter).SizeSquared();
const FVector::FReal CombinedRadiusSq = FMath::Square(ProjectedShadowInfo->ShadowBounds.W + PrimitiveBounds.SphereRadius);
// Include all primitives for movable lights, but only statically shadowed primitives from a light with static shadowing,
// Since lights with static shadowing still create per-object shadows for primitives without static shadowing.
if ((!LightProxy->HasStaticLighting() || (!LightSceneInfo.IsPrecomputedLightingValid() || LightProxy->UseCSMForDynamicObjects()))
// Check if this primitive is in the shadow's cylinder
&& PrimitiveDistanceFromCylinderAxisSq < CombinedRadiusSq
// Check if the primitive is closer than the cylinder cap toward the light
// next line is commented as it breaks large world shadows, if this was meant to be an optimization we should think about a better solution
//// && ProjectedDistanceFromShadowOriginAlongLightDir - PrimitiveBounds.SphereRadius < -ProjectedShadowInfo->MinPreSubjectZ
// If the primitive is further along the cone axis than the shadow bounds origin,
// Check if the primitive is inside the spherical cap of the cascade's bounds
&& !(ProjectedDistanceFromShadowOriginAlongLightDir < 0
&& PrimitiveToShadowCenter.SizeSquared() > CombinedRadiusSq))
{
FVisibleLightViewInfo& VisibleLightViewInfo = View.VisibleLightInfos[LightSceneInfo.Id];
const FPrimitiveViewRelevance& Relevance = View.PrimitiveViewRelevanceMap[PrimitiveSceneInfo->GetIndex()];
const bool bLit = (Relevance.ShadingModelMask != (1 << MSM_Unlit));
bool bCanReceiveDynamicShadow =
bLit
&& (Relevance.bOpaque || Relevance.bMasked)
&& IsReceiverFunc(PrimitiveBounds.Origin, PrimitiveBounds.BoxExtent, PrimitiveBounds.SphereRadius);
if (bCanReceiveDynamicShadow)
{
bool bMovableLightUsingCSM = LightProxy->IsMovable() && LightSceneInfo.ShouldRenderViewIndependentWholeSceneShadows();
bFoundCSMReceiver = EnableStaticMeshCSMVisibilityState(bMovableLightUsingCSM, PrimitiveSceneInfo, View.MobileCSMVisibilityInfo, View);
}
}
}
return bFoundCSMReceiver;
}
template<typename TReceiverFunc>
static bool MobileDetermineStaticMeshesCSMVisibilityState(FScene* Scene, FViewInfo& View, FProjectedShadowInfo* WholeSceneShadow, TReceiverFunc IsReceiverFunc)
{
bool bFoundReceiver = false;
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_ShadowOctreeTraversal);
// Find primitives that are in a shadow frustum in the octree.
Scene->PrimitiveOctree.FindElementsWithPredicate([&IsReceiverFunc](FScenePrimitiveOctree::FNodeIndex /*ParentNodeIndex*/, FScenePrimitiveOctree::FNodeIndex /*NodeIndex*/, const FBoxCenterAndExtent& NodeBounds)
{
return IsReceiverFunc(FVector(NodeBounds.Center), FVector(NodeBounds.Extent), NodeBounds.Extent.Size3());
},
[&bFoundReceiver, Scene, &View, WholeSceneShadow, &IsReceiverFunc](FScenePrimitiveOctree::FNodeIndex /*ParentNodeIndex*/, const FPrimitiveSceneInfoCompact& Primitive)
{
// gather the shadows for this one primitive
bFoundReceiver = MobileDetermineStaticMeshesCSMVisibilityStateInner(Scene, View, Primitive, WholeSceneShadow, IsReceiverFunc) || bFoundReceiver;
});
}
return bFoundReceiver;
}
static void VisualizeMobileDynamicCSMSubjectCapsules(FViewInfo& View, FLightSceneInfo* LightSceneInfo, FProjectedShadowInfo* ProjectedShadowInfo)
{
auto DrawDebugCapsule = [](FViewInfo& InView, const FLightSceneInfo* InLightSceneInfo, const FVector& Start, float CastLength, float CapsuleRadius)
{
const FMatrix& LightToWorld = InLightSceneInfo->Proxy->GetLightToWorld();
FViewElementPDI ShadowFrustumPDI(&InView, nullptr, nullptr);
FVector Dir = LightToWorld.GetUnitAxis(EAxis::X);
FVector End = Start + (Dir*CastLength);
DrawWireSphere(&ShadowFrustumPDI, FTransform(Start), FColor::White, CapsuleRadius, 40, 0);
DrawWireCapsule(&ShadowFrustumPDI, Start + Dir*0.5f*CastLength, LightToWorld.GetUnitAxis(EAxis::Z), LightToWorld.GetUnitAxis(EAxis::Y), Dir,
FColor(231, 0, 0, 255), CapsuleRadius, 0.5f * CastLength + CapsuleRadius, 25, SDPG_World);
ShadowFrustumPDI.DrawLine(Start, End, FColor::Black, 0);
};
FVisibleLightViewInfo& VisibleLightViewInfo = View.VisibleLightInfos[LightSceneInfo->Id];
FMobileCSMSubjectPrimitives& MobileCSMSubjectPrimitives = VisibleLightViewInfo.MobileCSMSubjectPrimitives;
FVector LightDir = LightSceneInfo->Proxy->GetDirection();
const FVector::FReal ShadowCastLength = WORLD_MAX;
const uint32 CullingMethod = CVarsCsmShaderCullingMethod.GetValueOnRenderThread() & 0xF;
const bool bSphereTest = (CVarsCsmShaderCullingMethod.GetValueOnRenderThread() & 0x10) != 0;
switch (CullingMethod)
{
case 2:
{
// Combined bounds
FVector CombinedCasterStart;
FVector CombinedCasterEnd;
FBoxSphereBounds::Builder CombinedBoundsBuilder;
for (auto& Caster : MobileCSMSubjectPrimitives.GetShadowSubjectPrimitives())
{
CombinedBoundsBuilder += Caster->Proxy->GetBounds();
}
FBoxSphereBounds CombinedBounds(CombinedBoundsBuilder);
CombinedCasterStart = CombinedBounds.Origin;
CombinedCasterEnd = CombinedBounds.Origin + (LightDir * ShadowCastLength);
DrawDebugCapsule(View, LightSceneInfo, CombinedCasterStart, ShadowCastLength, CombinedBounds.SphereRadius);
break;
}
case 3:
{
// All casters.
for (auto& Caster : MobileCSMSubjectPrimitives.GetShadowSubjectPrimitives())
{
const FBoxSphereBounds& CasterBounds = Caster->Proxy->GetBounds();
const FVector& CasterStart = CasterBounds.Origin;
const FVector CasterEnd = CasterStart + (LightDir * ShadowCastLength);
DrawDebugCapsule(View, LightSceneInfo, CasterStart, ShadowCastLength, CasterBounds.SphereRadius);
}
break;
}
default:
{
if (CullingMethod >= 1 && CullingMethod <= 3)
{
// all culling modes draw the receiver frustum.
FViewElementPDI ShadowFrustumPDI(&View, nullptr, nullptr);
FMatrix Reciever(ProjectedShadowInfo->InvReceiverInnerMatrix);
DrawFrustumWireframe(&ShadowFrustumPDI, Reciever * FTranslationMatrix(-ProjectedShadowInfo->PreShadowTranslation), FColor::Cyan, 0);
}
}
break;
}
}
/** Finds the visible dynamic shadows for each view. */
FDynamicShadowsTaskData* FMobileSceneRenderer::InitDynamicShadows(FRDGBuilder& GraphBuilder, FInstanceCullingManager& InstanceCullingManager)
{
const bool bCombinedStaticAndCSMEnabled = FReadOnlyCVARCache::MobileEnableStaticAndCSMShadowReceivers();
const bool bMobileEnableMovableLightCSMShaderCulling = FReadOnlyCVARCache::MobileEnableMovableLightCSMShaderCulling();
// initialize CSMVisibilityInfo for each eligible light.
for (FLightSceneInfo* MobileDirectionalLightSceneInfo : Scene->MobileDirectionalLights)
{
const bool bShouldRecordShadowSubjectsForMobile = MobileDirectionalLightSceneInfo ? MobileDirectionalLightSceneInfo->ShouldRecordShadowSubjectsForMobile() : false;
if (bShouldRecordShadowSubjectsForMobile)
{
int32 PrimitiveCount = Scene->Primitives.Num();
for (auto& View : Views)
{
FMobileCSMSubjectPrimitives& MobileCSMSubjectPrimitives = View.VisibleLightInfos[MobileDirectionalLightSceneInfo->Id].MobileCSMSubjectPrimitives;
MobileCSMSubjectPrimitives.InitShadowSubjectPrimitives(PrimitiveCount);
}
}
}
FDynamicShadowsTaskData* TaskData = FSceneRenderer::InitDynamicShadows(GraphBuilder, InstanceCullingManager);
bool bAlwaysUseCSM = false;
const bool bSkipCSMShaderCulling = MobileBasePassAlwaysUsesCSM(Scene->GetShaderPlatform());
if (bSkipCSMShaderCulling)
{
bAlwaysUseCSM = true;
}
else
{
PrepareViewVisibilityLists();
for (FLightSceneInfo* MobileDirectionalLightSceneInfo : Scene->MobileDirectionalLights)
{
const FLightSceneProxy* LightSceneProxy = MobileDirectionalLightSceneInfo ? MobileDirectionalLightSceneInfo->Proxy : nullptr;
if (LightSceneProxy)
{
bool bLightHasCombinedStaticAndCSMEnabled = bCombinedStaticAndCSMEnabled && LightSceneProxy->UseCSMForDynamicObjects();
bool bMovableLightUsingCSM = bMobileEnableMovableLightCSMShaderCulling && LightSceneProxy->IsMovable() && MobileDirectionalLightSceneInfo->ShouldRenderViewIndependentWholeSceneShadows();
// non-csm culling movable light will force all draws to use CSM shaders.
// TODO: Cases in which a light channel uses a shadow casting non-csm culled movable light we only really need to use CSM on primitives that match the light channel.
bAlwaysUseCSM = bAlwaysUseCSM || (!bMobileEnableMovableLightCSMShaderCulling && LightSceneProxy->IsMovable() && MobileDirectionalLightSceneInfo->ShouldRenderViewIndependentWholeSceneShadows());
if (bLightHasCombinedStaticAndCSMEnabled || bMovableLightUsingCSM)
{
BuildCSMVisibilityState(MobileDirectionalLightSceneInfo);
}
}
}
}
for (auto& View : Views)
{
FMobileCSMVisibilityInfo& MobileCSMVisibilityInfo = View.MobileCSMVisibilityInfo;
MobileCSMVisibilityInfo.bAlwaysUseCSM = bAlwaysUseCSM;
if (bSkipCSMShaderCulling)
{
MobileCSMVisibilityInfo.bMobileDynamicCSMInUse = true;
}
}
{
// Check for modulated shadows.
bModulatedShadowsInUse = false;
for (auto LightIt = Scene->Lights.CreateConstIterator(); LightIt && bModulatedShadowsInUse == false; ++LightIt)
{
const FLightSceneInfoCompact& LightSceneInfoCompact = *LightIt;
FLightSceneInfo* LightSceneInfo = LightSceneInfoCompact.LightSceneInfo;
FVisibleLightInfo& VisibleLightInfo = VisibleLightInfos[LightSceneInfo->Id];
// Mobile renderer only projects modulated shadows.
bModulatedShadowsInUse = VisibleLightInfo.ShadowsToProject.Num() > 0;
}
}
return TaskData;
}
// generate a single FProjectedShadowInfo to encompass LightSceneInfo.
// Used to determine whether a mesh is within shadow range only.
bool BuildSingleCascadeShadowInfo(FViewInfo &View, TArray<FVisibleLightInfo, SceneRenderingAllocator>& VisibleLightInfos, FLightSceneInfo* LightSceneInfo, FProjectedShadowInfo& OUTSingleCascadeInfo)
{
bool bSuccess = false;
int32 ViewMaxCascades = View.MaxShadowCascades;
View.MaxShadowCascades = 1;
FWholeSceneProjectedShadowInitializer WholeSceneInitializer;
if (LightSceneInfo->Proxy->GetViewDependentWholeSceneProjectedShadowInitializer(View, 0, LightSceneInfo->IsPrecomputedLightingValid(), WholeSceneInitializer))
{
// Create the projected shadow info.
FVisibleLightInfo& VisibleLightInfo = VisibleLightInfos[LightSceneInfo->Id];
if (VisibleLightInfo.AllProjectedShadows.Num())
{
// Use a pre-existing cascade tile for resolution.
FIntPoint ShadowBufferResolution;
ShadowBufferResolution.X = VisibleLightInfo.AllProjectedShadows[0]->ResolutionX;
ShadowBufferResolution.Y = VisibleLightInfo.AllProjectedShadows[0]->ResolutionY;
uint32 ShadowBorder = VisibleLightInfo.AllProjectedShadows[0]->BorderSize;
OUTSingleCascadeInfo.SetupWholeSceneProjection(
LightSceneInfo,
&View,
WholeSceneInitializer,
ShadowBufferResolution.X,
ShadowBufferResolution.Y,
ShadowBufferResolution.X,
ShadowBufferResolution.Y,
ShadowBorder
);
bSuccess = true;
}
}
View.MaxShadowCascades = ViewMaxCascades;
return bSuccess;
}
// Build visibility lists of CSM receivers and non-csm receivers.
void FMobileSceneRenderer::BuildCSMVisibilityState(FLightSceneInfo* LightSceneInfo)
{
SCOPE_CYCLE_COUNTER(STAT_BuildCSMVisibilityState);
const uint32 CSMCullingMethod = CVarsCsmShaderCullingMethod.GetValueOnRenderThread() & 0xF;
const bool bSphereTest = (CVarsCsmShaderCullingMethod.GetValueOnRenderThread() & 0x10) != 0;
bool bMovableLightUsingCSM = LightSceneInfo->Proxy->IsMovable() && LightSceneInfo->ShouldRenderViewIndependentWholeSceneShadows();
if (LightSceneInfo->Proxy->CastsDynamicShadow() &&
(bMovableLightUsingCSM || (LightSceneInfo->Proxy->HasStaticShadowing() && LightSceneInfo->Proxy->UseCSMForDynamicObjects()))
)
{
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
bool bStaticCSMReceiversFound = false;
FViewInfo& View = Views[ViewIndex];
FProjectedShadowInfo SingleCascadeInfo;
if (BuildSingleCascadeShadowInfo(View, VisibleLightInfos, LightSceneInfo, SingleCascadeInfo) == false)
{
continue;
}
FProjectedShadowInfo* ProjectedShadowInfo = &SingleCascadeInfo;
if (ViewFamily.EngineShowFlags.ShadowFrustums)
{
FViewElementPDI ShadowFrustumPDI(&View, nullptr, nullptr);
const FMatrix ViewMatrix = View.ViewMatrices.GetViewMatrix();
const FMatrix ProjectionMatrix = View.ViewMatrices.GetProjectionMatrix();
const FVector4 ViewOrigin = View.ViewMatrices.GetViewOrigin();
float AspectRatio = ProjectionMatrix.M[1][1] / ProjectionMatrix.M[0][0];
float ActualFOV = (ViewOrigin.W > 0.0f) ? FMath::Atan(1.0f / ProjectionMatrix.M[0][0]) : PI / 4.0f;
float Near = ProjectedShadowInfo->CascadeSettings.SplitNear;
float Mid = ProjectedShadowInfo->CascadeSettings.FadePlaneOffset;
float Far = ProjectedShadowInfo->CascadeSettings.SplitFar;
DrawFrustumWireframe(&ShadowFrustumPDI, (ViewMatrix * FPerspectiveMatrix(ActualFOV, AspectRatio, 1.0f, Near, Far)).Inverse(), FColor::Emerald, 0);
DrawFrustumWireframe(&ShadowFrustumPDI, FMatrix(ProjectedShadowInfo->TranslatedWorldToClipInnerMatrix.Inverse()) * FTranslationMatrix(-ProjectedShadowInfo->PreShadowTranslation), FColor::Cyan, 0);
}
FViewInfo* ShadowSubjectView = ProjectedShadowInfo->DependentView ? ProjectedShadowInfo->DependentView : &View;
FVisibleLightViewInfo& VisibleLightViewInfo = ShadowSubjectView->VisibleLightInfos[LightSceneInfo->Id];
FMobileCSMSubjectPrimitives& MobileCSMSubjectPrimitives = VisibleLightViewInfo.MobileCSMSubjectPrimitives;
FMobileCSMVisibilityInfo& MobileCSMVisibilityInfo = View.MobileCSMVisibilityInfo;
FVector LightDir = LightSceneInfo->Proxy->GetDirection();
const float ShadowCastLength = WORLD_MAX;
const auto& ShadowSubjectPrimitives = MobileCSMSubjectPrimitives.GetShadowSubjectPrimitives();
if (ShadowSubjectPrimitives.Num() != 0 || CSMCullingMethod == 0 || CSMCullingMethod == 1)
{
FConvexVolume ViewFrustum;
GetViewFrustumBounds(ViewFrustum, View.ViewMatrices.GetViewProjectionMatrix(), true);
//FConvexVolume& ShadowReceiverFrustum = ProjectedShadowInfo->CascadeSettings.ShadowBoundsAccurate;
//FVector PreShadowTranslation = FVector(0, 0, 0);
FConvexVolume& ShadowReceiverFrustum = ProjectedShadowInfo->ReceiverInnerFrustum;
FVector& PreShadowTranslation = ProjectedShadowInfo->PreShadowTranslation;
// Common receiver test functions.
// Test receiver bounding box against view+shadow frustum only
auto IsShadowReceiver = [&ViewFrustum, &ShadowReceiverFrustum, &PreShadowTranslation](const FVector& PrimOrigin, const FVector& PrimExtent)
{
return ViewFrustum.IntersectBox(PrimOrigin, PrimExtent)
&& ShadowReceiverFrustum.IntersectBox(PrimOrigin + PreShadowTranslation, PrimExtent);
};
//Test against caster capsule vs bounds sphere
auto IsShadowReceiverCasterVsSphere = [](const FVector& PrimOrigin, float PrimRadius, const FVector& CasterStart, const FVector& CasterEnd, float CasterRadius)
{
return FMath::PointDistToSegmentSquared(PrimOrigin, CasterStart, CasterEnd) < FMath::Square(PrimRadius + CasterRadius);
};
// Test receiver against single caster capsule vs bounding box
auto IsShadowReceiverCasterVsBox = [](const FVector& PrimOrigin, const FVector& PrimExtent, const FVector& CasterStart, const FVector& CasterEnd, float CasterRadius)
{
FBox PrimBox(PrimOrigin - (PrimExtent + CasterRadius), PrimOrigin + (PrimExtent + CasterRadius));
FVector Direction = CasterEnd - CasterStart;
return FMath::LineBoxIntersection(PrimBox, CasterStart, CasterEnd, Direction);
};
switch (CSMCullingMethod)
{
case 0:
{
// Set all prims to receive CSM
for (FSceneSetBitIterator BitIt(View.PrimitiveVisibilityMap); BitIt; ++BitIt)
{
EnableStaticMeshCSMVisibilityState(bMovableLightUsingCSM, Scene->Primitives[BitIt.GetIndex()], MobileCSMVisibilityInfo, View);
}
bStaticCSMReceiversFound = MobileCSMVisibilityInfo.bMobileDynamicCSMInUse = true;
break;
}
case 1:
{
auto IsShadowReceiverFrustumOnly = [&IsShadowReceiver](const FVector& PrimOrigin, const FVector& PrimExtent, float PrimRadius)
{
return IsShadowReceiver(PrimOrigin, PrimExtent);
};
bStaticCSMReceiversFound = MobileDetermineStaticMeshesCSMVisibilityState(Scene, View, ProjectedShadowInfo, IsShadowReceiverFrustumOnly);
break;
}
case 2: // combined casters:
{
// Calculate combined bounds
FBoxSphereBounds::Builder CombinedBoundsBuilder;
for (auto& Caster : ShadowSubjectPrimitives)
{
CombinedBoundsBuilder += Caster->Proxy->GetBounds();
}
FBoxSphereBounds CombinedBounds(CombinedBoundsBuilder);
FVector CombinedCasterStart = CombinedBounds.Origin;
FVector CombinedCasterEnd = CombinedBounds.Origin + (LightDir * ShadowCastLength);
if (bSphereTest)
{
// Test against view+shadow frustums and caster capsule vs bounding sphere
auto IsShadowReceiverCombined = [&IsShadowReceiver, &IsShadowReceiverCasterVsSphere, &CombinedBounds, &CombinedCasterStart, &CombinedCasterEnd](const FVector& PrimOrigin, const FVector& PrimExtent, float PrimRadius)
{
return IsShadowReceiver(PrimOrigin, PrimExtent) && IsShadowReceiverCasterVsSphere(PrimOrigin, PrimRadius, CombinedCasterStart, CombinedCasterEnd, CombinedBounds.SphereRadius);
};
bStaticCSMReceiversFound = MobileDetermineStaticMeshesCSMVisibilityState(Scene, View, ProjectedShadowInfo, IsShadowReceiverCombined);
}
else
{
// Test against view+shadow frustums and caster capsule vs bounding box
auto IsShadowReceiverCombinedBox = [&IsShadowReceiver, &IsShadowReceiverCasterVsBox, &CombinedBounds, &CombinedCasterStart, &CombinedCasterEnd](const FVector& PrimOrigin, const FVector& PrimExtent, float PrimRadius)
{
return IsShadowReceiver(PrimOrigin, PrimExtent) && IsShadowReceiverCasterVsBox(PrimOrigin, PrimExtent, CombinedCasterStart, CombinedCasterEnd, CombinedBounds.SphereRadius);
};
bStaticCSMReceiversFound = MobileDetermineStaticMeshesCSMVisibilityState(Scene, View, ProjectedShadowInfo, IsShadowReceiverCombinedBox);
}
break;
}
case 3: // All casters:
{
if (bSphereTest)
{
auto IsShadowReceiverAllCastersVsSphere = [&ShadowSubjectPrimitives, &IsShadowReceiverCasterVsSphere, &LightDir, &ShadowCastLength](const FVector& PrimOrigin, float PrimRadius)
{
for (auto& Caster : ShadowSubjectPrimitives)
{
const FBoxSphereBounds& CasterBounds = Caster->Proxy->GetBounds();
const FVector& CasterStart = CasterBounds.Origin;
float CasterRadius = CasterBounds.SphereRadius;
const FVector CasterEnd = CasterStart + (LightDir * ShadowCastLength);
if (IsShadowReceiverCasterVsSphere(PrimOrigin, PrimRadius, CasterStart, CasterEnd, CasterRadius))
{
return true;
}
}
return false;
};
// Test against view+shadow frustums and all caster capsules vs bounding sphere
auto IsShadowReceiverSphereAllCasters = [&IsShadowReceiver, &IsShadowReceiverAllCastersVsSphere](const FVector& PrimOrigin, const FVector& PrimExtent, float PrimRadius)
{
return IsShadowReceiver(PrimOrigin, PrimExtent) && IsShadowReceiverAllCastersVsSphere(PrimOrigin, PrimRadius);
};
bStaticCSMReceiversFound = MobileDetermineStaticMeshesCSMVisibilityState(Scene, View, ProjectedShadowInfo, IsShadowReceiverSphereAllCasters);
}
else
{
// Test against all caster capsules vs bounding box
auto IsShadowReceiverAllCastersVsBox = [&ShadowSubjectPrimitives, &IsShadowReceiverCasterVsBox, &LightDir, &ShadowCastLength](const FVector& PrimOrigin, const FVector& PrimExtent)
{
for (auto& Caster : ShadowSubjectPrimitives)
{
const FBoxSphereBounds& CasterBounds = Caster->Proxy->GetBounds();
const FVector& CasterStart = CasterBounds.Origin;
const FVector CasterEnd = CasterStart + (LightDir * ShadowCastLength);
float CasterRadius = CasterBounds.SphereRadius;
if (IsShadowReceiverCasterVsBox(PrimOrigin, PrimExtent, CasterStart, CasterEnd, CasterRadius))
{
return true;
}
}
return false;
};
// Test against view+shadow frustums and all caster capsules vs bounding box
auto IsShadowReceiverBoxAllCasters = [&IsShadowReceiver, &IsShadowReceiverAllCastersVsBox](const FVector& PrimOrigin, const FVector& PrimExtent, float PrimRadius)
{
return IsShadowReceiver(PrimOrigin, PrimExtent) && IsShadowReceiverAllCastersVsBox(PrimOrigin, PrimExtent);
};
bStaticCSMReceiversFound = MobileDetermineStaticMeshesCSMVisibilityState(Scene, View, ProjectedShadowInfo, IsShadowReceiverBoxAllCasters);
}
}
break;
case 4:
{
bStaticCSMReceiversFound = MobileCSMVisibilityInfo.bMobileDynamicCSMInUse = false;
}
break;
}
if (CVarCsmShaderCullingDebugGfx.GetValueOnRenderThread())
{
VisualizeMobileDynamicCSMSubjectCapsules(View, LightSceneInfo, ProjectedShadowInfo);
}
INC_DWORD_STAT_BY(STAT_CSMSubjects, ShadowSubjectPrimitives.Num());
}
MobileCSMVisibilityInfo.bMobileDynamicCSMInUse = bStaticCSMReceiversFound;
}
}
}