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..b354743 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,24 +661,18 @@ private void RemoveVertexPass(int startTrisCount, int targetTrisCount, double th
continue;
int ia0 = attributeIndexArr[edgeIndex];
+ int ia1 = attributeIndexArr[nextEdgeIndex];
+
+ // 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 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 (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);
- }
-
if (vertices[i0].seam)
{
ia0 = -1;
@@ -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.Aggressiveness);
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..0515ad4
--- /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,
+ Aggressiveness = 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 Aggressiveness;
+ }
+}
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)");
+ }
+ }
+}
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);
diff --git a/Obj2Tiles/Stages/DecimationStage.cs b/Obj2Tiles/Stages/DecimationStage.cs
index 541b3f3..eefe233 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,
+ Aggressiveness = 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)",
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)
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).
]