diff --git a/Documentation~/EXPERIMENTS.md b/Documentation~/EXPERIMENTS.md index 54ed2b86..b3877edc 100644 --- a/Documentation~/EXPERIMENTS.md +++ b/Documentation~/EXPERIMENTS.md @@ -3,6 +3,19 @@ > **Обновлять этот документ при каждом эксперименте с transfer pipeline.** > Последнее обновление: v0.15.39 (2026-04-07) +## Эксперимент 2026-08-06 — Изоляция cross-LOD hints по mesh group + +- **Проблема:** индексы source shell локальны для mesh, но overlap/match hints + накапливались в общих списках и могли влиять на несвязанный mesh с совпавшим + числовым индексом shell. +- **Изменение:** состояние hints разделено по паре source `MeshEntry` + + `meshGroupKey`; между LOD передаются только hints той же пары. +- **Сохранение поведения:** внутри одной LOD-цепочки приоритет issues → hint → + 3D distance не меняется; очищение состояния перед новым transfer/auto-tune + запуском сохранено. +- **Проверка:** требуется ручной Unity-прогон на нескольких mesh groups с + совпадающими локальными индексами shell и сравнение UV2 на всех LOD. + ## Правила экспериментов 1. Один PR = одно изменение. Не наслаивать фиксы. diff --git a/Editor/Tools/LightmapTransferTool.cs b/Editor/Tools/LightmapTransferTool.cs index 4e680229..223be997 100644 --- a/Editor/Tools/LightmapTransferTool.cs +++ b/Editor/Tools/LightmapTransferTool.cs @@ -179,10 +179,18 @@ static UvtLog.Category[] BuildLogCategoryList() // ── Transfer cache ── Dictionary shellTransformCache = new Dictionary(); - List accumulatedOverlapHints = - new List(); - List accumulatedMatchHints = - new List(); + sealed class CrossLodHintState + { + public readonly List overlapHints = + new List(); + public readonly List matchHints = + new List(); + } + + // Shell indices are local to a source mesh. Keep cross-LOD hints isolated + // to the source/mesh-group pair that produced them. + readonly Dictionary<(MeshEntry source, string meshGroupKey), CrossLodHintState> crossLodHints = + new Dictionary<(MeshEntry, string), CrossLodHintState>(); // ── Preview ── // Three mutually-exclusive preview modes. Only one should be active at a time. @@ -1807,7 +1815,7 @@ async Task ExecFullPipelineCoreImpl(bool useAsync) kv.Key.shellTransferResult = null; } ctx.ClearAllCaches(); - accumulatedOverlapHints.Clear(); + crossLodHints.Clear(); shellTransformCache.Clear(); ctx.HasRepack = false; ctx.HasTransfer = false; @@ -2121,8 +2129,7 @@ async Task ExecTransferAllImpl(bool useAsync) return; } - accumulatedOverlapHints.Clear(); - accumulatedMatchHints.Clear(); + crossLodHints.Clear(); int processed = 0; for (int li = 0; li < ctx.LodCount; li++) { @@ -2191,6 +2198,14 @@ async Task ExecTransferLodImpl(int tLod, bool useAsync) Mesh tgtMesh = tgt.originalMesh; if (srcMesh == null || tgtMesh == null) continue; + string meshGroupKey = tgt.meshGroupKey ?? tgt.renderer.name; + var hintKey = (source: srcEntry, meshGroupKey: meshGroupKey); + if (!crossLodHints.TryGetValue(hintKey, out var hintState)) + { + hintState = new CrossLodHintState(); + crossLodHints.Add(hintKey, hintState); + } + int srcId = srcMesh.GetInstanceID(); if (!shellTransformCache.TryGetValue(srcId, out var srcInfos)) { @@ -2202,26 +2217,26 @@ async Task ExecTransferLodImpl(int tLod, bool useAsync) UvProgress.Report(-1f, $"Transfer LOD{tLod} ← '{tgt.renderer.name}'"); var tr = useAsync ? await GroupedShellTransfer.TransferAsync(tgtMesh, srcMesh, - accumulatedOverlapHints.Count > 0 ? accumulatedOverlapHints : null, - accumulatedMatchHints.Count > 0 ? accumulatedMatchHints : null, + hintState.overlapHints.Count > 0 ? hintState.overlapHints : null, + hintState.matchHints.Count > 0 ? hintState.matchHints : null, srcEntry.repackedAtlasWidth > 0 ? (int)srcEntry.repackedAtlasWidth : 0, srcEntry.repackedAtlasHeight > 0 ? (int)srcEntry.repackedAtlasHeight : 0) : GroupedShellTransfer.Transfer(tgtMesh, srcMesh, - accumulatedOverlapHints.Count > 0 ? accumulatedOverlapHints : null, - accumulatedMatchHints.Count > 0 ? accumulatedMatchHints : null, + hintState.overlapHints.Count > 0 ? hintState.overlapHints : null, + hintState.matchHints.Count > 0 ? hintState.matchHints : null, srcEntry.repackedAtlasWidth > 0 ? (int)srcEntry.repackedAtlasWidth : 0, srcEntry.repackedAtlasHeight > 0 ? (int)srcEntry.repackedAtlasHeight : 0); if (tr.uv2 == null) { UvtLog.Warn($"[Transfer] Failed for '{tgt.renderer.name}'"); continue; } // Accumulate overlap hints for subsequent LODs if (tr.overlapHints != null && tr.overlapHints.Count > 0) - accumulatedOverlapHints.AddRange(tr.overlapHints); + hintState.overlapHints.AddRange(tr.overlapHints); // Replace match hints with this LOD's matches (latest LOD drives // next LOD's hint-guided matching; stale hints from older LODs // could conflict with changing geometry) - accumulatedMatchHints.Clear(); + hintState.matchHints.Clear(); if (tr.matchHints != null && tr.matchHints.Count > 0) - accumulatedMatchHints.AddRange(tr.matchHints); + hintState.matchHints.AddRange(tr.matchHints); // Build output mesh with UV2 applied var om = UnityEngine.Object.Instantiate(tgtMesh);