Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Documentation~/EXPERIMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = одно изменение. Не наслаивать фиксы.
Expand Down
15 changes: 11 additions & 4 deletions Editor/XatlasRepack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -1341,9 +1344,13 @@ static async Task<RepackResult[]> 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);
Expand Down
30 changes: 30 additions & 0 deletions Tests/Editor/XatlasRepackGroupMergeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading