isomesh

Engine-agnostic isosurface extraction in Rust. Signed distance field in, triangles out.

isomesh has to serve both a real-time voxel game and a CAD tool. That single constraint decides almost everything about it: no math library appears in a public signature, output buffers are caller-provided and reusable, the scalar type is generic over f32 and f64, the core crate is no_std with exactly one dependency, and seven extractors sit behind one trait. Every number on this site was produced by a command in the repository, on a machine named in FINDINGS.md's header, rather than quoted from a paper.

⚠️ Vibe Coded. This crate was written by an AI agent working from a research corpus and a ticket queue. Every number here is produced by a test in the repository and every algorithm cites its source, but it has not been through human code review. Read the tests before trusting it with anything that matters.


Try it right now

This is isomesh itself, compiled to WebAssembly, meshing in your browser at whatever resolution you drag the slider to. About 130 KB, because the core crate is no_std with one dependency and the renderer below it is 300 lines of hand-written WebGL2 rather than a game engine. Every counter in the panel comes from isomesh::validate — the same report that gates every test in the repository — recomputed on your machine on every re-mesh.

Two of them are worth watching rather than reading. χ is the Euler characteristic: 2 for a sphere, 0 for a torus, and steeply negative for the gyroid, which is what a high-genus surface means numerically. Non-manifold edges should be zero, and on gyroid under Surface Nets or Dual Contouring it is not — that is the measured, documented cost of placing one vertex per cell rather than a defect in this page.

drag to orbit · scroll to zoom

vertices – triangles – χ – non-manifold edges – boundary edges – degenerate triangles – extraction –

The extraction time is measured by the browser around the one call into the module, which is why the module itself keeps no clock and imports nothing at all — not one function, from anywhere. fbm_terrain reports non-zero boundary edges on purpose: it is a heightfield and it exits through the sides of its own domain, so a closed-surface gate would be the wrong gate to hold it to.


Play in your browser

Eleven of the demos are playable in your browser: real WebAssembly builds of the same examples the repository ships, no video and no replay. Three of them — the Phase 21 ones — measure their own figures in your browser and then print a cross-check against the CSV they were registered with; open the developer console and you can read the comparison, line by line, on your own hardware. A demo whose numbers stopped agreeing with its committed artefact would say so on screen.

They need WebGPU. game_dig offers Marching Cubes on the GPU as one of its eight meshers, Cargo features are per-package, and bevy_render gives its webgpu feature precedence over webgl2 on wasm32 — so there is no per-example choice to make and all eleven moved backend at once. The demo at the top of this page is unaffected, because it is hand-written WebGL2 rather than a Bevy build.

Each module is 36 MB, about 8.8 MB on the wire once the host has gzipped it, and it is cached after the first visit. Each also carries its own copy of the Bevy runtime, which is where almost all of that goes — the demo at the top of this page is the same library without it, at 130 KB.

▶ Play game_dig

A first-person camera carving tunnels through chunked terrain

A 16×8×16-unit sandbox of cracked concrete, 256 chunks, and the mesh is rebuilt while you are holding the mouse down. You start walking on it — WASD moves, the mouse looks, Space jumps a full chunk, and gravity, ground contact and the rock face you cannot walk through are all sampled straight out of the field under two spheres, because a collision mesh built from this terrain is stale the moment you carve. F switches to the fly mode the demo used to open in, where Q and E go up and down.

The translucent sphere on the rock under the crosshair is the brush a click would push, traced against the field itself rather than against a collider, so it sits on the surface and not at a fixed distance in front of your eye — hold the button and it carves a continuous tunnel along the sweep, at a fixed 12.5 edits a second, with the chunks that fall out of it re-meshed nearest-first under a 4 ms/frame budget so the frame time stays flat instead of collapsing as the edit log grows. 1–8 re-mesh all 256 chunks with a different mesher and print what that cost: the seven CPU extractors, then 8 for Marching Cubes on the GPU, a compute shader through isomesh-gpu — the rendering was always on the GPU, what moves on 8 is the extraction and the field. The terrain is not one of the four fields that shader knows, so a chunk's base samples are uploaded once and every carve after that ships only the pruned brush log — 64 bytes a brush — to be folded over them on the device; the panel's base term goes to (cached) and the running total of uploaded samples stops growing. At this chunk size the CPU still wins, and the demo says so: a chunk is 17³ samples, measured at CPU 0.06 ms against GPU 0.22, because a fixed ~0.22 ms of setup cannot amortise over 4,913 of them. The GPU takes the lead at 33³ and is 37× ahead at 129³ — that shape, not this row, is what the eighth key is here to show. A dimmed "Loading… please wait." panel covers the screen while that 256-chunk backlog drains, because it drains across frames now rather than blocking one.

