// Copyright Epic Games, Inc. All Rights Reserved. #include "Operations/OffsetMeshRegion.h" #include "Algo/Reverse.h" #include "DynamicMesh/MeshNormals.h" #include "DynamicMeshEditor.h" #include "Parameterization/DynamicMeshUVEditor.h" #include "Selections/MeshVertexSelection.h" #include "DynamicMesh/DynamicMeshChangeTracker.h" #include "Selections/MeshConnectedComponents.h" #include "Operations/ExtrudeMesh.h" #include "EdgeLoop.h" #include "DynamicSubmesh3.h" #include "Selections/QuadGridPatch.h" #include "Operations/PolyEditingUVUtil.h" #include "Operations/PolyEditingEdgeUtil.h" #include "Operations/QuadGridPatchUtil.h" #include "Operations/PolyModeling/PolyModelingMaterialUtil.h" using namespace UE::Geometry; FOffsetMeshRegion::FOffsetMeshRegion(FDynamicMesh3* mesh) : Mesh(mesh) { } bool FOffsetMeshRegion::Apply() { FMeshConnectedComponents RegionComponents(Mesh); RegionComponents.FindConnectedTriangles(Triangles); bool bAllOK = true; OffsetRegions.SetNum(RegionComponents.Num()); for (int k = 0; k < RegionComponents.Num(); ++k) { FOffsetInfo& Region = OffsetRegions[k]; Region.OffsetTids = MoveTemp(RegionComponents.Components[k].Indices); if (bOffsetFullComponentsAsSolids) { TArray AllTriangles; FMeshConnectedComponents::GrowToConnectedTriangles(Mesh, Region.OffsetTids, AllTriangles); Region.bIsSolid = AllTriangles.Num() == Region.OffsetTids.Num(); } bool bRegionOK = ApplyOffset(Region); bAllOK = bAllOK && bRegionOK; } return bAllOK; } namespace OffsetMeshRegionLocals { typedef TPair> TriVertPair; /** * @return true if mesh edges are parallel or either is degenerate (the degenerate tolerance is quite large though, 0.0001). */ bool EdgesAreParallel(FDynamicMesh3* Mesh, int32 Eid1, int32 Eid2) { FIndex2i Vids1 = Mesh->GetEdgeV(Eid1); FIndex2i Vids2 = Mesh->GetEdgeV(Eid2); FVector3d Vec1 = Mesh->GetVertex(Vids1.A) - Mesh->GetVertex(Vids1.B); FVector3d Vec2 = Mesh->GetVertex(Vids2.A) - Mesh->GetVertex(Vids2.B); if (!Vec1.Normalize(KINDA_SMALL_NUMBER) || !Vec2.Normalize(KINDA_SMALL_NUMBER)) { // A degenerate edge is parallel enough for our purposes return true; } return FMath::Abs(Vec1.Dot(Vec2)) >= 1 - KINDA_SMALL_NUMBER; } /** * Assuming GroupIDs is a list of integer IDs for a contiguous "loop", ie last * element is adjacent to first element, this function returns the index of the first * value in the first non-broken contiguous span of duplicate values. IE if the array * is [0,0,0,1,1,2,2] it will return "0" but if it were [0,0,1,1,2,2,0] it would return "2", * ie the inde of the first "1". The purpose is to allow iterating through the array * in the order [1,1,2,2,0,0,0], ie the "0" values are not split across the N-1/0 transition */ static int32 FindLoopShiftFromGroupIDs(const TArray& GroupIDs) { int32 N = GroupIDs.Num(); if (GroupIDs[0] != GroupIDs[N - 1]) { return 0; } for (int32 k = 0; k < N-1; ++k) { if (GroupIDs[k] != GroupIDs[k + 1]) { return k+1; } } return 0; // all values are the same, no shift } /** * Cycle/Shift the values in the Values array to the left ShiftNum times. * Uses a temporary array internally. */ template static void LeftShiftArray(TArray& Values, int ShiftNum) { if (ShiftNum == 0 ) return; int32 N = Values.Num(); // there is probably some clever way to do this w/o a temporary array... TArray Tmp; Tmp.SetNum(N); for (int32 k = 0; k < N; ++k) { Tmp[k] = Values[(ShiftNum+k)%N]; } Values = MoveTemp(Tmp); } struct FStitchConfigOptions { int NumSubdivisions = 0; double UVScaleFactor = 1.0; bool bGroupPerSubdivision = true; bool bUVIslandPerGroup = true; double CreaseAngleThreshold = 180.0; int SetMaterialID = 0; bool bInferMaterialID = false; }; /** * Stitch pairs of border loops of Mesh defined by LoopPairs. * */ static bool StitchRegionBorderLoopPairs_Version1( FDynamicMesh3& Mesh, const TArray& LoopPairs, const TArray>& LoopsPerEdgeNewGroupIDs, const TArray>& InnerTriOrderedEdgeLoops, FStitchConfigOptions StitchConfig, TArray& bLoopOK, TArray>& PerLoopTrianglesOut, TArray>& PerLoopGroupsOut ) { FDynamicMeshEditor Editor(&Mesh); bool bAllSuccess = true; int NumInitialLoops = LoopPairs.Num(); PerLoopTrianglesOut.SetNum(NumInitialLoops); PerLoopGroupsOut.SetNum(NumInitialLoops); bLoopOK.Init(false, NumInitialLoops); for (int32 LoopIndex = 0; LoopIndex < LoopPairs.Num(); ++LoopIndex) { // note we are assuming here that the loops are aligned w/ a group transition const TArray& OuterLoopV = LoopPairs[LoopIndex].OuterVertices; const TArray& InnerLoopV = LoopPairs[LoopIndex].InnerVertices; const TArray& InnerTriOrderedEdgeLoop = InnerTriOrderedEdgeLoops[LoopIndex]; int32 NV = OuterLoopV.Num(); // populate list of Material IDs attached to inner loop TArray MaterialIDs; FDynamicMeshMaterialAttribute* MaterialIDAttrib = (Mesh.HasAttributes() && Mesh.Attributes()->HasMaterialID()) ? Mesh.Attributes()->GetMaterialID() : nullptr; if ( MaterialIDAttrib && StitchConfig.bInferMaterialID ) { UE::Geometry::ComputeMaterialIDsForVertexPath(Mesh, InnerLoopV, true, MaterialIDs, StitchConfig.SetMaterialID); } else if ( MaterialIDAttrib ) { MaterialIDs.Init(StitchConfig.SetMaterialID, NV); } // LoopStack is the set of NumSubdivisions+1 loops that will be stitched, // where the "last" loop is the Outer loop. The "first" Inner loop is not // included in LoopStack, it has special handling below TArray> LoopStack; LoopStack.SetNum(StitchConfig.NumSubdivisions+1); LoopStack[StitchConfig.NumSubdivisions] = OuterLoopV; int LastTemp = LoopStack[StitchConfig.NumSubdivisions][0]; // build interior loops if there are any for ( int32 StackIdx = 0; StackIdx < StitchConfig.NumSubdivisions; ++StackIdx ) { LoopStack[StackIdx].Reserve(NV); double t = (double)(StackIdx+1) / (double)(StitchConfig.NumSubdivisions+1); for ( int32 k = 0; k < NV; ++k ) { FVector3d A = Mesh.GetVertex(InnerLoopV[k]); FVector3d B = Mesh.GetVertex(OuterLoopV[k]); FVector3d StackPos = Lerp(A, B, t); LoopStack[StackIdx].Add( Mesh.AppendVertex(StackPos) ); } LastTemp = LoopStack[StackIdx][0]; } // build the first strip from the Inner loop to the next loop FDynamicMeshEditResult StitchResult; TArray InnerVertexLoop, TempEdgeLoop; UE::Geometry::ConvertTriOrderedEdgeLoopToLoop(Mesh, InnerTriOrderedEdgeLoop, InnerVertexLoop, &TempEdgeLoop); bLoopOK[LoopIndex] = Editor.StitchVertexLoopsMinimal(InnerVertexLoop, LoopStack[0], StitchResult); if (!bLoopOK[LoopIndex]) { bAllSuccess = false; continue; } FQuadGridPatch QuadPatch; TArray TempSpan = LoopStack[0]; // annoying to have to make a copy like this...need to perserve for LoopStack[k-1] below though TempSpan.Add(LoopStack[0][0]); QuadPatch.InitializeFromQuadStrip(Mesh, StitchResult.NewQuads, TempSpan); QuadPatch.ReverseRows(); // need LoopStack[0] to be the second span but it's currently the first one... // incrementally stitch intermediate strips and accumulate into the Quad Patch for ( int32 k = 1; k <= StitchConfig.NumSubdivisions; ++k ) { FDynamicMeshEditResult StripResult; bLoopOK[LoopIndex] = Editor.StitchVertexLoopsMinimal(LoopStack[k-1], LoopStack[k], StripResult); FQuadGridPatch NextQuadStrip; TempSpan = LoopStack[k-1]; // must preserve LoopStack arrays currently TempSpan.Add(LoopStack[k-1][0]); NextQuadStrip.InitializeFromQuadStrip(Mesh, StripResult.NewQuads, TempSpan); QuadPatch.AppendQuadPatchRows(MoveTemp(NextQuadStrip), true); } QuadPatch.ReverseRows(); // for an extrude/offset this makes sense, quads will "start" at the transition from base loop // set group IDs on patch, propagating up the subdivisions columns const TArray& PerEdgeNewGroupIDs = LoopsPerEdgeNewGroupIDs[LoopIndex]; // is it always guaranteed to be the same length?? QuadPatch.ForEachQuad( [&](int32 QuadRow, int32 QuadCol, FIndex2i QuadTris) { int32 GroupID = PerEdgeNewGroupIDs[QuadCol]; Mesh.SetTriangleGroup(QuadTris.A, GroupID); Mesh.SetTriangleGroup(QuadTris.B, GroupID); if (MaterialIDAttrib) { MaterialIDAttrib->SetValue(QuadTris.A, MaterialIDs[QuadCol]); MaterialIDAttrib->SetValue(QuadTris.B, MaterialIDs[QuadCol]); } }); // split quad patch by group ID into a set of quad patches bool bSplitDueToCrease = false; TArray GroupStrips; QuadPatch.SplitColumnsByPredicate( [&](int32 ColumnIdx, int32 NextColumnIdx) { if (PerEdgeNewGroupIDs[ColumnIdx] != PerEdgeNewGroupIDs[NextColumnIdx]) { return true; } else if (QuadPatch.GetQuadOpeningAngleDeg(Mesh, NextColumnIdx, NextColumnIdx+1, 0) > StitchConfig.CreaseAngleThreshold) { bSplitDueToCrease = true; return true; } return false; }, GroupStrips); // compute normals and UVs for each group-quad-patch if (Mesh.HasAttributes()) { for (FQuadGridPatch& GroupPatch : GroupStrips) { UE::Geometry::ComputeNormalsForQuadPatch(Mesh, GroupPatch); if ( StitchConfig.bUVIslandPerGroup ) { UE::Geometry::ComputeUVIslandForQuadPatch(Mesh, GroupPatch, StitchConfig.UVScaleFactor); } } } // if not per-group UV islands, do it for the entire strip) if (StitchConfig.bUVIslandPerGroup == false) { UE::Geometry::ComputeUVIslandForQuadPatch(Mesh, QuadPatch, StitchConfig.UVScaleFactor); } // if we split groups due to a crease, we need to reassign groups in row 0 if (bSplitDueToCrease) { for (FQuadGridPatch& GroupPatch : GroupStrips) { int32 NewPatchGroup = Mesh.AllocateTriangleGroup(); GroupPatch.ForEachQuad([&](int32 Row, int32 Col, FIndex2i QuadTris) { Mesh.SetTriangleGroup(QuadTris.A, NewPatchGroup); Mesh.SetTriangleGroup(QuadTris.B, NewPatchGroup); }); } } // Assign a new group to each row of group-quad-patch if ( StitchConfig.bGroupPerSubdivision ) { for (FQuadGridPatch& GroupPatch : GroupStrips) { int32 NumQuadRows = GroupPatch.NumVertexRowsV-1; TArray RowGroups; RowGroups.SetNum(NumQuadRows); // if we split at a crease we need to reassign first column too... for ( int32 k =1; k < NumQuadRows; ++k ) { RowGroups[k] = Mesh.AllocateTriangleGroup(); } GroupPatch.ForEachQuad([&](int32 Row, int32 Col, FIndex2i QuadTris) { if ( Row > 0 ) { Mesh.SetTriangleGroup(QuadTris.A, RowGroups[Row]); Mesh.SetTriangleGroup(QuadTris.B, RowGroups[Row]); } }); } } // save the stitch triangles set and associated group IDs QuadPatch.GetAllTriangles(PerLoopTrianglesOut[LoopIndex]); for (int32 tid : PerLoopTrianglesOut[LoopIndex]) { PerLoopGroupsOut[LoopIndex].AddUnique( Mesh.GetTriangleGroup(tid) ); } } return bAllSuccess; } static void SetStitchStripNormalsUVs_Legacy( FDynamicMesh3& Mesh, FDynamicMeshEditor& Editor, FDynamicMeshEditResult& StitchResult, const TArray& OuterLoopV, double UVScaleFactor ) { int NumNewQuads = StitchResult.NewQuads.Num(); double AccumUVTranslation = 0; FFrame3d FirstProjectFrame; FVector3d FrameUp; for (int k = 0; k < NumNewQuads; k++) { FVector3f Normal = Editor.ComputeAndSetQuadNormal(StitchResult.NewQuads[k], true); // align axis 0 of projection frame to first edge, then for further edges, // rotate around 'up' axis to keep normal aligned and frame horizontal FFrame3d ProjectFrame; if (k == 0) { FVector3d FirstEdge = Mesh.GetVertex(OuterLoopV[1]) - Mesh.GetVertex(OuterLoopV[0]); Normalize(FirstEdge); FirstProjectFrame = FFrame3d(FVector3d::Zero(), (FVector3d)Normal); FirstProjectFrame.ConstrainedAlignAxis(0, FirstEdge, (FVector3d)Normal); FrameUp = FirstProjectFrame.GetAxis(1); ProjectFrame = FirstProjectFrame; } else { ProjectFrame = FirstProjectFrame; ProjectFrame.ConstrainedAlignAxis(2, (FVector3d)Normal, FrameUp); } if (k > 0) { AccumUVTranslation += Distance(Mesh.GetVertex(OuterLoopV[k]), Mesh.GetVertex(OuterLoopV[k-1])); } // translate horizontally such that vertical spans are adjacent in UV space (so textures tile/wrap properly) double TranslateU = UVScaleFactor * AccumUVTranslation; Editor.SetQuadUVsFromProjection(StitchResult.NewQuads[k], ProjectFrame, (float)UVScaleFactor, FVector2f((float)TranslateU, 0)); } } static bool StitchRegionBorderLoopPairs_Legacy( FDynamicMesh3& Mesh, const TArray& LoopPairs, const TArray>& LoopsPerEdgeNewGroupIDs, const TArray> OffsetStitchSides, double UVScaleFactor, TArray& bLoopOK, TArray>& PerLoopTrianglesOut, TArray>& PerLoopGroupsOut ) { FDynamicMeshEditor Editor(&Mesh); bool bAllSuccess = true; int NumInitialLoops = LoopPairs.Num(); PerLoopTrianglesOut.SetNum(NumInitialLoops); PerLoopGroupsOut.SetNum(NumInitialLoops); bLoopOK.Init(false, NumInitialLoops); for (int32 LoopIndex = 0; LoopIndex < LoopPairs.Num(); ++LoopIndex) { const FDynamicMeshEditor::FLoopPairSet& LoopPair = LoopPairs[LoopIndex]; const TArray& EdgeGroups = LoopsPerEdgeNewGroupIDs[LoopIndex]; const TArray& BaseLoopV = LoopPair.OuterVertices; const TArray& OffsetLoopV = LoopPair.InnerVertices; const TArray& OffsetLoopTriVertPairs = OffsetStitchSides[LoopIndex]; // stitch the loops FDynamicMeshEditResult StitchResult; bLoopOK[LoopIndex] = Editor.StitchVertexLoopToTriVidPairSequence(OffsetLoopTriVertPairs, LoopPair.OuterVertices, StitchResult); if (!bLoopOK[LoopIndex]) { bAllSuccess = false; continue; } // set the groups of the new quad strip created by the stitching int NumNewQuads = StitchResult.NewQuads.Num(); TArray NewGroupsInLoop; const TArray& PerEdgeNewGroupIDs = LoopsPerEdgeNewGroupIDs[LoopIndex]; // is it always guaranteed to be the same length?? for (int32 k = 0; k < NumNewQuads; k++) { Mesh.SetTriangleGroup(StitchResult.NewQuads[k].A, PerEdgeNewGroupIDs[k]); Mesh.SetTriangleGroup(StitchResult.NewQuads[k].B, PerEdgeNewGroupIDs[k]); NewGroupsInLoop.AddUnique(PerEdgeNewGroupIDs[k]); } // save the stitch triangles set and associated group IDs StitchResult.GetAllTriangles(PerLoopTrianglesOut[LoopIndex]); PerLoopGroupsOut[LoopIndex] = MoveTemp(NewGroupsInLoop); // for each polygon we created in stitch, set UVs and normals if (Mesh.HasAttributes()) { SetStitchStripNormalsUVs_Legacy(Mesh, Editor, StitchResult, BaseLoopV, UVScaleFactor); } } return bAllSuccess; } template FVector3d GetAngleWeightedAverageNormal(const FDynamicMesh3& Mesh, int32 VertexID, const ListType& TriangleList) { FVector3d ExtrusionVector = FVector3d::Zero(); // Get angle-weighted normalized average vector for (int32 TriangleID : Mesh.VtxTrianglesItr(VertexID)) { if (TriangleList.Contains(TriangleID)) { FIndex3i Triangle = Mesh.GetTriangle(TriangleID); double Angle = Mesh.GetTriInternalAngleR(TriangleID, Triangle.IndexOf(VertexID)); ExtrusionVector += Angle * Mesh.GetTriNormal(TriangleID); } } ExtrusionVector.Normalize(); return ExtrusionVector; } template FVector3d GetAngleWeightedAdjustedNormal(const FDynamicMesh3& Mesh, int32 VertexID, const ListType& TriangleList, double MaxAdjustmentScale) { FVector3d InitialExtrusionVector = GetAngleWeightedAverageNormal(Mesh, VertexID, TriangleList); // Perform an angle-weighted adjustment of the vector length. For each triangle normal, the // length needs to be multiplied by 1/cos(theta) to place the vertex in the plane that it // would be in if the face was moved a unit along triangle normal (where theta is angle of // triangle normal to the current extrusion vector). double AngleSum = 0; double Adjustment = 0; double InvertedMaxScale = FMath::Max(FMathd::ZeroTolerance, 1.0 / MaxAdjustmentScale); for (int32 TriangleID : Mesh.VtxTrianglesItr(VertexID)) { if (TriangleList.Contains(TriangleID)) { FIndex3i Triangle = Mesh.GetTriangle(TriangleID); double Angle = Mesh.GetTriInternalAngleR(TriangleID, Triangle.IndexOf(VertexID)); double CosTheta = Mesh.GetTriNormal(TriangleID).Dot(InitialExtrusionVector); if (CosTheta <= InvertedMaxScale) { CosTheta = InvertedMaxScale; } Adjustment += Angle / CosTheta; // For the average at the end AngleSum += Angle; } } Adjustment /= AngleSum; return InitialExtrusionVector * Adjustment; } } // end namespace OffsetMeshRegionLocals bool FOffsetMeshRegion::ApplyOffset(FOffsetInfo& Region) { if (UseVersion == EVersion::Legacy) { return ApplyOffset_Legacy(Region); } else // Version1, Current { return ApplyOffset_Version1(Region); } } bool FOffsetMeshRegion::ApplyOffset_Version1(FOffsetInfo& Region) { const TArray& RegionTriangles = Region.OffsetTids; // Split any bowties in the offset region. An extrusion of a bowtie creates a nonmanifold edge which is not permitted FDynamicMeshEditor TmpEditor(Mesh); FDynamicMeshEditResult IgnoreResult; TmpEditor.SplitBowtiesAtTriangles(RegionTriangles, IgnoreResult); TMap OffsetGroupMap; // Remap GroupIDs in offset region if (Mesh->HasTriangleGroups()) { for (int32 TriangleID : RegionTriangles) { int32 CurGroupID = Mesh->GetTriangleGroup(TriangleID); int32 NewGroupID = CurGroupID; int32* FoundNewGroupID = OffsetGroupMap.Find(CurGroupID); if (FoundNewGroupID == nullptr) { NewGroupID = Mesh->AllocateTriangleGroup(); OffsetGroupMap.Add(CurGroupID, NewGroupID); Region.OffsetGroups.Add(NewGroupID); } else { NewGroupID = *FoundNewGroupID; } Mesh->SetTriangleGroup(TriangleID, NewGroupID); } } FMeshRegionBoundaryLoops InitialLoops(Mesh, RegionTriangles, false); bool bOK = InitialLoops.Compute(); if (bOK == false) { return false; } AllModifiedAndNewTriangles.Append(RegionTriangles); TSet TriangleSet(RegionTriangles); // Before we start changing triangles, prepare by allocating group IDs that we'll use // for the stitched sides (doing it before changes to the mesh allows user-provided // LoopEdgesShouldHaveSameGroup functions to operate on the original mesh). TArray> LoopsEdgeGroups; TArray NewGroupIDs; LoopsEdgeGroups.SetNum(InitialLoops.Loops.Num()); for (int32 i = 0; i < InitialLoops.Loops.Num(); ++i) { TArray& LoopEids = InitialLoops.Loops[i].Edges; TArray& CurrentEdgeGroups = LoopsEdgeGroups[i]; UE::Geometry::ComputeNewGroupIDsAlongEdgeLoop(*Mesh, LoopEids, CurrentEdgeGroups, NewGroupIDs, LoopEdgesShouldHaveSameGroup); // if a group is split over the start/end of the loop, shift the loops to so that index 0 lies on a group transition, // this will simplify later processing int GroupShift = OffsetMeshRegionLocals::FindLoopShiftFromGroupIDs(CurrentEdgeGroups); if (GroupShift != 0) { OffsetMeshRegionLocals::LeftShiftArray(InitialLoops.Loops[i].Vertices, GroupShift); OffsetMeshRegionLocals::LeftShiftArray(InitialLoops.Loops[i].Edges, GroupShift); OffsetMeshRegionLocals::LeftShiftArray(CurrentEdgeGroups, GroupShift); } } FDynamicMeshEditor Editor(Mesh); TArray LoopPairs; FDynamicMeshEditResult DuplicateResult; if (Region.bIsSolid) { // In the solid case, we want to duplicate the region so we can cap it. FMeshIndexMappings IndexMap; Editor.DuplicateTriangles(RegionTriangles, IndexMap, DuplicateResult); AllModifiedAndNewTriangles.Append(DuplicateResult.NewTriangles); // Populate LoopPairs LoopPairs.SetNum(InitialLoops.Loops.Num()); for (int LoopIndex = 0; LoopIndex < InitialLoops.Loops.Num(); ++LoopIndex) { FEdgeLoop& BaseLoop = InitialLoops.Loops[LoopIndex]; FDynamicMeshEditor::FLoopPairSet& LoopPair = LoopPairs[LoopIndex]; // The original RegionTriangles are the ones that are offset, so InnerVertices/Edges // should be the boundaries of those. LoopPair.InnerVertices = BaseLoop.Vertices; LoopPair.InnerEdges = BaseLoop.Edges; // However depending on whether we extruded down or up, we may need to reverse // the loops to get them to be stitched right side out. if (!bIsPositiveOffset) { Algo::Reverse(LoopPair.InnerVertices); // Reversing the edges is slightly different because the last edge is between the first // and last vertex, and that needs to stay in the same place when vertices are reversed. int32 LastEid = LoopPair.InnerEdges.Pop(); Algo::Reverse(LoopPair.InnerEdges); LoopPair.InnerEdges.Add(LastEid); int32 LastEdgeGroupID = LoopsEdgeGroups[LoopIndex].Pop(); Algo::Reverse(LoopsEdgeGroups[LoopIndex]); LoopsEdgeGroups[LoopIndex].Add(LastEdgeGroupID); } // Now assemble the paired loop for (int32 Vid : LoopPair.InnerVertices) { LoopPair.OuterVertices.Add(IndexMap.GetNewVertex(Vid)); } FEdgeLoop::VertexLoopToEdgeLoop(Mesh, LoopPair.OuterVertices, LoopPair.OuterEdges); } } else { bOK = Editor.DisconnectTriangles(TriangleSet, InitialLoops.Loops, LoopPairs, true /*bHandleBoundaryVertices*/); } if (bOK == false) { return false; } // Store the vid-independent offset loop before we break bowties // NO LONGER NECESSARY TArray> OffsetTriOrderedEdgeLoops; OffsetTriOrderedEdgeLoops.SetNum(LoopPairs.Num()); for (int32 i = 0; i < LoopPairs.Num(); ++i) { bOK = bOK && UE::Geometry::ConvertLoopToTriOrderedEdgeLoop( *Mesh, LoopPairs[i].InnerVertices, LoopPairs[i].InnerEdges, OffsetTriOrderedEdgeLoops[i]); } if (bOK == false) { return false; } FMeshVertexSelection SelectionV(Mesh); SelectionV.SelectTriangleVertices(RegionTriangles); TArray SelectedVids = SelectionV.AsArray(); // If we need to, assemble the vertex vectors for us to use (before we actually start moving things) TArray VertexExtrudeVectors; if (ExtrusionVectorType == EVertexExtrusionVectorType::SelectionTriNormalsAngleWeightedAverage || ExtrusionVectorType == EVertexExtrusionVectorType::SelectionTriNormalsAngleWeightedAdjusted) { VertexExtrudeVectors.SetNumUninitialized(SelectedVids.Num()); // Used to test which triangles are in selection for (int32 i = 0; i < SelectedVids.Num(); ++i) { VertexExtrudeVectors[i] = ( ExtrusionVectorType == EVertexExtrusionVectorType::SelectionTriNormalsAngleWeightedAdjusted ) ? OffsetMeshRegionLocals::GetAngleWeightedAdjustedNormal(*Mesh, SelectedVids[i], TriangleSet, MaxScaleForAdjustingTriNormalsOffset) : OffsetMeshRegionLocals::GetAngleWeightedAverageNormal(*Mesh, SelectedVids[i], TriangleSet); } } else if (ExtrusionVectorType == EVertexExtrusionVectorType::VertexNormal) { VertexExtrudeVectors.SetNumUninitialized(SelectedVids.Num()); for (int32 i = 0; i < SelectedVids.Num(); ++i) { VertexExtrudeVectors[i] = Mesh->HasVertexNormals() ? (FVector3d)Mesh->GetVertexNormal(SelectedVids[i]) : FMeshNormals::ComputeVertexNormal(*Mesh, SelectedVids[i]); } } // Perform the actual vertex displacement. for (int32 i = 0; i < SelectedVids.Num(); ++i) { int32 VertexID = SelectedVids[i]; FVector3d OldPosition = Mesh->GetVertex(VertexID); FVector3d ExtrusionVector = (ExtrusionVectorType == EVertexExtrusionVectorType::Zero) ? FVector3d::Zero() : VertexExtrudeVectors[i]; FVector3d NewPosition = OffsetPositionFunc(OldPosition, ExtrusionVector, VertexID); Mesh->SetVertex(VertexID, NewPosition); } // Stitch the loops OffsetMeshRegionLocals::FStitchConfigOptions StitchConfig; StitchConfig.UVScaleFactor = this->UVScaleFactor; StitchConfig.NumSubdivisions = this->NumSubdivisions; StitchConfig.bGroupPerSubdivision = this->bGroupPerSubdivision; StitchConfig.bUVIslandPerGroup = this->bUVIslandPerGroup; StitchConfig.CreaseAngleThreshold = this->CreaseAngleThresholdDeg; StitchConfig.SetMaterialID = this->SetMaterialID; StitchConfig.bInferMaterialID = this->bInferMaterialID; TArray bLoopSuccess; bool bSuccess = OffsetMeshRegionLocals::StitchRegionBorderLoopPairs_Version1( *Mesh, LoopPairs, LoopsEdgeGroups, OffsetTriOrderedEdgeLoops, StitchConfig, bLoopSuccess, Region.StitchTriangles, Region.StitchPolygonIDs); int NumInitialLoops = LoopPairs.Num(); Region.BaseLoops.SetNum(NumInitialLoops); Region.OffsetLoops.SetNum(NumInitialLoops); for (int32 LoopIndex = 0; LoopIndex < NumInitialLoops; ++LoopIndex) { if (bLoopSuccess[LoopIndex]) { Region.BaseLoops[LoopIndex].InitializeFromVertices(Mesh, LoopPairs[LoopIndex].OuterVertices); Region.OffsetLoops[LoopIndex].InitializeFromVertices(Mesh, LoopPairs[LoopIndex].InnerVertices); } } if (Region.bIsSolid) { if (bIsPositiveOffset) { // Flip the "bottom" of the region to face outwards Editor.ReverseTriangleOrientations(DuplicateResult.NewTriangles, true); } else { Editor.ReverseTriangleOrientations(RegionTriangles, true); } } if (bSingleGroupPerArea && Mesh->HasTriangleGroups() && Region.OffsetGroups.Num() > 1) { int32 NewGroupID = Region.OffsetGroups[0]; for (int32 TriangleID : RegionTriangles) { Mesh->SetTriangleGroup(TriangleID, NewGroupID); } Region.OffsetGroups.SetNum(1); } return bSuccess; } bool FOffsetMeshRegion::ApplyOffset_Legacy(FOffsetInfo& Region) { // Store offset groups if (Mesh->HasTriangleGroups()) { for (int32 Tid : Region.OffsetTids) { Region.OffsetGroups.AddUnique(Mesh->GetTriangleGroup(Tid)); } } FMeshRegionBoundaryLoops InitialLoops(Mesh, Region.OffsetTids, false); bool bOK = InitialLoops.Compute(); if (bOK == false) { return false; } AllModifiedAndNewTriangles.Append(Region.OffsetTids); // Before we start changing triangles, prepare by allocating group IDs that we'll use // for the stitched sides (doing it before changes to the mesh allows user-provided // LoopEdgesShouldHaveSameGroup functions to operate on the original mesh). TArray> LoopsEdgeGroups; TArray NewGroupIDs; LoopsEdgeGroups.SetNum(InitialLoops.Loops.Num()); for (int32 i = 0; i < InitialLoops.Loops.Num(); ++i) { TArray& LoopEids = InitialLoops.Loops[i].Edges; int32 NumEids = LoopEids.Num(); if (!ensure(NumEids > 2)) { // Shouldn't actually happen because we're extruding triangles continue; } TArray& CurrentEdgeGroups = LoopsEdgeGroups[i]; CurrentEdgeGroups.SetNumUninitialized(NumEids); CurrentEdgeGroups[0] = Mesh->AllocateTriangleGroup(); NewGroupIDs.Add(CurrentEdgeGroups[0]); // Propagate the group backwards first so we don't allocate an unnecessary group // at the end and then have to fix it. int32 LastDifferentGroupIndex = NumEids - 1; while (LastDifferentGroupIndex > 0 && LoopEdgesShouldHaveSameGroup(LoopEids[0], LoopEids[LastDifferentGroupIndex])) { CurrentEdgeGroups[LastDifferentGroupIndex] = CurrentEdgeGroups[0]; --LastDifferentGroupIndex; } // Now add new groups forward for (int32 j = 1; j <= LastDifferentGroupIndex; ++j) { if (!LoopEdgesShouldHaveSameGroup(LoopEids[j], LoopEids[j - 1])) { CurrentEdgeGroups[j] = Mesh->AllocateTriangleGroup(); NewGroupIDs.Add(CurrentEdgeGroups[j]); } else { CurrentEdgeGroups[j] = CurrentEdgeGroups[j-1]; } } } FDynamicMeshEditor Editor(Mesh); TArray LoopPairs; FDynamicMeshEditResult DuplicateResult; if (Region.bIsSolid) { // In the solid case, we want to duplicate the region so we can cap it. FMeshIndexMappings IndexMap; Editor.DuplicateTriangles(Region.OffsetTids, IndexMap, DuplicateResult); AllModifiedAndNewTriangles.Append(DuplicateResult.NewTriangles); // Populate LoopPairs LoopPairs.SetNum(InitialLoops.Loops.Num()); for (int LoopIndex = 0; LoopIndex < InitialLoops.Loops.Num(); ++LoopIndex) { FEdgeLoop& BaseLoop = InitialLoops.Loops[LoopIndex]; FDynamicMeshEditor::FLoopPairSet& LoopPair = LoopPairs[LoopIndex]; // The original OffsetTids are the ones that are offset, so InnerVertices/Edges // should be the boundaries of those. LoopPair.InnerVertices = BaseLoop.Vertices; LoopPair.InnerEdges = BaseLoop.Edges; // However depending on whether we extruded down or up, we may need to reverse // the loops to get them to be stitched right side out. if (!bIsPositiveOffset) { Algo::Reverse(LoopPair.InnerVertices); // Reversing the edges is slightly different because the last edge is between the first // and last vertex, and that needs to stay in the same place when vertices are reversed. int32 LastEid = LoopPair.InnerEdges.Pop(); Algo::Reverse(LoopPair.InnerEdges); LoopPair.InnerEdges.Add(LastEid); int32 LastEdgeGroupID = LoopsEdgeGroups[LoopIndex].Pop(); Algo::Reverse(LoopsEdgeGroups[LoopIndex]); LoopsEdgeGroups[LoopIndex].Add(LastEdgeGroupID); } // Now assemble the paired loop for (int32 Vid : LoopPair.InnerVertices) { LoopPair.OuterVertices.Add(IndexMap.GetNewVertex(Vid)); } FEdgeLoop::VertexLoopToEdgeLoop(Mesh, LoopPair.OuterVertices, LoopPair.OuterEdges); } } else { // Disconnect the triangles. Note that this will recompute FMeshRegionBoundaryLoops internally, // and so the output LoopPairs will generally correspond to InitialLoops for that reason, // but there isn't a guarantee otherwise bOK = Editor.DisconnectTriangles(Region.OffsetTids, LoopPairs, true /*bHandleBoundaryVertices*/); } if (bOK == false) { return false; } // Store the vid-independent offset loop before we break bowties TArray> OffsetStitchSides; OffsetStitchSides.SetNum(LoopPairs.Num()); for (int32 i = 0; i < LoopPairs.Num(); ++i) { bOK = bOK && FDynamicMeshEditor::ConvertLoopToTriVidPairSequence(*Mesh, LoopPairs[i].InnerVertices, LoopPairs[i].InnerEdges, OffsetStitchSides[i]); } if (bOK == false) { return false; } // Split bowties in the chosen region FDynamicMeshEditResult Result; Editor.SplitBowtiesAtTriangles(Region.OffsetTids, Result); bool bSomeLoopsBroken = Result.NewVertices.Num() > 0; // BUG: once we split bowties, there some of the existing LoopPairs may actually no longer be boundary loops. // This occurs with an "interior" bowtie, where say a figure-8 shape that was two separate loops, becomes one loop. // In this case things go downhill as OffsetTriOrderedEdgeLoops can no longer be converted back to a valid // boundary loop (since it's not a loop anymore). The FDynamicMeshEditor::StitchVertexLoopsMinimal function called below // does not actually check that the loop it is stitching is a boundary loop, it simply stitches edge pairs with quads, // which means that the function will usually appear to succeed even though it may have created broken topology. // If we broke bowties, the loops in the offset region have changed, and our OffsetLoops no longer // match BaseLoops. if (bSomeLoopsBroken) { // NOTE: Region.OffsetLoops is only used for output here, this does not affect the loops that // will be stitched, it just needs to be computed before stitching FMeshRegionBoundaryLoops UpdatedOffsetLoops(Mesh, Region.OffsetTids, false); bOK = UpdatedOffsetLoops.Compute(); if (!bOK) { return false; } Region.OffsetLoops = UpdatedOffsetLoops.Loops; } FMeshVertexSelection SelectionV(Mesh); SelectionV.SelectTriangleVertices(Region.OffsetTids); TArray SelectedVids = SelectionV.AsArray(); // If we need to, assemble the vertex vectors for us to use (before we actually start moving things) TArray VertexExtrudeVectors; if (ExtrusionVectorType == EVertexExtrusionVectorType::SelectionTriNormalsAngleWeightedAverage || ExtrusionVectorType == EVertexExtrusionVectorType::SelectionTriNormalsAngleWeightedAdjusted) { VertexExtrudeVectors.SetNumUninitialized(SelectedVids.Num()); // Used to test which triangles are in selection TSet TriangleSet(Region.OffsetTids); // Used to test which triangles are in selection for (int32 i = 0; i < SelectedVids.Num(); ++i) { VertexExtrudeVectors[i] = ( ExtrusionVectorType == EVertexExtrusionVectorType::SelectionTriNormalsAngleWeightedAdjusted ) ? OffsetMeshRegionLocals::GetAngleWeightedAdjustedNormal(*Mesh, SelectedVids[i], TriangleSet, MaxScaleForAdjustingTriNormalsOffset) : OffsetMeshRegionLocals::GetAngleWeightedAverageNormal(*Mesh, SelectedVids[i], TriangleSet); } } else if (ExtrusionVectorType == EVertexExtrusionVectorType::VertexNormal) { VertexExtrudeVectors.SetNumUninitialized(SelectedVids.Num()); for (int32 i = 0; i < SelectedVids.Num(); ++i) { int32 Vid = SelectedVids[i]; VertexExtrudeVectors[i] = Mesh->HasVertexNormals() ? (FVector3d)Mesh->GetVertexNormal(Vid) : FMeshNormals::ComputeVertexNormal(*Mesh, Vid); } } // Perform the actual vertex displacement. for (int32 i = 0; i < SelectedVids.Num(); ++i) { int32 Vid = SelectedVids[i]; FVector3d OldPosition = Mesh->GetVertex(Vid); FVector3d ExtrusionVector = (ExtrusionVectorType == EVertexExtrusionVectorType::Zero) ? FVector3d::Zero() : VertexExtrudeVectors[i]; FVector3d NewPosition = OffsetPositionFunc(OldPosition, ExtrusionVector, Vid); Mesh->SetVertex(Vid, NewPosition); } // Stitch the loops bool bSuccess = true; int NumInitialLoops = LoopPairs.Num(); Region.BaseLoops.SetNum(NumInitialLoops); if (!bSomeLoopsBroken) { Region.OffsetLoops.SetNum(NumInitialLoops); } Region.StitchTriangles.SetNum(NumInitialLoops); Region.StitchPolygonIDs.SetNum(NumInitialLoops); for (int32 LoopIndex = 0; LoopIndex < LoopPairs.Num(); ++LoopIndex) { FDynamicMeshEditor::FLoopPairSet& LoopPair = LoopPairs[LoopIndex]; const TArray& EdgeGroups = LoopsEdgeGroups[LoopIndex]; TArray& BaseLoopV = LoopPair.OuterVertices; TArray& OffsetLoopV = LoopPair.InnerVertices; TArray& OffsetLoopTriVertPairs = OffsetStitchSides[LoopIndex]; // stitch the loops FDynamicMeshEditResult StitchResult; bool bStitchSuccess = Editor.StitchVertexLoopToTriVidPairSequence(OffsetLoopTriVertPairs, LoopPair.OuterVertices, StitchResult); if (!bStitchSuccess) { bSuccess = false; continue; } // set the groups of the new quads along the stitch int NumNewQuads = StitchResult.NewQuads.Num(); for (int32 k = 0; k < NumNewQuads; k++) { Mesh->SetTriangleGroup(StitchResult.NewQuads[k].A, EdgeGroups[k]); Mesh->SetTriangleGroup(StitchResult.NewQuads[k].B, EdgeGroups[k]); } // save the stitch triangles set and associated group IDs StitchResult.GetAllTriangles(Region.StitchTriangles[LoopIndex]); Region.StitchPolygonIDs[LoopIndex] = NewGroupIDs; AllModifiedAndNewTriangles.Append(Region.StitchTriangles[LoopIndex]); // for each polygon we created in stitch, set UVs and normals if (Mesh->HasAttributes()) { float AccumUVTranslation = 0; FFrame3d FirstProjectFrame; FVector3d FrameUp; for (int k = 0; k < NumNewQuads; k++) { FVector3f Normal = Editor.ComputeAndSetQuadNormal(StitchResult.NewQuads[k], true); // align axis 0 of projection frame to first edge, then for further edges, // rotate around 'up' axis to keep normal aligned and frame horizontal FFrame3d ProjectFrame; if (k == 0) { FVector3d FirstEdge = Mesh->GetVertex(BaseLoopV[1]) - Mesh->GetVertex(BaseLoopV[0]); Normalize(FirstEdge); FirstProjectFrame = FFrame3d(FVector3d::Zero(), (FVector3d)Normal); FirstProjectFrame.ConstrainedAlignAxis(0, FirstEdge, (FVector3d)Normal); FrameUp = FirstProjectFrame.GetAxis(1); ProjectFrame = FirstProjectFrame; } else { ProjectFrame = FirstProjectFrame; ProjectFrame.ConstrainedAlignAxis(2, (FVector3d)Normal, FrameUp); } if (k > 0) { AccumUVTranslation += (float)Distance(Mesh->GetVertex(BaseLoopV[k]), Mesh->GetVertex(BaseLoopV[k - 1])); } // translate horizontally such that vertical spans are adjacent in UV space (so textures tile/wrap properly) float TranslateU = UVScaleFactor * AccumUVTranslation; Editor.SetQuadUVsFromProjection(StitchResult.NewQuads[k], ProjectFrame, UVScaleFactor, FVector2f(TranslateU, 0)); } } Region.BaseLoops[LoopIndex].InitializeFromVertices(Mesh, BaseLoopV); if (!bSomeLoopsBroken) { Region.OffsetLoops[LoopIndex].InitializeFromVertices(Mesh, OffsetLoopV); } } if (Region.bIsSolid) { if (bIsPositiveOffset) { // Flip the "bottom" of the region to face outwards Editor.ReverseTriangleOrientations(DuplicateResult.NewTriangles, true); } else { Editor.ReverseTriangleOrientations(Region.OffsetTids, true); } } return bSuccess; } bool FOffsetMeshRegion::EdgesSeparateSameGroupsAndAreColinearAtBorder(FDynamicMesh3* Mesh, int32 Eid1, int32 Eid2, bool bCheckColinearityAtBorder) { if (!Mesh->IsEdge(Eid1) || !Mesh->IsEdge(Eid2)) { return ensure(false); } FIndex2i Tris1 = Mesh->GetEdgeT(Eid1); FIndex2i Groups1(Mesh->GetTriangleGroup(Tris1.A), Tris1.B == IndexConstants::InvalidID ? IndexConstants::InvalidID : Mesh->GetTriangleGroup(Tris1.B)); FIndex2i Tris2 = Mesh->GetEdgeT(Eid2); FIndex2i Groups2(Mesh->GetTriangleGroup(Tris2.A), Tris2.B == IndexConstants::InvalidID ? IndexConstants::InvalidID : Mesh->GetTriangleGroup(Tris2.B)); if (bCheckColinearityAtBorder && Groups1.A == Groups2.A && Groups1.B == IndexConstants::InvalidID && Groups2.B == IndexConstants::InvalidID) { return OffsetMeshRegionLocals::EdgesAreParallel(Mesh, Eid1, Eid2); } else return (Groups1.A == Groups2.A && Groups1.B == Groups2.B) || (Groups1.A == Groups2.B && Groups1.B == Groups2.A); }