Drag to orbit · scroll to zoom · sliders tune the fire · press Esc to exit

BUILD A REALISTIC CAMPFIRE
WITH GROK BOT + THREE.JS

By Shane Brumback · · Free Three.js tutorial

Use Grok Bot to build a glowing Three.js campfire — generated flame flipbook tiles, particle flames, rising embers, drifting smoke, a flickering light, bloom glow, and photo-textured logs and stones. One HTML file, built by prompting step by step, including the mistakes we fixed along the way.

Build a Realistic Campfire with Grok Bot and Three.js

LIVE DEMO

The campfire burning behind this page is the live demo — the real Three.js scene with flipbook flames, embers, smoke and bloom. Launch the full-screen demo to orbit the fire, scroll to zoom in, and tune it with the Intensity, Bloom and Smoke sliders, stoke the flames, and toggle quality. Scroll to keep reading.

Hides the page so you get the fire full screen — drag to orbit, use the sliders, press Esc to exit.

WANT MORE FIRE AND FURY?

Once your campfire burns, blow something up: build a 3D rockfire explosion in Three.js — flying debris rocks, fire, smoke, a shockwave ring, and camera shake, all in one copy-paste file with a live demo you can orbit.

3D rockfire explosion effect built in Three.js See the Rockfire Explosion Tutorial

Introduction

Fire is a classic real-time effect — and a great way to see how far an AI assistant can carry a graphics build. In this tutorial you prompt Grok Bot step by step, the same way this demo was actually made, including the mistakes we fixed on the way: a washed-out white flame column, logs that hid the fire, and a texture slowdown. You finish with a single campfire.html you can drop on any web host.

The flames are flipbook particles: one THREE.Points system whose ShaderMaterial plays a 4×4 sheet of fire frames, so hundreds of tiny looping fires draw in one call. Embers and smoke are two more Points systems. A flickering PointLight, a ground glow decal, and an UnrealBloomPass sell the heat, and photo-textured logs laid flat in a star plus a ring of noise-displaced stones ground it in a real place.

What you should know first: steps are plain-English prompts you paste into Grok Bot, so most of this needs no coding. A little Three.js (scenes, materials, shaders) helps you read the key-code snippets. If Three.js is brand new, start with How to Program a Simple Particle System or How to Program a Fire Fountain Particle System, then come back. Everything loads from a CDN — no build tools.

What You'll Need

  • The Grok Bot desktop app on Windows (it writes and runs code on its own cloud computer while you chat).
  • A modern desktop browser with hardware acceleration turned on.
  • Python, or any local web server, to serve the files over http — browsers block textures loaded from file://.
  • Basic familiarity with Three.js (scenes, materials, shaders) to follow the key-code snippets.

Frequently Asked Questions

How do you make a realistic campfire in Three.js?

Layer a few cheap effects over a dark night scene: particle flames that play a looping flame flipbook texture with additive blending, rising ember sparks, drifting smoke puffs, a flickering orange point light, a glow decal on the ground, and an UnrealBloomPass so the hot pixels bloom. Add photo-textured logs laid flat in a star and a ring of noise-displaced stones. The campfire on this page was built that way by prompting Grok Bot step by step.

How do flipbook flames work in Three.js?

A flipbook is a grid of flame frames packed into one texture (here a 4x4 sheet of 16 frames). Each flame particle is a THREE.Points sprite with a ShaderMaterial that picks one tile as a UV offset and advances through the frames at about 14 frames per second, so every particle animates like a tiny looping video of fire. Additive blending makes the flames glow and stack into a hot core.

Why did my Three.js fire look like a solid white column?

Too many large additive particles overlap and their brightness adds up until the whole column clips to white. The fix is fewer flame particles (about 26 instead of 70), lower per-particle brightness, and a bloom threshold so only the brightest pixels bloom. That keeps the orange and yellow detail instead of blowing it out.

How do you keep a Three.js campfire running fast?

The usual bottleneck is per-pixel fill rate, not triangles. Remove the bump map from the full-screen ground plane, cap texture anisotropy, share texture objects, cap gl_PointSize so zooming in cannot flood the screen with additive quads, cap the pixel ratio, and render bloom at half resolution. A lite mode that halves the particles and turns bloom off gives weaker machines a fast fallback.

Is this tutorial free?

Yes. The glowing campfire behind this page is a live Three.js demo, and the full single-file source is on the page. Signing in with Google to reveal the code is free — it takes one click and unlocks every free tutorial on the site.

1Set up the campfire scene

Start with an empty night scene. Ask Grok Bot for a single HTML file so you can open it, look at it, and keep iterating.

