The Explosive Lab

Three materials in this build detonate. They are not three effects — they are one pressure-vessel state machine with three sets of numbers, and every visible difference between a firecracker, a deflagrating charge and a buried keg comes from those numbers. This page reads the machine out of the shader, stage by stage, and shows why chains propagate without a single line of scripting.

Companion pages
Mechanics Relationship Map
Mechanics Status Report
Technique Guide
Solver Internals
Why This Version

Source of truth: shaders/physics/mpm_p2g.comp
Updated: 2026-09-08

The Cast

Six materials matter here, and only three of them are charges. The other three are the supporting cast that carries fire from one charge to the next — the reason a domino line works at all.

MaterialIdRoleDistinguishing behaviour
FIRECRACKER21charge Fast fuse, hard shell. Wants to be well cracked and well pressurised before it lets go.
REACTIVE_BURN25charge Deflagration, not detonation. Cracks earliest, pushes least, and its gas comes from pyrolysis rather than a fuse.
SEALED_CHARGE62charge The keg. Highest pressure, fastest fragments, and the only charge whose ignition temperature is randomised per particle.
FLAMMABLE_FLUID16fuel A spreading pool that relights itself at ~465 K while oxygen holds. This is how fire crosses a floor.
TINDER / PEAT— / 89fuse Ignite at 340 K and 360 K. Cheap, slow-burning bridges between stations.
EMBER8residue Burns at 350 K and stays hot for a long time afterwards. Turns a spent station into a delayed igniter.

One sentence

Every charge in the family vents at 520 K or hotter, and every ignition threshold in the family is 368 K or cooler. Nothing here can go off without putting its neighbours over the line.

That single inequality is the entire chain-reaction mechanism. There is no propagation code, no event queue, no scripted delay. A charge heats the air around it, the air heats the next charge, and the next charge crosses its own threshold whenever it happens to cross it.

Why the timing looks organic

Because it is not timed. Each particle carries a per-particle seed (base_params.w, called shell_seed in the shader, with core_seed = 1 - shell_seed) and that seed perturbs ignition rate, gas yield, shell strength and fragment speed independently. A sealed charge goes further and seeds the ignition temperature itself across a 48 K band, so a single keg does not light all at once — it lights from wherever the hot side is.

The Pressure-Vessel Machine

firecracker reactive burn sealed charge curves = the vent gate ramps the vessel crack bleeds the seal 1 ignite damage[idx] gate on temp 2 outgas phase_data gate on burn 3 pressurise Jp_data gas x sealed 4 vent reads all 3 product of two inside the vent gate: vent = smoothstep(crack) x smoothstep(pressure) crack 0 to 1 x pressure 0 to 1 burst v gas out T floor damp C a vent leaves 680 to 1500 K behind; every ignition threshold here is 320 to 368 K one charge, one substep · the chain reaction is that inequality, not a script
particle state — the three MPM-local lanes each charge ownsdamage, phase_data, Jp_data field state — heat that leaves the particle and lands in the air gridthis is the return path along the bottom a sampled coupling — the two points where the loop crosses between particle and field
All three charges run these four stages, in this order, every substep. The stages are not functions; they are consecutive blocks inside one else if (mat_type == ...) branch of mpm_p2g.comp.

Three lanes, borrowed

A charge needs three pieces of persistent per-particle state: how far the fuse has burnt, how much gas is inside, and how broken the shell is. It does not get three new buffers for that. The P2G shader sits at exactly sixteen std430 blocks — the driver cap — so there is no room for another one.

Instead the charges reuse three lanes that already exist for other purposes:

LaneNormally holdsHolds for a charge
damage[idx]continuum damage / burn progressfuse or cook progress, 0–1
phase_data[idx]phase fractiongas fraction inside the shell, 0–1
Jp_data[idx]plastic volume changecrack, 0–1

The Jp borrow is the one with a visible consequence. Because Jp no longer means volume, all three charges appear in material_uses_zero_jp() in mpm_solver.cpp and spawn with Jp = 0 instead of the usual 1. A charge that spawned at Jp = 1 would be born fully cracked and would vent on its first warm frame.

These three lanes are MPM-local: they are indexed by idx and sized to the MPM capacity. The particle's position, velocity and temperature are global and indexed by p = u_offset + idx. Both appear within a few lines of each other in every branch on this page. Mixing them is silent.

The four stages

1ignite A temperature test opens a rate. The rate is seeded, so two neighbouring particles at the same temperature burn at different speeds. Writes damage.
2outgas Burn progress becomes a gas target, and the gas fraction slews toward it. The slew is deliberately asymmetric — gas fills fast and bleeds slowly, so a charge cannot un-arm itself by cooling briefly. Writes phase_data.
3pressurise Pressure is gas × sealed, where sealed falls off as the crack grows. That pressure enters the stress tensor twice: it raises the rest volume J_rest, so the material wants to be bigger, and it adds an isotropic term directly. The resulting tension and shear then drive the crack forward. Writes Jp_data.
4vent The product of two smoothsteps — one on crack, one on pressure. When it opens, the particle is kicked in a uniformly random direction, its gas is dumped, its affine velocity field is damped and its temperature is forced to a floor. Reads all three lanes.

The loop that decides everything

Crack reduces sealed, which reduces pressure, which reduces crack growth. That negative feedback is drawn dashed in the figure and it is what separates a bang from a fizzle. A charge that cracks slowly bleeds its pressure away through the seal term and never reaches the vent gate; a charge that stays sealed until it is fully pressurised crosses both smoothsteps at once. The three charges differ mostly in how fast that race runs.

Three Charges, Side By Side

Every value below is read directly out of shaders/physics/mpm_p2g.comp. shell_seed is the per-particle seed in [0,1]; mix(a, b, shell_seed) therefore means “a at one end of the population, b at the other”. batch_heat, batch_cooling, batch_outgas and batch_loft are the four authored thermal-coupling channels described under Knobs.

StageFIRECRACKER (21)REACTIVE_BURN (25)SEALED_CHARGE (62)
ignition test temp > 340 temp > 350 chars;
gas needs temp > 405
temp > mix(320, 368, shell_seed) - 10
burn rate mix(2.35, 1.05, shell_seed) mix(0.36, 1.35, shell_seed) mix(3.80, 0.48, shell_seed)
self-heating 1480 + 260·heat, peaking at half-burnt 720 + 110·heat 760 + 260·heat, plus a sustain floor of mix(660, 420, shell_seed)
gas source fuse above mix(0.10, 0.22) pyrolysis, clamp((T - 405)/170) cook progress, scaled by batch_outgas
gas slew up 14/s, down 0.18/s up 2.6·(1−gas)/s up (4 + 2·core_seed)/s, down 0.03/s
seal lost between crack mix(.18,.32) → mix(.62,.78) crack mix(.24,.36) → mix(.68,.82) crack mix(.50,.62) → mix(.92,.988)
pressure gas·sealed·mix(1.45, 0.78, shell_seed) gas·sealed·mix(1.10, 0.72, charred) gas·sealed·(1.46 + enclosure)·mix(2.30, 0.88, shell_seed)
vent gate crack .56→.96 × pressure .36→.96 crack .42→.96 × pressure .22→.90 crack .58→.90 × pressure .18→.78
burst speed ceiling 14–46 m/s 6–26 m/s 28–84 m/s
vent temperature floor 680 + 220·p + 120·loft 520 + 180·p + 100·loft 760 + 260·p + 120·loft
temperature clamp1400 K1450 K1500 K

The burst-speed row is a ceiling, not a delivered speed: the kick is multiplied by the gate value, so a partially-open vent pushes proportionally less. It is also applied per particle per substep while the gate stays open, which is why a well-sealed charge that opens abruptly throws fragments so much harder than one that leaks.

Firecracker

The strictest gate in the family: it needs the crack past 0.56 and the pressure past 0.36. Combined with a violent 1480 K self-heating term, that produces a long quiet fuse and then a single sharp pop. Its shell also stiffens with the seed (mix(1.0, 1.45, shell_seed)), so some grains hold longer than others.

Reactive burn

The only charge whose gas is pure pyrolysis — it needs 405 K before it makes any, which is 55 K above its own char threshold. It also cracks the earliest (0.42) and pushes the least (6–26 m/s). The result reads as a deflagration: a lot of hot gas, not much shrapnel.

Sealed charge

Holds its seal until crack 0.50–0.62 and vents at the lightest pressure in the family (0.18). That combination — hold everything, then release on a hair trigger — is what makes it the keg. It is also the only charge that reads the pseudo-2.5D enclosure knob, so burying it actually raises its pressure.

The shell seed

One number per particle drives ignition rate, gas yield, shell stiffness, seal thresholds and fragment speed at once — and for the sealed charge, the ignition temperature too. It is why the same charge never detonates the same way twice and why fragments arrive as a spread rather than a shell.

The Ignition Ladder

every number here is read from mpm_p2g.comp linear in kelvin throughout detail · the ignition window, 315 to 375 K sealed charge: threshold seeded per particle 320 368 reactive burn 350 K firecracker 340 K peat 360 K flammable fluid 338 K tinder sits at 340 K too 152 K 300 K 1550 K temp clamps 1400 to 1500 K reactive 520 K firecracker 680 K sealed charge 760 K 368 K is the highest threshold in the family; 520 K is the lowest vent floor ignition thresholds against vent temperature floors · the gap is the whole mechanism
ignition thresholds — a per-particle material propertyeverything that starts a reaction vent temperature floors — heat that lands in the air grideverything a reaction leaves behind
One linear kelvin axis, with the 315–375 K window magnified. The whole story is the empty stretch in the middle.

The margin is 152 K

The highest ignition threshold anywhere in the family is 368 K, at the top of the sealed charge’s seeded band. The lowest vent floor anywhere in the family is 520 K, on reactive burn. The gap between them is 152 K, and it is one-directional: every vent clears every threshold, with room to spare.

This is why chains are robust rather than fragile. A domino line does not depend on tuning each station against the next one; it depends on a single inequality that holds for all nine pairs at once. The only thing that varies from station to station is when, not whether.

Where the delay comes from

Nothing in the shader waits. The delay between stations is transport: the vent dumps heat into the air grid, the air solve advects and diffuses it, and the next charge samples its own temperature some number of substeps later. Move the stations further apart and the chain slows down on its own. Put a draught across the line and it bends.

Why the sealed charge’s threshold is a band

Every other material on the axis is a single tick. The sealed charge is a 48 K bar, because its threshold is mix(320.0, 368.0, shell_seed) — drawn independently for every particle in the body.

The consequence is worth stating plainly, because it is the difference between a charge that reads as an object and one that reads as a texture. With a single threshold, a heated keg crosses it everywhere at once and the whole body converts in the same frame: a uniform, synthetic-looking flash. With a seeded threshold, the particles nearest the heat cross first, their vents heat their neighbours, and the conversion travels through the body as a front. The keg tears open from the hot side.

The same trick, at a smaller scale, is why the firecracker’s pop has a texture: its ignition rate is seeded even though its threshold is fixed, so the body still converts unevenly — just less dramatically.

Inside a Vent

Four things happen at once

The vent block is short, and each of its four lines exists to prevent a specific failure.

LineWhat it doesWhat breaks without it
vel += burst_dir * burst_speed Kicks the particle along a uniformly-sampled angle, vec2(cos(a), sin(a)) with a = hash × 2π. Sampling a random vector and normalising it clusters fragments toward the diagonals. Sampling the angle is uniform on the circle by construction.
gas *= 1 - vent·k Dumps the gas that just did the work. The pressure term would still be high next substep and the same particle would vent again, forever.
C *= 1 - vent·k Damps the affine velocity matrix. APIC would carry the pre-vent local spin into a fragment that no longer has anything to spin about, and the debris would corkscrew.
temp = max(temp, floor) Forces the particle to at least 520/680/760 K. This is the entire chain mechanism, and also the entire signal the CPU-side detector watches. Without it a detonation is invisible to everything downstream.

And then the shell gives up

After the vent block, all three charges run the same closing move: stress is multiplied down by the crack (to as little as 2–5% of its value), and past a crack threshold the deformation gradient is blended toward an isotropic mat2(sqrt(J)).

That second part is the one that makes fragments look like fragments. Blending F toward isotropic discards the accumulated shear history — the particle forgets which way it was being stretched. What is left is a lump with momentum, which is what a piece of a burst casing is. Keeping the full F would leave every fragment still trying to satisfy an elastic shape it no longer has any business having.

The blend rates differ, and they read differently on screen: the firecracker blends hardest (0.22) and produces the most uniform debris; the sealed charge blends least (mix(0.18, 0.08, shell_seed)) and keeps more structure, so its fragments stay chunkier.

Note what is not here: there is no explosion radius, no impulse falloff and no neighbour search. Each particle only ever acts on itself. The blast wave is emergent — it is what MPM does with a few hundred particles that all pushed outward in the same few substeps.

Downstream: How a GPU Detonation Becomes an Effect

measured on Chain: Powder Row Dominoes, 35 s run no new GPU readback vent per particle air grid T kept on CPU baseline tau 0.85 s excursion vs 165 K 8 effects shake, flash why one fixed threshold is enough 165 K steady fire flicker peaks 108 to 136 K detonations peaks 243 to 259 K nothing at all lands between 136 and 243 K 100 excursion above the bin baseline 280 K the frame-to-frame delta is only 1 to 8 K, so differencing frames finds nothing a detonation outruns its own baseline · a fire never does
particle state — the vent, which happens entirely on the GPU field state — the air temperature grid, already downloaded every frame the one crossing — where GPU state becomes a CPU event, at no extra readback cost
Detonation detection by baseline excursion. The measurements are from a 35-second run of Chain: Powder Row Dominoes.

