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, lastStep 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>