Uncharted 2 Filmic Tone Mapping Operator in GLSL
This article details the mathematical formulation and GLSL shader implementation of the Uncharted 2 filmic tone mapping operator, originally designed by John Hable. It breaks down the rational polynomial function that defines the curve, details the role of each constant parameter, and provides a direct, production-ready GLSL implementation for high dynamic range (HDR) rendering pipelines.
The Mathematical Formulation
The Uncharted 2 filmic curve is defined by a rational polynomial function that models the toe, linear section, and shoulder of traditional film stock. The primary evaluation function \(f(x)\) is defined as:
\[f(x) = \frac{x(Ax + CB) + DE}{x(Ax + B) + DF} - \frac{E}{F}\]
The subtraction of \(\frac{E}{F}\) at the end ensures that the toe passes through the origin, meaning a value of zero maps strictly to zero black levels.
Curve Constants
The operator uses six core coefficients alongside a linear white point scale:
- \(A\) (Shoulder Strength): \(0.15\) (controls the rate at which highlights compress)
- \(B\) (Linear Strength): \(0.50\) (determines the slope of the midtones)
- \(C\) (Linear Angle): \(0.10\) (shapes the transition into the shoulder)
- \(D\) (Toe Strength): \(0.20\) (determines the sharpness of the shadow curve)
- \(E\) (Toe Numerator): \(0.02\) (fine-tunes shadow detail)
- \(F\) (Toe Denominator): \(0.30\) (works with \(E\) to set the black floor)
- \(W\) (Linear White Point): \(11.2\) (the HDR input luminance value that maps to full display white)
Normalization
Because \(f(x)\) alone does not cap the maximum output at \(1.0\), the final output must be scaled by dividing the color curve by the curve evaluated at the target white point \(W\):
\[\text{Color}_{\text{out}} = \frac{f(\text{Exposure} \times \text{Color}_{\text{in}})}{f(W)}\]
GLSL Implementation
Below is the standard implementation of the Uncharted 2 tone mapping operator written in GLSL:
// Core polynomial evaluation
vec3 Uncharted2Tonemap(vec3 x) {
const float A = 0.15; // Shoulder Strength
const float B = 0.50; // Linear Strength
const float C = 0.10; // Linear Angle
const float D = 0.20; // Toe Strength
const float E = 0.02; // Toe Numerator
const float F = 0.30; // Toe Denominator
return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - (E / F);
}
// Complete tone mapping pipeline including exposure and white point normalization
vec3 TonemapUncharted2(vec3 color, float exposureBias) {
const float W = 11.2; // Linear White Point
// Apply exposure bias to HDR input
vec3 curr = Uncharted2Tonemap(color * exposureBias);
// Compute white point scale factor
vec3 whiteScale = 1.0 / Uncharted2Tonemap(vec3(W));
// Scale color to normalize the white point to 1.0
vec3 mappedColor = curr * whiteScale;
// Convert from linear space to sRGB display space (approximate gamma 2.2)
return pow(mappedColor, vec3(1.0 / 2.2));
}The operator expects input color values in linear color space. The final gamma correction ensures the resulting values are correctly mapped to non-linear display standards.