ACES Filmic Tone Mapping in DirectX Pipelines
This article explains how the Academy Color Encoding System (ACES) filmic tone mapping process functions within modern DirectX graphics pipelines. It explores the color-science principles behind ACES, breaks down the transformation pipeline from scene-referred high dynamic range (HDR) values to display-referred colors, and details the technical implementation within High-Level Shader Language (HLSL) under DirectX 11 and DirectX 12.
Understanding the ACES Tone Mapping Workflow
Tone mapping solves the problem of mapping high dynamic range scene data—where lighting values routinely exceed \(1.0\)—into the limited dynamic range of standard SDR displays (\([0, 1]\)) or target HDR specifications. Unlike simple Reinhard or standard clamping operators that cause color channel clipping and chromaticity shifts, ACES provides a photographic response curve (S-curve) characterized by:
- Toe: Controlled dark tones preserving shadow detail while suppressing noise.
- Midtones: Linear region preserving intended contrast and exposure.
- Shoulder: Smooth compression of highlights, desaturating bright areas naturally toward white (avoiding the "hue shift" or "ratty" yellow burn on overexposed skin and fire).
In the complete cinematic standard, ACES relies on a multi-stage process:
- Input Device Transform (IDT): Converts incoming camera or asset colors into ACES color spaces (typically AP0 or AP1).
- Reference Rendering Transform (RRT): Applies the non-linear, wide-gamut S-curve and scene-to-display colorimetric mapping.
- Output Device Transform (ODT): Converts the RRT data into target display spaces (such as Rec.709 for standard monitors or Rec.2020/PQ for HDR10).
Color Space Foundations in Real-Time Rendering
Most modern DirectX engines perform physical lighting computations in Linear sRGB or ACEScg color spaces. ACES defines two primary coordinate sets:
- AP0 (ACES2065-1): Ultra-wide color space designed for archiving and exchange, enclosing the entire visible spectrum.
- AP1 (ACEScc/ACEScg): Tighter wide-gamut space designed to align closer with practical display primaries, avoiding negative values and computational inefficiency during shading.
When shading in linear sRGB, the tone mapping stage must first translate linear sRGB primaries to the ACES AP1 space before evaluating the tone response curve, and subsequently convert the output back to the display profile.
Integration into the DirectX Pipeline
Tone mapping is typically executed as a late-stage post-processing pass using either a full-screen pixel shader or a compute shader.
1. Pipeline Placement and Render Targets
- Input Resource: The HDR scene color texture,
commonly allocated using floating-point formats such as
DXGI_FORMAT_R16G16B16A16_FLOATorDXGI_FORMAT_R11G11B10_FLOAT. This buffer holds the combined result of lighting, exposure compensation, atmospheric scattering, and bloom. - Execution Pass: Occurs after lens effects (such as bloom and depth of field) but before user interface (UI) rendering to ensure UI elements are not tone mapped.
- Output Resource: Writes to the backbuffer or
intermediate SDR/HDR buffer, typically formatted as
DXGI_FORMAT_R8G8B8A8_UNORM,DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, orDXGI_FORMAT_R10G10B10A2_UNORM.
2. Full RRT/ODT vs. Fitted Approximations
The official reference implementation of the ACES RRT and ODT is computationally heavy, involving multiple matrix conversions, 3D LUT lookups, and piecewise polynomial splines.
For real-time DirectX rendering, developers frequently employ analytical approximations that condense the RRT and ODT into a single mathematical step. The two most common curves used in HLSL are:
- Krzysztof Narkowicz's Fit: A fast, rational polynomial curve fitted directly to linear sRGB inputs.
- Stephen Hill’s Fit (ACES AP1): A two-matrix implementation that translates linear sRGB into ACEScg, computes the fitted curve, and maps back to sRGB.
HLSL Implementation Details
In DirectX, the tone mapper is expressed within HLSL. Below are the functional components of both common implementations.
Narkowicz Approximation
This fit evaluates the S-curve directly in the destination sRGB color space, trading off strict ACES color-space accuracy for maximum shader performance:
float3 ACESFilm(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
return saturate((x * (a * x + b)) / (x * (c * x + d) + e));
}
Stephen Hill Fit (Accurate RRT + ODT)
This implementation maps linear sRGB input to ACES AP1, evaluates the curve, and maps back to display sRGB:
// Converts sRGB linear to ACEScg (AP1)
static const float3x3 ACESInputMat = float3x3(
0.59719, 0.35458, 0.04823,
0.07600, 0.90834, 0.01566,
0.02840, 0.13383, 0.83777
);
// Converts ACEScg back to sRGB linear
static const float3x3 ACESOutputMat = float3x3(
1.60475, -0.53108, -0.07367,
-0.10208, 1.10813, -0.00605,
-0.00327, -0.07276, 1.07602
);
float3 RRTAndODTFit(float3 v)
{
float3 a = v * (v + 0.0245786f) - 0.000090537f;
float3 b = v * (0.983729f * v + 0.4329510f) + 0.238081f;
return a / b;
}
float3 ACESFitted(float3 color)
{
color = mul(ACESInputMat, color);
color = RRTAndODTFit(color);
color = mul(ACESOutputMat, color);
return saturate(color);
}
Display Encoding and Hardware Considerations
DirectX handles swapchain presentations differently depending on the display target:
- SDR Backbuffers
(
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709): The tone-mapped linear output must undergo display gamma encoding (either manual linear-to-sRGB power conversion in shader code or automatic conversion via an_SRGBrender target view). - HDR10 Backbuffers
(
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020): The ACES ODT step changes. The curve targets peak luminance values (e.g., 1000 or 4000 nits) within the Rec.2020 color gamut, followed by applying the SMPTE ST 2084 (PQ) transfer function rather than standard gamma. - Compute Dispatch vs. Rasterization: In modern
DirectX 12 renderers, tone mapping is frequently bundled with
post-processing steps (color grading, bloom composite, chromatic
aberration, dithering) into a single compute shader using shared memory
(
groupshared) to minimize memory bandwidth on round-trip render-target reads and writes.