BLOOM GLOW
THREE.JS GUIDE

Add cinematic Unreal-style bloom to any Three.js scene with EffectComposer and UnrealBloomPass.

EffectComposer UnrealBloomPass OutputPass Live bloom controls

By Shane Brumback · Indie Forge Studio · September 20, 2026

Bloom Glow Three.js tutorial — glowing torus knot

LIVE DEMO

The glowing torus knot behind this page is the live demo. Drag the sliders below to change the bloom in real time, or go full-screen for an immersive look — the controls come with you.

TRY IT: BLOOM CONTROLS

0.35
0.40
0.20

Tip: lower the threshold toward 0 to make more of the scene glow, then raise strength for a full cinematic bloom. These are the exact three values passed to UnrealBloomPass.

MADE A WHOLE RIGGED MODEL WITH AI

Want to see how far this goes? This rigged, animated capybara — model, Three.js code, and the video below — was built with Grok Bot from a few plain-English prompts. Same Three.js world, zero manual modeling.

Frequently Asked Questions

How do I add a bloom glow effect in Three.js?

Use three/addons/postprocessing. Create an EffectComposer, add a RenderPass, then add UnrealBloomPass. Call composer.render() instead of renderer.render() every frame. Bloom glows any pixel brighter than the threshold.

What is UnrealBloomPass in Three.js?

UnrealBloomPass is a Three.js post-processing pass inspired by Unreal Engine bloom. It extracts bright areas, blurs them at multiple scales, and composites the result back onto the scene for a cinematic glow.

Why is my Three.js bloom not showing?

The most common causes are still calling renderer.render() instead of composer.render(), a threshold that is too high, materials without emissive color, or a bright background that hides the glow.

Does bloom work on mobile in Three.js?

Yes, but it is GPU-heavy. Lower the bloom resolution, reduce strength, and skip bloom on low-end devices.

What is bloom, really?

Bloom is the soft halo that spills out of anything bright — neon signs, a sunset, a lightsaber. Your GPU can't actually make a pixel "brighter than white," so bloom fakes it: after the scene is drawn, Three.js grabs every pixel above a brightness threshold, blurs those bright spots, and layers the blur back on top. Your eye reads that overflow as glow. The whole effect is post-processing — it happens to the finished image, not to the mesh — so it works on any scene you already have. Two things make or break it: a dark background for the glow to bleed into, and a bright emissive material to be the light source. Here is the whole thing in four steps.

Step 1: The usual scene setup

Nothing special yet — a scene, a camera, and a renderer, the same three lines every Three.js project starts with. Bloom bolts on after this, which is the whole point: you can add it to a scene you have already built.

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });

Step 2: Give the glow something to feed on

Bloom only lights up pixels that are already bright, so the mesh has to emit light rather than just reflect it. That is what emissive does — it makes the material shine on its own, even with no lights in the scene. Crank the emissive color (or add emissiveIntensity) and you are handing the bloom pass exactly the bright pixels it looks for.

const geometry = new THREE.TorusKnotGeometry(2, 0.6, 150, 32);
const material = new THREE.MeshStandardMaterial({
  color: 0x2ad1d6,
  emissive: 0x2ad1d6,   // the color that will glow
  emissiveIntensity: 1.8,
  metalness: 0.4,
  roughness: 0.2
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

Step 3: Build the post-processing pipeline

This is where bloom actually happens, and where most people get stuck. An EffectComposer is a little assembly line: each pass takes the image, does one job, and hands it to the next. RenderPass draws your scene, UnrealBloomPass adds the glow, and OutputPass does the color correction last so the glow stays accurate. The three bloom numbers are the ones you tuned with the live sliders above: strength (how intense), radius (how far it spreads), and threshold (how bright a pixel must be before it glows at all).

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));   // 1. draw the scene
composer.addPass(new UnrealBloomPass(
  new THREE.Vector2(innerWidth, innerHeight),
  0.21,  // strength  - overall glow intensity
  0.53,  // radius    - how far the glow spreads
  0.0    // threshold - how bright a pixel must be to glow
));                                                // 2. add the glow
composer.addPass(new OutputPass());                // 3. tone mapping + sRGB, last

Step 4: Render through the composer

The one-line change that trips everyone up: in your animation loop, call composer.render() instead of renderer.render(scene, camera). If the glow never shows up, this is almost always why — the renderer is still drawing the plain scene and skipping your whole bloom pipeline. Make the swap and the knot lights up.

