Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Destruction

The signature voxlap moment: carve a tunnel under an overhang and the unsupported rock does not hang there politely — it breaks off, falls, and bursts into rubble where it lands. roxlap’s destruction pipeline reproduces that loop on top of pieces earlier chapters covered — edits, sprites (chapter 7), particles (chapter 8) — with two engine parts of its own:

  • Island detection (roxlap-scene): after any carve, detect_islands finds the voxel regions that no longer connect to support. It floods the chunk format’s RLE runs directly (one run is one search node — no dense decode), 6-connected, seeded from the carve’s bounding box. A region that reaches a bedrock-anchored column bottom is supported; one that grows past the budget is presumed supported (a cheap early exit); everything else comes back as an Island — its voxels, colours and bounds.
  • DebrisSystem (roxlap-render): the crumble loop. It extracts an island from its grid, optionally fractures it per material, registers each piece as a falling sprite at the exact world pose of the voxels it replaced, integrates the fall, and reports impacts for the shatter. Like the particle system it is host-owned and split into a pure simulation half (spawn_island / update — unit-testable, no renderer) and a facade half (sync — batched sprite updates).

The snippets below come from a runnable, windowless example that prints what each stage does:

cargo run -p roxlap-render --example book_destruction

Detecting what came loose

A scene with something to lose — a floor, an anchored pillar, a beam cantilevered off it, crystal grown near the tip:

    // A stone cantilever over a floor: a pillar anchored to the floor
    // (and through it to the format's bedrock at the column bottoms —
    // the support the detector floods toward), a beam sticking out
    // sideways, and a crystal cluster grown near the beam's tip.
    // z is DOWN: the floor is the high-z slab, the rock rises toward
    // smaller z.
    let mut scene = Scene::new();
    let grid = scene.add_grid(GridTransform::identity());
    let g = scene.grid_mut(grid).expect("grid just added");
    g.set_rect(IVec3::new(0, 0, 200), IVec3::new(63, 63, 255), Some(STONE));
    g.set_rect(
        IVec3::new(30, 30, 160),
        IVec3::new(31, 31, 199),
        Some(STONE),
    );
    g.set_rect(
        IVec3::new(32, 30, 158),
        IVec3::new(48, 31, 159),
        Some(STONE),
    );
    g.set_rect(
        IVec3::new(44, 30, 154),
        IVec3::new(47, 31, 157),
        Some(CRYSTAL),
    );
    g.bake(BakeMode::Directional);

Carve, then ask. Detection is a separate call on purpose: hosts decide when it runs (the cave demo runs it on its background carve worker, right after the carve, on the same chunk):

    // Shoot the beam off at its root, then ask what came loose: a
    // budgeted span-BFS floods every region the carve touched;
    // whatever cannot reach bedrock-anchored ground (and stays under
    // the budget) comes back as an `Island` — its voxels, colours and
    // bounds. The pillar survives (still anchored); the beam's tip
    // and its crystal do not.
    let g = scene.grid_mut(grid).expect("grid");
    g.set_sphere(IVec3::new(33, 30, 158), 4, None);
    let islands = detect_islands(
        g,
        IVec3::new(29, 26, 154), // the carve's bbox, any corner order
        IVec3::new(37, 34, 162),
        DEFAULT_ISLAND_BUDGET,
    );
    for isl in &islands {
        println!(
            "island: {} voxels, bbox {:?}..{:?}",
            isl.voxels.len(),
            isl.bbox.0,
            isl.bbox.1
        );
    }

Two properties matter in practice:

  • The budget is a design knob, not just a guard. A detached region bigger than budget voxels stays put — DEFAULT_ISLAND_BUDGET (4096) means shooting the single support out from under a whole gallery will not drop the gallery. Raise it if your game wants building-sized collapses; the flood’s cost is O(min(region, budget)) per component, so the worst case is priced in advance.
  • Support means the format’s bedrock. Every materialised chunk’s column bottom (local z = 255) is uncarvable by construction, so a region touching it can never fall — this holds on stacked-chunk grids too.