Prompt to paste

Make a single-file Three.js page (importmap from a CDN) for a night campfire scene: dark fog, ground, OrbitControls aimed at the fire, ACES tone mapping and an UnrealBloomPass so the flames glow. Add Intensity and Bloom sliders.

Your chat with Grok BotIllustration
Make a single-file Three.js page for a night campfire scene: dark fog, ground, OrbitControls, ACES tone mapping and an UnrealBloomPass so the flames glow. Add Intensity and Bloom sliders.
Done. campfire.html loads Three.js r160 from a CDN via an importmap, sets up the renderer, fog, orbit camera and an EffectComposer with RenderPass, UnrealBloomPass and OutputPass. Sliders are top-left.
Illustration, not a screenshot. The bot reply is shortened.

Key code

Everything renders through an EffectComposer. Bloom only picks up pixels brighter than the threshold (0.7), so the fire glows but the rest of the scene stays crisp.

// Bloom
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(innerWidth / 2, innerHeight / 2), 0.45, 0.35, 0.7);
composer.addPass(bloom);
composer.addPass(new OutputPass());
Tip: one file, CDN importsAn importmap with three and three/addons/ means no build step. Drop the file on any web host.

2Generate the fire tile images

Realistic flames come from real-looking images, not from shapes. Ask Grok Bot to generate the textures, then to prepare them for a particle flipbook.

Prompt to paste

Generate an image of 12 realistic campfire flame frames in a 4-column by 3-row layout on a pure black background, for an additive-blended flipbook. Also generate a soft grey smoke puff on black.

Generated flame frames, 4 by 3, on black
Generated flame frames (not yet a square grid).
Generated smoke puff on black
Generated smoke puff.

The generated sheet is not a square grid, and the flames have hard flat bottoms. Ask for a cleanup script:

Prompt to paste

With PIL, find each flame's bounding box by brightness, crop each into a square cell with the flame bottom-centered, and repack the 12 frames plus 4 repeats into a 4x4 sheet at 1024px on pure black. Then fade each cell's bottom 28% with a rounded falloff so the flame bases are soft, and fade the sides so nothing touches a cell edge.

Repacked 4 by 4 flame flipbook with soft rounded bases
The repacked 4×4 flipbook with soft rounded bases.
Tip: pure black, power of twoBlack backgrounds vanish under additive blending. Square power-of-two sizes (256, 512, 1024) get mipmaps and wrap cleanly. Ask for both every time.

3Flipbook flames, embers, smoke and flicker

Prompt to paste

Use the 4x4 flipbook for flame particles: one THREE.Points per system with a ShaderMaterial that picks a tile per particle with UV offsets, additive blending, about 14 fps per particle, varied sizes and a slight random tilt. Add rising embers, soft smoke with normal blending, a flickering orange point light and a glow decal on the ground.

Close-up of flipbook flames, embers and glowing ember cracks
Close-up: flipbook flames, embers and glowing ember cracks.

Slicing the flipbook in the shader

aFrame picks one of the 16 tiles. The tile's column and row become a UV offset, and gl_PointCoord (rotated by aRot) samples inside that tile.

const flameFrag = `uniform sampler2D uAtlas;
varying float vRot; varying float vLife; varying float vFrame;
void main(){
  float f = floor(vFrame);
  vec2 tile = vec2(mod(f, 4.0), 3.0 - floor(f / 4.0));
  vec2 pc = gl_PointCoord - 0.5;
  pc = mat2(cos(vRot), -sin(vRot), sin(vRot), cos(vRot)) * pc + 0.5;
  if (any(lessThan(pc, vec2(0.0))) || any(greaterThan(pc, vec2(1.0)))) discard;
  vec2 uv = (tile + vec2(pc.x, 1.0 - pc.y)) / 4.0;   // flipbook slice
  vec3 c = texture2D(uAtlas, uv).rgb;
  float fade = smoothstep(0.0, 0.15, vLife) * smoothstep(1.0, 0.6, vLife);
  gl_FragColor = vec4(c * fade * 0.5, 1.0);
}`;

One reusable particle system

Each system keeps a plain JS array of particles and writes straight into Float32Array buffers — no per-particle meshes and no allocations per frame.

function makeSystem(count, frag, u, blending, spawn, update) {
  const g = new THREE.BufferGeometry();
  const arr = n => new THREE.BufferAttribute(new Float32Array(count * n), n);
  g.setAttribute('position', arr(3)); g.setAttribute('aSize', arr(1));
  g.setAttribute('aLife', arr(1)); g.setAttribute('aFrame', arr(1)); g.setAttribute('aRot', arr(1));
  const m = new THREE.ShaderMaterial({ uniforms: u, vertexShader: vert, fragmentShader: frag,
    transparent: true, depthWrite: false, blending });
  const pts = new THREE.Points(g, m); pts.frustumCulled = false; scene.add(pts);
  // ...each frame: advance life, respawn dead particles, write the buffers once.
  return pts;
}

