Based on the Three.js example "Volumetric Fire" by sunag.
It is a Three.js TSL WebGPU module to render an Eulerian fire simulation via Raymarching using a VolumeNodeMaterial using either Lagrangian or MacCormack (MacCormack semi-Lagrangian advection with a GPU-adapted continuous limiter...) methods for advection.
To make this module is to make adding fire something a bit easyer so the basics are somewhat covered and you can focus on the layers on top adding your artistic imprint. This fire class offers ways to edit the fire and extend it, so it serves as a very nice base to build on top of.
> Talk with the docs (+audio): Gemini Notebook
- Highly configurable + Automatic Inspector panel per instance.
- Collision detection: The fire interacts with dynamic colliders vía Signed distance fields
- Supports for one unique special color ( on top of the base fire color )
- Extendable: add custom SDF Shapes.
- Proxy system: When the fire is applied, you are offered a proxy object that you can control and the class will automatically keep everything in sync.
- Debug flags to visually see the colliders, bounding box of the simulation ( useful when things are not appearing where they should and you don't know why )
To see implementation code check the demo source Feed the llms.txt to your favourite assistant to know more.
The more you know: this will create a total of 11 textures so be mindful of the resolution you chose (mayority will use the physics grid resolution). If you know how to optimize this, do a PR.
npm install threejs-easyfireimport { EasyFire } from "threejs-easyfire";// objects you want to burn...
const teapot = new Mesh(new TeapotGeometry(1, 11), new MeshNormalMaterial());
scene.add(teapot);
// the fire volume handler. Add it to your scene.
const myFire = new EasyFire(renderer, {
renderLayer: LAYER_VOLUMETRIC_LIGHTING,
/**
* Play with these resolutions, it is a trial and error process
*/
size: {
boundingBox: new Vector3(8, 8, 8), // volume world space size
renderResolution: new Vector3(100, 100, 100), // rendering voxel size (visual)
physicsResolution: new Vector3(80, 80, 80), // physics voxel size
},
steps: 22, // ray marching ( more = nice look but slow )
/**
* define the "template" for creating the burn effect of your desired meshes
*/
burnableMeshes: [
{ geometriesInsideOf: teapot, maxCount: 1, id: "teapot" },
//...
],
});
scene.add(myFire); //<--- Don't forget to add the volume to your scene.By default SemiLagrangian is used which produces ok results, the other option is MacCormack that costs more but look way better.
import { EasyFire, AdvectionMethod } from "threejs-easyfire";
const myFire = new EasyFire(renderer, {
//...
advectionMethod: AdvectionMethod.MacCormack, //heavyer but looks better
advectionMethod: AdvectionMethod.SemiLagrangian, //faster (default)
//...
});teapot.add(
myFire.getFireFor("teapot") //will return a proxy Object3D that will represent the fire.
);const box = new Mesh(new SphereGeometry(0.5), new MeshPhysicalMaterial({ color: "darkgrey" }));
box.scale.set(3, 0.5, 3.5); //change the size...
box.position.y = 1;
scene.add(box);
// flag it as a collider...
myFire.makeObjectCollidable(box, "ellipsoid");Optionally you can create your own colliders using Signed Distance fields, just:
- Create a class that extends
SDFShape
import { SDFShape } from "threejs-easyfire";
import { Node } from "three/webgpu";
import { abs, length, max, min } from "three/tsl";
export class MyCustomSDFBox extends SDFShape {
override sdf(localPos: Node<"vec3">, halfExtents: Node<"vec3">): Node<"float"> {
const q = abs(localPos).sub(halfExtents);
return length(max(q, 0.0)).add(min(max(q.x, max(q.y, q.z)), 0.0));
}
}- And add it to the EasyFire config:
const myFire = new EasyFire(renderer, {
//...
collisions: {
sdfShapes: [
new MyCustomSDFBox(10,"myCustomBox") // says MAX count of this type will be 10
]
}
});- Use it...
myFire.makeObjectCollidable(box, "myCustomBox");To play with SDF Shapes and test things out if you are not familiar with sdf check out this all: threejs-sdf-shapes playground
Must call this before the scene renders, this will create
await myFire.initialize();const scenePass = pass(scene, camera);
const sceneDepth = scenePass.getTextureNode("depth"); // needs this
const fireScene = myFire.getRenderPass(scene, camera, sceneDepth);
const scenePassColor = scenePass.add(fireScene); // add on top of your scene...
renderPipeline.outputNode = scenePassColor;yourRenderLoop( delta:number ) {
box.rotateY(delta);
teapot.rotateX(delta*0.1);
// here you can move your objects, the teapot or the box...
myFire.update?.(delta);
renderPipeline.render();
}myFire.addSettingsToInspector(renderer.inspector as Inspector, "Fire's Settings");And vía the buttons provided there, download the json settins or copy them to clipboard, then...
The volume has 2 voxel grids, the render grid and the physics grid, they have different purposes. When you see "voxel units" it means how many times the world volume will be sliced.
WARNING: I still feel these methods are unstable, the simulation behaves diferently depending on the size of the voxels, obviously, so yeah, if you reduce the voxels too much it will act weird and explode or if there are too many too tiny they will be hard to notice unless you increase temperature and touch the settings again. Just a heads up.
- renderResolution: in voxel units. Determines the detail of the fire, higher values = more detail.
- physicsResolution: in voxel units. Determines the resolution of the simulation (buoyancy, vorticity, etc), higher values = more accurate simulation. You can programatically change the size ( but remember this will dispose the old textures and create new textures )
myFire.setRenderGridResolution({ x:100, y:100, z: 100 }); //voxels
myFire.setPhysicsGridResolution({ x:100, y:100, z: 100 }); //voxels
myFire.setNoiseGridResolution(64); //64x64x64// call this AFTER having called the myFire.getRenderPass(...) or it will throw error.
myFire.applySettingsSnapshot( settingsJson );This was a hard one, thanks to these amazing brains for assisting me.
- Lead Assistant + Code assistant: Gemini 3.1 Pro & 3.6 Flash
- Assistant #2: Claude Sonnet 5 on max
- Assistant #3: ChatGPT GPT-5.5
- Three.js and its amazing community.
- sunag: The original fire example was a masterpiece that served as a great inspiration and learning material for me.
Ask me vía @bandinopla

