From 25de414814a1c3bcbd32f92801617b6d03468d45 Mon Sep 17 00:00:00 2001 From: Amir Ebrahimi Date: Thu, 25 Jan 2018 11:47:27 -0800 Subject: [PATCH 1/6] Set submesh count when assigning triangles via AddSubMeshTriangles --- Scripts/MeshSimplifier.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Scripts/MeshSimplifier.cs b/Scripts/MeshSimplifier.cs index 5e4f1e6..742dd1d 100644 --- a/Scripts/MeshSimplifier.cs +++ b/Scripts/MeshSimplifier.cs @@ -1069,6 +1069,8 @@ public void AddSubMeshTriangles(int[][] triangles) triangleIndex += subMeshTriangleCount; } + + subMeshCount = triangles.Length; } #endregion From 2af4b651f9908ca6772768d6760d6873bb4067f9 Mon Sep 17 00:00:00 2001 From: Amir Ebrahimi Date: Thu, 25 Jan 2018 16:52:48 -0800 Subject: [PATCH 2/6] Revert previous change --- Scripts/MeshSimplifier.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Scripts/MeshSimplifier.cs b/Scripts/MeshSimplifier.cs index 742dd1d..5e4f1e6 100644 --- a/Scripts/MeshSimplifier.cs +++ b/Scripts/MeshSimplifier.cs @@ -1069,8 +1069,6 @@ public void AddSubMeshTriangles(int[][] triangles) triangleIndex += subMeshTriangleCount; } - - subMeshCount = triangles.Length; } #endregion From 209b6a2d8256d4eb0ea4d4250e50bb2d03755af0 Mon Sep 17 00:00:00 2001 From: Amir Ebrahimi Date: Wed, 5 Sep 2018 17:52:44 -0700 Subject: [PATCH 3/6] Update to latest (#2) * Bone weights are now copied when moving vertex attributes. * Added a comment to make a line more clear as to what the purpose is. * Bone weights will now always be the same for linked vertices. Can there be instances where this is not desired? * Bindposes of meshes are now automatically copied if a mesh is initialized with the MeshSimplifier and the ToMesh method is sequentially used. In all other cases, the bindposes have to be copied over manually. * Added proper support for Unity 2017.4 as well as Unity 2018.X * Created an assembly definition for the scripts that should only be used for Unity versions that do support it. * Added a new section to the readme file regarding a problem with the Visual Studio solution file. * Updated the compatibility section of the readme. * Updated the readme file with another paragraph at the top about platform and runtime support. * Created a change log file. * Added a feature to change the maximum iteration count for the mesh simplification. * Added the default value to the property XML comments of MeshSimplifier. * Fixed a potential problem where the bindposes were never reset if someone would intend to reuse an instance of the MeshSimplifier. * Updated the changelog for the release of version 1.0.0 * The sub mesh triangle offsets are now computed when compacting the mesh after a simplification has completed. * Added a last resort to calculate the sub-mesh triangle offsets if there are none when accessing them. This will only be needed when someone retrieves the sub-meshes before a simplification has been made. * Updated and added more exception throwing. * The sub-mesh triangle offsets array is now reset when clearing sub-meshes. * Fixed issues with the sub-mesh offsets. * Made a code section slightly easier to read. * Added assertions when getting the triangle indices for a sub-mesh. * If the sub-mesh start offset when getting the sub-mesh triangle indices is greater than or equals to the triangle count, an empty array is returned. This is to avoid assertions from triggering, because the offset is outside of the range. This can happen when the last sub-meshes (in index order) end up with no triangles after simplification. * Heavily optimized the method for updating triangle references. * Minor optimizations for the quadrics initialization. * Fixed an issue with the Min method in MathHelper one unnecessary check. * Greatly optimized the CalculateError method. * Minor optimization when finding border indices. * Further optimized the UpdateReferences method. * Optimized the collection of border vertices. * Optimized the Flipped method slightly. * Optimized the compating of triangles per iteration slightly. * Optimized the RemoveVertexPass method. * Removed the references to triangle areas. * Optimized the vertex binding of the smart linking feature. * Optimized the smart linking feature further. * Fixed a bug with the border vertex sorting. * Updated the readme file with the compatible Unity versions. * Updated the change log in preparation for the release of version 1.0.1 * Fixed a minor mistake. * Fixed a documentation mistake with the VertexLinkDistanceSqr property on the MeshSimplifier class. * Updated the change log in preparation for the release of version 1.0.2 --- CHANGELOG.md | 36 ++ CHANGELOG.md.meta | 7 + README.md | 7 +- Scripts/MathHelper.cs | 2 +- Scripts/MeshSimplifier.cs | 505 +++++++++++++++--------- Scripts/UnityMeshSimplifier.asmdef | 8 + Scripts/UnityMeshSimplifier.asmdef.meta | 7 + 7 files changed, 384 insertions(+), 188 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.md.meta create mode 100644 Scripts/UnityMeshSimplifier.asmdef create mode 100644 Scripts/UnityMeshSimplifier.asmdef.meta diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bd3dd2c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Change Log + +## [v1.0.2] - 2018-07-05 + +### Fixed +- Fixed a documentation mistake with the VertexLinkDistanceSqr property on the MeshSimplifier class. + +## [v1.0.1] - 2018-06-03 + +### Fixed +- Added more exception throwing on invalid parameters that wasn't previously handled. +- Added assertions when getting the triangle indices for a sub-mesh, to detect a faulty state more easily. +- Optimized the retrieving of sub-mesh triangles when having a large number of sub-meshes. +- Heavily optimized the initialization and simplification process. + +## [v1.0.0] - 2018-05-12 + +### Added +- Unity assembly definition file. +- Feature to change the maximum iteration count for the mesh simplification. + +### Fixed +- Better support for skinned meshes. +- Support for Unity 2017.4 and 2018.X + +## v0.1.0 - 2018-04-01 + +### Added +- A mesh simplification algorithm based on the [Fast Quadric Mesh Simplification](https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification) algorithm. +- A feature (Smart Linking) that attempts to solve problems where holes could appear in simplified meshes. +- Support for static and skinned meshes. +- Support for 2D, 3D and 4D UVs. + +[v1.0.2]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v1.0.1...v1.0.2 +[v1.0.1]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v1.0.0...v1.0.1 +[v1.0.0]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v0.1.0...v1.0.0 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..01aa686 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5e029678e8aeb0044b335e85de4f2948 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index d1807fc..c669411 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # UnityMeshSimplifier Mesh simplification for [Unity](https://unity3d.com/). The project is deeply based on the [Fast Quadric Mesh Simplification](https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification) algorithm, but rewritten entirely in C# and released under the MIT license. +Because of the fact that this project is entirely in C# it *should* work on all platforms that Unity officially supports, as well as both in the editor and at runtime in builds. + ## Compatibility -These scripts have been tested and confirmed working with Unity 5.6.0f3, Unity 2017.1.0f3, Unity 2017.2.1f1 and Unity 2017.3.0f3. +These scripts have been tested and confirmed working with Unity 5.6.0f3, Unity 2017.1.0f3, Unity 2017.2.1f1, Unity 2017.3.0f3, Unity 2017.4.0f1 and Unity 2018.1.2f1. ## Installation into Unity project 1. Copy the contents of this repository into a folder named *UnityMeshSimplifier* in your Assets directory within your Unity project. @@ -41,5 +43,8 @@ There are several ways to solve this problem. The smart linking feature (mention The recommendation is to use the smart linking feature that is enabled by default, but the options for preservation exists in those cases where you may want it. +## The Unity-generated Visual Studio solution file appears broken +This can be a problem because of an assembly definition provided with this repository, if you are using Unity 2017.3 or above. Make sure that you have the latest version of [Visual Studio Tools for Unity](https://www.visualstudio.com/vs/unity-tools/). If you are using Visual Studio 2017, make sure that Visual Studio is up to date and that you have installed the *Game development with Unity* component. For other versions of Visual Studio you would have to download a separate installer. Please go to the [Microsoft Documentation](https://docs.microsoft.com/en-us/visualstudio/cross-platform/getting-started-with-visual-studio-tools-for-unity) for more information. + ## Other projects If you are interested in mesh simplification in .NET outside of Unity you can visit my other project [MeshDecimator](https://github.com/Whinarn/MeshDecimator). diff --git a/Scripts/MathHelper.cs b/Scripts/MathHelper.cs index 502c821..50418b4 100644 --- a/Scripts/MathHelper.cs +++ b/Scripts/MathHelper.cs @@ -75,7 +75,7 @@ public static class MathHelper /// The minimum value. public static double Min(double val1, double val2, double val3) { - return (val1 < val2 ? (val1 < val3 ? val1 : (val2 < val3 ? val2 : val3)) : (val2 < val3 ? val2 : val3)); + return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3)); } #endregion diff --git a/Scripts/MeshSimplifier.cs b/Scripts/MeshSimplifier.cs index aa34ac7..faa8c8d 100644 --- a/Scripts/MeshSimplifier.cs +++ b/Scripts/MeshSimplifier.cs @@ -68,8 +68,6 @@ private struct Triangle public int va1; public int va2; - public double area; - public double err0; public double err1; public double err2; @@ -119,7 +117,6 @@ public Triangle(int v0, int v1, int v2, int subMeshIndex) this.va1 = v1; this.va2 = v2; - area = 0; err0 = err1 = err2 = err3 = 0; deleted = dirty = false; n = new Vector3d(); @@ -258,6 +255,32 @@ public void Resize(int capacity, bool trimExess = false) } } #endregion + + #region Border Vertex + private struct BorderVertex + { + public int index; + public int hash; + + public BorderVertex(int index, int hash) + { + this.index = index; + this.hash = hash; + } + } + #endregion + + #region Border Vertex Comparer + private class BorderVertexComparer : IComparer + { + public static readonly BorderVertexComparer instance = new BorderVertexComparer(); + + public int Compare(BorderVertex x, BorderVertex y) + { + return x.hash.CompareTo(y.hash); + } + } + #endregion #endregion #region Fields @@ -265,12 +288,14 @@ public void Resize(int capacity, bool trimExess = false) private bool preserveSeams = false; private bool preserveFoldovers = false; private bool enableSmartLink = true; + private int maxIterationCount = 100; private double agressiveness = 7.0; private bool verbose = false; private double vertexLinkDistanceSqr = double.Epsilon; private int subMeshCount = 0; + private int[] subMeshOffsets = null; private ResizableArray triangles = null; private ResizableArray vertices = null; private ResizableArray refs = null; @@ -283,6 +308,8 @@ public void Resize(int capacity, bool trimExess = false) private ResizableArray vertColors = null; private ResizableArray vertBoneWeights = null; + private Matrix4x4[] bindposes = null; + // Pre-allocated buffers private double[] errArr = new double[3]; private int[] attributeIndexArr = new int[3]; @@ -291,6 +318,7 @@ public void Resize(int capacity, bool trimExess = false) #region Properties /// /// Gets or sets if borders should be preserved. + /// Default value: false /// [Obsolete("Use the 'MeshSimplifier.PreserveBorders' property instead.", false)] public bool KeepBorders @@ -301,6 +329,7 @@ public bool KeepBorders /// /// Gets or sets if borders should be preserved. + /// Default value: false /// public bool PreserveBorders { @@ -310,6 +339,7 @@ public bool PreserveBorders /// /// Gets or sets if seams should be preserved. + /// Default value: false /// public bool PreserveSeams { @@ -319,6 +349,7 @@ public bool PreserveSeams /// /// Gets or sets if foldovers should be preserved. + /// Default value: false /// public bool PreserveFoldovers { @@ -330,6 +361,7 @@ public bool PreserveFoldovers /// Gets or sets if a feature for smarter vertex linking should be enabled, reducing artifacts in the /// decimated result at the cost of a slightly more expensive initialization by treating vertices at /// the same position as the same vertex while separating the attributes. + /// Default value: true /// public bool EnableSmartLink { @@ -337,8 +369,20 @@ public bool EnableSmartLink set { enableSmartLink = value; } } + /// + /// Gets or sets the maximum iteration count. Higher number is more expensive but can bring you closer to your target quality. + /// Sometimes a lower maximum count might be desired in order to lower the performance cost. + /// Default value: 100 + /// + public int MaxIterationCount + { + get { return maxIterationCount; } + set { maxIterationCount = value; } + } + /// /// Gets or sets the agressiveness of the mesh simplification. Higher number equals higher quality, but more expensive to run. + /// Default value: 7.0 /// public double Agressiveness { @@ -348,6 +392,7 @@ public double Agressiveness /// /// Gets or sets if verbose information should be printed to the console. + /// Default value: false /// public bool Verbose { @@ -357,7 +402,8 @@ public bool Verbose /// /// Gets or sets the maximum squared distance between two vertices in order to link them. - /// Note that this value is only used if PreventHoles is true. + /// Note that this value is only used if EnableSmartLink is true. + /// Default value: double.Epsilon /// public double VertexLinkDistanceSqr { @@ -386,6 +432,7 @@ public Vector3[] Vertices if (value == null) throw new ArgumentNullException("value"); + bindposes = null; vertices.Resize(value.Length); var vertArr = vertices.Data; for (int i = 0; i < value.Length; i++) @@ -552,14 +599,11 @@ private double VertexError(ref SymmetricMatrix q, double x, double y, double z) + 2 * q.m5 * y * z + 2 * q.m6 * y + q.m7 * z * z + 2 * q.m8 * z + q.m9; } - private double CalculateError(int i0, int i1, out Vector3d result, out int resultIndex) + private double CalculateError(ref Vertex vert0, ref Vertex vert1, out Vector3d result, out int resultIndex) { // compute interpolated vertex - var vertices = this.vertices.Data; - Vertex v0 = vertices[i0]; - Vertex v1 = vertices[i1]; - SymmetricMatrix q = v0.q + v1.q; - bool border = (v0.border & v1.border); + SymmetricMatrix q = (vert0.q + vert1.q); + bool border = (vert0.border & vert1.border); double error = 0.0; double det = q.Determinant1(); if (det != 0.0 && !border) @@ -575,8 +619,8 @@ private double CalculateError(int i0, int i1, out Vector3d result, out int resul else { // det = 0 -> try to find best result - Vector3d p1 = v0.p; - Vector3d p2 = v1.p; + Vector3d p1 = vert0.p; + Vector3d p2 = vert1.p; Vector3d p3 = (p1 + p2) * 0.5f; double error1 = VertexError(ref q, p1.x, p1.y, p1.z); double error2 = VertexError(ref q, p2.x, p2.y, p2.z); @@ -611,7 +655,7 @@ private double CalculateError(int i0, int i1, out Vector3d result, out int resul /// /// Check if a triangle flips when this edge is removed /// - private bool Flipped(Vector3d p, int i0, int i1, ref Vertex v0, ResizableArray deleted) + private bool Flipped(ref Vector3d p, int i0, int i1, ref Vertex v0, bool[] deleted) { int tcount = v0.tcount; var refs = this.refs.Data; @@ -620,13 +664,12 @@ private bool Flipped(Vector3d p, int i0, int i1, ref Vertex v0, ResizableArray /// Update triangle connections and edge error after a edge is collapsed. @@ -672,6 +707,7 @@ private void UpdateTriangles(int i0, int ia0, ref Vertex v, ResizableArray int pIndex; int tcount = v.tcount; var triangles = this.triangles.Data; + var vertices = this.vertices.Data; for (int k = 0; k < tcount; k++) { Ref r = refs[v.tstart + k]; @@ -694,10 +730,9 @@ private void UpdateTriangles(int i0, int ia0, ref Vertex v, ResizableArray } t.dirty = true; - //t.area = CalculateArea(t.v0, t.v1, t.v2); - t.err0 = CalculateError(t.v0, t.v1, out p, out pIndex); - t.err1 = CalculateError(t.v1, t.v2, out p, out pIndex); - t.err2 = CalculateError(t.v2, t.v0, out p, out pIndex); + t.err0 = CalculateError(ref vertices[t.v0], ref vertices[t.v1], out p, out pIndex); + t.err1 = CalculateError(ref vertices[t.v1], ref vertices[t.v2], out p, out pIndex); + t.err2 = CalculateError(ref vertices[t.v2], ref vertices[t.v0], out p, out pIndex); t.err3 = MathHelper.Min(t.err0, t.err1, t.err2); triangles[tid] = t; refs.Add(r); @@ -753,6 +788,10 @@ private void MoveVertexAttributes(int i0, int i1) { vertColors[i0] = vertColors[i1]; } + if (vertBoneWeights != null) + { + vertBoneWeights[i0] = vertBoneWeights[i1]; + } } private void MergeVertexAttributes(int i0, int i1) @@ -802,6 +841,8 @@ private void MergeVertexAttributes(int i0, int i1) { vertColors[i0] = (vertColors[i0] + vertColors[i1]) * 0.5f; } + + // TODO: Do we have to blend bone weights at all or can we just keep them as it is in this scenario? } #endregion @@ -855,95 +896,90 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th int triangleCount = this.triangles.Length; var vertices = this.vertices.Data; - Vertex v0, v1; Vector3d p; int pIndex; - for (int i = 0; i < triangleCount; i++) + for (int tid = 0; tid < triangleCount; tid++) { - var t = triangles[i]; - if (t.dirty || t.deleted || t.err3 > threshold) + if (triangles[tid].dirty || triangles[tid].deleted || triangles[tid].err3 > threshold) continue; - t.GetErrors(errArr); - t.GetAttributeIndices(attributeIndexArr); - for (int j = 0; j < 3; j++) + triangles[tid].GetErrors(errArr); + triangles[tid].GetAttributeIndices(attributeIndexArr); + for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { - if (errArr[j] > threshold) + if (errArr[edgeIndex] > threshold) continue; - int k = ((j + 1) % 3); - int i0 = t[j]; - int i1 = t[k]; - v0 = vertices[i0]; - v1 = vertices[i1]; + int nextEdgeIndex = ((edgeIndex + 1) % 3); + int i0 = triangles[tid][edgeIndex]; + int i1 = triangles[tid][nextEdgeIndex]; // Border check - if (v0.border != v1.border) + if (vertices[i0].border != vertices[i1].border) continue; // Seam check - else if (v0.seam != v1.seam) + else if (vertices[i0].seam != vertices[i1].seam) continue; // Foldover check - else if (v0.foldover != v1.foldover) + else if (vertices[i0].foldover != vertices[i1].foldover) continue; // If borders should be preserved - else if (preserveBorders && v0.border) + else if (preserveBorders && vertices[i0].border) continue; // If seams should be preserved - else if (preserveSeams && v0.seam) + else if (preserveSeams && vertices[i0].seam) continue; // If foldovers should be preserved - else if (preserveFoldovers && v0.foldover) + else if (preserveFoldovers && vertices[i0].foldover) continue; // Compute vertex to collapse to - CalculateError(i0, i1, out p, out pIndex); - deleted0.Resize(v0.tcount); // normals temporarily - deleted1.Resize(v1.tcount); // normals temporarily + CalculateError(ref vertices[i0], ref vertices[i1], out p, out pIndex); + deleted0.Resize(vertices[i0].tcount); // normals temporarily + deleted1.Resize(vertices[i1].tcount); // normals temporarily // Don't remove if flipped - if (Flipped(p, i0, i1, ref v0, deleted0)) + if (Flipped(ref p, i0, i1, ref vertices[i0], deleted0.Data)) continue; - if (Flipped(p, i1, i0, ref v1, deleted1)) + if (Flipped(ref p, i1, i0, ref vertices[i1], deleted1.Data)) continue; - int ia0 = attributeIndexArr[j]; + int ia0 = attributeIndexArr[edgeIndex]; // Not flipped, so remove edge - v0.p = p; - v0.q += v1.q; - vertices[i0] = v0; + vertices[i0].p = p; + vertices[i0].q += vertices[i1].q; if (pIndex == 1) { // Move vertex attributes from ia1 to ia0 - int ia1 = attributeIndexArr[k]; + int ia1 = attributeIndexArr[nextEdgeIndex]; MoveVertexAttributes(ia0, ia1); } else if (pIndex == 2) { // Merge vertex attributes ia0 and ia1 into ia0 - int ia1 = attributeIndexArr[k]; + int ia1 = attributeIndexArr[nextEdgeIndex]; MergeVertexAttributes(ia0, ia1); } - if (v0.seam) + if (vertices[i0].seam) { ia0 = -1; } int tstart = refs.Length; - UpdateTriangles(i0, ia0, ref v0, deleted0, ref deletedTris); - UpdateTriangles(i0, ia0, ref v1, deleted1, ref deletedTris); + UpdateTriangles(i0, ia0, ref vertices[i0], deleted0, ref deletedTris); + UpdateTriangles(i0, ia0, ref vertices[i1], deleted1, ref deletedTris); int tcount = refs.Length - tstart; - if (tcount <= v0.tcount) + if (tcount <= vertices[i0].tcount) { // save ram if (tcount > 0) { var refsArr = refs.Data; - Array.Copy(refsArr, tstart, refsArr, v0.tstart, tcount); + Array.Copy(refsArr, tstart, refsArr, vertices[i0].tstart, tcount); } } else @@ -980,12 +1016,11 @@ private void UpdateMesh(int iteration) int dst = 0; for (int i = 0; i < triangleCount; i++) { - var triangle = triangles[i]; - if (!triangle.deleted) + if (!triangles[i].deleted) { if (dst != i) { - triangles[dst] = triangle; + triangles[dst] = triangles[i]; } dst++; } @@ -1004,6 +1039,7 @@ private void UpdateMesh(int iteration) var vcount = new List(8); var vids = new List(8); + int vsize = 0; for (int i = 0; i < vertexCount; i++) { vertices[i].border = false; @@ -1014,22 +1050,24 @@ private void UpdateMesh(int iteration) int ofs; int id; int borderVertexCount = 0; + double borderMinX = double.MaxValue; + double borderMaxX = double.MinValue; for (int i = 0; i < vertexCount; i++) { - var vertex = vertices[i]; + int tstart = vertices[i].tstart; + int tcount = vertices[i].tcount; vcount.Clear(); vids.Clear(); + vsize = 0; - int tcount = vertex.tcount; for (int j = 0; j < tcount; j++) { - int k = refs[vertex.tstart + j].tid; - Triangle t = triangles[k]; - for (k = 0; k < 3; k++) + int tid = refs[tstart + j].tid; + for (int k = 0; k < 3; k++) { ofs = 0; - id = t[k]; - while (ofs < vcount.Count) + id = triangles[tid][k]; + while (ofs < vsize) { if (vids[ofs] == id) break; @@ -1037,10 +1075,11 @@ private void UpdateMesh(int iteration) ++ofs; } - if (ofs == vcount.Count) + if (ofs == vsize) { vcount.Add(1); vids.Add(id); + ++vsize; } else { @@ -1049,14 +1088,25 @@ private void UpdateMesh(int iteration) } } - int vcountCount = vcount.Count; - for (int j = 0; j < vcountCount; j++) + for (int j = 0; j < vsize; j++) { if (vcount[j] == 1) { id = vids[j]; vertices[id].border = true; ++borderVertexCount; + + if (enableSmartLink) + { + if (vertices[id].p.x < borderMinX) + { + borderMinX = vertices[id].p.x; + } + if (vertices[id].p.x > borderMaxX) + { + borderMaxX = vertices[id].p.x; + } + } } } } @@ -1064,35 +1114,47 @@ private void UpdateMesh(int iteration) if (enableSmartLink) { // First find all border vertices - var borderIndices = new int[borderVertexCount]; + var borderVertices = new BorderVertex[borderVertexCount]; int borderIndexCount = 0; + double borderAreaWidth = borderMaxX - borderMinX; for (int i = 0; i < vertexCount; i++) { - var v0 = vertices[i]; - if (!v0.border) - continue; - - borderIndices[borderIndexCount++] = i; + if (vertices[i].border) + { + int vertexHash = (int)((((vertices[i].p.x - borderMinX) / borderAreaWidth) - 0.5) * int.MaxValue); + borderVertices[borderIndexCount] = new BorderVertex(i, vertexHash); + ++borderIndexCount; + } } + // Sort the border vertices by hash + Array.Sort(borderVertices, 0, borderIndexCount, BorderVertexComparer.instance); + // Then find identical border vertices and bind them together as one for (int i = 0; i < borderIndexCount; i++) { - var myIndex = borderIndices[i]; + int myIndex = borderVertices[i].index; if (myIndex == -1) continue; - var myVertex = vertices[myIndex]; + var myPoint = vertices[myIndex].p; for (int j = i + 1; j < borderIndexCount; j++) { - var otherIndex = borderIndices[j]; + int otherIndex = borderVertices[j].index; if (otherIndex == -1) continue; + else if ((borderVertices[j].hash - borderVertices[i].hash) > 1) // There is no point to continue beyond this point + break; - var otherVertex = vertices[otherIndex]; - if ((myVertex.p - otherVertex.p).MagnitudeSqr <= vertexLinkDistanceSqr) + var otherPoint = vertices[otherIndex].p; + var sqrX = ((myPoint.x - otherPoint.x) * (myPoint.x - otherPoint.x)); + var sqrY = ((myPoint.y - otherPoint.y) * (myPoint.y - otherPoint.y)); + var sqrZ = ((myPoint.z - otherPoint.z) * (myPoint.z - otherPoint.z)); + var sqrMagnitude = sqrX + sqrY + sqrZ; + + if (sqrMagnitude <= vertexLinkDistanceSqr) { - borderIndices[j] = -1; + borderVertices[j].index = -1; // NOTE: This makes sure that the "other" vertex is not processed again vertices[myIndex].border = false; vertices[otherIndex].border = false; @@ -1107,9 +1169,11 @@ private void UpdateMesh(int iteration) vertices[otherIndex].seam = true; } - for (int k = 0; k < otherVertex.tcount; k++) + int otherTriangleCount = vertices[otherIndex].tcount; + int otherTriangleStart = vertices[otherIndex].tstart; + for (int k = 0; k < otherTriangleCount; k++) { - var r = refs[otherVertex.tstart + k]; + var r = refs[otherTriangleStart + k]; triangles[r.tid][r.tvertex] = myIndex; } } @@ -1130,18 +1194,19 @@ private void UpdateMesh(int iteration) vertices[i].q = new SymmetricMatrix(); } + int v0, v1, v2; Vector3d n, p0, p1, p2, p10, p20, dummy; int dummy2; SymmetricMatrix sm; for (int i = 0; i < triangleCount; i++) { - var triangle = triangles[i]; - var vert0 = vertices[triangle.v0]; - var vert1 = vertices[triangle.v1]; - var vert2 = vertices[triangle.v2]; - p0 = vert0.p; - p1 = vert1.p; - p2 = vert2.p; + v0 = triangles[i].v0; + v1 = triangles[i].v1; + v2 = triangles[i].v2; + + p0 = vertices[v0].p; + p1 = vertices[v1].p; + p2 = vertices[v2].p; p10 = p1 - p0; p20 = p2 - p0; Vector3d.Cross(ref p10, ref p20, out n); @@ -1149,24 +1214,19 @@ private void UpdateMesh(int iteration) triangles[i].n = n; sm = new SymmetricMatrix(n.x, n.y, n.z, -Vector3d.Dot(ref n, ref p0)); - vert0.q += sm; - vert1.q += sm; - vert2.q += sm; - vertices[triangle.v0] = vert0; - vertices[triangle.v1] = vert1; - vertices[triangle.v2] = vert2; + vertices[v0].q += sm; + vertices[v1].q += sm; + vertices[v2].q += sm; } for (int i = 0; i < triangleCount; i++) { // Calc Edge Error var triangle = triangles[i]; - //triangle.area = CalculateArea(triangle.v0, triangle.v1, triangle.v2); - triangle.err0 = CalculateError(triangle.v0, triangle.v1, out dummy, out dummy2); - triangle.err1 = CalculateError(triangle.v1, triangle.v2, out dummy, out dummy2); - triangle.err2 = CalculateError(triangle.v2, triangle.v0, out dummy, out dummy2); - triangle.err3 = MathHelper.Min(triangle.err0, triangle.err1, triangle.err2); - triangles[i] = triangle; + triangles[i].err0 = CalculateError(ref vertices[triangle.v0], ref vertices[triangle.v1], out dummy, out dummy2); + triangles[i].err1 = CalculateError(ref vertices[triangle.v1], ref vertices[triangle.v2], out dummy, out dummy2); + triangles[i].err2 = CalculateError(ref vertices[triangle.v2], ref vertices[triangle.v0], out dummy, out dummy2); + triangles[i].err3 = MathHelper.Min(triangles[i].err0, triangles[i].err1, triangles[i].err2); } } } @@ -1183,28 +1243,23 @@ private void UpdateReferences() // Init Reference ID list for (int i = 0; i < vertexCount; i++) { - var vertex = vertices[i]; - vertex.tstart = 0; - vertex.tcount = 0; - vertices[i] = vertex; + vertices[i].tstart = 0; + vertices[i].tcount = 0; } for (int i = 0; i < triangleCount; i++) { - var triangle = triangles[i]; - ++vertices[triangle.v0].tcount; - ++vertices[triangle.v1].tcount; - ++vertices[triangle.v2].tcount; + ++vertices[triangles[i].v0].tcount; + ++vertices[triangles[i].v1].tcount; + ++vertices[triangles[i].v2].tcount; } int tstart = 0; for (int i = 0; i < vertexCount; i++) { - var vertex = vertices[i]; - vertex.tstart = tstart; - tstart += vertex.tcount; - vertex.tcount = 0; - vertices[i] = vertex; + vertices[i].tstart = tstart; + tstart += vertices[i].tcount; + vertices[i].tcount = 0; } // Write References @@ -1212,21 +1267,23 @@ private void UpdateReferences() var refs = this.refs.Data; for (int i = 0; i < triangleCount; i++) { - var triangle = triangles[i]; - var vert0 = vertices[triangle.v0]; - var vert1 = vertices[triangle.v1]; - var vert2 = vertices[triangle.v2]; + int v0 = triangles[i].v0; + int v1 = triangles[i].v1; + int v2 = triangles[i].v2; + int start0 = vertices[v0].tstart; + int count0 = vertices[v0].tcount; + int start1 = vertices[v1].tstart; + int count1 = vertices[v1].tcount; + int start2 = vertices[v2].tstart; + int count2 = vertices[v2].tcount; - refs[vert0.tstart + vert0.tcount].Set(i, 0); - refs[vert1.tstart + vert1.tcount].Set(i, 1); - refs[vert2.tstart + vert2.tcount].Set(i, 2); - ++vert0.tcount; - ++vert1.tcount; - ++vert2.tcount; + refs[start0 + count0].Set(i, 0); + refs[start1 + count1].Set(i, 1); + refs[start2 + count2].Set(i, 2); - vertices[triangle.v0] = vert0; - vertices[triangle.v1] = vert1; - vertices[triangle.v2] = vert2; + ++vertices[v0].tcount; + ++vertices[v1].tcount; + ++vertices[v2].tcount; } } #endregion @@ -1245,6 +1302,17 @@ private void CompactMesh() vertices[i].tcount = 0; } + var vertNormals = (this.vertNormals != null ? this.vertNormals.Data : null); + var vertTangents = (this.vertTangents != null ? this.vertTangents.Data : null); + var vertUV2D = (this.vertUV2D != null ? this.vertUV2D.Data : null); + var vertUV3D = (this.vertUV3D != null ? this.vertUV3D.Data : null); + var vertUV4D = (this.vertUV4D != null ? this.vertUV4D.Data : null); + var vertColors = (this.vertColors != null ? this.vertColors.Data : null); + var vertBoneWeights = (this.vertBoneWeights != null ? this.vertBoneWeights.Data : null); + + int lastSubMeshIndex = -1; + subMeshOffsets = new int[subMeshCount]; + var triangles = this.triangles.Data; int triangleCount = this.triangles.Length; for (int i = 0; i < triangleCount; i++) @@ -1254,38 +1322,64 @@ private void CompactMesh() { if (triangle.va0 != triangle.v0) { - vertices[triangle.va0].p = vertices[triangle.v0].p; + int iDest = triangle.va0; + int iSrc = triangle.v0; + vertices[iDest].p = vertices[iSrc].p; + if (vertBoneWeights != null) + { + vertBoneWeights[iDest] = vertBoneWeights[iSrc]; + } triangle.v0 = triangle.va0; } if (triangle.va1 != triangle.v1) { - vertices[triangle.va1].p = vertices[triangle.v1].p; + int iDest = triangle.va1; + int iSrc = triangle.v1; + vertices[iDest].p = vertices[iSrc].p; + if (vertBoneWeights != null) + { + vertBoneWeights[iDest] = vertBoneWeights[iSrc]; + } triangle.v1 = triangle.va1; } if (triangle.va2 != triangle.v2) { - vertices[triangle.va2].p = vertices[triangle.v2].p; + int iDest = triangle.va2; + int iSrc = triangle.v2; + vertices[iDest].p = vertices[iSrc].p; + if (vertBoneWeights != null) + { + vertBoneWeights[iDest] = vertBoneWeights[iSrc]; + } triangle.v2 = triangle.va2; } - triangles[dst++] = triangle; + int newTriangleIndex = dst++; + triangles[newTriangleIndex] = triangle; vertices[triangle.v0].tcount = 1; vertices[triangle.v1].tcount = 1; vertices[triangle.v2].tcount = 1; + + if (triangle.subMeshIndex > lastSubMeshIndex) + { + for (int j = lastSubMeshIndex + 1; j < triangle.subMeshIndex; j++) + { + subMeshOffsets[j] = newTriangleIndex; + } + subMeshOffsets[triangle.subMeshIndex] = newTriangleIndex; + lastSubMeshIndex = triangle.subMeshIndex; + } } } - this.triangles.Resize(dst); - triangles = this.triangles.Data; triangleCount = dst; + for (int i = lastSubMeshIndex + 1; i < subMeshCount; i++) + { + subMeshOffsets[i] = triangleCount; + } - var vertNormals = (this.vertNormals != null ? this.vertNormals.Data : null); - var vertTangents = (this.vertTangents != null ? this.vertTangents.Data : null); - var vertUV2D = (this.vertUV2D != null ? this.vertUV2D.Data : null); - var vertUV3D = (this.vertUV3D != null ? this.vertUV3D.Data : null); - var vertUV4D = (this.vertUV4D != null ? this.vertUV4D.Data : null); - var vertColors = (this.vertColors != null ? this.vertColors.Data : null); - var vertBoneWeights = (this.vertBoneWeights != null ? this.vertBoneWeights.Data : null); + this.triangles.Resize(triangleCount); + triangles = this.triangles.Data; dst = 0; for (int i = 0; i < vertexCount; i++) @@ -1350,61 +1444,91 @@ private void CompactMesh() triangles[i] = triangle; } - this.vertices.Resize(dst); - if (vertNormals != null) this.vertNormals.Resize(dst, true); - if (vertTangents != null) this.vertTangents.Resize(dst, true); - if (vertUV2D != null) this.vertUV2D.Resize(dst, true); - if (vertUV3D != null) this.vertUV3D.Resize(dst, true); - if (vertUV4D != null) this.vertUV4D.Resize(dst, true); - if (vertColors != null) this.vertColors.Resize(dst, true); - if (vertBoneWeights != null) this.vertBoneWeights.Resize(dst, true); + vertexCount = dst; + this.vertices.Resize(vertexCount); + if (vertNormals != null) this.vertNormals.Resize(vertexCount, true); + if (vertTangents != null) this.vertTangents.Resize(vertexCount, true); + if (vertUV2D != null) this.vertUV2D.Resize(vertexCount, true); + if (vertUV3D != null) this.vertUV3D.Resize(vertexCount, true); + if (vertUV4D != null) this.vertUV4D.Resize(vertexCount, true); + if (vertColors != null) this.vertColors.Resize(vertexCount, true); + if (vertBoneWeights != null) this.vertBoneWeights.Resize(vertexCount, true); } #endregion - #endregion - #region Public Methods - #region Sub-Meshes - /// - /// Returns the triangle indices for a specific sub-mesh. - /// - /// The sub-mesh index. - /// The triangle indices. - public int[] GetSubMeshTriangles(int subMeshIndex) + #region Calculate Sub Mesh Offsets + private void CalculateSubMeshOffsets() { - // First get the sub-mesh offsets + int lastSubMeshIndex = -1; + subMeshOffsets = new int[subMeshCount]; + + var triangles = this.triangles.Data; int triangleCount = this.triangles.Length; - var triArr = this.triangles.Data; - int[] subMeshOffsets = new int[subMeshCount]; - int lastSubMeshOffset = -1; for (int i = 0; i < triangleCount; i++) { - var triangle = triArr[i]; - if (triangle.subMeshIndex >= subMeshIndex && triangle.subMeshIndex != lastSubMeshOffset) + var triangle = triangles[i]; + if (triangle.subMeshIndex > lastSubMeshIndex) { - for (int j = lastSubMeshOffset + 1; j < triangle.subMeshIndex; j++) + for (int j = lastSubMeshIndex + 1; j < triangle.subMeshIndex; j++) { - subMeshOffsets[j] = i - 1; + subMeshOffsets[j] = i; } subMeshOffsets[triangle.subMeshIndex] = i; - lastSubMeshOffset = triangle.subMeshIndex; - if (lastSubMeshOffset >= (subMeshIndex + 1)) - break; + lastSubMeshIndex = triangle.subMeshIndex; } } - for (int i = lastSubMeshOffset + 1; i < subMeshCount; i++) + + for (int i = lastSubMeshIndex + 1; i < subMeshCount; i++) { subMeshOffsets[i] = triangleCount; } + } + #endregion + #endregion + + #region Public Methods + #region Sub-Meshes + /// + /// Returns the triangle indices for a specific sub-mesh. + /// + /// The sub-mesh index. + /// The triangle indices. + public int[] GetSubMeshTriangles(int subMeshIndex) + { + if (subMeshIndex < 0) + throw new ArgumentOutOfRangeException("subMeshIndex", "The sub-mesh index is negative."); + + // First get the sub-mesh offsets + if (subMeshOffsets == null) + { + CalculateSubMeshOffsets(); + } + + if (subMeshIndex >= subMeshOffsets.Length) + throw new ArgumentOutOfRangeException("subMeshIndex", "The sub-mesh index is greater than or equals to the sub mesh count."); + else if (subMeshOffsets.Length != subMeshCount) + throw new InvalidOperationException("The sub-mesh triangle offsets array is not the same size as the count of sub-meshes. This should not be possible to happen."); + + var triangles = this.triangles.Data; + int triangleCount = this.triangles.Length; int startOffset = subMeshOffsets[subMeshIndex]; - int endOffset = ((subMeshIndex + 1) < subMeshCount ? subMeshOffsets[subMeshIndex + 1] : triangleCount) - 1; - int subMeshTriangleCount = endOffset - startOffset + 1; + if (startOffset >= triangleCount) + return new int[0]; + + int endOffset = ((subMeshIndex + 1) < subMeshCount ? subMeshOffsets[subMeshIndex + 1] : triangleCount); + int subMeshTriangleCount = endOffset - startOffset; if (subMeshTriangleCount < 0) subMeshTriangleCount = 0; int[] subMeshIndices = new int[subMeshTriangleCount * 3]; - for (int triangleIndex = startOffset; triangleIndex <= endOffset; triangleIndex++) + Debug.AssertFormat(startOffset >= 0, "The start sub mesh offset at index {0} was below zero ({1}).", subMeshIndex, startOffset); + Debug.AssertFormat(endOffset >= 0, "The end sub mesh offset at index {0} was below zero ({1}).", subMeshIndex + 1, endOffset); + Debug.AssertFormat(startOffset < triangleCount, "The start sub mesh offset at index {0} was higher or equal to the triangle count ({1} >= {2}).", subMeshIndex, startOffset, triangleCount); + Debug.AssertFormat(endOffset <= triangleCount, "The end sub mesh offset at index {0} was higher than the triangle count ({1} > {2}).", subMeshIndex + 1, endOffset, triangleCount); + + for (int triangleIndex = startOffset; triangleIndex < endOffset; triangleIndex++) { - var triangle = triArr[triangleIndex]; + var triangle = triangles[triangleIndex]; int offset = (triangleIndex - startOffset) * 3; subMeshIndices[offset] = triangle.v0; subMeshIndices[offset + 1] = triangle.v1; @@ -1420,6 +1544,7 @@ public int[] GetSubMeshTriangles(int subMeshIndex) public void ClearSubMeshes() { subMeshCount = 0; + subMeshOffsets = null; triangles.Resize(0); } @@ -1463,7 +1588,9 @@ public void AddSubMeshTriangles(int[][] triangles) int totalTriangleCount = 0; for (int i = 0; i < triangles.Length; i++) { - if ((triangles[i].Length % 3) != 0) + if (triangles[i] == null) + throw new ArgumentException(string.Format("The index array at index {0} is null.", i)); + else if ((triangles[i].Length % 3) != 0) throw new ArgumentException(string.Format("The index array length at index {0} must be a multiple of 3 in order to represent triangles.", i), "triangles"); totalTriangleCount += triangles[i].Length / 3; @@ -1934,6 +2061,7 @@ public void Initialize(Mesh mesh) this.UV4 = mesh.uv4; this.Colors = mesh.colors; this.BoneWeights = mesh.boneWeights; + this.bindposes = mesh.bindposes; ClearSubMeshes(); @@ -1965,7 +2093,7 @@ public void SimplifyMesh(float quality) var vertices = this.vertices.Data; int targetTrisCount = Mathf.RoundToInt(triangleCount * quality); - for (int iteration = 0; iteration < 100; iteration++) + for (int iteration = 0; iteration < maxIterationCount; iteration++) { if ((startTrisCount - deletedTris) <= targetTrisCount) break; @@ -2079,12 +2207,17 @@ public Mesh ToMesh() var newMesh = new Mesh(); -#if UNITY_2017_3 +#if UNITY_2017_3 || UNITY_2017_4 || UNITY_2018 // TODO: Use baseVertex if all submeshes are within the ushort.MaxValue range even though the total vertex count is above bool use32BitIndex = (vertices.Length > ushort.MaxValue); newMesh.indexFormat = (use32BitIndex ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16); #endif + if (bindposes != null && bindposes.Length > 0) + { + newMesh.bindposes = bindposes; + } + newMesh.subMeshCount = subMeshCount; newMesh.vertices = this.Vertices; if (normals != null) newMesh.normals = normals; diff --git a/Scripts/UnityMeshSimplifier.asmdef b/Scripts/UnityMeshSimplifier.asmdef new file mode 100644 index 0000000..3b2bcfd --- /dev/null +++ b/Scripts/UnityMeshSimplifier.asmdef @@ -0,0 +1,8 @@ +{ + "name": "UnityMeshSimplifier", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false +} \ No newline at end of file diff --git a/Scripts/UnityMeshSimplifier.asmdef.meta b/Scripts/UnityMeshSimplifier.asmdef.meta new file mode 100644 index 0000000..a529ab5 --- /dev/null +++ b/Scripts/UnityMeshSimplifier.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 77ccaf49895b0d64e87cd4b4faf83c49 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From e074d855e0d6342c5220bab3dbd5b73bbc397502 Mon Sep 17 00:00:00 2001 From: Amir Ebrahimi Date: Fri, 30 Aug 2019 14:39:53 -0700 Subject: [PATCH 4/6] Merge remote-tracking branch 'upstream/master' into HEAD # Conflicts: # CHANGELOG.md # README.md # Runtime/MeshSimplifier.cs --- CHANGELOG.md | 43 +- Editor.meta | 8 + Editor/LODGeneratorHelperEditor.cs | 623 +++++++++++++ Editor/LODGeneratorHelperEditor.cs.meta | 11 + Editor/SerializedPropertyExtensions.cs | 50 + Editor/SerializedPropertyExtensions.cs.meta | 11 + .../Whinarn.UnityMeshSimplifier.Editor.asmdef | 17 + ...arn.UnityMeshSimplifier.Editor.asmdef.meta | 2 +- README.md | 55 +- Scripts.meta => Runtime.meta | 0 Runtime/BlendShape.cs | 97 ++ Runtime/BlendShape.cs.meta | 11 + Runtime/Components.meta | 8 + Runtime/Components/LODBackupComponent.cs | 43 + Runtime/Components/LODBackupComponent.cs.meta | 11 + Runtime/Components/LODGeneratorHelper.cs | 172 ++++ Runtime/Components/LODGeneratorHelper.cs.meta | 11 + Runtime/LODGenerator.cs | 872 ++++++++++++++++++ Runtime/LODGenerator.cs.meta | 11 + Runtime/LODLevel.cs | 246 +++++ Runtime/LODLevel.cs.meta | 11 + Runtime/MeshCombiner.cs | 521 +++++++++++ Runtime/MeshCombiner.cs.meta | 11 + {Scripts => Runtime}/MeshSimplifier.cs | 772 +++++++++++----- {Scripts => Runtime}/MeshSimplifier.cs.meta | 4 +- Runtime/SimplificationOptions.cs | 99 ++ Runtime/SimplificationOptions.cs.meta | 11 + Runtime/Utility.meta | 8 + {Scripts => Runtime/Utility}/MathHelper.cs | 4 + .../Utility}/MathHelper.cs.meta | 0 Runtime/Utility/MeshUtils.cs | 442 +++++++++ Runtime/Utility/MeshUtils.cs.meta | 11 + .../Utility}/ResizableArray.cs | 46 +- .../Utility}/ResizableArray.cs.meta | 0 .../Utility}/SymmetricMatrix.cs | 11 + .../Utility}/SymmetricMatrix.cs.meta | 0 {Scripts => Runtime/Utility}/Vector3d.cs | 29 + {Scripts => Runtime/Utility}/Vector3d.cs.meta | 0 ...Whinarn.UnityMeshSimplifier.Runtime.asmdef | 13 + ...rn.UnityMeshSimplifier.Runtime.asmdef.meta | 7 + Scripts/UnityMeshSimplifier.asmdef | 8 - Third Party Notices.md | 7 + Third Party Notices.md.meta | 7 + package.json | 21 + package.json.meta | 7 + 45 files changed, 4100 insertions(+), 252 deletions(-) create mode 100644 Editor.meta create mode 100644 Editor/LODGeneratorHelperEditor.cs create mode 100644 Editor/LODGeneratorHelperEditor.cs.meta create mode 100644 Editor/SerializedPropertyExtensions.cs create mode 100644 Editor/SerializedPropertyExtensions.cs.meta create mode 100644 Editor/Whinarn.UnityMeshSimplifier.Editor.asmdef rename Scripts/UnityMeshSimplifier.asmdef.meta => Editor/Whinarn.UnityMeshSimplifier.Editor.asmdef.meta (76%) rename Scripts.meta => Runtime.meta (100%) create mode 100644 Runtime/BlendShape.cs create mode 100644 Runtime/BlendShape.cs.meta create mode 100644 Runtime/Components.meta create mode 100644 Runtime/Components/LODBackupComponent.cs create mode 100644 Runtime/Components/LODBackupComponent.cs.meta create mode 100644 Runtime/Components/LODGeneratorHelper.cs create mode 100644 Runtime/Components/LODGeneratorHelper.cs.meta create mode 100644 Runtime/LODGenerator.cs create mode 100644 Runtime/LODGenerator.cs.meta create mode 100644 Runtime/LODLevel.cs create mode 100644 Runtime/LODLevel.cs.meta create mode 100644 Runtime/MeshCombiner.cs create mode 100644 Runtime/MeshCombiner.cs.meta rename {Scripts => Runtime}/MeshSimplifier.cs (74%) rename {Scripts => Runtime}/MeshSimplifier.cs.meta (71%) create mode 100644 Runtime/SimplificationOptions.cs create mode 100644 Runtime/SimplificationOptions.cs.meta create mode 100644 Runtime/Utility.meta rename {Scripts => Runtime/Utility}/MathHelper.cs (94%) rename {Scripts => Runtime/Utility}/MathHelper.cs.meta (100%) create mode 100644 Runtime/Utility/MeshUtils.cs create mode 100644 Runtime/Utility/MeshUtils.cs.meta rename {Scripts => Runtime/Utility}/ResizableArray.cs (76%) rename {Scripts => Runtime/Utility}/ResizableArray.cs.meta (100%) rename {Scripts => Runtime/Utility}/SymmetricMatrix.cs (93%) rename {Scripts => Runtime/Utility}/SymmetricMatrix.cs.meta (100%) rename {Scripts => Runtime/Utility}/Vector3d.cs (90%) rename {Scripts => Runtime/Utility}/Vector3d.cs.meta (100%) create mode 100644 Runtime/Whinarn.UnityMeshSimplifier.Runtime.asmdef create mode 100644 Runtime/Whinarn.UnityMeshSimplifier.Runtime.asmdef.meta delete mode 100644 Scripts/UnityMeshSimplifier.asmdef create mode 100644 Third Party Notices.md create mode 100644 Third Party Notices.md.meta create mode 100644 package.json create mode 100644 package.json.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3dd2c..06bb8c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,40 @@ -# Change Log +# Changelog +All notable changes to this project will be documented in this file. -## [v1.0.2] - 2018-07-05 +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [v2.0.1] - 2019-07-12 +### Fixed +- Fixed compilation errors in Unity 2018 + +## [v2.0.0] - 2019-07-07 +### Added +- Unity package manifest file. +- LOD generator. +- Component to assist with LOD generation. +- Added support to interpolate blend shapes. +- Added support for up to 8 UV channels. + +### Removed +- Removed the long obsolete KeepBorders property on the MeshSimplifier class. +### Changed +- Reorganized the project layout to match the Unity convention. +- The vertex attributes are now interpolated using barycentric coordinates. + +## [v1.0.3] - 2018-10-20 +### Fixed +- The maximum hash distance is now calculated based on the VertexLinkDistanceSqr property value instead of being hardcoded to 1. +- Fixed an issue with the vertex hashes not using the entire integer range, but instead was using only half of it. + +## [v1.0.2] - 2018-07-05 ### Fixed - Fixed a documentation mistake with the VertexLinkDistanceSqr property on the MeshSimplifier class. ## [v1.0.1] - 2018-06-03 - ### Fixed - Added more exception throwing on invalid parameters that wasn't previously handled. - Added assertions when getting the triangle indices for a sub-mesh, to detect a faulty state more easily. @@ -14,7 +42,6 @@ - Heavily optimized the initialization and simplification process. ## [v1.0.0] - 2018-05-12 - ### Added - Unity assembly definition file. - Feature to change the maximum iteration count for the mesh simplification. @@ -23,14 +50,18 @@ - Better support for skinned meshes. - Support for Unity 2017.4 and 2018.X -## v0.1.0 - 2018-04-01 - +## [v0.1.0] - 2018-04-01 ### Added - A mesh simplification algorithm based on the [Fast Quadric Mesh Simplification](https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification) algorithm. - A feature (Smart Linking) that attempts to solve problems where holes could appear in simplified meshes. - Support for static and skinned meshes. - Support for 2D, 3D and 4D UVs. +[Unreleased]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v2.0.1...HEAD +[v2.0.1]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v2.0.0...v2.0.1 +[v2.0.0]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v1.0.3...v2.0.0 +[v1.0.3]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v1.0.2...v1.0.3 [v1.0.2]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v1.0.1...v1.0.2 [v1.0.1]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v1.0.0...v1.0.1 [v1.0.0]: https://github.com/Whinarn/UnityMeshSimplifier/compare/v0.1.0...v1.0.0 +[v0.1.0]: https://github.com/Whinarn/UnityMeshSimplifier/releases/tag/v0.1.0 diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..9775d38 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e9816c7903bd8744b806177ae3266ec9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/LODGeneratorHelperEditor.cs b/Editor/LODGeneratorHelperEditor.cs new file mode 100644 index 0000000..46b8cb1 --- /dev/null +++ b/Editor/LODGeneratorHelperEditor.cs @@ -0,0 +1,623 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEditor; + +namespace UnityMeshSimplifier.Editor +{ + [CustomEditor(typeof(LODGeneratorHelper))] + internal sealed class LODGeneratorHelperEditor : UnityEditor.Editor + { + private const string FadeModeFieldName = "fadeMode"; + private const string AnimateCrossFadingFieldName = "animateCrossFading"; + private const string AutoCollectRenderersFieldName = "autoCollectRenderers"; + private const string SimplificationOptionsFieldName = "simplificationOptions"; + private const string SaveAssetsPathFieldName = "saveAssetsPath"; + private const string LevelsFieldName = "levels"; + private const string IsGeneratedFieldName = "isGenerated"; + private const string LevelScreenRelativeHeightFieldName = "screenRelativeTransitionHeight"; + private const string LevelFadeTransitionWidthFieldName = "fadeTransitionWidth"; + private const string LevelQualityFieldName = "quality"; + private const string LevelCombineMeshesFieldName = "combineMeshes"; + private const string LevelCombineSubMeshesFieldName = "combineSubMeshes"; + private const string LevelRenderersFieldName = "renderers"; + private const string SimplificationOptionsEnableSmartLinkFieldName = "EnableSmartLink"; + private const string SimplificationOptionsVertexLinkDistanceFieldName = "VertexLinkDistance"; + private const float RemoveLevelButtonSize = 20f; + private const float RendererButtonWidth = 60f; + private const float RemoveRendererButtonSize = 20f; + + private SerializedProperty fadeModeProperty = null; + private SerializedProperty animateCrossFadingProperty = null; + private SerializedProperty autoCollectRenderersProperty = null; + private SerializedProperty simplificationOptionsProperty = null; + private SerializedProperty saveAssetsPathProperty = null; + private SerializedProperty levelsProperty = null; + private SerializedProperty isGeneratedProperty = null; + + private bool overrideSaveAssetsPath = false; + private bool[] settingsExpanded = null; + private LODGeneratorHelper lodGeneratorHelper = null; + + private static readonly GUIContent createLevelButtonContent = new GUIContent("Create Level", "Creates a new LOD level."); + private static readonly GUIContent deleteLevelButtonContent = new GUIContent("X", "Deletes this LOD level."); + private static readonly GUIContent generateLODButtonContent = new GUIContent("Generate LODs", "Generates the LOD levels."); + private static readonly GUIContent destroyLODButtonContent = new GUIContent("Destroy LODs", "Destroys the LOD levels."); + private static readonly GUIContent settingsContent = new GUIContent("Settings", "The settings for the LOD level."); + private static readonly GUIContent renderersHeaderContent = new GUIContent("Renderers:", "The renderers used for this LOD level."); + private static readonly GUIContent removeRendererButtonContent = new GUIContent("X", "Removes this renderer."); + private static readonly GUIContent addRendererButtonContent = new GUIContent("Add", "Adds a renderer to this LOD level."); + private static readonly GUIContent overrideSaveAssetsPathContent = new GUIContent("Override Save Assets Path", "If you want to override the path where the generated assets are saved."); + private static readonly Color removeColor = new Color(1f, 0.6f, 0.6f, 1f); + + private static readonly int ObjectPickerControlID = "LODGeneratorSelector".GetHashCode(); + + private void OnEnable() + { + fadeModeProperty = serializedObject.FindProperty(FadeModeFieldName); + animateCrossFadingProperty = serializedObject.FindProperty(AnimateCrossFadingFieldName); + autoCollectRenderersProperty = serializedObject.FindProperty(AutoCollectRenderersFieldName); + simplificationOptionsProperty = serializedObject.FindProperty(SimplificationOptionsFieldName); + saveAssetsPathProperty = serializedObject.FindProperty(SaveAssetsPathFieldName); + levelsProperty = serializedObject.FindProperty(LevelsFieldName); + isGeneratedProperty = serializedObject.FindProperty(IsGeneratedFieldName); + + overrideSaveAssetsPath = (saveAssetsPathProperty.stringValue.Length > 0); + lodGeneratorHelper = target as LODGeneratorHelper; + } + + public override void OnInspectorGUI() + { + serializedObject.UpdateIfRequiredOrScript(); + + bool isGenerated = isGeneratedProperty.boolValue; + if (isGenerated) + { + DrawGeneratedView(); + } + else + { + DrawNotGeneratedView(); + } + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawGeneratedView() + { + if (GUILayout.Button(destroyLODButtonContent)) + { + DestroyLODs(); + } + } + + private void DrawNotGeneratedView() + { + EditorGUILayout.PropertyField(fadeModeProperty); + var fadeMode = (LODFadeMode)fadeModeProperty.intValue; + + bool hasCrossFade = (fadeMode == LODFadeMode.CrossFade || fadeMode == LODFadeMode.SpeedTree); + if (hasCrossFade) + { + EditorGUILayout.PropertyField(animateCrossFadingProperty); + } + + EditorGUILayout.PropertyField(autoCollectRenderersProperty); + DrawSimplificationOptions(); + + bool newHasSaveAssetsPath = EditorGUILayout.Toggle(overrideSaveAssetsPathContent, overrideSaveAssetsPath); + if (newHasSaveAssetsPath != overrideSaveAssetsPath) + { + overrideSaveAssetsPath = newHasSaveAssetsPath; + saveAssetsPathProperty.stringValue = string.Empty; + serializedObject.ApplyModifiedProperties(); + GUIUtility.ExitGUI(); + } + + if (overrideSaveAssetsPath) + { + EditorGUILayout.PropertyField(saveAssetsPathProperty); + } + + if (settingsExpanded == null || settingsExpanded.Length != levelsProperty.arraySize) + { + var newSettingsExpanded = new bool[levelsProperty.arraySize]; + if (settingsExpanded != null) + { + System.Array.Copy(settingsExpanded, 0, newSettingsExpanded, 0, Mathf.Min(settingsExpanded.Length, newSettingsExpanded.Length)); + } + settingsExpanded = newSettingsExpanded; + } + + for (int levelIndex = 0; levelIndex < levelsProperty.arraySize; levelIndex++) + { + var levelProperty = levelsProperty.GetArrayElementAtIndex(levelIndex); + DrawLevel(levelIndex, levelProperty, hasCrossFade); + } + + if (GUILayout.Button(createLevelButtonContent)) + { + CreateLevel(); + } + + if (GUILayout.Button(generateLODButtonContent)) + { + GenerateLODs(); + } + } + + private void DrawSimplificationOptions() + { + if (EditorGUILayout.PropertyField(simplificationOptionsProperty, false)) + { + ++EditorGUI.indentLevel; + + var enableSmartLinkProperty = simplificationOptionsProperty.FindPropertyRelative(SimplificationOptionsEnableSmartLinkFieldName); + + var childProperties = simplificationOptionsProperty.GetChildProperties(); + foreach (var childProperty in childProperties) + { + if (!enableSmartLinkProperty.boolValue && string.Equals(childProperty.name, SimplificationOptionsVertexLinkDistanceFieldName)) + continue; + + EditorGUILayout.PropertyField(childProperty, true); + } + + --EditorGUI.indentLevel; + } + } + + private void DrawLevel(int index, SerializedProperty levelProperty, bool hasCrossFade) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); + GUILayout.Label(string.Format("Level {0}", index + 1), EditorStyles.boldLabel); + + var previousBackgroundColor = GUI.backgroundColor; + GUI.backgroundColor = removeColor; + if (GUILayout.Button(deleteLevelButtonContent, GUILayout.Width(RemoveLevelButtonSize))) + { + DeleteLevel(index); + } + GUI.backgroundColor = previousBackgroundColor; + EditorGUILayout.EndHorizontal(); + + ++EditorGUI.indentLevel; + + var screenRelativeHeightProperty = levelProperty.FindPropertyRelative(LevelScreenRelativeHeightFieldName); + EditorGUILayout.PropertyField(screenRelativeHeightProperty); + + var qualityProperty = levelProperty.FindPropertyRelative(LevelQualityFieldName); + EditorGUILayout.PropertyField(qualityProperty); + + bool animateCrossFading = (hasCrossFade ? animateCrossFadingProperty.boolValue : false); + settingsExpanded[index] = EditorGUILayout.Foldout(settingsExpanded[index], settingsContent); + if (settingsExpanded[index]) + { + ++EditorGUI.indentLevel; + + var combineMeshesProperty = levelProperty.FindPropertyRelative(LevelCombineMeshesFieldName); + EditorGUILayout.PropertyField(combineMeshesProperty); + + if (combineMeshesProperty.boolValue) + { + var combineSubMeshesProperty = levelProperty.FindPropertyRelative(LevelCombineSubMeshesFieldName); + EditorGUILayout.PropertyField(combineSubMeshesProperty); + } + + var childProperties = levelProperty.GetChildProperties(); + foreach (var childProperty in childProperties) + { + if (string.Equals(childProperty.name, LevelScreenRelativeHeightFieldName) || string.Equals(childProperty.name, LevelQualityFieldName) || + string.Equals(childProperty.name, LevelCombineMeshesFieldName) || string.Equals(childProperty.name, LevelCombineSubMeshesFieldName) || + string.Equals(childProperty.name, LevelRenderersFieldName)) + { + continue; + } + else if ((!hasCrossFade || !animateCrossFading) && string.Equals(childProperty.name, LevelFadeTransitionWidthFieldName)) + { + continue; + } + + EditorGUILayout.PropertyField(childProperty, true); + } + + --EditorGUI.indentLevel; + } + + // Remove any null renderers + var renderersProperty = levelProperty.FindPropertyRelative(LevelRenderersFieldName); + for (int rendererIndex = renderersProperty.arraySize - 1; rendererIndex >= 0; rendererIndex--) + { + var rendererProperty = renderersProperty.GetArrayElementAtIndex(rendererIndex); + var renderer = rendererProperty.objectReferenceValue as Renderer; + if (renderer == null) + { + renderersProperty.DeleteArrayElementAtIndex(rendererIndex); + } + } + + bool autoCollectRenderers = autoCollectRenderersProperty.boolValue; + if (!autoCollectRenderers) + { + DrawRendererList(renderersProperty, EditorGUIUtility.currentViewWidth); + } + + --EditorGUI.indentLevel; + EditorGUILayout.EndVertical(); + } + + private void DrawRendererList(SerializedProperty renderersProperty, float availableWidth) + { + GUILayout.Label(renderersHeaderContent, EditorStyles.boldLabel); + + int rendererCount = renderersProperty.arraySize; + int renderersPerRow = Mathf.Max(1, Mathf.FloorToInt(availableWidth / RendererButtonWidth)); + int rendererRowCount = Mathf.CeilToInt((float)(rendererCount + 1) / (float)renderersPerRow); + + var listPosition = GUILayoutUtility.GetRect(0f, rendererRowCount * RendererButtonWidth, GUILayout.ExpandWidth(true)); + GUI.Box(listPosition, GUIContent.none, EditorStyles.helpBox); + + var listInnerPosition = new Rect(listPosition.x + 3f, listPosition.y, listPosition.width - 6f, listPosition.height); + float buttonWidth = listInnerPosition.width / (float)renderersPerRow; + for (int rendererIndex = 0; rendererIndex < renderersProperty.arraySize; rendererIndex++) + { + int rowIndex = rendererIndex / renderersPerRow; + int colIndex = rendererIndex % renderersPerRow; + var rendererProperty = renderersProperty.GetArrayElementAtIndex(rendererIndex); + var renderer = rendererProperty.objectReferenceValue as Renderer; + + var buttonPosition = new Rect(listInnerPosition.x + (colIndex * buttonWidth), listInnerPosition.y + (rowIndex * RendererButtonWidth) + 2f, + buttonWidth - 4f, RendererButtonWidth - 4f); + DrawRendererButton(buttonPosition, renderersProperty, rendererIndex, renderer); + } + + int addButtonRowIndex = rendererCount / renderersPerRow; + int addButtonColIndex = rendererCount % renderersPerRow; + var addButtonPosition = new Rect(listInnerPosition.x + (addButtonColIndex * buttonWidth), listInnerPosition.y + (addButtonRowIndex * RendererButtonWidth) + 2f, + buttonWidth - 4f, RendererButtonWidth - 4f); + HandleAddRenderer(addButtonPosition, listPosition, renderersProperty); + } + + private void DrawRendererButton(Rect position, SerializedProperty renderersProperty, int rendererIndex, Renderer renderer) + { + var current = Event.current; + var currentEvent = current.type; + var removeButtonPosition = new Rect(position.xMax - RemoveRendererButtonSize, position.yMax - RemoveRendererButtonSize, RemoveRendererButtonSize, RemoveRendererButtonSize); + + if (currentEvent != EventType.Repaint) + { + if (currentEvent == EventType.MouseDown && current.button == 0) + { + if (removeButtonPosition.Contains(current.mousePosition)) + { + renderersProperty.DeleteArrayElementAtIndex(rendererIndex); + current.Use(); + serializedObject.ApplyModifiedProperties(); + GUIUtility.ExitGUI(); + } + else if (position.Contains(current.mousePosition)) + { + Debug.Log(""); + EditorGUIUtility.PingObject(renderer); + current.Use(); + } + } + } + else + { + if (renderer != null) + { + GUIContent content = null; + var skinnedMeshRenderer = (renderer as SkinnedMeshRenderer); + if (skinnedMeshRenderer != null) + { + var meshPreview = AssetPreview.GetAssetPreview(skinnedMeshRenderer.sharedMesh); + content = new GUIContent(meshPreview, renderer.gameObject.name); + } + else + { + var meshFilter = renderer.GetComponent(); + if (meshFilter != null && meshFilter.sharedMesh != null) + { + var meshPreview = AssetPreview.GetAssetPreview(meshFilter.sharedMesh); + content = new GUIContent(meshPreview, renderer.gameObject.name); + } + else + { + string niceRendererTypeName = ObjectNames.NicifyVariableName(renderer.GetType().Name); + content = new GUIContent(niceRendererTypeName, renderer.gameObject.name); + } + } + + var buttonPosition = new Rect(position.x + 2f, position.y + 2f, position.width - 4f, position.height - 4f); + GUI.Box(position, GUIContent.none, EditorStyles.helpBox); + GUI.Box(buttonPosition, content); + } + else + { + GUI.Box(position, GUIContent.none, EditorStyles.helpBox); + } + + var previousBackgroundColor = GUI.backgroundColor; + GUI.backgroundColor = removeColor; + GUI.Box(removeButtonPosition, removeRendererButtonContent, EditorStyles.miniButton); + GUI.backgroundColor = previousBackgroundColor; + } + } + + private void HandleAddRenderer(Rect position, Rect listArea, SerializedProperty renderersProperty) + { + if (GUI.Button(position, addRendererButtonContent)) + { + EditorGUIUtility.ShowObjectPicker(null, true, string.Empty, ObjectPickerControlID); + GUIUtility.ExitGUI(); + } + + var current = Event.current; + var currentEvent = current.type; + if (currentEvent == EventType.DragUpdated || currentEvent == EventType.DragPerform) + { + if (listArea.Contains(current.mousePosition)) + { + var dragObjects = DragAndDrop.objectReferences; + if (dragObjects != null && dragObjects.Length > 0) + { + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + if (currentEvent == EventType.DragPerform) + { + var draggedGameObjects = from go in dragObjects + where go as GameObject != null + select go as GameObject; + var draggedRenderers = from renderer in dragObjects + where renderer as Renderer != null + select renderer as Renderer; + var gameObjectRenderers = GetRenderers(draggedGameObjects, true); + AddRenderers(renderersProperty, draggedRenderers, true); + AddRenderers(renderersProperty, gameObjectRenderers, true); + DragAndDrop.AcceptDrag(); + } + } + + current.Use(); + } + } + else if (currentEvent == EventType.ExecuteCommand) + { + string commandName = current.commandName; + if (string.Equals(commandName, "ObjectSelectorClosed") && EditorGUIUtility.GetObjectPickerControlID() == ObjectPickerControlID) + { + var gameObject = EditorGUIUtility.GetObjectPickerObject() as GameObject; + if (gameObject != null) + { + var gameObjectRenderers = GetRenderers(new GameObject[] { gameObject }, true); + AddRenderers(renderersProperty, gameObjectRenderers, true); + } + current.Use(); + GUIUtility.ExitGUI(); + } + } + } + + private void AddRenderers(SerializedProperty renderersProperty, IEnumerable renderers, bool append) + { + if (!append) + { + renderersProperty.ClearArray(); + } + + var existingRendererList = new List(renderersProperty.arraySize); + for (int i = 0; i < renderersProperty.arraySize; i++) + { + var rendererProperty = renderersProperty.GetArrayElementAtIndex(i); + var renderer = rendererProperty.objectReferenceValue as Renderer; + if (renderer != null) + { + existingRendererList.Add(renderer); + } + } + + foreach (var renderer in renderers) + { + if (!existingRendererList.Contains(renderer)) + { + ++renderersProperty.arraySize; + var rendererProperty = renderersProperty.GetArrayElementAtIndex(renderersProperty.arraySize - 1); + rendererProperty.objectReferenceValue = renderer; + existingRendererList.Add(renderer); + } + } + + serializedObject.ApplyModifiedProperties(); + } + + private void CreateLevel() + { + int newIndex = levelsProperty.arraySize; + levelsProperty.InsertArrayElementAtIndex(newIndex); + var newLevelProperty = levelsProperty.GetArrayElementAtIndex(newIndex); + var lastLevelProperty = (newIndex > 0 ? levelsProperty.GetArrayElementAtIndex(newIndex - 1) : null); + var newScreenRelativeHeightProperty = newLevelProperty.FindPropertyRelative(LevelScreenRelativeHeightFieldName); + var newQualityProperty = newLevelProperty.FindPropertyRelative(LevelQualityFieldName); + + if (lastLevelProperty != null) + { + var lastScreenRelativeHeightProperty = lastLevelProperty.FindPropertyRelative(LevelScreenRelativeHeightFieldName); + var lastQualityProperty = lastLevelProperty.FindPropertyRelative(LevelQualityFieldName); + newScreenRelativeHeightProperty.floatValue = lastScreenRelativeHeightProperty.floatValue * 0.5f; + newQualityProperty.floatValue = lastQualityProperty.floatValue * 0.65f; + } + else + { + newScreenRelativeHeightProperty.floatValue = 0.6f; + newQualityProperty.floatValue = 1f; + } + + serializedObject.ApplyModifiedProperties(); + GUIUtility.ExitGUI(); + } + + private void DeleteLevel(int index) + { + levelsProperty.DeleteArrayElementAtIndex(index); + serializedObject.ApplyModifiedProperties(); + GUIUtility.ExitGUI(); + } + + private void GenerateLODs() + { + try + { + EditorUtility.DisplayProgressBar("Generating LODs", "Generating LODs...", 0f); + var lodGroup = LODGenerator.GenerateLODs(lodGeneratorHelper); + if (lodGroup != null) + { + using (var serializedObject = new SerializedObject(lodGeneratorHelper)) + { + var isGeneratedProperty = serializedObject.FindProperty(IsGeneratedFieldName); + serializedObject.UpdateIfRequiredOrScript(); + isGeneratedProperty.boolValue = true; + serializedObject.ApplyModifiedPropertiesWithoutUndo(); + } + } + } + catch (System.Exception ex) + { + Debug.LogException(ex); + DisplayError("Failed to generate LODs!", ex.Message, "OK", lodGeneratorHelper); + } + finally + { + EditorUtility.ClearProgressBar(); + } + } + + private void DestroyLODs() + { + try + { + EditorUtility.DisplayProgressBar("Destroying LODs", "Destroying LODs...", 0f); + LODGenerator.DestroyLODs(lodGeneratorHelper); + + using (var serializedObject = new SerializedObject(lodGeneratorHelper)) + { + var isGeneratedProperty = serializedObject.FindProperty(IsGeneratedFieldName); + serializedObject.UpdateIfRequiredOrScript(); + isGeneratedProperty.boolValue = false; + serializedObject.ApplyModifiedPropertiesWithoutUndo(); + } + } + catch (System.Exception ex) + { + Debug.LogException(ex); + DisplayError("Failed to destroy LODs!", ex.Message, "OK", lodGeneratorHelper); + } + finally + { + EditorUtility.ClearProgressBar(); + } + } + + private Renderer[] GetRenderers(IEnumerable gameObjects, bool searchChildren) + { + // Filter out game objects that aren't children of the generator + var ourTransform = lodGeneratorHelper.transform; + var childGameObjects = from go in gameObjects + where go.transform.IsChildOf(ourTransform) + select go; + + var notChildGameObjects = from go in gameObjects + where !go.transform.IsChildOf(ourTransform) +#if UNITY_2018_3 || UNITY_2018_4 || UNITY_2019 + && !PrefabUtility.IsPartOfAnyPrefab(go) +#endif + select go; + +#if UNITY_2018_3 || UNITY_2018_4 || UNITY_2019 + var prefabGameObjects = from go in gameObjects + where !go.transform.IsChildOf(ourTransform) && + PrefabUtility.IsPartOfAnyPrefab(go) + select go; + + if (prefabGameObjects.Any()) + { + EditorUtility.DisplayDialog("Invalid GameObjects", "Some objects are not children of the LODGenerator GameObject," + + " as well as being part of a prefab. They will not be added.", "OK"); + } +#endif + + if (notChildGameObjects.Any()) + { + if (EditorUtility.DisplayDialog("Reparent GameObjects", "Some objects are not children of the LODGenerator GameObject." + + " Do you want to reparent them and add them to the LODGenerator?", "Yes, Reparent", "No, Use Only Existing Children")) + { + var relocatedList = new List(); + foreach (var gameObject in notChildGameObjects) + { + gameObject.transform.SetParent(ourTransform, true); + relocatedList.Add(gameObject); + } + + childGameObjects = childGameObjects.Union(relocatedList); + } + } + + var rendererList = new List(); + foreach (var gameObject in childGameObjects) + { + if (searchChildren) + { + var renderers = gameObject.GetComponentsInChildren(); + foreach (var renderer in renderers) + { + if (!rendererList.Contains(renderer)) + { + rendererList.Add(renderer); + } + } + } + else + { + var renderer = gameObject.GetComponent(); + if (renderer != null) + { + rendererList.Add(renderer); + } + } + } + + return rendererList.ToArray(); + } + + private static void DisplayError(string title, string message, string ok, Object context) + { + EditorUtility.DisplayDialog(title, message, ok); + } + } +} diff --git a/Editor/LODGeneratorHelperEditor.cs.meta b/Editor/LODGeneratorHelperEditor.cs.meta new file mode 100644 index 0000000..a06d298 --- /dev/null +++ b/Editor/LODGeneratorHelperEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9ac9095af6a98548ba6dd116c3e5136 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SerializedPropertyExtensions.cs b/Editor/SerializedPropertyExtensions.cs new file mode 100644 index 0000000..b5b72c7 --- /dev/null +++ b/Editor/SerializedPropertyExtensions.cs @@ -0,0 +1,50 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System.Collections.Generic; +using UnityEditor; + +namespace UnityMeshSimplifier.Editor +{ + internal static class SerializedPropertyExtensions + { + public static IEnumerable GetChildProperties(this SerializedProperty property) + { + int originalDepth = property.depth; + var childProperty = property.Copy(); + if (!childProperty.NextVisible(true)) + yield break; // There was no more properties + + while (childProperty.depth > originalDepth) + { + yield return childProperty; + + if (!childProperty.NextVisible(false)) + break; + } + } + } +} \ No newline at end of file diff --git a/Editor/SerializedPropertyExtensions.cs.meta b/Editor/SerializedPropertyExtensions.cs.meta new file mode 100644 index 0000000..e9c06dc --- /dev/null +++ b/Editor/SerializedPropertyExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 17ae6f438c9465c49b2c66a289fdd1c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Whinarn.UnityMeshSimplifier.Editor.asmdef b/Editor/Whinarn.UnityMeshSimplifier.Editor.asmdef new file mode 100644 index 0000000..b2bce0a --- /dev/null +++ b/Editor/Whinarn.UnityMeshSimplifier.Editor.asmdef @@ -0,0 +1,17 @@ +{ + "name": "Whinarn.UnityMeshSimplifier.Editor", + "references": [ + "Whinarn.UnityMeshSimplifier.Runtime" + ], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [] +} \ No newline at end of file diff --git a/Scripts/UnityMeshSimplifier.asmdef.meta b/Editor/Whinarn.UnityMeshSimplifier.Editor.asmdef.meta similarity index 76% rename from Scripts/UnityMeshSimplifier.asmdef.meta rename to Editor/Whinarn.UnityMeshSimplifier.Editor.asmdef.meta index a529ab5..d73fb8a 100644 --- a/Scripts/UnityMeshSimplifier.asmdef.meta +++ b/Editor/Whinarn.UnityMeshSimplifier.Editor.asmdef.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 77ccaf49895b0d64e87cd4b4faf83c49 +guid: a09b502a3463f794a83cf80a67a246d2 AssemblyDefinitionImporter: externalObjects: {} userData: diff --git a/README.md b/README.md index c669411..31e1318 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,30 @@ Mesh simplification for [Unity](https://unity3d.com/). The project is deeply bas Because of the fact that this project is entirely in C# it *should* work on all platforms that Unity officially supports, as well as both in the editor and at runtime in builds. ## Compatibility -These scripts have been tested and confirmed working with Unity 5.6.0f3, Unity 2017.1.0f3, Unity 2017.2.1f1, Unity 2017.3.0f3, Unity 2017.4.0f1 and Unity 2018.1.2f1. +Because this project is now using Unity packages, you should use a Unity version from 2018.1 and beyond. +Although some scripts have been tested and confirmed working as far back as Unity 5.6, it will no longer be officially supported. +Unity introduced the package manager in Unity 2017.2, but at a very early state. ## Installation into Unity project -1. Copy the contents of this repository into a folder named *UnityMeshSimplifier* in your Assets directory within your Unity project. -2. Done! +1. Read the instructions from the official Unity documentation: https://docs.unity3d.com/Manual/upm-dependencies.html#Git +2. Open up *manifest.json* inside the *Packages* directory in your Unity project using a text editor. +3. Under the dependencies section of this file, you should add the following line at the top: +```"com.whinarn.unitymeshsimplifier": "https://github.com/Whinarn/UnityMeshSimplifier.git",``` +4. You should now see something like this: +``` +{ + "dependencies": { + "com.whinarn.unitymeshsimplifier": "https://github.com/Whinarn/UnityMeshSimplifier.git", + "com.unity.burst": "1.0.4", + "com.unity.mathematics": "1.0.1", + "com.unity.package-manager-ui": "2.1.2", + ... + } +} +``` +5. You can also specify to use a specific version of UnityMeshSimplifier if you wish by appending # to the Git URL followed by the package version. For example: +```"com.whinarn.unitymeshsimplifier": "https://github.com/Whinarn/UnityMeshSimplifier.git#v1.1.0",``` +6. Success! Start up Unity with your Unity project and you should see UnityMeshSimplifier appear in the Unity Package Manager. ## How do I use this? ```c# @@ -31,6 +50,16 @@ var newVertices = meshSimplifier.Vertices; var newIndices = meshSimplifier.GetSubMeshTriangles(0); ``` +## How do I contribute? +1. Create a new empty Unity project, or use an existing one if you wish. +2. Fork your own copy of this repository. +2. Clone your UnityMeshSimplifier fork into the *Packages* directory of your Unity project. +3. Start up your Unity project and you should see UnityMeshSimplifier appear in the Unity Package Manager. +4. Open the scripts inside of the Unity package as you would normally do with scripts in your *Assets* directory. +5. Make your changes inside a branch based on *master*. +6. Create a pull request to the official repository. +7. Success! + ## The Smart Linking feature In order to solve artifacts in the mesh simplification process where holes or other serious issues could arise, a new feature called smart linking has been introduced. This feature is enabled by default but can be disabled through the *EnableSmartLink* property on the *MeshSimplifier* class. Disabling this could give you a minor performance gain in cases where you do not need this. @@ -43,6 +72,26 @@ There are several ways to solve this problem. The smart linking feature (mention The recommendation is to use the smart linking feature that is enabled by default, but the options for preservation exists in those cases where you may want it. +## My animated meshes don't work, why? +This is most probably because there is currently no code for moving the [bindposes](https://docs.unity3d.com/ScriptReference/Mesh-bindposes.html) over between the original and the simplified mesh. This can be easily resolved by copying over (no need to modify) the bindposes like this: + +```c# +float quality = 0.5f; +var meshSimplifier = new UnityMeshSimplifier.MeshSimplifier(); +meshSimplifier.Initialize(sourceMesh); +meshSimplifier.SimplifyMesh(quality); +var destMesh = meshSimplifier.ToMesh(); +destMesh.bindposes = sourceMesh.bindposes; // <-- this line should fix your issue +``` + +## How can I automatically generated LOD Groups? +There is a component named *LOD Generator Helper* that you add to the game object that you want to generate LODs for. You can customize, generate and destroy the LOD levels directly through the inspector. Any changes is saved within the component so that you can easily make the changes that you want without having to waste time reconfiguring everything again. Additional steps have been taken in order to protect your game objects from damage and makes sure that they can be restored back to their original state. Backups are always recommended however, to make sure that you do not ever lose any configuration that you have made. + +There is also a static API at *UnityMeshSimplifier.LODGenerator* that you can use from code to generate and destroy LODs, both at runtime and in the editor. + +## Some objects are not animated correctly after I have generated LOD Groups +The most probable cause for this is that you have objects that are parented under bones that will move with the animations. Currently there is no code to deal with this, and the best way to do this is to use nested LOD Groups. Any such object that you know is parented under a bone should have its own LOD Group. + ## The Unity-generated Visual Studio solution file appears broken This can be a problem because of an assembly definition provided with this repository, if you are using Unity 2017.3 or above. Make sure that you have the latest version of [Visual Studio Tools for Unity](https://www.visualstudio.com/vs/unity-tools/). If you are using Visual Studio 2017, make sure that Visual Studio is up to date and that you have installed the *Game development with Unity* component. For other versions of Visual Studio you would have to download a separate installer. Please go to the [Microsoft Documentation](https://docs.microsoft.com/en-us/visualstudio/cross-platform/getting-started-with-visual-studio-tools-for-unity) for more information. diff --git a/Scripts.meta b/Runtime.meta similarity index 100% rename from Scripts.meta rename to Runtime.meta diff --git a/Runtime/BlendShape.cs b/Runtime/BlendShape.cs new file mode 100644 index 0000000..69634d3 --- /dev/null +++ b/Runtime/BlendShape.cs @@ -0,0 +1,97 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using UnityEngine; + +namespace UnityMeshSimplifier +{ + /// + /// A blend shape. + /// + [Serializable] + public struct BlendShape + { + /// + /// The name of the blend shape. + /// + public string ShapeName; + /// + /// The blend shape frames. + /// + public BlendShapeFrame[] Frames; + + /// + /// Creates a new blend shape. + /// + /// The name of the blend shape. + /// The blend shape frames. + public BlendShape(string shapeName, BlendShapeFrame[] frames) + { + this.ShapeName = shapeName; + this.Frames = frames; + } + } + + /// + /// A blend shape frame. + /// + [Serializable] + public struct BlendShapeFrame + { + /// + /// The weight of the blend shape frame. + /// + public float FrameWeight; + /// + /// The delta vertices of the blend shape frame. + /// + public Vector3[] DeltaVertices; + /// + /// The delta normals of the blend shape frame. + /// + public Vector3[] DeltaNormals; + /// + /// The delta tangents of the blend shape frame. + /// + public Vector3[] DeltaTangents; + + /// + /// Creates a new blend shape frame. + /// + /// The weight of the blend shape frame. + /// The delta vertices of the blend shape frame. + /// The delta normals of the blend shape frame. + /// The delta tangents of the blend shape frame. + public BlendShapeFrame(float frameWeight, Vector3[] deltaVertices, Vector3[] deltaNormals, Vector3[] deltaTangents) + { + this.FrameWeight = frameWeight; + this.DeltaVertices = deltaVertices; + this.DeltaNormals = deltaNormals; + this.DeltaTangents = deltaTangents; + } + } +} diff --git a/Runtime/BlendShape.cs.meta b/Runtime/BlendShape.cs.meta new file mode 100644 index 0000000..1fe72a6 --- /dev/null +++ b/Runtime/BlendShape.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f4953476e185d34c97f4c00f8c9c5ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Components.meta b/Runtime/Components.meta new file mode 100644 index 0000000..28f5a53 --- /dev/null +++ b/Runtime/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fd13b03ef549b27499e01e3202d66c51 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Components/LODBackupComponent.cs b/Runtime/Components/LODBackupComponent.cs new file mode 100644 index 0000000..68468d3 --- /dev/null +++ b/Runtime/Components/LODBackupComponent.cs @@ -0,0 +1,43 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using UnityEngine; + +namespace UnityMeshSimplifier +{ + [AddComponentMenu("")] + internal class LODBackupComponent : MonoBehaviour + { + [SerializeField] + private Renderer[] originalRenderers = null; + + public Renderer[] OriginalRenderers + { + get { return originalRenderers; } + set { originalRenderers = value; } + } + } +} diff --git a/Runtime/Components/LODBackupComponent.cs.meta b/Runtime/Components/LODBackupComponent.cs.meta new file mode 100644 index 0000000..7479b46 --- /dev/null +++ b/Runtime/Components/LODBackupComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4e0eb9661dcb8ea4f8acb45586df32e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Components/LODGeneratorHelper.cs b/Runtime/Components/LODGeneratorHelper.cs new file mode 100644 index 0000000..c162164 --- /dev/null +++ b/Runtime/Components/LODGeneratorHelper.cs @@ -0,0 +1,172 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using UnityEngine; + +namespace UnityMeshSimplifier +{ + /// + /// A LOD (level of detail) generator helper. + /// + [AddComponentMenu("Rendering/LOD Generator Helper")] + public sealed class LODGeneratorHelper : MonoBehaviour + { + #region Fields + [SerializeField, Tooltip("The fade mode used by the created LOD group.")] + private LODFadeMode fadeMode = LODFadeMode.None; + [SerializeField, Tooltip("If the cross-fading should be animated by time.")] + private bool animateCrossFading = false; + + [SerializeField, Tooltip("If the renderers under this game object and any children should be automatically collected.")] + private bool autoCollectRenderers = true; + + [SerializeField, Tooltip("The simplification options.")] + private SimplificationOptions simplificationOptions = SimplificationOptions.Default; + + [SerializeField, Tooltip("The path within the project to save the generated assets. Leave this empty to use the default path.")] + private string saveAssetsPath = string.Empty; + + [SerializeField, Tooltip("The LOD levels.")] + private LODLevel[] levels = null; + + [SerializeField] + private bool isGenerated = false; + #endregion + + #region Properties + /// + /// Gets or sets the fade mode used by the created LOD group. + /// + public LODFadeMode FadeMode + { + get { return fadeMode; } + set { fadeMode = value; } + } + + /// + /// Gets or sets if the cross-fading should be animated by time. The animation duration + /// is specified globally as crossFadeAnimationDuration. + /// + public bool AnimateCrossFading + { + get { return animateCrossFading; } + set { animateCrossFading = value; } + } + + /// + /// Gets or sets if the renderers under this game object and any children should be automatically collected. + /// + public bool AutoCollectRenderers + { + get { return autoCollectRenderers; } + set { autoCollectRenderers = value; } + } + + /// + /// Gets or sets the simplification options. + /// + public SimplificationOptions SimplificationOptions + { + get { return simplificationOptions; } + set { simplificationOptions = value; } + } + + /// + /// Gets or sets the path within the project to save the generated assets. + /// Leave this empty to use the default path. + /// + public string SaveAssetsPath + { + get { return saveAssetsPath; } + set { saveAssetsPath = value; } + } + + /// + /// Gets or sets the LOD levels for this generator. + /// + public LODLevel[] Levels + { + get { return levels; } + set { levels = value; } + } + + /// + /// Gets if the LODs have been generated. + /// + public bool IsGenerated + { + get { return isGenerated; } + } + #endregion + + #region Unity Events + private void Reset() + { + fadeMode = LODFadeMode.None; + animateCrossFading = false; + autoCollectRenderers = true; + simplificationOptions = SimplificationOptions.Default; + + levels = new LODLevel[] + { + new LODLevel(0.5f, 1f) + { + CombineMeshes = false, + CombineSubMeshes = false, + SkinQuality = SkinQuality.Auto, + ShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On, + ReceiveShadows = true, + SkinnedMotionVectors = true, + LightProbeUsage = UnityEngine.Rendering.LightProbeUsage.BlendProbes, + ReflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.BlendProbes, + }, + new LODLevel(0.17f, 0.65f) + { + CombineMeshes = true, + CombineSubMeshes = false, + SkinQuality = SkinQuality.Auto, + ShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On, + ReceiveShadows = true, + SkinnedMotionVectors = true, + LightProbeUsage = UnityEngine.Rendering.LightProbeUsage.BlendProbes, + ReflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Simple + }, + new LODLevel(0.02f, 0.4225f) + { + CombineMeshes = true, + CombineSubMeshes = true, + SkinQuality = SkinQuality.Bone2, + ShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off, + ReceiveShadows = false, + SkinnedMotionVectors = false, + LightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off, + ReflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off + } + }; + } + #endregion + } +} \ No newline at end of file diff --git a/Runtime/Components/LODGeneratorHelper.cs.meta b/Runtime/Components/LODGeneratorHelper.cs.meta new file mode 100644 index 0000000..d4d861c --- /dev/null +++ b/Runtime/Components/LODGeneratorHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3b6501244c613244685e05adcefbedff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LODGenerator.cs b/Runtime/LODGenerator.cs new file mode 100644 index 0000000..fad6226 --- /dev/null +++ b/Runtime/LODGenerator.cs @@ -0,0 +1,872 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace UnityMeshSimplifier +{ + /// + /// Contains methods for generating LODs (level of details) for game objects. + /// + public static class LODGenerator + { + #region Consts + /// + /// The name of the game object where generated LODs are parented under. + /// + public const string LODParentGameObjectName = "_UMS_LODs_"; + + /// + /// The parent path for generated LOD assets. + /// + public const string LODAssetParentPath = "Assets/UMS_LODs/"; + #endregion + + #region Structs + private struct StaticRenderer + { + public string name; + public bool isNewMesh; + public Transform transform; + public Mesh mesh; + public Material[] materials; + } + + private struct SkinnedRenderer + { + public string name; + public bool isNewMesh; + public Transform transform; + public Mesh mesh; + public Material[] materials; + public Transform rootBone; + public Transform[] bones; + } + #endregion + + #region Public Methods + /// + /// Generates the LODs and sets up a LOD Group for the LOD generator helper component. + /// + /// The LOD generator helper. + /// The generated LOD Group. + public static LODGroup GenerateLODs(LODGeneratorHelper generatorHelper) + { + if (generatorHelper == null) + throw new System.ArgumentNullException(nameof(generatorHelper)); + + var gameObject = generatorHelper.gameObject; + var levels = generatorHelper.Levels; + bool autoCollectRenderers = generatorHelper.AutoCollectRenderers; + var simplificationOptions = generatorHelper.SimplificationOptions; + string saveAssetsPath = generatorHelper.SaveAssetsPath; + + var lodGroup = GenerateLODs(gameObject, levels, autoCollectRenderers, simplificationOptions, saveAssetsPath); + if (lodGroup == null) + return null; + + lodGroup.animateCrossFading = generatorHelper.AnimateCrossFading; + lodGroup.fadeMode = generatorHelper.FadeMode; + return lodGroup; + } + + /// + /// Generates the LODs and sets up a LOD Group for the specified game object. + /// + /// The game object to set up. + /// The LOD levels to set up. + /// If the renderers under the game object and any children should be automatically collected. + /// Enabling this will ignore any renderers defined under each LOD level. + /// The mesh simplification options. + /// The generated LOD Group. + public static LODGroup GenerateLODs(GameObject gameObject, LODLevel[] levels, bool autoCollectRenderers, SimplificationOptions simplificationOptions) + { + return GenerateLODs(gameObject, levels, autoCollectRenderers, simplificationOptions, null); + } + + /// + /// Generates the LODs and sets up a LOD Group for the specified game object. + /// + /// The game object to set up. + /// The LOD levels to set up. + /// If the renderers under the game object and any children should be automatically collected. + /// Enabling this will ignore any renderers defined under each LOD level. + /// The mesh simplification options. + /// The path to where the generated assets should be saved. Can be null or empty to use the default path. + /// The generated LOD Group. + public static LODGroup GenerateLODs(GameObject gameObject, LODLevel[] levels, bool autoCollectRenderers, SimplificationOptions simplificationOptions, string saveAssetsPath) + { + if (gameObject == null) + throw new System.ArgumentNullException(nameof(gameObject)); + else if (levels == null) + throw new System.ArgumentNullException(nameof(levels)); + + var transform = gameObject.transform; + var existingLodParent = transform.Find(LODParentGameObjectName); + if (existingLodParent != null) + throw new System.InvalidOperationException("The game object already appears to have LODs. Please remove them first."); + + var existingLodGroup = gameObject.GetComponent(); + if (existingLodGroup != null) + throw new System.InvalidOperationException("The game object already appears to have a LOD Group. Please remove it first."); + + saveAssetsPath = ValidateSaveAssetsPath(saveAssetsPath); + + var lodParentGameObject = new GameObject(LODParentGameObjectName); + var lodParent = lodParentGameObject.transform; + ParentAndResetTransform(lodParent, transform); + + var lodGroup = gameObject.AddComponent(); + + Renderer[] allRenderers = null; + if (autoCollectRenderers) + { + // Collect all enabled renderers under the game object + allRenderers = GetChildRenderersForLOD(gameObject); + } + + var renderersToDisable = new List((allRenderers != null ? allRenderers.Length : 10)); + var lods = new LOD[levels.Length]; + for (int levelIndex = 0; levelIndex < levels.Length; levelIndex++) + { + var level = levels[levelIndex]; + var levelGameObject = new GameObject(string.Format("Level{0:00}", levelIndex)); + var levelTransform = levelGameObject.transform; + ParentAndResetTransform(levelTransform, lodParent); + + Renderer[] originalLevelRenderers = allRenderers ?? level.Renderers; + var levelRenderers = new List((originalLevelRenderers != null ? originalLevelRenderers.Length : 0)); + + if (originalLevelRenderers != null && originalLevelRenderers.Length > 0) + { + var meshRenderers = (from renderer in originalLevelRenderers + where renderer.enabled && renderer as MeshRenderer != null + select renderer as MeshRenderer).ToArray(); + var skinnedMeshRenderers = (from renderer in originalLevelRenderers + where renderer.enabled && renderer as SkinnedMeshRenderer != null + select renderer as SkinnedMeshRenderer).ToArray(); + + StaticRenderer[] staticRenderers; + SkinnedRenderer[] skinnedRenderers; + if (level.CombineMeshes) + { + staticRenderers = CombineStaticMeshes(transform, levelIndex, meshRenderers); + skinnedRenderers = CombineSkinnedMeshes(transform, levelIndex, skinnedMeshRenderers); + } + else + { + staticRenderers = GetStaticRenderers(meshRenderers); + skinnedRenderers = GetSkinnedRenderers(skinnedMeshRenderers); + } + + if (staticRenderers != null) + { + for (int rendererIndex = 0; rendererIndex < staticRenderers.Length; rendererIndex++) + { + var renderer = staticRenderers[rendererIndex]; + var mesh = renderer.mesh; + + // Simplify the mesh if necessary + if (level.Quality < 1f) + { + mesh = SimplifyMesh(mesh, level.Quality, simplificationOptions); + SaveLODMeshAsset(mesh, gameObject.name, renderer.name, levelIndex, renderer.mesh.name, saveAssetsPath); + + if (renderer.isNewMesh) + { + DestroyObject(renderer.mesh); + renderer.mesh = null; + } + } + + string rendererName = string.Format("{0:000}_static_{1}", rendererIndex, renderer.name); + var levelRenderer = CreateLevelRenderer(rendererName, levelTransform, renderer.transform, mesh, renderer.materials, ref level); + levelRenderers.Add(levelRenderer); + } + } + + if (skinnedRenderers != null) + { + for (int rendererIndex = 0; rendererIndex < skinnedRenderers.Length; rendererIndex++) + { + var renderer = skinnedRenderers[rendererIndex]; + var mesh = renderer.mesh; + + // Simplify the mesh if necessary + if (level.Quality < 1f) + { + mesh = SimplifyMesh(mesh, level.Quality, simplificationOptions); + SaveLODMeshAsset(mesh, gameObject.name, renderer.name, levelIndex, renderer.mesh.name, saveAssetsPath); + + if (renderer.isNewMesh) + { + DestroyObject(renderer.mesh); + renderer.mesh = null; + } + } + + string rendererName = string.Format("{0:000}_skinned_{1}", rendererIndex, renderer.name); + var levelRenderer = CreateSkinnedLevelRenderer(rendererName, levelTransform, renderer.transform, mesh, renderer.materials, renderer.rootBone, renderer.bones, ref level); + levelRenderers.Add(levelRenderer); + } + } + } + + foreach (var renderer in originalLevelRenderers) + { + if (!renderersToDisable.Contains(renderer)) + { + renderersToDisable.Add(renderer); + } + } + + lods[levelIndex] = new LOD(level.ScreenRelativeTransitionHeight, levelRenderers.ToArray()); + } + + CreateBackup(gameObject, renderersToDisable.ToArray()); + foreach (var renderer in renderersToDisable) + { + renderer.enabled = false; + } + + lodGroup.animateCrossFading = false; + lodGroup.SetLODs(lods); + return lodGroup; + } + + /// + /// Destroys the generated LODs and LOD Group for the specified game object. + /// + /// The LOD generator helper. + /// If the LODs were successfully destroyed. + public static bool DestroyLODs(LODGeneratorHelper generatorHelper) + { + if (generatorHelper == null) + throw new System.ArgumentNullException(nameof(generatorHelper)); + + return DestroyLODs(generatorHelper.gameObject); + } + + /// + /// Destroys the generated LODs and LOD Group for the specified game object. + /// + /// The game object to destroy LODs for. + /// If the LODs were successfully destroyed. + public static bool DestroyLODs(GameObject gameObject) + { + if (gameObject == null) + throw new System.ArgumentNullException(nameof(gameObject)); + + RestoreBackup(gameObject); + + var transform = gameObject.transform; + var lodParent = transform.Find(LODParentGameObjectName); + if (lodParent == null) + return false; + + // Destroy LOD assets + DestroyLODAssets(lodParent); + + // Destroy the LOD parent + DestroyObject(lodParent.gameObject); + + // Destroy the LOD Group if there is one + var lodGroup = gameObject.GetComponent(); + if (lodGroup != null) + { + DestroyObject(lodGroup); + } + + return true; + } + #endregion + + #region Private Methods + private static StaticRenderer[] GetStaticRenderers(MeshRenderer[] renderers) + { + var newRenderers = new List(renderers.Length); + for (int rendererIndex = 0; rendererIndex < renderers.Length; rendererIndex++) + { + var renderer = renderers[rendererIndex]; + var meshFilter = renderer.GetComponent(); + if (meshFilter == null) + { + Debug.LogWarning("A renderer was missing a mesh filter and was ignored.", renderer); + continue; + } + + var mesh = meshFilter.sharedMesh; + if (mesh == null) + { + Debug.LogWarning("A renderer was missing a mesh and was ignored.", renderer); + continue; + } + + newRenderers.Add(new StaticRenderer() + { + name = renderer.name, + isNewMesh = false, + transform = renderer.transform, + mesh = mesh, + materials = renderer.sharedMaterials + }); + } + return newRenderers.ToArray(); + } + + private static SkinnedRenderer[] GetSkinnedRenderers(SkinnedMeshRenderer[] renderers) + { + var newRenderers = new List(renderers.Length); + for (int rendererIndex = 0; rendererIndex < renderers.Length; rendererIndex++) + { + var renderer = renderers[rendererIndex]; + + var mesh = renderer.sharedMesh; + if (mesh == null) + { + Debug.LogWarning("A renderer was missing a mesh and was ignored.", renderer); + continue; + } + + newRenderers.Add(new SkinnedRenderer() + { + name = renderer.name, + isNewMesh = false, + transform = renderer.transform, + mesh = mesh, + materials = renderer.sharedMaterials, + rootBone = renderer.rootBone, + bones = renderer.bones + }); + } + return newRenderers.ToArray(); + } + + private static StaticRenderer[] CombineStaticMeshes(Transform transform, int levelIndex, MeshRenderer[] renderers) + { + if (renderers.Length == 0) + return null; + + // TODO: Support to merge sub-meshes and atlas textures + + var newRenderers = new List(renderers.Length); + + Material[] combinedMaterials; + var combinedMesh = MeshCombiner.CombineMeshes(transform, renderers, out combinedMaterials); + combinedMesh.name = string.Format("{0}_static{1:00}", transform.name, levelIndex); + string rendererName = string.Format("{0}_combined_static", transform.name); + newRenderers.Add(new StaticRenderer() + { + name = rendererName, + isNewMesh = true, + transform = null, + mesh = combinedMesh, + materials = combinedMaterials + }); + + return newRenderers.ToArray(); + } + + private static SkinnedRenderer[] CombineSkinnedMeshes(Transform transform, int levelIndex, SkinnedMeshRenderer[] renderers) + { + if (renderers.Length == 0) + return null; + + // TODO: Support to merge sub-meshes and atlas textures + + var newRenderers = new List(renderers.Length); + var blendShapeRenderers = (from renderer in renderers + where renderer.sharedMesh != null && renderer.sharedMesh.blendShapeCount > 0 + select renderer); + var renderersWithoutMesh = (from renderer in renderers + where renderer.sharedMesh == null + select renderer); + var combineRenderers = (from renderer in renderers + where renderer.sharedMesh != null && renderer.sharedMesh.blendShapeCount == 0 + select renderer).ToArray(); + + // Warn about renderers without a mesh + foreach (var renderer in renderersWithoutMesh) + { + Debug.LogWarning("A renderer was missing a mesh and was ignored.", renderer); + } + + // Don't combine meshes with blend shapes + foreach (var renderer in blendShapeRenderers) + { + newRenderers.Add(new SkinnedRenderer() + { + name = renderer.name, + isNewMesh = false, + transform = renderer.transform, + mesh = renderer.sharedMesh, + materials = renderer.sharedMaterials, + rootBone = renderer.rootBone, + bones = renderer.bones + }); + } + + if (combineRenderers.Length > 0) + { + Material[] combinedMaterials; + Transform[] combinedBones; + var combinedMesh = MeshCombiner.CombineMeshes(transform, combineRenderers, out combinedMaterials, out combinedBones); + combinedMesh.name = string.Format("{0}_skinned{1:00}", transform.name, levelIndex); + + var rootBone = FindBestRootBone(transform, combineRenderers); + string rendererName = string.Format("{0}_combined_skinned", transform.name); + newRenderers.Add(new SkinnedRenderer() + { + name = rendererName, + isNewMesh = false, + transform = null, + mesh = combinedMesh, + materials = combinedMaterials, + rootBone = rootBone, + bones = combinedBones + }); + } + + return newRenderers.ToArray(); + } + + private static void ParentAndResetTransform(Transform transform, Transform parentTransform) + { + transform.SetParent(parentTransform); + transform.localPosition = Vector3.zero; + transform.localRotation = Quaternion.identity; + transform.localScale = Vector3.one; + } + + private static void ParentAndOffsetTransform(Transform transform, Transform parentTransform, Transform originalTransform) + { + transform.position = originalTransform.position; + transform.rotation = originalTransform.rotation; + transform.localScale = originalTransform.lossyScale; + transform.SetParent(parentTransform, true); + } + + private static MeshRenderer CreateLevelRenderer(string name, Transform parentTransform, Transform originalTransform, Mesh mesh, Material[] materials, ref LODLevel level) + { + var levelGameObject = new GameObject(name, typeof(MeshFilter), typeof(MeshRenderer)); + var levelTransform = levelGameObject.transform; + if (originalTransform != null) + { + ParentAndOffsetTransform(levelTransform, parentTransform, originalTransform); + } + else + { + ParentAndResetTransform(levelTransform, parentTransform); + } + + var meshFilter = levelGameObject.GetComponent(); + meshFilter.sharedMesh = mesh; + + var meshRenderer = levelGameObject.GetComponent(); + meshRenderer.sharedMaterials = materials; + SetupLevelRenderer(meshRenderer, ref level); + return meshRenderer; + } + + private static SkinnedMeshRenderer CreateSkinnedLevelRenderer(string name, Transform parentTransform, Transform originalTransform, Mesh mesh, Material[] materials, Transform rootBone, Transform[] bones, ref LODLevel level) + { + var levelGameObject = new GameObject(name, typeof(SkinnedMeshRenderer)); + var levelTransform = levelGameObject.transform; + if (originalTransform != null) + { + ParentAndOffsetTransform(levelTransform, parentTransform, originalTransform); + } + else + { + ParentAndResetTransform(levelTransform, parentTransform); + } + + var skinnedMeshRenderer = levelGameObject.GetComponent(); + skinnedMeshRenderer.sharedMesh = mesh; + skinnedMeshRenderer.sharedMaterials = materials; + skinnedMeshRenderer.rootBone = rootBone; + skinnedMeshRenderer.bones = bones; + SetupLevelRenderer(skinnedMeshRenderer, ref level); + return skinnedMeshRenderer; + } + + private static Transform FindBestRootBone(Transform transform, SkinnedMeshRenderer[] skinnedMeshRenderers) + { + if (skinnedMeshRenderers == null || skinnedMeshRenderers.Length == 0) + return null; + + Transform bestBone = null; + float bestDistance = float.MaxValue; + for (int i = 0; i < skinnedMeshRenderers.Length; i++) + { + if (skinnedMeshRenderers[i] == null || skinnedMeshRenderers[i].rootBone == null) + continue; + + var rootBone = skinnedMeshRenderers[i].rootBone; + var distance = (rootBone.position - transform.position).sqrMagnitude; + if (distance < bestDistance) + { + bestBone = rootBone; + bestDistance = distance; + } + } + + return bestBone; + } + + private static void SetupLevelRenderer(Renderer renderer, ref LODLevel level) + { + renderer.shadowCastingMode = level.ShadowCastingMode; + renderer.receiveShadows = level.ReceiveShadows; + renderer.motionVectorGenerationMode = level.MotionVectorGenerationMode; + renderer.lightProbeUsage = level.LightProbeUsage; + renderer.reflectionProbeUsage = level.ReflectionProbeUsage; + + var skinnedMeshRenderer = renderer as SkinnedMeshRenderer; + if (skinnedMeshRenderer != null) + { + skinnedMeshRenderer.quality = level.SkinQuality; + skinnedMeshRenderer.skinnedMotionVectors = level.SkinnedMotionVectors; + } + } + + private static Renderer[] GetChildRenderersForLOD(GameObject gameObject) + { + var resultRenderers = new List(); + CollectChildRenderersForLOD(gameObject.transform, resultRenderers); + return resultRenderers.ToArray(); + } + + private static void CollectChildRenderersForLOD(Transform transform, List resultRenderers) + { + // Collect the rendererers of this transform + var childRenderers = transform.GetComponents(); + resultRenderers.AddRange(childRenderers); + + int childCount = transform.childCount; + for (int i = 0; i < childCount; i++) + { + // Skip children that are not active + var childTransform = transform.GetChild(i); + if (!childTransform.gameObject.activeSelf) + continue; + + // If the transform have the identical name as to our LOD Parent GO name, then we also skip it + if (string.Equals(childTransform.name, LODParentGameObjectName)) + continue; + + // Skip children that has a LOD Group or a LOD Generator Helper component + if (childTransform.GetComponent() != null) + continue; + else if (childTransform.GetComponent() != null) + continue; + + // Continue recursively through the children of this transform + CollectChildRenderersForLOD(childTransform, resultRenderers); + } + } + + private static Mesh SimplifyMesh(Mesh mesh, float quality, SimplificationOptions options) + { + var meshSimplifier = new MeshSimplifier(); + meshSimplifier.PreserveBorderEdges = options.PreserveBorderEdges; + meshSimplifier.PreserveUVSeamEdges = options.PreserveUVSeamEdges; + meshSimplifier.PreserveUVFoldoverEdges = options.PreserveUVFoldoverEdges; + meshSimplifier.EnableSmartLink = options.EnableSmartLink; + meshSimplifier.VertexLinkDistance = options.VertexLinkDistance; + meshSimplifier.MaxIterationCount = options.MaxIterationCount; + meshSimplifier.Agressiveness = options.Agressiveness; + + meshSimplifier.Initialize(mesh); + meshSimplifier.SimplifyMesh(quality); + + var simplifiedMesh = meshSimplifier.ToMesh(); + simplifiedMesh.bindposes = mesh.bindposes; + return simplifiedMesh; + } + + private static void DestroyObject(Object obj) + { + if (obj == null) + throw new System.ArgumentNullException(nameof(obj)); + +#if UNITY_EDITOR + if (Application.isPlaying) + { + Object.Destroy(obj); + } + else + { + Object.DestroyImmediate(obj, false); + } +#else + Object.Destroy(obj); +#endif + } + + private static void CreateBackup(GameObject gameObject, Renderer[] originalRenderers) + { + var backupComponent = gameObject.AddComponent(); + backupComponent.hideFlags = HideFlags.HideInInspector; + backupComponent.OriginalRenderers = originalRenderers; + } + + private static void RestoreBackup(GameObject gameObject) + { + var backupComponents = gameObject.GetComponents(); + foreach (var backupComponent in backupComponents) + { + var originalRenderers = backupComponent.OriginalRenderers; + if (originalRenderers != null) + { + foreach (var renderer in originalRenderers) + { + renderer.enabled = true; + } + } + DestroyObject(backupComponent); + } + } + + private static void DestroyLODAssets(Transform transform) + { +#if UNITY_EDITOR + var renderers = transform.GetComponentsInChildren(true); + foreach (var renderer in renderers) + { + var meshRenderer = renderer as MeshRenderer; + var skinnedMeshRenderer = renderer as SkinnedMeshRenderer; + + if (meshRenderer != null) + { + var meshFilter = meshRenderer.GetComponent(); + if (meshFilter != null) + { + DestroyLODAsset(meshFilter.sharedMesh); + } + } + else if (skinnedMeshRenderer != null) + { + DestroyLODAsset(skinnedMeshRenderer.sharedMesh); + } + + foreach (var material in renderer.sharedMaterials) + { + DestroyLODMaterialAsset(material); + } + } + + // Delete any empty LOD asset directories + DeleteEmptyDirectory(LODAssetParentPath.TrimEnd('/')); +#endif + } + + private static void DestroyLODMaterialAsset(Material material) + { + if (material == null) + return; + +#if UNITY_EDITOR + var shader = material.shader; + if (shader == null) + return; + + // We find all texture properties of materials and delete those assets also + int propertyCount = UnityEditor.ShaderUtil.GetPropertyCount(shader); + for (int propertyIndex = 0; propertyIndex < propertyCount; propertyIndex++) + { + var propertyType = UnityEditor.ShaderUtil.GetPropertyType(shader, propertyIndex); + if (propertyType == UnityEditor.ShaderUtil.ShaderPropertyType.TexEnv) + { + string propertyName = UnityEditor.ShaderUtil.GetPropertyName(shader, propertyIndex); + var texture = material.GetTexture(propertyName); + DestroyLODAsset(texture); + } + } + + DestroyLODAsset(material); +#endif + } + + private static void DestroyLODAsset(Object asset) + { + if (asset == null) + return; + +#if UNITY_EDITOR + // We only delete assets that we have automatically generated + string assetPath = UnityEditor.AssetDatabase.GetAssetPath(asset); + if (assetPath.StartsWith(LODAssetParentPath)) + { + UnityEditor.AssetDatabase.DeleteAsset(assetPath); + } +#endif + } + + private static void SaveLODMeshAsset(Object asset, string gameObjectName, string rendererName, int levelIndex, string meshName, string saveAssetsPath) + { + gameObjectName = MakePathSafe(gameObjectName); + rendererName = MakePathSafe(rendererName); + meshName = MakePathSafe(meshName); + meshName = string.Format("{0:00}_{1}", levelIndex, meshName); + + string path; + if (!string.IsNullOrEmpty(saveAssetsPath)) + { + path = string.Format("{0}{1}/{2}.mesh", LODAssetParentPath, saveAssetsPath, meshName); + } + else + { + path = string.Format("{0}{1}/{2}/{3}.mesh", LODAssetParentPath, gameObjectName, rendererName, meshName); + } + + SaveAsset(asset, path); + } + + private static void SaveAsset(Object asset, string path) + { +#if UNITY_EDITOR + CreateParentDirectory(path); + + // Make sure that there is no asset with the same path already + path = UnityEditor.AssetDatabase.GenerateUniqueAssetPath(path); + + UnityEditor.AssetDatabase.CreateAsset(asset, path); +#endif + } + + private static void CreateParentDirectory(string path) + { +#if UNITY_EDITOR + int lastSlashIndex = path.LastIndexOf('/'); + if (lastSlashIndex != -1) + { + string parentPath = path.Substring(0, lastSlashIndex); + if (!UnityEditor.AssetDatabase.IsValidFolder(parentPath)) + { + lastSlashIndex = parentPath.LastIndexOf('/'); + if (lastSlashIndex != -1) + { + string folderName = parentPath.Substring(lastSlashIndex + 1); + string folderParentPath = parentPath.Substring(0, lastSlashIndex); + CreateParentDirectory(parentPath); + UnityEditor.AssetDatabase.CreateFolder(folderParentPath, folderName); + } + else + { + UnityEditor.AssetDatabase.CreateFolder(string.Empty, parentPath); + } + } + } +#endif + } + + private static string MakePathSafe(string name) + { + var sb = new System.Text.StringBuilder(name.Length); + bool lastWasSeparator = false; + for (int i = 0; i < name.Length; i++) + { + char c = name[i]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) + { + lastWasSeparator = false; + sb.Append(c); + } + else if (c == '_' || c == '-') + { + if (!lastWasSeparator) + { + lastWasSeparator = true; + sb.Append(c); + } + } + else + { + if (!lastWasSeparator) + { + lastWasSeparator = true; + sb.Append('_'); + } + } + } + return sb.ToString(); + } + + private static string ValidateSaveAssetsPath(string saveAssetsPath) + { + if (string.IsNullOrEmpty(saveAssetsPath)) + return null; + + saveAssetsPath = saveAssetsPath.Replace('\\', '/'); + saveAssetsPath = saveAssetsPath.Trim('/'); + + if (System.IO.Path.IsPathRooted(saveAssetsPath)) + throw new System.InvalidOperationException("The save assets path cannot be rooted."); + else if (saveAssetsPath.Length > 100) + throw new System.InvalidOperationException("The save assets path cannot be more than 100 characters long to avoid I/O issues."); + + // Make the path safe + var pathParts = saveAssetsPath.Split('/'); + for (int i = 0; i < pathParts.Length; i++) + { + pathParts[i] = MakePathSafe(pathParts[i]); + } + saveAssetsPath = string.Join("/", pathParts); + + return saveAssetsPath; + } + + private static bool DeleteEmptyDirectory(string path) + { +#if UNITY_EDITOR + bool deletedAllSubFolders = true; + var subFolders = UnityEditor.AssetDatabase.GetSubFolders(path); + for (int i = 0; i < subFolders.Length; i++) + { + if (!DeleteEmptyDirectory(subFolders[i])) + { + deletedAllSubFolders = false; + } + } + + if (!deletedAllSubFolders) + return false; + + string[] assetGuids = UnityEditor.AssetDatabase.FindAssets(string.Empty, new string[] { path }); + if (assetGuids.Length > 0) + return false; + + return UnityEditor.AssetDatabase.DeleteAsset(path); +#else + return false; +#endif + } + #endregion + } +} diff --git a/Runtime/LODGenerator.cs.meta b/Runtime/LODGenerator.cs.meta new file mode 100644 index 0000000..ba61e44 --- /dev/null +++ b/Runtime/LODGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d41729de84583e4d8ffb74a04e292c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/LODLevel.cs b/Runtime/LODLevel.cs new file mode 100644 index 0000000..eed91a5 --- /dev/null +++ b/Runtime/LODLevel.cs @@ -0,0 +1,246 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using UnityEngine; +using UnityEngine.Rendering; + +namespace UnityMeshSimplifier +{ + /// + /// A LOD (level of detail) level. + /// + [Serializable] + public struct LODLevel + { + #region Fields + [SerializeField, Range(0f, 1f), Tooltip("The screen relative height to use for the transition.")] + private float screenRelativeTransitionHeight; + [SerializeField, Range(0f, 1f), Tooltip("The width of the cross-fade transition zone (proportion to the current LOD's whole length).")] + private float fadeTransitionWidth; + [SerializeField, Range(0f, 1f), Tooltip("The desired quality for this level.")] + private float quality; + [SerializeField, Tooltip("If all renderers and meshes under this level should be combined into one, where possible.")] + private bool combineMeshes; + [SerializeField, Tooltip("If all sub-meshes should be combined into one, where possible.")] + private bool combineSubMeshes; + + [SerializeField, Tooltip("The renderers used in this level.")] + private Renderer[] renderers; + + [SerializeField, Tooltip("The skin quality to use for renderers on this level.")] + private SkinQuality skinQuality; + [SerializeField, Tooltip("The shadow casting mode for renderers on this level.")] + private ShadowCastingMode shadowCastingMode; + [SerializeField, Tooltip("If renderers on this level should receive shadows.")] + private bool receiveShadows; + [SerializeField, Tooltip("The motion vector generation mode for renderers on this level.")] + private MotionVectorGenerationMode motionVectorGenerationMode; + [SerializeField, Tooltip("If renderers on this level should use skinned motion vectors.")] + private bool skinnedMotionVectors; + [SerializeField, Tooltip("The light probe usage for renderers on this level.")] + private LightProbeUsage lightProbeUsage; + [SerializeField, Tooltip("The reflection probe usage for renderers on this level.")] + private ReflectionProbeUsage reflectionProbeUsage; + #endregion + + #region Properties + /// + /// Gets or sets the screen relative height to use for the transition [0-1]. + /// + public float ScreenRelativeTransitionHeight + { + get { return screenRelativeTransitionHeight; } + set { screenRelativeTransitionHeight = Mathf.Clamp01(value); } + } + + /// + /// Gets or sets the width of the cross-fade transition zone (proportion to the current LOD's whole length) [0-1]. Only used if it's not animated. + /// + public float FadeTransitionWidth + { + get { return fadeTransitionWidth; } + set { fadeTransitionWidth = Mathf.Clamp01(value); } + } + + /// + /// Gets or sets the quality of this level [0-1]. + /// + public float Quality + { + get { return quality; } + set { quality = Mathf.Clamp01(value); } + } + + /// + /// Gets or sets if all renderers and meshes under this level should be combined into one, where possible. + /// + public bool CombineMeshes + { + get { return combineMeshes; } + set { combineMeshes = value; } + } + + /// + /// Gets or sets if all sub-meshes should be combined into one, where possible. + /// NOTE: This is only used if is true. + /// + public bool CombineSubMeshes + { + get { return combineSubMeshes; } + set { combineSubMeshes = value; } + } + + /// + /// Gets or sets the renderers used in this level. + /// These will have no purpose if automatic collection is used for the LOD generator. + /// + public Renderer[] Renderers + { + get { return renderers; } + set { renderers = value; } + } + + /// + /// Gets or sets the skin quality to use for renderers on this level. + /// + public SkinQuality SkinQuality + { + get { return skinQuality; } + set { skinQuality = value; } + } + + /// + /// Gets or sets the shadow casting mode for renderers on this level. + /// + public ShadowCastingMode ShadowCastingMode + { + get { return shadowCastingMode; } + set { shadowCastingMode = value; } + } + + /// + /// Gets or sets if renderers on this level should receive shadows. + /// + public bool ReceiveShadows + { + get { return receiveShadows; } + set { receiveShadows = value; } + } + + /// + /// Gets or sets the motion vector generation mode for renderers on this level. + /// + public MotionVectorGenerationMode MotionVectorGenerationMode + { + get { return motionVectorGenerationMode; } + set { motionVectorGenerationMode = value; } + } + + /// + /// Gets or sets if renderers on this level should use skinned motion vectors. + /// + public bool SkinnedMotionVectors + { + get { return skinnedMotionVectors; } + set { skinnedMotionVectors = value; } + } + + /// + /// Gets or sets the light probe usage for renderers on this level. + /// + public LightProbeUsage LightProbeUsage + { + get { return lightProbeUsage; } + set { lightProbeUsage = value; } + } + + /// + /// Gets or sets the reflection probe usage for renderers on this level. + /// + public ReflectionProbeUsage ReflectionProbeUsage + { + get { return reflectionProbeUsage; } + set { reflectionProbeUsage = value; } + } + #endregion + + #region Constructors + /// + /// Creates a new LOD level. + /// + /// The screen relative height to use for the transition [0-1]. + /// The quality of this level [0-1]. + public LODLevel(float screenRelativeTransitionHeight, float quality) + : this(screenRelativeTransitionHeight, 0f, quality, false, false, null) + { + + } + + /// + /// Creates a new LOD level. + /// + /// The screen relative height to use for the transition [0-1]. + /// The width of the cross-fade transition zone (proportion to the current LOD's whole length) [0-1]. Only used if it's not animated. + /// The quality of this level [0-1]. + /// If all renderers and meshes under this level should be combined into one, where possible. + /// If all sub-meshes should be combined into one, where possible. + public LODLevel(float screenRelativeTransitionHeight, float fadeTransitionWidth, float quality, bool combineMeshes, bool combineSubMeshes) + : this(screenRelativeTransitionHeight, fadeTransitionWidth, quality, combineMeshes, combineSubMeshes, null) + { + + } + + /// + /// Creates a new LOD level. + /// + /// The screen relative height to use for the transition [0-1]. + /// The width of the cross-fade transition zone (proportion to the current LOD's whole length) [0-1]. Only used if it's not animated. + /// The quality of this level [0-1]. + /// If all renderers and meshes under this level should be combined into one, where possible. + /// If all sub-meshes should be combined into one, where possible. + /// The renderers used in this level. + public LODLevel(float screenRelativeTransitionHeight, float fadeTransitionWidth, float quality, bool combineMeshes, bool combineSubMeshes, Renderer[] renderers) + { + this.screenRelativeTransitionHeight = Mathf.Clamp01(screenRelativeTransitionHeight); + this.fadeTransitionWidth = fadeTransitionWidth; + this.quality = Mathf.Clamp01(quality); + this.combineMeshes = combineMeshes; + this.combineSubMeshes = combineSubMeshes; + + this.renderers = renderers; + + this.skinQuality = SkinQuality.Auto; + this.shadowCastingMode = ShadowCastingMode.On; + this.receiveShadows = true; + this.motionVectorGenerationMode = MotionVectorGenerationMode.Object; + this.skinnedMotionVectors = true; + this.lightProbeUsage = LightProbeUsage.BlendProbes; + this.reflectionProbeUsage = ReflectionProbeUsage.BlendProbes; + } + #endregion + } +} diff --git a/Runtime/LODLevel.cs.meta b/Runtime/LODLevel.cs.meta new file mode 100644 index 0000000..81b23c2 --- /dev/null +++ b/Runtime/LODLevel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4fe6be35045bd0a40b6cb1c638269240 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/MeshCombiner.cs b/Runtime/MeshCombiner.cs new file mode 100644 index 0000000..0829392 --- /dev/null +++ b/Runtime/MeshCombiner.cs @@ -0,0 +1,521 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +#if UNITY_2017_3 || UNITY_2017_4 || UNITY_2018 || UNITY_2019 +#define UNITY_MESH_INDEXFORMAT_SUPPORT +#endif + +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace UnityMeshSimplifier +{ + /// + /// Contains methods for combining meshes. + /// + public static class MeshCombiner + { + #region Public Methods + /// + /// Combines an array of mesh renderers into one single mesh. + /// + /// The root transform to create the combine mesh based from, essentially the origin of the new mesh. + /// The array of mesh renderers to combine. + /// The resulting materials for the combined mesh. + /// The combined mesh. + public static Mesh CombineMeshes(Transform rootTransform, MeshRenderer[] renderers, out Material[] resultMaterials) + { + if (rootTransform == null) + throw new System.ArgumentNullException(nameof(rootTransform)); + else if (renderers == null) + throw new System.ArgumentNullException(nameof(renderers)); + + var meshes = new Mesh[renderers.Length]; + var transforms = new Matrix4x4[renderers.Length]; + var materials = new Material[renderers.Length][]; + + for (int i = 0; i < renderers.Length; i++) + { + var renderer = renderers[i]; + if (renderer == null) + throw new System.ArgumentException(string.Format("The renderer at index {0} is null.", i), nameof(renderers)); + + var rendererTransform = renderer.transform; + var meshFilter = renderer.GetComponent(); + if (meshFilter == null) + throw new System.ArgumentException(string.Format("The renderer at index {0} has no mesh filter.", i), nameof(renderers)); + else if (meshFilter.sharedMesh == null) + throw new System.ArgumentException(string.Format("The mesh filter for renderer at index {0} has no mesh.", i), nameof(renderers)); + else if (!meshFilter.sharedMesh.isReadable) + throw new System.ArgumentException(string.Format("The mesh in the mesh filter for renderer at index {0} is not readable.", i), nameof(renderers)); + + meshes[i] = meshFilter.sharedMesh; + transforms[i] = rootTransform.worldToLocalMatrix * rendererTransform.localToWorldMatrix; + materials[i] = renderer.sharedMaterials; + } + + return CombineMeshes(meshes, transforms, materials, out resultMaterials); + } + + /// + /// Combines an array of skinned mesh renderers into one single skinned mesh. + /// + /// The root transform to create the combine mesh based from, essentially the origin of the new mesh. + /// The array of skinned mesh renderers to combine. + /// The resulting materials for the combined mesh. + /// The resulting bones for the combined mesh. + /// The combined mesh. + public static Mesh CombineMeshes(Transform rootTransform, SkinnedMeshRenderer[] renderers, out Material[] resultMaterials, out Transform[] resultBones) + { + if (rootTransform == null) + throw new System.ArgumentNullException(nameof(rootTransform)); + else if (renderers == null) + throw new System.ArgumentNullException(nameof(renderers)); + + var meshes = new Mesh[renderers.Length]; + var transforms = new Matrix4x4[renderers.Length]; + var materials = new Material[renderers.Length][]; + var bones = new Transform[renderers.Length][]; + + for (int i = 0; i < renderers.Length; i++) + { + var renderer = renderers[i]; + if (renderer == null) + throw new System.ArgumentException(string.Format("The renderer at index {0} is null.", i), nameof(renderers)); + else if (renderer.sharedMesh == null) + throw new System.ArgumentException(string.Format("The renderer at index {0} has no mesh.", i), nameof(renderers)); + else if (!renderer.sharedMesh.isReadable) + throw new System.ArgumentException(string.Format("The mesh in the renderer at index {0} is not readable.", i), nameof(renderers)); + + var rendererTransform = renderer.transform; + meshes[i] = renderer.sharedMesh; + transforms[i] = rootTransform.worldToLocalMatrix * rendererTransform.localToWorldMatrix; + materials[i] = renderer.sharedMaterials; + bones[i] = renderer.bones; + } + + return CombineMeshes(meshes, transforms, materials, bones, out resultMaterials, out resultBones); + } + + /// + /// Combines an array of meshes into a single mesh. + /// + /// The array of meshes to combine. + /// The array of transforms for the meshes. + /// The array of materials for each mesh to combine. + /// The resulting materials for the combined mesh. + /// The combined mesh. + public static Mesh CombineMeshes(Mesh[] meshes, Matrix4x4[] transforms, Material[][] materials, out Material[] resultMaterials) + { + if (meshes == null) + throw new System.ArgumentNullException(nameof(meshes)); + else if (transforms == null) + throw new System.ArgumentNullException(nameof(transforms)); + else if (materials == null) + throw new System.ArgumentNullException(nameof(materials)); + + Transform[] resultBones; + return CombineMeshes(meshes, transforms, materials, null, out resultMaterials, out resultBones); + } + + /// + /// Combines an array of meshes into a single mesh. + /// + /// The array of meshes to combine. + /// The array of transforms for the meshes. + /// The array of materials for each mesh to combine. + /// The array of bones for each mesh to combine. + /// The resulting materials for the combined mesh. + /// The resulting bones for the combined mesh. + /// The combined mesh. + public static Mesh CombineMeshes(Mesh[] meshes, Matrix4x4[] transforms, Material[][] materials, Transform[][] bones, out Material[] resultMaterials, out Transform[] resultBones) + { + if (meshes == null) + throw new System.ArgumentNullException(nameof(meshes)); + else if (transforms == null) + throw new System.ArgumentNullException(nameof(transforms)); + else if (materials == null) + throw new System.ArgumentNullException(nameof(materials)); + else if (transforms.Length != meshes.Length) + throw new System.ArgumentException("The array of transforms doesn't have the same length as the array of meshes.", nameof(transforms)); + else if (materials.Length != meshes.Length) + throw new System.ArgumentException("The array of materials doesn't have the same length as the array of meshes.", nameof(materials)); + else if (bones != null && bones.Length != meshes.Length) + throw new System.ArgumentException("The array of bones doesn't have the same length as the array of meshes.", nameof(bones)); + + int totalVertexCount = 0; + int totalSubMeshCount = 0; + for (int meshIndex = 0; meshIndex < meshes.Length; meshIndex++) + { + var mesh = meshes[meshIndex]; + if (mesh == null) + throw new System.ArgumentException(string.Format("The mesh at index {0} is null.", meshIndex), nameof(meshes)); + else if (!mesh.isReadable) + throw new System.ArgumentException(string.Format("The mesh at index {0} is not readable.", meshIndex), nameof(meshes)); + + totalVertexCount += mesh.vertexCount; + totalSubMeshCount += mesh.subMeshCount; + + // Validate the mesh materials + var meshMaterials = materials[meshIndex]; + if (meshMaterials == null) + throw new System.ArgumentException(string.Format("The materials for mesh at index {0} is null.", meshIndex), nameof(materials)); + else if (meshMaterials.Length != mesh.subMeshCount) + throw new System.ArgumentException(string.Format("The materials for mesh at index {0} doesn't match the submesh count ({1} != {2}).", meshIndex, meshMaterials.Length, mesh.subMeshCount), nameof(materials)); + + for (int materialIndex = 0; materialIndex < meshMaterials.Length; materialIndex++) + { + if (meshMaterials[materialIndex] == null) + throw new System.ArgumentException(string.Format("The material at index {0} for mesh at index {1} is null.", materialIndex, meshIndex), nameof(materials)); + } + + // Validate the mesh bones + if (bones != null) + { + var meshBones = bones[meshIndex]; + if (meshBones == null) + throw new System.ArgumentException(string.Format("The bones for mesh at index {0} is null.", meshIndex), nameof(meshBones)); + + for (int boneIndex = 0; boneIndex < meshBones.Length; boneIndex++) + { + if (meshBones[boneIndex] == null) + throw new System.ArgumentException(string.Format("The bone at index {0} for mesh at index {1} is null.", boneIndex, meshIndex), nameof(meshBones)); + } + } + } + + var combinedVertices = new List(totalVertexCount); + var combinedIndices = new List(totalSubMeshCount); + List combinedNormals = null; + List combinedTangents = null; + List combinedColors = null; + List combinedBoneWeights = null; + var combinedUVs = new List[MeshUtils.UVChannelCount]; + + List usedBindposes = null; + List usedBones = null; + var usedMaterials = new List(totalSubMeshCount); + var materialMap = new Dictionary(totalSubMeshCount); + + int currentVertexCount = 0; + for (int meshIndex = 0; meshIndex < meshes.Length; meshIndex++) + { + var mesh = meshes[meshIndex]; + var meshTransform = transforms[meshIndex]; + var meshMaterials = materials[meshIndex]; + var meshBones = (bones != null ? bones[meshIndex] : null); + + int subMeshCount = mesh.subMeshCount; + int meshVertexCount = mesh.vertexCount; + var meshVertices = mesh.vertices; + var meshNormals = mesh.normals; + var meshTangents = mesh.tangents; + var meshUVs = MeshUtils.GetMeshUVs(mesh); + var meshColors = mesh.colors; + var meshBoneWeights = mesh.boneWeights; + var meshBindposes = mesh.bindposes; + + // Transform vertices with bones to keep only one bindpose + if (meshBones != null && meshBoneWeights != null && meshBoneWeights.Length > 0 && meshBindposes != null && meshBindposes.Length > 0 && meshBones.Length == meshBindposes.Length) + { + if (usedBindposes == null) + { + usedBindposes = new List(meshBindposes); + usedBones = new List(meshBones); + } + + bool bindPoseMismatch = false; + int[] boneIndices = new int[meshBones.Length]; + for (int i = 0; i < meshBones.Length; i++) + { + int usedBoneIndex = usedBones.IndexOf(meshBones[i]); + if (usedBoneIndex == -1) + { + usedBoneIndex = usedBones.Count; + usedBones.Add(meshBones[i]); + usedBindposes.Add(meshBindposes[i]); + } + else + { + if (meshBindposes[i] != usedBindposes[usedBoneIndex]) + { + bindPoseMismatch = true; + } + } + boneIndices[i] = usedBoneIndex; + } + + // If any bindpose is mismatching, we correct it first + if (bindPoseMismatch) + { + var correctedBindposes = new Matrix4x4[meshBindposes.Length]; + for (int i = 0; i < meshBindposes.Length; i++) + { + int usedBoneIndex = boneIndices[i]; + correctedBindposes[i] = usedBindposes[usedBoneIndex]; + } + TransformVertices(meshVertices, meshBoneWeights, meshBindposes, correctedBindposes); + } + + // Then we remap the bones + RemapBones(meshBoneWeights, boneIndices); + } + + // Transforms the vertices, normals and tangents using the mesh transform + TransformVertices(meshVertices, ref meshTransform); + TransformNormals(meshNormals, ref meshTransform); + TransformTangents(meshTangents, ref meshTransform); + + // Copy vertex positions & attributes + CopyVertexPositions(combinedVertices, meshVertices); + CopyVertexAttributes(ref combinedNormals, meshNormals, currentVertexCount, meshVertexCount, totalVertexCount, new Vector3(1f, 0f, 0f)); + CopyVertexAttributes(ref combinedTangents, meshTangents, currentVertexCount, meshVertexCount, totalVertexCount, new Vector4(0f, 0f, 1f, 1f)); + CopyVertexAttributes(ref combinedColors, meshColors, currentVertexCount, meshVertexCount, totalVertexCount, new Color(1f, 1f, 1f, 1f)); + CopyVertexAttributes(ref combinedBoneWeights, meshBoneWeights, currentVertexCount, meshVertexCount, totalVertexCount, new BoneWeight()); + + for (int channel = 0; channel < meshUVs.Length; channel++) + { + CopyVertexAttributes(ref combinedUVs[channel], meshUVs[channel], currentVertexCount, meshVertexCount, totalVertexCount, new Vector4(0f, 0f, 0f, 0f)); + } + + for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) + { + var subMeshMaterial = meshMaterials[subMeshIndex]; +#if UNITY_MESH_INDEXFORMAT_SUPPORT + var subMeshIndices = mesh.GetTriangles(subMeshIndex, true); +#else + var subMeshIndices = mesh.GetTriangles(subMeshIndex); +#endif + + if (currentVertexCount > 0) + { + for (int index = 0; index < subMeshIndices.Length; index++) + { + subMeshIndices[index] += currentVertexCount; + } + } + + int existingSubMeshIndex; + if (materialMap.TryGetValue(subMeshMaterial, out existingSubMeshIndex)) + { + combinedIndices[existingSubMeshIndex] = MergeArrays(combinedIndices[existingSubMeshIndex], subMeshIndices); + } + else + { + int materialIndex = combinedIndices.Count; + materialMap.Add(subMeshMaterial, materialIndex); + usedMaterials.Add(subMeshMaterial); + combinedIndices.Add(subMeshIndices); + } + } + + currentVertexCount += meshVertexCount; + } + + var resultVertices = combinedVertices.ToArray(); + var resultIndices = combinedIndices.ToArray(); + var resultNormals = (combinedNormals != null ? combinedNormals.ToArray() : null); + var resultTangents = (combinedTangents != null ? combinedTangents.ToArray() : null); + var resultColors = (combinedColors != null ? combinedColors.ToArray() : null); + var resultBoneWeights = (combinedBoneWeights != null ? combinedBoneWeights.ToArray() : null); + var resultUVs = combinedUVs.ToArray(); + var resultBindposes = (usedBindposes != null ? usedBindposes.ToArray() : null); + resultMaterials = usedMaterials.ToArray(); + resultBones = (usedBones != null ? usedBones.ToArray() : null); + return MeshUtils.CreateMesh(resultVertices, resultIndices, resultNormals, resultTangents, resultColors, resultBoneWeights, resultUVs, resultBindposes, null); + } + #endregion + + #region Private Methods + private static void CopyVertexPositions(List list, Vector3[] arr) + { + if (arr == null || arr.Length == 0) + return; + + for (int i = 0; i < arr.Length; i++) + { + list.Add(arr[i]); + } + } + + private static void CopyVertexAttributes(ref List dest, IEnumerable src, int previousVertexCount, int meshVertexCount, int totalVertexCount, T defaultValue) + { + if (src == null || src.Count() == 0) + { + if (dest != null) + { + for (int i = 0; i < meshVertexCount; i++) + { + dest.Add(defaultValue); + } + } + return; + } + + if (dest == null) + { + dest = new List(totalVertexCount); + for (int i = 0; i < previousVertexCount; i++) + { + dest.Add(defaultValue); + } + } + + dest.AddRange(src); + } + + private static T[] MergeArrays(T[] arr1, T[] arr2) + { + var newArr = new T[arr1.Length + arr2.Length]; + System.Array.Copy(arr1, 0, newArr, 0, arr1.Length); + System.Array.Copy(arr2, 0, newArr, arr1.Length, arr2.Length); + return newArr; + } + + private static void TransformVertices(Vector3[] vertices, ref Matrix4x4 transform) + { + for (int i = 0; i < vertices.Length; i++) + { + vertices[i] = transform.MultiplyPoint3x4(vertices[i]); + } + } + + private static void TransformNormals(Vector3[] normals, ref Matrix4x4 transform) + { + if (normals == null) + return; + + for (int i = 0; i < normals.Length; i++) + { + normals[i] = transform.MultiplyVector(normals[i]); + } + } + + private static void TransformTangents(Vector4[] tangents, ref Matrix4x4 transform) + { + if (tangents == null) + return; + + Vector3 tengentDir; + for (int i = 0; i < tangents.Length; i++) + { + tengentDir = transform.MultiplyVector(new Vector3(tangents[i].x, tangents[i].y, tangents[i].z)); + tangents[i] = new Vector4(tengentDir.x, tengentDir.y, tengentDir.z, tangents[i].w); + } + } + + private static void TransformVertices(Vector3[] vertices, BoneWeight[] boneWeights, Matrix4x4[] oldBindposes, Matrix4x4[] newBindposes) + { + // TODO: Is this method doing what it is supposed to?? It has not been properly tested + + // First invert the old bindposes + for (int i = 0; i < oldBindposes.Length; i++) + { + oldBindposes[i] = oldBindposes[i].inverse; + } + + // The transform the vertices + for (int i = 0; i < vertices.Length; i++) + { + if (boneWeights[i].weight0 > 0f) + { + int boneIndex = boneWeights[i].boneIndex0; + float weight = boneWeights[i].weight0; + vertices[i] = ScaleMatrix(ref newBindposes[boneIndex], weight) * (ScaleMatrix(ref oldBindposes[boneIndex], weight) * vertices[i]); + } + if (boneWeights[i].weight1 > 0f) + { + int boneIndex = boneWeights[i].boneIndex1; + float weight = boneWeights[i].weight1; + vertices[i] = ScaleMatrix(ref newBindposes[boneIndex], weight) * (ScaleMatrix(ref oldBindposes[boneIndex], weight) * vertices[i]); + } + if (boneWeights[i].weight2 > 0f) + { + int boneIndex = boneWeights[i].boneIndex2; + float weight = boneWeights[i].weight2; + vertices[i] = ScaleMatrix(ref newBindposes[boneIndex], weight) * (ScaleMatrix(ref oldBindposes[boneIndex], weight) * vertices[i]); + } + if (boneWeights[i].weight3 > 0f) + { + int boneIndex = boneWeights[i].boneIndex3; + float weight = boneWeights[i].weight3; + vertices[i] = ScaleMatrix(ref newBindposes[boneIndex], weight) * (ScaleMatrix(ref oldBindposes[boneIndex], weight) * vertices[i]); + } + } + } + + private static void RemapBones(BoneWeight[] boneWeights, int[] boneIndices) + { + for (int i = 0; i < boneWeights.Length; i++) + { + if (boneWeights[i].weight0 > 0) + { + boneWeights[i].boneIndex0 = boneIndices[boneWeights[i].boneIndex0]; + } + if (boneWeights[i].weight1 > 0) + { + boneWeights[i].boneIndex1 = boneIndices[boneWeights[i].boneIndex1]; + } + if (boneWeights[i].weight2 > 0) + { + boneWeights[i].boneIndex2 = boneIndices[boneWeights[i].boneIndex2]; + } + if (boneWeights[i].weight3 > 0) + { + boneWeights[i].boneIndex3 = boneIndices[boneWeights[i].boneIndex3]; + } + } + } + + private static Matrix4x4 ScaleMatrix(ref Matrix4x4 matrix, float scale) + { + return new Matrix4x4() + { + m00 = matrix.m00 * scale, + m01 = matrix.m01 * scale, + m02 = matrix.m02 * scale, + m03 = matrix.m03 * scale, + + m10 = matrix.m10 * scale, + m11 = matrix.m11 * scale, + m12 = matrix.m12 * scale, + m13 = matrix.m13 * scale, + + m20 = matrix.m20 * scale, + m21 = matrix.m21 * scale, + m22 = matrix.m22 * scale, + m23 = matrix.m23 * scale, + + m30 = matrix.m30 * scale, + m31 = matrix.m31 * scale, + m32 = matrix.m32 * scale, + m33 = matrix.m33 * scale + }; + } + #endregion + } +} \ No newline at end of file diff --git a/Runtime/MeshCombiner.cs.meta b/Runtime/MeshCombiner.cs.meta new file mode 100644 index 0000000..e68ddef --- /dev/null +++ b/Runtime/MeshCombiner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f0289beff54ae394cb463a92daebe60f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/MeshSimplifier.cs b/Runtime/MeshSimplifier.cs similarity index 74% rename from Scripts/MeshSimplifier.cs rename to Runtime/MeshSimplifier.cs index faa8c8d..70a1cba 100644 --- a/Scripts/MeshSimplifier.cs +++ b/Runtime/MeshSimplifier.cs @@ -37,8 +37,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification #endregion +#if UNITY_2018_2 || UNITY_2018_3 || UNITY_2018_4 || UNITY_2019 +#define UNITY_8UV_SUPPORT +#endif + +#if UNITY_2017_3 || UNITY_2017_4 || UNITY_2018 || UNITY_2019 +#define UNITY_MESH_INDEXFORMAT_SUPPORT +#endif + using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using UnityEngine; namespace UnityMeshSimplifier @@ -51,7 +60,7 @@ public sealed class MeshSimplifier { #region Consts private const double DoubleEpsilon = 1.0E-3; - private const int UVChannelCount = 4; + private const int UVChannelCount = MeshUtils.UVChannelCount; #endregion #region Classes @@ -81,10 +90,12 @@ private struct Triangle #region Properties public int this[int index] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return (index == 0 ? v0 : (index == 1 ? v1 : v2)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] set { switch (index) @@ -106,6 +117,7 @@ public int this[int index] #endregion #region Constructor + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Triangle(int v0, int v1, int v2, int subMeshIndex) { this.v0 = v0; @@ -124,6 +136,7 @@ public Triangle(int v0, int v1, int v2, int subMeshIndex) #endregion #region Public Methods + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GetAttributeIndices(int[] attributeIndices) { attributeIndices[0] = va0; @@ -131,6 +144,7 @@ public void GetAttributeIndices(int[] attributeIndices) attributeIndices[2] = va2; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetAttributeIndex(int index, int value) { switch (index) @@ -149,6 +163,7 @@ public void SetAttributeIndex(int index, int value) } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GetErrors(double[] err) { err[0] = err0; @@ -166,19 +181,20 @@ private struct Vertex public int tstart; public int tcount; public SymmetricMatrix q; - public bool border; - public bool seam; - public bool foldover; + public bool borderEdge; + public bool uvSeamEdge; + public bool uvFoldoverEdge; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vertex(Vector3d p) { this.p = p; this.tstart = 0; this.tcount = 0; this.q = new SymmetricMatrix(); - this.border = true; - this.seam = false; - this.foldover = false; + this.borderEdge = true; + this.uvSeamEdge = false; + this.uvFoldoverEdge = false; } } #endregion @@ -189,6 +205,7 @@ private struct Ref public int tid; public int tvertex; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Set(int tid, int tvertex) { this.tid = tid; @@ -205,6 +222,7 @@ private class UVChannels public TVec[][] Data { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { for (int i = 0; i < UVChannelCount; i++) @@ -228,7 +246,9 @@ public TVec[][] Data /// The channel index. public ResizableArray this[int index] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return channels[index]; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] set { channels[index] = value; } } @@ -256,12 +276,114 @@ public void Resize(int capacity, bool trimExess = false) } #endregion + #region Blend Shape + private class BlendShapeContainer + { + private string shapeName; + private BlendShapeFrameContainer[] frames; + + public BlendShapeContainer(BlendShape blendShape) + { + shapeName = blendShape.ShapeName; + frames = new BlendShapeFrameContainer[blendShape.Frames.Length]; + for (int i = 0; i < frames.Length; i++) + { + frames[i] = new BlendShapeFrameContainer(blendShape.Frames[i]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MoveVertexElement(int dst, int src) + { + for (int i = 0; i < frames.Length; i++) + { + frames[i].MoveVertexElement(dst, src); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void InterpolateVertexAttributes(int dst, int i0, int i1, int i2, ref Vector3 barycentricCoord) + { + for (int i = 0; i < frames.Length; i++) + { + frames[i].InterpolateVertexAttributes(dst, i0, i1, i2, ref barycentricCoord); + } + } + + public void Resize(int length, bool trimExess = false) + { + for (int i = 0; i < frames.Length; i++) + { + frames[i].Resize(length, trimExess); + } + } + + public BlendShape ToBlendShape() + { + var shapeFrames = new BlendShapeFrame[frames.Length]; + for (int i = 0; i < shapeFrames.Length; i++) + { + shapeFrames[i] = frames[i].ToBlendShapeFrame(); + } + return new BlendShape(shapeName, shapeFrames); + } + } + + private class BlendShapeFrameContainer + { + private float frameWeight; + private ResizableArray deltaVertices; + private ResizableArray deltaNormals; + private ResizableArray deltaTangents; + + public BlendShapeFrameContainer(BlendShapeFrame frame) + { + frameWeight = frame.FrameWeight; + deltaVertices = new ResizableArray(frame.DeltaVertices); + deltaNormals = new ResizableArray(frame.DeltaNormals); + deltaTangents = new ResizableArray(frame.DeltaTangents); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MoveVertexElement(int dst, int src) + { + deltaVertices[dst] = deltaVertices[src]; + deltaNormals[dst] = deltaNormals[src]; + deltaTangents[dst] = deltaTangents[src]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void InterpolateVertexAttributes(int dst, int i0, int i1, int i2, ref Vector3 barycentricCoord) + { + deltaVertices[dst] = (deltaVertices[i0] * barycentricCoord.x) + (deltaVertices[i1] * barycentricCoord.y) + (deltaVertices[i2] * barycentricCoord.z); + deltaNormals[dst] = Vector3.Normalize((deltaNormals[i0] * barycentricCoord.x) + (deltaNormals[i1] * barycentricCoord.y) + (deltaNormals[i2] * barycentricCoord.z)); + deltaTangents[dst] = Vector3.Normalize((deltaTangents[i0] * barycentricCoord.x) + (deltaTangents[i1] * barycentricCoord.y) + (deltaTangents[i2] * barycentricCoord.z)); + } + + public void Resize(int length, bool trimExess = false) + { + deltaVertices.Resize(length, trimExess); + deltaNormals.Resize(length, trimExess); + deltaTangents.Resize(length, trimExess); + } + + public BlendShapeFrame ToBlendShapeFrame() + { + var resultVertices = deltaVertices.ToArray(); + var resultNormals = deltaNormals.ToArray(); + var resultTangents = deltaTangents.ToArray(); + return new BlendShapeFrame(frameWeight, resultVertices, resultNormals, resultTangents); + } + } + #endregion + #region Border Vertex private struct BorderVertex { public int index; public int hash; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public BorderVertex(int index, int hash) { this.index = index; @@ -275,6 +397,7 @@ private class BorderVertexComparer : IComparer { public static readonly BorderVertexComparer instance = new BorderVertexComparer(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Compare(BorderVertex x, BorderVertex y) { return x.hash.CompareTo(y.hash); @@ -284,9 +407,9 @@ public int Compare(BorderVertex x, BorderVertex y) #endregion #region Fields - private bool preserveBorders = false; - private bool preserveSeams = false; - private bool preserveFoldovers = false; + private bool preserveBorderEdges = false; + private bool preserveUVSeamEdges = false; + private bool preserveUVFoldoverEdges = false; private bool enableSmartLink = true; private int maxIterationCount = 100; private double agressiveness = 7.0; @@ -307,6 +430,7 @@ public int Compare(BorderVertex x, BorderVertex y) private UVChannels vertUV4D = null; private ResizableArray vertColors = null; private ResizableArray vertBoneWeights = null; + private ResizableArray blendShapes = null; private Matrix4x4[] bindposes = null; @@ -317,44 +441,66 @@ public int Compare(BorderVertex x, BorderVertex y) #region Properties /// - /// Gets or sets if borders should be preserved. + /// Gets or sets if the border edges should be preserved. /// Default value: false /// - [Obsolete("Use the 'MeshSimplifier.PreserveBorders' property instead.", false)] - public bool KeepBorders + [Obsolete("Use the 'MeshSimplifier.PreserveBorderEdges' property instead.", false)] + public bool PreserveBorders { - get { return preserveBorders; } - set { preserveBorders = value; } + get { return this.PreserveBorderEdges; } + set { this.PreserveBorderEdges = value; } } /// - /// Gets or sets if borders should be preserved. + /// Gets or sets if the border edges should be preserved. /// Default value: false /// - public bool PreserveBorders + public bool PreserveBorderEdges { - get { return preserveBorders; } - set { preserveBorders = value; } + get { return preserveBorderEdges; } + set { preserveBorderEdges = value; } } /// - /// Gets or sets if seams should be preserved. + /// Gets or sets if the UV seam edges should be preserved. /// Default value: false /// + [Obsolete("Use the 'MeshSimplifier.PreserveUVSeamEdges' property instead.", false)] public bool PreserveSeams { - get { return preserveSeams; } - set { preserveSeams = value; } + get { return this.PreserveUVSeamEdges; } + set { this.PreserveUVSeamEdges = value; } } /// - /// Gets or sets if foldovers should be preserved. + /// Gets or sets if the UV seam edges should be preserved. /// Default value: false /// + public bool PreserveUVSeamEdges + { + get { return preserveUVSeamEdges; } + set { preserveUVSeamEdges = value; } + } + + /// + /// Gets or sets if the UV foldover edges should be preserved. + /// Default value: false + /// + [Obsolete("Use the 'MeshSimplifier.PreserveUVFoldoverEdges' property instead.", false)] public bool PreserveFoldovers { - get { return preserveFoldovers; } - set { preserveFoldovers = value; } + get { return this.PreserveUVFoldoverEdges; } + set { this.PreserveUVFoldoverEdges = value; } + } + + /// + /// Gets or sets if the UV foldover edges should be preserved. + /// Default value: false + /// + public bool PreserveUVFoldoverEdges + { + get { return preserveUVFoldoverEdges; } + set { preserveUVFoldoverEdges = value; } } /// @@ -400,6 +546,16 @@ public bool Verbose set { verbose = value; } } + /// + /// Gets or sets the maximum distance between two vertices in order to link them. + /// Note that this value is only used if EnableSmartLink is true. + /// + public double VertexLinkDistance + { + get { return Math.Sqrt(vertexLinkDistanceSqr); } + set { vertexLinkDistanceSqr = (value > double.Epsilon ? value * value : double.Epsilon); } + } + /// /// Gets or sets the maximum squared distance between two vertices in order to link them. /// Note that this value is only used if EnableSmartLink is true. @@ -430,7 +586,7 @@ public Vector3[] Vertices set { if (value == null) - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); bindposes = null; vertices.Resize(value.Length); @@ -450,6 +606,14 @@ public int SubMeshCount get { return subMeshCount; } } + /// + /// Gets the count of blend shapes. + /// + public int BlendShapeCount + { + get { return (blendShapes != null ? blendShapes.Length : 0); } + } + /// /// Gets or sets the vertex normals. /// @@ -475,7 +639,7 @@ public Vector4[] Tangents } /// - /// Gets or sets the vertex UV set 1. + /// Gets or sets the vertex 2D UV set 1. /// public Vector2[] UV1 { @@ -484,7 +648,7 @@ public Vector2[] UV1 } /// - /// Gets or sets the vertex UV set 2. + /// Gets or sets the vertex 2D UV set 2. /// public Vector2[] UV2 { @@ -493,7 +657,7 @@ public Vector2[] UV2 } /// - /// Gets or sets the vertex UV set 3. + /// Gets or sets the vertex 2D UV set 3. /// public Vector2[] UV3 { @@ -502,7 +666,7 @@ public Vector2[] UV3 } /// - /// Gets or sets the vertex UV set 4. + /// Gets or sets the vertex 2D UV set 4. /// public Vector2[] UV4 { @@ -510,6 +674,44 @@ public Vector2[] UV4 set { SetUVs(3, value); } } +#if UNITY_8UV_SUPPORT + /// + /// Gets or sets the vertex 2D UV set 5. + /// + public Vector2[] UV5 + { + get { return GetUVs2D(4); } + set { SetUVs(4, value); } + } + + /// + /// Gets or sets the vertex 2D UV set 6. + /// + public Vector2[] UV6 + { + get { return GetUVs2D(5); } + set { SetUVs(5, value); } + } + + /// + /// Gets or sets the vertex 2D UV set 7. + /// + public Vector2[] UV7 + { + get { return GetUVs2D(6); } + set { SetUVs(6, value); } + } + + /// + /// Gets or sets the vertex 2D UV set 8. + /// + public Vector2[] UV8 + { + get { return GetUVs2D(7); } + set { SetUVs(7, value); } + } +#endif + /// /// Gets or sets the vertex colors. /// @@ -551,15 +753,12 @@ public MeshSimplifier() /// /// The original mesh to simplify. public MeshSimplifier(Mesh mesh) + : this() { - if (mesh == null) - throw new ArgumentNullException("mesh"); - - triangles = new ResizableArray(0); - vertices = new ResizableArray(0); - refs = new ResizableArray(0); - - Initialize(mesh); + if (mesh != null) + { + Initialize(mesh); + } } #endregion @@ -593,20 +792,21 @@ private void InitializeVertexAttribute(T[] attributeValues, ref ResizableArra #endregion #region Calculate Error + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double VertexError(ref SymmetricMatrix q, double x, double y, double z) { return q.m0 * x * x + 2 * q.m1 * x * y + 2 * q.m2 * x * z + 2 * q.m3 * x + q.m4 * y * y + 2 * q.m5 * y * z + 2 * q.m6 * y + q.m7 * z * z + 2 * q.m8 * z + q.m9; } - private double CalculateError(ref Vertex vert0, ref Vertex vert1, out Vector3d result, out int resultIndex) + private double CalculateError(ref Vertex vert0, ref Vertex vert1, out Vector3d result) { // compute interpolated vertex SymmetricMatrix q = (vert0.q + vert1.q); - bool border = (vert0.border & vert1.border); + bool borderEdge = (vert0.borderEdge & vert1.borderEdge); double error = 0.0; double det = q.Determinant1(); - if (det != 0.0 && !border) + if (det != 0.0 && !borderEdge) { // q_delta is invertible result = new Vector3d( @@ -614,7 +814,6 @@ private double CalculateError(ref Vertex vert0, ref Vertex vert1, out Vector3d r 1.0 / det * q.Determinant3(), // vy = A42/det(q_delta) -1.0 / det * q.Determinant4()); // vz = A43/det(q_delta) error = VertexError(ref q, result.x, result.y, result.z); - resultIndex = 2; } else { @@ -629,28 +828,51 @@ private double CalculateError(ref Vertex vert0, ref Vertex vert1, out Vector3d r if (error == error3) { result = p3; - resultIndex = 2; } else if (error == error2) { result = p2; - resultIndex = 1; } else if (error == error1) { result = p1; - resultIndex = 0; } else { result = p3; - resultIndex = 2; } } return error; } #endregion + #region Calculate Barycentric Coordinates + private static void CalculateBarycentricCoords(ref Vector3d point, ref Vector3d a, ref Vector3d b, ref Vector3d c, out Vector3 result) + { + Vector3 v0 = (Vector3)(b - a), v1 = (Vector3)(c - a), v2 = (Vector3)(point - a); + float d00 = Vector3.Dot(v0, v0); + float d01 = Vector3.Dot(v0, v1); + float d11 = Vector3.Dot(v1, v1); + float d20 = Vector3.Dot(v2, v0); + float d21 = Vector3.Dot(v2, v1); + float denom = d00 * d11 - d01 * d01; + float v = (d11 * d20 - d01 * d21) / denom; + float w = (d00 * d21 - d01 * d20) / denom; + float u = 1f - v - w; + result = new Vector3(u, v, w); + } + #endregion + + #region Normalize Tangent + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector4 NormalizeTangent(Vector4 tangent) + { + var tangentVec = new Vector3(tangent.x, tangent.y, tangent.z); + tangentVec.Normalize(); + return new Vector4(tangentVec.x, tangentVec.y, tangentVec.z, tangent.w); + } + #endregion + #region Flipped /// /// Check if a triangle flips when this edge is removed @@ -704,7 +926,6 @@ private bool Flipped(ref Vector3d p, int i0, int i1, ref Vertex v0, bool[] delet private void UpdateTriangles(int i0, int ia0, ref Vertex v, ResizableArray deleted, ref int deletedTriangles) { Vector3d p; - int pIndex; int tcount = v.tcount; var triangles = this.triangles.Data; var vertices = this.vertices.Data; @@ -730,9 +951,9 @@ private void UpdateTriangles(int i0, int ia0, ref Vertex v, ResizableArray } t.dirty = true; - t.err0 = CalculateError(ref vertices[t.v0], ref vertices[t.v1], out p, out pIndex); - t.err1 = CalculateError(ref vertices[t.v1], ref vertices[t.v2], out p, out pIndex); - t.err2 = CalculateError(ref vertices[t.v2], ref vertices[t.v0], out p, out pIndex); + t.err0 = CalculateError(ref vertices[t.v0], ref vertices[t.v1], out p); + t.err1 = CalculateError(ref vertices[t.v1], ref vertices[t.v2], out p); + t.err2 = CalculateError(ref vertices[t.v2], ref vertices[t.v0], out p); t.err3 = MathHelper.Min(t.err0, t.err1, t.err2); triangles[tid] = t; refs.Add(r); @@ -740,16 +961,16 @@ private void UpdateTriangles(int i0, int ia0, ref Vertex v, ResizableArray } #endregion - #region Move/Merge Vertex Attributes - private void MoveVertexAttributes(int i0, int i1) + #region Interpolate Vertex Attributes + private void InterpolateVertexAttributes(int dst, int i0, int i1, int i2, ref Vector3 barycentricCoord) { if (vertNormals != null) { - vertNormals[i0] = vertNormals[i1]; + vertNormals[dst] = Vector3.Normalize((vertNormals[i0] * barycentricCoord.x) + (vertNormals[i1] * barycentricCoord.y) + (vertNormals[i2] * barycentricCoord.z)); } if (vertTangents != null) { - vertTangents[i0] = vertTangents[i1]; + vertTangents[dst] = NormalizeTangent((vertTangents[i0] * barycentricCoord.x) + (vertTangents[i1] * barycentricCoord.y) + (vertTangents[i2] * barycentricCoord.z)); } if (vertUV2D != null) { @@ -758,7 +979,7 @@ private void MoveVertexAttributes(int i0, int i1) var vertUV = vertUV2D[i]; if (vertUV != null) { - vertUV[i0] = vertUV[i1]; + vertUV[dst] = (vertUV[i0] * barycentricCoord.x) + (vertUV[i1] * barycentricCoord.y) + (vertUV[i2] * barycentricCoord.z); } } } @@ -769,7 +990,7 @@ private void MoveVertexAttributes(int i0, int i1) var vertUV = vertUV3D[i]; if (vertUV != null) { - vertUV[i0] = vertUV[i1]; + vertUV[dst] = (vertUV[i0] * barycentricCoord.x) + (vertUV[i1] * barycentricCoord.y) + (vertUV[i2] * barycentricCoord.z); } } } @@ -780,69 +1001,23 @@ private void MoveVertexAttributes(int i0, int i1) var vertUV = vertUV4D[i]; if (vertUV != null) { - vertUV[i0] = vertUV[i1]; + vertUV[dst] = (vertUV[i0] * barycentricCoord.x) + (vertUV[i1] * barycentricCoord.y) + (vertUV[i2] * barycentricCoord.z); } } } if (vertColors != null) { - vertColors[i0] = vertColors[i1]; - } - if (vertBoneWeights != null) - { - vertBoneWeights[i0] = vertBoneWeights[i1]; - } - } - - private void MergeVertexAttributes(int i0, int i1) - { - if (vertNormals != null) - { - vertNormals[i0] = (vertNormals[i0] + vertNormals[i1]) * 0.5f; - } - if (vertTangents != null) - { - vertTangents[i0] = (vertTangents[i0] + vertTangents[i1]) * 0.5f; - } - if (vertUV2D != null) - { - for (int i = 0; i < UVChannelCount; i++) - { - var vertUV = vertUV2D[i]; - if (vertUV != null) - { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; - } - } - } - if (vertUV3D != null) - { - for (int i = 0; i < UVChannelCount; i++) - { - var vertUV = vertUV3D[i]; - if (vertUV != null) - { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; - } - } + vertColors[dst] = (vertColors[i0] * barycentricCoord.x) + (vertColors[i1] * barycentricCoord.y) + (vertColors[i2] * barycentricCoord.z); } - if (vertUV4D != null) + if (blendShapes != null) { - for (int i = 0; i < UVChannelCount; i++) + for (int i = 0; i < blendShapes.Length; i++) { - var vertUV = vertUV4D[i]; - if (vertUV != null) - { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; - } + blendShapes[i].InterpolateVertexAttributes(dst, i0, i1, i2, ref barycentricCoord); } } - if (vertColors != null) - { - vertColors[i0] = (vertColors[i0] + vertColors[i1]) * 0.5f; - } - // TODO: Do we have to blend bone weights at all or can we just keep them as it is in this scenario? + // TODO: How do we interpolate the bone weights? Do we have to? } #endregion @@ -897,7 +1072,7 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th var vertices = this.vertices.Data; Vector3d p; - int pIndex; + Vector3 barycentricCoord; for (int tid = 0; tid < triangleCount; tid++) { if (triangles[tid].dirty || triangles[tid].deleted || triangles[tid].err3 > threshold) @@ -915,26 +1090,26 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th int i1 = triangles[tid][nextEdgeIndex]; // Border check - if (vertices[i0].border != vertices[i1].border) + if (vertices[i0].borderEdge != vertices[i1].borderEdge) continue; // Seam check - else if (vertices[i0].seam != vertices[i1].seam) + else if (vertices[i0].uvSeamEdge != vertices[i1].uvSeamEdge) continue; // Foldover check - else if (vertices[i0].foldover != vertices[i1].foldover) + else if (vertices[i0].uvFoldoverEdge != vertices[i1].uvFoldoverEdge) continue; // If borders should be preserved - else if (preserveBorders && vertices[i0].border) + else if (preserveBorderEdges && vertices[i0].borderEdge) continue; // If seams should be preserved - else if (preserveSeams && vertices[i0].seam) + else if (preserveUVSeamEdges && vertices[i0].uvSeamEdge) continue; // If foldovers should be preserved - else if (preserveFoldovers && vertices[i0].foldover) + else if (preserveUVFoldoverEdges && vertices[i0].uvFoldoverEdge) continue; // Compute vertex to collapse to - CalculateError(ref vertices[i0], ref vertices[i1], out p, out pIndex); + CalculateError(ref vertices[i0], ref vertices[i1], out p); deleted0.Resize(vertices[i0].tcount); // normals temporarily deleted1.Resize(vertices[i1].tcount); // normals temporarily @@ -944,26 +1119,22 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th if (Flipped(ref p, i1, i0, ref vertices[i1], deleted1.Data)) continue; - int ia0 = attributeIndexArr[edgeIndex]; + // Calculate the barycentric coordinates within the triangle + int nextNextEdgeIndex = ((edgeIndex + 2) % 3); + int i2 = triangles[tid][nextNextEdgeIndex]; + CalculateBarycentricCoords(ref p, ref vertices[i0].p, ref vertices[i1].p, ref vertices[i2].p, out barycentricCoord); // Not flipped, so remove edge vertices[i0].p = p; vertices[i0].q += vertices[i1].q; - if (pIndex == 1) - { - // Move vertex attributes from ia1 to ia0 - int ia1 = attributeIndexArr[nextEdgeIndex]; - MoveVertexAttributes(ia0, ia1); - } - else if (pIndex == 2) - { - // Merge vertex attributes ia0 and ia1 into ia0 - int ia1 = attributeIndexArr[nextEdgeIndex]; - MergeVertexAttributes(ia0, ia1); - } + // Interpolate the vertex attributes + int ia0 = attributeIndexArr[edgeIndex]; + int ia1 = attributeIndexArr[nextEdgeIndex]; + int ia2 = attributeIndexArr[nextNextEdgeIndex]; + InterpolateVertexAttributes(ia0, ia0, ia1, ia2, ref barycentricCoord); - if (vertices[i0].seam) + if (vertices[i0].uvSeamEdge) { ia0 = -1; } @@ -1042,9 +1213,9 @@ private void UpdateMesh(int iteration) int vsize = 0; for (int i = 0; i < vertexCount; i++) { - vertices[i].border = false; - vertices[i].seam = false; - vertices[i].foldover = false; + vertices[i].borderEdge = false; + vertices[i].uvSeamEdge = false; + vertices[i].uvFoldoverEdge = false; } int ofs; @@ -1093,7 +1264,7 @@ private void UpdateMesh(int iteration) if (vcount[j] == 1) { id = vids[j]; - vertices[id].border = true; + vertices[id].borderEdge = true; ++borderVertexCount; if (enableSmartLink) @@ -1119,9 +1290,9 @@ private void UpdateMesh(int iteration) double borderAreaWidth = borderMaxX - borderMinX; for (int i = 0; i < vertexCount; i++) { - if (vertices[i].border) + if (vertices[i].borderEdge) { - int vertexHash = (int)((((vertices[i].p.x - borderMinX) / borderAreaWidth) - 0.5) * int.MaxValue); + int vertexHash = (int)(((((vertices[i].p.x - borderMinX) / borderAreaWidth) * 2.0) - 1.0) * int.MaxValue); borderVertices[borderIndexCount] = new BorderVertex(i, vertexHash); ++borderIndexCount; } @@ -1130,6 +1301,10 @@ private void UpdateMesh(int iteration) // Sort the border vertices by hash Array.Sort(borderVertices, 0, borderIndexCount, BorderVertexComparer.instance); + // Calculate the maximum hash distance based on the maximum vertex link distance + double vertexLinkDistance = Math.Sqrt(vertexLinkDistanceSqr); + int hashMaxDistance = Math.Max((int)((vertexLinkDistance / borderAreaWidth) * int.MaxValue), 1); + // Then find identical border vertices and bind them together as one for (int i = 0; i < borderIndexCount; i++) { @@ -1143,7 +1318,7 @@ private void UpdateMesh(int iteration) int otherIndex = borderVertices[j].index; if (otherIndex == -1) continue; - else if ((borderVertices[j].hash - borderVertices[i].hash) > 1) // There is no point to continue beyond this point + else if ((borderVertices[j].hash - borderVertices[i].hash) > hashMaxDistance) // There is no point to continue beyond this point break; var otherPoint = vertices[otherIndex].p; @@ -1155,18 +1330,18 @@ private void UpdateMesh(int iteration) if (sqrMagnitude <= vertexLinkDistanceSqr) { borderVertices[j].index = -1; // NOTE: This makes sure that the "other" vertex is not processed again - vertices[myIndex].border = false; - vertices[otherIndex].border = false; + vertices[myIndex].borderEdge = false; + vertices[otherIndex].borderEdge = false; if (AreUVsTheSame(0, myIndex, otherIndex)) { - vertices[myIndex].foldover = true; - vertices[otherIndex].foldover = true; + vertices[myIndex].uvFoldoverEdge = true; + vertices[otherIndex].uvFoldoverEdge = true; } else { - vertices[myIndex].seam = true; - vertices[otherIndex].seam = true; + vertices[myIndex].uvSeamEdge = true; + vertices[otherIndex].uvSeamEdge = true; } int otherTriangleCount = vertices[otherIndex].tcount; @@ -1196,7 +1371,6 @@ private void UpdateMesh(int iteration) int v0, v1, v2; Vector3d n, p0, p1, p2, p10, p20, dummy; - int dummy2; SymmetricMatrix sm; for (int i = 0; i < triangleCount; i++) { @@ -1223,9 +1397,9 @@ private void UpdateMesh(int iteration) { // Calc Edge Error var triangle = triangles[i]; - triangles[i].err0 = CalculateError(ref vertices[triangle.v0], ref vertices[triangle.v1], out dummy, out dummy2); - triangles[i].err1 = CalculateError(ref vertices[triangle.v1], ref vertices[triangle.v2], out dummy, out dummy2); - triangles[i].err2 = CalculateError(ref vertices[triangle.v2], ref vertices[triangle.v0], out dummy, out dummy2); + triangles[i].err0 = CalculateError(ref vertices[triangle.v0], ref vertices[triangle.v1], out dummy); + triangles[i].err1 = CalculateError(ref vertices[triangle.v1], ref vertices[triangle.v2], out dummy); + triangles[i].err2 = CalculateError(ref vertices[triangle.v2], ref vertices[triangle.v0], out dummy); triangles[i].err3 = MathHelper.Min(triangles[i].err0, triangles[i].err1, triangles[i].err2); } } @@ -1309,6 +1483,7 @@ private void CompactMesh() var vertUV4D = (this.vertUV4D != null ? this.vertUV4D.Data : null); var vertColors = (this.vertColors != null ? this.vertColors.Data : null); var vertBoneWeights = (this.vertBoneWeights != null ? this.vertBoneWeights.Data : null); + var blendShapes = (this.blendShapes != null ? this.blendShapes.Data : null); int lastSubMeshIndex = -1; subMeshOffsets = new int[subMeshCount]; @@ -1430,6 +1605,14 @@ private void CompactMesh() } if (vertColors != null) vertColors[dst] = vertColors[i]; if (vertBoneWeights != null) vertBoneWeights[dst] = vertBoneWeights[i]; + + if (blendShapes != null) + { + for (int shapeIndex = 0; shapeIndex < this.blendShapes.Length; shapeIndex++) + { + blendShapes[shapeIndex].MoveVertexElement(dst, i); + } + } } ++dst; } @@ -1453,6 +1636,14 @@ private void CompactMesh() if (vertUV4D != null) this.vertUV4D.Resize(vertexCount, true); if (vertColors != null) this.vertColors.Resize(vertexCount, true); if (vertBoneWeights != null) this.vertBoneWeights.Resize(vertexCount, true); + + if (blendShapes != null) + { + for (int i = 0; i < this.blendShapes.Length; i++) + { + blendShapes[i].Resize(vertexCount, false); + } + } } #endregion @@ -1488,6 +1679,20 @@ private void CalculateSubMeshOffsets() #region Public Methods #region Sub-Meshes + /// + /// Returns the triangle indices for all sub-meshes. + /// + /// The triangle indices for all sub-meshes. + public int[][] GetAllSubMeshTriangles() + { + var indices = new int[subMeshCount][]; + for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) + { + indices[subMeshIndex] = GetSubMeshTriangles(subMeshIndex); + } + return indices; + } + /// /// Returns the triangle indices for a specific sub-mesh. /// @@ -1496,7 +1701,7 @@ private void CalculateSubMeshOffsets() public int[] GetSubMeshTriangles(int subMeshIndex) { if (subMeshIndex < 0) - throw new ArgumentOutOfRangeException("subMeshIndex", "The sub-mesh index is negative."); + throw new ArgumentOutOfRangeException(nameof(subMeshIndex), "The sub-mesh index is negative."); // First get the sub-mesh offsets if (subMeshOffsets == null) @@ -1505,7 +1710,7 @@ public int[] GetSubMeshTriangles(int subMeshIndex) } if (subMeshIndex >= subMeshOffsets.Length) - throw new ArgumentOutOfRangeException("subMeshIndex", "The sub-mesh index is greater than or equals to the sub mesh count."); + throw new ArgumentOutOfRangeException(nameof(subMeshIndex), "The sub-mesh index is greater than or equals to the sub mesh count."); else if (subMeshOffsets.Length != subMeshCount) throw new InvalidOperationException("The sub-mesh triangle offsets array is not the same size as the count of sub-meshes. This should not be possible to happen."); @@ -1555,9 +1760,9 @@ public void ClearSubMeshes() public void AddSubMeshTriangles(int[] triangles) { if (triangles == null) - throw new ArgumentNullException("triangles"); + throw new ArgumentNullException(nameof(triangles)); else if ((triangles.Length % 3) != 0) - throw new ArgumentException("The index array length must be a multiple of 3 in order to represent triangles.", "triangles"); + throw new ArgumentException("The index array length must be a multiple of 3 in order to represent triangles.", nameof(triangles)); int subMeshIndex = subMeshCount++; int triangleIndex = this.triangles.Length; @@ -1583,7 +1788,7 @@ public void AddSubMeshTriangles(int[] triangles) public void AddSubMeshTriangles(int[][] triangles) { if (triangles == null) - throw new ArgumentNullException("triangles"); + throw new ArgumentNullException(nameof(triangles)); int totalTriangleCount = 0; for (int i = 0; i < triangles.Length; i++) @@ -1591,7 +1796,7 @@ public void AddSubMeshTriangles(int[][] triangles) if (triangles[i] == null) throw new ArgumentException(string.Format("The index array at index {0} is null.", i)); else if ((triangles[i].Length % 3) != 0) - throw new ArgumentException(string.Format("The index array length at index {0} must be a multiple of 3 in order to represent triangles.", i), "triangles"); + throw new ArgumentException(string.Format("The index array length at index {0} must be a multiple of 3 in order to represent triangles.", i), nameof(triangles)); totalTriangleCount += triangles[i].Length / 3; } @@ -1629,7 +1834,7 @@ public void AddSubMeshTriangles(int[][] triangles) public Vector2[] GetUVs2D(int channel) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (vertUV2D != null && vertUV2D[channel] != null) { @@ -1649,7 +1854,7 @@ public Vector2[] GetUVs2D(int channel) public Vector3[] GetUVs3D(int channel) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (vertUV3D != null && vertUV3D[channel] != null) { @@ -1669,7 +1874,7 @@ public Vector3[] GetUVs3D(int channel) public Vector4[] GetUVs4D(int channel) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (vertUV4D != null && vertUV4D[channel] != null) { @@ -1689,9 +1894,9 @@ public Vector4[] GetUVs4D(int channel) public void GetUVs(int channel, List uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); else if (uvs == null) - throw new ArgumentNullException("uvs"); + throw new ArgumentNullException(nameof(uvs)); uvs.Clear(); if (vertUV2D != null && vertUV2D[channel] != null) @@ -1712,9 +1917,9 @@ public void GetUVs(int channel, List uvs) public void GetUVs(int channel, List uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); else if (uvs == null) - throw new ArgumentNullException("uvs"); + throw new ArgumentNullException(nameof(uvs)); uvs.Clear(); if (vertUV3D != null && vertUV3D[channel] != null) @@ -1735,9 +1940,9 @@ public void GetUVs(int channel, List uvs) public void GetUVs(int channel, List uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); else if (uvs == null) - throw new ArgumentNullException("uvs"); + throw new ArgumentNullException(nameof(uvs)); uvs.Clear(); if (vertUV4D != null && vertUV4D[channel] != null) @@ -1760,7 +1965,7 @@ public void GetUVs(int channel, List uvs) public void SetUVs(int channel, Vector2[] uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (uvs != null && uvs.Length > 0) { @@ -1808,7 +2013,7 @@ public void SetUVs(int channel, Vector2[] uvs) public void SetUVs(int channel, Vector3[] uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (uvs != null && uvs.Length > 0) { @@ -1856,7 +2061,7 @@ public void SetUVs(int channel, Vector3[] uvs) public void SetUVs(int channel, Vector4[] uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (uvs != null && uvs.Length > 0) { @@ -1904,7 +2109,7 @@ public void SetUVs(int channel, Vector4[] uvs) public void SetUVs(int channel, List uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (uvs != null && uvs.Count > 0) { @@ -1952,7 +2157,7 @@ public void SetUVs(int channel, List uvs) public void SetUVs(int channel, List uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (uvs != null && uvs.Count > 0) { @@ -2000,7 +2205,7 @@ public void SetUVs(int channel, List uvs) public void SetUVs(int channel, List uvs) { if (channel < 0 || channel >= UVChannelCount) - throw new ArgumentOutOfRangeException("channel"); + throw new ArgumentOutOfRangeException(nameof(channel)); if (uvs != null && uvs.Count > 0) { @@ -2039,7 +2244,140 @@ public void SetUVs(int channel, List uvs) vertUV3D[channel] = null; } } + + /// + /// Sets the UVs for a specific channel and automatically detects the used components. + /// + /// The channel index. + /// The UVs. + public void SetUVsAuto(int channel, List uvs) + { + if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException(nameof(channel)); + + if (uvs != null && uvs.Count > 0) + { + int usedComponents = MeshUtils.GetUsedUVComponents(uvs); + if (usedComponents <= 2) + { + var uv2D = MeshUtils.ConvertUVsTo2D(uvs); + SetUVs(channel, uv2D); + } + else if (usedComponents == 3) + { + var uv3D = MeshUtils.ConvertUVsTo3D(uvs); + SetUVs(channel, uv3D); + } + else + { + SetUVs(channel, uvs); + } + } + else + { + if (vertUV2D != null) + { + vertUV2D[channel] = null; + } + if (vertUV3D != null) + { + vertUV3D[channel] = null; + } + if (vertUV4D != null) + { + vertUV4D[channel] = null; + } + } + } + #endregion #endregion + + #region Blend Shapes + /// + /// Returns all blend shapes. + /// + /// An array of all blend shapes. + public BlendShape[] GetAllBlendShapes() + { + if (blendShapes == null) + return null; + + var results = new BlendShape[blendShapes.Length]; + for (int i = 0; i < results.Length; i++) + { + results[i] = blendShapes[i].ToBlendShape(); + } + return results; + } + + /// + /// Returns a specific blend shape. + /// + /// The blend shape index. + /// The blend shape. + public BlendShape GetBlendShape(int blendShapeIndex) + { + if (blendShapes == null || blendShapeIndex < 0 || blendShapeIndex >= blendShapes.Length) + throw new ArgumentOutOfRangeException(nameof(blendShapeIndex)); + + return blendShapes[blendShapeIndex].ToBlendShape(); + } + + /// + /// Clears all blend shapes. + /// + public void ClearBlendShapes() + { + if (blendShapes != null) + { + blendShapes.Clear(); + blendShapes = null; + } + } + + /// + /// Adds a blend shape. + /// + /// The blend shape to add. + public void AddBlendShape(BlendShape blendShape) + { + var frames = blendShape.Frames; + if (frames == null || frames.Length == 0) + throw new ArgumentException("The frames cannot be null or empty.", nameof(blendShape)); + + if (this.blendShapes == null) + { + this.blendShapes = new ResizableArray(4, 0); + } + + var container = new BlendShapeContainer(blendShape); + this.blendShapes.Add(container); + } + + /// + /// Adds several blend shapes. + /// + /// The blend shapes to add. + public void AddBlendShapes(BlendShape[] blendShapes) + { + if (blendShapes == null) + throw new ArgumentNullException(nameof(blendShapes)); + + if (this.blendShapes == null) + { + this.blendShapes = new ResizableArray(Math.Max(4, blendShapes.Length), 0); + } + + for (int i = 0; i < blendShapes.Length; i++) + { + var frames = blendShapes[i].Frames; + if (frames == null || frames.Length == 0) + throw new ArgumentException(string.Format("The frames of blend shape at index {0} cannot be null or empty.", i), nameof(blendShapes)); + + var container = new BlendShapeContainer(blendShapes[i]); + this.blendShapes.Add(container); + } + } #endregion #region Initialize @@ -2050,23 +2388,32 @@ public void SetUVs(int channel, List uvs) public void Initialize(Mesh mesh) { if (mesh == null) - throw new ArgumentNullException("mesh"); + throw new ArgumentNullException(nameof(mesh)); this.Vertices = mesh.vertices; this.Normals = mesh.normals; this.Tangents = mesh.tangents; - this.UV1 = mesh.uv; - this.UV2 = mesh.uv2; - this.UV3 = mesh.uv3; - this.UV4 = mesh.uv4; + this.Colors = mesh.colors; this.BoneWeights = mesh.boneWeights; this.bindposes = mesh.bindposes; + for (int channel = 0; channel < UVChannelCount; channel++) + { + var uvs = MeshUtils.GetMeshUVs(mesh, channel); + SetUVsAuto(channel, uvs); + } + + var blendShapes = MeshUtils.GetMeshBlendShapes(mesh); + if (blendShapes != null && blendShapes.Length > 0) + { + AddBlendShapes(blendShapes); + } + ClearSubMeshes(); int subMeshCount = mesh.subMeshCount; - int[][] subMeshTriangles = new int[subMeshCount][]; + var subMeshTriangles = new int[subMeshCount][]; for (int i = 0; i < subMeshCount; i++) { subMeshTriangles[i] = mesh.GetTriangles(i); @@ -2204,84 +2551,55 @@ public Mesh ToMesh() var tangents = this.Tangents; var colors = this.Colors; var boneWeights = this.BoneWeights; + var indices = GetAllSubMeshTriangles(); + var blendShapes = GetAllBlendShapes(); - var newMesh = new Mesh(); - -#if UNITY_2017_3 || UNITY_2017_4 || UNITY_2018 - // TODO: Use baseVertex if all submeshes are within the ushort.MaxValue range even though the total vertex count is above - bool use32BitIndex = (vertices.Length > ushort.MaxValue); - newMesh.indexFormat = (use32BitIndex ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16); -#endif - - if (bindposes != null && bindposes.Length > 0) - { - newMesh.bindposes = bindposes; - } - - newMesh.subMeshCount = subMeshCount; - newMesh.vertices = this.Vertices; - if (normals != null) newMesh.normals = normals; - if (tangents != null) newMesh.tangents = tangents; - + List[] uvs2D = null; + List[] uvs3D = null; + List[] uvs4D = null; if (vertUV2D != null) { - List uvSet = null; - for (int i = 0; i < UVChannelCount; i++) + uvs2D = new List[UVChannelCount]; + for (int channel = 0; channel < UVChannelCount; channel++) { - if (vertUV2D[i] != null) + if (vertUV2D[channel] != null) { - if (uvSet == null) - uvSet = new List(vertUV2D[i].Length); - - GetUVs(i, uvSet); - newMesh.SetUVs(i, uvSet); + var uvs = new List(vertices.Length); + GetUVs(channel, uvs); + uvs2D[channel] = uvs; } } } if (vertUV3D != null) { - List uvSet = null; - for (int i = 0; i < UVChannelCount; i++) + uvs3D = new List[UVChannelCount]; + for (int channel = 0; channel < UVChannelCount; channel++) { - if (vertUV3D[i] != null) + if (vertUV3D[channel] != null) { - if (uvSet == null) - uvSet = new List(vertUV3D[i].Length); - - GetUVs(i, uvSet); - newMesh.SetUVs(i, uvSet); + var uvs = new List(vertices.Length); + GetUVs(channel, uvs); + uvs3D[channel] = uvs; } } } if (vertUV4D != null) { - List uvSet = null; - for (int i = 0; i < UVChannelCount; i++) + uvs4D = new List[UVChannelCount]; + for (int channel = 0; channel < UVChannelCount; channel++) { - if (vertUV4D[i] != null) + if (vertUV4D[channel] != null) { - if (uvSet == null) - uvSet = new List(vertUV4D[i].Length); - - GetUVs(i, uvSet); - newMesh.SetUVs(i, uvSet); + var uvs = new List(vertices.Length); + GetUVs(channel, uvs); + uvs4D[channel] = uvs; } } } - if (colors != null) newMesh.colors = colors; - if (boneWeights != null) newMesh.boneWeights = boneWeights; - - for (int i = 0; i < subMeshCount; i++) - { - var subMeshTriangles = GetSubMeshTriangles(i); - newMesh.SetTriangles(subMeshTriangles, i, false); - } - - newMesh.RecalculateBounds(); - return newMesh; + return MeshUtils.CreateMesh(vertices, indices, normals, tangents, colors, boneWeights, uvs2D, uvs3D, uvs4D, bindposes, blendShapes); } #endregion #endregion diff --git a/Scripts/MeshSimplifier.cs.meta b/Runtime/MeshSimplifier.cs.meta similarity index 71% rename from Scripts/MeshSimplifier.cs.meta rename to Runtime/MeshSimplifier.cs.meta index 2f0f2a5..8434728 100644 --- a/Scripts/MeshSimplifier.cs.meta +++ b/Runtime/MeshSimplifier.cs.meta @@ -1,7 +1,5 @@ fileFormatVersion: 2 -guid: 7f6414dd43e57ee4caaa2a11a7ac88b1 -timeCreated: 1516802301 -licenseType: Free +guid: fd1726abc7fa4e74bb63730bbee424c0 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/SimplificationOptions.cs b/Runtime/SimplificationOptions.cs new file mode 100644 index 0000000..ca372cb --- /dev/null +++ b/Runtime/SimplificationOptions.cs @@ -0,0 +1,99 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +using System; +using UnityEngine; + +namespace UnityMeshSimplifier +{ + /// + /// Options for mesh simplification. + /// + [Serializable] + public struct SimplificationOptions + { + /// + /// The default simplification options. + /// + public static readonly SimplificationOptions Default = new SimplificationOptions() + { + PreserveBorderEdges = false, + PreserveUVSeamEdges = false, + PreserveUVFoldoverEdges = false, + EnableSmartLink = true, + VertexLinkDistance = double.Epsilon, + MaxIterationCount = 100, + Agressiveness = 7.0 + }; + + /// + /// If the border edges should be preserved. + /// Default value: false + /// + [Tooltip("If the border edges should be preserved.")] + public bool PreserveBorderEdges; + /// + /// If the UV seam edges should be preserved. + /// Default value: false + /// + [Tooltip("If the UV seam edges should be preserved.")] + public bool PreserveUVSeamEdges; + /// + /// If the UV foldover edges should be preserved. + /// Default value: false + /// + [Tooltip("If the UV foldover edges should be preserved.")] + public bool PreserveUVFoldoverEdges; + /// + /// If a feature for smarter vertex linking should be enabled, reducing artifacts in the + /// decimated result at the cost of a slightly more expensive initialization by treating vertices at + /// the same position as the same vertex while separating the attributes. + /// Default value: true + /// + [Tooltip("If a feature for smarter vertex linking should be enabled, reducing artifacts at the cost of slower simplification.")] + public bool EnableSmartLink; + /// + /// The maximum distance between two vertices in order to link them. + /// Note that this value is only used if EnableSmartLink is true. + /// Default value: double.Epsilon + /// + [Tooltip("The maximum distance between two vertices in order to link them.")] + public double VertexLinkDistance; + /// + /// The maximum iteration count. Higher number is more expensive but can bring you closer to your target quality. + /// Sometimes a lower maximum count might be desired in order to lower the performance cost. + /// Default value: 100 + /// + [Tooltip("The maximum squared distance between two vertices in order to link them.")] + public int MaxIterationCount; + /// + /// The agressiveness of the mesh simplification. Higher number equals higher quality, but more expensive to run. + /// Default value: 7.0 + /// + [Tooltip("The agressiveness of the mesh simplification. Higher number equals higher quality, but more expensive to run.")] + public double Agressiveness; + } +} diff --git a/Runtime/SimplificationOptions.cs.meta b/Runtime/SimplificationOptions.cs.meta new file mode 100644 index 0000000..9f6ec22 --- /dev/null +++ b/Runtime/SimplificationOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 298beb5a5e067264fae5196f38be1c0f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utility.meta b/Runtime/Utility.meta new file mode 100644 index 0000000..f5acb6d --- /dev/null +++ b/Runtime/Utility.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 37d417c6208b4294cbb0c470f6b56d1e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/MathHelper.cs b/Runtime/Utility/MathHelper.cs similarity index 94% rename from Scripts/MathHelper.cs rename to Runtime/Utility/MathHelper.cs index 50418b4..9d3d783 100644 --- a/Scripts/MathHelper.cs +++ b/Runtime/Utility/MathHelper.cs @@ -25,6 +25,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #endregion using System; +using System.Runtime.CompilerServices; namespace UnityMeshSimplifier { @@ -73,6 +74,7 @@ public static class MathHelper /// The second value. /// The third value. /// The minimum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Min(double val1, double val2, double val3) { return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3)); @@ -87,6 +89,7 @@ public static double Min(double val1, double val2, double val3) /// The minimum value. /// The maximum value. /// The clamped value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Clamp(double value, double min, double max) { return (value >= min ? (value <= max ? value : max) : min); @@ -101,6 +104,7 @@ public static double Clamp(double value, double min, double max) /// The second point. /// The third point. /// The triangle area. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double TriangleArea(ref Vector3d p0, ref Vector3d p1, ref Vector3d p2) { var dx = p1 - p0; diff --git a/Scripts/MathHelper.cs.meta b/Runtime/Utility/MathHelper.cs.meta similarity index 100% rename from Scripts/MathHelper.cs.meta rename to Runtime/Utility/MathHelper.cs.meta diff --git a/Runtime/Utility/MeshUtils.cs b/Runtime/Utility/MeshUtils.cs new file mode 100644 index 0000000..b4a94c1 --- /dev/null +++ b/Runtime/Utility/MeshUtils.cs @@ -0,0 +1,442 @@ +#region License +/* +MIT License + +Copyright(c) 2019 Mattias Edlund + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#endregion + +#if UNITY_2018_2 || UNITY_2018_3 || UNITY_2018_4 || UNITY_2019 +#define UNITY_8UV_SUPPORT +#endif + +#if UNITY_2017_3 || UNITY_2017_4 || UNITY_2018 || UNITY_2019 +#define UNITY_MESH_INDEXFORMAT_SUPPORT +#endif + +using System; +using System.Collections.Generic; +using UnityEngine; + +#if UNITY_MESH_INDEXFORMAT_SUPPORT +using UnityEngine.Rendering; +#endif + +namespace UnityMeshSimplifier +{ + /// + /// Contains utility methods for meshes. + /// + public static class MeshUtils + { + #region Consts + /// + /// The count of supported UV channels. + /// +#if UNITY_8UV_SUPPORT + public const int UVChannelCount = 8; +#else + public const int UVChannelCount = 4; +#endif + #endregion + + #region Public Methods + /// + /// Creates a new mesh. + /// + /// The mesh vertices. + /// The mesh sub-mesh indices. + /// The mesh normals. + /// The mesh tangents. + /// The mesh colors. + /// The mesh bone-weights. + /// The mesh 4D UV sets. + /// The mesh bindposes. + /// The created mesh. + public static Mesh CreateMesh(Vector3[] vertices, int[][] indices, Vector3[] normals, Vector4[] tangents, Color[] colors, BoneWeight[] boneWeights, List[] uvs, Matrix4x4[] bindposes, BlendShape[] blendShapes) + { + return CreateMesh(vertices, indices, normals, tangents, colors, boneWeights, uvs, null, null, bindposes, blendShapes); + } + + /// + /// Creates a new mesh. + /// + /// The mesh vertices. + /// The mesh sub-mesh indices. + /// The mesh normals. + /// The mesh tangents. + /// The mesh colors. + /// The mesh bone-weights. + /// The mesh 4D UV sets. + /// The mesh bindposes. + /// The created mesh. + public static Mesh CreateMesh(Vector3[] vertices, int[][] indices, Vector3[] normals, Vector4[] tangents, Color[] colors, BoneWeight[] boneWeights, List[] uvs, Matrix4x4[] bindposes, BlendShape[] blendShapes) + { + return CreateMesh(vertices, indices, normals, tangents, colors, boneWeights, null, null, uvs, bindposes, blendShapes); + } + + /// + /// Creates a new mesh. + /// + /// The mesh vertices. + /// The mesh sub-mesh indices. + /// The mesh normals. + /// The mesh tangents. + /// The mesh colors. + /// The mesh bone-weights. + /// The mesh 2D UV sets. + /// The mesh 3D UV sets. + /// The mesh 4D UV sets. + /// The mesh bindposes. + /// The created mesh. + public static Mesh CreateMesh(Vector3[] vertices, int[][] indices, Vector3[] normals, Vector4[] tangents, Color[] colors, BoneWeight[] boneWeights, List[] uvs2D, List[] uvs3D, List[] uvs4D, Matrix4x4[] bindposes, BlendShape[] blendShapes) + { + var newMesh = new Mesh(); + int subMeshCount = indices.Length; + +#if UNITY_MESH_INDEXFORMAT_SUPPORT + IndexFormat indexFormat; + var indexMinMax = MeshUtils.GetSubMeshIndexMinMax(indices, out indexFormat); + newMesh.indexFormat = indexFormat; +#endif + + if (bindposes != null && bindposes.Length > 0) + { + newMesh.bindposes = bindposes; + } + + newMesh.subMeshCount = subMeshCount; + newMesh.vertices = vertices; + if (normals != null && normals.Length > 0) + { + newMesh.normals = normals; + } + if (tangents != null && tangents.Length > 0) + { + newMesh.tangents = tangents; + } + if (colors != null && colors.Length > 0) + { + newMesh.colors = colors; + } + if (boneWeights != null && boneWeights.Length > 0) + { + newMesh.boneWeights = boneWeights; + } + + if (uvs2D != null) + { + for (int uvChannel = 0; uvChannel < uvs2D.Length; uvChannel++) + { + if (uvs2D[uvChannel] != null && uvs2D[uvChannel].Count > 0) + { + newMesh.SetUVs(uvChannel, uvs2D[uvChannel]); + } + } + } + + if (uvs3D != null) + { + for (int uvChannel = 0; uvChannel < uvs3D.Length; uvChannel++) + { + if (uvs3D[uvChannel] != null && uvs3D[uvChannel].Count > 0) + { + newMesh.SetUVs(uvChannel, uvs3D[uvChannel]); + } + } + } + + if (uvs4D != null) + { + for (int uvChannel = 0; uvChannel < uvs4D.Length; uvChannel++) + { + if (uvs4D[uvChannel] != null && uvs4D[uvChannel].Count > 0) + { + newMesh.SetUVs(uvChannel, uvs4D[uvChannel]); + } + } + } + + if (blendShapes != null) + { + MeshUtils.ApplyMeshBlendShapes(newMesh, blendShapes); + } + + for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) + { + var subMeshTriangles = indices[subMeshIndex]; +#if UNITY_MESH_INDEXFORMAT_SUPPORT + var minMax = indexMinMax[subMeshIndex]; + if (indexFormat == UnityEngine.Rendering.IndexFormat.UInt16 && minMax.y > ushort.MaxValue) + { + int baseVertex = minMax.x; + for (int index = 0; index < subMeshTriangles.Length; index++) + { + subMeshTriangles[index] -= baseVertex; + } + newMesh.SetTriangles(subMeshTriangles, subMeshIndex, false, baseVertex); + } + else + { + newMesh.SetTriangles(subMeshTriangles, subMeshIndex, false, 0); + } +#else + newMesh.SetTriangles(subMeshTriangles, subMeshIndex, false); +#endif + } + + newMesh.RecalculateBounds(); + return newMesh; + } + + /// + /// Returns the blend shapes of a mesh. + /// + /// The mesh. + /// The mesh blend shapes. + public static BlendShape[] GetMeshBlendShapes(Mesh mesh) + { + if (mesh == null) + throw new ArgumentNullException(nameof(mesh)); + + int vertexCount = mesh.vertexCount; + int blendShapeCount = mesh.blendShapeCount; + if (blendShapeCount == 0) + return null; + + var blendShapes = new BlendShape[blendShapeCount]; + + for (int blendShapeIndex = 0; blendShapeIndex < blendShapeCount; blendShapeIndex++) + { + string shapeName = mesh.GetBlendShapeName(blendShapeIndex); + int frameCount = mesh.GetBlendShapeFrameCount(blendShapeIndex); + var frames = new BlendShapeFrame[frameCount]; + + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) + { + float frameWeight = mesh.GetBlendShapeFrameWeight(blendShapeIndex, frameIndex); + + var deltaVertices = new Vector3[vertexCount]; + var deltaNormals = new Vector3[vertexCount]; + var deltaTangents = new Vector3[vertexCount]; + mesh.GetBlendShapeFrameVertices(blendShapeIndex, frameIndex, deltaVertices, deltaNormals, deltaTangents); + + frames[frameIndex] = new BlendShapeFrame(frameWeight, deltaVertices, deltaNormals, deltaTangents); + } + + blendShapes[blendShapeIndex] = new BlendShape(shapeName, frames); + } + + return blendShapes; + } + + /// + /// Applies and overrides the specified blend shapes on the specified mesh. + /// + /// The mesh. + /// The mesh blend shapes. + public static void ApplyMeshBlendShapes(Mesh mesh, BlendShape[] blendShapes) + { + if (mesh == null) + throw new ArgumentNullException(nameof(mesh)); + + mesh.ClearBlendShapes(); + if (blendShapes == null || blendShapes.Length == 0) + return; + + for (int blendShapeIndex = 0; blendShapeIndex < blendShapes.Length; blendShapeIndex++) + { + string shapeName = blendShapes[blendShapeIndex].ShapeName; + var frames = blendShapes[blendShapeIndex].Frames; + + if (frames != null) + { + for (int frameIndex = 0; frameIndex < frames.Length; frameIndex++) + { + mesh.AddBlendShapeFrame(shapeName, frames[frameIndex].FrameWeight, frames[frameIndex].DeltaVertices, frames[frameIndex].DeltaNormals, frames[frameIndex].DeltaTangents); + } + } + } + } + + /// + /// Returns the UV sets for a specific mesh. + /// + /// The mesh. + /// The UV sets. + public static List[] GetMeshUVs(Mesh mesh) + { + if (mesh == null) + throw new ArgumentNullException(nameof(mesh)); + + var uvs = new List[UVChannelCount]; + for (int channel = 0; channel < UVChannelCount; channel++) + { + uvs[channel] = GetMeshUVs(mesh, channel); + } + return uvs; + } + + /// + /// Returns the UV list for a specific mesh and UV channel. + /// + /// The mesh. + /// The UV channel. + /// The UV list. + public static List GetMeshUVs(Mesh mesh, int channel) + { + if (mesh == null) + throw new ArgumentNullException(nameof(mesh)); + else if (channel < 0 || channel >= UVChannelCount) + throw new ArgumentOutOfRangeException(nameof(channel)); + + var uvList = new List(mesh.vertexCount); + mesh.GetUVs(channel, uvList); + return uvList; + } + + /// + /// Returns the number of used UV components in a UV set. + /// + /// The UV set. + /// The number of used UV components. + public static int GetUsedUVComponents(List uvs) + { + if (uvs == null || uvs.Count == 0) + return 0; + + int usedComponents = 1; + foreach (var uv in uvs) + { + if (usedComponents < 2 && uv.y != 0f) + { + usedComponents = 2; + } + if (usedComponents < 3 && uv.z != 0f) + { + usedComponents = 3; + } + if (usedComponents < 4 && uv.w != 0f) + { + usedComponents = 4; + break; + } + } + + return usedComponents; + } + + /// + /// Converts a list of 4D UVs into 2D. + /// + /// The list of UVs. + /// The array of 2D UVs. + public static Vector2[] ConvertUVsTo2D(List uvs) + { + if (uvs == null) + return null; + + var uv2D = new Vector2[uvs.Count]; + for (int i = 0; i < uv2D.Length; i++) + { + var uv = uvs[i]; + uv2D[i] = new Vector2(uv.x, uv.y); + } + return uv2D; + } + + /// + /// Converts a list of 4D UVs into 3D. + /// + /// The list of UVs. + /// The array of 3D UVs. + public static Vector3[] ConvertUVsTo3D(List uvs) + { + if (uvs == null) + return null; + + var uv3D = new Vector3[uvs.Count]; + for (int i = 0; i < uv3D.Length; i++) + { + var uv = uvs[i]; + uv3D[i] = new Vector3(uv.x, uv.y, uv.z); + } + return uv3D; + } + +#if UNITY_MESH_INDEXFORMAT_SUPPORT + /// + /// Returns the minimum and maximum indices for each submesh along with the needed index format. + /// + /// The indices for the submeshes. + /// The output index format. + /// The minimum and maximum indices for each submesh. + public static Vector2Int[] GetSubMeshIndexMinMax(int[][] indices, out IndexFormat indexFormat) + { + if (indices == null) + throw new ArgumentNullException(nameof(indices)); + + var result = new Vector2Int[indices.Length]; + indexFormat = IndexFormat.UInt16; + for (int subMeshIndex = 0; subMeshIndex < indices.Length; subMeshIndex++) + { + int minIndex, maxIndex; + GetIndexMinMax(indices[subMeshIndex], out minIndex, out maxIndex); + result[subMeshIndex] = new Vector2Int(minIndex, maxIndex); + + int indexRange = (maxIndex - minIndex); + if (indexRange > ushort.MaxValue) + { + indexFormat = IndexFormat.UInt32; + } + } + return result; + } +#endif + #endregion + + #region Private Methods + private static void GetIndexMinMax(int[] indices, out int minIndex, out int maxIndex) + { + if (indices == null || indices.Length == 0) + { + minIndex = maxIndex = 0; + return; + } + + minIndex = int.MaxValue; + maxIndex = int.MinValue; + + for (int i = 0; i < indices.Length; i++) + { + if (indices[i] < minIndex) + { + minIndex = indices[i]; + } + if (indices[i] > maxIndex) + { + maxIndex = indices[i]; + } + } + } + #endregion + } +} diff --git a/Runtime/Utility/MeshUtils.cs.meta b/Runtime/Utility/MeshUtils.cs.meta new file mode 100644 index 0000000..d83fe57 --- /dev/null +++ b/Runtime/Utility/MeshUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d73a73206296364385b0a41cf06eb95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/ResizableArray.cs b/Runtime/Utility/ResizableArray.cs similarity index 76% rename from Scripts/ResizableArray.cs rename to Runtime/Utility/ResizableArray.cs index e7b5e27..004d6b8 100644 --- a/Scripts/ResizableArray.cs +++ b/Runtime/Utility/ResizableArray.cs @@ -25,6 +25,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #endregion using System; +using System.Runtime.CompilerServices; namespace UnityMeshSimplifier { @@ -47,6 +48,7 @@ internal sealed class ResizableArray /// public int Length { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return length; } } @@ -55,6 +57,7 @@ public int Length /// public T[] Data { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return items; } } @@ -65,7 +68,9 @@ public T[] Data /// The element value. public T this[int index] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return items[index]; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] set { items[index] = value; } } #endregion @@ -89,9 +94,9 @@ public ResizableArray(int capacity) public ResizableArray(int capacity, int length) { if (capacity < 0) - throw new ArgumentOutOfRangeException("capacity"); + throw new ArgumentOutOfRangeException(nameof(capacity)); else if (length < 0 || length > capacity) - throw new ArgumentOutOfRangeException("length"); + throw new ArgumentOutOfRangeException(nameof(length)); if (capacity > 0) items = new T[capacity]; @@ -100,6 +105,28 @@ public ResizableArray(int capacity, int length) this.length = length; } + + /// + /// Creates a new resizable array. + /// + /// The initial array. + public ResizableArray(T[] initialArray) + { + if (initialArray == null) + throw new ArgumentNullException(nameof(initialArray)); + + if (initialArray.Length > 0) + { + items = new T[initialArray.Length]; + length = initialArray.Length; + Array.Copy(initialArray, 0, items, 0, initialArray.Length); + } + else + { + items = emptyArr; + length = 0; + } + } #endregion #region Private Methods @@ -129,7 +156,7 @@ public void Clear() public void Resize(int length, bool trimExess = false) { if (length < 0) - throw new ArgumentOutOfRangeException("capacity"); + throw new ArgumentOutOfRangeException(nameof(length)); if (length > items.Length) { @@ -156,7 +183,7 @@ public void TrimExcess() if (items.Length == length) // Nothing to do return; - T[] newItems = new T[length]; + var newItems = new T[length]; Array.Copy(items, 0, newItems, 0, length); items = newItems; } @@ -174,6 +201,17 @@ public void Add(T item) items[length++] = item; } + + /// + /// Returns a copy of the resizable array as an actually array. + /// + /// The array. + public T[] ToArray() + { + var newItems = new T[length]; + Array.Copy(items, 0, newItems, 0, length); + return newItems; + } #endregion } } \ No newline at end of file diff --git a/Scripts/ResizableArray.cs.meta b/Runtime/Utility/ResizableArray.cs.meta similarity index 100% rename from Scripts/ResizableArray.cs.meta rename to Runtime/Utility/ResizableArray.cs.meta diff --git a/Scripts/SymmetricMatrix.cs b/Runtime/Utility/SymmetricMatrix.cs similarity index 93% rename from Scripts/SymmetricMatrix.cs rename to Runtime/Utility/SymmetricMatrix.cs index efc3b29..fbbbff2 100644 --- a/Scripts/SymmetricMatrix.cs +++ b/Runtime/Utility/SymmetricMatrix.cs @@ -25,6 +25,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #endregion using System; +using System.Runtime.CompilerServices; namespace UnityMeshSimplifier { @@ -84,6 +85,7 @@ public struct SymmetricMatrix /// The value. public double this[int index] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { switch (index) @@ -120,6 +122,7 @@ public double this[int index] /// Creates a symmetric matrix with a value in each component. /// /// The component value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SymmetricMatrix(double c) { this.m0 = c; @@ -147,6 +150,7 @@ public SymmetricMatrix(double c) /// The m33 component. /// The m34 component. /// The m44 component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SymmetricMatrix(double m0, double m1, double m2, double m3, double m4, double m5, double m6, double m7, double m8, double m9) { @@ -169,6 +173,7 @@ public SymmetricMatrix(double m0, double m1, double m2, double m3, /// The plane y-component /// The plane z-component /// The plane w-component + [MethodImpl(MethodImplOptions.AggressiveInlining)] public SymmetricMatrix(double a, double b, double c, double d) { this.m0 = a * a; @@ -194,6 +199,7 @@ public SymmetricMatrix(double a, double b, double c, double d) /// The left hand side. /// The right hand side. /// The resulting matrix. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static SymmetricMatrix operator +(SymmetricMatrix a, SymmetricMatrix b) { return new SymmetricMatrix( @@ -210,6 +216,7 @@ public SymmetricMatrix(double a, double b, double c, double d) /// Determinant(0, 1, 2, 1, 4, 5, 2, 5, 7) /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal double Determinant1() { double det = @@ -226,6 +233,7 @@ internal double Determinant1() /// Determinant(1, 2, 3, 4, 5, 6, 5, 7, 8) /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal double Determinant2() { double det = @@ -242,6 +250,7 @@ internal double Determinant2() /// Determinant(0, 2, 3, 1, 5, 6, 2, 7, 8) /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal double Determinant3() { double det = @@ -258,6 +267,7 @@ internal double Determinant3() /// Determinant(0, 1, 3, 1, 4, 6, 2, 5, 8) /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal double Determinant4() { double det = @@ -285,6 +295,7 @@ internal double Determinant4() /// The a32 index. /// The a33 index. /// The determinant value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public double Determinant(int a11, int a12, int a13, int a21, int a22, int a23, int a31, int a32, int a33) diff --git a/Scripts/SymmetricMatrix.cs.meta b/Runtime/Utility/SymmetricMatrix.cs.meta similarity index 100% rename from Scripts/SymmetricMatrix.cs.meta rename to Runtime/Utility/SymmetricMatrix.cs.meta diff --git a/Scripts/Vector3d.cs b/Runtime/Utility/Vector3d.cs similarity index 90% rename from Scripts/Vector3d.cs rename to Runtime/Utility/Vector3d.cs index 83cd473..66177ea 100644 --- a/Scripts/Vector3d.cs +++ b/Runtime/Utility/Vector3d.cs @@ -25,6 +25,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #endregion using System; +using System.Runtime.CompilerServices; using UnityEngine; namespace UnityMeshSimplifier @@ -69,6 +70,7 @@ public struct Vector3d : IEquatable /// public double Magnitude { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return System.Math.Sqrt(x * x + y * y + z * z); } } @@ -77,6 +79,7 @@ public double Magnitude /// public double MagnitudeSqr { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return (x * x + y * y + z * z); } } @@ -85,6 +88,7 @@ public double MagnitudeSqr /// public Vector3d Normalized { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { Vector3d result; @@ -99,6 +103,7 @@ public Vector3d Normalized /// The component index. public double this[int index] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { switch (index) @@ -113,6 +118,7 @@ public double this[int index] throw new IndexOutOfRangeException("Invalid Vector3d index!"); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] set { switch (index) @@ -138,6 +144,7 @@ public double this[int index] /// Creates a new vector with one value for all components. /// /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3d(double value) { this.x = value; @@ -151,6 +158,7 @@ public Vector3d(double value) /// The x value. /// The y value. /// The z value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3d(double x, double y, double z) { this.x = x; @@ -162,6 +170,7 @@ public Vector3d(double x, double y, double z) /// Creates a new vector from a single precision vector. /// /// The single precision vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3d(Vector3 vector) { this.x = vector.x; @@ -177,6 +186,7 @@ public Vector3d(Vector3 vector) /// The first vector. /// The second vector. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3d operator +(Vector3d a, Vector3d b) { return new Vector3d(a.x + b.x, a.y + b.y, a.z + b.z); @@ -188,6 +198,7 @@ public Vector3d(Vector3 vector) /// The first vector. /// The second vector. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3d operator -(Vector3d a, Vector3d b) { return new Vector3d(a.x - b.x, a.y - b.y, a.z - b.z); @@ -199,6 +210,7 @@ public Vector3d(Vector3 vector) /// The vector. /// The scaling value. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3d operator *(Vector3d a, double d) { return new Vector3d(a.x * d, a.y * d, a.z * d); @@ -210,6 +222,7 @@ public Vector3d(Vector3 vector) /// The scaling vlaue. /// The vector. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3d operator *(double d, Vector3d a) { return new Vector3d(a.x * d, a.y * d, a.z * d); @@ -221,6 +234,7 @@ public Vector3d(Vector3 vector) /// The vector. /// The dividing float value. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3d operator /(Vector3d a, double d) { return new Vector3d(a.x / d, a.y / d, a.z / d); @@ -231,6 +245,7 @@ public Vector3d(Vector3 vector) /// /// The vector. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3d operator -(Vector3d a) { return new Vector3d(-a.x, -a.y, -a.z); @@ -242,6 +257,7 @@ public Vector3d(Vector3 vector) /// The left hand side vector. /// The right hand side vector. /// If equals. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(Vector3d lhs, Vector3d rhs) { return (lhs - rhs).MagnitudeSqr < Epsilon; @@ -253,6 +269,7 @@ public Vector3d(Vector3 vector) /// The left hand side vector. /// The right hand side vector. /// If not equals. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(Vector3d lhs, Vector3d rhs) { return (lhs - rhs).MagnitudeSqr >= Epsilon; @@ -262,6 +279,7 @@ public Vector3d(Vector3 vector) /// Implicitly converts from a single-precision vector into a double-precision vector. /// /// The single-precision vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Vector3d(Vector3 v) { return new Vector3d(v.x, v.y, v.z); @@ -271,6 +289,7 @@ public static implicit operator Vector3d(Vector3 v) /// Implicitly converts from a double-precision vector into a single-precision vector. /// /// The double-precision vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator Vector3(Vector3d v) { return new Vector3((float)v.x, (float)v.y, (float)v.z); @@ -285,6 +304,7 @@ public static explicit operator Vector3(Vector3d v) /// The x value. /// The y value. /// The z value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Set(double x, double y, double z) { this.x = x; @@ -296,6 +316,7 @@ public void Set(double x, double y, double z) /// Multiplies with another vector component-wise. /// /// The vector to multiply with. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Scale(ref Vector3d scale) { x *= scale.x; @@ -306,6 +327,7 @@ public void Scale(ref Vector3d scale) /// /// Normalizes this vector. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Normalize() { double mag = this.Magnitude; @@ -326,6 +348,7 @@ public void Normalize() /// /// The minimum component value. /// The maximum component value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clamp(double min, double max) { if (x < min) x = min; @@ -400,6 +423,7 @@ public string ToString(string format) /// /// The left hand side vector. /// The right hand side vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Dot(ref Vector3d lhs, ref Vector3d rhs) { return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; @@ -411,6 +435,7 @@ public static double Dot(ref Vector3d lhs, ref Vector3d rhs) /// The left hand side vector. /// The right hand side vector. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Cross(ref Vector3d lhs, ref Vector3d rhs, out Vector3d result) { result = new Vector3d(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x); @@ -422,6 +447,7 @@ public static void Cross(ref Vector3d lhs, ref Vector3d rhs, out Vector3d result /// The from vector. /// The to vector. /// The angle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double Angle(ref Vector3d from, ref Vector3d to) { Vector3d fromNormalized = from.Normalized; @@ -436,6 +462,7 @@ public static double Angle(ref Vector3d from, ref Vector3d to) /// The vector to interpolate to. /// The time fraction. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Lerp(ref Vector3d a, ref Vector3d b, double t, out Vector3d result) { result = new Vector3d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t); @@ -447,6 +474,7 @@ public static void Lerp(ref Vector3d a, ref Vector3d b, double t, out Vector3d r /// The first vector. /// The second vector. /// The resulting vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Scale(ref Vector3d a, ref Vector3d b, out Vector3d result) { result = new Vector3d(a.x * b.x, a.y * b.y, a.z * b.z); @@ -457,6 +485,7 @@ public static void Scale(ref Vector3d a, ref Vector3d b, out Vector3d result) /// /// The vector to normalize. /// The resulting normalized vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Normalize(ref Vector3d value, out Vector3d result) { double mag = value.Magnitude; diff --git a/Scripts/Vector3d.cs.meta b/Runtime/Utility/Vector3d.cs.meta similarity index 100% rename from Scripts/Vector3d.cs.meta rename to Runtime/Utility/Vector3d.cs.meta diff --git a/Runtime/Whinarn.UnityMeshSimplifier.Runtime.asmdef b/Runtime/Whinarn.UnityMeshSimplifier.Runtime.asmdef new file mode 100644 index 0000000..4e9c237 --- /dev/null +++ b/Runtime/Whinarn.UnityMeshSimplifier.Runtime.asmdef @@ -0,0 +1,13 @@ +{ + "name": "Whinarn.UnityMeshSimplifier.Runtime", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [] +} \ No newline at end of file diff --git a/Runtime/Whinarn.UnityMeshSimplifier.Runtime.asmdef.meta b/Runtime/Whinarn.UnityMeshSimplifier.Runtime.asmdef.meta new file mode 100644 index 0000000..b1cf743 --- /dev/null +++ b/Runtime/Whinarn.UnityMeshSimplifier.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 38ad7ec3e6bf98c4099e640225ecaf77 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/UnityMeshSimplifier.asmdef b/Scripts/UnityMeshSimplifier.asmdef deleted file mode 100644 index 3b2bcfd..0000000 --- a/Scripts/UnityMeshSimplifier.asmdef +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "UnityMeshSimplifier", - "references": [], - "optionalUnityReferences": [], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": false -} \ No newline at end of file diff --git a/Third Party Notices.md b/Third Party Notices.md new file mode 100644 index 0000000..41e39cd --- /dev/null +++ b/Third Party Notices.md @@ -0,0 +1,7 @@ +This package contains code that is largely based off of third-party software components governed by the license(s) indicated below: + +Component Name: Fast-Quadric-Mesh-Simplification + +License Type: "MIT" + +[Fast-Quadric-Mesh-Simplification License](https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification/blob/master/src.cmd/Simplify.h) diff --git a/Third Party Notices.md.meta b/Third Party Notices.md.meta new file mode 100644 index 0000000..97c037b --- /dev/null +++ b/Third Party Notices.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: db7b2ae7c9f7e034780980b943d92a9a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..a4cd57a --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "com.whinarn.unitymeshsimplifier", + "displayName": "Unity Mesh Simplifier", + "version": "2.0.0", + "unity": "2017.1", + "description": "Simplifies 3D meshes with ease. Works both in the editor and during runtime in builds.", + "keywords": [ + "3d", + "mesh", + "polygon", + "triangle", + "simplification", + "decimation", + "reduction", + "optimization" + ], + "author": { + "name": "Mattias Edlund", + "url": "https://github.com/Whinarn" + } +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..c5663e4 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e854774a70729fb4d817e9e5cf36cae8 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 38a1536a22468599b4b6ffc380f00b2ff7d2e718 Mon Sep 17 00:00:00 2001 From: Amir Ebrahimi Date: Fri, 21 Aug 2020 17:13:06 -0700 Subject: [PATCH 5/6] fix(tests): compile errors on 2018.4.26f1 --- Tests/Editor/MeshUtilsTest.cs | 4 ++++ .../Whinarn.UnityMeshSimplifier.Editor.Tests.asmdef | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Tests/Editor/MeshUtilsTest.cs b/Tests/Editor/MeshUtilsTest.cs index c13e2c2..4e44eb2 100644 --- a/Tests/Editor/MeshUtilsTest.cs +++ b/Tests/Editor/MeshUtilsTest.cs @@ -412,7 +412,11 @@ public void ShouldGetMeshUVs() mesh.vertices = new Vector3[4]; for (int i = 0; i < uvs.Length; i++) { +#if UNITY_2018 + mesh.SetUVs(i, uvs[i].ToList()); +#else mesh.SetUVs(i, uvs[i]); +#endif } var allUVs = MeshUtils.GetMeshUVs(mesh); diff --git a/Tests/Editor/Whinarn.UnityMeshSimplifier.Editor.Tests.asmdef b/Tests/Editor/Whinarn.UnityMeshSimplifier.Editor.Tests.asmdef index 1da497b..a38422d 100644 --- a/Tests/Editor/Whinarn.UnityMeshSimplifier.Editor.Tests.asmdef +++ b/Tests/Editor/Whinarn.UnityMeshSimplifier.Editor.Tests.asmdef @@ -1,7 +1,10 @@ { "name": "Whinarn.UnityMeshSimplifier.Editor.Tests", "references": [ - "GUID:77ccaf49895b0d64e87cd4b4faf83c49" + "Whinarn.UnityMeshSimplifier.Runtime" + ], + "optionalUnityReferences": [ + "TestAssemblies" ], "includePlatforms": [ "Editor" @@ -11,7 +14,5 @@ "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false + "defineConstraints": [] } \ No newline at end of file From a6d70fb447098e8f0c9162a63afd679ac410aabb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 25 Aug 2020 00:07:05 +0000 Subject: [PATCH 6/6] chore(release): 2.3.4 [skip ci] ## [2.3.4](https://github.com/Unity-Technologies/UnityMeshSimplifier/compare/v2.3.3...v2.3.4) (2020-08-25) ### Bug Fixes * **tests:** compile errors on 2018.4.26f1 ([38a1536](https://github.com/Unity-Technologies/UnityMeshSimplifier/commit/38a1536a22468599b4b6ffc380f00b2ff7d2e718)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4b8d5..f551091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2.3.4](https://github.com/Unity-Technologies/UnityMeshSimplifier/compare/v2.3.3...v2.3.4) (2020-08-25) + + +### Bug Fixes + +* **tests:** compile errors on 2018.4.26f1 ([38a1536](https://github.com/Unity-Technologies/UnityMeshSimplifier/commit/38a1536a22468599b4b6ffc380f00b2ff7d2e718)) + ## [2.3.3](https://github.com/Whinarn/UnityMeshSimplifier/compare/v2.3.2...v2.3.3) (2020-05-01) diff --git a/package.json b/package.json index 6af7028..b42bce4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.whinarn.unitymeshsimplifier", "displayName": "Unity Mesh Simplifier", - "version": "2.3.3", + "version": "2.3.4", "unity": "2017.1", "description": "Simplifies 3D meshes with ease. Works both in the editor and during runtime in builds.", "type": "library",