The problem

The blast effect stack — shockwave rings, explosion details, debris, foam, scorch and crack decals, blast lights, screen shake, the detonation flash and the volumetric god rays — all hung off a just_ruptured hook that only fires for pressure vessels, and pressure vessels were only ever created by the projectile-firing path.

Scene-authored charges detonate per-particle inside the P2G vent branch, entirely on the GPU. They produced no CPU event at all. The measured consequence: 35 seconds of a scene built around chained explosions reported blast_lights = 0, flash = 0.000, trauma = 0.000 for the whole run. Roughly eight effects were invisible in exactly the scenes authored to show explosions off.

Why rate of change does not work

The obvious detector — watch for a big frame-to-frame jump in air temperature — was built first and found nothing. Detection runs per physics substep, and a vent’s heat diffuses into the air grid over many of them, so the per-step delta is only 1–8 K even while the grid is reaching 1000 K. The first model measured 1.3–8.2 K against a 220 K threshold: it never fired once.

What does work

What separates a detonation from a steady fire is not how fast the temperature changes but how far it gets from its own recent average. A fire’s baseline catches up with it. A detonation outruns its baseline.

So each bin of the air grid keeps a slow running average (τ = 0.85 s) and fires when the current temperature excurses far enough above it and ends above an absolute floor. Measured on the chain scene, detonations peak at 243–259 K of excursion and steady fire flicker peaks at 108–136 K. The default threshold of 165 K sits in the empty gap with margin on both sides.

The air temperature grid is already downloaded to the CPU every frame — to be re-uploaded as a texture — and then discarded. Retaining that copy is the entire cost of the feature: no new readback, no new GPU work, and no hook inside the physics path.

Toggle: Scene Blast Events. On (the default), scene charges drive the whole effect stack. Off, only fired projectiles produce blasts, exactly as before. The excursion, floor, bin size and cooldown are all exposed as sliders next to it.

It is deliberately a detector, not a solver hook. It makes no claim to know which particle vented — only that something violent happened here. That is enough to drive visual effects and it keeps the whole feature outside the physics path.

The other coupling: airtightness

Separately from detection, three of the burning materials and both hard charges rasterise into the Eulerian airtight grid (euler_airtight_rasterize.comp handles BURNING, EMBER, FIRECRACKER, REACTIVE_BURN, STONEWARE and SEALED_CHARGE). They write an occupancy channel with atomicMax and a source channel with atomicAdd, which is how a charge going off inside a closed room raises the room’s pressure rather than just heating it.

Note that this coupling runs in one direction for detection purposes and the opposite direction for pressure: the charge writes air sources, and the sealed charge alone reads an enclosure factor back. See the coupling graph on the Mechanics Relationship Map for where this sits in the frame.

The Bench

Sixteen scenes carry the blast tag. They fall into three groups, and the grouping is worth knowing before you go looking: most of the armour scenes ship without a charge in them, because they are targets, not demonstrations.

SceneGroupWhat it isolates
Pressure PantryvesselThree compartments — gunpowder, pellets, popcorn — each kicking its own lid. The clearest look at pressure building against a seal.
Hybrid: Pressure PotteryvesselSealed-charge beads cooking under glaze, stoneware and blister lids. The vent gate against three different shell strengths.
Fix Test: Blast CooldownvesselOne heated sealed charge inside a ring of six ceramic witness pebbles. Built to check that the detector fires once, not repeatedly.
Blast Armor LanetargetMetal plate, stoneware tile and a layered stack with a witness sheet. No charge; you bring one.
Breach ChambertargetA closed room with a freestanding blast door. The airtight coupling matters here — the room holds pressure.
Spall Plate BenchtargetThree armour lanes with spall liners and backers. Fragment behaviour behind a plate.
Open Blast Range XLtargetTwelve units wide, widely spaced targets, far catch wall. Long fragment flight.
Breach Hall XLtargetA long roofed hall with a tall blast door and a rear bulkhead. Pressure in a big enclosed volume.
Spall Gallery XLtargetExtra-long three-lane gallery. The same test as the bench, with room for the fragments to separate.
Hybrid: Soft HEAT RangetargetThree layered armour lanes on plinths, waiting for fired charges.
Huge Weapon Rangetarget24 m range with spaced walls, pillars, a ramp plateau and a soft gel witness.
Huge Impact PlaygroundchainBouncy balls, columns, crates, wood, paper, oil and firecrackers in one box. Unstructured, and the best place to be surprised.
Chain: Powder Row DominoeschainThe reference scene. Nine stations: pops, then deflagrations, then a keg finale, with a quench tub at the right. Every measurement on this page came from it.
Combo: Magnet MinechainA magnet drags a soft-iron-jacketed charge onto a hot anvil, and the blast demagnetises the trap above it. Magnetic and thermal in one loop.
Chain: Meissner Dead-Man SwitchchainA levitating superconductor plate warms through its critical temperature, drops, and its firecracker satchel lands in a candle.
Showcase: Foundry MeltdownchainSix systems in sequence: oil fire, gantry collapse, keg, embers, shed backdraft, Curie cistern drop. The longest cascade in the build.

