// 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 CVarCsmShaderCullingDebugGfx( TEXT("r.Mobile.Shadow.CSMShaderCullingDebugGfx"), 0, TEXT(""), ECVF_RenderThreadSafe); const uint32 CSMShaderCullingMethodDefault = 1; static TAutoConsoleVariable 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 UsedFeatureLevels; for (TObjectIterator 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 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 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& 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; } } }