Skip to content
Closed
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
43 changes: 37 additions & 6 deletions Editor/Uv2AssetPostprocessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ namespace SashaRX.UnityMeshLab
{
public class Uv2AssetPostprocessor : AssetPostprocessor
{
// Serialized sidecars are untrusted import inputs. Bound expensive candidate
// comparisons so coincident/quantized vertices cannot make imports quadratic.
const int MaxRemapCandidateChecks = 1000000;

// Run well after Bakery and other postprocessors (default order = 0).
public override int GetPostprocessOrder() => 10000;
public override uint GetVersion() => 2;

/// <summary>
/// Paths to bypass during the next reimport. When ApplyUv2ToFbx needs the
Expand Down Expand Up @@ -1091,14 +1096,15 @@ static int[] RebuildRemapFromPositions(Mesh mesh, MeshUv2Entry entry)
for (int i = 0; i < newCount; i++) newRemap[i] = -1;
var coveredOpt = new bool[optCount > 0 ? optCount : 1];
int matched = 0;
int candidateChecksRemaining = MaxRemapCandidateChecks;

for (int i = 0; i < newCount; i++)
{
var key = QuantizePosForRemap(newPos[i]);
if (!posLookup.TryGetValue(key, out var candidates)) continue;

int bestOld = PickBestCandidate(candidates, i, origRemap, coveredOpt,
hasUv0 ? newUv0 : null, storedUv0);
hasUv0 ? newUv0 : null, storedUv0, ref candidateChecksRemaining);

if (bestOld >= 0)
{
Expand All @@ -1124,6 +1130,7 @@ static int[] RebuildRemapFromPositions(Mesh mesh, MeshUv2Entry entry)
int bestOld = -1;
for (int j = 0; j < storedCount; j++)
{
if (candidateChecksRemaining-- <= 0) break;
float d = Vector3.SqrMagnitude(newPos[i] - storedPos[j]);
if (d < bestDist) { bestDist = d; bestOld = j; }
}
Expand All @@ -1139,6 +1146,10 @@ static int[] RebuildRemapFromPositions(Mesh mesh, MeshUv2Entry entry)
}
}

if (candidateChecksRemaining <= 0)
UvtLog.Warn($"[UV2 Postprocess] '{mesh.name}': remap rebuild comparison limit reached; " +
"remaining vertices were left unmapped.");

if (matched < newCount)
UvtLog.Warn($"[UV2 Postprocess] '{mesh.name}': remap rebuild matched {matched}/{newCount} " +
$"({newCount - matched} unmapped)");
Expand Down Expand Up @@ -1174,7 +1185,8 @@ static int[] RebuildRemapFromPositions(Mesh mesh, MeshUv2Entry entry)
/// just because their opt slot is already covered.
/// </summary>
static int PickBestCandidate(List<int> candidates, int newIdx, int[] origRemap, bool[] coveredOpt,
List<Vector2> newUv0, Vector2[] storedUv0)
List<Vector2> newUv0, Vector2[] storedUv0,
ref int candidateChecksRemaining)
{
if (candidates.Count == 1)
{
Expand All @@ -1189,6 +1201,7 @@ static int PickBestCandidate(List<int> candidates, int newIdx, int[] origRemap,
float bestUv0Dist = float.MaxValue;
for (int k = 0; k < candidates.Count; k++)
{
if (candidateChecksRemaining-- <= 0) return -1;
int ci = candidates[k];
if (origRemap[ci] < 0) continue;
float d = hasUv ? Vector2.SqrMagnitude(newUv0[newIdx] - storedUv0[ci]) : 0f;
Expand All @@ -1209,6 +1222,7 @@ static int PickBestCandidate(List<int> candidates, int newIdx, int[] origRemap,

for (int k = 0; k < candidates.Count; k++)
{
if (candidateChecksRemaining-- <= 0) return -1;
int ci = candidates[k];
int opt = origRemap[ci];

Expand Down Expand Up @@ -1304,7 +1318,9 @@ static Vector2[] RemapUvSetIfNeeded(
var result = new Vector2[count];
var used = new bool[sourceUv.Length];
var meshMatched = new bool[count];
var candidateCursors = new Dictionary<(int, int, int), int>();
int matched = 0;
int candidateChecksRemaining = MaxRemapCandidateChecks;

for (int i = 0; i < count; i++)
{
Expand All @@ -1329,6 +1345,7 @@ static Vector2[] RemapUvSetIfNeeded(
int bestIdx = -1;
foreach (int ci in candidates)
{
if (candidateChecksRemaining-- <= 0) break;
if (used[ci]) continue;
float d = Vector2.SqrMagnitude(meshUv0[i] - entry.vertUv0[ci]);
if (d < bestDist)
Expand All @@ -1345,11 +1362,15 @@ static Vector2[] RemapUvSetIfNeeded(
matched++;
}
}
else

// Missing UV0, or a deliberately huge duplicate bucket: use the
// next unused index in this bucket. The cursor makes this linear.
if (!meshMatched[i] && (!hasUv0 || candidateChecksRemaining <= 0))
{
// No UV0 data — pick first unused candidate
foreach (int ci in candidates)
candidateCursors.TryGetValue(key, out int cursor);
while (cursor < candidates.Count)
{
int ci = candidates[cursor++];
if (!used[ci])
{
result[i] = sourceUv[ci];
Expand All @@ -1359,6 +1380,7 @@ static Vector2[] RemapUvSetIfNeeded(
break;
}
}
candidateCursors[key] = cursor;
}
}

Expand All @@ -1380,13 +1402,15 @@ static Vector2[] RemapUvSetIfNeeded(
int fallbackMatched = 0;
foreach (int mi in unmatchedMesh)
{
if (candidateChecksRemaining <= 0) break;
float bestDist = float.MaxValue;
int bestIdx = -1;
int bestListIdx = -1;

// First try: nearest UNUSED sidecar vertex within tight tolerance (1mm)
for (int j = 0; j < unusedSidecar.Count; j++)
{
if (candidateChecksRemaining-- <= 0) break;
int si = unusedSidecar[j];
float d = Vector3.SqrMagnitude(meshPos[mi] - entry.vertPositions[si]);
if (d < bestDist)
Expand All @@ -1401,7 +1425,9 @@ static Vector2[] RemapUvSetIfNeeded(
{
result[mi] = sourceUv[bestIdx];
used[bestIdx] = true;
unusedSidecar.RemoveAt(bestListIdx);
int lastListIdx = unusedSidecar.Count - 1;
unusedSidecar[bestListIdx] = unusedSidecar[lastListIdx];
unusedSidecar.RemoveAt(lastListIdx);
matched++;
fallbackMatched++;
nearestFallbackCount++;
Expand All @@ -1416,6 +1442,7 @@ static Vector2[] RemapUvSetIfNeeded(
bestIdx = -1;
for (int si = 0; si < entry.vertPositions.Length; si++)
{
if (candidateChecksRemaining-- <= 0) break;
float d = Vector3.SqrMagnitude(meshPos[mi] - entry.vertPositions[si]);
if (d < bestDist) { bestDist = d; bestIdx = si; }
}
Expand All @@ -1428,6 +1455,10 @@ static Vector2[] RemapUvSetIfNeeded(
}
}

if (candidateChecksRemaining <= 0)
UvtLog.Warn($"[UV2 Postprocess] '{mesh.name}': {channelLabel} remap comparison limit reached; " +
"remaining vertices will keep zero.");

if (nearestFallbackCount > 0)
UvtLog.Info($"[UV2 Postprocess] '{mesh.name}': {nearestFallbackCount} {channelLabel} vertices matched by nearest-unused fallback");

Expand Down
Loading