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.