// Copyright Epic Games, Inc. All Rights Reserved. #include "PlanarCut.h" #include "GeometryCollection/Facades/CollectionMeshFacade.h" #include "PlanarCutPlugin.h" #include "Spatial/SparseDynamicOctree3.h" #include "Util/IndexUtil.h" #include "GeometryCollection/GeometryCollectionAlgo.h" #include "GeometryCollection/GeometryCollectionClusteringUtility.h" #include "GeometryCollection/GeometryCollectionConvexUtility.h" #include "DynamicMeshEditor.h" #include "DynamicMesh/MeshTransforms.h" #include "Operations/MeshBoolean.h" #include "DynamicMesh/Operations/MergeCoincidentMeshEdges.h" #include "DynamicMesh/MeshNormals.h" #include "DynamicMesh/MeshTangents.h" #include "StaticMeshOperations.h" #include "MeshDescriptionToDynamicMesh.h" #include "DynamicMeshToMeshDescription.h" #include "Algo/Reverse.h" #include "GeometryMeshConversion.h" #include "Voronoi/Voronoi.h" using namespace UE::Geometry; using namespace UE::PlanarCut; #define LOCTEXT_NAMESPACE "PlanarCut" namespace PlanarCut_Locals { FVector SeparateTranslation(const FTransform& Transform, FTransform& OutCenteredTransform) { FVector Translation = Transform.GetTranslation(); OutCenteredTransform = FTransform(Transform.GetRotation(), FVector::ZeroVector, Transform.GetScale3D()); return Translation; } } // logic from FMeshUtility::GenerateGeometryCollectionFromBlastChunk, sets material IDs based on construction pattern that external materials have even IDs and are matched to internal materials at InternalID = ExternalID+1 int32 FInternalSurfaceMaterials::GetDefaultMaterialIDForGeometry(const FGeometryCollection& Collection, int32 GeometryIdx) const { auto FindMostCommonInternalMaterial = [](const FGeometryCollection& InCollection, int32 InFaceStart, int32 InFaceEnd) -> int32 { // find most common internal material TMap MaterialIDCount; int32 MaxCount = 0; int32 MostCommonMaterialID = INDEX_NONE; const TManagedArray& MaterialID = InCollection.MaterialID; const TManagedArray& Internal = InCollection.Internal; for (int i = InFaceStart; i < InFaceEnd; ++i) { if (!Internal[i]) { continue; } int32 CurrID = MaterialID[i]; int32& CurrCount = MaterialIDCount.FindOrAdd(CurrID); CurrCount++; if (CurrCount > MaxCount) { MaxCount = CurrCount; MostCommonMaterialID = CurrID; } } return MostCommonMaterialID; }; int32 MostCommonMaterialID = INDEX_NONE; int32 AllFaceStart = 0; int32 AllFaceEnd = Collection.Indices.Num(); if (GeometryIdx > -1) { int32 GeoFaceStart = Collection.FaceStart[GeometryIdx]; int32 GeoFaceEnd = Collection.FaceCount[GeometryIdx] + Collection.FaceStart[GeometryIdx]; MostCommonMaterialID = FindMostCommonInternalMaterial(Collection, GeoFaceStart, GeoFaceEnd); } if (MostCommonMaterialID == INDEX_NONE) { MostCommonMaterialID = FindMostCommonInternalMaterial(Collection, AllFaceStart, AllFaceEnd); // Failed to find any internal material IDs; default to the first material if (MostCommonMaterialID == INDEX_NONE) { MostCommonMaterialID = 0; } } return MostCommonMaterialID; } void FInternalSurfaceMaterials::SetUVScaleFromCollection(const GeometryCollection::Facades::FCollectionMeshFacade& CollectionMesh, int32 GeometryIdx) { const auto& VertexArray = CollectionMesh.VertexAttribute.Get(); const auto& UV0Array = CollectionMesh.GetUVLayer(0); const auto& IndicesArray = CollectionMesh.IndicesAttribute.Get(); const auto& FaceStartArray = CollectionMesh.FaceStartAttribute.Get(); const auto& FaceCountArray = CollectionMesh.FaceCountAttribute.Get(); int32 FaceStart = 0; int32 FaceEnd = IndicesArray.Num(); if (GeometryIdx > -1) { FaceStart = FaceStartArray[GeometryIdx]; FaceEnd = FaceCountArray[GeometryIdx] + FaceStartArray[GeometryIdx]; } double UVDistance = 0; float WorldDistance = 0; for (int32 FaceIdx = FaceStart; FaceIdx < FaceEnd; FaceIdx++) { const FIntVector& Tri = IndicesArray[FaceIdx]; WorldDistance += FVector3f::Distance(VertexArray[Tri.X], VertexArray[Tri.Y]); UVDistance += FVector2D::Distance(FVector2D(UV0Array[Tri.X]), FVector2D(UV0Array[Tri.Y])); WorldDistance += FVector3f::Distance(VertexArray[Tri.Z], VertexArray[Tri.Y]); UVDistance += FVector2D::Distance(FVector2D(UV0Array[Tri.Z]), FVector2D(UV0Array[Tri.Y])); WorldDistance += FVector3f::Distance(VertexArray[Tri.X], VertexArray[Tri.Z]); UVDistance += FVector2D::Distance(FVector2D(UV0Array[Tri.X]), FVector2D(UV0Array[Tri.Z])); } ensure(FMath::IsFinite(GlobalUVScale)); if (WorldDistance > 0) { GlobalUVScale = static_cast(UVDistance) / WorldDistance; } ensure(FMath::IsFinite(GlobalUVScale)); if (GlobalUVScale <= 0) { GlobalUVScale = 1; } ensure(FMath::IsFinite(GlobalUVScale)); } FPlanarCells::FPlanarCells(const FPlane& P) { NumCells = 2; AddPlane(P, 0, 1); } FPlanarCells::FPlanarCells(const TArrayView Sites, FVoronoiDiagram& Voronoi) { TArray VoronoiCells; Voronoi.ComputeAllCells(VoronoiCells); AssumeConvexCells = true; NumCells = VoronoiCells.Num(); for (int32 CellIdx = 0; CellIdx < NumCells; CellIdx++) { int32 LocalVertexStart = -1; const FVoronoiCellInfo& CellInfo = VoronoiCells[CellIdx]; int32 CellFaceVertexIndexStart = 0; for (int32 CellFaceIdx = 0; CellFaceIdx < CellInfo.Neighbors.Num(); CellFaceIdx++, CellFaceVertexIndexStart += 1 + CellInfo.Faces[CellFaceVertexIndexStart]) { int32 NeighborIdx = CellInfo.Neighbors[CellFaceIdx]; if (CellIdx < NeighborIdx) // Filter out faces that we expect to get by symmetry { continue; } FVector Normal = CellInfo.Normals[CellFaceIdx]; if (Normal.IsZero()) { if (NeighborIdx > -1) { Normal = Sites[NeighborIdx] - Sites[CellIdx]; bool bNormalizeSucceeded = Normal.Normalize(); ensureMsgf(bNormalizeSucceeded, TEXT("Voronoi diagram should not have Voronoi sites so close together!")); } else { // degenerate face on border; likely almost zero area so hopefully it won't matter if we just don't add it continue; } } FPlane P(Normal, FVector::DotProduct(Normal, CellInfo.Vertices[CellInfo.Faces[CellFaceVertexIndexStart + 1]])); if (LocalVertexStart < 0) { LocalVertexStart = PlaneBoundaryVertices.Num(); PlaneBoundaryVertices.Append(CellInfo.Vertices); } TArray PlaneBoundary; int32 FaceSize = CellInfo.Faces[CellFaceVertexIndexStart]; for (int32 i = 0; i < FaceSize; i++) { int32 CellVertexIdx = CellInfo.Faces[CellFaceVertexIndexStart + 1 + i]; PlaneBoundary.Add(LocalVertexStart + CellVertexIdx); } AddPlane(P, CellIdx, NeighborIdx, PlaneBoundary); } } } FPlanarCells::FPlanarCells(const TArrayView Boxes, bool bResolveAdjacencies) { AssumeConvexCells = true; NumCells = Boxes.Num(); TArray BoxesCopy(Boxes); if (!bResolveAdjacencies) // if the boxes aren't touching, we can just make a completely independent cell per box { for (int32 BoxIdx = 0; BoxIdx < NumCells; BoxIdx++) { const FBox& Box = Boxes[BoxIdx]; const FVector& Min = Box.Min; const FVector& Max = Box.Max; int32 VIdx = PlaneBoundaryVertices.Num(); PlaneBoundaryVertices.Add(Min); PlaneBoundaryVertices.Add(FVector(Max.X, Min.Y, Min.Z)); PlaneBoundaryVertices.Add(FVector(Max.X, Max.Y, Min.Z)); PlaneBoundaryVertices.Add(FVector(Min.X, Max.Y, Min.Z)); PlaneBoundaryVertices.Add(FVector(Min.X, Min.Y, Max.Z)); PlaneBoundaryVertices.Add(FVector(Max.X, Min.Y, Max.Z)); PlaneBoundaryVertices.Add(Max); PlaneBoundaryVertices.Add(FVector(Min.X, Max.Y, Max.Z)); AddPlane(FPlane(FVector(0, 0, -1), -Min.Z), BoxIdx, -1, { VIdx + 0, VIdx + 1, VIdx + 2, VIdx + 3 }); AddPlane(FPlane(FVector(0, 0, 1), Max.Z), BoxIdx, -1, { VIdx + 4, VIdx + 7, VIdx + 6, VIdx + 5 }); AddPlane(FPlane(FVector(0, -1, 0), -Min.Y), BoxIdx, -1, { VIdx + 0, VIdx + 4, VIdx + 5, VIdx + 1 }); AddPlane(FPlane(FVector(0, 1, 0), Max.Y), BoxIdx, -1, { VIdx + 3, VIdx + 2, VIdx + 6, VIdx + 7 }); AddPlane(FPlane(FVector(-1, 0, 0), -Min.X), BoxIdx, -1, { VIdx + 0, VIdx + 3, VIdx + 7, VIdx + 4 }); AddPlane(FPlane(FVector(1, 0, 0), Max.X), BoxIdx, -1, { VIdx + 1, VIdx + 5, VIdx + 6, VIdx + 2 }); } return; } // If boxes might be touching, we need to subdivide each box along each overlap, // and make sure to only construct one copy any vertex/face that is shared between multiple boxes // Build an octree of boxes to allow us to quickly find neighbors, and determine how to subdivide the boxes FSparseDynamicOctree3 BoxTree; double BoxMaxDim = 0; for (const FBox& Box : Boxes) { BoxMaxDim = FMath::Max(BoxMaxDim, Box.GetSize().GetMax()); } BoxTree.RootDimension = BoxMaxDim * 4; for (int32 BoxIdx = 0; BoxIdx < NumCells; BoxIdx++) { BoxTree.InsertObject(BoxIdx, Boxes[BoxIdx]); } // Build a hash grid for vertices, to share vertices across boxes constexpr double AddVertTolerance = UE_DOUBLE_KINDA_SMALL_NUMBER; TPointHashGrid3d PosToVert(AddVertTolerance * 10, INDEX_NONE); TMap VertsToPlane; // Track overlaps between neighboring boxes in each dimension separately; each overlap is a new subdivision on that axis TArray OverlapIndices; TArray Overlaps[3]; for (int32 BoxIdx = 0; BoxIdx < NumCells; BoxIdx++) { const FBox& Box = Boxes[BoxIdx]; const FVector& Min = Box.Min; const FVector& Max = Box.Max; Overlaps[0].Reset(); Overlaps[1].Reset(); Overlaps[2].Reset(); OverlapIndices.Reset(); constexpr double QueryExpandTolerance = AddVertTolerance; FBox ExpandedBox = Box.ExpandBy(QueryExpandTolerance); BoxTree.RangeQuery(ExpandedBox, OverlapIndices); for (int32 OverlapIdx : OverlapIndices) { // skip if same box or not an actual overlap const FBox& OtherBox = Boxes[OverlapIdx]; if (BoxIdx == OverlapIdx || !ExpandedBox.Intersect(OtherBox)) { continue; } auto AddIfInRange = [AddVertTolerance](double Val, double RangeMin, double RangeMax, TArray& AddTo) { if (Val > RangeMin + AddVertTolerance && Val < RangeMax - AddVertTolerance) { AddTo.Add(Val); } }; for (int32 Dim = 0; Dim < 3; ++Dim) { AddIfInRange(OtherBox.Min[Dim], Box.Min[Dim], Box.Max[Dim], Overlaps[Dim]); AddIfInRange(OtherBox.Max[Dim], Box.Min[Dim], Box.Max[Dim], Overlaps[Dim]); } } for (int32 Dim = 0; Dim < 3; ++Dim) { Overlaps[Dim].Add(Box.Min[Dim]); Overlaps[Dim].Add(Box.Max[Dim]); Overlaps[Dim].Sort(); double Last = Overlaps[Dim][0]; int32 FillIdx = 1; for (int32 Idx = 1; Idx < Overlaps[Dim].Num(); ++Idx) { if (Overlaps[Dim][Idx] - Last >= AddVertTolerance) { Overlaps[Dim][FillIdx] = Overlaps[Dim][Idx]; Last = Overlaps[Dim][Idx]; ++FillIdx; } // else overlap was too close to 'last', so we skip it } Overlaps[Dim].SetNum(FillIdx, EAllowShrinking::No); if (Overlaps[Dim].Num() == 1) { Overlaps[Dim].Add(Box.Max[Dim]); } else { // make sure to always end at the exact max Overlaps[Dim].Last() = Box.Max[Dim]; } } // Helper to get a shared vertex, first by looking if we've already made it locally, then by checking the global hash grid TMap GridToVert; // A local map for all the vertices we've already found on the box auto GetVertex = [this, &Overlaps, &GridToVert, &PosToVert, AddVertTolerance](int32 FaceDim, int32 UDim, int32 VDim, int32 FaceCoord, int32 U, int32 V) { FIndex3i Coord; Coord[FaceDim] = FaceCoord; Coord[UDim] = U; Coord[VDim] = V; int32* FoundV = GridToVert.Find(Coord); if (FoundV) { return *FoundV; } FVector Pos(Overlaps[0][Coord.A], Overlaps[1][Coord.B], Overlaps[2][Coord.C]); TPair Found = PosToVert.FindNearestInRadius(Pos, AddVertTolerance, [&](const int32& Idx) {return FVector::DistSquared(Pos, PlaneBoundaryVertices[Idx]);}); if (Found.Key != INDEX_NONE) { return Found.Key; } int32 NewV = PlaneBoundaryVertices.Add(Pos); GridToVert.Add(Coord, NewV); PosToVert.InsertPointUnsafe(NewV, Pos); return NewV; }; // Now that we have decided how to subdivide each dimension (w/ the Overlaps arrays), we need to construct each face (or link to the face, if it already exists) for (int32 FaceDim = 0; FaceDim < 3; ++FaceDim) { for (int32 Side = 0; Side < 2; ++Side) { int32 UDim = (FaceDim + 1) % 3; int32 VDim = (FaceDim + 2) % 3; int32 FaceCoord = (Side == 1) ? (Overlaps[FaceDim].Num() - 1) : 0; double FaceSign = double(Side * 2 - 1); FVector Normal(0, 0, 0); Normal[FaceDim] = FaceSign; double PlaneW = FaceSign * (Side == 0 ? Min[FaceDim] : Max[FaceDim]); for (int32 U = 0; U + 1 < Overlaps[UDim].Num(); ++U) { for (int32 V = 0; V + 1 < Overlaps[VDim].Num(); ++V) { int32 V00 = GetVertex(FaceDim, UDim, VDim, FaceCoord, U, V); int32 V10 = GetVertex(FaceDim, UDim, VDim, FaceCoord, U + 1, V); int32 V11 = GetVertex(FaceDim, UDim, VDim, FaceCoord, U + 1, V + 1); int32 V01 = GetVertex(FaceDim, UDim, VDim, FaceCoord, U, V + 1); FIntVector4 FaceKey(V00, V10, V11, V01); int32* FoundPlane = VertsToPlane.Find(FaceKey); if (FoundPlane) { PlaneCells[*FoundPlane].Value = BoxIdx; continue; } TArray BoundaryVerts; if (Side == 0) { BoundaryVerts = { V00, V10, V11, V01 }; } else // reverse winding for the 'far' face { BoundaryVerts = { V10, V00, V01, V11 }; } VertsToPlane.Add(FaceKey, AddPlane(FPlane(Normal, PlaneW), BoxIdx, -1, BoundaryVerts)); } } } } } } FPlanarCells::FPlanarCells(const FBox& Region, const FIntVector& CubesPerAxis) { AssumeConvexCells = true; NumCells = CubesPerAxis.X * CubesPerAxis.Y * CubesPerAxis.Z; // cube X, Y, Z integer indices to a single cell index auto ToIdx = [](const FIntVector &PerAxis, int32 Xi, int32 Yi, int32 Zi) { if (Xi < 0 || Xi >= PerAxis.X || Yi < 0 || Yi >= PerAxis.Y || Zi < 0 || Zi >= PerAxis.Z) { return -1; } else { return Xi + Yi * (PerAxis.X) + Zi * (PerAxis.X * PerAxis.Y); } }; auto ToIdxUnsafe = [](const FIntVector &PerAxis, int32 Xi, int32 Yi, int32 Zi) { return Xi + Yi * (PerAxis.X) + Zi * (PerAxis.X * PerAxis.Y); }; FIntVector VertsPerAxis = CubesPerAxis + FIntVector(1); PlaneBoundaryVertices.SetNum(VertsPerAxis.X * VertsPerAxis.Y * VertsPerAxis.Z); FVector Diagonal = Region.Max - Region.Min; FVector CellSizes( Diagonal.X / CubesPerAxis.X, Diagonal.Y / CubesPerAxis.Y, Diagonal.Z / CubesPerAxis.Z ); int32 VertIdx = 0; for (int32 Zi = 0; Zi < VertsPerAxis.Z; Zi++) { for (int32 Yi = 0; Yi < VertsPerAxis.Y; Yi++) { for (int32 Xi = 0; Xi < VertsPerAxis.X; Xi++) { PlaneBoundaryVertices[VertIdx] = Region.Min + FVector(Xi * CellSizes.X, Yi * CellSizes.Y, Zi * CellSizes.Z); ensure(VertIdx == ToIdxUnsafe(VertsPerAxis, Xi, Yi, Zi)); VertIdx++; } } } float Z = static_cast( Region.Min.Z ); int32 ZSliceSize = VertsPerAxis.X * VertsPerAxis.Y; int32 VIdxOffs[8] = { 0, 1, VertsPerAxis.X + 1, VertsPerAxis.X, ZSliceSize, ZSliceSize + 1, ZSliceSize + VertsPerAxis.X + 1, ZSliceSize + VertsPerAxis.X }; for (int32 Zi = 0; Zi < CubesPerAxis.Z; Zi++, Z += static_cast(CellSizes.Z)) { float Y = static_cast( Region.Min.Y ); float ZN = Z + static_cast(CellSizes.Z); for (int32 Yi = 0; Yi < CubesPerAxis.Y; Yi++, Y += static_cast(CellSizes.Y)) { float X = static_cast(Region.Min.X); float YN = Y + static_cast(CellSizes.Y); for (int32 Xi = 0; Xi < CubesPerAxis.X; Xi++, X += static_cast(CellSizes.X)) { float XN = X + static_cast( CellSizes.X ); int VIdx = ToIdxUnsafe(VertsPerAxis, Xi, Yi, Zi); int BoxIdx = ToIdxUnsafe(CubesPerAxis, Xi, Yi, Zi); AddPlane(FPlane(FVector(0, 0, -1), -Z), BoxIdx, ToIdx(CubesPerAxis, Xi, Yi, Zi-1), { VIdx + VIdxOffs[0], VIdx + VIdxOffs[1], VIdx + VIdxOffs[2], VIdx + VIdxOffs[3] }); AddPlane(FPlane(FVector(0, 0, 1), ZN), BoxIdx, ToIdx(CubesPerAxis, Xi, Yi, Zi+1), { VIdx + VIdxOffs[4], VIdx + VIdxOffs[7], VIdx + VIdxOffs[6], VIdx + VIdxOffs[5] }); AddPlane(FPlane(FVector(0, -1, 0), -Y), BoxIdx, ToIdx(CubesPerAxis, Xi, Yi-1, Zi), { VIdx + VIdxOffs[0], VIdx + VIdxOffs[4], VIdx + VIdxOffs[5], VIdx + VIdxOffs[1] }); AddPlane(FPlane(FVector(0, 1, 0), YN), BoxIdx, ToIdx(CubesPerAxis, Xi, Yi+1, Zi), { VIdx + VIdxOffs[3], VIdx + VIdxOffs[2], VIdx + VIdxOffs[6], VIdx + VIdxOffs[7] }); AddPlane(FPlane(FVector(-1, 0, 0), -X), BoxIdx, ToIdx(CubesPerAxis, Xi-1, Yi, Zi), { VIdx + VIdxOffs[0], VIdx + VIdxOffs[3], VIdx + VIdxOffs[7], VIdx + VIdxOffs[4] }); AddPlane(FPlane(FVector(1, 0, 0), XN), BoxIdx, ToIdx(CubesPerAxis, Xi+1, Yi, Zi), { VIdx + VIdxOffs[1], VIdx + VIdxOffs[5], VIdx + VIdxOffs[6], VIdx + VIdxOffs[2] }); } } } } FPlanarCells::FPlanarCells(const FBox &Region, const TArrayView Image, int32 Width, int32 Height) { const double SimplificationTolerance = 0.0; // TODO: implement simplification and make tolerance a param const FColor OutsideColor(0, 0, 0); int32 NumPix = Width * Height; check(Image.Num() == NumPix); // Union Find adapted from PBDRigidClustering.cpp version; customized to pixel grouping struct UnionFindInfo { int32 GroupIdx; int32 Size; }; TArray PixCellUnions; // union find info per pixel TArray PixCells; // Cell Index per pixel (-1 for OutsideColor pixels) PixCellUnions.SetNumUninitialized(NumPix); PixCells.SetNumUninitialized(NumPix); for (int32 i = 0; i < NumPix; ++i) { if (Image[i] == OutsideColor) { PixCellUnions[i].GroupIdx = -1; PixCellUnions[i].Size = 0; PixCells[i] = -1; } else { PixCellUnions[i].GroupIdx = i; PixCellUnions[i].Size = 1; PixCells[i] = -2; } } auto FindGroup = [&](int Idx) { int GroupIdx = Idx; int findIters = 0; while (PixCellUnions[GroupIdx].GroupIdx != GroupIdx) { ensure(findIters++ < 10); // if this while loop iterates more than a few times, there's probably a bug in the unionfind PixCellUnions[GroupIdx].GroupIdx = PixCellUnions[PixCellUnions[GroupIdx].GroupIdx].GroupIdx; GroupIdx = PixCellUnions[GroupIdx].GroupIdx; } return GroupIdx; }; auto MergeGroup = [&](int A, int B) { int GroupA = FindGroup(A); int GroupB = FindGroup(B); if (GroupA == GroupB) { return; } if (PixCellUnions[GroupA].Size > PixCellUnions[GroupB].Size) { Swap(GroupA, GroupB); } PixCellUnions[GroupA].GroupIdx = GroupB; PixCellUnions[GroupB].Size += PixCellUnions[GroupA].Size; }; // merge non-outside neighbors into groups int32 YOffs[4] = { -1, 0, 0, 1 }; int32 XOffs[4] = { 0, -1, 1, 0 }; for (int32 Yi = 0; Yi < Height; Yi++) { for (int32 Xi = 0; Xi < Width; Xi++) { int32 Pi = Xi + Yi * Width; if (PixCells[Pi] == -1) // outside cell { continue; } for (int Oi = 0; Oi < 4; Oi++) { int32 Yn = Yi + YOffs[Oi]; int32 Xn = Xi + XOffs[Oi]; int32 Pn = Xn + Yn * Width; if (Xn < 0 || Xn >= Width || Yn < 0 || Yn >= Height || PixCells[Pn] == -1) // outside nbr { continue; } MergeGroup(Pi, Pn); } } } // assign cell indices from compacted group IDs NumCells = 0; for (int32 Pi = 0; Pi < NumPix; Pi++) { if (PixCells[Pi] == -1) { continue; } int32 GroupID = FindGroup(Pi); if (PixCells[GroupID] == -2) { PixCells[GroupID] = NumCells++; } PixCells[Pi] = PixCells[GroupID]; } // Dimensions of pixel corner data int32 CWidth = Width + 1; int32 CHeight = Height + 1; int32 NumCorners = CWidth * CHeight; TArray CornerIndices; CornerIndices.SetNumZeroed(NumCorners); TArray>> PerCellBoundaryEdgeArrays; TArray>> CellBoundaryCorners; PerCellBoundaryEdgeArrays.SetNum(NumCells); CellBoundaryCorners.SetNum(NumCells); int32 COffX1[4] = { 1,0,1,0 }; int32 COffX0[4] = { 0,0,1,1 }; int32 COffY1[4] = { 0,0,1,1 }; int32 COffY0[4] = { 0,1,0,1 }; for (int32 Yi = 0; Yi < Height; Yi++) { for (int32 Xi = 0; Xi < Width; Xi++) { int32 Pi = Xi + Yi * Width; int32 Cell = PixCells[Pi]; if (Cell == -1) // outside cell { continue; } for (int Oi = 0; Oi < 4; Oi++) { int32 Yn = Yi + YOffs[Oi]; int32 Xn = Xi + XOffs[Oi]; int32 Pn = Xn + Yn * Width; // boundary edge found if (Xn < 0 || Xn >= Width || Yn < 0 || Yn >= Height || PixCells[Pn] != PixCells[Pi]) { int32 C0 = Xi + COffX0[Oi] + CWidth * (Yi + COffY0[Oi]); int32 C1 = Xi + COffX1[Oi] + CWidth * (Yi + COffY1[Oi]); TArray Chain = { C0, C1 }; int32 Last; while (PerCellBoundaryEdgeArrays[Cell].Contains(Last = Chain.Last())) { Chain.Pop(EAllowShrinking::No); Chain.Append(PerCellBoundaryEdgeArrays[Cell][Last]); PerCellBoundaryEdgeArrays[Cell].Remove(Last); } if (Last == C0) { CellBoundaryCorners[Cell].Add(Chain); } else { PerCellBoundaryEdgeArrays[Cell].Add(Chain[0], Chain); } } } } } FVector RegionDiagonal = Region.Max - Region.Min; for (int32 CellIdx = 0; CellIdx < NumCells; CellIdx++) { ensure(CellBoundaryCorners[CellIdx].Num() > 0); // there must not be any regions with no boundary ensure(PerCellBoundaryEdgeArrays[CellIdx].Num() == 0); // all boundary edge array should have been consumed and turned to full boundary loops ensureMsgf(CellBoundaryCorners[CellIdx].Num() == 1, TEXT("Have not implemented support for regions with holes!")); int32 BoundaryStart = PlaneBoundaryVertices.Num(); const TArray& Bounds = CellBoundaryCorners[CellIdx][0]; int32 Dx = 0, Dy = 0; auto CornerIdxToPos = [&](int32 CornerID) { int32 Xi = CornerID % CWidth; int32 Yi = CornerID / CWidth; return FVector2D( Region.Min.X + Xi * RegionDiagonal.X / float(Width), Region.Min.Y + Yi * RegionDiagonal.Y / float(Height) ); }; FVector2D LastP = CornerIdxToPos(Bounds[0]); int32 NumBoundVerts = 0; TArray FrontBound; for (int32 BoundIdx = 1; BoundIdx < Bounds.Num(); BoundIdx++) { FVector2D NextP = CornerIdxToPos(Bounds[BoundIdx]); FVector2D Dir = NextP - LastP; Dir.Normalize(); int BoundSkip = BoundIdx; while (++BoundSkip < Bounds.Num()) { FVector2D SkipP = CornerIdxToPos(Bounds[BoundSkip]); if (FVector2D::DotProduct(SkipP - NextP, Dir) < 1e-6) { break; } NextP = SkipP; BoundIdx = BoundSkip; } PlaneBoundaryVertices.Add(FVector(NextP.X, NextP.Y, Region.Min.Z)); PlaneBoundaryVertices.Add(FVector(NextP.X, NextP.Y, Region.Max.Z)); int32 Front = BoundaryStart + NumBoundVerts * 2; int32 Back = Front + 1; FrontBound.Add(Front); if (NumBoundVerts > 0) { AddPlane(FPlane(PlaneBoundaryVertices.Last(), FVector(Dir.Y, -Dir.X, 0)), CellIdx, -1, {Back, Front, Front - 2, Back - 2}); } NumBoundVerts++; LastP = NextP; } // add the last edge, connecting the start and end FVector2D Dir = CornerIdxToPos(Bounds[1]) - LastP; Dir.Normalize(); AddPlane(FPlane(PlaneBoundaryVertices.Last(), FVector(Dir.Y, -Dir.X, 0)), CellIdx, -1, {BoundaryStart+1, BoundaryStart, BoundaryStart+NumBoundVerts*2-2, BoundaryStart+NumBoundVerts*2-1}); // add the front and back faces AddPlane(FPlane(Region.Min, FVector(0, 0, -1)), CellIdx, -1, FrontBound); TArray BackBound; BackBound.SetNum(FrontBound.Num()); for (int32 Idx = 0, N = BackBound.Num(); Idx < N; Idx++) { BackBound[Idx] = FrontBound[N - 1 - Idx] + 1; } AddPlane(FPlane(Region.Max, FVector(0, 0, 1)), CellIdx, -1, BackBound); } AssumeConvexCells = false; // todo could set this to true if the 2D shape of each image region is convex } void FPlanarCells::DiscardCells(TFunctionRef KeepFunc, bool bKeepNeighbors) { TArray OldToNew; OldToNew.Init(-1, NumCells); int32 KeptCells = 0; for (int32 CellIdx = 0; CellIdx < NumCells; ++CellIdx) { if (KeepFunc(CellIdx)) { OldToNew[CellIdx] = KeptCells++; } } if (bKeepNeighbors && KeptCells < NumCells) { for (const TPair& Neighbors : PlaneCells) { if (Neighbors.Key < 0 || Neighbors.Value < 0) { continue; } bool bKeptKey = KeepFunc(Neighbors.Key); bool bKeptValue = KeepFunc(Neighbors.Value); if (bKeptKey != bKeptValue) { int32 Nbr = bKeptKey ? Neighbors.Value : Neighbors.Key; if (OldToNew[Nbr] < 0) { OldToNew[Nbr] = KeptCells++; } } } } if (KeptCells == NumCells) { return; } NumCells = KeptCells; for (int32 PlaneIdx = 0; PlaneIdx < Planes.Num(); ++PlaneIdx) { TPair Cells = PlaneCells[PlaneIdx]; Cells.Key = Cells.Key > -1 ? OldToNew[Cells.Key] : -1; Cells.Value = Cells.Value > -1 ? OldToNew[Cells.Value] : -1; if (Cells.Key == Cells.Value && Cells.Key == -1) { PlaneCells.RemoveAtSwap(PlaneIdx, EAllowShrinking::No); Planes.RemoveAtSwap(PlaneIdx, EAllowShrinking::No); PlaneBoundaries.RemoveAtSwap(PlaneIdx, EAllowShrinking::No); PlaneIdx--; // consider the swapped-in value in the next iteration } else { // on boundary to outside, the 'outside' index must always be second // if the discards above broke that invariant, flip the plane to fix it if (Cells.Key < 0) { Swap(Cells.Key, Cells.Value); Algo::Reverse(PlaneBoundaries[PlaneIdx]); Planes[PlaneIdx] = Planes[PlaneIdx].Flip(); } PlaneCells[PlaneIdx] = Cells; } } // Compress vertices array to only the used vertices TArray OldToNewVertex; OldToNewVertex.Init(-1, PlaneBoundaryVertices.Num()); TArray NewBoundaryVertices; for (TArray& PlaneBoundary : PlaneBoundaries) { for (int32& VID : PlaneBoundary) { int32& NewVID = OldToNewVertex[VID]; if (NewVID < 0) { NewVID = NewBoundaryVertices.Add(PlaneBoundaryVertices[VID]); } VID = NewVID; } } PlaneBoundaryVertices = MoveTemp(NewBoundaryVertices); } // Simpler invocation of CutWithPlanarCells w/ reasonable defaults int32 CutWithPlanarCells( FPlanarCells& Cells, FGeometryCollection& Source, int32 TransformIdx, double Grout, double CollisionSampleSpacing, int32 RandomSeed, const TOptional& TransformCollection, bool bIncludeOutsideCellInOutput, bool bSetDefaultInternalMaterialsFromCollection, FProgressCancel* Progress, FVector CellsOrigin, bool bSplitIslands ) { TArray TransformIndices { TransformIdx }; return CutMultipleWithPlanarCells(Cells, Source, TransformIndices, Grout, CollisionSampleSpacing, RandomSeed, TransformCollection, bIncludeOutsideCellInOutput, bSetDefaultInternalMaterialsFromCollection, Progress, CellsOrigin, bSplitIslands); } int32 CutMultipleWithMultiplePlanes( const TArrayView& Planes, FInternalSurfaceMaterials& InternalSurfaceMaterials, FGeometryCollection& Collection, const TArrayView& TransformIndices, double Grout, double CollisionSampleSpacing, int32 RandomSeed, const TOptional& TransformCollection, bool bSetDefaultInternalMaterialsFromCollection, FProgressCancel* Progress, bool bSplitIslands ) { FProgressCancel::FProgressScope PrepareScope = FProgressCancel::CreateScopeTo(Progress, .1, LOCTEXT("CutWithMultiplePlanesInit", "Preparing to cut with planes")); int32 OrigNumGeom = Collection.FaceCount.Num(); int32 CurNumGeom = OrigNumGeom; GeometryCollection::Facades::FCollectionMeshFacade CollectionMesh(Collection); if (!CollectionMesh.IsValid()) { return -1; } if (bSetDefaultInternalMaterialsFromCollection) { InternalSurfaceMaterials.SetUVScaleFromCollection(CollectionMesh); } // Compute cuts in a local space where the collection has been translated to the origin, for more consistent noise + better LWC accuracy FTransform CollectionToWorld = TransformCollection.Get(FTransform::Identity); FTransform CollectionToWorldCentered; FVector Origin = PlanarCut_Locals::SeparateTranslation(CollectionToWorld, CollectionToWorldCentered); // Move planes to the same local space TArray CenteredPlanes; CenteredPlanes.Reserve(Planes.Num()); for (const FPlane& Plane : Planes) { CenteredPlanes.Add(Plane.TranslateBy(-Origin)); } FDynamicMeshCollection MeshCollection(&Collection, TransformIndices, CollectionToWorldCentered); PrepareScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope CutScope = FProgressCancel::CreateScopeTo(Progress, .99, LOCTEXT("CutWithMultiplePlanesBody", "Cutting with planes")); int32 NewGeomStartIdx = -1; NewGeomStartIdx = MeshCollection.CutWithMultiplePlanes(CenteredPlanes, Grout, CollisionSampleSpacing, bSplitIslands, RandomSeed, &Collection, InternalSurfaceMaterials, bSetDefaultInternalMaterialsFromCollection, Progress); CutScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope ReindexScope = FProgressCancel::CreateScopeTo(Progress, 1); Collection.ReindexMaterials(); ReindexScope.Done(); return NewGeomStartIdx; } void CreateCuttingSurfacePreview( const FPlanarCells& Cells, const FBox& Bounds, double Grout, int32 RandomSeed, FDynamicMesh3& OutCuttingMeshes, TFunctionRef FilterCellsFunc, const TOptional& TransformCollection, FProgressCancel* Progress, FVector CellsOrigin ) { FTransform CollectionToWorld = TransformCollection.Get(FTransform::Identity); // Put Collection in the same local space as the Cells FTransform CollectionToWorldCentered = CollectionToWorld * FTransform(-CellsOrigin); FProgressCancel::FProgressScope SurfaceScope = FProgressCancel::CreateScopeTo(Progress, .99); UE::Geometry::FAxisAlignedBox3d GeoBounds(Bounds); double OnePercentExtend = GeoBounds.MaxDim() * .01; FRandomStream RandomStream(RandomSeed); FCellMeshes CellMeshes(0, RandomStream, Cells, GeoBounds, Grout, OnePercentExtend, false); SurfaceScope.Done(); if (Progress && Progress->Cancelled()) { return; } FProgressCancel::FProgressScope AppendScope = FProgressCancel::CreateScopeTo(Progress, 1.0); OutCuttingMeshes.Clear(); OutCuttingMeshes.EnableAttributes(); FDynamicMeshEditor Editor(&OutCuttingMeshes); FMeshIndexMappings IndexMaps; // Needed for Editor.AppendMesh, not used otherwise for (int32 CellIdx = 0; CellIdx < CellMeshes.CellMeshes.Num(); ++CellIdx) { if (FilterCellsFunc(CellIdx)) { Editor.AppendMesh(&CellMeshes.CellMeshes[CellIdx].AugMesh, IndexMaps); } } AppendScope.Done(); } // 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 int32 CutMultipleWithPlanarCells( FPlanarCells& Cells, FGeometryCollection& Source, const TArrayView& TransformIndices, double Grout, double CollisionSampleSpacing, int32 RandomSeed, const TOptional& TransformCollection, bool bIncludeOutsideCellInOutput, bool bSetDefaultInternalMaterialsFromCollection, FProgressCancel* Progress, FVector CellsOrigin, bool bSplitIslands ) { FProgressCancel::FProgressScope CreateMeshCollectionScope = FProgressCancel::CreateScopeTo(Progress, .1); if (bSetDefaultInternalMaterialsFromCollection) { Cells.InternalSurfaceMaterials.SetUVScaleFromCollection(Source); } FTransform CollectionToWorld = TransformCollection.Get(FTransform::Identity); // Put Collection in the same local space as the Cells FTransform CollectionToWorldCentered = CollectionToWorld * FTransform(-CellsOrigin); FDynamicMeshCollection MeshCollection(&Source, TransformIndices, CollectionToWorldCentered); CreateMeshCollectionScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope CellMeshScope = FProgressCancel::CreateScopeTo(Progress, .1); double OnePercentExtend = MeshCollection.Bounds.MaxDim() * .01; FRandomStream RandomStream(RandomSeed); FCellMeshes CellMeshes(Source.NumUVLayers(), RandomStream, Cells, MeshCollection.Bounds, Grout, OnePercentExtend, bIncludeOutsideCellInOutput); CellMeshScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope CutScope = FProgressCancel::CreateScopeTo(Progress, .99); int32 NewGeomStartIdx = -1; NewGeomStartIdx = MeshCollection.CutWithCellMeshes(Cells.InternalSurfaceMaterials, Cells.PlaneCells, CellMeshes, bSplitIslands, &Source, bSetDefaultInternalMaterialsFromCollection, CollisionSampleSpacing); CutScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope ReindexScope = FProgressCancel::CreateScopeTo(Progress, 1); Source.ReindexMaterials(); ReindexScope.Done(); return NewGeomStartIdx; } int32 SplitIslands( FGeometryCollection& Collection, const TArrayView& TransformIndices, double CollisionSampleSpacing, FProgressCancel* Progress ) { FProgressCancel::FProgressScope CreateMeshCollectionScope = FProgressCancel::CreateScopeTo(Progress, .1); FTransform CollectionToWorld = FTransform::Identity; FDynamicMeshCollection MeshCollection(&Collection, TransformIndices, CollectionToWorld); CreateMeshCollectionScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope SplitScope = FProgressCancel::CreateScopeTo(Progress, .99); int32 NewGeomStartIdx = -1; NewGeomStartIdx = MeshCollection.SplitAllIslands(&Collection, CollisionSampleSpacing); SplitScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope ReindexScope = FProgressCancel::CreateScopeTo(Progress, 1); Collection.ReindexMaterials(); ReindexScope.Done(); return NewGeomStartIdx; } FDynamicMesh3 ConvertMeshDescriptionToCuttingDynamicMesh(const FMeshDescription* CuttingMesh, int32 NumUVLayers, FProgressCancel* Progress) { // populate the BaseMesh with a conversion of the input mesh. FMeshDescriptionToDynamicMesh Converter; FDynamicMesh3 FullMesh; // full-featured conversion of the source mesh Converter.Convert(CuttingMesh, FullMesh, true); bool bHasInvalidNormals, bHasInvalidTangents; FStaticMeshOperations::HasInvalidVertexInstanceNormalsOrTangents(*CuttingMesh, bHasInvalidNormals, bHasInvalidTangents); if (bHasInvalidNormals || bHasInvalidTangents) { FDynamicMeshAttributeSet& Attribs = *FullMesh.Attributes(); FDynamicMeshNormalOverlay* NTB[3]{ Attribs.PrimaryNormals(), Attribs.PrimaryTangents(), Attribs.PrimaryBiTangents() }; if (bHasInvalidNormals) { FMeshNormals::InitializeOverlayToPerVertexNormals(NTB[0], false); } FMeshTangentsf Tangents(&FullMesh); Tangents.ComputeTriVertexTangents(NTB[0], Attribs.PrimaryUV(), { true, true }); Tangents.CopyToOverlays(FullMesh); } if (Progress && Progress->Cancelled()) { return FDynamicMesh3(); // return empty mesh on cancel } FDynamicMesh3 DynamicCuttingMesh; // version of mesh that is split apart at seams to be compatible w/ geometry collection, with corresponding attributes set SetGeometryCollectionAttributes(DynamicCuttingMesh, NumUVLayers); if (Progress && Progress->Cancelled()) { return FDynamicMesh3(); // return empty mesh on cancel } // Note: This conversion will likely go away, b/c I plan to switch over to doing the boolean operations on the fuller rep, but the code can be adapted // to the dynamic mesh -> geometry collection conversion phase, as this same splitting will then need to happen there. if (ensure(FullMesh.HasAttributes() && FullMesh.Attributes()->NumUVLayers() >= 1 && FullMesh.Attributes()->NumNormalLayers() == 3)) { if (!ensure(FullMesh.IsCompact())) { FullMesh.CompactInPlace(); } // Triangles array is 1:1 with the input mesh TArray Triangles; Triangles.Init(FIndex3i::Invalid(), FullMesh.TriangleCount()); FDynamicMesh3& OutMesh = DynamicCuttingMesh; FDynamicMeshAttributeSet& Attribs = *FullMesh.Attributes(); FDynamicMeshNormalOverlay* NTB[3]{ Attribs.PrimaryNormals(), Attribs.PrimaryTangents(), Attribs.PrimaryBiTangents() }; FDynamicMeshUVOverlay* UV = Attribs.PrimaryUV(); TMap ElIDsToVID; int OrigMaxVID = FullMesh.MaxVertexID(); for (int VID = 0; VID < OrigMaxVID; VID++) { check(FullMesh.IsVertex(VID)); FVector3d Pos = FullMesh.GetVertex(VID); ElIDsToVID.Reset(); FullMesh.EnumerateVertexTriangles(VID, [&FullMesh, &Triangles, &OutMesh, &NTB, &UV, &ElIDsToVID, Pos, VID, NumUVLayers](int32 TID) { FIndex3i InTri = FullMesh.GetTriangle(TID); int VOnT = IndexUtil::FindTriIndex(VID, InTri); FIndex4i ElIDs( NTB[0]->GetTriangle(TID)[VOnT], NTB[1]->GetTriangle(TID)[VOnT], NTB[2]->GetTriangle(TID)[VOnT], UV->GetTriangle(TID)[VOnT]); const int* FoundVID = ElIDsToVID.Find(ElIDs); FIndex3i& OutTri = Triangles[TID]; if (FoundVID) { OutTri[VOnT] = *FoundVID; } else { FVector3f Normal = NTB[0]->GetElement(ElIDs.A); FVertexInfo Info(Pos, Normal, FVector3f(1, 1, 1)); int OutVID = OutMesh.AppendVertex(Info); OutTri[VOnT] = OutVID; AugmentedDynamicMesh::SetTangent(OutMesh, OutVID, Normal, NTB[1]->GetElement(ElIDs.B), NTB[2]->GetElement(ElIDs.C)); for (int32 UVLayerIdx = 0; UVLayerIdx < NumUVLayers; UVLayerIdx++) { AugmentedDynamicMesh::SetUV(OutMesh, OutVID, UV->GetElement(ElIDs.D), UVLayerIdx); } ElIDsToVID.Add(ElIDs, OutVID); } }); } FDynamicMeshMaterialAttribute* OutMaterialID = OutMesh.Attributes()->GetMaterialID(); for (int TID = 0; TID < Triangles.Num(); TID++) { FIndex3i& Tri = Triangles[TID]; int AddedTID = OutMesh.AppendTriangle(Tri); if (ensure(AddedTID > -1)) { OutMaterialID->SetValue(AddedTID, -1); // just use a single negative material ID by convention to indicate internal material AugmentedDynamicMesh::SetVisibility(OutMesh, AddedTID, true); } } } return DynamicCuttingMesh; } int32 CutWithMesh( const FDynamicMesh3& DynamicCuttingMesh, FTransform CuttingMeshTransform, FInternalSurfaceMaterials& InternalSurfaceMaterials, FGeometryCollection& Collection, const TArrayView& TransformIndices, double CollisionSampleSpacing, const TOptional& TransformCollection, bool bSetDefaultInternalMaterialsFromCollection, FProgressCancel* Progress, bool bSplitIslands ) { FProgressCancel::FProgressScope PrepareScope = FProgressCancel::CreateScopeTo(Progress, .1); int32 NewGeomStartIdx = -1; if (bSetDefaultInternalMaterialsFromCollection) { InternalSurfaceMaterials.SetUVScaleFromCollection(Collection); } ensureMsgf(!InternalSurfaceMaterials.NoiseSettings.IsSet(), TEXT("Noise settings not yet supported for mesh-based fracture")); FTransform CollectionToWorld = TransformCollection.Get(FTransform::Identity); FTransform CollectionToWorldCentered; FVector Origin = PlanarCut_Locals::SeparateTranslation(CollectionToWorld, CollectionToWorldCentered); FTransform CuttingMeshTransformCentered = CuttingMeshTransform; CuttingMeshTransformCentered.SetTranslation(CuttingMeshTransform.GetTranslation() - Origin); if (Progress && Progress->Cancelled()) { return -1; } FDynamicMeshCollection MeshCollection(&Collection, TransformIndices, CollectionToWorldCentered); int32 NumUVLayers = Collection.NumUVLayers(); FCellMeshes CellMeshes(NumUVLayers, DynamicCuttingMesh, InternalSurfaceMaterials, CuttingMeshTransformCentered); TArray> CellConnectivity; CellConnectivity.Add(TPair(0, -1)); // there's only one 'inside' cell (0), so all cut surfaces are connecting the 'inside' cell (0) to the 'outside' cell (-1) PrepareScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope CutScope = FProgressCancel::CreateScopeTo(Progress, .99); NewGeomStartIdx = MeshCollection.CutWithCellMeshes(InternalSurfaceMaterials, CellConnectivity, CellMeshes, bSplitIslands, &Collection, bSetDefaultInternalMaterialsFromCollection, CollisionSampleSpacing); CutScope.Done(); if (Progress && Progress->Cancelled()) { return -1; } FProgressCancel::FProgressScope ReindexScope = FProgressCancel::CreateScopeTo(Progress, 1); Collection.ReindexMaterials(); ReindexScope.Done(); return NewGeomStartIdx; } void FindBoneVolumes( FGeometryCollection& Collection, const TArrayView& TransformIndices, TArray& OutVolumes, double ScalePerDimension, bool bIncludeClusters ) { OutVolumes.Reset(); // make sure volume attributes are up to date FGeometryCollectionConvexUtility::SetVolumeAttributes(&Collection); const TManagedArray& VolumesAttrib = Collection.GetAttribute("Volume", FTransformCollection::TransformGroup); TArray Transforms; TArray TransformIndicesArray(TransformIndices); if (TransformIndicesArray.Num() == 0) { for (int32 TransformIdx = 0; TransformIdx < Collection.TransformToGeometryIndex.Num(); TransformIdx++) { TransformIndicesArray.Add(TransformIdx); } } double VolumeScale = ScalePerDimension * ScalePerDimension * ScalePerDimension; OutVolumes.SetNum(TransformIndicesArray.Num()); for (int32 Idx = 0; Idx < TransformIndicesArray.Num(); ++Idx) { int32 TransformIdx = TransformIndicesArray[Idx]; if (bIncludeClusters || Collection.IsRigid(TransformIdx)) { double UnscaledVolume = (double)VolumesAttrib[Idx]; OutVolumes[Idx] = UnscaledVolume * VolumeScale; } else { OutVolumes[Idx] = 0.0; } } } void FilterBonesByVolume( const FGeometryCollection& Collection, const TArrayView& TransformIndices, const TArrayView& Volumes, TFunctionRef Filter, TArray& OutSmallBones, bool bIncludeClusters ) { OutSmallBones.Reset(); auto AddIdx = [&Collection, &Volumes, &Filter, &OutSmallBones, bIncludeClusters](int32 TransformIdx, double Volume) { constexpr int8 ClusterType = FGeometryCollection::ESimulationTypes::FST_Clustered; constexpr int8 RigidType = FGeometryCollection::ESimulationTypes::FST_Rigid; const int32 SimType = Collection.SimulationType[TransformIdx]; const bool bCanConsiderIdx = (bIncludeClusters && SimType == ClusterType) || (SimType == RigidType && Collection.TransformToGeometryIndex[TransformIdx] > -1); if (bCanConsiderIdx && Filter(Volume, TransformIdx)) { OutSmallBones.Add(TransformIdx); } }; TArray Transforms; if (TransformIndices.Num() == 0) { int32 NumTransforms = Collection.TransformToGeometryIndex.Num(); if (!ensure(Volumes.Num() == NumTransforms)) { return; } for (int32 TransformIdx = 0; TransformIdx < NumTransforms; TransformIdx++) { AddIdx(TransformIdx, Volumes[TransformIdx]); } } else { if (!ensure(Volumes.Num() == TransformIndices.Num())) { return; } for (int32 Idx = 0; Idx < TransformIndices.Num(); ++Idx) { int32 TransformIdx = TransformIndices[Idx]; AddIdx(TransformIdx, Volumes[Idx]); } } } void FindSmallBones( const FGeometryCollection& Collection, const TArrayView& TransformIndices, const TArrayView& Volumes, double MinVolume, TArray& OutSmallBones, bool bIncludeClusters ) { FilterBonesByVolume(Collection, TransformIndices, Volumes, [MinVolume](double Volume, int32 BoneIdx) -> bool { return Volume < MinVolume; }, OutSmallBones, bIncludeClusters); } int32 MergeBones( FGeometryCollection& Collection, const TArrayView& TransformIndicesView, const TArrayView& Volumes, double MinVolume, const TArrayView& SmallTransformIndices, bool bUnionJoinedPieces, UE::PlanarCut::ENeighborSelectionMethod NeighborSelectionMethod, bool bUseCollectionProximity ) { FTransform CellsToWorld = FTransform::Identity; FGeometryCollectionProximityUtility ProximityUtility(&Collection); TArray> LocalProximity; const TArray>* UseProximity = nullptr; if (!bUseCollectionProximity) { LocalProximity = ProximityUtility.ComputePreciseProximity(Collection); UseProximity = &LocalProximity; } else { ProximityUtility.RequireProximity(); UseProximity = &Collection.GetAttribute>("Proximity", FGeometryCollection::GeometryGroup).GetConstArray(); } // local array so we can populate it with all transforms if input was empty TArray TransformIndices(TransformIndicesView); if (TransformIndices.Num() == 0) { for (int32 TransformIdx = 0; TransformIdx < Collection.TransformToGeometryIndex.Num(); TransformIdx++) { TransformIndices.Add(TransformIdx); } } if (!ensure(TransformIndices.Num() == Volumes.Num())) { return INDEX_NONE; } struct FRemoveGroup { int32 MergeTo = INDEX_NONE; double MergeTargetVolume = 0; double TotalVolume = 0; bool bRemoveMergeTarget = false; TArray ToRemove; FRemoveGroup() {} FRemoveGroup(const TMap& GeomVolMaps, double MinVolume, int32 SmallIdx, int32 BigIdx) { ToRemove.Add(SmallIdx); double BigVol = GeomVolMaps[BigIdx]; if (BigVol < MinVolume) { ToRemove.Add(BigIdx); bRemoveMergeTarget = true; } MergeTargetVolume = BigVol; TotalVolume = BigVol + GeomVolMaps[SmallIdx]; MergeTo = BigIdx; } bool IsValid() { return MergeTo != INDEX_NONE; } void UpdateMergeTarget(int Idx, double Volume) { if (bRemoveMergeTarget) { if (Volume > MergeTargetVolume) { MergeTo = Idx; MergeTargetVolume = Volume; } } } // add a too-small geometry to this group void AddSmall(const TMap& GeomVolMaps, int32 SmallIdx) { double SmallVol = GeomVolMaps[SmallIdx]; TotalVolume += SmallVol; UpdateMergeTarget(SmallIdx, SmallVol); checkSlow(!ToRemove.Contains(SmallIdx)); ToRemove.Add(SmallIdx); } // add a neighbor of a too-small geometry to an existing group void AddBig(const TMap& GeomVolMaps, double MinVolume, int32 BigIdx) { double BigVol = GeomVolMaps[BigIdx]; TotalVolume += BigVol; UpdateMergeTarget(BigIdx, BigVol); if (BigVol < MinVolume) { checkSlow(!ToRemove.Contains(BigIdx)); ToRemove.Add(BigIdx); } else { bRemoveMergeTarget = false; } } void TransferGroup(FRemoveGroup& SmallGroup, TMap& GeomIdxToRemoveGroupIdx, int32 NewIdx) { for (int32 RmIdx : SmallGroup.ToRemove) { checkSlow(!ToRemove.Contains(RmIdx)); ToRemove.Add(RmIdx); GeomIdxToRemoveGroupIdx[RmIdx] = NewIdx; } if (!SmallGroup.bRemoveMergeTarget) { checkSlow(!ToRemove.Contains(SmallGroup.MergeTo)); ToRemove.Add(SmallGroup.MergeTo); GeomIdxToRemoveGroupIdx[SmallGroup.MergeTo] = NewIdx; } TotalVolume += SmallGroup.TotalVolume; SmallGroup = FRemoveGroup(); // clear old group } bool IsGroupSmall(double MinVolume) { return TotalVolume < MinVolume; } }; TMap GeomToVol; TMap GeomIdxToRemoveGroupIdx; TArray RemoveGroups; TSet TooSmalls; TSet CanMerge; // GeomToCenter is just a cache for GetCenter; may not be worth caching as long as 'center' == bounding box center // TODO: consider switching to a more accurate 'center' and/or removing the cache TMap GeomToCenter; auto GetCenter = [&Collection, &GeomToCenter](int32 GeomIdx) -> FVector { FVector* CachedCenter = GeomToCenter.Find(GeomIdx); if (CachedCenter) { return *CachedCenter; } int32 TransformIdx = Collection.TransformIndex[GeomIdx]; FTransform Transform = GeometryCollectionAlgo::GlobalMatrix(Collection.Transform, Collection.Parent, TransformIdx); FVector Center = Transform.TransformPosition(Collection.BoundingBox[GeomIdx].GetCenter()); GeomToCenter.Add(GeomIdx, Center); return Center; }; for (int32 Idx = 0; Idx < TransformIndices.Num(); ++Idx) { int32 TransformIdx = TransformIndices[Idx]; int32 GeomIdx = Collection.TransformToGeometryIndex[TransformIdx]; if (GeomIdx > -1 && Collection.IsRigid(TransformIdx)) { CanMerge.Add(GeomIdx); GeomToVol.Add(GeomIdx, Volumes[Idx]); } } for (int32 TransformIdx : SmallTransformIndices) { int32 GeomIdx = Collection.TransformToGeometryIndex[TransformIdx]; if (GeomIdx > -1 && Collection.IsRigid(TransformIdx) && GeomToVol.Contains(GeomIdx)) { TooSmalls.Add(GeomIdx); } else { ensureMsgf(false, TEXT("Cannot merge bones that have no geometry attached")); } } for (int32 SmallIdx : TooSmalls) { int32* SmallRemoveGroupIdx = GeomIdxToRemoveGroupIdx.Find(SmallIdx); if (SmallRemoveGroupIdx) { if (RemoveGroups[*SmallRemoveGroupIdx].TotalVolume >= MinVolume) { continue; } } const TSet& Prox = (*UseProximity)[SmallIdx]; double BestScore = -FMathd::MaxReal; int32 BestNbrIdx = INDEX_NONE; for (int32 NbrIdx : Prox) { if (NbrIdx != SmallIdx && CanMerge.Contains(NbrIdx)) { double Score; if (NeighborSelectionMethod == UE::PlanarCut::ENeighborSelectionMethod::LargestNeighbor) { Score = GeomToVol[NbrIdx]; } else // Nearest center { Score = 1.0 / (DOUBLE_SMALL_NUMBER + DistanceSquared(GetCenter(NbrIdx), GetCenter(SmallIdx))); } if (Score > BestScore) { BestScore = Score; BestNbrIdx = NbrIdx; } } } if (BestNbrIdx == INDEX_NONE) { UE_LOG(LogPlanarCut, Warning, TEXT("Couldn't fix Bone %d: No neighbors found in proximity graph"), SmallIdx); continue; } if (SmallRemoveGroupIdx) { int32 OldSGIdx = *SmallRemoveGroupIdx; int32* BigRemoveGroupIdx = GeomIdxToRemoveGroupIdx.Find(BestNbrIdx); if (BigRemoveGroupIdx) { int32 BigRGIdx = *BigRemoveGroupIdx; if (OldSGIdx != BigRGIdx) { RemoveGroups[BigRGIdx].TransferGroup(RemoveGroups[OldSGIdx], GeomIdxToRemoveGroupIdx, BigRGIdx); checkSlow(GeomIdxToRemoveGroupIdx.FindKey(OldSGIdx) == nullptr); } } else { RemoveGroups[OldSGIdx].AddBig(GeomToVol, MinVolume, BestNbrIdx); checkSlow(!GeomIdxToRemoveGroupIdx.Contains(BestNbrIdx)); GeomIdxToRemoveGroupIdx.Add(BestNbrIdx, OldSGIdx); } } else { int32* BigRemoveGroupIdx = GeomIdxToRemoveGroupIdx.Find(BestNbrIdx); if (BigRemoveGroupIdx) { int32 BigRGIdx = *BigRemoveGroupIdx; RemoveGroups[BigRGIdx].AddSmall(GeomToVol, SmallIdx); checkSlow(!GeomIdxToRemoveGroupIdx.Contains(SmallIdx)); GeomIdxToRemoveGroupIdx.Add(SmallIdx, BigRGIdx); } else { int32 RemoveGroupIdx = RemoveGroups.Emplace(GeomToVol, MinVolume, SmallIdx, BestNbrIdx); checkSlow(!GeomIdxToRemoveGroupIdx.Contains(SmallIdx)); checkSlow(!GeomIdxToRemoveGroupIdx.Contains(BestNbrIdx)); GeomIdxToRemoveGroupIdx.Add(SmallIdx, RemoveGroupIdx); GeomIdxToRemoveGroupIdx.Add(BestNbrIdx, RemoveGroupIdx); } } } TArray AllRemoveIndices; TArray AllUpdateIndices; for (FRemoveGroup& Group : RemoveGroups) { if (!Group.IsValid()) { continue; } if (Group.bRemoveMergeTarget) { Group.ToRemove.RemoveSingle(Group.MergeTo); } for (int32 RmIdx : Group.ToRemove) { AllRemoveIndices.Add(Collection.TransformIndex[RmIdx]); } AllUpdateIndices.Add(Collection.TransformIndex[Group.MergeTo]); } AllRemoveIndices.Sort(); AllUpdateIndices.Sort(); FDynamicMeshCollection RemoveCollection(&Collection, AllRemoveIndices, CellsToWorld, true); FDynamicMeshCollection UpdateCollection(&Collection, AllUpdateIndices, CellsToWorld, true); TMap GeoIdxToRmMeshIdx; for (int32 RmMeshIdx = 0; RmMeshIdx < RemoveCollection.Meshes.Num(); RmMeshIdx++) { int32 TransformIdx = RemoveCollection.Meshes[RmMeshIdx].TransformIndex; GeoIdxToRmMeshIdx.Add( Collection.TransformToGeometryIndex[TransformIdx], RmMeshIdx ); } using FMeshData = UE::PlanarCut::FDynamicMeshCollection::FMeshData; for (int32 UpMeshIdx = 0; UpMeshIdx < UpdateCollection.Meshes.Num(); UpMeshIdx++) { FMeshData& UpMeshData = UpdateCollection.Meshes[UpMeshIdx]; int32 UpGeoIdx = Collection.TransformToGeometryIndex[UpMeshData.TransformIndex]; FRemoveGroup& Group = RemoveGroups[GeomIdxToRemoveGroupIdx[UpGeoIdx]]; if (!ensure(Group.IsValid())) { continue; } FDynamicMeshEditor MeshEditor(&UpMeshData.AugMesh); for (int32 RmGeoIdx : Group.ToRemove) { FMeshData& RmMeshData = RemoveCollection.Meshes[GeoIdxToRmMeshIdx[RmGeoIdx]]; if (bUnionJoinedPieces) { FMeshBoolean Boolean(&UpMeshData.AugMesh, &RmMeshData.AugMesh, &UpMeshData.AugMesh, FMeshBoolean::EBooleanOp::Union); Boolean.bWeldSharedEdges = false; Boolean.bSimplifyAlongNewEdges = true; Boolean.Compute(); } else { FMeshIndexMappings IndexMaps_Unused; MeshEditor.AppendMesh(&RmMeshData.AugMesh, IndexMaps_Unused); } } } UpdateCollection.UpdateAllCollections(Collection); for (FRemoveGroup& Group : RemoveGroups) { if (!Group.IsValid()) { continue; } int32 MergeTransformIdx = Collection.TransformIndex[Group.MergeTo]; TArray Children; for (int32 RmIdx : Group.ToRemove) { int32 RmTransformIdx = Collection.TransformIndex[RmIdx]; for (int32 ChildIdx : Collection.Children[RmTransformIdx]) { Children.Add(ChildIdx); } } if (Children.Num()) { GeometryCollectionAlgo::ParentTransforms(&Collection, MergeTransformIdx, Children); } } // remove transforms for all geometry that was merged in FManagedArrayCollection::FProcessingParameters ProcessingParams; #if !UE_BUILD_DEBUG ProcessingParams.bDoValidation = false; #endif Collection.RemoveElements(FGeometryCollection::TransformGroup, AllRemoveIndices, ProcessingParams); return INDEX_NONE; // TODO: consider tracking smallest index of updated groups? but no reason to do so currently } namespace { static double GetClusterVolumeFromRigidsHelper(const FGeometryCollection& Collection, const TArrayView& Volumes, int32 BoneIdx) { if (Collection.SimulationType[BoneIdx] == FGeometryCollection::ESimulationTypes::FST_Rigid) { return Volumes[BoneIdx]; } double Sum = 0; for (int32 Child : Collection.Children[BoneIdx]) { Sum += GetClusterVolumeFromRigidsHelper(Collection, Volumes, Child); } return Sum; } static FBox GetClusterBoundsFromRigidsHelper(const FGeometryCollection& Collection, const TArray& GeoBoxesInWorldSpace, int32 BoneIdx) { int32 GeoIdx = Collection.TransformToGeometryIndex[BoneIdx]; if (GeoIdx != INDEX_NONE) { return GeoBoxesInWorldSpace[GeoIdx]; } FBox Combined(ForceInit); for (int32 Child : Collection.Children[BoneIdx]) { Combined += GetClusterBoundsFromRigidsHelper(Collection, GeoBoxesInWorldSpace, Child); } return Combined; } } void MergeClusters( FGeometryCollection& Collection, const TArrayView& Volumes, double MinVolume, const TArrayView& SmallTransformIndices, UE::PlanarCut::ENeighborSelectionMethod NeighborSelectionMethod, bool bOnlyMergeInProximity, bool bOnlySameParent, bool bUseCollectionProximity ) { if (Collection.NumElements(FGeometryCollection::TransformGroup) == 0) { // nothing to do, early exit return; } FGeometryCollectionProximityUtility ProximityUtility(&Collection); TArray> LocalProximity; const TArray>* UseProximity = nullptr; if (!bUseCollectionProximity) { LocalProximity = ProximityUtility.ComputePreciseProximity(Collection); UseProximity = &LocalProximity; } else { ProximityUtility.RequireProximity(); UseProximity = &Collection.GetAttribute>("Proximity", FGeometryCollection::GeometryGroup).GetConstArray(); } const TManagedArray* Level = Collection.FindAttribute("Level", FGeometryCollection::TransformGroup); if (!ensure(Level != nullptr)) { // cluster merging requires a level attribute return; } TArray GeoBoxesInWorldSpace; TArray GlobalTransforms; GeometryCollectionAlgo::GlobalMatrices(Collection.Transform, Collection.Parent, GlobalTransforms); GeoBoxesInWorldSpace.SetNum(Collection.TransformIndex.Num()); for (int32 GeoIdx = 0; GeoIdx < GeoBoxesInWorldSpace.Num(); ++GeoIdx) { FTransform ToWorld = GlobalTransforms[Collection.TransformIndex[GeoIdx]]; FBox GeoBox(ForceInit); int32 VStart = Collection.VertexStart[GeoIdx]; int32 VEnd = VStart + Collection.VertexCount[GeoIdx]; for (int32 VIdx = VStart; VIdx < VEnd; ++VIdx) { GeoBox += ToWorld.TransformPosition((FVector)Collection.Vertex[VIdx]); } GeoBoxesInWorldSpace[GeoIdx] = GeoBox; } // helper to grab same level neighbors from the proximity info + traversal // only selects rigid or cluster bones // Note we do the traversal each time rather using a pre-computed connection graph because the cluster neighbors will change as we merge clusters // (the Proximity attribute should be stable because it is on the geometry group, not the transform group) auto GetSameLevelNeighbors = [&Collection, &UseProximity, &Level, bOnlySameParent](int32 BoneIdx, TSet& OutNeighbors) -> void { int32 TargetLevel = (*Level)[BoneIdx]; TArray ToProcess; ToProcess.Add(BoneIdx); int32 BoneParent = Collection.Parent[BoneIdx]; while (!ToProcess.IsEmpty()) { int32 ProcessIdx = ToProcess.Pop(); // we're processing a rigid node: access the proximity graph and look for neighbor nodes if (Collection.SimulationType[ProcessIdx] == FGeometryCollection::ESimulationTypes::FST_Rigid && Collection.TransformToGeometryIndex[ProcessIdx] != INDEX_NONE) { for (int32 NbrGeo : (*UseProximity)[Collection.TransformToGeometryIndex[ProcessIdx]]) { // Traverse parents to find a same-level cluster on this neighbor int32 TraverseParent = Collection.TransformIndex[NbrGeo]; while (TraverseParent != -1 && (*Level)[TraverseParent] > TargetLevel) { TraverseParent = Collection.Parent[TraverseParent]; } if (TraverseParent == -1) // failed to find a same-level neighbor { continue; } // any different, same-level cluster is a valid neighbor to consider bool bIsRigid = Collection.SimulationType[TraverseParent] == FGeometryCollection::ESimulationTypes::FST_Rigid; // A valid cluster is one that has not been emptied by a previous merge (though we should not reach such an empty cluster in this traversal) bool bIsValidCluster = !Collection.Children[TraverseParent].IsEmpty() && Collection.SimulationType[TraverseParent] == FGeometryCollection::ESimulationTypes::FST_Clustered; bool bValidParent = !bOnlySameParent || Collection.Parent[TraverseParent] == BoneParent; if (TraverseParent != BoneIdx && TraverseParent != -1 && (*Level)[TraverseParent] == TargetLevel && bValidParent && (bIsRigid || bIsValidCluster)) // only merge to rigid or cluster bones { OutNeighbors.Add(TraverseParent); } } } // we're processing a cluster node: traverse down to children to find a rigid node with proximity info else if (Collection.SimulationType[ProcessIdx] == FGeometryCollection::ESimulationTypes::FST_Clustered) { ToProcess.Reserve(ToProcess.Num() + Collection.Children[ProcessIdx].Num()); for (int32 ChildIdx : Collection.Children[ProcessIdx]) { ToProcess.Add(ChildIdx); } } } }; TSet ReconsiderIfSmall, DoNotReconsider; // Sets to track nodes we've already merged, and may not need to re-merge TSet Neighbors; // Set to be re-used in the below loop, tracking neighbors to consider TArray ChildNodes; // Array to be re-used in the below loop, tracking nodes to merge under a new parent TArray ToProcess(SmallTransformIndices); // Need a mutable copy of SmallTransformIndices to update the array with merged nodes for (int32 ProcessIdx = 0; ProcessIdx < ToProcess.Num(); ++ProcessIdx) { int32 MergeIdx = ToProcess[ProcessIdx]; if (Collection.Parent[MergeIdx] == -1 || DoNotReconsider.Contains(MergeIdx)) // can't merge root nodes { continue; } bool bIsCluster = Collection.SimulationType[MergeIdx] == FGeometryCollection::ESimulationTypes::FST_Clustered; if (ReconsiderIfSmall.Contains(MergeIdx)) { double Volume = GetClusterVolumeFromRigidsHelper(Collection, Volumes, MergeIdx); if (Volume > MinVolume) { continue; } } Neighbors.Reset(); GetSameLevelNeighbors(MergeIdx, Neighbors); int32 BestNeighbor = INDEX_NONE; if (Neighbors.IsEmpty() && !bOnlyMergeInProximity) { FVector Center = GetClusterBoundsFromRigidsHelper(Collection, GeoBoxesInWorldSpace, MergeIdx).GetCenter(); double ClosestDistSq = FMathd::MaxReal; for (int32 TransformIdx = 0; TransformIdx < Collection.Transform.Num(); ++TransformIdx) { if (TransformIdx == MergeIdx || (*Level)[MergeIdx] != (*Level)[TransformIdx] || Collection.SimulationType[TransformIdx] != FGeometryCollection::ESimulationTypes::FST_Clustered) { continue; } FVector OtherCenter = GetClusterBoundsFromRigidsHelper(Collection, GeoBoxesInWorldSpace, TransformIdx).GetCenter(); double DistSq = FVector::DistSquared(Center, OtherCenter); if (BestNeighbor == INDEX_NONE || DistSq < ClosestDistSq) { BestNeighbor = TransformIdx; ClosestDistSq = DistSq; } } } else { if (NeighborSelectionMethod == ENeighborSelectionMethod::NearestCenter) { FVector Center = GetClusterBoundsFromRigidsHelper(Collection, GeoBoxesInWorldSpace, MergeIdx).GetCenter(); double ClosestDistSq = FMathd::MaxReal; for (int32 NeighborIdx : Neighbors) { FVector OtherCenter = GetClusterBoundsFromRigidsHelper(Collection, GeoBoxesInWorldSpace, NeighborIdx).GetCenter(); double DistSq = FVector::DistSquared(Center, OtherCenter); if (BestNeighbor == INDEX_NONE || DistSq < ClosestDistSq) { BestNeighbor = NeighborIdx; ClosestDistSq = DistSq; } } } else // ENeighborSelectionMethod::LargestNeighbor { double Volume = GetClusterVolumeFromRigidsHelper(Collection, Volumes, MergeIdx); double LargestVolume = -1; for (int32 NeighborIdx : Neighbors) { double NbrVolume = GetClusterVolumeFromRigidsHelper(Collection, Volumes, NeighborIdx); if (BestNeighbor == INDEX_NONE || NbrVolume > LargestVolume) { BestNeighbor = NeighborIdx; LargestVolume = NbrVolume; } } } } if (BestNeighbor == INDEX_NONE) { continue; } int32 ToMerge = MergeIdx; if (Collection.SimulationType[BestNeighbor] != FGeometryCollection::ESimulationTypes::FST_Clustered) { if (Collection.SimulationType[ToMerge] == FGeometryCollection::ESimulationTypes::FST_Clustered) { Swap(ToMerge, BestNeighbor); } else { // both nodes are rigid: need to create a new cluster for them ChildNodes.Reset(); ChildNodes.Add(MergeIdx); ChildNodes.Add(BestNeighbor); int32 NewCluster = FGeometryCollectionClusteringUtility::ClusterBonesUnderNewNode(&Collection, MergeIdx, ChildNodes, true, false); // Now that the node is clustered at a lower level, we shouldn't try to re-merge it with any other nodes DoNotReconsider.Add(BestNeighbor); double Volume = GetClusterVolumeFromRigidsHelper(Collection, Volumes, NewCluster); if (Volume < MinVolume) { // if the new cluster is still small, we may want to merge it again ToProcess[ProcessIdx] = NewCluster; ProcessIdx--; // this re-considers ProcessIdx on the next iteration } continue; } } // From the if / swap above, BestNeighbor must now be a cluster node, so we can merge under it checkSlow(Collection.SimulationType[BestNeighbor] == FGeometryCollection::ESimulationTypes::FST_Clustered); int32 ParentNode = BestNeighbor; ChildNodes.Reset(); if (!bIsCluster) { ChildNodes.Add(ToMerge); } else { ChildNodes.Append(Collection.Children[ToMerge].Array()); } // Note: This updates the Level, Children and Parent attributes FGeometryCollectionClusteringUtility::ClusterBonesUnderExistingNode(&Collection, ParentNode, ChildNodes); ReconsiderIfSmall.Add(BestNeighbor); } FGeometryCollectionClusteringUtility::RemoveDanglingClusters(&Collection); FGeometryCollectionClusteringUtility::RemoveClustersOfOnlyOneChild(&Collection); } namespace { void AddAllChildrenExceptEmbedded(FGeometryCollection& Collection, TSet& NodesSet, int32 Node) { if (Collection.SimulationType[Node] != FGeometryCollection::ESimulationTypes::FST_None) { NodesSet.Add(Node); for (int32 Child : Collection.Children[Node]) { AddAllChildrenExceptEmbedded(Collection, NodesSet, Child); } } } } void MergeAllSelectedBones( FGeometryCollection& Collection, const TArrayView& TransformIndices, bool bUnionJoinedPieces ) { if (TransformIndices.IsEmpty()) { return; } if (!Collection.HasAttribute("Level", FGeometryCollection::TransformGroup)) { FGeometryCollectionClusteringUtility::UpdateHierarchyLevelOfChildren(&Collection, -1); } const TManagedArray& Level = Collection.GetAttribute("Level", FGeometryCollection::TransformGroup); int32 TopLevelNode = TransformIndices[0]; int32 TopLevel = Level[TopLevelNode]; TSet AllNodesSet; for (int32 Node : TransformIndices) { if (Level[Node] < TopLevel) { TopLevel = Level[Node]; TopLevelNode = Node; } AddAllChildrenExceptEmbedded(Collection, AllNodesSet, Node); } TArray AllNodes = AllNodesSet.Array(); if (AllNodes.Num() < 2) { return; // not enough pieces to need any merging } TArray AllUpdateIndices, AllRemoveIndices; AllUpdateIndices.Add(TopLevelNode); AllRemoveIndices.Reserve(AllNodes.Num() - 1); for (int32 Node : AllNodes) { if (Node != TopLevelNode) { AllRemoveIndices.Add(Node); } } AllRemoveIndices.Sort(); AllUpdateIndices.Sort(); FTransform CellsToWorld = FTransform::Identity; FDynamicMeshCollection RemoveCollection(&Collection, AllRemoveIndices, CellsToWorld, true); FDynamicMeshCollection UpdateCollection(&Collection, AllUpdateIndices, CellsToWorld, true); using FMeshData = UE::PlanarCut::FDynamicMeshCollection::FMeshData; bool bNeedAddGeometry = false; if (UpdateCollection.Meshes.IsEmpty()) { int32 NumUVLayers = Collection.NumUVLayers(); FMeshData* MeshData = new FMeshData(NumUVLayers); MeshData->FromCollection = GeometryCollectionAlgo::GlobalMatrix(Collection.Transform, Collection.Parent, TopLevelNode); MeshData->TransformIndex = TopLevelNode; UpdateCollection.Meshes.Add(MeshData); bNeedAddGeometry = true; } check(UpdateCollection.Meshes.Num() == 1); FMeshData& UpMeshData = UpdateCollection.Meshes[0]; FDynamicMeshEditor MeshEditor(&UpMeshData.AugMesh); for (int32 RmMeshIdx = 0; RmMeshIdx < RemoveCollection.Meshes.Num(); RmMeshIdx++) { FMeshData& RmMeshData = RemoveCollection.Meshes[RmMeshIdx]; if (bUnionJoinedPieces) { FMeshBoolean Boolean(&UpMeshData.AugMesh, &RmMeshData.AugMesh, &UpMeshData.AugMesh, FMeshBoolean::EBooleanOp::Union); Boolean.bWeldSharedEdges = false; Boolean.bSimplifyAlongNewEdges = true; Boolean.Compute(); } else { FMeshIndexMappings IndexMaps_Unused; MeshEditor.AppendMesh(&RmMeshData.AugMesh, IndexMaps_Unused); } } if (UpMeshData.AugMesh.TriangleCount() > 0) { if (bNeedAddGeometry) { if (Collection.TransformToGeometryIndex[TopLevelNode] == -1) { // Create a new geometry element to fill w/ the merged geometry int32 GeometryIdx = Collection.AddElements(1, FGeometryCollection::GeometryGroup); Collection.TransformToGeometryIndex[TopLevelNode] = GeometryIdx; Collection.TransformIndex[GeometryIdx] = TopLevelNode; Collection.FaceCount[GeometryIdx] = 0; Collection.FaceStart[GeometryIdx] = Collection.Indices.Num(); Collection.VertexCount[GeometryIdx] = 0; Collection.VertexStart[GeometryIdx] = Collection.Vertex.Num(); } } Collection.SimulationType[TopLevelNode] = FGeometryCollection::ESimulationTypes::FST_Rigid; UpdateCollection.UpdateAllCollections(Collection); } TArray Children; for (int32 RmTransformIdx : AllRemoveIndices) { for (int32 ChildIdx : Collection.Children[RmTransformIdx]) { Children.Add(ChildIdx); } } if (Children.Num()) { GeometryCollectionAlgo::ParentTransforms(&Collection, TopLevelNode, Children); } // remove transforms for all geometry that was merged in FManagedArrayCollection::FProcessingParameters ProcessingParams; #if !UE_BUILD_DEBUG ProcessingParams.bDoValidation = false; #endif Collection.RemoveElements(FGeometryCollection::TransformGroup, AllRemoveIndices, ProcessingParams); } void RecomputeNormalsAndTangents(bool bOnlyTangents, bool bMakeSharpEdges, float SharpAngleDegrees, FGeometryCollection& Collection, const TArrayView& TransformIndices, bool bOnlyInternalSurfaces) { FTransform CellsToWorld = FTransform::Identity; FDynamicMeshCollection MeshCollection(&Collection, TransformIndices, CellsToWorld, true); for (int MeshIdx = 0; MeshIdx < MeshCollection.Meshes.Num(); MeshIdx++) { FDynamicMesh3& Mesh = MeshCollection.Meshes[MeshIdx].AugMesh; AugmentedDynamicMesh::ComputeTangents(Mesh, bOnlyInternalSurfaces, !bOnlyTangents, bMakeSharpEdges, SharpAngleDegrees); } MeshCollection.UpdateAllCollections(Collection); Collection.ReindexMaterials(); } int32 AddCollisionSampleVertices(double CollisionSampleSpacing, FGeometryCollection& Collection, const TArrayView& TransformIndices) { FTransform CellsToWorld = FTransform::Identity; FDynamicMeshCollection MeshCollection(&Collection, TransformIndices, CellsToWorld); MeshCollection.AddCollisionSamples(CollisionSampleSpacing); MeshCollection.UpdateAllCollections(Collection); Collection.ReindexMaterials(); // TODO: This function does not create any new bones, so we could change it to not return anything return INDEX_NONE; } template void ConvertToDynamicMeshTemplate( FDynamicMesh3& CombinedMesh, FTransform& TransformOut, bool bCenterPivot, const FGeometryCollection& Collection, TArrayView BoneTransforms, TArrayView TransformIndices, TFunction RemapMaterialIDs, bool bClearCustomAttributes = true, bool bWeldEdges = true, bool bComponentSpaceTransforms = false, bool bAllowInvisible = true, bool bSetPolygroupPerBone = false ) { FTransform CellsToWorld = FTransform::Identity; TransformOut = FTransform::Identity; FDynamicMeshCollection MeshCollection; MeshCollection.bSkipInvisible = !bAllowInvisible; MeshCollection.bComponentSpaceTransforms = bComponentSpaceTransforms && !BoneTransforms.IsEmpty(); if (BoneTransforms.Num()) { MeshCollection.Init(&Collection, BoneTransforms, TransformIndices, CellsToWorld); } else { MeshCollection.Init(&Collection, Collection.Transform, TransformIndices, CellsToWorld); } SetGeometryCollectionAttributes(CombinedMesh, Collection.NumUVLayers()); CombinedMesh.Attributes()->EnableTangents(); if (bSetPolygroupPerBone) { CombinedMesh.EnableTriangleGroups(); } int32 NumMeshes = MeshCollection.Meshes.Num(); for (int32 MeshIdx = 0; MeshIdx < NumMeshes; MeshIdx++) { FDynamicMesh3& Mesh = MeshCollection.Meshes[MeshIdx].AugMesh; if (bSetPolygroupPerBone) { Mesh.EnableTriangleGroups(); } const FTransform& FromCollection = MeshCollection.Meshes[MeshIdx].FromCollection; FMeshNormals::InitializeOverlayToPerVertexNormals(Mesh.Attributes()->PrimaryNormals(), true); AugmentedDynamicMesh::InitializeOverlayToPerVertexUVs(Mesh, Collection.NumUVLayers()); AugmentedDynamicMesh::InitializeOverlayToPerVertexTangents(Mesh); if (RemapMaterialIDs) { FDynamicMeshMaterialAttribute* MaterialIDs = Mesh.Attributes()->GetMaterialID(); for (int32 TID : Mesh.TriangleIndicesItr()) { int32 MatID = MaterialIDs->GetValue(TID); bool bIsInternal = AugmentedDynamicMesh::GetInternal(Mesh, TID); int32 NewMatID = RemapMaterialIDs(MatID, bIsInternal); MaterialIDs->SetValue(TID, NewMatID); } } if (bWeldEdges && Mesh.TriangleCount() > 0) { FMergeCoincidentMeshEdges EdgeMerge(&Mesh); EdgeMerge.Apply(); } if (MeshIdx > 0) { FDynamicMeshEditor MeshAppender(&CombinedMesh); FMeshIndexMappings IndexMaps_Unused; MeshAppender.AppendMesh(&Mesh, IndexMaps_Unused); } else { CombinedMesh = Mesh; } } if (bCenterPivot) { FAxisAlignedBox3d Bounds = CombinedMesh.GetBounds(true); FVector3d Translate = -Bounds.Center(); MeshTransforms::Translate(CombinedMesh, Translate); TransformOut = FTransform((FVector)-Translate); } if (bClearCustomAttributes) { ClearCustomGeometryCollectionAttributes(CombinedMesh); } } template void ConvertToMeshDescriptionTemplate( FMeshDescription& MeshOut, FTransform& TransformOut, bool bCenterPivot, FGeometryCollection& Collection, const TManagedArray& BoneTransforms, const TArrayView& TransformIndices, TFunction RemapMaterialIDs ) { FDynamicMesh3 CombinedMesh; ConvertToDynamicMeshTemplate(CombinedMesh, TransformOut, bCenterPivot, Collection, BoneTransforms.GetConstArray(), TransformIndices, RemapMaterialIDs, false); FDynamicMeshToMeshDescription Converter; Converter.Convert(&CombinedMesh, MeshOut, true); } void ConvertToMeshDescription( FMeshDescription& MeshOut, FTransform& TransformOut, bool bCenterPivot, FGeometryCollection& Collection, const TManagedArray& BoneTransforms, const TArrayView& TransformIndices, TFunction RemapMaterialIDs ) { ConvertToMeshDescriptionTemplate(MeshOut, TransformOut, bCenterPivot, Collection, BoneTransforms, TransformIndices, RemapMaterialIDs); } void ConvertToMeshDescription( FMeshDescription& MeshOut, FTransform& TransformOut, bool bCenterPivot, FGeometryCollection& Collection, const TManagedArray& BoneTransforms, const TArrayView& TransformIndices, TFunction RemapMaterialIDs ) { ConvertToMeshDescriptionTemplate(MeshOut, TransformOut, bCenterPivot, Collection, BoneTransforms, TransformIndices, RemapMaterialIDs); } void ConvertGeometryCollectionToDynamicMesh( FDynamicMesh3& OutputMesh, FTransform& TransformOut, bool bCenterPivot, const FGeometryCollection& Collection, bool bWeldEdges, TArrayView BoneTransforms, bool bUseRelativeTransforms, TArrayView TransformIndices, TFunction RemapMaterialIDs, bool bAllowInvisible, bool bSetPolygroupPerBone ) { ConvertToDynamicMeshTemplate(OutputMesh, TransformOut, bCenterPivot, Collection, BoneTransforms, TransformIndices, RemapMaterialIDs, true, bWeldEdges, !bUseRelativeTransforms, bAllowInvisible, bSetPolygroupPerBone); } #undef LOCTEXT_NAMESPACE