From 02e13e6d5e80a0b498992513eac42af4518786be Mon Sep 17 00:00:00 2001 From: HeDo Date: Sun, 15 Mar 2026 22:10:49 +0100 Subject: [PATCH 1/6] Updated simplification algorithm + refactor --- .../Algorithms/DecimationAlgorithm.cs | 5 +- .../FastQuadricMeshSimplification.cs | 281 +++++++++--------- MeshDecimatorCore/BoneWeight.cs | 248 ---------------- MeshDecimatorCore/Mesh.cs | 150 ---------- MeshDecimatorCore/SimplificationOptions.cs | 78 +++++ Obj2Tiles/Stages/DecimationStage.cs | 18 +- 6 files changed, 229 insertions(+), 551 deletions(-) delete mode 100644 MeshDecimatorCore/BoneWeight.cs create mode 100644 MeshDecimatorCore/SimplificationOptions.cs diff --git a/MeshDecimatorCore/Algorithms/DecimationAlgorithm.cs b/MeshDecimatorCore/Algorithms/DecimationAlgorithm.cs index da69998..8cd51a3 100644 --- a/MeshDecimatorCore/Algorithms/DecimationAlgorithm.cs +++ b/MeshDecimatorCore/Algorithms/DecimationAlgorithm.cs @@ -52,10 +52,9 @@ public abstract class DecimationAlgorithm #region Properties /// - /// Gets or sets if borders should be preserved. - /// Default value: false + /// Gets or sets the simplification options. /// - public bool PreserveBorders { get; set; } = false; + public SimplificationOptions Options { get; set; } = SimplificationOptions.Default; /// /// Gets or sets the maximum vertex count. Set to zero for no limitation. diff --git a/MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs b/MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs index bc728fa..c232a64 100644 --- a/MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs +++ b/MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs @@ -223,24 +223,17 @@ public int Compare(BorderVertex x, BorderVertex y) #region Fields - private bool preserveFoldovers = false; - private bool enableSmartLink = true; - private int maxIterationCount = 100; - private double agressiveness = 7.0; - private double vertexLinkDistanceSqr = double.Epsilon; - private int subMeshCount = 0; private ResizableArray triangles = null; private ResizableArray vertices = null; private ResizableArray refs = null; private ResizableArray vertNormals = null; - private ResizableArray vertTangents = null; private UVChannels vertUV2D = null; private UVChannels vertUV3D = null; private UVChannels vertUV4D = null; private ResizableArray vertColors = null; - private ResizableArray vertBoneWeights = null; + private double[] vertCurvatures = null; private int remainingVertices = 0; @@ -249,15 +242,6 @@ public int Compare(BorderVertex x, BorderVertex y) private int[] attributeIndexArr = new int[3]; #endregion - #region Properties - /// - /// Gets or sets if seams should be preserved. - /// Default value: false - /// - public bool PreserveSeams { get; set; } = false; - - #endregion - #region Constructor /// /// Creates a new fast quadric mesh simplification algorithm. @@ -344,6 +328,19 @@ private double CalculateError(ref Vertex vert0, ref Vertex vert1, out Vector3d r resultIndex = 2; } } + + return error; + } + + private double CalculateErrorWithCurvature(int vertIndex0, int vertIndex1, out Vector3d result, out int resultIndex) + { + var vertices = this.vertices.Data; + double error = CalculateError(ref vertices[vertIndex0], ref vertices[vertIndex1], out result, out resultIndex); + if (vertCurvatures != null) + { + double curvature = System.Math.Max(vertCurvatures[vertIndex0], vertCurvatures[vertIndex1]); + error += error * curvature; + } return error; } #endregion @@ -427,9 +424,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 = CalculateErrorWithCurvature(t.v0, t.v1, out p, out pIndex); + t.err1 = CalculateErrorWithCurvature(t.v1, t.v2, out p, out pIndex); + t.err2 = CalculateErrorWithCurvature(t.v2, t.v0, out p, out pIndex); t.err3 = MathHelper.Min(t.err0, t.err1, t.err2); triangles[tid] = t; refs.Add(r); @@ -437,109 +434,85 @@ 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 Barycentric Interpolation + private static void CalculateBarycentricCoords(ref Vector3d point, ref Vector3d a, ref Vector3d b, ref Vector3d c, out double u, out double v, out double w) { - if (vertNormals != null) - { - vertNormals[i0] = vertNormals[i1]; - } - if (vertTangents != null) - { - vertTangents[i0] = vertTangents[i1]; - } - if (vertUV2D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV2D[i]; - if (vertUV != null) - { - vertUV[i0] = vertUV[i1]; - } - } - } - if (vertUV3D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV3D[i]; - if (vertUV != null) - { - vertUV[i0] = vertUV[i1]; - } - } - } - if (vertUV4D != null) - { - for (int i = 0; i < Mesh.UVChannelCount; i++) - { - var vertUV = vertUV4D[i]; - if (vertUV != null) - { - vertUV[i0] = vertUV[i1]; - } - } - } - if (vertColors != null) - { - vertColors[i0] = vertColors[i1]; - } - if (vertBoneWeights != null) - { - vertBoneWeights[i0] = vertBoneWeights[i1]; - } + const double denomEpsilon = 1e-8; + var v0 = b - a; + var v1 = c - a; + var v2 = point - a; + double d00 = Vector3d.Dot(ref v0, ref v0); + double d01 = Vector3d.Dot(ref v0, ref v1); + double d11 = Vector3d.Dot(ref v1, ref v1); + double d20 = Vector3d.Dot(ref v2, ref v0); + double d21 = Vector3d.Dot(ref v2, ref v1); + double denom = d00 * d11 - d01 * d01; + if (System.Math.Abs(denom) < denomEpsilon) + denom = denomEpsilon; + + v = (d11 * d20 - d01 * d21) / denom; + w = (d00 * d21 - d01 * d20) / denom; + u = 1.0 - v - w; } - private void MergeVertexAttributes(int i0, int i1) + private void InterpolateVertexAttributes(int dst, int i0, int i1, int i2, ref Vector3d point) { + var verts = this.vertices.Data; + var p0 = verts[i0].p; + var p1 = verts[i1].p; + var p2 = verts[i2].p; + CalculateBarycentricCoords(ref point, ref p0, ref p1, ref p2, out double u, out double v, out double w); + + float fu = (float)u, fv = (float)v, fw = (float)w; + if (vertNormals != null) { - vertNormals[i0] = (vertNormals[i0] + vertNormals[i1]) * 0.5f; - } - if (vertTangents != null) - { - vertTangents[i0] = (vertTangents[i0] + vertTangents[i1]) * 0.5f; + var n = vertNormals.Data; + var result = n[i0] * fu + n[i1] * fv + n[i2] * fw; + result.Normalize(); + n[dst] = result; } if (vertUV2D != null) { - for (int i = 0; i < Mesh.UVChannelCount; i++) + for (int ch = 0; ch < Mesh.UVChannelCount; ch++) { - var vertUV = vertUV2D[i]; + var vertUV = vertUV2D[ch]; if (vertUV != null) { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; + var d = vertUV.Data; + d[dst] = d[i0] * fu + d[i1] * fv + d[i2] * fw; } } } if (vertUV3D != null) { - for (int i = 0; i < Mesh.UVChannelCount; i++) + for (int ch = 0; ch < Mesh.UVChannelCount; ch++) { - var vertUV = vertUV3D[i]; + var vertUV = vertUV3D[ch]; if (vertUV != null) { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; + var d = vertUV.Data; + d[dst] = d[i0] * fu + d[i1] * fv + d[i2] * fw; } } } if (vertUV4D != null) { - for (int i = 0; i < Mesh.UVChannelCount; i++) + for (int ch = 0; ch < Mesh.UVChannelCount; ch++) { - var vertUV = vertUV4D[i]; + var vertUV = vertUV4D[ch]; if (vertUV != null) { - vertUV[i0] = (vertUV[i0] + vertUV[i1]) * 0.5f; + var d = vertUV.Data; + d[dst] = d[i0] * fu + d[i1] * fv + d[i2] * fw; } } } if (vertColors != null) { - vertColors[i0] = (vertColors[i0] + vertColors[i1]) * 0.5f; + var c = vertColors.Data; + c[dst] = c[i0] * fu + c[i1] * fv + c[i2] * fw; } - - // TODO: Do we have to blend bone weights at all or can we just keep them as it is in this scenario? } #endregion @@ -583,6 +556,45 @@ private bool AreUVsTheSame(int channel, int indexA, int indexB) } #endregion + #region Surface Curvature + private void CalculateVertexCurvatures(Triangle[] triangles, Vertex[] vertices, Ref[] refs, int vertexCount, int triangleCount) + { + vertCurvatures = new double[vertexCount]; + for (int i = 0; i < vertexCount; i++) + { + int tstart = vertices[i].tstart; + int tcount = vertices[i].tcount; + if (tcount <= 1) + { + vertCurvatures[i] = 0; + continue; + } + + double maxCurvature = 0; + for (int j = 0; j < tcount; j++) + { + int tidA = refs[tstart + j].tid; + if (triangles[tidA].deleted) continue; + + var nA = triangles[tidA].n; + for (int k = j + 1; k < tcount; k++) + { + int tidB = refs[tstart + k].tid; + if (triangles[tidB].deleted) continue; + + var nB = triangles[tidB].n; + double dot = Vector3d.Dot(ref nA, ref nB); + dot = MathHelper.Clamp(dot, -1.0, 1.0); + double curvature = (1.0 - dot) * 0.5; // 0 = flat, 1 = opposite normals + if (curvature > maxCurvature) + maxCurvature = curvature; + } + } + vertCurvatures[i] = maxCurvature; + } + } + #endregion + #region Remove Vertex Pass /// /// Remove vertices and mark deleted triangles @@ -593,13 +605,15 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th int triangleCount = this.triangles.Length; var vertices = this.vertices.Data; - bool preserveBorders = base.PreserveBorders; + var options = Options; + bool preserveBorderEdges = options.PreserveBorderEdges; + bool preserveUVSeamEdges = options.PreserveUVSeamEdges; + bool preserveUVFoldoverEdges = options.PreserveUVFoldoverEdges; int maxVertexCount = base.MaxVertexCount; if (maxVertexCount <= 0) maxVertexCount = int.MaxValue; Vector3d p; - int pIndex; for (int tid = 0; tid < triangleCount; tid++) { if (triangles[tid].dirty || triangles[tid].deleted || triangles[tid].err3 > threshold) @@ -625,18 +639,18 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th // Foldover check if (vertices[i0].foldover != vertices[i1].foldover) continue; - // If borders should be preserved - if (preserveBorders && vertices[i0].border) + // If border edges should be preserved + if (preserveBorderEdges && vertices[i0].border) continue; - // If seams should be preserved - if (PreserveSeams && vertices[i0].seam) + // If UV seam edges should be preserved + if (preserveUVSeamEdges && vertices[i0].seam) continue; - // If foldovers should be preserved - if (preserveFoldovers && vertices[i0].foldover) + // If UV foldover edges should be preserved + if (preserveUVFoldoverEdges && vertices[i0].foldover) continue; // Compute vertex to collapse to - CalculateError(ref vertices[i0], ref vertices[i1], out p, out pIndex); + CalculateErrorWithCurvature(i0, i1, out p, out _); deleted0.Resize(vertices[i0].tcount); // normals temporarily deleted1.Resize(vertices[i1].tcount); // normals temporarily @@ -647,23 +661,17 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th continue; int ia0 = attributeIndexArr[edgeIndex]; + int ia1 = attributeIndexArr[nextEdgeIndex]; // 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); - } + // Find the third vertex of the current triangle for barycentric interpolation + int thirdEdgeIndex = 3 - edgeIndex - nextEdgeIndex; + int i2 = triangles[tid][thirdEdgeIndex]; + int ia2 = attributeIndexArr[thirdEdgeIndex]; + InterpolateVertexAttributes(ia0, ia0, ia1, ia2, ref p); if (vertices[i0].seam) { @@ -799,7 +807,7 @@ private void UpdateMesh(int iteration) vertices[id].border = true; ++borderVertexCount; - if (enableSmartLink) + if (Options.EnableSmartLink) { if (vertices[id].p.x < borderMinX) { @@ -814,7 +822,7 @@ private void UpdateMesh(int iteration) } } - if (enableSmartLink) + if (Options.EnableSmartLink) { // First find all border vertices var borderVertices = new BorderVertex[borderVertexCount]; @@ -834,7 +842,8 @@ private void UpdateMesh(int iteration) Array.Sort(borderVertices, 0, borderIndexCount, BorderVertexComparer.instance); // Calculate the maximum hash distance based on the maximum vertex link distance - double vertexLinkDistance = System.Math.Sqrt(vertexLinkDistanceSqr); + double vertexLinkDistanceSqr = Options.VertexLinkDistance * Options.VertexLinkDistance; + double vertexLinkDistance = Options.VertexLinkDistance; int hashMaxDistance = System.Math.Max((int)((vertexLinkDistance / borderAreaWidth) * int.MaxValue), 1); // Then find identical border vertices and bind them together as one @@ -926,13 +935,19 @@ private void UpdateMesh(int iteration) vertices[v2].q += sm; } + // Calculate per-vertex surface curvature if requested + if (Options.PreserveSurfaceCurvature) + { + CalculateVertexCurvatures(triangles, vertices, refs, vertexCount, triangleCount); + } + for (int i = 0; i < triangleCount; i++) { // 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 = CalculateErrorWithCurvature(triangle.v0, triangle.v1, out dummy, out dummy2); + triangles[i].err1 = CalculateErrorWithCurvature(triangle.v1, triangle.v2, out dummy, out dummy2); + triangles[i].err2 = CalculateErrorWithCurvature(triangle.v2, triangle.v0, out dummy, out dummy2); triangles[i].err3 = MathHelper.Min(triangles[i].err0, triangles[i].err1, triangles[i].err2); } } @@ -1015,12 +1030,10 @@ private void CompactMesh() } 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); var triangles = this.triangles.Data; int triangleCount = this.triangles.Length; @@ -1034,10 +1047,6 @@ private void CompactMesh() 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) @@ -1045,10 +1054,6 @@ private void CompactMesh() 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) @@ -1056,10 +1061,6 @@ private void CompactMesh() 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; } @@ -1088,7 +1089,6 @@ private void CompactMesh() { vertices[dst].p = vert.p; if (vertNormals != null) vertNormals[dst] = vertNormals[i]; - if (vertTangents != null) vertTangents[dst] = vertTangents[i]; if (vertUV2D != null) { for (int j = 0; j < Mesh.UVChannelCount; j++) @@ -1123,7 +1123,6 @@ private void CompactMesh() } } if (vertColors != null) vertColors[dst] = vertColors[i]; - if (vertBoneWeights != null) vertBoneWeights[dst] = vertBoneWeights[i]; } ++dst; } @@ -1141,12 +1140,10 @@ private void CompactMesh() 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 @@ -1166,9 +1163,7 @@ public override void Initialize(Mesh mesh) int meshTriangleCount = mesh.TriangleCount; var meshVertices = mesh.Vertices; var meshNormals = mesh.Normals; - var meshTangents = mesh.Tangents; var meshColors = mesh.Colors; - var meshBoneWeights = mesh.BoneWeights; subMeshCount = meshSubMeshCount; vertices.Resize(meshVertices.Length); @@ -1196,9 +1191,7 @@ public override void Initialize(Mesh mesh) } vertNormals = InitializeVertexAttribute(meshNormals, "normals"); - vertTangents = InitializeVertexAttribute(meshTangents, "tangents"); vertColors = InitializeVertexAttribute(meshColors, "colors"); - vertBoneWeights = InitializeVertexAttribute(meshBoneWeights, "boneWeights"); for (int i = 0; i < Mesh.UVChannelCount; i++) { @@ -1254,7 +1247,7 @@ public override void DecimateMesh(int targetTrisCount) if (maxVertexCount <= 0) maxVertexCount = int.MaxValue; - for (int iteration = 0; iteration < maxIterationCount; iteration++) + for (int iteration = 0; iteration < Options.MaxIterationCount; iteration++) { ReportStatus(iteration, startTrisCount, (startTrisCount - deletedTris), targetTrisCount); if ((startTrisCount - deletedTris) <= targetTrisCount && remainingVertices < maxVertexCount) @@ -1279,7 +1272,7 @@ public override void DecimateMesh(int targetTrisCount) // // The following numbers works well for most models. // If it does not, try to adjust the 3 parameters - double threshold = 0.000000001 * System.Math.Pow(iteration + 3, agressiveness); + double threshold = 0.000000001 * System.Math.Pow(iteration + 3, Options.Agressiveness); if (Verbose && (iteration % 5) == 0) { @@ -1424,18 +1417,10 @@ public override Mesh ToMesh() { newMesh.Normals = vertNormals.Data; } - if (vertTangents != null) - { - newMesh.Tangents = vertTangents.Data; - } if (vertColors != null) { newMesh.Colors = vertColors.Data; } - if (vertBoneWeights != null) - { - newMesh.BoneWeights = vertBoneWeights.Data; - } if (vertUV2D != null) { diff --git a/MeshDecimatorCore/BoneWeight.cs b/MeshDecimatorCore/BoneWeight.cs deleted file mode 100644 index d59dfa4..0000000 --- a/MeshDecimatorCore/BoneWeight.cs +++ /dev/null @@ -1,248 +0,0 @@ -#region License -/* -MIT License - -Copyright(c) 2017-2018 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 MeshDecimatorCore.Math; - -namespace MeshDecimatorCore -{ - /// - /// A bone weight. - /// - public struct BoneWeight : IEquatable - { - #region Fields - /// - /// The first bone index. - /// - public int boneIndex0; - /// - /// The second bone index. - /// - public int boneIndex1; - /// - /// The third bone index. - /// - public int boneIndex2; - /// - /// The fourth bone index. - /// - public int boneIndex3; - - /// - /// The first bone weight. - /// - public float boneWeight0; - /// - /// The second bone weight. - /// - public float boneWeight1; - /// - /// The third bone weight. - /// - public float boneWeight2; - /// - /// The fourth bone weight. - /// - public float boneWeight3; - #endregion - - #region Constructor - /// - /// Creates a new bone weight. - /// - /// The first bone index. - /// The second bone index. - /// The third bone index. - /// The fourth bone index. - /// The first bone weight. - /// The second bone weight. - /// The third bone weight. - /// The fourth bone weight. - public BoneWeight(int boneIndex0, int boneIndex1, int boneIndex2, int boneIndex3, float boneWeight0, float boneWeight1, float boneWeight2, float boneWeight3) - { - this.boneIndex0 = boneIndex0; - this.boneIndex1 = boneIndex1; - this.boneIndex2 = boneIndex2; - this.boneIndex3 = boneIndex3; - - this.boneWeight0 = boneWeight0; - this.boneWeight1 = boneWeight1; - this.boneWeight2 = boneWeight2; - this.boneWeight3 = boneWeight3; - } - #endregion - - #region Operators - /// - /// Returns if two bone weights equals eachother. - /// - /// The left hand side bone weight. - /// The right hand side bone weight. - /// If equals. - public static bool operator ==(BoneWeight lhs, BoneWeight rhs) - { - return (lhs.boneIndex0 == rhs.boneIndex0 && lhs.boneIndex1 == rhs.boneIndex1 && lhs.boneIndex2 == rhs.boneIndex2 && lhs.boneIndex3 == rhs.boneIndex3 && - new Vector4(lhs.boneWeight0, lhs.boneWeight1, lhs.boneWeight2, lhs.boneWeight3) == new Vector4(rhs.boneWeight0, rhs.boneWeight1, rhs.boneWeight2, rhs.boneWeight3)); - } - - /// - /// Returns if two bone weights don't equal eachother. - /// - /// The left hand side bone weight. - /// The right hand side bone weight. - /// If not equals. - public static bool operator !=(BoneWeight lhs, BoneWeight rhs) - { - return !(lhs == rhs); - } - #endregion - - #region Private Methods - private void MergeBoneWeight(int boneIndex, float weight) - { - if (boneIndex == boneIndex0) - { - boneWeight0 = (boneWeight0 + weight) * 0.5f; - } - else if (boneIndex == boneIndex1) - { - boneWeight1 = (boneWeight1 + weight) * 0.5f; - } - else if (boneIndex == boneIndex2) - { - boneWeight2 = (boneWeight2 + weight) * 0.5f; - } - else if (boneIndex == boneIndex3) - { - boneWeight3 = (boneWeight3 + weight) * 0.5f; - } - else if(boneWeight0 == 0f) - { - boneIndex0 = boneIndex; - boneWeight0 = weight; - } - else if (boneWeight1 == 0f) - { - boneIndex1 = boneIndex; - boneWeight1 = weight; - } - else if (boneWeight2 == 0f) - { - boneIndex2 = boneIndex; - boneWeight2 = weight; - } - else if (boneWeight3 == 0f) - { - boneIndex3 = boneIndex; - boneWeight3 = weight; - } - Normalize(); - } - - private void Normalize() - { - float mag = (float)System.Math.Sqrt(boneWeight0 * boneWeight0 + boneWeight1 * boneWeight1 + boneWeight2 * boneWeight2 + boneWeight3 * boneWeight3); - if (mag > float.Epsilon) - { - boneWeight0 /= mag; - boneWeight1 /= mag; - boneWeight2 /= mag; - boneWeight3 /= mag; - } - else - { - boneWeight0 = boneWeight1 = boneWeight2 = boneWeight3 = 0f; - } - } - #endregion - - #region Public Methods - #region Object - /// - /// Returns a hash code for this vector. - /// - /// The hash code. - public override int GetHashCode() - { - return boneIndex0.GetHashCode() ^ boneIndex1.GetHashCode() << 2 ^ boneIndex2.GetHashCode() >> 2 ^ boneIndex3.GetHashCode() >> - 1 ^ boneWeight0.GetHashCode() << 5 ^ boneWeight1.GetHashCode() << 4 ^ boneWeight2.GetHashCode() >> 4 ^ boneWeight3.GetHashCode() >> 3; - } - - /// - /// Returns if this bone weight is equal to another object. - /// - /// The other object to compare to. - /// If equals. - public override bool Equals(object obj) - { - if (!(obj is BoneWeight)) - { - return false; - } - BoneWeight other = (BoneWeight)obj; - return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 && - boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3); - } - - /// - /// Returns if this bone weight is equal to another one. - /// - /// The other bone weight to compare to. - /// If equals. - public bool Equals(BoneWeight other) - { - return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 && - boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3); - } - - /// - /// Returns a nicely formatted string for this bone weight. - /// - /// The string. - public override string ToString() - { - return string.Format("({0}:{4:F1}, {1}:{5:F1}, {2}:{6:F1}, {3}:{7:F1})", - boneIndex0, boneIndex1, boneIndex2, boneIndex3, boneWeight0, boneWeight1, boneWeight2, boneWeight3); - } - #endregion - - #region Static - /// - /// Merges two bone weights and stores the merged result in the first parameter. - /// - /// The first bone weight, also stores result. - /// The second bone weight. - public static void Merge(ref BoneWeight a, ref BoneWeight b) - { - if (b.boneWeight0 > 0f) a.MergeBoneWeight(b.boneIndex0, b.boneWeight0); - if (b.boneWeight1 > 0f) a.MergeBoneWeight(b.boneIndex1, b.boneWeight1); - if (b.boneWeight2 > 0f) a.MergeBoneWeight(b.boneIndex2, b.boneWeight2); - if (b.boneWeight3 > 0f) a.MergeBoneWeight(b.boneIndex3, b.boneWeight3); - } - #endregion - #endregion - } -} \ No newline at end of file diff --git a/MeshDecimatorCore/Mesh.cs b/MeshDecimatorCore/Mesh.cs index 8117237..a3a0010 100644 --- a/MeshDecimatorCore/Mesh.cs +++ b/MeshDecimatorCore/Mesh.cs @@ -44,12 +44,10 @@ public sealed class Mesh private Vector3d[] vertices = null; private int[][] indices = null; private Vector3[] normals = null; - private Vector4[] tangents = null; private Vector2[][] uvs2D = null; private Vector3[][] uvs3D = null; private Vector4[][] uvs4D = null; private Vector4[] colors = null; - private BoneWeight[] boneWeights = null; private static readonly int[] emptyIndices = []; #endregion @@ -166,21 +164,6 @@ public Vector3[] Normals } } - /// - /// Gets or sets the tangents for this mesh. - /// - public Vector4[] Tangents - { - get { return tangents; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The vertex tangents must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - tangents = value; - } - } - /// /// Gets or sets the first UV set for this mesh. /// @@ -232,20 +215,6 @@ public Vector4[] Colors } } - /// - /// Gets or sets the vertex bone weights for this mesh. - /// - public BoneWeight[] BoneWeights - { - get { return boneWeights; } - set - { - if (value != null && value.Length != vertices.Length) - throw new ArgumentException(string.Format("The vertex bone weights must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length)); - - boneWeights = value; - } - } #endregion #region Constructor @@ -295,12 +264,10 @@ public Mesh(Vector3d[] vertices, int[][] indices) private void ClearVertexAttributes() { normals = null; - tangents = null; uvs2D = null; uvs3D = null; uvs4D = null; colors = null; - boneWeights = null; } #endregion @@ -353,123 +320,6 @@ public void RecalculateNormals() } #endregion - #region Recalculate Tangents - /// - /// Recalculates the tangents for this mesh. - /// - public void RecalculateTangents() - { - // Make sure we have the normals first - if (normals == null) - return; - - // Also make sure that we have the first UV set - bool uvIs2D = (uvs2D != null && uvs2D[0] != null); - bool uvIs3D = (uvs3D != null && uvs3D[0] != null); - bool uvIs4D = (uvs4D != null && uvs4D[0] != null); - if (!uvIs2D && !uvIs3D && !uvIs4D) - return; - - int vertexCount = vertices.Length; - - var tangents = new Vector4[vertexCount]; - var tan1 = new Vector3[vertexCount]; - var tan2 = new Vector3[vertexCount]; - - Vector2[] uv2D = (uvIs2D ? uvs2D[0] : null); - Vector3[] uv3D = (uvIs3D ? uvs3D[0] : null); - Vector4[] uv4D = (uvIs4D ? uvs4D[0] : null); - - int subMeshCount = this.indices.Length; - for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++) - { - int[] indices = this.indices[subMeshIndex]; - if (indices == null) - continue; - - int indexCount = indices.Length; - for (int i = 0; i < indexCount; i += 3) - { - int i0 = indices[i]; - int i1 = indices[i + 1]; - int i2 = indices[i + 2]; - - var v0 = vertices[i0]; - var v1 = vertices[i1]; - var v2 = vertices[i2]; - - float s1, s2, t1, t2; - if (uvIs2D) - { - var w0 = uv2D[i0]; - var w1 = uv2D[i1]; - var w2 = uv2D[i2]; - s1 = w1.x - w0.x; - s2 = w2.x - w0.x; - t1 = w1.y - w0.y; - t2 = w2.y - w0.y; - } - else if (uvIs3D) - { - var w0 = uv3D[i0]; - var w1 = uv3D[i1]; - var w2 = uv3D[i2]; - s1 = w1.x - w0.x; - s2 = w2.x - w0.x; - t1 = w1.y - w0.y; - t2 = w2.y - w0.y; - } - else - { - var w0 = uv4D[i0]; - var w1 = uv4D[i1]; - var w2 = uv4D[i2]; - s1 = w1.x - w0.x; - s2 = w2.x - w0.x; - t1 = w1.y - w0.y; - t2 = w2.y - w0.y; - } - - - float x1 = (float)(v1.x - v0.x); - float x2 = (float)(v2.x - v0.x); - float y1 = (float)(v1.y - v0.y); - float y2 = (float)(v2.y - v0.y); - float z1 = (float)(v1.z - v0.z); - float z2 = (float)(v2.z - v0.z); - float r = 1f / (s1 * t2 - s2 * t1); - - var sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r); - var tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r); - - tan1[i0] += sdir; - tan1[i1] += sdir; - tan1[i2] += sdir; - tan2[i0] += tdir; - tan2[i1] += tdir; - tan2[i2] += tdir; - } - } - - for (int i = 0; i < vertexCount; i++) - { - var n = normals[i]; - var t = tan1[i]; - - var tmp = (t - n * Vector3.Dot(ref n, ref t)); - tmp.Normalize(); - - Vector3 c; - Vector3.Cross(ref n, ref t, out c); - float dot = Vector3.Dot(ref c, ref tan2[i]); - float w = (dot < 0f ? -1f : 1f); - tangents[i] = new Vector4(tmp.x, tmp.y, tmp.z, w); - } - - this.tangents = tangents; - } - #endregion - #region Triangles /// /// Returns the count of triangles for a specific sub-mesh in this mesh. diff --git a/MeshDecimatorCore/SimplificationOptions.cs b/MeshDecimatorCore/SimplificationOptions.cs new file mode 100644 index 0000000..959b43e --- /dev/null +++ b/MeshDecimatorCore/SimplificationOptions.cs @@ -0,0 +1,78 @@ +namespace MeshDecimatorCore +{ + /// + /// Options for mesh simplification algorithms. + /// Based on UnityMeshSimplifier SimplificationOptions. + /// + public struct SimplificationOptions + { + /// + /// Default simplification options. + /// + public static readonly SimplificationOptions Default = new SimplificationOptions + { + PreserveBorderEdges = false, + PreserveUVSeamEdges = false, + PreserveUVFoldoverEdges = false, + PreserveSurfaceCurvature = false, + EnableSmartLink = true, + VertexLinkDistance = double.Epsilon, + MaxIterationCount = 100, + Agressiveness = 7.0 + }; + + /// + /// If enabled, border edges (open mesh boundaries) will not be collapsed. + /// Default value: false + /// + public bool PreserveBorderEdges; + + /// + /// If enabled, UV seam edges will not be collapsed, + /// preventing texture discontinuity artifacts. + /// Default value: false + /// + public bool PreserveUVSeamEdges; + + /// + /// If enabled, UV foldover edges will not be collapsed. + /// Default value: false + /// + public bool PreserveUVFoldoverEdges; + + /// + /// If enabled, an additional curvature penalty is applied during + /// error calculation to better preserve surface shape. + /// Default value: false + /// + public bool PreserveSurfaceCurvature; + + /// + /// If enabled, border vertices at the same position are linked together + /// as seam or foldover edges instead of being treated as borders. + /// This prevents holes while still allowing decimation of shared edges. + /// Default value: true + /// + public bool EnableSmartLink; + + /// + /// The maximum distance between two vertices to be linked together + /// when smart linking is enabled. + /// Default value: double.Epsilon + /// + public double VertexLinkDistance; + + /// + /// The maximum number of iterations for the decimation algorithm. + /// Default value: 100 + /// + public int MaxIterationCount; + + /// + /// The aggressiveness of the decimation algorithm. + /// Higher values result in faster decimation with potentially lower quality. + /// Default value: 7.0 + /// + public double Agressiveness; + } +} diff --git a/Obj2Tiles/Stages/DecimationStage.cs b/Obj2Tiles/Stages/DecimationStage.cs index 541b3f3..25078bb 100644 --- a/Obj2Tiles/Stages/DecimationStage.cs +++ b/Obj2Tiles/Stages/DecimationStage.cs @@ -89,9 +89,17 @@ private static void InternalDecimate(ObjMesh sourceObjMesh, string destPath, flo var algorithm = new FastQuadricMeshSimplification { - PreserveSeams = true, Verbose = true, - PreserveBorders = true + Options = new SimplificationOptions + { + EnableSmartLink = true, + PreserveUVSeamEdges = false, + PreserveBorderEdges = quality > 0.2f, + PreserveSurfaceCurvature = true, + Agressiveness = 7.0, + MaxIterationCount = 100, + VertexLinkDistance = double.Epsilon + } }; var destMesh = MeshDecimation.DecimateMesh(algorithm, sourceMesh, targetTriangleCount); @@ -124,6 +132,12 @@ private static void InternalDecimate(ObjMesh sourceObjMesh, string destPath, flo var outputTriangleCount = destIndices.Sum(t => (t.Length / 3)); + if (outputTriangleCount >= currentTriangleCount * 0.95) + { + Console.WriteLine(" ?> WARNING: LOD at quality {0:0.00} could only reduce to {1}/{2} triangles", + quality, outputTriangleCount, currentTriangleCount); + } + var reduction = (float)outputTriangleCount / currentTriangleCount; var timeTaken = (float)stopwatch.Elapsed.TotalSeconds; Console.WriteLine(" ?> Output: {0} vertices, {1} triangles ({2} reduction; {3:0.0000} sec)", From 991d4575912d44bb1e537f83fa0f0c3f19e0bd2c Mon Sep 17 00:00:00 2001 From: HeDo Date: Mon, 16 Mar 2026 00:09:06 +0100 Subject: [PATCH 2/6] Fixed typo --- MeshDecimatorCore/SimplificationOptions.cs | 4 ++-- Obj2Tiles/Stages/DecimationStage.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MeshDecimatorCore/SimplificationOptions.cs b/MeshDecimatorCore/SimplificationOptions.cs index 959b43e..0515ad4 100644 --- a/MeshDecimatorCore/SimplificationOptions.cs +++ b/MeshDecimatorCore/SimplificationOptions.cs @@ -18,7 +18,7 @@ public struct SimplificationOptions EnableSmartLink = true, VertexLinkDistance = double.Epsilon, MaxIterationCount = 100, - Agressiveness = 7.0 + Aggressiveness = 7.0 }; /// @@ -73,6 +73,6 @@ public struct SimplificationOptions /// Higher values result in faster decimation with potentially lower quality. /// Default value: 7.0 /// - public double Agressiveness; + public double Aggressiveness; } } diff --git a/Obj2Tiles/Stages/DecimationStage.cs b/Obj2Tiles/Stages/DecimationStage.cs index 25078bb..eefe233 100644 --- a/Obj2Tiles/Stages/DecimationStage.cs +++ b/Obj2Tiles/Stages/DecimationStage.cs @@ -96,7 +96,7 @@ private static void InternalDecimate(ObjMesh sourceObjMesh, string destPath, flo PreserveUVSeamEdges = false, PreserveBorderEdges = quality > 0.2f, PreserveSurfaceCurvature = true, - Agressiveness = 7.0, + Aggressiveness = 7.0, MaxIterationCount = 100, VertexLinkDistance = double.Epsilon } From b6c5f023f129f7b0611b51c2cd529bd44a86f02d Mon Sep 17 00:00:00 2001 From: HeDo Date: Mon, 16 Mar 2026 00:09:21 +0100 Subject: [PATCH 3/6] Added tests --- .../FastQuadricMeshSimplification.cs | 12 +-- Obj2Tiles.Test/DecimationLodTests.cs | 73 +++++++++++++++++++ 2 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 Obj2Tiles.Test/DecimationLodTests.cs diff --git a/MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs b/MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs index c232a64..b354743 100644 --- a/MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs +++ b/MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs @@ -663,16 +663,16 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th int ia0 = attributeIndexArr[edgeIndex]; int ia1 = attributeIndexArr[nextEdgeIndex]; - // Not flipped, so remove edge - vertices[i0].p = p; - vertices[i0].q += vertices[i1].q; - // Find the third vertex of the current triangle for barycentric interpolation + // Must happen BEFORE updating vertex position, otherwise barycentric coords degenerate int thirdEdgeIndex = 3 - edgeIndex - nextEdgeIndex; - int i2 = triangles[tid][thirdEdgeIndex]; int ia2 = attributeIndexArr[thirdEdgeIndex]; InterpolateVertexAttributes(ia0, ia0, ia1, ia2, ref p); + // Not flipped, so remove edge + vertices[i0].p = p; + vertices[i0].q += vertices[i1].q; + if (vertices[i0].seam) { ia0 = -1; @@ -1272,7 +1272,7 @@ public override void DecimateMesh(int targetTrisCount) // // The following numbers works well for most models. // If it does not, try to adjust the 3 parameters - double threshold = 0.000000001 * System.Math.Pow(iteration + 3, Options.Agressiveness); + double threshold = 0.000000001 * System.Math.Pow(iteration + 3, Options.Aggressiveness); if (Verbose && (iteration % 5) == 0) { diff --git a/Obj2Tiles.Test/DecimationLodTests.cs b/Obj2Tiles.Test/DecimationLodTests.cs new file mode 100644 index 0000000..4b9711a --- /dev/null +++ b/Obj2Tiles.Test/DecimationLodTests.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using Obj2Tiles.Stages; +using Obj2Tiles.Stages.Model; +using Shouldly; + +namespace Obj2Tiles.Test; + +/// +/// Regression tests for the decimation stage. +/// Verifies that successive LODs have monotonically decreasing triangle counts (issue #23). +/// +public class DecimationLodTests +{ + private const string TestOutputPath = "TestOutput"; + + private static string GetTestOutputPath(string testName) + { + var folder = Path.Combine(TestOutputPath, testName); + if (Directory.Exists(folder)) + Directory.Delete(folder, true); + Directory.CreateDirectory(folder); + return folder; + } + + [SetUp] + public void Setup() + { + Directory.CreateDirectory(TestOutputPath); + } + + private static int CountTriangles(string objPath) + { + var mesh = new ObjMesh(); + mesh.ReadFile(objPath); + return mesh.SubMeshIndices!.Sum(idx => idx.Length / 3); + } + + [Test] + public async Task Decimate_LodTriangleCounts_MonotonicallyDecrease() + { + var testPath = GetTestOutputPath(nameof(Decimate_LodTriangleCounts_MonotonicallyDecrease)); + + // Use real test mesh (Tile2) which has UV coords and materials + var srcObj = Path.GetFullPath("TestData/Tile2/Mesh-XL-YR-XR-YL.obj"); + File.Exists(srcObj).ShouldBeTrue($"Test fixture not found: {srcObj}"); + + var srcTriangles = CountTriangles(srcObj); + srcTriangles.ShouldBeGreaterThan(1000, "Source mesh should have enough triangles for meaningful decimation"); + + var lodOutputPath = Path.Combine(testPath, "lods"); + Directory.CreateDirectory(lodOutputPath); + + // 3 LODs: LOD-0 is original, LOD-1/2 are decimated at quality 0.66 and 0.33 + var result = await StagesFacade.Decimate(srcObj, lodOutputPath, lods: 3); + + result.DestFiles.Length.ShouldBe(3, "Should produce 3 LOD files (original + 2 decimated)"); + + var triCounts = result.DestFiles.Select(CountTriangles).ToArray(); + + Console.WriteLine("LOD triangle counts: " + string.Join(", ", triCounts)); + + // Each successive LOD must have strictly fewer triangles + for (int i = 1; i < triCounts.Length; i++) + { + triCounts[i].ShouldBeLessThan(triCounts[i - 1], + $"LOD-{i} ({triCounts[i]} tris) should have fewer triangles than LOD-{i - 1} ({triCounts[i - 1]} tris)"); + } + } +} From ca3d3f979879ca1730cd7c8148b8b20cf79406b2 Mon Sep 17 00:00:00 2001 From: HeDo Date: Mon, 16 Mar 2026 16:16:44 +0100 Subject: [PATCH 4/6] Fixed bug in split strategy --- Obj2Tiles/Stages/SplitStage.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Obj2Tiles/Stages/SplitStage.cs b/Obj2Tiles/Stages/SplitStage.cs index fc866a2..f5f52d2 100644 --- a/Obj2Tiles/Stages/SplitStage.cs +++ b/Obj2Tiles/Stages/SplitStage.cs @@ -8,7 +8,7 @@ namespace Obj2Tiles.Stages; public static partial class StagesFacade { public static async Task[]> Split(string[] sourceFiles, string destFolder, int divisions, - bool zsplit, Box3 bounds, bool keepOriginalTextures = false) + bool zsplit, Box3 bounds, bool keepOriginalTextures = false, SplitPointStrategy splitPointStrategy = SplitPointStrategy.VertexBaricenter) { var tasks = new List>>(); @@ -22,7 +22,7 @@ public static async Task[]> Split(string[] sourceFiles, var textureStrategy = keepOriginalTextures ? TexturesStrategy.KeepOriginal : index == 0 ? TexturesStrategy.Repack : TexturesStrategy.RepackCompressed; - var splitTask = Split(file, dest, divisions, zsplit, bounds, textureStrategy); + var splitTask = Split(file, dest, divisions, zsplit, bounds, textureStrategy, splitPointStrategy); tasks.Add(splitTask); } @@ -73,7 +73,7 @@ public static async Task> Split(string sourcePath, stri int count; - if (bounds is not null) + if (splitPointStrategy == SplitPointStrategy.AbsoluteCenter && bounds is not null) { count = zSplit ? await MeshUtils.RecurseSplitXYZ(mesh, divisions, bounds, meshes) From 2e4c773513c60055077641147416c1af670b8c07 Mon Sep 17 00:00:00 2001 From: HeDo Date: Mon, 16 Mar 2026 16:17:03 +0100 Subject: [PATCH 5/6] Added new parameter --split-strategy --- Obj2Tiles/Options.cs | 4 ++++ Obj2Tiles/Program.cs | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Obj2Tiles/Options.cs b/Obj2Tiles/Options.cs index 3cc2e7f..97418bd 100644 --- a/Obj2Tiles/Options.cs +++ b/Obj2Tiles/Options.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using CommandLine; +using Obj2Tiles.Stages; namespace Obj2Tiles; @@ -26,6 +27,9 @@ public sealed class Options [Option('k', "keeptextures", Required = false, HelpText = "Keeps original textures", Default = false)] public bool KeepOriginalTextures { get; set; } + [Option('g', "split-strategy", Required = false, HelpText = "Split strategy: AbsoluteCenter or VertexBaricenter", Default = SplitPointStrategy.VertexBaricenter)] + public SplitPointStrategy SplitPointStrategy { get; set; } = SplitPointStrategy.VertexBaricenter; + [Option("lat", Required = false, HelpText = "Latitude of the mesh", Default = null)] public double? Latitude { get; set; } diff --git a/Obj2Tiles/Program.cs b/Obj2Tiles/Program.cs index 9ee33f1..11c8b17 100644 --- a/Obj2Tiles/Program.cs +++ b/Obj2Tiles/Program.cs @@ -72,9 +72,11 @@ private static async Task Run(Options opts) destFolderSplit = opts.StopAt == Stage.Splitting ? opts.Output : createTempFolder($"{pipelineId}-obj2tiles-split"); + + Console.WriteLine($" ?> Keep original textures: {opts.KeepOriginalTextures}, Split strategy: {opts.SplitPointStrategy}"); var boundsMapper = await StagesFacade.Split(decimateRes.DestFiles, destFolderSplit, opts.Divisions, - opts.ZSplit, decimateRes.Bounds, opts.KeepOriginalTextures); + opts.ZSplit, decimateRes.Bounds, opts.KeepOriginalTextures, opts.SplitPointStrategy); Console.WriteLine(" ?> Splitting stage done in {0}", sw.Elapsed); From d48bafd9f86e45cd5a834ce29f5e1a7f894f9d50 Mon Sep 17 00:00:00 2001 From: HeDo Date: Mon, 16 Mar 2026 16:18:17 +0100 Subject: [PATCH 6/6] Updated readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f2b84b4..37a175d 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ You can download precompiled binaries for Windows, Linux and macOS from https:// -d, --divisions (Default: 2) How many tiles divisions -z, --zsplit (Default: false) Splits along z-axis too + -g, --split-strategy (Default: VertexBaricenter) Split point strategy: AbsoluteCenter or VertexBaricenter -k, --keeptextures (Default: false) Keeps original textures --lat Latitude of the mesh @@ -72,6 +73,11 @@ If you want to preserve the original textures, use the `--keeptextures` flag (no You can control how many times the split is performed by using the `--divisions` flag. The model will be split into `divisions^2` meshes (or `divisions^3` if `--zsplit` is used). +The `--split-strategy` flag controls how the split point is determined at each recursive step: + +- **`VertexBaricenter`** (default): the split point is the barycenter of the vertices of the current sub-mesh. This produces tiles that are more balanced in terms of vertex/face count, because the cut plane adapts to where the geometry is actually concentrated. +- **`AbsoluteCenter`**: the split point is the geometric center of the bounding box. This produces a spatially uniform grid, which is more predictable and reproducible but can result in uneven tiles when the geometry is not uniformly distributed. + ### 3D Tiles conversion Each split mesh is converted to B3DM format using [ObjConvert](https://github.com/SilentWave/ObjConvert).