Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- Reverse UV: the audit report is written only at the Verbose log level (the last 20 are kept) and a failed write no longer fails the transfer; the audit itself still gates every result. The ancestry kept per mesh and saved in the sidecar is a compact record of per-face overlap flags, charts, chain keys and overlap relations instead of the node's whole report (earlier sidecars are read and rewritten on restore), and the viewer's quality check no longer hashes it every frame.

### Fixed
- Place Collision at Root (Sidecar) works on a model whose Read/Write is off, Unity's default: every import refused the listed nodes as "unreadable" and the place step then dropped them from the sidecar, though a model's import may change its meshes whatever that setting.
- A model's sidecar no longer counts as holding UV Transfer settings when none were saved in it. Unity gives every sidecar a settings block with the defaults, so selecting a model whose sidecar had none applied those defaults over the current settings, and a sidecar left with nothing else (after a UV2 reset, say) was kept instead of deleted. Settings saved before this change count when they differ from the defaults.
- Prepare's Preset step no longer applies the preset's avatar description (the importer's human description: bone mapping and its scale): it is the model's own, as its remaps are.
- Prepare's surface binding reports a warning, instead of failing, when Unity does not add the surface component to a collider's object (a script in an editor assembly, say).
Expand Down
12 changes: 3 additions & 9 deletions Editor/Mesh/CollisionRootPlacement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal static void ApplyOnImport(GameObject root, IList<string> paths, string
{
var node = CollisionHierarchyAnalyzer.Find(root.transform, path);
string problem = node == null ? "is not in the model"
: Refusal(root.transform, node, users) ?? CurveRefusal(root.transform, node, animated) ?? UnreadableMesh(node);
: Refusal(root.transform, node, users) ?? CurveRefusal(root.transform, node, animated);
if (problem != null)
{
UvtLog.Warn($"[Collision] '{modelPath}': '{CollisionHierarchyAnalyzer.NodeName(path)}' {problem}; the sidecar does not place it at the root.");
Expand All @@ -60,7 +60,8 @@ static void PlaceAtRoot(Transform root, Transform node)
{
var filters = node.GetComponentsInChildren<MeshFilter>(true).Where(f => f.sharedMesh != null).ToList();
var toRoot = filters.Select(f => root.worldToLocalMatrix * f.transform.localToWorldMatrix).ToList();
for (int i = 0; i < filters.Count; i++) MeshTransform.BakeMatrix(filters[i].sharedMesh, toRoot[i]);
// On import a mesh takes the change whether or not the model's Read/Write is on.
for (int i = 0; i < filters.Count; i++) MeshTransform.BakeMatrix(filters[i].sharedMesh, toRoot[i], importing: true);
foreach (var t in node.GetComponentsInChildren<Transform>(true))
{
t.localPosition = Vector3.zero;
Expand Down Expand Up @@ -88,13 +89,6 @@ static string MeshRefusal(Mesh mesh, Dictionary<Mesh, int> users)
return mesh.blendShapeCount > 0 ? $"has blend shapes on '{mesh.name}'" : null;
}

// On import every mesh is readable; one that is not would keep its old positions while its node moves.
static string UnreadableMesh(Transform node)
{
var mesh = node.GetComponentsInChildren<MeshFilter>(true).Select(f => f.sharedMesh).FirstOrDefault(m => m != null && !m.isReadable);
return mesh != null ? $"has '{mesh.name}' unreadable" : null;
}

// How many renderers (static or skinned) show each mesh of the model.
static Dictionary<Mesh, int> MeshUsers(GameObject root)
{
Expand Down
81 changes: 49 additions & 32 deletions Editor/Mesh/MeshTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,67 @@ internal static class MeshTransform
/// not), tangents the matrix itself and are re-orthogonalised against the new
/// normal; a mirroring matrix (negative determinant) flips every triangle's
/// winding and the tangent handedness so the surface still faces out and the
/// normal map still decodes. Bounds are recalculated. Readable mesh required.
/// normal map still decodes. Bounds are recalculated. Readable mesh required, except
/// while its model imports.
/// </summary>
public static void BakeMatrix(Mesh mesh, Matrix4x4 matrix)
/// <param name="importing">
/// Called from a model's OnPostprocessModel, where Unity lets a mesh be changed whatever
/// its Read/Write setting (its isReadable is false there when the setting is off).
/// </param>
public static void BakeMatrix(Mesh mesh, Matrix4x4 matrix, bool importing = false)
{
if (mesh == null || !mesh.isReadable) return;
if (mesh == null || (!importing && !mesh.isReadable)) return;
if (matrix.isIdentity) return;
bool mirrored = matrix.determinant < 0f;
var normalMatrix = matrix.inverse.transpose;
var vertices = mesh.vertices;
for (int i = 0; i < vertices.Length; ++i) vertices[i] = matrix.MultiplyPoint3x4(vertices[i]);
mesh.vertices = vertices;
var normals = BakeNormals(mesh, matrix.inverse.transpose, vertices.Length);
BakeTangents(mesh, matrix, normals, vertices.Length, mirrored);
if (mirrored)
for (int sub = 0; sub < mesh.subMeshCount; ++sub) FlipWinding(mesh, sub);
mesh.RecalculateBounds();
}

// The mesh's normals by the inverse transpose, written back; none when it has none per vertex.
static Vector3[] BakeNormals(Mesh mesh, Matrix4x4 normalMatrix, int vertexCount)
{
var normals = mesh.normals;
bool hasNormals = normals != null && normals.Length == vertices.Length;
if (hasNormals)
if (normals == null || normals.Length != vertexCount) return System.Array.Empty<Vector3>();
for (int i = 0; i < normals.Length; ++i) normals[i] = normalMatrix.MultiplyVector(normals[i]).normalized;
mesh.normals = normals;
return normals;
}

// Tangents by the matrix, re-orthogonalised against the new normals; the handedness flips when mirrored.
static void BakeTangents(Mesh mesh, Matrix4x4 matrix, Vector3[] normals, int vertexCount, bool mirrored)
{
var tangents = mesh.tangents;
if (tangents == null || tangents.Length != vertexCount) return;
bool hasNormals = normals.Length == vertexCount;
for (int i = 0; i < tangents.Length; ++i)
{
for (int i = 0; i < normals.Length; ++i) normals[i] = normalMatrix.MultiplyVector(normals[i]).normalized;
mesh.normals = normals;
var t = matrix.MultiplyVector(new Vector3(tangents[i].x, tangents[i].y, tangents[i].z));
if (hasNormals) t -= normals[i] * Vector3.Dot(t, normals[i]);
t.Normalize();
tangents[i] = new Vector4(t.x, t.y, t.z, mirrored ? -tangents[i].w : tangents[i].w);
}
var tangents = mesh.tangents;
if (tangents != null && tangents.Length == vertices.Length)
mesh.tangents = tangents;
}

// Each submesh keeps its topology; only faces (triangles, quads) have a winding to flip.
static void FlipWinding(Mesh mesh, int sub)
{
var topology = mesh.GetTopology(sub);
int corners = topology switch { MeshTopology.Triangles => 3, MeshTopology.Quads => 4, _ => 0 };
if (corners == 0) return;
var indices = mesh.GetIndices(sub);
// The second and the last corner swap: 0 1 2 → 0 2 1, 0 1 2 3 → 0 3 2 1.
for (int i = 0; i + corners - 1 < indices.Length; i += corners)
{
for (int i = 0; i < tangents.Length; ++i)
{
var t = matrix.MultiplyVector(new Vector3(tangents[i].x, tangents[i].y, tangents[i].z));
if (hasNormals) t -= normals[i] * Vector3.Dot(t, normals[i]);
t.Normalize();
tangents[i] = new Vector4(t.x, t.y, t.z, mirrored ? -tangents[i].w : tangents[i].w);
}
mesh.tangents = tangents;
int tmp = indices[i + 1]; indices[i + 1] = indices[i + corners - 1]; indices[i + corners - 1] = tmp;
}
if (mirrored)
for (int sub = 0; sub < mesh.subMeshCount; ++sub)
{
// Each submesh keeps its topology; only faces have a winding to flip.
var topology = mesh.GetTopology(sub);
var indices = mesh.GetIndices(sub);
if (topology == MeshTopology.Triangles)
for (int i = 0; i + 2 < indices.Length; i += 3) { int tmp = indices[i + 1]; indices[i + 1] = indices[i + 2]; indices[i + 2] = tmp; }
else if (topology == MeshTopology.Quads)
for (int i = 0; i + 3 < indices.Length; i += 4) { int tmp = indices[i + 1]; indices[i + 1] = indices[i + 3]; indices[i + 3] = tmp; }
else continue;
mesh.SetIndices(indices, topology, sub, calculateBounds: false);
}
mesh.RecalculateBounds();
mesh.SetIndices(indices, topology, sub, calculateBounds: false);
}

/// <summary>The union of every renderer's world bounds under root; <paramref name="fallback"/> when it has none.</summary>
Expand Down
2 changes: 1 addition & 1 deletion Tests/Editor/ToolAssetSaveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public void ModelReplacementInvalidatesOnlyUvSidecarBeforeFailingFirstImport()
var data = SidecarStore.LoadOrCreate(source.path);
string meshName = source.entry.fbxMesh.name;
data.Set(meshName, new[] { Vector2.zero, Vector2.right, Vector2.up });
data.toolSettings = new ToolSettings();
data.toolSettings = new ToolSettings { saved = true }; // as SaveSettings leaves them
SidecarStore.Save(data);
var before = File.ReadAllBytes(source.path);
int committed = 0, imports = 0;
Expand Down
Loading