function animate() {
  requestAnimationFrame(animate);
  mesh.rotation.x += 0.01;
  mesh.rotation.y += 0.01;
  composer.render();   // NOT renderer.render(scene, camera)
}
animate();

THE FULL CODE

Paste this into a file named bloom-glow-threejs.html and open it in any modern browser — or hit Launch to run it in a new tab. Everything is self-contained; Three.js loads from a CDN via the import map.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Three.js Bloom Glow Demo</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
      "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
    }
  }
  </script>
  <style>
    html,body{margin:0;height:100%;background:#0a1428;overflow:hidden;font-family:system-ui,Segoe UI,sans-serif;color:#dfe8ef}
    #panel{position:fixed;top:14px;left:14px;width:230px;padding:16px;background:rgba(10,15,28,.85);
      border:1px solid rgba(42,209,214,.4);border-radius:14px;backdrop-filter:blur(6px)}
    #panel h1{font-size:15px;margin:0 0 10px;color:#2ad1d6;letter-spacing:1px}
    #panel label{display:block;font-size:12px;margin:12px 0 4px;color:#9fb0bd}
    #panel .val{float:right;color:#2ad1d6;font-family:ui-monospace,Menlo,Consolas,monospace}
    #panel input[type=range]{width:100%;accent-color:#2ad1d6;cursor:pointer}
    #panel button{margin-top:14px;width:100%;padding:8px;border-radius:8px;cursor:pointer;
      background:#11131a;border:1px solid rgba(42,209,214,.4);color:#2ad1d6;font-weight:600}
  </style>
</head>
<body>
<div id="panel">
  <h1>BLOOM CONTROLS</h1>
  <label>Strength <span class="val" id="v-str">0.21</span></label>
  <input id="strength" type="range" min="0" max="3" step="0.01" value="0.21">
  <label>Radius <span class="val" id="v-rad">0.53</span></label>
  <input id="radius" type="range" min="0" max="1" step="0.01" value="0.53">
  <label>Threshold <span class="val" id="v-thr">0.00</span></label>
  <input id="threshold" type="range" min="0" max="1" step="0.01" value="0">
  <button id="reset">Reset</button>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';

// --- scene / camera / renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a1428);
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 1, 7);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// --- a bright, emissive object so the bloom has something to glow
const mesh = new THREE.Mesh(
  new THREE.TorusKnotGeometry(2, 0.6, 220, 32),
  new THREE.MeshStandardMaterial({
    color: 0x2ad1d6, emissive: 0x2ad1d6, emissiveIntensity: 1.8,
    metalness: 0.4, roughness: 0.2
  })
);
scene.add(mesh);
scene.add(new THREE.AmbientLight(0xffffff, 0.3));
const p1 = new THREE.PointLight(0x9fe8ff, 3, 200); p1.position.set(8, 10, 12); scene.add(p1);
const p2 = new THREE.PointLight(0x2ad1d6, 2, 200); p2.position.set(-8, -6, 6); scene.add(p2);

// --- EffectComposer: RenderPass -> UnrealBloomPass -> OutputPass
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 0.21, 0.53, 0.0);
composer.addPass(bloomPass);
composer.addPass(new OutputPass());   // tone mapping + sRGB, after bloom

// --- wire the sliders to the bloom pass
const DEFAULTS = { strength: 0.21, radius: 0.53, threshold: 0.0 };
function bind(id, key, valId) {
  const el = document.getElementById(id), out = document.getElementById(valId);
  el.addEventListener('input', () => { bloomPass[key] = parseFloat(el.value); out.textContent = (+el.value).toFixed(2); });
}
bind('strength', 'strength', 'v-str');
bind('radius', 'radius', 'v-rad');
bind('threshold', 'threshold', 'v-thr');
document.getElementById('reset').addEventListener('click', () => {
  for (const k in DEFAULTS) {
    const el = document.getElementById(k); el.value = DEFAULTS[k];
    el.dispatchEvent(new Event('input'));
  }
});

// --- render loop (render through the composer, NOT renderer.render)
function tick() {
  requestAnimationFrame(tick);
  mesh.rotation.x += 0.004;
  mesh.rotation.y += 0.006;
  controls.update();
  composer.render();
}
tick();

addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
  composer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

More 3D Projects