HDR Bloom Extraction with Thresholds in GLSL
Bloom extraction isolates the brightest regions of a high-dynamic-range (HDR) scene to generate a luminous glow effect during post-processing. In OpenGL Shading Language (GLSL), this is achieved by reading an HDR render target, calculating the perceived luminance of each pixel, and applying a mathematical threshold function to filter out low-energy light. This guide covers how luminance calculations, hard thresholds, and soft knee functions operate within an efficient GLSL extraction pass.
The HDR Render Target Requirement
A standard low-dynamic-range (LDR) buffer clamps color values between
0.0 and 1.0, making it impossible to
distinguish between a standard white surface and an intense light
source. HDR render targets—typically formatted as GL_RGB16F
or GL_RGBA16F—preserve color values exceeding
1.0. Bloom extraction analyzes these uncapped
floating-point values to ensure that only true light emitters and
intense specular highlights bleed into adjacent pixels.
Calculating Perceived Luminance
Before applying a threshold, the incoming RGB color must be mapped to a scalar brightness value. Human vision is most sensitive to green light and least sensitive to blue light, so standard linear weighting coefficients (defined by the Rec. 709 standard) are used:
float getLuminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}Hard Threshold vs. Soft Knee
1. Hard Threshold
A hard threshold completely discards any pixel whose luminance falls below a defined cutoff value (\(T\)):
vec3 extractHardThreshold(vec3 color, float threshold) {
float lum = getLuminance(color);
return lum > threshold ? color : vec3(0.0);
}While computationally cheap, hard thresholds introduce sharp, unnatural edges and severe temporal aliasing (specular flickering or crawling pixels) when objects move across the threshold boundary.
2. Soft Knee Threshold (Smoothstep / Quadratic Curve)
To eliminate harsh boundaries, modern bloom pipelines use a soft knee curve. The soft knee introduces an transition zone \([T - k, T + k]\), where \(k\) is the knee softness factor. Luminance values below the knee are set to zero, values within the knee are scaled smoothly, and values above the transition proceed linearly.
The standard formulation calculates a quadratic contribution factor:
vec3 extractSoftThreshold(vec3 color, float threshold, float knee) {
float lum = getLuminance(color);
// Calculate the softness curve bounds
float soft = lum - threshold + knee;
soft = clamp(soft, 0.0, 2.0 * knee);
soft = (soft * soft) / (4.0 * knee + 0.00001);
// Scale the output
float contribution = max(soft, lum - threshold) / max(lum, 0.00001);
return color * contribution;
}Complete GLSL Fragment Shader Implementation
Below is a complete extraction fragment shader that samples an HDR scene texture and outputs the extracted glow colors to an offscreen bloom buffer (often at half or quarter resolution):
#version 330 core
in vec2 TexCoords;
out vec4 FragColor;
uniform sampler2D hdrBuffer;
// Extraction uniforms
uniform float uThreshold = 1.0; // Point where bloom starts
uniform float uSoftness = 0.5; // Controls the width of the transition knee
float getLuminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}
void main() {
vec3 color = texture(hdrBuffer, TexCoords).rgb;
float lum = getLuminance(color);
// Compute knee parameters
float knee = uThreshold * uSoftness;
float soft = lum - (uThreshold - knee);
soft = clamp(soft, 0.0, 2.0 * knee);
soft = (soft * soft) / (4.0 * knee + 1e-5);
// Calculate dynamic scaling factor
float weight = max(soft, lum - uThreshold);
weight = max(weight, 0.0) / max(lum, 1e-5);
FragColor = vec4(color * weight, 1.0);
}Downsampling and Anti-Flicker Filtering
Directly sampling a single pixel during extraction can lead to lost energy on thin, high-intensity highlights. For best results:
- 13-Tap Downsample Filter: Combine the extraction pass with an initial downsample stage using a dual-grid 13-tap filter (sampling 4 corner pixels, 4 edge pixels, and 5 center-weighted pixels) to maintain spatial stability.
- Firefly Suppression (Karis Average): Very bright sub-pixel specular highlights can produce single-pixel artifacts called fireflies. When downsampling during extraction, weighting samples by their inverse luminance (\(1 / (1 + \text{lum})\)) limits extreme outliers before Gaussian or dual-filtering blurs are applied.