NatureGL Magicv1.0.0

Guides

Rendering into your scene

NatureGL Magic never takes over your scene graph. It adds a few objects to the scene you pass in, does its simulation offscreen in update(), and either renders your scene for you or composites over a colour and depth pair you already have.

#The contract

  • Offscreen work happens in update(dt): the effects, one fluid step, the sparks and the glow-light readback. It draws nothing on screen.
  • render(target?) replaces renderer.render(scene, camera). It renders your scene into an internal HDR colour + depth target, raymarches the volume against that depth, and writes the tone-mapped, sRGB-encoded result to target (the canvas by default).
  • What lights your scene lives in your scene. The objects MagicSystem adds are ordinary three.js objects, so your own lights, shadows and materials treat them like anything else.
ObjectNameAdded by
THREE.Points (4096 GPU sparks)—always (magic.sparks.points)
PointLightMagicGlowLightalways (magic.glowLight; hidden with glowLight: false)
PointLight per orbMagicOrbLightspellOrb effects
Mesh per orbMagicOrbspellOrb effects
Shell Mesh + coreMagicForceFieldforceField effects

dispose() frees every GPU resource and removes what it added.

#The simulation box

All the fluid lives in one box. By default it's 4 m wide and deep and 6 m tall, from [-2, 0, -2]. The bottom face is a solid floor; the sides and top are open, and density fades out toward them over render.edgeFade (0.6 m), so the box never shows.

js
// twice as big (8 × 12 × 8 m), centred on x = 12, standing on a stage 0.5 m up
const magic = await MagicSystem.create({ renderer, scene, camera, domain: { min: [8, 0.5, -4], width: 8 } });

The height is always 1.5 × width. The grid resolution comes from the quality tier, not from the width, so a bigger box has bigger cells. Effects outside the box do nothing, and the demo clamps its drags to stay inside it.

#Your own colour and depth: composite()

If you already render the scene into a target, hand that target to composite() instead of rendering twice.

js
const target = new THREE.WebGLRenderTarget(w, h, {
  type: THREE.HalfFloatType,                  // linear HDR
  depthTexture: new THREE.DepthTexture(w, h),
});

renderer.setAnimationLoop(t => {
  timer.update(t);
  magic.update(timer.getDelta());
  renderer.setRenderTarget(target);
  renderer.setClearColor(0x000000, 1);        // alpha 1 = "not reflective"
  renderer.clear();
  renderer.render(scene, camera);
  magic.composite(target.texture, target.depthTexture);   // to the canvas
});

The rules for the input:

  • Colour is linear HDR, not tone-mapped, from the same camera as magic.camera.
  • Depth is the matching DepthTexture. The volume is clipped against it, so your geometry occludes the magic and the magic sits in front of what's behind it.
  • Alpha is the reflectivity mask. 1 − alpha reflects the volume on surfaces at the domain floor. Clear to alpha 1, and use patchReflectiveFloor() on the materials that should reflect.

render() does exactly this with its own target. Call magic.resize() after renderer.setSize() or setPixelRatio() either way.

#Your own post chain

render() and composite() both finish with bloom, the grade and ACES + sRGB output. Pass a render target to keep the image for your own passes:

js
const out = new THREE.WebGLRenderTarget(w, h);
magic.render(out);   // display-referred, sRGB-encoded

Treat out as a finished, display-referred image: add UI, film grain or colour-safe effects, but don't tone-map or sRGB-encode it again. To take bloom out of NatureGL Magic's grade, set magic.setParams({ post: { bloom: 0 } }).

#Several effects, one system

One MagicSystem runs one box and one screen pipeline. Put every effect for that area into the same system, where they share the fluid and interact, up to the per-frame limits. Stacking several systems in one view isn't supported yet.

#With other NatureGL packs

Every library in the series follows the same contract: it renders into a scene and camera you own, and does its offscreen work in update(dt).

  • Anything that's a mesh in your scene is drawn by render() like the rest of your geometry. NatureGL Sky's backdrop, NatureGL Grass's blades and your props all get the magic composited over them against depth. NatureGL Magic applies its own exposure (post.exposure), not renderer.toneMappingExposure, so match the two if a sky drives the renderer's exposure.
  • A pack that hands you a linear HDR colour texture plus its depth can feed composite(). NatureGL Fire's composite() writes linear HDR, and it uses the same reflectivity-mask convention (alpha = 1 − reflectivity).