// Copyright Epic Games, Inc. All Rights Reserved. #include "LidarPointCloudLODManager.h" #include "LidarPointCloud.h" #include "LidarPointCloudComponent.h" #include "LidarPointCloudOctree.h" #include "LidarPointCloudSettings.h" #include "Rendering/LidarPointCloudRenderBuffers.h" #include "Engine/LocalPlayer.h" #include "Kismet/GameplayStatics.h" #include "Async/Async.h" #include "Misc/ScopeLock.h" #include "Misc/ScopeTryLock.h" #include "Engine/Engine.h" #include "Engine/GameViewportClient.h" #include "EngineUtils.h" #include "SceneView.h" #include "UnrealClient.h" #if WITH_EDITOR #include "Settings/EditorStyleSettings.h" #include "EditorViewportClient.h" #endif DECLARE_CYCLE_STAT(TEXT("Node Selection"), STAT_NodeSelection, STATGROUP_LidarPointCloud) DECLARE_CYCLE_STAT(TEXT("Node Processing"), STAT_NodeProcessing, STATGROUP_LidarPointCloud) DECLARE_CYCLE_STAT(TEXT("Render Data Update"), STAT_UpdateRenderData, STATGROUP_LidarPointCloud) DECLARE_CYCLE_STAT(TEXT("Node Streaming"), STAT_NodeStreaming, STATGROUP_LidarPointCloud); DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Total Point Count [thousands]"), STAT_PointCountTotal, STATGROUP_LidarPointCloud) DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Point Budget"), STAT_PointBudget, STATGROUP_LidarPointCloud) DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Visible Points"), STAT_PointCount, STATGROUP_LidarPointCloud) DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Loaded Nodes"), STAT_LoadedNodes, STATGROUP_LidarPointCloud) static TAutoConsoleVariable CVarLidarPointBudget( TEXT("r.LidarPointBudget"), 0, TEXT("If set to > 0, this will overwrite the Target FPS setting, and apply a fixed budget.\n") TEXT("Determines the maximum number of points to be visible on the screen.\n") TEXT("Higher values will produce better image quality, but will require faster hardware."), ECVF_Scalability); static TAutoConsoleVariable CVarLidarScreenCenterImportance( TEXT("r.LidarScreenCenterImportance"), 0.0f, TEXT("Determines the preference towards selecting nodes closer to screen center\n") TEXT("with larger values giving more priority towards screen center.\n") TEXT("Usefulf for VR, where edge vision is blurred anyway.\n") TEXT("0 to disable."), ECVF_Scalability); static TAutoConsoleVariable CVarBaseLODImportance( TEXT("r.LidarBaseLODImportance"), 0.1f, TEXT("Determines the importance of selecting at least the base LOD of far assets.\n") TEXT("Increase it, if you're experiencing actor 'popping'.\n") TEXT("0 to use purely screensize-driven algorithm."), ECVF_Scalability); static TAutoConsoleVariable CVarTargetFPS( TEXT("r.LidarTargetFPS"), 59.0f, TEXT("The LOD system will continually adjust the quality of the assets to maintain\n") TEXT("the specified target FPS."), ECVF_Scalability); static TAutoConsoleVariable CVarLidarIncrementalBudget( TEXT("r.LidarIncrementalBudget"), false, TEXT("If enabled, the point budget will automatically increase whenever the\n") TEXT("camera's location and orientation remain unchanged."), ECVF_Scalability); FLidarPointCloudViewData::FLidarPointCloudViewData(bool bCompute) : bValid(false) , ViewOrigin(FVector::ZeroVector) , ViewDirection(FVector::ForwardVector) , ScreenSizeFactor(0) , bSkipMinScreenSize(false) , bPIE(false) , bHasFocus(false) { if (bCompute) { Compute(); } } void FLidarPointCloudViewData::Compute() { bool bForceSkipLocalPlayer = false; #if WITH_EDITOR bForceSkipLocalPlayer = GIsEditor && GEditor && GEditor->bIsSimulatingInEditor; #endif // Attempt to get the first local player's viewport if (GEngine && !bForceSkipLocalPlayer) { ULocalPlayer* const LP = GEngine->FindFirstLocalPlayerFromControllerId(0); if (LP && LP->ViewportClient) { FSceneViewProjectionData ProjectionData; if (LP->GetProjectionData(LP->ViewportClient->Viewport, ProjectionData, GEngine->IsStereoscopic3D() ? EStereoscopicEye::eSSE_LEFT_EYE : EStereoscopicEye::eSSE_MONOSCOPIC)) { ViewOrigin = ProjectionData.ViewOrigin; FMatrix ViewRotationMatrix = ProjectionData.ViewRotationMatrix; if (!ViewRotationMatrix.GetOrigin().IsNearlyZero(0.0f)) { ViewOrigin += ViewRotationMatrix.InverseTransformPosition(FVector::ZeroVector); ViewRotationMatrix = ViewRotationMatrix.RemoveTranslation(); } FMatrix ViewMatrix = FTranslationMatrix(-ViewOrigin) * ViewRotationMatrix; ViewDirection = ViewMatrix.GetColumn(2); FMatrix ProjectionMatrix = AdjustProjectionMatrixForRHI(ProjectionData.ProjectionMatrix); ScreenSizeFactor = FMath::Square(FMath::Max(0.5f * ProjectionMatrix.M[0][0], 0.5f * ProjectionMatrix.M[1][1])); // Skip SS check, if not in the projection view nor game world bSkipMinScreenSize = (ProjectionMatrix.M[3][3] >= 1.0f) && !LP->GetWorld()->IsGameWorld(); GetViewFrustumBounds(ViewFrustum, ViewMatrix * ProjectionMatrix, false); bHasFocus = LP->ViewportClient->Viewport->HasFocus(); bValid = true; } } } #if WITH_EDITOR bPIE = false; if (GIsEditor && GEditor && GEditor->GetActiveViewport()) { bPIE = GEditor->GetActiveViewport() == GEditor->GetPIEViewport(); // PIE needs a different computation method if (!bValid && !bPIE) { ComputeFromEditorViewportClient(GEditor->GetActiveViewport()->GetClient()); } // Simulating counts as PIE for the purpose of LOD calculation bPIE |= GEditor->bIsSimulatingInEditor; } #endif } bool FLidarPointCloudViewData::ComputeFromEditorViewportClient(FViewportClient* ViewportClient) { #if WITH_EDITOR if (FEditorViewportClient* Client = (FEditorViewportClient*)ViewportClient) { if (Client->Viewport && Client->Viewport->GetSizeXY() != FIntPoint::ZeroValue) { FSceneViewFamily::ConstructionValues CVS(nullptr, nullptr, FEngineShowFlags(EShowFlagInitMode::ESFIM_Game)); CVS.SetTime(FGameTime()); FSceneViewFamilyContext ViewFamily(CVS); FSceneView* View = Client->CalcSceneView(&ViewFamily); const FMatrix& ProjectionMatrix = View->ViewMatrices.GetProjectionMatrix(); ScreenSizeFactor = FMath::Square(FMath::Max(0.5f * ProjectionMatrix.M[0][0], 0.5f * ProjectionMatrix.M[1][1])); ViewOrigin = View->ViewMatrices.GetViewOrigin(); ViewDirection = View->GetViewDirection(); ViewFrustum = View->ViewFrustum; bSkipMinScreenSize = !View->bIsGameView && !View->IsPerspectiveProjection(); bHasFocus = Client->Viewport->HasFocus(); bValid = true; return true; } } #endif return false; } int32 FLidarPointCloudTraversalOctree::GetVisibleNodes(TArray& NodeSizeData, const FLidarPointCloudViewData* ViewData, const int32& ProxyIndex, const FLidarPointCloudNodeSelectionParams& SelectionParams) { const int32 CurrentNodeCount = NodeSizeData.Num(); // Only process, if the asset is visible if (ViewData->ViewFrustum.IntersectBox(GetCenter(), GetExtent())) { const float BoundsScaleSq = SelectionParams.BoundsScale * SelectionParams.BoundsScale; const float BaseLODImportance = FMath::Max(0.0f, CVarBaseLODImportance.GetValueOnAnyThread()); TQueue Nodes; FLidarPointCloudTraversalOctreeNode* CurrentNode = nullptr; Nodes.Enqueue(&Root); while (Nodes.Dequeue(CurrentNode)) { // Reset selection flag CurrentNode->bSelected = false; // Update number of visible points, if needed CurrentNode->DataNode->UpdateNumVisiblePoints(); const FVector3f NodeExtent = Extents[CurrentNode->Depth] * SelectionParams.BoundsScale; bool bFullyContained = true; if (SelectionParams.bUseFrustumCulling && (CurrentNode->Depth == 0 || !CurrentNode->bFullyContained) && !ViewData->ViewFrustum.IntersectBox((FVector)CurrentNode->Center, (FVector)NodeExtent, bFullyContained)) { continue; } // Only process this node if it has any visible points - do not use continue; as the children may still contain visible points! if (CurrentNode->DataNode->GetNumVisiblePoints() > 0 && CurrentNode->Depth >= SelectionParams.MinDepth) { float ScreenSizeSq = 0; FVector3f VectorToNode = CurrentNode->Center - (FVector3f)ViewData->ViewOrigin; const float DistSq = VectorToNode.SizeSquared(); const float AdjustedRadiusSq = RadiiSq[CurrentNode->Depth] * BoundsScaleSq; // Make sure to show at least the minimum depth for each visible asset if (CurrentNode->Depth == SelectionParams.MinDepth) { // Add screen size to maintain hierarchy ScreenSizeSq = BaseLODImportance + ViewData->ScreenSizeFactor * AdjustedRadiusSq / FMath::Max(1.0f, DistSq); } else { // If the camera is within this node's bounds, it should always be qualified for rendering if (DistSq <= AdjustedRadiusSq) { // Subtract Depth to maintain hierarchy ScreenSizeSq = 1000 - CurrentNode->Depth; } else { ScreenSizeSq = ViewData->ScreenSizeFactor * AdjustedRadiusSq / FMath::Max(1.0f, DistSq); // Check for minimum screen size if (!ViewData->bSkipMinScreenSize && ScreenSizeSq < SelectionParams.MinScreenSize) { continue; } // Add optional preferential selection for nodes closer to the screen center if (SelectionParams.ScreenCenterImportance > 0) { VectorToNode.Normalize(); float Dot = FVector3f::DotProduct((FVector3f)ViewData->ViewDirection, VectorToNode); ScreenSizeSq = FMath::Lerp(ScreenSizeSq, ScreenSizeSq * Dot, SelectionParams.ScreenCenterImportance); } } } NodeSizeData.Emplace(CurrentNode, ScreenSizeSq, ProxyIndex); } if (SelectionParams.MaxDepth < 0 || CurrentNode->Depth < SelectionParams.MaxDepth) { for (FLidarPointCloudTraversalOctreeNode& Child : CurrentNode->Children) { Child.bFullyContained = bFullyContained; Nodes.Enqueue(&Child); } } } } return NodeSizeData.Num() - CurrentNodeCount; } /** Calculates the correct point budget to use for current frame */ uint32 GetPointBudget(int64 NumPointsInFrustum) { constexpr int32 NumFramesToAcumulate = 30; constexpr int32 DeltaBudgetDeadzone = 10000; static int64 CurrentPointBudget = 0; static int64 LastDynamicPointBudget = 0; static bool bLastFrameIncremental = false; static FLidarPointCloudViewData LastViewData; static TArray AcumulatedFrameTime; static double CurrentRealTime = 0; static double LastRealTime = 0; static double RealDeltaTime = 0; CurrentRealTime = FPlatformTime::Seconds(); RealDeltaTime = CurrentRealTime - LastRealTime; LastRealTime = CurrentRealTime; if (AcumulatedFrameTime.Num() == 0) { AcumulatedFrameTime.Reserve(NumFramesToAcumulate + 1); } const FLidarPointCloudViewData ViewData(true); if (!LastViewData.bValid) { LastViewData = ViewData; } bool bUseIncrementalBudget = CVarLidarIncrementalBudget.GetValueOnAnyThread(); const int32 ManualPointBudget = CVarLidarPointBudget.GetValueOnAnyThread(); if (bUseIncrementalBudget && ViewData.ViewOrigin.Equals(LastViewData.ViewOrigin) && ViewData.ViewDirection.Equals(LastViewData.ViewDirection)) { CurrentPointBudget += 500000; bLastFrameIncremental = true; } else { // Check if the point budget is manually set if (ManualPointBudget > 0) { CurrentPointBudget = ManualPointBudget; } else { CurrentPointBudget = LastDynamicPointBudget; // Do not recalculate if just exiting incremental budget, to avoid spikes if (!bLastFrameIncremental) { if (AcumulatedFrameTime.Add(RealDeltaTime) == NumFramesToAcumulate) { AcumulatedFrameTime.RemoveAt(0); } const float MaxTickRate = GEngine->GetMaxTickRate(0.001f, false); const float RequestedTargetFPS = CVarTargetFPS.GetValueOnAnyThread(); const float TargetFPS = GEngine->bUseFixedFrameRate ? GEngine->FixedFrameRate : (MaxTickRate > 0 ? FMath::Min(RequestedTargetFPS, MaxTickRate) : RequestedTargetFPS); // The -0.5f is to prevent the system treating values as unachievable (as the frame time is usually just under) const float AdjustedTargetFPS = FMath::Max(TargetFPS - 0.5f, 1.0f); TArray CurrentFrameTimes = AcumulatedFrameTime; CurrentFrameTimes.Sort(); const float AvgFrameTime = CurrentFrameTimes[CurrentFrameTimes.Num() / 2]; const int32 DeltaBudget = (1 / AdjustedTargetFPS - AvgFrameTime) * 10000000 * (GEngine->bUseFixedFrameRate ? 4 : 1);; // Prevent constant small fluctuations, unless using fixed frame rate if (GEngine->bUseFixedFrameRate || FMath::Abs(DeltaBudget) > DeltaBudgetDeadzone) { // Not having enough points in frustum to fill the requested budget would otherwise continually increase the value if (DeltaBudget < 0 || NumPointsInFrustum >= CurrentPointBudget) { CurrentPointBudget += DeltaBudget; } } } } bLastFrameIncremental = false; } // Just in case if (ManualPointBudget == 0) { CurrentPointBudget = FMath::Clamp(CurrentPointBudget, 350000LL, 100000000LL); } if (!bUseIncrementalBudget) { LastDynamicPointBudget = CurrentPointBudget; } LastViewData = ViewData; return CurrentPointBudget; } FLidarPointCloudLODManager::FLidarPointCloudLODManager() : NumPointsInFrustum(0) { } void FLidarPointCloudLODManager::Tick(float DeltaTime) { // Skip processing, if a previous one is still going if (bProcessing) { return; } bProcessing = true; Time += DeltaTime; LastPointBudget = GetPointBudget(NumPointsInFrustum.GetValue()); SET_DWORD_STAT(STAT_PointBudget, LastPointBudget); PrepareProxies(); Async(EAsyncExecution::ThreadPool, [this, CurrentRegisteredProxies = RegisteredProxies, PointBudget = LastPointBudget, ClippingVolumes = GetClippingVolumes()] { NumPointsInFrustum.Set(ProcessLOD(CurrentRegisteredProxies, Time, PointBudget, ClippingVolumes)); bProcessing = false; }); } TStatId FLidarPointCloudLODManager::GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(LidarPointCloudLODManager, STATGROUP_Tickables); } void FLidarPointCloudLODManager::RegisterProxy(ULidarPointCloudComponent* Component, TWeakPtr SceneProxyWrapper) { if (IsValid(Component)) { static FCriticalSection ProxyLock; if (TSharedPtr SceneProxyWrapperShared = SceneProxyWrapper.Pin()) { FScopeLock Lock(&ProxyLock); Get().RegisteredProxies.Emplace(Component, SceneProxyWrapper); Get().ForceProcessLOD(); } } } void FLidarPointCloudLODManager::RefreshLOD() { Get().ForceProcessLOD(); } int64 FLidarPointCloudLODManager::ProcessLOD(const TArray& InRegisteredProxies, const float CurrentTime, const uint32 PointBudget, const TArray& ClippingVolumes) { struct FSelectionFilterParams { float MinScreenSize; uint32 NumNodes; }; static TArray SelectionFilterParams; { const int32 DeltaSize = InRegisteredProxies.Num() - SelectionFilterParams.Num(); if (DeltaSize > 0) { SelectionFilterParams.AddZeroed(DeltaSize); } } const bool bUseRayTracing = GetDefault()->bEnableLidarRayTracing; uint32 TotalPointsSelected = 0; int64 NewNumPointsInFrustum = 0; TArray> SelectedNodesData; // Node selection { SCOPE_CYCLE_COUNTER(STAT_NodeSelection); const float ScreenCenterImportance = CVarLidarScreenCenterImportance.GetValueOnAnyThread(); int32 NumSelectedNodes = 0; TArray NodeSizeData; for (int32 i = 0; i < InRegisteredProxies.Num(); ++i) { const FLidarPointCloudLODManager::FRegisteredProxy& RegisteredProxy = InRegisteredProxies[i]; // Acquire a Shared Pointer from the Weak Pointer and check that it references a valid object if (TSharedPtr SceneProxyWrapper = RegisteredProxy.SceneProxyWrapper.Pin()) { #if WITH_EDITOR // Avoid doubling the point allocation of the same asset (once in Editor world and once in PIE world) if (RegisteredProxy.bSkip) { continue; } #endif // Attempt to get a data lock and check if the traversal octree is still valid FScopeTryLock OctreeLock(&RegisteredProxy.Octree->DataLock); if (OctreeLock.IsLocked() && RegisteredProxy.TraversalOctree->bValid) { FLidarPointCloudNodeSelectionParams SelectionParams; SelectionParams.MinScreenSize = SelectionFilterParams[i].MinScreenSize; SelectionParams.ScreenCenterImportance = ScreenCenterImportance; SelectionParams.MinDepth = RegisteredProxy.ComponentRenderParams.MinDepth; SelectionParams.MaxDepth = RegisteredProxy.ComponentRenderParams.MaxDepth; SelectionParams.BoundsScale = RegisteredProxy.ComponentRenderParams.BoundsScale; SelectionParams.bUseFrustumCulling = RegisteredProxy.ComponentRenderParams.bUseFrustumCulling && !bUseRayTracing; SelectionParams.ClippingVolumes = RegisteredProxy.ComponentRenderParams.bOwnedByEditor ? nullptr : &ClippingVolumes; // Ignore clipping if in editor viewport // Append visible nodes SelectionFilterParams[i].NumNodes = RegisteredProxy.TraversalOctree->GetVisibleNodes(NodeSizeData, &RegisteredProxy.ViewData, i, SelectionParams); } } } // Sort Nodes Algo::Sort(NodeSizeData, [](const FLidarPointCloudTraversalOctreeNodeSizeData& A, const FLidarPointCloudTraversalOctreeNodeSizeData& B) { return A.Size > B.Size; }); TArray NewMinScreenSizes; NewMinScreenSizes.AddZeroed(InRegisteredProxies.Num()); // Limit nodes using specified Point Budget SelectedNodesData.AddDefaulted(InRegisteredProxies.Num()); for (FLidarPointCloudTraversalOctreeNodeSizeData& Element : NodeSizeData) { const uint32 NumPoints = Element.Node->DataNode->GetNumVisiblePoints(); const uint32 NewNumPointsSelected = TotalPointsSelected + NumPoints; NewNumPointsInFrustum += NumPoints; if (NewNumPointsSelected <= PointBudget) { NewMinScreenSizes[Element.ProxyIndex] = FMath::Max(Element.Size, 0.0f); SelectedNodesData[Element.ProxyIndex].Add(Element.Node); TotalPointsSelected = NewNumPointsSelected; Element.Node->bSelected = true; --SelectionFilterParams[Element.ProxyIndex].NumNodes; ++NumSelectedNodes; } } for (int32 i = 0; i < InRegisteredProxies.Num(); ++i) { // If point budget is saturated, apply new Min Screen Sizes // Otherwise, decrease Min Screen Sizes, to allow for more points SelectionFilterParams[i].MinScreenSize = SelectionFilterParams[i].NumNodes > 0 ? NewMinScreenSizes[i] : (SelectionFilterParams[i].MinScreenSize * 0.9f); } SET_DWORD_STAT(STAT_PointCount, TotalPointsSelected); } // Used to pass render data updates to render thread TArray ProxyUpdateData; // Holds a per-octree list of nodes to stream-in or extend TMultiMap> OctreeStreamingMap; // Process Nodes { SCOPE_CYCLE_COUNTER(STAT_NodeProcessing); const bool bUseStaticBuffers = GetDefault()->bUseFastRendering; for (int32 i = 0; i < SelectedNodesData.Num(); ++i) { const FLidarPointCloudLODManager::FRegisteredProxy& RegisteredProxy = InRegisteredProxies[i]; // Attempt to get a data lock and check if the traversal octree is still valid FScopeTryLock OctreeLock(&RegisteredProxy.Octree->DataLock); if (OctreeLock.IsLocked() && RegisteredProxy.TraversalOctree->bValid) { FLidarPointCloudProxyUpdateData UpdateData; UpdateData.SceneProxyWrapper = RegisteredProxy.SceneProxyWrapper; UpdateData.NumElements = 0; UpdateData.VDMultiplier = RegisteredProxy.TraversalOctree->ReversedVirtualDepthMultiplier; UpdateData.RootCellSize = RegisteredProxy.Octree->GetRootCellSize(); UpdateData.ClippingVolumes = ClippingVolumes; UpdateData.bUseStaticBuffers = bUseStaticBuffers && !RegisteredProxy.Octree->IsOptimizedForDynamicData(); UpdateData.bUseRayTracing = bUseRayTracing; UpdateData.RenderParams = RegisteredProxy.ComponentRenderParams; #if !(UE_BUILD_SHIPPING) const bool bDrawNodeBounds = RegisteredProxy.ComponentRenderParams.bDrawNodeBounds; if (bDrawNodeBounds) { UpdateData.Bounds.Reset(SelectedNodesData[i].Num()); } #endif const bool bCalculateVirtualDepth = RegisteredProxy.ComponentRenderParams.PointSize > 0; TArray LocalLevelWeights; TArray* LevelWeightsPtr = &RegisteredProxy.TraversalOctree->LevelWeights; float PointSizeBias = RegisteredProxy.ComponentRenderParams.PointSizeBias; // Only calculate if needed if (bCalculateVirtualDepth && (RegisteredProxy.ComponentRenderParams.ScalingMethod == ELidarPointCloudScalingMethod::PerNodeAdaptive || RegisteredProxy.ComponentRenderParams.ScalingMethod == ELidarPointCloudScalingMethod::PerPoint)) { RegisteredProxy.TraversalOctree->CalculateLevelWeightsForSelectedNodes(LocalLevelWeights); LevelWeightsPtr = &LocalLevelWeights; PointSizeBias = 0; } // Queue nodes to be streamed TArray& NodesToStream = OctreeStreamingMap.FindOrAdd(RegisteredProxy.Octree); NodesToStream.Reserve(NodesToStream.Num() + SelectedNodesData[i].Num()); for (FLidarPointCloudTraversalOctreeNode* Node : SelectedNodesData[i]) { NodesToStream.Add(Node->DataNode); if (Node->DataNode->HasData()) { // Only calculate if needed if (bCalculateVirtualDepth) { Node->CalculateVirtualDepth(*LevelWeightsPtr, PointSizeBias); } const uint32 NumVisiblePoints = Node->DataNode->GetNumVisiblePoints(); UpdateData.NumElements += NumVisiblePoints; UpdateData.SelectedNodes.Emplace(bCalculateVirtualDepth ? Node->VirtualDepth : 0, NumVisiblePoints, Node->DataNode); #if !(UE_BUILD_SHIPPING) if (bDrawNodeBounds) { const FVector3f Extent = RegisteredProxy.TraversalOctree->Extents[Node->Depth]; UpdateData.Bounds.Emplace(Node->Center - Extent, Node->Center + Extent); } #endif } } // Only calculate if needed if (bCalculateVirtualDepth && RegisteredProxy.ComponentRenderParams.ScalingMethod == ELidarPointCloudScalingMethod::PerPoint) { RegisteredProxy.TraversalOctree->CalculateVisibilityStructure(UpdateData.TreeStructure); } ProxyUpdateData.Add(UpdateData); } } } // Perform data streaming in a separate thread Async(EAsyncExecution::ThreadPool, [OctreeStreamingMap = MoveTemp(OctreeStreamingMap), CurrentTime]() mutable { SCOPE_CYCLE_COUNTER(STAT_NodeStreaming); int32 LoadedNodes = 0; for (TPair>& OctreeStreamingData : OctreeStreamingMap) { FScopeTryLock OctreeLock(&OctreeStreamingData.Key->DataLock); if (OctreeLock.IsLocked()) { OctreeStreamingData.Key->StreamNodes(OctreeStreamingData.Value, CurrentTime); LoadedNodes += OctreeStreamingData.Key->GetNumNodesInUse(); } } SET_DWORD_STAT(STAT_LoadedNodes, LoadedNodes); }); // Update Render Data if (TotalPointsSelected > 0) { ENQUEUE_RENDER_COMMAND(ProcessLidarPointCloudLOD)([ProxyUpdateData](FRHICommandListImmediate& RHICmdList) mutable { SCOPE_CYCLE_COUNTER(STAT_UpdateRenderData); uint32 MaxPointsPerNode = 0; const double ProcessingTime = FPlatformTime::Seconds(); const bool bUseRenderDataSmoothing = GetDefault()->bUseRenderDataSmoothing; const float RenderDataSmoothingMaxFrametime = GetDefault()->RenderDataSmoothingMaxFrametime; // Iterate over proxies and, if valid, update their data for (FLidarPointCloudProxyUpdateData& UpdateData : ProxyUpdateData) { // Check for proxy's validity, in case it has been destroyed since the update was issued if (TSharedPtr SceneProxyWrapper = UpdateData.SceneProxyWrapper.Pin()) { if (SceneProxyWrapper->Proxy) { for (FLidarPointCloudProxyUpdateDataNode& Node : UpdateData.SelectedNodes) { if (Node.BuildDataCache(UpdateData.bUseStaticBuffers, UpdateData.bUseRayTracing)) { MaxPointsPerNode = FMath::Max(MaxPointsPerNode, (uint32)Node.NumVisiblePoints); } // Split building render data across multiple frames, to avoid stuttering if (bUseRenderDataSmoothing && (FPlatformTime::Seconds() - ProcessingTime > RenderDataSmoothingMaxFrametime)) { break; } } SceneProxyWrapper->Proxy->UpdateRenderData(UpdateData); } } } if (MaxPointsPerNode > GLidarPointCloudIndexBuffer.GetCapacity()) { GLidarPointCloudIndexBuffer.Resize(MaxPointsPerNode); } }); } return NewNumPointsInFrustum; } void FLidarPointCloudLODManager::ForceProcessLOD() { if(IsInGameThread()) { PrepareProxies(); ProcessLOD(RegisteredProxies, Time, LastPointBudget, GetClippingVolumes()); } } void FLidarPointCloudLODManager::PrepareProxies() { FLidarPointCloudViewData ViewData(true); const bool bPrioritizeActiveViewport = GetDefault()->bPrioritizeActiveViewport; // Contains the total number of points contained by all assets (including invisible and culled) int64 TotalPointCount = 0; // Prepare proxies for (int32 i = 0; i < RegisteredProxies.Num(); ++i) { FRegisteredProxy& RegisteredProxy = RegisteredProxies[i]; bool bValidProxy = false; // Acquire a Shared Pointer from the Weak Pointer and check that it references a valid object if (TSharedPtr SceneProxyWrapper = RegisteredProxy.SceneProxyWrapper.Pin()) { if (ULidarPointCloudComponent* Component = RegisteredProxy.Component.Get()) { if (ULidarPointCloud* PointCloud = RegisteredProxy.PointCloud.Get()) { // Just in case if (Component->GetPointCloud() == PointCloud) { #if WITH_EDITOR // Avoid doubling the point allocation of the same asset (once in Editor world and once in PIE world) RegisteredProxy.bSkip = ViewData.bPIE && Component->GetWorld() && Component->GetWorld()->WorldType == EWorldType::Type::Editor; #endif // Check if the component's transform has changed, and invalidate the Traversal Octree if so const FTransform Transform = Component->GetComponentTransform(); if (!RegisteredProxy.LastComponentTransform.Equals(Transform)) { RegisteredProxy.TraversalOctree->bValid = false; RegisteredProxy.LastComponentTransform = Transform; } // Re-initialize the traversal octree, if needed if (!RegisteredProxy.TraversalOctree->bValid) { // Update asset reference RegisteredProxy.PointCloud = PointCloud; // Recreate the Traversal Octree RegisteredProxy.TraversalOctree = MakeShareable(new FLidarPointCloudTraversalOctree(&PointCloud->Octree, Component->GetComponentTransform())); // If the recreation of the Traversal Octree was unsuccessful, skip further processing if (!RegisteredProxy.TraversalOctree->bValid) { continue; } RegisteredProxy.PointCloud->Octree.RegisterTraversalOctree(RegisteredProxy.TraversalOctree); } // If this is an editor component, use its own ViewportClient if (TSharedPtr Client = Component->GetOwningViewportClient().Pin()) { // If the ViewData cannot be successfully retrieved from the editor viewport, fall back to using main view if (!RegisteredProxy.ViewData.ComputeFromEditorViewportClient(Client.Get())) { RegisteredProxy.ViewData = ViewData; } } // ... otherwise, use the ViewData provided else { RegisteredProxy.ViewData = ViewData; } // Increase priority, if the viewport has focus if (bPrioritizeActiveViewport && RegisteredProxy.ViewData.bHasFocus) { RegisteredProxy.ViewData.ScreenSizeFactor *= 6; } // Don't count the skippable proxies if (!RegisteredProxy.bSkip) { TotalPointCount += PointCloud->GetNumPoints(); } // Update render params RegisteredProxy.ComponentRenderParams.UpdateFromComponent(Component); bValidProxy = true; } } } } // If the SceneProxy has been destroyed, remove it from the list and reiterate if(!bValidProxy) { RegisteredProxies.RemoveAtSwap(i--, EAllowShrinking::No); } } SET_DWORD_STAT(STAT_PointCountTotal, TotalPointCount / 1000); } TArray FLidarPointCloudLODManager::GetClippingVolumes() const { TArray ClippingVolumes; TArray Worlds; for (int32 i = 0; i < RegisteredProxies.Num(); ++i) { if (ULidarPointCloudComponent* Component = RegisteredProxies[i].Component.Get()) { if (!Component->IsOwnedByEditor()) { if (UWorld* World = Component->GetWorld()) { Worlds.AddUnique(World); } } } } for (UWorld* World : Worlds) { for (TActorIterator It(World); It; ++It) { ALidarClippingVolume* Volume = *It; if (Volume->bEnabled) { ClippingVolumes.Emplace(Volume); } } } ClippingVolumes.Sort(); return ClippingVolumes; } FLidarPointCloudLODManager& FLidarPointCloudLODManager::Get() { static FLidarPointCloudLODManager Instance; return Instance; } FLidarPointCloudLODManager::FRegisteredProxy::FRegisteredProxy(TWeakObjectPtr Component, TWeakPtr SceneProxyWrapper) : Component(Component) , PointCloud(Component->GetPointCloud()) , Octree(&PointCloud->Octree) , SceneProxyWrapper(SceneProxyWrapper) , TraversalOctree(new FLidarPointCloudTraversalOctree(Octree, Component->GetComponentTransform())) , LastComponentTransform(Component->GetComponentTransform()) , bSkip(false) { Octree->RegisterTraversalOctree(TraversalOctree); }