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.
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 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

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

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 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 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

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 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

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

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

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.