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; } }