Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

852 lines
29 KiB
C++

// 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<int32> 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<float> 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<float> 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<float> 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<bool> 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<FLidarPointCloudTraversalOctreeNodeSizeData>& 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<FLidarPointCloudTraversalOctreeNode*> 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<float> 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<float> 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<FLidarPointCloudSceneProxyWrapper, ESPMode::ThreadSafe> SceneProxyWrapper)
{
if (IsValid(Component))
{
static FCriticalSection ProxyLock;
if (TSharedPtr<FLidarPointCloudSceneProxyWrapper, ESPMode::ThreadSafe> SceneProxyWrapperShared = SceneProxyWrapper.Pin())
{
FScopeLock Lock(&ProxyLock);
Get().RegisteredProxies.Emplace(Component, SceneProxyWrapper);
Get().ForceProcessLOD();
}
}
}
void FLidarPointCloudLODManager::RefreshLOD()
{
Get().ForceProcessLOD();
}
int64 FLidarPointCloudLODManager::ProcessLOD(const TArray<FLidarPointCloudLODManager::FRegisteredProxy>& InRegisteredProxies, const float CurrentTime, const uint32 PointBudget, const TArray<FLidarPointCloudClippingVolumeParams>& ClippingVolumes)
{
struct FSelectionFilterParams
{
float MinScreenSize;
uint32 NumNodes;
};
static TArray<FSelectionFilterParams> SelectionFilterParams;
{
const int32 DeltaSize = InRegisteredProxies.Num() - SelectionFilterParams.Num();
if (DeltaSize > 0)
{
SelectionFilterParams.AddZeroed(DeltaSize);
}
}
const bool bUseRayTracing = GetDefault<ULidarPointCloudSettings>()->bEnableLidarRayTracing;
uint32 TotalPointsSelected = 0;
int64 NewNumPointsInFrustum = 0;
TArray<TArray<FLidarPointCloudTraversalOctreeNode*>> SelectedNodesData;
// Node selection
{
SCOPE_CYCLE_COUNTER(STAT_NodeSelection);
const float ScreenCenterImportance = CVarLidarScreenCenterImportance.GetValueOnAnyThread();
int32 NumSelectedNodes = 0;
TArray<FLidarPointCloudTraversalOctreeNodeSizeData> 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<FLidarPointCloudSceneProxyWrapper, ESPMode::ThreadSafe> 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<float> 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<FLidarPointCloudProxyUpdateData> ProxyUpdateData;
// Holds a per-octree list of nodes to stream-in or extend
TMultiMap<FLidarPointCloudOctree*, TArray<FLidarPointCloudOctreeNode*>> OctreeStreamingMap;
// Process Nodes
{
SCOPE_CYCLE_COUNTER(STAT_NodeProcessing);
const bool bUseStaticBuffers = GetDefault<ULidarPointCloudSettings>()->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<float> LocalLevelWeights;
TArray<float>* 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<FLidarPointCloudOctreeNode*>& 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<FLidarPointCloudOctree*, TArray<FLidarPointCloudOctreeNode*>>& 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<ULidarPointCloudSettings>()->bUseRenderDataSmoothing;
const float RenderDataSmoothingMaxFrametime = GetDefault<ULidarPointCloudSettings>()->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<FLidarPointCloudSceneProxyWrapper, ESPMode::ThreadSafe> 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<ULidarPointCloudSettings>()->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<FLidarPointCloudSceneProxyWrapper, ESPMode::ThreadSafe> 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<FViewportClient> 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<FLidarPointCloudClippingVolumeParams> FLidarPointCloudLODManager::GetClippingVolumes() const
{
TArray<FLidarPointCloudClippingVolumeParams> ClippingVolumes;
TArray<UWorld*> 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<ALidarClippingVolume> 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<ULidarPointCloudComponent> Component, TWeakPtr<FLidarPointCloudSceneProxyWrapper, ESPMode::ThreadSafe> 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);
}