Skip to content

Commit c4cb84b

Browse files
Merge pull request #616 from Unity-Technologies/bugfix/pbld-164-auto-stitching-fix
Fixed a bug with UV autostitching where the position offset would not take into account the face rotation center offset.
2 parents 02c4c28 + 3935dd3 commit c4cb84b

File tree

5 files changed

+149
-6
lines changed

5 files changed

+149
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111

1212
- [PBLD-242] Fixed a bug where edges were being incorrectly selected when one or both vertices were behind the camera's near plane, causing flipped lines and inconsistent selection behavior.
1313
- [PBLD-226] Fixed a bug where ProBuilder faces could not selected when obscured by another GameObject
14+
- [PBLD-164] Fixed a bug with UV autostitching where the position offset would not take into account the face rotation center offset.
1415

1516
## [6.0.6] - 2025-07-01
1617

Runtime/Core/UvAutoManualConversion.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,29 @@ internal static UVTransform CalculateDelta(IList<Vector2> src, IList<int> srcInd
140140
Vector2 dstSize = GetRotatedSize(dst, dstIndices, dstCenter, -rotation);
141141
Bounds2D srcBounds = srcIndices == null ? new Bounds2D(src) : new Bounds2D(src, srcIndices);
142142
Vector2 scale = dstSize.DivideBy(srcBounds.size);
143-
Vector2 srcCenter = srcBounds.center * scale;
143+
144+
// Calculate new bounds after rotation
145+
Vector2 min = new Vector2(float.MaxValue, float.MaxValue);
146+
Vector2 max = new Vector2(float.MinValue, float.MinValue);
147+
148+
int count = srcIndices?.Count ?? src.Count;
149+
for (int i = 0; i < count; i++)
150+
{
151+
int index = GetIndex(srcIndices, i);
152+
Vector2 rotated = Math.RotateAroundPoint(src[index], srcBounds.center, rotation);
153+
min.x = Mathf.Min(min.x, rotated.x);
154+
min.y = Mathf.Min(min.y, rotated.y);
155+
max.x = Mathf.Max(max.x, rotated.x);
156+
max.y = Mathf.Max(max.y, rotated.y);
157+
}
158+
159+
// Calculate center of rotated bounds, and apply the calculated scale afterwards
160+
Vector2 rotatedCenter = (min + max) * 0.5f;
161+
Vector2 srcTransformedCenter = rotatedCenter * scale;
144162

145163
return new UVTransform()
146164
{
147-
translation = dstCenter - srcCenter,
165+
translation = dstCenter - srcTransformedCenter,
148166
rotation = rotation,
149167
scale = dstSize.DivideBy(srcBounds.size)
150168
};

Runtime/MeshOperations/UV/TextureStitching.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ static partial class UVEditing
88
/// Provided two faces, this method will attempt to project @f2 and align its size, rotation, and position to match
99
/// the shared edge on f1. Returns true on success, false otherwise.
1010
/// </summary>
11-
/// <param name="mesh"></param>
12-
/// <param name="f1"></param>
13-
/// <param name="f2"></param>
11+
/// <param name="mesh">The mesh containing the faces</param>
12+
/// <param name="f1">The anchor face</param>
13+
/// <param name="f2">The face to align to the anchor</param>
1414
/// <param name="channel"></param>
15-
/// <returns></returns>
15+
/// <returns>true if the autostitching succeeded, else fase</returns>
1616
public static bool AutoStitch(ProBuilderMesh mesh, Face f1, Face f2, int channel)
1717
{
1818
var wings = WingedEdge.GetWingedEdges(mesh, new [] { f1, f2 });
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System.Linq;
2+
using NUnit.Framework;
3+
using UnityEngine;
4+
using UnityEngine.ProBuilder;
5+
using UnityEngine.ProBuilder.MeshOperations;
6+
using UnityEngine.ProBuilder.Shapes;
7+
8+
[TestFixture]
9+
public class AutoStitchTests
10+
{
11+
const float k_Tolerance = 0.0001f;
12+
[Test]
13+
public void AutoStitch_AlignsEdgesCorrectly()
14+
{
15+
// Step 1: Create a cube and deform one face
16+
var cube = ShapeFactory.Instantiate<Cube>();
17+
Assume.That(cube, Is.Not.Null);
18+
19+
var f0 = cube.faces[0]; // idx : 0,1,2,3
20+
var f1 = cube.faces[1]; // idx : 4,5,6,7
21+
var vertices = cube.positionsInternal;
22+
23+
// let's modify the non adjacent edge
24+
vertices[5] += new Vector3(0, -0.5f, 0);
25+
vertices[8] += new Vector3(0, -0.5f, 0);
26+
vertices[7] += new Vector3(0, 0.5f, 0);
27+
vertices[10] += new Vector3(0, 0.5f, 0);
28+
cube.positionsInternal = vertices;
29+
30+
cube.ToMesh();
31+
cube.Refresh();
32+
33+
// Step 2: Perform AutoStitch
34+
bool succeeded = UVEditing.AutoStitch(cube, f0, f1, 0);
35+
Assert.IsTrue(succeeded, "AutoStitch operation failed.");
36+
37+
// Step 3: Verify that the edge of one face UV is the same as the other face UV
38+
var uvs = cube.texturesInternal;
39+
40+
// Get the shared edge between f0 and f1
41+
var sharedEdge = WingedEdge.GetWingedEdges(cube, new[] { f0, f1 })
42+
.FirstOrDefault(x => x.face == f0 && x.opposite != null && x.opposite.face == f1);
43+
44+
Assume.That(sharedEdge, Is.Not.Null, "No shared edge found between the two faces.");
45+
46+
// Check if UVs on the shared edge are aligned
47+
var f0Edge = sharedEdge.opposite.edge.common;
48+
var f1Edge = sharedEdge.opposite.edge.local;
49+
50+
// Compare UVs for each vertex in the shared edge
51+
AssertUVsAlmostEqual(uvs[f0Edge.a], uvs[f1Edge.a], k_Tolerance, $"UV mismatch at vertex {f0Edge.a}.");
52+
AssertUVsAlmostEqual(uvs[f0Edge.b], uvs[f1Edge.b], k_Tolerance, $"UV mismatch at vertex {f0Edge.b}.");
53+
54+
// Cleanup
55+
Object.DestroyImmediate(cube.gameObject);
56+
}
57+
58+
[Test]
59+
public void AutoStitch_AlignsEdgesCorrectly_WhenRotated()
60+
{
61+
// Step 1: Create a cube
62+
var cube = ShapeFactory.Instantiate<Cube>();
63+
Assume.That(cube, Is.Not.Null);
64+
65+
// Step 2: Rotate all faces of the cube
66+
var rotation = Quaternion.Euler(45, 45, 45);
67+
var vertices = cube.positionsInternal;
68+
69+
for (int i = 0; i < vertices.Length; i++)
70+
{
71+
vertices[i] = rotation * vertices[i];
72+
}
73+
74+
cube.positionsInternal = vertices;
75+
cube.ToMesh();
76+
cube.Refresh();
77+
78+
// Deform one face slightly to ensure edge misalignment
79+
var f0 = cube.faces[0]; // idx : 0,1,2,3
80+
var f1 = cube.faces[1]; // idx : 4,5,6,7
81+
82+
// let's modify the non adjacent edge
83+
vertices[5] += new Vector3(0, -0.5f, 0);
84+
vertices[8] += new Vector3(0, -0.5f, 0);
85+
vertices[7] += new Vector3(0, 0.5f, 0);
86+
vertices[10] += new Vector3(0, 0.5f, 0);
87+
cube.positionsInternal = vertices;
88+
89+
cube.ToMesh();
90+
cube.Refresh();
91+
92+
// Step 3: Perform AutoStitch
93+
bool succeeded = UVEditing.AutoStitch(cube, f0, f1, 0);
94+
Assert.IsTrue(succeeded, "AutoStitch operation failed.");
95+
96+
// Step 4: Verify that the edge of one face UV is the same as the other face UV
97+
var uvs = cube.texturesInternal;
98+
99+
// Get the shared edge between f0 and f1
100+
var sharedEdge = WingedEdge.GetWingedEdges(cube, new[] { f0, f1 })
101+
.FirstOrDefault(x => x.face == f0 && x.opposite != null && x.opposite.face == f1);
102+
103+
Assume.That(sharedEdge, Is.Not.Null, "No shared edge found between the two faces.");
104+
105+
// Check if UVs on the shared edge are aligned
106+
var f0Edge = sharedEdge.opposite.edge.common;
107+
var f1Edge = sharedEdge.opposite.edge.local;
108+
109+
// Compare UVs for each vertex in the shared edge
110+
AssertUVsAlmostEqual(uvs[f0Edge.a], uvs[f1Edge.a], k_Tolerance, $"UV mismatch at vertex {f0Edge.a}.");
111+
AssertUVsAlmostEqual(uvs[f0Edge.b], uvs[f1Edge.b], k_Tolerance, $"UV mismatch at vertex {f0Edge.b}.");
112+
113+
// Cleanup
114+
Object.DestroyImmediate(cube.gameObject);
115+
}
116+
117+
private void AssertUVsAlmostEqual(Vector2 uv1, Vector2 uv2, float tolerance, string message)
118+
{
119+
Assert.That(uv1.x, Is.EqualTo(uv2.x).Within(tolerance), $"{message} on X coordinate.");
120+
Assert.That(uv1.y, Is.EqualTo(uv2.y).Within(tolerance), $"{message} on Y coordinate.");
121+
}
122+
}

Tests/Editor/UV/AutoStitchingTests.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)