ACES Filmic Tone Mapping Curve

Careful mapping of HDR values to LDR is an important part of a modern game rendering pipeline. One of the goals of our new renderer was to replace Reinhard‘s tone mapping curve with some kind of a filmic tone mapping curve. We tried one from Ucharted 2 and tried rolling our own, but weren’t happy with either of this solutions. Finally, we settled on the one from ACES, which is currently a default tone mapping curve in Unreal Engine 4.

ACES color encoding system was designed for seamless working with color images regardless of input or output color space. It also features a carefully crafted filmic curve for displaying HDR images on LDR output devices. Full ACES integration is a bit of overkill for games, but we can just sample ODT( RRT( x ) ) transform and fit a simple curve to this data. We don’t even need to run any ACES code at all, as ACES provides reference images for all transforms. Although there is no linear RGB D65 ODT transform, but we can just use REC709 D65 and remove 2.4 gamma from it.

Curve was manually fitted (max fit error: 0.0138) to be more precise in the blacks – after all we will be applying some kind gamma afterwards. Additionally, data was pre-exposed, so 1 on input maps to ~0.8 on output and resulting image’s brightness is more consistent with the one without any tone mapping curve at all. For the original ACES curve just multiply input (x) by 0.6.

Fitted curve’s HLSL source code:

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));

Fitted curve plotted against source data’s sample points:


UPDATE: This is a very simple luminance only fit, which over saturates brights. This was actually something consistent with our art direction, but for a more realistic rendering you may want a more complex fit like this one from Stephen Hill.

This entry was posted in Graphics, Lighting. Bookmark the permalink.

17 Responses to ACES Filmic Tone Mapping Curve

  1. opioidwp says:

    Should this be used like the uncharted function (U()) like this: tonemapped = U(color) / U(White); or simply like tonemapped = KN(color); and done?


  2. ProtonFactor says:

    Hey, I’m interested in running the ACES code to see how close your curve matches for myself (I like to verify data and modify it sometimes). But at the RTT function in the source code it says the colors should be in aces color space as input… Should I be using linear rgb as input for RTT? or is there a transform from linear to aces color space?


    • Yes, basically you can just use linear space RGB as input (ACES is RGB linear color space with a D60 white point). Keep in mind that my curve is shifted (pre-exposed), so it won’t match a by-the-book ACES curve.


      • ProtonFactor says:

        Ah okay, thanks for the clarification. Duly noted about the shift, I’ll account for that. And also thanks for the post, it works really well by the way. My team would just like to be able to modify the curve without straying too far from the “ground truth” and hence we have to test it against the code. Anyway, thanks again.


  3. Pingback: HDR Display – First Steps | Krzysztof Narkowicz

  4. Pingback: Image dynamic range | Bart Wronski

  5. Pingback: KlayGE 4.10中渲染的改进(二):Tone mapping - KlayGE游戏引擎

  6. Pingback: HDR Rendering With WebGL -

  7. jj99 says:

    I’ve tried the Stephen Hill’s code, but it gave me quite different result (more saturated). Then I tried the ACES code in UE and it matches more closely your fitting. The UE code of course is even more complicated and not very suitable for use without baking to LUT.


    • It should be visible in very bright pixels – in Stephen Hill’s fit they tend to loose their saturation. We are currently doing a more realistic game and I had to refit ACES, so it doesn’t saturate so much in those very bright pixels.


      • David Clamage says:

        Can you clarify if the input and output to your function are in linear or sRGB space? I noticed that Stephen Hill’s fit that you linked comments that there’s an sRGB->Linear and then Linear->sRGB conversion. Is there any function that goes linear->linear? I’d prefer to use the hardware sRGB conversion if possible.


        • Both in my fit and in linked snippet (Hill’s fit) inputs are linear RGB (BT709 primaries with a linear transfer function). Yes, mentioned comments refer to sRGB, but having sRGB input/ouput makes no sense on modern hardware and linked sample uses this fit as linear->AcesFitted()->linear. Clearly there is no linear->sRGB on input there and there is linear->sRGB on output (“output = LinearTosRGB(ACESFitted(color) * 1.8f);”


          • Stephen Hill says:

            To further clarify: “sRGB” in my code comments refers to the sRGB (or Rec709) colour gamut, not the display transform (which isn’t a linear transform, so can’t possibly be represented by a 3×3 matrix!). The matrices are actually a concatenation of several separate transformations. For instance:

            // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT

            Here we start by transforming RGB (in an assumed sRGB/Rec709 colour gamut) to CIE XYZ. Then a D65 to D60 chromatic adaptation transform is applied, since sRGB/Rec709 has a D65 white point, while ACES uses D60. Then we transform from XYZ back to RGB, but with a wider AP1 (ACEScg) gamut. Finally, the ACES RRT’s (slight) desaturation transform is applied.


  8. Pingback: Games Look Bad, Part 1: HDR and Tone Mapping | Promit's Ventspace

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s