Skip to content

Repository files navigation

voxelengine

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.

Quickstart

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.

Controls

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

The hotbar

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.

CLI

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 (Bring Your Own ROM)

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.

Setup

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
  1. Obtain an SM64 US ROM (.z64 format). The expected file has SHA1: 9bef1128717f958171a4afac3ed78ee2bb4e86ce
  2. Run enable_mario.bat (Windows), either:
    • Double-click and it auto-searches for .z64 files, or
    • Drag-and-drop your ROM onto the script, or
    • enable_mario.bat "C:\path\to\your_rom.z64"
  3. The script validates the ROM by SHA1 and copies it to roms/baserom.us.z64
  4. Launch with cargo run -p vox-app --features mario and press M

Without a ROM, pressing M logs a warning and does nothing — the game works fully without Mario mode.

Mario features

  • 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

Rendering

  • 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_intensity uniform (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.

Simulation

  • 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 T to spawn a 5-segment rope near the player. Rope can be cut, burned, and destroyed — severing a segment breaks the joint chain.

Editor & Tools

  • 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 (R to record, P to play): Snapshot-based replay capturing player, camera, game time, and debris body transforms every ~1 second. 10 minutes of recording capacity.

Architecture

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-world knows nothing about rendering or physics.
  • vox-mesh is pure data-in/data-out — no GPU types, runs headless.
  • Everything below vox-render runs headless (unit-testable, CI-able).
  • vox-render has no winit dependency; windows enter only as wgpu::SurfaceTarget. vox-debug owns egui entirely — vox-app never imports the egui crate directly.

Unit contract

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.

The destruction pipeline

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.

Destructible debris and impact fracture

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

Progressive damage

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.

Bomb debris chips

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.

Performance notes

Two hot paths were measured (cargo run -p vox-app --release --example stress) and fixed at the root, not patched around:

  • World::edit_box resolves each chunk a bulk edit touches once instead of once per voxel (get_voxel/set_voxel each cost a hash-map lookup); carve_sphere/carve_capsule build 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_VOXELS in vox-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 below DEBRIS_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 a SolidLookup (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.

Extending the engine

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.

Testing

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.

Roadmap

Implemented (post-MVP)

  • 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_intensity uniform.
  • 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.

Not yet built

  • 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) — explosive flag 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, and vox-debug.

License

MIT OR Apache-2.0.

About

This is a voxel engine designed and curated by Claude Fable 5, GLM 5.2 and Claude Sonnet 5.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages