Voxel destruction
Voxel structures for three.js on the GPU: a town block, a stone bridge and a family house, each thousands of little voxel bodies that really fall when cannonballs knock out what holds them up.
The physics is three-voxel-destruction by Steven Bobyn (MIT), running on his GPU rigid-body solver three-avbd (MIT). Every voxel here is one quarter-metre cube and one body in the solver, fixed until something knocks it loose, and fixed bodies cost next to nothing, so whole buildings stand in the solver and only what's falling is simulated. And everything that falls is simulated: a wall topples whole, breaks where it lands, and lies where it stopped. What holds a building up is kept as a graph of chunks, so a blast finds what no longer stands in a walk over chunks, not a flood through every voxel. Shoot out a third of what carries a storey and everything above comes down; shoot out a bridge's pier and its arches go with it. In the live demo, click to fire a cannonball from the camera at what's under the pointer, drag to orbit and scroll to zoom. The fixes and the look are from Patch's demolition game Brickfall.
Physics: three-voxel-destruction and three-avbd by Steven Bobyn (MIT). Code here is MIT, art is CC0, and the sound is CC0 and CC-BY.
Make anything breakable
A breakable thing here is voxels on a grid, one material each, then a blast where it's hit. There are no meshes to fracture and no welds to tune, so to voxelise something you only say which cells are filled:
- Make a builder (
worldBuilder) and place one or more objects: each is a box of cells and a function that puts voxels in them. An object stands on its voxels in its bottom layer and falls when it's cut off from them, so the object is the unit of collapse. One house is one object. A bridge is one object per pier, so it goes bay by bay. - Give each voxel a material (
Mat): it sets how hard it is (TIER), how much it carries (CARRY), how it bonds when it falls (MAT_BOND), what it weighs and how it's drawn. Glass joined face to face is a pane, and shatters whole. - Build the chunk graph (
new Structure(city)) and the solver (new CityPhysics(device, capacity, buffer),physics.load(city)). - Hit it:
blasts(city, structure, physics, [{ at, radius, radii, push }]). It turns the voxels at the heart to dust, knocks the shell loose in clumps, and drops whatever no longer stands. - Read what's loose back a few times a second (
physics.readLoose, insrc/game/site.ts), so it can come to rest and cost nothing again.
import { worldBuilder, Mat, M } from './src/voxel/world.ts';
import { Structure } from './src/voxel/structure.ts';
import { CityPhysics } from './src/voxel/physics.ts';
import { blasts, RADII } from './src/voxel/destruction.ts';
// A brick hut, 4 x 3 m and 2.5 m tall: M is voxels a metre (four)
const w = worldBuilder(7);
w.place(0, 0, 4 * M, 3 * M, 3 * M, (put) => {
for (let z = 0; z < 10; z++)
for (let y = 0; y < 3 * M; y++)
for (let x = 0; x < 4 * M; x++) {
const wall = x === 0 || y === 0 || x === 4 * M - 1 || y === 3 * M - 1;
if (z === 0) put(x, y, z, Mat.Concrete); // the floor
else if (wall) put(x, y, z, y === 0 && x > 5 && x < 10 && z > 3 && z < 8 ? Mat.Glass : Mat.Brick); // walls, a window
}
for (let y = 0; y < 3 * M; y++) for (let x = 0; x < 4 * M; x++) put(x, y, 10, Mat.Roof);
});
const city = w.finish({ pitch: 46, street: 16, blocks: 1 });
// The voxels draw straight from the solver's body buffer (src/render/view.ts attachVoxels returns it)
const physics = new CityPhysics(device, 60000, bodyBuffer);
physics.load(city);
const structure = new Structure(city);
// Each frame: physics.step(). A hit, breaking soft, medium and hard materials to these radii (m):
blasts(city, structure, physics, [{ at: [2, 0, 1], radius: 1.2, radii: RADII.ball(0, 1), push: 20, core: 0.4 }]);
src/level/structures.ts builds the three structures this way, and is the place to copy from.
What this demo adds on top, all in src/, all reusable:
- One solver for every structure (
CityPhysics.load): the solver and its pipelines are made once, with room for the largest. A structure's voxels are written into its bodies in space-filling order and the solver is told how many are in play, so swapping a structure or rebuilding takes about 10 ms and compiles nothing. The buffers, materials and backdrops are made once too. - A cannonball from the camera (
src/game/cannon.ts): a click finds what's under the pointer in the voxel grids at once, the ball's arc is walked through those grids when it's fired, and each wall it meets gets a blow, due a moment before the ball arrives (a body can't break what's fixed by touching it). Each blow takes 30% off the next. The ball itself is a real sphere in the solver, flying the same arc, so what's loose bounces off it. - The rules that keep a collapse right (
src/game/site.ts, from Brickfall): debris breaks what it runs into by its speed squared; rubble with nothing under it falls again, so nothing hovers; what's carried by steel sags for a moment and creaks before it goes. - A budget for loose voxels (
budget, 16,000): past it what's slow settles where it lies, and a new piece falls as one rigid block with its voxels drawn on it, the fallback for a slow machine. A slow frame rate turns the resolution and the budget down by itself. Nothing is ever deleted to make room. - Slow motion that stays smooth (
speed): smaller solver steps, still one a frame.
Lessons, measured here:
-
Compile by drawing, under a cover.
renderer.compileAsynccompiles for the canvas, and evenscenePass.compileAsyncleft 16 pipelines keyed differently to be compiled again in the first frames, so the 3 s it took was wasted. Here the first frames draw everything, a part a frame under an opaque cover, and wait fordevice.queue.onSubmittedWorkDone()after each, so the page keeps answering while the GPU compiles (all in one frame it answered nothing for 3.6 s) and nothing is left to stall the first frames afterready. Ready went from 13.7 s to 7.5 s. -
Warm up by doing it. Before
ready, every structure is put up, a real shot is fired at the first one, and it's all put back, under an opaque cover from start to finish so none of it is ever seen (hiding the canvas itself stalls the first frame shown). That runs the blast and gather passes, joints and chips, so the first real shot has nothing left to compile. The shots must land in time: from the camera a ball takes 36 frames to arrive, so these are fired from 5 m away. Run this way nothing compiles afterready, measured by wrappingcreateRenderPipelineandcreateComputePipeline. -
Don't hide the physics to save it. Brickfall and three-voxel-destruction, built for tens of thousands of voxels at once, turn three quarters of a buckled storey to dust at once, carry the rest as rigid boxes, and on landing send up to half of each box to dust and snap the rest into a squashed heap, with only 3 to 12% simulated. So a storey vanished behind the dust and rubble appeared. Here all that is gone. With 16,000 voxels loose the solver takes 4 to 8 ms a step on an RTX 4090, and bonds a third as strong let what lands really break. It's why the block has no tower: a 43,000-voxel tower falling can't be all real.
-
Dust is the easiest way to spoil this. Brickfall's amounts, from a first-person camera, filled a block demo's frame for seconds at a time. What did it: a puff for every piece that hit the ground (up to 20 a readback, now 4 and smaller), and smoke at the muzzle, which sits right in front of a camera that's firing (gone).
-
Neighbours hold each other up by accident. Floors of a fallen shop stayed hanging on the hotel next door, for two reasons: rubble could rest against a wall beside it, and a piece as wide as the gap between two flush walls wedges there. Here only what's under a voxel holds it, and buildings stand a voxel apart.
-
Load margin. Brickfall's storeys go when a third of what carries them is gone (with steel counted at six bricks), and that left a house standing on its back wall after four cannonballs. Here a storey goes at about a quarter (
MARGIN1.32, steel two bricks, render as much as brick): a shop takes about three clicks, a bridge pier one or two. -
Walls one voxel thick. A quarter metre is a good wall, and two voxels thick cost twice the bodies for nothing you can see. The town block is 50,000 voxels, with interiors.
-
Bodies past the structure's aren't dispatched or drawn.
solver.bodyCountand the mesh's instance count follow the structure, so a house at 15,000 voxels costs what a house should. -
A GPU hang is the worst thing a demo can do. A device hung by a bad compute dispatch (
DXGI_ERROR_DEVICE_HUNG) wedges the browser's GPU process, and every WebGPU page in that browser after it, until it restarts. It happened here after a rebuild in the middle of a collapse, on a bigger structure after a smaller one: the solver remade a scan for the bodies then in play. If you changesolver.bodyCount, the scan must cover the capacity (vendor/three-avbd/VENDOR.md). It only showed in a certain order, a small structure played before a big one, so play your demo in every order, in real Chrome, hard, before trusting it. Over 24 rebuilds with shots the live buffers now stay level at 288 and 153 MB.
WebGPU only. three-avbd has no WebGL path. The demo plays in Patch Desktop and in Chrome or Edge 113 or later; without WebGPU it shows a message in its frame instead. If the browser has WebGPU but gives no adapter, it asks a few more times over five seconds. If that fails, or the device is lost, or the warm-up hears nothing for 90 s, the frame says so, with a Reload button (a lost device reloads by itself first, once a minute at most). src/gpu-trouble.ts is all of this.
Controls
The block page talks to the demo over the patch-block message channel (lib/patch-block.js). Firing happens in the frame: click fires a cannonball from the camera at what's under the pointer, drag orbits, the wheel zooms.
For people (the block page shows these):
| id | Control | Type | Range | What it does |
|---|---|---|---|---|
structure | Structure | choice | Town block, Stone bridge, Family house, each with a picture; default Town block | What stands there to knock down. |
strength | Strength | number | 0.4 to 3 ×, default 1 | How hard the structure is: how far a hit breaks it, how well falling pieces hold together, and how much of a storey can go before it gives way. |
ball | Cannonball | number | 0.12 to 0.8 m, default 0.32 | The iron ball's radius. A bigger ball breaks more at each hit and goes through more walls. |
speed | Speed | number | 0.1 to 1 ×, default 1 | How fast time runs: below 1 is slow motion. |
Trigger: Rebuild (rebuild): puts the structure back up, whole, and clears the rubble and cannonballs.
For agents (6 more, in recipe.json with every range and default):
| id | Control | Type | Range | What it does |
|---|---|---|---|---|
power | Ball speed | number | 20 to 120 m/s, default 62 | How fast the ball flies when it is fired. |
gravity | Gravity | number | 1 to 20 m/s², default 9.81 | How hard everything falls once it is loose. |
iterations | Solver iterations | number | 3 to 16, default 6 | Solver passes a step. |
budget | Loose pieces | number | 1,000 to 40,000, default 16,000 | How many voxels can be loose at once. |
auto | Keep firing | toggle | default off | Fires at the structure every two seconds and rebuilds it once it's down. |
shake | Camera shake | toggle | default on | Whether the camera shakes when a ball is fired and things land. |
Warm-up before ready: every structure built, each put up and drawn once, every material and the smoke compiled by drawing them, the particles' compute kernels run, and one practice shot fired at the first structure with the canvas hidden (the ball and its shadow, the blast and gather passes, joints, pieces falling and landing, chips and shards), then all of it put back.
The structures
| Structure | Voxels | Objects | Built like |
|---|---|---|---|
| Town block | about 50,000 | 23 | Built with three-voxel-destruction's street props: four shops in a row, BAKERY, BOOKS, HOTEL and CAFE, with floors, stairs and shelves inside, on a street with parked cars, trees, lamps and a bus shelter. Each shop falls on its own. |
| Stone bridge | about 43,000 | 12 | Five stone piers and four arches over a river and a lane, on earth ramps. Each pier is an object with its half-arches and its piece of deck, so a pier shot out takes those down and the fall takes its neighbours. |
| Family house | about 15,000 | 10 | Two storeys of brick and render with a slate roof, furnished, in a garden with a shed, a picket fence, a hedge, crates and a car. |
Build it
npm install
npm run build # writes recipe.json, then dist/ (~8 MB), with thumbs/ and the licences copied in
npm run dev # http://127.0.0.1:5362/
npm run thumbs # with the dev server running: thumbs/structures/*.webp (needs ffmpeg)
dist/ is the live demo exactly as released, with index.html at the top and nothing loaded from other hosts.
| Path | What |
|---|---|
src/demo.ts, index.html | The demo page's light part: the block wiring and loading bar, then the rest behind import() |
src/app.ts | The orbit camera, input, warm-up and the frame |
src/gpu-trouble.ts | No adapter, a lost device: asking again, the words for it, getting it back |
src/voxel/ | The voxel world and its builder, the solver's bodies, the chunk graph, the blasts (three-voxel-destruction, by way of Brickfall) |
src/level/structures.ts | The three structures |
src/game/ | The structure at run time and its readback (site.ts), the cannonball (cannon.ts), what a hit looks and sounds like (fx.ts) |
src/render/ | The look: voxels drawn straight from the solver, ground, light and post (view.ts), sky (sky.ts), the backdrops (skyline.ts, suburb.ts) |
src/fx/ | GPU smoke, dust and sparks (particles.ts), chips and shards (chips.ts), shockwave rings (impacts.ts) |
src/audio/sounds.ts, public/audio/ | The sound: layered recordings by material and size, from Patch's destruction block |
vendor/three-avbd/ | three-avbd 0.2.1 as published on npm with three small fixes (VENDOR.md), with its LICENSE and THIRD_PARTY_NOTICES.md |
recipe.json, make-recipe.mjs | The release recipe (mode browser with its block section) |
thumbs/structures/, make-thumbs.mjs | The Structure choice's pictures, stills of this demo |
public/ | Textures and the sky light |
make-audio.mjs, audio-build/ | How the sound layers are built from their recordings, and what each is |
lib/patch-block.js | Patch's block demo helper, as served at patch.gg/patch-block.js |
LICENCE-NOTES.md | Where every part came from |
Credits and licences
- Physics: three-voxel-destruction and three-avbd by Steven Bobyn (MIT). three-voxel-destruction at commit
8c4b058("Voxel City"):vendor/LICENSE-three-voxel-destruction.txt. three-avbd 0.2.1, with ports of Chris Giles' avbd-demo2d and avbd-demo3d (MIT) and box2d-lite by Erin Catto (MIT):vendor/three-avbd/LICENSE,THIRD_PARTY_NOTICES.md. The method is Augmented Vertex Block Descent (Giles, Diaz and Yuksel, SIGGRAPH 2025). - Renderer, sky and particles: from three-voxel-destruction by Steven Bobyn (MIT), by way of Patch's Brickfall.
- Textures and sky light: Poly Haven, CC0.
- Red Hat Mono: The Red Hat Project Authors, SIL Open Font License 1.1.
- three.js: MIT.
- The sound: CC0 and CC-BY recordings, every one listed with its author in
CREDITS.txt, cut and mastered for Patch's destruction block. - The house, the fixes from Brickfall, the other structures and the demo: Patch, MIT. The pictures in
thumbs/: stills of this demo, Patch, CC0.
Code: MIT (LICENSE). Art: CC0. Sound: CC0 and CC-BY. The font is OFL 1.1.