@@ -14,41 +14,23 @@ public EdgeAwareFaceUnwrapper( MeshFace[] meshFaces )
1414 faces = meshFaces ;
1515 }
1616
17- public UnwrapResult UnwrapToSquare ( )
17+ public UnwrapResult Unwrap ( MappingMode mode )
1818 {
1919 if ( faces . Length == 0 )
2020 return new UnwrapResult ( ) ;
2121
22- foreach ( var face in faces )
23- {
24- if ( ! face . IsValid )
25- continue ;
22+ bool straighten = mode == MappingMode . UnwrapSquare ;
2623
27- var vertices = face . Component . Mesh . GetFaceVertices ( face . Handle ) ;
28- var indices = new List < int > ( ) ;
29-
30- foreach ( var vertexHandle in vertices )
31- {
32- var key = ( face , vertexHandle ) ;
33- if ( ! faceVertexToIndex . TryGetValue ( key , out var index ) )
34- {
35- index = vertexPositions . Count ;
36- faceVertexToIndex [ key ] = index ;
37- vertexPositions . Add ( face . Component . Mesh . GetVertexPosition ( vertexHandle ) ) ;
38- }
39- indices . Add ( index ) ;
40- }
41-
42- faceToVertexIndices [ face ] = indices ;
43- }
24+ InitializeVertexMap ( ) ;
4425
4526 var unwrappedUVs = new List < Vector2 > ( new Vector2 [ vertexPositions . Count ] ) ;
4627 var processedFaces = new HashSet < MeshFace > ( ) ;
4728 var faceQueue = new Queue < MeshFace > ( ) ;
4829
4930 if ( faces . Length > 0 && faces [ 0 ] . IsValid )
5031 {
51- UnwrapFirstFace ( faces [ 0 ] , unwrappedUVs ) ;
32+ // Seed the unwrap with the first face
33+ UnwrapFirstFace ( faces [ 0 ] , unwrappedUVs , straighten ) ;
5234 processedFaces . Add ( faces [ 0 ] ) ;
5335
5436 for ( int i = 1 ; i < faces . Length ; i ++ )
@@ -69,7 +51,7 @@ public UnwrapResult UnwrapToSquare()
6951 if ( processedFaces . Contains ( currentFace ) )
7052 continue ;
7153
72- if ( TryUnfoldFace ( currentFace , processedFaces , unwrappedUVs ) )
54+ if ( TryUnfoldFace ( currentFace , processedFaces , unwrappedUVs , straighten ) )
7355 {
7456 processedFaces . Add ( currentFace ) ;
7557 attempts = 0 ;
@@ -80,62 +62,185 @@ public UnwrapResult UnwrapToSquare()
8062 }
8163 }
8264
83- var finalFaceIndices = new List < List < int > > ( ) ;
65+ return BuildResult ( unwrappedUVs ) ;
66+ }
67+
68+ private void InitializeVertexMap ( )
69+ {
8470 foreach ( var face in faces )
8571 {
86- if ( face . IsValid && faceToVertexIndices . TryGetValue ( face , out var indices ) )
72+ if ( ! face . IsValid )
73+ continue ;
74+
75+ var vertices = face . Component . Mesh . GetFaceVertices ( face . Handle ) ;
76+ var indices = new List < int > ( ) ;
77+
78+ foreach ( var vertexHandle in vertices )
8779 {
88- finalFaceIndices . Add ( indices ) ;
80+ var key = ( face , vertexHandle ) ;
81+ if ( ! faceVertexToIndex . TryGetValue ( key , out var index ) )
82+ {
83+ index = vertexPositions . Count ;
84+ faceVertexToIndex [ key ] = index ;
85+ vertexPositions . Add ( face . Component . Mesh . GetVertexPosition ( vertexHandle ) ) ;
86+ }
87+ indices . Add ( index ) ;
8988 }
90- }
9189
92- return new UnwrapResult
93- {
94- VertexPositions = unwrappedUVs ,
95- FaceIndices = finalFaceIndices ,
96- OriginalPositions = vertexPositions
97- } ;
90+ faceToVertexIndices [ face ] = indices ;
91+ }
9892 }
9993
100- private void UnwrapFirstFace ( MeshFace face , List < Vector2 > unwrappedUVs )
94+ private void UnwrapFirstFace ( MeshFace face , List < Vector2 > unwrappedUVs , bool straighten )
10195 {
10296 if ( ! faceToVertexIndices . TryGetValue ( face , out var indices ) || indices . Count < 3 )
10397 return ;
10498
105- var pos0 = vertexPositions [ indices [ 0 ] ] ;
106- var pos1 = vertexPositions [ indices [ 1 ] ] ;
107- var pos2 = vertexPositions [ indices [ 2 ] ] ;
99+ if ( straighten && indices . Count == 4 )
100+ {
101+ var p0 = vertexPositions [ indices [ 0 ] ] ;
102+ var p1 = vertexPositions [ indices [ 1 ] ] ;
103+ var p3 = vertexPositions [ indices [ 3 ] ] ;
108104
109- var u = ( pos1 - pos0 ) . Normal ;
110- var normal = u . Cross ( ( pos2 - pos0 ) . Normal ) . Normal ;
111- var v = normal . Cross ( u ) ;
105+ float width = p0 . Distance ( p1 ) ;
106+ float height = p0 . Distance ( p3 ) ;
112107
113- foreach ( var vertexIndex in indices )
108+ unwrappedUVs [ indices [ 0 ] ] = new Vector2 ( 0 , 0 ) ;
109+ unwrappedUVs [ indices [ 1 ] ] = new Vector2 ( width , 0 ) ;
110+ unwrappedUVs [ indices [ 2 ] ] = new Vector2 ( width , height ) ;
111+ unwrappedUVs [ indices [ 3 ] ] = new Vector2 ( 0 , height ) ;
112+ }
113+ else
114114 {
115- var pos = vertexPositions [ vertexIndex ] ;
116- var relative = pos - pos0 ;
117- unwrappedUVs [ vertexIndex ] = new Vector2 ( relative . Dot ( u ) , relative . Dot ( v ) ) ;
115+ var p0 = vertexPositions [ indices [ 0 ] ] ;
116+ var p1 = vertexPositions [ indices [ 1 ] ] ;
117+ var p2 = vertexPositions [ indices [ 2 ] ] ;
118+
119+ var uDir = ( p1 - p0 ) . Normal ;
120+ var normal = uDir . Cross ( ( p2 - p0 ) . Normal ) . Normal ;
121+ var vDir = normal . Cross ( uDir ) ;
122+
123+ foreach ( var idx in indices )
124+ {
125+ var relative = vertexPositions [ idx ] - p0 ;
126+ unwrappedUVs [ idx ] = new Vector2 ( relative . Dot ( uDir ) , relative . Dot ( vDir ) ) ;
127+ }
118128 }
119129 }
120130
121- private bool TryUnfoldFace ( MeshFace currentFace , HashSet < MeshFace > processedFaces , List < Vector2 > unwrappedUVs )
131+ private bool TryUnfoldFace ( MeshFace currentFace , HashSet < MeshFace > processedFaces , List < Vector2 > unwrappedUVs , bool straighten )
122132 {
123133 if ( ! faceToVertexIndices . TryGetValue ( currentFace , out var currentIndices ) )
124134 return false ;
125135
126136 foreach ( var processedFace in processedFaces )
127137 {
128- var sharedVertices = FindSharedVertices ( currentFace , processedFace , unwrappedUVs ) ;
129- if ( sharedVertices . HasValue )
138+ var sharedEdge = FindSharedVertices ( currentFace , processedFace , unwrappedUVs ) ;
139+ if ( sharedEdge . HasValue )
130140 {
131- UnfoldFaceAlongEdge ( currentFace , currentIndices , sharedVertices . Value , unwrappedUVs ) ;
141+ UnfoldFaceAlongEdge ( currentFace , currentIndices , sharedEdge . Value , unwrappedUVs , straighten ) ;
132142 return true ;
133143 }
134144 }
135145
136146 return false ;
137147 }
138148
149+ private void UnfoldFaceAlongEdge ( MeshFace face , List < int > faceIndices , ( int idx1 , int idx2 , Vector2 uv1 , Vector2 uv2 ) edge , List < Vector2 > unwrappedUVs , bool straighten )
150+ {
151+ unwrappedUVs [ edge . idx1 ] = edge . uv1 ;
152+ unwrappedUVs [ edge . idx2 ] = edge . uv2 ;
153+
154+ // SQUARE MODE.
155+ if ( straighten && faceIndices . Count == 4 )
156+ {
157+ UnfoldQuadStraight ( faceIndices , edge , unwrappedUVs ) ;
158+ }
159+ // CONFORMING MODE.
160+ else
161+ {
162+ UnfoldGeometric ( faceIndices , edge , unwrappedUVs ) ;
163+ }
164+ }
165+
166+ /// <summary>
167+ /// Unfolds a quad by extruding the shared edge perpendicularly by the average length of the connecting sides.
168+ /// This forces the UV strip to remain straight (grid-like) even if the 3D mesh curves.
169+ /// </summary>
170+ private void UnfoldQuadStraight ( List < int > faceIndices , ( int idx1 , int idx2 , Vector2 uv1 , Vector2 uv2 ) edge , List < Vector2 > unwrappedUVs )
171+ {
172+ var uvVec = edge . uv2 - edge . uv1 ;
173+ var uvLen = uvVec . Length ;
174+ var uvNormal = uvLen > 0.000001f ? uvVec / uvLen : new Vector2 ( 1 , 0 ) ;
175+ var uvPerp = new Vector2 ( - uvNormal . y , uvNormal . x ) ; // 90 degrees Left
176+
177+ int ptr1 = faceIndices . IndexOf ( edge . idx1 ) ;
178+ int ptr2 = faceIndices . IndexOf ( edge . idx2 ) ;
179+
180+ int connectedTo1 ;
181+ int connectedTo2 ;
182+
183+ if ( ( ptr1 + 1 ) % 4 == ptr2 )
184+ {
185+ connectedTo2 = faceIndices [ ( ptr2 + 1 ) % 4 ] ;
186+ connectedTo1 = faceIndices [ ( ptr1 + 3 ) % 4 ] ;
187+ }
188+ else
189+ {
190+ connectedTo1 = faceIndices [ ( ptr1 + 1 ) % 4 ] ;
191+ connectedTo2 = faceIndices [ ( ptr2 + 3 ) % 4 ] ;
192+ }
193+
194+ float len1 = vertexPositions [ edge . idx1 ] . Distance ( vertexPositions [ connectedTo1 ] ) ;
195+ float len2 = vertexPositions [ edge . idx2 ] . Distance ( vertexPositions [ connectedTo2 ] ) ;
196+ float avgLen = ( len1 + len2 ) * 0.5f ;
197+
198+ unwrappedUVs [ connectedTo1 ] = edge . uv1 + uvPerp * avgLen ;
199+ unwrappedUVs [ connectedTo2 ] = edge . uv2 + uvPerp * avgLen ;
200+ }
201+
202+ /// <summary>
203+ /// Unfolds a face by projecting its vertices onto a 2D plane defined by the shared edge.
204+ /// This preserves the original geometric angles and shapes.
205+ /// </summary>
206+ private void UnfoldGeometric ( List < int > faceIndices , ( int idx1 , int idx2 , Vector2 uv1 , Vector2 uv2 ) edge , List < Vector2 > unwrappedUVs )
207+ {
208+ var pA = vertexPositions [ edge . idx1 ] ;
209+ var pB = vertexPositions [ edge . idx2 ] ;
210+ var edge3D = pB - pA ;
211+ var edge2D = edge . uv2 - edge . uv1 ;
212+
213+ Vector3 pThird = Vector3 . Zero ;
214+ foreach ( var idx in faceIndices )
215+ {
216+ if ( idx != edge . idx1 && idx != edge . idx2 )
217+ {
218+ pThird = vertexPositions [ idx ] ;
219+ break ;
220+ }
221+ }
222+
223+ var faceNormal = edge3D . Cross ( pThird - pA ) . Normal ;
224+ var localU = edge3D . Normal ;
225+ var localV = faceNormal . Cross ( localU ) ;
226+
227+ var edge2DDir = edge2D . Normal ;
228+ var edge2DPerp = new Vector2 ( - edge2DDir . y , edge2DDir . x ) ;
229+ var scale = edge3D . Length > 0 ? edge2D . Length / edge3D . Length : 1.0f ;
230+
231+ foreach ( var idx in faceIndices )
232+ {
233+ if ( idx == edge . idx1 || idx == edge . idx2 )
234+ continue ;
235+
236+ var relative3D = vertexPositions [ idx ] - pA ;
237+ float u = relative3D . Dot ( localU ) ;
238+ float v = relative3D . Dot ( localV ) ;
239+
240+ unwrappedUVs [ idx ] = edge . uv1 + edge2DDir * u * scale + edge2DPerp * v * scale ;
241+ }
242+ }
243+
139244 private ( int idx1 , int idx2 , Vector2 uv1 , Vector2 uv2 ) ? FindSharedVertices ( MeshFace face1 , MeshFace face2 , List < Vector2 > unwrappedUVs )
140245 {
141246 if ( ! faceToVertexIndices . TryGetValue ( face1 , out var indices1 ) ||
@@ -147,74 +252,52 @@ private bool TryUnfoldFace( MeshFace currentFace, HashSet<MeshFace> processedFac
147252 var idx1a = indices1 [ i ] ;
148253 var idx1b = indices1 [ ( i + 1 ) % indices1 . Count ] ;
149254
255+ var pos1a = vertexPositions [ idx1a ] ;
256+ var pos1b = vertexPositions [ idx1b ] ;
257+
150258 for ( int j = 0 ; j < indices2 . Count ; j ++ )
151259 {
152260 var idx2a = indices2 [ j ] ;
153261 var idx2b = indices2 [ ( j + 1 ) % indices2 . Count ] ;
154262
155- var pos1a = vertexPositions [ idx1a ] ;
156- var pos1b = vertexPositions [ idx1b ] ;
157263 var pos2a = vertexPositions [ idx2a ] ;
158264 var pos2b = vertexPositions [ idx2b ] ;
159265
160266 const float tolerance = 0.001f ;
161267 bool matchForward = pos1a . Distance ( pos2a ) < tolerance && pos1b . Distance ( pos2b ) < tolerance ;
162268 bool matchReverse = pos1a . Distance ( pos2b ) < tolerance && pos1b . Distance ( pos2a ) < tolerance ;
163269
164- if ( matchForward || matchReverse )
270+ if ( matchForward )
271+ {
272+ return ( idx1a , idx1b , unwrappedUVs [ idx2a ] , unwrappedUVs [ idx2b ] ) ;
273+ }
274+ if ( matchReverse )
165275 {
166- return matchForward
167- ? ( idx1a , idx1b , unwrappedUVs [ idx2a ] , unwrappedUVs [ idx2b ] )
168- : ( idx1a , idx1b , unwrappedUVs [ idx2b ] , unwrappedUVs [ idx2a ] ) ;
276+ return ( idx1a , idx1b , unwrappedUVs [ idx2b ] , unwrappedUVs [ idx2a ] ) ;
169277 }
170278 }
171279 }
172280
173281 return null ;
174282 }
175283
176- private void UnfoldFaceAlongEdge ( MeshFace face , List < int > faceIndices , ( int idx1 , int idx2 , Vector2 uv1 , Vector2 uv2 ) sharedEdge , List < Vector2 > unwrappedUVs )
284+ private UnwrapResult BuildResult ( List < Vector2 > unwrappedUVs )
177285 {
178- unwrappedUVs [ sharedEdge . idx1 ] = sharedEdge . uv1 ;
179- unwrappedUVs [ sharedEdge . idx2 ] = sharedEdge . uv2 ;
180-
181- var edge3DA = vertexPositions [ sharedEdge . idx1 ] ;
182- var edge3DB = vertexPositions [ sharedEdge . idx2 ] ;
183- var edge3D = edge3DB - edge3DA ;
184- var edge2D = sharedEdge . uv2 - sharedEdge . uv1 ;
185-
186- Vector3 thirdVertex = Vector3 . Zero ;
187- foreach ( var idx in faceIndices )
286+ var finalFaceIndices = new List < List < int > > ( ) ;
287+ foreach ( var face in faces )
188288 {
189- if ( idx != sharedEdge . idx1 && idx != sharedEdge . idx2 )
289+ if ( face . IsValid && faceToVertexIndices . TryGetValue ( face , out var indices ) )
190290 {
191- thirdVertex = vertexPositions [ idx ] ;
192- break ;
291+ finalFaceIndices . Add ( indices ) ;
193292 }
194293 }
195294
196- var faceNormal = edge3D . Cross ( thirdVertex - edge3DA ) . Normal ;
197- var localU = edge3D . Normal ;
198- var localV = faceNormal . Cross ( localU ) ;
199-
200- foreach ( var idx in faceIndices )
295+ return new UnwrapResult
201296 {
202- if ( idx == sharedEdge . idx1 || idx == sharedEdge . idx2 )
203- continue ;
204-
205- var pos3D = vertexPositions [ idx ] ;
206- var relative3D = pos3D - edge3DA ;
207- var localPos = new Vector2 ( relative3D . Dot ( localU ) , relative3D . Dot ( localV ) ) ;
208-
209- var edgeLength2D = edge2D . Length ;
210- var edgeLength3D = edge3D . Length ;
211- var scale = edgeLength3D > 0 ? edgeLength2D / edgeLength3D : 1.0f ;
212-
213- var edge2DDir = edge2D . Normal ;
214- var edge2DPerp = new Vector2 ( - edge2DDir . y , edge2DDir . x ) ;
215-
216- unwrappedUVs [ idx ] = sharedEdge . uv1 + edge2DDir * localPos . x * scale + edge2DPerp * localPos . y * scale ;
217- }
297+ VertexPositions = unwrappedUVs ,
298+ FaceIndices = finalFaceIndices ,
299+ OriginalPositions = vertexPositions
300+ } ;
218301 }
219302
220303 public class UnwrapResult
0 commit comments