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

431 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "FractureEngineUtility.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "FractureEngineSelection.h"
#include "Dataflow/DataflowSelection.h"
#include "GeometryCollection/GeometryCollectionClusteringUtility.h"
#include "GeometryCollection/GeometryCollectionAlgo.h"
#include "GeometryCollection/TransformCollection.h"
#include "Voronoi/Voronoi.h"
#include "PlanarCut.h"
#include "GeometryCollection/Facades/CollectionMeshFacade.h"
#include "Algo/RemoveIf.h"
#include "GeometryCollection/Facades/CollectionTransformSelectionFacade.h"
#include "GeometryCollection/GeometryCollectionClusteringUtility.h"
void FFractureEngineUtility::ConvertBoxToVertexAndTriangleData(const FBox& InBox, TArray<FVector3f>& OutVertices, TArray<FIntVector>& OutTriangles)
{
const int32 NumVertices = 8;
const int32 NumTriangles = 12;
OutVertices.AddUninitialized(NumVertices);
OutTriangles.AddUninitialized(NumTriangles);
FVector Min = InBox.Min;
FVector Max = InBox.Max;
// Add vertices
OutVertices[0] = FVector3f(Min);
OutVertices[1] = FVector3f(Max.X, Min.Y, Min.Z);
OutVertices[2] = FVector3f(Max.X, Max.Y, Min.Z);
OutVertices[3] = FVector3f(Min.X, Max.Y, Min.Z);
OutVertices[4] = FVector3f(Min.X, Min.Y, Max.Z);
OutVertices[5] = FVector3f(Max.X, Min.Y, Max.Z);
OutVertices[6] = FVector3f(Max);
OutVertices[7] = FVector3f(Min.X, Max.Y, Max.Z);
// Add triangles
OutTriangles[0] = FIntVector(0, 1, 3); OutTriangles[1] = FIntVector(1, 2, 3);
OutTriangles[2] = FIntVector(0, 4, 1); OutTriangles[3] = FIntVector(4, 5, 1);
OutTriangles[4] = FIntVector(5, 2, 1); OutTriangles[5] = FIntVector(5, 6, 2);
OutTriangles[6] = FIntVector(3, 2, 6); OutTriangles[7] = FIntVector(7, 3, 6);
OutTriangles[8] = FIntVector(0, 3, 7); OutTriangles[9] = FIntVector(4, 0, 7);
OutTriangles[10] = FIntVector(5, 4, 7); OutTriangles[11] = FIntVector(5, 7, 6);
}
void FFractureEngineUtility::ConstructMesh(UE::Geometry::FDynamicMesh3& OutMesh, const TArray<FVector3f>& InVertices, const TArray<FIntVector>& InTriangles)
{
for (int32 VertexIdx = 0; VertexIdx < InVertices.Num(); ++VertexIdx)
{
OutMesh.AppendVertex(FVector(InVertices[VertexIdx]));
}
int GroupID = 0;
for (int32 TriangleIdx = 0; TriangleIdx < InTriangles.Num(); ++TriangleIdx)
{
OutMesh.AppendTriangle(InTriangles[TriangleIdx].X, InTriangles[TriangleIdx].Y, InTriangles[TriangleIdx].Z, GroupID);
}
}
void FFractureEngineUtility::DeconstructMesh(const UE::Geometry::FDynamicMesh3& InMesh, TArray<FVector3f>& OutVertices, TArray<FIntVector>& OutTriangles)
{
const int32 NumVertices = InMesh.VertexCount();
const int32 NumTriangles = InMesh.TriangleCount();
if (NumVertices > 0 && NumTriangles > 0)
{
// This will contain the valid triangles only
OutTriangles.Reserve(InMesh.TriangleCount());
// DynamicMesh.TrianglesItr() returns the valid triangles only
for (UE::Geometry::FIndex3i Tri : InMesh.TrianglesItr())
{
OutTriangles.Add(FIntVector(Tri.A, Tri.B, Tri.C));
}
// This will contain all the vertices (invalid ones too)
// Otherwise the IDs need to be remaped
OutVertices.AddZeroed(InMesh.MaxVertexID());
// DynamicMesh.VertexIndicesItr() returns the valid vertices only
for (int32 VertexID : InMesh.VertexIndicesItr())
{
OutVertices[VertexID] = (FVector3f)InMesh.GetVertex(VertexID);
}
}
}
namespace UE::Dataflow::Private
{
static constexpr double VolDimScale = .01; // compute volumes in meters instead of cm, for saner units at typical scales
}
static double GetTotalVolume(const FGeometryCollection& Collection, const TArray<double>& Volumes)
{
double Sum = 0.0;
if (Volumes.Num() != Collection.Transform.Num())
{
return 0.0f;
}
for (int32 BoneIdx = 0, NumBones = Collection.Transform.Num(); BoneIdx < NumBones; ++BoneIdx)
{
if (Collection.SimulationType[BoneIdx] == FGeometryCollection::ESimulationTypes::FST_Rigid)
{
Sum += Volumes[BoneIdx];
}
}
return Sum;
}
static double GetMinVolume(double TotalVolume,
const EFixTinyGeoGeometrySelectionMethod InSelectionMethod,
const float InMinVolumeCubeRoot,
const float InRelativeVolume)
{
double MinVolume = 0.0;
if (InSelectionMethod == EFixTinyGeoGeometrySelectionMethod::VolumeCubeRoot)
{
MinVolume = InMinVolumeCubeRoot * UE::Dataflow::Private::VolDimScale;
MinVolume = MinVolume * MinVolume * MinVolume;
}
else // EGeometrySelectionMethod::RelativeVolume
{
MinVolume = FMath::Pow(TotalVolume, 1.0 / 3.0) * InRelativeVolume;
MinVolume = MinVolume * MinVolume * MinVolume;
}
return MinVolume;
}
static bool CollectTargetBones(FGeometryCollection& Collection,
const TArray<int32>& Selection,
TArray<int32>& OutSmallIndices,
TArray<double>& OutVolumes,
double& OutMinVolume,
const EFixTinyGeoMergeType InMergeType,
const bool InOnFractureLevel,
const EFixTinyGeoGeometrySelectionMethod InSelectionMethod,
const float InMinVolumeCubeRoot,
const float InRelativeVolume,
const EFixTinyGeoUseBoneSelection InUseBoneSelection,
const bool InOnlyClusters)
{
const bool bClusterMode = InMergeType == EFixTinyGeoMergeType::MergeClusters;
const bool bRestrictToLevel = bClusterMode && InOnFractureLevel;
int32 InFractureLevel = -1;
FindBoneVolumes(
Collection,
TArrayView<int32>(), /*Empty array => use all transforms*/
OutVolumes,
UE::Dataflow::Private::VolDimScale,
bClusterMode
);
double TotalVolume = GetTotalVolume(Collection, OutVolumes);
OutMinVolume = GetMinVolume(TotalVolume, InSelectionMethod, InMinVolumeCubeRoot, InRelativeVolume);
TArray<int32> FilteredSelection;
GeometryCollection::Facades::FCollectionTransformSelectionFacade SelectionFacade(Collection);
if (InUseBoneSelection == EFixTinyGeoUseBoneSelection::OnlyMergeSelected)
{
if (!bClusterMode)
{
OutSmallIndices = Selection;
SelectionFacade.ConvertSelectionToRigidNodes(OutSmallIndices);
}
else
{
OutSmallIndices = Selection;
}
return !OutSmallIndices.IsEmpty();
}
FindSmallBones(
Collection,
TArrayView<int32>(),
OutVolumes,
OutMinVolume,
OutSmallIndices,
bClusterMode
);
// Filter bones that aren't at the target level
const int32 TargetLevel = InFractureLevel;
const TManagedArray<int32>* LevelAttrib = Collection.FindAttribute<int32>("Level", FGeometryCollection::TransformGroup);
bool bFilterByLevel = bRestrictToLevel && TargetLevel > -1 && LevelAttrib;
if (bFilterByLevel)
{
OutSmallIndices.SetNum(Algo::RemoveIf(OutSmallIndices, [&](int32 BoneIdx)
{
return (*LevelAttrib)[BoneIdx] != TargetLevel;
}));
}
// Filter bones that aren't clusters
if (bClusterMode && (!bFilterByLevel || InOnlyClusters))
{
OutSmallIndices.SetNum(Algo::RemoveIf(OutSmallIndices, [&](int32 BoneIdx)
{
return Collection.SimulationType[BoneIdx] != FGeometryCollection::ESimulationTypes::FST_Clustered;
}));
}
if (InUseBoneSelection == EFixTinyGeoUseBoneSelection::AlsoMergeSelected)
{
TArray<int32> ProcessedSelection = Selection;
if (!bClusterMode)
{
SelectionFacade.ConvertSelectionToRigidNodes(ProcessedSelection);
}
for (int32 Bone : ProcessedSelection)
{
OutSmallIndices.AddUnique(Bone);
}
}
return !OutSmallIndices.IsEmpty();
}
void FFractureEngineUtility::FixTinyGeo(FManagedArrayCollection& InOutCollection,
FDataflowTransformSelection InTransformSelection,
const EFixTinyGeoMergeType InMergeType,
const bool InOnFractureLevel,
const EFixTinyGeoGeometrySelectionMethod InSelectionMethod,
const float InMinVolumeCubeRoot,
const float InRelativeVolume,
const EFixTinyGeoUseBoneSelection InUseBoneSelection,
const bool InOnlyClusters,
const EFixTinyGeoNeighborSelectionMethod InNeighborSelection,
const bool InOnlyToConnected,
const bool InOnlySameParent,
const bool bUseCollectionProximity)
{
if (TUniquePtr<FGeometryCollection> GeomCollection = TUniquePtr<FGeometryCollection>(InOutCollection.NewCopy<FGeometryCollection>()))
{
const TArray<int32> TransformSelectionArr = InTransformSelection.AsArray();
TArray<int32> SmallIndices;
TArray<double> Volumes;
double MinVolume;
if (CollectTargetBones(*GeomCollection,
TransformSelectionArr,
SmallIndices,
Volumes,
MinVolume,
InMergeType,
InOnFractureLevel,
InSelectionMethod,
InMinVolumeCubeRoot,
InRelativeVolume,
InUseBoneSelection,
InOnlyClusters))
{
// convert ENeighborSelectionMethod to the PlanarCut module version (the only difference is it's not a UENUM)
UE::PlanarCut::ENeighborSelectionMethod SelectionMethod = UE::PlanarCut::ENeighborSelectionMethod::LargestNeighbor;
if (InNeighborSelection == EFixTinyGeoNeighborSelectionMethod::NearestCenter)
{
SelectionMethod = UE::PlanarCut::ENeighborSelectionMethod::NearestCenter;
}
// Make sure we have non-stale proximity data if we will use it
if (bUseCollectionProximity)
{
FGeometryCollectionProximityUtility ProximityUtility(GeomCollection.Get());
ProximityUtility.UpdateProximity();
}
if (InMergeType == EFixTinyGeoMergeType::MergeGeometry)
{
MergeBones(*GeomCollection,
TArrayView<const int32>(), // empty view == consider all bones
Volumes,
MinVolume,
SmallIndices,
false, /*bUnionJoinedPieces*/ // Note: Union-ing the pieces is nicer in theory, but can leave cracks and non-manifold garbage
SelectionMethod,
bUseCollectionProximity);
}
else
{
MergeClusters(*GeomCollection,
Volumes,
MinVolume,
SmallIndices,
SelectionMethod,
InOnlyToConnected,
InOnlySameParent,
bUseCollectionProximity);
}
InOutCollection = (const FManagedArrayCollection&)(*GeomCollection);
}
}
}
void FFractureEngineUtility::RecomputeNormalsInGeometryCollection(FManagedArrayCollection& InOutCollection,
FDataflowTransformSelection InTransformSelection,
const bool InOnlyTangents,
const bool InRecomputeSharpEdges,
const float InSharpEdgeAngleThreshold,
const bool InOnlyInternalSurfaces)
{
if (TUniquePtr<FGeometryCollection> GeomCollection = TUniquePtr<FGeometryCollection>(InOutCollection.NewCopy<FGeometryCollection>()))
{
TArray<int32> TransformSelectionArr = InTransformSelection.AsArray();
RecomputeNormalsAndTangents(InOnlyTangents, InRecomputeSharpEdges, InSharpEdgeAngleThreshold, *GeomCollection, TransformSelectionArr, InOnlyInternalSurfaces);
InOutCollection = (const FManagedArrayCollection&)(*GeomCollection);
}
}
int32 FFractureEngineUtility::ResampleGeometryCollection(FManagedArrayCollection& InOutCollection,
FDataflowTransformSelection InTransformSelection,
const float InCollisionSampleSpacing)
{
if (TUniquePtr<FGeometryCollection> GeomCollection = TUniquePtr<FGeometryCollection>(InOutCollection.NewCopy<FGeometryCollection>()))
{
TArray<int32> TransformSelectionArr = InTransformSelection.AsArray();
AddCollisionSampleVertices(InCollisionSampleSpacing, *GeomCollection, TransformSelectionArr);
InOutCollection = (const FManagedArrayCollection&)(*GeomCollection);
}
return INDEX_NONE;
}
static void AddSingleRootNodeIfRequired(TUniquePtr<FGeometryCollection>& InOutCollection)
{
if (InOutCollection)
{
if (FGeometryCollectionClusteringUtility::ContainsMultipleRootBones(InOutCollection.Get()))
{
FGeometryCollectionClusteringUtility::ClusterAllBonesUnderNewRoot(InOutCollection.Get());
}
}
}
DECLARE_LOG_CATEGORY_EXTERN(LogFractureEngineUtility, Log, All);
DEFINE_LOG_CATEGORY(LogFractureEngineUtility);
void FFractureEngineUtility::ValidateGeometryCollection(FManagedArrayCollection& InOutCollection,
const bool InRemoveUnreferencedGeometry,
const bool InRemoveClustersOfOne,
const bool InRemoveDanglingClusters)
{
if (TUniquePtr<FGeometryCollection> GeometryCollection = TUniquePtr<FGeometryCollection>(InOutCollection.NewCopy<FGeometryCollection>()))
{
bool bDirty = false;
TManagedArray<int32>& TransformToGeometry = GeometryCollection->TransformToGeometryIndex;
constexpr bool bClustersCanHaveGeometry = true;
if (!bClustersCanHaveGeometry)
{
// Optionally ensure that clusters do not point to geometry (currently disabled; keeping the geometry can be useful)
const int32 ElementCount = TransformToGeometry.Num();
for (int32 Idx = 0; Idx < ElementCount; ++Idx)
{
if (GeometryCollection->IsClustered(Idx) && TransformToGeometry[Idx] != INDEX_NONE)
{
TransformToGeometry[Idx] = INDEX_NONE;
UE_LOG(LogFractureEngineUtility, Verbose, TEXT("Removed geometry index from cluster %d."), Idx);
bDirty = true;
}
}
}
// Remove any unreferenced geometry
if (InRemoveUnreferencedGeometry)
{
TManagedArray<int32>& TransformIndex = GeometryCollection->TransformIndex;
const int32 GeometryCount = TransformIndex.Num();
TArray<int32> RemoveGeometry;
RemoveGeometry.Reserve(GeometryCount);
for (int32 Idx = 0; Idx < GeometryCount; ++Idx)
{
if ((TransformIndex[Idx] == INDEX_NONE) || (TransformToGeometry[TransformIndex[Idx]] != Idx))
{
RemoveGeometry.Add(Idx);
UE_LOG(LogFractureEngineUtility, Verbose, TEXT("Removed dangling geometry at index %d."), Idx);
bDirty = true;
}
}
if (RemoveGeometry.Num() > 0)
{
FManagedArrayCollection::FProcessingParameters Params;
Params.bDoValidation = false; // for perf reasons
GeometryCollection->RemoveElements(FGeometryCollection::GeometryGroup, RemoveGeometry);
}
}
if (InRemoveClustersOfOne)
{
if (FGeometryCollectionClusteringUtility::RemoveClustersOfOnlyOneChild(GeometryCollection.Get()))
{
UE_LOG(LogFractureEngineUtility, Verbose, TEXT("Removed one or more clusters of only one child."));
bDirty = true;
}
}
if (InRemoveDanglingClusters)
{
if (FGeometryCollectionClusteringUtility::RemoveDanglingClusters(GeometryCollection.Get()))
{
UE_LOG(LogFractureEngineUtility, Verbose, TEXT("Removed one or more dangling clusters."));
bDirty = true;
}
}
if (bDirty)
{
FGeometryCollectionClusteringUtility::UpdateHierarchyLevelOfChildren(*GeometryCollection, -1);
AddSingleRootNodeIfRequired(GeometryCollection);
}
InOutCollection = (const FManagedArrayCollection&)(*GeometryCollection);
}
}