Bloom
Source: src/gpu/pipelines/bloom.ts
Bloom is the glow around bright heads. It’s the classic three-step image-space effect: extract bright pixels, blur them, add them back. It runs at half resolution (cheap) into HDR targets, only when bloom is enabled.
1. Bright-pass extract
Section titled “1. Bright-pass extract”Keep only what’s brighter than the threshold, per channel, with a hard knee:
Dim pixels go to black; bright pixels survive minus the threshold. Subtracting (rather than just masking) makes the transition into the glow smooth. threshold is the bloom.threshold knob (default 0.8). This is drawn into a half-resolution target — bloom is low-frequency, so half-res is invisible in the result and a 4× pixel saving.
2. Separable Gaussian blur
Section titled “2. Separable Gaussian blur”A true 2D Gaussian blur of radius costs texture samples per pixel. But a Gaussian is separable — a 2D blur equals a horizontal 1D blur followed by a vertical one:
so the cost drops to samples. This effect does exactly that: a horizontal blur pass, then a vertical one (direction = [1,0] then [0,1]), each a 9-tap kernel (radius 4).
The weights are a sampled Gaussian, normalized so the symmetric kernel sums to 1:
and the blurred value is the weighted sum of the center and the four taps each side:
Taps step one texel at a time (2 / resolution in the half-res target’s uv space), along the pass direction.
3. Combine
Section titled “3. Combine”The blurred bloom is added back over the full-resolution scene, scaled by intensity:
intensity is bloom.intensity (default 1.5). The result goes into an HDR combine target — values can exceed 1.0 here, which is what lets the CRT tone-map step blow highlights out to white.
Head emission — giving bloom something to extract
Section titled “Head emission — giving bloom something to extract”There’s a catch the threshold and intensity can’t fix on their own: if nothing in the scene exceeds 1.0, there’s barely anything above the threshold to extract, and on the bright pixels themselves scene + intensity·bloom just clamps back to 1.0 (no visible change). Cranking intensity multiplies a near-empty signal; lowering the threshold just blooms the whole scene uniformly. Either way the glow stays weak.
The fix is head emission: the glyph pass multiplies the head by bloom.emission (default 2) so it writes above 1.0 into the HDR target — burning the head hotter than the display can show. Now the extract has real headroom (emission − threshold instead of 1 − threshold), so blooming the heads produces a strong halo, and threshold/intensity actually bite.
A side effect: pushing all channels past 1.0 means the head core clamps toward white while the green survives in the surrounding glow (the green channel dominates after the blur) — the classic white-hot head + green halo. Lower emission toward 1 to keep greener cores; emission = 1 disables it (heads stay in [0,1]). Emission only applies while bloom is enabled.
Why HDR + half-res
Section titled “Why HDR + half-res”- HDR (
rgba16float) throughout so the extracted highlights and thescene + intensity·bloomsum aren’t clipped before the final tone-map. - Half-res for extract + blur because the glow is smooth; you can’t see the resolution drop, and it quarters the work.
When bloom={false}, the whole extract → blur → blur → combine chain is skipped and the scene goes straight to the final pass — a real GPU saving, which is why it’s a boolean gate rather than intensity: 0.