HDR Bloom Threshold and Knee Curve Formulation in GLSL
This article provides an overview and technical breakdown of the thresholding and soft knee curve formulation used in High Dynamic Range (HDR) bloom extraction passes. In real-time rendering, extracting highlights using a naive step function causes harsh boundaries and severe temporal flickering on specular edges. By implementing a quadratic soft knee curve in GLSL, shaders can smoothly transition pixels into the bloom buffer, maintaining visual stability and energy balance across varying exposure levels.
The Purpose of the Knee Curve
In HDR post-processing, bloom simulates the light scattering within the human eye or camera lens caused by intense light sources. A bright-pass filter isolates regions of the screen exceeding a chosen luminance threshold.
A hard threshold discards all values below a cutoff \(T\):
\[\text{Output} = \max(0.0, L - T)\]
This abrupt cutoff creates edge artifacts and temporal instability when specular highlights oscillate across the threshold boundary. A soft knee introduces a quadratic curve around the threshold, parameterized by a knee width \(K\), to smoothly blend between zero contribution and the fully linear bright region.
Mathematical Formulation
The standard soft knee formulation—widely adopted from implementations by Epic Games and Activision—segments luminance extraction into three distinct response zones based on pixel luminance \(L\):
- Below the Knee (\(L \le T - K\)): The contribution is zero.
- Inside the Knee (\(T - K < L \le T + K\)): A quadratic ease-in curve smoothly ramps up brightness.
- Above the Knee (\(L > T + K\)): The response becomes linear (\(L - T\)).
The quadratic function within the knee is defined as:
\[\text{Soft}(L) = \frac{(L - T + K)^2}{4K}\]
To avoid branching and preserve color ratios without tint shifts, the response factor is computed as a scalar multiplier against the original RGB color vector:
\[\text{Weight} = \frac{\max(\text{Soft}(L), L - T)}{\max(L, \epsilon)}\]
\[\text{Final Color} = \text{Color} \times \text{Weight}\]
Where \(\epsilon\) is a small non-zero value to prevent division by zero.
GLSL Implementation
The following GLSL snippet demonstrates how to precalculate curve parameters on the CPU and perform branchless bloom extraction in the fragment shader.
Uniform Setup
To optimize shader performance, precompute the knee vector
vec4(T, T - K, 2.0 * K, 0.25 / max(K, 1e-5)) and pass it as
a uniform:
// Precomputed CPU-side parameters:
// curve.x = threshold
// curve.y = threshold - knee
// curve.z = 2.0 * knee
// curve.w = 0.25 / max(knee, 1e-5)
uniform vec4 u_FilterParameters; Fragment Shader
#version 330 core
in vec2 v_TexCoord;
out vec4 FragColor;
uniform sampler2D u_SceneColor;
uniform vec4 u_FilterParameters;
// Standard Rec. 709 luminance weights
float getLuminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}
void main() {
vec3 color = texture(u_SceneColor, v_TexCoord).rgb;
// Pixel luminance
float luma = getLuminance(color);
// Unpack precomputed parameters
float threshold = u_FilterParameters.x;
float knee = u_FilterParameters.z * 0.5; // Reconstructed K
// Branchless soft-knee calculation
float rq = clamp(luma - u_FilterParameters.y, 0.0, u_FilterParameters.z);
rq = u_FilterParameters.w * rq * rq;
// Take the maximum between the soft knee and the linear threshold
float bloomLuma = max(rq, luma - threshold);
// Compute normalization factor to preserve original color hues
float factor = bloomLuma / max(luma, 1e-5);
FragColor = vec4(color * factor, 1.0);
}Considerations for Implementation
- Luminance Metrics: Using Rec. 709 or Rec. 2020 luma coefficients ensures extraction responds accurately to perceived human brightness. Alternatively, taking \(\max(R, G, B)\) can better preserve intense, highly saturated primary colors that have low perceptual luma weights.
- Knee Clamping: The knee parameter \(K\) should be clamped between \(0.0\) and \(T\). Setting \(K = 0.0\) degenerates the formulation into a standard hard threshold, while \(K = T\) extends the smoothing curve all the way down to complete black.