Files
UnrealEngine/Engine/Plugins/Experimental/Fracture/Source/FractureEngine/Private/FractureEngineClustering.cpp
2025-05-18 13:04:45 +08:00

1261 lines
42 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "FractureEngineClustering.h"
#include "GeometryCollection/GeometryCollection.h"
#include "GeometryCollection/GeometryCollectionUtility.h"
#include "GeometryCollection/Facades/CollectionTransformSelectionFacade.h"
#include "GeometryCollection/Facades/CollectionTransformFacade.h"
#include "GeometryCollection/Facades/CollectionHierarchyFacade.h"
#include "GeometryCollection/GeometryCollectionClusteringUtility.h"
#include "GeometryCollection/GeometryCollectionAlgo.h"
#include "Math/BoxSphereBounds.h"
#include "VertexConnectedComponents.h"
#include "CompGeom/ConvexDecomposition3.h"
#include "CompGeom/ConvexHull3.h"
namespace UE::Private::ClusterMagnet
{
struct FClusterMagnet
{
TSet<int32> ClusteredNodes;
TSet<int32> Connections;
};
TMap<int32, TSet<int32>> InitializeConnectivity(const TSet<int32>& TopNodes, FGeometryCollection* GeometryCollection, int32 OperatingLevel);
void CollectTopNodeConnections(FGeometryCollection* GeometryCollection, int32 Index, int32 OperatingLevel, TSet<int32>& OutConnections);
void SeparateClusterMagnets(const TSet<int32>& TopNodes, const TArray<int32>& Selection, const TMap<int32, TSet<int32>>& TopNodeConnectivity, TArray<FClusterMagnet>& OutClusterMagnets, TSet<int32>& OutRemainingPool);
bool AbsorbClusterNeighbors(const TMap<int32, TSet<int32>> TopNodeConnectivity, FClusterMagnet& OutClusterMagnets, TSet<int32>& OutRemainingPool);
TMap<int32, TSet<int32>> InitializeConnectivity(const TSet<int32>& TopNodes, FGeometryCollection* GeometryCollection, int32 OperatingLevel)
{
FGeometryCollectionProximityUtility ProximityUtility(GeometryCollection);
ProximityUtility.RequireProximity();
TMap<int32, TSet<int32>> ConnectivityMap;
for (int32 Index : TopNodes)
{
// Collect the proximity indices of all the leaf nodes under this top node,
// traced back up to its parent top node, so that all connectivity describes
// relationships only between top nodes.
TSet<int32> Connections;
CollectTopNodeConnections(GeometryCollection, Index, OperatingLevel, Connections);
Connections.Remove(Index);
// Remove any connections outside the current operating branch.
ConnectivityMap.Add(Index, Connections.Intersect(TopNodes));
}
return ConnectivityMap;
}
void CollectTopNodeConnections(FGeometryCollection* GeometryCollection, int32 Index, int32 OperatingLevel, TSet<int32>& OutConnections)
{
const TManagedArray<int32>& TransformToGeometryIndex = GeometryCollection->TransformToGeometryIndex;
if (GeometryCollection->SimulationType[Index] == FGeometryCollection::ESimulationTypes::FST_Rigid
&& TransformToGeometryIndex[Index] != INDEX_NONE) // rigid node with geometry, leaf of the simulated part
{
const TManagedArray<TSet<int32>>& Proximity = GeometryCollection->GetAttribute<TSet<int32>>("Proximity", FGeometryCollection::GeometryGroup);
const TManagedArray<int32>& GeometryToTransformIndex = GeometryCollection->TransformIndex;
for (int32 Neighbor : Proximity[TransformToGeometryIndex[Index]])
{
int32 NeighborTransformIndex = GeometryToTransformIndex[Neighbor];
OutConnections.Add(FGeometryCollectionClusteringUtility::GetParentOfBoneAtSpecifiedLevel(GeometryCollection, NeighborTransformIndex, OperatingLevel));
}
}
else
{
const TManagedArray<TSet<int32>>& Children = GeometryCollection->Children;
for (int32 ChildIndex : Children[Index])
{
CollectTopNodeConnections(GeometryCollection, ChildIndex, OperatingLevel, OutConnections);
}
}
}
void SeparateClusterMagnets(
const TSet<int32>& TopNodes,
const TArray<int32>& Selection,
const TMap<int32, TSet<int32>>& TopNodeConnectivity,
TArray<FClusterMagnet>& OutClusterMagnets,
TSet<int32>& OutRemainingPool)
{
OutClusterMagnets.Reserve(TopNodes.Num());
OutRemainingPool.Reserve(TopNodes.Num());
for (int32 Index : TopNodes)
{
if (Selection.Contains(Index))
{
OutClusterMagnets.AddDefaulted();
FClusterMagnet& NewMagnet = OutClusterMagnets.Last();
NewMagnet.ClusteredNodes.Add(Index);
NewMagnet.Connections = TopNodeConnectivity[Index];
}
else
{
OutRemainingPool.Add(Index);
}
}
}
bool AbsorbClusterNeighbors(const TMap<int32, TSet<int32>> TopNodeConnectivity, FClusterMagnet& OutClusterMagnet, TSet<int32>& OutRemainingPool)
{
// Return true if neighbors were absorbed.
bool bNeighborsAbsorbed = false;
TSet<int32> NewConnections;
for (int32 NeighborIndex : OutClusterMagnet.Connections)
{
// If the neighbor is still in the pool, absorb it and its connections.
if (OutRemainingPool.Contains(NeighborIndex))
{
OutClusterMagnet.ClusteredNodes.Add(NeighborIndex);
NewConnections.Append(TopNodeConnectivity[NeighborIndex]);
OutRemainingPool.Remove(NeighborIndex);
bNeighborsAbsorbed = true;
}
}
OutClusterMagnet.Connections.Append(NewConnections);
return bNeighborsAbsorbed;
}
}
FVoronoiPartitioner::FVoronoiPartitioner(const FGeometryCollection* GeometryCollection, int32 ClusterIndex)
{
// Collect children of cluster
const TManagedArray<TSet<int32>>& Children = GeometryCollection->Children;
TransformIndices = Children[ClusterIndex].Array();
GenerateCentroids(GeometryCollection);
}
void FVoronoiPartitioner::KMeansPartition(int32 InPartitionCount, int32 MaxIterations, TArrayView<const FVector> InitialCenters)
{
PartitionCount = InPartitionCount;
if (InitialCenters.Num())
{
PartitionCount = InitialCenters.Num();
}
InitializePartitions(InitialCenters);
// Note: We run max iterations + 1 here because the first iteration just applies the initial cluster assignment
for (int32 Iteration = 0; Iteration < MaxIterations + 1; Iteration++)
{
// Refinement is complete when no nodes change partition.
if (!Refine())
{
break;
}
}
}
void FVoronoiPartitioner::MergeSingleElementPartitions(FGeometryCollection* GeometryCollection)
{
if (Connectivity.IsEmpty())
{
GenerateConnectivity(GeometryCollection);
}
for (int32 ElIdx = 0; ElIdx < TransformIndices.Num(); ElIdx++)
{
int32 Partition = Partitions[ElIdx];
if (PartitionSize[Partition] == 1)
{
// Find the smallest neighboring partition to merge to
// (to help keep partition sizes balanced)
int32 SmallestNbrPartition = -1;
int32 SmallestNbrSize = TransformIndices.Num()+1;
for (int32 NbrEl : Connectivity[ElIdx])
{
int32 NbrPartition = Partitions[NbrEl];
if (NbrPartition == Partition)
{
continue;
}
int32 NbrSize = PartitionSize[NbrPartition];
if (NbrSize > 0 && (SmallestNbrPartition == -1 || NbrSize < SmallestNbrSize))
{
SmallestNbrPartition = NbrPartition;
SmallestNbrSize = NbrSize;
}
}
if (SmallestNbrPartition != -1)
{
Partitions[ElIdx] = SmallestNbrPartition;
PartitionSize[Partition]--;
PartitionSize[SmallestNbrPartition]++;
}
}
}
}
void FVoronoiPartitioner::MergeSmallPartitions(FGeometryCollection* GeometryCollection, float SizeThreshold)
{
if (Connectivity.IsEmpty())
{
GenerateConnectivity(GeometryCollection);
}
FGeometryCollectionConvexUtility::SetVolumeAttributes(GeometryCollection);
const TManagedArray<float>& Volumes = GeometryCollection->GetAttribute<float>("Volume", FTransformCollection::TransformGroup);
float VolumeThreshold = SizeThreshold * SizeThreshold * SizeThreshold;
TArray<float> PartitionVolumes;
PartitionVolumes.Init(0, PartitionSize.Num());
int32 NonEmptyPartitions = GetNonEmptyPartitionCount();
for (int32 ElIdx = 0; ElIdx < TransformIndices.Num(); ElIdx++)
{
int32 Partition = Partitions[ElIdx];
PartitionVolumes[Partition] += Volumes[TransformIndices[ElIdx]];
}
TArray<int32> ToConsider;
ToConsider.Reserve(Partitions.Num());
for (int32 Partition = 0; Partition < PartitionVolumes.Num(); ++Partition)
{
if (PartitionVolumes[Partition] < VolumeThreshold)
{
ToConsider.Emplace(Partition);
}
}
while (!ToConsider.IsEmpty())
{
int32 Partition = ToConsider.Pop(EAllowShrinking::No);
if (NonEmptyPartitions < 3)
{
break; // if we only have two partitions, stop merging
}
if (PartitionVolumes[Partition] < VolumeThreshold)
{
int32 SmallestNbrPartition = INDEX_NONE;
float SmallestNbrVolume = FMathf::MaxReal;
for (int32 ElIdx = 0; ElIdx < TransformIndices.Num(); ++ElIdx)
{
if (Partitions[ElIdx] == Partition)
{
for (int32 NbrEl : Connectivity[ElIdx])
{
int32 NbrPartition = Partitions[NbrEl];
if (NbrPartition != Partition)
{
if (SmallestNbrPartition == INDEX_NONE || PartitionVolumes[NbrPartition] < SmallestNbrVolume)
{
SmallestNbrVolume = PartitionVolumes[NbrPartition];
SmallestNbrPartition = NbrPartition;
}
}
}
}
}
if (SmallestNbrPartition != INDEX_NONE)
{
PartitionVolumes[SmallestNbrPartition] += PartitionVolumes[Partition];
PartitionVolumes[Partition] = 0;
float CombinedSize = PartitionSize[SmallestNbrPartition] + PartitionSize[Partition];
PartitionSize[SmallestNbrPartition] = CombinedSize;
if (CombinedSize < VolumeThreshold)
{
ToConsider.Push(SmallestNbrPartition);
Swap(ToConsider.Last(), ToConsider[0]); // make sure we don't always merge to the same cluster
}
PartitionSize[Partition] = 0;
NonEmptyPartitions--;
for (int32 ElIdx = 0; ElIdx < TransformIndices.Num(); ++ElIdx)
{
if (Partitions[ElIdx] == Partition)
{
Partitions[ElIdx] = SmallestNbrPartition;
}
}
}
}
}
}
void FVoronoiPartitioner::SplitDisconnectedPartitions(FGeometryCollection* GeometryCollection)
{
Visited.Init(false, TransformIndices.Num());
if (Connectivity.IsEmpty())
{
GenerateConnectivity(GeometryCollection);
}
for (int32 PartitionIndex = 0; PartitionIndex < PartitionCount; ++PartitionIndex)
{
int32 FirstElementInPartitionIndex = Partitions.Find(PartitionIndex);
if (FirstElementInPartitionIndex > INDEX_NONE)
{
MarkVisited(FirstElementInPartitionIndex, PartitionIndex);
// Place unvisited partition members in a new partition.
bool bFoundUnattached = false;
for (int32 Index = 0; Index < TransformIndices.Num(); ++Index)
{
if (Partitions[Index] == PartitionIndex)
{
if (!Visited[Index])
{
if (!bFoundUnattached)
{
bFoundUnattached = true;
PartitionCount++;
PartitionSize.Add(0);
}
Partitions[Index] = PartitionCount - 1;
PartitionSize[PartitionCount - 1]++;
PartitionSize[PartitionIndex]--;
}
}
}
}
}
}
void FVoronoiPartitioner::MarkVisited(int32 Index, int32 PartitionIndex)
{
Visited[Index] = true;
for (int32 Adjacent : Connectivity[Index])
{
if (!Visited[Adjacent] && Partitions[Adjacent] == PartitionIndex)
{
MarkVisited(Adjacent, PartitionIndex);
}
}
}
TArray<int32> FVoronoiPartitioner::GetPartition(int32 PartitionIndex) const
{
TArray<int32> PartitionContents;
PartitionContents.Reserve(TransformIndices.Num());
for (int32 Index = 0; Index < TransformIndices.Num(); ++Index)
{
if (Partitions[Index] == PartitionIndex)
{
PartitionContents.Add(TransformIndices[Index]);
}
}
return PartitionContents;
}
void FVoronoiPartitioner::GenerateConnectivity(const FGeometryCollection* GeometryCollection)
{
Connectivity.SetNum(TransformIndices.Num());
if (!ensure(GeometryCollection->HasAttribute("Level", FGeometryCollection::TransformGroup)))
{
return;
}
const TManagedArray<int32>& Levels = GeometryCollection->GetAttribute<int32>("Level", FGeometryCollection::TransformGroup);
for (int32 Index = 0; Index < TransformIndices.Num(); ++Index)
{
CollectConnections(GeometryCollection, TransformIndices[Index], Levels[TransformIndices[Index]], Connectivity[Index]);
}
}
void FVoronoiPartitioner::CollectConnections(const FGeometryCollection* GeometryCollection, int32 Index, int32 OperatingLevel, TSet<int32>& OutConnections) const
{
const TManagedArray<TSet<int32>>& Children = GeometryCollection->Children;
if (GeometryCollection->IsRigid(Index))
{
const TManagedArray<TSet<int32>>& Proximity = GeometryCollection->GetAttribute<TSet<int32>>("Proximity", FGeometryCollection::GeometryGroup);
const TManagedArray<int32>& GeometryToTransformIndex = GeometryCollection->TransformIndex;
const TManagedArray<int32>& TransformToGeometryIndex = GeometryCollection->TransformToGeometryIndex;
for (int32 Neighbor : Proximity[TransformToGeometryIndex[Index]])
{
int32 NeighborTransformIndex = FGeometryCollectionClusteringUtility::GetParentOfBoneAtSpecifiedLevel(GeometryCollection, GeometryToTransformIndex[Neighbor], OperatingLevel);
int32 ContextIndex = TransformIndices.Find(NeighborTransformIndex);
if (ContextIndex > INDEX_NONE)
{
OutConnections.Add(ContextIndex);
}
}
}
else
{
for (int32 ChildIndex : Children[Index])
{
CollectConnections(GeometryCollection, ChildIndex, OperatingLevel, OutConnections);
}
}
}
void FVoronoiPartitioner::GenerateCentroids(const FGeometryCollection* GeometryCollection)
{
Centroids.SetNum(TransformIndices.Num());
ParallelFor(TransformIndices.Num(), [this, GeometryCollection](int32 Index)
{
Centroids[Index] = GenerateCentroid(GeometryCollection, TransformIndices[Index]);
});
}
FVector FVoronoiPartitioner::GenerateCentroid(const FGeometryCollection* GeometryCollection, int32 TransformIndex) const
{
FBox Bounds = GenerateBounds(GeometryCollection, TransformIndex);
return Bounds.GetCenter();
}
FBox FVoronoiPartitioner::GenerateBounds(const FGeometryCollection* GeometryCollection, int32 TransformIndex)
{
check(GeometryCollection->IsRigid(TransformIndex) || GeometryCollection->IsClustered(TransformIndex));
// Return the bounds of all the vertices contained by this branch.
if (GeometryCollection->IsRigid(TransformIndex))
{
const TManagedArray<FTransform3f>& Transforms = GeometryCollection->Transform;
const TManagedArray<int32>& Parents = GeometryCollection->Parent;
FTransform3f GlobalTransform = GeometryCollectionAlgo::GlobalMatrix3f(Transforms, Parents, TransformIndex);
int32 GeometryIndex = GeometryCollection->TransformToGeometryIndex[TransformIndex];
int32 VertexStart = GeometryCollection->VertexStart[GeometryIndex];
int32 VertexCount = GeometryCollection->VertexCount[GeometryIndex];
const TManagedArray<FVector3f>& GCVertices = GeometryCollection->Vertex;
TArray<FVector> Vertices;
Vertices.SetNum(VertexCount);
for (int32 VertexOffset = 0; VertexOffset < VertexCount; ++VertexOffset)
{
Vertices[VertexOffset] = FVector(GlobalTransform.TransformPosition(GCVertices[VertexStart + VertexOffset]));
}
return FBox(Vertices);
}
else // recurse the cluster
{
const TArray<int32> Children = GeometryCollection->Children[TransformIndex].Array();
// Empty clusters are problematic
if (Children.Num() == 0)
{
return FBox();
}
FBox Bounds = GenerateBounds(GeometryCollection, Children[0]);
for (int32 ChildIndex = 1; ChildIndex < Children.Num(); ++ChildIndex)
{
Bounds += GenerateBounds(GeometryCollection, Children[ChildIndex]);
}
return Bounds;
}
}
void FVoronoiPartitioner::InitializePartitions(TArrayView<const FVector> InitialCenters)
{
check(PartitionCount > 0);
if (InitialCenters.Num() > 0)
{
PartitionCenters.Reset();
PartitionCenters.Append(InitialCenters);
}
else
{
PartitionCount = FMath::Min(PartitionCount, TransformIndices.Num());
// Set initial partition centers as selects from the vertex set
PartitionCenters.SetNum(PartitionCount);
int32 TransformStride = FMath::Max(1, FMath::FloorToInt(float(TransformIndices.Num()) / float(PartitionCount)));
for (int32 PartitionIndex = 0; PartitionIndex < PartitionCount; ++PartitionIndex)
{
PartitionCenters[PartitionIndex] = Centroids[TransformStride * PartitionIndex];
}
}
// At beginning, all nodes belong to first partition.
Partitions.Init(0, TransformIndices.Num());
PartitionSize.Init(0, PartitionCount);
if (PartitionCount > 0)
{
PartitionSize[0] = TransformIndices.Num();
}
}
bool FVoronoiPartitioner::Refine()
{
// Assign each transform to its closest partition center.
bool bUnchanged = true;
for (int32 Index = 0; Index < Centroids.Num(); ++Index)
{
int32 ClosestPartition = FindClosestPartitionCenter(Centroids[Index]);
if (ClosestPartition != Partitions[Index])
{
bUnchanged = false;
PartitionSize[Partitions[Index]]--;
PartitionSize[ClosestPartition]++;
Partitions[Index] = ClosestPartition;
}
}
if (bUnchanged)
{
return false;
}
// Recalculate partition centers
for (int32 PartitionIndex = 0; PartitionIndex < PartitionCount; ++PartitionIndex)
{
if (PartitionSize[PartitionIndex] > 0)
{
PartitionCenters[PartitionIndex] = FVector(0.0f, 0.0f, 0.0f);
}
}
for (int32 Index = 0; Index < Partitions.Num(); ++Index)
{
PartitionCenters[Partitions[Index]] += Centroids[Index];
}
for (int32 PartitionIndex = 0; PartitionIndex < PartitionCount; ++PartitionIndex)
{
if (PartitionSize[PartitionIndex] > 0)
{
PartitionCenters[PartitionIndex] /= float(PartitionSize[PartitionIndex]);
}
}
return true;
}
int32 FVoronoiPartitioner::FindClosestPartitionCenter(const FVector& Location) const
{
int32 ClosestPartition = INDEX_NONE;
float SmallestDistSquared = TNumericLimits<float>::Max();
for (int32 PartitionIndex = 0; PartitionIndex < PartitionCount; ++PartitionIndex)
{
float DistSquared = FVector::DistSquared(Location, PartitionCenters[PartitionIndex]);
if (DistSquared < SmallestDistSquared)
{
SmallestDistSquared = DistSquared;
ClosestPartition = PartitionIndex;
}
}
check(ClosestPartition > INDEX_NONE);
return ClosestPartition;
}
class FFractureEngineBoneSelection
{
public:
FFractureEngineBoneSelection(const FGeometryCollection& InGeometryCollection, const TArray<int32>& InSelectedBones)
: GeometryCollection(InGeometryCollection)
, SelectedBones(InSelectedBones)
{}
const TArray<int32>& GetSelectedBones() const { return SelectedBones; }
void ConvertSelectionToClusterNodes();
private:
bool IsValidBone(int32 Index) const;
bool HasSelectedAncestor(int32 Index) const;
void Sanitize(bool bFavorParents = true);
private:
const FGeometryCollection& GeometryCollection;
TArray<int32> SelectedBones;
};
bool FFractureEngineBoneSelection::IsValidBone(int32 Index) const
{
return Index >= 0 && Index < GeometryCollection.Parent.Num();
}
bool FFractureEngineBoneSelection::HasSelectedAncestor(int32 Index) const
{
const TManagedArray<int32>& Parents = GeometryCollection.GetAttribute<int32>("Parent", FGeometryCollection::TransformGroup);
if (!ensureMsgf(Index >= 0 && Index < Parents.Num(), TEXT("Invalid index in selection: %d"), Index))
{
return false;
}
int32 CurrIndex = Index;
while (Parents[CurrIndex] != INDEX_NONE)
{
CurrIndex = Parents[CurrIndex];
if (SelectedBones.Contains(CurrIndex))
{
return true;
}
}
// We've arrived at the top of the hierarchy with no selected ancestors
return false;
}
void FFractureEngineBoneSelection::Sanitize(bool bFavorParents)
{
// Ensure that selected indices are valid
int NumTransforms = GeometryCollection.NumElements(FGeometryCollection::TransformGroup);
SelectedBones.RemoveAll([NumTransforms](int32 Index) {
return Index == INDEX_NONE || !ensure(Index < NumTransforms);
});
// Ensure that children of a selected node are not also selected.
if (bFavorParents)
{
SelectedBones.RemoveAll([this](int32 Index) {
return !IsValidBone(Index) || HasSelectedAncestor(Index);
});
}
SelectedBones.Sort();
}
void FFractureEngineBoneSelection::ConvertSelectionToClusterNodes()
{
// If this is a non-cluster node, select the cluster containing it instead.
const TManagedArray<TSet<int32>>& Children = GeometryCollection.Children;
const TManagedArray<int32>& Parents = GeometryCollection.Parent;
const TManagedArray<int32>& SimulationType = GeometryCollection.SimulationType;
Sanitize();
TArray<int32> AddedClusterSelections;
for (int32 Index : SelectedBones)
{
int32 AddParent = FGeometryCollection::Invalid;
if (SimulationType[Index] == FGeometryCollection::ESimulationTypes::FST_Rigid)
{
AddParent = Parents[Index];
}
else if (SimulationType[Index] == FGeometryCollection::ESimulationTypes::FST_None)
{
AddParent = Parents[Parents[Index]];
}
if (AddParent != FGeometryCollection::Invalid)
{
AddedClusterSelections.AddUnique(AddParent);
}
}
SelectedBones.Append(AddedClusterSelections);
Sanitize();
}
void FFractureEngineClustering::AutoCluster(FGeometryCollection& GeometryCollection,
const TArray<int32>& BoneIndices,
const EFractureEngineClusterSizeMethod ClusterSizeMethod,
const uint32 SiteCount,
const float SiteCountFraction,
const float SiteSize,
const bool bEnforceConnectivity,
const bool bAvoidIsolated,
const bool bEnforceSiteParameters,
const int32 GridX,
const int32 GridY,
const int32 GridZ,
const float MinimumClusterSize,
const int32 KMeansIterations,
const bool bPreferConvexity,
const float ConcavityTolerance)
{
FFractureEngineBoneSelection Selection(GeometryCollection, BoneIndices);
Selection.ConvertSelectionToClusterNodes();
for (const int32 ClusterIndex : Selection.GetSelectedBones())
{
AutoCluster(GeometryCollection, ClusterIndex, ClusterSizeMethod, SiteCount, SiteCountFraction, SiteSize,
bEnforceConnectivity, bAvoidIsolated, bEnforceSiteParameters, GridX, GridY, GridZ, MinimumClusterSize,
KMeansIterations, bPreferConvexity, ConcavityTolerance);
}
}
void FFractureEngineClustering::ConvexityBasedCluster(FGeometryCollection& GeometryCollection,
int32 ClusterIndex,
uint32 SiteCount,
bool bEnforceConnectivity,
bool bAvoidIsolated,
float ConcavityTolerance
)
{
const bool bUseVolumesOfConvexHulls = true;
if (bUseVolumesOfConvexHulls)
{
FGeometryCollectionConvexUtility::SetVolumeAttributes(&GeometryCollection);
}
FGeometryCollectionProximityUtility ProximityUtility(&GeometryCollection);
ProximityUtility.UpdateProximity();
const TManagedArray<float>& GeometryVolumes = GeometryCollection.GetAttribute<float>("Volume", FTransformCollection::TransformGroup);
UE::Geometry::FConvexDecomposition3 ConvexDecomp;
const TSet<int32>& ChildrenSet = GeometryCollection.Children[ClusterIndex];
const TArray<int32> Children = ChildrenSet.Array();
UE::Geometry::FSizedDisjointSet Components(Children.Num());
Chaos::Facades::FCollectionHierarchyFacade HierarchyFacade(GeometryCollection);
HierarchyFacade.GenerateLevelAttribute();
const TManagedArray<int32>& Levels = GeometryCollection.GetAttribute<int32>("Level", FGeometryCollection::TransformGroup);
int32 Level = Levels[ClusterIndex];
// use source geometry as input convex vertices
TArray<int32> TransformVertexCounts;
TArray<int32> TransformVertexStarts;
TransformVertexCounts.SetNumUninitialized(Children.Num());
TransformVertexStarts.SetNumUninitialized(Children.Num());
TArray<double> Volumes;
Volumes.SetNumZeroed(Children.Num());
TArray<FVector3d> GlobalVertices;
TArray<int32> ToProcess;
TArray<FTransform> GlobalTransforms;
if (!bUseVolumesOfConvexHulls)
{
for (int32 ChildIdx = 0; ChildIdx < Children.Num(); ++ChildIdx)
{
Volumes[ChildIdx] = GeometryVolumes[Children[ChildIdx]];
}
}
GeometryCollectionAlgo::GlobalMatrices(GeometryCollection.Transform, GeometryCollection.Parent, GlobalTransforms);
for (int32 ChildIdx = 0; ChildIdx < Children.Num(); ++ChildIdx)
{
TransformVertexStarts[ChildIdx] = GlobalVertices.Num();
int32 TransformIdx = Children[ChildIdx];
ToProcess.Reset();
ToProcess.Add(TransformIdx);
double Volume = 0;
while (!ToProcess.IsEmpty())
{
int32 ProcessTransformIdx = ToProcess.Pop(EAllowShrinking::No);
FGeometryCollection::ESimulationTypes SimType = (FGeometryCollection::ESimulationTypes)GeometryCollection.SimulationType[ProcessTransformIdx];
if (SimType == FGeometryCollection::ESimulationTypes::FST_Clustered)
{
for (int32 ProcChild : GeometryCollection.Children[ProcessTransformIdx])
{
ToProcess.Add(ProcChild);
}
}
else if (SimType == FGeometryCollection::ESimulationTypes::FST_Rigid)
{
int32 ProcessGeometryIdx = GeometryCollection.TransformToGeometryIndex[ProcessTransformIdx];
if (ProcessGeometryIdx == INDEX_NONE)
{
continue;
}
int32 GCVStart = GeometryCollection.VertexStart[ProcessGeometryIdx];
int32 GCVEnd = GCVStart + GeometryCollection.VertexCount[ProcessGeometryIdx];
int32 GlobalStart = GlobalVertices.Num();
for (int32 VIdx = GCVStart; VIdx < GCVEnd; ++VIdx)
{
GlobalVertices.Add(GlobalTransforms[ProcessTransformIdx].TransformPosition((FVector3d)GeometryCollection.Vertex[VIdx]));
}
if (bUseVolumesOfConvexHulls)
{
double HullVolume = UE::Geometry::FConvexHull3d::ComputeVolume(TArrayView<const FVector3d>(&GlobalVertices[GlobalStart], GlobalVertices.Num() - GlobalStart));
Volumes[ChildIdx] += HullVolume;
}
}
}
TransformVertexCounts[ChildIdx] = GlobalVertices.Num() - TransformVertexStarts[ChildIdx];
}
TArray<TPair<int32, int32>> ChildrenProximity;
if (bEnforceConnectivity)
{
TMap<int32, TSet<int32>> Connectivity = UE::Private::ClusterMagnet::InitializeConnectivity(ChildrenSet, &GeometryCollection, Level + 1);
TMap<int32, int32> TransformIdxToChildrenIdx;
for (int32 LocalIdx = 0; LocalIdx < Children.Num(); ++LocalIdx)
{
TransformIdxToChildrenIdx.Add(Children[LocalIdx], LocalIdx);
}
for (const TPair<int32, TSet<int32>>& ConnectionsA : Connectivity)
{
int32 ChildIdxA = TransformIdxToChildrenIdx[ConnectionsA.Key];
const TSet<int32>& ConnectedTo = ConnectionsA.Value;
for (int32 TransformB : ConnectedTo)
{
int32 ChildIdxB = TransformIdxToChildrenIdx[TransformB];
ChildrenProximity.Emplace(ChildIdxA, ChildIdxB);
}
}
}
ConvexDecomp.InitializeFromHulls(Children.Num(),
[&](int32 Idx)->double { return Volumes[Idx]; },
[&](int32 Idx)->int32 { return TransformVertexCounts[Idx]; },
[&](int32 Idx, int32 VertIdx) -> FVector3d { return GlobalVertices[TransformVertexStarts[Idx] + VertIdx]; },
ChildrenProximity);
if (!bEnforceConnectivity)
{
// If we're not getting connectivity from the geometry collection, add it based on bounding box overlaps
ConvexDecomp.InitializeProximityFromDecompositionBoundingBoxOverlaps(1, .5, 1);
}
UE::Geometry::FConvexDecomposition3::FMergeSettings MergeSettings;
MergeSettings.TargetNumParts = SiteCount;
double TotalVolume = 0;
for (double Volume : Volumes)
{
TotalVolume += Volume;
}
MergeSettings.ErrorTolerance = ConcavityTolerance;
MergeSettings.bErrorToleranceOverridesNumParts = false;
double MaxVolumeToMerge = TotalVolume / SiteCount; // stop merging parts that are larger than their share of the volume ...
double MinVolumeToSkip = .2 * TotalVolume / SiteCount; // except still allow merging very small parts into large parts
MergeSettings.CustomAllowMergeParts =
[MaxVolumeToMerge, MinVolumeToSkip](const UE::Geometry::FConvexDecomposition3::FConvexPart& PartA, const UE::Geometry::FConvexDecomposition3::FConvexPart& PartB)
{
if (PartA.bMustMerge || PartB.bMustMerge)
{
return true;
}
return ((PartA.SumHullsVolume < MaxVolumeToMerge && PartB.SumHullsVolume < MaxVolumeToMerge) ||
(PartA.SumHullsVolume < MinVolumeToSkip || PartB.SumHullsVolume < MinVolumeToSkip));
};
MergeSettings.MergeCallback = [&Components](int32 ChildA, int32 ChildB)
{
Components.Union(ChildA, ChildB);
};
ConvexDecomp.MergeBest(MergeSettings);
TArray<int32> CompactIdxToGroupID, GroupIDToCompactIdx;
int32 PartitionCount = Components.CompactedGroupIndexToGroupID(&CompactIdxToGroupID, &GroupIDToCompactIdx);
TArray<int32> NewCluster;
int32 NewClusterIndexStart = GeometryCollection.AddElements(PartitionCount, FGeometryCollection::TransformGroup);
bool bHasEmptyClusters = false;
for (int32 Index = 0; Index < PartitionCount; ++Index)
{
NewCluster.Reset();
int32 GroupID = CompactIdxToGroupID[Index];
for (int32 ChildIdx = 0; ChildIdx < Children.Num(); ++ChildIdx)
{
if (Components.Find(ChildIdx) == GroupID)
{
NewCluster.Add(Children[ChildIdx]);
}
}
if (bAvoidIsolated && NewCluster.Num() == 1)
{
bHasEmptyClusters = true;
NewCluster.Reset();
}
int32 NewClusterIndex = NewClusterIndexStart + Index;
GeometryCollection.Parent[NewClusterIndex] = ClusterIndex;
GeometryCollection.Children[ClusterIndex].Add(NewClusterIndex);
GeometryCollection.BoneName[NewClusterIndex] = "ClusterBone";
GeometryCollection.Children[NewClusterIndex] = TSet<int32>(NewCluster);
GeometryCollection.SimulationType[NewClusterIndex] = FGeometryCollection::ESimulationTypes::FST_Clustered;
GeometryCollection.Transform[NewClusterIndex] = FTransform3f::Identity;
GeometryCollectionAlgo::ParentTransforms(&GeometryCollection, NewClusterIndex, NewCluster);
}
if (bHasEmptyClusters)
{
FGeometryCollectionClusteringUtility::RemoveDanglingClusters(&GeometryCollection);
}
FGeometryCollectionClusteringUtility::UpdateHierarchyLevelOfChildren(&GeometryCollection, ClusterIndex);
FGeometryCollectionClusteringUtility::RecursivelyUpdateChildBoneNames(ClusterIndex, GeometryCollection.Children, GeometryCollection.BoneName);
FGeometryCollectionClusteringUtility::ValidateResults(&GeometryCollection);
}
void FFractureEngineClustering::AutoCluster(FGeometryCollection& GeometryCollection,
const int32 ClusterIndex,
const EFractureEngineClusterSizeMethod ClusterSizeMethod,
const uint32 SiteCount,
const float SiteCountFraction,
const float SiteSize,
const bool bEnforceConnectivity,
const bool bAvoidIsolated,
const bool bEnforceSiteParameters,
const int32 InGridX,
const int32 InGridY,
const int32 InGridZ,
const float MinimumClusterSize,
const int32 KMeansIterations,
const bool bPreferConvexity,
const float ConcavityTolerance)
{
// Used by the ByGrid method, which manually distributes initial positions
TArray<FVector> PartitionPositions;
int32 DesiredSiteCountToUse = 1;
int32 IterationsToUse = KMeansIterations;
int32 NumChildren = GeometryCollection.Children[ClusterIndex].Num();
if (ClusterSizeMethod == EFractureEngineClusterSizeMethod::ByNumber)
{
DesiredSiteCountToUse = SiteCount;
}
else if (ClusterSizeMethod == EFractureEngineClusterSizeMethod::ByFractionOfInput)
{
DesiredSiteCountToUse = FMath::Max(2, NumChildren * SiteCountFraction);
}
else if (ClusterSizeMethod == EFractureEngineClusterSizeMethod::BySize)
{
float TotalVolume = GeometryCollection.GetBoundingBox().GetBox().GetVolume();
float DesiredVolume = FMath::Pow(SiteSize, 3);
DesiredSiteCountToUse = FMath::Max(1, TotalVolume / DesiredVolume);
}
else if (ClusterSizeMethod == EFractureEngineClusterSizeMethod::ByGrid)
{
PartitionPositions = GenerateGridSites(GeometryCollection, ClusterIndex, InGridX, InGridY, InGridZ);
DesiredSiteCountToUse = PartitionPositions.Num();
}
if (bPreferConvexity)
{
ConvexityBasedCluster(GeometryCollection, ClusterIndex, SiteCount, bEnforceConnectivity, bAvoidIsolated, ConcavityTolerance);
return;
}
FVoronoiPartitioner VoronoiPartition(&GeometryCollection, ClusterIndex);
// Stop if we only want one cluster or there aren't enough children to do any clustering
if (DesiredSiteCountToUse <= 1 || NumChildren <= 1)
{
return;
}
bool bNeedsVolume = MinimumClusterSize > 0;
if (bNeedsVolume)
{
FGeometryCollectionConvexUtility::SetVolumeAttributes(&GeometryCollection);
}
int32 SiteCountToUse = DesiredSiteCountToUse;
int32 PreviousPartitionCount = TNumericLimits<int32>::Max();
bool bIterate = false;
do
{
VoronoiPartition.KMeansPartition(SiteCountToUse, IterationsToUse, PartitionPositions);
if (VoronoiPartition.GetPartitionCount() == 0)
{
return;
}
bool bNeedProximity = bEnforceConnectivity || bAvoidIsolated;
if (bNeedProximity)
{
FGeometryCollectionProximityUtility ProximityUtility(&GeometryCollection);
ProximityUtility.UpdateProximity();
}
if (bEnforceConnectivity)
{
VoronoiPartition.SplitDisconnectedPartitions(&GeometryCollection);
}
// Note: Previously if bAvoidIsolated was set, we'd attempt to merge clusters of one here via
// VoronoiPartition.MergeSingleElementPartitions(&GeometryCollection);
// However this results in some overly-large clusters, especially with grid clustering,
// so we prefer to simply skip clustering in such cases.
if (MinimumClusterSize > 0)
{
// attempt to remove isolated via cluster merging (may not succeed, as it only merges if there is a cluster in proximity)
VoronoiPartition.MergeSmallPartitions(&GeometryCollection, MinimumClusterSize);
}
const int32 IsolatedPartitionToIgnore = bAvoidIsolated ? VoronoiPartition.GetIsolatedPartitionCount() : 0;
const int32 PartitionCount = VoronoiPartition.GetNonEmptyPartitionCount() - IsolatedPartitionToIgnore;
bIterate = bEnforceSiteParameters // Is the feature enabled?
&& PartitionPositions.IsEmpty() // Is the explicit position array empty? (otherwise, site count is ignored)
&& (PartitionCount > DesiredSiteCountToUse) // Have we reached the desired outcome
&& (SiteCountToUse > 1) // Is SiteCount large enough ?
&& (PartitionCount < PreviousPartitionCount);// Are we progressing ?
if(bIterate)
{
SiteCountToUse--;
}
} while (bIterate);
int32 NonEmptyPartitionCount = VoronoiPartition.GetNonEmptyPartitionCount();
bool bHasEmptyClusters = NonEmptyPartitionCount > 0;
if (bAvoidIsolated && NonEmptyPartitionCount == 1)
{
return;
}
int32 PartitionCount = VoronoiPartition.GetPartitionCount();
int32 NewClusterIndexStart = GeometryCollection.AddElements(PartitionCount, FGeometryCollection::TransformGroup);
for (int32 Index = 0; Index < PartitionCount; ++Index)
{
TArray<int32> NewCluster = VoronoiPartition.GetPartition(Index);
if (bAvoidIsolated && NewCluster.Num() == 1)
{
bHasEmptyClusters = true;
NewCluster.Reset();
}
int32 NewClusterIndex = NewClusterIndexStart + Index;
GeometryCollection.Parent[NewClusterIndex] = ClusterIndex;
GeometryCollection.Children[ClusterIndex].Add(NewClusterIndex);
GeometryCollection.BoneName[NewClusterIndex] = "ClusterBone";
GeometryCollection.Children[NewClusterIndex] = TSet<int32>(NewCluster);
GeometryCollection.SimulationType[NewClusterIndex] = FGeometryCollection::ESimulationTypes::FST_Clustered;
GeometryCollection.Transform[NewClusterIndex] = FTransform3f::Identity;
GeometryCollectionAlgo::ParentTransforms(&GeometryCollection, NewClusterIndex, NewCluster);
}
if (bHasEmptyClusters)
{
FGeometryCollectionClusteringUtility::RemoveDanglingClusters(&GeometryCollection);
}
FGeometryCollectionClusteringUtility::UpdateHierarchyLevelOfChildren(&GeometryCollection, ClusterIndex);
FGeometryCollectionClusteringUtility::RecursivelyUpdateChildBoneNames(ClusterIndex, GeometryCollection.Children, GeometryCollection.BoneName);
FGeometryCollectionClusteringUtility::ValidateResults(&GeometryCollection);
}
TArray<FVector> FFractureEngineClustering::GenerateGridSites(
const FGeometryCollection& GeometryCollection,
const TArray<int32>& BoneIndices,
const int32 GridX,
const int32 GridY,
const int32 GridZ)
{
TArray<FVector> AllPoints;
FFractureEngineBoneSelection Selection(GeometryCollection, BoneIndices);
Selection.ConvertSelectionToClusterNodes();
for (const int32 ClusterIndex : Selection.GetSelectedBones())
{
AllPoints.Append(GenerateGridSites(GeometryCollection, ClusterIndex, GridX, GridY, GridZ));
}
return AllPoints;
}
TArray<FVector> FFractureEngineClustering::GenerateGridSites(
const FGeometryCollection& GeometryCollection,
const int32 ClusterIndex,
const int32 InGridX,
const int32 InGridY,
const int32 InGridZ,
FBox* OutBounds)
{
TArray<FVector> PartitionPositions;
FBox Bounds = FVoronoiPartitioner::GenerateBounds(&GeometryCollection, ClusterIndex);
if (OutBounds)
{
*OutBounds = Bounds;
}
int32 GridX = FMath::Max(1, InGridX);
int32 GridY = FMath::Max(1, InGridY);
int32 GridZ = FMath::Max(1, InGridZ);
PartitionPositions.Reserve(GridX * GridY * GridZ);
auto StepToParam = [](int32 Step, int32 NumSteps) -> double
{
return (double(Step) + .5) / double(NumSteps);
};
for (int32 StepX = 0; StepX < GridX; ++StepX)
{
double Xt = StepToParam(StepX, GridX);
double X = FMath::Lerp(Bounds.Min.X, Bounds.Max.X, Xt);
for (int32 StepY = 0; StepY < GridY; ++StepY)
{
double Yt = StepToParam(StepY, GridY);
double Y = FMath::Lerp(Bounds.Min.Y, Bounds.Max.Y, Yt);
for (int32 StepZ = 0; StepZ < GridZ; ++StepZ)
{
double Zt = StepToParam(StepZ, GridZ);
double Z = FMath::Lerp(Bounds.Min.Z, Bounds.Max.Z, Zt);
PartitionPositions.Emplace(X, Y, Z);
}
}
}
return PartitionPositions;
}
bool FFractureEngineClustering::ClusterSelected(
FGeometryCollection& GeometryCollection,
TArray<int32>& Selection)
{
if (GeometryCollection.NumElements(FGeometryCollection::TransformGroup) == 0)
{
return false;
}
GeometryCollection::Facades::FCollectionTransformSelectionFacade SelectionFacade(GeometryCollection);
SelectionFacade.RemoveRootNodes(Selection);
SelectionFacade.Sanitize(Selection);
if (Selection.Num() <= 1)
{
// Don't make a cluster of 1
return false;
}
Chaos::Facades::FCollectionHierarchyFacade HierarchyFacade(GeometryCollection);
int32 StartTransformCount = GeometryCollection.NumElements(FGeometryCollection::TransformGroup);
int32 LowestCommonAncestor = FGeometryCollectionClusteringUtility::FindLowestCommonAncestor(&GeometryCollection, Selection);
// Cluster selected bones beneath common parent
if (LowestCommonAncestor != INDEX_NONE)
{
FGeometryCollectionClusteringUtility::ClusterBonesUnderNewNodeWithParent(&GeometryCollection, LowestCommonAncestor, Selection, true);
GeometryCollection::GenerateTemporaryGuids(&GeometryCollection, StartTransformCount, true);
FGeometryCollectionClusteringUtility::RemoveDanglingClusters(&GeometryCollection);
HierarchyFacade.GenerateLevelAttribute();
return true;
}
return false;
}
bool FFractureEngineClustering::MergeSelectedClusters(FGeometryCollection& GeometryCollection, TArray<int32>& Selection)
{
Chaos::Facades::FCollectionHierarchyFacade HierarchyFacade(GeometryCollection);
const int32 NumTransforms = GeometryCollection.NumElements(FGeometryCollection::TransformGroup);
if (Selection.Num() > NumTransforms)
{
return false;
}
GeometryCollection::Facades::FCollectionTransformSelectionFacade SelectionFacade(GeometryCollection);
SelectionFacade.ConvertEmbeddedSelectionToParents(Selection); // embedded geo must stay attached to parent
// Collect children of context clusters
TArray<int32> ChildBones;
int32 StartTransformCount = GeometryCollection.NumElements(FGeometryCollection::TransformGroup);
ChildBones.Reserve(StartTransformCount);
bool bHasClusters = false;
for (int32 Select : Selection)
{
if (GeometryCollection.SimulationType[Select] == FGeometryCollection::ESimulationTypes::FST_Clustered)
{
bHasClusters = true;
ChildBones.Append(HierarchyFacade.GetChildrenAsArray(Select));
}
else
{
ChildBones.Add(Select);
}
}
// If there were no clusters in the selection, we create a cluster
if (!bHasClusters)
{
// Cluster selected bones beneath common parent
int32 LowestCommonAncestor = FGeometryCollectionClusteringUtility::FindLowestCommonAncestor(&GeometryCollection, Selection);
if (LowestCommonAncestor != INDEX_NONE)
{
FGeometryCollectionClusteringUtility::ClusterBonesUnderNewNodeWithParent(&GeometryCollection, LowestCommonAncestor, Selection, true);
GeometryCollection::GenerateTemporaryGuids(&GeometryCollection, StartTransformCount, true);
Selection.SetNum(1);
Selection[0] = LowestCommonAncestor;
if (FGeometryCollectionClusteringUtility::RemoveDanglingClusters(&GeometryCollection))
{
// Bone index is unreliable after removal of some bones
Selection.Empty();
}
HierarchyFacade.GenerateLevelAttribute();
return true;
}
}
else
{
int32 MergeNode = FGeometryCollectionClusteringUtility::PickBestNodeToMergeTo(&GeometryCollection, Selection);
if (MergeNode >= 0)
{
FGeometryCollectionClusteringUtility::ClusterBonesUnderExistingNode(&GeometryCollection, MergeNode, ChildBones);
Selection.SetNum(1);
Selection[0] = MergeNode;
if (FGeometryCollectionClusteringUtility::RemoveDanglingClusters(&GeometryCollection))
{
// Bone index is unreliable after removal of some bones
Selection.Empty();
}
HierarchyFacade.GenerateLevelAttribute();
return true;
}
}
return false;
}
bool FFractureEngineClustering::ClusterMagnet(
FGeometryCollection& GeometryCollection,
TArray<int32>& InOutSelection,
int32 Iterations
)
{
using namespace UE::Private::ClusterMagnet;
if (GeometryCollection.NumElements(FGeometryCollection::TransformGroup) == 0)
{
return false;
}
Chaos::Facades::FCollectionHierarchyFacade HierarchyFacade(GeometryCollection);
HierarchyFacade.GenerateLevelAttribute();
const TManagedArray<int32>& Levels = GeometryCollection.GetAttribute<int32>("Level", FGeometryCollection::TransformGroup);
GeometryCollection::Facades::FCollectionTransformSelectionFacade SelectionFacade(GeometryCollection);
SelectionFacade.Sanitize(InOutSelection);
SelectionFacade.ConvertEmbeddedSelectionToParents(InOutSelection); // embedded geo must stay attached to parent
const TManagedArray<TSet<int32>>& Children = GeometryCollection.Children;
TMap<int32, TArray<int32>> ClusteredSelection = SelectionFacade.GetClusteredSelections(InOutSelection);
for (TPair<int32, TArray<int32>>& Group : ClusteredSelection)
{
if (Group.Key == INDEX_NONE) // Group is top level
{
continue;
}
// We have the connections for the leaf nodes of our geometry collection. We want to percolate those up to the top nodes.
TMap<int32, TSet<int32>> TopNodeConnectivity = InitializeConnectivity(Children[Group.Key], &GeometryCollection, Levels[Group.Key]+1);
// Separate the top nodes into cluster magnets and a pool of available nodes.
TArray<FClusterMagnet> ClusterMagnets;
TSet<int32> RemainingPool;
SeparateClusterMagnets(Children[Group.Key], Group.Value, TopNodeConnectivity, ClusterMagnets, RemainingPool);
for (int32 Iteration = 0; Iteration < Iterations; ++Iteration)
{
bool bNeighborsAbsorbed = false;
// each cluster gathers adjacent nodes from the pool
for (FClusterMagnet& ClusterMagnet : ClusterMagnets)
{
bNeighborsAbsorbed |= AbsorbClusterNeighbors(TopNodeConnectivity, ClusterMagnet, RemainingPool);
}
// early termination
if (!bNeighborsAbsorbed)
{
break;
}
}
// Create new clusters from the cluster magnets
for (const FClusterMagnet& ClusterMagnet : ClusterMagnets)
{
if (ClusterMagnet.ClusteredNodes.Num() > 1)
{
TArray<int32> NewChildren = ClusterMagnet.ClusteredNodes.Array();
NewChildren.Sort();
FGeometryCollectionClusteringUtility::ClusterBonesUnderNewNode(&GeometryCollection, NewChildren[0], NewChildren, false, false);
}
}
}
return true;
}