Flicker

const flick = 0.75 + 0.15 * Math.sin(t * 13) + 0.1 * Math.sin(t * 27.3) + Math.random() * 0.08;
fireLight.intensity = 9 * k * flick;
fireLight.position.x = Math.sin(t * 7) * 0.05;
Tip: less is more with additive blendingOur first pass used 70 large flame particles and the fire became a white column. Fewer particles (26), lower brightness and a bloom threshold kept the orange and yellow detail.

4More smoke, with a Smoke slider

The first smoke was thin. Ask for bigger, fuller smoke and a control for it — and say how to handle memory so the slider stays cheap:

Prompt to paste

Add larger and more smoke plus a Smoke slider (0 to 2, default 1.2) next to Intensity and Bloom. The slider scales the smoke spawn count, puff size and opacity. Allocate the buffer for the max count and use drawRange for the active count, with no reallocation when the slider moves. Puffs should grow as they rise, sway in a light wind drift and fade out. Cap the puff size, use normal blending with low opacity, and halve it in lite mode.

Key code

The particle loop only updates active particles, and setDrawRange tells the GPU how many to draw. Moving the slider never allocates anything.

function setSmoke(v) {
  params.smokeSize = 0.6 + 0.4 * v;
  smokeU.uOpacity.value = Math.min(v, 1.4);
  smoke.active = Math.min(smoke.max, Math.round(smoke.max * v / 2));
  smoke.g.setDrawRange(0, smoke.active);   // no reallocation
}

Opacity is a uniform, so the shader fades each puff in and out over its life and multiplies by the slider:

const smokeFrag = `uniform sampler2D uTex; uniform float uOpacity; varying float vLife;
void main(){
  float d = length(gl_PointCoord - 0.5);
  float a = texture2D(uTex, gl_PointCoord).r
    * smoothstep(0.5, 0.2, d) * smoothstep(0.0, 0.3, vLife)
    * smoothstep(1.0, 0.45, vLife) * 0.16 * uOpacity;
  gl_FragColor = vec4(vec3(0.16, 0.15, 0.15), a);
}`;
Tip: smoke is overdrawBig overlapping puffs cost fill rate. Keep per-puff opacity low (0.16), cap the size, and let the slider and lite mode reduce the count.

5Lay the logs flat so the fire shows

The first version stacked six upright logs in a teepee. They hid the flames from most angles. Say what looks wrong in plain words:

Prompt to paste

Remove the upright teepee logs. Keep the two crossed logs on the ground and add two more lying flat, so four flat logs in a star pattern with slight rotation jitter. Keep the ember cracks, and lower the flame spawn so the flames sit on top.

Your chat with Grok BotIllustration
Remove the upright teepee logs. Keep the two crossed logs and add two more lying flat — four flat logs in a star with slight rotation jitter. Keep the ember cracks, and lower the flame spawn so the flames sit on top.
Done. Four logs now lie 45° apart, each raised slightly so they stack, and the flames spawn just above them.
Illustration, not a screenshot. The bot reply is shortened.

Key code

Each log is an open cylinder plus two cap discs. All the sides merge into one mesh (bark) and all the caps into another (charred wood with ember cracks), so every log costs just two draw calls total.

const sideGeos = [], capGeos = [];
function addLog(len, r, rotY, y) {              // sides + caps built separately, merged into 2 meshes
  const side = new THREE.CylinderGeometry(r * 0.92, r, len, 18, 8, true);
  // ...noise-wobble the radius so it is not a perfect cylinder...
  const top = new THREE.CircleGeometry(r * 0.92, 18).rotateX(-Math.PI / 2).translate(0, len / 2, 0);
  const bot = new THREE.CircleGeometry(r, 18).rotateX(Math.PI / 2).translate(0, -len / 2, 0);
  _m.compose(_p.set(0, y, 0), _q.setFromEuler(_e.set(0, rotY, Math.PI / 2, 'YXZ')), _s.set(1, 1, 1));
  sideGeos.push(side.applyMatrix4(_m)); capGeos.push(top.applyMatrix4(_m), bot.applyMatrix4(_m));
}
for (let i = 0; i < 4; i++) addLog(1.8, 0.12, i * Math.PI / 4 + 0.4 + (Math.random() - 0.5) * 0.2, 0.12 + i * 0.07);
logs.add(new THREE.Mesh(mergeGeometries(sideGeos), barkMat),
         new THREE.Mesh(mergeGeometries(capGeos), logMat));

