Gameplay demos

The examples that use the crate the way a game does: chunked, edited, streamed, budgeted and collided against. These are the ones that decide whether the crate is usable, because a meshing library that produces a beautiful sphere and falls apart at a chunk seam is not one you can ship.

Every figure here came from a command you can run, and the command is under each demo.

← back to the README


The edit tape you keep is twenty times bigger than the one you need

A chunked destructible world; brush gizmos empty out of the frame as the tape steps from 64 to survivors to necessary, while the rock itself never changes and four hashes on the HUD stay equal

game_edit_tape_trim — P-39's fixture, brush for brush: a 64-brush tape of spheres, capsules and smooth adds folded over a base sphere, meshed in 64 chunks. Cyan brushes are necessary; amber survive the bound but are droppable because they are far from the surface; magenta survive and are droppable because another brush dominates them; grey are pruned. Four mesh_hash values sit on the HUD so "bit-identical" is four integers rather than an adjective.

A destructible world is a tape: a base field with a list of brush edits folded over it. Meshing a chunk means evaluating that whole fold at every sample, so the tape's length is the cost of every re-mesh, and the tape only grows as the player plays.

The standard answer is a conservative bound. Take a Lipschitz interval enclosure of each brush over the chunk's box, and any brush that provably cannot change the min/max fold anywhere in the chunk is deleted. That works, and it was measured: a 64-brush tape prunes to a median of 19 survivors with the mesh byte-identical on 64 of 64 chunks (M-341). The question nobody had asked is how many of the 19 are actually needed.

Ninety-five percent of them are not.

tape brushes, world total median per chunk mesh
full 4,096 64 reference
survivors, after the bound 1,507 19 bit-identical, 64/64
necessary, by leave-one-out 73 0 bit-identical, 64/64

Read the last row twice. It is not "each of these can be dropped on its own" — that is the weaker claim, and it is also true: 1,434 of 1,507 survivors can be removed individually with the hash unchanged. The last row is the joint claim. Re-mesh each chunk from its necessary brushes alone, every individually-unnecessary survivor removed at the same time, order preserved, and the hash is still identical on all 64 chunks. The world's tape goes from 1,507 to 73, a further 20.6× beyond what the bound already achieved.

The demo shows it as three tapes on one rock. Step FULL → SURVIVORS → NECESSARY and the brush gizmos drain out of the frame while the geometry does not move a pixel. On the busiest chunk the tape goes 64 → 15 and 29 of the survivors are magenta. On one chunk the necessary tape has length zero and the mesh — 3,227 triangles of it — is unchanged.

The cost is on screen next to the win, because it has to be. Deciding necessity took 1,571 re-meshes across the world, roughly survivors + 1 per chunk, and the demo runs the whole ablation at startup and reports the wall time it took. This is a measurement of headroom, not a shippable pruner. What it says is that the direction is open: a bound with 20× of slack in it is a bound worth improving, and before this nobody knew whether there was any slack at all.

The second mechanism, and why the pre-registered explanation failed. The prediction was that at least 90% of the droppable survivors would be droppable because they are far from the surface — they win the min/max chain only where the field is nowhere near zero. That is real and dominant, and it is not enough: 1,218 of 1,434, or 84.94%, against a registered 90%. The other 216 straddle the surface band and are droppable anyway, because another brush dominates them there. They concentrate exactly where you would least like them to — the highest-triangle chunks, 29 in the busiest, 24 in the second. A cheap replacement for leave-one-out has to be conservative, and those 216 are what make that hard.

Half of the registered predicate turns out to be structurally dead, which the demo also prints: the rule reads symmetrically, lo > cell_size || hi < −cell_size, and the second arm never fires once in 1,434 opportunities. The tape's brush radii cap at 1.1 by construction and the largest the seed actually draws is 0.706948, so the field's minimum over a chunk cannot reach the −3.589 that arm would need against a Lipschitz reach of 3.464. "Distance from zero" here only ever means above zero.

cargo run --example game_edit_tape_trim --release    # [ ] step chunks, T cycles the tape, X resumes the tour

Every figure above is reproduced live from docs/experiments/p-59.csv — thirteen quantities including all 64 committed per-chunk hashes (M-358 / ✗41, P-59, R-057). The demo runs in f64 throughout, because mesh_hash is defined on MeshBuffer<f64> and an f32 copy of this same fixture measures a survivor fraction of 0.3281 against the committed 0.2969 — it could not have reproduced a single hash.


The rim of your own tunnel is lit wrong, and a finer mesh will not fix it

A shaft bored into a rock face, its rim glowing red with normal error, triangles climbing while the error refuses to fall, then the rim fixed at the cheapest resolution

game_carve_seams — a capsule brush bores a shaft into a rock face. The rim is a 90° crease, and its normals are 44.3° wrong. The red tint is the per-vertex angular error. Then the resolution ladder runs, and the red band gets thinner but never less red.

You dig a tunnel. The rock face and the bore meet at a crease, and a crease in a CSG field is a min/max seam where the field is C⁰ and not C¹. A central difference straddling that seam averages two different gradients, so the normal it returns can be off by up to half the crease angle — at a right angle, 45°. That is a bright or dark rim arc at exactly the place the player just dug, and it is on the shipped path: BrushStack does not override Sdf::gradient, so these are the normals the mesher actually writes.

Here is the part that costs a studio real money. The obvious response to a bad shaded edge is to mesh finer. Watch the ladder:

chunk triangles rim error vertices straddling
25³ 2,588 44.28° 2.77%
41³ 7,492 42.99° 1.60%
65³ 19,146 44.16° 1.21%
97³ 43,516 44.73° 0.92%
129³ 78,100 44.94° 0.73%

30.2× the triangles, and the rim got 0.66° worse. It cannot improve: the stencil step scales with position, not with the cell, so refining the grid narrows the wrong band without making it less wrong.

The fix is 2.8% of the vertices, at the cheapest resolution. Straddling vertices sit on a curve, not a surface — O(n) against O(n²) — so at 25³ there are 38 of 1,374. Give those the analytic gradient of the active branch and the rim error drops from 44.28° to 0.0012°, on the same 2,588-triangle mesh. And P-47 already measured exact gradients as 2.84× cheaper than the six-sample difference on composed tapes, so the correct path is also the faster one.

Away from the rim the normals were never in doubt: 0.0012° over the other 1,336 vertices, which is the f32 round-off floor. The defect is a curve one cell wide, and nothing in the crate's validity suite looks at it — this mesh passes manifoldness, Euler and Hausdorff (M-350).

cd bevy_isomesh && cargo run --example game_carve_seams --release

The chunk knows it is losing detail, so stop guessing from camera distance

A terrain of spires and boulders under a chunk heat map, cycling uniform, range-based and oracle-driven LOD with a triangle scoreboard

game_lod_oracle — 16 chunks, four resolution rungs. The heat map is each chunk's own answer to am I losing detail? Same visual quality, 1.81× fewer triangles than distance-based LOD.

Every LOD scheme in shipping games picks resolution from camera distance, because that is the only signal available. Distance does not know what is in a chunk. Watch it fail: at 10 px/cell it spends 33³ on the nearest chunks, which are plain rolling ground, and drops the chunks holding thin spires to 17³ because they are further away. Result — 24,076 triangles and 9 of 16 chunks still losing detail, which is worse on the metric than uniform 9³ while costing 8.8× more.

The field can answer the question directly. Every sample asserts a sphere of radius |d| that the surface should touch. Count the ones your mesh never reaches and you have a detail-loss number computed from the field and the mesh you already have — no reference model, no artist judgement, no offline comparison against a ground truth that does not exist for procedural content.

strategy triangles chunks losing detail
uniform 9³ 2,738 8 of 16
range LOD, 10 px/cell 24,076 9 of 16
range LOD, tuned to 4.1 px/cell until it clears 147,504 0
oracle LOD 81,276 0

Each chunk climbs its own ladder until its untouched fraction drops under a 15% gate: eight chunks stay at 9³, three go to 33³, five to 65³. To reach the same detail bar, the range policy has to be tuned all the way down to 4.1 px/cell — and then it spends 1.81× the triangles, because it has to over-resolve everything to satisfy the worst thing.

Stated honestly: this is a bake, not a frame. The exhaustive scan costs 875 ms on the worst rung. It belongs in a content pipeline or a chunk's first-touch, not in an every-frame LOD selector — and the predicate reproduces P-51's committed row exactly, 717.1816 per 1,000 live against 717.1816 in the CSV (M-355).

cd bevy_isomesh && cargo run --example game_lod_oracle --release

1 uniform · 2 range · 3 range matched · 4 oracle · 5 cycle the beats.


A world with a roof over your head

Flying through caves and arches

Streamed, meshed while the camera moves, and flown through rather than over. 440,000 triangles, 376 chunks, 60 fps.

Every other demo on this page runs on fbm_terrain, which is a heightfield — one height per column, no overhangs, no caves, nothing above anything else. That is a good test field and a bad advertisement, because a heightfield is precisely the case you do not need a voxel mesher for.

This one composes a field that has no height at all:

solid(p) = max( p.y − height(x, z) ,  |gyroid(p)| − thickness )

The gyroid tunnels in all three axes, so intersecting it with the space under a terrain surface leaves caves that connect, arches, and rock overhead. Press 1, 2, 3 to thin or thicken the shell.

