2021-11-01 12:01:36 -04:00
// 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"
2021-11-16 13:24:21 -05:00
# include "MeshRegionBoundaryLoops.h"
2023-12-12 17:55:33 -05:00
# include "MeshBoundaryLoops.h"
2021-11-03 17:40:43 -04:00
# include "Operations/PolyEditingEdgeUtil.h"
2021-11-04 18:24:07 -04:00
# include "Operations/PolyEditingUVUtil.h"
2021-11-01 12:01:36 -04:00
# include "Algo/Count.h"
2024-05-31 13:54:23 -04:00
# include "Algo/RemoveIf.h"
2021-11-03 17:40:43 -04:00
# include "Distance/DistLine3Line3.h"
2023-12-12 17:55:33 -05:00
# 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"
2021-11-01 12:01:36 -04:00
using namespace UE : : Geometry ;
2022-06-24 09:42:08 -04:00
// 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)
2023-07-24 12:27:01 -04:00
# define MESH_BEVEL_DEBUG_ENSURE(Expr) !!(Expr)
2022-06-24 09:42:08 -04:00
# endif
2021-11-04 18:24:07 -04:00
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 ) ;
}
}
} ;
}
2021-11-01 12:01:36 -04:00
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 ) ;
}
2021-11-16 13:24:21 -05:00
void FMeshBevel : : InitializeFromGroupTopologyEdges ( const FDynamicMesh3 & Mesh , const FGroupTopology & Topology , const TArray < int32 > & GroupEdges )
2021-11-01 12:01:36 -04:00
{
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 ) ;
}
2024-09-10 10:26:02 -04:00
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 ) ;
}
2021-11-01 12:01:36 -04:00
2021-11-16 13:24:21 -05:00
bool FMeshBevel : : InitializeFromGroupTopologyFaces ( const FDynamicMesh3 & Mesh , const FGroupTopology & Topology , const TArray < int32 > & GroupFaces )
{
2024-04-25 11:33:43 -04:00
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 ;
2021-11-16 13:24:21 -05:00
}
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 ;
}
2024-05-31 13:54:23 -04:00
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 ] ;
2021-11-16 13:24:21 -05:00
2024-05-31 13:54:23 -04:00
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 ) ;
}
}
2021-11-16 13:24:21 -05:00
2021-11-01 12:01:36 -04:00
bool FMeshBevel : : Apply ( FDynamicMesh3 & Mesh , FDynamicMeshChangeTracker * ChangeTracker )
{
2024-05-31 13:54:23 -04:00
FixBowties ( Mesh , ChangeTracker ) ;
2021-11-01 12:01:36 -04:00
// 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 ;
}
2021-11-03 17:40:43 -04:00
FixUpUnlinkedBevelEdges ( Mesh ) ;
if ( ResultInfo . CheckAndSetCancelled ( Progress ) )
{
return false ;
}
2021-11-01 12:01:36 -04:00
// update vertex positions
DisplaceVertices ( Mesh , InsetDistance ) ;
if ( ResultInfo . CheckAndSetCancelled ( Progress ) )
{
return false ;
}
// mesh the bevel corners and edges
2023-12-12 17:55:33 -05:00
if ( NumSubdivisions < = 0 )
{
CreateBevelMeshing ( Mesh ) ;
}
else
{
CreateBevelMeshing_Multi ( Mesh ) ;
}
2021-11-01 12:01:36 -04:00
if ( ResultInfo . CheckAndSetCancelled ( Progress ) )
{
return false ;
}
2021-11-04 18:24:07 -04:00
// 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 ) ;
}
2021-11-01 12:01:36 -04:00
// compute normals
ComputeNormals ( Mesh ) ;
if ( ResultInfo . CheckAndSetCancelled ( Progress ) )
{
return false ;
}
2021-11-04 18:24:07 -04:00
// compute UVs
ComputeUVs ( Mesh ) ;
if ( ResultInfo . CheckAndSetCancelled ( Progress ) )
{
return false ;
}
ComputeMaterialIDs ( Mesh ) ;
if ( ResultInfo . CheckAndSetCancelled ( Progress ) )
{
return false ;
}
// todo: compute other attribs
2021-11-01 12:01:36 -04:00
ResultInfo . SetSuccess ( true , Progress ) ;
return true ;
}
2023-12-12 17:55:33 -05:00
FMeshBevel : : FBevelVertex * FMeshBevel : : GetBevelVertexFromVertexID ( int32 VertexID , int32 * IndexOut )
2021-11-01 12:01:36 -04:00
{
int32 * FoundIndex = VertexIDToIndexMap . Find ( VertexID ) ;
if ( FoundIndex = = nullptr )
{
return nullptr ;
}
2023-12-12 17:55:33 -05:00
if ( IndexOut ! = nullptr )
{
* IndexOut = * FoundIndex ;
}
2021-11-01 12:01:36 -04:00
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 ;
2023-12-12 17:55:33 -05:00
int32 NewBevelEdgeIndex = Edges . Num ( ) ;
2021-11-01 12:01:36 -04:00
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 ( ) ;
2023-12-12 17:55:33 -05:00
int32 BevelVertexIndex = - 1 ;
FBevelVertex * VertInfo = GetBevelVertexFromVertexID ( VertexID , & BevelVertexIndex ) ;
2021-11-01 12:01:36 -04:00
if ( VertInfo = = nullptr )
{
FBevelVertex NewVertex ;
2024-05-31 13:54:23 -04:00
PRAGMA_DISABLE_DEPRECATION_WARNINGS
2021-11-01 12:01:36 -04:00
NewVertex . CornerID = CornerID ;
2024-05-31 13:54:23 -04:00
PRAGMA_ENABLE_DEPRECATION_WARNINGS
2021-11-01 12:01:36 -04:00
NewVertex . VertexID = VertexID ;
2023-12-12 17:55:33 -05:00
BevelVertexIndex = Vertices . Num ( ) ;
2021-11-01 12:01:36 -04:00
Vertices . Add ( NewVertex ) ;
2023-12-12 17:55:33 -05:00
VertexIDToIndexMap . Add ( VertexID , BevelVertexIndex ) ;
VertInfo = & Vertices [ BevelVertexIndex ] ;
2021-11-01 12:01:36 -04:00
}
VertInfo - > IncomingBevelMeshEdges . Add ( IncomingEdgeID ) ;
2024-05-31 13:54:23 -04:00
PRAGMA_DISABLE_DEPRECATION_WARNINGS
2021-11-01 12:01:36 -04:00
VertInfo - > IncomingBevelTopoEdges . Add ( GroupEdgeID ) ;
2024-05-31 13:54:23 -04:00
PRAGMA_ENABLE_DEPRECATION_WARNINGS
2023-12-12 17:55:33 -05:00
VertInfo - > IncomingBevelEdgeIndices . Add ( NewBevelEdgeIndex ) ;
Edge . BevelVertices [ ci ] = BevelVertexIndex ;
2021-11-01 12:01:36 -04:00
}
Edge . MeshEdges . Append ( MeshEdgeList ) ;
Edge . MeshVertices . Append ( Topology . Edges [ GroupEdgeID ] . Span . Vertices ) ;
2024-05-31 13:54:23 -04:00
PRAGMA_DISABLE_DEPRECATION_WARNINGS
2021-11-01 12:01:36 -04:00
Edge . GroupEdgeID = GroupEdgeID ;
Edge . GroupIDs = Topology . Edges [ GroupEdgeID ] . Groups ;
2024-05-31 13:54:23 -04:00
PRAGMA_ENABLE_DEPRECATION_WARNINGS
2021-11-04 18:24:07 -04:00
Edge . MeshEdgeTris . Reserve ( Edge . MeshEdges . Num ( ) ) ;
for ( int32 eid : Edge . MeshEdges )
{
Edge . MeshEdgeTris . Add ( Mesh . GetEdgeT ( eid ) ) ;
}
2023-12-12 17:55:33 -05:00
Edge . InitialPositions . Reserve ( Edge . MeshVertices . Num ( ) ) ;
for ( int32 vid : Edge . MeshVertices )
{
Edge . InitialPositions . Add ( Mesh . GetVertex ( vid ) ) ;
}
Edge . EdgeIndex = Edges . Num ( ) ;
2021-11-01 12:01:36 -04:00
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 ;
2021-11-04 18:24:07 -04:00
Loop . MeshEdgeTris . Reserve ( Loop . MeshEdges . Num ( ) ) ;
for ( int32 eid : Loop . MeshEdges )
{
Loop . MeshEdgeTris . Add ( Mesh . GetEdgeT ( eid ) ) ;
}
2023-12-12 17:55:33 -05:00
Loop . InitialPositions . Reserve ( Loop . MeshVertices . Num ( ) ) ;
for ( int32 vid : Loop . MeshVertices )
{
Loop . InitialPositions . Add ( Mesh . GetVertex ( vid ) ) ;
}
2021-11-01 12:01:36 -04:00
Loops . Add ( Loop ) ;
}
2024-05-31 13:54:23 -04:00
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 ;
}
2021-11-01 12:01:36 -04:00
2024-05-31 13:54:23 -04:00
// 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 ;
}
}
}
}
}
2021-11-01 12:01:36 -04:00
void FMeshBevel : : BuildVertexSets ( const FDynamicMesh3 & Mesh )
{
// can be parallel
for ( FBevelVertex & Vertex : Vertices )
{
2024-05-31 13:54:23 -04:00
InitVertexSet ( Mesh , Vertex ) ;
2021-11-01 12:01:36 -04:00
if ( ResultInfo . CheckAndSetCancelled ( Progress ) )
{
return ;
}
}
2022-09-08 20:09:26 -04:00
// 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 )
{
2024-05-31 13:54:23 -04:00
FinalizeTerminatorVertex ( Mesh , Vertex ) ;
2022-09-08 20:09:26 -04:00
}
2021-11-01 12:01:36 -04:00
}
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 ) ;
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( SharedEdge ! = - 1 ) ;
2021-11-01 12:01:36 -04:00
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 ;
2022-06-24 09:42:08 -04:00
if ( MESH_BEVEL_DEBUG_ENSURE ( Vertex . IncomingBevelMeshEdges . Num ( ) = = 1 ) = = false )
2021-11-01 12:01:36 -04:00
{
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 ) ) ;
2022-09-08 20:09:26 -04:00
// 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 ;
}
}
2021-11-01 12:01:36 -04:00
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
2022-09-08 20:09:26 -04:00
for ( int32 k = 0 ; k < NumTriangles ; + + k )
2021-11-01 12:01:36 -04:00
{
2022-09-08 20:09:26 -04:00
// 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 ] ;
2021-11-01 12:01:36 -04:00
int32 gid = Mesh . GetTriangleGroup ( tid ) ;
if ( IncomingEdgeGroups . Contains ( gid ) = = false )
{
OtherGroupTris . Add ( tid ) ;
OtherGroups . AddUnique ( gid ) ;
}
}
2022-09-08 20:09:26 -04:00
int32 NumRemainingTris = OtherGroupTris . Num ( ) ;
2021-11-01 12:01:36 -04:00
// 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
{
2022-09-08 20:09:26 -04:00
// try using the 'middlest' triangle as the 'middlest' edge
2021-11-01 12:01:36 -04:00
// 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 ] ) ;
2022-09-08 20:09:26 -04:00
// 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 ;
}
}
}
2021-11-01 12:01:36 -04:00
}
}
else
{
2022-09-08 20:09:26 -04:00
// 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 )
2021-11-01 12:01:36 -04:00
{
2022-09-08 20:09:26 -04:00
int32 TriangleA = OtherGroupTris [ k ] , TriangleB = OtherGroupTris [ ( k + 1 ) % NumRemainingTris ] ;
if ( Mesh . GetTriangleGroup ( TriangleA ) ! = Mesh . GetTriangleGroup ( TriangleB ) )
2021-11-01 12:01:36 -04:00
{
2022-09-08 20:09:26 -04:00
RingSplitEdgeID = FindSharedEdgeInTriangles ( Mesh , TriangleA , TriangleB ) ;
2021-11-16 13:24:21 -05:00
Vertex . NewGroupID = - 1 ; // allocate a new group for this triangle, this is usually what one would want
2021-11-01 12:01:36 -04:00
break ;
}
}
}
if ( RingSplitEdgeID = = - 1 )
{
return ;
}
2021-12-16 19:34:36 -05:00
// save edgeid/vertexid for the 'terminator edge' that we will disconnect at
2021-11-01 12:01:36 -04:00
FIndex2i SplitEdgeV = Mesh . GetEdgeV ( RingSplitEdgeID ) ;
Vertex . TerminatorInfo = FIndex2i ( RingSplitEdgeID , SplitEdgeV . OtherElement ( Vertex . VertexID ) ) ;
2021-12-16 19:34:36 -05:00
// split the terminator vertex into two wedges
2021-11-01 12:01:36 -04:00
TArray < int32 > SplitTriSets [ 2 ] ;
if ( SplitInteriorVertexTrianglesIntoSubsets ( & Mesh , Vertex . VertexID , IncomingEdgeID , RingSplitEdgeID , SplitTriSets [ 0 ] , SplitTriSets [ 1 ] ) = = false )
{
return ;
}
2021-12-16 19:34:36 -05:00
// make the wedge list
2021-11-01 12:01:36 -04:00
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 ] ) ;
2021-12-16 19:34:36 -05:00
// 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 ) ;
}
2021-11-01 12:01:36 -04:00
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 ) ;
2022-06-24 09:42:08 -04:00
if ( MESH_BEVEL_DEBUG_ENSURE ( Result = = EMeshResult : : Ok ) )
2021-11-01 12:01:36 -04:00
{
BevelEdge . NewMeshVertices . Add ( SplitInfo . NewVertex ) ;
bDone = true ;
}
}
if ( ! bDone )
{
BevelEdge . NewMeshVertices . Add ( Split . VertexID ) ;
}
}
// now build edge correspondences
N = BevelEdge . MeshVertices . Num ( ) ;
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( N = = BevelEdge . NewMeshVertices . Num ( ) ) ;
2021-11-01 12:01:36 -04:00
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 ) ;
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( Edge1 > = 0 ) ;
2021-11-01 12:01:36 -04:00
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 ) ;
2022-06-24 09:42:08 -04:00
if ( MESH_BEVEL_DEBUG_ENSURE ( Result = = EMeshResult : : Ok ) )
2021-11-01 12:01:36 -04:00
{
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 ( ) ;
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( N = = BevelLoop . NewMeshVertices . Num ( ) ) ;
2021-11-01 12:01:36 -04:00
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 ) ;
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( Edge1 > = 0 ) ;
2021-11-01 12:01:36 -04:00
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 )
{
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( Vertex . VertexType = = EBevelVertexType : : JunctionVertex ) ;
2021-11-01 12:01:36 -04:00
if ( ChangeTracker )
{
ChangeTracker - > SaveVertexOneRingTriangles ( Vertex . VertexID , true ) ;
}
int32 NumWedges = Vertex . Wedges . Num ( ) ;
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( NumWedges > 1 ) ;
2021-11-01 12:01:36 -04:00
// 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 ) ;
2022-06-24 09:42:08 -04:00
if ( MESH_BEVEL_DEBUG_ENSURE ( Result = = EMeshResult : : Ok ) )
2021-11-01 12:01:36 -04:00
{
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 )
{
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( BevelVertex . VertexType = = EBevelVertexType : : TerminatorVertex ) ;
MESH_BEVEL_DEBUG_CHECK ( BevelVertex . Wedges . Num ( ) = = 2 ) ;
2021-11-01 12:01:36 -04:00
if ( ChangeTracker )
{
ChangeTracker - > SaveVertexOneRingTriangles ( BevelVertex . VertexID , true ) ;
}
// split the vertex
FDynamicMesh3 : : FVertexSplitInfo SplitInfo ;
EMeshResult Result = Mesh . SplitVertex ( BevelVertex . VertexID , BevelVertex . Wedges [ 1 ] . Triangles , SplitInfo ) ;
2022-06-24 09:42:08 -04:00
if ( MESH_BEVEL_DEBUG_ENSURE ( Result = = EMeshResult : : Ok ) )
2021-11-01 12:01:36 -04:00
{
BevelVertex . Wedges [ 1 ] . WedgeVertex = SplitInfo . NewVertex ;
2021-12-16 19:34:36 -05:00
// 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 ;
}
}
}
2021-11-01 12:01:36 -04:00
}
2021-12-16 19:34:36 -05:00
2021-11-03 17:40:43 -04:00
}
2021-11-01 12:01:36 -04:00
2021-11-03 17:40:43 -04:00
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 )
{
2024-02-08 18:51:40 -05:00
// 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 )
2021-11-03 17:40:43 -04:00
{
2024-02-08 18:51:40 -05:00
bool bNewEdgeIsOld = Edge . MeshEdges [ Idx ] = = Edge . NewMeshEdges [ Idx ] ;
if ( ! bSingleEdge & & ! bNewEdgeIsOld )
{
continue ;
}
int32 * FoundOtherEdge = MeshEdgePairs . Find ( Edge . MeshEdges [ Idx ] ) ;
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( FoundOtherEdge ! = nullptr ) ;
2021-11-03 17:40:43 -04:00
if ( FoundOtherEdge ! = nullptr )
{
2024-02-08 18:51:40 -05:00
Edge . NewMeshEdges [ Idx ] = * FoundOtherEdge ;
2021-11-03 17:40:43 -04:00
}
else
{
2024-02-08 18:51:40 -05:00
bFailedEdgePairing = true ; // something went wrong, loop below will break things
break ;
2021-11-03 17:40:43 -04:00
}
}
2024-02-08 18:51:40 -05:00
if ( bFailedEdgePairing )
{
continue ;
}
2021-11-03 17:40:43 -04:00
// 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 )
{
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( Mesh . GetEdgeV ( E0 ) . Contains ( Wedge . WedgeVertex ) ) ;
2021-11-03 17:40:43 -04:00
V0 = Wedge . WedgeVertex ;
bFoundV0 = true ;
break ;
}
else if ( TriEdges . Contains ( E1 ) & & bFoundV1 = = false )
{
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( Mesh . GetEdgeV ( E1 ) . Contains ( Wedge . WedgeVertex ) ) ;
2021-11-03 17:40:43 -04:00
V1 = Wedge . WedgeVertex ;
bFoundV1 = true ;
break ;
}
}
}
}
}
2021-11-01 12:01:36 -04:00
}
void FMeshBevel : : DisplaceVertices ( FDynamicMesh3 & Mesh , double Distance )
{
2021-11-03 17:40:43 -04:00
// fallback (very bad) technique to compute an inset vertex
2021-11-01 12:01:36 -04:00
auto GetDisplacedVertexPos = [ Distance ] ( const FDynamicMesh3 & Mesh , int32 VertexID ) - > FVector3d
{
FVector3d CurPos = Mesh . GetVertex ( VertexID ) ;
FVector3d Centroid = FMeshWeights : : MeanValueCentroid ( Mesh , VertexID ) ;
2021-11-03 17:40:43 -04:00
return CurPos + Distance * Normalized ( Centroid - CurPos ) ;
2021-11-01 12:01:36 -04:00
} ;
2021-11-03 17:40:43 -04:00
// 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).
2021-11-01 12:01:36 -04:00
2021-11-03 17:40:43 -04:00
// 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
2021-11-01 12:01:36 -04:00
{
2021-11-03 17:40:43 -04:00
TArray < FLine3d > InsetLines0 ;
TArray < FLine3d > InsetLines1 ;
2021-11-01 12:01:36 -04:00
} ;
2021-11-03 17:40:43 -04:00
TArray < FEdgePathInsetLines > AllInsetLines ;
AllInsetLines . SetNum ( Edges . Num ( ) ) ;
2021-11-01 12:01:36 -04:00
2021-11-03 17:40:43 -04:00
// solve open paths
for ( int32 k = 0 ; k < Edges . Num ( ) ; + + k )
2021-11-01 12:01:36 -04:00
{
2021-11-03 17:40:43 -04:00
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 ) ;
2021-11-01 12:01:36 -04:00
}
2021-11-03 17:40:43 -04:00
// solve loops
2021-11-01 12:01:36 -04:00
for ( FBevelLoop & Loop : Loops )
{
2021-11-03 17:40:43 -04:00
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 ) ;
2021-11-01 12:01:36 -04:00
}
2021-11-03 17:40:43 -04:00
// Now solve corners. For corners, we want to find the 1 or 2 inset-lines corresponding
2023-12-15 14:57:40 -05:00
// to the outgoing bevel-edges at each bevel-vertex-wedge.
2021-11-01 12:01:36 -04:00
for ( FBevelVertex & Vertex : Vertices )
{
2023-12-15 14:57:40 -05:00
if ( Vertex . VertexType = = EBevelVertexType : : Unknown )
2021-11-01 12:01:36 -04:00
{
2023-12-15 14:57:40 -05:00
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 )
2021-11-01 12:01:36 -04:00
{
2023-12-15 14:57:40 -05:00
if ( Edges [ j ] . MeshVertices [ 0 ] = = Wedge . WedgeVertex )
2021-11-03 17:40:43 -04:00
{
2023-12-15 14:57:40 -05:00
SolveLines . Add ( AllInsetLines [ j ] . InsetLines0 [ 0 ] ) ;
2021-11-03 17:40:43 -04:00
}
2023-12-15 14:57:40 -05:00
else if ( Edges [ j ] . MeshVertices . Last ( ) = = Wedge . WedgeVertex )
2021-11-03 17:40:43 -04:00
{
2023-12-15 14:57:40 -05:00
SolveLines . Add ( AllInsetLines [ j ] . InsetLines0 . Last ( ) ) ;
2021-11-03 17:40:43 -04:00
}
2023-12-15 14:57:40 -05:00
else if ( Edges [ j ] . NewMeshVertices [ 0 ] = = Wedge . WedgeVertex )
2021-11-03 17:40:43 -04:00
{
2023-12-15 14:57:40 -05:00
SolveLines . Add ( AllInsetLines [ j ] . InsetLines1 [ 0 ] ) ;
2021-11-03 17:40:43 -04:00
}
2023-12-15 14:57:40 -05:00
else if ( Edges [ j ] . NewMeshVertices . Last ( ) = = Wedge . WedgeVertex )
2021-11-03 17:40:43 -04:00
{
2023-12-15 14:57:40 -05:00
SolveLines . Add ( AllInsetLines [ j ] . InsetLines1 . Last ( ) ) ;
2021-11-03 17:40:43 -04:00
}
2021-11-01 12:01:36 -04:00
}
2023-12-15 14:57:40 -05:00
// 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 )
{
2023-12-19 13:48:50 -05:00
// TODO: this ensure has been sporadically hit in Lyra. Needs to be investigated.
//ensure(SolveLines.Num() == 1);
if ( SolveLines . Num ( ) = = 1 )
2023-12-15 14:57:40 -05:00
{
// 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
{
2024-02-08 18:51:40 -05:00
MESH_BEVEL_DEBUG_CHECK ( SolveLines . Num ( ) > = 2 ) ;
2023-12-19 13:48:50 -05:00
if ( SolveLines . Num ( ) > = 2 )
2023-12-15 14:57:40 -05:00
{
Wedge . NewPosition = UE : : Geometry : : SolveInsetVertexPositionFromLinePair ( CurPos , SolveLines [ 0 ] , SolveLines [ 1 ] ) ;
Wedge . bHaveNewPosition = true ;
}
}
2021-11-01 12:01:36 -04:00
}
}
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 )
{
2023-12-15 14:57:40 -05:00
for ( FOneRingWedge & Wedge : Vertex . Wedges )
2021-11-01 12:01:36 -04:00
{
2023-12-15 14:57:40 -05:00
if ( Wedge . bHaveNewPosition )
2021-11-01 12:01:36 -04:00
{
Mesh . SetVertex ( Wedge . WedgeVertex , Wedge . NewPosition ) ;
}
}
}
}
void FMeshBevel : : AppendJunctionVertexPolygon ( FDynamicMesh3 & Mesh , FBevelVertex & Vertex )
{
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( Vertex . VertexType = = EBevelVertexType : : JunctionVertex ) ;
2021-11-01 12:01:36 -04:00
// 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 ;
2021-11-03 09:39:16 -04:00
int32 tid = Mesh . AppendTriangle ( A , B , C , Vertex . NewGroupID ) ;
2021-11-01 12:01:36 -04:00
if ( Mesh . IsTriangle ( tid ) )
{
Vertex . NewTriangles . Add ( tid ) ;
}
}
}
void FMeshBevel : : AppendTerminatorVertexTriangle ( FDynamicMesh3 & Mesh , FBevelVertex & Vertex )
{
2022-06-24 09:42:08 -04:00
MESH_BEVEL_DEBUG_CHECK ( Vertex . VertexType = = EBevelVertexType : : TerminatorVertex ) ;
2021-11-01 12:01:36 -04:00
// 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 ) ;
}
2022-09-08 20:09:26 -04:00
else
{
MESH_BEVEL_DEBUG_CHECK ( false ) ;
}
2021-11-01 12:01:36 -04:00
}
2022-09-08 20:09:26 -04:00
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 ) ;
2021-11-01 12:01:36 -04:00
}
}
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 ) ;
2022-06-24 09:42:08 -04:00
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 ) ;
}
2021-11-01 12:01:36 -04:00
}
Edge . StripQuads . Add ( QuadTris ) ;
}
}
void FMeshBevel : : AppendLoopQuads ( FDynamicMesh3 & Mesh , FBevelLoop & Loop )
{
int32 NumEdges = Loop . MeshEdges . Num ( ) ;
if ( NumEdges ! = Loop . NewMeshEdges . Num ( ) )
{
return ;
}
2021-11-16 13:24:21 -05:00
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 ] ) ;
}
}
2021-11-01 12:01:36 -04:00
// 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 ) )
{
2021-11-16 13:24:21 -05:00
FIndex2i GroupKey = GetGroupKey ( k ) ;
int32 NewGroupID = NewGroupIDs [ GroupKey ] ;
2021-11-01 12:01:36 -04:00
FIndex2i EdgeV0 = Mesh . GetOrientedBoundaryEdgeV ( EdgeID0 ) ;
FIndex2i EdgeV1 = Mesh . GetOrientedBoundaryEdgeV ( EdgeID1 ) ;
2021-11-16 13:24:21 -05:00
QuadTris . A = Mesh . AppendTriangle ( EdgeV0 . B , EdgeV0 . A , EdgeV1 . B , NewGroupID ) ;
QuadTris . B = Mesh . AppendTriangle ( EdgeV1 . B , EdgeV1 . A , EdgeV0 . B , NewGroupID ) ;
2021-11-01 12:01:36 -04:00
}
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 ) ;
}
2022-09-08 20:09:26 -04:00
// easier to do terminators last so that we can use quad edge to orient the triangle
TSet < FIndex2i > HandledQuadVtxPairs ;
2021-11-01 12:01:36 -04:00
for ( FBevelVertex & Vertex : Vertices )
{
if ( Vertex . VertexType = = EBevelVertexType : : TerminatorVertex )
{
2022-09-08 20:09:26 -04:00
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 ) ;
}
2021-11-01 12:01:36 -04:00
}
}
}
2021-11-04 18:24:07 -04:00
2023-12-12 17:55:33 -05:00
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 ;
} ;
2024-02-01 15:29:23 -05:00
// 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
2024-02-02 17:17:23 -05:00
if ( SequentialQuadEdges . Num ( ) > 1 & & SequentialQuadEdges [ 0 ] . EdgeV0 . B = = SequentialQuadEdges [ 1 ] . EdgeV0 . A )
2024-02-01 15:29:23 -05:00
{
Algo : : Reverse ( SequentialQuadEdges ) ;
}
2023-12-12 17:55:33 -05:00
2024-02-01 15:29:23 -05:00
// SequentialQuadEdges ordering may not be in agreement with the [BevelVertices.A, BevelVertices.B]
// ordering of the BevelEdge. If so, then the Prev/Next QuadGridPatch
2023-12-12 17:55:33 -05:00
// 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 ;
}
2024-02-02 17:17:23 -05:00
// 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 ) ;
2023-12-12 17:55:33 -05:00
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 ;
2024-02-02 17:17:23 -05:00
if ( bEdgeHasReverseOrder )
{
EdgeV0 . Swap ( ) ;
EdgeV1 . Swap ( ) ;
}
2023-12-12 17:55:33 -05:00
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 ) ;
2024-02-02 17:17:23 -05:00
// Offsets for flipping triangles in cases where the edges had the opposite ordering from our expectation
int32 SwapOffset0 = ( int32 ) ( bEdgeHasReverseOrder ) ;
int32 SwapOffset1 = ( int32 ) ( ! bEdgeHasReverseOrder ) ;
2023-12-12 17:55:33 -05:00
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 ) ;
2024-02-02 17:17:23 -05:00
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 ) ;
2023-12-12 17:55:33 -05:00
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 ;
} ;
2024-05-02 17:24:58 -04:00
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 ;
}
2023-12-12 17:55:33 -05:00
// 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
2023-12-15 14:57:40 -05:00
// 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 ] ) ;
}
}
2023-12-12 17:55:33 -05:00
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 ] ;
2023-12-15 14:57:40 -05:00
// 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 ) ;
2023-12-12 17:55:33 -05:00
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 ;
2023-12-15 14:57:40 -05:00
bool bPatchIsFlippedX = false ;
2023-12-12 17:55:33 -05:00
int32 NumCols = Patch . NumVertexCols ( ) ;
for ( int32 Col = 0 ; Col < NumCols ; + + Col )
{
TArray < int32 > ColVerts ;
Patch . GetVertexColumn ( Col , ColVerts ) ;
2023-12-15 14:57:40 -05:00
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 ;
}
2023-12-12 17:55:33 -05:00
int32 A = ColVerts [ 0 ] ;
int32 B = ColVerts . Last ( ) ;
FVector3d PosA = Mesh . GetVertex ( A ) ;
FVector3d PosB = Mesh . GetVertex ( B ) ;
2023-12-15 14:57:40 -05:00
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 )
2023-12-12 17:55:33 -05:00
{
2023-12-15 14:57:40 -05:00
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 ;
}
2023-12-12 17:55:33 -05:00
}
2023-12-15 14:57:40 -05:00
FVector3d InitialPosition = Edge . InitialPositions [ UseEdgeVertIndex ] ;
FVector3d Direction1 = Normalized ( PosA - InitialPosition ) ;
FVector3d Direction2 = Normalized ( PosB - InitialPosition ) ;
FVector3d SectionPlaneNormal = Normalized ( Cross ( Direction1 , Direction2 ) ) ;
if ( ! bInferTangentPlaneFromInitialPosition )
2023-12-12 17:55:33 -05:00
{
2023-12-15 14:57:40 -05:00
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 ( ) ;
}
2023-12-12 17:55:33 -05:00
}
2023-12-15 14:57:40 -05:00
// project normals onto section plane
NormalA = Normalized ( NormalA - NormalA . Dot ( SectionPlaneNormal ) * SectionPlaneNormal ) ;
NormalB = Normalized ( NormalB - NormalB . Dot ( SectionPlaneNormal ) * SectionPlaneNormal ) ;
2023-12-12 17:55:33 -05:00
// 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 ;
2023-12-15 14:57:40 -05:00
CurveVerts . SetNum ( NumColVerts ) ;
CurveVerts [ 0 ] = PosA ; CurveVerts [ NumColVerts - 1 ] = PosB ;
for ( int32 k = 1 ; k < ( NumColVerts - 1 ) ; + + k )
2023-12-12 17:55:33 -05:00
{
int32 VID = ColVerts [ k ] ;
FVector3d Pos = Mesh . GetVertex ( VID ) ;
2023-12-15 14:57:40 -05:00
double T = ( double ) k / ( double ) ( NumColVerts - 1 ) ;
2023-12-12 17:55:33 -05:00
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??)
2023-12-15 14:57:40 -05:00
for ( int32 k = 1 ; k < ( NumColVerts - 1 ) ; + + k )
2023-12-12 17:55:33 -05:00
{
int32 VID = ColVerts [ k ] ;
FVector3d CurveEdgeNormal = FMeshNormals : : ComputeVertexNormal ( Mesh , VID ,
2023-12-12 18:09:03 -05:00
[ & ] ( int32 TriangleID ) { return Mesh . GetTriangleGroup ( TriangleID ) = = Edge . NewGroupID ; } , true , true ) ;
2023-12-12 17:55:33 -05:00
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 ) ;
}
}
}
2021-11-01 12:01:36 -04:00
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 )
{
2021-11-04 18:24:07 -04:00
UELocal : : QuadsToTris ( Mesh , Edge . StripQuads , TriList , true ) ;
2021-11-01 12:01:36 -04:00
SetNormalsOnTriRegion ( TriList ) ;
}
for ( FBevelLoop & Loop : Loops )
{
2021-11-04 18:24:07 -04:00
UELocal : : QuadsToTris ( Mesh , Loop . StripQuads , TriList , true ) ;
2021-11-01 12:01:36 -04:00
SetNormalsOnTriRegion ( TriList ) ;
}
}
2021-11-04 18:24:07 -04:00
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 )
{
2021-12-16 19:34:36 -05:00
int32 NbrTriangleID = TriNbrs [ j ] ;
if ( Mesh . IsTriangle ( NbrTriangleID ) = = false | | Triangles . Contains ( NbrTriangleID ) )
2021-11-04 18:24:07 -04:00
{
continue ;
}
2021-12-16 19:34:36 -05:00
int MatID = MaterialAttrib . GetValue ( NbrTriangleID ) ;
2021-11-04 18:24:07 -04:00
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 ) ;
}
}
}
2021-11-16 13:24:21 -05:00
}