The three Chain scenes and the Showcase are the ones that demonstrate the propagation mechanism. Everything in the target group is a measuring instrument: it shows what a blast does to something, not what a charge does on its own.

Knobs

The four authored thermal channels

Every charge reads a vec4 of thermal coupling, fetched per particle from a texture and falling back to a per-material default when unauthored. The four channels are named after what they do, and each charge uses a different subset:

ChannelName in shaderEffectUsed by
.xbatch_outgasScales the gas target directly.sealed charge only
.ybatch_heatAdds to the self-heating rate.all three
.zbatch_coolingScales the ignition rate.all three
.wbatch_loftAdds 100–120 K to the vent temperature floor.all three

batch_cooling is the confusingly-named one: it does not cool anything in these branches, it modulates how quickly the fuse takes. It is shared with the baking materials, where the name makes more sense.

Pseudo-2.5D enclosure

With the pseudo-2.5D system enabled, the enclosure knob produces pseudo_enclosure_hold = 1 + enclosure × (0.14 + 0.86 × core_seed), and the sealed charge folds it straight into its cavity term:

cavity_hold = (1.46 + pseudo_enclosure_hold) × mix(2.30, 0.88, shell_seed)

With pseudo-2.5D off that resolves to 2.46 × mix(2.30, 0.88, shell_seed) — between 2.16 and 5.66 depending on the seed. Turning enclosure up raises the whole band, so a buried keg genuinely holds more pressure before it lets go, and lets go harder when it does.

Of the three charges, only the sealed charge reads this. The firecracker and reactive burn ignore it entirely, which is intentional: a firecracker is a small thing in open air and burying it should not turn it into a bomb.

Where to look in the app

The Scene Blast Events checkbox and its four sliders live in the effects section. The blast-model selector (Legacy per-frame push versus Sedov Front) is a separate, independent toggle that governs how the shockwave is drawn, not how the charge behaves.

What Is Approximate

Honest limitations

  • There is no detonation physics. Nothing here models a reaction front, a Chapman–Jouguet state or an equation of state for hot product gas. What exists is a scalar gas fraction that inflates a hyperelastic solid until it tears. It is a pressure vessel, and it is honest about being one.
  • The burst direction is uniformly random, not radial. Fragments do not fly outward from a centre — each particle picks its own angle. The outward appearance comes from the fact that the outside of the charge vents first and the inside is still solid. A charge that vents from its middle outward will look wrong.
  • Fragment speed is applied per substep while the gate is open, so total delivered impulse depends on how long the gate stays open, which depends on how fast gas bleeds. The quoted 28–84 m/s is a per-substep ceiling, not a muzzle velocity.
  • The detector cannot distinguish two charges in the same bin. Bins are 8 grid cells on a side with a 0.45 s cooldown, so a tight cluster reports one event, not five.
  • Terrain never moves. The SDF pushes on particles; particles never push back. A charge against a wall cannot crater it. See the coupling graph on the Relationship Map — that missing return edge is drawn there explicitly.

Things that look like bugs and are not

  • A charge that heats slowly may never go off. That is the seal-bleed feedback working as designed: crack grows, sealed falls, pressure never reaches the gate. Heat it faster and it detonates.
  • A sealed charge tears open asymmetrically. Intended — the ignition threshold is seeded per particle across 48 K, so the hot side converts first.
  • Reactive burn makes a lot of noise and little shrapnel. Also intended: it cracks earliest and pushes least of the three. It is the deflagration in the family.
  • Nothing happens below 405 K with reactive burn even though it is visibly charring. Charring starts at 350 K but gas generation does not begin until 405 K. There is a 55 K window where it looks like it is doing something and physically is not.

If a chain stalls, the first thing to check is not the charges — it is the air. The propagation path runs entirely through the air temperature grid, so a scene with strong ventilation, a large cell size, or an oxygen-starved pocket can break a chain that is otherwise correctly spaced.