From d09f90a035ec1bc66e3603daec0c56c6218cb8f3 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Tue, 14 Jul 2026 20:28:46 -0700 Subject: [PATCH 01/41] demo: Add initial remesh scaffolding This is a heavily reduced copy of simplify.html; it is a somewhat similar structure but it only supports one model and has a much reduced simplfication pipeline. We keep the debug visualization code for now although it doesn't work correctly yet, because I'd expect that it will be useful for post-simplify topology visualization. --- demo/remesh.html | 520 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 demo/remesh.html diff --git a/demo/remesh.html b/demo/remesh.html new file mode 100644 index 000000000..79270b4bd --- /dev/null +++ b/demo/remesh.html @@ -0,0 +1,520 @@ + + + + meshoptimizer - demo + + + + + + + + +
+ + + + + From e6c019dcfd7e0584ff6c36b76f5011dfe9f7cd54 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Tue, 14 Jul 2026 21:04:17 -0700 Subject: [PATCH 02/41] demo: Remeshing harness now merges input scene Making progress towards the full remeshing pipeline; we now merge all input meshes (pre-skinned at their current pose) into a single position-only mesh; that is then simplified and displayed. The normals on the mesh are auto-generated using three.js logic. Also remove the debug overlay logic for now; we will need it back but it's too messy to maintain alongside the rest of the code. --- demo/remesh.html | 241 +++++++++++++++++------------------------------ 1 file changed, 88 insertions(+), 153 deletions(-) diff --git a/demo/remesh.html b/demo/remesh.html index 79270b4bd..b0196e34b 100644 --- a/demo/remesh.html +++ b/demo/remesh.html @@ -74,18 +74,17 @@ import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js'; import { OBJLoader } from 'three-examples/loaders/OBJLoader.js'; import { OrbitControls } from 'three-examples/controls/OrbitControls.js'; + import { mergeGeometries } from 'three-examples/utils/BufferGeometryUtils.js'; import { MeshoptDecoder } from '../js/meshopt_decoder.mjs'; import { MeshoptSimplifier } from '../js/meshopt_simplifier.js'; import { GUI } from 'lil-gui'; var container; - var camera, renderer, scene, controls, model, stats; + var camera, renderer, scene, controls, model, merged, remeshed, remeshMaterial, stats; var settings = { wireframe: false, - wireframeOverlay: false, ratio: 1.0, - debugOverlay: false, regularize: false, errorThreshold: 0.1, @@ -103,7 +102,7 @@ }, remesh: function () { - simplify(); + remesh(); }, revert: function () { @@ -112,7 +111,7 @@ updateModule: function () { reload(); - simplify(); + remesh(); }, autoUpdate: false, autoUpdateStatus: '', @@ -121,8 +120,6 @@ var gui = new GUI({ width: 300 }); var guiDisplay = gui.addFolder('Display'); guiDisplay.add(settings, 'wireframe').onChange(update); - guiDisplay.add(settings, 'wireframeOverlay'); - guiDisplay.add(settings, 'debugOverlay'); var guiSimplify = gui.addFolder('Remesh'); guiSimplify.add(settings, 'ratio', 0, 1, 0.01); @@ -190,6 +187,8 @@ pointLightL.decay = 0.5; scene.add(pointLightL); + remeshMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd }); + // Create controls controls = new OrbitControls(camera, renderer.domElement); @@ -197,173 +196,100 @@ updateRendererSizes(); } + function mergeMesh(model) { + var geometries = []; + var vertex = new THREE.Vector3(); + + model.updateMatrixWorld(true); + model.traverse(function (object) { + if (object.isMesh && object.geometry.attributes.position) { + var source = object.geometry.attributes.position; + var positions = new Float32Array(source.count * 3); + + // getVertexPosition applies the skinning transform, if present + for (var i = 0; i < source.count; ++i) { + object.getVertexPosition(i, vertex); + vertex.toArray(positions, i * 3); + } + + var geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + if (object.geometry.index) { + geometry.setIndex(object.geometry.index); + } else { + var indices = new Uint32Array(source.count); + for (var i = 0; i < source.count; ++i) indices[i] = i; + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + } + geometry.applyMatrix4(object.matrixWorld); + geometries.push(geometry); + } + }); + + return geometries.length ? mergeGeometries(geometries) : null; + } + function simplifyMesh(geo, threshold) { var positions = new Float32Array(geo.attributes.position.array); - var indices = new Uint32Array(geo.index.array); // upcast 16-bit indices for _InternalDebug + var indices = new Uint32Array(geo.index.array); var target = Math.floor((indices.length * settings.ratio) / 3) * 3; var stride = geo.attributes.position instanceof THREE.InterleavedBufferAttribute ? geo.attributes.position.data.stride : 3; - var flags = []; + var flags = ['Permissive', 'Prune']; if (settings.regularize) { flags.push('Regularize'); } - if (settings.debugOverlay) { - flags.push('_InternalDebug'); - } var res = MeshoptSimplifier.simplify(indices, positions, stride, target, threshold, flags); var rgeo = geo.clone(); + rgeo.setIndex(new THREE.BufferAttribute(res[0], 1)); + rgeo.computeVertexNormals(); - var dind = res[0]; - var dlen = dind.length; - - if (settings.debugOverlay) { - // we need extra indices for debug overlay - dind = Array.from(dind); - - var mask = (1 << 28) - 1; - - for (var kind = 1; kind <= 6; ++kind) { - var offset = dind.length; - - for (var i = 0; i < dlen; i += 3) { - for (var e = 0; e < 3; ++e) { - var a = dind[i + e], - b = dind[i + ((e + 1) % 3)]; - var ak = (a >> 28) & 7, - bk = (b >> 28) & 7; - - if (a >> 31 != 0 && (ak == kind || bk == kind) && (ak == kind || ak == 4) && (bk == kind || bk == 4)) { - // loop edge of current kind (allow one of the vertices to be locked) - // note: complex edges ignore loop metadata - dind.push(a & mask); - dind.push(a & mask); - dind.push(b & mask); - } else if (a >> 31 != 0 && kind == 5 && ak != bk && ak != 4 && bk != 4) { - // mixed edge (neither vertex is locked, and they are of different kinds) - dind.push(a & mask); - dind.push(a & mask); - dind.push(b & mask); - } else if (a >> 31 == 0 && kind == 4 && ak == kind && bk == kind) { - // locked edge (not marked as a loop) - dind.push(a & mask); - dind.push(a & mask); - dind.push(b & mask); - } else if (a >> 31 == 0 && kind == 6 && ak == 3 && bk == 3) { - // complex edge (not marked as a loop) - dind.push(a & mask); - dind.push(a & mask); - dind.push(b & mask); - } - } - } + return [rgeo, res[0].length / 3, res[1]]; + } - if (offset != dind.length) { - rgeo.addGroup(offset, dind.length - offset, kind + 1); // +1 to skip overlay - offset = dind.length; - } - } + function remesh() { + MeshoptSimplifier.ready.then(function () { + if (!merged) return; - // clear debug bits for actual indices - for (var i = 0; i < dlen; i++) { - dind[i] &= mask; - } + var [geometry, triangles, error] = simplifyMesh(merged, settings.errorThreshold); - if (settings.wireframeOverlay) { - rgeo.addGroup(0, dlen, 1); // 1=overlay + if (remeshed) { + scene.remove(remeshed); } - } else if (settings.wireframeOverlay) { - rgeo.addGroup(0, dind.length, 1); // 1=overlay - } - rgeo.index.array = new Uint32Array(dind); - rgeo.index.count = dind.length; - rgeo.index.needsUpdate = true; + remeshed = new THREE.Mesh(geometry, remeshMaterial); + model.visible = false; + scene.add(remeshed); + update(); - rgeo.addGroup(0, dlen, 0); - - return [rgeo, dlen / 3, res[1]]; - } - - function getRadius(obj) { - var box = new THREE.Box3().setFromObject(obj); - return box.max.sub(box.min).length() / 2; - } - - function simplify() { - MeshoptSimplifier.ready.then(function () { - var threshold = settings.errorThreshold; - - stats.triangles = 0; - stats.vertices = 0; - stats.error = 0; - stats.ratio = 0; - - var rnum = 0; - var rden = 0; - var extent = getRadius(scene); - - scene.traverse(function (object) { - if (object.isMesh && object.geometry.index) { - if (!object.original) { - object.original = object.geometry.clone(); - object.originalMaterial = object.material; - - // 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 - var material = (Array.isArray(object.material) ? object.material[0] : object.material).clone(); - material.polygonOffset = true; - material.polygonOffsetFactor = 0.5; - material.polygonOffsetUnits = 16; - - object.remeshMaterial = [ - material, - new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true }), // overlay - new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true }), // border - new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true }), // seam - new THREE.MeshBasicMaterial({ color: 0x009f9f, wireframe: true }), // complex - new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }), // locked - new THREE.MeshBasicMaterial({ color: 0xff9f00, wireframe: true }), // mixed edge - new THREE.MeshBasicMaterial({ color: 0x9f5f9f, wireframe: true }), // complex, no loop - ]; - } - object.material = object.remeshMaterial; - - var scale = getRadius(object) / extent; - var [geo, tri, err] = simplifyMesh(object.original, threshold / scale); - - object.geometry = geo; - - stats.error = Math.max(stats.error, err * scale); - stats.triangles += tri; - stats.vertices += object.geometry.attributes.position.count; - - rnum += tri; - rden += object.original.index.count / 3; - } - }); - - stats.ratio = rden > 0 ? (rnum / rden) * 100 : 0; + stats.triangles = triangles; + stats.vertices = geometry.attributes.position.count; + stats.error = error; + stats.ratio = (triangles / (merged.index.count / 3)) * 100; updateStatsDisplay(); }); } function revert() { + if (!model) return; + + model.visible = true; + if (remeshed) { + scene.remove(remeshed); + remeshed = undefined; + } + stats.triangles = 0; stats.vertices = 0; stats.error = 0; stats.ratio = 100; - scene.traverse(function (object) { - if (object.isMesh && object.geometry.index) { - if (object.original) { - object.geometry = object.original.clone(); - object.material = object.originalMaterial; - } - - stats.triangles += object.geometry.index.count / 3; + model.traverse(function (object) { + if (object.isMesh) { + stats.triangles += (object.geometry.index ? object.geometry.index.count : object.geometry.attributes.position.count) / 3; stats.vertices += object.geometry.attributes.position.count; } }); @@ -409,7 +335,7 @@ if (last != moduleLastModified) { moduleLastModified = last; reload(); - simplify(); + remesh(); } settings.autoUpdateStatus = new Date(last).toLocaleTimeString(); @@ -438,6 +364,13 @@ scene.remove(model); model = undefined; } + if (merged) { + merged = undefined; + } + if (remeshed) { + scene.remove(remeshed); + remeshed = undefined; + } var onProgress = function (xhr) {}; var onError = function (e) { @@ -453,16 +386,21 @@ model.position.set(-offset.x, -offset.y, -offset.z); } + function finish(object) { + model = object; + center(model); + scene.add(model); + merged = mergeMesh(model); + revert(); + } + if (ext == 'gltf' || ext == 'glb') { var loader = new GLTFLoader(); loader.setMeshoptDecoder(MeshoptDecoder); loader.load( path, function (gltf) { - model = gltf.scene; - center(model); - scene.add(model); - revert(); + finish(gltf.scene); }, onProgress, onError @@ -472,10 +410,7 @@ loader.load( path, function (obj) { - model = obj; - center(model); - scene.add(model); - revert(); + finish(obj); }, onProgress, onError From e9af02697d16816154cc4c1c066b8d826cfd10f9 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Tue, 14 Jul 2026 21:56:12 -0700 Subject: [PATCH 03/41] demo: Use a separate pseudo-remesh step before simplification We do not have the actual remesher implemented yet so we approximate it with the sloppy simplifier; after that, the regular simplification remains as an optional step. The remesher uses resolution instead of error target, and the post-simplification just targets an error bound instead of ratio now. Running two simplification steps looks a little awkward but the initial sloppy simplify will be replaced with a remesher in the future. --- demo/remesh.html | 77 ++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/demo/remesh.html b/demo/remesh.html index b0196e34b..51c1a890d 100644 --- a/demo/remesh.html +++ b/demo/remesh.html @@ -84,9 +84,12 @@ var settings = { wireframe: false, - ratio: 1.0, - regularize: false, - errorThreshold: 0.1, + resolution: 96, + simplify: { + enabled: false, + regularize: false, + errorThresholdLog10: 3, + }, loadFile: function () { var input = document.getElementById('fileInput'); @@ -121,12 +124,15 @@ var guiDisplay = gui.addFolder('Display'); guiDisplay.add(settings, 'wireframe').onChange(update); - var guiSimplify = gui.addFolder('Remesh'); - guiSimplify.add(settings, 'ratio', 0, 1, 0.01); - guiSimplify.add(settings, 'regularize'); - guiSimplify.add(settings, 'errorThreshold', 0, 1, 0.001); - guiSimplify.add(settings, 'remesh'); - guiSimplify.add(settings, 'revert'); + var guiRemesh = gui.addFolder('Remesh'); + guiRemesh.add(settings, 'resolution', 1, 256, 1); + guiRemesh.add(settings, 'remesh'); + guiRemesh.add(settings, 'revert'); + + var guiSimplify = gui.addFolder('Simplify'); + guiSimplify.add(settings.simplify, 'enabled'); + guiSimplify.add(settings.simplify, 'regularize'); + guiSimplify.add(settings.simplify, 'errorThresholdLog10', 0, 3, 0.1); var guiLoad = gui.addFolder('Load'); guiLoad.add(settings, 'loadFile'); @@ -159,7 +165,6 @@ stats = { triangles: 0, vertices: 0, - error: 0, ratio: 0, label: statsLabel, }; @@ -229,32 +234,30 @@ return geometries.length ? mergeGeometries(geometries) : null; } - function simplifyMesh(geo, threshold) { - var positions = new Float32Array(geo.attributes.position.array); - var indices = new Uint32Array(geo.index.array); - var target = Math.floor((indices.length * settings.ratio) / 3) * 3; - - var stride = geo.attributes.position instanceof THREE.InterleavedBufferAttribute ? geo.attributes.position.data.stride : 3; - - var flags = ['Permissive', 'Prune']; - if (settings.regularize) { - flags.push('Regularize'); - } - - var res = MeshoptSimplifier.simplify(indices, positions, stride, target, threshold, flags); + function simplifyMesh(geo, threshold, sloppy, flags) { + var res = sloppy + ? MeshoptSimplifier.simplifySloppy(geo.index.array, geo.attributes.position.array, 3, null, 0, threshold) + : MeshoptSimplifier.simplify(geo.index.array, geo.attributes.position.array, 3, 0, threshold, flags); var rgeo = geo.clone(); rgeo.setIndex(new THREE.BufferAttribute(res[0], 1)); - rgeo.computeVertexNormals(); - return [rgeo, res[0].length / 3, res[1]]; + return rgeo; } function remesh() { MeshoptSimplifier.ready.then(function () { if (!merged) return; - var [geometry, triangles, error] = simplifyMesh(merged, settings.errorThreshold); + var geometry = simplifyMesh(merged, 1 / settings.resolution, true); + + if (settings.simplify.enabled) { + var flags = settings.simplify.regularize ? ['Regularize'] : []; + geometry = simplifyMesh(geometry, Math.pow(10, -settings.simplify.errorThresholdLog10), false, flags); + } + + geometry.computeVertexNormals(); + var triangles = geometry.index.count / 3; if (remeshed) { scene.remove(remeshed); @@ -267,7 +270,6 @@ stats.triangles = triangles; stats.vertices = geometry.attributes.position.count; - stats.error = error; stats.ratio = (triangles / (merged.index.count / 3)) * 100; updateStatsDisplay(); }); @@ -284,7 +286,6 @@ stats.triangles = 0; stats.vertices = 0; - stats.error = 0; stats.ratio = 100; model.traverse(function (object) { @@ -305,9 +306,6 @@ 'Vertices: ' + stats.vertices + '
' + - 'Error: ' + - stats.error.toExponential(3) + - '
' + 'Ratio: ' + stats.ratio.toFixed(1) + '%'; @@ -360,17 +358,12 @@ } function load(path, ext) { - if (model) { - scene.remove(model); - model = undefined; - } - if (merged) { - merged = undefined; - } - if (remeshed) { - scene.remove(remeshed); - remeshed = undefined; - } + if (model) scene.remove(model); + if (remeshed) scene.remove(remeshed); + + model = undefined; + merged = undefined; + remeshed = undefined; var onProgress = function (xhr) {}; var onError = function (e) { From faa20ceb3049e7ca08639b5928a0bc495fd7e2d8 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 15 Jul 2026 09:24:31 -0700 Subject: [PATCH 04/41] demo: Implement basic vertex color transfer We now build a BVH around the merged mesh, and also track triangle -> source geometry correspondence. This allows us to query an arbitrary point against the BVH and translate this into source mesh/triangle. We use this capability to generate vertex colors for the resampled mesh. Each vertex and each triangle center is evaluated against a very basic material model (color/map/ao/metalness); result is averaged per vertex and stored in each vertex for display. To perform resampling we need access to source images from JS. This... is more difficult than makes sense, and the most straightforward route seems to involve making a canvas, drawing the texture into the canvas and getting the pixel data. This is not very fast but we do it once per image. --- demo/remesh.html | 142 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/demo/remesh.html b/demo/remesh.html index 51c1a890d..340661d4b 100644 --- a/demo/remesh.html +++ b/demo/remesh.html @@ -59,6 +59,7 @@ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js", "three-examples/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/", + "three-mesh-bvh": "https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.9.10/build/index.module.js", "lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.esm.js" } } @@ -75,12 +76,14 @@ import { OBJLoader } from 'three-examples/loaders/OBJLoader.js'; import { OrbitControls } from 'three-examples/controls/OrbitControls.js'; import { mergeGeometries } from 'three-examples/utils/BufferGeometryUtils.js'; + import { MeshBVH } from 'three-mesh-bvh'; import { MeshoptDecoder } from '../js/meshopt_decoder.mjs'; import { MeshoptSimplifier } from '../js/meshopt_simplifier.js'; import { GUI } from 'lil-gui'; var container; var camera, renderer, scene, controls, model, merged, remeshed, remeshMaterial, stats; + var textureImages = new WeakMap(); var settings = { wireframe: false, @@ -90,6 +93,9 @@ regularize: false, errorThresholdLog10: 3, }, + paint: { + mode: 'vertex', + }, loadFile: function () { var input = document.getElementById('fileInput'); @@ -134,6 +140,9 @@ guiSimplify.add(settings.simplify, 'regularize'); guiSimplify.add(settings.simplify, 'errorThresholdLog10', 0, 3, 0.1); + var guiPaint = gui.addFolder('Paint'); + guiPaint.add(settings.paint, 'mode', ['none', 'vertex', 'texture']); + var guiLoad = gui.addFolder('Load'); guiLoad.add(settings, 'loadFile'); guiLoad.add(settings, 'updateModule'); @@ -204,6 +213,8 @@ function mergeMesh(model) { var geometries = []; var vertex = new THREE.Vector3(); + var sourceTriangle = []; + var sourceMesh = []; model.updateMatrixWorld(true); model.traverse(function (object) { @@ -228,10 +239,128 @@ } geometry.applyMatrix4(object.matrixWorld); geometries.push(geometry); + + for (var i = 0; i < geometry.index.count / 3; ++i) { + sourceTriangle.push(i); + sourceMesh.push(object); + } } }); - return geometries.length ? mergeGeometries(geometries) : null; + var result = mergeGeometries(geometries); + result.sourceTriangle = sourceTriangle; + result.sourceMesh = sourceMesh; + result.bvh = new MeshBVH(result, { indirect: true }); + return result; + } + + function getTextureImage(texture) { + if (!textureImages.has(texture.source)) { + var canvas = document.createElement('canvas'); + canvas.width = texture.image.width; + canvas.height = texture.image.height; + var context = canvas.getContext('2d'); + context.drawImage(texture.image, 0, 0); + textureImages.set(texture.source, context.getImageData(0, 0, texture.image.width, texture.image.height)); + } + return textureImages.get(texture.source); + } + + function sampleTexture(texture, uv) { + var image = getTextureImage(texture); + var point = texture.transformUv(uv); + var x = Math.min(Math.floor(point.x * image.width), image.width - 1); + var y = Math.min(Math.floor(point.y * image.height), image.height - 1); + var offset = (y * image.width + x) * 4; + return new THREE.Color(image.data[offset] / 255, image.data[offset + 1] / 255, image.data[offset + 2] / 255); + } + + function interpolate(attribute, a, b, c, bary, component) { + return attribute.getComponent(a, component) * bary.x + attribute.getComponent(b, component) * bary.y + attribute.getComponent(c, component) * bary.z; + } + + function sampleColorBary(mesh, triangle, bary) { + var geometry = mesh.geometry; + var a = geometry.index.array[triangle * 3 + 0], b = geometry.index.array[triangle * 3 + 1], c = geometry.index.array[triangle * 3 + 2]; + + var material = mesh.material; + if (Array.isArray(material)) { + var group = geometry.groups.find(function (group) { return offset >= group.start && offset < group.start + group.count; }); + material = material[group.materialIndex]; + } + + var result = material.color.clone(); + + var uv = new THREE.Vector2(); + if (geometry.attributes.uv) { + uv.x = interpolate(geometry.attributes.uv, a, b, c, bary, 0); + uv.y = interpolate(geometry.attributes.uv, a, b, c, bary, 1); + } + + if (material.map) { + result.multiply(sampleTexture(material.map, uv).convertSRGBToLinear()); + } + + if (material.vertexColors) { + result.r *= interpolate(geometry.attributes.color, a, b, c, bary, 0); + result.g *= interpolate(geometry.attributes.color, a, b, c, bary, 1); + result.b *= interpolate(geometry.attributes.color, a, b, c, bary, 2); + } + + if (material.aoMap) { + var ao = sampleTexture(material.aoMap, uv).r; + result.multiplyScalar(1 + material.aoMapIntensity * (ao - 1)); + } + + if (material.metalnessMap) { + var metalness = sampleTexture(material.metalnessMap, uv).b; + result.multiplyScalar(1 - material.metalness * metalness); + } + + return result; + } + + function sampleColor(model, point) { + var hit = model.bvh.closestPointToPoint(point); + var a = new THREE.Vector3(), b = new THREE.Vector3(), c = new THREE.Vector3(), bary = new THREE.Vector3(); + a.fromBufferAttribute(model.attributes.position, model.index.array[hit.faceIndex * 3 + 0]); + b.fromBufferAttribute(model.attributes.position, model.index.array[hit.faceIndex * 3 + 1]); + c.fromBufferAttribute(model.attributes.position, model.index.array[hit.faceIndex * 3 + 2]); + THREE.Triangle.getBarycoord(hit.point, a, b, c, bary); + return sampleColorBary(model.sourceMesh[hit.faceIndex], model.sourceTriangle[hit.faceIndex], bary); + } + + function transferColors(model, geometry) { + var colors = new Float32Array(geometry.attributes.position.count * 3); + var counts = new Uint32Array(geometry.attributes.position.count); + var query = new THREE.Vector3(), temp = new THREE.Vector3(); + + for (var i = 0; i < geometry.attributes.position.count; ++i) { + query.fromBufferAttribute(geometry.attributes.position, i); + sampleColor(model, query).toArray(colors, i * 3); + counts[i] = 1; + } + + for (var i = 0; i < geometry.index.array.length; i += 3) { + query.fromBufferAttribute(geometry.attributes.position, geometry.index.array[i]); + query.add(temp.fromBufferAttribute(geometry.attributes.position, geometry.index.array[i + 1])); + query.add(temp.fromBufferAttribute(geometry.attributes.position, geometry.index.array[i + 2])); + query.multiplyScalar(1 / 3); + + var color = sampleColor(model, query); + + for (var j = 0; j < 3; ++j) { + var v = geometry.index.array[i + j]; + colors[v * 3] += color.r, colors[v * 3 + 1] += color.g, colors[v * 3 + 2] += color.b; + counts[v]++; + } + } + + for (var i = 0; i < counts.length; ++i) { + colors[i * 3] /= counts[i], colors[i * 3 + 1] /= counts[i], colors[i * 3 + 2] /= counts[i]; + } + + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); } function simplifyMesh(geo, threshold, sloppy, flags) { @@ -257,6 +386,17 @@ } geometry.computeVertexNormals(); + + if (settings.paint.mode == 'vertex') { + transferColors(merged, geometry); + remeshMaterial.vertexColors = true; + remeshMaterial.color.set(0xffffff); + } else { + remeshMaterial.vertexColors = false; + remeshMaterial.color.set(0xdddddd); + } + remeshMaterial.needsUpdate = true; + var triangles = geometry.index.count / 3; if (remeshed) { From 76fba2c98a7bbcf1bf4e2dd0ac78d66ee21e187d Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 15 Jul 2026 11:03:58 -0700 Subject: [PATCH 05/41] demo: Implement texture unwrapping and color transfer In addition to the vertex painting we now provide texture painting. We use watlas to UV unwrap the remeshed geometry, and then fill it based on source material. For transfer, each triangle is rasterized into UV space and each pixel is converted back into world space, followed by the same BVH query. This is fairly slow at higher texture resolutions; there might be ways to reuse this information but also maybe the lookup can be faster in the future. The texture has unfilled areas that result in bilinear artifacts; we'll fix this separately. --- demo/remesh.html | 79 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/demo/remesh.html b/demo/remesh.html index 340661d4b..9da41f9ce 100644 --- a/demo/remesh.html +++ b/demo/remesh.html @@ -60,6 +60,7 @@ "three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js", "three-examples/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/", "three-mesh-bvh": "https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.9.10/build/index.module.js", + "watlas": "https://cdn.jsdelivr.net/npm/watlas@1.0.1/dist/watlas.js", "lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.esm.js" } } @@ -75,10 +76,11 @@ import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js'; import { OBJLoader } from 'three-examples/loaders/OBJLoader.js'; import { OrbitControls } from 'three-examples/controls/OrbitControls.js'; - import { mergeGeometries } from 'three-examples/utils/BufferGeometryUtils.js'; + import { mergeGeometries, mergeVertices } from 'three-examples/utils/BufferGeometryUtils.js'; import { MeshBVH } from 'three-mesh-bvh'; import { MeshoptDecoder } from '../js/meshopt_decoder.mjs'; import { MeshoptSimplifier } from '../js/meshopt_simplifier.js'; + import { Initialize as initializeWatlas, Atlas } from 'watlas'; import { GUI } from 'lil-gui'; var container; @@ -95,6 +97,7 @@ }, paint: { mode: 'vertex', + resolution: 1024, }, loadFile: function () { @@ -142,6 +145,7 @@ var guiPaint = gui.addFolder('Paint'); guiPaint.add(settings.paint, 'mode', ['none', 'vertex', 'texture']); + guiPaint.add(settings.paint, 'resolution', [256, 512, 1024, 2048]); var guiLoad = gui.addFolder('Load'); guiLoad.add(settings, 'loadFile'); @@ -363,6 +367,71 @@ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); } + function unwrapMesh(geometry) { + var positions = geometry.attributes.position.array; + var indices = new Uint32Array(geometry.index.array); + + var atlas = new Atlas(); + atlas.addMesh({ vertexPositionData: positions, vertexCount: positions.length / 3, vertexPositionStride: 12, indexData: indices, indexCount: indices.length }); + atlas.generate(); + + var mesh = atlas.getMesh(0); + var atlasIndices = new Uint32Array(mesh.indexCount); + var uv = new Float32Array(mesh.indexCount * 2); + mesh.getIndexArray(atlasIndices); + for (var i = 0; i < mesh.indexCount; ++i) { + var v = mesh.getVertex(atlasIndices[i]); + uv[i * 2 + 0] = v.uv[0] / atlas.width; + uv[i * 2 + 1] = v.uv[1] / atlas.height; + } + atlas.delete(); + + geometry = geometry.toNonIndexed(); + geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2)); + return mergeVertices(geometry); + } + + function transferTexture(model, geometry, size) { + var data = new Uint8Array(size * size * 4); + var position = geometry.attributes.position, uv = geometry.attributes.uv, indices = geometry.index.array; + var bary = new THREE.Vector3(), point = new THREE.Vector3(); + + for (var i = 0; i < indices.length; i += 3) { + var a = indices[i], b = indices[i + 1], c = indices[i + 2]; + var x0 = uv.getX(a) * size, y0 = uv.getY(a) * size; + var x1 = uv.getX(b) * size, y1 = uv.getY(b) * size; + var x2 = uv.getX(c) * size, y2 = uv.getY(c) * size; + var minX = Math.max(0, Math.floor(Math.min(x0, x1, x2))), maxX = Math.min(size - 1, Math.ceil(Math.max(x0, x1, x2))); + var minY = Math.max(0, Math.floor(Math.min(y0, y1, y2))), maxY = Math.min(size - 1, Math.ceil(Math.max(y0, y1, y2))); + var denominator = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2); + if (Math.abs(denominator) < 1e-7) continue; + + for (var y = minY; y <= maxY; ++y) { + for (var x = minX; x <= maxX; ++x) { + bary.x = ((y1 - y2) * (x + 0.5 - x2) + (x2 - x1) * (y + 0.5 - y2)) / denominator; + bary.y = ((y2 - y0) * (x + 0.5 - x2) + (x0 - x2) * (y + 0.5 - y2)) / denominator; + bary.z = 1 - bary.x - bary.y; + if (bary.x < -1e-4 || bary.y < -1e-4 || bary.z < -1e-4) continue; + + point.set(interpolate(position, a, b, c, bary, 0), interpolate(position, a, b, c, bary, 1), interpolate(position, a, b, c, bary, 2)); + var color = sampleColor(model, point).convertLinearToSRGB(); + var offset = (y * size + x) * 4; + data[offset] = color.r * 255; + data[offset + 1] = color.g * 255; + data[offset + 2] = color.b * 255; + data[offset + 3] = 255; + } + } + } + + var texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat); + texture.colorSpace = THREE.SRGBColorSpace; + texture.magFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearFilter; + texture.needsUpdate = true; + return texture; + } + function simplifyMesh(geo, threshold, sloppy, flags) { var res = sloppy ? MeshoptSimplifier.simplifySloppy(geo.index.array, geo.attributes.position.array, 3, null, 0, threshold) @@ -375,7 +444,7 @@ } function remesh() { - MeshoptSimplifier.ready.then(function () { + MeshoptSimplifier.ready.then(initializeWatlas).then(function () { if (!merged) return; var geometry = simplifyMesh(merged, 1 / settings.resolution, true); @@ -386,11 +455,17 @@ } geometry.computeVertexNormals(); + remeshMaterial.map = null; if (settings.paint.mode == 'vertex') { transferColors(merged, geometry); remeshMaterial.vertexColors = true; remeshMaterial.color.set(0xffffff); + } else if (settings.paint.mode == 'texture') { + geometry = unwrapMesh(geometry); + remeshMaterial.vertexColors = false; + remeshMaterial.color.set(0xffffff); + remeshMaterial.map = transferTexture(merged, geometry, settings.paint.resolution); } else { remeshMaterial.vertexColors = false; remeshMaterial.color.set(0xdddddd); From 6a931899d1dc18bda63457a19c4a8afb5bc8c5cb Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 15 Jul 2026 11:38:08 -0700 Subject: [PATCH 06/41] demo: Implement texture gutters for remeshing transfer We now ask watlas to leave a bit of space between charts and fill the resulting space. This could be done as a post-process, but we seem to get higher quality results on seams if instead each triangle projection covers the gutter instead and resamples the original data there. We accumulate the samples in each pixel and average them at the end; also rework color average to work off the same structure for clarity. --- demo/remesh.html | 62 ++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/demo/remesh.html b/demo/remesh.html index 9da41f9ce..388578dfe 100644 --- a/demo/remesh.html +++ b/demo/remesh.html @@ -98,6 +98,7 @@ paint: { mode: 'vertex', resolution: 1024, + gutter: 1, }, loadFile: function () { @@ -146,6 +147,7 @@ var guiPaint = gui.addFolder('Paint'); guiPaint.add(settings.paint, 'mode', ['none', 'vertex', 'texture']); guiPaint.add(settings.paint, 'resolution', [256, 512, 1024, 2048]); + guiPaint.add(settings.paint, 'gutter', 0, 8, 1); var guiLoad = gui.addFolder('Load'); guiLoad.add(settings, 'loadFile'); @@ -335,14 +337,13 @@ } function transferColors(model, geometry) { - var colors = new Float32Array(geometry.attributes.position.count * 3); - var counts = new Uint32Array(geometry.attributes.position.count); + var colors = new Float32Array(geometry.attributes.position.count * 4); var query = new THREE.Vector3(), temp = new THREE.Vector3(); for (var i = 0; i < geometry.attributes.position.count; ++i) { query.fromBufferAttribute(geometry.attributes.position, i); - sampleColor(model, query).toArray(colors, i * 3); - counts[i] = 1; + sampleColor(model, query).toArray(colors, i * 4); + colors[i * 4 + 3] = 1; } for (var i = 0; i < geometry.index.array.length; i += 3) { @@ -355,25 +356,24 @@ for (var j = 0; j < 3; ++j) { var v = geometry.index.array[i + j]; - colors[v * 3] += color.r, colors[v * 3 + 1] += color.g, colors[v * 3 + 2] += color.b; - counts[v]++; + colors[v * 4] += color.r, colors[v * 4 + 1] += color.g, colors[v * 4 + 2] += color.b, colors[v * 4 + 3]++; } } - for (var i = 0; i < counts.length; ++i) { - colors[i * 3] /= counts[i], colors[i * 3 + 1] /= counts[i], colors[i * 3 + 2] /= counts[i]; + for (var i = 0; i < colors.length; i += 4) { + colors[i] /= colors[i + 3], colors[i + 1] /= colors[i + 3], colors[i + 2] /= colors[i + 3], colors[i + 3] = 1; } - geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4)); } - function unwrapMesh(geometry) { + function unwrapMesh(geometry, resolution, gutter) { var positions = geometry.attributes.position.array; var indices = new Uint32Array(geometry.index.array); var atlas = new Atlas(); atlas.addMesh({ vertexPositionData: positions, vertexCount: positions.length / 3, vertexPositionStride: 12, indexData: indices, indexCount: indices.length }); - atlas.generate(); + atlas.generate({}, { resolution: resolution, padding: gutter }); var mesh = atlas.getMesh(0); var atlasIndices = new Uint32Array(mesh.indexCount); @@ -391,8 +391,8 @@ return mergeVertices(geometry); } - function transferTexture(model, geometry, size) { - var data = new Uint8Array(size * size * 4); + function transferTexture(model, geometry, size, gutter) { + var data = new Float32Array(size * size * 4); var position = geometry.attributes.position, uv = geometry.attributes.uv, indices = geometry.index.array; var bary = new THREE.Vector3(), point = new THREE.Vector3(); @@ -401,30 +401,46 @@ var x0 = uv.getX(a) * size, y0 = uv.getY(a) * size; var x1 = uv.getX(b) * size, y1 = uv.getY(b) * size; var x2 = uv.getX(c) * size, y2 = uv.getY(c) * size; - var minX = Math.max(0, Math.floor(Math.min(x0, x1, x2))), maxX = Math.min(size - 1, Math.ceil(Math.max(x0, x1, x2))); - var minY = Math.max(0, Math.floor(Math.min(y0, y1, y2))), maxY = Math.min(size - 1, Math.ceil(Math.max(y0, y1, y2))); + var minX = Math.max(0, Math.floor(Math.min(x0, x1, x2) - gutter)), maxX = Math.min(size - 1, Math.ceil(Math.max(x0, x1, x2) + gutter)); + var minY = Math.max(0, Math.floor(Math.min(y0, y1, y2) - gutter)), maxY = Math.min(size - 1, Math.ceil(Math.max(y0, y1, y2) + gutter)); var denominator = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2); if (Math.abs(denominator) < 1e-7) continue; + var scale = gutter / Math.abs(denominator); + var epsilonX = 1e-4 + Math.hypot(x1 - x2, y1 - y2) * scale; + var epsilonY = 1e-4 + Math.hypot(x2 - x0, y2 - y0) * scale; + var epsilonZ = 1e-4 + Math.hypot(x0 - x1, y0 - y1) * scale; + for (var y = minY; y <= maxY; ++y) { for (var x = minX; x <= maxX; ++x) { bary.x = ((y1 - y2) * (x + 0.5 - x2) + (x2 - x1) * (y + 0.5 - y2)) / denominator; bary.y = ((y2 - y0) * (x + 0.5 - x2) + (x0 - x2) * (y + 0.5 - y2)) / denominator; bary.z = 1 - bary.x - bary.y; - if (bary.x < -1e-4 || bary.y < -1e-4 || bary.z < -1e-4) continue; + if (bary.x < -epsilonX || bary.y < -epsilonY || bary.z < -epsilonZ) continue; point.set(interpolate(position, a, b, c, bary, 0), interpolate(position, a, b, c, bary, 1), interpolate(position, a, b, c, bary, 2)); var color = sampleColor(model, point).convertLinearToSRGB(); + var offset = (y * size + x) * 4; - data[offset] = color.r * 255; - data[offset + 1] = color.g * 255; - data[offset + 2] = color.b * 255; - data[offset + 3] = 255; + data[offset] += color.r * 255; + data[offset + 1] += color.g * 255; + data[offset + 2] += color.b * 255; + data[offset + 3]++; } } } - var texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat); + for (var i = 0; i < data.length; i += 4) { + var count = data[i + 3]; + if (count) { + data[i] /= count; + data[i + 1] /= count; + data[i + 2] /= count; + data[i + 3] = 255; + } + } + + var texture = new THREE.DataTexture(new Uint8Array(data), size, size, THREE.RGBAFormat); texture.colorSpace = THREE.SRGBColorSpace; texture.magFilter = THREE.LinearFilter; texture.minFilter = THREE.LinearFilter; @@ -462,10 +478,10 @@ remeshMaterial.vertexColors = true; remeshMaterial.color.set(0xffffff); } else if (settings.paint.mode == 'texture') { - geometry = unwrapMesh(geometry); + geometry = unwrapMesh(geometry, settings.paint.resolution, settings.paint.gutter); remeshMaterial.vertexColors = false; remeshMaterial.color.set(0xffffff); - remeshMaterial.map = transferTexture(merged, geometry, settings.paint.resolution); + remeshMaterial.map = transferTexture(merged, geometry, settings.paint.resolution, settings.paint.gutter); } else { remeshMaterial.vertexColors = false; remeshMaterial.color.set(0xdddddd); From 5f4ac36c87975e8c64f7bd6b8c03f63870c7ba35 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 15 Jul 2026 11:57:10 -0700 Subject: [PATCH 07/41] demo: Reimplement wireframe overlay setting Remeshing pipeline is too slow to re-trigger it; so we always add another group to the new mesh and dynamically toggle material visibility instead. This only works on the remeshed mesh, not on the original geometry, but that's a reasonable limitation for now. --- demo/remesh.html | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/demo/remesh.html b/demo/remesh.html index 388578dfe..3df7e6cbd 100644 --- a/demo/remesh.html +++ b/demo/remesh.html @@ -84,11 +84,12 @@ import { GUI } from 'lil-gui'; var container; - var camera, renderer, scene, controls, model, merged, remeshed, remeshMaterial, stats; + var camera, renderer, scene, controls, model, merged, remeshed, remeshMaterial, wireframeMaterial, stats; var textureImages = new WeakMap(); var settings = { wireframe: false, + wireframeOverlay: false, resolution: 96, simplify: { enabled: false, @@ -133,6 +134,7 @@ var gui = new GUI({ width: 300 }); var guiDisplay = gui.addFolder('Display'); guiDisplay.add(settings, 'wireframe').onChange(update); + guiDisplay.add(settings, 'wireframeOverlay').onChange(update); var guiRemesh = gui.addFolder('Remesh'); guiRemesh.add(settings, 'resolution', 1, 256, 1); @@ -207,7 +209,8 @@ pointLightL.decay = 0.5; scene.add(pointLightL); - remeshMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd }); + remeshMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd, polygonOffset: true, polygonOffsetFactor: 0.5, polygonOffsetUnits: 16 }); + wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true, visible: false }); // Create controls controls = new OrbitControls(camera, renderer.domElement); @@ -488,17 +491,20 @@ } remeshMaterial.needsUpdate = true; - var triangles = geometry.index.count / 3; + // remesh material + wireframe overlay material + geometry.addGroup(0, geometry.index.count, 0); + geometry.addGroup(0, geometry.index.count, 1); if (remeshed) { scene.remove(remeshed); } - remeshed = new THREE.Mesh(geometry, remeshMaterial); + remeshed = new THREE.Mesh(geometry, [remeshMaterial, wireframeMaterial]); model.visible = false; scene.add(remeshed); update(); + var triangles = geometry.index.count / 3; stats.triangles = triangles; stats.vertices = geometry.attributes.position.count; stats.ratio = (triangles / (merged.index.count / 3)) * 100; @@ -577,6 +583,8 @@ } function update() { + wireframeMaterial.visible = settings.wireframeOverlay; + scene.traverse(function (child) { if (child.isMesh) { if (Array.isArray(child.material)) { From b7505b16da4b4da817dc904049e00c1d6a19dfdd Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 15 Jul 2026 17:01:50 -0700 Subject: [PATCH 08/41] demo: Fix a few remeshing issues - Use ray cast along the normal and fall back to closest point; this improves quality if the mesh has multiple layers close to each other. - Fix behavior for non-indexed source geometry and support multiple material groups. - Change some defaults to have more reasonable performance. --- demo/remesh.html | 64 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/demo/remesh.html b/demo/remesh.html index 3df7e6cbd..e2af8076e 100644 --- a/demo/remesh.html +++ b/demo/remesh.html @@ -90,7 +90,7 @@ var settings = { wireframe: false, wireframeOverlay: false, - resolution: 96, + resolution: 64, simplify: { enabled: false, regularize: false, @@ -98,7 +98,7 @@ }, paint: { mode: 'vertex', - resolution: 1024, + resolution: 512, gutter: 1, }, @@ -257,9 +257,11 @@ }); var result = mergeGeometries(geometries); + result.computeBoundingBox(); result.sourceTriangle = sourceTriangle; result.sourceMesh = sourceMesh; result.bvh = new MeshBVH(result, { indirect: true }); + result.thickness = result.boundingBox.getSize(new THREE.Vector3()).length() * 0.01; return result; } @@ -290,7 +292,8 @@ function sampleColorBary(mesh, triangle, bary) { var geometry = mesh.geometry; - var a = geometry.index.array[triangle * 3 + 0], b = geometry.index.array[triangle * 3 + 1], c = geometry.index.array[triangle * 3 + 2]; + var offset = triangle * 3, indices = geometry.index; + var a = indices ? indices.array[offset] : offset, b = indices ? indices.array[offset + 1] : offset + 1, c = indices ? indices.array[offset + 2] : offset + 2; var material = mesh.material; if (Array.isArray(material)) { @@ -329,8 +332,15 @@ return result; } - function sampleColor(model, point) { - var hit = model.bvh.closestPointToPoint(point); + function sampleColor(model, point, normal) { + var hit = undefined; + if (normal) { + var ray = new THREE.Ray(); + ray.origin.copy(point).addScaledVector(normal, model.thickness); + ray.direction.copy(normal).negate(); + hit = model.bvh.raycastFirst(ray, THREE.DoubleSide, 1e-12, model.thickness * 2); + } + if (!hit) hit = model.bvh.closestPointToPoint(point); var a = new THREE.Vector3(), b = new THREE.Vector3(), c = new THREE.Vector3(), bary = new THREE.Vector3(); a.fromBufferAttribute(model.attributes.position, model.index.array[hit.faceIndex * 3 + 0]); b.fromBufferAttribute(model.attributes.position, model.index.array[hit.faceIndex * 3 + 1]); @@ -341,24 +351,32 @@ function transferColors(model, geometry) { var colors = new Float32Array(geometry.attributes.position.count * 4); - var query = new THREE.Vector3(), temp = new THREE.Vector3(); + var position = geometry.attributes.position, normal = geometry.attributes.normal, indices = geometry.index.array; + var query = new THREE.Vector3(), direction = new THREE.Vector3(), temp = new THREE.Vector3(); + + for (var i = 0; i < position.count; ++i) { + query.fromBufferAttribute(position, i); + direction.fromBufferAttribute(normal, i); - for (var i = 0; i < geometry.attributes.position.count; ++i) { - query.fromBufferAttribute(geometry.attributes.position, i); - sampleColor(model, query).toArray(colors, i * 4); + sampleColor(model, query, direction).toArray(colors, i * 4); colors[i * 4 + 3] = 1; } - for (var i = 0; i < geometry.index.array.length; i += 3) { - query.fromBufferAttribute(geometry.attributes.position, geometry.index.array[i]); - query.add(temp.fromBufferAttribute(geometry.attributes.position, geometry.index.array[i + 1])); - query.add(temp.fromBufferAttribute(geometry.attributes.position, geometry.index.array[i + 2])); + for (var i = 0; i < indices.length; i += 3) { + query.fromBufferAttribute(position, indices[i]); + query.add(temp.fromBufferAttribute(position, indices[i + 1])); + query.add(temp.fromBufferAttribute(position, indices[i + 2])); query.multiplyScalar(1 / 3); - var color = sampleColor(model, query); + direction.fromBufferAttribute(normal, indices[i]); + direction.add(temp.fromBufferAttribute(normal, indices[i + 1])); + direction.add(temp.fromBufferAttribute(normal, indices[i + 2])); + direction.normalize(); + + var color = sampleColor(model, query, direction); for (var j = 0; j < 3; ++j) { - var v = geometry.index.array[i + j]; + var v = indices[i + j]; colors[v * 4] += color.r, colors[v * 4 + 1] += color.g, colors[v * 4 + 2] += color.b, colors[v * 4 + 3]++; } } @@ -391,13 +409,14 @@ geometry = geometry.toNonIndexed(); geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2)); - return mergeVertices(geometry); + geometry = mergeVertices(geometry); + return geometry; } function transferTexture(model, geometry, size, gutter) { var data = new Float32Array(size * size * 4); - var position = geometry.attributes.position, uv = geometry.attributes.uv, indices = geometry.index.array; - var bary = new THREE.Vector3(), point = new THREE.Vector3(); + var position = geometry.attributes.position, normal = geometry.attributes.normal, uv = geometry.attributes.uv, indices = geometry.index.array; + var bary = new THREE.Vector3(), point = new THREE.Vector3(), direction = new THREE.Vector3(); for (var i = 0; i < indices.length; i += 3) { var a = indices[i], b = indices[i + 1], c = indices[i + 2]; @@ -422,7 +441,9 @@ if (bary.x < -epsilonX || bary.y < -epsilonY || bary.z < -epsilonZ) continue; point.set(interpolate(position, a, b, c, bary, 0), interpolate(position, a, b, c, bary, 1), interpolate(position, a, b, c, bary, 2)); - var color = sampleColor(model, point).convertLinearToSRGB(); + direction.set(interpolate(normal, a, b, c, bary, 0), interpolate(normal, a, b, c, bary, 1), interpolate(normal, a, b, c, bary, 2)).normalize(); + + var color = sampleColor(model, point, direction).convertLinearToSRGB(); var offset = (y * size + x) * 4; data[offset] += color.r * 255; @@ -474,7 +495,9 @@ } geometry.computeVertexNormals(); + remeshMaterial.map = null; + remeshMaterial.vertexColors = false; if (settings.paint.mode == 'vertex') { transferColors(merged, geometry); @@ -482,13 +505,12 @@ remeshMaterial.color.set(0xffffff); } else if (settings.paint.mode == 'texture') { geometry = unwrapMesh(geometry, settings.paint.resolution, settings.paint.gutter); - remeshMaterial.vertexColors = false; remeshMaterial.color.set(0xffffff); remeshMaterial.map = transferTexture(merged, geometry, settings.paint.resolution, settings.paint.gutter); } else { - remeshMaterial.vertexColors = false; remeshMaterial.color.set(0xdddddd); } + remeshMaterial.needsUpdate = true; // remesh material + wireframe overlay material From 88bbfaf79d1c90af5cf5a69d8e98bd9f7fd2ebd1 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 16 Jul 2026 20:46:07 -0700 Subject: [PATCH 09/41] remesher: Add initial scaffolding for remesher We'll likely have more functions and an enum with options in the future but we'll start with just a function. The output will use a flat triangle array, with an uncertain number of per-corner floats (might need to communicate extra data to establish corner ids). --- CMakeLists.txt | 1 + src/meshoptimizer.h | 5 +++++ src/remesher.cpp | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 src/remesher.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ba512d133..35047d154 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ set(SOURCES src/partition.cpp src/quantization.cpp src/rasterizer.cpp + src/remesher.cpp src/simplifier.cpp src/spatialorder.cpp src/stripifier.cpp diff --git a/src/meshoptimizer.h b/src/meshoptimizer.h index e6aa58eb4..38f5df0e2 100644 --- a/src/meshoptimizer.h +++ b/src/meshoptimizer.h @@ -512,6 +512,11 @@ enum */ MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options, float* result_error); +/** + * Experimental: Voxel remesher + */ +MESHOPTIMIZER_API size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int resolution, unsigned int options); + /** * Mesh simplifier with attribute metric * Reduces the number of triangles in the mesh, attempting to preserve mesh appearance as much as possible. diff --git a/src/remesher.cpp b/src/remesher.cpp new file mode 100644 index 000000000..9e14c3c62 --- /dev/null +++ b/src/remesher.cpp @@ -0,0 +1,28 @@ +// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details +#include "meshoptimizer.h" + +#include +#include +#include +#include + +#ifndef TRACE +#define TRACE 0 +#endif + +#if TRACE +#include +#endif + +// This work is based on: +// TODO + +size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int resolution, unsigned int options) +{ + assert(index_count % 3 == 0); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); + assert(vertex_positions_stride % sizeof(float) == 0); + assert(resolution > 0 && resolution <= 250); // TBD + + return 0; +} From c4f92642a8eef3f82b26e020ddcdc09be146939e Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 16 Jul 2026 21:14:55 -0700 Subject: [PATCH 10/41] remesher: Use input mesh bounds to map mesh to the grid For now we use resolution-2 as the target because we'll need the grid padding to be left in tact to simplify meshing, as the outer shell should be treated as empty; this might change in the future. --- src/remesher.cpp | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/remesher.cpp b/src/remesher.cpp index 9e14c3c62..1e73bb15c 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -16,13 +16,78 @@ // This work is based on: // TODO +namespace meshopt +{ + +static float measureGrid(const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, int resolution, float* out_offset) +{ + size_t vertex_stride_float = vertex_positions_stride / sizeof(float); + + float minv[3] = {FLT_MAX, FLT_MAX, FLT_MAX}; + float maxv[3] = {-FLT_MAX, -FLT_MAX, -FLT_MAX}; + + for (size_t i = 0; i < vertex_count; ++i) + { + const float* v = vertex_positions_data + i * vertex_stride_float; + + for (int j = 0; j < 3; ++j) + { + float vj = v[j]; + + minv[j] = minv[j] > vj ? vj : minv[j]; + maxv[j] = maxv[j] < vj ? vj : maxv[j]; + } + } + + float extent = 0.f; + + extent = (maxv[0] - minv[0]) < extent ? extent : (maxv[0] - minv[0]); + extent = (maxv[1] - minv[1]) < extent ? extent : (maxv[1] - minv[1]); + extent = (maxv[2] - minv[2]) < extent ? extent : (maxv[2] - minv[2]); + + // note: we rescale model extents to [0..resolution - 2], because the first and last voxel are used as empty padding + float scale = extent == 0 ? 0.f : (resolution - 2) / extent; + + out_offset[0] = minv[0]; + out_offset[1] = minv[1]; + out_offset[2] = minv[2]; + + return scale; +} + +static void voxelize(unsigned char* grid, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int resolution, float scale, const float offset[3]) +{ +} + +} // namespace meshopt size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int resolution, unsigned int options) { + using namespace meshopt; + assert(index_count % 3 == 0); assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); assert(resolution > 0 && resolution <= 250); // TBD + meshopt_Allocator allocator; + + // measure voxel grid to compute position => voxel mapping + float offset[3] = {}; + float scale = measureGrid(vertex_positions, vertex_count, vertex_positions_stride, resolution, offset); + + // rasterize triangles into the voxel grid: in the first pass, this simply tags occupied voxels + unsigned char* grid = allocator.allocate(size_t(resolution) * size_t(resolution) * size_t(resolution)); + memset(grid, 0, size_t(resolution) * size_t(resolution) * size_t(resolution)); + + voxelize(grid, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride, resolution, scale, offset); + +#if TRACE + size_t count = 0; + for (size_t i = 0; i < size_t(resolution) * size_t(resolution) * size_t(resolution); ++i) + count += grid[i] != 0; + printf("remesher: %zu voxels occupied\n", count); +#endif + return 0; } From 75c35cfbd08d8773633f9ac3abd0e5d67da0dad4 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Thu, 16 Jul 2026 21:52:35 -0700 Subject: [PATCH 11/41] remesher: Implement initial version of the voxelizer In this pass, we only tag the voxels that intersect any triangles. We will need more information in boundary voxels to be able to mesh the shape accurately, but that requires a lot more storage which should not scale as N^3. For the implementation, for now instead of doing precise box-triangle intersection tests, we estimate a barycentric sampling grid and splat the points into voxels instead. This should guarantee hole-free voxelization as long as the point density is high enough that the distance between the two points is never more than a voxel's edge along any axis. For thin triangles, we currently over-sample which might be unreasonably slow; this and some other inefficiencies could be fixed later. --- src/remesher.cpp | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/remesher.cpp b/src/remesher.cpp index 1e73bb15c..9a2b688e8 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -57,6 +57,58 @@ static float measureGrid(const float* vertex_positions_data, size_t vertex_count static void voxelize(unsigned char* grid, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int resolution, float scale, const float offset[3]) { + (void)vertex_count; + + size_t vertex_stride_float = vertex_positions_stride / sizeof(float); + + for (size_t i = 0; i < index_count; i += 3) + { + unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2]; + assert(a < vertex_count && b < vertex_count && c < vertex_count); + + const float* va = vertex_positions + a * vertex_stride_float; + const float* vb = vertex_positions + b * vertex_stride_float; + const float* vc = vertex_positions + c * vertex_stride_float; + + float ex = vb[0] - va[0], ey = vb[1] - va[1], ez = vb[2] - va[2]; + float fx = vc[0] - va[0], fy = vc[1] - va[1], fz = vc[2] - va[2]; + float gx = vc[0] - vb[0], gy = vc[1] - vb[1], gz = vc[2] - vb[2]; + + // use maximum edge length to establish sampling rate + // TODO: this is wasteful for thin triangles + float el = sqrtf(ex * ex + ey * ey + ez * ez); + float fl = sqrtf(fx * fx + fy * fy + fz * fz); + float gl = sqrtf(gx * gx + gy * gy + gz * gz); + + float max_edge = el > fl ? el : fl; + max_edge = max_edge > gl ? max_edge : gl; + + // we target 2 samples per voxel edge which should be enough to hit all voxels at any rotation + int samples = int(max_edge * scale * 2.f); + samples = samples > 1 ? samples : 1; + samples = samples < resolution * 2 ? samples : resolution * 2; + + float sx = va[0] - offset[0], sy = va[1] - offset[1], sz = va[2] - offset[2]; + float sr = 1.f / float(samples); + + for (int u = 0; u <= samples; ++u) + for (int v = 0; v <= samples - u; ++v) + { + float su = float(u) * sr, sv = float(v) * sr; + int x = int((sx + su * ex + sv * fx) * scale); + int y = int((sy + su * ey + sv * fy) * scale); + int z = int((sz + su * ez + sv * fz) * scale); + + // safety: rounding errors and non-finite inputs may produce out of bounds coordinates, so we clamp them + // TODO: codegen + x = (x < 0) ? 0 : (x > resolution - 2 ? resolution - 2 : x); + y = (y < 0) ? 0 : (y > resolution - 2 ? resolution - 2 : y); + z = (z < 0) ? 0 : (z > resolution - 2 ? resolution - 2 : z); + + size_t index = (x + 1) + size_t(resolution) * ((y + 1) + size_t(resolution) * (z + 1)); + grid[index] = 1; + } + } } } // namespace meshopt From d6b4ce64a638910711324b0b1e7befe988de4eba Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 17 Jul 2026 00:26:40 -0700 Subject: [PATCH 12/41] remesher: Initial implementation of Marching Cubes We use a somewhat unconventional implementation of Marching Cubes, where instead of interpolating the point on the edge, we take the value from the occupied point as is. This will need to change a bit as we add actual voxel solve, but this is necessary because the voxels we use for occupancy tracking are also going to be the unit of positional accumulation, so we don't really have data in non-occupied voxels. This doesn't yet produce a coherent mesh due to some occupancy connectivity issues, but we're getting closer. Because of this construction we produce many degenerate triangles. We could use positions to filter them out, but positions may not be available if the destination buffer is NULL, so we use vertex codes instead. --- src/remesher.cpp | 218 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 217 insertions(+), 1 deletion(-) diff --git a/src/remesher.cpp b/src/remesher.cpp index 9a2b688e8..2fe181433 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -15,10 +15,150 @@ #endif // This work is based on: +// Paul Bourke. Polygonising a scalar field. 1994 // TODO namespace meshopt { +static const signed char kTriangleTable[256][16] = { + {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1}, + {3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1}, + {3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1}, + {3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1}, {9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1}, + {1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1}, + {9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1}, {2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1}, + {8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1}, + {9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1}, {4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1}, + {3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1}, {1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1}, + {4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1}, {4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1}, + {9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1}, + {1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1}, + {5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1}, {2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1}, + {9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1}, + {0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1}, {2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1}, + {10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1}, {4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1}, + {5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1}, {5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1}, + {9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1}, + {0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1}, {1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1}, {10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1}, + {8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1}, {2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1}, + {7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1}, {9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1}, + {2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1}, {11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1}, + {9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1}, {5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1}, + {11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1}, {11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1}, + {1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1}, + {9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1}, {5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1}, + {2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1}, + {0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1}, {5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1}, + {6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1}, + {3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1}, {6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1}, + {5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1}, + {1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1}, {10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1}, + {6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1}, {1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1}, + {8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1}, {7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1}, + {3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1}, {5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1}, + {0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1}, {9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1}, + {8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1}, {5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1}, + {0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1}, {6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1}, + {10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1}, + {10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1}, {8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1}, + {1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1}, + {0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1}, + {10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1}, + {3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1}, {6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1}, + {9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1}, {8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1}, + {3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1}, {6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1}, {0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1}, + {10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1}, {10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1}, + {1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1}, {2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1}, + {7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1}, {7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1}, {2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1}, + {1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1}, {11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1}, + {8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1}, {0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1}, {7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1}, + {10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1}, + {2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1}, {6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1}, + {7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1}, + {2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1}, {1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1}, + {10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1}, {10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1}, + {0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1}, {7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1}, + {6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1}, + {8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1}, {9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1}, + {6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1}, {1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1}, + {4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1}, {10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1}, + {8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1}, {0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1}, {1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1}, + {8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1}, {10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1}, + {4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1}, {10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1}, + {5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1}, {11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1}, + {9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1}, {6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1}, + {7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1}, {3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1}, + {7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1}, {9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1}, + {3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1}, {6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1}, + {9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1}, {1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1}, + {4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1}, {7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1}, + {6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1}, {3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1}, + {0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1}, {6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1}, + {1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1}, {0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1}, + {11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1}, {6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1}, + {5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1}, {9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1}, + {1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1}, {1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1}, {10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1}, + {0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1}, + {5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1}, {10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1}, + {11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1}, + {9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1}, {7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1}, + {2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1}, {8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1}, + {9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1}, {9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1}, + {1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1}, + {9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1}, {9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1}, {5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1}, + {0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1}, {10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1}, + {2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1}, {0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1}, + {0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1}, {9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1}, {5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1}, + {3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1}, {5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1}, + {8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1}, {0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1}, {9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1}, + {1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1}, {3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1}, + {4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1}, {9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1}, + {11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1}, {11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1}, + {2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1}, {9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1}, + {3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1}, {1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1}, {4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1}, + {4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1}, + {0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1}, {3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1}, + {0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1}, {9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1}, {1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, + {0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}}; + +// corner index per Bourke's indexing (must match table above!) +static const unsigned char kCornerX[8] = {0, 1, 1, 0, 0, 1, 1, 0}; +static const unsigned char kCornerY[8] = {0, 0, 0, 0, 1, 1, 1, 1}; +static const unsigned char kCornerZ[8] = {0, 0, 1, 1, 0, 0, 1, 1}; + +// edge index per Bourke's indexing (must match table above!) +static const unsigned char kEdgeA[12] = {0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3}; +static const unsigned char kEdgeB[12] = {1, 2, 3, 0, 5, 6, 7, 4, 4, 5, 6, 7}; + static float measureGrid(const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, int resolution, float* out_offset) { size_t vertex_stride_float = vertex_positions_stride / sizeof(float); @@ -111,6 +251,76 @@ static void voxelize(unsigned char* grid, const unsigned int* indices, size_t in } } +static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int edge, const unsigned char* grid, int resolution, float scale, const float offset[3]) +{ + int a = kEdgeA[edge], b = kEdgeB[edge]; + int ax = kCornerX[a], ay = kCornerY[a], az = kCornerZ[a]; + int bx = kCornerX[b], by = kCornerY[b], bz = kCornerZ[b]; + + // TODO: store directional edges in the table so that we know which corner is occupied + unsigned char ag = grid[(x + ax) + size_t(resolution) * ((y + ay) + size_t(resolution) * (z + az))]; + unsigned char bg = grid[(x + bx) + size_t(resolution) * ((y + by) + size_t(resolution) * (z + bz))]; + assert((ag != 0) + (bg != 0) == 1); // one of the corners must be occupied + + int dx = (ag == 0) ? bx : ax; + int dy = (ag == 0) ? by : ay; + int dz = (ag == 0) ? bz : az; + + destination[index * 3 + 0] = (x + dx + 0.5f) / scale + offset[0]; + destination[index * 3 + 1] = (y + dy + 0.5f) / scale + offset[1]; + destination[index * 3 + 2] = (z + dz + 0.5f) / scale + offset[2]; + + return (x + dx) + size_t(resolution) * ((y + dy) + size_t(resolution) * (z + dz)); +} + +static size_t polygonize(float* destination, size_t max_triangle_count, const unsigned char* grid, int resolution, float scale, const float offset[3]) +{ + size_t result = 0; + + for (int z = 0; z < resolution - 1; ++z) + for (int y = 0; y < resolution - 1; ++y) + for (int x = 0; x < resolution - 1; ++x) + { + int cube = 0; + + for (int i = 0; i < 8; ++i) + { + int ix = x + kCornerX[i]; + int iy = y + kCornerY[i]; + int iz = z + kCornerZ[i]; + + size_t index = ix + size_t(resolution) * (iy + size_t(resolution) * iz); + cube |= (grid[index] != 0) << i; + } + + if (cube == 0 || cube == 0xff) + continue; + + const signed char* tris = kTriangleTable[cube]; + + for (int i = 0; tris[i] != -1; i += 3) + { + int a = tris[i + 0]; + int b = tris[i + 1]; + int c = tris[i + 2]; + bool valid = true; + + if (destination && result < max_triangle_count) + { + size_t ca = emitVertex(destination, result * 3 + 0, x, y, z, a, grid, resolution, scale, offset); + size_t cb = emitVertex(destination, result * 3 + 1, x, y, z, b, grid, resolution, scale, offset); + size_t cc = emitVertex(destination, result * 3 + 2, x, y, z, c, grid, resolution, scale, offset); + + valid = (ca != cb) && (cb != cc) && (cc != ca); // degenerate triangle + } + + result += valid; + } + } + + return result; +} + } // namespace meshopt size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int resolution, unsigned int options) @@ -141,5 +351,11 @@ size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsig printf("remesher: %zu voxels occupied\n", count); #endif - return 0; + size_t result = polygonize(destination, max_triangle_count, grid, resolution, scale, offset); + +#if TRACE + printf("remesher: %zu triangles (%zu capacity)\n", result, max_triangle_count); +#endif + + return result; } From f78cb463eb07b6012508df5e7bec5a6a0fd33525 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 17 Jul 2026 01:11:33 -0700 Subject: [PATCH 13/41] remesher: Implement voxel position accumulation Instead of using voxel grid coordinates for the output vertices, we now accumulate triangle samples into each occupied voxel and use the resulting position during meshing. To avoid storing N^3 voxels, we compact the data and replace the original binary occupancy with 1-255 row offsets; since resolution is limited, each row can have at most 255 occupied voxels. Because we do not track the triangle indices per voxel, to accumulate the data we need to run voxelization again, using the same code and the same source triangles. It's important that this results in the same samples, otherwise the addressing logic may break and we'll end up overwriting data from other rows. --- src/remesher.cpp | 106 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/src/remesher.cpp b/src/remesher.cpp index 2fe181433..6853ab69b 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -159,6 +159,12 @@ static const unsigned char kCornerZ[8] = {0, 0, 1, 1, 0, 0, 1, 1}; static const unsigned char kEdgeA[12] = {0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3}; static const unsigned char kEdgeB[12] = {1, 2, 3, 0, 5, 6, 7, 4, 4, 5, 6, 7}; +struct Voxel +{ + float px, py, pz; + float w; +}; + static float measureGrid(const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, int resolution, float* out_offset) { size_t vertex_stride_float = vertex_positions_stride / sizeof(float); @@ -195,7 +201,7 @@ static float measureGrid(const float* vertex_positions_data, size_t vertex_count return scale; } -static void voxelize(unsigned char* grid, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int resolution, float scale, const float offset[3]) +static void voxelize(unsigned char* grid, Voxel* voxels, const unsigned int* voxel_rows, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, int resolution, float scale, const float offset[3]) { (void)vertex_count; @@ -235,9 +241,13 @@ static void voxelize(unsigned char* grid, const unsigned int* indices, size_t in for (int v = 0; v <= samples - u; ++v) { float su = float(u) * sr, sv = float(v) * sr; - int x = int((sx + su * ex + sv * fx) * scale); - int y = int((sy + su * ey + sv * fy) * scale); - int z = int((sz + su * ez + sv * fz) * scale); + float px = sx + su * ex + sv * fx; + float py = sy + su * ey + sv * fy; + float pz = sz + su * ez + sv * fz; + + int x = int(px * scale); + int y = int(py * scale); + int z = int(pz * scale); // safety: rounding errors and non-finite inputs may produce out of bounds coordinates, so we clamp them // TODO: codegen @@ -245,13 +255,28 @@ static void voxelize(unsigned char* grid, const unsigned int* indices, size_t in y = (y < 0) ? 0 : (y > resolution - 2 ? resolution - 2 : y); z = (z < 0) ? 0 : (z > resolution - 2 ? resolution - 2 : z); - size_t index = (x + 1) + size_t(resolution) * ((y + 1) + size_t(resolution) * (z + 1)); - grid[index] = 1; + size_t row = (y + 1) + size_t(resolution) * (z + 1); + size_t idx = (x + 1) + size_t(resolution) * row; + + if (voxels) + { + assert(grid[idx]); + Voxel& vox = voxels[voxel_rows[row] + (grid[idx] - 1)]; + + vox.px += px; + vox.py += py; + vox.pz += pz; + vox.w += 1.f; + } + else + { + grid[idx] = 1; + } } } } -static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int edge, const unsigned char* grid, int resolution, float scale, const float offset[3]) +static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int edge, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3]) { int a = kEdgeA[edge], b = kEdgeB[edge]; int ax = kCornerX[a], ay = kCornerY[a], az = kCornerZ[a]; @@ -266,14 +291,23 @@ static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int dy = (ag == 0) ? by : ay; int dz = (ag == 0) ? bz : az; - destination[index * 3 + 0] = (x + dx + 0.5f) / scale + offset[0]; - destination[index * 3 + 1] = (y + dy + 0.5f) / scale + offset[1]; - destination[index * 3 + 2] = (z + dz + 0.5f) / scale + offset[2]; + size_t row = (y + dy) + size_t(resolution) * (z + dz); + size_t idx = (x + dx) + size_t(resolution) * row; + + if (voxels) + { + assert(grid[idx]); + const Voxel& vox = voxels[voxel_rows[row] + (grid[idx] - 1)]; + + destination[index * 3 + 0] = vox.px / vox.w + offset[0]; + destination[index * 3 + 1] = vox.py / vox.w + offset[1]; + destination[index * 3 + 2] = vox.pz / vox.w + offset[2]; + } - return (x + dx) + size_t(resolution) * ((y + dy) + size_t(resolution) * (z + dz)); + return idx; } -static size_t polygonize(float* destination, size_t max_triangle_count, const unsigned char* grid, int resolution, float scale, const float offset[3]) +static size_t polygonize(float* destination, size_t max_triangle_count, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3]) { size_t result = 0; @@ -307,9 +341,9 @@ static size_t polygonize(float* destination, size_t max_triangle_count, const un if (destination && result < max_triangle_count) { - size_t ca = emitVertex(destination, result * 3 + 0, x, y, z, a, grid, resolution, scale, offset); - size_t cb = emitVertex(destination, result * 3 + 1, x, y, z, b, grid, resolution, scale, offset); - size_t cc = emitVertex(destination, result * 3 + 2, x, y, z, c, grid, resolution, scale, offset); + size_t ca = emitVertex(destination, result * 3 + 0, x, y, z, a, grid, voxels, voxel_rows, resolution, scale, offset); + size_t cb = emitVertex(destination, result * 3 + 1, x, y, z, b, grid, voxels, voxel_rows, resolution, scale, offset); + size_t cc = emitVertex(destination, result * 3 + 2, x, y, z, c, grid, voxels, voxel_rows, resolution, scale, offset); valid = (ca != cb) && (cb != cc) && (cc != ca); // degenerate triangle } @@ -342,16 +376,46 @@ size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsig unsigned char* grid = allocator.allocate(size_t(resolution) * size_t(resolution) * size_t(resolution)); memset(grid, 0, size_t(resolution) * size_t(resolution) * size_t(resolution)); - voxelize(grid, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride, resolution, scale, offset); + voxelize(grid, NULL, NULL, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride, resolution, scale, offset); + + // allocate additional voxel data for each occupied voxel; this can be filled in the second pass to compute positions + // note that we only do this if we need to compute output triangles; counting runs skip it for performance + Voxel* voxels = NULL; + unsigned int* voxel_rows = NULL; + size_t voxel_count = 0; + + if (destination) + { + voxel_rows = allocator.allocate(size_t(resolution) * size_t(resolution)); + + for (size_t i = 0; i < size_t(resolution) * size_t(resolution); ++i) + { + unsigned char* row = grid + i * size_t(resolution); + + int count = 0; + for (int j = 0; j < resolution; ++j) + if (row[j] != 0) + row[j] = (unsigned char)++count; + + assert(count < 256); // we store offsets in a single byte, with 0 reserved for empty voxels + + voxel_rows[i] = unsigned(voxel_count); + voxel_count += count; + } + + voxels = allocator.allocate(voxel_count); + memset(voxels, 0, voxel_count * sizeof(Voxel)); #if TRACE - size_t count = 0; - for (size_t i = 0; i < size_t(resolution) * size_t(resolution) * size_t(resolution); ++i) - count += grid[i] != 0; - printf("remesher: %zu voxels occupied\n", count); + printf("remesher: %zu voxels occupied\n", voxel_count); #endif + } + + // accumulate voxel positions: in the second pass, this computes enough data in each voxel to calculate positions + if (voxels) + voxelize(grid, voxels, voxel_rows, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride, resolution, scale, offset); - size_t result = polygonize(destination, max_triangle_count, grid, resolution, scale, offset); + size_t result = polygonize(destination, max_triangle_count, grid, voxels, voxel_rows, resolution, scale, offset); #if TRACE printf("remesher: %zu triangles (%zu capacity)\n", result, max_triangle_count); From 4d0f34782d4210a2cf7814bf5a61e10e3e2123ac Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 17 Jul 2026 09:06:02 -0700 Subject: [PATCH 14/41] remesher: Fix triangle capacity accounting to exclude degenerates Previously, we were disabling degenerate triangle filtering when destination buffer was not provided; this resulted in the initial call reporting a higher number of triangles vs the actual number that was returned after. This is perhaps not critical and we might need to go back to the approximate capacity in the future if this helps with performance, but for now this results in more confusing behavior so we now use triangle codes to reject degenerate triangles regardless of whether positions are computed. --- src/remesher.cpp | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/remesher.cpp b/src/remesher.cpp index 6853ab69b..abd52d13e 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -294,7 +294,7 @@ static size_t emitVertex(float* destination, size_t index, int x, int y, int z, size_t row = (y + dy) + size_t(resolution) * (z + dz); size_t idx = (x + dx) + size_t(resolution) * row; - if (voxels) + if (destination) { assert(grid[idx]); const Voxel& vox = voxels[voxel_rows[row] + (grid[idx] - 1)]; @@ -334,21 +334,17 @@ static size_t polygonize(float* destination, size_t max_triangle_count, const un for (int i = 0; tris[i] != -1; i += 3) { - int a = tris[i + 0]; - int b = tris[i + 1]; - int c = tris[i + 2]; - bool valid = true; + int ea = tris[i + 0], eb = tris[i + 1], ec = tris[i + 2]; - if (destination && result < max_triangle_count) - { - size_t ca = emitVertex(destination, result * 3 + 0, x, y, z, a, grid, voxels, voxel_rows, resolution, scale, offset); - size_t cb = emitVertex(destination, result * 3 + 1, x, y, z, b, grid, voxels, voxel_rows, resolution, scale, offset); - size_t cc = emitVertex(destination, result * 3 + 2, x, y, z, c, grid, voxels, voxel_rows, resolution, scale, offset); + // note: we only emit the triangle if we have space for it, but we reject degenerate triangles purely based on vertex codes + // this results in consistent capacity estimation, as result advances the same way regardless of whether triangle data is written + float* target = (destination && result < max_triangle_count) ? destination : NULL; - valid = (ca != cb) && (cb != cc) && (cc != ca); // degenerate triangle - } + size_t ca = emitVertex(target, result * 3 + 0, x, y, z, ea, grid, voxels, voxel_rows, resolution, scale, offset); + size_t cb = emitVertex(target, result * 3 + 1, x, y, z, eb, grid, voxels, voxel_rows, resolution, scale, offset); + size_t cc = emitVertex(target, result * 3 + 2, x, y, z, ec, grid, voxels, voxel_rows, resolution, scale, offset); - result += valid; + result += (ca != cb) && (cb != cc) && (cc != ca); // degenerate triangle } } From 8f56f4e4a9518fe4d1cc97b763cd76fbf6e540e1 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 17 Jul 2026 10:38:19 -0700 Subject: [PATCH 15/41] remesher: Flip polygonization polarity While it's canonical and correct to classify occupied voxels as 1 during the MC lookup, this is resulting in some holes in diagonal sheet surfaces; this appears to be due to ambiguous cases in the MC meshing being resolved in a particular way. Interestingly, flipping the categorization (treating occupied as 0 and empty as 1) fixes these gaps. Not sure if this is a durable fix and if we'll need further changes to the tables, but for now flip this - and also flip the triangle winding to maintain consistent orientation. --- src/remesher.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/remesher.cpp b/src/remesher.cpp index abd52d13e..5b9119bb6 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -324,7 +324,7 @@ static size_t polygonize(float* destination, size_t max_triangle_count, const un int iz = z + kCornerZ[i]; size_t index = ix + size_t(resolution) * (iy + size_t(resolution) * iz); - cube |= (grid[index] != 0) << i; + cube |= (grid[index] == 0) << i; } if (cube == 0 || cube == 0xff) @@ -334,7 +334,7 @@ static size_t polygonize(float* destination, size_t max_triangle_count, const un for (int i = 0; tris[i] != -1; i += 3) { - int ea = tris[i + 0], eb = tris[i + 1], ec = tris[i + 2]; + int ea = tris[i + 0], eb = tris[i + 2], ec = tris[i + 1]; // note: we only emit the triangle if we have space for it, but we reject degenerate triangles purely based on vertex codes // this results in consistent capacity estimation, as result advances the same way regardless of whether triangle data is written From 4e6a87439e6dd62cecbb34ea03a32644e3d0667e Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 17 Jul 2026 11:15:09 -0700 Subject: [PATCH 16/41] remesher: Reindex tables to remove redundant indirections and flips Instead of using original Bourke tables as is, we reindex them so that vertex codes map to the more intuitive XYZ bit order (or rather ZYX). Instead of storing edge ids and remapping them back to vertex ids, we embed this directly into the table, using nibbles to signify the edge. This is valuable because it allows us to orient each edge such that its first vertex always points at the occupied voxel. Also apply another triangle winding flip; our previous corner data was actually incorrectly indexed and flipped Y & Z, these tables should more clearly reflect original data. --- src/remesher.cpp | 302 ++++++++++++++++++++++------------------------- 1 file changed, 144 insertions(+), 158 deletions(-) diff --git a/src/remesher.cpp b/src/remesher.cpp index 5b9119bb6..2171c05ea 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -20,144 +20,137 @@ namespace meshopt { -static const signed char kTriangleTable[256][16] = { - {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1}, - {3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1}, - {3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1}, - {3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1}, {9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1}, - {1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1}, - {9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1}, {2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1}, - {8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1}, - {9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1}, {4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1}, - {3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1}, {1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1}, - {4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1}, {4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1}, - {9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1}, - {1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1}, - {5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1}, {2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1}, - {9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1}, - {0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1}, {2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1}, - {10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1}, {4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1}, - {5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1}, {5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1}, - {9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1}, - {0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1}, {1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1}, {10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1}, - {8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1}, {2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1}, - {7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1}, {9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1}, - {2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1}, {11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1}, - {9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1}, {5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1}, - {11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1}, {11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1}, - {1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1}, - {9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1}, {5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1}, - {2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1}, - {0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1}, {5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1}, - {6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1}, - {3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1}, {6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1}, - {5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1}, - {1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1}, {10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1}, - {6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1}, {1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1}, - {8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1}, {7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1}, - {3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1}, {5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1}, - {0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1}, {9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1}, - {8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1}, {5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1}, - {0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1}, {6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1}, - {10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1}, - {10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1}, {8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1}, - {1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1}, - {0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1}, - {10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1}, - {3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1}, {6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1}, - {9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1}, {8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1}, - {3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1}, {6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1}, {0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1}, - {10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1}, {10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1}, - {1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1}, {2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1}, - {7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1}, {7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1}, {2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1}, - {1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1}, {11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1}, - {8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1}, {0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1}, {7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1}, - {10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1}, - {2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1}, {6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1}, - {7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1}, - {2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1}, {1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1}, - {10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1}, {10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1}, - {0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1}, {7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1}, - {6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1}, - {8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1}, {9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1}, - {6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1}, {1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1}, - {4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1}, {10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1}, - {8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1}, {0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1}, {1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1}, - {8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1}, {10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1}, - {4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1}, {10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1}, - {5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1}, {11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1}, - {9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1}, {6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1}, - {7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1}, {3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1}, - {7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1}, {9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1}, - {3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1}, {6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1}, - {9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1}, {1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1}, - {4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1}, {7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1}, - {6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1}, {3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1}, - {0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1}, {6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1}, - {1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1}, {0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1}, - {11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1}, {6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1}, - {5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1}, {9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1}, - {1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1}, {1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1}, {10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1}, - {0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1}, - {5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1}, {10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1}, - {11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1}, - {9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1}, {7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1}, - {2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1}, {8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1}, - {9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1}, {9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1}, - {1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1}, - {9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1}, {9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1}, {5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1}, - {0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1}, {10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1}, - {2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1}, {0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1}, - {0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1}, {9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1}, {5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1}, - {3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1}, {5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1}, - {8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1}, {0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1}, {9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1}, {0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1}, - {1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1}, {3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1}, - {4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1}, {9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1}, - {11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1}, {11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1}, - {2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1}, {9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1}, - {3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1}, {1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1}, {4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1}, - {4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1}, - {0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1}, {3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1}, {3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1}, - {0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1}, {9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1}, {1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, - {0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}}; - -// corner index per Bourke's indexing (must match table above!) -static const unsigned char kCornerX[8] = {0, 1, 1, 0, 0, 1, 1, 0}; -static const unsigned char kCornerY[8] = {0, 0, 0, 0, 1, 1, 1, 1}; -static const unsigned char kCornerZ[8] = {0, 0, 1, 1, 0, 0, 1, 1}; - -// edge index per Bourke's indexing (must match table above!) -static const unsigned char kEdgeA[12] = {0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3}; -static const unsigned char kEdgeB[12] = {1, 2, 3, 0, 5, 6, 7, 4, 4, 5, 6, 7}; +// the tables are taken from Bourke's source data, but each triangle corner is encoded as an edge with two nibbles corresponding to vertex codes +// additionally, vertex codes have been renumbered to match the bit order XYZ (e.g. 3 maps to X=1 Y=1 Z=0) +static const unsigned char kTriangleTable[256][16] = { + {}, {0x10, 0x40, 0x20}, + {0x01, 0x31, 0x51}, {0x31, 0x40, 0x20, 0x51, 0x40, 0x31}, + {0x02, 0x62, 0x32}, {0x10, 0x62, 0x32, 0x40, 0x62, 0x10}, + {0x31, 0x51, 0x01, 0x32, 0x02, 0x62}, {0x31, 0x62, 0x32, 0x31, 0x51, 0x62, 0x51, 0x40, 0x62}, + {0x13, 0x23, 0x73}, {0x10, 0x40, 0x20, 0x13, 0x23, 0x73}, + {0x51, 0x23, 0x73, 0x01, 0x23, 0x51}, {0x23, 0x40, 0x20, 0x23, 0x73, 0x40, 0x73, 0x51, 0x40}, + {0x02, 0x73, 0x13, 0x62, 0x73, 0x02}, {0x10, 0x73, 0x13, 0x10, 0x40, 0x73, 0x40, 0x62, 0x73}, + {0x02, 0x51, 0x01, 0x02, 0x62, 0x51, 0x62, 0x73, 0x51}, {0x51, 0x40, 0x73, 0x73, 0x40, 0x62}, + {0x54, 0x64, 0x04}, {0x54, 0x20, 0x10, 0x64, 0x20, 0x54}, + {0x01, 0x31, 0x51, 0x04, 0x54, 0x64}, {0x54, 0x31, 0x51, 0x54, 0x64, 0x31, 0x64, 0x20, 0x31}, + {0x04, 0x54, 0x64, 0x02, 0x62, 0x32}, {0x62, 0x54, 0x64, 0x62, 0x32, 0x54, 0x32, 0x10, 0x54}, + {0x51, 0x01, 0x31, 0x04, 0x54, 0x64, 0x32, 0x02, 0x62}, {0x54, 0x64, 0x62, 0x51, 0x54, 0x62, 0x51, 0x62, 0x32, 0x51, 0x32, 0x31}, + {0x13, 0x23, 0x73, 0x04, 0x54, 0x64}, {0x20, 0x54, 0x64, 0x20, 0x10, 0x54, 0x13, 0x23, 0x73}, + {0x51, 0x23, 0x73, 0x51, 0x01, 0x23, 0x04, 0x54, 0x64}, {0x23, 0x73, 0x51, 0x23, 0x51, 0x64, 0x23, 0x64, 0x20, 0x64, 0x51, 0x54}, + {0x02, 0x73, 0x13, 0x02, 0x62, 0x73, 0x64, 0x04, 0x54}, {0x13, 0x62, 0x73, 0x13, 0x54, 0x62, 0x13, 0x10, 0x54, 0x64, 0x62, 0x54}, + {0x54, 0x64, 0x04, 0x51, 0x01, 0x62, 0x51, 0x62, 0x73, 0x62, 0x01, 0x02}, {0x54, 0x64, 0x62, 0x54, 0x62, 0x51, 0x51, 0x62, 0x73}, + {0x15, 0x75, 0x45}, {0x15, 0x75, 0x45, 0x10, 0x40, 0x20}, + {0x01, 0x75, 0x45, 0x31, 0x75, 0x01}, {0x40, 0x75, 0x45, 0x40, 0x20, 0x75, 0x20, 0x31, 0x75}, + {0x15, 0x75, 0x45, 0x32, 0x02, 0x62}, {0x10, 0x62, 0x32, 0x10, 0x40, 0x62, 0x45, 0x15, 0x75}, + {0x01, 0x75, 0x45, 0x01, 0x31, 0x75, 0x32, 0x02, 0x62}, {0x32, 0x31, 0x75, 0x32, 0x75, 0x40, 0x32, 0x40, 0x62, 0x45, 0x40, 0x75}, + {0x13, 0x23, 0x73, 0x15, 0x75, 0x45}, {0x20, 0x10, 0x40, 0x13, 0x23, 0x73, 0x45, 0x15, 0x75}, + {0x75, 0x23, 0x73, 0x75, 0x45, 0x23, 0x45, 0x01, 0x23}, {0x23, 0x73, 0x75, 0x20, 0x23, 0x75, 0x20, 0x75, 0x45, 0x20, 0x45, 0x40}, + {0x73, 0x02, 0x62, 0x73, 0x13, 0x02, 0x15, 0x75, 0x45}, {0x45, 0x15, 0x75, 0x10, 0x40, 0x13, 0x40, 0x73, 0x13, 0x40, 0x62, 0x73}, + {0x75, 0x45, 0x01, 0x75, 0x01, 0x62, 0x75, 0x62, 0x73, 0x62, 0x01, 0x02}, {0x75, 0x45, 0x40, 0x75, 0x40, 0x73, 0x73, 0x40, 0x62}, + {0x15, 0x64, 0x04, 0x75, 0x64, 0x15}, {0x15, 0x20, 0x10, 0x15, 0x75, 0x20, 0x75, 0x64, 0x20}, + {0x01, 0x64, 0x04, 0x01, 0x31, 0x64, 0x31, 0x75, 0x64}, {0x31, 0x75, 0x20, 0x20, 0x75, 0x64}, + {0x64, 0x15, 0x75, 0x64, 0x04, 0x15, 0x02, 0x62, 0x32}, {0x15, 0x75, 0x64, 0x15, 0x64, 0x32, 0x15, 0x32, 0x10, 0x32, 0x64, 0x62}, + {0x32, 0x02, 0x62, 0x01, 0x31, 0x04, 0x31, 0x64, 0x04, 0x31, 0x75, 0x64}, {0x62, 0x32, 0x31, 0x62, 0x31, 0x64, 0x64, 0x31, 0x75}, + {0x15, 0x64, 0x04, 0x15, 0x75, 0x64, 0x73, 0x13, 0x23}, {0x73, 0x13, 0x23, 0x15, 0x75, 0x10, 0x75, 0x20, 0x10, 0x75, 0x64, 0x20}, + {0x04, 0x01, 0x23, 0x04, 0x23, 0x75, 0x04, 0x75, 0x64, 0x73, 0x75, 0x23}, {0x23, 0x73, 0x75, 0x23, 0x75, 0x20, 0x20, 0x75, 0x64}, + {0x15, 0x75, 0x04, 0x04, 0x75, 0x64, 0x73, 0x13, 0x02, 0x73, 0x02, 0x62}, {0x75, 0x64, 0x10, 0x75, 0x10, 0x15, 0x64, 0x62, 0x10, 0x13, 0x10, 0x73, 0x62, 0x73, 0x10}, + {0x62, 0x73, 0x01, 0x62, 0x01, 0x02, 0x73, 0x75, 0x01, 0x04, 0x01, 0x64, 0x75, 0x64, 0x01}, {0x62, 0x73, 0x75, 0x64, 0x62, 0x75}, + {0x46, 0x76, 0x26}, {0x20, 0x10, 0x40, 0x26, 0x46, 0x76}, + {0x01, 0x31, 0x51, 0x26, 0x46, 0x76}, {0x40, 0x31, 0x51, 0x40, 0x20, 0x31, 0x26, 0x46, 0x76}, + {0x46, 0x32, 0x02, 0x76, 0x32, 0x46}, {0x46, 0x10, 0x40, 0x46, 0x76, 0x10, 0x76, 0x32, 0x10}, + {0x32, 0x46, 0x76, 0x32, 0x02, 0x46, 0x01, 0x31, 0x51}, {0x31, 0x76, 0x32, 0x31, 0x40, 0x76, 0x31, 0x51, 0x40, 0x40, 0x46, 0x76}, + {0x73, 0x13, 0x23, 0x76, 0x26, 0x46}, {0x13, 0x23, 0x73, 0x20, 0x10, 0x40, 0x76, 0x26, 0x46}, + {0x23, 0x51, 0x01, 0x23, 0x73, 0x51, 0x76, 0x26, 0x46}, {0x76, 0x26, 0x46, 0x23, 0x73, 0x20, 0x73, 0x40, 0x20, 0x73, 0x51, 0x40}, + {0x73, 0x46, 0x76, 0x73, 0x13, 0x46, 0x13, 0x02, 0x46}, {0x73, 0x46, 0x76, 0x13, 0x46, 0x73, 0x13, 0x40, 0x46, 0x13, 0x10, 0x40}, + {0x01, 0x02, 0x46, 0x01, 0x46, 0x73, 0x01, 0x73, 0x51, 0x76, 0x73, 0x46}, {0x46, 0x76, 0x73, 0x46, 0x73, 0x40, 0x40, 0x73, 0x51}, + {0x76, 0x04, 0x54, 0x26, 0x04, 0x76}, {0x20, 0x76, 0x26, 0x20, 0x10, 0x76, 0x10, 0x54, 0x76}, + {0x04, 0x76, 0x26, 0x04, 0x54, 0x76, 0x51, 0x01, 0x31}, {0x51, 0x54, 0x76, 0x51, 0x76, 0x20, 0x51, 0x20, 0x31, 0x26, 0x20, 0x76}, + {0x04, 0x32, 0x02, 0x04, 0x54, 0x32, 0x54, 0x76, 0x32}, {0x10, 0x54, 0x32, 0x54, 0x76, 0x32}, + {0x31, 0x51, 0x01, 0x32, 0x02, 0x54, 0x32, 0x54, 0x76, 0x54, 0x02, 0x04}, {0x31, 0x51, 0x54, 0x31, 0x54, 0x32, 0x32, 0x54, 0x76}, + {0x76, 0x04, 0x54, 0x76, 0x26, 0x04, 0x23, 0x73, 0x13}, {0x13, 0x23, 0x73, 0x20, 0x10, 0x26, 0x10, 0x76, 0x26, 0x10, 0x54, 0x76}, + {0x54, 0x26, 0x04, 0x54, 0x76, 0x26, 0x01, 0x23, 0x51, 0x23, 0x73, 0x51}, {0x73, 0x51, 0x20, 0x73, 0x20, 0x23, 0x51, 0x54, 0x20, 0x26, 0x20, 0x76, 0x54, 0x76, 0x20}, + {0x04, 0x13, 0x02, 0x04, 0x76, 0x13, 0x04, 0x54, 0x76, 0x76, 0x73, 0x13}, {0x73, 0x13, 0x10, 0x73, 0x10, 0x76, 0x76, 0x10, 0x54}, + {0x54, 0x76, 0x02, 0x54, 0x02, 0x04, 0x76, 0x73, 0x02, 0x01, 0x02, 0x51, 0x73, 0x51, 0x02}, {0x73, 0x51, 0x54, 0x76, 0x73, 0x54}, + {0x45, 0x15, 0x75, 0x46, 0x76, 0x26}, {0x10, 0x40, 0x20, 0x45, 0x15, 0x75, 0x26, 0x46, 0x76}, + {0x75, 0x01, 0x31, 0x75, 0x45, 0x01, 0x46, 0x76, 0x26}, {0x26, 0x46, 0x76, 0x40, 0x20, 0x45, 0x20, 0x75, 0x45, 0x20, 0x31, 0x75}, + {0x46, 0x32, 0x02, 0x46, 0x76, 0x32, 0x75, 0x45, 0x15}, {0x15, 0x75, 0x45, 0x10, 0x40, 0x76, 0x10, 0x76, 0x32, 0x76, 0x40, 0x46}, + {0x02, 0x76, 0x32, 0x02, 0x46, 0x76, 0x31, 0x75, 0x01, 0x75, 0x45, 0x01}, {0x76, 0x32, 0x40, 0x76, 0x40, 0x46, 0x32, 0x31, 0x40, 0x45, 0x40, 0x75, 0x31, 0x75, 0x40}, + {0x15, 0x75, 0x45, 0x73, 0x13, 0x23, 0x46, 0x76, 0x26}, {0x76, 0x26, 0x46, 0x13, 0x23, 0x73, 0x10, 0x40, 0x20, 0x45, 0x15, 0x75}, + {0x46, 0x76, 0x26, 0x75, 0x45, 0x73, 0x45, 0x23, 0x73, 0x45, 0x01, 0x23}, {0x20, 0x45, 0x40, 0x20, 0x75, 0x45, 0x20, 0x23, 0x75, 0x73, 0x75, 0x23, 0x26, 0x46, 0x76}, + {0x15, 0x75, 0x45, 0x73, 0x13, 0x76, 0x13, 0x46, 0x76, 0x13, 0x02, 0x46}, {0x13, 0x76, 0x73, 0x13, 0x46, 0x76, 0x13, 0x10, 0x46, 0x40, 0x46, 0x10, 0x15, 0x75, 0x45}, + {0x45, 0x01, 0x73, 0x45, 0x73, 0x75, 0x01, 0x02, 0x73, 0x76, 0x73, 0x46, 0x02, 0x46, 0x73}, {0x46, 0x76, 0x73, 0x46, 0x73, 0x40, 0x75, 0x45, 0x73, 0x45, 0x40, 0x73}, + {0x76, 0x15, 0x75, 0x76, 0x26, 0x15, 0x26, 0x04, 0x15}, {0x20, 0x76, 0x26, 0x10, 0x76, 0x20, 0x10, 0x75, 0x76, 0x10, 0x15, 0x75}, + {0x01, 0x26, 0x04, 0x01, 0x75, 0x26, 0x01, 0x31, 0x75, 0x75, 0x76, 0x26}, {0x76, 0x26, 0x20, 0x76, 0x20, 0x75, 0x75, 0x20, 0x31}, + {0x75, 0x04, 0x15, 0x75, 0x32, 0x04, 0x75, 0x76, 0x32, 0x02, 0x04, 0x32}, {0x15, 0x75, 0x76, 0x15, 0x76, 0x10, 0x10, 0x76, 0x32}, + {0x31, 0x75, 0x04, 0x31, 0x04, 0x01, 0x75, 0x76, 0x04, 0x02, 0x04, 0x32, 0x76, 0x32, 0x04}, {0x31, 0x75, 0x76, 0x32, 0x31, 0x76}, + {0x13, 0x23, 0x73, 0x15, 0x75, 0x26, 0x15, 0x26, 0x04, 0x26, 0x75, 0x76}, {0x10, 0x26, 0x20, 0x10, 0x76, 0x26, 0x10, 0x15, 0x76, 0x75, 0x76, 0x15, 0x13, 0x23, 0x73}, + {0x26, 0x04, 0x75, 0x26, 0x75, 0x76, 0x04, 0x01, 0x75, 0x73, 0x75, 0x23, 0x01, 0x23, 0x75}, {0x76, 0x26, 0x20, 0x76, 0x20, 0x75, 0x23, 0x73, 0x20, 0x73, 0x75, 0x20}, + {0x13, 0x02, 0x76, 0x13, 0x76, 0x73, 0x02, 0x04, 0x76, 0x75, 0x76, 0x15, 0x04, 0x15, 0x76}, {0x73, 0x13, 0x10, 0x73, 0x10, 0x76, 0x15, 0x75, 0x10, 0x75, 0x76, 0x10}, + {0x01, 0x02, 0x04, 0x75, 0x76, 0x73}, {0x73, 0x75, 0x76}, + {0x37, 0x67, 0x57}, {0x10, 0x40, 0x20, 0x57, 0x37, 0x67}, + {0x51, 0x01, 0x31, 0x57, 0x37, 0x67}, {0x31, 0x40, 0x20, 0x31, 0x51, 0x40, 0x57, 0x37, 0x67}, + {0x32, 0x02, 0x62, 0x37, 0x67, 0x57}, {0x62, 0x10, 0x40, 0x62, 0x32, 0x10, 0x37, 0x67, 0x57}, + {0x01, 0x31, 0x51, 0x32, 0x02, 0x62, 0x57, 0x37, 0x67}, {0x57, 0x37, 0x67, 0x31, 0x51, 0x32, 0x51, 0x62, 0x32, 0x51, 0x40, 0x62}, + {0x13, 0x67, 0x57, 0x23, 0x67, 0x13}, {0x13, 0x67, 0x57, 0x13, 0x23, 0x67, 0x20, 0x10, 0x40}, + {0x51, 0x67, 0x57, 0x51, 0x01, 0x67, 0x01, 0x23, 0x67}, {0x57, 0x51, 0x40, 0x57, 0x40, 0x23, 0x57, 0x23, 0x67, 0x20, 0x23, 0x40}, + {0x67, 0x02, 0x62, 0x67, 0x57, 0x02, 0x57, 0x13, 0x02}, {0x10, 0x40, 0x62, 0x10, 0x62, 0x57, 0x10, 0x57, 0x13, 0x57, 0x62, 0x67}, + {0x02, 0x62, 0x67, 0x01, 0x02, 0x67, 0x01, 0x67, 0x57, 0x01, 0x57, 0x51}, {0x67, 0x57, 0x51, 0x67, 0x51, 0x62, 0x62, 0x51, 0x40}, + {0x57, 0x37, 0x67, 0x54, 0x64, 0x04}, {0x54, 0x20, 0x10, 0x54, 0x64, 0x20, 0x67, 0x57, 0x37}, + {0x31, 0x51, 0x01, 0x57, 0x37, 0x67, 0x04, 0x54, 0x64}, {0x37, 0x67, 0x57, 0x31, 0x51, 0x64, 0x31, 0x64, 0x20, 0x64, 0x51, 0x54}, + {0x02, 0x62, 0x32, 0x64, 0x04, 0x54, 0x37, 0x67, 0x57}, {0x57, 0x37, 0x67, 0x54, 0x64, 0x32, 0x54, 0x32, 0x10, 0x32, 0x64, 0x62}, + {0x01, 0x31, 0x51, 0x54, 0x64, 0x04, 0x32, 0x02, 0x62, 0x57, 0x37, 0x67}, {0x51, 0x32, 0x31, 0x51, 0x62, 0x32, 0x51, 0x54, 0x62, 0x64, 0x62, 0x54, 0x57, 0x37, 0x67}, + {0x67, 0x13, 0x23, 0x67, 0x57, 0x13, 0x54, 0x64, 0x04}, {0x13, 0x23, 0x57, 0x57, 0x23, 0x67, 0x20, 0x10, 0x54, 0x20, 0x54, 0x64}, + {0x04, 0x54, 0x64, 0x51, 0x01, 0x57, 0x01, 0x67, 0x57, 0x01, 0x23, 0x67}, {0x64, 0x20, 0x51, 0x64, 0x51, 0x54, 0x20, 0x23, 0x51, 0x57, 0x51, 0x67, 0x23, 0x67, 0x51}, + {0x04, 0x54, 0x64, 0x02, 0x62, 0x57, 0x02, 0x57, 0x13, 0x57, 0x62, 0x67}, {0x57, 0x13, 0x62, 0x57, 0x62, 0x67, 0x13, 0x10, 0x62, 0x64, 0x62, 0x54, 0x10, 0x54, 0x62}, + {0x01, 0x57, 0x51, 0x01, 0x67, 0x57, 0x01, 0x02, 0x67, 0x62, 0x67, 0x02, 0x04, 0x54, 0x64}, {0x67, 0x57, 0x51, 0x67, 0x51, 0x62, 0x54, 0x64, 0x51, 0x64, 0x62, 0x51}, + {0x37, 0x45, 0x15, 0x67, 0x45, 0x37}, {0x45, 0x37, 0x67, 0x45, 0x15, 0x37, 0x10, 0x40, 0x20}, + {0x37, 0x01, 0x31, 0x37, 0x67, 0x01, 0x67, 0x45, 0x01}, {0x40, 0x20, 0x31, 0x40, 0x31, 0x67, 0x40, 0x67, 0x45, 0x67, 0x31, 0x37}, + {0x37, 0x45, 0x15, 0x37, 0x67, 0x45, 0x62, 0x32, 0x02}, {0x10, 0x40, 0x32, 0x32, 0x40, 0x62, 0x45, 0x15, 0x37, 0x45, 0x37, 0x67}, + {0x02, 0x62, 0x32, 0x01, 0x31, 0x67, 0x01, 0x67, 0x45, 0x67, 0x31, 0x37}, {0x67, 0x45, 0x31, 0x67, 0x31, 0x37, 0x45, 0x40, 0x31, 0x32, 0x31, 0x62, 0x40, 0x62, 0x31}, + {0x13, 0x45, 0x15, 0x13, 0x23, 0x45, 0x23, 0x67, 0x45}, {0x20, 0x10, 0x40, 0x13, 0x23, 0x15, 0x23, 0x45, 0x15, 0x23, 0x67, 0x45}, + {0x01, 0x23, 0x45, 0x45, 0x23, 0x67}, {0x40, 0x20, 0x23, 0x40, 0x23, 0x45, 0x45, 0x23, 0x67}, + {0x15, 0x67, 0x45, 0x15, 0x02, 0x67, 0x15, 0x13, 0x02, 0x62, 0x67, 0x02}, {0x40, 0x62, 0x13, 0x40, 0x13, 0x10, 0x62, 0x67, 0x13, 0x15, 0x13, 0x45, 0x67, 0x45, 0x13}, + {0x02, 0x62, 0x67, 0x02, 0x67, 0x01, 0x01, 0x67, 0x45}, {0x67, 0x45, 0x40, 0x62, 0x67, 0x40}, + {0x64, 0x37, 0x67, 0x64, 0x04, 0x37, 0x04, 0x15, 0x37}, {0x10, 0x64, 0x20, 0x10, 0x37, 0x64, 0x10, 0x15, 0x37, 0x67, 0x64, 0x37}, + {0x37, 0x67, 0x64, 0x31, 0x37, 0x64, 0x31, 0x64, 0x04, 0x31, 0x04, 0x01}, {0x37, 0x67, 0x64, 0x37, 0x64, 0x31, 0x31, 0x64, 0x20}, + {0x32, 0x02, 0x62, 0x37, 0x67, 0x04, 0x37, 0x04, 0x15, 0x04, 0x67, 0x64}, {0x32, 0x10, 0x64, 0x32, 0x64, 0x62, 0x10, 0x15, 0x64, 0x67, 0x64, 0x37, 0x15, 0x37, 0x64}, + {0x31, 0x04, 0x01, 0x31, 0x64, 0x04, 0x31, 0x37, 0x64, 0x67, 0x64, 0x37, 0x32, 0x02, 0x62}, {0x62, 0x32, 0x31, 0x62, 0x31, 0x64, 0x37, 0x67, 0x31, 0x67, 0x64, 0x31}, + {0x13, 0x23, 0x67, 0x13, 0x67, 0x04, 0x13, 0x04, 0x15, 0x04, 0x67, 0x64}, {0x23, 0x67, 0x15, 0x23, 0x15, 0x13, 0x67, 0x64, 0x15, 0x10, 0x15, 0x20, 0x64, 0x20, 0x15}, + {0x64, 0x04, 0x01, 0x64, 0x01, 0x67, 0x67, 0x01, 0x23}, {0x64, 0x20, 0x23, 0x67, 0x64, 0x23}, + {0x04, 0x15, 0x67, 0x04, 0x67, 0x64, 0x15, 0x13, 0x67, 0x62, 0x67, 0x02, 0x13, 0x02, 0x67}, {0x10, 0x15, 0x13, 0x62, 0x67, 0x64}, + {0x64, 0x04, 0x01, 0x64, 0x01, 0x67, 0x02, 0x62, 0x01, 0x62, 0x67, 0x01}, {0x64, 0x62, 0x67}, + {0x26, 0x57, 0x37, 0x46, 0x57, 0x26}, {0x26, 0x57, 0x37, 0x26, 0x46, 0x57, 0x40, 0x20, 0x10}, + {0x57, 0x26, 0x46, 0x57, 0x37, 0x26, 0x31, 0x51, 0x01}, {0x37, 0x46, 0x57, 0x37, 0x26, 0x46, 0x51, 0x40, 0x31, 0x40, 0x20, 0x31}, + {0x32, 0x57, 0x37, 0x32, 0x02, 0x57, 0x02, 0x46, 0x57}, {0x40, 0x32, 0x10, 0x40, 0x57, 0x32, 0x40, 0x46, 0x57, 0x37, 0x32, 0x57}, + {0x51, 0x01, 0x31, 0x57, 0x37, 0x02, 0x57, 0x02, 0x46, 0x02, 0x37, 0x32}, {0x51, 0x40, 0x32, 0x51, 0x32, 0x31, 0x40, 0x46, 0x32, 0x37, 0x32, 0x57, 0x46, 0x57, 0x32}, + {0x26, 0x13, 0x23, 0x26, 0x46, 0x13, 0x46, 0x57, 0x13}, {0x10, 0x40, 0x20, 0x13, 0x23, 0x46, 0x13, 0x46, 0x57, 0x46, 0x23, 0x26}, + {0x51, 0x46, 0x57, 0x51, 0x23, 0x46, 0x51, 0x01, 0x23, 0x23, 0x26, 0x46}, {0x46, 0x57, 0x23, 0x46, 0x23, 0x26, 0x57, 0x51, 0x23, 0x20, 0x23, 0x40, 0x51, 0x40, 0x23}, + {0x13, 0x02, 0x57, 0x02, 0x46, 0x57}, {0x10, 0x40, 0x46, 0x10, 0x46, 0x13, 0x13, 0x46, 0x57}, + {0x51, 0x01, 0x02, 0x51, 0x02, 0x57, 0x57, 0x02, 0x46}, {0x51, 0x40, 0x46, 0x57, 0x51, 0x46}, + {0x57, 0x04, 0x54, 0x57, 0x37, 0x04, 0x37, 0x26, 0x04}, {0x57, 0x10, 0x54, 0x57, 0x26, 0x10, 0x57, 0x37, 0x26, 0x26, 0x20, 0x10}, + {0x01, 0x31, 0x51, 0x04, 0x54, 0x37, 0x04, 0x37, 0x26, 0x37, 0x54, 0x57}, {0x37, 0x26, 0x54, 0x37, 0x54, 0x57, 0x26, 0x20, 0x54, 0x51, 0x54, 0x31, 0x20, 0x31, 0x54}, + {0x32, 0x57, 0x37, 0x02, 0x57, 0x32, 0x02, 0x54, 0x57, 0x02, 0x04, 0x54}, {0x57, 0x37, 0x32, 0x57, 0x32, 0x54, 0x54, 0x32, 0x10}, + {0x02, 0x37, 0x32, 0x02, 0x57, 0x37, 0x02, 0x04, 0x57, 0x54, 0x57, 0x04, 0x01, 0x31, 0x51}, {0x57, 0x37, 0x32, 0x57, 0x32, 0x54, 0x31, 0x51, 0x32, 0x51, 0x54, 0x32}, + {0x23, 0x57, 0x13, 0x23, 0x04, 0x57, 0x23, 0x26, 0x04, 0x54, 0x57, 0x04}, {0x10, 0x54, 0x26, 0x10, 0x26, 0x20, 0x54, 0x57, 0x26, 0x23, 0x26, 0x13, 0x57, 0x13, 0x26}, + {0x01, 0x23, 0x57, 0x01, 0x57, 0x51, 0x23, 0x26, 0x57, 0x54, 0x57, 0x04, 0x26, 0x04, 0x57}, {0x51, 0x54, 0x57, 0x23, 0x26, 0x20}, + {0x04, 0x54, 0x57, 0x04, 0x57, 0x02, 0x02, 0x57, 0x13}, {0x10, 0x54, 0x57, 0x13, 0x10, 0x57}, + {0x04, 0x54, 0x57, 0x04, 0x57, 0x02, 0x51, 0x01, 0x57, 0x01, 0x02, 0x57}, {0x51, 0x54, 0x57}, + {0x45, 0x26, 0x46, 0x45, 0x15, 0x26, 0x15, 0x37, 0x26}, {0x10, 0x40, 0x20, 0x45, 0x15, 0x46, 0x15, 0x26, 0x46, 0x15, 0x37, 0x26}, + {0x31, 0x37, 0x26, 0x31, 0x26, 0x45, 0x31, 0x45, 0x01, 0x46, 0x45, 0x26}, {0x20, 0x31, 0x45, 0x20, 0x45, 0x40, 0x31, 0x37, 0x45, 0x46, 0x45, 0x26, 0x37, 0x26, 0x45}, + {0x32, 0x15, 0x37, 0x32, 0x46, 0x15, 0x32, 0x02, 0x46, 0x46, 0x45, 0x15}, {0x15, 0x37, 0x46, 0x15, 0x46, 0x45, 0x37, 0x32, 0x46, 0x40, 0x46, 0x10, 0x32, 0x10, 0x46}, + {0x02, 0x46, 0x37, 0x02, 0x37, 0x32, 0x46, 0x45, 0x37, 0x31, 0x37, 0x01, 0x45, 0x01, 0x37}, {0x31, 0x37, 0x32, 0x40, 0x46, 0x45}, + {0x45, 0x26, 0x46, 0x15, 0x26, 0x45, 0x15, 0x23, 0x26, 0x15, 0x13, 0x23}, {0x15, 0x46, 0x45, 0x15, 0x26, 0x46, 0x15, 0x13, 0x26, 0x23, 0x26, 0x13, 0x10, 0x40, 0x20}, + {0x26, 0x46, 0x45, 0x26, 0x45, 0x23, 0x23, 0x45, 0x01}, {0x26, 0x46, 0x45, 0x26, 0x45, 0x23, 0x40, 0x20, 0x45, 0x20, 0x23, 0x45}, + {0x45, 0x15, 0x13, 0x45, 0x13, 0x46, 0x46, 0x13, 0x02}, {0x45, 0x15, 0x13, 0x45, 0x13, 0x46, 0x10, 0x40, 0x13, 0x40, 0x46, 0x13}, + {0x45, 0x01, 0x02, 0x46, 0x45, 0x02}, {0x45, 0x40, 0x46}, + {0x15, 0x37, 0x04, 0x37, 0x26, 0x04}, {0x20, 0x10, 0x15, 0x20, 0x15, 0x26, 0x26, 0x15, 0x37}, + {0x01, 0x31, 0x37, 0x01, 0x37, 0x04, 0x04, 0x37, 0x26}, {0x20, 0x31, 0x37, 0x26, 0x20, 0x37}, + {0x32, 0x02, 0x04, 0x32, 0x04, 0x37, 0x37, 0x04, 0x15}, {0x15, 0x37, 0x32, 0x10, 0x15, 0x32}, + {0x32, 0x02, 0x04, 0x32, 0x04, 0x37, 0x01, 0x31, 0x04, 0x31, 0x37, 0x04}, {0x31, 0x37, 0x32}, + {0x13, 0x23, 0x26, 0x13, 0x26, 0x15, 0x15, 0x26, 0x04}, {0x20, 0x10, 0x15, 0x20, 0x15, 0x26, 0x13, 0x23, 0x15, 0x23, 0x26, 0x15}, + {0x01, 0x23, 0x26, 0x04, 0x01, 0x26}, {0x20, 0x23, 0x26}, + {0x13, 0x02, 0x04, 0x15, 0x13, 0x04}, {0x10, 0x15, 0x13}, + {0x01, 0x02, 0x04}, {}}; struct Voxel { @@ -278,21 +271,17 @@ static void voxelize(unsigned char* grid, Voxel* voxels, const unsigned int* vox static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int edge, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3]) { - int a = kEdgeA[edge], b = kEdgeB[edge]; - int ax = kCornerX[a], ay = kCornerY[a], az = kCornerZ[a]; - int bx = kCornerX[b], by = kCornerY[b], bz = kCornerZ[b]; + int a = edge >> 4, b = edge & 0xf; + int ax = a & 1, ay = (a >> 1) & 1, az = (a >> 2) & 1; + int bx = b & 1, by = (b >> 1) & 1, bz = (b >> 2) & 1; - // TODO: store directional edges in the table so that we know which corner is occupied unsigned char ag = grid[(x + ax) + size_t(resolution) * ((y + ay) + size_t(resolution) * (z + az))]; unsigned char bg = grid[(x + bx) + size_t(resolution) * ((y + by) + size_t(resolution) * (z + bz))]; - assert((ag != 0) + (bg != 0) == 1); // one of the corners must be occupied + assert(ag != 0 && bg == 0); // our edges are always from occupied to empty voxel + (void)ag, (void)bg; - int dx = (ag == 0) ? bx : ax; - int dy = (ag == 0) ? by : ay; - int dz = (ag == 0) ? bz : az; - - size_t row = (y + dy) + size_t(resolution) * (z + dz); - size_t idx = (x + dx) + size_t(resolution) * row; + size_t row = (y + ay) + size_t(resolution) * (z + az); + size_t idx = (x + ax) + size_t(resolution) * row; if (destination) { @@ -319,22 +308,19 @@ static size_t polygonize(float* destination, size_t max_triangle_count, const un for (int i = 0; i < 8; ++i) { - int ix = x + kCornerX[i]; - int iy = y + kCornerY[i]; - int iz = z + kCornerZ[i]; - - size_t index = ix + size_t(resolution) * (iy + size_t(resolution) * iz); - cube |= (grid[index] == 0) << i; + int ix = x + (i & 1), iy = y + ((i >> 1) & 1), iz = z + ((i >> 2) & 1); + size_t idx = ix + size_t(resolution) * (iy + size_t(resolution) * iz); + cube |= (grid[idx] == 0) << i; } if (cube == 0 || cube == 0xff) continue; - const signed char* tris = kTriangleTable[cube]; + const unsigned char* tris = kTriangleTable[cube]; - for (int i = 0; tris[i] != -1; i += 3) + for (int i = 0; tris[i]; i += 3) { - int ea = tris[i + 0], eb = tris[i + 2], ec = tris[i + 1]; + int ea = tris[i + 0], eb = tris[i + 1], ec = tris[i + 2]; // note: we only emit the triangle if we have space for it, but we reject degenerate triangles purely based on vertex codes // this results in consistent capacity estimation, as result advances the same way regardless of whether triangle data is written From 3e93776e1da9ee1c8b6bc8ba1d753c33ca3cab39 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 17 Jul 2026 14:35:33 -0700 Subject: [PATCH 17/41] remesher: Fix point clamping at the positive grid boundary With resolution voxels and first & last column being reserved for empty padding, the coordinates inside range from 1..resolution-2, and because we use 0-based coordinates in the voxelization loops, the range is 0..resolution-3. We were off by 1 which could result in writing voxel data to the boundary that should remain open. Also update resolution limits accordingly; resolution up to 256 is actually safe because that guarantees up to 254 active voxels in each row (with two values remaining for storing voxel states; we only use one but that's about to change). --- src/remesher.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/remesher.cpp b/src/remesher.cpp index 2171c05ea..a876a35f2 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -243,10 +243,10 @@ static void voxelize(unsigned char* grid, Voxel* voxels, const unsigned int* vox int z = int(pz * scale); // safety: rounding errors and non-finite inputs may produce out of bounds coordinates, so we clamp them - // TODO: codegen - x = (x < 0) ? 0 : (x > resolution - 2 ? resolution - 2 : x); - y = (y < 0) ? 0 : (y > resolution - 2 ? resolution - 2 : y); - z = (z < 0) ? 0 : (z > resolution - 2 ? resolution - 2 : z); + int cutoff = resolution - 3; + x = unsigned(x) < unsigned(cutoff) ? x : cutoff; + y = unsigned(y) < unsigned(cutoff) ? y : cutoff; + z = unsigned(z) < unsigned(cutoff) ? z : cutoff; size_t row = (y + 1) + size_t(resolution) * (z + 1); size_t idx = (x + 1) + size_t(resolution) * row; @@ -346,7 +346,7 @@ size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsig assert(index_count % 3 == 0); assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); - assert(resolution > 0 && resolution <= 250); // TBD + assert(resolution >= 4 && resolution <= 256); meshopt_Allocator allocator; From 17052c7f518b666ae68abe12cb78a280a7e08854 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Fri, 17 Jul 2026 14:50:06 -0700 Subject: [PATCH 18/41] remesher: Implement optional sheet thickening A single-voxel thin sheet does polygonize from both sides; due to how we compute the positions, they are identical on both sides so we get a clean double-sided infinitely thin layer. This is fine by itself; but code running downstream of this may or may not expect this. As an option, we now allow expanding this into a thin closed plate instead. This requires reading the opposite voxel to the two voxels that the edge we are meshing connects; because we now orient edges in a consistent direction, this voxel is always in the same logical direction (O -> A -> B), and is guaranteed to be in bounds. The voxels on the side of the plate do not get offset along the orthogonal direction (e.g. if plate has a constant Z, then computing edge from the boundary that also has a constant Z does not get recognized as a thin sheet because the opposite voxel is occupied. To ensure we only get *one* layer, not two, for thin sheets, we restrict this to positive directions for now: the negative-facing side is not offset, but the positive-facing side is. --- src/meshoptimizer.h | 8 ++++++++ src/remesher.cpp | 32 ++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/meshoptimizer.h b/src/meshoptimizer.h index 38f5df0e2..c29bd622a 100644 --- a/src/meshoptimizer.h +++ b/src/meshoptimizer.h @@ -512,6 +512,14 @@ enum */ MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options, float* result_error); +/** + * Experimental: Remeshing flags; in high degree of flux + */ +enum +{ + meshopt_RemeshThicken = 1 << 0, +}; + /** * Experimental: Voxel remesher */ diff --git a/src/remesher.cpp b/src/remesher.cpp index a876a35f2..74bd7096b 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -269,7 +269,7 @@ static void voxelize(unsigned char* grid, Voxel* voxels, const unsigned int* vox } } -static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int edge, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3]) +static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int edge, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3], bool thicken) { int a = edge >> 4, b = edge & 0xf; int ax = a & 1, ay = (a >> 1) & 1, az = (a >> 2) & 1; @@ -280,6 +280,18 @@ static size_t emitVertex(float* destination, size_t index, int x, int y, int z, assert(ag != 0 && bg == 0); // our edges are always from occupied to empty voxel (void)ag, (void)bg; + int oc = 0; + float os = 0.1f / scale; + + if (thicken) + { + // look at the opposite voxel to see if we have a thin sheet; the lookup is in bounds by construction since a->b is occupied->empty edge + int ox = bx - ax, oy = by - ay, oz = bz - az; + unsigned char og = grid[(x + ax - ox) + size_t(resolution) * ((y + ay - oy) + size_t(resolution) * (z + az - oz))]; + + oc = (ox + oy + oz > 0 && og == 0) ? (ox & 1) | ((oy & 1) << 1) | ((oz & 1) << 2) : 0; + } + size_t row = (y + ay) + size_t(resolution) * (z + az); size_t idx = (x + ax) + size_t(resolution) * row; @@ -288,15 +300,15 @@ static size_t emitVertex(float* destination, size_t index, int x, int y, int z, assert(grid[idx]); const Voxel& vox = voxels[voxel_rows[row] + (grid[idx] - 1)]; - destination[index * 3 + 0] = vox.px / vox.w + offset[0]; - destination[index * 3 + 1] = vox.py / vox.w + offset[1]; - destination[index * 3 + 2] = vox.pz / vox.w + offset[2]; + destination[index * 3 + 0] = vox.px / vox.w + float((oc >> 0) & 1) * os + offset[0]; + destination[index * 3 + 1] = vox.py / vox.w + float((oc >> 1) & 1) * os + offset[1]; + destination[index * 3 + 2] = vox.pz / vox.w + float((oc >> 2) & 1) * os + offset[2]; } - return idx; + return idx * 8 + oc; } -static size_t polygonize(float* destination, size_t max_triangle_count, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3]) +static size_t polygonize(float* destination, size_t max_triangle_count, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3], bool thicken) { size_t result = 0; @@ -326,9 +338,9 @@ static size_t polygonize(float* destination, size_t max_triangle_count, const un // this results in consistent capacity estimation, as result advances the same way regardless of whether triangle data is written float* target = (destination && result < max_triangle_count) ? destination : NULL; - size_t ca = emitVertex(target, result * 3 + 0, x, y, z, ea, grid, voxels, voxel_rows, resolution, scale, offset); - size_t cb = emitVertex(target, result * 3 + 1, x, y, z, eb, grid, voxels, voxel_rows, resolution, scale, offset); - size_t cc = emitVertex(target, result * 3 + 2, x, y, z, ec, grid, voxels, voxel_rows, resolution, scale, offset); + size_t ca = emitVertex(target, result * 3 + 0, x, y, z, ea, grid, voxels, voxel_rows, resolution, scale, offset, thicken); + size_t cb = emitVertex(target, result * 3 + 1, x, y, z, eb, grid, voxels, voxel_rows, resolution, scale, offset, thicken); + size_t cc = emitVertex(target, result * 3 + 2, x, y, z, ec, grid, voxels, voxel_rows, resolution, scale, offset, thicken); result += (ca != cb) && (cb != cc) && (cc != ca); // degenerate triangle } @@ -397,7 +409,7 @@ size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsig if (voxels) voxelize(grid, voxels, voxel_rows, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride, resolution, scale, offset); - size_t result = polygonize(destination, max_triangle_count, grid, voxels, voxel_rows, resolution, scale, offset); + size_t result = polygonize(destination, max_triangle_count, grid, voxels, voxel_rows, resolution, scale, offset, (options & meshopt_RemeshThicken) != 0); #if TRACE printf("remesher: %zu triangles (%zu capacity)\n", result, max_triangle_count); From 1916e495f4f3ff7d6ec286d15109aa2040173933 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 20 Jul 2026 09:08:57 -0700 Subject: [PATCH 19/41] remesher: Add debug option that disables position computation In the future it might make sense to emit cube geometry if Debug is specified, but for now it's sufficient to simply output original grid position that does not depend on the accumulation code or future solve. --- src/meshoptimizer.h | 1 + src/remesher.cpp | 29 +++++++++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/meshoptimizer.h b/src/meshoptimizer.h index c29bd622a..e916c98b2 100644 --- a/src/meshoptimizer.h +++ b/src/meshoptimizer.h @@ -518,6 +518,7 @@ MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsig enum { meshopt_RemeshThicken = 1 << 0, + meshopt_RemeshDebug = 1 << 1, }; /** diff --git a/src/remesher.cpp b/src/remesher.cpp index 74bd7096b..74c925a0b 100644 --- a/src/remesher.cpp +++ b/src/remesher.cpp @@ -269,7 +269,7 @@ static void voxelize(unsigned char* grid, Voxel* voxels, const unsigned int* vox } } -static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int edge, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3], bool thicken) +static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int edge, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3], unsigned int options) { int a = edge >> 4, b = edge & 0xf; int ax = a & 1, ay = (a >> 1) & 1, az = (a >> 2) & 1; @@ -283,7 +283,7 @@ static size_t emitVertex(float* destination, size_t index, int x, int y, int z, int oc = 0; float os = 0.1f / scale; - if (thicken) + if (options & meshopt_RemeshThicken) { // look at the opposite voxel to see if we have a thin sheet; the lookup is in bounds by construction since a->b is occupied->empty edge int ox = bx - ax, oy = by - ay, oz = bz - az; @@ -300,15 +300,24 @@ static size_t emitVertex(float* destination, size_t index, int x, int y, int z, assert(grid[idx]); const Voxel& vox = voxels[voxel_rows[row] + (grid[idx] - 1)]; - destination[index * 3 + 0] = vox.px / vox.w + float((oc >> 0) & 1) * os + offset[0]; - destination[index * 3 + 1] = vox.py / vox.w + float((oc >> 1) & 1) * os + offset[1]; - destination[index * 3 + 2] = vox.pz / vox.w + float((oc >> 2) & 1) * os + offset[2]; + if (options & meshopt_RemeshDebug) + { + destination[index * 3 + 0] = (x + ax) / scale + offset[0]; + destination[index * 3 + 1] = (y + ay) / scale + offset[1]; + destination[index * 3 + 2] = (z + az) / scale + offset[2]; + } + else + { + destination[index * 3 + 0] = vox.px / vox.w + float((oc >> 0) & 1) * os + offset[0]; + destination[index * 3 + 1] = vox.py / vox.w + float((oc >> 1) & 1) * os + offset[1]; + destination[index * 3 + 2] = vox.pz / vox.w + float((oc >> 2) & 1) * os + offset[2]; + } } return idx * 8 + oc; } -static size_t polygonize(float* destination, size_t max_triangle_count, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3], bool thicken) +static size_t polygonize(float* destination, size_t max_triangle_count, const unsigned char* grid, const Voxel* voxels, const unsigned int* voxel_rows, int resolution, float scale, const float offset[3], unsigned int options) { size_t result = 0; @@ -338,9 +347,9 @@ static size_t polygonize(float* destination, size_t max_triangle_count, const un // this results in consistent capacity estimation, as result advances the same way regardless of whether triangle data is written float* target = (destination && result < max_triangle_count) ? destination : NULL; - size_t ca = emitVertex(target, result * 3 + 0, x, y, z, ea, grid, voxels, voxel_rows, resolution, scale, offset, thicken); - size_t cb = emitVertex(target, result * 3 + 1, x, y, z, eb, grid, voxels, voxel_rows, resolution, scale, offset, thicken); - size_t cc = emitVertex(target, result * 3 + 2, x, y, z, ec, grid, voxels, voxel_rows, resolution, scale, offset, thicken); + size_t ca = emitVertex(target, result * 3 + 0, x, y, z, ea, grid, voxels, voxel_rows, resolution, scale, offset, options); + size_t cb = emitVertex(target, result * 3 + 1, x, y, z, eb, grid, voxels, voxel_rows, resolution, scale, offset, options); + size_t cc = emitVertex(target, result * 3 + 2, x, y, z, ec, grid, voxels, voxel_rows, resolution, scale, offset, options); result += (ca != cb) && (cb != cc) && (cc != ca); // degenerate triangle } @@ -409,7 +418,7 @@ size_t meshopt_remesh(float* destination, size_t max_triangle_count, const unsig if (voxels) voxelize(grid, voxels, voxel_rows, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride, resolution, scale, offset); - size_t result = polygonize(destination, max_triangle_count, grid, voxels, voxel_rows, resolution, scale, offset, (options & meshopt_RemeshThicken) != 0); + size_t result = polygonize(destination, max_triangle_count, grid, voxels, voxel_rows, resolution, scale, offset, options); #if TRACE printf("remesher: %zu triangles (%zu capacity)\n", result, max_triangle_count); From b856629ca7827ccb3a12834ec75c5176fed8ae57 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 20 Jul 2026 09:12:14 -0700 Subject: [PATCH 20/41] demo: Enhance remesher demo with texture / normal options - Use creased normals with a customizable cutoff instead of naive average - Show transferred texture in a 2D canvas - Add a 'solve' option for post-remesh simplification - Compact the mesh after simplification to see real vertex counts - Disable polygon offset during regular rendering --- demo/remesh.html | 53 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/demo/remesh.html b/demo/remesh.html index e2af8076e..b7109bee6 100644 --- a/demo/remesh.html +++ b/demo/remesh.html @@ -52,6 +52,14 @@ font-size: 12px; line-height: 1.4; } + #texture-view { + position: fixed; + left: 10px; + bottom: 10px; + width: 256px; + height: 256px; + display: none; + }