From d8b2033de82e51090e546780df7e9f6b90d93477 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Sun, 18 Aug 2024 16:01:52 -0700 Subject: [PATCH 1/8] js: "Expose" InternalDebug option to simplifier This is a very undocumented option that will generate visualization data without any backwards compatibility promises; this is incompatible with modes like Sparse, 16-bit indices, and may have other limitations. This is only added for the benefit of demo visualization. Please do not use it. --- js/meshopt_simplifier.js | 1 + js/meshopt_simplifier.module.js | 1 + 2 files changed, 2 insertions(+) diff --git a/js/meshopt_simplifier.js b/js/meshopt_simplifier.js index e1c7abce..7e2cce06 100644 --- a/js/meshopt_simplifier.js +++ b/js/meshopt_simplifier.js @@ -197,6 +197,7 @@ var MeshoptSimplifier = (function () { LockBorder: 1, Sparse: 2, ErrorAbsolute: 4, + _InternalDebug: 1 << 30, }; return { diff --git a/js/meshopt_simplifier.module.js b/js/meshopt_simplifier.module.js index 95e8ed25..9337ccd1 100644 --- a/js/meshopt_simplifier.module.js +++ b/js/meshopt_simplifier.module.js @@ -196,6 +196,7 @@ var MeshoptSimplifier = (function () { LockBorder: 1, Sparse: 2, ErrorAbsolute: 4, + _InternalDebug: 1 << 30, }; return { From 52f27387c95b0a11c9a47278d2f40095e880b87e Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Sun, 18 Aug 2024 16:05:38 -0700 Subject: [PATCH 2/8] demo: Adjust simplification to create new BufferGeometry objects This is going to be necessary if we were to implement optional preprocessing steps like welding as well as debug visualization; we will try to limit the modifications to index data but that may not always be possible. --- demo/simplify.html | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/demo/simplify.html b/demo/simplify.html index 6f9ccd03..07e3334c 100644 --- a/demo/simplify.html +++ b/demo/simplify.html @@ -119,14 +119,10 @@ animate(); function simplifyMesh(geo) { - if (!geo.index.original) { - geo.index.original = geo.index.array; - } - var attributes = 6; // 3 color, 3 normal var positions = new Float32Array(geo.attributes.position.array); - var indices = geo.index.original; + var indices = geo.index.array; var target = Math.floor((indices.length * settings.ratio) / 3) * 3; if (settings.useAttributes) { @@ -185,11 +181,14 @@ console.log('simplified to', res[0].length / 3, 'with error', res[1]); - geo.index.array = res[0]; - geo.index.count = res[0].length; - geo.index.needsUpdate = true; + var dind = Array.from(res[0]); - return res[1]; + var rgeo = geo.clone(); + rgeo.index.array = new Uint32Array(dind); + rgeo.index.count = dind.length; + rgeo.index.needsUpdate = true; + + return [rgeo, res[1]]; } function simplifyPoints(geo) { @@ -232,7 +231,13 @@ scene.traverse(function (object) { if (object.isMesh) { - var err = simplifyMesh(object.geometry); + if (!object.original) { + object.original = object.geometry.clone(); + } + + var [geo, err] = simplifyMesh(object.original); + + object.geometry = geo; error = Math.max(error, err); // note: we are ignoring the possibility of different mesh scales atm triangles += object.geometry.index.count / 3; From 03496b28eab9876a355487149623d1735892d633 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Sun, 18 Aug 2024 16:32:00 -0700 Subject: [PATCH 3/8] demo: Implement simplification debug overlay To get data for debug overlay, we run simplification process again and output loop/kind metadata. To minimize the changes to the rendering code, we parse the index buffer and generate extra triangles (for now using the same color) for any loop edge, and use three's material groups to render that with a separate material. Conveniently, even if the original mesh has vertex colors, these get ignored by MeshBasicMaterial so this works properly, and is compatible with skinning because we use wireframe on degenerate triangles (three doesn't support skinned lines otherwise). This is a little crude and will be refined in the future but gets the job done. --- demo/simplify.html | 73 +++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/demo/simplify.html b/demo/simplify.html index 07e3334c..610d8316 100644 --- a/demo/simplify.html +++ b/demo/simplify.html @@ -62,6 +62,7 @@ wireframe: false, pointSize: 1.0, ratio: 1.0, + debugOverlay: false, lockBorder: false, useAttributes: false, errorThresholdLog10: 1, @@ -96,6 +97,7 @@ var guiDisplay = gui.addFolder('Display'); guiDisplay.add(settings, 'wireframe').onChange(update); guiDisplay.add(settings, 'pointSize', 1, 16).onChange(update); + guiDisplay.add(settings, 'debugOverlay').onChange(simplify); // requires debug data rebuild var guiSimplify = gui.addFolder('Simplify'); guiSimplify.add(settings, 'ratio', 0, 1, 0.01).onChange(simplify); @@ -122,7 +124,7 @@ var attributes = 6; // 3 color, 3 normal var positions = new Float32Array(geo.attributes.position.array); - var indices = geo.index.array; + var indices = new Uint32Array(geo.index.array); // needed for _InternalDebug to work var target = Math.floor((indices.length * settings.ratio) / 3) * 3; if (settings.useAttributes) { @@ -162,33 +164,57 @@ console.time('simplify'); - var res = settings.useAttributes - ? MeshoptSimplifier.simplifyWithAttributes( - indices, - positions, - stride, - attrib, - attributes, - attrib_weights, - null, - target, - threshold, - flags, - ) - : MeshoptSimplifier.simplify(indices, positions, stride, target, threshold, flags); + function run() { + var S = MeshoptSimplifier; // to avoid line breaks below... + return settings.useAttributes + ? S.simplifyWithAttributes(indices, positions, stride, attrib, attributes, attrib_weights, null, target, threshold, flags) + : MeshoptSimplifier.simplify(indices, positions, stride, target, threshold, flags); + } + + var res = run(); + + if (settings.debugOverlay) { + flags.push('_InternalDebug'); + var dres = run(); + } console.timeEnd('simplify'); console.log('simplified to', res[0].length / 3, 'with error', res[1]); - var dind = Array.from(res[0]); - var rgeo = geo.clone(); + + var dind = res[0]; + + if (settings.debugOverlay) { + // we need extra indices for debug overlay + dind = Array.from(res[0]); + + for (var i = 0; i < dres[0].length; i += 3) { + var mask = (1 << 28) - 1; + + for (var e = 0; e < 3; ++e) { + var a = dres[0][i + e], + b = dres[0][i + ((e + 1) % 3)]; + + if (a >> 31 != 0) { + dind.push(a & mask); + dind.push(a & mask); + dind.push(b & mask); + } + } + } + + rgeo.addGroup(res[0].length, dind.length - res[0].length, 1); + } + rgeo.index.array = new Uint32Array(dind); rgeo.index.count = dind.length; rgeo.index.needsUpdate = true; - return [rgeo, res[1]]; + rgeo.addGroup(0, res[0].length, 0); + + return [rgeo, res[0].length / 3, res[1]]; } function simplifyPoints(geo) { @@ -233,14 +259,15 @@ if (object.isMesh) { if (!object.original) { object.original = object.geometry.clone(); + object.material = [object.material, new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })]; } - var [geo, err] = simplifyMesh(object.original); + var [geo, tri, err] = simplifyMesh(object.original); object.geometry = geo; error = Math.max(error, err); // note: we are ignoring the possibility of different mesh scales atm - triangles += object.geometry.index.count / 3; + triangles += tri; vertices += object.geometry.attributes.position.count; } if (object.isPoints) { @@ -270,7 +297,11 @@ function update() { scene.traverse(function (child) { if (child.isMesh) { - child.material.wireframe = settings.wireframe; + if (Array.isArray(child.material)) { + child.material[0].wireframe = settings.wireframe; + } else { + child.material.wireframe = settings.wireframe; + } } if (child.isPoints) { child.material.size = settings.pointSize; From 5a8a52f6dc2b9a1d70110b83d06d80475a34f34a Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Sun, 18 Aug 2024 17:11:47 -0700 Subject: [PATCH 4/8] demo: Implement auto-update for simplification module Instead of having to manually click "update module" every time the module changes, we now support auto update checkbox that polls the file every second and reloads it when the modification date has changed. This, combined with running 'make' in a shell in a loop, results in a nice hot reload support without too much work. --- demo/simplify.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/demo/simplify.html b/demo/simplify.html index 610d8316..1426412e 100644 --- a/demo/simplify.html +++ b/demo/simplify.html @@ -91,6 +91,7 @@ reload(); simplify(); }, + autoUpdate: false, }; var gui = new GUI({ width: 300 }); @@ -110,6 +111,7 @@ var guiLoad = gui.addFolder('Load'); guiLoad.add(settings, 'loadFile'); guiLoad.add(settings, 'updateModule'); + guiLoad.add(settings, 'autoUpdate').onChange(autoReload); var guiStats = gui.addFolder('Stats'); guiStats.add(settings.stats, 'triangles').listen(); @@ -294,6 +296,24 @@ }); } + var moduleLastModified = 0; + + function autoReload() { + if (!settings.autoUpdate) return; + + var simph = fetch('/js/meshopt_simplifier.module.js?x=' + Date.now(), { method: 'HEAD' }); + simph.then(function (r) { + var last = r.headers.get('Last-Modified'); + if (last != moduleLastModified) { + moduleLastModified = last; + reload(); + simplify(); + } + + setTimeout(autoReload, 1000); + }); + } + function update() { scene.traverse(function (child) { if (child.isMesh) { From 408adb39f74846cacba91be6ef385f7d19e9c12b Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Sun, 18 Aug 2024 17:43:39 -0700 Subject: [PATCH 5/8] demo: Expand debug visualization to show all classifiers Since we have issues with rendering points using our current method, we just render edges: either when they are marked as loops in the metadata (which is the case for seam/border), or if both vertices are locked. When only one vertex is locked, this can sometimes get classified as a loop (in which case we render it) but in general there is some ambiguity for edges adjacent to locked vertices, and isolated locked vertices may not get rendered as such. --- demo/simplify.html | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/demo/simplify.html b/demo/simplify.html index 1426412e..084bfe9b 100644 --- a/demo/simplify.html +++ b/demo/simplify.html @@ -182,8 +182,6 @@ console.timeEnd('simplify'); - console.log('simplified to', res[0].length / 3, 'with error', res[1]); - var rgeo = geo.clone(); var dind = res[0]; @@ -192,22 +190,35 @@ // we need extra indices for debug overlay dind = Array.from(res[0]); - for (var i = 0; i < dres[0].length; i += 3) { - var mask = (1 << 28) - 1; + for (var kind = 1; kind <= 4; ++kind) { + var offset = dind.length; - for (var e = 0; e < 3; ++e) { - var a = dres[0][i + e], - b = dres[0][i + ((e + 1) % 3)]; + for (var i = 0; i < dres[0].length; i += 3) { + var mask = (1 << 28) - 1; - if (a >> 31 != 0) { - dind.push(a & mask); - dind.push(a & mask); - dind.push(b & mask); + for (var e = 0; e < 3; ++e) { + var a = dres[0][i + e], + b = dres[0][i + ((e + 1) % 3)]; + + if (a >> 31 != 0 && ((a >> 28) & 7) == kind) { + // loop of current kind + dind.push(a & mask); + dind.push(a & mask); + dind.push(b & mask); + } else if (kind == 4 && ((a >> 28) & 7) == kind && ((b >> 28) & 7) == kind) { + // locked edge (may not be marked as a loop) + dind.push(a & mask); + dind.push(a & mask); + dind.push(b & mask); + } } } - } - rgeo.addGroup(res[0].length, dind.length - res[0].length, 1); + if (offset != dind.length) { + rgeo.addGroup(offset, dind.length - offset, kind); + offset = dind.length; + } + } } rgeo.index.array = new Uint32Array(dind); @@ -261,7 +272,13 @@ if (object.isMesh) { if (!object.original) { object.original = object.geometry.clone(); - object.material = [object.material, new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })]; + object.material = [ + object.material, + new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true }), // border + new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true }), // seam + new THREE.MeshBasicMaterial({ color: 0xff00ff, wireframe: true }), // complex + new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }), // locked + ]; } var [geo, tri, err] = simplifyMesh(object.original); From ba6260c484f239f6c801120db7b8ee404c74a7c5 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Sun, 18 Aug 2024 17:48:20 -0700 Subject: [PATCH 6/8] demo: Apply small depth offset to geometry to avoid overlay z-fighting This has to be done on the material itself as lines do not support depth offset in three.js; unfortunately this also means this doesn't help when wireframe rendering is used. --- demo/simplify.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/demo/simplify.html b/demo/simplify.html index 084bfe9b..eb32583a 100644 --- a/demo/simplify.html +++ b/demo/simplify.html @@ -272,6 +272,12 @@ if (object.isMesh) { if (!object.original) { object.original = object.geometry.clone(); + + // use small depth offset to avoid overlay z-fighting with the original mesh + // has to be done on the main material as overlays use lines that don't support depth offset + object.material.polygonOffset = true; + object.material.polygonOffsetUnits = 16; + object.material = [ object.material, new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true }), // border From 31e6283317a149ea707378c6bdcf611f7d563b92 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Sun, 18 Aug 2024 18:01:08 -0700 Subject: [PATCH 7/8] demo: Add basic (optional) welding to the simplifier Without this, some glTF models can't be properly simplified because of export or conversion issues resulting in duplicate vertices with equal or slightly-different normals or tangents. Ideally in the future simplifier (with attributes?) should be more tolerant or we should add a pre-process step, but for now we can use mergeVertices from three.js. The tolerance is set to be reasonable for normals; unfortunately mergeVertices does not allow to specify a separate value for positions or UVs (which we would rather keep unwelded). --- demo/simplify.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/demo/simplify.html b/demo/simplify.html index eb32583a..008824fc 100644 --- a/demo/simplify.html +++ b/demo/simplify.html @@ -50,6 +50,7 @@ import * as THREE from 'three'; import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js'; import { OrbitControls } from 'three-examples/controls/OrbitControls.js'; + import { mergeVertices } from 'three-examples/utils/BufferGeometryUtils.js'; import { MeshoptDecoder } from '../js/meshopt_decoder.module.js'; import { MeshoptSimplifier } from '../js/meshopt_simplifier.module.js'; import { GUI } from 'https://unpkg.com/lil-gui@0.17.0/dist/lil-gui.esm.js'; @@ -64,6 +65,7 @@ ratio: 1.0, debugOverlay: false, lockBorder: false, + weldVertices: false, useAttributes: false, errorThresholdLog10: 1, normalWeight: 0.5, @@ -103,6 +105,7 @@ var guiSimplify = gui.addFolder('Simplify'); guiSimplify.add(settings, 'ratio', 0, 1, 0.01).onChange(simplify); guiSimplify.add(settings, 'lockBorder').onChange(simplify); + guiSimplify.add(settings, 'weldVertices').onChange(simplify); guiSimplify.add(settings, 'errorThresholdLog10', 0, 3, 0.1).onChange(simplify); guiSimplify.add(settings, 'useAttributes').onChange(simplify); guiSimplify.add(settings, 'normalWeight', 0, 2, 0.01).onChange(simplify); @@ -123,6 +126,12 @@ animate(); function simplifyMesh(geo) { + if (settings.weldVertices) { + // pre-welding is important for some meshes that have very close normals in adjacent face corners + // for now we use the welder from three.js; this is not as performant as it could be + geo = mergeVertices(geo, 1e-2); + } + var attributes = 6; // 3 color, 3 normal var positions = new Float32Array(geo.attributes.position.array); From ea385ce376ed02d93275108662d18a92c9f48ea5 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 19 Aug 2024 08:49:59 -0700 Subject: [PATCH 8/8] demo: Make auto-update more robust Instead of terminating the loop when encountering an error, handle it and wait a little longer for the next request. Also we now display the last time the module got updated which makes it easier to confirm that the update flow is working. --- demo/simplify.html | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/demo/simplify.html b/demo/simplify.html index 008824fc..5911ee1a 100644 --- a/demo/simplify.html +++ b/demo/simplify.html @@ -94,6 +94,7 @@ simplify(); }, autoUpdate: false, + autoUpdateStatus: '', }; var gui = new GUI({ width: 300 }); @@ -115,6 +116,7 @@ guiLoad.add(settings, 'loadFile'); guiLoad.add(settings, 'updateModule'); guiLoad.add(settings, 'autoUpdate').onChange(autoReload); + guiLoad.add(settings, 'autoUpdateStatus').listen(); var guiStats = gui.addFolder('Stats'); guiStats.add(settings.stats, 'triangles').listen(); @@ -333,17 +335,22 @@ function autoReload() { if (!settings.autoUpdate) return; - var simph = fetch('/js/meshopt_simplifier.module.js?x=' + Date.now(), { method: 'HEAD' }); - simph.then(function (r) { - var last = r.headers.get('Last-Modified'); - if (last != moduleLastModified) { - moduleLastModified = last; - reload(); - simplify(); - } + fetch('/js/meshopt_simplifier.module.js?x=' + Date.now(), { method: 'HEAD' }) + .then(function (r) { + var last = r.headers.get('Last-Modified'); + if (last != moduleLastModified) { + moduleLastModified = last; + reload(); + simplify(); + } - setTimeout(autoReload, 1000); - }); + settings.autoUpdateStatus = new Date(last).toLocaleTimeString(); + setTimeout(autoReload, 1000); + }) + .catch(function (e) { + settings.autoUpdateStatus = 'error'; + setTimeout(autoReload, 5000); + }); } function update() {