Files
UnrealEngine/Engine/Plugins/Runtime/GeometryProcessing/Source/DynamicMesh/Private/Operations/MeshBevel.cpp
2025-05-18 13:04:45 +08:00

3855 lines
133 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Operations/MeshBevel.h"
#include "GroupTopology.h"
#include "EdgeLoop.h"
#include "DynamicMesh/DynamicMeshChangeTracker.h"
#include "MeshWeights.h"
#include "CompGeom/PolygonTriangulation.h"
#include "DynamicMesh/MeshNormals.h"
#include "DynamicMesh/MeshIndexUtil.h"
#include "MeshRegionBoundaryLoops.h"
#include "MeshBoundaryLoops.h"
#include "Operations/PolyEditingEdgeUtil.h"
#include "Operations/PolyEditingUVUtil.h"
#include "Algo/Count.h"
#include "Algo/RemoveIf.h"
#include "Distance/DistLine3Line3.h"
#include "Operations/UniformTessellate.h"
#include "DynamicSubmesh3.h"
#include "Solvers/ConstrainedMeshSmoother.h"
#include "Solvers/ConstrainedMeshDeformer.h"
#include "Selections/MeshFaceSelection.h"
#include "Parameterization/DynamicMeshUVEditor.h"
#include "Polygon2.h"
#include "VectorUtil.h"
#include "Spatial/DenseGrid2.h"
using namespace UE::Geometry;
// Uncomment to enable various checks and ensures in the Bevel code.
// We cannot default-enable these checks even in Debug builds, because
// Bevel still frequently hits some of these cases, and those checks/ensures
// make it very difficult to debug a game using Bevel in Geometry Script (eg Lyra sample)
//#define ENABLE_MESH_BEVEL_DEBUG
#ifdef ENABLE_MESH_BEVEL_DEBUG
#define MESH_BEVEL_DEBUG_CHECK(Expr) checkSlow(Expr)
#define MESH_BEVEL_DEBUG_ENSURE(Expr) ensure(Expr)
#else
#define MESH_BEVEL_DEBUG_CHECK(Expr)
#define MESH_BEVEL_DEBUG_ENSURE(Expr) !!(Expr)
#endif
namespace UELocal
{
static void QuadsToTris(const FDynamicMesh3& Mesh, const TArray<FIndex2i>& Quads, TArray<int32>& TrisOut, bool bReset)
{
int32 N = Quads.Num();
if (bReset)
{
TrisOut.Reset();
TrisOut.Reserve(2 * N);
}
for (const FIndex2i& Quad : Quads)
{
if (Mesh.IsTriangle(Quad.A))
{
TrisOut.Add(Quad.A);
}
if (Mesh.IsTriangle(Quad.B))
{
TrisOut.Add(Quad.B);
}
}
};
}
void FMeshBevel::InitializeFromGroupTopology(const FDynamicMesh3& Mesh, const FGroupTopology& Topology)
{
ResultInfo = FGeometryResult(EGeometryResultType::InProgress);
// set up initial problem inputs
for (int32 TopoEdgeID = 0; TopoEdgeID < Topology.Edges.Num(); ++TopoEdgeID)
{
if (Topology.IsIsolatedLoop(TopoEdgeID))
{
FEdgeLoop NewLoop;
NewLoop.InitializeFromEdges(&Mesh, Topology.Edges[TopoEdgeID].Span.Edges);
AddBevelEdgeLoop(Mesh, NewLoop);
}
else
{
AddBevelGroupEdge(Mesh, Topology, TopoEdgeID);
}
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return;
}
}
// precompute topological information necessary to apply bevel to vertices/edges/loops
BuildVertexSets(Mesh);
}
void FMeshBevel::InitializeFromGroupTopologyEdges(const FDynamicMesh3& Mesh, const FGroupTopology& Topology, const TArray<int32>& GroupEdges)
{
ResultInfo = FGeometryResult(EGeometryResultType::InProgress);
// set up initial problem inputs
for (int32 TopoEdgeID : GroupEdges)
{
if (Topology.IsIsolatedLoop(TopoEdgeID))
{
FEdgeLoop NewLoop;
NewLoop.InitializeFromEdges(&Mesh, Topology.Edges[TopoEdgeID].Span.Edges);
AddBevelEdgeLoop(Mesh, NewLoop);
}
else
{
AddBevelGroupEdge(Mesh, Topology, TopoEdgeID);
}
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return;
}
}
// precompute topological information necessary to apply bevel to vertices/edges/loops
BuildVertexSets(Mesh);
}
void FMeshBevel::InitializeFromTriangleEdges(const FDynamicMesh3& Mesh, TConstArrayView<int32> TriangleEdges, TFunctionRef<bool(int32)> IsCornerVertex)
{
ResultInfo = FGeometryResult(EGeometryResultType::InProgress);
// VID to TriangleEdges Index map, used for walking chains/loops
TMultiMap<int32, int32> V2EIdx;
TArray<uint8> EdgeUsed;
EdgeUsed.SetNumZeroed(TriangleEdges.Num());
TSet<int32> AllEdgeVIDs;
for (int32 Idx = 0; Idx < TriangleEdges.Num(); ++Idx)
{
int32 EID = TriangleEdges[Idx];
if (Mesh.IsBoundaryEdge(EID))
{
// bevel doesn't support boundary edges; mark them as processed without adding them to the bevel data
EdgeUsed[Idx] = 1;
continue;
}
FIndex2i VIDs = Mesh.GetEdgeV(EID);
V2EIdx.Add(VIDs.A, Idx);
V2EIdx.Add(VIDs.B, Idx);
AllEdgeVIDs.Add(VIDs.A);
AllEdgeVIDs.Add(VIDs.B);
}
// edge chains, stored as [VID,EID] pairs and a EndChain or EndLoop as the EID at the end of each continguous chain or loop respectively
constexpr int32 EndChain = -1, EndLoop = -2;
TArray<TPair<int32,int32>> Chains;
auto ProcessEdgeIdx = [&EdgeUsed, &TriangleEdges, &Mesh, &IsCornerVertex, &V2EIdx, &Chains, EndChain, EndLoop](int32 VID, int32 EdgeIdx)
{
EdgeUsed[EdgeIdx] = 1;
int32 WalkEID = TriangleEdges[EdgeIdx];
Chains.Emplace(VID, WalkEID);
int32 StartVID = VID;
int32 WalkVID = VID;
int32 MaxIters = TriangleEdges.Num() + 2; // safety to prevent infinite loop
while (--MaxIters > 0)
{
FIndex2i EdgeV = Mesh.GetEdgeV(WalkEID);
// step WalkVID to next vertex
WalkVID = EdgeV[1 - EdgeV.IndexOf(WalkVID)];
// detect loops
if (WalkVID == StartVID)
{
Chains.Emplace(WalkVID, EndLoop);
break;
}
// stop chains at specified corner vertices
if (IsCornerVertex(WalkVID))
{
Chains.Emplace(WalkVID, EndChain);
break;
}
// non-loop case, look for a next edge
int32 WalkEIdx = -1;
int32 ECount = 0;
for (TMultiMap<int32, int32>::TConstKeyIterator It = V2EIdx.CreateConstKeyIterator(WalkVID); It; ++It)
{
ECount++;
if (EdgeUsed[It.Value()] != 0)
{
continue;
}
WalkEIdx = It.Value();
}
// if next vertex had valence != 2, it's the end of this chain
if (ECount != 2)
{
Chains.Emplace(WalkVID, EndChain);
break;
}
// walk to the next edge and mark it as processed
EdgeUsed[WalkEIdx] = 1;
WalkEID = TriangleEdges[WalkEIdx];
Chains.Emplace(WalkVID, WalkEID);
}
check(MaxIters > 0);
};
// Process all loops and chains that have at least one vertex on multiple selected edges
for (int32 VID : AllEdgeVIDs)
{
int32 NumEdges = V2EIdx.Num(VID);
if (IsCornerVertex(VID) || NumEdges != 2)
{
for (TMultiMap<int32, int32>::TConstKeyIterator It = V2EIdx.CreateConstKeyIterator(VID); It; ++It)
{
if (!EdgeUsed[It.Value()])
{
ProcessEdgeIdx(VID, It.Value());
}
}
}
}
// Process remaining (isolated) loops where all vertices had only two adjacent selected edges
for (int32 Idx = 0; Idx < EdgeUsed.Num(); ++Idx)
{
if (!EdgeUsed[Idx])
{
ProcessEdgeIdx(Mesh.GetEdgeV(TriangleEdges[Idx]).A, Idx);
}
}
// Add the edge chains and loops to the bevel data structures
TArray<int32> EdgeLoop, VertexLoop;
for (int32 ChainIdx = 0, ChainLen = 0; ChainIdx < Chains.Num(); ChainIdx += ChainLen + 1)
{
for (ChainLen = 0; ChainLen + ChainIdx < Chains.Num() && Chains[ChainIdx + ChainLen].Value >= 0; ++ChainLen)
{}
check(ChainLen > 0 && ChainIdx + ChainLen < Chains.Num());
bool bIsLoop = Chains[ChainIdx + ChainLen].Value == EndLoop;
if (bIsLoop)
{
EdgeLoop.Reset(ChainLen);
VertexLoop.Reset(ChainLen);
for (int32 Idx = ChainIdx; Idx < ChainIdx + ChainLen; ++Idx)
{
EdgeLoop.Add(Chains[Idx].Value);
VertexLoop.Add(Chains[Idx].Key);
}
// Note: Could initialize the bevel edge loop data directly rather than going via FEdgeLoop and avoid some copying
FEdgeLoop NewLoop;
NewLoop.Initialize(&Mesh, VertexLoop, EdgeLoop);
AddBevelEdgeLoop(Mesh, NewLoop);
}
else
{
FIndex2i CornerVIDs(Chains[ChainIdx].Key, Chains[ChainIdx + ChainLen].Key);
FBevelEdge Edge;
int32 NewBevelEdgeIndex = Edges.Num();
for (int32 ci = 0; ci < 2; ++ci)
{
int32 VertexID = CornerVIDs[ci];
Edge.bEndpointBoundaryFlag[ci] = Mesh.IsBoundaryVertex(VertexID);
int32 IncomingEdgeID = (ci == 0) ? Chains[ChainIdx].Value : Chains[ChainIdx + ChainLen - 1].Value;
int32 BevelVertexIndex = -1;
FBevelVertex* VertInfo = GetBevelVertexFromVertexID(VertexID, &BevelVertexIndex);
if (VertInfo == nullptr)
{
FBevelVertex NewVertex;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
NewVertex.CornerID = INDEX_NONE;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
NewVertex.VertexID = VertexID;
BevelVertexIndex = Vertices.Num();
Vertices.Add(NewVertex);
VertexIDToIndexMap.Add(VertexID, BevelVertexIndex);
VertInfo = &Vertices[BevelVertexIndex];
}
VertInfo->IncomingBevelMeshEdges.Add(IncomingEdgeID);
PRAGMA_DISABLE_DEPRECATION_WARNINGS
VertInfo->IncomingBevelTopoEdges.Add(INDEX_NONE);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
VertInfo->IncomingBevelEdgeIndices.Add(NewBevelEdgeIndex);
Edge.BevelVertices[ci] = BevelVertexIndex;
}
Edge.MeshEdges.Reset(ChainLen);
for (int32 Idx = ChainIdx; Idx < ChainIdx + ChainLen; ++Idx)
{
Edge.MeshEdges.Add(Chains[Idx].Value);
}
Edge.MeshVertices.Reset(ChainLen + 1);
for (int32 Idx = ChainIdx; Idx < ChainIdx + ChainLen + 1; ++Idx)
{
Edge.MeshVertices.Add(Chains[Idx].Key);
}
PRAGMA_DISABLE_DEPRECATION_WARNINGS
Edge.GroupEdgeID = INDEX_NONE;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
Edge.MeshEdgeTris.Reserve(Edge.MeshEdges.Num());
for (int32 eid : Edge.MeshEdges)
{
Edge.MeshEdgeTris.Add(Mesh.GetEdgeT(eid));
}
Edge.InitialPositions.Reserve(Edge.MeshVertices.Num());
for (int32 vid : Edge.MeshVertices)
{
Edge.InitialPositions.Add(Mesh.GetVertex(vid));
}
Edge.EdgeIndex = NewBevelEdgeIndex;
Edges.Add(MoveTemp(Edge));
}
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return;
}
}
// precompute topological information necessary to apply bevel to vertices/edges/loops
BuildVertexSets(Mesh);
}
bool FMeshBevel::InitializeFromGroupTopologyFaces(const FDynamicMesh3& Mesh, const FGroupTopology& Topology, const TArray<int32>& GroupFaces)
{
TSet<int32> GroupSelection;
GroupSelection.Append(GroupFaces);
bool bFoundAnythingToBevel = false;
for (int32 GroupID : GroupFaces)
{
const FGroupTopology::FGroup* Group = Topology.FindGroupByID(GroupID);
for (const FGroupTopology::FGroupBoundary& Boundary : Group->Boundaries)
{
for (int32 GroupEdgeID : Boundary.GroupEdges)
{
const FGroupTopology::FGroupEdge& GroupEdge = Topology.Edges[GroupEdgeID];
int32 OtherGroupID = GroupEdge.OtherGroupID(GroupID);
// Do not bevel edges where both sides are selected, or boundary edges
if (OtherGroupID == IndexConstants::InvalidID || GroupSelection.Contains(OtherGroupID))
{
continue;
}
if (Topology.IsIsolatedLoop(GroupEdgeID))
{
FEdgeLoop NewLoop;
NewLoop.InitializeFromEdges(&Mesh, Topology.Edges[GroupEdgeID].Span.Edges);
AddBevelEdgeLoop(Mesh, NewLoop);
bFoundAnythingToBevel = true;
}
else
{
AddBevelGroupEdge(Mesh, Topology, GroupEdgeID);
bFoundAnythingToBevel = true;
}
}
}
}
if (bFoundAnythingToBevel)
{
// precompute topological information necessary to apply bevel to vertices/edges/loops
BuildVertexSets(Mesh);
}
return bFoundAnythingToBevel;
}
bool FMeshBevel::InitializeFromTriangleSet(const FDynamicMesh3& Mesh, const TArray<int32>& Triangles)
{
ResultInfo = FGeometryResult(EGeometryResultType::InProgress);
FMeshRegionBoundaryLoops RegionLoops(&Mesh, Triangles, true);
if (RegionLoops.bFailed)
{
ResultInfo.SetFailed();
return false;
}
// cannot bevel a selection-bowtie vertex, so we have to check for those and fail here
TSet<int32> AllVertices;
for (const FEdgeLoop& Loop : RegionLoops.Loops)
{
for (int32 vid : Loop.Vertices)
{
if (AllVertices.Contains(vid))
{
return false;
}
AllVertices.Add(vid);
}
}
for (const FEdgeLoop& Loop : RegionLoops.Loops)
{
AddBevelEdgeLoop(Mesh, Loop);
}
// precompute topological information necessary to apply bevel to vertices/edges/loops
BuildVertexSets(Mesh);
return true;
}
void FMeshBevel::FixBowties(FDynamicMesh3& Mesh, FDynamicMeshChangeTracker* ChangeTracker)
{
TMultiMap<int32, int32> SplitMeshVIDs; // map from original VID to new VIDs for any bowtie vertices that were split
auto SplitBowtie = [this, &Mesh, &SplitMeshVIDs, &ChangeTracker](int32 VID, int32 EID) -> int32
{
FIndex2i EdgeV = Mesh.GetEdgeV(EID);
int32 SubIdx = EdgeV.IndexOf(VID);
check(SubIdx != INDEX_NONE);
int32 OtherVID = EdgeV[1 - SubIdx];
if (ChangeTracker)
{
ChangeTracker->SaveVertexOneRingTriangles(VID, true);
}
FDynamicMeshEditor Edit(&Mesh);
FDynamicMeshEditResult EditResult;
Edit.SplitBowties(VID, EditResult);
FIndex2i UpdatedEdgeV = Mesh.GetEdgeV(EID);
int32 OtherVIDSubIdx = UpdatedEdgeV.IndexOf(OtherVID);
check(OtherVIDSubIdx != INDEX_NONE);
int32 NewVID = UpdatedEdgeV[1 - OtherVIDSubIdx];
for (int32 AddedVID : EditResult.NewVertices)
{
SplitMeshVIDs.Add(VID, AddedVID);
}
return NewVID;
};
auto RemapVertexID = [this, &Mesh, &SplitMeshVIDs, &SplitBowtie](int32 OrigVID, int32 EID, int32& RemapVID) -> bool
{
bool bWasBowtie = SplitMeshVIDs.Contains(OrigVID);
RemapVID = OrigVID;
if (bWasBowtie)
{
FIndex2i EdgeV = Mesh.GetEdgeV(EID);
if (!EdgeV.Contains(OrigVID))
{
for (auto KeyIter = SplitMeshVIDs.CreateConstKeyIterator(OrigVID); KeyIter; ++KeyIter)
{
int32 NewVID = KeyIter.Value();
if (EdgeV.Contains(NewVID))
{
RemapVID = NewVID;
return true;
}
}
// the edge should always include one of the vertices that the bowtie was split in to
checkSlow(false);
}
return true;
}
else if (Mesh.IsBowtieVertex(OrigVID))
{
RemapVID = SplitBowtie(OrigVID, EID);
return true;
}
return false;
};
for (FBevelLoop& Loop : Loops)
{
checkSlow(Loop.MeshVertices.Num() == Loop.MeshEdges.Num());
for (int32 VertPathIdx = 0; VertPathIdx < Loop.MeshVertices.Num(); ++VertPathIdx)
{
int32 OrigVID = Loop.MeshVertices[VertPathIdx];
int32 EdgePathIdx = VertPathIdx;
int32 EID = Loop.MeshEdges[EdgePathIdx];
int32 RemapVID = OrigVID;
if (RemapVertexID(OrigVID, EID, RemapVID))
{
Loop.MeshVertices[VertPathIdx] = RemapVID;
}
}
}
int32 OrigNumVertices = Vertices.Num();
TSet<int32> BevelVerticesToRelink;
TMap<int32, int32> MeshVIDToBevelVertexIdx;
for (FBevelEdge& Edge : Edges)
{
checkSlow(Edge.MeshVertices.Num() == Edge.MeshEdges.Num() + 1);
for (int32 VertPathIdx = 0; VertPathIdx < Edge.MeshVertices.Num(); ++VertPathIdx)
{
int32 OrigVID = Edge.MeshVertices[VertPathIdx];
int32 EdgePathIdx = FMath::Min(VertPathIdx, Edge.MeshEdges.Num() - 1);
int32 EID = Edge.MeshEdges[EdgePathIdx];
int32 RemapVID = OrigVID;
if (RemapVertexID(OrigVID, EID, RemapVID))
{
Edge.MeshVertices[VertPathIdx] = RemapVID;
// if we're at an endpoint of the bevel edge, also update the bevel vertex, and make the edge point to the updated bevel vertex
if (VertPathIdx == 0 || VertPathIdx + 1 == Edge.MeshVertices.Num())
{
int32 BevelVertexSubIdx = VertPathIdx == 0 ? 0 : 1;
int32 BevelVertexIdx = Edge.BevelVertices[BevelVertexSubIdx];
VertexIDToIndexMap.Remove(OrigVID);
bool bWasAlreadyRelinked;
BevelVerticesToRelink.Add(BevelVertexIdx, &bWasAlreadyRelinked);
// after splitting bowties, recompute whether the split vertex is still a boundary vertex
Edge.bEndpointBoundaryFlag[BevelVertexSubIdx] = Mesh.IsBoundaryVertex(RemapVID);
if (!bWasAlreadyRelinked)
{
// First time encountering this bevel vertex; just remap it to the split vertex on the current edge
Vertices[BevelVertexIdx].VertexID = RemapVID;
MeshVIDToBevelVertexIdx.Add(RemapVID, BevelVertexIdx);
}
else
{
// We've encountered this bevel vertex before; use a previously-found mapping, or copy the vertex to create a new one
int32* FoundBevelVert = MeshVIDToBevelVertexIdx.Find(RemapVID);
int32 NewBevelVertIdx = INDEX_NONE;
if (FoundBevelVert)
{
NewBevelVertIdx = *FoundBevelVert;
}
else
{
// Add a copy of the existing bevel vertex
NewBevelVertIdx = Vertices.Add(FBevelVertex(Vertices[BevelVertexIdx]));
Vertices[NewBevelVertIdx].VertexID = RemapVID;
BevelVerticesToRelink.Add(NewBevelVertIdx);
MeshVIDToBevelVertexIdx.Add(RemapVID, NewBevelVertIdx);
}
Edge.BevelVertices[BevelVertexSubIdx] = NewBevelVertIdx;
}
}
}
}
}
// Fix up edge and triangle references on any relinked bevel vertices
for (int32 BevelVertexIdx : BevelVerticesToRelink)
{
FBevelVertex& Vertex = Vertices[BevelVertexIdx];
VertexIDToIndexMap.Add(Vertices[BevelVertexIdx].VertexID, BevelVertexIdx);
Vertex.VertexType = EBevelVertexType::Unknown; // reset type to unknown; will be set by InitVertexSet below
Vertex.IncomingBevelEdgeIndices.SetNum(
Algo::RemoveIf(Vertex.IncomingBevelEdgeIndices, [this, BevelVertexIdx](int32 BevelEdgeIdx)
{
return !Edges[BevelEdgeIdx].BevelVertices.Contains(BevelVertexIdx);
}));
int32 MeshVertexID = Vertex.VertexID;
Vertex.IncomingBevelMeshEdges.SetNum(
Algo::RemoveIf(Vertex.IncomingBevelMeshEdges, [this, MeshVertexID, &Mesh](int32 EdgeID)
{
return !Mesh.GetEdgeV(EdgeID).Contains(MeshVertexID);
}));
InitVertexSet(Mesh, Vertices[BevelVertexIdx]);
}
for (int32 BevelVertexIdx : BevelVerticesToRelink)
{
FBevelVertex& Vertex = Vertices[BevelVertexIdx];
FinalizeTerminatorVertex(Mesh, Vertex);
}
}
bool FMeshBevel::Apply(FDynamicMesh3& Mesh, FDynamicMeshChangeTracker* ChangeTracker)
{
FixBowties(Mesh, ChangeTracker);
// disconnect along bevel graph edges/vertices and save necessary info
UnlinkEdges(Mesh, ChangeTracker);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
UnlinkLoops(Mesh, ChangeTracker);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
UnlinkVertices(Mesh, ChangeTracker);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
FixUpUnlinkedBevelEdges(Mesh);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
// update vertex positions
DisplaceVertices(Mesh, InsetDistance);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
// mesh the bevel corners and edges
if (NumSubdivisions <= 0)
{
CreateBevelMeshing(Mesh);
}
else
{
CreateBevelMeshing_Multi(Mesh);
}
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
// build output list of triangles so it can be re-used in operations below if useful
NewTriangles.Reset();
for (FBevelVertex& Vertex : Vertices)
{
NewTriangles.Append(Vertex.NewTriangles);
}
for (FBevelEdge& Edge : Edges)
{
UELocal::QuadsToTris(Mesh, Edge.StripQuads, NewTriangles, false);
}
for (FBevelLoop& Loop : Loops)
{
UELocal::QuadsToTris(Mesh, Loop.StripQuads, NewTriangles, false);
}
// compute normals
ComputeNormals(Mesh);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
// compute UVs
ComputeUVs(Mesh);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
ComputeMaterialIDs(Mesh);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return false;
}
// todo: compute other attribs
ResultInfo.SetSuccess(true, Progress);
return true;
}
FMeshBevel::FBevelVertex* FMeshBevel::GetBevelVertexFromVertexID(int32 VertexID, int32* IndexOut)
{
int32* FoundIndex = VertexIDToIndexMap.Find(VertexID);
if (FoundIndex == nullptr)
{
return nullptr;
}
if (IndexOut != nullptr)
{
*IndexOut = *FoundIndex;
}
return &Vertices[*FoundIndex];
}
void FMeshBevel::AddBevelGroupEdge(const FDynamicMesh3& Mesh, const FGroupTopology& Topology, int32 GroupEdgeID)
{
const TArray<int32>& MeshEdgeList = Topology.Edges[GroupEdgeID].Span.Edges;
// currently cannot handle boundary edges
if ( Algo::CountIf(MeshEdgeList, [&Mesh](int EdgeID) { return Mesh.IsBoundaryEdge(EdgeID); }) > 0 )
{
return;
}
FIndex2i EdgeCornerIDs = Topology.Edges[GroupEdgeID].EndpointCorners;
FBevelEdge Edge;
int32 NewBevelEdgeIndex = Edges.Num();
for (int32 ci = 0; ci < 2; ++ci)
{
int32 CornerID = EdgeCornerIDs[ci];
FGroupTopology::FCorner Corner = Topology.Corners[CornerID];
int32 VertexID = Corner.VertexID;
Edge.bEndpointBoundaryFlag[ci] = Mesh.IsBoundaryVertex(VertexID);
int32 IncomingEdgeID = (ci == 0) ? MeshEdgeList[0] : MeshEdgeList.Last();
int32 BevelVertexIndex = -1;
FBevelVertex* VertInfo = GetBevelVertexFromVertexID(VertexID, &BevelVertexIndex);
if (VertInfo == nullptr)
{
FBevelVertex NewVertex;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
NewVertex.CornerID = CornerID;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
NewVertex.VertexID = VertexID;
BevelVertexIndex = Vertices.Num();
Vertices.Add(NewVertex);
VertexIDToIndexMap.Add(VertexID, BevelVertexIndex);
VertInfo = &Vertices[BevelVertexIndex];
}
VertInfo->IncomingBevelMeshEdges.Add(IncomingEdgeID);
PRAGMA_DISABLE_DEPRECATION_WARNINGS
VertInfo->IncomingBevelTopoEdges.Add(GroupEdgeID);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
VertInfo->IncomingBevelEdgeIndices.Add(NewBevelEdgeIndex);
Edge.BevelVertices[ci] = BevelVertexIndex;
}
Edge.MeshEdges.Append(MeshEdgeList);
Edge.MeshVertices.Append(Topology.Edges[GroupEdgeID].Span.Vertices);
PRAGMA_DISABLE_DEPRECATION_WARNINGS
Edge.GroupEdgeID = GroupEdgeID;
Edge.GroupIDs = Topology.Edges[GroupEdgeID].Groups;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
Edge.MeshEdgeTris.Reserve(Edge.MeshEdges.Num());
for (int32 eid : Edge.MeshEdges)
{
Edge.MeshEdgeTris.Add(Mesh.GetEdgeT(eid));
}
Edge.InitialPositions.Reserve(Edge.MeshVertices.Num());
for (int32 vid : Edge.MeshVertices)
{
Edge.InitialPositions.Add(Mesh.GetVertex(vid));
}
Edge.EdgeIndex = Edges.Num();
Edges.Add(MoveTemp(Edge));
}
void FMeshBevel::AddBevelEdgeLoop(const FDynamicMesh3& Mesh, const FEdgeLoop& MeshEdgeLoop)
{
// currently cannot handle boundary edges
if ( Algo::CountIf(MeshEdgeLoop.Edges, [&Mesh](int EdgeID) { return Mesh.IsBoundaryEdge(EdgeID); }) > 0 )
{
return;
}
FBevelLoop Loop;
Loop.MeshEdges = MeshEdgeLoop.Edges;
Loop.MeshVertices = MeshEdgeLoop.Vertices;
Loop.MeshEdgeTris.Reserve(Loop.MeshEdges.Num());
for (int32 eid : Loop.MeshEdges)
{
Loop.MeshEdgeTris.Add(Mesh.GetEdgeT(eid));
}
Loop.InitialPositions.Reserve(Loop.MeshVertices.Num());
for (int32 vid : Loop.MeshVertices)
{
Loop.InitialPositions.Add(Mesh.GetVertex(vid));
}
Loops.Add(Loop);
}
void FMeshBevel::InitVertexSet(const FDynamicMesh3& Mesh, FMeshBevel::FBevelVertex& Vertex)
{
// get sorted list of triangles around the vertex
TArray<int> GroupLengths;
TArray<bool> bGroupIsLoop;
EMeshResult Result = Mesh.GetVtxContiguousTriangles(Vertex.VertexID, Vertex.SortedTriangles, GroupLengths, bGroupIsLoop);
if (Result != EMeshResult::Ok || GroupLengths.Num() != 1 || Vertex.SortedTriangles.Num() < 2)
{
Vertex.VertexType = EBevelVertexType::Unknown;
return;
}
// GetVtxContiguousTriangles does not return triangles sorted in a consistent direction. This check will
// reverse the ordering such that it is consistently walking counter-clockwise around the vertex (I think...)
FIndex3i Tri0 = Mesh.GetTriangle(Vertex.SortedTriangles[0]).GetCycled(Vertex.VertexID);
FIndex3i Tri1 = Mesh.GetTriangle(Vertex.SortedTriangles[1]).GetCycled(Vertex.VertexID);
if (Tri0.C == Tri1.B)
{
Algo::Reverse(Vertex.SortedTriangles);
}
if (Mesh.IsBoundaryVertex(Vertex.VertexID))
{
Vertex.VertexType = EBevelVertexType::BoundaryVertex;
// TODO: we should have a BuildBoundaryVertex function here that correctly populates the
// Wedges for the boundary vertex. The currently BuildJunctionVertex will not be able to do
// this because it assumes it can just walk forward from any edge
return;
}
MESH_BEVEL_DEBUG_CHECK(Vertex.IncomingBevelMeshEdges.Num() != 0); // shouldn't ever happen
if (Vertex.IncomingBevelMeshEdges.Num() == 1)
{
BuildTerminatorVertex(Vertex, Mesh);
}
else
{
BuildJunctionVertex(Vertex, Mesh);
}
}
void FMeshBevel::FinalizeTerminatorVertex(const FDynamicMesh3& Mesh, FBevelVertex& Vertex)
{
if (Vertex.VertexType == EBevelVertexType::TerminatorVertex)
{
int32 OtherVertexID = Vertex.TerminatorInfo.B;
int32* OtherBevelVtxIdx = VertexIDToIndexMap.Find(OtherVertexID);
if (OtherBevelVtxIdx != nullptr)
{
// does other vertex have to be a terminator? or can this also happen w/ a junction?
FBevelVertex& OtherVertex = Vertices[*OtherBevelVtxIdx];
if (OtherVertex.VertexType == EBevelVertexType::TerminatorVertex)
{
// want to skip this if the ring-split edge is already a bevel edge
int32 MeshEdgeID = Mesh.FindEdge(Vertex.VertexID, OtherVertex.VertexID);
MESH_BEVEL_DEBUG_CHECK(MeshEdgeID >= 0);
if (Mesh.IsEdge(MeshEdgeID) &&
Vertex.TerminatorInfo.A == MeshEdgeID &&
OtherVertex.TerminatorInfo.A == MeshEdgeID && // do we need the other vertex to use the same edge here? (is this actually a hard constraint on that edge that we should be enforcing??)
Vertex.IncomingBevelMeshEdges.Contains(MeshEdgeID) == false)
{
Vertex.ConnectedBevelVertex = *OtherBevelVtxIdx;
}
}
}
}
}
void FMeshBevel::BuildVertexSets(const FDynamicMesh3& Mesh)
{
// can be parallel
for (FBevelVertex& Vertex : Vertices)
{
InitVertexSet(Mesh, Vertex);
if (ResultInfo.CheckAndSetCancelled(Progress))
{
return;
}
}
// At a 'Terminator' vertex we are going to split the one-ring and fill the bevel-side with a quad, which
// will usually leave a single-triangle hole. However if two Terminator vertices are directly connected
// via a non-bevel mesh edge, the two triangles will be connected, ie the hole is quad-shaped. It's easier
// to detect this case here /before/ we split the mesh up into disconnected parts...
for (FBevelVertex& Vertex : Vertices)
{
FinalizeTerminatorVertex(Mesh, Vertex);
}
}
void FMeshBevel::BuildJunctionVertex(FBevelVertex& Vertex, const FDynamicMesh3& Mesh)
{
//
// Now split up the single contiguous one-ring into "Wedges" created by the incoming split-edges
//
// find first split edge, and the triangle/index "after" that first split edge
int32 NT = Vertex.SortedTriangles.Num();
int32 StartTriIndex = -1;
for (int32 k = 0; k < NT; ++k)
{
if (FindSharedEdgeInTriangles(Mesh, Vertex.SortedTriangles[k], Vertex.SortedTriangles[(k + 1) % NT]) == Vertex.IncomingBevelMeshEdges[0])
{
StartTriIndex = (k + 1) % NT; // start at second tri, so that bevel-edge is first edge in wedge
break;
}
}
if (StartTriIndex == -1)
{
Vertex.VertexType = EBevelVertexType::Unknown;
return;
}
// now walk around the one-ring tris, accumulating into current Wedge until we hit another split-edge,
// at which point a new Wedge is spawned
int32 CurTriIndex = StartTriIndex;
FOneRingWedge CurWedge;
CurWedge.WedgeVertex = Vertex.VertexID;
CurWedge.Triangles.Add(Vertex.SortedTriangles[CurTriIndex]);
CurWedge.BorderEdges.A = Vertex.IncomingBevelMeshEdges[0];
for (int32 k = 0; k < NT; ++k)
{
int32 CurTri = Vertex.SortedTriangles[CurTriIndex % NT];
int32 NextTri = Vertex.SortedTriangles[(CurTriIndex + 1) % NT];
int32 SharedEdge = FindSharedEdgeInTriangles(Mesh, CurTri, NextTri);
MESH_BEVEL_DEBUG_CHECK(SharedEdge != -1);
if (Vertex.IncomingBevelMeshEdges.Contains(SharedEdge))
{
// if we found a bevel-edge, close the current wedge and start a new one
CurWedge.BorderEdges.B = SharedEdge;
Vertex.Wedges.Add(CurWedge);
CurWedge = FOneRingWedge();
CurWedge.WedgeVertex = Vertex.VertexID;
CurWedge.BorderEdges.A = SharedEdge;
}
CurWedge.Triangles.Add(NextTri);
CurTriIndex++;
}
// ?? is there a chance that we have a final open wedge here? we iterate one extra time so it shouldn't happen (or could we get an extra wedge then??)
for (FOneRingWedge& Wedge : Vertex.Wedges)
{
Wedge.BorderEdgeTriEdgeIndices.A = Mesh.GetTriEdges(Wedge.Triangles[0]).IndexOf(Wedge.BorderEdges.A);
Wedge.BorderEdgeTriEdgeIndices.B = Mesh.GetTriEdges(Wedge.Triangles.Last()).IndexOf(Wedge.BorderEdges.B);
}
if (Vertex.Wedges.Num() > 1)
{
Vertex.VertexType = EBevelVertexType::JunctionVertex;
}
else
{
Vertex.VertexType = EBevelVertexType::Unknown;
}
}
void FMeshBevel::BuildTerminatorVertex(FBevelVertex& Vertex, const FDynamicMesh3& Mesh)
{
Vertex.VertexType = EBevelVertexType::Unknown;
if (MESH_BEVEL_DEBUG_ENSURE(Vertex.IncomingBevelMeshEdges.Num() == 1) == false)
{
return;
}
int32 IncomingEdgeID = Vertex.IncomingBevelMeshEdges[0];
int32 NumTris = Vertex.SortedTriangles.Num();
// We have one edge coming into the vertex one ring, our main problem is to pick a second
// edge to split the one-ring with. There is no obvious right answer in many cases.
// "other" split edge that we pick
int32 RingSplitEdgeID = -1;
// NOTE: code below assumes we have polygroups on the mesh. In theory we could also support not having polygroups,
// by (eg) picking an arbitrary edge "furthest" from IncomingEdgeID in the exponential map
// Find the ordered set of triangles that are not in either of the groups connected to IncomingEdgeID. Eg imagine
// a cube corner, if we want to bevel IncomingEdgeID along one of the cube edges, we want to add the new edge
// in the "furthest" face (perpendicular to the edge)
FIndex2i IncomingEdgeT = Mesh.GetEdgeT(IncomingEdgeID);
FIndex2i IncomingEdgeGroups(Mesh.GetTriangleGroup(IncomingEdgeT.A), Mesh.GetTriangleGroup(IncomingEdgeT.B));
// Find the first index of Vertex.SortedTriangles that we want to remove.
// This should not be able to fail but if it does, we will just start at 0
// and maybe end up with some bevel failures below
int32 NumTriangles = Vertex.SortedTriangles.Num();
int32 StartIndex = 0;
for (int32 k = 0; k < NumTriangles; ++k)
{
int32 gid = Mesh.GetTriangleGroup(Vertex.SortedTriangles[k]);
if (IncomingEdgeGroups.Contains(gid))
{
StartIndex = k;
break;
}
}
TArray<int32> OtherGroupTris; // sorted wedge of triangles that are not in either of the groups connected to incoming edge
TArray<int32> OtherGroups; // list of group IDs encountered, in-order
for ( int32 k = 0; k < NumTriangles; ++k )
{
// Vertex.SortedTriangles was ordered, ie sequential triangles were connected, but if we filter
// out some triangles it may no longer be sequential in OtherGroupTris, leading to badness.
// But the two groups we are removing should be contiguous, so if we start there, then the
// remaining tris should be contiguous. StartIndex found above should give us that triangle.
int32 ShiftedIndex = (StartIndex + k) % NumTriangles;
int32 tid = Vertex.SortedTriangles[ShiftedIndex];
int32 gid = Mesh.GetTriangleGroup(tid);
if (IncomingEdgeGroups.Contains(gid) == false)
{
OtherGroupTris.Add(tid);
OtherGroups.AddUnique(gid);
}
}
int32 NumRemainingTris = OtherGroupTris.Num();
// Determine which edge to split at in the "other" group triangles. If we only have one group
// then we can try to pick the "middlest" edge. The worst case is when there is only one triangle,
// then we are picking a not-very-good edge no matter what (potentially we should do an edge split or
// face poke in that situation). If we have multiple groups then we probably want to pick one
// of the group-boundary edges inside the triangle-span, ideally the "middlest" but currently we
// are just picking one arbitrarily...
if (OtherGroups.Num() == 1)
{
Vertex.NewGroupID = OtherGroups[0];
if (OtherGroupTris.Num() == 1)
{
FIndex3i TriEdges = Mesh.GetTriEdges(OtherGroupTris[0]);
for (int32 j = 0; j < 3; ++j)
{
if (Mesh.GetEdgeV(TriEdges[j]).Contains(Vertex.VertexID))
{
RingSplitEdgeID = TriEdges[j];
break;
}
}
}
else if (OtherGroupTris.Num() == 2)
{
RingSplitEdgeID = FindSharedEdgeInTriangles(Mesh, OtherGroupTris[0], OtherGroupTris[1]);
}
else
{
// try using the 'middlest' triangle as the 'middlest' edge
// TODO: should compute opening angles here and pick the edge closest to the middle of the angular span!!
int32 j = OtherGroupTris.Num() / 2;
RingSplitEdgeID = FindSharedEdgeInTriangles(Mesh, OtherGroupTris[j], OtherGroupTris[j+1]);
// If the OtherGroupTris list ended up being not contiguous (see above for how that can happen), then
// it's possible that (j) and (j+1) are not connected and the share-edge search will fail. In that case
// we will just linear-search for two connected triangles. If this fails then this vertex will not be bevelled.
if (RingSplitEdgeID == -1)
{
for (int32 k = 0; k < NumRemainingTris; ++k)
{
RingSplitEdgeID = FindSharedEdgeInTriangles(Mesh, OtherGroupTris[k], OtherGroupTris[(k+1)%NumRemainingTris]);
if (RingSplitEdgeID != -1)
{
break;
}
}
}
}
}
else
{
// Search for two adjacent triangles in different groups that have a shared edge. This search
// may need to wrap around if we got unlucky in the triangle ordering in OtherGroupTris
for (int32 k = 0; k < OtherGroupTris.Num(); ++k)
{
int32 TriangleA = OtherGroupTris[k], TriangleB = OtherGroupTris[(k+1)%NumRemainingTris];
if (Mesh.GetTriangleGroup(TriangleA) != Mesh.GetTriangleGroup(TriangleB))
{
RingSplitEdgeID = FindSharedEdgeInTriangles(Mesh, TriangleA, TriangleB);
Vertex.NewGroupID = -1; // allocate a new group for this triangle, this is usually what one would want
break;
}
}
}
if (RingSplitEdgeID == -1)
{
return;
}
// save edgeid/vertexid for the 'terminator edge' that we will disconnect at
FIndex2i SplitEdgeV = Mesh.GetEdgeV(RingSplitEdgeID);
Vertex.TerminatorInfo = FIndex2i(RingSplitEdgeID, SplitEdgeV.OtherElement(Vertex.VertexID));
// split the terminator vertex into two wedges
TArray<int32> SplitTriSets[2];
if (SplitInteriorVertexTrianglesIntoSubsets(&Mesh, Vertex.VertexID, IncomingEdgeID, RingSplitEdgeID, SplitTriSets[0], SplitTriSets[1]) == false)
{
return;
}
// make the wedge list
Vertex.Wedges.SetNum(2);
Vertex.Wedges[0].WedgeVertex = Vertex.VertexID;
Vertex.Wedges[0].Triangles.Append(SplitTriSets[0]);
Vertex.Wedges[1].WedgeVertex = Vertex.VertexID;
Vertex.Wedges[1].Triangles.Append(SplitTriSets[1]);
// We need to know the two border edges of each wedge, because we will use this information
// in later stages. Note that this block is not specific to the TerminatorVertex case
for (FOneRingWedge& Wedge : Vertex.Wedges)
{
int32 NumWedgeTris = Wedge.Triangles.Num();
FIndex2i VtxEdges0 = IndexUtil::FindVertexEdgesInTriangle(Mesh, Wedge.Triangles[0], Vertex.VertexID);
if (NumWedgeTris == 1)
{
// If the wedge only has one tri, both edges are the border edges.
// Note that these are not sorted, ie B might not be the edge shared with the next Wedge.
// Currently this does not matter but it might in the future?
Wedge.BorderEdges.A = VtxEdges0.A;
Wedge.BorderEdges.B = VtxEdges0.B;
}
else
{
// the wedge-border-edge is *not* the edge connected to the next triangle in the wedge-triangle-list
Wedge.BorderEdges.A = ( Mesh.GetEdgeT(VtxEdges0.A).Contains(Wedge.Triangles[1]) ) ? VtxEdges0.B : VtxEdges0.A;
// final wedge-border-edge is the same case, with the last and second-last tris
FIndex2i VtxEdges1 = IndexUtil::FindVertexEdgesInTriangle(Mesh, Wedge.Triangles[NumWedgeTris-1], Vertex.VertexID);
Wedge.BorderEdges.B = ( Mesh.GetEdgeT(VtxEdges1.A).Contains(Wedge.Triangles[NumWedgeTris-2]) ) ? VtxEdges1.B : VtxEdges1.A;
}
// save the index of the border edge in it's triangle, so that when we disconnect the wedges later,
// we can find the new edge ID
Wedge.BorderEdgeTriEdgeIndices.A = Mesh.GetTriEdges(Wedge.Triangles[0]).IndexOf(Wedge.BorderEdges.A);
Wedge.BorderEdgeTriEdgeIndices.B = Mesh.GetTriEdges(Wedge.Triangles.Last()).IndexOf(Wedge.BorderEdges.B);
}
Vertex.VertexType = EBevelVertexType::TerminatorVertex;
}
void FMeshBevel::UnlinkEdges(FDynamicMesh3& Mesh, FDynamicMeshChangeTracker* ChangeTracker)
{
for (FBevelEdge& Edge : Edges)
{
UnlinkBevelEdgeInterior(Mesh, Edge, ChangeTracker);
}
}
namespace UELocal
{
// decomposition of a vertex one-ring into two connected triangle subsets
struct FVertexSplit
{
int32 VertexID;
bool bOK;
TArray<int32> TriSets[2];
};
// walk along a sequence of vertex-splits and make sure that the split triangle sets
// maintain consistent "sides" (see call in UnlinkBevelEdgeInterior for more details)
static void ReconcileTriangleSets(TArray<FVertexSplit>& SplitSequence)
{
int32 N = SplitSequence.Num();
TArray<int32> PrevTriSet0;
for (int32 k = 0; k < N; ++k)
{
if (PrevTriSet0.Num() == 0 && SplitSequence[k].TriSets[0].Num() > 0)
{
PrevTriSet0 = SplitSequence[k].TriSets[0];
}
else
{
bool bFoundInSet0 = false;
for (int32 tid : SplitSequence[k].TriSets[0])
{
if (PrevTriSet0.Contains(tid))
{
bFoundInSet0 = true;
break;
}
}
if (!bFoundInSet0)
{
Swap(SplitSequence[k].TriSets[0], SplitSequence[k].TriSets[1]);
}
PrevTriSet0 = SplitSequence[k].TriSets[0];
}
}
}
};
void FMeshBevel::UnlinkBevelEdgeInterior(
FDynamicMesh3& Mesh,
FBevelEdge& BevelEdge,
FDynamicMeshChangeTracker* ChangeTracker)
{
// figure out what sets of triangles to split each vertex into
int32 N = BevelEdge.MeshVertices.Num();
TArray<UELocal::FVertexSplit> SplitsToProcess;
SplitsToProcess.SetNum(N);
// precompute triangle sets for each vertex we want to split, by "cutting" the one ring into two halves based
// on edges - 2 edges for interior vertices, and 1 edge for a boundary vertex at the start/end of the edge-span
SplitsToProcess[0] = UELocal::FVertexSplit{ BevelEdge.MeshVertices[0], false };
if (BevelEdge.bEndpointBoundaryFlag[0])
{
SplitsToProcess[0].bOK = SplitBoundaryVertexTrianglesIntoSubsets(&Mesh, SplitsToProcess[0].VertexID, BevelEdge.MeshEdges[0],
SplitsToProcess[0].TriSets[0], SplitsToProcess[0].TriSets[1]);
}
for (int32 k = 1; k < N - 1; ++k)
{
SplitsToProcess[k].VertexID = BevelEdge.MeshVertices[k];
if (Mesh.IsBoundaryVertex(SplitsToProcess[k].VertexID))
{
SplitsToProcess[k].bOK = false;
}
else
{
SplitsToProcess[k].bOK = SplitInteriorVertexTrianglesIntoSubsets(&Mesh, SplitsToProcess[k].VertexID,
BevelEdge.MeshEdges[k-1], BevelEdge.MeshEdges[k], SplitsToProcess[k].TriSets[0], SplitsToProcess[k].TriSets[1]);
}
}
SplitsToProcess[N-1] = UELocal::FVertexSplit{ BevelEdge.MeshVertices[N - 1], false };
if (BevelEdge.bEndpointBoundaryFlag[1])
{
SplitsToProcess[N-1].bOK = SplitBoundaryVertexTrianglesIntoSubsets(&Mesh, SplitsToProcess[N-1].VertexID, BevelEdge.MeshEdges[N-2],
SplitsToProcess[N-1].TriSets[0], SplitsToProcess[N-1].TriSets[1]);
}
// SplitInteriorVertexTrianglesIntoSubsets does not consistently order its output sets, ie, if you imagine [Edge0,Edge1] as a path
// cutting through the one ring, the "side" that Set0 and Set1 end up is arbitrary, and depends on the ordering of edges in the triangles of Edge1.
// This might ideally be fixed in the future, but for the time being, all we need is consistency. So we walk from the start of the
// edge to the end, checking for overlap between each tri-one-ring-wedge. If Split[k].TriSet0 does not overlap with Split[k-1].TriSet0, then
// we want to swap TriSet0 and TriSet1 at Split[k].
UELocal::ReconcileTriangleSets(SplitsToProcess);
// apply vertex splits and accumulate new list
N = SplitsToProcess.Num();
for (int32 k = 0; k < N; ++k)
{
const UELocal::FVertexSplit& Split = SplitsToProcess[k];
if (ChangeTracker)
{
ChangeTracker->SaveVertexOneRingTriangles(Split.VertexID, true);
}
bool bDone = false;
if (Split.bOK)
{
FDynamicMesh3::FVertexSplitInfo SplitInfo;
EMeshResult Result = Mesh.SplitVertex(Split.VertexID, Split.TriSets[0], SplitInfo);
if (MESH_BEVEL_DEBUG_ENSURE(Result == EMeshResult::Ok))
{
BevelEdge.NewMeshVertices.Add(SplitInfo.NewVertex);
bDone = true;
}
}
if (!bDone)
{
BevelEdge.NewMeshVertices.Add(Split.VertexID);
}
}
// now build edge correspondences
N = BevelEdge.MeshVertices.Num();
MESH_BEVEL_DEBUG_CHECK(N == BevelEdge.NewMeshVertices.Num());
for (int32 k = 0; k < N-1; ++k)
{
int32 Edge0 = BevelEdge.MeshEdges[k];
int32 Edge1 = Mesh.FindEdge(BevelEdge.NewMeshVertices[k], BevelEdge.NewMeshVertices[k + 1]);
BevelEdge.NewMeshEdges.Add(Edge1);
MESH_BEVEL_DEBUG_CHECK(Edge1 >= 0);
if ( Mesh.IsEdge(Edge1) && Edge0 != Edge1 && MeshEdgePairs.Contains(Edge0) == false )
{
MeshEdgePairs.Add(Edge0, Edge1);
MeshEdgePairs.Add(Edge1, Edge0);
}
}
}
void FMeshBevel::UnlinkBevelLoop(FDynamicMesh3& Mesh, FBevelLoop& BevelLoop, FDynamicMeshChangeTracker* ChangeTracker)
{
int32 N = BevelLoop.MeshVertices.Num();
TArray<UELocal::FVertexSplit> SplitsToProcess;
SplitsToProcess.SetNum(N);
// precompute triangle sets for each vertex we want to split
for (int32 k = 0; k < N; ++k)
{
SplitsToProcess[k].VertexID = BevelLoop.MeshVertices[k];
if (Mesh.IsBoundaryVertex(SplitsToProcess[k].VertexID))
{
// cannot split boundary vertex
SplitsToProcess[k].bOK = false;
}
else
{
int32 PrevEdge = (k == 0) ? BevelLoop.MeshEdges.Last() : BevelLoop.MeshEdges[k-1];
int32 CurEdge = BevelLoop.MeshEdges[k];
SplitsToProcess[k].bOK = SplitInteriorVertexTrianglesIntoSubsets(&Mesh, SplitsToProcess[k].VertexID,
PrevEdge, CurEdge, SplitsToProcess[k].TriSets[0], SplitsToProcess[k].TriSets[1]);
}
}
// fix up triangle sets - see call in UnlinkBevelEdgeInterior() for more info
UELocal::ReconcileTriangleSets(SplitsToProcess);
// apply vertex splits and accumulate new list
N = SplitsToProcess.Num();
for (int32 k = 0; k < N; ++k)
{
const UELocal::FVertexSplit& Split = SplitsToProcess[k];
if (ChangeTracker)
{
ChangeTracker->SaveVertexOneRingTriangles(Split.VertexID, true);
}
bool bDone = false;
if (Split.bOK)
{
FDynamicMesh3::FVertexSplitInfo SplitInfo;
EMeshResult Result = Mesh.SplitVertex(Split.VertexID, Split.TriSets[1], SplitInfo);
if (MESH_BEVEL_DEBUG_ENSURE(Result == EMeshResult::Ok))
{
BevelLoop.NewMeshVertices.Add(SplitInfo.NewVertex);
bDone = true;
}
}
if (!bDone)
{
BevelLoop.NewMeshVertices.Add(Split.VertexID); // failed to split, so we have a shared vertex on both "sides"
}
}
// now build edge correspondences
N = BevelLoop.MeshVertices.Num();
MESH_BEVEL_DEBUG_CHECK(N == BevelLoop.NewMeshVertices.Num());
for (int32 k = 0; k < N; ++k)
{
int32 Edge0 = BevelLoop.MeshEdges[k];
int32 Edge1 = Mesh.FindEdge(BevelLoop.NewMeshVertices[k], BevelLoop.NewMeshVertices[(k + 1)%N]);
BevelLoop.NewMeshEdges.Add(Edge1);
MESH_BEVEL_DEBUG_CHECK(Edge1 >= 0);
if (Mesh.IsEdge(Edge1) && Edge0 != Edge1 && MeshEdgePairs.Contains(Edge0) == false )
{
MeshEdgePairs.Add(Edge0, Edge1);
MeshEdgePairs.Add(Edge1, Edge0);
}
}
}
void FMeshBevel::UnlinkLoops(FDynamicMesh3& Mesh, FDynamicMeshChangeTracker* ChangeTracker)
{
for (FBevelLoop& Loop : Loops)
{
UnlinkBevelLoop(Mesh, Loop, ChangeTracker);
}
}
void FMeshBevel::UnlinkVertices(FDynamicMesh3& Mesh, FDynamicMeshChangeTracker* ChangeTracker)
{
// TODO: currently have to do terminator vertices first because we do some of the
// determination inside the unlink code...
for (FBevelVertex& Vertex : Vertices)
{
if (Vertex.VertexType == EBevelVertexType::TerminatorVertex)
{
UnlinkTerminatorVertex(Mesh, Vertex, ChangeTracker);
}
}
for (FBevelVertex& Vertex : Vertices)
{
if (Vertex.VertexType == EBevelVertexType::JunctionVertex)
{
UnlinkJunctionVertex(Mesh, Vertex, ChangeTracker);
}
}
}
void FMeshBevel::UnlinkJunctionVertex(FDynamicMesh3& Mesh, FBevelVertex& Vertex, FDynamicMeshChangeTracker* ChangeTracker)
{
MESH_BEVEL_DEBUG_CHECK(Vertex.VertexType == EBevelVertexType::JunctionVertex);
if (ChangeTracker)
{
ChangeTracker->SaveVertexOneRingTriangles(Vertex.VertexID, true);
}
int32 NumWedges = Vertex.Wedges.Num();
MESH_BEVEL_DEBUG_CHECK(NumWedges > 1);
// Split triangles around vertex into separate tri-sets based on wedges.
// This will create a new vertex for each wedge.
for (int32 k = 1; k < NumWedges; ++k)
{
FOneRingWedge& Wedge = Vertex.Wedges[k];
FDynamicMesh3::FVertexSplitInfo SplitInfo;
EMeshResult Result = Mesh.SplitVertex(Vertex.VertexID, Wedge.Triangles, SplitInfo);
if (MESH_BEVEL_DEBUG_ENSURE(Result == EMeshResult::Ok))
{
Wedge.WedgeVertex = SplitInfo.NewVertex;
}
}
// update end start/end pairs for each wedge. If we created new edges above, this is
// the first time we will encounter them, so save in edge correspondence map
for (int32 k = 0; k < NumWedges; ++k)
{
FOneRingWedge& Wedge = Vertex.Wedges[k];
for (int32 j = 0; j < 2; ++j)
{
int32 OldWedgeEdgeID = Wedge.BorderEdges[j];
int32 OldWedgeEdgeIndex = Wedge.BorderEdgeTriEdgeIndices[j];
int32 TriangleID = (j == 0) ? Wedge.Triangles[0] : Wedge.Triangles.Last();
int32 CurWedgeEdgeID = Mesh.GetTriEdges(TriangleID)[OldWedgeEdgeIndex];
if (OldWedgeEdgeID != CurWedgeEdgeID)
{
if (MeshEdgePairs.Contains(OldWedgeEdgeID) == false)
{
MeshEdgePairs.Add(OldWedgeEdgeID, CurWedgeEdgeID);
MeshEdgePairs.Add(CurWedgeEdgeID, OldWedgeEdgeID);
}
Wedge.BorderEdges[j] = CurWedgeEdgeID;
}
}
}
}
void FMeshBevel::UnlinkTerminatorVertex(FDynamicMesh3& Mesh, FBevelVertex& BevelVertex, FDynamicMeshChangeTracker* ChangeTracker)
{
MESH_BEVEL_DEBUG_CHECK(BevelVertex.VertexType == EBevelVertexType::TerminatorVertex);
MESH_BEVEL_DEBUG_CHECK(BevelVertex.Wedges.Num() == 2);
if (ChangeTracker)
{
ChangeTracker->SaveVertexOneRingTriangles(BevelVertex.VertexID, true);
}
// split the vertex
FDynamicMesh3::FVertexSplitInfo SplitInfo;
EMeshResult Result = Mesh.SplitVertex(BevelVertex.VertexID, BevelVertex.Wedges[1].Triangles, SplitInfo);
if (MESH_BEVEL_DEBUG_ENSURE(Result == EMeshResult::Ok))
{
BevelVertex.Wedges[1].WedgeVertex = SplitInfo.NewVertex;
// update end start/end pairs for each wedge, and save in the edge correspondence map.
// Note that this is the same block as in UnlinkJunctionVertex
int32 NumWedges = BevelVertex.Wedges.Num();
for (int32 k = 0; k < NumWedges; ++k)
{
FOneRingWedge& Wedge = BevelVertex.Wedges[k];
for (int32 j = 0; j < 2; ++j)
{
int32 OldWedgeEdgeID = Wedge.BorderEdges[j];
int32 OldWedgeEdgeIndex = Wedge.BorderEdgeTriEdgeIndices[j];
int32 TriangleID = (j == 0) ? Wedge.Triangles[0] : Wedge.Triangles.Last();
int32 CurWedgeEdgeID = Mesh.GetTriEdges(TriangleID)[OldWedgeEdgeIndex];
if (OldWedgeEdgeID != CurWedgeEdgeID)
{
if (MeshEdgePairs.Contains(OldWedgeEdgeID) == false)
{
MeshEdgePairs.Add(OldWedgeEdgeID, CurWedgeEdgeID);
MeshEdgePairs.Add(CurWedgeEdgeID, OldWedgeEdgeID);
}
Wedge.BorderEdges[j] = CurWedgeEdgeID;
}
}
}
}
}
void FMeshBevel::FixUpUnlinkedBevelEdges(FDynamicMesh3& Mesh)
{
// Rewrite vertex IDs in BevelEdge vertex lists to correctly match the vertices in the new unlinked wedges.
// We did not know these new vertices in UnlinkBevelEdgeInterior() because we didn't unlink the vertices
// into wedges until afterwards.
for (FBevelEdge& Edge : Edges)
{
// In some cases, like single edges, Edge.NewMeshEdges is incorrect (mapped to itself); e.g., because we
// could not actually unlink the edge in UnlinkBevelEdgeInterior (if there were no interior vertices).
// Now that all vertices are unlinked, take one more pass through and fix the single-edge or self-mapped edges.
bool bSingleEdge = Edge.MeshEdges.Num() == 1;
bool bFailedEdgePairing = false;
for (int32 Idx = 0; Idx < Edge.MeshEdges.Num(); ++Idx)
{
bool bNewEdgeIsOld = Edge.MeshEdges[Idx] == Edge.NewMeshEdges[Idx];
if (!bSingleEdge && !bNewEdgeIsOld)
{
continue;
}
int32* FoundOtherEdge = MeshEdgePairs.Find(Edge.MeshEdges[Idx]);
MESH_BEVEL_DEBUG_CHECK(FoundOtherEdge != nullptr);
if (FoundOtherEdge != nullptr)
{
Edge.NewMeshEdges[Idx] = *FoundOtherEdge;
}
else
{
bFailedEdgePairing = true; // something went wrong, loop below will break things
break;
}
}
if (bFailedEdgePairing)
{
continue;
}
// process start and end vertices of the path
for (int32 j = 0; j < 2; ++j)
{
int32 vi = (j == 0) ? 0 : (Edge.MeshVertices.Num()-1);
int32 ei = (j == 0) ? 0 : (Edge.MeshEdges.Num()-1);
const FBevelVertex* BevelVertex = GetBevelVertexFromVertexID(Edge.MeshVertices[vi]);
int32& V0 = Edge.MeshVertices[vi];
int32& V1 = Edge.NewMeshVertices[vi];
int32 E0 = Edge.MeshEdges[ei], E1 = Edge.NewMeshEdges[ei];
bool bFoundV0 = false, bFoundV1 = false;
for (const FOneRingWedge& Wedge : BevelVertex->Wedges)
{
for (int32 tid : Wedge.Triangles)
{
FIndex3i TriEdges = Mesh.GetTriEdges(tid);
if (TriEdges.Contains(E0) && bFoundV0 == false)
{
MESH_BEVEL_DEBUG_CHECK( Mesh.GetEdgeV(E0).Contains(Wedge.WedgeVertex) );
V0 = Wedge.WedgeVertex;
bFoundV0 = true;
break;
}
else if (TriEdges.Contains(E1) && bFoundV1 == false)
{
MESH_BEVEL_DEBUG_CHECK( Mesh.GetEdgeV(E1).Contains(Wedge.WedgeVertex) );
V1 = Wedge.WedgeVertex;
bFoundV1 = true;
break;
}
}
}
}
}
}
void FMeshBevel::DisplaceVertices(FDynamicMesh3& Mesh, double Distance)
{
// fallback (very bad) technique to compute an inset vertex
auto GetDisplacedVertexPos = [Distance](const FDynamicMesh3& Mesh, int32 VertexID) -> FVector3d
{
FVector3d CurPos = Mesh.GetVertex(VertexID);
FVector3d Centroid = FMeshWeights::MeanValueCentroid(Mesh, VertexID);
return CurPos + Distance * Normalized(Centroid - CurPos);
};
// Basically want to inset any beveled edges inwards into the existing poly-faces.
// To do this we will solve using our standard inset technique, eg similar to FInsetMeshRegion,
// which involves computing 'inset lines' for each edge and then finding nearest-points between
// pairs of lines (which will be the intersection point if the face is planar).
// Need to keep track of the inset line sets for open paths because at the corner vertices we
// will need to combine data from multiple path-line-sets (possibly could do this more efficiently
// as we only ever need the first and last...but small in context)
struct FEdgePathInsetLines
{
TArray<FLine3d> InsetLines0;
TArray<FLine3d> InsetLines1;
};
TArray<FEdgePathInsetLines> AllInsetLines;
AllInsetLines.SetNum(Edges.Num());
// solve open paths
for ( int32 k = 0; k < Edges.Num(); ++k)
{
FBevelEdge& Edge = Edges[k];
UE::Geometry::ComputeInsetLineSegmentsFromEdges(Mesh, Edge.MeshEdges, InsetDistance, AllInsetLines[k].InsetLines0);
UE::Geometry::SolveInsetVertexPositionsFromInsetLines(Mesh, AllInsetLines[k].InsetLines0, Edge.MeshVertices, Edge.NewPositions0, false);
UE::Geometry::ComputeInsetLineSegmentsFromEdges(Mesh, Edge.NewMeshEdges, InsetDistance, AllInsetLines[k].InsetLines1);
UE::Geometry::SolveInsetVertexPositionsFromInsetLines(Mesh, AllInsetLines[k].InsetLines1, Edge.NewMeshVertices, Edge.NewPositions1, false);
}
// solve loops
for (FBevelLoop& Loop : Loops)
{
TArray<FLine3d> InsetLines;
UE::Geometry::ComputeInsetLineSegmentsFromEdges(Mesh, Loop.MeshEdges, InsetDistance, InsetLines);
UE::Geometry::SolveInsetVertexPositionsFromInsetLines(Mesh, InsetLines, Loop.MeshVertices, Loop.NewPositions0, true);
UE::Geometry::ComputeInsetLineSegmentsFromEdges(Mesh, Loop.NewMeshEdges, InsetDistance, InsetLines);
UE::Geometry::SolveInsetVertexPositionsFromInsetLines(Mesh, InsetLines, Loop.NewMeshVertices, Loop.NewPositions1, true);
}
// Now solve corners. For corners, we want to find the 1 or 2 inset-lines corresponding
// to the outgoing bevel-edges at each bevel-vertex-wedge.
for (FBevelVertex& Vertex : Vertices)
{
if (Vertex.VertexType == EBevelVertexType::Unknown)
{
continue;
}
int32 NumWedges = Vertex.Wedges.Num();
for (int32 k = 0; k < NumWedges; ++k)
{
FOneRingWedge& Wedge = Vertex.Wedges[k];
FVector3d CurPos = Mesh.GetVertex(Wedge.WedgeVertex);
// collect up set of inset lines relevant to this vertex
TArray<FLine3d> SolveLines;
for (int32 j : Vertex.IncomingBevelEdgeIndices)
{
if (Edges[j].MeshVertices[0] == Wedge.WedgeVertex)
{
SolveLines.Add(AllInsetLines[j].InsetLines0[0]);
}
else if (Edges[j].MeshVertices.Last() == Wedge.WedgeVertex)
{
SolveLines.Add(AllInsetLines[j].InsetLines0.Last());
}
else if (Edges[j].NewMeshVertices[0] == Wedge.WedgeVertex)
{
SolveLines.Add(AllInsetLines[j].InsetLines1[0]);
}
else if (Edges[j].NewMeshVertices.Last() == Wedge.WedgeVertex)
{
SolveLines.Add(AllInsetLines[j].InsetLines1.Last());
}
}
// todo: BoundaryVertex case never actually gets here because currently we do not initialize wedges of BoundaryVertex!
bool bIsSimpleBoundary = (Vertex.VertexType == EBevelVertexType::BoundaryVertex && SolveLines.Num() == 1);
if (Vertex.VertexType == EBevelVertexType::TerminatorVertex || bIsSimpleBoundary)
{
// TODO: this ensure has been sporadically hit in Lyra. Needs to be investigated.
//ensure(SolveLines.Num() == 1);
if (SolveLines.Num() == 1)
{
// This will be on the inset edge-line but possibly pulled away from the face the incoming terminating edge is 'hitting'
// It's fine on right-angles but you can see the problem on the front edge of a cube shaped like:
//
// *---*
// *-*
FVector3d InsetLinePosition = SolveLines[0].NearestPoint(CurPos);
Wedge.NewPosition = InsetLinePosition;
// What we ought to do is determine which group topology edges each wedge vertex should 'slide along'.
// However this is a bit complex to figure out and so for the shorter term we are just going to
// do a hack by finding the wedge-mesh-edge most aligned w/ the line inset edge.
// This will obviously fail if there are sliver triangles in the wedge that it can get confused by...
FVector3d BaseInsetDir = Normalized(InsetLinePosition - CurPos);
double MaxDot = -1;
FLine3d MaxDotEdgeLine;
Mesh.EnumerateVertexVertices(Wedge.WedgeVertex, [&](int32 othervid)
{
FLine3d EdgeLine = FLine3d::FromPoints(CurPos, Mesh.GetVertex(othervid));
double DirDot = EdgeLine.Direction.Dot(BaseInsetDir);
if (DirDot > MaxDot )
{
MaxDot = DirDot;
MaxDotEdgeLine = EdgeLine;
}
});
if (MaxDot > -1)
{
FDistLine3Line3d LineIntersection(SolveLines[0], MaxDotEdgeLine);
LineIntersection.Get();
Wedge.NewPosition = LineIntersection.Line2ClosestPoint;
}
Wedge.bHaveNewPosition = true;
}
}
else
{
MESH_BEVEL_DEBUG_CHECK(SolveLines.Num() >= 2);
if (SolveLines.Num() >= 2)
{
Wedge.NewPosition = UE::Geometry::SolveInsetVertexPositionFromLinePair(CurPos, SolveLines[0], SolveLines[1]);
Wedge.bHaveNewPosition = true;
}
}
}
}
auto SetDisplacedPositions = [&GetDisplacedVertexPos](FDynamicMesh3& Mesh, TArray<int32>& VerticesIn, TArray<FVector3d>& PositionsIn, int32 InsetStart, int32 InsetEnd)
{
int32 NumVertices = VerticesIn.Num();
if (PositionsIn.Num() == NumVertices)
{
int32 Stop = NumVertices - InsetEnd;
for (int32 k = InsetStart; k < Stop; ++k)
{
Mesh.SetVertex(VerticesIn[k], PositionsIn[k]);
}
}
};
// now bake in new positions
for (FBevelEdge& Edge : Edges)
{
SetDisplacedPositions(Mesh, Edge.MeshVertices, Edge.NewPositions0, Edge.bEndpointBoundaryFlag[0]?0:1, Edge.bEndpointBoundaryFlag[1]?0:1);
SetDisplacedPositions(Mesh, Edge.NewMeshVertices, Edge.NewPositions1, Edge.bEndpointBoundaryFlag[0]?0:1, Edge.bEndpointBoundaryFlag[1]?0:1);
}
for (FBevelLoop& Loop : Loops)
{
SetDisplacedPositions(Mesh, Loop.MeshVertices, Loop.NewPositions0, 0, 0);
SetDisplacedPositions(Mesh, Loop.NewMeshVertices, Loop.NewPositions1, 0, 0);
}
for (FBevelVertex& Vertex : Vertices)
{
for (FOneRingWedge& Wedge : Vertex.Wedges)
{
if (Wedge.bHaveNewPosition)
{
Mesh.SetVertex(Wedge.WedgeVertex, Wedge.NewPosition);
}
}
}
}
void FMeshBevel::AppendJunctionVertexPolygon(FDynamicMesh3& Mesh, FBevelVertex& Vertex)
{
MESH_BEVEL_DEBUG_CHECK(Vertex.VertexType == EBevelVertexType::JunctionVertex);
// UnlinkJunctionVertex() split the terminator vertex into N vertices, one for each
// (now disconnected) triangle-wedge. The wedges are ordered such that their wedge-vertices
// define a polygon with correct winding, so we can just mesh it and append the triangles
TArray<FVector3d> PolygonPoints;
for (FOneRingWedge& Wedge : Vertex.Wedges)
{
PolygonPoints.Add(Mesh.GetVertex(Wedge.WedgeVertex));
}
TArray<FIndex3i> Triangles;
PolygonTriangulation::TriangulateSimplePolygon<double>(PolygonPoints, Triangles);
Vertex.NewGroupID = Mesh.AllocateTriangleGroup();
for (FIndex3i Tri : Triangles)
{
int32 A = Vertex.Wedges[Tri.A].WedgeVertex;
int32 B = Vertex.Wedges[Tri.B].WedgeVertex;
int32 C = Vertex.Wedges[Tri.C].WedgeVertex;
int32 tid = Mesh.AppendTriangle(A, B, C, Vertex.NewGroupID);
if (Mesh.IsTriangle(tid))
{
Vertex.NewTriangles.Add(tid);
}
}
}
void FMeshBevel::AppendTerminatorVertexTriangle(FDynamicMesh3& Mesh, FBevelVertex& Vertex)
{
MESH_BEVEL_DEBUG_CHECK(Vertex.VertexType == EBevelVertexType::TerminatorVertex);
// UnlinkTerminatorVertex() opened up a triangle-shaped hole adjacent to the incoming edge quad-strip
// at the terminator vertex. The Wedges of the terminator vertex contain the vertex IDs of the two
// verts on the quad-strip edge. We need the third vertex. We stored [SplitEdge, FarVertexID] in
// .TerminatorInfo, however FarVertexID may have become a different vertex when we unlinked other
// vertices. So, we will try to use SplitEdge to find it.
// If this turns out to have problems, basically the QuadEdgeID is on the boundary of a 3-edge hole,
// and so it should be straightforward to find the two other boundary edges and that gives the vertex.
int32 RingSplitEdgeID = Vertex.TerminatorInfo.A;
if (Mesh.IsEdge(RingSplitEdgeID))
{
FIndex2i SplitEdgeV = Mesh.GetEdgeV(RingSplitEdgeID);
int32 FarVertexID = SplitEdgeV.OtherElement(Vertex.VertexID);
int32 QuadEdgeID = Mesh.FindEdge(Vertex.Wedges[0].WedgeVertex, Vertex.Wedges[1].WedgeVertex);
if (Mesh.IsEdge(QuadEdgeID))
{
FIndex2i QuadEdgeV = Mesh.GetOrientedBoundaryEdgeV(QuadEdgeID);
// should have computed this GroupID in initial setup
int32 UseGroupID = (Vertex.NewGroupID >= 0) ? Vertex.NewGroupID : Mesh.AllocateTriangleGroup();
int32 tid = Mesh.AppendTriangle(QuadEdgeV.B, QuadEdgeV.A, FarVertexID, UseGroupID);
if (Mesh.IsTriangle(tid))
{
Vertex.NewTriangles.Add(tid);
}
else
{
MESH_BEVEL_DEBUG_CHECK(false);
}
}
else
{
MESH_BEVEL_DEBUG_CHECK(false);
}
}
}
void FMeshBevel::AppendTerminatorVertexPairQuad(FDynamicMesh3& Mesh, FBevelVertex& Vertex0, FBevelVertex& Vertex1)
{
MESH_BEVEL_DEBUG_CHECK(Vertex0.VertexType == EBevelVertexType::TerminatorVertex);
MESH_BEVEL_DEBUG_CHECK(Vertex1.VertexType == EBevelVertexType::TerminatorVertex);
// This is a variant of AppendTerminatorVertexTriangle that handles the case where basically two
// Terminator Vertices are directly connected by a non-beveled mesh edge that was used as the ring-split-edge.
// Since both sides were opened, we have a quad-shaped hole instead of a triangle-shaped hole, with a quad-edge
// at each end. The quad can be filled directly, we just need to sort out ordering/etc
// does not seem like we need to do anything w/ the TerminatorInfo here, we can get everything from wedges
int32 QuadEdgeID0 = Mesh.FindEdge(Vertex0.Wedges[0].WedgeVertex, Vertex0.Wedges[1].WedgeVertex);
int32 QuadEdgeID1 = Mesh.FindEdge(Vertex1.Wedges[0].WedgeVertex, Vertex1.Wedges[1].WedgeVertex);
MESH_BEVEL_DEBUG_CHECK(Mesh.IsEdge(QuadEdgeID0) && Mesh.IsEdge(QuadEdgeID1));
FIndex2i QuadEdgeV0 = Mesh.GetOrientedBoundaryEdgeV(QuadEdgeID0);
FIndex2i QuadEdgeV1 = Mesh.GetOrientedBoundaryEdgeV(QuadEdgeID1);
// make sure that the two opposing/connecting edges exist
MESH_BEVEL_DEBUG_CHECK(
Mesh.FindEdge(QuadEdgeV0.A, QuadEdgeV1.B) != IndexConstants::InvalidID &&
Mesh.FindEdge(QuadEdgeV0.B, QuadEdgeV1.A) != IndexConstants::InvalidID);
// BIASED? should have computed this GroupID in initial setup
int32 UseGroupID = (Vertex0.NewGroupID >= 0) ? Vertex0.NewGroupID : Mesh.AllocateTriangleGroup();
// quad order is V0.B, V0.A, V1.B, V1.A
int32 tid0 = Mesh.AppendTriangle(QuadEdgeV0.B, QuadEdgeV0.A, QuadEdgeV1.B, UseGroupID);
MESH_BEVEL_DEBUG_CHECK(tid0 >= 0);
if (Mesh.IsTriangle(tid0))
{
Vertex0.NewTriangles.Add(tid0);
}
int32 tid1 = Mesh.AppendTriangle(QuadEdgeV0.B, QuadEdgeV1.B, QuadEdgeV1.A, UseGroupID);
MESH_BEVEL_DEBUG_CHECK(tid1 >= 0);
if (Mesh.IsTriangle(tid1))
{
Vertex1.NewTriangles.Add(tid1);
}
}
void FMeshBevel::AppendEdgeQuads(FDynamicMesh3& Mesh, FBevelEdge& Edge)
{
int32 NumEdges = Edge.MeshEdges.Num();
if (NumEdges != Edge.NewMeshEdges.Num())
{
return;
}
Edge.NewGroupID = Mesh.AllocateTriangleGroup();
// At this point each edge-span should be fully disconnected into a set of paired edges,
// so we can trivially join each edge pair with a quad.
for (int32 k = 0; k < NumEdges; ++k)
{
int32 EdgeID0 = Edge.MeshEdges[k];
int32 EdgeID1 = Edge.NewMeshEdges[k];
// in certain cases, like bevel topo-edges with a single mesh-edge, we would not
// have been able to construct the "other" mesh edge when processing the topo-edge
// (where .NewMeshEdges is computed), it would only have been created when processing the
// junction vertex. Currently we do not go back and update .NewMeshEdges in that case, but
// we do store the edge-pair-correspondence in the MeshEdgePairs map.
if (EdgeID0 == EdgeID1)
{
int32* FoundEdgeID1 = MeshEdgePairs.Find(EdgeID0);
if (FoundEdgeID1 != nullptr)
{
EdgeID1 = *FoundEdgeID1;
}
}
FIndex2i QuadTris(IndexConstants::InvalidID, IndexConstants::InvalidID);
if (EdgeID0 != EdgeID1 && Mesh.IsEdge(EdgeID1) )
{
FIndex2i EdgeV0 = Mesh.GetOrientedBoundaryEdgeV(EdgeID0);
FIndex2i EdgeV1 = Mesh.GetOrientedBoundaryEdgeV(EdgeID1);
if (EdgeV0.Contains(EdgeV1.A) || EdgeV0.Contains(EdgeV1.B))
{
// If we hit this case, it means that Edge0 and Edge1 are still connected at one end, and
// so cannot be connected by a Quad, they can only be connected by a single triangle.
// It is unclear how we end up in this situation, it does occur somewhat regularly in complex
// geometry scripts though (eg see UE-157531 for a potential repro).
int32 OtherV = EdgeV0.Contains(EdgeV1.A) ? EdgeV1.B : EdgeV1.A;
QuadTris.A = Mesh.AppendTriangle(EdgeV0.B, EdgeV0.A, OtherV, Edge.NewGroupID);
QuadTris.B = IndexConstants::InvalidID;
}
else
{
QuadTris.A = Mesh.AppendTriangle(EdgeV0.B, EdgeV0.A, EdgeV1.B, Edge.NewGroupID);
QuadTris.B = Mesh.AppendTriangle(EdgeV1.B, EdgeV1.A, EdgeV0.B, Edge.NewGroupID);
}
}
Edge.StripQuads.Add(QuadTris);
}
}
void FMeshBevel::AppendLoopQuads(FDynamicMesh3& Mesh, FBevelLoop& Loop)
{
int32 NumEdges = Loop.MeshEdges.Num();
if (NumEdges != Loop.NewMeshEdges.Num())
{
return;
}
auto GetGroupKey = [&Mesh, &Loop](int32 k)
{
FIndex2i EdgeTris = Loop.MeshEdgeTris[k];
int32 Group0 = Mesh.GetTriangleGroup(EdgeTris.A);
int32 Group1 = Mesh.IsTriangle(EdgeTris.B) ? Mesh.GetTriangleGroup(EdgeTris.B) : -1;
return FIndex2i(FMath::Max(Group0, Group1), FMath::Min(Group0, Group1));
};
TMap<FIndex2i, int> NewGroupIDs;
for (int32 k = 0; k < NumEdges; ++k)
{
FIndex2i GroupKey = GetGroupKey(k);
if (NewGroupIDs.Contains(GroupKey) == false)
{
NewGroupIDs.Add(GroupKey, Mesh.AllocateTriangleGroup());
Loop.NewGroupIDs.Add(NewGroupIDs[GroupKey]);
}
}
// At this point each edge-span should be fully disconnected into a set of paired edges,
// so we can trivially join each edge pair with a quad.
for (int32 k = 0; k < NumEdges; ++k)
{
int32 EdgeID0 = Loop.MeshEdges[k];
int32 EdgeID1 = Loop.NewMeshEdges[k];
// case that happens in AppendEdgeQuads() should never happen for loops...
FIndex2i QuadTris(IndexConstants::InvalidID, IndexConstants::InvalidID);
if (EdgeID0 != EdgeID1 && Mesh.IsEdge(EdgeID1))
{
FIndex2i GroupKey = GetGroupKey(k);
int32 NewGroupID = NewGroupIDs[GroupKey];
FIndex2i EdgeV0 = Mesh.GetOrientedBoundaryEdgeV(EdgeID0);
FIndex2i EdgeV1 = Mesh.GetOrientedBoundaryEdgeV(EdgeID1);
QuadTris.A = Mesh.AppendTriangle(EdgeV0.B, EdgeV0.A, EdgeV1.B, NewGroupID);
QuadTris.B = Mesh.AppendTriangle(EdgeV1.B, EdgeV1.A, EdgeV0.B, NewGroupID);
}
Loop.StripQuads.Add(QuadTris);
}
}
void FMeshBevel::CreateBevelMeshing(FDynamicMesh3& Mesh)
{
for (FBevelVertex& Vertex : Vertices)
{
if (Vertex.VertexType == EBevelVertexType::JunctionVertex)
{
if (Vertex.Wedges.Num() > 2)
{
AppendJunctionVertexPolygon(Mesh, Vertex);
}
}
}
for (FBevelEdge& Edge : Edges)
{
AppendEdgeQuads(Mesh, Edge);
}
for (FBevelLoop& Loop : Loops)
{
AppendLoopQuads(Mesh, Loop);
}
// easier to do terminators last so that we can use quad edge to orient the triangle
TSet<FIndex2i> HandledQuadVtxPairs;
for (FBevelVertex& Vertex : Vertices)
{
if (Vertex.VertexType == EBevelVertexType::TerminatorVertex)
{
if (Vertex.ConnectedBevelVertex >= 0)
{
FBevelVertex& OtherVertex = Vertices[Vertex.ConnectedBevelVertex];
FIndex2i VtxPair(Vertex.VertexID, OtherVertex.VertexID);
VtxPair.Sort();
if (HandledQuadVtxPairs.Contains(VtxPair) == false)
{
AppendTerminatorVertexPairQuad(Mesh, Vertex, OtherVertex);
HandledQuadVtxPairs.Add(VtxPair);
}
}
else
{
AppendTerminatorVertexTriangle(Mesh, Vertex);
}
}
}
}
void FMeshBevel::AppendEdgeQuads_Multi(FDynamicMesh3& Mesh, FBevelEdge& Edge)
{
int32 NumEdges = Edge.MeshEdges.Num();
if (NumEdges != Edge.NewMeshEdges.Num())
{
return;
}
Edge.NewGroupID = Mesh.AllocateTriangleGroup();
struct FEdgePair
{
FIndex2i EdgeV0;
FIndex2i EdgeV1;
};
TArray<FEdgePair> SequentialQuadEdges;
bool bFoundInvalidCase = false;
// At this point each edge-span should be fully disconnected into a set of paired edges,
// so we can trivially join each edge pair with a quad. (In the multi-case we will further
// subdivide this quad instead of adding it directly)
for (int32 k = 0; k < NumEdges && bFoundInvalidCase == false; ++k)
{
int32 EdgeID0 = Edge.MeshEdges[k];
int32 EdgeID1 = Edge.NewMeshEdges[k];
// in certain cases, like bevel topo-edges with a single mesh-edge, we would not
// have been able to construct the "other" mesh edge when processing the topo-edge
// (where .NewMeshEdges is computed), it would only have been created when processing the
// junction vertex. Currently we do not go back and update .NewMeshEdges in that case, but
// we do store the edge-pair-correspondence in the MeshEdgePairs map.
if (EdgeID0 == EdgeID1)
{
int32* FoundEdgeID1 = MeshEdgePairs.Find(EdgeID0);
if (FoundEdgeID1 != nullptr)
{
EdgeID1 = *FoundEdgeID1;
}
}
if (EdgeID0 != EdgeID1 && Mesh.IsEdge(EdgeID1))
{
FIndex2i EdgeV0 = Mesh.GetOrientedBoundaryEdgeV(EdgeID0);
FIndex2i EdgeV1 = Mesh.GetOrientedBoundaryEdgeV(EdgeID1);
if (EdgeV0.Contains(EdgeV1.A) || EdgeV0.Contains(EdgeV1.B))
{
// If we hit this case, it means that Edge0 and Edge1 are still connected at one end, and
// so cannot be connected by a Quad, they can only be connected by a single triangle.
// It is unclear how we end up in this situation, it does occur somewhat regularly in complex
// geometry scripts though (eg see UE-157531 for a potential repro).
// In this case we cannot add subdivisions currently, and will fall back to simpler geometry
bFoundInvalidCase = true;
}
else
{
SequentialQuadEdges.Add({ EdgeV0, EdgeV1 });
}
}
else
{
bFoundInvalidCase = true;
}
}
// if something went wrong above, fall back to adding a single quad to avoid holes/etc
// (it's possible that this path may still result in catastrophic failure...)
if (bFoundInvalidCase || SequentialQuadEdges.Num() != NumEdges)
{
AppendEdgeQuads(Mesh, Edge);
return;
}
// all the code below is going to generate the bevel edge geometry and populate this list of
// vertex-rows, which will be used to assemble the final FQuadGridPatch for the bevel edge-strip
int32 N = NumSubdivisions;
TArray<TArray<int32>> VertexSpans;
VertexSpans.SetNum(N + 2);
// this function appends a new column to the rows in VertexSpans, by generating new
// vertices on the interior of the edge between StartVID and EndVID
auto AppendNewVertColumn = [&Mesh, &VertexSpans, N](int32 StartVID, int32 EndVID)
{
FVector3d StartPos = Mesh.GetVertex(StartVID);
FVector3d EndPos = Mesh.GetVertex(EndVID);
VertexSpans[0].Add(StartVID);
for (int32 j = 0; j < N; ++j)
{
double T = (double)(j+1) / (double)(N + 1);
int32 NewVertID = Mesh.AppendVertex(Lerp(StartPos, EndPos, T));
VertexSpans[j+1].Add(NewVertID);
}
VertexSpans[N+1].Add(EndVID);
};
// this function appends a new column to the rows in VertexSpans by copying
// the column from an existing adjacent FQuadGridPatch. This happens if we are
// meshing a bevel-edge that is connected to another bevel-edge that was already
// generated, and the connecting bevel-vertex only has 2 bevel-edges (ie no polygon will be inserted)
auto AppendExistingVertColumn = [&VertexSpans, N](const FQuadGridPatch* AdjacentQuadPatch, int32 CornerVertexID) -> bool
{
int32 ColumnIdx = AdjacentQuadPatch->FindColumnIndex(CornerVertexID);
if (ColumnIdx >= 0)
{
TArray<int32> ColumnVerts;
AdjacentQuadPatch->GetVertexColumn(ColumnIdx, ColumnVerts);
// Our column should start w/ CornerVertexID, but it may be reversed due to different orientations when building the adjacent patch...
// (could that be fixed further upstream? possibly it should be!)
if (ColumnVerts.Last() == CornerVertexID)
{
Algo::Reverse(ColumnVerts);
}
for (int32 j = 0; j <= (N + 1); ++j)
{
VertexSpans[j].Add(ColumnVerts[j]);
}
return true;
}
return false;
};
// start and end vertex columns may already exist in some other FBevelEdge that has already been generated.
// In that case, instead of appending new vertices, we want to look up the existing vertices and stitch to them.
// This case only happens for BevelVertices connected to exactly 2 BevelEdges (<2 => Terminator, >2 => Polygon at vertex).
auto GetConnectedQuadStripRef = [this](int BevelVertexIdx, int CurBevelEdgeIdx)
{
if (BevelVertexIdx == -1) return (const FQuadGridPatch*)nullptr;
const FBevelVertex& Vtx = Vertices[BevelVertexIdx];
if (Vtx.VertexType != EBevelVertexType::JunctionVertex || Vtx.Wedges.Num() != 2) return (const FQuadGridPatch*)nullptr;
for (int EdgeIdx : Vtx.IncomingBevelEdgeIndices)
{
if (EdgeIdx != CurBevelEdgeIdx)
{
const FBevelEdge& OtherEdge = Edges[EdgeIdx];
if (OtherEdge.StripQuadPatch.IsEmpty() == false)
{
return (const FQuadGridPatch*)&OtherEdge.StripQuadPatch;
}
return (const FQuadGridPatch*)nullptr;
}
}
return (const FQuadGridPatch*)nullptr;
};
// Below code expects SequentialQuadEdges to be ordered s.t. for edges spanning
// vertices 0,1,2 the edges should be (1,0),(2,1) not (0,1),(1,2) ...
// We enforce this by reversing the array if needed
if (SequentialQuadEdges.Num() > 1 && SequentialQuadEdges[0].EdgeV0.B == SequentialQuadEdges[1].EdgeV0.A)
{
Algo::Reverse(SequentialQuadEdges);
}
// SequentialQuadEdges ordering may not be in agreement with the [BevelVertices.A, BevelVertices.B]
// ordering of the BevelEdge. If so, then the Prev/Next QuadGridPatch
// search below would return the Prev & Next flipped from where we need them, causing the later AppendExistingVertColumn
// to fail. The simplest fix for this here is to swap PrevQuadPatch/NextQuadPatch...
int OriginalPrevVtxID = (Edge.BevelVertices.A >= 0) ? Vertices[Edge.BevelVertices.A].VertexID : -1;
int OriginalNextVtxID = (Edge.BevelVertices.B >= 0) ? Vertices[Edge.BevelVertices.B].VertexID : -1;
FIndex2i EdgeStartQuadVerts(SequentialQuadEdges[0].EdgeV0.B, SequentialQuadEdges[0].EdgeV1.A);
FIndex2i EdgeEndQuadVerts(SequentialQuadEdges[NumEdges-1].EdgeV0.A, SequentialQuadEdges[NumEdges-1].EdgeV1.B);
FIndex2i BevelEdgeVerts(Edge.BevelVertices.A, Edge.BevelVertices.B);
if (EdgeStartQuadVerts.Contains(OriginalNextVtxID) || EdgeEndQuadVerts.Contains(OriginalPrevVtxID))
{
Swap(BevelEdgeVerts.A, BevelEdgeVerts.B);
}
// find quadgrid patches connected to start and end of current bevel edge, if the yexist
const FQuadGridPatch* PrevQuadPatch = GetConnectedQuadStripRef(BevelEdgeVerts.A, Edge.EdgeIndex);
const FQuadGridPatch* NextQuadPatch = GetConnectedQuadStripRef(BevelEdgeVerts.B, Edge.EdgeIndex);
// now add columns of vertices for each inital vertex along the bevel-edge (by iterating along mesh-edges)
for (int32 k = 0; k < NumEdges; ++k)
{
FIndex2i EdgeV0 = SequentialQuadEdges[k].EdgeV0;
FIndex2i EdgeV1 = SequentialQuadEdges[k].EdgeV1;
int32 QuadA = EdgeV0.B;
int32 QuadB = EdgeV0.A;
int32 QuadC = EdgeV1.B;
int32 QuadD = EdgeV1.A;
// for first and last edges, we have to add an extra row of vertices
if (k == 0)
{
if (PrevQuadPatch == nullptr || AppendExistingVertColumn(PrevQuadPatch, QuadA) == false)
{
AppendNewVertColumn(QuadA, QuadD);
}
}
if (k != NumEdges-1 || NextQuadPatch == nullptr || AppendExistingVertColumn(NextQuadPatch, QuadB) == false)
{
AppendNewVertColumn(QuadB, QuadC);
}
}
// now append the quads that connect up the vertices
TArray<TArray<FIndex2i>> QuadSpans;
QuadSpans.SetNum(N + 1);
int32 NumStrips = VertexSpans.Num() - 1;
for (int32 k = 0; k < NumEdges; ++k)
{
for ( int32 j = 0; j < NumStrips; ++j)
{
FIndex2i QuadTris(IndexConstants::InvalidID, IndexConstants::InvalidID);
QuadTris.A = Mesh.AppendTriangle(VertexSpans[j][k], VertexSpans[j][k+1], VertexSpans[j+1][k], Edge.NewGroupID);
QuadTris.B = Mesh.AppendTriangle(VertexSpans[j+1][k+1], VertexSpans[j+1][k], VertexSpans[j][k+1], Edge.NewGroupID);
QuadSpans[j].Add(QuadTris);
Edge.StripQuads.Add(QuadTris);
}
}
// finally save the quad-patch we generated in the FBevelEdge
Edge.StripQuadPatch.InitializeFromQuadPatch(Mesh, QuadSpans, VertexSpans);
}
void FMeshBevel::AppendLoopQuads_Multi(FDynamicMesh3& Mesh, FBevelLoop& Loop)
{
// As in other places, the Loop case is a simplified version of the Edge case, where
// many special cases do not have to be handled. See the comments in
// AppendEdgeQuads_Multi() for anything non-obvious below
int32 NumEdges = Loop.MeshEdges.Num();
if (NumEdges != Loop.NewMeshEdges.Num())
{
return;
}
auto GetGroupKey = [&Mesh, &Loop](int32 k)
{
FIndex2i EdgeTris = Loop.MeshEdgeTris[k];
int32 Group0 = Mesh.GetTriangleGroup(EdgeTris.A);
int32 Group1 = Mesh.IsTriangle(EdgeTris.B) ? Mesh.GetTriangleGroup(EdgeTris.B) : -1;
return FIndex2i(FMath::Max(Group0, Group1), FMath::Min(Group0, Group1));
};
TMap<FIndex2i, int> NewGroupIDs;
for (int32 k = 0; k < NumEdges; ++k)
{
FIndex2i GroupKey = GetGroupKey(k);
if (NewGroupIDs.Contains(GroupKey) == false)
{
NewGroupIDs.Add(GroupKey, Mesh.AllocateTriangleGroup());
Loop.NewGroupIDs.Add(NewGroupIDs[GroupKey]);
}
}
struct FEdgePair
{
FIndex2i EdgeV0;
FIndex2i EdgeV1;
};
TArray<FEdgePair> SequentialQuadEdges;
bool bFoundInvalidCase = false;
// At this point each edge-span should be fully disconnected into a set of paired edges,
// so we can trivially join each edge pair with a quad.
for (int32 k = 0; k < NumEdges && bFoundInvalidCase == false; ++k)
{
int32 EdgeID0 = Loop.MeshEdges[k];
int32 EdgeID1 = Loop.NewMeshEdges[k];
// case that happens in AppendEdgeQuads() should never happen for loops...
FIndex2i QuadTris(IndexConstants::InvalidID, IndexConstants::InvalidID);
if (EdgeID0 != EdgeID1 && Mesh.IsEdge(EdgeID1))
{
FIndex2i GroupKey = GetGroupKey(k);
int32 NewGroupID = NewGroupIDs[GroupKey];
FIndex2i EdgeV0 = Mesh.GetOrientedBoundaryEdgeV(EdgeID0);
FIndex2i EdgeV1 = Mesh.GetOrientedBoundaryEdgeV(EdgeID1);
SequentialQuadEdges.Add({ EdgeV0, EdgeV1 });
}
else
{
bFoundInvalidCase = true;
}
}
if (bFoundInvalidCase || SequentialQuadEdges.Num() != NumEdges)
{
AppendLoopQuads(Mesh, Loop);
return;
}
// Test the edge ordering -- i.e. for vertices 0,1,2, if bEdgeHasReverseOrder == true,
// edge vertices will be (0,1),(1,2); otherwise it will be (1,0),(2,1)
bool bEdgeHasReverseOrder = (SequentialQuadEdges.Num() > 1 && SequentialQuadEdges[0].EdgeV0.B == SequentialQuadEdges[1].EdgeV0.A);
int32 N = NumSubdivisions;
TArray<TArray<int32>> VertexSpans;
VertexSpans.SetNum(N + 2);
auto AppendVertColumn = [&Mesh, &VertexSpans, &N](int32 StartVID, int32 EndVID)
{
FVector3d StartPos = Mesh.GetVertex(StartVID);
FVector3d EndPos = Mesh.GetVertex(EndVID);
VertexSpans[0].Add(StartVID);
for (int32 j = 0; j < N; ++j)
{
double T = (double)(j + 1) / (double)(N + 1);
int32 NewVertID = Mesh.AppendVertex(Lerp(StartPos, EndPos, T));
VertexSpans[j + 1].Add(NewVertID);
}
VertexSpans[N + 1].Add(EndVID);
};
for (int32 k = 0; k < NumEdges; ++k)
{
FIndex2i EdgeV0 = SequentialQuadEdges[k].EdgeV0;
FIndex2i EdgeV1 = SequentialQuadEdges[k].EdgeV1;
if (bEdgeHasReverseOrder)
{
EdgeV0.Swap();
EdgeV1.Swap();
}
int32 QuadA = EdgeV0.B;
int32 QuadB = EdgeV0.A;
int32 QuadC = EdgeV1.B;
int32 QuadD = EdgeV1.A;
if (k == 0)
{
AppendVertColumn(QuadA, QuadD);
}
if (k != NumEdges - 1)
{
AppendVertColumn(QuadB, QuadC);
}
else
{
for (int32 j = 0; j <= (N+1); ++j)
{
int FirstColVal = VertexSpans[j][0];
VertexSpans[j].Add(FirstColVal);
}
}
}
TArray<TArray<FIndex2i>> QuadSpans;
QuadSpans.SetNum(N + 1);
// Offsets for flipping triangles in cases where the edges had the opposite ordering from our expectation
int32 SwapOffset0 = (int32)(bEdgeHasReverseOrder);
int32 SwapOffset1 = (int32)(!bEdgeHasReverseOrder);
int32 NumStrips = VertexSpans.Num() - 1;
for (int32 k = 0; k < NumEdges; ++k)
{
FIndex2i GroupKey = GetGroupKey(k);
int32 NewGroupID = NewGroupIDs[GroupKey];
for (int32 j = 0; j < NumStrips; ++j)
{
FIndex2i QuadTris(IndexConstants::InvalidID, IndexConstants::InvalidID);
QuadTris.A = Mesh.AppendTriangle(VertexSpans[j][k + SwapOffset0], VertexSpans[j][k + SwapOffset1], VertexSpans[j+1][k], NewGroupID);
QuadTris.B = Mesh.AppendTriangle(VertexSpans[j+1][k + SwapOffset1], VertexSpans[j+1][k + SwapOffset0], VertexSpans[j][k+1], NewGroupID);
QuadSpans[j].Add(QuadTris);
Loop.StripQuads.Add(QuadTris);
}
}
Loop.StripQuadPatch.InitializeFromQuadPatch(Mesh, QuadSpans, VertexSpans);
}
void FMeshBevel::AppendJunctionVertexPolygon_Multi(FDynamicMesh3& Mesh, FBevelVertex& Vertex)
{
// This function appends triangulations for junction vertices, ie where 3+ bevel-edges meet and
// so a polygonal face must be inserted. For a multi-segment bevel, we need to tessellate the
// polygon with interior vertices so that the interior vertices can be displaced later based
// on the bevel profile shape. And this is also where we will precompute certain information about
// the interior vertices, like (eg) barycentric coords of interior vertices, so that we can later
// deform them relative to the profile shapes on the boundary.
//
// This is probably the most complex part of doing the bevel, and there are special cases for
// valence-3 and valence-4, with lots of similar-but-slightly-different code.
MESH_BEVEL_DEBUG_CHECK(Vertex.VertexType == EBevelVertexType::JunctionVertex);
// TODO: all the setup here could be done in parallel, which might matter given that
// we are doing a tessellation, conformal unwrap, etc. Would need to cache temporary data.
// Junction vertices can never be directly connected so there should not be any order effects.
// UnlinkJunctionVertex() split the junction vertex into N vertices, one for each
// (now disconnected) triangle-wedge. The wedges are ordered such that their wedge-vertices
// define a polygon with correct winding. However in this case we have additional interior
// vertices along the polygon edges. So we need to construct a mesh for the junction vertex
// that has a compatible boundary and (in most cases) additional interior vertices, which
// is done in this function via a regular tessellation of the polygon
int32 NumEdgeVerts = 0; // this should be determined by NumSubdivisions but we will compute from actual mesh here
bool bAllEdgesHaveSameNumVerts = true; // current code only handles the case where all edges have same # of subdivisions.
// First step is to find the loop of vertices around the border of the junction vertex.
// Each incoming source-mesh edge became a "wedge", with start vertex A and end B, and
// then a quad-patch was appended connecting A to B. So we need to get the right "column"
// of the quad patch, which will give us the vertex span [A, ..., B]. These are accumulated
// in-wedge-sequence into PolygonPoints/Vertices. The start/end vertices A and B are accumulated
// in-order in the PolygonCorners/Indices/VIDs lists.
// (In both sets, the 'B' vertex is always skipped because it will be added as A in the next span)
TArray<FVector3d> PolygonPoints, PolygonCorners;
TArray<int32> PolygonVertices, PolygonCornerVIDs;
int32 NumWedges = Vertex.Wedges.Num();
for (int32 wi = 0; wi < NumWedges; ++wi)
{
FOneRingWedge& CurWedge = Vertex.Wedges[wi];
FOneRingWedge& NextWedge = Vertex.Wedges[(wi+1)%NumWedges];
int32 A = CurWedge.WedgeVertex;
int32 B = NextWedge.WedgeVertex;
// We don't know which Edge will contain the [A,B] vertices, and whether it is the 'start'
// or 'end' of the Edge. So currently just linear searching through all of them and checking
// each end. Maybe Vertex.IncomingBevelEdgeIndices would work here instead
TArray<int32> BevelEdgeQuadStripEndVertices;
bool bFound = false;
for (FBevelEdge& Span : Edges)
{
// this helper appends the vertex-column of an adjacent edge-quadstrip to the current vertex polygon loop
auto AppendStrip = [&](TArray<int32>& OrderedVertices)
{
PolygonCorners.Add(Mesh.GetVertex(OrderedVertices[0]));
PolygonCornerVIDs.Add(OrderedVertices[0]);
for (int32 j = 0; j < OrderedVertices.Num()-1; ++j)
{
int32 VertexID = OrderedVertices[j];
PolygonVertices.Add(VertexID);
PolygonPoints.Add(Mesh.GetVertex(VertexID));
}
if (NumEdgeVerts == 0)
{
NumEdgeVerts = OrderedVertices.Num();
}
else if (OrderedVertices.Num() != NumEdgeVerts)
{
bAllEdgesHaveSameNumVerts = false;
}
bFound = true;
};
if (Span.StripQuadPatch.VertexSpans.IsEmpty())
{
// The no-vertex-span case can happen for cases where the smooth bevel has failed and fallen back to non-smooth bevel
continue;
}
// try start and end columns, and handle case where vertex ordering might be reversed (should this be possible, because of consistent mesh winding??)
Span.StripQuadPatch.GetVertexColumn(0, BevelEdgeQuadStripEndVertices);
if (BevelEdgeQuadStripEndVertices[0] == A && BevelEdgeQuadStripEndVertices.Last() == B)
{
AppendStrip(BevelEdgeQuadStripEndVertices);
break;
}
if (BevelEdgeQuadStripEndVertices[0] == B && BevelEdgeQuadStripEndVertices.Last() == A)
{
Algo::Reverse(BevelEdgeQuadStripEndVertices);
AppendStrip(BevelEdgeQuadStripEndVertices);
break;
}
Span.StripQuadPatch.GetVertexColumn(Span.StripQuadPatch.NumVertexCols() - 1, BevelEdgeQuadStripEndVertices);
if (BevelEdgeQuadStripEndVertices[0] == A && BevelEdgeQuadStripEndVertices.Last() == B)
{
AppendStrip(BevelEdgeQuadStripEndVertices);
break;
}
if (BevelEdgeQuadStripEndVertices[0] == B && BevelEdgeQuadStripEndVertices.Last() == A)
{
Algo::Reverse(BevelEdgeQuadStripEndVertices);
AppendStrip(BevelEdgeQuadStripEndVertices);
break;
}
}
if (!bFound)
{
PolygonVertices.Add(CurWedge.WedgeVertex);
PolygonPoints.Add(Mesh.GetVertex(CurWedge.WedgeVertex));
bAllEdgesHaveSameNumVerts = false;
}
}
// code below won't work unless each edge has same vert count, in that case just triangulate the polygon (ie fail)
if (bAllEdgesHaveSameNumVerts == false || NumEdgeVerts == 0)
{
TArray<FIndex3i> Triangles;
PolygonTriangulation::TriangulateSimplePolygon<double>(PolygonPoints, Triangles);
Vertex.NewGroupID = Mesh.AllocateTriangleGroup();
for (FIndex3i Tri : Triangles)
{
int32 tid = Mesh.AppendTriangle(PolygonVertices[Tri.A], PolygonVertices[Tri.B], PolygonVertices[Tri.C], Vertex.NewGroupID);
if (Mesh.IsTriangle(tid))
{
Vertex.NewTriangles.Add(tid);
}
}
return;
}
// find centroid of polygon
FVector3d Centroid = FVector3d::Zero();
for (FVector3d Pos : PolygonPoints)
{
Centroid += Pos;
}
Centroid /= (double)PolygonPoints.Num();
// Now we have enough information to tessellate and fill the hole.
// Apologies for the convoluted code here. The general case was implemented
// first, and then the 3/4 special-cases were added. Probably they should
// be refactored out into 3 separate functions. Sorry! -rms
// Append corners of the polygon into a temporary mesh, and then triangles
// For the case of > 4 vertices, add the centroid and triangulate as a fan
FDynamicMesh3 TmpMesh;
int32 NCorners = PolygonCorners.Num();
for (int32 k = 0; k < NCorners; ++k)
{
int NewVID = TmpMesh.AppendVertex(PolygonCorners[k]);
ensure(NewVID == k);
}
if (NCorners == 3) // single triangle
{
TmpMesh.AppendTriangle(0, 1, 2);
}
else if (NCorners == 4) // quad
{
TmpMesh.AppendTriangle(0, 1, 2);
TmpMesh.AppendTriangle(2, 3, 0);
}
else // make triangle fan
{
int32 CentroidID = TmpMesh.AppendVertex(Centroid);
for (int32 i = 0; i < NCorners; ++i)
{
int NewTID = TmpMesh.AppendTriangle(i, ((i + 1) % NCorners), CentroidID);
}
}
// Ok, for the case of a triangle, we will use barycentric coordinates of the initial
// triangle rather than trying to do general polygon barycentrics. To avoid writing custom
// triangle-tess code here, FUniformTessellate is still used, but it doesn't return the barycentrics.
// So instead we store the 3 vertex-coordinate-functions in vertex colors and allow them to be lerp'd
if (NCorners == 3)
{
// note that this construction of TriTess will be the same for every valence-3 corner, and
// probably should be cached unless NumEdgeVerts changes...
FDynamicMesh3 TriTess;
TriTess = TmpMesh;
TriTess.SetVertex(0, FVector3d(1, 0, 0));
TriTess.SetVertex(1, FVector3d(0, 1, 0));
TriTess.SetVertex(2, FVector3d(0.5, 0.75, 0));
TriTess.EnableVertexColors(FVector3f::Zero());
TriTess.SetVertexColor(0, FVector3f(1, 0, 0));
TriTess.SetVertexColor(1, FVector3f(0, 1, 0));
TriTess.SetVertexColor(2, FVector3f(0, 0, 1));
FUniformTessellate Tesselator(&TriTess);
Tesselator.TessellationNum = NumEdgeVerts - 2;
bool bTesselateOK = Tesselator.Compute();
checkSlow(bTesselateOK);
// need mapping between the boundary of this tessellated mesh and the triangle-shaped polygon
// surrounding the hole in the original mesh. Derive this from the known correspondence
// between the first vertex in each
FMeshBoundaryLoops BoundaryLoops(&TriTess, true);
TArray<int32> TriLoopVerts = BoundaryLoops.Loops[0].Vertices;
int32 LoopN = TriLoopVerts.Num();
checkSlow(PolygonVertices[0] == PolygonCornerVIDs[0]);
checkSlow(PolygonVertices.Num() == LoopN);
int32 StartIndex = TriLoopVerts.IndexOfByKey(0); // index of first vertex in loop
TArray<int32> TriTessVertIDToPolygonIndexMap;
TriTessVertIDToPolygonIndexMap.SetNum(TriTess.MaxVertexID());
for (int32 k = 0; k < LoopN; ++k)
{
int32 LoopVertID = TriLoopVerts[(k + StartIndex) % LoopN];
TriTessVertIDToPolygonIndexMap[LoopVertID] = k;
}
FVector3d PosA = TmpMesh.GetVertex(0);
FVector3d PosB = TmpMesh.GetVertex(1);
FVector3d PosC = TmpMesh.GetVertex(2);
// for this special case we only store the 3 corners in the InteriorBorderLoop, this will be detected in
// the profile code and used correctly there
Vertex.InteriorVertices.Reserve(FMath::Max((NumEdgeVerts-1)*(NumEdgeVerts-1), 0)); // this is for quad so it's more than needed...
Vertex.InteriorBorderLoop = TArray<int32>({ PolygonCornerVIDs[0], PolygonCornerVIDs[1], PolygonCornerVIDs[2] });
TArray<int32> VertexMap;
VertexMap.SetNum(TriTess.MaxVertexID());
// Append the new interior vertices, boundary vertices already exist and are just updated in the map.
// For interior vertices the barycentric coords are stored in the BorderFrameWeights
for (int32 vid : TriTess.VertexIndicesItr())
{
if (TriTess.IsBoundaryVertex(vid))
{
int32 LoopIndex = TriTessVertIDToPolygonIndexMap[vid];
int32 ExistingVertID = PolygonVertices[LoopIndex];
VertexMap[vid] = ExistingVertID;
}
else
{
FVector3f BaryCoords = TriTess.GetVertexColor(vid);
BaryCoords /= (BaryCoords.X + BaryCoords.Y + BaryCoords.Z);
FVector3d InterpPos = BaryCoords.X * PosA + BaryCoords.Y * PosB + BaryCoords.Z * PosC;
int32 NewVID = Mesh.AppendVertex(InterpPos);
FBevelVertex_InteriorVertex& InteriorVertex = Vertex.InteriorVertices.Emplace_GetRef();
InteriorVertex.VertexID = NewVID;
InteriorVertex.BorderFrameWeight.Add(FVector3d(BaryCoords.X, BaryCoords.Y, BaryCoords.Z));
VertexMap[vid] = NewVID;
}
}
// finally append the triangles
TArray<FIndex3i> Triangles;
Vertex.NewGroupID = Mesh.AllocateTriangleGroup();
for (FIndex3i Triangle : TriTess.TrianglesItr())
{
// NOTE FLIP HERE! This is because outer loop is oriented for outside tris, so we need to flip
int T0 = Mesh.AppendTriangle(VertexMap[Triangle.A], VertexMap[Triangle.C], VertexMap[Triangle.B], Vertex.NewGroupID);
if (Mesh.IsTriangle(T0))
{
Vertex.NewTriangles.Add(T0);
}
}
// we are done the valnce=3 case, exit this function
return;
}
// For a quad, the tessellation is simple and is just done directly here
if (NCorners == 4)
{
FDenseGrid2i VertexGrid;
VertexGrid.Resize(NumEdgeVerts, NumEdgeVerts);
VertexGrid.AssignAll(-1);
// transfer vertex polygon into border of grid
int32 PolygonIdx = 0;
for (int32 xi = 0; xi < NumEdgeVerts; ++xi)
{
VertexGrid.At(xi,0) = PolygonVertices[PolygonIdx++];
}
for ( int32 yi = 1; yi < NumEdgeVerts-1; ++yi )
{
VertexGrid.At(NumEdgeVerts-1, yi) = PolygonVertices[PolygonIdx++];
}
for (int32 xi = NumEdgeVerts-1; xi >= 0; --xi)
{
VertexGrid.At(xi, NumEdgeVerts-1) = PolygonVertices[PolygonIdx++];
}
for (int32 yi = NumEdgeVerts - 2; yi >= 1; --yi)
{
VertexGrid.At(0, yi) = PolygonVertices[PolygonIdx++];
}
ensure(PolygonIdx == PolygonVertices.Num());
int c00 = VertexGrid.At(0, 0);
int c10 = VertexGrid.At(NumEdgeVerts-1, 0);
int c01 = VertexGrid.At(0, NumEdgeVerts-1);
int c11 = VertexGrid.At(NumEdgeVerts-1, NumEdgeVerts-1);
FVector3d V00 = Mesh.GetVertex(c00);
FVector3d V10 = Mesh.GetVertex(c10);
FVector3d V01 = Mesh.GetVertex(c01);
FVector3d V11 = Mesh.GetVertex(c11);
// for this special case we only store the 4 corners in the InteriorBorderLoop, this will be detected in
// the profile code and used correctly there
Vertex.InteriorVertices.Reserve(FMath::Max((NumEdgeVerts - 1)* (NumEdgeVerts - 1), 0));
Vertex.InteriorBorderLoop = TArray<int32>({ c00, c10, c01, c11 });
// append new vertices for the interior, while storing their U/V coords in the BorderFrameWeights
for (int yi = 1; yi < NumEdgeVerts-1; ++yi)
{
double ty = (double)yi / (double)(NumEdgeVerts - 1);
FVector3d A = Lerp(V00, V01, ty);
FVector3d B = Lerp(V10, V11, ty);
for (int xi = 1; xi < NumEdgeVerts-1; ++xi)
{
ensure(VertexGrid.At(xi, yi) == -1);
double tx = (double)xi / (double)(NumEdgeVerts - 1);
FVector3d InterpPos = Lerp(A, B, tx);
int32 NewVID = Mesh.AppendVertex(InterpPos);
VertexGrid.At(xi, yi) = NewVID;
FBevelVertex_InteriorVertex& InteriorVertex = Vertex.InteriorVertices.Emplace_GetRef();
InteriorVertex.VertexID = NewVID;
InteriorVertex.BorderFrameWeight.Add(FVector3d(tx, ty, 0));
}
}
// create new vertices for any that don't already have a mapping back to boundary ring
TArray<FIndex3i> Triangles;
Vertex.NewGroupID = Mesh.AllocateTriangleGroup();
for (int y0 = 0; y0 < NumEdgeVerts - 1; ++y0)
{
for (int x0 = 0; x0 < NumEdgeVerts - 1; ++x0)
{
int i00 = VertexGrid.At(x0, y0);
int i10 = VertexGrid.At(x0+1, y0);
int i01 = VertexGrid.At(x0, y0+1);
int i11 = VertexGrid.At(x0+1, y0+1);
int T0 = Mesh.AppendTriangle(i10, i00, i01, Vertex.NewGroupID);
if (Mesh.IsTriangle(T0))
{
Vertex.NewTriangles.Add(T0);
}
int T1 = Mesh.AppendTriangle(i01, i11, i10, Vertex.NewGroupID);
if (Mesh.IsTriangle(T1))
{
Vertex.NewTriangles.Add(T1);
}
}
}
return;
}
// Ok, now we are in the general case (currently used for >= 5-vertex polygons)
// We will use Uniform Tessellation to subdivide the initial polygon triangulation up to
// the target polycount. This is a bit annoying because we have to reconstruct the boundary
// correspondence, perhaps FUniformTessellate could be improved to return this information
FUniformTessellate Tesselator(&TmpMesh);
Tesselator.TessellationNum = NumEdgeVerts - 2;
bool bTesselateOK = Tesselator.Compute();
// create flattened 2D version of this tessellated mesh (maybe should optimize in 2D for inner fairness??)
FDynamicMeshUVEditor UVEditor(&TmpMesh, 0, true);
TArray<int32> AllTriangles;
for (int32 tid : TmpMesh.TriangleIndicesItr()) AllTriangles.Add(tid);
TArray<int32> TmpMeshToUVMap; bool bMapIsCompact = false;
UVEditor.SetToPerVertexUVs(TmpMeshToUVMap, bMapIsCompact);
check(bMapIsCompact == true);
UVEditor.SetTriangleUVsFromFreeBoundarySpectralConformal(AllTriangles, true, true);
UVEditor.ScaleUVAreaTo3DArea(AllTriangles, true);
FDynamicMeshUVOverlay* UVOverlay = UVEditor.GetOverlay();
// Now we need to append this triangulated patch back into the main mesh, ie "fill the hole"
// that exists for this junction vertex. Ideally we will stitch at the compatible patch border, via this map
TArray<int32> VertexMap;
VertexMap.Init(-1, TmpMesh.MaxVertexID());
// We should have an exact match between the existing loop of boundary vertices (in PolygonVertices) and
// the boundary loop of the patch. And we know the correspondences at the Corners as we constructed them
// above in the TmpMesh.AppendVertex calls. So we should be able to find Corner 0 in the boundary loop,
// and walk around the two loops from that initial correspondence. This block will do that and store it in VertexMap, if possible.
int32 NV = PolygonVertices.Num();
FMeshBoundaryLoops BoundaryLoops(&TmpMesh, true); // should only ever be one loop...
bool bFoundLoop = false;
TArray<int32> BoundaryLoopVerts;
for (int32 k = 0; k < BoundaryLoops.GetLoopCount() && bFoundLoop == false; ++k)
{
if (BoundaryLoops[k].Vertices.Num() == NV)
{
BoundaryLoopVerts = BoundaryLoops[k].Vertices;
bFoundLoop = true; // conceivably it's possible this isn't the correct loop, somehow. That case is not handled
// find our known-correspondence vertices, from the initial polygon corners
int32 CornerVID = PolygonCornerVIDs[0];
int32 FoundIdx = BoundaryLoopVerts.IndexOfByKey(0);
int32 SecondCornerVID = PolygonCornerVIDs[1];
int32 FoundSecondIdx = BoundaryLoopVerts.IndexOfByKey(1);
if (FoundIdx != INDEX_NONE && FoundSecondIdx != INDEX_NONE)
{
// detect if the boundary loop is reversed, and if so, flip it
if (BoundaryLoopVerts[(FoundIdx+(NumEdgeVerts-1)) % NV] != BoundaryLoopVerts[FoundSecondIdx] )
{
Algo::Reverse(BoundaryLoopVerts);
FoundIdx = BoundaryLoopVerts.IndexOfByKey(0);
FoundSecondIdx = BoundaryLoopVerts.IndexOfByKey(1);
}
if (ensure(BoundaryLoopVerts[(FoundIdx+(NumEdgeVerts-1)) % NV] == BoundaryLoopVerts[FoundSecondIdx]))
{
// walk around loop at corresponding indices and populate VertexMap
for (int32 j = 0; j < NV; ++j)
{
int32 ExistingID = PolygonVertices[j];
int32 NewID = BoundaryLoopVerts[(FoundIdx + j) % NV];
VertexMap[NewID] = ExistingID;
}
}
}
}
}
Vertex.InteriorVertices.Reserve(FMath::Max(TmpMesh.VertexCount() - NV, 0));
int32 NumLoopVerts = BoundaryLoopVerts.Num();
FPolygon2d BorderPolygon;
for (int32 vid : BoundaryLoopVerts)
{
BorderPolygon.AppendVertex( (FVector2d)UVOverlay->GetElement(vid) );
}
// Create new vertices for any that don't already have a mapping back to boundary ring.
// For those interior vertices we also want to compute 2D Mean Value Coordinates (MVC)
// relative to the boundary polygon. We will use these later to interpolate 3D positions from
// the 3D border.
TArray<FIndex3i> Triangles;
Vertex.NewGroupID = Mesh.AllocateTriangleGroup();
for (int32 vid : TmpMesh.VertexIndicesItr())
{
if (VertexMap[vid] == -1)
{
int32 NewVID = Mesh.AppendVertex(TmpMesh.GetVertex(vid));
VertexMap[vid] = NewVID;
// construct interior MVC weights from 2D mesh
FBevelVertex_InteriorVertex& InteriorVertex = Vertex.InteriorVertices.Emplace_GetRef();
InteriorVertex.VertexID = NewVID;
InteriorVertex.BorderFrameWeight.Reserve(BoundaryLoopVerts.Num());
FVector2d UVPosition = (FVector2d)UVOverlay->GetElement(vid);
double WeightSum = 0;
for (int32 k = 0; k < NumLoopVerts; ++k)
{
int32 BoundaryVID = BoundaryLoopVerts[k];
int32 NewBoundaryVID = VertexMap[BoundaryVID];
checkSlow(NewBoundaryVID >= 0);
FVector2d BoundaryUVPosition = BorderPolygon[k];
FVector2d Prev = BorderPolygon[(k-1+NumLoopVerts) % NumLoopVerts];
FVector2d Next = BorderPolygon[(k+1) % NumLoopVerts];
FVector2d BoundaryFrameX = Normalized(Next - Prev);
FVector2d BoundaryFrameY = PerpCW(BoundaryFrameX);
FVector2d DeltaUV = UVPosition - BoundaryUVPosition;
double DeltaX = BoundaryFrameX.Dot(DeltaUV);
double DeltaY = BoundaryFrameY.Dot(DeltaUV);
// compute Polygon MVC weight ( https://cgvr.cs.uni-bremen.de/teaching/cg_literatur/barycentric_floater.pdf )
double Dist = Distance(UVPosition, BoundaryUVPosition);
double Weight = 1.0;
if ( Dist > FMathd::ZeroTolerance )
{
FVector2d DeltaP = Normalized(BoundaryUVPosition - UVPosition);
double T1 = UE::Geometry::VectorUtil::VectorTanHalfAngle(Normalized(Prev - UVPosition), DeltaP);
double T2 = UE::Geometry::VectorUtil::VectorTanHalfAngle(Normalized(Next - UVPosition), DeltaP);
Weight = (T1 + T2) / Dist;
}
InteriorVertex.BorderFrameWeight.Add( FVector3d(DeltaX, DeltaY, Weight) );
WeightSum += Weight;
}
for (FVector3d& Weight : InteriorVertex.BorderFrameWeight)
{
Weight.Z /= WeightSum; // normalize MVC weights
}
}
}
Vertex.InteriorBorderLoop.Reserve(NumLoopVerts);
for (int32 vid : BoundaryLoopVerts)
{
Vertex.InteriorBorderLoop.Add(VertexMap[vid]);
}
// PolygonVertices array is always constructed in reversed orientation, so patch will be flipped otherwise
TmpMesh.ReverseOrientation();
// append new triangles
for (FIndex3i Tri : TmpMesh.TrianglesItr())
{
int32 A = VertexMap[Tri.A];
int32 B = VertexMap[Tri.B];
int32 C = VertexMap[Tri.C];
int32 tid = Mesh.AppendTriangle(A, B, C, Vertex.NewGroupID);
if (Mesh.IsTriangle(tid))
{
Vertex.NewTriangles.Add(tid);
}
}
}
void FMeshBevel::AppendTerminatorVertexTriangles_Multi(FDynamicMesh3& Mesh, FBevelVertex& Vertex)
{
// A Terminator Vertex occurs at the end of an non-loop bevel-edge, where the bevel "ends".
// Disconnecting the mesh to insert the bevel edge-strip will have created a triangle-shaped hole
// in the mesh that must be filled. In the Multi-case, we have already meshed the adjacent bevel-edge,
// so it's not a simple triangle but one with subdivisions along the edge adajacent to the bevel-edge.
// So basically we have to find that already-meshed edge, and extract the column from it's quadpatch
// that we want to stitch to, and then add a triangle-fan to fill the hole polygon
// TODO: maybe could do delaunay triangulation if the polygon below is nearly coplanar...
MESH_BEVEL_DEBUG_CHECK(Vertex.VertexType == EBevelVertexType::TerminatorVertex);
check(Vertex.IncomingBevelEdgeIndices.Num() == 1);
int32 RingSplitEdgeID = Vertex.TerminatorInfo.A;
if (Mesh.IsEdge(RingSplitEdgeID))
{
FIndex2i SplitEdgeV = Mesh.GetEdgeV(RingSplitEdgeID);
int32 FarVertexID = SplitEdgeV.OtherElement(Vertex.VertexID);
int32 BevelEdgeIndex = Vertex.IncomingBevelEdgeIndices[0];
const FBevelEdge& IncomingEdge = Edges[BevelEdgeIndex];
int32 WedgeVertexA = Vertex.Wedges[0].WedgeVertex;
int32 WedgeVertexB = Vertex.Wedges[1].WedgeVertex;
// figure out which side of the incoming-edge quad-strip the terminator connects to
int32 ColumnIndex = IncomingEdge.StripQuadPatch.FindColumnIndex(WedgeVertexA);
check(ColumnIndex == 0 || ColumnIndex == IncomingEdge.StripQuadPatch.NumVertexCols() - 1);
TArray<int32> QuadStripEdgeVerts;
IncomingEdge.StripQuadPatch.GetVertexColumn(ColumnIndex, QuadStripEdgeVerts);
check(QuadStripEdgeVerts.Contains(WedgeVertexB));
// make sure it is oriented consistently
int32 FirstEdgeID = Mesh.FindEdge(QuadStripEdgeVerts[0], QuadStripEdgeVerts[1]);
FIndex2i QuadEdgeV = Mesh.GetOrientedBoundaryEdgeV(FirstEdgeID);
if (QuadEdgeV.A != QuadStripEdgeVerts[0])
{
Algo::Reverse(QuadStripEdgeVerts);
}
// should have computed this GroupID in initial setup
int32 UseGroupID = (Vertex.NewGroupID >= 0) ? Vertex.NewGroupID : Mesh.AllocateTriangleGroup();
int32 NumEdges = QuadStripEdgeVerts.Num()-1;
for (int32 k = 0; k < NumEdges; ++k)
{
int32 tid = Mesh.AppendTriangle(QuadStripEdgeVerts[k+1], QuadStripEdgeVerts[k], FarVertexID, UseGroupID);
MESH_BEVEL_DEBUG_CHECK(tid >= 0);
if (Mesh.IsTriangle(tid))
{
Vertex.NewTriangles.Add(tid);
}
}
}
}
void FMeshBevel::AppendTerminatorVertexPairQuad_Multi(FDynamicMesh3& Mesh, FBevelVertex& Vertex0, FBevelVertex& Vertex1)
{
MESH_BEVEL_DEBUG_CHECK(Vertex0.VertexType == EBevelVertexType::TerminatorVertex);
MESH_BEVEL_DEBUG_CHECK(Vertex1.VertexType == EBevelVertexType::TerminatorVertex);
// This is a variant of AppendTerminatorVertexTriangle that handles the case where basically two
// Terminator Vertices are directly connected by a non-beveled mesh edge that was used as the ring-split-edge.
// Since both sides were opened, we have a quad-shaped hole instead of a triangle-shaped hole, with a
// (subdivided, in the multi-case) quad-edge at each end. So this hole can be filled with a quad-patch that
// stitches together the existing edges (which must have already been meshed)
auto CollectTerminatorVtxInfo = [this](FDynamicMesh3& Mesh, FBevelVertex& Vertex, TArray<int32>& QuadStripEdgeVertsOut)
{
int32 BevelEdgeIndex = Vertex.IncomingBevelEdgeIndices[0];
const FBevelEdge& IncomingEdge = Edges[BevelEdgeIndex];
int32 ColumnIndex = IncomingEdge.StripQuadPatch.FindColumnIndex(Vertex.Wedges[0].WedgeVertex);
check(ColumnIndex == 0 || ColumnIndex == IncomingEdge.StripQuadPatch.NumVertexCols() - 1);
IncomingEdge.StripQuadPatch.GetVertexColumn(ColumnIndex, QuadStripEdgeVertsOut);
check(QuadStripEdgeVertsOut.Contains(Vertex.Wedges[1].WedgeVertex));
int32 FirstEdgeID = Mesh.FindEdge(QuadStripEdgeVertsOut[0], QuadStripEdgeVertsOut[1]);
FIndex2i QuadEdgeV = Mesh.GetOrientedBoundaryEdgeV(FirstEdgeID);
if (QuadEdgeV.A != QuadStripEdgeVertsOut[0])
{
Algo::Reverse(QuadStripEdgeVertsOut);
}
};
TArray<int32> QuadStripEdgeVerts0, QuadStripEdgeVerts1;
CollectTerminatorVtxInfo(Mesh, Vertex0, QuadStripEdgeVerts0);
CollectTerminatorVtxInfo(Mesh, Vertex1, QuadStripEdgeVerts1);
if (QuadStripEdgeVerts0.Num() != QuadStripEdgeVerts1.Num() || QuadStripEdgeVerts0.Num() == 0)
{
// this could happen if the quadstrip failed on one or both sides? will leave a hole.
MESH_BEVEL_DEBUG_CHECK(false);
return;
}
// BIASED? should have computed this GroupID in initial setup
int32 UseGroupID = (Vertex0.NewGroupID >= 0) ? Vertex0.NewGroupID : Mesh.AllocateTriangleGroup();
int32 NumEdges = QuadStripEdgeVerts0.Num() - 1;
for (int32 k = 0; k < NumEdges; ++k)
{
int32 A0 = QuadStripEdgeVerts0[k];
int32 B0 = QuadStripEdgeVerts0[k+1];
int32 A1 = QuadStripEdgeVerts1[NumEdges-(k)];
int32 B1 = QuadStripEdgeVerts1[NumEdges-(k+1)];
int32 tid0 = Mesh.AppendTriangle(B0, A0, A1, UseGroupID);
MESH_BEVEL_DEBUG_CHECK(tid0 >= 0);
if (Mesh.IsTriangle(tid0))
{
Vertex0.NewTriangles.Add(tid0);
}
int32 tid1 = Mesh.AppendTriangle(A1, B1, B0, UseGroupID);
MESH_BEVEL_DEBUG_CHECK(tid1 >= 0);
if (Mesh.IsTriangle(tid1))
{
Vertex0.NewTriangles.Add(tid1);
}
}
}
void FMeshBevel::CreateBevelMeshing_Multi(FDynamicMesh3& Mesh)
{
// This is the top-level driver function that is called after the mesh has been pulled apart along
// the bevelled edges. There are four cases - open edge spans, edge loops, corners/vertices with bevel-valence > 3 (become polygons),
// and "terminator" vertices of bevel-valence 1 (become triangles, except if directly connected, then they become quads).
// First we figure out the mesh connectivity, ie the 'stitching' between the pulled-apart geometry,
// and then we (optionally) apply a profile-curve shape along the bevel strips
// We will later need normals along each exterior "side" of the bevel edge to define the arcs along
// rounded bevel edges, ie basically these are the smooth boundary conditions.
// It ought to be possible to compute this after adding the bevel geometry, by filtering out the bevel-edge tris,
// but for now it's simpler to just do it here before we add the bevel tris and cache it...
for (FBevelEdge& Edge : Edges)
{
Edge.NormalsA.SetNum(Edge.MeshVertices.Num());
Edge.NormalsB.SetNum(Edge.MeshVertices.Num());
for (int32 k = 0; k < Edge.MeshVertices.Num(); ++k)
{
Edge.NormalsA[k] = FMeshNormals::ComputeVertexNormal(Mesh, Edge.MeshVertices[k]);
Edge.NormalsB[k] = FMeshNormals::ComputeVertexNormal(Mesh, Edge.NewMeshVertices[k]);
}
}
for (FBevelLoop& Loop : Loops)
{
Loop.NormalsA.SetNum(Loop.MeshVertices.Num());
Loop.NormalsB.SetNum(Loop.MeshVertices.Num());
for (int32 k = 0; k < Loop.MeshVertices.Num(); ++k)
{
Loop.NormalsA[k] = FMeshNormals::ComputeVertexNormal(Mesh, Loop.MeshVertices[k]);
Loop.NormalsB[k] = FMeshNormals::ComputeVertexNormal(Mesh, Loop.NewMeshVertices[k]);
}
}
for (FBevelEdge& Edge : Edges)
{
AppendEdgeQuads_Multi(Mesh, Edge);
}
for (FBevelLoop& Loop : Loops)
{
AppendLoopQuads_Multi(Mesh, Loop);
}
for (FBevelVertex& Vertex : Vertices)
{
if (Vertex.VertexType == EBevelVertexType::JunctionVertex)
{
if (Vertex.Wedges.Num() > 2)
{
AppendJunctionVertexPolygon_Multi(Mesh, Vertex);
}
}
}
// easier to do terminators last so that we can use quad edge to orient the triangle
TSet<FIndex2i> HandledQuadVtxPairs;
for (FBevelVertex& Vertex : Vertices)
{
if (Vertex.VertexType == EBevelVertexType::TerminatorVertex)
{
if (Vertex.ConnectedBevelVertex >= 0)
{
FBevelVertex& OtherVertex = Vertices[Vertex.ConnectedBevelVertex];
FIndex2i VtxPair(Vertex.VertexID, OtherVertex.VertexID);
VtxPair.Sort();
if (HandledQuadVtxPairs.Contains(VtxPair) == false)
{
AppendTerminatorVertexPairQuad_Multi(Mesh, Vertex, OtherVertex);
HandledQuadVtxPairs.Add(VtxPair);
}
}
else
{
AppendTerminatorVertexTriangles_Multi(Mesh, Vertex);
}
}
}
// At this point, all topology is determined. Now if desired a profile-curve shape can be applied as a postprocess.
// (Not entirely certain this is the right strategy for all profile types but it works OK for a simple round profile)
if (FMathd::Abs(RoundWeight) > FMathf::ZeroTolerance)
{
ApplyProfileShape_Round(Mesh);
}
}
FInterpCurveVector FMeshBevel::MakeArcSplineCurve(const FVector3d& PosA, FVector3d& NormalA, const FVector3d& PosB, FVector3d& NormalB) const
{
FInterpCurveVector Curve;
// basic idea here is to approximate an arc w/ a bezier curve. We know A and B,
// and we need TangentA (T_A) and TangentB. We have a surface Normal at A and B, so
// for T_A we can project B onto the surface tangent plane at A (ie with NormalA),
// and then B-A gives us a direction for T_A, and the same works for for B
//
// T_A
// <-----
// ^ __-*A
// | /
// T_B| / (this is the arc, sorry)
// | |
// *B
//
// The lengths of the Tangents in this approach end up being the lengths of the sides
// of the square, if this were a 2D situation. Which could effectively define a 2D arc.
// For now, we are use a FInterpCurve instead, which is not quite a Bezier. With those
// lengths, the curve will end up too flat, so it is scaled by a parameter (default sqrt 2).
// This could perhaps be exposed as an option...
//
// The advantage of using a curve here instead of an arc is that it allows something reasonable
// to happen for messy cases, like where the curve might not be planar. But likely some
// issues are going to arise with ugly projections...
// project the vector (B-A) onto the plane (A,NormalA).
FFrame3d PlaneNormalA(PosA, NormalA);
FVector3d TangentA = PlaneNormalA.ToPlane(PosB) - PosA;
// same for A-B onto (B, NormalB)
FFrame3d PlaneNormalB(PosB, NormalB);
FVector3d TangentB = PlaneNormalB.ToPlane(PosA) - PosB;
Curve.AddPoint(0, PosA);
Curve.AddPoint(1, PosB);
Curve.Points[0].InterpMode = EInterpCurveMode::CIM_CurveUser;
Curve.Points[1].InterpMode = EInterpCurveMode::CIM_CurveUser;
double TangentScale = FMathd::Abs(RoundWeight) * FMathd::Sqrt2;
if (RoundWeight >= 0)
{
Curve.Points[0].ArriveTangent = Curve.Points[0].LeaveTangent = TangentScale * TangentA;
Curve.Points[1].ArriveTangent = Curve.Points[1].LeaveTangent = -TangentScale * TangentB;
}
else
{
Curve.Points[0].ArriveTangent = Curve.Points[0].LeaveTangent = -TangentScale * TangentB;
Curve.Points[1].ArriveTangent = Curve.Points[1].LeaveTangent = TangentScale * TangentA;
}
return Curve;
}
void FMeshBevel::ApplyProfileShape_Round(FDynamicMesh3& Mesh)
{
// This function applies round profile curves to the bevel edge quadstrips &
// bevel vertex tessellated-polygons that were computed in the _Multi() functions.
// All the geometry already exists so the job here is just to deform it into rounder shapes.
//
// The current code is based on various different strategies but the basic idea is to
// compute a arc-shaped 3D splines between correspondeng edge-vertex pairs , and then derive
// the shape of the vertex patches using the now-curved borders. For 4-sided patches
// this is relatively simple, 5+-sided we use a sort of MVC-based hole-filling strategy, and
// for 3-sided currently using a 3-sided spline batch taken from PN-tessellation.
// The 3-sided case currently does not work very well though.
// Loops are an easy case as there are no junction vertices, so each vertex-pair gets a separate
// spline and everything is done.
for (FBevelLoop& Loop : Loops)
{
FQuadGridPatch& Patch = Loop.StripQuadPatch;
int32 NumCols = Patch.NumVertexCols();
for (int32 Col = 0; Col < NumCols-1; ++Col)
{
TArray<int32> ColVerts;
Patch.GetVertexColumn(Col, ColVerts);
int32 NV = ColVerts.Num();
int32 A = ColVerts[0], B = ColVerts.Last();
FVector3d PosA = Mesh.GetVertex(A), PosB = Mesh.GetVertex(B);
FVector3d NormalA = Loop.NormalsA[Col], NormalB = Loop.NormalsB[Col];
// project normals onto section plane defined by original and inset vertex positions.
// (do we even need the normals anymore? could they be defined by the 2D perp operator in the section plane?)
FVector3d InitialPosition = Loop.InitialPositions[Col];
FVector3d Direction1 = Normalized(PosA - InitialPosition);
FVector3d Direction2 = Normalized(PosB - InitialPosition);
FVector3d PlaneNormal = Normalized(Cross(Direction1, Direction2));
NormalA = Normalized(NormalA - NormalA.Dot(PlaneNormal) * PlaneNormal);
NormalB = Normalized(NormalB - NormalB.Dot(PlaneNormal) * PlaneNormal);
FInterpCurveVector Curve = MakeArcSplineCurve(PosA, NormalA, PosB, NormalB);
// TODO: should have special case here for flat patches? they get squished out a bit...
for (int32 k = 1; k < (NV - 1); ++k)
{
int32 VID = ColVerts[k];
FVector3d Pos = Mesh.GetVertex(VID);
double T = (double)k / (double)(NV-1);
FVector3d CurvePos = Curve.Eval((float)T, Lerp(PosA, PosB, T));
Mesh.SetVertex(VID, CurvePos);
}
}
}
// kinda gross approach to keeping track of the normals along the ends of edge spans,
// ie after we make them curved, we want the normals along those curves, ie what we
// want to use as the normal/tangent constraint for the bevelvertex-polygon. At this point
// the mesh topo should be finalized so we can just use an array...
TArray<FVector3d> DeformNormals;
DeformNormals.Init(FVector3d::Zero(), Mesh.MaxVertexID());
// we need to keep track of the curve used between each split pair of beveledge-vertices,
// and we need to be able to loop them up based on the vertex-pair.
TMap<FIndex2i, FInterpCurveVector> BorderCurves;
// search function for BorderCurves list
auto FindCurve = [&BorderCurves](int i0, int i1, bool& bReversed)
{
FInterpCurveVector* FoundCurve = BorderCurves.Find(FIndex2i(i0, i1));
bReversed = false;
if (FoundCurve == nullptr)
{
FoundCurve = BorderCurves.Find(FIndex2i(i1, i0));
bReversed = true;
}
return FoundCurve;
};
// Each edge is processed similar to a loop, ie the column of vertices bewteen each vertex-pair on
// either side of the quad-strip gets deformed by fitting a spline curve based on the endpoints and end-normals.
//
for (FBevelEdge& Edge : Edges)
{
FQuadGridPatch& Patch = Edge.StripQuadPatch;
bool bPatchIsFlippedX = false;
int32 NumCols = Patch.NumVertexCols();
for (int32 Col = 0; Col < NumCols; ++Col)
{
TArray<int32> ColVerts;
Patch.GetVertexColumn(Col, ColVerts);
int32 NumColVerts = ColVerts.Num();
// The Edge contains various vertex lists like Edge.MeshVertices that have a consistent ordering.
// However because of how the patches are constructed, the columns may be reversed (unclear why...)
// Detect that case so that the indexing Col/Array indexing can be inverted where necessary
if (Col == 0 && ColVerts.Contains(Edge.MeshVertices[0]) == false)
{
bPatchIsFlippedX = true;
}
int32 A = ColVerts[0];
int32 B = ColVerts.Last();
FVector3d PosA = Mesh.GetVertex(A);
FVector3d PosB = Mesh.GetVertex(B);
int UseEdgeVertIndex = (bPatchIsFlippedX) ? (NumCols-Col-1) : Col;
FVector3d NormalA = Edge.NormalsA[UseEdgeVertIndex];
FVector3d NormalB = Edge.NormalsB[UseEdgeVertIndex];
// Ok for each column along a bevel edge-strip, we want to bend the column into a curve.
// We have the endpoints we want to interpolate, and we have normals at those points that we
// computed elsewhere and are going to assume are good, ie those are the normals we want the
// perfect rounded bevel to have, if it had infinite sections.
//
// The issue is that we have 2 points and 2 normals and they may not all lie in the same plane.
// For most cases, the original vertex position has been inset in two directions (to PosA and PosB), and
// so that gives us the plane they all should lie in. However at valence3+ junction vertices this is not true,
// as the one corner vertex was inset along 3+ edges and so we don't have the 'InitialPosition'
// value below for each of those edges, that we would need to define the simple arc-sections.
//
// (todo: maybe keeping track of those positions would be better than what we do now
bool bInferTangentPlaneFromInitialPosition = true;
bool bIsEndpoint = (UseEdgeVertIndex == 0) || (UseEdgeVertIndex == NumCols - 1);
if ( bIsEndpoint )
{
int32 BevelVertexIndex = (UseEdgeVertIndex == 0) ? Edge.BevelVertices.A : Edge.BevelVertices.B;
const FBevelVertex& BevelVtx = Vertices[BevelVertexIndex];
if (BevelVtx.VertexType == EBevelVertexType::JunctionVertex && BevelVtx.IncomingBevelEdgeIndices.Num() > 2)
{
bInferTangentPlaneFromInitialPosition = false;
}
}
FVector3d InitialPosition = Edge.InitialPositions[UseEdgeVertIndex];
FVector3d Direction1 = Normalized(PosA - InitialPosition);
FVector3d Direction2 = Normalized(PosB - InitialPosition);
FVector3d SectionPlaneNormal = Normalized(Cross(Direction1, Direction2));
if (!bInferTangentPlaneFromInitialPosition)
{
FVector3d InitialEdgeDirection = (UseEdgeVertIndex == 0) ?
(Edge.InitialPositions[1] - InitialPosition) : (InitialPosition - Edge.InitialPositions[NumCols-2]);
if (InitialEdgeDirection.Normalize())
{
FFrame3d TempFrame(PosA, Normalized(PosB - PosA));
TempFrame.ConstrainedAlignAxis(1, InitialEdgeDirection, TempFrame.Z());
SectionPlaneNormal = TempFrame.Y();
}
}
// project normals onto section plane
NormalA = Normalized(NormalA - NormalA.Dot(SectionPlaneNormal) * SectionPlaneNormal);
NormalB = Normalized(NormalB - NormalB.Dot(SectionPlaneNormal) * SectionPlaneNormal);
// TODO: should have special case here for flat patches? they get squished out a bit...
// This may update NormalA/NormalB for 'inverted' bevels (ie w/ negative RoundWeight)
FInterpCurveVector Curve = MakeArcSplineCurve(PosA, NormalA, PosB, NormalB);
// accumulate normal at endpoints
DeformNormals[A] += NormalA;
DeformNormals[B] += NormalB;
// save this curve in the curves list
BorderCurves.Add(FIndex2i(A, B), Curve);
// map linear-interpolation to curve parameter and evaluate curve
TArray<FVector3d> CurveVerts;
CurveVerts.SetNum(NumColVerts);
CurveVerts[0] = PosA; CurveVerts[NumColVerts-1] = PosB;
for (int32 k = 1; k < (NumColVerts - 1); ++k)
{
int32 VID = ColVerts[k];
FVector3d Pos = Mesh.GetVertex(VID);
double T = (double)k / (double)(NumColVerts-1);
FVector3d CurvePos = Curve.Eval((float)T, Lerp(PosA, PosB, T));
Mesh.SetVertex(VID, CurvePos);
CurveVerts[k] = CurvePos;
}
// accumulate new interior normals (only triangles inside the edge are considered)
// (do we need these anywhere??)
for (int32 k = 1; k < (NumColVerts - 1); ++k)
{
int32 VID = ColVerts[k];
FVector3d CurveEdgeNormal = FMeshNormals::ComputeVertexNormal(Mesh, VID,
[&](int32 TriangleID) { return Mesh.GetTriangleGroup(TriangleID) == Edge.NewGroupID; }, true, true);
DeformNormals[VID] += CurveEdgeNormal;
}
}
}
// re-normalize here because we accumulated normals above...
for (FVector3d& Normal : DeformNormals)
{
Normal.Normalize();
}
// Process vertices. This code has special-cases for valence 3 and 4, which should be factored out into separate functions.
for (FBevelVertex& Vertex : Vertices)
{
int32 LoopN = Vertex.InteriorBorderLoop.Num();
for (const FBevelVertex_InteriorVertex& InteriorVtx : Vertex.InteriorVertices)
{
// Special case for valence-3 vertex, ie triangular patch. In this case we use
// a triangular spline, the math is based on PN triangulation, where the vertex/normal
// pairs at the endpoints are used. However a center-point/normal is also necessary.
// This does an OK job but seems to come out too flat in many cases...ultimately it is
// not going to even be C0 with the boundary spline curves we already decided on.
// Perhaps a better option would be to do it similar to the valence-4 case, where basically
// we construct interpolated splines based on the barycentrics and border splines
if (InteriorVtx.BorderFrameWeight.Num() == 1 && Vertex.InteriorBorderLoop.Num() == 3)
{
// Nearly all of this work below is identical for every interior vertex!!! It should be done once and cached.
double w = InteriorVtx.BorderFrameWeight[0].X, u = InteriorVtx.BorderFrameWeight[0].Y, v = InteriorVtx.BorderFrameWeight[0].Z;
int i300 = Vertex.InteriorBorderLoop[0];
FVector3d b300 = Mesh.GetVertex(i300);
int i030 = Vertex.InteriorBorderLoop[1];
FVector3d b030 = Mesh.GetVertex(i030);
int i003 = Vertex.InteriorBorderLoop[2];
FVector3d b003 = Mesh.GetVertex(i003);
FVector3d N300 = DeformNormals[i300];
FVector3d N030 = DeformNormals[i030];
FVector3d N003 = DeformNormals[i003];
bool bReversedA = false, bReversedB = false, bReversedC = false;
FInterpCurveVector* CurveA = FindCurve(i300, i030, bReversedA);
FInterpCurveVector* CurveB = FindCurve(i030, i003, bReversedB);
FInterpCurveVector* CurveC = FindCurve(i003, i300, bReversedC);
if ( !ensure(CurveA != nullptr && CurveB != nullptr && CurveC != nullptr )) continue;
// PN triangle sides are defined by bezier curves, however FInterpCurve is not actually a Bezier, it's missing a 3
// on the tangents in the math. So scaling by 1/3 here corrects for this (...ish?)
const double TangentScale = 1.0 / 3.0;
FVector3d b210 = TangentScale * ((bReversedA) ? -CurveA->Points[1].LeaveTangent : CurveA->Points[0].LeaveTangent);
b210 += b300;
FVector3d b120 = TangentScale * ((bReversedA) ? CurveA->Points[0].LeaveTangent : -CurveA->Points[1].LeaveTangent);
b120 += b030;
FVector3d b021 = TangentScale * ((bReversedB) ? -CurveB->Points[1].LeaveTangent : CurveB->Points[0].LeaveTangent);
b021 += b030;
FVector3d b012 = TangentScale * ((bReversedB) ? CurveB->Points[0].LeaveTangent : -CurveB->Points[1].LeaveTangent);
b012 += b003;
FVector3d b102 = TangentScale * ((bReversedC) ? -CurveC->Points[1].LeaveTangent : CurveC->Points[0].LeaveTangent);
b102 += b003;
FVector3d b201 = TangentScale * ((bReversedC) ? CurveC->Points[0].LeaveTangent : -CurveC->Points[1].LeaveTangent);
b201 += b300;
// this computed midpoint tends to be too flat...
FVector3d E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0;
FVector3d V = (b300 + b030 + b003) / 3.0;
FVector3d b111 = E + RoundWeight * (E - V) / 2.0;
FVector InterpPos = b300*w*w*w + b030*u*u*u + b003*v*v*v
+ b210*3*w*w*u + b120*3*w*u*u + b201*3*w*w*v
+ b021*3*u*u*v + b102*3*w*v*v + b012*3*u*v*v
+ b111*6*w*u*v;
Mesh.SetVertex(InteriorVtx.VertexID, InterpPos);
continue;
}
// this block handles special case of valence-4 vertex that can be done w/ a quad patch interpolating boundary curves
// TODO: refactor this to a separate first pass so that we can pull the curve setup out of each iteration
if (InteriorVtx.BorderFrameWeight.Num() == 1 && Vertex.InteriorBorderLoop.Num() == 4)
{
// Nearly all of this work below is identical for every interior vertex!!! It should be done once and cached.
// here is the situation: We have a tessellated quad patch, when we tessellated it, it was planar (or
// at least the edges were straight). At tessellation time we computed barycentric coordinates for each interior vertex.
// Now we have curved the edges of the patch along spline curves (above). So basically we want to compute new interior
// positions by blending the four edge curve positions. If we think of the quad as having 2 axes X and Y, we are
// going to blend the two X curves along the two Y curves, and then evaluate the blended curve to get the vertex position.
// This is messy because (1) we have to look up the curves and (2) each one might have swapped end-vertices with a reversed parameter space.
// Most of the code below is about handling that weirdness.
//
// Note that since these are (almost) Beziers, the 'curve blending' is strictly about the tangents, as the endpoints are fixed/known
//
//c01 X2 c11 (diagram corresponds to variable naming below)
// *--------------* ty=1
// Y1| |Y2
// | XInterp |
// A*==============*B
// | |
// *--------------* ty=0
//c00 X1 c10
//
// tx=0 tx=1
// these are the UV (XY) coords of the vertex inside the initial planar quad
double tx = InteriorVtx.BorderFrameWeight[0].X, ty = InteriorVtx.BorderFrameWeight[0].Y;
// four ordered corner vertices of the initial planar quad
int c00 = Vertex.InteriorBorderLoop[0];
int c10 = Vertex.InteriorBorderLoop[1];
int c01 = Vertex.InteriorBorderLoop[2];
int c11 = Vertex.InteriorBorderLoop[3];
// locate the two "Y" edge curves from their corner vertices, handling case of curve being reversed
FInterpCurveVector* CurveY1 = BorderCurves.Find(FIndex2i(c00, c01));
bool bReversed1 = false;
if (CurveY1 == nullptr)
{
CurveY1 = BorderCurves.Find(FIndex2i(c01, c00));
bReversed1 = true;
}
if ( ! ensure(CurveY1 != nullptr) ) continue;
FInterpCurveVector* CurveY2 = BorderCurves.Find(FIndex2i(c10, c11));
bool bReversed2 = false;
if (CurveY2 == nullptr)
{
CurveY2 = BorderCurves.Find(FIndex2i(c11, c10));
bReversed2 = true;
}
if (!ensure(CurveY2 != nullptr)) continue;
// evaluate each edge curve at the Y coord
double tA = bReversed1 ? (1.0 - ty) : ty;
FVector A = CurveY1->Eval((float)tA, FVector::Zero()); // TODO: should lerp here instead of using ::Zero() obviously...
double tB = bReversed2 ? (1.0 - ty) : ty;
FVector B = CurveY2->Eval((float)tB, FVector::Zero()); // TODO: should lerp here instead of using ::Zero() obviously...
// now find the two "X" edge curves from their corner vertices, again handling case of reversed edge
FInterpCurveVector* CurveX1 = BorderCurves.Find(FIndex2i(c00, c10));
bool bReversedX1 = false;
if (CurveX1 == nullptr)
{
CurveX1 = BorderCurves.Find(FIndex2i(c10, c00));
bReversedX1 = true;
}
if (!ensure(CurveX1 != nullptr)) continue;
FInterpCurveVector* CurveX2 = BorderCurves.Find(FIndex2i(c01, c11));
bool bReversedX2 = false;
if (CurveX2 == nullptr)
{
CurveX2 = BorderCurves.Find(FIndex2i(c11, c01));
bReversedX2 = true;
}
if (!ensure(CurveX2 != nullptr)) continue;
// extract tangents of X curves, need to blend these to compute interpolated tangents at Y-curve points A/B
FVector3d Tangent00 = (bReversedX1) ? CurveX1->Points[1].LeaveTangent : -CurveX1->Points[0].LeaveTangent;
FVector3d Tangent10 = (bReversedX1) ? -CurveX1->Points[0].LeaveTangent : CurveX1->Points[1].LeaveTangent;
FVector3d Tangent01 = (bReversedX2) ? CurveX2->Points[1].LeaveTangent : -CurveX2->Points[0].LeaveTangent;
FVector3d Tangent11 = (bReversedX2) ? -CurveX2->Points[0].LeaveTangent : CurveX2->Points[1].LeaveTangent;
// should perhaps be based on interpolating tangents along Y splines instead of just lerp? is it equivalent?
FVector TangentA = Lerp(Tangent00, Tangent01, ty);
FVector TangentB = Lerp(Tangent10, Tangent11, ty);
// construct interpolated X curve, has endpoints evaluated along Y curves, with X curve endpoint tangents
// blended along Y curves
FInterpCurveVector InterpolatedXCurve;
InterpolatedXCurve.AddPoint(0, A);
InterpolatedXCurve.AddPoint(1, B);
InterpolatedXCurve.Points[0].ArriveTangent = InterpolatedXCurve.Points[0].LeaveTangent = -TangentA;
InterpolatedXCurve.Points[1].ArriveTangent = InterpolatedXCurve.Points[1].LeaveTangent = TangentB;
InterpolatedXCurve.Points[0].InterpMode = InterpolatedXCurve.Points[1].InterpMode = EInterpCurveMode::CIM_CurveUser;
// evaluate this new X curve at the vertex X position
FVector InterpPos = InterpolatedXCurve.Eval((float)tx, Lerp(A, B, tx));
Mesh.SetVertex(InteriorVtx.VertexID, InterpPos);
continue;
}
// This should only happen for valence 3 and 4 that we already handled. But if it happens
// elsewhere, it means we will just leave that patch flat
if (InteriorVtx.BorderFrameWeight.Num() != LoopN) continue;
// ok the general-case solution is basically to compute the new position for this vertex in the rotated
// frame of each boundary-polygon vertex, and blend those positions using the MVC coordinates. This produces
// a smooth interpolating surface but there is no continuity at the border, and the 'far' vertices will tend
// to exert too much influence
FVector3d BlendedPos = FVector3d::Zero();
double WeightSum = 0;
for (int32 k = 0; k < LoopN; ++k)
{
int32 BorderVID = Vertex.InteriorBorderLoop[k];
FVector3d BorderPos = Mesh.GetVertex(BorderVID);
FVector3d BorderFrameX = Mesh.GetVertex(Vertex.InteriorBorderLoop[(k+1) % LoopN]) -
Mesh.GetVertex(Vertex.InteriorBorderLoop[(k-1 + LoopN) % LoopN]);
BorderFrameX = Normalized(BorderFrameX);
FVector3d BorderFrameN = DeformNormals[BorderVID];
if (RoundWeight < 0)
{
FQuaterniond RotQuat(BorderFrameX, -45, true);
BorderFrameN = RotQuat * BorderFrameN;
}
FVector3d BorderFrameY = Cross(BorderFrameN, BorderFrameX);
FVector3d FrameDeltaWeight = InteriorVtx.BorderFrameWeight[k];
FVector3d ReconstructedPos = BorderPos + (FrameDeltaWeight.X*BorderFrameX) + (FrameDeltaWeight.Y*BorderFrameY);
BlendedPos += FrameDeltaWeight.Z * ReconstructedPos;
WeightSum += FrameDeltaWeight.Z;
}
BlendedPos *= (1.0 / WeightSum);
Mesh.SetVertex(InteriorVtx.VertexID, BlendedPos);
}
}
}
void FMeshBevel::ComputeNormals(FDynamicMesh3& Mesh)
{
if (Mesh.HasAttributes() == false)
{
return;
}
FDynamicMeshNormalOverlay* NormalOverlay = Mesh.Attributes()->PrimaryNormals();
auto SetNormalsOnTriRegion = [NormalOverlay](const TArray<int32>& Triangles)
{
if (Triangles.Num() > 0)
{
FMeshNormals::InitializeOverlayRegionToPerVertexNormals(NormalOverlay, Triangles);
}
};
for (FBevelVertex& Vertex : Vertices)
{
SetNormalsOnTriRegion(Vertex.NewTriangles);
}
TArray<int32> TriList;
for (FBevelEdge& Edge : Edges)
{
UELocal::QuadsToTris(Mesh, Edge.StripQuads, TriList, true);
SetNormalsOnTriRegion(TriList);
}
for (FBevelLoop& Loop : Loops)
{
UELocal::QuadsToTris(Mesh, Loop.StripQuads, TriList, true);
SetNormalsOnTriRegion(TriList);
}
}
void FMeshBevel::ComputeUVs(FDynamicMesh3& Mesh)
{
if (Mesh.HasAttributes() == false)
{
return;
}
FDynamicMeshUVOverlay* UVOverlay = Mesh.Attributes()->PrimaryUV();
auto SetUVsOnTriRegion = [&Mesh, UVOverlay](const TArray<int32>& Triangles)
{
if (Triangles.Num() > 0)
{
UE::Geometry::ComputeArbitraryTrianglePatchUVs(Mesh, *UVOverlay, Triangles);
}
};
TArray<int32> TriList;
for (FBevelEdge& Edge : Edges)
{
UELocal::QuadsToTris(Mesh, Edge.StripQuads, TriList, true);
SetUVsOnTriRegion(TriList);
}
for (FBevelLoop& Loop : Loops)
{
UELocal::QuadsToTris(Mesh, Loop.StripQuads, TriList, true);
SetUVsOnTriRegion(TriList);
}
// do vertices last because until edges have UVs, the vertex polygons have no neighbour UVs islands
for (FBevelVertex& Vertex : Vertices)
{
SetUVsOnTriRegion(Vertex.NewTriangles);
}
}
void FMeshBevel::ComputeMaterialIDs(FDynamicMesh3& Mesh)
{
if (Mesh.HasAttributes() == false || Mesh.Attributes()->HasMaterialID() == false)
{
return;
}
FDynamicMeshMaterialAttribute* MaterialIDs = Mesh.Attributes()->GetMaterialID();
if (MaterialIDMode == EMaterialIDMode::ConstantMaterialID)
{
for (int32 tid : NewTriangles)
{
MaterialIDs->SetValue(tid, SetConstantMaterialID);
}
}
else
{
auto SetQuadMaterial = [&Mesh, MaterialIDs](const FIndex2i& Quad, int32 MaterialID)
{
if (Quad.A >= 0)
{
MaterialIDs->SetValue(Quad.A, MaterialID);
}
if (Quad.B >= 0)
{
MaterialIDs->SetValue(Quad.B, MaterialID);
}
};
// Try to set materials on the new triangles along a beveled edge based on the adjacent pre-bevel triangles.
// Could be improved to at least be more consistent in ambiguous cases
auto SetEdgeMaterials = [&Mesh, MaterialIDs, this, SetQuadMaterial](const TArray<FIndex2i>& StripQuads, const TArray<FIndex2i>& EdgeTris)
{
int32 NumEdges = EdgeTris.Num();
if (StripQuads.Num() == NumEdges)
{
TArray<int32> SawMaterialIDs;
TArray<int32> AmbiguousEdges;
for (int32 k = 0; k < StripQuads.Num(); ++k)
{
FIndex2i NbrTris = EdgeTris[k];
int MatIDA = MaterialIDs->GetValue(NbrTris.A);
int MatIDB = (NbrTris.A >= 0) ? MaterialIDs->GetValue(NbrTris.B) : MatIDA;
SawMaterialIDs.AddUnique(MatIDA);
SawMaterialIDs.AddUnique(MatIDB);
int SetMaterialID = (MatIDA == MatIDB) ? MatIDA : SetConstantMaterialID;
if (MatIDA != MatIDB)
{
if (MaterialIDMode == EMaterialIDMode::InferMaterialID_ConstantIfAmbiguous)
{
SetMaterialID = SetConstantMaterialID;
}
else
{
AmbiguousEdges.Add(k);
}
}
SetQuadMaterial(StripQuads[k], SetMaterialID);
}
if (AmbiguousEdges.Num() > 0)
{
SawMaterialIDs.Sort();
if (AmbiguousEdges.Num() == NumEdges) // if all ambigous, just pick one
{
for (int32 k : AmbiguousEdges)
{
SetQuadMaterial(StripQuads[k], SawMaterialIDs[0]);
}
}
else
{
// TODO: what we probably want to do here is "infill" from known neighbours.
// for now we will just punt and pick one
for (int32 k : AmbiguousEdges)
{
SetQuadMaterial(StripQuads[k], SawMaterialIDs[0]);
}
}
}
}
else
{
for (const FIndex2i& Quad : StripQuads)
{
SetQuadMaterial(Quad, SetConstantMaterialID);
}
}
};
for (FBevelEdge& Edge : Edges)
{
SetEdgeMaterials(Edge.StripQuads, Edge.MeshEdgeTris);
}
for (FBevelLoop& Loop : Loops)
{
SetEdgeMaterials(Loop.StripQuads, Loop.MeshEdgeTris);
}
// find all the unique material IDs of neighbours of the Triangles list (that are not in Triangles list) and
// return (MaterialID, NbrTriCount) tuples as a pair of lists
auto CountUniqueBorderMaterialIDs = [&](const FDynamicMesh3& Mesh, const FDynamicMeshMaterialAttribute& MaterialAttrib, const TArray<int32>& Triangles, TArray<int32>& MaterialIDs, TArray<int32>& Counts)
{
MaterialIDs.Reset(); Counts.Reset();
for (int32 tid : Triangles)
{
FIndex3i TriNbrs = Mesh.GetTriNeighbourTris(tid);
for (int32 j = 0; j < 3; ++j)
{
int32 NbrTriangleID = TriNbrs[j];
if ( Mesh.IsTriangle(NbrTriangleID) == false || Triangles.Contains(NbrTriangleID) )
{
continue;
}
int MatID = MaterialAttrib.GetValue(NbrTriangleID);
int32 Index = MaterialIDs.AddUnique(MatID);
if (Counts.Num() != MaterialIDs.Num())
{
Counts.Add(0);
Counts[Index]++;
}
}
}
};
// For each bevel-vertex-polygon, pick the nbr material ID that was most frequent.
// Terminator vertices are also handled this way, which is not ideal, should probably
// ignore the new 'edge' faces for the terminator vertex
for (FBevelVertex& Vertex : Vertices)
{
TArray<int32> NbrMaterialIDs, NbrMaterialIDCounts;
CountUniqueBorderMaterialIDs(Mesh, *MaterialIDs, Vertex.NewTriangles, NbrMaterialIDs, NbrMaterialIDCounts);
int32 SetMaterialID = SetConstantMaterialID;
if (NbrMaterialIDs.Num() > 0)
{
int32 MinIndex = 0;
for (int32 k = 1; k < NbrMaterialIDs.Num(); ++k)
{
if (NbrMaterialIDCounts[k] < NbrMaterialIDCounts[MinIndex])
{
MinIndex = k;
}
}
SetMaterialID = NbrMaterialIDs[MinIndex];
}
for (int32 tid : Vertex.NewTriangles)
{
MaterialIDs->SetValue(tid, SetMaterialID);
}
}
}
}