From 73d25ac453209f2715dd94baa4dcf95c3877ab7d Mon Sep 17 00:00:00 2001 From: max Date: Sun, 17 Jan 2021 04:27:57 +0100 Subject: [PATCH] MeshSimplifier Basic setup to simplify meshes and generate LODs. --- Editor/Scripts/ModelBaker/VA_ModelBaker.cs | 10 +- Runtime/Scripts/ModelBaker/MeshCombiner.cs | 2 +- .../Scripts/ModelBaker/MeshLodGenerator.cs | 33 ++ .../ModelBaker/MeshLodGenerator.cs.meta | 11 + Runtime/Scripts/ModelBaker/MeshSimplifier.cs | 289 ++++++++++++++++++ .../Scripts/ModelBaker/MeshSimplifier.cs.meta | 11 + Runtime/Scripts/ModelBaker/MeshUtils.cs | 32 ++ Runtime/Scripts/ModelBaker/MeshUtils.cs.meta | 11 + 8 files changed, 395 insertions(+), 4 deletions(-) create mode 100644 Runtime/Scripts/ModelBaker/MeshLodGenerator.cs create mode 100644 Runtime/Scripts/ModelBaker/MeshLodGenerator.cs.meta create mode 100644 Runtime/Scripts/ModelBaker/MeshSimplifier.cs create mode 100644 Runtime/Scripts/ModelBaker/MeshSimplifier.cs.meta create mode 100644 Runtime/Scripts/ModelBaker/MeshUtils.cs create mode 100644 Runtime/Scripts/ModelBaker/MeshUtils.cs.meta diff --git a/Editor/Scripts/ModelBaker/VA_ModelBaker.cs b/Editor/Scripts/ModelBaker/VA_ModelBaker.cs index f72da5a..2720a13 100644 --- a/Editor/Scripts/ModelBaker/VA_ModelBaker.cs +++ b/Editor/Scripts/ModelBaker/VA_ModelBaker.cs @@ -16,6 +16,8 @@ namespace TAO.VertexAnimation.Editor public int textureWidth = 512; public bool generateLODS = true; + // TODO: Improve curve/lod settings. LOD-Mesh Quality pair. + //public Vector2[] lodLevels = new Vector2[4] { new Vector2(32, 100), new Vector2(32, 65), new Vector2(32, 30), new Vector2(3, 0) }; public AnimationCurve lodCurve = new AnimationCurve(new Keyframe(0, 1), new Keyframe(1, 0.01f)); public bool saveBakedDataToAsset = true; public bool generateAnimationBook = true; @@ -41,8 +43,7 @@ namespace TAO.VertexAnimation.Editor if (generateLODS) { - // TODO: LODs. - meshes = new Mesh[1] { bakedData.mesh }; + meshes = bakedData.mesh.GenerateLOD(3, lodCurve); } else { @@ -57,7 +58,10 @@ namespace TAO.VertexAnimation.Editor AssetDatabaseUtils.RemoveChildAssets(this, new Object[2] { book, material }); // TODO: LODs - AssetDatabase.AddObjectToAsset(bakedData.mesh, this); + foreach (var m in meshes) + { + AssetDatabase.AddObjectToAsset(m, this); + } foreach (var pm in bakedData.positionMaps) { diff --git a/Runtime/Scripts/ModelBaker/MeshCombiner.cs b/Runtime/Scripts/ModelBaker/MeshCombiner.cs index 04a8b4a..6a64d7d 100644 --- a/Runtime/Scripts/ModelBaker/MeshCombiner.cs +++ b/Runtime/Scripts/ModelBaker/MeshCombiner.cs @@ -243,5 +243,5 @@ namespace TAO.VertexAnimation SkinnedMeshRenderer target = gameObject.AddComponent(); target.Combine(skinnedMeshes, meshes); } - } + } } \ No newline at end of file diff --git a/Runtime/Scripts/ModelBaker/MeshLodGenerator.cs b/Runtime/Scripts/ModelBaker/MeshLodGenerator.cs new file mode 100644 index 0000000..e6ba71d --- /dev/null +++ b/Runtime/Scripts/ModelBaker/MeshLodGenerator.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace TAO.VertexAnimation +{ + public static class MeshLodGenerator + { + public static Mesh[] GenerateLOD(this Mesh mesh, int lods, float[] quality) + { + Mesh[] lodMeshes = new Mesh[lods]; + + for (int lm = 0; lm < lodMeshes.Length; lm++) + { + lodMeshes[lm] = mesh.Copy(); + lodMeshes[lm] = lodMeshes[lm].Simplify(quality[lm]); + lodMeshes[lm].name = string.Format("{0}_LOD{1}", lodMeshes[lm].name, lm); + } + + return lodMeshes; + } + + public static Mesh[] GenerateLOD(this Mesh mesh, int lods, AnimationCurve qualityCurve) + { + float[] quality = new float[lods]; + + for (int q = 0; q < quality.Length; q++) + { + quality[q] = qualityCurve.Evaluate(1f / quality.Length * q); + } + + return GenerateLOD(mesh, lods, quality); + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/ModelBaker/MeshLodGenerator.cs.meta b/Runtime/Scripts/ModelBaker/MeshLodGenerator.cs.meta new file mode 100644 index 0000000..86c80ea --- /dev/null +++ b/Runtime/Scripts/ModelBaker/MeshLodGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c65b5fa78d8074a439c6ba86df4c4ffc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/ModelBaker/MeshSimplifier.cs b/Runtime/Scripts/ModelBaker/MeshSimplifier.cs new file mode 100644 index 0000000..d64b400 --- /dev/null +++ b/Runtime/Scripts/ModelBaker/MeshSimplifier.cs @@ -0,0 +1,289 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace TAO.VertexAnimation +{ + public static class MeshSimplifier + { + // Convert mesh into Triangles. + // Change triangles. + // Generate new mesh data based on Triangles. + + // Everything is basically done through the triangle. + // When something changes in the triangle all correlated sub data changes as well (uv, normals, verts, etc). + + public class Triangle + { + // Vertices (Vector3) + // Normals (Vector3) + // UVs (UV0, UV1, ..., UV7) + // Other... + + public List vertices = new List(3); + public List normals = new List(3); + public Dictionary> uvs = new Dictionary>(); + + public float Perimeter() + { + return Vector3.Distance(vertices[0], vertices[1]) + Vector3.Distance(vertices[1], vertices[2]) + Vector3.Distance(vertices[2], vertices[0]); + } + + // If two or more points have the same values the triangle has no surface area and will be 'zero'. + public bool IsZero() + { + if (vertices[0] == vertices[1] || vertices[0] == vertices[2] || vertices[1] == vertices[2]) + { + return true; + } + + return false; + } + + // Returns the closest vertex index of a vertex within this triangle. + public int GetClosestVertexIndex(Vector3 vertex) + { + float distance = Mathf.Infinity; + int closestVertex = -1; + + for (int v = 0; v < vertices.Count; v++) + { + if (vertices[v] != vertex) + { + float dist = Vector3.Distance(vertices[v], vertex); + + if (dist < distance) + { + distance = dist; + closestVertex = v; + } + } + } + + return closestVertex; + } + + // Update triangle by copying data from a point in this triangle. + public bool UpdateVertex(int curVertexIndex, int newVertexIndex) + { + vertices[curVertexIndex] = vertices[newVertexIndex]; + normals[curVertexIndex] = normals[newVertexIndex]; + + foreach (var uv in uvs) + { + uv.Value[curVertexIndex] = uv.Value[newVertexIndex]; + } + + return true; + } + + // Update triangle by copying data from an other triangle. + public bool UpdateVertex(Triangle sourceTriangle, int sourceVertexIndex, int newSourceVertexIndex) + { + if (sourceTriangle != this) + { + Vector3 sourceVertex = sourceTriangle.vertices[sourceVertexIndex]; + int index = vertices.IndexOf(sourceVertex); + + if (index != -1) + { + // Set all the new data. + vertices[index] = sourceTriangle.vertices[newSourceVertexIndex]; + normals[index] = sourceTriangle.normals[newSourceVertexIndex]; + + foreach (var uv in uvs) + { + uv.Value[index] = sourceTriangle.uvs[uv.Key][newSourceVertexIndex]; + } + + return true; + } + } + + return false; + } + } + + public static Mesh Simplify(this Mesh mesh, float quality) + { + string name = mesh.name; + + List triangles = mesh.ToTriangles(); + + int targetCount = Mathf.FloorToInt(triangles.Count * quality); + int loopCount = 0; + + while (triangles.Count > targetCount) + { + // Sort by perimeter. + // TODO: Better priority system. Maybe allow user to pass in method. + if (loopCount % triangles.Count == 0) + { + triangles.SortByPerimeter(); + } + + // Select tri/vert to simplify. + int curTriIndex = 0; + // TODO: Select vert by shortest total distance to the two other verts in the triangle. + int curVertIndex = 0; + Vector3 curVert = triangles[curTriIndex].vertices[curVertIndex]; + + // Select closest vert within triangle to merge into. + int newVertIndex = triangles[curTriIndex].GetClosestVertexIndex(curVert); + + // Update all triangles. + // TODO: Apply only to connected triangles. + for (int t = 0; t < triangles.Count; t++) + { + triangles[t].UpdateVertex(triangles[curTriIndex], curVertIndex, newVertIndex); + } + triangles[curTriIndex].UpdateVertex(curVertIndex, newVertIndex); + + // Remove all zero triangles. + triangles.RemoveAll(t => t.IsZero()); + + loopCount++; + } + + mesh.Clear(); + mesh = triangles.ToMesh(); + mesh.name = name; + + return mesh; + } + + public static List ToTriangles(this Mesh mesh) + { + List triangles = new List(); + + List verts = new List(mesh.vertices); + List normals = new List(mesh.normals); + List tris = new List(mesh.triangles); + + Dictionary> uvs = new Dictionary>(); + for (int u = 0; u < 8; u++) + { + List coordinates = new List(); + mesh.GetUVs(u, coordinates); + + if (coordinates != null && coordinates.Any()) + { + uvs.Add(u, coordinates); + } + } + + for (int t = 0; t < tris.Count; t += 3) + { + Triangle tri = new Triangle(); + + tri.vertices.Add(verts[tris[t + 0]]); + tri.vertices.Add(verts[tris[t + 1]]); + tri.vertices.Add(verts[tris[t + 2]]); + + tri.normals.Add(normals[tris[t + 0]]); + tri.normals.Add(normals[tris[t + 1]]); + tri.normals.Add(normals[tris[t + 2]]); + + foreach (var uv in uvs) + { + if (tri.uvs.TryGetValue(uv.Key, out List coordinates)) + { + coordinates.Add(uv.Value[tris[t + 0]]); + coordinates.Add(uv.Value[tris[t + 1]]); + coordinates.Add(uv.Value[tris[t + 2]]); + } + else + { + tri.uvs.Add(uv.Key, new List + { + uv.Value[tris[t + 0]], + uv.Value[tris[t + 1]], + uv.Value[tris[t + 2]] + }); + } + } + + triangles.Add(tri); + } + + return triangles; + } + + public static Mesh ToMesh(this List triangles) + { + Mesh mesh = new Mesh(); + mesh.Clear(); + + List vertices = new List(triangles.Count * 3); + List tris = new List(triangles.Count * 3); + List normals = new List(triangles.Count * 3); + Dictionary> uvs = new Dictionary>(); + + int skipped = 0; + for (int t = 0; t < triangles.Count; t++) + { + for (int v = 0; v < triangles[t].vertices.Count; v++) + { + // Check for existing matching vert. + int vIndex = vertices.IndexOf(triangles[t].vertices[v]); + if (vIndex != -1) + { + // Check for existing matching normal. + if (normals[vIndex] == triangles[t].normals[v]) + { + // We have a duplicate. + // Don't add the data and instead point to existing. + tris.Add(vIndex); + skipped++; + continue; + } + } + + // Add data when it doesn't exist. + vertices.Add(triangles[t].vertices[v]); + normals.Add(triangles[t].normals[v]); + + foreach (var uv in triangles[t].uvs) + { + if (uvs.TryGetValue(uv.Key, out List coordinates)) + { + coordinates.Add(uv.Value[v]); + } + else + { + uvs.Add(uv.Key, new List + { + uv.Value[v], + }); + } + } + + tris.Add(t * 3 + v - skipped); + } + } + + mesh.vertices = vertices.ToArray(); + mesh.normals = normals.ToArray(); + + foreach (var uv in uvs) + { + mesh.SetUVs(uv.Key, uv.Value); + } + + mesh.triangles = tris.ToArray(); + + mesh.Optimize(); + mesh.RecalculateBounds(); + mesh.RecalculateTangents(); + + return mesh; + } + + public static List SortByPerimeter(this List triangles) + { + triangles.Sort((x, y) => x.Perimeter().CompareTo(y.Perimeter())); + + return triangles; + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/ModelBaker/MeshSimplifier.cs.meta b/Runtime/Scripts/ModelBaker/MeshSimplifier.cs.meta new file mode 100644 index 0000000..5d8eb67 --- /dev/null +++ b/Runtime/Scripts/ModelBaker/MeshSimplifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a779acf4568ca24484403e1eb61b7f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/ModelBaker/MeshUtils.cs b/Runtime/Scripts/ModelBaker/MeshUtils.cs new file mode 100644 index 0000000..faa9897 --- /dev/null +++ b/Runtime/Scripts/ModelBaker/MeshUtils.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace TAO.VertexAnimation +{ + public static class MeshUtils + { + public static Mesh Copy(this Mesh mesh) + { + Mesh copy = new Mesh + { + name = mesh.name, + vertices = mesh.vertices, + triangles = mesh.triangles, + normals = mesh.normals, + tangents = mesh.tangents, + colors = mesh.colors, + uv = mesh.uv, + uv2 = mesh.uv2, + uv3 = mesh.uv3, + uv4 = mesh.uv4, + uv5 = mesh.uv5, + uv6 = mesh.uv6, + uv7 = mesh.uv7, + uv8 = mesh.uv8, + }; + + copy.RecalculateBounds(); + + return copy; + } + } +} diff --git a/Runtime/Scripts/ModelBaker/MeshUtils.cs.meta b/Runtime/Scripts/ModelBaker/MeshUtils.cs.meta new file mode 100644 index 0000000..c73789b --- /dev/null +++ b/Runtime/Scripts/ModelBaker/MeshUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af0818363d7ad6640b54b45d0b879c52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: