Repository: zeux/meshoptimizer (language: cpp)

Issue:
Simplification should recognize and handle UV mirroring seams
Rare case but on some models, simplification lead to UV artefacts : UVs are getting fully streched (same value on both vertices) when many other triangles could have been optimized instead, I suggest to give a lower priority to simplify/remove vertices/faces of the mesh leading to these results.

It might also be a bug in the way the uvs get re-assigned.

The model used to illustrate this example : 
https://www.dropbox.com/scl/fi/po761ykdiqdmtbzd9fitf/rhino.glb?rlkey=s6jp06vtr4id0opnv76wwys1p&dl=0

Before simplify : 
<img width="527" alt="Screenshot 2024-07-08 at 16 25 05" src="https://github.com/zeux/meshoptimizer/assets/213351/080e3431-bef9-4f61-811d-c5dbe638cf40">

After simplify :
<img width="536" alt="Screenshot 2024-07-08 at 16 14 54" src="https://github.com/zeux/meshoptimizer/assets/213351/2da68c85-190d-441b-a59b-3789170bed2b">


Discussion / hints:
Are UVs mirrored here? If so then yeah it’s a known issue atm, and will get addressed eventually.
Yeah I looked at the scene closer and it's indeed UV mirroring. This needs special handling in the simplifier, probably as a separate pre-process.

Re-attaching the file so that this isn't lost in the future (I also regenerated the normals here because they were just broken in the input file):
[rhino.zip](https://github.com/user-attachments/files/16131681/rhino.zip)
[rhino-tbn.zip](https://github.com/user-attachments/files/16132753/rhino-tbn.zip)

Worth noting is that if the GLB file gets re-exported with tangent data and a normal map (without a normal map gltfpack needs `-kv` flag), the simplifier will automatically recognize the mirroring seam and simplify correctly (of course, this ideally should work without tangent information present to begin with):
![image](https://github.com/zeux/meshoptimizer/assets/1106629/eb73bcc6-f4df-42e0-8cb7-7c3385fc6e16)


Fix patch:
diff --git a/gltf/gltfpack.cpp b/gltf/gltfpack.cpp
index a3ec5a60a..a61f057c9 100644
--- a/gltf/gltfpack.cpp
+++ b/gltf/gltfpack.cpp
@@ -409,6 +409,9 @@ static void process(cgltf_data* data, const char* input_path, const char* output
 	{
 		const Mesh& mesh = meshes[i];
 
+		if (mesh.type != cgltf_primitive_type_triangles)
+			continue;
+
 		if (settings.simplify_debug > 0)
 		{
 			Mesh kinds = {};
diff --git a/gltf/mesh.cpp b/gltf/mesh.cpp
index ae44322f3..c691b48b7 100644
--- a/gltf/mesh.cpp
+++ b/gltf/mesh.cpp
@@ -762,6 +762,78 @@ static void simplifyAttributes(std::vector<float>& attrs, float* attrw, size_t s
 	}
 }
 
+static void simplifyUvSplit(Mesh& mesh, std::vector<unsigned int>& remap)
+{
+	assert(mesh.type == cgltf_primitive_type_triangles);
+	assert(!mesh.indices.empty());
+
+	const Stream* uv = getStream(mesh, cgltf_attribute_type_texcoord);
+	if (!uv)
+		return;
+
+	size_t vertex_count = uv->data.size();
+
+	std::vector<unsigned char> uvsign(mesh.indices.size() / 3);
+	std::vector<unsigned char> flipseam(vertex_count);
+
+	for (size_t i = 0; i < mesh.indices.size(); i += 3)
+	{
+		unsigned int a = mesh.indices[i + 0];
+		unsigned int b = mesh.indices[i + 1];
+		unsigned int c = mesh.indices[i + 2];
+
+		const Attr& va = uv->data[a];
+		const Attr& vb = uv->data[b];
+		const Attr& vc = uv->data[c];
+
+		float uvarea = (vb.f[0] - va.f[0]) * (vc.f[1] - va.f[1]) - (vc.f[0] - va.f[0]) * (vb.f[1] - va.f[1]);
+		unsigned char flag = uvarea > 0 ? 1 : (uvarea < 0 ? 2 : 0);
+
+		uvsign[i / 3] = flag;
+		flipseam[a] |= flag;
+		flipseam[b] |= flag;
+		flipseam[c] |= flag;
+	}
+
+	std::vector<unsigned int> split(vertex_count);
+	size_t splits = 0;
+
+	remap.resize(vertex_count);
+
+	for (size_t i = 0; i < vertex_count; ++i)
+	{
+		remap[i] = unsigned(i);
+
+		if (flipseam[i] == 3)
+		{
+			assert(remap.size() == vertex_count + splits);
+			remap.push_back(unsigned(i));
+
+			split[i] = unsigned(vertex_count + splits);
+			splits++;
+
+			for (size_t k = 0; k < mesh.streams.size(); ++k)
+				mesh.streams[k].data.push_back(mesh.streams[k].data[i]);
+		}
+	}
+
+	for (size_t i = 0; i < mesh.indices.size(); i += 3)
+	{
+		unsigned int a = mesh.indices[i + 0];
+		unsigned int b = mesh.indices[i + 1];
+		unsigned int c = mesh.indices[i + 2];
+
+		unsigned char sign = uvsign[i / 3];
+
+		if (flipseam[a] == 3 && sign == 2)
+			mesh.indices[i + 0] = split[a];
+		if (flipseam[b] == 3 && sign == 2)
+			mesh.indices[i + 1] = split[b];
+		if (flipseam[c] == 3 && sign == 2)
+			mesh.indices[i + 2] = split[c];
+	}
+}
+
 static void simplifyMesh(Mesh& mesh, float threshold, float error, bool attributes, bool aggressive, bool lock_borders, bool debug = false)
 {
 	enum
@@ -778,6 +850,10 @@ static void simplifyMesh(Mesh& mesh, float threshold, float error, bool attribut
 	if (!positions)
 		return;
 
+	std::vector<unsigned int> uvremap;
+	if (attributes)
+		simplifyUvSplit(mesh, uvremap);
+
 	size_t vertex_count = mesh.streams[0].data.size();
 
 	size_t target_index_count = size_t(double(size_t(mesh.indices.size() / 3)) * threshold) * 3;
@@ -819,6 +895,9 @@ static void simplifyMesh(Mesh& mesh, float threshold, float error, bool attribut
 		indices.resize(meshopt_simplifySloppy(&indices[0], &mesh.indices[0], mesh.indices.size(), positions->data[0].f, vertex_count, sizeof(Attr), target_index_count, target_error_aggressive));
 		mesh.indices.swap(indices);
 	}
+
+	if (uvremap.size() && mesh.indices.size() && !debug)
+		meshopt_remapIndexBuffer(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), &uvremap[0]);
 }
 
 static void optimizeMesh(Mesh& mesh, bool compressmore)
@@ -1048,8 +1127,6 @@ void debugSimplify(const Mesh& source, Mesh& kinds, Mesh& loops, float ratio, fl
 	reindexMesh(mesh, quantize_tbn);
 	filterTriangles(mesh);
 
-	size_t vertex_count = mesh.streams[0].data.size();
-
 	simplifyMesh(mesh, ratio, error, attributes, /* aggressive= */ false, /* lock_borders= */ false, /* debug= */ true);
 
 	// color palette for display
@@ -1079,6 +1156,8 @@ void debugSimplify(const Mesh& source, Mesh& kinds, Mesh& loops, float ratio, fl
 		}
 	}
 
+	size_t vertex_count = mesh.streams[0].data.size();
+
 	// transform kind/loop data into lines & points
 	Stream colors = {cgltf_attribute_type_color};
 	colors.data.resize(vertex_count, kPalette[0]);


Test patch: