Shader Lab
32.Bloom

Bloom

Arc 7 · Post-Pipeline · Lesson 32 of 35
coordinate signal color
0.85

Purpose

Bloom is a three-step recipe — threshold the bright pixels, blur them, add them back on top — and that threshold is the knob that decides whether the effect feels like polish or feels like fog.

Key insight

Cameras and eyes can't cleanly resolve very bright light; it leaks into its surroundings. Shaders fake this with a pipeline. Step one: build a second image that only contains pixels above some brightness threshold — everything else goes black. Step two: blur that image heavily (downsample, blur, upsample). Step three: add the blurred bright-only image on top of the original.

The original scene is untouched; the glow is a second image layered over it. Because a pixel can only see its own coordinate, the blur is what gives each pixel knowledge of its bright neighbors — the multi-pass contract from Lesson 1. Bloom is also why scene values are allowed to exceed 1.0 (HDR): if every pixel maxed at 1.0, nothing would ever cross the threshold.

Break it

Pull radiance to 0. Every pixel crosses the threshold, so every pixel bleeds into its neighbors. The image looks like it's been hit with a soft-focus filter — contrast collapses, blacks lift, the scene looks underwater. Teaches: bloom is a soft-blur layered over the source; without selectivity, it becomes the entire image. The threshold isn't a minor knob — it's the reason bloom reads as "highlights glowing" instead of "the scene is blurry."

Direct Claude

"more magical / dreamier" radiance down "more grounded / less hazy" radiance up "punchier highlights" radiance up, bloom strength up "soft-focus / filmic diffusion" radiance low, bloom strength moderate
Meta-phrase you gain here: "bloom in HDR." Bloom must run on raw lighting values before anything squashes them to 0–1. Ask for bloom placed "before tone-mapping" whenever you brief a full post chain.
Combines with: L33 (ACES) — bloom runs first on HDR, then tone mapping compresses the result. L34 (LUT) — LUT is applied after tone mapping; bloom's job is already done by then. L35 (SSAO) — SSAO multiplies darkness in before bloom thresholds, so deeper AO means slightly less bloom in those crevices. Canonical order: SSAO → bloom → ACES → LUT.
the post-FX wiring running above
// Base scene: sphere (emissive) + cube + ground plane under a sun.
// Emissive color on the sphere is (5, 3, 1) — ABOVE 1.0.
// That's HDR: raw scene values can exceed the display's 0-1 range,
// which is what makes bloom possible at all.

const renderTarget = new THREE.WebGLRenderTarget(w, h, {
  type: THREE.HalfFloatType,  // float precision so values > 1 survive
});

const composer = new EffectComposer(renderer, renderTarget);
composer.addPass(new RenderPass(scene, camera));

// --- The three-step bloom recipe ---
// UnrealBloomPass does all of it in one pass object:
//   step 1: threshold (keep only pixels above `threshold`)
//   step 2: blur (mip-chain downsample + gaussian)
//   step 3: add the blurred bright-only image back on top

const bloom = new UnrealBloomPass(
  new THREE.Vector2(w, h),
  1.2,   // strength — how much glow adds in
  0.6,   // radius — how far the glow spreads
  0.85   // THRESHOLD — this is u_radiance below
);
composer.addPass(bloom);

// Tone-map + display (bloom runs BEFORE this, on the raw HDR values)
composer.addPass(new OutputPass());

// The slider just updates the threshold:
function setRadiance(v) { bloom.threshold = v; }