Tutorial
⏱️ ~55 minutes 🎓 Intermediate–Advanced 🛠️ GLSL · Three.js · ShaderMaterial

GLSL Fire Shader Effect

Procedural fire is one of the classic GPU effects — achieved entirely in the fragment shader with no textures or particles. This tutorial builds it step by step: a value noise function, fractional Brownian motion (fBm) layering, a fire colour palette using mix(), upward UV drift, and alpha masking for soft edges on a billboard plane.

Prerequisites

Value Noise in GLSL

Noise is the foundation of organic-looking effects. We can't use Math.random() in GLSL; instead we build a deterministic hash from position:

// --- Paste at top of fragment shader ---

// Hash: maps a 2D coordinate to a pseudo-random float [0,1]
float hash(vec2 p) {
  p = fract(p * vec2(234.34, 435.345));
  p += dot(p, p + 34.23);
  return fract(p.x * p.y);
}

// Bilinear value noise
float noise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  // Smooth the interpolation (Ken Perlin's quintic: 6t^5-15t^4+10t^3)
  vec2 u = f * f * (3.0 - 2.0 * f);

  float a = hash(i);
  float b = hash(i + vec2(1.0, 0.0));
  float c = hash(i + vec2(0.0, 1.0));
  float d = hash(i + vec2(1.0, 1.0));

  return mix(mix(a, b, u.x),
             mix(c, d, u.x), u.y);
}

This is value noise — fast but visible grid artefacts at low frequency. For higher quality use gradient noise (Perlin) or simplex noise. For fire, value noise is fine and cheaper.

Fractional Brownian Motion (fBm)

A single noise layer looks flat. fBm stacks multiple octaves at progressively higher frequencies and lower amplitudes — mimicking the multi-scale structure of natural phenomena:

// fBm: 5 octaves of value noise
float fbm(vec2 p) {
  float value  = 0.0;
  float amp    = 0.5;   // amplitude (halves each octave)
  float freq   = 1.0;   // frequency (doubles each octave)
  for (int i = 0; i < 5; i++) {
    value += amp * noise(p * freq);
    amp  *= 0.5;
    freq *= 2.0;
  }
  return value;
}

After 5 octaves the total amplitude sums to ≈1 (0.5+0.25+0.125+0.0625+0.03125 = 0.97). Call fbm(vUv * 3.0 + vec2(0.0, uTime)) and you'll see animated organic turbulence.

Fire Colour Palette

Fire transitions from dark red at the base → bright orange → yellow → white at the core. Map the noise value through three mix() calls:

vec3 fireColor(float t) {
  // t in [0,1]: 0 = dark/cold, 1 = hot/bright
  vec3 black  = vec3(0.0, 0.0, 0.0);
  vec3 red    = vec3(0.8, 0.1, 0.0);
  vec3 orange = vec3(1.0, 0.5, 0.0);
  vec3 yellow = vec3(1.0, 0.95, 0.1);
  vec3 white  = vec3(1.0, 1.0, 0.9);

  vec3 col = black;
  col = mix(col, red,    smoothstep(0.0, 0.25, t));
  col = mix(col, orange, smoothstep(0.2, 0.5,  t));
  col = mix(col, yellow, smoothstep(0.4, 0.75, t));
  col = mix(col, white,  smoothstep(0.7, 1.0,  t));
  return col;
}

UV Distortion + Upward Drift

Raw noise scrolled upward looks like lava, not fire. Two tricks make it look like fire:

  1. Upward drift: subtract uTime * speed from the UV y-coordinate so the pattern rises
  2. Height fadeout: multiply by (1 - vUv.y) so the top of the plane is transparent — fire tapers
void main() {
  vec2 uv = vUv;

  // Upward drift — fire rises
  float speed = 0.8;
  uv.y -= uTime * speed;

  // Turbulent x-distortion for waviness
  uv.x += 0.15 * sin(uv.y * 4.0 + uTime * 2.0);

  float n = fbm(uv * 2.5);

  // Height mask: fire is strongest at base, fades at top
  float heightMask = 1.0 - vUv.y;              // vUv.y=0 is bottom
  heightMask = pow(heightMask, 1.5);            // sharpen falloff

  float intensity = n * heightMask;

  vec3 col = fireColor(intensity * 1.8);       // multiply to push into hot range
  float alpha = smoothstep(0.0, 0.3, intensity);

  gl_FragColor = vec4(col, alpha);
}

Alpha Mask and Transparency

Three.js materials need extra flags to render transparency correctly:

