NatureGL Firev1.0.0

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.
Two frames of 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.
One explosion at three moments. Top row: lumpiness 0, about what the page did. Bottom row: the new lumpy injection and jittered push.
  • 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 readRenderTargetPixelsAsync instead of a synchronous readPixels every third frame.
  • three r186. THREE.Timer instead of Clock, PCFShadowMap (PCFSoftShadowMap was removed), and no deprecation warnings.