From 2e73a1d7cb93ec9c5c8ea7451c9ec72029763e1e Mon Sep 17 00:00:00 2001 From: SashaRX Date: Thu, 6 Aug 2026 13:52:24 +0200 Subject: [PATCH] Fix standalone repack UV winding normalization --- Documentation~/EXPERIMENTS.md | 11 ++++++++ Editor/XatlasRepack.cs | 15 ++++++++--- Tests/Editor/XatlasRepackGroupMergeTests.cs | 30 +++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/Documentation~/EXPERIMENTS.md b/Documentation~/EXPERIMENTS.md index 54ed2b86..dab008e0 100644 --- a/Documentation~/EXPERIMENTS.md +++ b/Documentation~/EXPERIMENTS.md @@ -3,6 +3,17 @@ > **Обновлять этот документ при каждом эксперименте с transfer pipeline.** > Последнее обновление: v0.15.39 (2026-04-07) +## Эксперимент 2026-08-06 — Самодостаточная нормализация winding в repack + +- **Проблема:** standalone `Repack All` и отключаемый Weld в full pipeline + позволяли вызвать `RepackSingle`/`RepackMulti` с mirrored UV0 shell. +- **Изменение:** оба repack entry point нормализуют собственные копии UV0 + непосредственно перед передачей данных в xatlas; исходный UV0 mesh не + изменяется. +- **Ожидание/проверка:** mirrored shell учитывается в `flippedShells`, UV2 + упаковывается в положительном winding, а тест неизменности UV0 продолжает + проходить. + ## Правила экспериментов 1. Один PR = одно изменение. Не наслаивать фиксы. diff --git a/Editor/XatlasRepack.cs b/Editor/XatlasRepack.cs index dacba5dc..4fdc46b1 100644 --- a/Editor/XatlasRepack.cs +++ b/Editor/XatlasRepack.cs @@ -988,8 +988,11 @@ public static RepackResult RepackSingle(Mesh mesh, RepackOptions opts) // faceShellIds. A future opt-in will materialise the split. LogHardEdgeAnalysis(shells, tris, mesh.vertices, meshLabel: mesh.name); - // UV0 winding normalized by ExecWeldUv0. - result.flippedShells = 0; + // Repack is also exposed as a standalone operation, so it cannot + // rely on the optional Weld stage having normalized UV0 first. + // mesh.uv returns a copy; normalize that working copy so the + // caller's UV0 channel remains unchanged. + result.flippedShells = NormalizeShellWinding(uv0, tris, shells); // ── Flatten UV0 ── float[] uvFlat = new float[vertCount * 2]; @@ -1341,9 +1344,13 @@ static async Task RepackMultiCore(Mesh[] meshes, RepackOptions o LogHardEdgeAnalysis(shells, allTris[m], allPositions[m], meshLabel: mesh.name); } - // UV0 winding normalized by ExecWeldUv0. + // RepackMulti can be invoked directly from the Repack tab (and + // Weld is optional in the full pipeline), so normalize every + // local UV0 copy at this API boundary instead of assuming a + // previous stage ran. for (int m = 0; m < meshCount; m++) - results[m].flippedShells = 0; + results[m].flippedShells = NormalizeShellWinding( + allUv0[m], allTris[m], allShells[m]); // Local UV0 copies (flattened) per mesh — fed to xatlas, mutated // by pre-pack passes (ARAP + density normalisation + perturbation); diff --git a/Tests/Editor/XatlasRepackGroupMergeTests.cs b/Tests/Editor/XatlasRepackGroupMergeTests.cs index d86c0c6f..e74b3643 100644 --- a/Tests/Editor/XatlasRepackGroupMergeTests.cs +++ b/Tests/Editor/XatlasRepackGroupMergeTests.cs @@ -144,6 +144,36 @@ public void RepackSingle_DoesNotModifyUv0() } } + [Test] + public void RepackSingle_NormalizesMirroredShellWithoutModifyingUv0() + { + if (!NativeAvailable()) Assert.Ignore("xatlas native plugin not available"); + + var mesh = BuildTiledMesh(1); + try + { + var mirroredUv0 = mesh.uv; + for (int i = 0; i < mirroredUv0.Length; i++) + mirroredUv0[i].x = 0.5f - mirroredUv0[i].x; + mesh.uv = mirroredUv0; + + var opts = RepackOptions.Default; + opts.resolution = 256; + opts.padding = 2; + var result = XatlasRepack.RepackSingle(mesh, opts); + + Assert.IsTrue(result.ok, $"Repack failed: {result.error}"); + Assert.AreEqual(1, result.flippedShells, + "Standalone repack must normalize mirrored shells even when Weld was not run"); + CollectionAssert.AreEqual(mirroredUv0, mesh.uv, + "Repack normalization must only modify its local UV0 copy"); + } + finally + { + Object.DestroyImmediate(mesh); + } + } + [Test] public void PackPreflight_DisablesBruteForce_WhenInternalOversampleIsAboveOne() {