using HalfEdgeMesh;
namespace Sandbox;
///
/// An editable mesh made up of polygons, triangulated into a model
///
[Expose]
public sealed partial class PolygonMesh : IJsonConvert
{
private HalfEdgeMesh.Mesh Topology { get; init; } = new();
private readonly List _triangleFaces = new();
private readonly List _meshIndices = new();
private readonly List _meshVertices = new();
private readonly Dictionary _meshFaces = new();
private readonly Dictionary _materialsById = new();
private readonly Dictionary _materialIdsByName = new();
private int _materialId = 0;
private float _smoothingThreshold;
[Expose]
public enum EdgeSmoothMode
{
Default,
Hard,
Soft
}
private struct FaceMesh
{
public int VertexStart;
public int IndexStart;
public int VertexCount;
public int IndexCount;
}
private class Submesh
{
public List Vertices { get; init; } = new();
public List Indices { get; init; } = new();
public List UvDensity { get; set; } = new();
public Material Material { get; set; }
}
private struct FaceData
{
public Vector3 TextureUAxis { get; set; }
public Vector3 TextureVAxis { get; set; }
public Vector2 TextureScale { get; set; }
public Vector2 TextureOffset { get; set; }
public int MaterialId { get; set; }
}
private static readonly Material DefaultMaterial = Material.Load( "materials/dev/reflectivity_30.vmat" );
private static readonly Vector2 DefaultTextureSize = CalculateTextureSize( DefaultMaterial );
private VertexData Positions { get; init; }
private HalfEdgeData TextureCoord { get; init; }
private FaceData TextureUAxis { get; init; }
private FaceData TextureVAxis { get; init; }
private FaceData TextureScale { get; init; }
private FaceData TextureOffset { get; init; }
private FaceData MaterialIndex { get; init; }
private HalfEdgeData EdgeSmoothing { get; init; }
private HalfEdgeData EdgeFlags { get; init; }
private FaceData TextureOriginUnused { get; init; }
private FaceData TextureRotationUnused { get; init; }
private FaceData TextureAngleUnused { get; init; }
public VertexHandle VertexHandleFromIndex( int index ) => new( index, Topology );
public HalfEdgeHandle HalfEdgeHandleFromIndex( int index ) => new( index, Topology );
public FaceHandle FaceHandleFromIndex( int index ) => new( index, Topology );
public void MergeMesh( PolygonMesh sourceMesh, Transform transform,
out Dictionary newVertices,
out Dictionary newHalfEdges,
out Dictionary newFaces )
{
Topology.AppendComponentsFromMesh( sourceMesh.Topology,
out newVertices, out newHalfEdges, out newFaces );
foreach ( var pair in newVertices )
{
Positions[pair.Value] = transform.PointToWorld( sourceMesh.Positions[pair.Key] );
}
foreach ( var pair in newHalfEdges )
{
TextureCoord[pair.Value] = sourceMesh.TextureCoord[pair.Key];
EdgeSmoothing[pair.Value] = sourceMesh.EdgeSmoothing[pair.Key];
}
foreach ( var pair in newFaces )
{
TextureUAxis[pair.Value] = sourceMesh.TextureUAxis[pair.Key];
TextureVAxis[pair.Value] = sourceMesh.TextureVAxis[pair.Key];
TextureScale[pair.Value] = sourceMesh.TextureScale[pair.Key];
TextureOffset[pair.Value] = sourceMesh.TextureOffset[pair.Key];
SetFaceMaterial( pair.Value, sourceMesh.GetFaceMaterial( pair.Key ) );
}
IsDirty = true;
}
internal int GetFaceMaterialIndex( FaceHandle hFace )
{
return MaterialIndex[hFace];
}
private void SetFaceMaterialIndex( FaceHandle hFace, int materialIndex )
{
MaterialIndex[hFace] = materialIndex;
}
internal IEnumerable Materials => Enumerable.Range( 0, _materialsById.Count )
.Select( x => _materialsById[x] );
internal IEnumerable GetFaceVertexNormals()
{
foreach ( var hFace in Topology.FaceHandles )
{
ComputeFaceNormal( hFace, out var normal );
var vertexCount = Topology.ComputeNumEdgesInFace( hFace );
for ( var i = 0; i < vertexCount; ++i )
yield return normal;
}
}
internal IEnumerable GetFaceVertexTexCoords()
{
foreach ( var hFace in Topology.FaceHandles )
{
GetFaceVerticesConnectedToFace( hFace, out var hEdges );
foreach ( var hEdge in hEdges )
yield return TextureCoord[hEdge];
}
}
///
/// Has there been changes to the mesh that need rebuilding?
///
public bool IsDirty { get; private set; }
private Transform _transform = Transform.Zero;
///
/// Where is the mesh in worldspace.
///
public Transform Transform
{
get => _transform;
set
{
if ( _transform == value )
return;
_transform = value;
ComputeFaceTextureParametersFromCoordinates();
IsDirty = true;
}
}
public PolygonMesh()
{
Positions = Topology.CreateVertexData( nameof( Positions ) );
TextureCoord = Topology.CreateHalfEdgeData( nameof( TextureCoord ) );
TextureUAxis = Topology.CreateFaceData( nameof( TextureUAxis ) );
TextureVAxis = Topology.CreateFaceData( nameof( TextureVAxis ) );
TextureScale = Topology.CreateFaceData( nameof( TextureScale ) );
TextureOffset = Topology.CreateFaceData( nameof( TextureOffset ) );
MaterialIndex = Topology.CreateFaceData( nameof( MaterialIndex ) );
EdgeSmoothing = Topology.CreateHalfEdgeData( nameof( EdgeSmoothing ) );
EdgeFlags = Topology.CreateHalfEdgeData( nameof( EdgeFlags ) );
TextureOriginUnused = Topology.CreateFaceData( nameof( TextureOriginUnused ) );
TextureRotationUnused = Topology.CreateFaceData( nameof( TextureRotationUnused ) );
TextureAngleUnused = Topology.CreateFaceData( nameof( TextureAngleUnused ) );
}
///
/// Called on serialize, goes through and cleans up any unused materials, and remaps material indices
///
private void CleanupUnusedMaterials()
{
var usedIds = new HashSet();
foreach ( var face in Topology.FaceHandles )
{
int id = MaterialIndex[face];
if ( id >= 0 ) usedIds.Add( id );
}
if ( usedIds.Count == _materialsById.Count )
return;
var remap = new int[_materialsById.Count];
var newMaterials = new Dictionary( usedIds.Count );
int newId = 0;
foreach ( var oldId in usedIds )
{
remap[oldId] = newId;
newMaterials[newId] = _materialsById[oldId];
newId++;
}
foreach ( var face in Topology.FaceHandles )
{
int oldId = MaterialIndex[face];
if ( oldId >= 0 )
MaterialIndex[face] = remap[oldId];
}
_materialsById.Clear();
_materialIdsByName.Clear();
foreach ( var kv in newMaterials )
{
_materialsById[kv.Key] = kv.Value;
_materialIdsByName[kv.Value.Name] = kv.Key;
}
_materialId = newId;
IsDirty = true;
}
///
/// All of the vertex handles being used
///
public IEnumerable VertexHandles => Topology.VertexHandles;
///
/// All of the face handles being used
///
public IEnumerable FaceHandles => Topology.FaceHandles;
///
/// All of the half edge handles being used
///
public IEnumerable HalfEdgeHandles => Topology.HalfEdgeHandles;
///
/// Add a vertex to the topology
///
public VertexHandle AddVertex( Vector3 position )
{
var hVertex = Topology.AddVertex();
Positions[hVertex] = position;
IsDirty = true;
return hVertex;
}
///
/// Add multiple vertices to the topology
///
public VertexHandle[] AddVertices( params Vector3[] positions )
{
if ( positions.Length <= 0 )
return default;
var hVertices = Topology.AddVertices( positions.Length ).ToArray();
for ( int i = 0; i < positions.Length; i++ )
Positions[hVertices[i]] = positions[i];
IsDirty = true;
return hVertices;
}
///
/// Connect these vertices to make a face
///
public FaceHandle AddFace( params VertexHandle[] hVertices )
{
var hFace = Topology.AddFace( hVertices );
if ( !hFace.IsValid )
return hFace;
MaterialIndex[hFace] = -1;
IsDirty = true;
return hFace;
}
///
/// Calculate bounds of all vertices
///
public BBox CalculateBounds()
{
return BBox.FromPoints( GetVertexPositions() );
}
///
/// Calculate bounds of all transformed vertices
///
public BBox CalculateBounds( Transform transform )
{
return BBox.FromPoints( GetVertexPositions().Select( x => transform.PointToWorld( x ) ) );
}
///
/// Scale all vertices
///
public void Scale( Vector3 scale )
{
foreach ( var hVertex in Topology.VertexHandles )
{
Positions[hVertex] = Positions[hVertex] * scale;
}
ComputeFaceTextureCoordinatesFromParameters();
IsDirty = true;
}
///
/// Assign a material to a face
///
public void SetFaceMaterial( FaceHandle hFace, Material material )
{
var id = AddMaterial( material );
if ( id == MaterialIndex[hFace] )
return;
MaterialIndex[hFace] = id;
IsDirty = true;
}
///
/// Assign a material to a face
///
public void SetFaceMaterial( FaceHandle hFace, string material )
{
SetFaceMaterial( hFace, Material.Load( material ) );
}
///
/// Get a material a face is using
///
public Material GetFaceMaterial( FaceHandle hFace )
{
return GetMaterial( MaterialIndex[hFace] );
}
///
/// Get the smoothing of this edge
///
public EdgeSmoothMode GetEdgeSmoothing( HalfEdgeHandle hEdge )
{
return (EdgeSmoothMode)EdgeFlags[hEdge];
}
///
/// Set the smoothing of this edge
///
public void SetEdgeSmoothing( HalfEdgeHandle hEdge, EdgeSmoothMode mode )
{
EdgeFlags[hEdge] = (int)mode;
IsDirty = true;
}
public void SetSmoothingAngle( float smoothingAngle )
{
var threshold = MathF.Cos( MathF.Min( smoothingAngle, 180.0f ).DegreeToRadian() );
if ( threshold != _smoothingThreshold )
{
_smoothingThreshold = threshold;
IsDirty = true;
}
}
///
/// Convert a triangle index to a face handle
///
public FaceHandle TriangleToFace( int triangle )
{
if ( triangle < 0 || triangle >= _triangleFaces.Count )
return FaceHandle.Invalid;
var hFace = _triangleFaces[triangle];
if ( !hFace.IsValid )
return FaceHandle.Invalid;
return hFace;
}
///
/// Extrude multiple faces along an offset
///
public void ExtrudeFaces( FaceHandle[] faces, out List newFaces, out List connectingFaces, Vector3 offset = default )
{
BevelFaces( faces, out newFaces, out connectingFaces, true, offset );
}
///
/// Detatch multiple faces
///
public void DetachFaces( FaceHandle[] faces, out List newFaces )
{
BevelFaces( faces, out newFaces, out _, false );
}
private void BevelFaces( FaceHandle[] faces, out List newFaces, out List connectingFaces, bool createConnectingFaces, Vector3 offset = default )
{
var numFaces = faces.Length;
var faceDataIndices = new int[numFaces];
var numTotalFaceDataSamples = 0;
for ( var i = 0; i < numFaces; ++i )
{
faceDataIndices[i] = numTotalFaceDataSamples;
numTotalFaceDataSamples += Topology.ComputeNumEdgesInFace( faces[i] );
}
var vertexPositions = new Vector3[numTotalFaceDataSamples];
var faceData = new FaceData[numFaces];
for ( var i = 0; i < numFaces; ++i )
{
var hFace = faces[i];
faceData[i] = new FaceData
{
TextureUAxis = TextureUAxis[hFace],
TextureVAxis = TextureVAxis[hFace],
TextureScale = TextureScale[hFace],
TextureOffset = TextureOffset[hFace],
MaterialId = MaterialIndex[hFace],
};
var hStartFaceVertex = Topology.GetFirstEdgeInFaceLoop( hFace );
var hCurrentFaceVertex = hStartFaceVertex;
var vertexIndex = 0;
do
{
var hCurrentVertex = Topology.GetEndVertexConnectedToEdge( hCurrentFaceVertex );
var nDstDataIndex = faceDataIndices[i] + vertexIndex;
vertexPositions[nDstDataIndex] = GetVertexPosition( hCurrentVertex );
hCurrentFaceVertex = Topology.GetNextEdgeInFaceLoop( hCurrentFaceVertex );
++vertexIndex;
}
while ( hCurrentFaceVertex != hStartFaceVertex );
}
if ( !Topology.BevelFaces( faces, faces.Length, createConnectingFaces,
out newFaces,
out connectingFaces,
out var connectingTargetFaces,
out var connectingOriginFaces ) )
{
return;
}
for ( var i = 0; i < numFaces; ++i )
{
var hNewFace = newFaces[i];
if ( !hNewFace.IsValid )
continue;
var newFaceData = faceData[i];
TextureUAxis[hNewFace] = newFaceData.TextureUAxis;
TextureVAxis[hNewFace] = newFaceData.TextureVAxis;
TextureScale[hNewFace] = newFaceData.TextureScale;
TextureOffset[hNewFace] = newFaceData.TextureOffset;
MaterialIndex[hNewFace] = newFaceData.MaterialId;
var vertexIndex = 0;
var hStartFaceVertex = Topology.GetFirstEdgeInFaceLoop( hNewFace );
var hCurrentFaceVertex = hStartFaceVertex;
do
{
var hCurrentVertex = Topology.GetEndVertexConnectedToEdge( hCurrentFaceVertex );
var nSrcDataIndex = faceDataIndices[i] + vertexIndex;
SetVertexPosition( hCurrentVertex, vertexPositions[nSrcDataIndex] + offset );
hCurrentFaceVertex = Topology.GetNextEdgeInFaceLoop( hCurrentFaceVertex );
++vertexIndex;
}
while ( hCurrentFaceVertex != hStartFaceVertex );
}
ComputeFaceTextureCoordinatesFromParameters( newFaces );
var numConnectingFaces = connectingFaces.Count;
Assert.True( connectingOriginFaces.Count == numConnectingFaces );
for ( var i = 0; i < numConnectingFaces; ++i )
{
var hConnectingFace = connectingFaces[i];
var hOriginFace = connectingOriginFaces[i];
if ( !hOriginFace.IsValid )
hOriginFace = connectingTargetFaces[i];
SetFaceMaterialIndex( hConnectingFace, GetFaceMaterialIndex( hOriginFace ) );
TextureAlignToGrid( Transform, hConnectingFace );
}
IsDirty = true;
}
public bool ExtendEdges( IReadOnlyList edges, float amount, out List newEdges, out List newFaces )
{
newFaces = new();
if ( !Topology.ExtendEdges( edges, edges.Count, out newEdges, out var originalEdges, out _, out _ ) )
return false;
if ( originalEdges.Count == newEdges.Count )
{
int nNumEdges = newEdges.Count;
newFaces = new List( nNumEdges );
for ( int iEdge = 0; iEdge < nNumEdges; ++iEdge )
{
var hNewEdge = newEdges[iEdge];
var hOriginalEdge = originalEdges[iEdge];
Topology.GetFacesConnectedToFullEdge( hNewEdge, out var hFaceA, out var hFaceB );
var hNewFace = (hFaceA == FaceHandle.Invalid) ? hFaceB : hFaceA;
GetVerticesConnectedToEdge( hOriginalEdge, out var hVertexA, out var hVertexB );
var vVertexPositionA = Vector3.Zero;
var vVertexPositionB = Vector3.Zero;
var hOriginalFace = FaceHandle.Invalid;
hFaceA = Topology.FindFaceWithEdgeConnectingVertices( hVertexA, hVertexB );
hFaceB = Topology.FindFaceWithEdgeConnectingVertices( hVertexB, hVertexA );
if ( hFaceA == hNewFace )
{
vVertexPositionA = GetVertexPosition( hVertexA );
vVertexPositionB = GetVertexPosition( hVertexB );
hOriginalFace = hFaceB;
}
else if ( hFaceB == hNewFace )
{
vVertexPositionA = GetVertexPosition( hVertexB );
vVertexPositionB = GetVertexPosition( hVertexA );
hOriginalFace = hFaceA;
}
Assert.True( hOriginalFace != FaceHandle.Invalid );
if ( hOriginalFace != FaceHandle.Invalid )
{
// Compute the position of the new edge base on the orginal face
GetFacePlane( hOriginalFace, Transform.Zero, out var facePlane );
var vEdgeDirection = vVertexPositionB - vVertexPositionA;
vEdgeDirection = vEdgeDirection.Normal;
var vExtendDirection = facePlane.Normal.Cross( vEdgeDirection );
GetVerticesConnectedToEdge( hNewEdge, out var hNewVertexA, out var hNewVertexB );
if ( Topology.FindFaceWithEdgeConnectingVertices( hNewVertexB, hNewVertexA ) != hNewFace )
{
(hNewVertexB, hNewVertexA) = (hNewVertexA, hNewVertexB);
Assert.True( Topology.FindFaceWithEdgeConnectingVertices( hNewVertexB, hNewVertexA ) == hNewFace );
}
var vNewPositionA = GetVertexPosition( hNewVertexA );
var vNewPositionB = GetVertexPosition( hNewVertexB );
var vEdgeA = vNewPositionA - vVertexPositionA;
var vEdgeB = vNewPositionB - vVertexPositionB;
vNewPositionA += (vExtendDirection * vEdgeA.Dot( vExtendDirection )) + (vExtendDirection * amount);
vNewPositionB += (vExtendDirection * vEdgeB.Dot( vExtendDirection )) + (vExtendDirection * amount);
SetVertexPosition( hNewVertexA, vNewPositionA );
SetVertexPosition( hNewVertexB, vNewPositionB );
TextureUAxis[hNewFace] = TextureUAxis[hOriginalFace];
TextureVAxis[hNewFace] = TextureVAxis[hOriginalFace];
TextureScale[hNewFace] = TextureScale[hOriginalFace];
TextureOffset[hNewFace] = TextureOffset[hOriginalFace];
MaterialIndex[hNewFace] = MaterialIndex[hOriginalFace];
newFaces.Add( hNewFace );
}
}
ComputeFaceTextureCoordinatesFromParameters( newFaces );
}
IsDirty = true;
return true;
}
private bool DissolveEdge( HalfEdgeHandle hFullEdge, out FaceHandle hOutFace )
{
if ( !Topology.DissolveEdge( hFullEdge, out hOutFace ) )
return false;
return true;
}
private void GetEdgeVertexPositions( HalfEdgeHandle hEdge, out Vector3 pOutVertexA, out Vector3 pOutVertexB )
{
if ( !hEdge.IsValid )
{
pOutVertexA = default;
pOutVertexB = default;
return;
}
Topology.GetVerticesConnectedToFullEdge( hEdge, out var hVertexA, out var hVertexB );
pOutVertexA = GetVertexPosition( hVertexA );
pOutVertexB = GetVertexPosition( hVertexB );
}
private bool AreEdgesCoLinear( HalfEdgeHandle hEdgeA, HalfEdgeHandle hEdgeB, float flAngleToleranceInDegrees )
{
float flTolerance = MathF.Cos( MathF.Min( flAngleToleranceInDegrees, 180.0f ).DegreeToRadian() );
if ( (hEdgeA == HalfEdgeHandle.Invalid) || (hEdgeB == HalfEdgeHandle.Invalid) )
return false;
GetEdgeVertexPositions( hEdgeA, out var vPositionA1, out var vPositionA2 );
GetEdgeVertexPositions( hEdgeB, out var vPositionB1, out var vPositionB2 );
var vEdgeA = vPositionA2 - vPositionA1;
vEdgeA = vEdgeA.Normal;
var vEdgeB = vPositionB2 - vPositionB1;
vEdgeB = vEdgeB.Normal;
float flCosAngle = MathF.Abs( vEdgeA.Dot( vEdgeB ) );
return flCosAngle > flTolerance;
}
public void CombineFaces( IReadOnlyList faces )
{
FindEdgesConnectedToFaces( faces, faces.Count, out var connectedEdges, out var edgeFaceCounts );
connectedEdges = connectedEdges
.Where( ( edge, i ) => edgeFaceCounts[i] >= 2 )
.ToArray();
DissolveEdges( connectedEdges, true, DissolveRemoveVertexCondition.Colinear );
IsDirty = true;
}
public void DissolveEdges( IReadOnlyList edges, bool bFaceMustBePlanar, DissolveRemoveVertexCondition removeCondition )
{
const float flColinearTolerance = 5.0f; // Edges may be at an angle of up to this many degrees and still be considered co-linear
const float flPlanarTolerance = 0.01f;
var nNumEdges = edges.Count;
var verticesToRemove = new List( nNumEdges );
var combinedFaces = new List( nNumEdges );
for ( var iEdge = 0; iEdge < nNumEdges; ++iEdge )
{
var hEdge = edges[iEdge];
// Get the two faces connected to the edge, if the faces are not in the same plane then any two
// edge vertices will left behind will be removed to prevent the creation of a non-planar polygon.
Topology.GetFacesConnectedToFullEdge( hEdge, out var hFaceA, out var hFaceB );
if ( (hFaceA == FaceHandle.Invalid) || (hFaceB == FaceHandle.Invalid) )
continue;
ComputeFaceNormal( hFaceA, out var planeA );
ComputeFaceNormal( hFaceB, out var planeB );
float flFaceAngle = planeA.Normal.Dot( planeB.Normal );
if ( bFaceMustBePlanar && (flFaceAngle < (1.0f - flPlanarTolerance)) )
continue;
// Get the vertices connected to the edge
Topology.GetVerticesConnectedToFullEdge( hEdge, out var hVertexA, out var hVertexB );
// Dissolve the edge
DissolveEdge( hEdge, out var hFace );
// Determine if the vertices at the ends of the edge should be removed. A vertex should be
// removed if after the edge is dissolved it has only 2 edges connected to it and it passes
// the specified removal criteria. Note the vertices are not removed here, but are placed
// in a list of vertices to be removed, this is because removing the vertices might result
// in removing one of the edges in the list of edges to be dissolved.
if ( ShouldDissolveRemoveVertex( hVertexA, removeCondition, flColinearTolerance ) )
{
verticesToRemove.Add( hVertexA );
}
if ( ShouldDissolveRemoveVertex( hVertexB, removeCondition, flColinearTolerance ) )
{
verticesToRemove.Add( hVertexB );
}
combinedFaces.Add( hFace );
}
// Now that all of the edges have been dissolved remove all of the vertices that were
// determined should be removed while dissolving the edges.
int nNumVerticesToRemove = verticesToRemove.Count;
for ( int iVertex = 0; iVertex < nNumVerticesToRemove; ++iVertex )
{
Topology.RemoveVertex( verticesToRemove[iVertex], true );
}
// Make sure there are no remaining co-linear edges in the face, this may happen if removing
// the edge cause there to be a loose edge in the face that is subsequently removed.
if ( removeCondition != DissolveRemoveVertexCondition.None )
{
int nNumFaces = combinedFaces.Count;
for ( int iFace = 0; iFace < nNumFaces; ++iFace )
{
RemoveVerticesFromColinearEdgesInFace( combinedFaces[iFace], flColinearTolerance );
}
}
IsDirty = true;
}
private void RemoveVerticesFromColinearEdgesInFace( FaceHandle hFace, float flColinearAngleTolerance )
{
// Get all of the vertices in the face
Topology.GetVerticesConnectedToFace( hFace, out var verticesInFace );
if ( verticesInFace is null || verticesInFace.Length == 0 )
return;
// Iterate over all of the vertices connected to the face, find the ones which are only connected
// to two edges and determine if those two edges are co-linear, if so remove the vertex.
int nNumVertices = verticesInFace.Length;
for ( var iVertex = 0; iVertex < nNumVertices; ++iVertex )
{
RemoveColinearVertex( verticesInFace[iVertex], flColinearAngleTolerance );
}
}
private bool RemoveColinearVertex( VertexHandle hVertex, float flColinearAngleTolerance )
{
Topology.GetFullEdgesConnectedToVertex( hVertex, out var edgesConnectedToVertex );
if ( edgesConnectedToVertex.Count == 2 )
{
if ( AreEdgesCoLinear( edgesConnectedToVertex[0], edgesConnectedToVertex[1], flColinearAngleTolerance ) )
{
// Remove the vertex, combining the two edges into a single edge
if ( Topology.RemoveVertex( hVertex, true ) )
return true;
}
}
return false;
}
public enum DissolveRemoveVertexCondition
{
None, // Never remove vertices
Colinear, // Remove vertices with only 2 edges attached that are colinear
InteriorOrColinear, // Remove vertices with only 2 edges attached that are interior edges (not open) or are colinear
All // Remove all vertices with only 2 edges attached
};
private bool ShouldDissolveRemoveVertex( VertexHandle hVertex, DissolveRemoveVertexCondition removeCondition, float flColinearTolerance )
{
if ( removeCondition == DissolveRemoveVertexCondition.None )
return false;
if ( Topology.ComputeNumEdgesConnectedToVertex( hVertex ) != 2 )
return false;
Topology.GetFullEdgesConnectedToVertex( hVertex, out var connectedEdges );
var bInterior = !IsEdgeOpen( connectedEdges[0] );
var bColinear = AreEdgesCoLinear( connectedEdges[0], connectedEdges[1], flColinearTolerance );
return removeCondition switch
{
DissolveRemoveVertexCondition.InteriorOrColinear => bInterior || bColinear,
DissolveRemoveVertexCondition.Colinear => bColinear,
DissolveRemoveVertexCondition.All => true,
_ => false,
};
}
private bool AddFace( HalfEdgeHandle hOpenEdge, out FaceHandle hOutFace )
{
hOutFace = FaceHandle.Invalid;
var hStartHalfEdge = Topology.GetHalfEdgeForFaceEdge( FaceHandle.Invalid, hOpenEdge );
if ( !hStartHalfEdge.IsValid )
return false;
// Count the number of edges in the loop
int numEdgesInLoop = 0;
var hCurrentEdge = hStartHalfEdge;
do
{
++numEdgesInLoop;
hCurrentEdge = Topology.GetNextEdgeInFaceLoop( hCurrentEdge );
}
while ( hCurrentEdge != hStartHalfEdge );
// Construct the list of vertices
var hVertices = new VertexHandle[numEdgesInLoop];
var nVertexCount = 0;
hCurrentEdge = hStartHalfEdge;
do
{
hVertices[nVertexCount++] = Topology.GetEndVertexConnectedToEdge( hCurrentEdge );
hCurrentEdge = Topology.GetNextEdgeInFaceLoop( hCurrentEdge );
}
while ( hCurrentEdge != hStartHalfEdge );
// Create the face using the vertices
hOutFace = AddFace( hVertices );
return hOutFace.IsValid;
}
private HalfEdgeHandle FindEdgeConnectingVertices( VertexHandle hVertexA, VertexHandle hVertexB )
{
return Topology.FindFullEdgeConnectingVertices( hVertexA, hVertexB );
}
public bool MergeEdges( HalfEdgeHandle hEdgeA, HalfEdgeHandle hEdgeB, out HalfEdgeHandle hOutNewEdge )
{
hOutNewEdge = HalfEdgeHandle.Invalid;
if ( !Topology.GetEdgeMergeVertexPairs( hEdgeA, hEdgeB, out var hVertexPairA1, out var hVertexPairA2, out var hVertexPairB1, out var hVertexPairB2 ) )
return false;
// Check to see of the edges share a single vertex,
// if so just merge the other vertex instead of the edge.
var hSharedVertex = VertexHandle.Invalid;
var hMergeVertexA = VertexHandle.Invalid;
var hMergeVertexB = VertexHandle.Invalid;
if ( hVertexPairA1 == hVertexPairA2 )
{
hSharedVertex = hVertexPairA1;
hMergeVertexA = hVertexPairB1;
hMergeVertexB = hVertexPairB2;
}
else if ( hVertexPairB1 == hVertexPairB2 )
{
hSharedVertex = hVertexPairB1;
hMergeVertexA = hVertexPairA1;
hMergeVertexB = hVertexPairA2;
}
if ( hSharedVertex != VertexHandle.Invalid )
{
if ( !MergeVertices( hMergeVertexA, hMergeVertexB, 0.5f, out var hNewVertex ) )
return false;
hOutNewEdge = FindEdgeConnectingVertices( hNewVertex, hSharedVertex );
IsDirty = true;
return true;
}
// No vertices are shared between the edges, merge them.
var a = Vector3.Lerp( GetVertexPosition( hVertexPairA1 ), GetVertexPosition( hVertexPairA2 ), 0.5f );
var b = Vector3.Lerp( GetVertexPosition( hVertexPairB1 ), GetVertexPosition( hVertexPairB2 ), 0.5f );
if ( Topology.MergeEdges( hEdgeA, hEdgeB, out var hNewVertexA, out var hNewVertexB ) )
{
SetVertexPosition( hNewVertexA, a );
SetVertexPosition( hNewVertexB, b );
hOutNewEdge = FindEdgeConnectingVertices( hNewVertexA, hNewVertexB );
IsDirty = true;
return true;
}
return false;
}
private bool MergeVertices( VertexHandle hVertexA, VertexHandle hVertexB, float flParam, out VertexHandle hOutNewVertex )
{
// If there is an edge connecting the vertices, just call edge collapse so that
// the proper interpolation is done for the face vertices of the merged edge.
var hEdge = Topology.FindHalfEdgeConnectingVertices( hVertexA, hVertexB );
if ( hEdge != HalfEdgeHandle.Invalid )
{
return CollapseEdge( hEdge, flParam, out hOutNewVertex );
}
// Interpolate the data on the two vertices and store a copy before they are destroyed
var newVertex = GetVertexPosition( hVertexA ).LerpTo( GetVertexPosition( hVertexB ), flParam );
// Merge the two vertices and create a new one with
// the interpolated values of the original vertices.
if ( Topology.MergeVertices( hVertexA, hVertexB, out hOutNewVertex ) )
{
SetVertexPosition( hOutNewVertex, newVertex );
return true;
}
return false;
}
private static bool IsVertexInMesh( VertexHandle hVertex ) => hVertex is not null && hVertex.IsValid;
private static bool IsHalfEdgeInMesh( HalfEdgeHandle hHalfEdge ) => hHalfEdge is not null && hHalfEdge.IsValid;
private static bool IsFaceInMesh( FaceHandle hFace ) => hFace is not null && hFace.IsValid;
public void FlipAllFaces()
{
Topology.FlipAllFaces();
ComputeFaceTextureCoordinatesFromParameters();
IsDirty = true;
}
public int MergeVerticesWithinDistance( IReadOnlyList originalVertices, float flMaxDistance, bool bPreConnect, bool bAveragePositions, out List pOutFinalVertices )
{
pOutFinalVertices = new();
var bUseDistance = (flMaxDistance >= 0.0f);
var vDistance = new Vector3( flMaxDistance, flMaxDistance, flMaxDistance );
var nMaxIterations = 10;
var flMaxDistSq = bUseDistance ? (flMaxDistance * flMaxDistance) : float.MaxValue;
var verticesToMerge = new List();
verticesToMerge.EnsureCapacity( originalVertices.Count );
foreach ( var hVertex in originalVertices )
{
if ( IsVertexInMesh( hVertex ) )
{
verticesToMerge.Add( hVertex );
}
}
int nNumOriginalVertices = verticesToMerge.Count;
if ( nNumOriginalVertices < 2 )
return 0;
var nNumTotalVerticesMerged = 0;
pOutFinalVertices.EnsureCapacity( nNumOriginalVertices );
// Assign the vertices to groups based on their positions. Each group will contain all of the
// vertices within the specified maximum distance of the first vertex in the group.
var verticesSortedByGroup = new List( nNumOriginalVertices );
var groupVertexCounts = new int[nNumOriginalVertices];
var groupOffsets = new int[nNumOriginalVertices];
for ( int iIteration = 0; iIteration < nMaxIterations; ++iIteration )
{
// Stop if there are not at least two vertices left.
var nNumVerticesToMerge = verticesToMerge.Count;
if ( nNumVerticesToMerge < 2 )
break;
var vertexGroupAssignments = new List( nNumVerticesToMerge );
vertexGroupAssignments.AddRange( Enumerable.Repeat( -1, nNumVerticesToMerge ) );
verticesSortedByGroup.Clear();
var nNumGroups = 0;
if ( bUseDistance )
{
// Build an array of the positions specifically for the vertices
// to merge instead of all the positions in the mesh
var vertexPositions = new List( nNumVerticesToMerge );
for ( var iVertex = 0; iVertex < nNumVerticesToMerge; ++iVertex )
{
vertexPositions.Add( GetVertexPosition( verticesToMerge[iVertex] ) );
}
// Build a kd-tree of the vertex positions that we can use to find nearby vertices more efficiently
var vertexPositionTree = new VertexKDTree();
vertexPositionTree.BuildMidpoint( vertexPositions );
for ( var iVertex = 0; iVertex < nNumVerticesToMerge; ++iVertex )
{
// Check to see if the vertex has already been added to a group.
if ( vertexGroupAssignments[iVertex] >= 0 )
continue;
// If the vertex has not been assigned to a group assign it the next available group.
var hVertexA = verticesToMerge[iVertex];
var vGroupPosition = GetVertexPosition( hVertexA );
var nGroupIndex = nNumGroups++;
vertexGroupAssignments[iVertex] = nGroupIndex;
// Set the index of the start of the group in the sorted vertex array.
groupOffsets[nGroupIndex] = verticesSortedByGroup.Count;
// Add the vertex to the sorted array
verticesSortedByGroup.Add( hVertexA );
// Search the the rest of the vertices to see if there are any which have not yet been
// assigned a group that are close enough to the current vertex to be grouped with it.
var vGroupMin = vGroupPosition - vDistance;
var vGroupMax = vGroupPosition + vDistance;
var verticesInBox = vertexPositionTree.FindVertsInBox( vGroupMin, vGroupMax );
var nNumVerticesInBox = verticesInBox.Count;
// There are some cases where the behavior of merging is order dependent, ideally it
// wouldn't be, but it is due to the constraint of not being able to connect more
// than two faces at a single vertex by merging. So to maintain the same behavior
// as the old approach we need to add the vertices in the order they were supplied
// in the input list.
verticesInBox.Sort();
for ( var iVertexInBox = 0; iVertexInBox < nNumVerticesInBox; ++iVertexInBox )
{
var nVertexIndexB = verticesInBox[iVertexInBox];
var pVertexPosition = vertexPositions[nVertexIndexB];
if ( pVertexPosition.DistanceSquared( vGroupPosition ) < flMaxDistSq )
{
if ( vertexGroupAssignments[nVertexIndexB] < 0 )
{
var hVertexB = verticesToMerge[nVertexIndexB];
verticesSortedByGroup.Add( hVertexB );
vertexGroupAssignments[nVertexIndexB] = nGroupIndex;
}
}
}
// Compute the number of vertices that were assigned to the group
groupVertexCounts[nGroupIndex] = verticesSortedByGroup.Count - groupOffsets[nGroupIndex];
}
}
else
{
// If not using the distance just add all the vertices to a single group for merging
nNumGroups = 1;
verticesSortedByGroup = new( verticesToMerge );
for ( var i = 0; i < vertexGroupAssignments.Count; i++ )
{
vertexGroupAssignments[i] = 0;
}
groupVertexCounts[0] = nNumVerticesToMerge;
groupOffsets[0] = 0;
}
var groupsMergedVertexCount = new int[nNumGroups]; // Number of vertices in each group that were successfully merged
var groupsSumPosition = new Vector3[nNumGroups]; // Average position of the vertices in the group that were merged
var groupsTargetVertex = new VertexHandle[nNumGroups]; // Target vertex with which other vertices in the group should be merged.
for ( var iGroup = 0; iGroup < nNumGroups; ++iGroup )
{
var nGroupVertexOffset = groupOffsets[iGroup];
var hFirstVertex = verticesSortedByGroup[nGroupVertexOffset];
groupsMergedVertexCount[iGroup] = 1;
groupsTargetVertex[iGroup] = hFirstVertex;
groupsSumPosition[iGroup] = GetVertexPosition( hFirstVertex );
// Clear the first vertex in the group, it does not need to be merged.
verticesSortedByGroup[nGroupVertexOffset] = VertexHandle.Invalid;
}
// Merge all of the vertices in each group. Multiple iterations are done until all of the vertices
// in all of the groups have been merged or until no vertices were merged in the previous iteration.
for ( var iGroupPass = 0; iGroupPass < nNumVerticesToMerge; ++iGroupPass )
{
var nNumMerged = 0;
var nNumUnmerged = 0;
for ( var iGroup = 0; iGroup < nNumGroups; ++iGroup )
{
var nGroupVertexCount = groupVertexCounts[iGroup];
if ( nGroupVertexCount < 2 )
continue;
var nGroupVertexOffset = groupOffsets[iGroup];
var nNumUnmergedInGroup = 0;
var hTargetVertex = groupsTargetVertex[iGroup];
for ( var iVertex = 1; iVertex < nGroupVertexCount; ++iVertex )
{
var hMergeVertex = verticesSortedByGroup[nGroupVertexOffset + iVertex];
if ( IsVertexInMesh( hMergeVertex ) == false )
continue;
// Get the position of the vertex to be merged before
// merging it, which will delete the vertex.
var vMergeVertexPosition = GetVertexPosition( hMergeVertex );
// If bPreConnect is specified connect vertices that share a face allowing them to
// be merged even if they did not originally have a common edge.
if ( bPreConnect )
{
var hSharedFace = Topology.FindFaceSharedByVertices( hTargetVertex, hMergeVertex );
if ( hSharedFace != FaceHandle.Invalid )
{
AddEdgeToFace( hSharedFace, hTargetVertex, hMergeVertex, out _ );
}
}
// If averaging positions, set the merge interpolation parameter to 0.5f,
// otherwise set it to 1.0 so that the data of the merge vertex is preserved.
var flParam = bAveragePositions ? 0.5f : 1.0f;
if ( MergeVertices( hTargetVertex, hMergeVertex, flParam, out var hNewVertex ) )
{
// Add the position of the vertex to the group sum position
groupsSumPosition[iGroup] += vMergeVertexPosition;
groupsMergedVertexCount[iGroup] += 1;
// Update the merged vertex of the group
groupsTargetVertex[iGroup] = hNewVertex;
// Update the target vertex to be the new vertex since the target vertex has
// be removed, if we don't update the target, then there is no way to merge
// the remaining vertices in this pass.
hTargetVertex = hNewVertex;
// Set the original vertex in the group to invalid so we
// don't try to merge it again in subsequent passes.
verticesSortedByGroup[nGroupVertexOffset + iVertex] = VertexHandle.Invalid;
++nNumMerged;
}
else
{
++nNumUnmerged;
++nNumUnmergedInGroup;
}
}
// If all of the vertices in the group were merged mark the group as not having any
// vertices so that it is not touched in any future iterations.
if ( nNumUnmergedInGroup == 0 )
{
groupVertexCounts[iGroup] = -1;
}
}
if ( (nNumUnmerged == 0) || (nNumMerged == 0) )
break;
}
// Set the merged vertex positions to the average position
var nNumVerticesMerged = 0;
for ( var iGroup = 0; iGroup < nNumGroups; ++iGroup )
{
if ( bAveragePositions )
{
var hVertex = groupsTargetVertex[iGroup];
if ( IsVertexInMesh( hVertex ) )
{
var vAveragePosition = groupsSumPosition[iGroup] / (float)groupsMergedVertexCount[iGroup];
SetVertexPosition( hVertex, vAveragePosition );
}
}
var nNumVerticesMergedInGroup = groupsMergedVertexCount[iGroup];
if ( nNumVerticesMergedInGroup > 1 )
{
nNumVerticesMerged += nNumVerticesMergedInGroup;
}
}
// Add the merged vertices from the groups
for ( var iGroup = 0; iGroup < nNumGroups; ++iGroup )
{
if ( IsVertexInMesh( groupsTargetVertex[iGroup] ) )
{
pOutFinalVertices.Add( groupsTargetVertex[iGroup] );
}
}
// Build the remaining list of vertices to merge
verticesToMerge.Clear();
for ( var iVertex = 0; iVertex < nNumVerticesToMerge; ++iVertex )
{
if ( IsVertexInMesh( verticesSortedByGroup[iVertex] ) )
{
verticesToMerge.Add( verticesSortedByGroup[iVertex] );
}
}
nNumTotalVerticesMerged += nNumVerticesMerged;
}
// Add all of the vertices which were not merged
pOutFinalVertices.AddRange( verticesToMerge );
return nNumTotalVerticesMerged;
}
private bool CollapseEdge( HalfEdgeHandle hHalfEdgeA, float flParam, out VertexHandle pOutNewVertex )
{
var hHalfEdgeB = Topology.GetOppositeHalfEdge( hHalfEdgeA );
// Get the vertices connected to the edge and average the values
var hVertexA = Topology.GetEndVertexConnectedToEdge( hHalfEdgeB );
var hVertexB = Topology.GetEndVertexConnectedToEdge( hHalfEdgeA );
var newVertex = GetVertexPosition( hVertexA ).LerpTo( GetVertexPosition( hVertexB ), flParam );
var hEdge = Topology.GetFullEdgeForHalfEdge( hHalfEdgeA );
var bRemoved = Topology.CollapseEdge( hEdge, out pOutNewVertex, out var _ );
if ( pOutNewVertex != VertexHandle.Invalid )
{
SetVertexPosition( pOutNewVertex, newVertex );
}
return bRemoved;
}
///
/// Add a face filling in the open edge loop specified by the provided edge
///
public bool CreateFaceInEdgeLoop( HalfEdgeHandle hOpenEdge, out FaceHandle hNewFace )
{
hNewFace = FaceHandle.Invalid;
// Find the face to which the new face will be connected
Topology.GetFacesConnectedToFullEdge( hOpenEdge, out var hFaceA, out var hFaceB );
var hSourceFace = hFaceA.IsValid ? hFaceA : hFaceB;
if ( !hSourceFace.IsValid )
return false;
// Try to add a face for the specified open edge. If the edge is not open no face will be
// created. Creation of the face may also fail if it would create invalid topology.
if ( AddFace( hOpenEdge, out hNewFace ) )
{
SetFaceMaterialIndex( hNewFace, GetFaceMaterialIndex( hSourceFace ) );
TextureAlignToGrid( Transform, hNewFace );
return true;
}
return false;
}
///
/// Get the face connected to this half edge
///
public FaceHandle GetHalfEdgeFace( HalfEdgeHandle hEdge )
{
return hEdge.Face;
}
///
/// Determine if the specified edge is open (only has one connected face)
///
public bool IsEdgeOpen( HalfEdgeHandle hEdge )
{
return Topology.IsFullEdgeOpen( hEdge );
}
///
/// Find all of the edges in the ring with the specified edge. An edge ring is the set of edges that
/// are connected by a loop of faces.
///
public void FindEdgeRing( HalfEdgeHandle hEdge, out List outEdgeList )
{
Topology.FindEdgeRing( hEdge, out outEdgeList );
}
///
/// Bridge two edges (create a face connecting them). The edges must both be open and belong to
/// different faces.
///
public bool BridgeEdges( HalfEdgeHandle hEdgeA, HalfEdgeHandle hEdgeB, out FaceHandle hOutNewFace )
{
Topology.GetFacesConnectedToFullEdge( hEdgeA, out var hFaceA, out var hFaceB );
var hSourceFace = hFaceA.IsValid ? hFaceA : hFaceB;
if ( !hSourceFace.IsValid )
{
hOutNewFace = FaceHandle.Invalid;
return false;
}
if ( !Topology.BridgeEdges( hEdgeA, hEdgeB, out hOutNewFace ) )
{
// Do the edges share a vertex, if so try to create a triangle face
var hSharedVertex = Topology.FindVertexConnectingFullEdges( hEdgeA, hEdgeB );
if ( hSharedVertex.IsValid )
{
Topology.GetVerticesConnectedToHalfEdge( hEdgeA, out var hVertexA1, out var hVertexA2 );
Topology.GetVerticesConnectedToHalfEdge( hEdgeB, out var hVertexB1, out var hVertexB2 );
var vertices = new VertexHandle[3];
vertices[0] = (hSharedVertex != hVertexA1) ? hVertexA1 : hVertexA2;
vertices[1] = hSharedVertex;
vertices[2] = (hSharedVertex != hVertexB1) ? hVertexB1 : hVertexB2;
if ( !Topology.AddFace( out hOutNewFace, vertices ) )
{
(vertices[2], vertices[0]) = (vertices[0], vertices[2]);
Topology.AddFace( out hOutNewFace, vertices );
}
}
}
if ( !hOutNewFace.IsValid )
return false;
SetFaceMaterialIndex( hOutNewFace, GetFaceMaterialIndex( hSourceFace ) );
TextureAlignToGrid( Transform, hOutNewFace );
IsDirty = true;
return true;
}
///
/// Connect the specified edges by adding a vertex to their mid point of each edge and then
/// connecting the vertices.
///
public bool ConnectEdges( IReadOnlyList edges, out List newEdges )
{
newEdges = null;
if ( edges is null || edges.Count < 2 )
return false;
var nNumEdges = edges.Count;
var edgesToCut = new List( nNumEdges );
var vertices = new List( nNumEdges );
// First build a list of the edges that should be cut, skip any which do not share a face with
// any of the other edges. This prevents adding new vertices that will not be used in a new edge.
for ( int iEdge = 0; iEdge < nNumEdges; ++iEdge )
{
for ( int jEdge = 0; jEdge < nNumEdges; ++jEdge )
{
if ( jEdge == iEdge )
continue;
var hSharedFace = Topology.FindFaceConnectingFullEdges( edges[iEdge], edges[jEdge] );
if ( hSharedFace.IsValid )
{
edgesToCut.Add( edges[iEdge] );
break;
}
}
}
// Add the vertices to the edges
int nNumEdgesToCut = edgesToCut.Count;
for ( int iEdge = 0; iEdge < nNumEdgesToCut; ++iEdge )
{
Topology.GetVerticesConnectedToHalfEdge( edgesToCut[iEdge], out var hVertexA, out var hVertexB );
AddVertexToEdge( hVertexA, hVertexB, 0.5f, out var hNewVertex );
if ( hNewVertex.IsValid )
vertices.Add( hNewVertex );
}
// Connect all of the vertices that were added
return ConnectVertices( vertices, out newEdges );
}
public bool AddVertexToEdge( VertexHandle hVertexA, VertexHandle hVertexB, float flParam, out VertexHandle pOutNewVertex )
{
pOutNewVertex = null;
var hEdge = Topology.FindHalfEdgeConnectingVertices( hVertexA, hVertexB );
if ( !hEdge.IsValid )
return false;
var hPrevEdge = Topology.FindPreviousEdgeInFaceLoop( hEdge );
var hOpposite = Topology.GetOppositeHalfEdge( hEdge );
var hOppositePrev = Topology.FindPreviousEdgeInFaceLoop( hOpposite );
// Add the new vertex to the edge, this will result in the edge being split into two edges.
if ( !Topology.AddVertexToEdge( hEdge, out var hNewVertex ) )
return false;
// Interpolate the values of the vertices to compute the value of the new vertex
InterpolateVertexData( hNewVertex, hVertexA, hVertexB, flParam );
// Interpolate the value of the face vertices connected to each face to compute the value of the
// new face vertex on each side of the edge.
var hEdgeAToNew = Topology.FindHalfEdgeConnectingVertices( hVertexA, hNewVertex );
var hEdgeNewToB = Topology.FindHalfEdgeConnectingVertices( hNewVertex, hVertexB );
InterpolateFaceVertexData( hEdgeAToNew, hPrevEdge, hEdgeNewToB, flParam );
var hEdgeBToNew = Topology.FindHalfEdgeConnectingVertices( hVertexB, hNewVertex );
var hEdgeNewToA = Topology.FindHalfEdgeConnectingVertices( hNewVertex, hVertexA );
InterpolateFaceVertexData( hEdgeBToNew, hEdgeNewToA, hOppositePrev, flParam );
pOutNewVertex = hNewVertex;
return true;
}
public bool RemoveVertex( VertexHandle hVertex, bool removeFreeVerts )
{
return Topology.RemoveVertex( hVertex, removeFreeVerts );
}
private bool AddVertexToEdgeAtDistance( VertexHandle hVertexA, VertexHandle hVertexB, float distance, float maxParam, out VertexHandle newVertex )
{
var edgeLength = GetVertexPosition( hVertexA ).Distance( GetVertexPosition( hVertexB ) );
var param = maxParam;
if ( edgeLength > 0.000001f )
{
param = MathF.Min( distance / edgeLength, maxParam );
}
if ( param >= 1.0f )
{
newVertex = hVertexB;
return true;
}
param = 1.0f - param;
return AddVertexToEdge( hVertexB, hVertexA, param, out newVertex );
}
private bool BevelVertex( VertexHandle hVertex, bool replaceVertexWithFace, float distance, out VertexHandle[] hNewVertices, out FaceHandle hNewFace )
{
Topology.GetVerticesConnectedToVertexByEdge( hVertex, out var connectedVertices );
var nNumVertices = connectedVertices.Count;
hNewVertices = new VertexHandle[nNumVertices];
var connectedFaces = new FaceHandle[nNumVertices];
for ( var iVertex = 0; iVertex < nNumVertices; ++iVertex )
{
var hHalfEdge = Topology.FindHalfEdgeConnectingVertices( hVertex, connectedVertices[iVertex] );
connectedFaces[iVertex] = Topology.GetFaceConnectedToHalfEdge( hHalfEdge );
AddVertexToEdgeAtDistance( hVertex, connectedVertices[iVertex], distance, 1.0f, out hNewVertices[iVertex] );
}
for ( int iVertex = 0, iPrevVertex = nNumVertices - 1; iVertex < nNumVertices; iPrevVertex = iVertex++ )
{
AddEdgeToFace( connectedFaces[iVertex], hNewVertices[iVertex], hNewVertices[iPrevVertex], out _ );
}
hNewFace = FaceHandle.Invalid;
if ( replaceVertexWithFace )
{
var hVerticesForFace = new VertexHandle[nNumVertices];
for ( var i = 0; i < nNumVertices; ++i )
{
hVerticesForFace[i] = hNewVertices[nNumVertices - 1 - i];
}
RemoveVertex( hVertex, true );
hNewFace = AddFace( hVerticesForFace );
}
IsDirty = true;
return true;
}
public bool BevelVertices( IReadOnlyList vertices, float distance, out List newVertices )
{
var numVertices = vertices.Count;
newVertices = new List( numVertices * 8 );
var success = true;
for ( var i = 0; i < numVertices; ++i )
{
if ( BevelVertex( vertices[i], true, distance, out var newVerticesForVertex, out _ ) )
{
newVertices.AddRange( newVerticesForVertex );
}
else
{
success = false;
}
}
Topology.FindFacesConnectedToVertices( newVertices, newVertices.Count, out var connectedFaces, out _ );
foreach ( var hFace in connectedFaces )
{
TextureAlignToGrid( Transform, hFace );
}
IsDirty = true;
return success;
}
private bool ConnectVertices( IReadOnlyList pVertices, out List outNewEdges )
{
var numVertices = pVertices.Count;
outNewEdges = new List( numVertices * 2 );
var connected = false;
for ( var i = 0; i < numVertices; ++i )
{
for ( int j = i + 1; j < numVertices; ++j )
{
if ( ConnectVertices( pVertices[i], pVertices[j], out var hNewEdge ) )
{
if ( hNewEdge.IsValid )
{
outNewEdges.Add( hNewEdge );
connected = true;
}
}
}
}
return connected;
}
public bool GetFacesConnectedToVertex( VertexHandle hVertex, out List faces )
{
return Topology.GetFacesConnectedToVertex( hVertex, out faces );
}
public HalfEdgeHandle FindFaceVertexConnectedToVertex( VertexHandle hVertex, FaceHandle hFace )
{
return Topology.FindEdgeConnectedToFaceEndingAtVertex( hFace, hVertex );
}
public HalfEdgeHandle GetNextVertexInFace( HalfEdgeHandle hFaceVertex )
{
return Topology.GetNextEdgeInFaceLoop( hFaceVertex );
}
public bool ConnectVertices( VertexHandle hVertexA, VertexHandle hVertexB, out HalfEdgeHandle hNewEdge )
{
hNewEdge = null;
Topology.FindFacesSharedByVertices( hVertexA, hVertexB, out var sharedFaces );
foreach ( var hSharedFace in sharedFaces )
{
if ( IsLineBetweenVerticesInsideFace( hSharedFace, hVertexA, hVertexB ) )
{
if ( AddEdgeToFace( hSharedFace, hVertexA, hVertexB, out hNewEdge ) )
{
IsDirty = true;
return true;
}
}
}
return false;
}
private bool IsLineBetweenVerticesInsideFace( FaceHandle hFace, VertexHandle hVertexA, VertexHandle hVertexB )
{
Assert.True( hVertexA != hVertexB );
if ( hVertexA == hVertexB )
return false;
var positionA = GetVertexPosition( hVertexA );
var positionB = GetVertexPosition( hVertexB );
var dirAB = (positionB - positionA).Normal;
var positions = GetFaceVertexPositions( hFace, Transform.Zero ).ToArray();
var numVertices = positions.Length;
PlaneEquation( positions, out var vNormal, out var flPlaneDist );
var intersectPlane = new Plane( positionA, dirAB.Cross( vNormal ).Normal );
var planeA = new Plane( positionA, dirAB );
var planeB = new Plane( positionB, -dirAB );
for ( int i = (numVertices - 1), j = 0; j < numVertices; i = j++ )
{
var intersectPoint = intersectPlane.IntersectLine( positions[i], positions[j] );
if ( intersectPoint.HasValue )
{
if ( (planeA.GetDistance( intersectPoint.Value ) >= 0.01f) &&
(planeB.GetDistance( intersectPoint.Value ) >= 0.01f) )
{
return false;
}
}
}
var indices = Mesh.TriangulatePolygon( positions );
var min = positions[0];
var max = min;
foreach ( var v in positions[1..] )
{
min = Vector3.Min( min, v );
max = Vector3.Max( max, v );
}
return IsPointInPolygon( positions, indices, min, max, positionA.LerpTo( positionB, 1.0f / 3.0f ) ) ||
IsPointInPolygon( positions, indices, min, max, positionA.LerpTo( positionB, 2.0f / 3.0f ) );
}
private static bool IsPointInPolygon( ReadOnlySpan vertices, ReadOnlySpan indices, Vector3 min, Vector3 max, Vector3 point )
{
if ( (point.x < min.x) || (point.y < min.y) || (point.z < min.z) ||
(point.x > max.x) || (point.y > max.y) || (point.z > max.z) )
return false;
int numIndices = indices.Length;
for ( int i = 0; (i + 2) < numIndices; i += 3 )
{
var a = vertices[indices[i + 0]];
var b = vertices[indices[i + 1]];
var c = vertices[indices[i + 2]];
var ab = b - a;
var ac = c - a;
var normal = ab.Cross( ac ).Normal;
if ( InsideTriangle( a, b, c, point, normal ) )
{
return true;
}
}
return false;
}
public static bool InsideTriangle( Vector3 a, Vector3 b, Vector3 c, Vector3 p, Vector3 normal )
{
const float EPSILON = 0.0000001f;
const float MAX_EDGE_DIST_SQ = EPSILON * EPSILON;
var bc = c - b;
var ca = a - c;
var ab = b - a;
var ap = p - a;
var bp = p - b;
var cp = p - c;
if ( Vector3.Dot( Vector3.Cross( bc, bp ), normal ) > -EPSILON &&
Vector3.Dot( Vector3.Cross( ca, cp ), normal ) > -EPSILON &&
Vector3.Dot( Vector3.Cross( ab, ap ), normal ) > -EPSILON )
{
return true;
}
return IsPointOnEdge( p, a, b, MAX_EDGE_DIST_SQ ) ||
IsPointOnEdge( p, b, c, MAX_EDGE_DIST_SQ ) ||
IsPointOnEdge( p, c, a, MAX_EDGE_DIST_SQ );
}
private static bool IsPointOnEdge( Vector3 p, Vector3 a, Vector3 b, float maxEdgeDistSq )
{
var dir = b - a;
var div = Vector3.Dot( dir, dir );
var t = (div < 0.00001f) ? 0.0f : (Vector3.Dot( dir, p ) - Vector3.Dot( dir, a )) / div;
var closestPoint = a + dir * t;
return (Vector3.DistanceBetweenSquared( p, closestPoint ) < maxEdgeDistSq) && (t >= 0.0f) && (t <= 1.0f);
}
private bool AddEdgeToFace( FaceHandle hFace, VertexHandle hVertexA, VertexHandle hVertexB, out HalfEdgeHandle pOutNewEdge )
{
pOutNewEdge = null;
if ( !hVertexA.IsValid || !hVertexB.IsValid )
return false;
// Must be two different vertices
if ( hVertexA == hVertexB )
return false;
var hFaceVertexA = Topology.FindEdgeConnectedToFaceEndingAtVertex( hFace, hVertexA );
var hFaceVertexB = Topology.FindEdgeConnectedToFaceEndingAtVertex( hFace, hVertexB );
// If either of the vertices is internal the edge must be connected in the correct winding
// order, use the vertex which is not internal to determine the correct winding order.
var hExternalVertex = VertexHandle.Invalid;
var hInternalVertex = VertexHandle.Invalid;
if ( Topology.IsVertexInternal( hVertexA ) )
{
// Cannot connect two internal vertices
if ( Topology.IsVertexInternal( hVertexB ) )
return false;
hInternalVertex = hVertexA;
hExternalVertex = hVertexB;
}
else if ( Topology.IsVertexInternal( hVertexB ) )
{
hInternalVertex = hVertexB;
hExternalVertex = hVertexA;
}
// If there is a position stream use the direction of the edge to disambiguate
// which of the incoming half edge to internal vertex is the best one to use.
if ( hInternalVertex.IsValid && hExternalVertex.IsValid )
{
// Compute the direction of the new edge to be added, going from the internal vertex to the external
var vExternalPos = GetVertexPosition( hExternalVertex );
var vInternalPos = GetVertexPosition( hInternalVertex );
var vNewEdgeDir = (vExternalPos - vInternalPos).Normal;
// Compute the normal of the face
ComputeFaceNormal( hFace, out var vFaceNormal );
// Determine which of the edges terminating at the internal edge should be used by finding
// the direction perpendicular to each edge that points into the face and comparing the
// direction of the new edge to the vector pointing into the face.
float flBestDot = float.MinValue;
var hBestInternalFaceVertex = HalfEdgeHandle.Invalid;
Topology.GetIncomingHalfEdgesConnectedToVertex( hInternalVertex, out var incomingEdges );
var nNumIncomingEdges = incomingEdges.Count;
for ( var iEdge = 0; iEdge < nNumIncomingEdges; ++iEdge )
{
var hIncomingEdge = incomingEdges[iEdge];
if ( Topology.GetFaceConnectedToHalfEdge( hIncomingEdge ) == hFace )
{
var hPreviousFaceVertex = Topology.FindPreviousEdgeInFaceLoop( hIncomingEdge );
var hPreviousVertex = Topology.GetEndVertexConnectedToEdge( hPreviousFaceVertex );
var vPreviousPos = GetVertexPosition( hPreviousVertex );
// Compute the direction of the internal edge
var vInternalEdgeDir = (vInternalPos - vPreviousPos).Normal;
// Compute the direction pointing into the face
var vInteriorDir = vFaceNormal.Cross( vInternalEdgeDir );
// Is the new edge direction closer to the interior direction
// of this edge than any of the previous edges?
float flDot = vInteriorDir.Dot( vNewEdgeDir );
if ( flDot > flBestDot )
{
hBestInternalFaceVertex = hIncomingEdge;
flBestDot = flDot;
}
}
}
Assert.True( hBestInternalFaceVertex.IsValid );
if ( hBestInternalFaceVertex.IsValid )
{
if ( hInternalVertex == hVertexA )
{
hFaceVertexA = hBestInternalFaceVertex;
}
else
{
hFaceVertexB = hBestInternalFaceVertex;
}
}
}
return Topology.AddEdgeToFace( hFaceVertexA, hFaceVertexB, out pOutNewEdge );
}
private bool FindOpenEdgeLoop( HalfEdgeHandle hEdge, out List pOutEdgeList )
{
pOutEdgeList = new List();
var hStartHalfEdge = Topology.GetHalfEdgeForFaceEdge( FaceHandle.Invalid, hEdge );
if ( hStartHalfEdge == HalfEdgeHandle.Invalid )
return false;
// Count the number of edges in the loop
int nNumEdgesInLoop = 0;
var hCurrentEdge = hStartHalfEdge;
do
{
++nNumEdgesInLoop;
hCurrentEdge = Topology.GetNextEdgeInFaceLoop( hCurrentEdge );
}
while ( hCurrentEdge != hStartHalfEdge );
// Allocate the space in the provided list
pOutEdgeList.EnsureCapacity( pOutEdgeList.Count + nNumEdgesInLoop );
// Add the edges to the list
hCurrentEdge = hStartHalfEdge;
do
{
pOutEdgeList.Add( Topology.GetFullEdgeForHalfEdge( hCurrentEdge ) );
hCurrentEdge = Topology.GetNextEdgeInFaceLoop( hCurrentEdge );
}
while ( hCurrentEdge != hStartHalfEdge );
return true;
}
private FaceHandle FindFaceSharedEdges( IReadOnlyList edgeList )
{
int nNumEdges = edgeList.Count;
if ( nNumEdges < 2 )
return FaceHandle.Invalid;
var hFaceSharedFace = Topology.FindFaceConnectingFullEdges( edgeList[0], edgeList[1] );
for ( int iEdge = 2; iEdge < nNumEdges; ++iEdge )
{
if ( Topology.FindFaceConnectingFullEdges( edgeList[0], edgeList[iEdge] ) != hFaceSharedFace )
{
hFaceSharedFace = FaceHandle.Invalid;
}
}
return hFaceSharedFace;
}
public void FindEdgeLoopForEdges( IReadOnlyList originalEdges, out HalfEdgeHandle[] pOutEdgeLoopEdges )
{
var loopEdges = new Dictionary( 256 );
var subSetLoopEdges = new List( 1024 );
var index = 0;
// First find the existing loops
Topology.FindEdgeIslands( originalEdges, out var edgeIslands );
for ( int iIsland = 0; iIsland < edgeIslands.Count; ++iIsland )
{
var edgeIsland = edgeIslands[iIsland];
if ( Topology.ClassifyEdgeListConnectivity( edgeIsland, edgeIsland.Count, out _ ) == ComponentConnectivityType.Loop )
{
for ( int i = 0; i < edgeIsland.Count; ++i )
{
if ( !loopEdges.ContainsKey( edgeIsland[i] ) )
loopEdges.Add( edgeIsland[i], index++ );
}
}
}
// Look for open edge loops, or general edge loops.
for ( int iEdge = 0; iEdge < originalEdges.Count; ++iEdge )
{
var hEdge = originalEdges[iEdge];
// Skip any edges that are already in the list of edges used by the loops
if ( loopEdges.ContainsKey( hEdge ) )
continue;
List edgeLoop;
// First check to see if the edge is open, if so select the loop of open edges it is
// connected to, otherwise find the general edge loop to which the edge belongs.
if ( IsEdgeOpen( hEdge ) )
{
FindOpenEdgeLoop( hEdge, out edgeLoop );
}
else
{
Topology.FindEdgeLoop( hEdge, -1, out edgeLoop );
}
if ( edgeLoop.Count > 1 )
{
// Find the sub-set of the loop containing the original edges
if ( FindShortestEdgeLoopSubSetContainingEdges( edgeLoop, originalEdges, out edgeLoop ) == false )
{
subSetLoopEdges.AddRange( edgeLoop );
}
// Add whatever edge loop was found to the selection
for ( int i = 0; i < edgeLoop.Count; ++i )
{
if ( !loopEdges.ContainsKey( edgeLoop[i] ) )
loopEdges.Add( edgeLoop[i], index++ );
}
}
}
// Build a combine list of the original edges and loop edges, if there are any remaining isolated
// edge islands where all of the edges share a common face select the loop of that face.
var allEdges = new List( originalEdges.Count + loopEdges.Count );
for ( int iEdge = 0; iEdge < originalEdges.Count; ++iEdge )
{
if ( !loopEdges.ContainsKey( originalEdges[iEdge] ) )
{
allEdges.Add( originalEdges[iEdge] );
}
}
var loopEdgesList = new HalfEdgeHandle[loopEdges.Count];
foreach ( var edge in loopEdges )
loopEdgesList[edge.Value] = edge.Key;
allEdges.AddRange( loopEdgesList );
Topology.FindEdgeIslands( allEdges, out edgeIslands );
for ( int iIsland = 0; iIsland < edgeIslands.Count; ++iIsland )
{
var edgeIsland = edgeIslands[iIsland];
int nNumIslandEdges = edgeIsland.Count;
if ( nNumIslandEdges <= 0 )
continue;
// Skip the island if any of its edges are in the loop selection
bool bEdgeInLoopSelection = false;
for ( int iEdge = 0; iEdge < nNumIslandEdges; ++iEdge )
{
if ( loopEdges.ContainsKey( edgeIsland[iEdge] ) )
{
bEdgeInLoopSelection = true;
break;
}
}
if ( bEdgeInLoopSelection )
continue;
var hStartEdge = edgeIsland[0];
FaceHandle hLoopFace;
if ( nNumIslandEdges == 1 )
{
Topology.GetFacesConnectedToFullEdge( hStartEdge, out var hFaceA, out var hFaceB );
// Compute the number of edges in each face and select the
// edge loop of the face which has more edges.
int nNumEdgesFaceA = Topology.ComputeNumEdgesInFace( hFaceA );
int nNumEdgesFaceB = Topology.ComputeNumEdgesInFace( hFaceB );
hLoopFace = (nNumEdgesFaceA >= nNumEdgesFaceB) ? hFaceA : hFaceB;
}
else
{
// If all of the edges in the edgeIsland share a
// face add the loop of that face to the list.
hLoopFace = FindFaceSharedEdges( edgeIsland );
}
if ( hLoopFace != FaceHandle.Invalid )
{
// Get the edges of the face and add them to the loop edges
Topology.GetFullEdgesConnectedToFace( hLoopFace, out var faceEdgeLoop );
int nNumFaceEdges = faceEdgeLoop.Count;
int nStartIndex = faceEdgeLoop.IndexOf( hStartEdge );
var orderedFaceLoop = new List( nNumFaceEdges );
for ( int iEdge = nStartIndex; iEdge < nNumFaceEdges; ++iEdge )
{
orderedFaceLoop.Add( faceEdgeLoop[iEdge] );
}
for ( int iEdge = 0; iEdge < nStartIndex; ++iEdge )
{
orderedFaceLoop.Add( faceEdgeLoop[iEdge] );
}
// Find the sub-set of the loop containing the original edges
if ( !FindShortestEdgeLoopSubSetContainingEdges( orderedFaceLoop, originalEdges, out faceEdgeLoop ) )
{
subSetLoopEdges.AddRange( faceEdgeLoop );
}
for ( int i = 0; i < faceEdgeLoop.Count; ++i )
{
if ( !loopEdges.ContainsKey( faceEdgeLoop[i] ) )
loopEdges.Add( faceEdgeLoop[i], index++ );
}
}
}
// If there are sub-set loop edges use only those so that loops
// are not selected from existing connected edges.
if ( subSetLoopEdges.Count > 0 )
{
int nNumSubsetLoopEdges = subSetLoopEdges.Count;
loopEdges.Clear();
loopEdges.EnsureCapacity( nNumSubsetLoopEdges );
for ( int iEdge = 0; iEdge < nNumSubsetLoopEdges; ++iEdge )
{
if ( !loopEdges.ContainsKey( subSetLoopEdges[iEdge] ) )
loopEdges.Add( subSetLoopEdges[iEdge], index++ );
}
}
// Build the list of unique edges to be returned from the tree
pOutEdgeLoopEdges = new HalfEdgeHandle[loopEdges.Count];
foreach ( var edge in loopEdges )
pOutEdgeLoopEdges[edge.Value] = edge.Key;
}
public void FindEdgeIslands( IReadOnlyList edgeList, out List> outEdgeList )
{
Topology.FindEdgeIslands( edgeList, out outEdgeList );
}
public int FindEdgeRibs( IReadOnlyList edges, out List> outLeftRibs, out List> outRightRibs )
{
var numRibs = Topology.FindEdgeRibs( edges, edges.Count, out var leftRibs, out var rightRibs, out _ );
outLeftRibs = new List>();
outRightRibs = new List>();
for ( var i = 0; i < numRibs; i++ )
{
outLeftRibs.Add( new List() );
outRightRibs.Add( new List() );
}
for ( var ribIndex = 0; ribIndex < numRibs; ++ribIndex )
{
var numLeftRibEdges = leftRibs[ribIndex].Count;
var outLeftRibEdges = outLeftRibs[ribIndex];
for ( var edgeIndex = 0; edgeIndex < numLeftRibEdges; ++edgeIndex )
{
outLeftRibEdges.Add( Topology.GetFullEdgeForHalfEdge( leftRibs[ribIndex][edgeIndex] ) );
}
var numRightRibEdges = rightRibs[ribIndex].Count;
var outRightRibEdges = outRightRibs[ribIndex];
for ( var edgeIndex = 0; edgeIndex < numRightRibEdges; ++edgeIndex )
{
outRightRibEdges.Add( Topology.GetFullEdgeForHalfEdge( rightRibs[ribIndex][edgeIndex] ) );
}
}
return numRibs;
}
private bool FindShortestEdgeLoopSubSetContainingEdges( IReadOnlyList edgeLoop, IReadOnlyList originalEdges, out List pOutEdgeLoop )
{
var subSetEdgeList = new List();
// Build a list of the original edges which are in the loop
var originalEdgesInLoop = new List( Math.Min( edgeLoop.Count, originalEdges.Count ) );
int nNumOriginalEdges = originalEdges.Count;
for ( int iEdge = 0; iEdge < nNumOriginalEdges; ++iEdge )
{
if ( edgeLoop.Contains( originalEdges[iEdge] ) )
{
originalEdgesInLoop.Add( originalEdges[iEdge] );
}
}
// Determine if all original edges in the list are connected or not. If they are all connected
// return the entire loop, but if they are disjoint return the smallest section of the loop
// which contains all of the original edges.
var originalEdgesConnectivity = Topology.ClassifyEdgeListConnectivity( originalEdgesInLoop, originalEdgesInLoop.Count, out _ );
if ( (originalEdgesConnectivity == ComponentConnectivityType.Mixed) ||
(originalEdgesConnectivity == ComponentConnectivityType.None) )
{
var loopConnectivity = Topology.ClassifyEdgeListConnectivity( edgeLoop, edgeLoop.Count, out _ );
if ( loopConnectivity == ComponentConnectivityType.List )
{
int nFirstEdgeInLoop = -1;
int nLastEdgeInLoop = 0;
int nNumEdgesInLoop = edgeLoop.Count;
for ( int iEdge = 0; iEdge < nNumEdgesInLoop; ++iEdge )
{
if ( originalEdgesInLoop.Contains( edgeLoop[iEdge] ) )
{
if ( nFirstEdgeInLoop < 0 )
{
nFirstEdgeInLoop = iEdge;
}
nLastEdgeInLoop = Math.Max( nLastEdgeInLoop, iEdge );
}
}
subSetEdgeList.EnsureCapacity( nLastEdgeInLoop - nFirstEdgeInLoop + 1 );
for ( int iEdge = nFirstEdgeInLoop; iEdge <= nLastEdgeInLoop; ++iEdge )
{
subSetEdgeList.Add( edgeLoop[iEdge] );
}
}
else if ( loopConnectivity == ComponentConnectivityType.Loop )
{
int nCurrentGapStart = -1;
int nCurrentGapEnd = -1;
int nLargestGapStart = -1;
int nLargestGapEnd = -1;
int nLargestGapSize = -1;
int nNumEdgesInLoop = edgeLoop.Count;
int nCurrentGapSize;
for ( int iEdge = 1; iEdge < nNumEdgesInLoop; ++iEdge )
{
if ( !originalEdgesInLoop.Contains( edgeLoop[iEdge] ) )
{
if ( nCurrentGapStart < 0 )
{
nCurrentGapStart = iEdge;
}
nCurrentGapEnd = iEdge;
}
else if ( nCurrentGapStart >= 0 )
{
nCurrentGapSize = (nCurrentGapEnd - nCurrentGapStart) + 1;
if ( nCurrentGapSize > nLargestGapSize )
{
nLargestGapStart = nCurrentGapStart;
nLargestGapEnd = nCurrentGapEnd;
nLargestGapSize = nCurrentGapSize;
}
nCurrentGapStart = -1;
nCurrentGapEnd = -1;
}
}
nCurrentGapSize = (nCurrentGapEnd - nCurrentGapStart) + 1;
if ( nCurrentGapSize > nLargestGapSize )
{
nLargestGapStart = nCurrentGapStart;
nLargestGapEnd = nCurrentGapEnd;
nLargestGapSize = nCurrentGapSize;
}
subSetEdgeList.EnsureCapacity( nNumEdgesInLoop - nLargestGapSize );
for ( int iEdge = 0; iEdge < nNumEdgesInLoop; ++iEdge )
{
if ( (iEdge >= nLargestGapStart) && (iEdge <= nLargestGapEnd) )
continue;
subSetEdgeList.Add( edgeLoop[iEdge] );
}
}
}
else
{
pOutEdgeLoop = new List( edgeLoop );
return true;
}
pOutEdgeLoop = subSetEdgeList;
return false;
}
private void InterpolateVertexData( VertexHandle hDstVertex, VertexHandle hVertexA, VertexHandle hVertexB, float param )
{
if ( !hDstVertex.IsValid )
return;
if ( !hVertexA.IsValid )
return;
if ( !hVertexB.IsValid )
return;
var a = GetVertexPosition( hVertexA );
var b = GetVertexPosition( hVertexB );
SetVertexPosition( hDstVertex, a.LerpTo( b, param ) );
}
private void InterpolateFaceVertexData( HalfEdgeHandle hDstFaceVertex, HalfEdgeHandle hFaceVertexA, HalfEdgeHandle hFaceVertexB, float param )
{
if ( !hDstFaceVertex.IsValid )
return;
if ( !hFaceVertexA.IsValid )
return;
if ( !hFaceVertexB.IsValid )
return;
var a = TextureCoord[hFaceVertexA];
var b = TextureCoord[hFaceVertexB];
TextureCoord[hDstFaceVertex] = a.LerpTo( b, param );
}
///
/// Get start and end points of an edge
///
public Line GetEdgeLine( HalfEdgeHandle hEdge )
{
if ( !hEdge.IsValid )
return default;
GetEdgeVertices( hEdge, out var hVertexA, out var hVertexB );
var a = Positions[hVertexA];
var b = Positions[hVertexB];
return new Line( a, b );
}
///
/// Get the two vertices of this half edge
///
public void GetEdgeVertices( HalfEdgeHandle hEdge, out VertexHandle hVertexA, out VertexHandle hVertexB )
{
Topology.GetVerticesConnectedToHalfEdge( hEdge, out hVertexA, out hVertexB );
}
///
/// Set the position of a vertex
///
public void SetVertexPosition( VertexHandle hVertex, Vector3 position )
{
if ( !hVertex.IsValid )
return;
Positions[hVertex] = position;
IsDirty = true;
}
///
/// Get the position of a vertex
///
public Vector3 GetVertexPosition( VertexHandle hVertex )
{
return Positions[hVertex];
}
///
/// Get the position of a vertex
///
public void GetVertexPosition( VertexHandle hVertex, Transform transform, out Vector3 outPosition )
{
outPosition = transform.PointToWorld( GetVertexPosition( hVertex ) );
}
///
/// Get the positions of all vertices
///
public IEnumerable GetVertexPositions()
{
foreach ( var hVertex in Topology.VertexHandles )
yield return Positions[hVertex];
}
public void ComputeFaceNormal( FaceHandle hFace, out Vector3 pOutNormal )
{
var positions = GetFaceVertexPositions( hFace, Transform.Zero ).ToArray();
PlaneEquation( positions, out pOutNormal, out _ );
}
///
/// Calculate the center point of a face
///
public Vector3 GetFaceCenter( FaceHandle hFace )
{
var centroid = Vector3.Zero;
int count = 0;
foreach ( var i in Topology.GetFaceVertices( hFace ) )
{
centroid += Positions[i];
count++;
}
centroid *= 1f / count;
return centroid;
}
///
/// Get the start and end points of all edges
///
public IEnumerable GetEdges()
{
foreach ( var hEdge in Topology.HalfEdgeHandles )
{
if ( hEdge.Index > Topology.GetOppositeHalfEdge( hEdge ).Index )
continue;
yield return GetEdgeLine( hEdge );
}
}
public IEnumerable GetFaceVertexPositions( FaceHandle hFace, Transform transform )
{
var hFirstFaceVertex = Topology.GetFirstEdgeInFaceLoop( hFace );
var hCurrentFaceVertex = hFirstFaceVertex;
do
{
var hVertex = Topology.GetEndVertexConnectedToEdge( hCurrentFaceVertex );
yield return transform.PointToWorld( GetVertexPosition( hVertex ) );
hCurrentFaceVertex = Topology.GetNextEdgeInFaceLoop( hCurrentFaceVertex );
}
while ( hCurrentFaceVertex != hFirstFaceVertex );
}
public bool GetFaceVerticesConnectedToFace( FaceHandle hFace, out HalfEdgeHandle[] hEdges )
{
return Topology.GetHalfEdgesConnectedToFace( hFace, out hEdges );
}
public VertexHandle GetVertexConnectedToFaceVertex( HalfEdgeHandle hFaceVertex )
{
return Topology.GetEndVertexConnectedToEdge( hFaceVertex );
}
public void ComputeFaceTextureParametersFromCoordinates()
{
ComputeFaceTextureParametersFromCoordinates( FaceHandles );
}
public void ComputeFaceTextureParametersFromCoordinates( IEnumerable faces )
{
var textureSizes = Materials.Select( CalculateTextureSize ).ToArray();
ComputeFaceTextureParametersFromCoordinates( faces, textureSizes, Transform );
}
public void ComputeFaceTextureCoordinatesFromParameters()
{
ComputeFaceTextureCoordinatesFromParameters( FaceHandles );
}
public void ComputeFaceTextureCoordinatesFromParameters( IEnumerable faces )
{
var textureSizes = Materials.Select( CalculateTextureSize ).ToArray();
ComputeFaceTextureCoordinatesFromParameters( faces, Transform, textureSizes, 0.25f );
}
public void ComputeFaceTextureParametersFromCoordinates( IEnumerable faces, IReadOnlyList textureSizes, Transform transform )
{
foreach ( var hFace in faces )
{
if ( !hFace.IsValid )
continue;
GetFaceVerticesConnectedToFace( hFace, out var faceVertices );
var facePositions = new List();
var faceTexCoords = new List();
var nNumVertices = faceVertices.Length;
for ( var iVertex = 0; iVertex < nNumVertices; ++iVertex )
{
var hFaceVertex = faceVertices[iVertex];
var hVertex = GetVertexConnectedToFaceVertex( hFaceVertex );
var vLocalPosition = Positions[hVertex];
var vWorldPosition = transform.PointToWorld( vLocalPosition );
facePositions.Add( vWorldPosition );
faceTexCoords.Add( TextureCoord[hFaceVertex] );
}
GetBestThreeTextureBasisVerticies( facePositions, faceTexCoords, nNumVertices, out var bestPositions, out var bestTexCoords );
if ( CalcTextureBasisFromUVs( bestPositions, bestTexCoords, out _, out _ ) )
{
var materialIndex = MaterialIndex[hFace];
var textureSize = materialIndex >= 0 && materialIndex < textureSizes.Count ? textureSizes[materialIndex] : DefaultTextureSize;
ComputeFaceTextureParametersFromUVs( bestPositions, bestTexCoords, textureSize,
out var axisU, out var axisV, out var scale );
TextureUAxis[hFace] = (Vector3)axisU;
TextureVAxis[hFace] = (Vector3)axisV;
TextureOffset[hFace] = new Vector2( axisU.w, axisV.w );
TextureScale[hFace] = scale;
}
else
{
TextureUAxis[hFace] = new Vector3( 1.0f, 0.0f, 0.0f );
TextureVAxis[hFace] = new Vector3( 0.0f, 1.0f, 0.0f );
TextureOffset[hFace] = Vector2.Zero;
TextureScale[hFace] = Vector2.One;
}
}
}
public void ComputeFaceTextureCoordinatesFromParameters( IEnumerable faces, Transform transform, IReadOnlyList textureSizes, float defaultScale )
{
foreach ( var hFace in faces )
{
if ( !hFace.IsValid )
continue;
var materialIndex = MaterialIndex[hFace];
var textureSize = materialIndex >= 0 && materialIndex < textureSizes.Count ? textureSizes[materialIndex] : DefaultTextureSize;
var axisU = TextureUAxis[hFace];
var axisV = TextureVAxis[hFace];
var scale = TextureScale[hFace];
var offset = TextureOffset[hFace];
scale.x = (scale.x.AlmostEqual( 0.0f ) || float.IsNaN( scale.x )) ? defaultScale : scale.x;
scale.y = (scale.y.AlmostEqual( 0.0f ) || float.IsNaN( scale.y )) ? defaultScale : scale.y;
GetFaceVerticesConnectedToFace( hFace, out var faceVertices );
var numVertices = faceVertices.Length;
for ( var iVertex = 0; iVertex < numVertices; ++iVertex )
{
var hFaceVertex = faceVertices[iVertex];
var hVertex = GetVertexConnectedToFaceVertex( hFaceVertex );
var position = Positions[hVertex];
var worldPos = transform.PointToWorld( position );
var u = Vector3.Dot( axisU, worldPos ) / scale.x + offset.x;
var v = Vector3.Dot( axisV, worldPos ) / scale.y + offset.y;
var texCoord = new Vector2( u / MathF.Max( textureSize.x, 1.0f ), v / MathF.Max( textureSize.y, 1.0f ) );
TextureCoord[hFaceVertex] = texCoord;
}
}
}
private static bool CalcTextureBasisFromUVs( Vector3[] vVertPos, Vector2[] vTexCoord, out Vector3 vOutU, out Vector3 vOutV )
{
const float flEpsilon = 0.000001f;
vOutU = new Vector3( 1.0f, 0.0f, 0.0f );
vOutV = new Vector3( 0.0f, 1.0f, 0.0f );
Vector3[] E = { vVertPos[1] - vVertPos[0], vVertPos[2] - vVertPos[0] };
Vector2[] T = { vTexCoord[1] - vTexCoord[0], vTexCoord[2] - vTexCoord[0] };
if ( T[0].LengthSquared < flEpsilon && T[1].LengthSquared < flEpsilon )
return false;
var eDet = T[0].x * T[1].y - T[1].x * T[0].y;
if ( MathF.Abs( eDet ) < flEpsilon )
eDet = flEpsilon;
var textureU = 1.0f / eDet * (T[1].y * E[0] - T[0].y * E[1]);
var textureV = 1.0f / eDet * (-T[1].x * E[0] + T[0].x * E[1]);
var textureNormal = Vector3.Cross( textureU, textureV );
var mTextureToWorld = new System.Numerics.Matrix4x4(
textureU.x, textureV.x, textureNormal.x, 0,
textureU.y, textureV.y, textureNormal.y, 0,
textureU.z, textureV.z, textureNormal.z, 0,
0, 0, 0, 1 );
if ( System.Numerics.Matrix4x4.Invert( mTextureToWorld, out var mWorldToTexture ) )
{
vOutU = new Vector3( mWorldToTexture.M11, mWorldToTexture.M12, mWorldToTexture.M13 );
vOutV = new Vector3( mWorldToTexture.M21, mWorldToTexture.M22, mWorldToTexture.M23 );
return true;
}
return false;
}
private static void ComputeFaceTextureParametersFromUVs( Vector3[] vVertPos, Vector2[] vTexCoord, Vector2 vTextureDimensions, out Vector4 pAxisU, out Vector4 pAxisV, out Vector2 pScale )
{
if ( !CalcTextureBasisFromUVs( vVertPos, vTexCoord, out var vWorldU, out var vWorldV ) )
{
pAxisU = default;
pAxisV = default;
pScale = default;
return;
}
var flWorldUScale = vWorldU.Length;
var flWorldVScale = vWorldV.Length;
vWorldU = vWorldU.Normal;
vWorldV = vWorldV.Normal;
pScale = new Vector2( 1.0f / (vTextureDimensions.x * flWorldUScale), 1.0f / (vTextureDimensions.y * flWorldVScale) );
pAxisU = new Vector4( vWorldU );
pAxisV = new Vector4( vWorldV );
var vWorldOffset = new Vector2(
Vector3.Dot( vWorldU, vVertPos[0] ) * flWorldUScale,
Vector3.Dot( vWorldV, vVertPos[0] ) * flWorldVScale );
var vWorldOffsetFrac = new Vector2( vWorldOffset.x - (int)vWorldOffset.x, vWorldOffset.y - (int)vWorldOffset.y );
var vTexCoordFrac = new Vector2( vTexCoord[0].x - (int)vTexCoord[0].x, vTexCoord[0].y - (int)vTexCoord[0].y );
var uvOffset = vTexCoordFrac - vWorldOffsetFrac;
uvOffset.x -= (int)uvOffset.x;
uvOffset.y -= (int)uvOffset.y;
if ( uvOffset.x < 0 ) uvOffset.x += 1.0f;
if ( uvOffset.y < 0 ) uvOffset.y += 1.0f;
var uOffset = uvOffset.x * vTextureDimensions.x;
var vOffset = uvOffset.y * vTextureDimensions.y;
if ( uOffset >= vTextureDimensions.x ) uOffset -= vTextureDimensions.x;
if ( vOffset >= vTextureDimensions.y ) vOffset -= vTextureDimensions.y;
pAxisU.w = uOffset;
pAxisV.w = vOffset;
}
private static void GetBestThreeTextureBasisVerticies( IReadOnlyList pPositions, IReadOnlyList pTexCoords, int nNumPositions, out Vector3[] pOutPositions, out Vector2[] pOutTexCoords )
{
pOutPositions = new Vector3[3];
pOutTexCoords = new Vector2[3];
if ( nNumPositions < 3 )
return;
var nBestVert = 0;
var flBestHeuristic = -1.0f;
for ( var i = 0; i < nNumPositions; ++i )
{
var nVert0 = (i + nNumPositions - 1) % nNumPositions;
var nVert1 = i;
var nVert2 = (i + 1) % nNumPositions;
var vEdge0 = pPositions[nVert0] - pPositions[nVert1];
var vEdge1 = pPositions[nVert2] - pPositions[nVert1];
var flOneMinsDot = (1.0f - MathF.Abs( vEdge0.Normal.Dot( vEdge1.Normal ) ));
var flHeuristic = vEdge0.LengthSquared * vEdge1.LengthSquared * flOneMinsDot;
if ( flHeuristic > flBestHeuristic )
{
nBestVert = nVert1;
flBestHeuristic = flHeuristic;
}
}
var nPrevVert = (nBestVert + nNumPositions - 1) % nNumPositions;
var nNextVert = (nBestVert + 1) % nNumPositions;
pOutPositions[0] = pPositions[nBestVert];
pOutPositions[1] = pPositions[nPrevVert];
pOutPositions[2] = pPositions[nNextVert];
pOutTexCoords[0] = pTexCoords[nBestVert];
pOutTexCoords[1] = pTexCoords[nPrevVert];
pOutTexCoords[2] = pTexCoords[nNextVert];
}
///
/// Transform all the vertices
///
public void ApplyTransform( Transform transform )
{
foreach ( var hVertex in VertexHandles )
{
SetVertexPosition( hVertex, transform.PointToWorld( GetVertexPosition( hVertex ) ) );
}
}
///
/// Get all edge handles of a face
///
public HalfEdgeHandle[] GetFaceEdges( FaceHandle hFace )
{
GetEdgesConnectedToFace( hFace, out var edges );
return edges.ToArray();
}
///
/// Get all vertex handles of a face
///
public VertexHandle[] GetFaceVertices( FaceHandle hFace )
{
return Topology.GetFaceVertices( hFace );
}
///
/// Get texture offset of a face
///
public Vector2 GetTextureOffset( FaceHandle hFace )
{
return TextureOffset[hFace];
}
///
/// Set texture offset of a face
///
public void SetTextureOffset( FaceHandle hFace, Vector2 offset )
{
TextureOffset[hFace] = offset;
ComputeFaceTextureCoordinatesFromParameters( new[] { hFace } );
IsDirty = true;
}
///
/// Get texture scale of a face
///
public Vector2 GetTextureScale( FaceHandle hFace )
{
return TextureScale[hFace];
}
///
/// Set texture scale of a face
///
public void SetTextureScale( FaceHandle hFace, Vector2 scale )
{
TextureScale[hFace] = scale;
ComputeFaceTextureCoordinatesFromParameters( new[] { hFace } );
IsDirty = true;
}
///
/// Align face texture properties to grid
///
public void TextureAlignToGrid( Transform transform, FaceHandle hFace )
{
TextureOffset[hFace] = 0;
TextureScale[hFace] = 0.25f;
GetFacePlane( hFace, transform, out var plane );
var normal = plane.Normal;
ComputeTextureAxes( normal, out var uAxis, out var vAxis );
TextureUAxis[hFace] = uAxis;
TextureVAxis[hFace] = vAxis;
ComputeFaceTextureCoordinatesFromParameters( new[] { hFace } );
IsDirty = true;
}
///
/// Align face texture properties to face
///
public void TextureAlignToFace( Transform transform, FaceHandle hFace )
{
TextureOffset[hFace] = 0;
TextureScale[hFace] = 0.25f;
GetFacePlane( hFace, transform, out var plane );
var normal = plane.Normal;
var orientation = GetOrientationForPlane( normal );
var vAxis = FaceDownVectors[orientation];
var uAxis = normal.Cross( vAxis ).Normal;
vAxis = uAxis.Cross( normal ).Normal;
TextureUAxis[hFace] = uAxis;
TextureVAxis[hFace] = vAxis;
ComputeFaceTextureCoordinatesFromParameters( new[] { hFace } );
IsDirty = true;
}
///
/// Set face texture coords
///
public void SetFaceTextureCoords( FaceHandle hFace, IReadOnlyList texcoords )
{
if ( texcoords is null )
return;
if ( texcoords.Count <= 0 )
return;
if ( !hFace.IsValid )
return;
GetFaceVerticesConnectedToFace( hFace, out var hEdges );
var numCoords = Math.Min( texcoords.Count, hEdges.Length );
if ( numCoords == 0 )
return;
for ( var i = 0; i < numCoords; i++ )
{
TextureCoord[hEdges[i]] = texcoords[i];
}
ComputeFaceTextureParametersFromCoordinates( new[] { hFace } );
}
///
/// Set face texture properties
///
public void SetFaceTextureParameters( FaceHandle hFace, Vector2 offset, Vector3 uAxis, Vector3 vAxis )
{
TextureOffset[hFace] = offset;
TextureScale[hFace] = 0.25f;
TextureUAxis[hFace] = uAxis;
TextureVAxis[hFace] = vAxis;
ComputeFaceTextureCoordinatesFromParameters( new[] { hFace } );
IsDirty = true;
}
public void GetFaceTextureParameters( FaceHandle hFace, out Vector4 outAxisU, out Vector4 outAxisV, out Vector2 outScale )
{
var offset = TextureOffset[hFace];
outAxisU = new Vector4( TextureUAxis[hFace], offset.x );
outAxisV = new Vector4( TextureVAxis[hFace], offset.y );
outScale = TextureScale[hFace];
}
public void SetFaceTextureParameters( FaceHandle hFace, Vector4 axisU, Vector4 axisV, Vector2 scale )
{
TextureUAxis[hFace] = (Vector3)axisU;
TextureVAxis[hFace] = (Vector3)axisV;
TextureOffset[hFace] = new Vector2( axisU.w, axisV.w );
TextureScale[hFace] = scale;
ComputeFaceTextureCoordinatesFromParameters( new[] { hFace } );
IsDirty = true;
}
///
/// Align all face texture properties to grid
///
public void TextureAlignToGrid( Transform transform )
{
foreach ( var hFace in Topology.FaceHandles )
{
TextureAlignToGrid( transform, hFace );
}
IsDirty = true;
}
///
/// Remove these faces
///
public void RemoveFaces( IEnumerable hFaces )
{
foreach ( var hFace in hFaces )
{
Topology.RemoveFace( hFace, true );
}
IsDirty = true;
}
///
/// Remove these vertices
///
public void RemoveVertices( IEnumerable hVertices )
{
foreach ( var hVertex in hVertices )
{
Topology.RemoveVertex( hVertex, true );
}
IsDirty = true;
}
///
/// Remove these edges
///
public void RemoveEdges( IEnumerable hEdges )
{
foreach ( var hEdge in hEdges )
{
Topology.RemoveEdge( hEdge, true );
}
IsDirty = true;
}
public bool CollapseEdge( HalfEdgeHandle hEdge, out VertexHandle pOutNewVertex, out List<(HalfEdgeHandle, HalfEdgeHandle)> pOutReplacedEdges )
{
Topology.GetHalfEdgesConnectedToFullEdge( hEdge, out var hHalfEdgeA, out var _ );
return CollapseEdge( hHalfEdgeA, 0.5f, out pOutNewVertex, out pOutReplacedEdges );
}
public bool CollapseEdge( HalfEdgeHandle hHalfEdgeA, float flParam, out VertexHandle pOutNewVertex, out List<(HalfEdgeHandle, HalfEdgeHandle)> pOutReplacedEdges )
{
var hHalfEdgeB = Topology.GetOppositeHalfEdge( hHalfEdgeA );
var hVertexA = Topology.GetEndVertexConnectedToEdge( hHalfEdgeB );
var hVertexB = Topology.GetEndVertexConnectedToEdge( hHalfEdgeA );
var newVertex = GetVertexPosition( hVertexA ).LerpTo( GetVertexPosition( hVertexB ), flParam );
var hEdge = Topology.GetFullEdgeForHalfEdge( hHalfEdgeA );
var bRemoved = Topology.CollapseEdge( hEdge, out var hNewVertex, out pOutReplacedEdges );
if ( hNewVertex is not null && hNewVertex.IsValid )
SetVertexPosition( hNewVertex, newVertex );
pOutNewVertex = hNewVertex;
IsDirty = true;
return bRemoved;
}
public bool CollapseFace( FaceHandle hFace, out VertexHandle hOutVertex )
{
hOutVertex = null;
if ( !hFace.IsValid )
return false;
var nNumVerticesInFace = Topology.ComputeNumEdgesInFace( hFace );
var nIndex = 0;
var hFirstEdge = Topology.GetFirstEdgeInFaceLoop( hFace );
var hEdge = hFirstEdge;
var newVertex = Vector3.Zero;
do
{
Assert.True( nIndex < nNumVerticesInFace );
if ( nIndex >= nNumVerticesInFace )
break;
newVertex += GetVertexPosition( Topology.GetEndVertexConnectedToEdge( hEdge ) );
++nIndex;
hEdge = Topology.GetNextEdgeInFaceLoop( hEdge );
}
while ( hEdge != hFirstEdge );
if ( !Topology.CollapseFace( hFace, out hOutVertex ) )
return false;
newVertex /= nNumVerticesInFace;
SetVertexPosition( hOutVertex, newVertex );
IsDirty = true;
return true;
}
public bool SplitEdges( IReadOnlyList edges, out HalfEdgeHandle[] newEdgesA, out HalfEdgeHandle[] pOutNewEdgesB )
{
return Topology.SplitEdges( edges, out newEdgesA, out pOutNewEdgesB );
}
public void CollapseEdges( IReadOnlyList edges )
{
Topology.FindEdgeIslands( edges, out var edgeIslands );
var nNumEdgeIslands = edgeIslands.Count;
var nNumEdges = edges.Count;
var edgeIslandsAvergeVertex = new Vector3[nNumEdges];
{
for ( var iGroup = 0; iGroup < nNumEdgeIslands; ++iGroup )
{
var edgeGroup = edgeIslands[iGroup];
Topology.FindVerticesConnectedToFullEdges( edgeGroup, out var connectedVertices );
var averageVertex = Vector3.Zero;
foreach ( var p in connectedVertices )
averageVertex += GetVertexPosition( p );
averageVertex /= connectedVertices.Length;
edgeIslandsAvergeVertex[iGroup] = averageVertex;
}
}
//
// Collapse all of the edges in each group and apply the averaged values to the final vertex
//
for ( int iGroup = 0; iGroup < nNumEdgeIslands; ++iGroup )
{
var edgeGroup = edgeIslands[iGroup];
var nNumEdgesInGroup = edgeGroup.Count;
var hGroupVertex = VertexHandle.Invalid;
for ( var iEdge = 0; iEdge < nNumEdgesInGroup; ++iEdge )
{
var hEdge = edgeGroup[iEdge];
if ( CollapseEdge( hEdge, out var hEdgeCollapseVertex, out var replacedEdges ) )
{
// If any additional edges were replaced by the collapse of the current edge,
// search the group for the replaced edges and re-map the them to the new edge.
var nNumReplacedEdges = replacedEdges.Count;
for ( var iReplacedEdge = 0; iReplacedEdge < nNumReplacedEdges; ++iReplacedEdge )
{
var nEdgeInGroup = edgeGroup.IndexOf( replacedEdges[iReplacedEdge].Item1 );
if ( nEdgeInGroup != -1 )
{
edgeGroup[nEdgeInGroup] = replacedEdges[iReplacedEdge].Item2;
}
}
// Update the final vertex the group of edges was collapsed to
if ( hEdgeCollapseVertex.IsValid )
{
hGroupVertex = hEdgeCollapseVertex;
}
}
}
if ( hGroupVertex.IsValid )
{
var groupAverageVertex = edgeIslandsAvergeVertex[iGroup];
SetVertexPosition( hGroupVertex, groupAverageVertex );
}
}
IsDirty = true;
}
///
/// Triangulate the polygons into a model
///
public Model Rebuild()
{
_triangleFaces.Clear();
_meshIndices.Clear();
_meshVertices.Clear();
_meshFaces.Clear();
var submeshes = new Dictionary();
foreach ( var hFace in Topology.FaceHandles )
{
var materialId = MaterialIndex[hFace];
var material = GetMaterial( MaterialIndex[hFace] );
if ( !submeshes.TryGetValue( materialId, out var submesh ) )
{
submesh = new()
{
Material = material,
};
submeshes.Add( materialId, submesh );
}
TriangulateFace( hFace, submesh );
}
var builder = Model.Builder;
if ( _meshVertices.Count >= 3 && _meshIndices.Count >= 3 )
{
builder.AddCollisionHull( _meshVertices );
builder.AddCollisionMesh( _meshVertices, _meshIndices );
builder.AddTraceMesh( _meshVertices, _meshIndices );
}
foreach ( var submesh in submeshes.Values )
{
var vertices = submesh.Vertices;
var indices = submesh.Indices;
if ( vertices.Count < 3 )
continue;
if ( indices.Count < 3 )
continue;
var bounds = BBox.FromPoints( vertices.Select( x => x.position ) );
var material = submesh.Material ?? DefaultMaterial;
var mesh = new Mesh( material );
mesh.CreateVertexBuffer( vertices.Count, SimpleVertex.Layout, vertices );
mesh.CreateIndexBuffer( indices.Count, indices );
mesh.Bounds = bounds;
var uvDensity = submesh.UvDensity;
if ( uvDensity.Count > 0 )
{
uvDensity.Sort();
mesh.UvDensity = uvDensity[2 * (uvDensity.Count - 1) / 10];
}
builder.AddMesh( mesh );
}
IsDirty = false;
return builder.Create();
}
private int AddMaterial( Material material )
{
if ( material is null )
return -1;
if ( _materialIdsByName.TryGetValue( material.Name, out var existingId ) )
return existingId;
int id = _materialId++;
_materialsById[id] = material;
_materialIdsByName[material.Name] = id;
return id;
}
private Material GetMaterial( int id )
{
_materialsById.TryGetValue( id, out var material );
return material;
}
private static Vector2 CalculateTextureSize( Material material )
{
Vector2 textureSize = 512;
if ( material is null )
return textureSize;
var width = material.Attributes.GetInt( "WorldMappingWidth" );
var height = material.Attributes.GetInt( "WorldMappingHeight" );
var texture = material.FirstTexture;
if ( texture != null )
{
textureSize = texture.Size;
if ( width > 0 ) textureSize.x = width / 0.25f;
if ( height > 0 ) textureSize.y = height / 0.25f;
}
return textureSize;
}
private bool IsEdgeSmooth( HalfEdgeHandle hEdge )
{
if ( IsEdgeOpen( hEdge ) )
return false;
const float epsilon = 0.00001f;
if ( _smoothingThreshold > (1.0f - epsilon) )
return false;
var mode = GetEdgeSmoothing( hEdge );
if ( mode == EdgeSmoothMode.Hard )
return false;
if ( mode == EdgeSmoothMode.Soft )
return true;
if ( _smoothingThreshold < epsilon )
return true;
var hFaceA = hEdge.Face;
if ( !hFaceA.IsValid )
return false;
var hFaceB = Topology.GetOppositeHalfEdge( hEdge ).Face;
if ( !hFaceB.IsValid )
return false;
ComputeFaceNormal( hFaceA, out var vNormalA );
ComputeFaceNormal( hFaceB, out var vNormalB );
return (vNormalA.Dot( vNormalB ) > (_smoothingThreshold + epsilon));
}
private HalfEdgeHandle GetNextFaceVertexConnectedToVertex( HalfEdgeHandle hEdge )
{
return Topology.GetOppositeHalfEdge( hEdge.NextEdge );
}
public bool GetEdgesConnectedToFace( FaceHandle hFace, out List edges )
{
return Topology.GetFullEdgesConnectedToFace( hFace, out edges );
}
private bool GetVerticesConnectedToEdge( HalfEdgeHandle hEdge, FaceHandle hFace, out VertexHandle hOutVertexA, out VertexHandle hOutVertexB )
{
hOutVertexA = VertexHandle.Invalid;
hOutVertexB = VertexHandle.Invalid;
Topology.GetHalfEdgesConnectedToFullEdge( hEdge, out var hHalfEdgeA, out var hHalfEdgeB );
if ( Topology.GetFaceConnectedToHalfEdge( hHalfEdgeA ) == hFace )
{
Topology.GetVerticesConnectedToHalfEdge( hHalfEdgeA, out hOutVertexA, out hOutVertexB );
return true;
}
if ( Topology.GetFaceConnectedToHalfEdge( hHalfEdgeB ) == hFace )
{
Topology.GetVerticesConnectedToHalfEdge( hHalfEdgeB, out hOutVertexA, out hOutVertexB );
return true;
}
return false;
}
public void GetVerticesConnectedToEdge( HalfEdgeHandle hEdge, out VertexHandle hOutVertexA, out VertexHandle hOutVertexB )
{
Topology.GetVerticesConnectedToFullEdge( hEdge, out hOutVertexA, out hOutVertexB );
}
public void GetEdgeVertexPositions( HalfEdgeHandle hEdge, Transform transform, out Vector3 outVertexA, out Vector3 outVertexB )
{
GetVerticesConnectedToEdge( hEdge, out var hVertexA, out var hVertexB );
outVertexA = transform.PointToWorld( GetVertexPosition( hVertexA ) );
outVertexB = transform.PointToWorld( GetVertexPosition( hVertexB ) );
}
private HalfEdgeHandle FindEdgeConnectingFaces( FaceHandle hFaceA, FaceHandle hFaceB )
{
return Topology.FindEdgeConnectingFaces( hFaceA, hFaceB );
}
public FaceHandle GetOppositeFaceConnectedToEdge( HalfEdgeHandle hEdge, FaceHandle hFace )
{
return Topology.GetOppositeFaceConnectedToFullEdge( hEdge, hFace );
}
private void FindVerticesConnectedToEdges( IReadOnlyList edgeList, out VertexHandle[] outVertices )
{
Topology.FindVerticesConnectedToFullEdges( edgeList, out outVertices );
}
private void FindFacesConnectedToVertices( IReadOnlyList hVertices, int nNumVertices, out FaceHandle[] newFaces, out int[] faceVertexCounts )
{
Topology.FindFacesConnectedToVertices( hVertices, nNumVertices, out newFaces, out faceVertexCounts );
}
private void FindEdgesConnectedToFaces( IReadOnlyList pFaceList, int nNumFaces, out HalfEdgeHandle[] newEdges, out int[] edgeFaceCounts )
{
Topology.FindFullEdgesConnectedToFaces( pFaceList, nNumFaces, out newEdges, out edgeFaceCounts );
}
private HalfEdgeHandle FindOppositeEdgeInFace( FaceHandle hFace, HalfEdgeHandle hEdge )
{
Topology.GetHalfEdgesConnectedToFullEdge( hEdge, out var hHalfEdgeA, out var hHalfEdgeB );
if ( Topology.GetFaceConnectedToHalfEdge( hHalfEdgeA ) == hFace )
{
return Topology.GetFullEdgeForHalfEdge( Topology.FindOppositeHalfEdgeInFace( hHalfEdgeA ) );
}
else if ( Topology.GetFaceConnectedToHalfEdge( hHalfEdgeB ) == hFace )
{
return Topology.GetFullEdgeForHalfEdge( Topology.FindOppositeHalfEdgeInFace( hHalfEdgeB ) );
}
return HalfEdgeHandle.Invalid;
}
private bool GetVerticesConnectedToFace( FaceHandle hFace, out VertexHandle[] vertices )
{
return Topology.GetVerticesConnectedToFace( hFace, out vertices );
}
private HalfEdgeHandle GetFirstVertexInFace( FaceHandle hFace )
{
return Topology.GetFirstEdgeInFaceLoop( hFace );
}
private HalfEdgeHandle FindPreviousVertexInFace( HalfEdgeHandle hFaceVertex )
{
return Topology.FindPreviousEdgeInFaceLoop( hFaceVertex );
}
private void FindCornerVerticesForFace( FaceHandle hFace, float minCornerAngle, out List outCornerVertices )
{
outCornerVertices = new List();
var threshold = MathF.Cos( minCornerAngle.Clamp( 0.0f, 180.0f ).DegreeToRadian() );
var hStartFaceVertex = GetFirstVertexInFace( hFace );
var hCurrentFaceVertex = hStartFaceVertex;
var hPreviousFaceVertex = FindPreviousVertexInFace( hCurrentFaceVertex );
var hNextFaceVertex = GetNextVertexInFace( hCurrentFaceVertex );
do
{
var hVertexPrev = GetVertexConnectedToFaceVertex( hPreviousFaceVertex );
var hVertexCurr = GetVertexConnectedToFaceVertex( hCurrentFaceVertex );
var hVertexNext = GetVertexConnectedToFaceVertex( hNextFaceVertex );
var posPrev = GetVertexPosition( hVertexPrev );
var posCurr = GetVertexPosition( hVertexCurr );
var posNext = GetVertexPosition( hVertexNext );
var dirIn = (posCurr - posPrev).Normal;
var dirOut = (posNext - posCurr).Normal;
var dot = dirIn.Dot( dirOut );
if ( dot <= threshold )
{
outCornerVertices.Add( hVertexCurr );
}
hPreviousFaceVertex = hCurrentFaceVertex;
hCurrentFaceVertex = hNextFaceVertex;
hNextFaceVertex = GetNextVertexInFace( hCurrentFaceVertex );
}
while ( hCurrentFaceVertex != hStartFaceVertex );
}
public void QuadSliceFaces( IReadOnlyList faces, int cutsX, int cutsY, float minCornerAngleDegrees, List outNewFaceList )
{
var numFaces = faces.Count;
var newFaces = new List( numFaces * (cutsX + 1) * (cutsY + 1) );
foreach ( var hFace in faces )
{
GetVerticesConnectedToFace( hFace, out var vertices );
if ( vertices.Length != 4 )
{
// If the face isn't actually a quad, see if there are 4 distinct corner
// vertices that can be identified based on the shape.
FindCornerVerticesForFace( hFace, minCornerAngleDegrees, out var cornerVertices );
vertices = cornerVertices.ToArray();
}
// If we didn't identify exactly 4 corner vertices the operation cannot be applied to the face
if ( vertices.Length != 4 )
continue;
QuadSliceFace( hFace, vertices, cutsX, cutsY, newFaces );
}
// Mark the new faces as having their uv texture coordinates explicitly set so that the
// texture parameters will be recomputed from the uvs.
ComputeFaceTextureCoordinatesFromParameters( newFaces );
if ( outNewFaceList is not null )
{
outNewFaceList.Clear();
outNewFaceList.AddRange( newFaces );
}
}
private bool QuadSliceFace( FaceHandle hFace, IReadOnlyList cornerVertices, int numCutsX, int numCutsY, List outNewFaces = null,
List outEdgeSpansX = null, List outEdgeSpansY = null, List outVertices = null, List outVertexParameters = null )
{
var hFaceVertices = new HalfEdgeHandle[4];
if ( FindFaceVerticesInWindingOrder( hFace, cornerVertices, 4, hFaceVertices ) == false )
return false;
var edgeSpanX1 = new EdgeSpan();
var edgeSpanX2 = new EdgeSpan();
edgeSpanX1.InitializeFromFace( this, hFace, hFaceVertices[3], hFaceVertices[0] );
edgeSpanX1.Reverse();
edgeSpanX2.InitializeFromFace( this, hFace, hFaceVertices[1], hFaceVertices[2] );
var edgeSpanY1 = new EdgeSpan();
var edgeSpanY2 = new EdgeSpan();
edgeSpanY1.InitializeFromFace( this, hFace, hFaceVertices[0], hFaceVertices[1] );
edgeSpanY2.InitializeFromFace( this, hFace, hFaceVertices[2], hFaceVertices[3] );
edgeSpanY2.Reverse();
var edgeSpansX = new List();
edgeSpansX.EnsureCapacity( 2 + numCutsX );
edgeSpansX.Add( edgeSpanX1 );
var edgeSpansY = new List();
edgeSpansY.EnsureCapacity( 2 + numCutsY );
edgeSpansY.Add( edgeSpanY1 );
edgeSpansY.Add( edgeSpanY2 );
if ( AddEdgesConnectingSpans( edgeSpansY, edgeSpansY.Count, numCutsX, edgeSpansX, outNewFaces ) == false )
return false;
edgeSpansX.Add( edgeSpanX2 );
// Remove the last y edge span so that the new ones can be inserted before it.
edgeSpansY.RemoveAt( edgeSpansY.Count - 1 );
if ( AddEdgesConnectingSpans( edgeSpansX, edgeSpansX.Count, numCutsY, edgeSpansY, outNewFaces ) == false )
return false;
// Re-add the last x edge span
edgeSpansY.Add( edgeSpanY2 );
Assert.True( edgeSpansX.Count == (2 + numCutsX) );
Assert.True( edgeSpansY.Count == (2 + numCutsY) );
// Since the operation only adds new edges to faces, which creates
// one new face each time, the original face is still in use.
outNewFaces?.Add( hFace );
if ( outEdgeSpansX is not null )
{
outEdgeSpansX.Clear();
outEdgeSpansX.AddRange( edgeSpansY );
}
if ( outEdgeSpansY is not null )
{
outEdgeSpansY.Clear();
outEdgeSpansY.AddRange( edgeSpansX );
}
// If requested, get all of the vertices from the spans and
// optionally compute their [ 0, 1 ] coordinates within the quad.
if ( outVertices is not null || outVertexParameters is not null )
{
var nNumTotalVertices = (2 + numCutsX) * (2 + numCutsY);
var allVertices = new List( nNumTotalVertices );
var vertexParameters = new List( nNumTotalVertices );
var nNumSpansX = edgeSpansX.Count;
for ( int iX = 0; iX < nNumSpansX; ++iX )
{
var edgeSpan = edgeSpansX[iX];
var nNumSpanVertices = edgeSpan.NumVertices;
for ( int iY = 0; iY < nNumSpanVertices; ++iY )
{
var hVertex = edgeSpan.GetVertex( iY );
allVertices.Add( hVertex );
vertexParameters.Add( new Vector2( iX / (float)(nNumSpansX - 1), iY / (float)(nNumSpanVertices - 1) ) );
}
}
if ( outVertices is not null )
{
outVertices.Clear();
outVertices.AddRange( allVertices );
}
if ( outVertexParameters is not null )
{
outVertexParameters.Clear();
outVertexParameters.AddRange( vertexParameters );
}
}
return true;
}
private FaceHandle FindFaceConnectingEdges( HalfEdgeHandle hEdgeA, HalfEdgeHandle hEdgeB )
{
return Topology.FindFaceConnectingFullEdges( hEdgeA, hEdgeB );
}
private void GetFaceTriangulatedPositions( FaceHandle hFace, Transform mTransform, out Vector3[] pOutPositions, out int[] pOutIndices )
{
pOutPositions = GetFaceVertexPositions( hFace, mTransform ).ToArray();
pOutIndices = null;
int nNumFaceVertices = pOutPositions.Length;
if ( nNumFaceVertices < 3 )
return;
PlaneEquation( pOutPositions, out _, out _ );
pOutIndices = Mesh.TriangulatePolygon( pOutPositions ).ToArray();
}
private bool IsFaceShapeValid( FaceHandle hFace )
{
if ( hFace == FaceHandle.Invalid )
return false;
GetFaceTriangulatedPositions( hFace, Transform.Zero, out var positions, out var indices );
int nNumExpectedIndices = (positions.Length - 2) * 3;
return indices.Length == nNumExpectedIndices;
}
public void RemoveBadFaces()
{
foreach ( var hFace in FaceHandles )
{
if ( IsFaceShapeValid( hFace ) )
continue;
if ( Topology.RemoveFace( hFace, true ) )
{
IsDirty = true;
}
}
}
private bool AddEdgesConnectingSpans( IReadOnlyList pEdgeSpans, int nNumSpans, int nNumEdgesToAdd, List pOutEdgeSpans, List pOutNewFaces )
{
// Must have at least two spans
if ( nNumSpans < 2 )
return false;
var pMesh = pEdgeSpans[0].Mesh;
if ( pMesh is null )
return false;
// All spans must be from the same mesh
for ( int iSpan = 0; iSpan < nNumSpans; ++iSpan )
{
if ( pEdgeSpans[iSpan].Mesh != pMesh )
return false;
}
var newVerticesOnSpan = new int[nNumSpans][];
// Add the required vertices to all of the spans
for ( int iSpan = 0; iSpan < nNumSpans; ++iSpan )
{
pEdgeSpans[iSpan].AddVertices( nNumEdgesToAdd, out newVerticesOnSpan[iSpan] );
}
// Add edges between the new vertices on each successive pair of spans
for ( int iEdgeToAdd = 0; iEdgeToAdd < nNumEdgesToAdd; ++iEdgeToAdd )
{
var hEdgeA = pEdgeSpans[0].GetEdge( newVerticesOnSpan[0][iEdgeToAdd] );
var hVertexA = pEdgeSpans[0].GetVertex( newVerticesOnSpan[0][iEdgeToAdd] );
var vertices = new List( nNumSpans )
{
hVertexA
};
for ( int iSpan = 1; iSpan < nNumSpans; ++iSpan )
{
var hEdgeB = pEdgeSpans[iSpan].GetEdge( newVerticesOnSpan[iSpan][iEdgeToAdd] );
var hVertexB = pEdgeSpans[iSpan].GetVertex( newVerticesOnSpan[iSpan][iEdgeToAdd] );
var hFace = pMesh.FindFaceConnectingEdges( hEdgeA, hEdgeB );
if ( hFace == FaceHandle.Invalid )
break;
pMesh.AddEdgeToFace( hFace, hVertexA, hVertexB, out var hNewEdge );
if ( hNewEdge == HalfEdgeHandle.Invalid )
break;
var hNewFace = pMesh.GetOppositeFaceConnectedToEdge( hNewEdge, hFace );
if ( (hNewFace != FaceHandle.Invalid) && (pOutNewFaces is not null) )
{
pOutNewFaces.Add( hNewFace );
}
vertices.Add( hVertexB );
hEdgeA = hEdgeB;
hVertexA = hVertexB;
}
if ( (vertices.Count == nNumSpans) && (pOutEdgeSpans is not null) )
{
var pNewEdgeSpan = new EdgeSpan();
pNewEdgeSpan.InitializeFromVertices( pMesh, vertices );
pOutEdgeSpans.Add( pNewEdgeSpan );
}
}
return true;
}
private bool FindFaceVerticesInWindingOrder( FaceHandle hFace, IReadOnlyList pVertices, int nNumVertices, HalfEdgeHandle[] pOutFaceVertices )
{
if ( nNumVertices <= 0 )
return false;
var faceVertices = new HalfEdgeHandle[nNumVertices];
// Find the face vertices corresponding to the provided vertices
for ( int i = 0; i < nNumVertices; ++i )
{
faceVertices[i] = FindFaceVertexConnectedToVertex( pVertices[i], hFace );
if ( faceVertices[i] == HalfEdgeHandle.Invalid )
return false;
}
int nVertexOut = 0;
var hCurentFaceVertex = faceVertices[0];
do
{
var nVertexIndex = Array.IndexOf( faceVertices, hCurentFaceVertex );
if ( nVertexIndex != -1 )
{
pOutFaceVertices[nVertexOut++] = faceVertices[nVertexIndex];
}
// Stop once they have all been found
if ( nVertexOut == nNumVertices )
return true;
hCurentFaceVertex = GetNextVertexInFace( hCurentFaceVertex );
}
while ( hCurentFaceVertex != faceVertices[0] );
// Didn't find all the vertices in the face (this shouldn't happen).
return false;
}
private void ApplyOffsetToFaceUVs( FaceHandle hFace, Vector2 vOffset )
{
GetFaceVerticesConnectedToFace( hFace, out var faceVertices );
foreach ( var hVertex in faceVertices )
{
var uv = TextureCoord[hVertex];
uv += vOffset;
TextureCoord[hVertex] = uv;
}
}
public void AverageEdgeUVs( IReadOnlyList edges )
{
if ( edges.Count <= 0 )
return;
foreach ( var hEdge in edges )
{
// Get the two faces connected to the edge
Topology.GetFacesConnectedToFullEdge( hEdge, out var hFace1, out var hFace2 );
// Get the two vertices connected to the edge
GetVerticesConnectedToEdge( hEdge, out var hVertexA, out var hVertexB );
// Get the two pairs of face vertices
var hFaceVertexA1 = FindFaceVertexConnectedToVertex( hVertexA, hFace1 );
var hFaceVertexA2 = FindFaceVertexConnectedToVertex( hVertexA, hFace2 );
var hFaceVertexB1 = FindFaceVertexConnectedToVertex( hVertexB, hFace1 );
var hFaceVertexB2 = FindFaceVertexConnectedToVertex( hVertexB, hFace2 );
var uvA1 = TextureCoord[hFaceVertexA1];
var uvA2 = TextureCoord[hFaceVertexA2];
var uvB1 = TextureCoord[hFaceVertexB1];
var uvB2 = TextureCoord[hFaceVertexB2];
// Find the uv at the center of the edge on each face
var uvCenterEdge1 = (uvA1 + uvB1) * 0.5f;
var uvCenterEdge2 = (uvA2 + uvB2) * 0.5f;
// Compute the integer offset that will place center of edge2 nearest the center of edge1 and
// then apply that offset to all of the uvs in face2. Since the offset is an integer value
// and we apply the offset to all uvs of the face, this won't visually change the texturing.
var delta = uvCenterEdge1 - uvCenterEdge2;
var deltaX = (int)(delta.x + (delta.x < 0 ? -0.5f : 0.5f));
var deltaY = (int)(delta.y + (delta.y < 0 ? -0.5f : 0.5f));
ApplyOffsetToFaceUVs( hFace2, new Vector2( deltaX, deltaY ) );
// Read the offset uv values
var uvOffsetA1 = TextureCoord[hFaceVertexA1];
var uvOffsetA2 = TextureCoord[hFaceVertexA2];
var uvOffsetB1 = TextureCoord[hFaceVertexB1];
var uvOffsetB2 = TextureCoord[hFaceVertexB2];
// Compute the average for each vertex
var averageA = (uvOffsetA1 + uvOffsetA2) * 0.5f;
TextureCoord[hFaceVertexA1] = averageA;
TextureCoord[hFaceVertexA2] = averageA;
var averageB = (uvOffsetB1 + uvOffsetB2) * 0.5f;
TextureCoord[hFaceVertexB1] = averageB;
TextureCoord[hFaceVertexB2] = averageB;
ComputeFaceTextureParametersFromCoordinates( new[] { hFace1, hFace2 } );
}
IsDirty = true;
}
public void AverageVertexUVs( IReadOnlyList vertices )
{
if ( vertices.Count <= 0 )
return;
foreach ( var hVertex in vertices )
{
// Get all of the faces connected to the vertex. We get the faces and then the face vertices
// from the faces because there may be face vertices with no corresponding face for open
// edges and we only want the face vertices connected to valid faces.
GetFacesConnectedToVertex( hVertex, out var connectedFaces );
var numFaces = connectedFaces.Count;
if ( numFaces <= 0 )
continue;
// Get the face vertices corresponding to each face
var numFaceVertices = numFaces;
var connectedFaceVertices = new HalfEdgeHandle[numFaceVertices];
for ( int i = 0; i < numFaces; ++i )
{
connectedFaceVertices[i] = FindFaceVertexConnectedToVertex( hVertex, connectedFaces[i] );
}
// First offset the uvs of all of the faces (in 1 unit increments) connected to the vertex
// so that the uvs of face vertices connected to the target vertex are as close together as
// possible.
var uv0 = TextureCoord[connectedFaceVertices[0]];
for ( int i = 1; i < numFaceVertices; ++i )
{
var hFaceVertex = connectedFaceVertices[i];
var hFace = connectedFaces[i];
var uv = TextureCoord[hFaceVertex];
var delta = uv0 - uv;
var deltaX = (int)(delta.x + (delta.x < 0 ? -0.5f : 0.5f));
var deltaY = (int)(delta.y + (delta.y < 0 ? -0.5f : 0.5f));
ApplyOffsetToFaceUVs( hFace, new Vector2( deltaX, deltaY ) );
}
// Once the uvs have been moved to be as close as possible average the remaining difference
var uvSum = Vector2.Zero;
for ( int i = 0; i < numFaceVertices; ++i )
{
var hFaceVertex = connectedFaceVertices[i];
var uv = TextureCoord[hFaceVertex];
uvSum += uv;
}
var uvAverage = uvSum / numFaceVertices;
for ( int i = 0; i < numFaceVertices; ++i )
{
var hFaceVertex = connectedFaceVertices[i];
TextureCoord[hFaceVertex] = uvAverage;
}
ComputeFaceTextureParametersFromCoordinates( connectedFaces );
}
IsDirty = true;
}
public static void GetBestPlanesForEdgeBetweenFaces( PolygonMesh pMesh1, FaceHandle hFace1, Transform mLocalToWorld1,
PolygonMesh pMesh2, FaceHandle hFace2, Transform mLocalToWorld2,
out Plane pOutPlane1, out Plane pOutPlane2 )
{
var hEdge1 = HalfEdgeHandle.Invalid;
var hEdge2 = HalfEdgeHandle.Invalid;
if ( pMesh1 == pMesh2 )
{
var hSharedEdge = pMesh1.FindEdgeConnectingFaces( hFace1, hFace2 );
if ( hSharedEdge != HalfEdgeHandle.Invalid )
{
hEdge1 = hSharedEdge;
hEdge2 = hSharedEdge;
}
}
if ( hEdge1 == HalfEdgeHandle.Invalid )
{
if ( !GetNearestEdgesBetweenFaces( pMesh1, hFace1, mLocalToWorld1, pMesh2, hFace2, mLocalToWorld2, out hEdge1, out hEdge2 ) )
{
pMesh1.GetFacePlane( hFace1, mLocalToWorld1, out pOutPlane1 );
pMesh2.GetFacePlane( hFace2, mLocalToWorld2, out pOutPlane2 );
return;
}
}
pMesh1.GetFacePlaneUsingEdge( hFace1, hEdge1, mLocalToWorld1, out pOutPlane1 );
pMesh2.GetFacePlaneUsingEdge( hFace2, hEdge2, mLocalToWorld2, out pOutPlane2 );
}
public static bool GetNearestEdgesBetweenFaces( PolygonMesh pMesh1, FaceHandle hFace1, Transform mLocalToWorld1,
PolygonMesh pMesh2, FaceHandle hFace2, Transform mLocalToWorld2,
out HalfEdgeHandle pOutEdge1, out HalfEdgeHandle pOutEdge2 )
{
pOutEdge1 = HalfEdgeHandle.Invalid;
pOutEdge2 = HalfEdgeHandle.Invalid;
pMesh1.GetEdgesConnectedToFace( hFace1, out var edgesForFace1 );
pMesh2.GetEdgesConnectedToFace( hFace2, out var edgesForFace2 );
var nNumEdgesForFace1 = edgesForFace1.Count;
var nNumEdgesForFace2 = edgesForFace2.Count;
var flBestDistSq = 3.402823466e+38F;
var bFound = false;
for ( int iEdge1 = 0; iEdge1 < nNumEdgesForFace1; ++iEdge1 )
{
var hEdge1 = edgesForFace1[iEdge1];
pMesh1.GetEdgeVertexPositions( hEdge1, mLocalToWorld1, out var vEdge1PosA, out var vEdge1PosB );
var vEdge1Norm = (vEdge1PosB - vEdge1PosA).Normal;
for ( int iEdge2 = 0; iEdge2 < nNumEdgesForFace2; ++iEdge2 )
{
var hEdge2 = edgesForFace2[iEdge2];
pMesh2.GetEdgeVertexPositions( hEdge2, mLocalToWorld2, out var vEdge2PosA, out var vEdge2PosB );
var vEdge2Norm = (vEdge2PosB - vEdge2PosA).Normal;
var flDot = vEdge1Norm.Dot( vEdge2Norm );
if ( MathF.Abs( flDot ) >= 0.95f )
{
CalcLineToLineIntersectionSegment( vEdge1PosA, vEdge1PosB, vEdge2PosA, vEdge2PosB, out var vDistA, out var vDistB );
float flDistSq = (vDistA - vDistB).LengthSquared;
if ( flDistSq < flBestDistSq )
{
flBestDistSq = flDistSq;
pOutEdge1 = hEdge1;
pOutEdge2 = hEdge2;
bFound = true;
}
}
}
}
return bFound;
}
private static bool CalcLineToLineIntersectionSegment( Vector3 pt1, Vector3 pt2, Vector3 pt3, Vector3 pt4, out Vector3 s1, out Vector3 s2 )
{
s1 = default;
s2 = default;
var p13 = new Vector3( pt1.x - pt3.x, pt1.y - pt3.y, pt1.z - pt3.z );
var p43 = new Vector3( pt4.x - pt3.x, pt4.y - pt3.y, pt4.z - pt3.z );
var p21 = new Vector3( pt2.x - pt1.x, pt2.y - pt1.y, pt2.z - pt1.z );
const float eps = 0.000001f;
if ( MathF.Abs( p43.x ) < eps && MathF.Abs( p43.y ) < eps && MathF.Abs( p43.z ) < eps )
return false;
if ( MathF.Abs( p21.x ) < eps && MathF.Abs( p21.y ) < eps && MathF.Abs( p21.z ) < eps )
return false;
var d1343 = p13.x * p43.x + p13.y * p43.y + p13.z * p43.z;
var d4321 = p43.x * p21.x + p43.y * p21.y + p43.z * p21.z;
var d1321 = p13.x * p21.x + p13.y * p21.y + p13.z * p21.z;
var d4343 = p43.x * p43.x + p43.y * p43.y + p43.z * p43.z;
var d2121 = p21.x * p21.x + p21.y * p21.y + p21.z * p21.z;
var denom = d2121 * d4343 - d4321 * d4321;
if ( MathF.Abs( denom ) < eps )
return false;
var numer = d1343 * d4321 - d1321 * d4343;
var t1 = numer / denom;
var t2 = (d1343 + d4321 * t1) / d4343;
s1.x = pt1.x + t1 * p21.x;
s1.y = pt1.y + t1 * p21.y;
s1.z = pt1.z + t1 * p21.z;
s2.x = pt3.x + t2 * p43.x;
s2.y = pt3.y + t2 * p43.y;
s2.z = pt3.z + t2 * p43.z;
return true;
}
private static void CalcClosestPointOnLineSegment( Vector3 P, Vector3 A, Vector3 B, out Vector3 closest, out float t )
{
var dir = B - A;
var div = dir.Dot( dir );
t = (div < 0.00001f) ? 0.0f : Math.Clamp( dir.Dot( P - A ) / div, 0.0f, 1.0f );
closest = A + dir * t;
}
public void GetFacePlaneUsingEdge( FaceHandle hFace, HalfEdgeHandle hEdge, Transform transform, out Plane outPlane )
{
GetVerticesConnectedToEdge( hEdge, hFace, out var hVertA, out var hVertB );
GetVertexPosition( hVertA, transform, out var vPosA );
GetVertexPosition( hVertB, transform, out var vPosB );
var line = new Line( vPosA, vPosB );
var positions = GetFaceVertexPositions( hFace, transform ).ToArray();
var nNumVertices = positions.Length;
var flMaxDist = 0.0f;
var vFurthestPoint = vPosA;
for ( var i = 0; i < nNumVertices; ++i )
{
var flDist = line.Distance( positions[i] );
if ( flDist > flMaxDist )
{
flMaxDist = flDist;
vFurthestPoint = positions[i];
}
}
var vEdgeDir = (vPosB - vPosA).Normal;
var vOtherPointDir = (vFurthestPoint - vPosA).Normal;
var vNormal = vEdgeDir.Cross( vOtherPointDir ).Normal;
outPlane = new Plane( vPosA, vNormal );
}
private void GetFacePlane( FaceHandle hFace, Transform transform, out Plane pOutPlane )
{
var positions = GetFaceVertexPositions( hFace, transform ).ToArray();
PlaneEquation( positions, out var normal, out var distance );
pOutPlane = new Plane( normal, -distance );
}
private static void PlaneEquation( IReadOnlyList pVerts, out Vector3 pOutNormal, out float pOutPlaneDistance )
{
var refpt = Vector3.Zero;
var vNormal = Vector3.Zero;
var nVertCount = pVerts.Count;
for ( var i = 0; i < nVertCount; i++ )
{
var pU = pVerts[i];
var pV = pVerts[(i + 1) % nVertCount];
vNormal.x += (pU.y - pV.y) * (pU.z + pV.z);
vNormal.y += (pU.z - pV.z) * (pU.x + pV.x);
vNormal.z += (pU.x - pV.x) * (pU.y + pV.y);
refpt += pU;
}
var len = vNormal.Length + 1.192092896e-07F;
pOutNormal = vNormal * (1.0f / len);
len *= nVertCount;
pOutPlaneDistance = -Vector3.Dot( refpt, vNormal ) / len;
}
private Vector3 ComputeFaceVertexNormal( HalfEdgeHandle hTargetFaceVertex )
{
var hFirstFaceVertex = hTargetFaceVertex;
var hLastFaceVertex = hTargetFaceVertex;
var hCurrentFaceVertex = hTargetFaceVertex;
var foundHardEdge = false;
do
{
var hNextFaceVertex = GetNextFaceVertexConnectedToVertex( hCurrentFaceVertex );
var hEdge = Topology.GetFullEdgeForHalfEdge( hNextFaceVertex );
if ( !foundHardEdge )
{
hLastFaceVertex = hNextFaceVertex;
}
if ( !IsEdgeSmooth( hEdge ) )
{
foundHardEdge = true;
hFirstFaceVertex = hNextFaceVertex;
}
hCurrentFaceVertex = hNextFaceVertex;
}
while ( hCurrentFaceVertex != hTargetFaceVertex );
var normal = Vector3.Zero;
hCurrentFaceVertex = hFirstFaceVertex;
do
{
var hFace = hCurrentFaceVertex.Face;
if ( hFace.IsValid )
{
ComputeFaceNormal( hFace, out var faceNormal );
normal += faceNormal;
}
hCurrentFaceVertex = GetNextFaceVertexConnectedToVertex( hCurrentFaceVertex );
}
while ( hCurrentFaceVertex != hLastFaceVertex );
return normal.Normal;
}
public HalfEdgeHandle GetOppositeHalfEdge( HalfEdgeHandle hEdge )
{
if ( !hEdge.IsValid )
return HalfEdgeHandle.Invalid;
return Topology.GetOppositeHalfEdge( hEdge );
}
public enum ExtentType
{
XMin,
XMax,
YMin,
YMax,
ZMin,
ZMax
}
public class FaceExtents
{
private readonly Vector3[] _extents = new Vector3[6];
public FaceExtents()
{
for ( int i = 0; i < _extents.Length; i++ )
{
_extents[i] = i % 2 == 0 ? new Vector3( float.MaxValue, float.MaxValue, float.MaxValue ) :
new Vector3( float.MinValue, float.MinValue, float.MinValue );
}
}
public void AddPoint( Vector3 position )
{
if ( position.x < _extents[(int)ExtentType.XMin].x ) _extents[(int)ExtentType.XMin] = position;
if ( position.x > _extents[(int)ExtentType.XMax].x ) _extents[(int)ExtentType.XMax] = position;
if ( position.y < _extents[(int)ExtentType.YMin].y ) _extents[(int)ExtentType.YMin] = position;
if ( position.y > _extents[(int)ExtentType.YMax].y ) _extents[(int)ExtentType.YMax] = position;
if ( position.z < _extents[(int)ExtentType.ZMin].z ) _extents[(int)ExtentType.ZMin] = position;
if ( position.z > _extents[(int)ExtentType.ZMax].z ) _extents[(int)ExtentType.ZMax] = position;
}
public Vector3 Get( ExtentType type ) => _extents[(int)type];
public void AddExtents( FaceExtents other )
{
if ( other._extents[(int)ExtentType.XMin].x < _extents[(int)ExtentType.XMin].x )
_extents[(int)ExtentType.XMin] = other._extents[(int)ExtentType.XMin];
if ( other._extents[(int)ExtentType.XMax].x > _extents[(int)ExtentType.XMax].x )
_extents[(int)ExtentType.XMax] = other._extents[(int)ExtentType.XMax];
if ( other._extents[(int)ExtentType.YMin].y < _extents[(int)ExtentType.YMin].y )
_extents[(int)ExtentType.YMin] = other._extents[(int)ExtentType.YMin];
if ( other._extents[(int)ExtentType.YMax].y > _extents[(int)ExtentType.YMax].y )
_extents[(int)ExtentType.YMax] = other._extents[(int)ExtentType.YMax];
if ( other._extents[(int)ExtentType.ZMin].z < _extents[(int)ExtentType.ZMin].z )
_extents[(int)ExtentType.ZMin] = other._extents[(int)ExtentType.ZMin];
if ( other._extents[(int)ExtentType.ZMax].z > _extents[(int)ExtentType.ZMax].z )
_extents[(int)ExtentType.ZMax] = other._extents[(int)ExtentType.ZMax];
}
}
public static void GetTextureExtents( Vector4 vAxisU, Vector4 vAxisV, Vector2 vScale, FaceExtents extents, out Vector2 topLeft, out Vector2 bottomRight )
{
bool first = true;
topLeft = Vector2.Zero;
bottomRight = Vector2.Zero;
for ( var nPoint = 0; nPoint < 6; nPoint++ )
{
var test = new Vector2( Vector3.Dot( extents.Get( (ExtentType)nPoint ), (Vector3)vAxisU ) / vScale.x,
Vector3.Dot( extents.Get( (ExtentType)nPoint ), (Vector3)vAxisV ) / vScale.y );
if ( (test.x < topLeft.x) || first )
topLeft.x = test.x;
if ( (test.y < topLeft.y) || first )
topLeft.y = test.y;
if ( (test.x > bottomRight.x) || first )
bottomRight.x = test.x;
if ( (test.y > bottomRight.y) || first )
bottomRight.y = test.y;
first = false;
}
}
public enum TextureJustification
{
None = 0,
Top,
Bottom,
Left,
Center,
Right,
Fit,
FitX,
FitY,
};
void JustifyTextureUsingExtents( TextureJustification eJustification, FaceExtents Extents, Vector2 vTextureDimensions, ref Vector4 vAxisU, ref Vector4 vAxisV, ref Vector2 vScale )
{
if ( vScale.x.AlmostEqual( 0.0f ) )
vScale.x = 512;
if ( vScale.y.AlmostEqual( 0.0f ) )
vScale.y = 512;
if ( eJustification == TextureJustification.None )
{
vAxisU.w = 0;
vAxisV.w = 0;
return;
}
if ( eJustification == TextureJustification.Fit ||
eJustification == TextureJustification.FitX )
{
vScale.x = 1.0f;
}
if ( eJustification == TextureJustification.Fit ||
eJustification == TextureJustification.FitY )
{
vScale.y = 1.0f;
}
GetTextureExtents( vAxisU, vAxisV, vScale, Extents, out var topLeft, out var bottomRight );
var Center = new Vector2( (topLeft.x + bottomRight.x) / 2, (topLeft.y + bottomRight.y) / 2 );
switch ( eJustification )
{
case TextureJustification.Top:
{
vAxisV.w = -topLeft.y;
break;
}
case TextureJustification.Bottom:
{
vAxisV.w = -bottomRight.y + vTextureDimensions.y;
break;
}
case TextureJustification.Left:
{
vAxisU.w = -topLeft.x;
break;
}
case TextureJustification.Right:
{
vAxisU.w = -bottomRight.x + vTextureDimensions.x;
break;
}
case TextureJustification.Center:
{
vAxisU.w = -Center.x + (vTextureDimensions.x * 0.5f);
vAxisV.w = -Center.y + (vTextureDimensions.y * 0.5f);
break;
}
case TextureJustification.Fit:
case TextureJustification.FitX:
case TextureJustification.FitY:
{
if ( eJustification != TextureJustification.FitX )
{
vScale.y *= (bottomRight.y - topLeft.y) / vTextureDimensions.y;
}
if ( eJustification != TextureJustification.FitY )
{
vScale.x *= (bottomRight.x - topLeft.x) / vTextureDimensions.x;
}
JustifyTextureUsingExtents( TextureJustification.Top, Extents, vTextureDimensions, ref vAxisU, ref vAxisV, ref vScale );
JustifyTextureUsingExtents( TextureJustification.Left, Extents, vTextureDimensions, ref vAxisU, ref vAxisV, ref vScale );
break;
}
}
}
public void JustifyFaceTextureParameters( IEnumerable hFaces, TextureJustification justification, FaceExtents extents )
{
var textureSizes = Materials.Select( CalculateTextureSize ).ToArray();
JustifyFaceTextureParameters( hFaces, justification, Transform, textureSizes, extents );
}
private void JustifyFaceTextureParameters( IEnumerable hFaces, TextureJustification justification, Transform transform, IReadOnlyList textureSizes, FaceExtents extents )
{
foreach ( var hFace in hFaces )
{
var faceExtents = extents;
if ( faceExtents is null )
{
faceExtents = new FaceExtents();
UnionExtentsForFaces( new[] { hFace }, transform, faceExtents );
}
var offset = TextureOffset[hFace];
var axisU = new Vector4( TextureUAxis[hFace], offset.x );
var axisV = new Vector4( TextureVAxis[hFace], offset.y );
var scale = TextureScale[hFace];
var materialIndex = MaterialIndex[hFace];
var textureSize = materialIndex >= 0 && materialIndex < textureSizes.Count ? textureSizes[materialIndex] : DefaultTextureSize;
JustifyTextureUsingExtents( justification, faceExtents, textureSize, ref axisU, ref axisV, ref scale );
SetFaceTextureParameters( hFace, axisU, axisV, scale );
}
}
public void UnionExtentsForFaces( IEnumerable hFaces, Transform transform, FaceExtents extents )
{
if ( extents is null )
return;
foreach ( var hFace in hFaces )
{
var positions = GetFaceVertexPositions( hFace, transform );
foreach ( var position in positions )
extents.AddPoint( position );
}
}
private HalfEdgeHandle FindProceedingHalfEdgeEndingAtVertex( HalfEdgeHandle hStartEdge, VertexHandle hEndVertex )
{
var hCurrentEdge = hStartEdge;
var hIncomingEdge = HalfEdgeHandle.Invalid;
do
{
if ( Topology.GetEndVertexConnectedToEdge( hCurrentEdge ) == hEndVertex )
{
hIncomingEdge = hCurrentEdge;
}
hCurrentEdge = Topology.GetNextEdgeInFaceLoop( hCurrentEdge );
}
while ( hCurrentEdge != hStartEdge );
return hIncomingEdge;
}
private bool GetFaceVertexPositionsStartingAtVertex( HalfEdgeHandle hFaceVertex, Vector3[] outVertexPositions )
{
var maxVertices = outVertexPositions.Length;
var sum = Vector3.Zero;
// Iterate all of the vertices in face of the specified face
var hFirstFaceVertex = hFaceVertex;
var hCurrentFaceVertex = hFirstFaceVertex;
var numVertices = 0;
do
{
if ( numVertices < maxVertices )
{
var hVertex = GetVertexConnectedToFaceVertex( hCurrentFaceVertex );
var position = GetVertexPosition( hVertex );
outVertexPositions[numVertices] = position;
sum += position;
}
++numVertices;
hCurrentFaceVertex = GetNextVertexInFace( hCurrentFaceVertex );
}
while ( hCurrentFaceVertex != hFirstFaceVertex );
return numVertices <= maxVertices;
}
private void TriangulateFace( FaceHandle hFace, Submesh submesh )
{
if ( !hFace.IsValid )
return;
var vertexPositions = Topology.GetFaceVertices( hFace )
.Select( x => Positions[x] )
.ToArray();
var faceIndices = Mesh.TriangulatePolygon( vertexPositions );
if ( faceIndices.Length < 3 )
return;
if ( faceIndices.Length % 3 != 0 )
return;
var triangleCount = faceIndices.Length / 3;
var vertices = submesh.Vertices;
var triangles = submesh.Indices;
var uvDensity = submesh.UvDensity;
int startVertex = vertices.Count;
int startCollisionVertex = _meshVertices.Count;
var tangent = TextureUAxis[hFace].Normal;
var bitangent = TextureVAxis[hFace].Normal;
GetFaceVerticesConnectedToFace( hFace, out var faceEdges );
for ( var i = 0; i < faceEdges.Length; ++i )
{
var n = ComputeFaceVertexNormal( faceEdges[i] );
// Project the face U axis onto the tangent plane of the smoothed normal
var t = tangent - n * Vector3.Dot( n, tangent );
// Fix orientation so cross(N,T) aligns with the bitangent direction
var b = bitangent;
if ( b.LengthSquared > 1e-12f )
{
var handed = Vector3.Dot( Vector3.Cross( n, t ), b.Normal );
if ( handed < 0.0f ) t = -t;
}
vertices.Add( new SimpleVertex
{
position = vertexPositions[i],
normal = n,
tangent = t,
texcoord = TextureCoord[faceEdges[i]],
} );
}
_meshVertices.AddRange( vertexPositions );
if ( startVertex == vertices.Count )
return;
var startIndex = _meshIndices.Count;
for ( int index = 0; index < triangleCount; ++index )
{
var triangle = index * 3;
var a = startVertex + faceIndices[triangle];
var b = startVertex + faceIndices[triangle + 1];
var c = startVertex + faceIndices[triangle + 2];
if ( a < 0 || a >= vertices.Count )
return;
if ( b < 0 || b >= vertices.Count )
return;
if ( c < 0 || c >= vertices.Count )
return;
var ab = vertices[b].position - vertices[a].position;
var ac = vertices[c].position - vertices[a].position;
var area = Vector3.Cross( ab, ac ).Length * 0.5f;
if ( area.AlmostEqual( 0.0f ) )
continue;
triangles.Add( a );
triangles.Add( b );
triangles.Add( c );
_triangleFaces.Add( hFace );
_meshIndices.Add( startCollisionVertex + faceIndices[triangle] );
_meshIndices.Add( startCollisionVertex + faceIndices[triangle + 1] );
_meshIndices.Add( startCollisionVertex + faceIndices[triangle + 2] );
var areaUV = CalculateTriangleAreaUV( vertices[a].texcoord, vertices[b].texcoord, vertices[c].texcoord );
if ( areaUV > 0.0f )
uvDensity.Add( MathF.Sqrt( area / areaUV ) );
}
_meshFaces.Add( hFace, new FaceMesh
{
VertexCount = _meshVertices.Count - startCollisionVertex,
IndexCount = _meshIndices.Count - startIndex,
VertexStart = startCollisionVertex,
IndexStart = startIndex,
} );
}
private static float CalculateTriangleAreaUV( Vector2 a, Vector2 b, Vector2 c )
{
return 0.5f * MathF.Abs( -b.x * a.y + c.x * a.y + a.x * b.y - c.x * b.y - a.x * c.y + b.x * c.y );
}
public Vertex[] CreateFace( FaceHandle hFace, Transform transform, Color color )
{
if ( _meshFaces == null || !_meshFaces.TryGetValue( hFace, out FaceMesh faceMesh ) )
return null;
return Enumerable.Range( faceMesh.IndexStart, faceMesh.IndexCount )
.Select( x => new Vertex( transform.PointToWorld( _meshVertices[_meshIndices[x]] ), color ) )
.ToArray();
}
private static readonly Vector3[] FaceNormals =
{
new( 0, 0, 1 ),
new( 0, 0, -1 ),
new( 0, -1, 0 ),
new( 0, 1, 0 ),
new( -1, 0, 0 ),
new( 1, 0, 0 ),
};
private static readonly Vector3[] FaceRightVectors =
{
new( 1, 0, 0 ),
new( 1, 0, 0 ),
new( 1, 0, 0 ),
new( -1, 0, 0 ),
new( 0, -1, 0 ),
new( 0, 1, 0 ),
};
private static readonly Vector3[] FaceDownVectors =
{
new( 0, -1, 0 ),
new( 0, -1, 0 ),
new( 0, 0, -1 ),
new( 0, 0, -1 ),
new( 0, 0, -1 ),
new( 0, 0, -1 ),
};
private static void ComputeTextureAxes( Vector3 normal, out Vector3 uAxis, out Vector3 vAxis )
{
var orientation = GetOrientationForPlane( normal );
uAxis = FaceRightVectors[orientation];
vAxis = FaceDownVectors[orientation];
}
private static int GetOrientationForPlane( Vector3 plane )
{
plane = plane.Normal;
var maxDot = 0.0f;
int orientation = 0;
for ( int i = 0; i < 6; i++ )
{
var dot = Vector3.Dot( plane, FaceNormals[i] );
if ( dot >= maxDot )
{
maxDot = dot;
orientation = i;
}
}
return orientation;
}
}