const mat = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader: /* ... */,
  fragmentShader: /* ... */,
  transparent: true,       // enable alpha blending
  depthWrite: false,       // don't write to depth buffer (avoids sorting artefacts)
  side: THREE.DoubleSide,  // visible from both sides
  blending: THREE.AdditiveBlending, // optional: additive = fire glow over dark bg
});

AdditiveBlending adds the fire colour on top of whatever is behind it — perfect for emissive/glow effects. Use NormalBlending if the fire should occlude background objects.

Billboard Plane Setup

A billboard always faces the camera. Use PlaneGeometry and rotate the mesh in the animate loop:

const firePlane = new THREE.Mesh(
  new THREE.PlaneGeometry(2, 3), // width, height
  mat
);
scene.add(firePlane);

// In animate():
// Simple billboard: copy camera quaternion to mesh
firePlane.quaternion.copy(camera.quaternion);

Or use Three.js Sprite for automatic camera-facing — but Sprite doesn't support ShaderMaterial, so the mesh approach is better here.

Complete Fire Shader

<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.168/build/three.module.js';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x080808);
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 100);
camera.position.set(0, 1.5, 5);

const vertGLSL = /* glsl */`
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const fragGLSL = /* glsl */`
  uniform float uTime;
  varying vec2 vUv;

  float hash(vec2 p) {
    p = fract(p * vec2(234.34, 435.345));
    p += dot(p, p + 34.23);
    return fract(p.x * p.y);
  }
  float noise(vec2 p) {
    vec2 i = floor(p), f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);
    return mix(mix(hash(i), hash(i+vec2(1,0)), u.x),
               mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), u.x), u.y);
  }
  float fbm(vec2 p) {
    float v = 0.0, a = 0.5, fr = 1.0;
    for (int i = 0; i < 5; i++) { v += a*noise(p*fr); a*=.5; fr*=2.; }
    return v;
  }
  vec3 fireColor(float t) {
    vec3 col = vec3(0);
    col = mix(col, vec3(.8,.1,0), smoothstep(0.,.25, t));
    col = mix(col, vec3(1.,.5,0), smoothstep(.2,.5,  t));
    col = mix(col, vec3(1.,.95,.1), smoothstep(.4,.75, t));
    col = mix(col, vec3(1.,1.,.9), smoothstep(.7,1.,  t));
    return col;
  }
  void main() {
    vec2 uv = vUv;
    uv.y -= uTime * 0.8;
    uv.x += 0.15 * sin(uv.y * 4.0 + uTime * 2.0);
    float n = fbm(uv * 2.5);
    float h = pow(1.0 - vUv.y, 1.5);
    float intensity = n * h;
    vec3 col = fireColor(intensity * 1.8);
    float alpha = smoothstep(0.0, 0.3, intensity);
    gl_FragColor = vec4(col, alpha);
  }
`;

const mat = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader: vertGLSL,
  fragmentShader: fragGLSL,
  transparent: true,
  depthWrite: false,
  side: THREE.DoubleSide,
  blending: THREE.AdditiveBlending,
});

const flame = new THREE.Mesh(new THREE.PlaneGeometry(2, 3), mat);
scene.add(flame);

// Point light to illuminate surroundings
const light = new THREE.PointLight(0xff6600, 3, 15);
light.position.set(0, 1.5, 1);
scene.add(light);

window.addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

(function loop(t) {
  requestAnimationFrame(loop);
  mat.uniforms.uTime.value = t * 0.001;
  flame.quaternion.copy(camera.quaternion);
  light.intensity = 2.5 + 0.8 * Math.sin(t * 0.009); // flicker
  renderer.render(scene, camera);
})(performance.now());
</script>

Continue Learning

Frequently Asked Questions

What will I learn in this tutorial?

Build a procedural fire shader in GLSL: layered Perlin noise, colour palettes, UV distortion, and animated billboarding in Three.js.

What topics are covered in this tutorial?

This tutorial covers: Value noise in GLSL, Fractional Brownian Motion (fBm), Fire colour palette, UV distortion + upward drift, Alpha mask and transparency, Billboard plane setup, Complete fire shader.

What tools and technologies does this tutorial use?

This tutorial uses GLSL, Three.js, ShaderMaterial.

How long does this tutorial take?

This tutorial takes approximately 55 minutes to complete.

What prerequisites do I need before starting?

Before starting, you should have: Completed WebGL Shaders Intro — you must know varyings, uniforms and ShaderMaterial; Familiarity with sin , fract , mix , smoothstep GLSL built-ins.

🛠

Experiment in Playground

Tweak the fire shader live — run GLSL code directly in your browser, no compilation needed.

Open Playground → View Simulation ↗