More
Under the hood
What the solver and the renderer do each frame, and what changed from the firepro.html page the library grew from.
#The grid
The simulation is a uniform voxel grid, stored as a 2D tile atlas. Each z-slice of the grid is a tile, and the tiles are packed into one float texture. Every pass is a fullscreen draw into an atlas, so the whole solver runs on plain WebGL2 fragment shaders with no compute and no 3D render targets. The state atlas holds temperature, fuel, smoke and the burn rate per voxel, and a second atlas holds the velocity.
On high with the default domain, the grid is 64 × 96 × 64 (393K voxels) with 6.25 cm cells. Each shader pass has its own file in src/shaders/.
#One simulation step
update(dt) runs these passes in this order:
#Mesh-emitter splat
The surface samples of every mesh emitter are drawn as points into an emission atlas, using each mesh's current world matrix.
#Obstacle mask
The sphere and box obstacles are rasterised into a solid mask, with each obstacle's measured velocity.
#MacCormack helpers
On medium and above, a forward and a backward semi-Lagrangian pass over the state (and over the velocity on ultra). The state and velocity passes combine them into a second-order result, limited to the range of the sampled cell so no new extremes appear.
#State: advect, emit, burn
Temperature, fuel and smoke are advected. Emitters and bursts inject into them, fuel burns where it's hot, and heat and smoke decay. See the combustion model.
#Velocity: advect and force
Self-advection, then buoyancy, height-scaled wind, noise turbulence, emitter jets, mesh-emitter push and the burst impulses.
#Curl and vorticity confinement
The curl of the velocity field, then a force that feeds small vortices back in. The gain is 1.2 + 2.4 × turbulence.
#Divergence
The velocity divergence, minus the volume released by combustion and the burst divergence sources. This is where the expansion comes from: a radial push alone would be projected away.
#Pressure: Jacobi × N
12 to 32 Jacobi iterations, depending on the tier, solve for the pressure that removes the divergence.
#Gradient subtract
The pressure gradient is subtracted from the velocity, which leaves it divergence-free. Obstacles are respected as solid boundaries.
#Light volume
Per voxel: transmittance toward the key light, the shadowed fire light, and the upward transmittance that shadows the ambient light.
After the solver, the embers are stepped: 4096 particles in a 64 × 64 float texture, advected by the fluid velocity. Every third frame the reduction pass sums the flame into a 32 × 16 target, and it's read back asynchronously for the fire light.
#The screen pass
render() and composite() then raymarch the state atlas at reduced resolution, depth-aware against your scene, with a second ray for wet floors. Two octaves of rising noise add sub-grid flame tongues at the hot edge. The march is jittered per pixel and per frame, accumulated in a temporal pass with YCoCg variance clipping, and upsampled with a joint-bilateral filter. The full sequence is on Rendering and compositing.
#What changed from firepro.html
The library started as a single-page demo. These are the changes made while porting it:
- A library, not a page. The solver, the screen pipeline, the lights and the embers are separate modules, and each GPU pass has its own shader file. The domain size and position are configurable.
- Many emitters instead of one. Up to 8 analytic sources plus any number of mesh emitters, each a handle with its own fuel, temperature, smoke, flicker, jet velocity and ember rate. There can also be up to 4 bursts and 4 moving obstacles at once.
- Sharper flame licks at the same grid size. MacCormack advection for temperature and smoke (and velocity on
ultra) keeps curled tongues and thin smoke wisps that first-order advection smears out. Per-tier cooling compensation keeps the flame height consistent.
campfire. Top row: the page's first-order solver. Bottom row: MacCormack with cooling compensation.- Less blobby explosions. Burst injection is carved into noise pockets and the radial push is jittered, so the seed ball blooms into separate lobes.
- CIE blackbody ramp. The flame and the fire light share a lookup that integrates Planck's law against the CIE 1931 observer. The page used a 3-wavelength approximation.
- Smoke self-shadows its ambient light. The light volume gained the upward-transmittance channel.
- Less ghosting. The TAA variance clip runs in YCoCg.
- No GPU stalls. The fire probe is read back with
readRenderTargetPixelsAsyncinstead of a synchronousreadPixelsevery third frame. - three r186.
THREE.Timerinstead ofClock,PCFShadowMap(PCFSoftShadowMapwas removed), and no deprecation warnings.