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

1937 lines
62 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LidarPointCloud.h"
#include "Framework/Application/SlateApplication.h"
#include "LidarPointCloudShared.h"
#include "LidarPointCloudActor.h"
#include "LidarPointCloudComponent.h"
#include "IO/LidarPointCloudFileIO.h"
#include "Async/Async.h"
#include "Async/Future.h"
#include "Serialization/CustomVersion.h"
#include "Misc/ScopeTryLock.h"
#include "Misc/ScopedSlowTask.h"
#include "Engine/Engine.h"
#include "LatentActions.h"
#include "PhysicsEngine/BodySetup.h"
#include "Framework/Notifications/NotificationManager.h"
#include "EngineUtils.h"
#include "Components/BrushComponent.h"
#include "UObject/AssetRegistryTagsContext.h"
#include "UObject/GCObjectScopeGuard.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "UObject/ObjectSaveContext.h"
#include "UObject/UObjectGlobals.h"
#if WITH_EDITOR
#include "IContentBrowserSingleton.h"
#include "ContentBrowserModule.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Styling/SlateStyleRegistry.h"
#include "Misc/MessageDialog.h"
#endif
#define IS_PROPERTY(Name) PropertyChangedEvent.MemberProperty->GetName().Equals(#Name)
const FGuid ULidarPointCloud::PointCloudFileGUID('P', 'C', 'P', 'F');
const int32 ULidarPointCloud::PointCloudFileVersion(20);
FCustomVersionRegistration PCPFileVersion(ULidarPointCloud::PointCloudFileGUID, ULidarPointCloud::PointCloudFileVersion, TEXT("LiDAR Point Cloud File Version"));
#define LOCTEXT_NAMESPACE "LidarPointCloud"
#if WITH_EDITOR
#include "PackageTools.h"
/* Registers for the package modified callback to catch point clouds that have been saved and automatically reloads the asset to release the bulk data */
class FLidarPackageReloader : public FTickableGameObject
{
TArray<TWeakObjectPtr<UPackage>> PackagesToReload;
public:
FLidarPackageReloader() { UPackage::PackageSavedWithContextEvent.AddRaw(this, &FLidarPackageReloader::OnPackageSaved); }
virtual ~FLidarPackageReloader() { UPackage::PackageSavedWithContextEvent.RemoveAll(this); }
virtual TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(LidarPackageReloader, STATGROUP_Tickables); }
virtual ETickableTickType GetTickableTickType() const override { return ETickableTickType::Always; }
virtual bool IsTickableInEditor() const override { return true; }
virtual void Tick(float DeltaTime) override
{
TArray<UPackage*> TopLevelPackages;
for (TWeakObjectPtr<UPackage> Package : PackagesToReload)
{
if (UPackage* PackagePtr = Package.Get())
{
TopLevelPackages.Add(PackagePtr);
}
}
if (TopLevelPackages.Num() > 0)
{
UPackageTools::ReloadPackages(TopLevelPackages);
}
PackagesToReload.Reset();
}
private:
void OnPackageSaved(const FString& Filename, UPackage* Package, FObjectPostSaveContext ObjectSaveContext)
{
if (GetDefault<ULidarPointCloudSettings>()->bReleaseAssetAfterSaving)
{
if(Package->GetLinker())
{
if (Cast<ULidarPointCloud>(Package->FindAssetInPackage()))
{
PackagesToReload.Add(Package);
}
}
}
}
};
#endif
/////////////////////////////////////////////////
// FPointCloudLatentAction
class FPointCloudSimpleLatentAction : public FPendingLatentAction
{
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
public:
bool bComplete;
FPointCloudSimpleLatentAction(const FLatentActionInfo& LatentInfo)
: ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
, bComplete(false)
{
}
virtual void UpdateOperation(FLatentResponse& Response) override
{
Response.FinishAndTriggerIf(bComplete, ExecutionFunction, OutputLink, CallbackTarget);
}
};
class FPointCloudLatentAction : public FPendingLatentAction
{
public:
FName ExecutionFunction;
int32 OutputLink;
FWeakObjectPtr CallbackTarget;
ELidarPointCloudAsyncMode* Mode = nullptr;
FPointCloudLatentAction(const FLatentActionInfo& LatentInfo, ELidarPointCloudAsyncMode& Mode)
: ExecutionFunction(LatentInfo.ExecutionFunction)
, OutputLink(LatentInfo.Linkage)
, CallbackTarget(LatentInfo.CallbackTarget)
, Mode(&Mode)
{
}
virtual void UpdateOperation(FLatentResponse& Response) override
{
if (*Mode != ELidarPointCloudAsyncMode::Progress)
{
Response.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget);
}
else
{
Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);
}
}
};
/////////////////////////////////////////////////
// FLidarPointCloudNotification
/** Wrapper around a NotificationItem to make the notification handling more centralized */
class FLidarPointCloudNotification
{
/** Stores the pointer to the actual notification item */
TSharedPtr<SNotificationItem> NotificationItem;
FString CurrentText;
int8 CurrentProgress;
public:
FLidarPointCloudNotification(const FString& Text, TWeakObjectPtr<ULidarPointCloud> Owner, FThreadSafeBool* bCancelPtr, const FString& Icon)
: CurrentText(Text)
, CurrentProgress(-1)
{
#if WITH_EDITOR
if (GIsEditor)
{
// Build the notification widget
FNotificationInfo Info(FText::FromString(CurrentText));
Info.bFireAndForget = false;
Info.Image = FSlateStyleRegistry::FindSlateStyle("LidarPointCloudStyle")->GetBrush(*Icon);
if (ULidarPointCloud* OwnerPtr = Owner.Get())
{
const FSoftObjectPath SoftObjectPath(OwnerPtr);
Info.Hyperlink = FSimpleDelegate::CreateLambda([SoftObjectPath] {
// Select the cloud in Content Browser when the hyperlink is clicked
TArray<FAssetData> AssetData;
AssetData.Add(FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get().GetAssetByObjectPath(SoftObjectPath.GetWithoutSubPath()));
FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser").Get().SyncBrowserToAssets(AssetData);
});
Info.HyperlinkText = FText::FromString(FPaths::GetBaseFilename(SoftObjectPath.ToString()));
}
if (bCancelPtr)
{
Info.ButtonDetails.Emplace(
LOCTEXT("OpCancel", "Cancel"),
LOCTEXT("OpCancelToolTip", "Cancels the point cloud operation in progress."),
FSimpleDelegate::CreateLambda([bCancelPtr] { *bCancelPtr = true; })
);
}
NotificationItem = FSlateNotificationManager::Get().AddNotification(Info);
if (NotificationItem.IsValid())
{
NotificationItem->SetCompletionState(SNotificationItem::CS_Pending);
}
}
#endif
}
~FLidarPointCloudNotification()
{
Close(false);
}
void SetText(const FString& Text)
{
#if WITH_EDITOR
CurrentText = Text;
UpdateStatus();
#endif
}
void SetProgress(int8 Progress)
{
#if WITH_EDITOR
CurrentProgress = Progress;
UpdateStatus();
#endif
}
void SetTextWithProgress(const FString& Text, int8 Progress)
{
#if WITH_EDITOR
CurrentText = Text;
CurrentProgress = Progress;
UpdateStatus();
#endif
}
void Close(bool bSuccess)
{
#if WITH_EDITOR
if (NotificationItem.IsValid())
{
// Do not use fadeout if the engine is shutting down
if(FSlateApplication::IsInitialized())
{
CurrentText.Append(bSuccess ? " Complete" : " Failed");
CurrentProgress = -1;
UpdateStatus();
NotificationItem->SetCompletionState(bSuccess ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail);
NotificationItem->ExpireAndFadeout();
}
NotificationItem.Reset();
}
#endif
}
private:
void UpdateStatus()
{
#if WITH_EDITOR
if (NotificationItem.IsValid())
{
if (IsInGameThread())
{
FString Message;
if (CurrentProgress >= 0)
{
Message = FString::Printf(TEXT("%s: %d%%"), *CurrentText, CurrentProgress);
}
else
{
Message = FString::Printf(TEXT("%s"), *CurrentText);
}
NotificationItem->SetText(FText::FromString(Message));
}
else
{
AsyncTask(ENamedThreads::GameThread, [this] { UpdateStatus(); });
}
}
#endif
}
};
TSharedRef<FLidarPointCloudNotification, ESPMode::ThreadSafe> ULidarPointCloud::FLidarPointCloudNotificationManager::Create(const FString& Text, FThreadSafeBool* bCancelPtr, const FString& Icon)
{
return Notifications[Notifications.Add(MakeShared<FLidarPointCloudNotification, ESPMode::ThreadSafe>(Text, Owner, bCancelPtr, Icon))];
}
void ULidarPointCloud::FLidarPointCloudNotificationManager::CloseAll()
{
for (TSharedRef<FLidarPointCloudNotification, ESPMode::ThreadSafe> Notification : Notifications)
{
Notification->Close(false);
}
Notifications.Empty();
}
/////////////////////////////////////////////////
// ULidarPointCloud
ULidarPointCloud::ULidarPointCloud()
: MaxCollisionError(100)
, NormalsQuality(40)
, NormalsNoiseTolerance(1)
, OriginalCoordinates(FVector::ZeroVector)
, bOptimizedForDynamicData(false)
, Octree(this)
, LocationOffset(FVector::ZeroVector)
, Notifications(this)
, BodySetup(nullptr)
, NewBodySetup(nullptr)
, bCollisionBuildInProgress(false)
{
// Make sure we are transactional to allow undo redo
this->SetFlags(RF_Transactional);
#if WITH_EDITOR
static FLidarPackageReloader LidarPackageReloader;
#endif
}
void ULidarPointCloud::Serialize(FArchive& Ar)
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("ULidarPointCloud::Serialize"), STAT_PointCLoud_Serialize, STATGROUP_LoadTime);
Ar.UsingCustomVersion(PointCloudFileGUID);
Super::Serialize(Ar);
int32 Version = Ar.CustomVer(PointCloudFileGUID);
if (Version > 13)
{
Ar << BodySetup;
if (Ar.IsCountingMemory())
{
if (BodySetup)
{
BodySetup->Serialize(Ar);
}
}
}
// Make sure to serialize only actual data
if (Ar.ShouldSkipBulkData() || Ar.IsObjectReferenceCollector() || !Ar.IsPersistent())
{
return;
}
ULidarPointCloudFileIO::SerializeImportSettings(Ar, ImportSettings);
// Do not save the Octree, if in the middle of processing or the access to the data is blocked
{
FScopeTryLock LockProcessing(&ProcessingLock);
FScopeTryLock LockOctree(&Octree.DataLock);
bool bValidOctree = LockProcessing.IsLocked() && LockOctree.IsLocked();
Ar << bValidOctree;
if (bValidOctree)
{
Ar << Octree;
}
}
}
void ULidarPointCloud::PostLoad()
{
Super::PostLoad();
InitializeCollisionRendering();
}
void ULidarPointCloud::GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const
{
PRAGMA_DISABLE_DEPRECATION_WARNINGS;
Super::GetAssetRegistryTags(OutTags);
PRAGMA_ENABLE_DEPRECATION_WARNINGS;
}
void ULidarPointCloud::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const
{
Context.AddTag(FAssetRegistryTag("PointCount", PointCloudAssetRegistryCache.PointCount, FAssetRegistryTag::TT_Numerical));
Context.AddTag(FAssetRegistryTag("ApproxSize", PointCloudAssetRegistryCache.ApproxSize, FAssetRegistryTag::TT_Dimensional));
Super::GetAssetRegistryTags(Context);
}
void ULidarPointCloud::BeginDestroy()
{
Super::BeginDestroy();
// Cancel async import and wait for it to exit
bAsyncCancelled = true;
FScopeLock LockImport(&ProcessingLock);
// Hide any notifications, if still present
Notifications.CloseAll();
// Wait for ongoing data access to finish
FScopeLock LockOctree(&Octree.DataLock);
// Release and destroy any collision rendering data, if present
ReleaseCollisionRendering(true);
}
void ULidarPointCloud::PreSave(FObjectPreSaveContext ObjectSaveContext)
{
Super::PreSave(ObjectSaveContext);
OnPreSaveCleanupEvent.Broadcast();
}
#if WITH_EDITOR
void ULidarPointCloud::ClearAllCachedCookedPlatformData()
{
if (GetDefault<ULidarPointCloudSettings>()->bReleaseAssetAfterCooking)
{
Octree.ReleaseAllNodes(true);
}
}
void ULidarPointCloud::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
if (PropertyChangedEvent.MemberProperty)
{
if (IS_PROPERTY(SourcePath))
{
SetSourcePath(SourcePath.FilePath);
}
if (IS_PROPERTY(bOptimizedForDynamicData))
{
SetOptimizedForDynamicData(bOptimizedForDynamicData);
}
if (IS_PROPERTY(MaxCollisionError))
{
if (MaxCollisionError < Octree.GetEstimatedPointSpacing())
{
FMessageDialog::Open(EAppMsgType::Type::Ok, FText::FromString(FString::Printf(TEXT("Average point spacing is estimated to be around %f cm.\nSetting accuracy close to or lower than that value may result in collision holes."), FMath::RoundToFloat(Octree.GetEstimatedPointSpacing() * 100) * 0.01f)));
}
}
}
Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif
int32 ULidarPointCloud::GetDataSize() const
{
const int64 OctreeSize = Octree.GetAllocatedSize();
const int64 CollisionSize = Octree.GetCollisionData()->Indices.GetAllocatedSize() + Octree.GetCollisionData()->Vertices.GetAllocatedSize();
return (OctreeSize + CollisionSize) >> 20;
}
void ULidarPointCloud::RefreshBounds()
{
Octree.RefreshBounds();
}
bool ULidarPointCloud::HasCollisionData() const
{
return Octree.HasCollisionData();
}
int32 ULidarPointCloud::GetColliderPolys() const
{
return Octree.HasCollisionData() ? Octree.GetCollisionData()->Indices.Num() : 0;
}
void ULidarPointCloud::RefreshRendering()
{
Octree.MarkRenderDataDirty();
OnPointCloudRebuiltEvent.Broadcast();
}
void ULidarPointCloud::GetPoints(TArray64<FLidarPointCloudPoint*>& Points, int64 StartIndex /*= 0*/, int64 Count /*= -1*/)
{
GetPoints_Internal(Points, StartIndex, Count);
}
void ULidarPointCloud::GetPoints(TArray<FLidarPointCloudPoint*>& Points, int64 StartIndex /*= 0*/, int64 Count /*= -1*/)
{
GetPoints_Internal(Points, StartIndex, Count);
}
void ULidarPointCloud::GetPointsInSphere(TArray64<FLidarPointCloudPoint*>& SelectedPoints, const FSphere& Sphere, const bool& bVisibleOnly)
{
GetPointsInSphere_Internal(SelectedPoints, Sphere, bVisibleOnly);
}
void ULidarPointCloud::GetPointsInSphere(TArray<FLidarPointCloudPoint*>& SelectedPoints, const FSphere& Sphere, const bool& bVisibleOnly)
{
GetPointsInSphere_Internal(SelectedPoints, Sphere, bVisibleOnly);
}
void ULidarPointCloud::GetPointsInBox(TArray64<FLidarPointCloudPoint*>& SelectedPoints, const FBox& Box, const bool& bVisibleOnly)
{
GetPointsInBox_Internal(SelectedPoints, Box, bVisibleOnly);
}
void ULidarPointCloud::GetPointsInBox(TArray<FLidarPointCloudPoint*>& SelectedPoints, const FBox& Box, const bool& bVisibleOnly)
{
GetPointsInBox_Internal(SelectedPoints, Box, bVisibleOnly);
}
void ULidarPointCloud::GetPointsInConvexVolume(TArray64<FLidarPointCloudPoint*>& SelectedPoints, const FConvexVolume& ConvexVolume, const bool& bVisibleOnly)
{
GetPointsInConvexVolume_Internal(SelectedPoints, ConvexVolume, bVisibleOnly);
}
void ULidarPointCloud::GetPointsInConvexVolume(TArray<FLidarPointCloudPoint*>& SelectedPoints, const FConvexVolume& ConvexVolume, const bool& bVisibleOnly)
{
GetPointsInConvexVolume_Internal(SelectedPoints, ConvexVolume, bVisibleOnly);
}
void ULidarPointCloud::GetPointsInFrustum(TArray64<FLidarPointCloudPoint*>& SelectedPoints, const FConvexVolume& Frustum, const bool& bVisibleOnly)
{
GetPointsInConvexVolume_Internal(SelectedPoints, Frustum, bVisibleOnly);
}
void ULidarPointCloud::GetPointsInFrustum(TArray<FLidarPointCloudPoint*>& SelectedPoints, const FConvexVolume& Frustum, const bool& bVisibleOnly)
{
GetPointsInConvexVolume_Internal(SelectedPoints, Frustum, bVisibleOnly);
}
TArray<FLidarPointCloudPoint> ULidarPointCloud::GetPointsAsCopies(bool bReturnWorldSpace, int32 StartIndex, int32 Count) const
{
TArray<FLidarPointCloudPoint> Points;
GetPointsAsCopies(Points, bReturnWorldSpace, StartIndex, Count);
return Points;
}
void ULidarPointCloud::GetPointsAsCopies(TArray64<FLidarPointCloudPoint>& Points, bool bReturnWorldSpace, int64 StartIndex /*= 0*/, int64 Count /*= -1*/) const
{
GetPointsAsCopies_Internal(Points, bReturnWorldSpace, StartIndex, Count);
}
void ULidarPointCloud::GetPointsAsCopies(TArray<FLidarPointCloudPoint>& Points, bool bReturnWorldSpace, int64 StartIndex /*= 0*/, int64 Count /*= -1*/) const
{
GetPointsAsCopies_Internal(Points, bReturnWorldSpace, StartIndex, Count);
}
TArray<FLidarPointCloudPoint> ULidarPointCloud::GetPointsInSphereAsCopies(FVector Center, float Radius, bool bVisibleOnly, bool bReturnWorldSpace)
{
TArray<FLidarPointCloudPoint> Points;
GetPointsInSphereAsCopies(Points, FSphere(Center, Radius), bVisibleOnly, bReturnWorldSpace);
return Points;
}
void ULidarPointCloud::GetPointsInSphereAsCopies(TArray64<FLidarPointCloudPoint>& SelectedPoints, const FSphere& Sphere, const bool& bVisibleOnly, bool bReturnWorldSpace) const
{
GetPointsInSphereAsCopies_Internal(SelectedPoints, Sphere, bVisibleOnly, bReturnWorldSpace);
}
void ULidarPointCloud::GetPointsInSphereAsCopies(TArray<FLidarPointCloudPoint>& SelectedPoints, const FSphere& Sphere, const bool& bVisibleOnly, bool bReturnWorldSpace) const
{
GetPointsInSphereAsCopies_Internal(SelectedPoints, Sphere, bVisibleOnly, bReturnWorldSpace);
}
TArray<FLidarPointCloudPoint> ULidarPointCloud::GetPointsInBoxAsCopies(FVector Center, FVector Extent, bool bVisibleOnly, bool bReturnWorldSpace)
{
TArray<FLidarPointCloudPoint> Points;
GetPointsInBoxAsCopies(Points, FBox(Center - Extent, Center + Extent), bVisibleOnly, bReturnWorldSpace);
return Points;
}
void ULidarPointCloud::GetPointsInBoxAsCopies(TArray64<FLidarPointCloudPoint>& SelectedPoints, const FBox& Box, const bool& bVisibleOnly, bool bReturnWorldSpace) const
{
GetPointsInBoxAsCopies_Internal(SelectedPoints, Box, bVisibleOnly, bReturnWorldSpace);
}
void ULidarPointCloud::GetPointsInBoxAsCopies(TArray<FLidarPointCloudPoint>& SelectedPoints, const FBox& Box, const bool& bVisibleOnly, bool bReturnWorldSpace) const
{
GetPointsInBoxAsCopies_Internal(SelectedPoints, Box, bVisibleOnly, bReturnWorldSpace);
}
bool ULidarPointCloud::LineTraceSingle(FVector Origin, FVector Direction, float Radius, bool bVisibleOnly, FLidarPointCloudPoint& PointHit)
{
FLidarPointCloudPoint* Point = LineTraceSingle(FLidarPointCloudRay((FVector3f)Origin, (FVector3f)Direction), Radius, bVisibleOnly);
if (Point)
{
PointHit = *Point;
return true;
}
return false;
}
#if WITH_EDITOR
void ULidarPointCloud::SelectByConvexVolume(FConvexVolume ConvexVolume, bool bAdditive, bool bApplyLocationOffset, bool bVisibleOnly)
{
if(bApplyLocationOffset)
{
for(FPlane& Plane : ConvexVolume.Planes)
{
Plane = Plane.TranslateBy(-LocationOffset);
}
ConvexVolume.Init();
}
Octree.SelectByConvexVolume(ConvexVolume, bAdditive, bVisibleOnly);
}
void ULidarPointCloud::SelectBySphere(FSphere Sphere, bool bAdditive, bool bApplyLocationOffset, bool bVisibleOnly)
{
if(bApplyLocationOffset)
{
Sphere.Center -= LocationOffset;
}
Octree.SelectBySphere(Sphere, bAdditive, bVisibleOnly);
}
void ULidarPointCloud::HideSelected()
{
Octree.HideSelected();
}
void ULidarPointCloud::DeleteSelected()
{
Octree.DeleteSelected();
}
void ULidarPointCloud::InvertSelection()
{
Octree.InvertSelection();
}
int64 ULidarPointCloud::NumSelectedPoints() const
{
return Octree.NumSelectedPoints();
}
bool ULidarPointCloud::HasSelectedPoints() const
{
return Octree.HasSelectedPoints();
}
template <typename T>
void ULidarPointCloud::GetSelectedPointsAsCopies_Internal(TArray<FLidarPointCloudPoint, T>& SelectedPoints, FTransform Transform) const
{
Transform.AddToTranslation(LocationOffset);
Octree.GetSelectedPointsAsCopies(SelectedPoints, Transform);
}
void ULidarPointCloud::GetSelectedPointsAsCopies(TArray<FLidarPointCloudPoint>& SelectedPoints, const FTransform& Transform) const
{
GetSelectedPointsAsCopies_Internal(SelectedPoints, Transform);
}
void ULidarPointCloud::GetSelectedPointsAsCopies(TArray64<FLidarPointCloudPoint>& SelectedPoints, const FTransform& Transform) const
{
GetSelectedPointsAsCopies_Internal(SelectedPoints, Transform);
}
void ULidarPointCloud::CalculateNormalsForSelection()
{
TArray64<FLidarPointCloudPoint*>* SelectedPoints = new TArray64<FLidarPointCloudPoint*>();
Octree.GetSelectedPoints(*SelectedPoints);
CalculateNormals(SelectedPoints, [SelectedPoints]
{
delete SelectedPoints;
});
}
void ULidarPointCloud::ClearSelection()
{
Octree.ClearSelection();
}
void ULidarPointCloud::BuildStaticMeshBuffersForSelection(float CellSize, LidarPointCloudMeshing::FMeshBuffers* OutMeshBuffers, const FTransform& Transform)
{
Octree.BuildStaticMeshBuffersForSelection(CellSize, OutMeshBuffers, Transform);
}
#endif
void ULidarPointCloud::SetSourcePath(const FString& NewSourcePath)
{
SourcePath.FilePath = NewSourcePath;
if (FPaths::FileExists(SourcePath.FilePath))
{
if (FPaths::IsRelative(SourcePath.FilePath))
{
SourcePath.FilePath = FPaths::ConvertRelativePathToFull(SourcePath.FilePath);
}
// Generate new ImportSettings if the source path has changed
ImportSettings = ULidarPointCloudFileIO::GetImportSettings(SourcePath.FilePath);
}
else
{
// Invalidate ImportSettings if the source path is invalid too
ImportSettings = nullptr;
}
}
void ULidarPointCloud::SetOptimizedForDynamicData(bool bNewOptimizedForDynamicData)
{
#if WITH_EDITOR
FScopedSlowTask ProgressDialog(1, LOCTEXT("Optimize", "Optimizing Point Clouds..."));
ProgressDialog.MakeDialog();
#endif
bOptimizedForDynamicData = bNewOptimizedForDynamicData;
if (bOptimizedForDynamicData)
{
Octree.OptimizeForDynamicData();
}
else
{
Octree.OptimizeForStaticData();
}
}
void ULidarPointCloud::SetOptimalCollisionError()
{
MaxCollisionError = FMath::CeilToInt(Octree.GetEstimatedPointSpacing() * 300) * 0.01f;
}
void ULidarPointCloud::BuildCollisionWithCallback(UObject* WorldContextObject, FLatentActionInfo LatentInfo, bool& bSuccess)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
if (LatentActionManager.FindExistingAction<FPointCloudLatentAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
{
FPointCloudSimpleLatentAction* CompletionAction = new FPointCloudSimpleLatentAction(LatentInfo);
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, CompletionAction);
BuildCollision([CompletionAction, &bSuccess](bool bCompletedSuccessfully)
{
bSuccess = bCompletedSuccessfully;
CompletionAction->bComplete = true;
});
}
}
}
void ULidarPointCloud::BuildCollision(TFunction<void(bool)> CompletionCallback)
{
if (bCollisionBuildInProgress)
{
PC_ERROR("Another collision operation already in progress.");
if(CompletionCallback)
{
CompletionCallback(false);
}
return;
}
TSharedRef<FLidarPointCloudNotification, ESPMode::ThreadSafe> Notification = Notifications.Create("Building Collision", nullptr, "LidarPointCloudEditor.BuildCollision");
bCollisionBuildInProgress = true;
MarkPackageDirty();
NewBodySetup = NewObject<UBodySetup>(this);
NewBodySetup->BodySetupGuid = FGuid::NewGuid();
NewBodySetup->CollisionTraceFlag = CTF_UseComplexAsSimple;
NewBodySetup->bHasCookedCollisionData = true;
TWeakObjectPtr<ULidarPointCloud> WeakThis = this;
Async(EAsyncExecution::Thread, [WeakThis = MoveTemp(WeakThis), Notification, CompletionCallback = MoveTemp(CompletionCallback)]() mutable
{
if (ULidarPointCloud* RawPtr = WeakThis.Get())
{
FGCObjectScopeGuard Guard(RawPtr);
RawPtr->Octree.BuildCollision(RawPtr->MaxCollisionError, true);
FBenchmarkTimer::Reset();
RawPtr->NewBodySetup->CreatePhysicsMeshes();
AsyncTask(ENamedThreads::GameThread, [WeakThis = MoveTemp(WeakThis), Notification, CompletionCallback = MoveTemp(CompletionCallback)]
{
if (ULidarPointCloud* RawPtr = WeakThis.Get())
{
RawPtr->FinishPhysicsAsyncCook(true, Notification);
if(CompletionCallback)
{
CompletionCallback(true);
}
}
});
}
});
}
void ULidarPointCloud::RemoveCollision()
{
if (bCollisionBuildInProgress)
{
PC_ERROR("Another collision operation already in progress.");
return;
}
bCollisionBuildInProgress = true;
MarkPackageDirty();
Octree.RemoveCollision();
BodySetup = NewObject<UBodySetup>(this);
ReleaseCollisionRendering(false);
InitializeCollisionRendering();
OnPointCloudUpdateCollisionEvent.Broadcast();
bCollisionBuildInProgress = false;
}
void ULidarPointCloud::BuildStaticMeshBuffers(float CellSize, LidarPointCloudMeshing::FMeshBuffers* OutMeshBuffers, const FTransform& Transform)
{
Octree.BuildStaticMeshBuffers(CellSize, OutMeshBuffers, Transform);
}
void ULidarPointCloud::SetLocationOffset(FVector Offset)
{
LocationOffset = Offset;
MarkPackageDirty();
OnPointCloudRebuiltEvent.Broadcast();
}
void ULidarPointCloud::Reimport(const FLidarPointCloudAsyncParameters& AsyncParameters)
{
if (FPaths::FileExists(SourcePath.FilePath))
{
FScopeTryLock Lock(&ProcessingLock);
if (!Lock.IsLocked())
{
PC_ERROR("Cannot reimport the asset - data is currently being used.");
return;
}
bAsyncCancelled = false;
TSharedRef<FLidarPointCloudNotification, ESPMode::ThreadSafe> Notification = Notifications.Create("Importing Point Cloud", &bAsyncCancelled);
const bool bCenter = GetDefault<ULidarPointCloudSettings>()->bAutoCenterOnImport;
// Reset the data optimization flag on import
bOptimizedForDynamicData = false;
// The actual import function to be executed
auto ImportFunction = [this, Notification, AsyncParameters, bCenter]
{
// This will take over the lock
FScopeLock Lock(&ProcessingLock);
bool bSuccess = false;
// Wait for rendering to complete before proceeding and lock the access to the data
FScopeLock DataLock(&Octree.DataLock);
FLidarPointCloudImportResults ImportResults;
// If the file supports concurrent insertion, we can stream the data in chunks and perform async insertion at the same time
if (ULidarPointCloudFileIO::FileSupportsConcurrentInsertion(SourcePath.FilePath))
{
PC_LOG("Using Concurrent Insertion");
ImportResults = FLidarPointCloudImportResults(&bAsyncCancelled,
[this, Notification, AsyncParameters](float Progress)
{
Notification->SetProgress(100.0f * Progress);
if (AsyncParameters.ProgressCallback)
{
AsyncParameters.ProgressCallback(100.0f * Progress);
}
},
[this](const FBox& Bounds, FVector InOriginalCoordinates)
{
Initialize(Bounds.ShiftBy(-InOriginalCoordinates));
},
[this](TArray64<FLidarPointCloudPoint>* Points)
{
Octree.InsertPoints(Points->GetData(), Points->Num(), GetDefault<ULidarPointCloudSettings>()->DuplicateHandling, false, (FVector3f)-LocationOffset);
});
bSuccess = ULidarPointCloudFileIO::Import(SourcePath.FilePath, ImportSettings, ImportResults);
}
else
{
ImportResults = FLidarPointCloudImportResults(&bAsyncCancelled, [this, Notification, AsyncParameters](float Progress)
{
Notification->SetProgress(50.0f * Progress);
if (AsyncParameters.ProgressCallback)
{
AsyncParameters.ProgressCallback(50.0f * Progress);
}
});
if (ULidarPointCloudFileIO::Import(SourcePath.FilePath, ImportSettings, ImportResults))
{
// Re-initialize the Octree
Initialize(ImportResults.Bounds);
FScopeBenchmarkTimer BenchmarkTimer("Octree Build-Up");
bSuccess = InsertPoints_NoLock(ImportResults.Points.GetData(), ImportResults.Points.Num(), GetDefault<ULidarPointCloudSettings>()->DuplicateHandling, false, -LocationOffset, &bAsyncCancelled, [this, Notification, AsyncParameters](float Progress)
{
Notification->SetProgress(50.0f + 50.0f * Progress);
if (AsyncParameters.ProgressCallback)
{
AsyncParameters.ProgressCallback(50.0f + 50.0f * Progress);
}
});
if (!bSuccess)
{
BenchmarkTimer.bActive = false;
}
}
}
if (bSuccess)
{
ClassificationsImported = ImportResults.ClassificationsImported;
RefreshBounds();
OriginalCoordinates = LocationOffset + ImportResults.OriginalCoordinates;
// Show the cloud at its original location, if selected
LocationOffset = bCenter ? FVector::ZeroVector : OriginalCoordinates;
// Adjust default max collision error
SetOptimalCollisionError();
}
else
{
Octree.Empty(true);
OriginalCoordinates = FVector::ZeroVector;
LocationOffset = FVector::ZeroVector;
// Update PointCloudAssetRegistryCache
PointCloudAssetRegistryCache.PointCount = FString::FromInt(Octree.GetNumPoints());
}
// Only process those if not being destroyed
if (!HasAnyFlags(RF_BeginDestroyed))
{
auto PostFunction = [this, Notification, bSuccess]() {
MarkPackageDirty();
Notification->Close(bSuccess);
OnPointCloudRebuiltEvent.Broadcast();
if (bSuccess)
{
FScopeLock Lock(&ProcessingLock);
if (GetDefault<ULidarPointCloudSettings>()->bAutoCalculateNormalsOnImport)
{
CalculateNormals(nullptr, nullptr);
}
if (GetDefault<ULidarPointCloudSettings>()->bAutoBuildCollisionOnImport)
{
BuildCollision();
}
}
};
// Make sure the call is executed on the correct thread if using async
if (IsInGameThread())
{
PostFunction();
}
else
{
AsyncTask(ENamedThreads::GameThread, MoveTemp(PostFunction));
}
}
if (AsyncParameters.CompletionCallback)
{
AsyncParameters.CompletionCallback(bSuccess);
}
if(!bSuccess)
{
PC_ERROR("Point Cloud importing failed or cancelled.");
}
};
if (AsyncParameters.bUseAsync)
{
Async(EAsyncExecution::Thread, MoveTemp(ImportFunction));
}
else
{
ImportFunction();
}
}
else
{
PC_ERROR("Reimport failed, provided source path '%s' could not be found.", *SourcePath.FilePath);
if (AsyncParameters.CompletionCallback)
{
AsyncParameters.CompletionCallback(false);
}
}
}
void ULidarPointCloud::Reimport(UObject* WorldContextObject, bool bUseAsync, FLatentActionInfo LatentInfo, ELidarPointCloudAsyncMode& AsyncMode, float& Progress)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
if (LatentActionManager.FindExistingAction<FPointCloudLatentAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
{
AsyncMode = ELidarPointCloudAsyncMode::Progress;
FPointCloudLatentAction* CompletionAction = new FPointCloudLatentAction(LatentInfo, AsyncMode);
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, CompletionAction);
Reimport(FLidarPointCloudAsyncParameters(bUseAsync,
[&Progress, &AsyncMode](float InProgress)
{
Progress = InProgress;
},
[&AsyncMode](bool bSuccess)
{
AsyncMode = bSuccess ? ELidarPointCloudAsyncMode::Success : ELidarPointCloudAsyncMode::Failure;
}));
}
}
}
bool ULidarPointCloud::Export(const FString& Filename)
{
return ULidarPointCloudFileIO::Export(Filename, this);
}
void ULidarPointCloud::InsertPoint(const FLidarPointCloudPoint& Point, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation)
{
FScopeLock Lock(&Octree.DataLock);
Octree.InsertPoint(&Point, DuplicateHandling, bRefreshPointsBounds, (FVector3f)Translation);
// Update PointCloudAssetRegistryCache
PointCloudAssetRegistryCache.PointCount = FString::FromInt(Octree.GetNumPoints());
}
bool ULidarPointCloud::InsertPoints(FLidarPointCloudPoint* InPoints, const int64& Count, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation, FThreadSafeBool* bCanceled /*= nullptr*/, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
return InsertPoints_Internal(InPoints, Count, DuplicateHandling, bRefreshPointsBounds, Translation, bCanceled, MoveTemp(ProgressCallback));
}
bool ULidarPointCloud::InsertPoints(const FLidarPointCloudPoint* InPoints, const int64& Count, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation, FThreadSafeBool* bCanceled /*= nullptr*/, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
return InsertPoints_Internal(InPoints, Count, DuplicateHandling, bRefreshPointsBounds, Translation, bCanceled, MoveTemp(ProgressCallback));
}
bool ULidarPointCloud::InsertPoints(FLidarPointCloudPoint** InPoints, const int64& Count, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation, FThreadSafeBool* bCanceled /*= nullptr*/, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
return InsertPoints_Internal(InPoints, Count, DuplicateHandling, bRefreshPointsBounds, Translation, bCanceled, MoveTemp(ProgressCallback));
}
bool ULidarPointCloud::InsertPoints_NoLock(FLidarPointCloudPoint* InPoints, const int64& Count, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation, FThreadSafeBool* bCanceled /*= nullptr*/, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
return InsertPoints_NoLock_Internal(InPoints, Count, DuplicateHandling, bRefreshPointsBounds, Translation, bCanceled, MoveTemp(ProgressCallback));
}
bool ULidarPointCloud::InsertPoints_NoLock(const FLidarPointCloudPoint* InPoints, const int64& Count, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation, FThreadSafeBool* bCanceled /*= nullptr*/, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
return InsertPoints_NoLock_Internal(InPoints, Count, DuplicateHandling, bRefreshPointsBounds, Translation, bCanceled, MoveTemp(ProgressCallback));
}
bool ULidarPointCloud::InsertPoints_NoLock(FLidarPointCloudPoint** InPoints, const int64& Count, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation, FThreadSafeBool* bCanceled /*= nullptr*/, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
return InsertPoints_NoLock_Internal(InPoints, Count, DuplicateHandling, bRefreshPointsBounds, Translation, bCanceled, MoveTemp(ProgressCallback));
}
template<typename T>
bool ULidarPointCloud::InsertPoints_NoLock_Internal(T InPoints, const int64& Count, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation, FThreadSafeBool* bCanceled, TFunction<void(float)> ProgressCallback)
{
if (bOptimizedForDynamicData)
{
Octree.InsertPoints(InPoints, Count, DuplicateHandling, bRefreshPointsBounds, (FVector3f)Translation);
if (ProgressCallback)
{
ProgressCallback(1.0);
}
return true;
}
else
{
const int32 MaxBatchSize = GetDefault<ULidarPointCloudSettings>()->MultithreadingInsertionBatchSize;
// Minimum amount of points to progress to count as 1%
int64 RefreshStatusFrequency = Count * 0.01f;
FThreadSafeCounter64 ProcessedPoints(0);
int64 TotalProcessedPoints = 0;
const int32 NumThreads = FMath::Min(FPlatformMisc::NumberOfCoresIncludingHyperthreads() - 1, (int32)(Count / MaxBatchSize) + 1);
TArray<TFuture<void>>ThreadResults;
ThreadResults.Reserve(NumThreads);
const int64 NumPointsPerThread = Count / NumThreads + 1;
FCriticalSection ProgressCallbackLock;
// Fire threads
for (int32 ThreadID = 0; ThreadID < NumThreads; ThreadID++)
{
ThreadResults.Add(Async(EAsyncExecution::Thread, [this, ThreadID, DuplicateHandling, bRefreshPointsBounds, MaxBatchSize, NumPointsPerThread, RefreshStatusFrequency, &ProcessedPoints, &TotalProcessedPoints, InPoints, Count, &ProgressCallback, &ProgressCallbackLock, &bCanceled, &Translation]
{
int64 Idx = ThreadID * NumPointsPerThread;
int64 MaxIdx = FMath::Min(Idx + NumPointsPerThread, Count);
T DataPointer = InPoints + Idx;
while (Idx < MaxIdx)
{
int32 BatchSize = FMath::Min(MaxIdx - Idx, (int64)MaxBatchSize);
Octree.InsertPoints(DataPointer, BatchSize, DuplicateHandling, bRefreshPointsBounds, (FVector3f)Translation);
if (ProgressCallback)
{
ProcessedPoints.Add(BatchSize);
if (ProcessedPoints.GetValue() > RefreshStatusFrequency)
{
FScopeLock Lock(&ProgressCallbackLock);
TotalProcessedPoints += ProcessedPoints.GetValue();
ProcessedPoints.Reset();
ProgressCallback((double)TotalProcessedPoints / Count);
}
}
if (bCanceled && *bCanceled)
{
break;
}
Idx += BatchSize;
DataPointer += BatchSize;
}
}));
}
// Sync
for (const TFuture<void>& ThreadResult : ThreadResults)
{
ThreadResult.Get();
}
// Do not attempt to touch Render Data if being destroyed
if (!HasAnyFlags(RF_BeginDestroyed))
{
// Update PointCloudAssetRegistryCache
PointCloudAssetRegistryCache.PointCount = FString::FromInt(Octree.GetNumPoints());
}
return !bCanceled || !(*bCanceled);
}
}
template<typename T>
bool ULidarPointCloud::SetData_Internal(T Points, const int64& Count, TFunction<void(float)> ProgressCallback)
{
// Lock the point cloud
FScopeLock Lock(&ProcessingLock);
// Calculate the bounds
FBox Bounds = CalculateBoundsFromPoints(Points, Count);
bool bSuccess = false;
// Only proceed if the bounds are valid
if (Bounds.IsValid)
{
// Wait for rendering to complete before proceeding and lock the access to the data
FScopeLock DataLock(&Octree.DataLock);
// Initialize the Octree
Initialize(Bounds);
bSuccess = InsertPoints_NoLock(Points, Count, GetDefault<ULidarPointCloudSettings>()->DuplicateHandling, false, -LocationOffset, nullptr, MoveTemp(ProgressCallback));
if (!bSuccess)
{
Octree.Empty(true);
}
// Only process those if not being destroyed
if (!HasAnyFlags(RF_BeginDestroyed))
{
auto PostFunction = [this, bSuccess]() {
MarkPackageDirty();
OnPointCloudRebuiltEvent.Broadcast();
};
// Make sure the call is executed on the correct thread if using async
if (IsInGameThread())
{
PostFunction();
}
else
{
AsyncTask(ENamedThreads::GameThread, MoveTemp(PostFunction));
}
}
}
if (!bSuccess)
{
PC_ERROR("Setting Point Cloud data failed.");
}
return bSuccess;
}
bool ULidarPointCloud::SetData(const FLidarPointCloudPoint* Points, const int64& Count, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
return SetData_Internal(Points, Count, MoveTemp(ProgressCallback));
}
bool ULidarPointCloud::SetData(FLidarPointCloudPoint** Points, const int64& Count, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
return SetData_Internal(Points, Count, MoveTemp(ProgressCallback));
}
void ULidarPointCloud::Merge(TArray<ULidarPointCloud*> PointCloudsToMerge, TFunction<void(void)> ProgressCallback)
{
for (int32 i = 0; i < PointCloudsToMerge.Num(); i++)
{
if (!IsValid(PointCloudsToMerge[i]) || PointCloudsToMerge[i] == this)
{
PointCloudsToMerge.RemoveAtSwap(i--, EAllowShrinking::No);
}
}
PointCloudsToMerge.Shrink();
// Abort if no valid assets are found
if (PointCloudsToMerge.Num() == 0)
{
return;
}
FScopeBenchmarkTimer Timer("Merge");
// Lock the point cloud
FScopeLock Lock(&ProcessingLock);
FScopeLock DataLock(&Octree.DataLock);
if (ProgressCallback)
{
ProgressCallback();
}
// Calculate new, combined bounds
FBox NewBounds(EForceInit::ForceInit);
FBox NewAbsoluteBounds(EForceInit::ForceInit);
// Only include this asset if it actually has any data
if (GetNumPoints() > 0)
{
NewBounds += GetBounds(false);
NewAbsoluteBounds += GetBounds(true);
}
for (ULidarPointCloud* Asset : PointCloudsToMerge)
{
NewBounds += Asset->GetBounds(false);
NewAbsoluteBounds += Asset->GetBounds(true);
for (uint8& Classification : Asset->ClassificationsImported)
{
ClassificationsImported.AddUnique(Classification);
}
}
// Make a copy of current points, as the data will be reinitialized
TArray<FLidarPointCloudPoint> Points;
GetPointsAsCopies(Points, false);
FVector OldLocationOffset = LocationOffset;
// Initialize the Octree
Initialize(NewBounds);
OriginalCoordinates = NewAbsoluteBounds.GetCenter();
// Re-insert original points
InsertPoints(Points, GetDefault<ULidarPointCloudSettings>()->DuplicateHandling, false, OldLocationOffset - LocationOffset);
Points.Empty();
TArray<TFuture<void>> ThreadResults;
const ULidarPointCloudSettings* Settings = GetDefault<ULidarPointCloudSettings>();
const int32 MaxBatchSize = Settings->MultithreadingInsertionBatchSize;
const ELidarPointCloudDuplicateHandling DuplicateHandling = Settings->DuplicateHandling;
// Insert other points
for (ULidarPointCloud* Asset : PointCloudsToMerge)
{
if (ProgressCallback)
{
ProgressCallback();
}
const FVector3f Translation = (FVector3f)(Asset->LocationOffset - LocationOffset);
Asset->Octree.GetPointsAsCopiesInBatches([this, &ThreadResults, DuplicateHandling, Translation](TSharedPtr<TArray64<FLidarPointCloudPoint>> Points)
{
ThreadResults.Add(Async(EAsyncExecution::ThreadPool, [this, Points, DuplicateHandling, Translation]() {
Octree.InsertPoints(Points->GetData(), Points->Num(), DuplicateHandling, false, Translation);
}));
}, MaxBatchSize, false);
}
// Sync
if (ProgressCallback)
{
ProgressCallback();
}
for (const TFuture<void>& ThreadResult : ThreadResults)
{
ThreadResult.Get();
}
SetOptimalCollisionError();
MarkPackageDirty();
OnPointCloudRebuiltEvent.Broadcast();
}
void ULidarPointCloud::CalculateNormals(FLatentActionInfo LatentInfo)
{
if (UWorld* World = GetWorld())
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
if (LatentActionManager.FindExistingAction<FPointCloudLatentAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
{
ELidarPointCloudAsyncMode AsyncMode = ELidarPointCloudAsyncMode::Progress;
FPointCloudLatentAction* CompletionAction = new FPointCloudLatentAction(LatentInfo, AsyncMode);
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, CompletionAction);
CalculateNormals(nullptr, [&AsyncMode] { AsyncMode = ELidarPointCloudAsyncMode::Success; });
}
}
}
void ULidarPointCloud::CalculateNormals(TArray64<FLidarPointCloudPoint*>* Points, TFunction<void(void)> CompletionCallback)
{
FScopeTryLock Lock(&ProcessingLock);
if (!Lock.IsLocked())
{
PC_ERROR("Cannot calculate normals for the asset - data is currently being used.");
return;
}
bAsyncCancelled = false;
TSharedRef<FLidarPointCloudNotification, ESPMode::ThreadSafe> Notification = Notifications.Create("Calculating Normals", &bAsyncCancelled);
Async(EAsyncExecution::Thread,
[this, Points]
{
// This will take over the lock
FScopeLock Lock(&ProcessingLock);
// Wait for rendering to complete before proceeding and lock the access to the data
FScopeLock DataLock(&Octree.DataLock);
Octree.CalculateNormals(&bAsyncCancelled, NormalsQuality, NormalsNoiseTolerance, Points);
},
[this, Notification, _CompletionCallback = MoveTemp(CompletionCallback)]
{
AsyncTask(ENamedThreads::GameThread, [this, Notification]
{
MarkPackageDirty();
Notification->Close(!bAsyncCancelled);
OnPointCloudNormalsUpdatedEvent.Broadcast();
});
if (_CompletionCallback)
{
_CompletionCallback();
}
});
}
bool ULidarPointCloud::GetTriMeshSizeEstimates(struct FTriMeshCollisionDataEstimates& OutTriMeshEstimates, bool bInUseAllTriData) const
{
OutTriMeshEstimates.VerticeCount = Octree.GetCollisionData()->Vertices.Num();
return true;
}
bool ULidarPointCloud::GetPhysicsTriMeshData(FTriMeshCollisionData* CollisionData, bool InUseAllTriData)
{
CollisionData->Vertices = Octree.GetCollisionData()->Vertices;
CollisionData->Indices = Octree.GetCollisionData()->Indices;
return true;
}
UBodySetup* ULidarPointCloud::GetBodySetup()
{
return IsValid(BodySetup) ? BodySetup : nullptr;
}
void ULidarPointCloud::AlignClouds(TArray<ULidarPointCloud*> PointCloudsToAlign)
{
FBox CombinedBounds(EForceInit::ForceInit);
// Calculate combined bounds
for (ULidarPointCloud* Asset : PointCloudsToAlign)
{
CombinedBounds += Asset->GetBounds(true);
}
// Calculate and apply individual shifts
for (ULidarPointCloud* Asset : PointCloudsToAlign)
{
Asset->SetLocationOffset(Asset->OriginalCoordinates - CombinedBounds.GetCenter());
}
}
ULidarPointCloud* ULidarPointCloud::CreateFromFile(const FString& Filename, const FLidarPointCloudAsyncParameters& AsyncParameters, TSharedPtr<struct FLidarPointCloudImportSettings> ImportSettings, UObject* InParent, FName InName, EObjectFlags Flags)
{
#if WITH_EDITOR
FOnPointCloudChanged OnPointCloudRebuiltEvent;
FOnPointCloudChanged OnPointCloudUpdateCollisionEvent;
bool bOldPointCloudExists = false;
// See if Point Cloud already exists
ULidarPointCloud* OldPointCloud = Cast<ULidarPointCloud>(StaticFindObjectFast(nullptr, InParent, InName, EFindObjectFlags::ExactClass));
if (OldPointCloud)
{
bOldPointCloudExists = true;
// If so, store event references to re-apply to the new object
OnPointCloudRebuiltEvent = OldPointCloud->OnPointCloudRebuiltEvent;
OnPointCloudUpdateCollisionEvent = OldPointCloud->OnPointCloudUpdateCollisionEvent;
}
#endif
ULidarPointCloud* PointCloud = NewObject<ULidarPointCloud>(InParent, InName, Flags);
#if WITH_EDITOR
if (bOldPointCloudExists)
{
PointCloud->OnPointCloudRebuiltEvent = OnPointCloudRebuiltEvent;
PointCloud->OnPointCloudUpdateCollisionEvent = OnPointCloudUpdateCollisionEvent;
}
#endif
PointCloud->SetSourcePath(Filename);
PointCloud->ImportSettings = ImportSettings;
PointCloud->Reimport(AsyncParameters);
return PointCloud;
}
template<typename T>
ULidarPointCloud* ULidarPointCloud::CreateFromData(T Points, const int64& Count, const FLidarPointCloudAsyncParameters& AsyncParameters)
{
ULidarPointCloud* PC = NewObject<ULidarPointCloud>();
// Process points, if there are any available
if (Points && Count > 0)
{
// Start the process
if (AsyncParameters.bUseAsync)
{
Async(EAsyncExecution::Thread, [PC, AsyncParameters, Points, Count]
{
bool bSuccess = PC->SetData(Points, Count, AsyncParameters.ProgressCallback);
if (AsyncParameters.CompletionCallback)
{
AsyncParameters.CompletionCallback(bSuccess);
}
});
}
else
{
PC->SetData(Points, Count);
}
}
return PC;
}
ULidarPointCloud* ULidarPointCloud::CreateFromData(const TArray<FLidarPointCloudPoint>& Points, const bool& bUseAsync)
{
return CreateFromData(Points.GetData(), Points.Num(), FLidarPointCloudAsyncParameters(bUseAsync));
}
ULidarPointCloud* ULidarPointCloud::CreateFromData(const TArray64<FLidarPointCloudPoint>& Points, const bool& bUseAsync)
{
return CreateFromData(Points.GetData(), Points.Num(), FLidarPointCloudAsyncParameters(bUseAsync));
}
ULidarPointCloud* ULidarPointCloud::CreateFromData(const TArray<FLidarPointCloudPoint>& Points, const FLidarPointCloudAsyncParameters& AsyncParameters)
{
return CreateFromData(Points.GetData(), Points.Num(), AsyncParameters);
}
ULidarPointCloud* ULidarPointCloud::CreateFromData(const TArray64<FLidarPointCloudPoint>& Points, const FLidarPointCloudAsyncParameters& AsyncParameters)
{
return CreateFromData(Points.GetData(), Points.Num(), AsyncParameters);
}
ULidarPointCloud* ULidarPointCloud::CreateFromData(TArray<FLidarPointCloudPoint*>& Points, const bool& bUseAsync)
{
return CreateFromData(Points.GetData(), Points.Num(), FLidarPointCloudAsyncParameters(bUseAsync));
}
ULidarPointCloud* ULidarPointCloud::CreateFromData(TArray64<FLidarPointCloudPoint*>& Points, const bool& bUseAsync)
{
return CreateFromData(Points.GetData(), Points.Num(), FLidarPointCloudAsyncParameters(bUseAsync));
}
ULidarPointCloud* ULidarPointCloud::CreateFromData(TArray<FLidarPointCloudPoint*>& Points, const FLidarPointCloudAsyncParameters& AsyncParameters)
{
return CreateFromData(Points.GetData(), Points.Num(), AsyncParameters);
}
ULidarPointCloud* ULidarPointCloud::CreateFromData(TArray64<FLidarPointCloudPoint*>& Points, const FLidarPointCloudAsyncParameters& AsyncParameters)
{
return CreateFromData(Points.GetData(), Points.Num(), AsyncParameters);
}
FBox ULidarPointCloud::CalculateBoundsFromPoints(const FLidarPointCloudPoint* Points, const int64& Count)
{
FBox Bounds(EForceInit::ForceInit);
// Process points, if there are any available
if (Points && Count > 0)
{
for (const FLidarPointCloudPoint* Data = Points, *DataEnd = Data + Count; Data != DataEnd; ++Data)
{
Bounds += (FVector)Data->Location;
}
}
return Bounds;
}
FBox ULidarPointCloud::CalculateBoundsFromPoints(FLidarPointCloudPoint** Points, const int64& Count)
{
FBox Bounds(EForceInit::ForceInit);
// Process points, if there are any available
if (Points && Count > 0)
{
for (FLidarPointCloudPoint** Data = Points, **DataEnd = Data + Count; Data != DataEnd; ++Data)
{
Bounds += (FVector)(*Data)->Location;
}
}
return Bounds;
}
void ULidarPointCloud::FinishPhysicsAsyncCook(bool bSuccess, TSharedRef<FLidarPointCloudNotification, ESPMode::ThreadSafe> Notification)
{
FBenchmarkTimer::Log("CookingCollision");
Notification->Close(bSuccess);
if (bSuccess)
{
BodySetup = NewBodySetup;
ReleaseCollisionRendering(false);
InitializeCollisionRendering();
OnPointCloudUpdateCollisionEvent.Broadcast();
}
bCollisionBuildInProgress = false;
}
template <typename T>
void ULidarPointCloud::GetPoints_Internal(TArray<FLidarPointCloudPoint*, T>& Points, int64 StartIndex /*= 0*/, int64 Count /*= -1*/)
{
Octree.GetPoints(Points, StartIndex, Count);
}
template <typename T>
void ULidarPointCloud::GetPointsInSphere_Internal(TArray<FLidarPointCloudPoint*, T>& SelectedPoints, FSphere Sphere, const bool& bVisibleOnly)
{
Sphere.Center -= LocationOffset;
Octree.GetPointsInSphere(SelectedPoints, Sphere, bVisibleOnly);
}
template <typename T>
void ULidarPointCloud::GetPointsInBox_Internal(TArray<FLidarPointCloudPoint*, T>& SelectedPoints, const FBox& Box, const bool& bVisibleOnly)
{
Octree.GetPointsInBox(SelectedPoints, Box.ShiftBy(-LocationOffset), bVisibleOnly);
}
template <typename T>
void ULidarPointCloud::GetPointsInConvexVolume_Internal(TArray<FLidarPointCloudPoint*, T>& SelectedPoints, const FConvexVolume& ConvexVolume, const bool& bVisibleOnly)
{
Octree.GetPointsInConvexVolume(SelectedPoints, ConvexVolume, bVisibleOnly);
}
template <typename T>
void ULidarPointCloud::GetPointsAsCopies_Internal(TArray<FLidarPointCloudPoint, T>& Points, bool bReturnWorldSpace, int64 StartIndex /*= 0*/, int64 Count /*= -1*/) const
{
FTransform LocalToWorld(LocationOffset);
Octree.GetPointsAsCopies(Points, bReturnWorldSpace ? &LocalToWorld : nullptr, StartIndex, Count);
}
template <typename T>
void ULidarPointCloud::GetPointsInSphereAsCopies_Internal(TArray<FLidarPointCloudPoint, T>& SelectedPoints, FSphere Sphere, const bool& bVisibleOnly, bool bReturnWorldSpace) const
{
FTransform LocalToWorld(LocationOffset);
Sphere.Center -= LocationOffset;
Octree.GetPointsInSphereAsCopies(SelectedPoints, Sphere, bVisibleOnly, bReturnWorldSpace ? &LocalToWorld : nullptr);
}
template <typename T>
void ULidarPointCloud::GetPointsInBoxAsCopies_Internal(TArray<FLidarPointCloudPoint, T>& SelectedPoints, const FBox& Box, const bool& bVisibleOnly, bool bReturnWorldSpace) const
{
FTransform LocalToWorld(LocationOffset);
Octree.GetPointsInBoxAsCopies(SelectedPoints, Box.ShiftBy(-LocationOffset), bVisibleOnly, bReturnWorldSpace ? &LocalToWorld : nullptr);
}
template<typename T>
bool ULidarPointCloud::InsertPoints_Internal(T InPoints, const int64& Count, ELidarPointCloudDuplicateHandling DuplicateHandling, bool bRefreshPointsBounds, const FVector& Translation, FThreadSafeBool* bCanceled /*= nullptr*/, TFunction<void(float)> ProgressCallback /*= TFunction<void(float)>()*/)
{
FScopeLock Lock(&Octree.DataLock);
return InsertPoints_NoLock_Internal(InPoints, Count, DuplicateHandling, bRefreshPointsBounds, Translation, bCanceled, MoveTemp(ProgressCallback));
}
/*********************************************************************************************** ULidarPointCloudBlueprintLibrary */
#define ITERATE_CLOUDS(Action)\
if (UWorld* World = WorldContextObject ? WorldContextObject->GetWorld() : nullptr)\
{\
for (TActorIterator<ALidarPointCloudActor> Itr(World); Itr; ++Itr)\
{\
ALidarPointCloudActor* Actor = *Itr;\
ULidarPointCloudComponent* Component = Actor->GetPointCloudComponent();\
{ Action }\
}\
}
void ULidarPointCloudBlueprintLibrary::CreatePointCloudFromFile(UObject* WorldContextObject, const FString& Filename, bool bUseAsync, FLatentActionInfo LatentInfo, ELidarPointCloudAsyncMode& AsyncMode, float& Progress, ULidarPointCloud*& PointCloud)
{
CreatePointCloudFromFile(WorldContextObject, Filename, bUseAsync, LatentInfo, FLidarPointCloudImportSettings::MakeGeneric(Filename), AsyncMode, Progress, PointCloud);
}
void ULidarPointCloudBlueprintLibrary::CreatePointCloudFromFile(UObject* WorldContextObject, const FString& Filename, bool bUseAsync, FLatentActionInfo LatentInfo, TSharedPtr<FLidarPointCloudImportSettings> ImportSettings, ELidarPointCloudAsyncMode& AsyncMode, float& Progress, ULidarPointCloud*& PointCloud)
{
PointCloud = nullptr;
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
if (LatentActionManager.FindExistingAction<FPointCloudLatentAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
{
AsyncMode = ELidarPointCloudAsyncMode::Progress;
FPointCloudLatentAction* CompletionAction = new FPointCloudLatentAction(LatentInfo, AsyncMode);
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, CompletionAction);
PointCloud = ULidarPointCloud::CreateFromFile(Filename, FLidarPointCloudAsyncParameters(bUseAsync,
[&Progress, &AsyncMode](float InProgress)
{
Progress = InProgress;
},
[&AsyncMode](bool bSuccess)
{
AsyncMode = bSuccess ? ELidarPointCloudAsyncMode::Success : ELidarPointCloudAsyncMode::Failure;
}),
ImportSettings);
}
}
}
void ULidarPointCloudBlueprintLibrary::CreatePointCloudFromData(UObject* WorldContextObject, const TArray<FLidarPointCloudPoint>& Points, bool bUseAsync, FLatentActionInfo LatentInfo, ELidarPointCloudAsyncMode& AsyncMode, float& Progress, ULidarPointCloud*& PointCloud)
{
PointCloud = nullptr;
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
if (LatentActionManager.FindExistingAction<FPointCloudLatentAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
{
AsyncMode = ELidarPointCloudAsyncMode::Progress;
FPointCloudLatentAction* CompletionAction = new FPointCloudLatentAction(LatentInfo, AsyncMode);
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, CompletionAction);
PointCloud = ULidarPointCloud::CreateFromData(Points, FLidarPointCloudAsyncParameters(bUseAsync,
[&Progress, &AsyncMode](float InProgress)
{
Progress = InProgress;
},
[&AsyncMode](bool bSuccess)
{
AsyncMode = bSuccess ? ELidarPointCloudAsyncMode::Success : ELidarPointCloudAsyncMode::Failure;
}));
}
}
}
bool ULidarPointCloudBlueprintLibrary::ArePointsInSphere(UObject* WorldContextObject, FVector Center, float Radius, bool bVisibleOnly)
{
ITERATE_CLOUDS({
if (Component->HasPointsInSphere(Center, Radius, bVisibleOnly))
{
return true;
}
});
return false;
}
bool ULidarPointCloudBlueprintLibrary::ArePointsInBox(UObject* WorldContextObject, FVector Center, FVector Extent, bool bVisibleOnly)
{
ITERATE_CLOUDS({
if(Component->HasPointsInBox(Center, Extent, bVisibleOnly))
{
return true;
}
});
return false;
}
bool ULidarPointCloudBlueprintLibrary::ArePointsByRay(UObject* WorldContextObject, FVector Origin, FVector Direction, float Radius, bool bVisibleOnly)
{
ITERATE_CLOUDS({
if(Component->HasPointsByRay(Origin, Direction, Radius, bVisibleOnly))
{
return true;
}
});
return false;
}
void ULidarPointCloudBlueprintLibrary::GetPointsInSphereAsCopies(UObject* WorldContextObject, TArray<FLidarPointCloudPoint>& SelectedPoints, FVector Center, float Radius, bool bVisibleOnly)
{
SelectedPoints.Reset();
const FSphere Sphere(Center, Radius);
ITERATE_CLOUDS({
TArray<FLidarPointCloudPoint> _SelectedPoints;
Component->GetPointsInSphereAsCopies(_SelectedPoints, Sphere, bVisibleOnly, true);
SelectedPoints.Append(_SelectedPoints);
});
}
void ULidarPointCloudBlueprintLibrary::GetPointsInBoxAsCopies(UObject* WorldContextObject, TArray<FLidarPointCloudPoint>& SelectedPoints, FVector Center, FVector Extent, const bool& bVisibleOnly)
{
SelectedPoints.Reset();
const FBox Box(Center - Extent, Center + Extent);
ITERATE_CLOUDS({
TArray<FLidarPointCloudPoint> _SelectedPoints;
Component->GetPointsInBoxAsCopies(_SelectedPoints, Box, bVisibleOnly, true);
SelectedPoints.Append(_SelectedPoints);
});
}
bool ULidarPointCloudBlueprintLibrary::LineTraceSingle(UObject* WorldContextObject, FVector Origin, FVector Direction, float Radius, bool bVisibleOnly, FLidarPointCloudTraceHit& Hit)
{
const FLidarPointCloudRay Ray((FVector3f)Origin, (FVector3f)Direction);
ITERATE_CLOUDS({
if (FLidarPointCloudPoint* Point = Component->LineTraceSingle(Ray, Radius, bVisibleOnly))
{
Hit = FLidarPointCloudTraceHit(Actor, Component);
Hit.Points.Add(*Point);
return true;
}
});
return false;
}
bool ULidarPointCloudBlueprintLibrary::LineTraceMulti(UObject* WorldContextObject, FVector Origin, FVector Direction, float Radius, bool bVisibleOnly, TArray<FLidarPointCloudTraceHit>& Hits)
{
Hits.Reset();
const FLidarPointCloudRay Ray((FVector3f)Origin, (FVector3f)Direction);
ITERATE_CLOUDS({
FLidarPointCloudTraceHit Hit(Actor, Component);
if (Component->LineTraceMulti(Ray, Radius, bVisibleOnly, true, Hit.Points))
{
Hits.Add(Hit);
return true;
}
});
return Hits.Num() > 0;
}
void ULidarPointCloudBlueprintLibrary::SetVisibilityOfPointsInSphere(UObject* WorldContextObject, bool bNewVisibility, FVector Center, float Radius)
{
ITERATE_CLOUDS({ Component->SetVisibilityOfPointsInSphere(bNewVisibility, Center, Radius); });
}
void ULidarPointCloudBlueprintLibrary::SetVisibilityOfPointsInBox(UObject* WorldContextObject, bool bNewVisibility, FVector Center, FVector Extent)
{
ITERATE_CLOUDS({ Component->SetVisibilityOfPointsInBox(bNewVisibility, Center, Extent); });
}
void ULidarPointCloudBlueprintLibrary::SetVisibilityOfFirstPointByRay(UObject* WorldContextObject, bool bNewVisibility, FVector Origin, FVector Direction, float Radius)
{
float MinDistance = FLT_MAX;
ULidarPointCloudComponent* ClosestComponent = nullptr;
const FLidarPointCloudRay Ray((FVector3f)Origin, (FVector3f)Direction);
ITERATE_CLOUDS({
if (FLidarPointCloudPoint* Point = Component->LineTraceSingle(Ray, Radius, false))
{
const float DistanceSq = ((FVector)Point->Location - Origin).SizeSquared();
if (DistanceSq < MinDistance)
{
MinDistance = DistanceSq;
ClosestComponent = Component;
}
}
});
if (ClosestComponent)
{
ClosestComponent->SetVisibilityOfFirstPointByRay(bNewVisibility, Ray, Radius);
}
}
void ULidarPointCloudBlueprintLibrary::SetVisibilityOfPointsByRay(UObject* WorldContextObject, bool bNewVisibility, FVector Origin, FVector Direction, float Radius)
{
ITERATE_CLOUDS({ Component->SetVisibilityOfPointsByRay(bNewVisibility, Origin, Direction, Radius); });
}
void ULidarPointCloudBlueprintLibrary::ApplyColorToPointsInSphere(UObject* WorldContextObject, FColor NewColor, FVector Center, float Radius, bool bVisibleOnly)
{
ITERATE_CLOUDS({ Component->ApplyColorToPointsInSphere(NewColor, Center, Radius, bVisibleOnly); });
}
void ULidarPointCloudBlueprintLibrary::ApplyColorToPointsInBox(UObject* WorldContextObject, FColor NewColor, FVector Center, FVector Extent, bool bVisibleOnly)
{
ITERATE_CLOUDS({ Component->ApplyColorToPointsInBox(NewColor, Center, Extent, bVisibleOnly); });
}
void ULidarPointCloudBlueprintLibrary::ApplyColorToFirstPointByRay(UObject* WorldContextObject, FColor NewColor, FVector Origin, FVector Direction, float Radius, bool bVisibleOnly)
{
float MinDistance = FLT_MAX;
ULidarPointCloudComponent* ClosestComponent = nullptr;
const FLidarPointCloudRay Ray((FVector3f)Origin, (FVector3f)Direction);
ITERATE_CLOUDS({
if (FLidarPointCloudPoint* Point = Component->LineTraceSingle(Ray, Radius, bVisibleOnly))
{
const float DistanceSq = ((FVector)Point->Location - Origin).SizeSquared();
if (DistanceSq < MinDistance)
{
MinDistance = DistanceSq;
ClosestComponent = Component;
}
}
});
if (ClosestComponent)
{
ClosestComponent->ApplyColorToFirstPointByRay(NewColor, Ray, Radius, bVisibleOnly);
}
}
void ULidarPointCloudBlueprintLibrary::ApplyColorToPointsByRay(UObject* WorldContextObject, FColor NewColor, FVector Origin, FVector Direction, float Radius, bool bVisibleOnly)
{
ITERATE_CLOUDS({ Component->ApplyColorToPointsByRay(NewColor, Origin, Direction, Radius, bVisibleOnly); });
}
void ULidarPointCloudBlueprintLibrary::RemovePointsInSphere(UObject* WorldContextObject, FVector Center, float Radius, bool bVisibleOnly)
{
ITERATE_CLOUDS({ Component->RemovePointsInSphere(Center, Radius, bVisibleOnly); });
}
void ULidarPointCloudBlueprintLibrary::RemovePointsInBox(UObject* WorldContextObject, FVector Center, FVector Extent, bool bVisibleOnly)
{
ITERATE_CLOUDS({ Component->RemovePointsInBox(Center, Extent, bVisibleOnly); });
}
void ULidarPointCloudBlueprintLibrary::RemoveFirstPointByRay(UObject* WorldContextObject, FVector Origin, FVector Direction, float Radius, bool bVisibleOnly)
{
float MinDistance = FLT_MAX;
ULidarPointCloudComponent* ClosestComponent = nullptr;
const FLidarPointCloudRay Ray((FVector3f)Origin, (FVector3f)Direction);
ITERATE_CLOUDS({
if (FLidarPointCloudPoint* Point = Component->LineTraceSingle(Ray, Radius, bVisibleOnly))
{
const float DistanceSq = ((FVector)Point->Location - Origin).SizeSquared();
if (DistanceSq < MinDistance)
{
MinDistance = DistanceSq;
ClosestComponent = Component;
}
}
});
if (ClosestComponent)
{
ClosestComponent->RemoveFirstPointByRay(Ray, Radius, bVisibleOnly);
}
}
void ULidarPointCloudBlueprintLibrary::RemovePointsByRay(UObject* WorldContextObject, FVector Origin, FVector Direction, float Radius, bool bVisibleOnly)
{
ITERATE_CLOUDS({ Component->RemovePointsByRay(Origin, Direction, Radius, bVisibleOnly); });
}
#undef ITERATE_CLOUDS
/*********************************************************************************************** ALidarClippingVolume */
ALidarClippingVolume::ALidarClippingVolume()
: bEnabled(true)
, Mode(ELidarClippingVolumeMode::ClipOutside)
, Priority(0)
{
bColored = true;
BrushColor.R = 0;
BrushColor.G = 128;
BrushColor.B = 128;
BrushColor.A = 255;
GetBrushComponent()->SetMobility(EComponentMobility::Movable);
SetActorScale3D(FVector(50));
}
#undef LOCTEXT_NAMESPACE