Skip to content

Commit d5daad5

Browse files
authored
Fast texture tool improvements (#3624)
https://files.facepunch.com/louie/1b1811b1/sbox-dev_wPVinlJTtE.png -Support Tiling in the fast texture tool. -Shortcuts. -Escaping out resets to orignal uv. -Unwrap Square better. -World Mapping. -Apply active material when entering fast texturing. -Clicking in the rect view with no rect file fills to the full uv. -Snapping toggle. -Reset uv and focus buttons. -New cleaner UI.
1 parent 9c76279 commit d5daad5

File tree

6 files changed

+1106
-109
lines changed

6 files changed

+1106
-109
lines changed

game/addons/tools/Code/Editor/RectEditor/EdgeAwareFaceUnwrapper.cs

Lines changed: 173 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)