MANY FIRES,
TWO DRAW CALLS
GPU FIRE IN THREE.JS

By Shane Brumback · · Free Three.js tutorial

To draw dozens of fires in Three.js without wrecking your frame rate, pack every fire's particles into one THREE.Points geometry per blend mode, animate each particle in the vertex shader from a time uniform, and cull far fires yourself. This tutorial pulls that pattern from Super Soldier Battle's real fire module.

Vertex-shader particles, per-fire distance fade, your own frustum test, and a glow sprite instead of PointLights — the ideas that make the burning ruins cheap, simplified so you can drop them into your own scene.

GPU fire particles — flames, embers and smoke rendered in two draw calls in Three.js

SEE IT LIVE

The flames drifting behind this page are a live run of this exact two-draw-call particle system. Click Live Demo to bring them forward and orbit the fire yourself — or drop into Super Soldier Battle and see them on the burning ruins and gunship wrecks. Both are free and run right in your browser, no download.

Play the Game

Live Demo runs the real fire module right here — drag to orbit, then click Done to keep reading.

Introduction

Super Soldier Battle's ruins and crashed gunship wrecks now burn with flames, embers, a warm glow and smoke. The effect is adapted from my campfire tutorial, but the campfire is built for a single fire. A multiplayer map needs lots of fires at once without hurting the frame rate.

The game's fire module (module/ssb-ruin-fire.js) solves this with a few rules: all fires in a set share two THREE.Points draw calls (flames, embers and glow in one additive call; smoke in a second normal-blended call), every particle is simulated in the vertex shader from a time uniform and per-particle seeds, there are no PointLights, and far fires are faded and culled per fire. Because the flame atlas is painted at startup, the module ships with zero texture bytes.

Read the dev log for the gameplay side: Burning Ruins and Gunship Wrecks Light Up Super Soldier Battle.

What you should know first: this is an intermediate build. You'll get the most out of it if you're comfortable with the Three.js basics (scene, camera, BufferGeometry) and some GLSL. The game runs on Three.js r128, and everything below works there; the runnable example uses a current CDN build.

What You'll Learn

  • Packing many fires into one BufferGeometry, with one vertex per particle and random seeds as attributes
  • Looping particle lifetimes in GLSL with fract(), and re-rolling randomness on every cycle
  • Fading and hard-culling far particles inside the vertex shader
  • Replacing PointLights with an additive glow sprite
  • Hiding the whole system with your own per-fire frustum test
  • Placing fires on a mesh surface with a one-time downward raycast, and guarding against NaN positions

Prerequisites

Comfortable with Three.js basics (scene, camera, BufferGeometry) and some GLSL. The game runs on Three.js r128 and everything below works there; the excerpts are simplified from the real module and variable names follow the game code. The runnable example uses a current CDN build.

Frequently Asked Questions

How do I render lots of fires in Three.js without lots of draw calls?

Put every fire's particles into one THREE.Points geometry per blend mode. Super Soldier Battle draws all its ruin fires in 2 draw calls: one additive call for flames, embers and glow, and one normal-blended call for smoke.

Can particles be animated entirely on the GPU in Three.js?

Yes. Give each particle random seeds as attributes and compute its position in the vertex shader from a time uniform, looping its lifetime with fract(). The CPU only updates uniforms each frame.

Why set frustumCulled to false on a particle system?

Because one geometry that spans the whole map has a bounding sphere that's almost always on screen, so built-in culling rarely helps. Test each fire's small sphere against the frustum yourself, and hide the objects when none are visible.

Should I use a PointLight for every fire?

Usually not in a large scene, because extra lights make lit materials more expensive. An additive glow sprite per fire, drawn in the same call as the flames, gives the warm look with no light cost.

Why do my particles sometimes stretch across the whole screen?

A NaN or huge position value is a common cause. Super Soldier Battle had exactly this with a wreck fire, and now checks every fire position with isFinite and falls back to a safe height when a raycast misses.

How It Works

  1. One geometry holds every fire's particles — one vertex per particle, with random seeds, a type and a per-fire scale as attributes, written once.
  2. The vertex shader simulates everything from a single uTime uniform, looping each particle's life with fract() and re-hashing its seeds every cycle.
  3. Far fires fade and hard-cull in the shader, and the point size is capped so a fire right in front of the camera can't fill the screen.
  4. A glow sprite replaces the PointLight — one extra additive particle per fire gives the warm base glow with no lighting cost.
  5. Two THREE.Points objects, two draw calls — additive fire on top, normal-blended smoke underneath, both sharing one uniforms object.
  6. Your own per-fire frustum test hides both objects when nothing is on screen, so off-screen fires cost zero draw calls.

Here is each layer on its own. Flames, embers and the glow all ride in one additive draw call; the soft smoke is a second, normal-blended draw call. Put them together and the whole fire is still just two draw calls.

Three.js GPU particle flames only, additive blended points from the Super Soldier Battle fire module
Flames only: the additive flame particles of one fire, animated entirely in the vertex shader.
Three.js ember spark particles rising from a fire, simulated on the GPU
Ember sparks only: tiny bright points that rise and drift, in the same draw call as the flames.
Three.js soft smoke particles rendered with normal blending in a separate Points object
Smoke only: soft puffs in the second Points object with normal blending (brightened for visibility).
Combined Three.js fire with flames, embers, glow and smoke on a ruin rubble pile, rendered in two draw calls
Everything combined on a real rubble pile: the whole fire system is just 2 draw calls (fire + smoke).

That's the whole system. The detailed code breakdown and the full copy-paste example are below — sign in free to unlock them.

How To Use It In Your Game

  • Collect your fire spots as [x, y, z, scale]. Use placement data, or raycast onto a surface once at load time, and guard everything with isFinite.
  • Build one geometry for flames, embers and glow, and one for smoke, with one vertex per particle and random seeds.
  • Animate everything in the vertex shader from uTime, and loop lifetimes with fract().
  • Fade and cull by distance in the shader, and use a glow sprite instead of PointLights.
  • Turn off frustumCulled, run your own per-fire frustum test, and hide both objects when nothing is visible.
  • Call update(dt, camera) every frame with the camera you render from.

SEE THESE FIRES IN A REAL GAME

This fire system ships in Super Soldier Battle's burning ruins and crashed gunship wrecks. Read the dev log on the fires update to see the gameplay side of the same feature.

Burning ruins and gunship wrecks in Super Soldier Battle Read the Fires Dev Log
Drag to orbit the fire