// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "GeometryCollection/GeometryCollection.h" #include "DynamicMesh/DynamicMesh3.h" #define UE_API PLANARCUT_API class FVoronoiDiagram; namespace GeometryCollection::Facades { class FCollectionMeshFacade; } struct FMeshDescription; class FProgressCancel; // Offsets to randomize where Perlin noise is sampled for each dimension of a noise vector struct FNoiseOffsets { FNoiseOffsets(FRandomStream& RandomStream) : X(RandomStream.VRand() * RandomOffsetScale), Y(RandomStream.VRand() * RandomOffsetScale), Z(RandomStream.VRand() * RandomOffsetScale) { } FNoiseOffsets() {} void SetOffsets(FRandomStream& RandomStream) { X = RandomStream.VRand() * RandomOffsetScale; Y = RandomStream.VRand() * RandomOffsetScale; Z = RandomStream.VRand() * RandomOffsetScale; } FVector X, Y, Z; private: // Offset scale chosen to keep reasonably small offsets while still de-correlating the noise in each dimension. static constexpr double RandomOffsetScale = 100.0; }; struct FNoiseSettings { float Amplitude = 2; float Frequency = .1; int32 Octaves = 4; float PointSpacing = 1; float Lacunarity = 2; float Persistence = .5; FVector NoiseVector(const FVector& Pos, const FNoiseOffsets& Offsets) const { FVector Base = Pos * Frequency; return FVector( OctaveNoise(Base + Offsets.X), OctaveNoise(Base + Offsets.Y), OctaveNoise(Base + Offsets.Z) ) * Amplitude; } float OctaveNoise(const FVector& V) const { float NoiseValue = 0; float FreqScale = 1; float AmpScale = 1; for (int32 Octave = 0; Octave < Octaves; Octave++, FreqScale *= Lacunarity, AmpScale *= Persistence) { NoiseValue += FMath::PerlinNoise3D(V * FreqScale) * AmpScale; } return NoiseValue; } }; // auxiliary structure for FPlanarCells to carry material info struct FInternalSurfaceMaterials { int32 GlobalMaterialID = 0; bool bGlobalVisibility = true; float GlobalUVScale = 1; TOptional NoiseSettings; // if unset, noise will not be added // TODO: add optional overrides per facet / per cell /** * @param Collection Reference collection to use for setting UV scale * @param GeometryIdx Reference geometry inside collection; if -1, use all geometries in collection */ UE_API void SetUVScaleFromCollection(const GeometryCollection::Facades::FCollectionMeshFacade& CollectionMesh, int32 GeometryIdx = -1); UE_API int32 GetDefaultMaterialIDForGeometry(const FGeometryCollection& Collection, int32 GeometryIdx = -1) const; }; // Stores planar facets that divide space into cells struct FPlanarCells { FPlanarCells() { } UE_API FPlanarCells(const FPlane& Plane); UE_API FPlanarCells(const TArrayView Sites, FVoronoiDiagram &Voronoi); UE_API FPlanarCells(const TArrayView Boxes, bool bResolveAdjacencies = false); UE_API FPlanarCells(const FBox &Region, const FIntVector& CubesPerAxis); UE_API FPlanarCells(const FBox &Region, const TArrayView Image, int32 Width, int32 Height); int32 NumCells; bool AssumeConvexCells = false; // enables optimizations in this common case (can create incorrect geometry if set to true when cells are not actually convex) TArray Planes; TArray> PlaneCells; // the two cells neighboring each plane, w/ the cell on the negative side of the plane first, positive side second TArray> PlaneBoundaries; TArray PlaneBoundaryVertices; FInternalSurfaceMaterials InternalSurfaceMaterials; // Discards cells from the diagram, used to support visualization of a subset of cells UE_API void DiscardCells(TFunctionRef KeepFunc, bool bKeepNeighbors); /** * @return true if this is a single, unbounded cutting plane */ bool IsInfinitePlane() const { return NumCells == 2 && Planes.Num() == 1 && PlaneBoundaries[0].Num() == 0; } /** * Debugging function to check that the plane boundary vertices are wound to match the orientation of the plane normal vectors * @return false if any plane boundary vertices are found in the 'wrong' orientation relative to the plane normal */ bool HasValidPlaneBoundaryOrientations() const { for (int32 PlaneIdx = 0; PlaneIdx < PlaneBoundaries.Num(); PlaneIdx++) { const TArray& Bdry = PlaneBoundaries[PlaneIdx]; if (Bdry.Num() < 3) { continue; } const FPlane &P = Planes[PlaneIdx]; FVector N(P.X, P.Y, P.Z); if (!N.IsNormalized()) // plane normals should be normalized { return false; } const FVector& A = PlaneBoundaryVertices[Bdry[0]]; const FVector& B = PlaneBoundaryVertices[Bdry[1]]; const FVector& C = PlaneBoundaryVertices[Bdry[2]]; FVector E1 = B - A; FVector E2 = C - B; FVector NormalDir = E2 ^ E1; for (int32 VIdx : Bdry) { const double SD = P.PlaneDot(PlaneBoundaryVertices[VIdx]); if (FMath::Abs(SD) > 1e-4) { return false; // vertices should be on plane! } } if (AssumeConvexCells && FVector::DotProduct(NormalDir, N) < 0) // vectors aren't pointing the same way at all? { return false; } const double AngleMeasure = (NormalDir ^ N).SizeSquared(); if (AngleMeasure > 1e-3) // vectors aren't directionally aligned? { return false; } } return true; } inline void AddPlane(const FPlane &P, int32 CellIdxBehind, int32 CellIdxInFront) { Planes.Add(P); PlaneCells.Emplace(CellIdxBehind, CellIdxInFront); PlaneBoundaries.Emplace(); } inline int32 AddPlane(const FPlane &P, int32 CellIdxBehind, int32 CellIdxInFront, const TArray& PlaneBoundary) { int32 PlaneIdx = Planes.Add(P); PlaneCells.Emplace(CellIdxBehind, CellIdxInFront); PlaneBoundaries.Add(PlaneBoundary); return PlaneIdx; } void SetNoise(FNoiseSettings Noise = FNoiseSettings()) { InternalSurfaceMaterials.NoiseSettings = Noise; } }; /** * Cut a Geometry inside a GeometryCollection with PlanarCells, and add each cut cell back to the GeometryCollection as a new child of the input Geometry. For geometries that would not be cut, nothing is added. * * @param Cells Defines the cutting planes and division of space * @param Collection The collection to be cut * @param TransformIdx Which transform inside the collection to cut * @param Grout Separation to leave between cutting cells * @param CollisionSampleSpacing Target spacing between collision sample vertices * @param RandomSeed Seed to be used for random noise displacement * @param TransformCollection Optional transform of the whole geometry collection; if unset, defaults to Identity * @param bIncludeOutsideCellInOutput If true, geometry that was not inside any of the cells (e.g. was outside of the bounds of all cutting geometry) will still be included in the output; if false, it will be discarded. * @param Progress Optionally tracks progress and supports early-cancel * @param CellsOrigin Optionally provide a local origin of the cutting Cells * @param bSplitIslands Whether to detect connected components and split them apart after the cut * @return index of first new geometry in the Output GeometryCollection, or -1 if no geometry was added */ int32 PLANARCUT_API CutWithPlanarCells( FPlanarCells &Cells, FGeometryCollection& Collection, int32 TransformIdx, double Grout, double CollisionSampleSpacing, int32 RandomSeed, const TOptional& TransformCollection = TOptional(), bool bIncludeOutsideCellInOutput = true, bool bSetDefaultInternalMaterialsFromCollection = true, FProgressCancel* Progress = nullptr, FVector CellsOrigin = FVector::ZeroVector, bool bSplitIslands = true ); /** * Generate cutting mesh surfaces as a single mesh * Useful for creating a preview of the cut surface. * * @param Cells Defines the cutting planes and division of space * @param Grout Separation to leave between cutting cells * @param RandomSeed Seed to be used for random noise displacement * @param OutCuttingMeshes Dynamic mesh representing the preview surface * @param FilterCellsFunc Filter which cells should be included in the preview * @param TransformCollection Optional transform that would be applied to the to-cut surface; if unset, defaults to Identity * @param Progress Optionally tracks progress and supports early-cancel * @param CellsOrigin Optionally provide a local origin of the cutting Cells */ void PLANARCUT_API CreateCuttingSurfacePreview( const FPlanarCells& Cells, const FBox& Bounds, double Grout, int32 RandomSeed, UE::Geometry::FDynamicMesh3& OutCuttingMeshes, TFunctionRef FilterCellsFunc, const TOptional& TransformCollection = TOptional(), FProgressCancel* Progress = nullptr, FVector CellsOrigin = FVector::ZeroVector ); /** * Cut multiple Geometry groups inside a GeometryCollection with PlanarCells, and add each cut cell back to the GeometryCollection as a new child of their source Geometry. For geometries that would not be cut, nothing is added. * * @param Cells Defines the cutting planes and division of space * @param Collection The collection to be cut * @param TransformIndices Which transform groups inside the collection to cut * @param Grout Separation to leave between cutting cells * @param CollisionSampleSpacing Target spacing between collision sample vertices * @param RandomSeed Seed to be used for random noise displacement * @param TransformCollection Optional transform of the whole geometry collection; if unset, defaults to Identity * @param bIncludeOutsideCellInOutput If true, geometry that was not inside any of the cells (e.g. was outside of the bounds of all cutting geometry) will still be included in the output; if false, it will be discarded. * @param Progress Optionally tracks progress and supports early-cancel * @param CellsOrigin Optionally provide a local origin of the cutting Cells * @param bSplitIslands Whether to detect connected components and split them apart after the cut * @return index of first new geometry in the Output GeometryCollection, or -1 if no geometry was added */ int32 PLANARCUT_API CutMultipleWithPlanarCells( FPlanarCells &Cells, FGeometryCollection& Collection, const TArrayView& TransformIndices, double Grout, double CollisionSampleSpacing, int32 RandomSeed, const TOptional& TransformCollection = TOptional(), bool bIncludeOutsideCellInOutput = true, bool bSetDefaultInternalMaterialsFromCollection = true, FProgressCancel* Progress = nullptr, FVector CellsOrigin = FVector::ZeroVector, bool bSplitIslands = true ); /** * Split the geometry at the given transforms into their connected components * * @param Collection The collection to be cut * @param TransformIndices Which transform groups inside the collection to cut * @param CollisionSampleSpacing Target spacing between collision sample vertices * @param Progress Optionally tracks progress and supports early-cancel * @return index of first new geometry in the Output GeometryCollection, or -1 if no geometry was added */ int32 PLANARCUT_API SplitIslands( FGeometryCollection& Collection, const TArrayView& TransformIndices, double CollisionSampleSpacing, FProgressCancel* Progress = nullptr ); /** * Cut multiple Geometry groups inside a GeometryCollection with Planes, and add each cut cell back to the GeometryCollection as a new child of their source Geometry. For geometries that would not be cut, nothing is added. * * @param Planes Defines the cutting planes and division of space * @param InternalSurfaceMaterials Defines material properties for any added internal surfaces * @param Collection The collection to be cut * @param TransformIndices Which transform groups inside the collection to cut * @param Grout Separation to leave between cutting cells * @param CollisionSampleSpacing Target spacing between collision sample vertices * @param RandomSeed Seed to be used for random noise displacement * @param TransformCollection Optional transform of the whole geometry collection; if unset, defaults to Identity * @param Progress Optionally tracks progress and supports early-cancel * @param bSplitIslands Whether to detect connected components and split them apart after the cut * @return index of first new geometry in the Output GeometryCollection, or -1 if no geometry was added */ int32 PLANARCUT_API CutMultipleWithMultiplePlanes( const TArrayView& Planes, FInternalSurfaceMaterials& InternalSurfaceMaterials, FGeometryCollection& Collection, const TArrayView& TransformIndices, double Grout, double CollisionSampleSpacing, int32 RandomSeed, const TOptional& TransformCollection = TOptional(), bool bSetDefaultInternalMaterialsFromCollection = true, FProgressCancel* Progress = nullptr, bool bSplitIslands = true ); /** * Populate an array of transform indices w/ those that are smaller than a threshold volume * * @param Collection The collection to be processed * @param TransformIndices The transform indices to process, or empty if all should be processed * @param OutVolumes Output array, to be filled w/ volumes of geometry; 1:1 w/ TransformIndices array * @param ScalePerDimension Scale to apply per dimension (e.g. 1/100 converts volume from centimeters^3 to meters^3) * @param bIncludeClusters Whether to set the volumes for clusters as a sum of the child volumes. Otherwise, cluster volumes are set to zero. */ void PLANARCUT_API FindBoneVolumes( FGeometryCollection& Collection, const TArrayView& TransformIndices, TArray& OutVolumes, double ScalePerDimension = .01, bool bIncludeClusters = false ); /** * Populate an array of transform indices w/ those that are smaller than a threshold volume * * @param Collection The collection to be processed * @param TransformIndices The transform indices to process, or empty if all should be processed * @param Volumes Volumes of geometry; 1:1 w/ TransformIndices array * @param MinVolume Geometry smaller than this quantity will be chosen * @param OutSmallBones Output array, to be filled with transform indices for small pieces of geometry * @param bIncludeClusters Whether to include clusters in the search */ void PLANARCUT_API FindSmallBones( const FGeometryCollection& Collection, const TArrayView& TransformIndices, const TArrayView& Volumes, double MinVolume, TArray& OutSmallBones, bool bIncludeClusters = false ); /** * Populate an array of transform indices w/ those that match a custom volume-based filter * * @param Collection The collection to be processed * @param TransformIndices The transform indices to process, or empty if all should be processed * @param Volumes Volumes of geometry; 1:1 w/ TransformIndices array * @param Filter Geometry for which the volume filter returns true will be chosen * @param OutSmallBones Output array, to be filled with transform indices for small pieces of geometry * @param bIncludeClusters Whether to allow clusters in the filter */ void PLANARCUT_API FilterBonesByVolume( const FGeometryCollection& Collection, const TArrayView& TransformIndices, const TArrayView& Volumes, TFunctionRef Filter, TArray& OutSmallBones, bool bIncludeClusters = false ); namespace UE { namespace PlanarCut { enum ENeighborSelectionMethod { LargestNeighbor, NearestCenter }; } } /** * Merge chosen geometry into neighboring geometry. * * @param Collection The collection to be processed * @param TransformIndices The transform indices to process, or empty if all should be processed * @param Volumes Volumes of geometry; 1:1 w/ TransformIndices array * @param MinVolume If merged small geometry is larger than this, it will not require further merging * @param SmallTransformIndices Transform indices of pieces that we want to merge * @param bUnionJoinedPieces Try to 'union' the merged pieces, removing internal triangles and connecting the shared cut boundary * @param NeighborSelectionMethod How to choose which neighbor to merge to * @param bUseCollectionProximity If true, will use the proximity data on the collection to decide which pieces are allowed to merge. Otherwise, a reasonable default proximity will be computed and used. */ int32 PLANARCUT_API MergeBones( FGeometryCollection& Collection, const TArrayView& TransformIndices, const TArrayView& Volumes, double MinVolume, const TArrayView& SmallTransformIndices, bool bUnionJoinedPieces, UE::PlanarCut::ENeighborSelectionMethod NeighborSelectionMethod, bool bUseCollectionProximity = true ); /** * Merge chosen bones into neighboring clusters. * * @param Collection The collection to be processed * @param Volumes Volumes of geometry; 1:1 w/ TransformIndices array * @param MinVolume If merged small geometry is larger than this, it will not require further merging * @param SmallTransformIndices Transform indices of pieces that we want to merge * @param NeighborSelectionMethod How to choose which neighbor to merge to * @param bOnlyMergeInProximity Whether to only follow proximity graph, or to fall back to nearest center when for when no proximity links are found * @param bOnlySameParent Whether to only merge nodes that have the same parent in the hierarchy * @param bUseCollectionProximity If true, will use the proximity data on the collection to decide which pieces are allowed to merge. Otherwise, a reasonable default proximity will be computed and used. */ void PLANARCUT_API MergeClusters( FGeometryCollection& Collection, const TArrayView& Volumes, double MinVolume, const TArrayView& SmallTransformIndices, UE::PlanarCut::ENeighborSelectionMethod NeighborSelectionMethod, bool bOnlyMergeInProximity, bool bOnlySameParent, bool bUseCollectionProximity = true ); /** * Merge all chosen nodes into a single node. Unlike MergeBones(), does not account for proximity and will always only merge the selected bones. * * @param Collection The collection to be processed * @param TransformIndices The transform indices to process, or empty if all should be processed * @param bUnionJoinedPieces Try to 'union' the merged pieces, removing internal triangles and connecting the shared cut boundary */ void PLANARCUT_API MergeAllSelectedBones( FGeometryCollection& Collection, const TArrayView& TransformIndices, bool bUnionJoinedPieces ); /** * Recompute normals and tangents of selected geometry, optionally restricted to faces with odd or given material IDs (i.e. to target internal faces) * * @param bOnlyTangents If true, leave normals unchanged and only recompute tangent&bitangent vectors * @param bMakeSharpEdges If true, recompute the normal topology to split normals at 'sharp' edges * @param SharpAngleDegrees If bMakeSharpEdges, edges w/ adjacent triangle normals deviating by more than this threshold will be sharp edges (w/ split normals) * @param Collection The Geometry Collection to be updated * @param TransformIndices Which transform groups on the Geometry Collection to be updated. If empty, all groups are updated. * @param bOnlyOddMaterials If true, restrict recomputation to odd-numbered material IDs * @param WhichMaterials If non-empty, restrict recomputation to only the listed material IDs */ void PLANARCUT_API RecomputeNormalsAndTangents(bool bOnlyTangents, bool bMakeSharpEdges, float SharpAngleDegrees, FGeometryCollection& Collection, const TArrayView& TransformIndices = TArrayView(), bool bOnlyInternalSurfaces = true); /** * Scatter additional vertices (w/ no associated triangle) as needed to satisfy minimum point spacing * * @param TargetSpacing The desired spacing between collision sample vertices * @param Collection The Geometry Collection to be updated * @param TransformIndices Which transform groups on the Geometry Collection to be updated. If empty, all groups are updated. * @return Index of first transform group w/ updated geometry. (To update geometry we delete and re-add, because geometry collection isn't designed for in-place updates) */ int32 PLANARCUT_API AddCollisionSampleVertices(double TargetSpacing, FGeometryCollection& Collection, const TArrayView& TransformIndices = TArrayView()); /** * Cut multiple Geometry groups inside a GeometryCollection with a mesh, and add each cut cell back to the GeometryCollection as a new child of their source Geometry. For geometries that would not be cut, nothing is added. * * @param CuttingMesh Mesh to be used to cut the geometry collection * @param CuttingMeshTransform Position of cutting mesh * @param InternalSurfaceMaterials Defines material properties for any added internal surfaces * @param Collection The collection to be cut * @param TransformIndices Which transform groups inside the collection to cut * @param CollisionSampleSpacing Target spacing between collision sample vertices * @param TransformCollection Optional transform of the collection; if unset, defaults to Identity * @param Progress Optionally tracks progress and supports early-cancel * @param bSplitIslands Whether to detect connected components and split them apart after the cut * @return index of first new geometry in the Output GeometryCollection, or -1 if no geometry was added */ int32 PLANARCUT_API CutWithMesh( const UE::Geometry::FDynamicMesh3& CuttingMesh, FTransform CuttingMeshTransform, FInternalSurfaceMaterials& InternalSurfaceMaterials, FGeometryCollection& Collection, const TArrayView& TransformIndices, double CollisionSampleSpacing, const TOptional& TransformCollection = TOptional(), bool bSetDefaultInternalMaterialsFromCollection = true, FProgressCancel* Progress = nullptr, bool bSplitIslands = true ); /// Convert a mesh description to a dynamic mesh *specifically* augmented / designed for cutting geometry collections, to be passed to CutWithMesh UE::Geometry::FDynamicMesh3 PLANARCUT_API ConvertMeshDescriptionToCuttingDynamicMesh(const FMeshDescription* CuttingMesh, int32 NumUVLayers, FProgressCancel* Progress = nullptr); inline int32 CutWithMesh( const FMeshDescription* CuttingMesh, FTransform CuttingMeshTransform, FInternalSurfaceMaterials& InternalSurfaceMaterials, FGeometryCollection& Collection, const TArrayView& TransformIndices, double CollisionSampleSpacing, const TOptional& TransformCollection = TOptional(), bool bSetDefaultInternalMaterialsFromCollection = true, FProgressCancel* Progress = nullptr, bool bSplitIslands = true ) { int32 NumUVLayers = Collection.NumUVLayers(); return CutWithMesh(ConvertMeshDescriptionToCuttingDynamicMesh(CuttingMesh, NumUVLayers, Progress), CuttingMeshTransform, InternalSurfaceMaterials, Collection, TransformIndices, CollisionSampleSpacing, TransformCollection, bSetDefaultInternalMaterialsFromCollection, Progress, bSplitIslands ); } /** * Convert chosen Geometry groups inside a GeometryCollection to a single Mesh Description. * * @param OutputMesh Mesh to be filled with the geometry collection geometry * @param TransformOut Transform taking output mesh geometry to local space of geometry collection * @param bCenterPivot Whether to center the geometry at the origin * @param Collection The collection to be converted * @param TransformIndices Which transform groups inside the collection to convert * @param RemapMaterialIDs Optional function to remap (MaterialID, bIsInternal) -> NewMaterialID */ void PLANARCUT_API ConvertToMeshDescription( FMeshDescription& OutputMesh, FTransform& TransformOut, bool bCenterPivot, FGeometryCollection& Collection, const TManagedArray& BoneTransforms, const TArrayView& TransformIndices, TFunction RemapMaterialIDs = nullptr ); /** * Convert chosen Geometry groups inside a GeometryCollection to a single Mesh Description. * * @param OutputMesh Mesh to be filled with the geometry collection geometry * @param TransformOut Transform taking output mesh geometry to local space of geometry collection * @param bCenterPivot Whether to center the geometry at the origin * @param Collection The collection to be converted * @param TransformIndices Which transform groups inside the collection to convert * @param RemapMaterialIDs Optional function to remap (MaterialID, bIsInternal) -> NewMaterialID */ void PLANARCUT_API ConvertToMeshDescription( FMeshDescription& OutputMesh, FTransform& TransformOut, bool bCenterPivot, FGeometryCollection& Collection, const TManagedArray& BoneTransforms, const TArrayView& TransformIndices, TFunction RemapMaterialIDs = nullptr ); /** * Convert chosen Geometry groups inside a GeometryCollection to a single Dynamic Mesh. * * @param OutputMesh Mesh to be filled with the geometry collection geometry * @param TransformOut Transform taking output mesh geometry to local space of geometry collection. This will be Identity unless bCenterPivot is true. * @param bCenterPivot Whether to center the geometry at the origin * @param Collection The collection to be converted * @param bWeldEdges Whether to weld edges on conversion * @param BoneTransforms Transforms for each bone in the geometry collection * @param bUseRelativeTransforms Whether the BoneTransforms are relative to each bone's parent, or in a shared space (e.g., Collection.Transform is relative, UGeometryCollectionComponent's component space transforms are not) * @param TransformIndices Which transform groups inside the collection to convert * @param RemapMaterialIDs Optional function to remap (MaterialID, bIsInternal) -> NewMaterialID * @param bAllowInvisible Allow mesh to include triangles that are not marked 'visible' (typically, pre-fracture geometry stored for potential history/LOD usage) * @param bSetPolygroupPerBone Enable PolyGroups on the OutputMesh and set one group per geometry */ void PLANARCUT_API ConvertGeometryCollectionToDynamicMesh( UE::Geometry::FDynamicMesh3& OutputMesh, FTransform& TransformOut, bool bCenterPivot, const FGeometryCollection& Collection, bool bWeldEdges, TArrayView BoneTransforms, bool bUseRelativeTransforms, TArrayView TransformIndices, TFunction RemapMaterialIDs = nullptr, bool bAllowInvisible = false, bool bSetPolygroupPerBone = true ); #undef UE_API