A from-scratch, modular voxel game engine — "Teardown-scale Minecraft." Walk a procedurally generated world with trees, place and break voxels, and blast structures into physically simulated debris that tumbles, collides, and settles. The same engine runs at Teardown-scale (10 cm voxels) or Minecraft-scale (1 m voxels), chosen per world at creation time.
Features a dynamic day/night cycle with cascaded shadow mapping, cel-shaded lighting with HDR post-processing, transparent water, 3D animated grass, a fire propagation system, undo/redo, an in-engine voxel editor, replay recording, and an optional Super Mario 64 integration (bring-your-own-ROM).
Everything that defines engine behavior — voxel storage, noise, worldgen,
meshing, the rigidbody solver, destruction — is custom Rust, no game engine
or physics engine underneath. Third-party crates are infrastructure only
(GPU, windowing, math, threading, UI, data formats). See
docs/plans/2026-07-04-voxel-engine-design.md
for the full design rationale.
cargo run -p vox-app --release
cargo run -p vox-app --release -- --scale 1.0 --seed 42
cargo run -p vox-app --release -- --help
Mario support is kept out of the default build so a clean clone does not
require the optional native library. Enable it explicitly after completing
the setup below with cargo run -p vox-app --release --features mario.
Requires a DX12 or Vulkan-capable GPU (via wgpu). First launch generates terrain and meshes it, which can take a second or two on the default 0.1 m world.
| Input | Action |
|---|---|
W A S D |
Move |
| Mouse | Look (click once to capture the cursor) |
Space |
Jump (walking) / fly up (noclip) |
Shift |
Fly down (noclip) |
Ctrl (held, noclip) |
5x fly speed |
F |
Toggle fly / noclip |
1-6 |
Hotbar: 1 Dig, 2 Scalable Dig, 3 Bomb, 4 Death Laser, 5 Place Water, 6 Ember |
| Mouse wheel | Adjust tool radius (Scalable Dig / Bomb / Place Water / Ember) or cycle build material (otherwise) |
| Left click | Use the active hotbar tool |
| Right click | Place selected material |
[ / ] |
Shrink / grow the Scalable Dig / Bomb / Place Water radius (0.5-4 m) |
B |
Spawn a wood debris cube in front of the player |
T |
Spawn a 5-segment rope near the player |
X |
Clear all sleeping (settled) debris |
E |
Toggle editor mode (LMB paint sphere, RMB erase sphere, wheel adjusts radius) |
Ctrl+Z |
Undo last world edit (safe edits only — no debris-spawning operations) |
Ctrl+Y |
Redo last undone edit |
R |
Toggle replay recording (snapshots player + camera + debris every ~1s) |
P |
Toggle replay playback |
M |
Toggle Mario mode (requires SM64 ROM — see below) |
F3 |
Toggle the debug overlay (FPS, timings, tuning sliders) |
Esc |
Release the cursor, then exit |
| Slot | Tool | Behavior |
|---|---|---|
| 1 | Dig | Breaks exactly the one voxel under the crosshair. |
| 2 | Scalable Dig | Carves a sphere of adjustable radius; severed material falls under gravity alone -- no impulse. |
| 3 | Bomb | Carves a sphere of adjustable radius and gives the debris an outward blast impulse. |
| 4 | Death Laser | An effectively infinite-range beam that tunnels straight through everything in its path in one shot -- no raycast gate, no impulse, just an instant, total cut. |
| 5 | Place Water | Fills a 0.5 m default, adjustable water source on the empty face of the targeted terrain; it starts flowing immediately. |
| 6 | Ember | Places an ember that ignites flammable materials (wood, leaves, planks, grass). Fire spreads via 6-neighbor CA, burns to ash, and emits smoke particles. |
Slots 7-9 are reserved for future tools.
voxelengine [--scale 0.1|1.0] [--mario-scale N] [--seed N] [--extent X,Y,Z] [--help]
--scale is the voxel edge length in meters — this is the one setting that
switches the whole engine between Teardown-scale and Minecraft-scale.
--extent is the world's footprint in meters. --mario-scale sets SM64
units per meter for Mario mode (default 125, Mario ~1.3m tall). See
crates/vox-app/src/args.rs.
Mario mode is an optional easter egg that integrates Super Mario 64's
movement and physics into the voxel world. Press M to toggle. Mario
runs, jumps, ground-pounds, and wall-kicks through destructible terrain.
The SM64 ROM is NOT included (it's copyrighted Nintendo content). You must provide your own legally-obtained SM64 US ROM.
Initialize the pinned public libsm64 submodule and generate its Mario geometry before enabling the feature:
The native library uses GNU C extensions. On Windows, build the Mario
feature from an MSYS2 MinGW shell (or another environment that provides
gcc); the default Rust build does not require that toolchain.
git submodule update --init
Push-Location libsm64
python import-mario-geo.py
Pop-Location- Obtain an SM64 US ROM (
.z64format). The expected file has SHA1:9bef1128717f958171a4afac3ed78ee2bb4e86ce - Run
enable_mario.bat(Windows), either:- Double-click and it auto-searches for
.z64files, or - Drag-and-drop your ROM onto the script, or
enable_mario.bat "C:\path\to\your_rom.z64"
- Double-click and it auto-searches for
- The script validates the ROM by SHA1 and copies it to
roms/baserom.us.z64 - Launch with
cargo run -p vox-app --features marioand pressM
Without a ROM, pressing M logs a warning and does nothing — the game
works fully without Mario mode.
- Full SM64 movement: run, walk, jump, double/triple jump, ground pound, wall-kick, wall-slide
- Wall collision via libsm64 surface objects (all 6 face directions)
- SM64 audio: jump sounds, footsteps, ground pound, background music (resampled 32→48 kHz via cpal, volume 0.5)
- Mario ↔ debris interaction: Mario can stand on, kick, and ride falling debris bodies (registered as SM64 surface objects)
- Ground-pound craters: carving shallow dents into terrain for traversal
- Ground-pound bombs: explosive blasts that detach structures (like the Bomb tool, aimed with a jump)
- Triple-jump-pound: raises a voxel mound for building ledges
- Third-person camera with yaw/pitch, interpolated between 30 Hz ticks
- Day/night cycle: 120-second cycle starting at noon. Sun direction, color, sky color, ambient, and fog all driven by time-of-day uniforms threaded through every pipeline.
- Cel-shading: Half-Lambert lighting quantized into 4 bands with smooth transitions for a painterly, comic-book look.
- HDR post-processing: Scene renders to an offscreen Rgba16Float buffer, then a fullscreen pass applies soft tone-mapping, +30% saturation boost, and warm color grading before compositing to the swapchain.
- Cascaded shadow mapping: 2048×2048 depth texture, PCF 3×3, orthographic camera following the player, sun-direction driven.
- Transparent water: Alpha-blended (0.85), blue tint scaled by sun strength (dims at night), depth-write disabled, rendered last so terrain/grass shows through translucency.
- 3D grass blades: Real geometry (blocky quads) standing up from grass voxels, scale-aware wind sway in the vertex shader, day/night lighting, and regeneration only after terrain edits or a 5m camera move.
- Crack decals: Procedural shader-based dark branching lines on
solid voxels, driven by a
crack_intensityuniform (default 0 = off). - Smoke particles: World-colliding billboard particles with inter-particle repulsion, enclosure-aware drag, emitted by fire.
- SSAO: Screen-space ambient occlusion reconstructs view-space normals from the depth buffer and samples a 32-direction hemisphere kernel to darken crevices, under-overhangs, and contact areas. Half-resolution AO buffer with 3×3 box blur, applied before tone mapping. Intensity and radius tunable via the F3 debug overlay.
- Bloom: Bright pixels (fire, ember, sun-lit surfaces) bleed light into surrounding areas via a luminance-threshold bright pass followed by a 13-tap separable Gaussian blur at half resolution. Added after color grading. Intensity and threshold tunable via the F3 debug overlay.
- Fire system: Ember ignition (hotbar slot 6), fire spreads through flammable materials (wood, leaves, planks, grass) via 6-neighbor CA. Burning cells show orange glow, then transition to ash (full burn) or char (water extinguished). Smoke emitted from burning/consumed cells. Unsupported material above consumed cells detaches as debris.
- Fluid sim: Water flows downhill with 8-direction drop-search, momentum memory for cohesive flow. Weathering: grass→dirt→mud, stone→sand erosion with waterfall boost, mud drying. Powders (mud, sand) fall and pile at an angle of repose.
- Water pollution: Mud adjacent to water dissolves into muddy_water (a murky fluid that flows like water). Pollution diffuses by contact to adjacent clean water. Still muddy_water settles after ~10s, clarifying to clean water and depositing sand on the floor below. Muddy water also extinguishes fire and provides buoyancy for floating debris.
- Buoyancy: Debris bodies float or sink based on material density vs fluid density. Wood floats, stone sinks.
- Joints + rope: Distance-constraint joints connect debris bodies,
maintaining a rest length between anchor points. Solved interleaved with
contacts in the sequential-impulse solver, with warm starting and split-
impulse position correction. Joint-connected bodies sleep/wake together
via island consensus. Rope segments (2×2×5 voxel bodies of rope material,
20 voxels each) connect end-to-end via joints. Press
Tto spawn a 5-segment rope near the player. Rope can be cut, burned, and destroyed — severing a segment breaks the joint chain.
- Editor mode (
E): LMB paints a sphere of the selected material, RMB erases a sphere, mouse wheel adjusts the radius. The player still flies/looks normally — only mouse-button meaning changes. - Undo/redo (
Ctrl+Z/Ctrl+Y): Tier-1 world-edit undo via voxel diffs. Only safe for operations that don't spawn debris. - Replay (
Rto record,Pto play): Snapshot-based replay capturing player, camera, game time, and debris body transforms every ~1 second. 10 minutes of recording capacity.
Ten crates, strictly layered — nothing lower depends on anything higher:
vox-app playable binary: game loop, player, tools, wiring
|
vox-debug egui debug overlay (HUD, timings, tuning) — quarantined
vox-render wgpu pipelines, camera, chunk/debris draw, shadows, post-process
vox-platform winit window, input mapping, fixed-timestep loop
|
vox-sm64 libsm64 FFI: Mario movement, collision, audio (bring-your-own-ROM)
vox-sim cellular automata: fluid sim, fire, weathering, powders
vox-physics rigidbody solver + destruction (carve -> connectivity -> debris)
vox-mesh greedy meshing (pure functions, headless)
vox-gen noise, terrain, trees (deterministic)
|
vox-world chunk storage, world edits, raycasting, dirty tracking
|
vox-core coordinates, voxel scale, material registry, config, errors
vox-worldknows nothing about rendering or physics.vox-meshis pure data-in/data-out — no GPU types, runs headless.- Everything below
vox-renderruns headless (unit-testable, CI-able). vox-renderhas no winit dependency; windows enter only aswgpu::SurfaceTarget.vox-debugowns egui entirely — vox-app never imports theeguicrate directly.
Gameplay-meaningful quantities are always in meters/SI in public APIs —
player height, tree height, blast radius, material density — converted to
voxel counts only at the point of use (vox_core::coords). This is what
makes one engine correctly run Teardown-scale or Minecraft-scale worlds:
every system is written against meters, not voxel counts, so changing
voxel_size_m doesn't require touching gameplay code. The scale-invariance
tests in vox-gen (terrain, trees) and vox-physics (character controller)
enforce this mechanically — the same seed produces matching terrain/tree
heights and matching player behavior at both 0.1 m and 1.0 m.
vox-physics::destruction: carve a shape from the world (a sphere for
Scalable Dig/Bomb, a capsule along a line for the Death Laser, recording what
was removed) -> flood 6-connected outward from each solid voxel exposed
by the removal, following the actual voxel shape (no artificial search box)
-> detach anything a flood proves is bounded into a VoxelGrid
rigidbody. Each flood is a proof, not a heuristic: it stops the instant it
reaches the world floor or exceeds a generous give-up cap (proof this
component connects to something far too large to be anything but ordinary
terrain) or exhausts naturally under that cap (proof it's a genuinely
isolated island, however large). Whether that proven-bounded component is
then small enough to spawn as one rigidbody is a separate, later decision
(MAX_BODY_VOXELS) — tiny fragments are discarded as dust, implausibly
large ones are left resident in the world; the two caps are deliberately
different numbers, since a real tree's canopy is routinely both "proven
disconnected" and "too big for one body" at once. Because there's no
bounding box to size, a severed tree trunk detaches its full disconnected
top even when it's standing on an enormous terrain mass, and a single voxel
broken out of ordinary terrain resolves in bounded time regardless of world
size — properties an earlier region-growing design, and then a follow-up
shared-cap bug, each got wrong in turn (see the module docs in
destruction.rs for what broke and why).
The rigidbody solver (vox-physics::solver) is a sequential-impulse solver
with warm starting, Baumgarte stabilization, Coulomb friction, and
island-consensus sleeping (touching bodies cross the sleep threshold and go
to sleep together, never individually mid-stack — see the commit history for
why that matters). Collision is voxel-grid-native: a body's surface voxels
are sampled directly against the world's or another body's voxel grid, no
convex-hull approximation.
Debris isn't a dead end: vox-physics::body_destruction runs the same
carve-then-split idea against an existing body's own grid instead of the
world. A body has no floor/anchor concept (it isn't "resting on" anything by
definition), so there's no connectivity proof to run — carving it just
splits its solid voxels into their 6-connected components, and every
component becomes its own fragment (subject to the same dust/oversize
policy), inheriting the parent's linear and angular velocity at its own
offset from the old center of mass. All four hotbar tools raycast against
both the static world and every live body (Tools::raycast_scene) and pick
whichever is closer, so debris is just as breakable as terrain.
Beyond direct tool hits, PhysicsWorld::step reports each body's hardest
single contact that step (ImpactEvent: world point + peak normal impulse).
fracture_radius_vox (in vox-app/src/main.rs, kept pure and unit-tested
apart from live GPU/registry state) compares the implied impact speed
(impulse/mass) against the actual material at that point's strength,
scaled by the live-tunable fracture_sensitivity — higher strength means
a higher threshold (harder to trigger at all), the same "higher survives
more" convention every destruction tool already uses. With the core
material set (leaves 0.5, wood 4.0, stone 8.0) that reads as: leaves give
way at the slightest bump, wood needs a real fall or throw, stone needs a
genuinely hard impact. Every material's radius starts from the same small base bite
(FRACTURE_RADIUS_VOX) at its own bare threshold — a tiny hit always
produces a tiny chip, never "an orb of voxels deleted from space" — and
only grows past that base as the impact clears the threshold by more,
scaled by how fragile the material is (stone's growth factor bottoms out
at zero, so a hard hit still only produces the base bite; leaves grows the
fastest). An earlier version scaled the entire radius by the per-material
factor instead of just the growth, so even leaves' gentlest fracturing hit
already carved a fixed 5x radius — "a tiny hit blows out a huge chunk."
(An even earlier version divided by strength instead of multiplying,
which inverted the whole scale the other way — stone fractured more
easily than wood.)
Impact fracture also scatters a sample of what it carves as small flying
debris chips (body_destruction::carve_body_sphere_at_impact, mirroring
the bomb's own chip idea below but launched along the actual contact push
direction and scaled by impact speed instead of blast power) rather than
letting it all simply vanish — a graze knocks a couple of chips loose, a
violent hit sends several flying, matching Teardown's "satisfying mess"
instead of a clean void. Two more things had to be reined in to actually
get that: MAX_FRACTURE_RADIUS_VOX puts a hard ceiling on the radius
regardless of how fragile the material or how violent the impact (the
per-material growth math could otherwise legitimately reach several
voxels on something as fragile as leaves, which read as "a chunk of the
tree's canopy vanished into a smooth spherical void," not a crumble); and
MAX_IMPACT_CHIPS was raised from an initial 6 to 24 so a fracture at
that radius actually scatters enough visible pieces to read as something
breaking apart, not a handful of specks next to an empty hole. Bodies
below MIN_FRACTURE_BODY_VOXELS are terminal rubble and never fracture
again — see the performance notes for the runaway cascade that rule
breaks.
A body resting or settling after a hit still needs a real contact impulse
every substep just to hold it up against gravity, and that impulse looks
identical, frame to frame, to the one from a body that just landed hard —
without a further check, a low-strength material (leaves) kept
re-reporting that steady load as a fresh impact on every single settling
frame, continuously re-fracturing and re-meshing itself for as long as it
took to fall asleep. This is what "flickering while breaking apart" turned
out to actually be — not a shading bug, a real repeated destruction event.
Fixed by tracking each contact's pre-solve closing speed
(Contact::approach_speed) alongside its accumulated impulse, and gating
impact-fracture eligibility on it (MIN_IMPACT_APPROACH_SPEED_M_S):
a resting/settling contact's closing speed stays near zero every step, a
genuine collision's does not.
Debris bodies accumulate per-voxel damage from sub-threshold impacts. An impact below the fracture threshold but above 30% of it weakens the contacted voxel and its neighbors — damage accumulates proportional to how close the impact was to the threshold. At full damage (1.0), a voxel crumbles to air and may trigger a connectivity-based split. Damage is visible as darkening on the body's mesh (via reduced ambient occlusion baking) and decays at 0.05/s while the body is awake, so a body left alone gradually returns to pristine. This means a beam doesn't snap cleanly on the first hard hit — it cracks, darkens, and weakens progressively before finally breaking, matching how real materials fail.
A plain carve leaves nothing behind but a void, which reads as "the
material vanished" rather than "something exploded." blast (in
vox-physics::destruction) samples a capped, deterministic fraction of
whatever it just carved away and turns those voxels into small flying
L-shaped debris chips instead of clearing them to air outright, launched
outward from the blast center at a modest, hard-capped speed. Two things
had to be fixed to get here, both left as regression tests: a literal
single-voxel chip is a physics degenerate case (its only contact point
sits exactly on its own center of mass, so friction can never generate
torque, and any spin it's given never damps out); a straight two-voxel bar
fixes that for rotation across its length but is still degenerate for
spin around its own long axis. An L-shaped chip (no straight line through
all its voxel centers) has no such axis.
Two hot paths were measured (cargo run -p vox-app --release --example stress) and fixed at the root, not patched around:
World::edit_boxresolves each chunk a bulk edit touches once instead of once per voxel (get_voxel/set_voxeleach cost a hash-map lookup);carve_sphere/carve_capsulebuild on it. Cut a large blast's cost by ~5x.- The connectivity flood (
destruction::flood_from) explores voxels in best-first order toward the world floor, not plain breadth-first, so proving "this connects to solid ground" doesn't require exploring a full sphere around the edit first when the floor is a short vertical hop away — the overwhelmingly common case. Cut the worst case (a beam tunneling through a massive terrain slab) by another ~2.8x on top of the above.
Every debris body is its own GPU buffer set and its own draw call
(VoxelPipeline::draw_bodies) — fine at modest counts, but unbounded over a
play session once bombs scatter debris chips: nothing despawned old debris,
and bodies were never frustum-culled at all. Two fixes address sustained
frame-time degradation specifically (as opposed to the momentary per-blast
cost above): VoxApp now caps live debris at MAX_DEBRIS_BODIES, evicting
the oldest already-asleep body first (never one still actively
flying/settling) via the pure, unit-tested evict_oldest_asleep_debris;
and draw_bodies now frustum-culls debris exactly like draw_chunks
already did for chunks. Also fixed: blast's debris-chip sampler was
fully sorting every removed voxel (tens of thousands, for a large
terrain blast) just to keep ~40 — replaced with a partial selection
(select_nth_unstable_by_key), O(n) instead of O(n log n) over the whole
removed set.
A small body has a disproportionately tiny moment of inertia, so an
off-center contact (landing on one corner) can spin it up far harder than
the same impulse would a large body — and unlike linear velocity (already
capped by MAX_SPEED), angular velocity had no ceiling at all. A 2x2x1
debris chip landing corner-first would settle into a stable 50-60 rad/s
that never decayed: never quiet enough to sleep (so it cost full
broadphase/narrowphase/solver/render work forever, directly worsening "lots
of debris causes lag"), and covering ~50 degrees of rotation per physics
step at 60Hz, well into the range where the render-side slerp between a
step's start/end orientation reads as visible judder rather than a smooth
spin — this is what "rotation causing stutters and glitches" on small
debris specifically turned out to be. Fixed with MAX_ANGULAR_SPEED_RAD_S,
the same idea as MAX_SPEED, applied both where gravity/velocity are
integrated and again after the solver's impulse resolution each substep so
a spike never persists past the substep it occurred in.
The broadphase (broadphase::Broadphase::candidate_pairs, a spatial hash
over body AABBs) used to allocate a fresh hash map, hash set, and output
vector on every single call — and it was called three times per physics
step (once per substep, plus once more for the end-of-step sleep-island
grouping), so with MAX_DEBRIS_BODIES debris around that was real,
repeated allocation/rehashing overhead for no behavioral benefit. Fixed by
making Broadphase a persistent, reusable piece of PhysicsWorld state
(.clear()-and-reuse instead of allocate-fresh) and having the
sleep-island grouping share the same scratch state instead of rebuilding
its own from scratch. The narrowphase's per-pair contact staging buffer is
hoisted and reused the same way.
Three deeper fixes came out of the "small debris rotation looks glitchy, and lots of debris still lags" report, each verified headlessly:
- The fracture cascade (
MIN_FRACTURE_BODY_VOXELSinvox-app): a fracture scatters 3-voxel chips; a chip's next bounce trivially clears a fragile material's fracture threshold (leaves need only 0.5 m/s of delta-v — every bounce qualifies); and every fragment of a 3-voxel body is belowDEBRIS_MIN_VOXELS, so the chip vanished as dust while fresh chips spawned from what was removed — grid clones, component labeling, remeshing, and GPU buffer churn on every bounce of every chip, compounding forever. Bodies below the new floor are terminal rubble: they bounce and settle, never re-fracture. This was both the visible popping/vanishing glitches and a large share of the sustained lag. - Angular damping (
ANGULAR_DAMPING_AIR+ANGULAR_DAMPING_ROLLING): the solver had no mechanism that removes rotation except contact friction, which has no leverage when contact points sit near the spin axis — so debris could spin at the sleep threshold indefinitely, keeping its whole contact island awake (lag) and visibly twitching (glitches). Everyone now gets a whisper of air drag; small bodies (by surface point count, so a tipping tree is exempt and still falls at full speed) get strong rolling resistance while in contact, so grounded rubble stops tumbling almost immediately, sleeps, and frees its island to sleep too. - World-inertia caching (
Body::inv_iw): the impulse loop re-derived the world-space inverse inertia tensor (quaternion→matrix + two mat3 multiplies) ~25 times per contact per substep; rotation only changes at substep boundaries, so it's now computed once per body per substep. Measured ~16% off the settling-pile average (A/B, 300 bodies), and the worst single step dropped from ~47ms to ~21ms across this round's changes combined.
For lower-end machines, two engine-wide constant-factor passes (no content or behavior change):
vox_core::fxhash(a dependency-free FxHash): Rust's default map hasher is SipHash, which pays for collision-flood resistance a game hashing its own voxel coordinates doesn't need. Every hot collection now uses the fast hasher — above all the world's chunk map, consulted on every voxel read engine-wide (contacts, raycasts, carves, floods), plus the broadphase cells/dedup, the solver's warm-start and impact-peak maps, the connectivity flood's visited set, and the render/remesh bookkeeping. Steady-state physics step cost measured roughly 2x faster across pile sizes (e.g. 100-body settling p50 0.175ms → 0.072ms), and it's also more deterministic (FxHash has no per-process random seed).- Chunk-caching in
world_contacts: each surface point costs up to 7 solidity queries, each formerly a fresh chunk-map lookup; consecutive points are spatially coherent, so aSolidLookup(the same cache the destruction flood already used) amortizes nearly all of them away. A falling tree — thousands of surface points, every substep until it sleeps — is exactly this case.
"Small debris caught under large debris makes the large debris react oddly" led to the solver's single deepest fix: split-impulse penetration recovery. The old Baumgarte term turned penetration depth into a velocity target inside the impulse solve, and a contact will spend however much impulse the touching bodies' masses require to reach a velocity target — real momentum injected from nothing. Debris chips spawn overlapping the fragment they chipped off of (by up to a voxel — routine, not exotic), and a probe reproduced the report exactly: one three-voxel chip wedged under a settled 5.6-tonne block gave the solver two contradictory demands (floor: "chip up"; block: "chip down relative to the block") that it could only satisfy by lifting the block — ramping it to ~1 m/s and shoving it centimeters. Penetration is now recovered positionally (bodies moved apart directly, weighted by inverse mass, sequentially so hundreds of same-face contacts converge to one correction instead of stacking), which by construction cannot add kinetic energy: the same probe now shows the block peaking at 0.000 m/s with 0.1 mm of drift, and every rest-height/stacking/settling test passes unchanged. Relatedly, warm-start keys changed from (body, target cell, face) to (body, target, surface-point index, face): several points can alias into one cell, and colliding keys made the warm-start map replay one contact's accumulated impulse into every contact sharing the key — plus point identity survives sliding across cell boundaries, so warm starting persists where it used to reset.
Debris body meshing is threaded (BodyMeshQueue, mirroring the chunk
RemeshQueue) for large one-off spawns, but a body has no uploaded mesh
until its meshing job is collected, so routing every spawn through the
queue meant a body was invisible for the frame or two that took — and
since splitting a body during destruction always produces several small
fragments in the very same frame the original is despawned, that read as
the whole cluster flickering/vanishing on every hit. Fixed by meshing
small bodies (INLINE_MESH_VOXEL_BUDGET, 200,000 voxels — raised from an
initial 64,000 once it turned out a felled tree's trunk-plus-canopy is one
connected mass that easily clears that: a single canopy ellipsoid alone is
tens of thousands of voxels) synchronously in upload_debris_mesh
instead: the stress example measures even a 40³ cube at ~1.7ms average,
cheap enough to eat inline, and extrapolates to only ~5ms at the new
budget — one rare tree-felling hitch, not a per-hit cost.
For whatever still clears even that (raised) budget, VoxApp::replace_body
closes the gap a different way instead of just accepting it: the old
mesh is kept exactly where it is (frozen, at its last known transform)
until every one of its replacement fragments' async meshes has arrived,
rather than being removed the instant the old body despawns. This matters
specifically because a large mass like a tree trunk stays that large
across many subsequent hits, not just the first one — the earlier,
budget-only fix made the initial felling instant but every later hit on
the same trunk would still pop it out of existence for a frame, which is
exactly what "invisible for a solid frame every time damage is applied"
turned out to mean.
The whole point of the crate layering is that new systems are additions, not edits to existing ones.
Add a material — pure data, no code: drop a .toml file into
assets/materials/. Every *.toml in that directory loads in
case-insensitive filename order (see the header comment in
assets/materials/core.toml for the schema).
Duplicate names across files are a load error, not a silent override.
Add a tool — add a variant to Tool in
crates/vox-app/src/tools.rs, a method on
Tools implementing it, a slot in the HOTBAR table, and one match arm in
VoxApp::apply_tools (in main.rs) to wire it to input. Tool::Bomb is the
fullest example: raycast -> vox_physics::blast -> done.
Add a whole new engine system (e.g. ecosystem/creature life) — add it as
a new sibling crate at the vox-gen/vox-physics tier, the way
vox-sim (the cellular-automata fluid sim) already does: it can depend on
vox-core and vox-world (and vox-physics if it needs bodies) without any
existing crate changing. This is deliberate: the layering was chosen so that
"add a concept" means "add a crate," not "thread a new dependency through
six existing files."
Dependency policy: third-party crates are infrastructure only (GPU, windowing, math, threading, UI, data formats, error derives). Everything that defines engine behavior is ours. Before adding a new crate dependency, ask whether it behaves like part of the game (noise, ECS, physics) — if so, it probably shouldn't be a dependency.
cargo test # ~253 tests, everything below vox-render runs headless
cargo clippy --all-targets -- -D warnings
cargo run -p vox-app --release --example stress # headless perf probe, not a test
Everything below vox-render is unit-tested, including the physics solver
(single/multi-body settling, stacking, a confined pile stress test),
destruction (bridge/pillar severing, floating-fragment detection, a severed
structure atop a huge terrain mass, and a bounded-time check on breaking a
single voxel deep in terrain), procedural generation
(deterministic noise, terrain and tree scale-invariance), and the greedy
mesher (watertightness verified against a brute-force reference on random
inputs). vox-app's tools/CLI have their own tests, including one that
drives the actual raycast-based blast entry point end to end: blast a
pillar's base, confirm the upper section detaches, tumbles, and sleeps.
- Day/night cycle (#10/#12): 120-second cycle, all lighting uniforms threaded through voxel + Mario + grass pipelines.
- 3D grass blades (#11): Real geometry, wind sway, day/night lighting.
- Transparent water (#13): Alpha-blended, blue tint, proper face culling.
- Cascaded shadow mapping (#14): 2048×2048, PCF 3×3, sun-direction driven.
- HDR post-processing + cel-shading (#15): Offscreen Rgba16Float, 4-band lighting quantization, saturation boost, color grading.
- Crack decals (#43): Procedural shader-based,
crack_intensityuniform. - Undo/redo (#36): Tier-1 voxel-diff undo (Ctrl+Z/Y).
- Editor mode (#37): Brush tools, LMB paint / RMB erase sphere.
- Replay (#39): Snapshot-based recording (R/P), 10-minute capacity.
- Mario wall collision (#21): All 6 face directions via surface objects.
- Mario audio (#22): cpal resampled 32→48 kHz, volume 0.5.
- Mario ↔ debris (#23): Surface objects for falling debris.
- Mario ground-pound bombs (#27): ACT_GROUND_POUND_LAND → blast pipeline.
- Mario ground-pound craters (#30): carve_sphere + triple-jump mound.
- Buoyancy (#20): Debris floats/sinks by material density (from upstream).
- Fire system: Ember ignition, CA spread, smoke, ash/char transitions.
- Fluid sim (
vox-sim): Water, weathering, powders, active-cell sleeping. - Smoke particles: World-colliding, repulsion, enclosure-aware drag.
- Performance (#51-57): SolidLookup caching, broadphase reuse, substep contact reuse, Mario surface slice-skip, draw_bodies distance culling, vertex version stamping.
- Streaming chunk load/unload (#7) — HashMap storage is already streaming-ready.
- Save/load (#8) — Uniform/Dense enum designed for palette-RLE serialization.
- Ecosystem & creatures (#9).
- Chain-reaction explosives (#16) —
explosiveflag in MaterialDef. - Sustained-load structural failure (#17) — stress analysis beyond connectivity.
- Mario cap throw (#24), power-up materials (#25), star power-up (#26).
- Lakitu camera modes (#28), coins & stars collectibles (#29).
- GPU compute meshing (#40) — shader written, integration deferred.
- SSR (#41), cone-traced GI (#44), minimimap (#45) — depend on HDR (now done).
- Rope, cloth, temperature, gravity wells, earthquakes (#46-50).
- Gameplay modes: siege, rally, storm survivor, creative, derby (#1-5, #31-35).
- Dependency modernization: wgpu 0.20 / winit 0.29 / egui 0.28 are
pinned to the exact combination proven to compile and render. Upgrading
is contained to
vox-render,vox-platform, andvox-debug.
MIT OR Apache-2.0.