A brush changes an SDF everywhere, because an SDF is global; what it changes visibly is a shell, and the number the whole incremental story rests on is how thin that shell is. E1 — the fraction of the brush's own bounding box that actually moved — was measured at 15–36% offline; here it is on screen, per edit. The rock is textured by the fragment shader rather than by the mesh, because an isosurface has no natural parameterisation: three world-plane projections, interpolated by the normal, so a freshly carved wall is continuous with the ground across the chunk boundary the brush straddled.

M-33 · G-002 · E-202 · C outlines exactly the chunks the last edit re-meshed · the numbers panel starts hidden behind a coloured headline naming the mesher, and H brings it back

▶ Play game_showcase

Flying through a landscape riddled with caves, arches and tunnels

Every other gameplay demo runs on a heightfield, which is exactly the case you do not need a voxel mesher for. This one is fBm terrain intersected with a thickened gyroid, so the camera flies through arches and out the far side of walls. It is also the demo that streams chunks through IsomeshPlugin, and the plugin called std::time::Instant::now() — which panics in a browser — until ✗44 was found building this page.

M-128 · E-210 · the chunk seam is why this streams on Marching Cubes: 0 boundary edges in the shared plane against Surface Nets' 5 and Dual Contouring's 4

▶ Play dual_contouring_cube

Surface Nets rounding a box corner beside Dual Contouring holding it, across a resolution sweep

The same field, the same grid, the same crossings, and one function different between the two meshes: where a cell's vertex goes. Distance from the true corner to the nearest vertex, recomputed live on every re-mesh — Surface Nets 0.58 cells, Dual Contouring 0.01. That gap is the entire case for dual contouring, and it is the image the README leads with.

A-009 · E-104 · press [ and ] and watch the corner snap in and out as the grid crosses an aligned resolution

▶ Play surface_nets_vs_marching_cubes

The same box meshed by Marching Cubes and Surface Nets side by side

The catalog used to bill this as "the triangle counts differ". They do not, and the reason is better than the claim: both methods are pinned to the number of crossed grid edges, so on every closed field F_sn − F_mc = 2χ exactly. The HUD recomputes the identity on every re-mesh; change field and resolution and watch it hold.

M-2 · E-103 · what does differ is visible rather than numeric, and the wireframe is where to look

▶ Play marching_cubes_tunnel

One cell meshed as two separate discs and as a single tunnel

One cell, meshed twice. The face rule alone gives two separate discs; adding the interior rule gives one cylinder passing through the cell, which is what the trilinear interpolant actually does there. A tunnel is a handle and a handle costs exactly two of χ, so the claim is arithmetic: χ falls by two per tunnel and by nothing else.

M-222 · E-213 · the gold ring is the six body saddles Grosso's construction is built out of

▶ Play quickstart

A sphere meshed as eight chunks and put on screen, in one file, with no HUD and nothing to filter out. There is no animation of it here because there is nothing to animate: the artefact is the source, which is 108 lines of which 59 are code. What it demonstrates is that the eight chunks are meshed independently and the result is still watertight.

The one example with no ticket number and no finding, because it teaches nothing — it is the shape of a working app, and the file to copy.

▶ Play game_mirror_dedup

The same carved chunk rotated and mirrored, the mirrored copy speckled with markers on every moved vertex

Of the 48 ways to reorient a chunk on the cubic lattice, exactly six give a bit-identical mesh, and they are precisely the six that never negate a coordinate. So mirroring an asset to reuse it silently misses on anything keyed to a content hash — instancing, collider caches, network deltas — while rotating it hits.

✗39 / M-356 · E-314 · cross-checked against docs/experiments/p-57.csv

▶ Play game_edit_tape_trim

A chunked destructible world, brush gizmos emptying from the frame while the rock does not move

A destructible world is a tape of brush edits folded over a base field, and a Lipschitz bound already cuts a 64-brush tape to a median of 19. Of the 1,507 brushes that survive that bound world-wide, 1,434 can be dropped and the mesh does not move by one bit — a further 20.6×, byte-identical on 64 of 64 chunks.

The first sixty-four frames are the ablation itself, one chunk per frame, because the 1,571 re-meshes it costs are the point: this is headroom, not a shippable pruner.

✗41 / M-358 · E-315 · cross-checked against docs/experiments/p-59.csv

▶ Play shifted_linear_root

A grid line with standard and shifted reconstructions, the error ratio tracing a closed-form curve as the crossing slides

Blu, Thévenaz & Unser buy "about 8 dB asymptotically" from linear interpolation by shifting the sampling knots. Does that move the root a mesher actually uses? It does, by exactly |σ − 2τ| / σ — so the gain is a lottery over where the crossing falls, and the pre-registered "at least 30% lower" is falsified at a median of 1.486. The honest number is that it clears the bar on 82% of positions.

✗42 / M-359 · E-316 · cross-checked against docs/experiments/p-60.csv


▶ Play hyperdeterminant_cells

The normalised hyperdeterminant magnitude as a per-cell heatmap, with the Delta = 0 stratum in grey

b*b - 4*a*c at trilinear.rs:246 is Cayley's 2×2×2 hyperdeterminant of the eight corner values — 12 terms against 12 at degree 4, all three pencil pairings agreeing, the exact-rational deviation 0/1 over 3,481 trials. The heatmap colours surface cells by the scale-free magnitude |Δ| / max|fᵢ|⁴; grey is the Δ = 0 stratum; magenta cages mark cells where the f32 sign disagrees with the exact one. The panel compares the live partition to P-130's committed one and prints P-130's two falsifications beside the comparison.

M-440 · E-317 · cited from docs/experiments/p-130.csv and docs/experiments/p-134.csv


▶ Play tpms_euler

Gyroid, Schwarz P and Schwarz D at N = 1, 2, 3 with chi measured against a derived prediction

Three triply periodic minimal surfaces whose Euler characteristic the crate can know: gyroid −8N³, Schwarz P −4N³, Schwarz D −16N³ (P-142's C1 held −8 / −64 / −216 to the integer; P-143's C2 is an exact i128 identity, which is why Schwarz P's lattice reads simple_cubic — its body-centring operation negates F_P rather than translating it). The prediction is derived live from a seven-half-shift symmetry census, not transcribed. The 32-voxel rung is a labelled degenerate control that misses by exactly its non-manifold edges — the π/4 lattice where Schwarz D cancels to exactly zero, M-48's class.

M-455 / M-456 · E-318 · cited from docs/experiments/p-142.csv and docs/experiments/p-143.csv


Read

Findings the ledger: every measurement, every falsified registration, in the order it was made
The demos every runnable example, with an animated capture, the exact command and the finding it demonstrates
Gameplay streaming a world past a camera · walking chunk seams with a rigid body · digging · debris that is the boolean · LOD ladders
Algorithms Marching Cubes · Surface Nets · Dual Contouring · Manifold Dual Contouring · Marching Tetrahedra · greedy quads · a seven-way shootout in one process
Correctness where a mesh stops being a manifold · what splitting the vertex costs · which way the surface faces · ambiguous faces · the crack between two chunks
Experiments the ones that held, and what each buys you
Readme the repository's own front page, rendered here

What is not on this site

The backlog, its archive and the research memos stay on github.com, and the reason is mechanical rather than editorial: the archive navigates itself with line-number anchors, which are a feature of GitHub's web view of a file and have no equivalent in rendered HTML. Every link into them from these pages goes to github.com.