Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ccef851
Add explanation of certain DecimationStage options - useful for furth…
pragmaware Jul 5, 2026
369ab24
Add decimation-mode option.
pragmaware Jul 5, 2026
4214e2a
Optimize *.obj writing - share vertices, normals etc..
pragmaware Jul 5, 2026
e860bbf
Better parameters for UV-seams / Uv-foldover preservation
pragmaware Jul 8, 2026
02ee3a3
Speed-up texture packing
pragmaware Jul 9, 2026
7345734
Add estimation of geometric error
pragmaware Jul 18, 2026
e822765
More work on normal map support
pragmaware Jul 19, 2026
908681e
Support normal map materials
pragmaware Jul 22, 2026
f4b8906
Improve commandline options. Add support for presets for quickly swit…
pragmaware Jul 22, 2026
cce3357
Add support for saving *.glb files instead of *.b3dm
pragmaware Jul 23, 2026
8bf9a84
Add statistics about decimatable edges and triangles
pragmaware Jul 23, 2026
e1a9374
Set jpeg quality for the various LODs
pragmaware Jul 23, 2026
cce018e
Fix comparison in Vertex3. Also use a meaningful comparison threshold…
pragmaware Jul 23, 2026
428d0e2
Improve geometric error 'estimation'. Add a factor related to texture…
pragmaware Jul 25, 2026
3f22a02
Add optional overlap to tiles plus a 'nudge' to avoid z-fighting
pragmaware Jul 25, 2026
a77538d
Allow forcing unlit materials and ignoring normal maps
pragmaware Jul 26, 2026
820b85a
Correct a not-so-rare UV mapping error related to precision errors in…
pragmaware Jul 29, 2026
995a091
Merge remote-tracking branch 'origin/master' into personal/szymon/202…
pragmaware Jul 29, 2026
e94a60d
Add the possiblity of specifying the finest LOD texture quality as a …
pragmaware Jul 29, 2026
3d90b30
Add the possiblity of specifying the finest LOD texture quality as a …
pragmaware Jul 29, 2026
3da7cb8
Set larger texture size for standard preset
pragmaware Jul 29, 2026
4136164
Fixed comparison logic
HeDo88TH Jul 30, 2026
71597d9
Housekeeping
HeDo88TH Jul 30, 2026
baf45f2
Documentation fixes
HeDo88TH Jul 30, 2026
5f0cf0e
Merge remote-tracking branch 'origin/master' into personal/szymon/202…
HeDo88TH Jul 30, 2026
df0c04a
Apply texture size limit also to the normal maps
pragmaware Aug 5, 2026
db9212a
Honor --ignore-normal-maps also in the root content generation
pragmaware Aug 5, 2026
f7da481
Add better explanation of the --divisions parameter and a README sect…
pragmaware Aug 5, 2026
f0098e2
Better estimate the root content geometric error
pragmaware Aug 17, 2026
26a97b0
Yet better error estimation for the root content
pragmaware Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ riderModule.iml
test_output/
test-output/
test-output2/
publish/
publish/
*.DotSettings.user
72 changes: 72 additions & 0 deletions MeshDecimatorCore/Algorithms/FastQuadricMeshSimplification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,9 @@ private void UpdateMesh(int iteration)
UpdateReferences();
}

if (Verbose)
LogDecimationStatistics(triangles, vertices, triangleCount);

// Init Quadrics by Plane & Edge Errors
//
// required at the beginning ( iteration == 0 )
Expand Down Expand Up @@ -961,6 +964,75 @@ private void UpdateMesh(int iteration)
}
}
}

/// <summary>
/// Logs how many of the mesh's edges and triangles are eligible for collapse/removal under
/// the current PreserveBorderEdges/PreserveUVSeamEdges/PreserveUVFoldoverEdges options,
/// mirroring the exact lock conditions applied per-edge in RemoveVertexPass. Counts half-edges
/// (one per triangle corner, matching how RemoveVertexPass itself walks edges) rather than
/// deduping into unique undirected edges, so interior edges count twice and border edges once
/// - this avoids a second full-mesh pass through a hash set, which is too costly on large
/// meshes. A triangle is "decimatable" if at least one of its 3 edges isn't locked (i.e. it
/// could disappear via some future collapse); "locked" means all 3 edges are locked, so the
/// triangle can never be removed as things stand.
/// </summary>
private void LogDecimationStatistics(Triangle[] triangles, Vertex[] vertices, int triangleCount)
{
bool preserveBorderEdges = Options.PreserveBorderEdges;
bool preserveUVSeamEdges = Options.PreserveUVSeamEdges;
bool preserveUVFoldoverEdges = Options.PreserveUVFoldoverEdges;

int totalEdges = 0, borderEdges = 0, seamEdges = 0, foldoverEdges = 0, decimatableEdges = 0;
int totalTriangles = 0, decimatableTriangles = 0;

for (int tid = 0; tid < triangleCount; tid++)
{
if (triangles[tid].deleted)
continue;

++totalTriangles;
bool triangleDecimatable = false;

for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++)
{
int i0 = triangles[tid][edgeIndex];
int i1 = triangles[tid][(edgeIndex + 1) % 3];

bool bothBorder = vertices[i0].border && vertices[i1].border;
bool bothSeam = vertices[i0].seam && vertices[i1].seam;
bool bothFoldover = vertices[i0].foldover && vertices[i1].foldover;
bool mismatched = vertices[i0].border != vertices[i1].border ||
vertices[i0].seam != vertices[i1].seam ||
vertices[i0].foldover != vertices[i1].foldover;

++totalEdges;
if (bothBorder) ++borderEdges;
if (bothSeam) ++seamEdges;
if (bothFoldover) ++foldoverEdges;

bool locked = mismatched ||
(preserveBorderEdges && bothBorder) ||
(preserveUVSeamEdges && bothSeam) ||
(preserveUVFoldoverEdges && bothFoldover);

if (!locked)
{
++decimatableEdges;
triangleDecimatable = true;
}
}

if (triangleDecimatable) ++decimatableTriangles;
}

Logging.LogVerbose(
" ?> Edges (half-edges): {0} total, {1} border, {2} uv-seam, {3} uv-foldover, {4} decimatable, {5} locked",
totalEdges, borderEdges, seamEdges, foldoverEdges, decimatableEdges, totalEdges - decimatableEdges);

Logging.LogVerbose(
" ?> Triangles: {0} total, {1} decimatable, {2} locked",
totalTriangles, decimatableTriangles, totalTriangles - decimatableTriangles);
}
#endregion

#region Update References
Expand Down
4 changes: 3 additions & 1 deletion MeshDecimatorCore/Math/Vector2d.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public struct Vector2d : IEquatable<Vector2d>
/// <summary>
/// The vector epsilon.
/// </summary>
public const double Epsilon = double.Epsilon;
// double.Epsilon is the smallest representable positive double (~4.9e-324), not a usable
// comparison tolerance - this is the double-precision machine epsilon (C/C++ DBL_EPSILON).
public const double Epsilon = 2.2204460492503131E-16;
#endregion

#region Fields
Expand Down
4 changes: 3 additions & 1 deletion MeshDecimatorCore/Math/Vector3d.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public struct Vector3d : IEquatable<Vector3d>
/// <summary>
/// The vector epsilon.
/// </summary>
public const double Epsilon = double.Epsilon;
// double.Epsilon is the smallest representable positive double (~4.9e-324), not a usable
// comparison tolerance - this is the double-precision machine epsilon (C/C++ DBL_EPSILON).
public const double Epsilon = 2.2204460492503131E-16;
#endregion

#region Fields
Expand Down
4 changes: 3 additions & 1 deletion MeshDecimatorCore/Math/Vector4d.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public struct Vector4d : IEquatable<Vector4d>
/// <summary>
/// The vector epsilon.
/// </summary>
public const double Epsilon = double.Epsilon;
// double.Epsilon is the smallest representable positive double (~4.9e-324), not a usable
// comparison tolerance - this is the double-precision machine epsilon (C/C++ DBL_EPSILON).
public const double Epsilon = 2.2204460492503131E-16;
#endregion

#region Fields
Expand Down
18 changes: 14 additions & 4 deletions MeshDecimatorCore/SimplificationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public struct SimplificationOptions
PreserveUVFoldoverEdges = false,
PreserveSurfaceCurvature = false,
EnableSmartLink = true,
VertexLinkDistance = double.Epsilon,
// double.Epsilon is the smallest representable positive double (~4.9e-324), not a usable
// welding tolerance - this is the double-precision machine epsilon (C/C++ DBL_EPSILON).
VertexLinkDistance = 2.2204460492503131E-16,
MaxIterationCount = 100,
Aggressiveness = 7.0
};
Expand All @@ -28,14 +30,22 @@ public struct SimplificationOptions
public bool PreserveBorderEdges;

/// <summary>
/// If enabled, UV seam edges will not be collapsed,
/// preventing texture discontinuity artifacts.
/// If enabled, UV seam edges will not be collapsed, preventing texture
/// discontinuity artifacts.
/// A UV seam edge is a border edge that is duplicated in two distinct
/// triangles with different UV coordinates (cut for texturing purposes).
/// This is relevant only if EnableSmartLink is set to true.
/// If EnableSmartLink is set to false the UV seam edges are always treated as borders.
/// Default value: false
/// </summary>
public bool PreserveUVSeamEdges;

/// <summary>
/// If enabled, UV foldover edges will not be collapsed.
/// A UV foldover edge is a border edge that is duplicated in two distinct
/// triangles with same UV coordinates (likely normal-related vertex duplication).
/// This is relevant only if EnableSmartLink is set to true.
/// If EnableSmartLink is set to false the UV foldover edges are always treated as borders.
/// Default value: false
/// </summary>
public bool PreserveUVFoldoverEdges;
Expand All @@ -58,7 +68,7 @@ public struct SimplificationOptions
/// <summary>
/// The maximum distance between two vertices to be linked together
/// when smart linking is enabled.
/// Default value: double.Epsilon
/// Default value: double-precision machine epsilon (2.2204460492503131E-16)
/// </summary>
public double VertexLinkDistance;

Expand Down
42 changes: 29 additions & 13 deletions Obj2Gltf/Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,19 @@ private void Convert(ObjModel objModel, string outputFile, GltfConverterOptions
using (var bufferState = new BufferState(gltfModel, outputFile, u32IndicesEnabled))
{
gltfModel.Scenes.Add(new Scene());
gltfModel.Materials.AddRange(objModel.Materials.Select(x => ConvertMaterial(x, t => GetTextureIndex(gltfModel, t))));
gltfModel.Materials.AddRange(objModel.Materials.Select(x =>
ConvertMaterial(x, t => GetTextureIndex(gltfModel, t), options.UnlitMaterials)));

if (options.UnlitMaterials)
gltfModel.UseExtension("KHR_materials_unlit", required: false);

var meshes = objModel.Geometries.ToArray();
var meshesLength = meshes.Length;
for (var i = 0; i < meshesLength; i++)
{
var mesh = meshes[i];
if (!mesh.Faces.Any()) continue;
var meshIndex = AddMesh(gltfModel, objModel, bufferState, mesh);
var meshIndex = AddMesh(gltfModel, objModel, bufferState, mesh, options.UnlitMaterials);
AddNode(gltfModel, mesh.Id, meshIndex, null);
}
}
Expand Down Expand Up @@ -194,7 +198,7 @@ private int AddTexture(GltfModel gltfModel, string textureFilename)



private Gltf.Material GetDefault(string name = "default", AlphaMode mode = AlphaMode.OPAQUE)
private Gltf.Material GetDefault(string name = "default", AlphaMode mode = AlphaMode.OPAQUE, bool unlit = false)
{
return new Gltf.Material
{
Expand All @@ -206,7 +210,10 @@ private Gltf.Material GetDefault(string name = "default", AlphaMode mode = Alpha
BaseColorFactor = new double[] { 0.5, 0.5, 0.5, 1 },
MetallicFactor = 1.0,
RoughnessFactor = 0.0
}
},
Extensions = unlit
? new Dictionary<string, object> { ["KHR_materials_unlit"] = new Dictionary<string, object>() }
: null
};
}

Expand Down Expand Up @@ -272,7 +279,8 @@ int GetTextureIndex(GltfModel gltfModel, string path)
return AddTexture(gltfModel, path);
}

public static Gltf.Material ConvertMaterial(WaveFront.Material mat, GetOrAddTexture getOrAddTextureFunction)
public static Gltf.Material ConvertMaterial(WaveFront.Material mat, GetOrAddTexture getOrAddTextureFunction,
bool unlit = false)
{
var roughnessFactor = ConvertTraditional2MetallicRoughness(mat);

Expand All @@ -282,6 +290,9 @@ public static Gltf.Material ConvertMaterial(WaveFront.Material mat, GetOrAddText
AlphaMode = AlphaMode.OPAQUE
};

if (unlit)
gMat.Extensions = new Dictionary<string, object> { ["KHR_materials_unlit"] = new Dictionary<string, object>() };

var alpha = mat.GetAlpha();
var metallicFactor = 0.0;
if (mat.Specular != null && mat.Specular.Color != null)
Expand Down Expand Up @@ -360,9 +371,9 @@ private int GetMaterialIndex(GltfModel gltfModel, string matName)

#region Meshes

private int AddMesh(GltfModel gltfModel, ObjModel objModel, BufferState buffer, Geometry mesh)
private int AddMesh(GltfModel gltfModel, ObjModel objModel, BufferState buffer, Geometry mesh, bool unlit = false)
{
var ps = AddVertexAttributes(gltfModel, objModel, buffer, mesh);
var ps = AddVertexAttributes(gltfModel, objModel, buffer, mesh, unlit);

var m = new Mesh
{
Expand All @@ -377,7 +388,8 @@ private int AddMesh(GltfModel gltfModel, ObjModel objModel, BufferState buffer,
private List<Primitive> AddVertexAttributes(GltfModel gltfModel,
ObjModel objModel,
BufferState bufferState,
Geometry mesh)
Geometry mesh,
bool unlit = false)
{
var facesGroup = mesh.Faces.GroupBy(c => c.MatName);
var faces = new List<Face>();
Expand Down Expand Up @@ -411,15 +423,15 @@ private List<Primitive> AddVertexAttributes(GltfModel gltfModel,
var hasNormals = f.Triangles.Any(d => d.V1.N > 0);
var hasColors = objModel.Colors.Count == objModel.Vertices.Count;

var materialIndex = GetMaterialIndexOrDefault(gltfModel, objModel, f.MatName);
var materialIndex = GetMaterialIndexOrDefault(gltfModel, objModel, f.MatName, unlit);
// Fix Issue #36: look up the OBJ material by name instead of relying
// on the gltfModel index, which can diverge from objModel.Materials
// when the default material is inserted at index 0.
var material = !string.IsNullOrEmpty(f.MatName)
? objModel.Materials.FirstOrDefault(m => m.Name == f.MatName)
?? (materialIndex < objModel.Materials.Count ? objModel.Materials[materialIndex] : null)
: objModel.Materials.FirstOrDefault();
var materialHasTexture = material?.DiffuseTextureFile != null;
var materialHasTexture = material?.DiffuseTextureFile != null || material?.NormalTextureFile != null;

// every primitive needs their own vertex indices(v,t,n)
var faceVertexCache = new Dictionary<string, int>();
Expand Down Expand Up @@ -450,6 +462,10 @@ private List<Primitive> AddVertexAttributes(GltfModel gltfModel,
{
gMat.PbrMetallicRoughness.BaseColorTexture = null;
}
if (gMat.normalTexture != null)
{
gMat.normalTexture = null;
}
}
}

Expand Down Expand Up @@ -615,7 +631,7 @@ private List<Primitive> AddVertexAttributes(GltfModel gltfModel,
return ps;
}

private int GetMaterialIndexOrDefault(GltfModel gltfModel, ObjModel objModel, string materialName)
private int GetMaterialIndexOrDefault(GltfModel gltfModel, ObjModel objModel, string materialName, bool unlit = false)
{
if (string.IsNullOrEmpty(materialName)) materialName = "default";

Expand All @@ -629,7 +645,7 @@ private int GetMaterialIndexOrDefault(GltfModel gltfModel, ObjModel objModel, st
materialIndex = GetMaterialIndex(gltfModel, materialName);
if (materialIndex == -1)
{
var gMat = GetDefault();
var gMat = GetDefault(unlit: unlit);
materialIndex = AddMaterial(gltfModel, gMat);
}
else
Expand All @@ -641,7 +657,7 @@ private int GetMaterialIndexOrDefault(GltfModel gltfModel, ObjModel objModel, st
}
else
{
var gMat = ConvertMaterial(objMaterial, t => GetTextureIndex(gltfModel, t));
var gMat = ConvertMaterial(objMaterial, t => GetTextureIndex(gltfModel, t), unlit);
materialIndex = AddMaterial(gltfModel, gMat);
}
}
Expand Down
7 changes: 7 additions & 0 deletions Obj2Gltf/Gltf/Material.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

Expand Down Expand Up @@ -67,6 +68,12 @@ public class Material
[JsonProperty("normalTexture")]
public TextureReferenceInfo normalTexture { get; set; }

/// <summary>
/// glTF extensions attached to this material (e.g. KHR_materials_unlit), keyed by extension name.
/// </summary>
[JsonProperty("extensions", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, object> Extensions { get; set; }

public override string ToString()
=> $"AM:{AlphaMode} DS:{(DoubleSided ? 1 : 0)} MRB:[{PbrMetallicRoughness.BaseColorFactor[0]}, {PbrMetallicRoughness.BaseColorFactor[1]}, {PbrMetallicRoughness.BaseColorFactor[2]}, {PbrMetallicRoughness.BaseColorFactor[3]}] E:[{EmissiveFactor[0]}, {EmissiveFactor[1]}, {EmissiveFactor[2]}] M:{PbrMetallicRoughness.MetallicFactor} R:{PbrMetallicRoughness.RoughnessFactor} T:{PbrMetallicRoughness.BaseColorTexture?.Index.ToString() ?? "<null>"} {Name}";
}
Expand Down
7 changes: 7 additions & 0 deletions Obj2Gltf/GltfConverterOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ public class GltfConverterOptions
/// </summary>
public bool DeleteOriginals { get; set; } = false;

/// <summary>
/// Marks every output material with the KHR_materials_unlit extension, so viewers render
/// the base color texture as-is without applying PBR lighting. Useful for photogrammetry
/// content where lighting is already baked into the textures. Default is false.
/// </summary>
public bool UnlitMaterials { get; set; } = false;

/// <summary>
/// When true, every referenced raster texture is re-encoded to KTX2 (Basis Universal) and the
/// textures are rewritten to use the KHR_texture_basisu extension. Requires the libktx native
Expand Down
18 changes: 18 additions & 0 deletions Obj2Gltf/WaveFront/MtlParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class MtlParser : IMtlParser
private const string map_kaPrefix = "map_Ka";
private const string map_KdPrefix = "map_Kd";
private const string normPrefix = "norm";
private const string bumpPrefix = "bump";
private const string map_BumpPrefix = "map_Bump";

private static Reflectivity GetReflectivity(string val)
{
Expand Down Expand Up @@ -204,6 +206,22 @@ public IEnumerable<Material> Parse(Stream stream, string searchPath, Encoding en
currentMaterial.NormalTextureFile = mn;
}
}
else if (line.StartsWith(map_BumpPrefix, StringComparison.OrdinalIgnoreCase))
{
var mn = line.Substring(map_BumpPrefix.Length).Trim();
if (File.Exists(Path.Combine(searchPath, mn)))
{
currentMaterial.NormalTextureFile = mn;
}
}
else if (line.StartsWith(bumpPrefix, StringComparison.OrdinalIgnoreCase))
{
var mn = line.Substring(bumpPrefix.Length).Trim();
if (File.Exists(Path.Combine(searchPath, mn)))
{
currentMaterial.NormalTextureFile = mn;
}
}
}
if (currentMaterial != null) yield return currentMaterial;
}
Expand Down
Loading
Loading