It is meshed with Marching Cubes, and that is not a preference. The world is chunked, and the dual methods do not tile across a chunk boundary: Marching Cubes puts vertices on grid edges, which two neighbouring chunks compute identically, while Surface Nets and Dual Contouring put one vertex per cell interior and a boundary quad needs the neighbour's vertex. Measured on two adjacent chunks, boundary edges in the shared plane: Marching Cubes 0, Surface Nets 5, Dual Contouring 4 — and 0 for Marching Cubes on every field tried, while the dual methods are 0 on some and open on others, so what they lack is the guarantee rather than the geometry on any one frame. This demo shipped on Dual Contouring for exactly one commit and the GIF above was a torn, slashed mess until someone looked at it (M-128).

Two other things worth knowing about how it got there. The gyroid's period is the look — the first attempt used K = 0.19, a 33 m period, where one tunnel is wider than the view and the world reads as a cracked plain rather than as caves. And the staircasing along the rim where the two surfaces meet turned out to be the field's aliasing, not the extractor's: swapping Surface Nets for Dual Contouring changed nothing, and what fixed it was a smooth intersection plus a shell thick enough to clear the sampling limit — 2.2 cells to 3.4. That is M-72's failure mode arriving by composition instead of by resolution.

cd bevy_isomesh && cargo run --example game_showcase --release

Space pause · [ ] fly speed · 1–3 cave density.


A body that slides, not a ray that hits

A capsule walking across streamed terrain under a physics engine

The same streamed fbm_terrain game_walk crosses, with an actual rigid body on it. 154 chunks resident, 77,492 triangles, 60 fps, and the collider handed to the physics engine is built from the very Handle<Mesh> the renderer is drawing.

game_walk casts 400 rays a frame and finds no holes — 495 seam crossings, zero misses. What it cannot find is a lip that stops something, because a ray is never caught on anything. A 0.2-unit step at a seam is, to a moving capsule, either nothing at all or a wall, and which one depends on the capsule, its speed and the angle it arrives at.

So this drives a dynamic capsule at 7 m/s along the same path and measures what it is prevented from doing. Over 66 seconds and 441 metres:

commanded distance actually covered 97.0%
stall frames at a seam 13
stall frames inside one chunk 70
worst single-frame shortfall at a seam 0.809
worst single-frame shortfall inside one chunk 1.000

The interior of a chunk stalls a moving body five times as often as a join does, and stops it dead at least once where no seam ever did. That is the same verdict the ray sweep reached from the other side — the joins are smoother than the terrain they join — reached by the one test that could have disagreed.

The body is dynamic, not kinematic, and that is the design rather than a detail: a kinematic capsule is moved by writing its transform, so it decides where it ends up and the terrain never gets to refuse. Only a dynamic body can be stopped by geometry, and being stopped is the measurement.

cd bevy_isomesh && cargo run --example game_capsule_walk --release

Space pause · [ ] view distance · R reset.


Shoot it, and the debris is the boolean

A hollow shell shot open, with the fragment resting on it

A hollow shell with a hole blown in it. The orange fragment is not a prop — it is the intersection of the shell with the charge, meshed. The crater and the fragment are two views of one boolean, and you can see straight through to the cavity that a convex hull would have filled in.

The cheap version of this demo pre-fractures a wall at build time and hides the pieces until you shoot. That proves nothing about a meshing crate. Here a shot appends Brush::subtract(sphere) to the solid's edit log and meshes the intersection of the solid-before-the-shot with that same sphere. Nothing is authored.

Fragments get a convex decomposition, not a hull, because both shapes this demo targets defeat a hull: a hollow shell's hull is a solid ball, and a spiral's is a fat cylinder that swallows every gap. The cost of that correctness is the number worth publishing:

target fragments convex parts parts per fragment mean worst
wall 23 211 9.2 240.7 ms 323.7 ms
hollow shell 24 305 12.7 271.8 ms 369.0 ms
spiral 23 224 9.7 249.0 ms 362.6 ms

A 60 fps frame is 16.7 ms. One fragment's decomposition is fourteen to twenty-two whole frames, and it lands on the frame the shot does. Zero fragments failed to get a collider on any target, and the shell — the shape whose hull is a lie — needs the most pieces, exactly as predicted.

So correct destruction colliders are achievable and must never be synchronous. This belongs on a worker with the finished collider swapped in later, which is how G-006 already treats meshing.

One honest number: 1 of 23 wall fragments still goes through the floor, and 0 of the shell's and spiral's. That is reported rather than tuned away. An earlier version of this demo accused 15 of 23 of tunnelling — and the scene simply had no floor for them to land on.

cd bevy_isomesh && cargo run --example game_destruction --release

Space fire · [ ] charge radius · T target · X reset.


Flying an LOD ladder, and counting what opens up

An LOD ladder with transition cells closing every seam

Twelve blocks stacked along x, each meshed at its own spacing — levels 0, 1 and 2, so 0.25, 0.5 and 1.0 world units — with the camera flying out along the ladder and back. Every 2:1 seam is bridged by Transvoxel transition cells, and the count of open edges lying in a seam plane is on the HUD.

Three claims, three measurements, and only one of them came out the way the ticket phrased it.

No cracks — confirmed, and on a configuration that had never run. transvoxel_seams meshes exactly one pair of blocks with the fine one on the low-x side, so inset_boundary had only ever been called with face_bit(0, 0). Flying out and back puts coarse blocks on both sides of the camera, so half the seams here are the mirror image — and a mirrored patch is the classic place for an inside-out winding, which no manifold or Euler check can see. Across the whole flight: 0 open edges on both sides.

That zero comes from an instrument that has itself been through falsification (E-211, M-195). The counter originally accepted an edge only within a quarter-cell of the seam plane, and that window was structurally blind: Eq-4.2's taper places transition geometry at seam ± w, at least four tolerances outside it, so a mirrored-seam winding bug once left 44 edges open while this HUD read zero. The counter now scans each seam's own ± w band, and the flight's zero above is the fixed counter's zero.

The zero is also only worth something because it can fail. With transitions off there is no taper — the cracks sit exactly on the seam plane, where the old and new counters agree — and the same worlds report 71 open edges on the low side and 102–111 on the high side:

The same ladder with transitions off, seams open

"No popping" is the wrong claim. A coarser mesh genuinely is a different surface, so the honest deliverable is the size of the jump, not a denial that it happens. Meshing a block at both its old and new level at the instant it switches: worst 3.136 cells, typically 0.6–1.6. That number decides whether a fade can hide it, and there is no figure for it anywhere in the literature review.

No hitching — but only after the naive version was fixed. Re-extracting all twelve blocks whenever any one changes level costs 12–23 ms and misses the frame. Caching each block's un-inset extraction and rebuilding only the one or two that actually changed takes it to 4.6–12.4 ms. What has to be cached is the extraction before the taper, because inset_boundary mutates positions in place and which faces need it depends on the neighbours' levels.

The ladder runs along one axis on purpose: every seam is an x seam, so a crack has no second axis to hide behind. A production terrain needs four faces, or six with caves, and this does not show that.

cd bevy_isomesh && cargo run --example game_lod_flyover --release

Space pause · T transitions on/off · [ ] speed · R reset.


Connected is not the same as passable

A bore widening through rock while a body-sized ball waits outside, the aperture readout counting up and flipping green

aperture_gate — a capsule bored through a slab at 65³, radius sweeping up. At the frame shown the aperture is 3.50 cells and the body is 4.00: the faces are connected, the ball does not fit, and the matrix cell is amber. A boolean connectivity query cannot tell amber from green.

Air can already answer is this sealed. What a game asks is can I get through, and that is not a boolean — it is the bottleneck: the largest sphere that survives the tightest point of the best path. Computed by a union-find over air voxels visited in descending (field value, grid index), which is a total order, so the answer is deterministic with no PRNG, no atomics and no hash map. The output is a 6×6 matrix over the chunk's faces plus a reachability mask, and it composes — neighbouring chunks combine theirs with no global solve.

The three-colour matrix bottom-right is the whole argument: dark is no air path, amber is connected but too tight, green is the body fits. Only one of those three is a connectivity question.

The demo checks itself against the measurement. On startup it replays P-49's registered fixture and prints r2 0.000000 r4 0.000000 r8 0.000000 cells of error — the aperture of a drilled channel comes back as exactly its radius, and the air census matches the closed form (585 / 2,925 / 12,545 samples). It also verifies determinism by solving twice and comparing entry by entry, and verifies the early exit by re-solving without it. The early exit is sound rather than heuristic — each aperture is fixed at the first and therefore highest value that connected its pair — and on the gyroid it fires after 5% of air samples (M-346).

cd bevy_isomesh && cargo run --example aperture_gate --release

1–3 slab / uncapped gyroid / capped gyroid · X restart the sweep.


Sixty-four edits deep, and the mesher only looks at twenty-one

A sculpting stroke building a 64-brush tape while the pruned per-chunk cost climbs at a third of the rate

tape_pruning — 4×4×4 chunks of 33 samples, a 64-brush stroke arriving one edit at a time. At the end: median 21 of 64 brushes survive, a chunk re-meshes in 6.46 ms pruned against 15.84 ms whole, the world aggregate is 2.45×, and the graph bottom-left is the whole argument — red is the cost against the full tape, green is the cost against the pruned one.

Every sample of every chunk evaluates every brush, so an edit history is a tax the mesher pays forever. It does not have to. Bound each brush's shape over the chunk's box — one field sample per brush, f(c) ± l·r for a Lipschitz constant l — and any brush that provably cannot win the min/max chain inside that chunk can be deleted before meshing starts. The deletion is exact, not approximate: IEEE min and max select an operand rather than computing a new value, so dropping a loser moves the result by zero ULP. The HUD's mesh identical: YES — 64/64 chunks bit-exact is that claim, checked live on positions, normals and indices every sweep.

P turns pruning off and the red number becomes the cost. Nothing else on screen changes — that is the point.

One thing the demo refuses to overstate. A uniformly scattered tape does not give a flat pruned cost: doubling the tape doubles the brushes overlapping any chunk, so a constant fraction survives and the green line still climbs — at 95.5 µs per brush against 241.6 µs, a 2.5× shallower slope rather than a horizontal one. The flat line belongs to a moving stroke, where a chunk's survivor count saturates, and that is the kinder case. What the mechanism buys is the divisor, and the divisor is measured.

The bound costs 0.99 µs per chunk — 1.5 × 10⁻⁴ of the meshing it enables (M-341).

cd bevy_isomesh && cargo run --example tape_pruning --release

P pruning on/off · H survivor heat map · C chunk boxes · X restart the stroke.


288 chunks re-meshed, without missing a frame

An overloaded edit queue draining under a 2 ms frame budget

Every chunk in the world dirty at once, and the queue re-fills the instant it empties. 28.6 chunks a frame at a 2 ms budget, 60 fps throughout.

A meshing paper reports throughput — chunks per second, milliseconds for a grid. A game cannot spend that, because it does not get a second. It gets 16.7 milliseconds and then it has to draw something. The question is not how fast the queue drains but what it costs the frame it lands on.

The flat line is worth nothing without the version that spikes, so U drains the identical queue through mesh_dirty, which does all of it now:

unbudgeted, one frame 20.62 ms — misses a 16.7 ms frame
budgeted, worst frame 2.10 ms
lower peak, same total work 9.8×

A budget does not make meshing cheaper. It decides which frame pays, and the total is unchanged.

The guarantee, priced. mesh_within_budget consults its predicate after each chunk, so a budget too small for one chunk still meshes one — the doc calls overshooting by at most one chunk "the price". Swept from 25 µs to 8 ms, a 320× range:

budget chunks/frame mean ms overshoot ms
0.025 1.00 0.085 0.157
0.200 3.49 0.251 0.123
1.000 15.15 1.016 0.099
2.000 28.80 1.905 0.117
8.000 96.02 6.269 0.122

One chunk here is about 0.072 ms, and the overshoot is bounded by it and flat across the whole range. At a 25 µs budget — a third of one chunk — the rate is exactly 1.00 chunks per frame: never zero, never two. Both halves of the doc's argument are now numbers.

cd bevy_isomesh && cargo run --example game_budget --release

Space overload · U drain it unbudgeted · [ ] budget.


Undo without a snapshot, and an order that is load-bearing

A carved block with the edit log and the commutation audit on the HUD

Eight brushes folded over a block. The field is the log — BrushStack { base, brushes: &log[..cursor] } — and undo moves the cursor back by one.

Nothing is snapshotted. A snapshot of a voxel world is the whole world; a log entry is a shape and an enum. The previous state is recomputed, which costs the re-fold — 8.9 ms here over 8 ops and 32 chunks, growing with history because every field sample walks the whole log.

Undo then redo returns a bit-identical world, checked with an FNV-1a hash over every position and index so one vertex moving by one bit anywhere fails it. The check also requires the undone state to have differed, or an undo that did nothing would pass.

The interesting part is the order. BrushOp::commutes_with promises "the honest answer rather than an optimistic one: only identical hard operations commute." This demo audits that against reality — swap every adjacent pair, re-fold, compare hashes:

said commute, mesh bit-identical 5
said no, mesh moved 2
said commute and the mesh moved (unsound) 0
said no, mesh unchanged (conservative) 0

Sound and tight. But it answers on operations alone and cannot see shapes, and that is where its conservatism lives: two brushes that never touch commute whatever their ops are. Run the same audit on a fixture whose brushes are disjoint and those 2 pairs come back conservative instead — which refines M-37 rather than contradicting it. Order matters across an add/subtract boundary when the shapes overlap.

That first fixture was the one this demo shipped with, briefly, and it reported 0 of 7 pairs where order mattered — on a demo whose entire subject is that order matters.

cd bevy_isomesh && cargo run --example game_editor --release

Z undo · Y redo · E edit · S swap the last two ops · X clear.


The whole pipeline on the GPU, and the bus is never touched

A carved sphere resting on a slab, drawn entirely by a mesh shader

16,436 triangles at 65³, extracted and drawn in 0.41 ms, 60 fps. Per frame the CPU sends a camera matrix and three brushes. Nothing else crosses the bus in either direction.

Four things happen per frame and none of them involve the CPU touching geometry:

the field is evaluated by a compute pass no samples are uploaded
three moving brushes are folded over it the edit log is a few hundred bytes
Marching Cubes runs, prefix-summed on the GPU four bytes come home: the triangle count
a mesh shader draws it straight from the position and normal buffers, no vertex or index buffer

The extraction alone went 15.01 ms → 0.54 ms at 129³ across three tickets, and none of that came from making the extractor faster. count + emit never moved — 0.11 ms then, 0.04 now. Every gain was data movement removed: the per-cell counts that used to be copied home to be added up (8.4 MB at 129³), the samples that used to be uploaded, and finally the geometry that used to be read back.

It refuses rather than falling back. On an adapter without EXPERIMENTAL_MESH_SHADER the pipeline is not built, the HUD says so, and nothing is drawn — no vertex-buffer pipeline is quietly substituted in its place. That branch is forced with ISOMESH_NO_MESH_SHADER=1 and screenshotted, because a branch nothing can execute is not a tested branch.

cd bevy_isomesh && cargo run --example gpu_mesh_shader --release

[ ] resolution · Space freeze the brushes.

← back to the README


Graffiti that survives the wall it was sprayed on

Coloured paint on a wall with two holes blown through it, the paint intact and the rims bare

Twelve sprays, then two holes blown through patches that were definitely painted. The paint around each hole has not moved, and the freshly exposed rim is bare — paint reaches a fixed depth from the surface as it stood when the spray happened, so a carve exposes material that was never painted.

The research this demo comes from prices the feature on L²-nearest attribute transfer over a common subdivision — machinery for carrying per-vertex colour from an old mesh to a new one, on the reasoning that the shared tetrahedral grid is the common refinement so the expensive half comes free.

There is none of it here, because there is nothing to transfer. A world in this crate is a base field plus an ordered log of edits, so paint goes in the log: a splat is an entry alongside the carves, and colour is a function of world position. The carve moves the surface. The paint was never on the surface.

So the answer is not a small number, it is an equality:

paint drift across two carves 0.000000
probes whose surface those carves destroyed 20 of 320
sprays that register drift (the control) 27 of 40, up to 0.886

That last row is the one that makes the first row mean anything. The first version of this measurement sprayed a single colour, so repainting red over red was numerically identical to paint staying put — it printed 0.000000 at every step and would have printed the same on an implementation that smeared the paint across the wall. Cycling the palette makes the same instrument sensitive, and only then is the zero across the carves evidence of anything.

The cost of exactness is that every sample walks the log, and it grows sub-linearly:

edits in the log 1–10 11–20 21–30 31–40
ms per re-meshed chunk 0.1375 0.1748 0.2450 0.3200

2.33× for 40× the log — the same shape game_dig measures for brushes alone. Meshing itself pays almost nothing for the paint: the field walk skips sprays without evaluating their shapes, and a log with no sprays in it samples bit-identically to the plain BrushStack.

One asymmetry worth knowing before you build on this. A carve changes geometry, so the chunks to re-mesh come from mark_edit comparing the field either side of it. A spray changes only colour, so the field either side is bit-identical and mark_edit correctly answers "nothing moved" — its chunks have to be named geometrically instead, from the nozzle's own bounding box.

cd bevy_isomesh && cargo run --example game_paint --release

LMB spray · RMB blow a hole · 1–5 colour · [ ] nozzle · X clear the log.

← back to the README


A concave edge, moving, measured every frame

An L-shaped notch cut from a block, with the edge distance for four extractors

solid(p) = max(box(p), min(p.x − cx, p.y − cy)) — a quarter-space subtracted from a block, leaving a reflex dihedral at exactly (cx, cy). The cutter orbits, so the edge sweeps the grid, and all four extractions are re-run every frame at 60 fps.

dual_contouring_cube measures a convex corner and finds Dual Contouring 101× closer than Surface Nets. That is the easy case: a convex corner's solution lies inside its own cell. A concave edge is what a CAD tool actually cuts — the inside corner of every pocket — and it comes out very differently:

mean worst
dual contouring 0.0798 0.3481
dual contouring, clamp off 0.0801 0.3481
surface nets 0.2895 0.5426
marching cubes 0.3412 0.5528

Distance from the exact edge to the nearest vertex, in cells. Dual Contouring is 3.6× better in the mean and only 1.56× in the worst case — decisively better typically, converging toward the others at the extreme.

The sweep is what makes that honest. E-104 measured one static configuration and needed a rule about which resolutions to skip to defend it against grid alignment. A moving feature is unaligned almost everywhere and aligned occasionally, and reporting the worst over the sweep is the only way the occasional case gets counted at all.

A prediction was registered before the run, and lost. The guess was that the cell clamp caps Dual Contouring here — M-28 found it costs nothing on a convex corner because the solution is interior to its cell, so a reflex edge whose solution "wants to sit outside" ought to be capped by it. Clamped and unclamped are identical, 0.3481 either way. The reasoning was wrong in a simple way: a reflex edge passing through a cell has its solution inside that cell too, so there is nothing to catch. M-28's result extends to concave features rather than being limited by them.

What does cap the worst case is left unestablished. It coincides with the edge lying near a sample plane, which is E-104's alignment trap arriving continuously, but this demo does not isolate it — and saying so is cheaper than guessing.

cd bevy_isomesh && cargo run --example game_csg_props --release

Space pause · A displayed extractor · [ ] resolution.

← back to the README


A world that streams past you

Terrain streaming past a flying camera

Chunks loading and unloading continuously as the camera flies. Nothing here is pre-baked — every chunk in frame was extracted while you were watching.

Unbroken fBm terrain to the horizon, with a HUD reading 234 chunks resident, 0 waiting, 60 fps

game_terrain_stream — 234 chunks resident, 117,792 triangles, 3.2 MB, 60 fps at 16.65 ms/frame while the camera flies and chunks load and unload continuously.

This is the first example where none of the pieces are visible on their own. G-007 decides which chunks exist, with a hysteresis band so a camera drifting across the boundary does not re-mesh the same chunk every frame. B-003 extracts them on the async task pool — never in a system — and applies finished meshes under a frame budget. G-001's layout is what makes each chunk's world position exact rather than merely close, which is what stops the seams (M-32).

The number to watch is not the triangle count. It is ms/frame while chunks are landing, because a streaming world that hitches is one doing its meshing on the main thread.

One correction is worth repeating, because it is the mistake the API invites. A radius-based residency rule loads a ball of chunks, and a heightfield does not need one: the first version held 952 chunks with 606 permanently waiting, and rendered as holes that never filled. Bounding the vertical extent to the two layers that can contain the surface — which is what a real game does — takes it to 234 resident and nothing waiting (M-104).

cd bevy_isomesh && cargo run --example game_terrain_stream --release

Space fly/pause · [ ] view distance · W wireframe.



Walking every seam

Terrain with a walker on it and a HUD reading 495 seam crossings, 0 holes, seam lip 0.412 cells against terrain roughness 0.539

game_walk — 495 seam crossings tested, 0 holes. The worst vertical discontinuity at a seam is 0.412 cells, against 0.539 cells within a single chunk: the joins are smoother than the terrain they join.

This example is designed to fail. Chunks are meshed independently, and whether two of them actually meet is decided by the overlap G-001 chose — get it wrong and you fall through the world at a boundary. So every frame casts a dense transect of rays straight down against the meshed triangles, through parry3d, and counts holes and lips.

Two details make the answer trustworthy. The ray hits the mesh, not the field — asking the field would test the field, which was never in doubt. And a lip is compared against the terrain's own roughness rather than against zero, because real terrain has real steps and a fixed threshold would be measuring the landscape.

The first version of that test reported 439 holes and declared the overlap broken. The bug was one operator wide in the test itself: a probe must only count as a hole once every chunk layer that could hold the surface has meshed, and the guard said || where it needed && (M-105).

cd bevy_isomesh && cargo run --example game_walk --release

Space walk/pause · [ ] view distance · W wireframe.



Digging, with the numbers a game actually cares about

Carving a tunnel into terrain, with the re-meshed chunks outlined

game_dig — first person, left click to carve. The blue boxes are the chunks the last edit re-meshed: 3 of them, in 0.41 ms. Nine chunks are resident; the other six were not touched and were not looked at.

This is the first example where the mesh is rebuilt while someone is holding the mouse down, and it exists to put two numbers on screen that no benchmark can produce:

Edits compose rather than mutate — the field is a stack of brushes over the terrain, which is what makes undo a re-fold of the log rather than a snapshot. So every field sample walks every brush, and the cost grows. Measured over a scripted 60-carve run, median ms per re-meshed chunk:

edits in the log 1–15 16–30 31–45 46–60
ms per chunk 0.158 0.354 0.525 0.589

3.7× for 7× the log, and flattening — real, and not proportional, which is weaker than "every sample walks every brush" makes it sound.

cd bevy_isomesh && cargo run --example game_dig --release

LMB carve · RMB fill · WASD/QE move · [ ] radius · X clear the log · C chunk outlines.



Two levels of detail, and the crack between them

A gyroid meshed at two resolutions with a jagged gap down the seam

The same pair with transition cells bridging the seam in orange

transvoxel_seams — one field, two blocks, the left at full resolution and the right at half. Meshed independently they do not meet: 184 unmatched boundary edges lie in the seam plane and you can see straight through them. Transition cells take that to 0, and the orange band is the 310 triangles doing it.

Both counts are taken in the seam plane only — each block is legitimately open at its outer borders, so a global boundary count would drown the signal.

