NatureGL Firev1.0.0

Guides

Rendering and compositing

The fire is raymarched against your scene's depth, so it sits correctly behind and in front of your geometry. Use render() for the whole screen pass, or composite() to put the fire into a pipeline you already have.

#render(): the whole frame

fire.render(target = null) replaces renderer.render(scene, camera):

#Your scene

Your scene is rendered into an internal colour target with a DepthTexture (fire.sceneTarget, fire.depthTexture).

#Embers

fire.embers is drawn into the same target, depth-tested against your geometry. This is skipped if you added the embers to a scene yourself.

#Volume raymarch

The grid is marched at reduced resolution (the tier's scale) with a jittered start, stopping at your scene depth. Rays that hit a wet floor continue along the mirror direction.

#Temporal accumulation

The jittered march is accumulated over frames. History is reprojected through the plane of the flame centroid, and stale flame is rejected with a variance clip in YCoCg.

#Composite and heat haze

A joint-bilateral upsample puts the low-resolution volume over the full-resolution scene without halos at depth edges. A quarter-resolution pass integrates the temperature along each ray, and the result refracts the scene behind hot gas.

#Bloom and tone mapping

A 6-level bloom, params.exposure, ACES (Hill fit), sRGB encoding and a 1/255 dither, written to target or the canvas.

render() saves and restores the renderer's render target, clear colour and autoClear. It checks the drawing-buffer size every frame and resizes its targets itself.

#composite(): your own pipeline

composite(inputColor, inputDepth, target = null, camera = fire.camera) raymarches the fire against a linear-HDR colour texture and its DepthTexture. It writes scene × transmittance + fire, with heat haze, to target. The output is linear HDR, with no bloom and no tone mapping.

js
const w = innerWidth * devicePixelRatio, h = innerHeight * devicePixelRatio;
const sceneRT = new THREE.WebGLRenderTarget(w, h, {
  type: THREE.HalfFloatType,
  depthTexture: new THREE.DepthTexture(w, h),
});
const hdrRT = new THREE.WebGLRenderTarget(w, h, { type: THREE.HalfFloatType });

scene.add(fire.embers);   // composite() doesn't draw embers: add them to your scene

renderer.setAnimationLoop(() => {
  fire.update(dt);
  renderer.setRenderTarget(sceneRT);
  renderer.render(scene, camera);
  fire.composite(sceneRT.texture, sceneRT.depthTexture, hdrRT);
  // … your bloom, grading and tone mapping on hdrRT, then to the screen
});

composite() resizes its internal buffers to match the input texture. The alpha of inputColor is read as 1 − reflectivity, so keep it at 1 unless you use wet floors.

#Wet-floor reflections

patchReflectiveMaterial(material, { strength }) patches a MeshStandardMaterial or MeshPhysicalMaterial so it writes alpha = 1 − wetness. Smooth texels count as wet: wetness is strength × (1 − 1.25 × roughness), clamped to 0–1. Where the floor is wet, the raymarch continues along the mirrored ray, with a little noise in the normal, and the fire shows up in it.

js
const wet = patchReflectiveMaterial(floor.material, { strength: 0.6 });
wet.value = 1;                         // the returned uniform: change it at runtime
fire.params.reflections = false;       // turn the reflection rays off
  • It only works for the domain's floor plane (the bottom of the box).
  • It changes only what goes into the fire's scene target. The canvas output stays opaque.
  • It keeps any onBeforeCompile the material already had.

#Look and screen parameters

ParamDefault
reflectionstrueWet-floor reflection rays
heatHazetrueRefractive heat haze
heatHazeStrength1
emberstrueGPU ember particles (4096, advected by the gas)
emberBrightness5
emberSize0.02Ember size in metres
temporalBlend0.88History weight of the volume TAA. 0 turns it off
temporalClip1.25Variance-clip width (σ). Lower values mean less ghosting and more noise
bloomStrength0.3render() only
bloomThreshold1.6render() only
exposure1render() only

The flame and smoke look (emission, colour, density) is on Fuel, heat and smoke.

#Cameras

The raymarch rebuilds rays from the camera's inverse view-projection, so perspective and orthographic cameras both work. It reads camera.near and camera.far to linearise depth. composite() takes a camera argument if you render from a camera other than fire.camera, but the temporal history is shared, so switching cameras every frame will ghost.