diff --git a/demo/simplify.html b/demo/simplify.html index 6f9ccd03..5911ee1a 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'; @@ -62,7 +63,9 @@ wireframe: false, pointSize: 1.0, ratio: 1.0, + debugOverlay: false, lockBorder: false, + weldVertices: false, useAttributes: false, errorThresholdLog10: 1, normalWeight: 0.5, @@ -90,16 +93,20 @@ reload(); simplify(); }, + autoUpdate: false, + autoUpdateStatus: '', }; var gui = new GUI({ width: 300 }); 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); 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); @@ -108,6 +115,8 @@ var guiLoad = gui.addFolder('Load'); 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(); @@ -119,14 +128,16 @@ animate(); function simplifyMesh(geo) { - if (!geo.index.original) { - geo.index.original = geo.index.array; + 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); - var indices = geo.index.original; + 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) { @@ -166,30 +177,68 @@ 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 rgeo = geo.clone(); - geo.index.array = res[0]; - geo.index.count = res[0].length; - geo.index.needsUpdate = true; + var dind = res[0]; - return res[1]; + if (settings.debugOverlay) { + // we need extra indices for debug overlay + dind = Array.from(res[0]); + + for (var kind = 1; kind <= 4; ++kind) { + var offset = dind.length; + + 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 && ((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); + } + } + } + + if (offset != dind.length) { + rgeo.addGroup(offset, dind.length - offset, kind); + offset = dind.length; + } + } + } + + rgeo.index.array = new Uint32Array(dind); + rgeo.index.count = dind.length; + rgeo.index.needsUpdate = true; + + rgeo.addGroup(0, res[0].length, 0); + + return [rgeo, res[0].length / 3, res[1]]; } function simplifyPoints(geo) { @@ -232,10 +281,29 @@ scene.traverse(function (object) { if (object.isMesh) { - var err = simplifyMesh(object.geometry); + 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 + 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); + + 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) { @@ -262,10 +330,37 @@ }); } + var moduleLastModified = 0; + + function autoReload() { + if (!settings.autoUpdate) return; + + 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(); + } + + settings.autoUpdateStatus = new Date(last).toLocaleTimeString(); + setTimeout(autoReload, 1000); + }) + .catch(function (e) { + settings.autoUpdateStatus = 'error'; + setTimeout(autoReload, 5000); + }); + } + 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; 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 {