6Realistic bark, rock and ground

Prompt to paste

Generate photo textures for tree bark, granite rock and forest ground. Make each square and seamless with PIL (crop, offset-blend the edges), 512px JPG. Use MeshStandardMaterial with the photo as map and bumpMap, roughness 0.9, smooth shading. Use smoother logs, subdivided noise-displaced rocks in a tight ring of about 22 stones, and a dark ash decal under the fire.

Seamless bark texture generated and tiled
Bark (generated, made tileable).
Seamless granite rock texture
Rock detail.

Key code

A small helper caches each texture and sets wrapping, sRGB and anisotropy once. Rocks are icosahedrons displaced by a cheap 3D noise and merged into a single mesh — 22 stones for one draw call.

function pbr(url, rep, bump, extra = {}) {
  return new THREE.MeshStandardMaterial({
    map: tex(url, rep),
    ...(bump ? { bumpMap: tex(url, rep, false, 1), bumpScale: bump } : {}),
    roughness: 0.9, ...extra });
}
const rockGeos = [];
for (let i = 0; i < 22; i++) {
  const geo = mergeVertices(new THREE.IcosahedronGeometry(1, 2));
  const p = geo.attributes.position;
  for (let j = 0; j < p.count; j++) {           // push each vertex out by layered noise
    _v.fromBufferAttribute(p, j);
    _v.multiplyScalar(1 + 0.18 * n3(_v.x * 1.5, _v.y * 1.5, _v.z * 1.5) + 0.06 * n3(_v.x * 4, _v.y * 4, _v.z * 4));
    p.setXYZ(j, _v.x, _v.y, _v.z);
  }
  // ...scale + place around the ring, then push into rockGeos...
  rockGeos.push(geo);
}
scene.add(new THREE.Mesh(mergeGeometries(rockGeos), stoneMat));  // 22 rocks = 1 draw call
Orbiting view of the finished campfire with logs, stones and smoke
Orbiting the finished campfire.

7Fix the slowdown

After the photo textures went in, the demo crawled on a Windows PC — even though it is only about 5,000 triangles and 23 draw calls. Ask Grok Bot to find the real cause instead of guessing:

Prompt to paste

It's very slow on my PC since the textures were added. Profile it, check for shader recompiles or needsUpdate in the loop, compare bloom off and particles off, and fix the real bottleneck. Add a Quality toggle.

Your chat with Grok BotIllustration
It's very slow on my PC since the textures were added. Profile it, check for shader recompiles, compare bloom off and particles off, and fix the real bottleneck. Add a Quality toggle.
No recompiles and no per-frame texture work; JavaScript is under 1% of frame time. The cost is per pixel: the ground filled the screen with a bump-mapped, 8x anisotropic, heavily repeated texture. I removed its bump map, cut anisotropy, shared texture objects, capped particle sizes and added a lite mode.
Illustration, not a screenshot. The bot reply is shortened.
  • Ground: no bumpMap on the full-screen plane, smaller plane, repeat about 19×.
  • Anisotropy: 4 for colour maps, 1 for bump maps. Each texture loads once.
  • Particles: gl_PointSize capped at 220px so zooming in can't flood the screen with additive quads.
  • Also: pixel ratio capped at 1.5 and bloom at half resolution.
  • Quality toggle: ?lite turns bloom off, halves the particles and sets pixel ratio to 1.
The campfire controls: Intensity, Bloom, Smoke, Stoke fire and a Quality toggle
The controls: Intensity, Bloom, Smoke, Stoke fire and the Quality toggle.
Tip: check that the GPU is actually usedIf the scene is slow and jerky, your browser may be rendering on the CPU. Open the page in a normal desktop browser (not an embedded preview), turn on "Use graphics acceleration when available", and check chrome://gpu. On laptops, set the browser to "High performance" in Windows Graphics settings.

What To Try Next

  • Recolour the flame flipbook and the point light for a blue gas flame, a green witch's fire, or cold blue embers.
  • Add a kettle or a log pile and let the flickering PointLight cast soft shadows onto them.
  • Drive the Stoke fire boost from a game event — throwing on a log, a gust of wind — instead of a button.
  • Spawn more embers and a wider smoke cone when Intensity is high, so the fire visibly roars.
  • Port the flipbook flames to Unity's VFX Graph or Godot's GPUParticles3D — the same 4×4 sheet works there too.