From 8ba0e860d25b07b930c5380ec157ef99d91bc494 Mon Sep 17 00:00:00 2001 From: Timon Roemer <t.roemer@vis.rwth-aachen.de> Date: Mon, 15 Jul 2024 17:45:07 +0200 Subject: [PATCH] Adds MarchingCubes support --- Content/MetaPointMap.umap | Bin 44870 -> 44922 bytes Content/SelectionVolume.uasset | Bin 8925 -> 9146 bytes Source/MetaCastBachelor/DensityField.cpp | 57 ++++--- Source/MetaCastBachelor/DensityField.h | 31 +--- Source/MetaCastBachelor/MetaPoint.cpp | 131 +++++++++++---- Source/MetaCastBachelor/MetaPoint.h | 13 +- ...{MarchingCubes.cpp => MyMarchingCubes.cpp} | 38 ++--- .../{MarchingCubes.h => MyMarchingCubes.h} | 6 +- Source/MetaCastBachelor/PointCloud.cpp | 57 +------ Source/MetaCastBachelor/PointCloud.h | 4 + Source/MetaCastBachelor/Utilities.cpp | 157 ++++++++++-------- Source/MetaCastBachelor/Utilities.h | 3 +- .../VoxelPointLookupTable.cpp | 29 ++++ .../MetaCastBachelor/VoxelPointLookupTable.h | 26 +++ 14 files changed, 324 insertions(+), 228 deletions(-) rename Source/MetaCastBachelor/{MarchingCubes.cpp => MyMarchingCubes.cpp} (86%) rename Source/MetaCastBachelor/{MarchingCubes.h => MyMarchingCubes.h} (99%) create mode 100644 Source/MetaCastBachelor/VoxelPointLookupTable.cpp create mode 100644 Source/MetaCastBachelor/VoxelPointLookupTable.h diff --git a/Content/MetaPointMap.umap b/Content/MetaPointMap.umap index f44a6fa3bd8788f0813c8f4c4351e9c5bf22240f..7e4733c7f9d52777e794102217da9ff21ea628ba 100644 GIT binary patch delta 11015 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 L!qXZOT5kdX&{>5_ delta 10870 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 T17UElAG3*sy(u)=ZM_Kq9O7dy diff --git a/Content/SelectionVolume.uasset b/Content/SelectionVolume.uasset index b4a7c48878aadac755046e2d70262c2c42fa0168..f5d2c569e5837f227551627058837d7703770703 100644 GIT binary patch delta 4694 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 Hi&7*2-}Jaj delta 4459 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 diff --git a/Source/MetaCastBachelor/DensityField.cpp b/Source/MetaCastBachelor/DensityField.cpp index f952939..ab49740 100644 --- a/Source/MetaCastBachelor/DensityField.cpp +++ b/Source/MetaCastBachelor/DensityField.cpp @@ -5,12 +5,26 @@ // INITIALIZATION FUNCTIONS -FDensityField::FDensityField(): XNum(0), YNum(0), ZNum(0), XStep(0), YStep(0), ZStep(0) +FDensityField::FDensityField(): XNum(0), YNum(0), ZNum(0), XStep(0), YStep(0), ZStep(0), VoxelPointLookupTable(nullptr) { VoxelList = TArray<FVoxel>(); } -void FDensityField::InitializeDensityField(const FVector& MinBounds, const FVector& MaxBounds, const int32 XAxisNum, const int32 YAxisNum, const int32 ZAxisNum) +void FDensityField::InitializeDensityField(const APointCloud* PointCloud, const FVector& MinBounds, const FVector& MaxBounds, const FIntVector AxisNumbers) +{ + InitializeDataStructures(MinBounds, MaxBounds, AxisNumbers.X, AxisNumbers.Y, AxisNumbers.Z); + + constexpr float InfluenceRadius = 1.0f; // Size of a voxel + constexpr float Sigma = 1.0f; // Standard deviation for the Gaussian kernel + + CalculateVoxelDensities(PointCloud, InfluenceRadius, Sigma); + CalculateAndStoreGradients(); + + VoxelPointLookupTable = new FVoxelPointLookupTable(); + VoxelPointLookupTable->Initialize(PointCloud->PositionVectors, this); +} + +void FDensityField::InitializeDataStructures(const FVector& MinBounds, const FVector& MaxBounds, const int32 XAxisNum, const int32 YAxisNum, const int32 ZAxisNum) { VoxelList.Empty(); @@ -57,12 +71,12 @@ void FDensityField::CalculateVoxelDensities(const APointCloud* PointCloud, const // Iterate over each particle for (const FVector& ParticlePosition : PointCloud->PositionVectors) { - const int32 StartX = FMath::Max(0, static_cast<int32>((ParticlePosition.X - InfluenceRadius - PointCloud->MinBounds.X) / XStep)); - const int32 EndX = FMath::Min(XNum - 1, static_cast<int32>((ParticlePosition.X + InfluenceRadius - PointCloud->MinBounds.X) / XStep)); - const int32 StartY = FMath::Max(0, static_cast<int32>((ParticlePosition.Y - InfluenceRadius - PointCloud->MinBounds.Y) / YStep)); - const int32 EndY = FMath::Min(YNum - 1, static_cast<int32>((ParticlePosition.Y + InfluenceRadius - PointCloud->MinBounds.Y) / YStep)); - const int32 StartZ = FMath::Max(0, static_cast<int32>((ParticlePosition.Z - InfluenceRadius - PointCloud->MinBounds.Z) / ZStep)); - const int32 EndZ = FMath::Min(ZNum - 1, static_cast<int32>((ParticlePosition.Z + InfluenceRadius - PointCloud->MinBounds.Z) / ZStep)); + const int32 StartX = FMath::Max(0, FMath::FloorToInt((ParticlePosition.X - InfluenceRadius - PointCloud->MinBounds.X) / XStep)); + const int32 EndX = FMath::Min(XNum - 1, FMath::FloorToInt((ParticlePosition.X + InfluenceRadius - PointCloud->MinBounds.X) / XStep)); + const int32 StartY = FMath::Max(0, FMath::FloorToInt((ParticlePosition.Y - InfluenceRadius - PointCloud->MinBounds.Y) / YStep)); + const int32 EndY = FMath::Min(YNum - 1, FMath::FloorToInt((ParticlePosition.Y + InfluenceRadius - PointCloud->MinBounds.Y) / YStep)); + const int32 StartZ = FMath::Max(0, FMath::FloorToInt((ParticlePosition.Z - InfluenceRadius - PointCloud->MinBounds.Z) / ZStep)); + const int32 EndZ = FMath::Min(ZNum - 1, FMath::FloorToInt((ParticlePosition.Z + InfluenceRadius - PointCloud->MinBounds.Z) / ZStep)); for (int32 Z = StartZ; Z <= EndZ; ++Z) { @@ -76,9 +90,11 @@ void FDensityField::CalculateVoxelDensities(const APointCloud* PointCloud, const FVector NodePosition = VoxelList[Index].GetVoxelPosition(); const float Distance = FVector::Dist(NodePosition, ParticlePosition); - if (Distance < InfluenceRadius) + if (Distance <= InfluenceRadius) { - const double Weight = FUtilities::GaussianKernel(Distance, Sigma); + //const double Weight = FUtilities::GaussianKernel(Distance, Sigma); + const double Weight = FUtilities::SmoothingKernel(Distance, InfluenceRadius); + //const double Weight = InfluenceRadius - Distance; VoxelList[Index].AddToVoxelDensity(Weight); VoxelList[Index].SetClosePointsNumber(VoxelList[Index].GetClosePointsNumber() + 1.0); } @@ -249,19 +265,19 @@ void FDensityField::DrawDebugVoxelDensity(const UWorld* World, const AActor* Act } - // WORLD POSITION CONVERSION FUNCTIONS int32 FDensityField::WorldPositionToIndex(const FVector &Position) const { // Convert world position to grid position const FIntVector3 GridPosition = WorldToGridPosition(Position); - // Check if the grid position is valid - if (!IsValidGridPosition(GridPosition.X, GridPosition.Y, GridPosition.Z)) { - UE_LOG(LogTemp, Warning, TEXT("Position out of grid bounds or invalid grid settings, cannot find box index.")); - return -1; // Grid position was invalid + // Check if the calculated grid coordinates are within the bounds + if (!IsValidGridPosition(GridPosition.X, GridPosition.Y, GridPosition.Z)) + { + //UE_LOG(LogTemp, Warning, TEXT("Position out of grid bounds, returning invalid index.")); + return -1; } - + return GridPositionToIndex(GridPosition.X, GridPosition.Y, GridPosition.Z); } @@ -277,14 +293,7 @@ FIntVector3 FDensityField::WorldToGridPosition(const FVector& Position) const const int32 GridX = FMath::FloorToInt((Position.X - GridOrigin.X) / XStep); const int32 GridY = FMath::FloorToInt((Position.Y - GridOrigin.Y) / YStep); const int32 GridZ = FMath::FloorToInt((Position.Z - GridOrigin.Z) / ZStep); - - // Check if the calculated grid coordinates are within the bounds - if (GridX < 0 || GridX >= XNum || GridY < 0 || GridY >= YNum || GridZ < 0 || GridZ >= ZNum) - { - UE_LOG(LogTemp, Warning, TEXT("Position out of grid bounds, returning invalid grid coordinates.")); - return FIntVector3(-1, -1, -1); // Return an invalid grid position if out of bounds - } - + return FIntVector3(GridX, GridY, GridZ); } diff --git a/Source/MetaCastBachelor/DensityField.h b/Source/MetaCastBachelor/DensityField.h index 3f4ff10..c854dcd 100644 --- a/Source/MetaCastBachelor/DensityField.h +++ b/Source/MetaCastBachelor/DensityField.h @@ -1,6 +1,7 @@ #pragma once #include "CoreMinimal.h" +#include "VoxelPointLookupTable.h" #include "DensityField.generated.h" class APointCloud; @@ -33,24 +34,6 @@ public: void AddToVoxelDensity(const double Dis) { VoxelDensity += Dis; } }; -USTRUCT(BlueprintType) -struct FLUTUnit -{ - GENERATED_BODY() - - TArray<int32> LUTUnit; - - void AddToLUT(const int32 TargetInt) - { - LUTUnit.Add(TargetInt); - } - - TArray<int32> GetLTUnit() const - { - return LUTUnit; - } -}; - class FDensityField { // VARIABLES @@ -58,17 +41,20 @@ class FDensityField int32 XNum, YNum, ZNum; float XStep, YStep, ZStep; FVector GridOrigin; + + void CalculateVoxelDensities(const APointCloud* PointCloud, float InfluenceRadius, float Sigma); + void CalculateVoxelDensitiesByNumber(const APointCloud* PointCloud, float InfluenceRadius); + void CalculateAndStoreGradients(); + void InitializeDataStructures(const FVector& MinBounds, const FVector& MaxBounds, int32 XAxisNum, int32 YAxisNum, int32 ZAxisNum); public: + FVoxelPointLookupTable* VoxelPointLookupTable; mutable FCriticalSection DataGuard; // CONSTRUCTOR FDensityField(); // INITIALIZATION FUNCTIONS - void InitializeDensityField(const FVector& MinBounds, const FVector& MaxBounds, const int32 XAxisNum, const int32 YAxisNum, const int32 ZAxisNum); - void CalculateVoxelDensities(const APointCloud* PointCloud, float InfluenceRadius, float Sigma); - void CalculateVoxelDensitiesByNumber(const APointCloud* PointCloud, float InfluenceRadius); - void CalculateAndStoreGradients(); + void InitializeDensityField(const APointCloud* PointCloud, const FVector& MinBounds, const FVector& MaxBounds, const FIntVector AxisNumbers); // DEBUG FUNCTIONS void DrawDebugVoxelDensity(const UWorld* World, const AActor* Actor, float Duration, float Scale, float DensityThreshold) const; @@ -97,7 +83,6 @@ public: bool IsValidWorldPosition(const FVector& Position) const; TArray<int32> IndexToVoxelNeighbors(const int32 Index) const; bool IsValidIndex(int32 Index) const; - bool IsValid(); void SetVoxelDensityByIndex(int32 Index, double Density); void SetVoxelGradientByIndex(int32 Index, const FVector& Gradient); }; diff --git a/Source/MetaCastBachelor/MetaPoint.cpp b/Source/MetaCastBachelor/MetaPoint.cpp index 4a71e32..cde81c9 100644 --- a/Source/MetaCastBachelor/MetaPoint.cpp +++ b/Source/MetaCastBachelor/MetaPoint.cpp @@ -1,10 +1,11 @@ #include "MetaPoint.h" #include "Utilities.h" +#include "Generators/MarchingCubes.h" UMetaPoint::UMetaPoint() : LocalMaximumIndex(0), MyDensityField(nullptr), MetaPointThreshold(0), World(nullptr) { // Initialize the Marching Cubes algorithm - MyMarchingCubes = new MarchingCubes(); + //MyMarchingCubes = new FMarchingCubes(); // Create the procedural mesh component ProceduralMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("GeneratedMesh")); @@ -24,6 +25,13 @@ void UMetaPoint::BeginPlay() ProceduralMesh->SetMaterial(0, SelectionVolumeMat); ProceduralMesh->SetMaterial(1, SelectionVolumeMat); ProceduralMesh->SetMaterial(2, SelectionVolumeMat); + ProceduralMesh->bCastDynamicShadow = false; + ProceduralMesh->bRenderCustomDepth = true; + ProceduralMesh->SetCastShadow(false); + ProceduralMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); + ProceduralMesh->SetMobility(EComponentMobility::Movable); + ProceduralMesh->SetVisibility(true); + } void UMetaPoint::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) @@ -31,6 +39,8 @@ void UMetaPoint::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompo Super::TickComponent(DeltaTime, TickType, ThisTickFunction); AccumulatedTime += DeltaTime; + + ProceduralMesh->SetVisibility(Select); } void UMetaPoint::EraseParticles(const FVector& InputPosition) @@ -42,8 +52,8 @@ void UMetaPoint::HandleMetaSelectReleased(const FInputActionInstance& Instance) { Super::HandleMetaSelectReleased(Instance); - ProceduralMesh->ClearAllMeshSections(); - ProceduralMesh->SetVisibility(false); + //ProceduralMesh->ClearAllMeshSections(); + //ProceduralMesh->SetVisibility(false); AsyncTask(ENamedThreads::Type::AnyBackgroundHiPriTask, [this]() { @@ -72,6 +82,8 @@ void UMetaPoint::InitMetaPointSelection() // Convert the local maximum back to world coordinates LocalMaximumIndex = MyDensityField->WorldPositionToIndex(LocalMaximum); + SelectStartPosition = LocalMaximum; + MetaPointThreshold = FUtilities::InterpolateDensityAtPosition(MyDensityField, SelectionLocalPosition); // Initialize Visited array to false for all voxels @@ -87,6 +99,8 @@ void UMetaPoint::InitMetaPointSelection() ProceduralMesh->ClearAllMeshSections(); World = GetWorld(); + + LastHandInput = SelectionWorldPosition; } void UMetaPoint::SelectParticles(const FVector& InputPosition) @@ -98,6 +112,15 @@ void UMetaPoint::SelectParticles(const FVector& InputPosition) AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this]() { const FVector SelectionWorldPosition = SelectionObject->GetComponentLocation(); + + if(FVector::Distance(LastHandInput, SelectionWorldPosition) < MinimalHandMovement) + { + AccumulatedTime = 0.0f; + return; + } + + LastHandInput = SelectionWorldPosition; + // Convert the world position of the selection object to the local position relative to the point cloud const FVector SelectionLocalPosition = MyPointCloud->PointCloudVisualizer->GetComponentTransform().InverseTransformPosition(SelectionWorldPosition); @@ -105,19 +128,19 @@ void UMetaPoint::SelectParticles(const FVector& InputPosition) LocalMaximum = FUtilities::FollowGradientToMaximum(MyDensityField, SelectionLocalPosition); // Convert the local maximum back to world coordinates - LocalMaximumIndex = MyDensityField->WorldPositionToIndex(LocalMaximum); + //LocalMaximumIndex = MyDensityField->WorldPositionToIndex(LocalMaximum); MetaPointThreshold = FUtilities::InterpolateDensityAtPosition(MyDensityField, SelectionLocalPosition); - FloodedIndices = FUtilities::FloodFilling(MyDensityField, LocalMaximumIndex, MetaPointThreshold); - - GenerateVoxelMesh(FloodedIndices); + + const float MaxDistance = FVector::Distance(SelectStartPosition, SelectionLocalPosition); + FloodedIndices = FUtilities::FloodFilling_2(MyDensityField, LocalMaximumIndex, MetaPointThreshold, MaxDistance); - AccumulatedTime = 0.0f; + GenerateVoxelMeshSmooth(FloodedIndices); }); } } -void UMetaPoint::GenerateVoxelMesh(const TArray<int32> Voxels) const +void UMetaPoint::GenerateVoxelMeshWithCubes(const TArray<int32> Voxels) const { TArray<FVector> Vertices; TArray<int32> Triangles; @@ -164,35 +187,79 @@ void UMetaPoint::GenerateVoxelMesh(const TArray<int32> Voxels) const }); } -void UMetaPoint::CreateMeshSections(const TArray<FVector>& Vertices, const TArray<int32>& Triangles, const TArray<FColor>& VertexColors, const int32 MaxVerticesPerSection) const +void UMetaPoint::GenerateVoxelMeshSmooth(TArray<int32> Voxels) { - const int32 NumVertices = Vertices.Num(); - const int32 NumTriangles = Triangles.Num() / 3; - - for (int32 SectionIndex = 0, VertexIndex = 0, TriangleIndex = 0; VertexIndex < NumVertices && TriangleIndex < NumTriangles; - ++SectionIndex, VertexIndex += MaxVerticesPerSection, TriangleIndex += MaxVerticesPerSection / 3) + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, Voxels]() { - const int32 NumVerticesInSection = FMath::Min(MaxVerticesPerSection, NumVertices - VertexIndex); - const int32 NumTrianglesInSection = FMath::Min(MaxVerticesPerSection / 3, NumTriangles - TriangleIndex); - - TArray<FVector> SectionVertices; - TArray<FColor> SectionVertexColors; - SectionVertices.Reserve(NumVerticesInSection); - SectionVertexColors.Reserve(NumVerticesInSection); - for (int32 i = 0; i < NumVerticesInSection; ++i) + FVector Min(FLT_MAX, FLT_MAX, FLT_MAX); + FVector Max(-FLT_MAX, -FLT_MAX, -FLT_MAX); + + for(const int FloodedIndex : Voxels) { - SectionVertices.Add(Vertices[VertexIndex + i]); - SectionVertexColors.Add(VertexColors[VertexIndex + i]); + Min = FVector::Min(Min, MyDensityField->IndexToVoxelPosition(FloodedIndex)); + Max = FVector::Max(Max, MyDensityField->IndexToVoxelPosition(FloodedIndex)); } - TArray<int32> SectionTriangles; - SectionTriangles.Reserve(NumTrianglesInSection * 3); - for (int32 i = 0; i < NumTrianglesInSection * 3; ++i) + UE::Geometry::FMarchingCubes MarchingCubes; + MarchingCubes.Bounds = UE::Geometry::TAxisAlignedBox3(Min, Max); + MarchingCubes.CubeSize = MyDensityField->GetStep().X; + MarchingCubes.IsoValue = 0; + + // Define the implicit function + MarchingCubes.Implicit = [this, Voxels](const FVector3d& Pos) { + const FVector PosConverted = FVector(Pos.X, Pos.Y, Pos.Z); + const int Index = MyDensityField->WorldPositionToIndex(PosConverted); + return Voxels.Contains(Index) ? 1 : -1; + }; + + MarchingCubes.bParallelCompute = true; + const UE::Geometry::FMeshShapeGenerator& Generator = MarchingCubes.Generate(); + TArray<FVector3d> Vertices3d = Generator.Vertices; + TArray<UE::Geometry::FIndex3i> Triangles = Generator.Triangles; + TArray<FVector3f> Normals3F = Generator.Normals; + + // Convert FVector3d to FVector + TArray<FVector> Vertices; + Vertices.Reserve(Generator.Vertices.Num()); + for (const FVector3d& Vertex : Vertices3d) { - SectionTriangles.Add(Triangles[TriangleIndex * 3 + i] - VertexIndex); + Vertices.Add(FVector(Vertex.X, Vertex.Y, Vertex.Z)); } - // Create or update the mesh section - ProceduralMesh->CreateMeshSection(SectionIndex, SectionVertices, SectionTriangles, TArray<FVector>(), TArray<FVector2D>(), SectionVertexColors, TArray<FProcMeshTangent>(), false); - } + // Convert FIndex3i to int32 + TArray<int32> Indices; + Indices.Reserve(Generator.Triangles.Num() * 3); + for (const UE::Geometry::FIndex3i& Triangle : Triangles) + { + Indices.Add(Triangle.A); + Indices.Add(Triangle.B); + Indices.Add(Triangle.C); + } + + // Convert FVector3f to FVector + TArray<FVector> Normals; + Normals.Reserve(Generator.Normals.Num()); + for (const FVector3f& Normal : Normals3F) + { + Normals.Add(FVector(Normal.X, Normal.Y, Normal.Z)); + } + + TArray<FColor> VertexColors; + VertexColors.Reserve(Vertices.Num()); + for (int32 i = 0; i < Vertices.Num(); ++i) + { + VertexColors.Add(FColor::Green); + } + + + AsyncTask(ENamedThreads::Type::GameThread, [this, Vertices, Indices, Normals, VertexColors]() + { + ProceduralMesh->CreateMeshSection(0, Vertices, Indices, Normals, TArray<FVector2D>(), VertexColors, TArray<FProcMeshTangent>(), false); + ProceduralMesh->SetVisibility(true); + AccumulatedTime = 0.0f; + }); + }); + + + } \ No newline at end of file diff --git a/Source/MetaCastBachelor/MetaPoint.h b/Source/MetaCastBachelor/MetaPoint.h index e221d59..ecb85c6 100644 --- a/Source/MetaCastBachelor/MetaPoint.h +++ b/Source/MetaCastBachelor/MetaPoint.h @@ -4,7 +4,7 @@ #include "CoreMinimal.h" #include "ProceduralMeshComponent.h" #include "MetaCastBaseline.h" -#include "MetaCastBachelor/MarchingCubes.h" +#include "Generators/MarchingCubes.h" #include "MetaPoint.generated.h" UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) @@ -12,7 +12,6 @@ class UMetaPoint : public UMetaCastBaseline { GENERATED_BODY() - MarchingCubes* MyMarchingCubes; FVector WorldMaximum; int32 LocalMaximumIndex; FVector LocalMaximum; @@ -21,6 +20,10 @@ class UMetaPoint : public UMetaCastBaseline UPROPERTY() UWorld* World; float AccumulatedTime = 0.0; + FVector SelectStartPosition; + FVector LastHandInput; + UPROPERTY(EditAnywhere) + float MinimalHandMovement = 0.01; UPROPERTY() UProceduralMeshComponent* ProceduralMesh; @@ -34,7 +37,7 @@ class UMetaPoint : public UMetaCastBaseline TArray<int32> RevisitIndices; UPROPERTY(EditAnywhere) - int MetaCastPerSecond = 10; + int MetaCastPerSecond = 1; public: UMetaPoint(); @@ -48,8 +51,8 @@ public: virtual void HandleMetaSelectPressed(const FInputActionInstance& Instance) override; void InitMetaPointSelection(); - void GenerateVoxelMesh(const TArray<int32> Voxels) const; - void CreateMeshSections(const TArray<FVector>& Vertices, const TArray<int32>& Triangles, const TArray<FColor>& VertexColors, int32 MaxVerticesPerSection) const; + void GenerateVoxelMeshWithCubes(const TArray<int32> Voxels) const; + void GenerateVoxelMeshSmooth(const TArray<int32> Voxels); virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; }; diff --git a/Source/MetaCastBachelor/MarchingCubes.cpp b/Source/MetaCastBachelor/MyMarchingCubes.cpp similarity index 86% rename from Source/MetaCastBachelor/MarchingCubes.cpp rename to Source/MetaCastBachelor/MyMarchingCubes.cpp index b44eb56..5786201 100644 --- a/Source/MetaCastBachelor/MarchingCubes.cpp +++ b/Source/MetaCastBachelor/MyMarchingCubes.cpp @@ -1,16 +1,16 @@ // Copyright 2017 Patrick Chadbourne (PChadbourne). All Rights Reserved. -#include "MarchingCubes.h" +#include "MyMarchingCubes.h" -MarchingCubes::MarchingCubes() +FMyMarchingCubes::FMyMarchingCubes() { } -MarchingCubes::~MarchingCubes() +FMyMarchingCubes::~FMyMarchingCubes() { } -void MarchingCubes::Polyganize(FCell Cell, FMeshData* Data) +void FMyMarchingCubes::Polyganize(FCell Cell, FMeshData* Data) { int CubeIndex = 0; FVector VertList[12]; @@ -72,12 +72,10 @@ void MarchingCubes::Polyganize(FCell Cell, FMeshData* Data) } } - - -void MarchingCubes::ProcessGrid(TArray<float> Grid, int Width, FMeshData* Data, FVector A, FVector B, FVector C, FVector D) +void FMyMarchingCubes::ProcessGrid(TArray<float> Grid, int Width, FMeshData* Data, FVector A, FVector B, FVector C, FVector D) { - int WidthSquared = Width * Width; - int GridSize = Width * Width * Width; + const int WidthSquared = Width * Width; + const int GridSize = Width * Width * Width; // Ensure the Grid array has enough elements if (Grid.Num() < GridSize) @@ -94,14 +92,14 @@ void MarchingCubes::ProcessGrid(TArray<float> Grid, int Width, FMeshData* Data, { for (int k = 0; k < Width - 1; k++) { - int idx7 = i + Width * j + WidthSquared * k; - int idx4 = idx7 + 1; - int idx6 = idx7 + Width; - int idx5 = idx6 + 1; - int idx3 = idx7 + WidthSquared; - int idx0 = idx3 + 1; - int idx2 = idx3 + Width; - int idx1 = idx2 + 1; + const int idx7 = i + Width * j + WidthSquared * k; + const int idx4 = idx7 + 1; + const int idx6 = idx7 + Width; + const int idx5 = idx6 + 1; + const int idx3 = idx7 + WidthSquared; + const int idx0 = idx3 + 1; + const int idx2 = idx3 + Width; + const int idx1 = idx2 + 1; // Ensure all indices are within bounds if (idx7 >= Grid.Num() || idx4 >= Grid.Num() || idx6 >= Grid.Num() || idx5 >= Grid.Num() || @@ -156,9 +154,7 @@ void MarchingCubes::ProcessGrid(TArray<float> Grid, int Width, FMeshData* Data, } } - - -FVector MarchingCubes::VertexInterp(FVector P1, FVector P2, float P1Val, float P2Val, float Value) +FVector FMyMarchingCubes::VertexInterp(FVector P1, FVector P2, float P1Val, float P2Val, float Value) { @@ -179,7 +175,7 @@ FVector MarchingCubes::VertexInterp(FVector P1, FVector P2, float P1Val, float P return(P); } -FVector MarchingCubes::CalculateTriangularInterpolation(int i, int j, int k, FVector A, FVector B, FVector C, FVector D, int Width) +FVector FMyMarchingCubes::CalculateTriangularInterpolation(int i, int j, int k, FVector A, FVector B, FVector C, FVector D, int Width) { if (i + j < Width) { FVector alpha = FMath::Lerp(A, B, (i*2.0f) / (float)Width); diff --git a/Source/MetaCastBachelor/MarchingCubes.h b/Source/MetaCastBachelor/MyMarchingCubes.h similarity index 99% rename from Source/MetaCastBachelor/MarchingCubes.h rename to Source/MetaCastBachelor/MyMarchingCubes.h index ea31fc9..d48a2ed 100644 --- a/Source/MetaCastBachelor/MarchingCubes.h +++ b/Source/MetaCastBachelor/MyMarchingCubes.h @@ -19,11 +19,11 @@ public: TArray<FVector> N; }; -class MarchingCubes +class FMyMarchingCubes { public: - MarchingCubes(); - ~MarchingCubes(); + FMyMarchingCubes(); + ~FMyMarchingCubes(); void ProcessGrid(TArray<float> Grid, int Width, FMeshData* Data, FVector A, FVector B, FVector C, FVector D); diff --git a/Source/MetaCastBachelor/PointCloud.cpp b/Source/MetaCastBachelor/PointCloud.cpp index 747d186..d319ce9 100644 --- a/Source/MetaCastBachelor/PointCloud.cpp +++ b/Source/MetaCastBachelor/PointCloud.cpp @@ -85,22 +85,9 @@ void APointCloud::UpdateBounds() void APointCloud::SetupDensityFieldFromPointCloud() const { UE_LOG(LogTemp, Log, TEXT("Initializing DensityField!")); - - constexpr int32 XAxisNum = 100; // Number of divisions in the grid along the X-axis - constexpr int32 YAxisNum = 100; // Number of divisions in the grid along the Y-axis - constexpr int32 ZAxisNum = 100; // Number of divisions in the grid along the Z-axis - MyDensityField->InitializeDensityField(MinBounds, MaxBounds, XAxisNum, YAxisNum, ZAxisNum); - - constexpr float InfluenceRadius = 2.0f; // Half the size of a voxel - constexpr float Sigma = 0.5f; // Standard deviation for the Gaussian kernel - - MyDensityField->CalculateVoxelDensitiesByNumber(this, InfluenceRadius); - MyDensityField->CalculateAndStoreGradients(); - if (const UWorld* World = GetWorld()) - { - //MyDensityField->DrawDebugVoxelDensity(World, this, 100.0f, 1.0f, 0.8f); - } + const FIntVector AxisNumbers(100, 100, 100); + MyDensityField->InitializeDensityField(this, MinBounds, MaxBounds, AxisNumbers); } void APointCloud::UpdateSelection() @@ -111,7 +98,7 @@ void APointCloud::UpdateSelection() { if (SelectionFlags[i]) { - PointColors[i] = FColor::Red; + PointColors[i] = FColor(0, 200, 0); }else { PointColors[i] = FColor::Orange; @@ -149,57 +136,31 @@ void APointCloud::DrawVoxel(const int Index, const float Time) const void APointCloud::ColorPointsInVoxels(const TArray<int32> VoxelIndices) { + FScopeLock Lock(&DataGuard); if (!MyDensityField) { UE_LOG(LogTemp, Warning, TEXT("DensityField is not initialized.")); return; } - TArray<FVector> MyPositionVectors = PositionVectors; - - FScopeLock Lock(&DataGuard); - FScopeLock Lock2(&MyDensityField->DataGuard); - // Iterate through each index in VoxelIndices for (const int32 VoxelIndex : VoxelIndices) { - // Ensure the voxel index is valid - if (!MyDensityField || !MyDensityField->IsValidIndex(VoxelIndex)) - { - UE_LOG(LogTemp, Warning, TEXT("Invalid voxel index: %d"), VoxelIndex); - continue; - } - - // Get the bounds of the voxel - FVector VoxelMin, VoxelMax; - MyDensityField->IndexToVoxelBounds(VoxelIndex, VoxelMin, VoxelMax); - - // Iterate over all points to check if they are within this voxel - for (int32 i = 0; i < MyPositionVectors.Num(); ++i) + const TArray<int32>& PointIndices = MyDensityField->VoxelPointLookupTable->GetPointsInVoxel(VoxelIndex); + for (const int32 PointIndex : PointIndices) { - if(!MyPositionVectors.IsValidIndex(i)) + if(SelectionFlags.IsValidIndex(PointIndex)) { - continue; - } - const FVector Point = MyPositionVectors[i]; - - // Check if the point is within the voxel bounds - if (Point.X >= VoxelMin.X && Point.X <= VoxelMax.X && - Point.Y >= VoxelMin.Y && Point.Y <= VoxelMax.Y && - Point.Z >= VoxelMin.Z && Point.Z <= VoxelMax.Z) - { - SelectionFlags[i] = true; + SelectionFlags[PointIndex] = true; } } } - - //UpdateSelection(); } + // Called every frame void APointCloud::Tick(float DeltaTime) { Super::Tick(DeltaTime); - UpdateSelection(); } diff --git a/Source/MetaCastBachelor/PointCloud.h b/Source/MetaCastBachelor/PointCloud.h index 4770f1b..6aae126 100644 --- a/Source/MetaCastBachelor/PointCloud.h +++ b/Source/MetaCastBachelor/PointCloud.h @@ -13,9 +13,13 @@ class APointCloud : public AActor public: FDensityField* MyDensityField; + UPROPERTY() TArray<FVector> PositionVectors; + UPROPERTY() TArray<bool> DefaultFlags; + UPROPERTY() TArray<bool> SelectionFlags; + UPROPERTY() TArray<FColor> PointColors; mutable FCriticalSection DataGuard; diff --git a/Source/MetaCastBachelor/Utilities.cpp b/Source/MetaCastBachelor/Utilities.cpp index 2362648..91ab449 100644 --- a/Source/MetaCastBachelor/Utilities.cpp +++ b/Source/MetaCastBachelor/Utilities.cpp @@ -31,24 +31,21 @@ double FUtilities::InterpolateDensityAtPosition(const FDensityField* DensityFiel if (!DensityField) return 0.0; - const FVector GridPos = (Position - DensityField->GetGridOrigin()) / DensityField->GetStep(); - - const int32 XBin = FMath::FloorToInt(GridPos.X); - const int32 YBin = FMath::FloorToInt(GridPos.Y); - const int32 ZBin = FMath::FloorToInt(GridPos.Z); - - const float XFraction = GridPos.X - XBin; - const float YFraction = GridPos.Y - YBin; - const float ZFraction = GridPos.Z - ZBin; - - const double D000 = DensityField->GridPositionToVoxelDensity(XBin, YBin, ZBin); - const double D100 = DensityField->GridPositionToVoxelDensity(XBin + 1, YBin, ZBin); - const double D010 = DensityField->GridPositionToVoxelDensity(XBin, YBin + 1, ZBin); - const double D001 = DensityField->GridPositionToVoxelDensity(XBin, YBin, ZBin + 1); - const double D101 = DensityField->GridPositionToVoxelDensity(XBin + 1, YBin, ZBin + 1); - const double D011 = DensityField->GridPositionToVoxelDensity(XBin, YBin + 1, ZBin + 1); - const double D110 = DensityField->GridPositionToVoxelDensity(XBin + 1, YBin + 1, ZBin); - const double D111 = DensityField->GridPositionToVoxelDensity(XBin + 1, YBin + 1, ZBin + 1); + const FVector GridPositionFraction = (Position - DensityField->GetGridOrigin()) / DensityField->GetStep(); + const FIntVector3 GridPosition = DensityField->WorldToGridPosition(Position - DensityField->GetGridOrigin()); + + const float XFraction = GridPositionFraction.X - GridPosition.X; + const float YFraction = GridPositionFraction.Y - GridPosition.Y; + const float ZFraction = GridPositionFraction.Z - GridPosition.Z; + + const double D000 = DensityField->GridPositionToVoxelDensity(GridPosition.X, GridPosition.Y, GridPosition.Z); + const double D100 = DensityField->GridPositionToVoxelDensity(GridPosition.X + 1, GridPosition.Y, GridPosition.Z); + const double D010 = DensityField->GridPositionToVoxelDensity(GridPosition.X, GridPosition.Y + 1, GridPosition.Z); + const double D001 = DensityField->GridPositionToVoxelDensity(GridPosition.X, GridPosition.Y, GridPosition.Z + 1); + const double D101 = DensityField->GridPositionToVoxelDensity(GridPosition.X + 1, GridPosition.Y, GridPosition.Z + 1); + const double D011 = DensityField->GridPositionToVoxelDensity(GridPosition.X, GridPosition.Y + 1, GridPosition.Z + 1); + const double D110 = DensityField->GridPositionToVoxelDensity(GridPosition.X + 1, GridPosition.Y + 1, GridPosition.Z); + const double D111 = DensityField->GridPositionToVoxelDensity(GridPosition.X + 1, GridPosition.Y + 1, GridPosition.Z + 1); // Trilinear interpolation const double DX00 = FMath::Lerp(D000, D100, XFraction); @@ -70,25 +67,22 @@ FVector FUtilities::InterpolateGradientAtPosition(const FDensityField* DensityFi return FVector::ZeroVector; FVector GridPos = (Position - DensityField->GetGridOrigin()) / DensityField->GetStep(); - - int32 XBin = FMath::FloorToInt(GridPos.X); - int32 YBin = FMath::FloorToInt(GridPos.Y); - int32 ZBin = FMath::FloorToInt(GridPos.Z); + const FIntVector3 GridPosition = DensityField->WorldToGridPosition(Position - DensityField->GetGridOrigin()); // Fractional parts to interpolate - float XFraction = GridPos.X - XBin; - float YFraction = GridPos.Y - YBin; - float ZFraction = GridPos.Z - ZBin; + float XFraction = GridPos.X - GridPosition.X; + float YFraction = GridPos.Y - GridPosition.Y; + float ZFraction = GridPos.Z - GridPosition.Z; // Fetch gradients from corners of the cell - FVector G000 = DensityField->GridPositionToVoxelGradient(XBin, YBin, ZBin); - FVector G100 = DensityField->GridPositionToVoxelGradient(XBin + 1, YBin, ZBin); - FVector G010 = DensityField->GridPositionToVoxelGradient(XBin, YBin + 1, ZBin); - FVector G001 = DensityField->GridPositionToVoxelGradient(XBin, YBin, ZBin + 1); - FVector G110 = DensityField->GridPositionToVoxelGradient(XBin + 1, YBin + 1, ZBin); - FVector G101 = DensityField->GridPositionToVoxelGradient(XBin + 1, YBin, ZBin + 1); - FVector G011 = DensityField->GridPositionToVoxelGradient(XBin, YBin + 1, ZBin + 1); - FVector G111 = DensityField->GridPositionToVoxelGradient(XBin + 1, YBin + 1, ZBin + 1); + FVector G000 = DensityField->GridPositionToVoxelGradient(GridPosition.X, GridPosition.Y, GridPosition.Z); + FVector G100 = DensityField->GridPositionToVoxelGradient(GridPosition.X + 1, GridPosition.Y, GridPosition.Z); + FVector G010 = DensityField->GridPositionToVoxelGradient(GridPosition.X, GridPosition.Y + 1, GridPosition.Z); + FVector G001 = DensityField->GridPositionToVoxelGradient(GridPosition.X, GridPosition.Y, GridPosition.Z + 1); + FVector G110 = DensityField->GridPositionToVoxelGradient(GridPosition.X + 1, GridPosition.Y + 1, GridPosition.Z); + FVector G101 = DensityField->GridPositionToVoxelGradient(GridPosition.X + 1, GridPosition.Y, GridPosition.Z + 1); + FVector G011 = DensityField->GridPositionToVoxelGradient(GridPosition.X, GridPosition.Y + 1, GridPosition.Z + 1); + FVector G111 = DensityField->GridPositionToVoxelGradient(GridPosition.X + 1, GridPosition.Y + 1, GridPosition.Z + 1); // Interpolate along x for each of the yz pairs FVector G00 = FMath::Lerp(G000, G100, XFraction); @@ -110,6 +104,11 @@ float FUtilities::GaussianKernel(const float Distance, const float Sigma) { return FMath::Exp(-0.5 * FMath::Square(Distance / Sigma)); } +float FUtilities::SmoothingKernel(const float Distance, const float Radius) { + const float Value = FMath::Max(0.0f, Radius * Radius - Distance * Distance); + return Value * Value * Value; +} + FVector FUtilities::FollowGradientToMaximum(const FDensityField* DensityField, const FVector& StartPosition) { FVector CurrentPosition = StartPosition; @@ -117,20 +116,20 @@ FVector FUtilities::FollowGradientToMaximum(const FDensityField* DensityField, c int Iterations = 0; constexpr int MaxIterations = 100; // Prevent infinite loops - constexpr float StepSize = 0.1f; // Define a reasonable step size + constexpr float StepSize = 0.005f; // Define a reasonable step size while (Iterations < MaxIterations) { FVector Gradient = InterpolateGradientAtPosition(DensityField, CurrentPosition); if (Gradient.IsNearlyZero()) { - UE_LOG(LogTemp, Log, TEXT("Gradient is zero at position %s"), *CurrentPosition.ToString()); + //UE_LOG(LogTemp, Log, TEXT("Gradient is zero at position %s"), *CurrentPosition.ToString()); break; // Gradient is zero, likely at a local maximum } // Move towards the direction of the gradient FVector NextPosition = CurrentPosition + (StepSize * Gradient.GetSafeNormal()); if (!DensityField->IsValidWorldPosition(NextPosition)) { - UE_LOG(LogTemp, Warning, TEXT("Next position out of bounds: %s"), *NextPosition.ToString()); + //UE_LOG(LogTemp, Warning, TEXT("Next position out of bounds: %s"), *NextPosition.ToString()); break; // Next position is out of valid bounds } @@ -184,9 +183,9 @@ TArray<int32> FUtilities::FloodFilling(const FDensityField* DensityField, const for (int32 NeighborIndex : Neighbors) { const double NeighborDensity = DensityField->IndexToVoxelDensity(NeighborIndex); - //UE_LOG(LogTemp, Log, TEXT("Lookign at Neighbor %d at Position %s with density: %f."), NeighborIndex, *DensityField->IndexToGridPosition(NeighborIndex).ToString(), NeighborDensity); + //UE_LOG(LogTemp, Log, TEXT("Looking at Neighbor %d at Position %s with density: %f."), NeighborIndex, *DensityField->IndexToGridPosition(NeighborIndex).ToString(), NeighborDensity); - if (NeighborDensity > Threshold && !Visited[NeighborIndex]) { + if (NeighborDensity > Threshold * 1.1f && !Visited[NeighborIndex]) { Stack.Push(NeighborIndex); //UE_LOG(LogTemp, Log, TEXT("Pushing Neighbor %d to stack."), NeighborIndex); } @@ -203,55 +202,71 @@ TArray<int32> FUtilities::FloodFilling(const FDensityField* DensityField, const return VisitedIndices; } -void FUtilities::FloodFilling_2(const FDensityField* DensityField, const float Threshold, TArray<bool>& VisitedIndices, TArray<int32>& IndicesToVisit, TArray<int32>& FloodedIndices, TArray<int32>& PotentialRevisit) +TArray<int32> FUtilities::FloodFilling_2(const FDensityField* DensityField, const int32 StartIndex, const float Threshold, const float MaxDistance) { + //UE_LOG(LogTemp, Warning, TEXT("Threshold for FloodFilling: %f"), Threshold); + + TArray<int32> VisitedIndices; if (!DensityField) { UE_LOG(LogTemp, Warning, TEXT("DensityField is null.")); - return; + return VisitedIndices; } - TArray<int32> CurrentPotentialRevisit; + if (!DensityField->IsValidIndex(StartIndex)) { + UE_LOG(LogTemp, Warning, TEXT("Start index %d is invalid."), StartIndex); + return VisitedIndices; + } - - constexpr int MaxIterations = 2000; - int IterationCount = 0; - - while (IndicesToVisit.Num() > 0) + TArray<int32> Stack; + Stack.Push(StartIndex); + TArray<bool> Visited; + Visited.Init(false, DensityField->GetVoxelNumber()); + + while (Stack.Num() > 0) { - - IterationCount++; - if(IterationCount > MaxIterations) + int32 CurrentIndex = Stack.Pop(); + if (!Visited[CurrentIndex]) { - while (IndicesToVisit.Num() > 0) { - CurrentPotentialRevisit.Add(IndicesToVisit.Pop()); - } - break; - } - - int32 CurrentIndex = IndicesToVisit.Pop(); + Visited[CurrentIndex] = true; + VisitedIndices.Add(CurrentIndex); - if(DensityField->IsValidIndex(CurrentIndex)) - { - VisitedIndices[CurrentIndex] = true; - const double CurrentDensity = DensityField->IndexToVoxelDensity(CurrentIndex); + TArray<int32> Neighbors = DensityField->IndexToVoxelNeighbors(CurrentIndex); + //UE_LOG(LogTemp, Log, TEXT("Visiting Node %d at Position %s with %d neighbors."), CurrentIndex, *DensityField->IndexToGridPosition(CurrentIndex).ToString(), Neighbors.Num()); + + for (int32 NeighborIndex : Neighbors) + { + const double NeighborDensity = DensityField->IndexToVoxelDensity(NeighborIndex); + //UE_LOG(LogTemp, Log, TEXT("Looking at Neighbor %d at Position %s with density: %f."), NeighborIndex, *DensityField->IndexToGridPosition(NeighborIndex).ToString(), NeighborDensity); - if (CurrentDensity >= Threshold) { - FloodedIndices.Add(CurrentIndex); + const float DistanceToStart = FVector::Dist(DensityField->IndexToVoxelPosition(StartIndex), DensityField->IndexToVoxelPosition(NeighborIndex)); + + if (!Visited[NeighborIndex]){ + if(DistanceToStart <= MaxDistance) + { + if(NeighborDensity > Threshold * 1.0f) + { + Stack.Push(NeighborIndex); + } - TArray<int32> Neighbors = DensityField->IndexToVoxelNeighbors(CurrentIndex); - for (int32 NeighborIndex : Neighbors) - { - if (!VisitedIndices[NeighborIndex]) + }else { - IndicesToVisit.Push(NeighborIndex); + const float Multiplier = 1 + FMath::Pow(DistanceToStart - MaxDistance, 2); + if(NeighborDensity > Threshold * Multiplier) + { + Stack.Push(NeighborIndex); + } } + //UE_LOG(LogTemp, Log, TEXT("Pushing Neighbor %d to stack."), NeighborIndex); } - } else { - CurrentPotentialRevisit.Add(CurrentIndex); } } } - // Update PotentialRevisit with the new list from this run - PotentialRevisit = CurrentPotentialRevisit; + // Logging final details + //UE_LOG(LogTemp, Log, TEXT("Flood filling completed from start index %d. Total voxels selected: %d"), StartIndex, VisitedIndices.Num()); + for (const int32 idx : VisitedIndices) { + //UE_LOG(LogTemp, Log, TEXT("Selected Voxel Index: %d at Grid Position: %s"), idx, *DensityField->IndexToGridPosition(idx).ToString()); + } + + return VisitedIndices; } diff --git a/Source/MetaCastBachelor/Utilities.h b/Source/MetaCastBachelor/Utilities.h index 6d406d7..b453f82 100644 --- a/Source/MetaCastBachelor/Utilities.h +++ b/Source/MetaCastBachelor/Utilities.h @@ -9,7 +9,8 @@ public: static double InterpolateDensityAtPosition(const FDensityField* DensityField, const FVector& Position); static FVector InterpolateGradientAtPosition(const FDensityField* DensityField, const FVector& Position); static float GaussianKernel(float Distance, float Sigma); + static float SmoothingKernel(float Distance, float Radius); static FVector FollowGradientToMaximum(const FDensityField* DensityField, const FVector& StartPosition); static TArray<int32> FloodFilling(const FDensityField* DensityField, const int32 StartIndex, const float Threshold); - static void FloodFilling_2(const FDensityField* DensityField, const float Threshold, TArray<bool>& VisitedIndices, TArray<int32>& IndicesToVisit, TArray<int32>& FloodedIndices, TArray<int32>& PotentialRevisit); + static TArray<int32> FloodFilling_2(const FDensityField* DensityField, const int32 StartIndex, const float Threshold, const float MaxDistance); }; diff --git a/Source/MetaCastBachelor/VoxelPointLookupTable.cpp b/Source/MetaCastBachelor/VoxelPointLookupTable.cpp new file mode 100644 index 0000000..a9499c2 --- /dev/null +++ b/Source/MetaCastBachelor/VoxelPointLookupTable.cpp @@ -0,0 +1,29 @@ +#include "VoxelPointLookupTable.h" +#include "DensityField.h" + + +FVoxelPointLookupTable::FVoxelPointLookupTable() : LinkedDensityField(nullptr) {} + +FVoxelPointLookupTable::~FVoxelPointLookupTable(){} + +void FVoxelPointLookupTable::Initialize(const TArray<FVector>& PositionVectors, const FDensityField* DensityField) +{ + LinkedDensityField = DensityField; + if (!LinkedDensityField) return; + + for (int32 i = 0; i < PositionVectors.Num(); ++i) + { + const FVector& Point = PositionVectors[i]; + int32 VoxelIndex = LinkedDensityField->WorldPositionToIndex(Point); + if (VoxelIndex != -1) + { + VoxelToPointIndicesMap.FindOrAdd(VoxelIndex).Add(i); + } + } +} + +TArray<int32> FVoxelPointLookupTable::GetPointsInVoxel(const int32 VoxelIndex) const +{ + return VoxelToPointIndicesMap.Contains(VoxelIndex) ? VoxelToPointIndicesMap[VoxelIndex] : TArray<int32>(); +} + diff --git a/Source/MetaCastBachelor/VoxelPointLookupTable.h b/Source/MetaCastBachelor/VoxelPointLookupTable.h new file mode 100644 index 0000000..52076a3 --- /dev/null +++ b/Source/MetaCastBachelor/VoxelPointLookupTable.h @@ -0,0 +1,26 @@ +#pragma once +#include "CoreMinimal.h" + +class FDensityField; + +class FVoxelPointLookupTable +{ + +public: + FVoxelPointLookupTable(); + + virtual ~FVoxelPointLookupTable(); + + // Initializes the lookup table with position vectors and a reference to the density field + void Initialize(const TArray<FVector>& PositionVectors, const FDensityField* DensityField); + + // Retrieves the array of point indices for a specific voxel index + TArray<int32> GetPointsInVoxel(int32 VoxelIndex) const; + +private: + // Maps voxel indices to arrays of point indices + TMap<int32, TArray<int32>> VoxelToPointIndicesMap; + + // Reference to the density field for voxel bounds queries + const FDensityField* LinkedDensityField; +}; -- GitLab