NatureGL Magicv1.0.0

More

Under the hood

What happens in update() and render() each frame, how the grid is stored, and what changed from the magicpro.html page the library grew from.

#The grid

The solver is a 3D grid of n × 1.5n × n cells stored as a 2D atlas: the z slices are tiled across a float texture. On high that's 64 × 96 × 64 cells in a 640 × 672 atlas (10 × 7 tiles). Each field is a ping-pong pair of atlas targets:

FieldChannels
velocityxyz velocity
statetemperature, fuel, smoke density, burn
dyeRGB dye density, glow
lightTransmittance toward the key light and toward the glow centroid

Plus scratch targets for the MacCormack half-step, pressure, divergence and curl. magic.solver.fields returns the current read side, and magic.solver.defines holds the layout as GLSL defines if you want to sample the fields in your own shaders.

#One simulation step

update(dt) collects every effect's sources for the frame (emitters, impulses, rings, swirls, spark spawners, the obstacle and the container), converts them to grid space and uploads them as uniforms. Then FluidSolver.step() runs these passes:

#State

Advects temperature, fuel and smoke density (MacCormack, limited, at physics.sharpness), then adds the sources. Fuel burns once the temperature passes the ignition point, at burnRate. Burning adds heat and soot. Temperature cools, and smoke and fuel dissipate. Within four cells of an open face, everything fades a little each step, which keeps the box from showing.

#Dye

Advects the RGB dye and glow with the same flow, adds the sources and the soot colour (flame.soot), and applies dissipation and glowDissipation separately.

#Velocity

Advects velocity (MacCormack only if velocitySharpness > 0), then adds buoyancy from temperature, weight from density, damping and noise turbulence where it's hot or dense. Emitters drive the fluid toward their velocity by blend. Swirl fields drive it toward a spinning, pulling, lifting target. Rings steer it toward a core that circulates and translates. Impulses push, and the force field pushes radially.

#Curl and confinement

Measures the curl per cell, then adds vorticity confinement at physics.vorticity, with an extra gain around each vortex-ring core (confinement).

#Pressure projection

Computes the divergence (with combustion expansion and impulse expansion as sources), runs jacobi iterations of the pressure solve (12 to 32 by tier), and subtracts the pressure gradient. The container walls, the obstacle sphere and the floor are solid.

#Light volume

Marches lsteps toward the key light and fsteps toward the glow centroid through the smoke and heat, so the renderer can look up self-shadowing per cell instead of marching light per sample.

After the solver, the sparks advance (4096 particles advected by the fluid), and every third frame the emission is reduced on the GPU to 32 × 16 texels and read back asynchronously to drive the glow light.

#One screen frame

#Scene

render() draws your scene into a linear HDR colour + depth target. composite() takes yours instead.

#Volume raymarch

At the tier's scale, with steps across the domain diagonal and golden-ratio jitter per frame. It writes two targets at once: colour with transmittance, and an opacity-weighted depth. The march stops at your scene's depth, On reflective pixels within 6 cm of the domain floor, it also marches along the mirror direction and adds the reflection.

#Temporal accumulation

Reprojects last frame's volume with the recorded depth and blends it in at post.temporal, clipping history to the current neighbourhood's variance.

#Composite

A depth-aware bilateral upsample of the volume over the scene, with heat haze from a quarter-resolution pass.

#Bloom and grade

Six-level bloom (soft knee, Karis-weighted first downsample), then exposure, hue-preserving ACES, saturation, contrast, vignette and sRGB, with a little dither.

#Changes from magicpro.html

The library is a port of the single-file magicpro.html page (three r160) to modules on three r186. Beyond the port itself:

  • Effects are building blocks. They're parameterised classes you can compose, move and recolour at runtime, and presets are plain data that round-trip through getParams().
  • It renders into your scene. Sparks, the glow light and effect meshes live in your scene graph, and render() or composite() does the screen work.
  • Second-order advection. Limited MacCormack for dye and state (and optionally velocity) gives crisper dye filaments and billows.
  • Vortex rings fixed. A local confinement boost keeps the core tight in the grid itself, and the analytic torus is thinner, has a crisper profile, rolls around its core with ring-local noise, and fades near the domain faces instead of being clipped into half-donuts. Rings live 4.5 s instead of 2.7 s.
  • Temporal stability. The volume is accumulated over frames with MRT depth, reprojection and variance clipping. Golden-ratio step jitter removes the shimmer.
  • No visible domain box. Density fades out toward the open sides and top.
  • Grading. Soft-knee, Karis-weighted bloom, so single sparks no longer flicker the glow. Hue-preserving tone mapping only on bright emissive colours. Saturation, contrast and vignette controls.
  • More at once. Up to 8 emitters, 4 impulses, 4 rings, 2 swirl fields and 4 spark spawners with their own colours per frame. The page had 4 sources, 1 burst and 1 spark palette.
  • No stall for the glow light. The emission readback uses readRenderTargetPixelsAsync.
  • Current three.js. THREE.Clock → THREE.Timer, PCFSoftShadowMap → PCFShadowMap, no deprecation warnings on r186.