The stitch has a width, and that is not cosmetic. A zero-width transition patch also closes the crack — Lengyel says so explicitly — and closing it is all it does: every one of its vertices lies in the seam plane, so the patch stands exactly perpendicular to the surface it is stitching and shades as a hard crease. Measured: |cos| against the surface normal is 0.000 at zero width and 1.000 at w = 2^(k−2). Giving it a width means the coarse block's boundary cells have to be scaled inward by the same amount, which is Lengyel's Equation 4.2 — and written in the block's own cells rather than level-0 cells, the level index cancels out of it entirely.

The property underneath all of it is bit-exactness: a crossing on a half-resolution edge lands on precisely the vertex the coarse neighbour's own Marching Cubes pass produced, at every spacing tried including 4/14. An earlier version of that arithmetic was off by 1.11e-16, which is a crack no weld can close — a weld merges vertices it can see are the same, and those two are not.

cd bevy_isomesh && cargo run --example transvoxel_seams --release

T transitions · 1–4 field · [ ] resolution.



Handing a mesh to a physics engine

parry3d's constructor is not a validity check. Its only documented failure is an empty index buffer — measured, it accepts a zero-area triangle and it accepts a two-chunk mesh with an unwelded seam. A renderer draws that seam correctly; a physics engine reads it as a hole and a character walks through the floor.

So collider::readiness reads the validator's report through a collider's eyes and says which of three different things you have:

means
is_usable() parry will take it and behave — no trailing index, no out-of-range index, no non-finite position
is_seam_free() no duplicate vertices, so nothing was assembled from chunks without welding
supports_inside_outside() closed, manifold and consistently oriented, so parry's ORIENTED pseudo-normals mean something

They are three predicates rather than one because a single chunk of a streamed world is open by construction and is still a perfectly good collider — it just cannot answer "is this point inside". Folding them together would make every chunk in a real world read as broken.

The seam, measured: two adjacent chunks of a torus concatenated give 36 duplicate vertices and 180 boundary edges. After welding, 0 and 108 — and those 108 are the slab's own outer border, which is genuinely open. The weld closed exactly the 72 that were the seam.

The crate takes no dependency on parry to do this. It emits the Vec<[u32; 3]> parry already wants, and the conversion is one line at the call site:

let indices = isomesh::collider::triangle_indices(&mesh);
let vertices = mesh.positions.iter().map(|p| Vector::new(p[0], p[1], p[2])).collect();
let trimesh = TriMesh::new(vertices, indices)?;


Where the caves join up

An isovalue sweep on cave terrain as the giant air component appears and takes over

cave_percolation turns P-176's transition into a dial a level designer can read: on noise_cavity the giant air component appears at an isovalue of 0.029340, and at iso = 0 one component holds 0.814493 of the air against 65 isolated pockets — C1 and C3 held on 84 of 84 rows, with the crate's own Air union-find agreeing with an independent one on the component count, the air-sample count and the full sorted size multiset on every row.

The 2D slice is the theorem's actual content: 6-connectivity in 3D, 4 in 2D, and on noise_cavity the 2D onset never comes. On fbm_terrain it does — at 7.27 — which the registration named in advance as a real risk, fbm_terrain being hash-based lattice noise and not a Gaussian field. C2's 42/42 split is that comparison, shown live.

← back to the README


sealed_cave — is this cave still connected to that one?

cd bevy_isomesh && cargo run --example sealed_cave --release

F plugs the tunnel, G re-opens it, C outlines the chunks.

The mesh cannot answer this question. validate will tell you a mesh is closed, manifold, correctly wound and Hausdorff-close to the field, and none of that says whether water in the left chamber can reach the right one. That is a question about the connected components of the air region, asked after every edit rather than once at build time — and the two chambers here are in different chunks, so no single chunk can answer it alone.

readout what it means
components air components across the world, seams included. 1 → 2 on the plug
A↔B AirWorld::connected between one sample in each chamber, in global sample coordinates
visited samples the replacement search touched — compare to chunk, not world
stitch the restitch, whose nodes are components rather than samples

The number to watch is visited, and what sets it is not obvious. Severing a passage is the shape that beats an unchunked structure: lockstep search stops when all but one frontier exhausts, so a fill splitting two pieces of similar size walks both — measured at 1.1× a full rebuild on one large grid (M-321). Chunking bounds that walk by the chunk (M-322).

But the bound is not the cost. This example reports about 0.03× a chunk, because the chambers are in chunks 0 and 2 while the plug is in chunk 1 — so the local search separates two short tunnel stubs, and the split between the large components is resolved by the boundary graph. Put both halves inside one chunk and the same edit costs 0.97× instead: 35× apart, decided entirely by geometry (M-323).

The plug drives the mesh and the connectivity from one flag, so the picture and the answer cannot disagree. A real engine would route one edit to both; this keeps them in step by construction.