Falling

    // Hand each island to the debris system. `spawn_island` extracts
    // the voxels from the grid (one carve + one incremental re-bake;
    // re-mip stays the caller's job, like any edit), splits them per
    // the fracture tables — rock into rounded Voronoi lumps, crystal
    // into sharp plates that keep their emissive material — and
    // registers every fragment as a falling body at the exact world
    // pose of the voxels it replaced.
    let mut debris = DebrisSystem::new();
    debris.set_fracture_patterns(
        &[(CRYSTAL.rgb_part(), CRYSTAL_MATERIAL)],
        &[
            (0, FracturePattern::Chunks { cell: 6 }),
            (CRYSTAL_MATERIAL, FracturePattern::Shards { plates: 3 }),
        ],
    );
    for isl in islands {
        debris.spawn_island(&mut scene, grid, isl, BakeMode::Directional);
    }
    println!("falling fragments: {}", debris.debris_count());

spawn_island does the irreversible part: the island’s voxels leave the grid (a coalesced carve plus one incremental re-bake — re-mip is your job, exactly as for your own edits; fold the island’s bbox into whatever remip_bbox call your carve path already makes, or distant mips keep drawing the rock that just fell). The sprite appears at the island’s world_pivot, so nothing visually jumps — the voxels become a falling body in place.

Fracture is data, keyed by material: Chunks { cell } breaks matter into rounded jittered-Voronoi lumps (stone), Shards { plates } slices it with near-parallel planes (glass, crystal); unmapped materials fall Whole. A mixed island splits per material group, and with the colour→material map installed the fragment sprites register with it — a crystal shard keeps its translucent, emissive material and glows all the way down (chapter 6). Fragments get a small outward drift (fracture_impulse) so a broken slab visibly comes apart instead of falling as a stack.

Physics is deliberately voxlap-simple and deterministic: vertical gravity with a terminal clamp, a cosmetic spin hashed from the island’s position (identical scenes crumble identically), and collision against the scene’s solid voxels with the descent marched in substeps — a fast body cannot skip through a one-voxel shelf on a slow frame. The AABB is the island’s unrotated box; the spin never affects collision.

Landing and shattering

    // Per frame a windowed host calls `debris.tick(renderer, &scene,
    // dt)` and shatters each drained impact into a colour-true
    // particle burst (`ParticleSystem::voxel_debris(&hit.burst_sites(),
    // …)`). The windowless half is `update` + `drain_impacts`: each
    // landed fragment reports where it hit, how fast, and the
    // world-space burst sites — one per voxel, in the voxel's own
    // colour — a particle system scatters.
    for _ in 0..600 {
        debris.update(&scene, 1.0 / 60.0);
        for hit in debris.drain_impacts() {
            println!(
                "impact at z={:.1} @ {:.1} u/s -> {} burst sites",
                hit.pos.z,
                hit.speed,
                hit.burst_sites().len(),
            );
        }
    }
    assert_eq!(debris.debris_count(), 0, "everything landed");

A windowed host wires the impact to the particle system from chapter 8burst_sites() hands back one world-space site per island voxel, in that voxel’s own colour, positioned where the body landed:

for hit in debris.drain_impacts() {
    particles.voxel_debris(&hit.burst_sites(), from, 4.0..9.0, &burst_def);
}

The burst is the same machinery carve_debris uses for bullet craters, so crater debris and crumble debris look and behave like the same rock. Feed hit.pos to your impact sound while you are at it — the cave demo routes it through the same occlusion-shaded boom as a bullet hit (chapter 9).

The cave demo’s wiring

roxlap-cave-demo shows the full production shape (chapter 15): detection runs on the background carve worker (same thread that already carves, relights and re-mips the chunk clone), the extraction happens there too so the batch’s re-mip covers it, and the main thread only spawns the returned islands and ticks the system. Rock is mapped to Chunks, crystals to Shards, and ROXLAP_NO_CRUMBLE=1 switches the whole thing back to plain carves.