Filmic Tonemapping Operators


The most common questions that I get about my GDC talk have to do with the tonemapping operators. In particular, I’ve always found that when I read through presentations for code snippets that I always miss something. Those 2.2s can be tricky! So this post is a quick reference for various operators that I talked about. Also, I copied and pasted this code from my RenderMonkey scene so there may be typos.

All of these examples use this HDR image of Habib’s killer condo. Also, click any image for the high-res version.

First off, there is good old linear. All it does is read the linear data, do an exposure adjustment, and adjust for the monitor’s gamma of 2.2.

float4 ps_main( float2 texCoord  : TEXCOORD0 ) : COLOR
{
   float3 texColor = tex2D(Texture0, texCoord );
   texColor *= 16;  // Hardcoded Exposure Adjustment
   float3 retColor = pow(texColor,1/2.2);
   return float4(retColor,1);
}

Pretty simple. It looks like this. Click for high-res.

Don’t forget the pow(1/2.2). If you forget that step, it looks like this:

float4 ps_main( float2 texCoord  : TEXCOORD0 ) : COLOR
{
   float3 texColor = tex2D(Texture0, texCoord );
   texColor *= 16;  // Hardcoded Exposure Adjustment
   float3 retColor = texColor;
   return float4(retColor,1);
}

Next up is Reinhard. There are many variations, bu I’ll do the simplest which is 1/(1+x). A common variation is to only do it on luminance. Don’t forget the pow(1/2.2) at the end!

float4 ps_main( float2 texCoord  : TEXCOORD0 ) : COLOR
{
   float3 texColor = tex2D(Texture0, texCoord );
   texColor *= 16;  // Hardcoded Exposure Adjustment
   texColor = texColor/(1+texColor);
   float3 retColor = pow(texColor,1/2.2);
   return float4(retColor,1);
}

Here it is with Haarm-Peter Duiker’s curve. This version is very similar to the Cineon node in Digital Fusion. The texture FilmLut refers to this TGA file. No pow(1/2.2) necessary.

float4 ps_main( float2 texCoord  : TEXCOORD0 ) : COLOR
{
   float3 texColor = tex2D(Texture0, texCoord );
   texColor *= 16;  // Hardcoded Exposure Adjustment

   float3 ld = 0.002;
   float linReference = 0.18;
   float logReference = 444;
   float logGamma = 0.45;

   float3 LogColor;
   LogColor.rgb = (log10(0.4*texColor.rgb/linReference)/ld*logGamma + logReference)/1023.f;
   LogColor.rgb = saturate(LogColor.rgb);

   float FilmLutWidth = 256;
   float Padding = .5/FilmLutWidth;

   //  apply response lookup and color grading for target display
   float3 retColor;
   retColor.r = tex2D(FilmLut, float2( lerp(Padding,1-Padding,LogColor.r), .5)).r;
   retColor.g = tex2D(FilmLut, float2( lerp(Padding,1-Padding,LogColor.g), .5)).r;
   retColor.b = tex2D(FilmLut, float2( lerp(Padding,1-Padding,LogColor.b), .5)).r;

   return float4(retColor,1);
}

Next up is the optimized formula by Jim Hejl and Richard Burgess-Dawson. I completely forgot about Richard in the GDC talk, but he shares the credit with Jim. Sorry Richard!! Note that you don’t need the pow(1/2.2) for this one either.

float4 ps_main( float2 texCoord  : TEXCOORD0 ) : COLOR
{
   float3 texColor = tex2D(Texture0, texCoord );
   texColor *= 16;  // Hardcoded Exposure Adjustment
   float3 x = max(0,texColor-0.004);
   float3 retColor = (x*(6.2*x+.5))/(x*(6.2*x+1.7)+0.06);
   return float4(retColor,1);
}

Finally for the Uncharted 2 operator made by yours-truly. For this image I changed the defaults slightly for A and B.

Edit: Oops, in the previous version, I had the exposure bias outside the tonemapping function. Now it is fixed, where it is inside the tonemapping function.


float A = 0.15;
float B = 0.50;
float C = 0.10;
float D = 0.20;
float E = 0.02;
float F = 0.30;
float W = 11.2;

float3 Uncharted2Tonemap(float3 x)
{
   return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}

float4 ps_main( float2 texCoord  : TEXCOORD0 ) : COLOR
{
   float3 texColor = tex2D(Texture0, texCoord );
   texColor *= 16;  // Hardcoded Exposure Adjustment

   float ExposureBias = 2.0f;
   float3 curr = Uncharted2Tonemap(ExposureBias*texColor);

   float3 whiteScale = 1.0f/Uncharted2Tonemap(W);
   float3 color = curr*whiteScale;

   float3 retColor = pow(color,1/2.2);
   return float4(retColor,1);
}

Hopefully, that should clear up most of the ambiguity about these operators.

25 Responses to “Filmic Tonemapping Operators”

  1. Thanks, it helps.
    I think there’s a type in second code snippet, it’s the same as the first one, line 6 should probably be removed and line 7 would read return float4(texColor, 1) then.


  2. Thanks. Fixed it.


  3. Thanks John, this clears up a lot of questions
    I had from the slides.


  4. I have one question though.
    Can you get Tonemapping and Gamma adjustmen on
    an engine framework without postprocessing?
    You can just render every object in your scene using these shader techniques right?


  5. Hi Dimi. Definitely. That was the original way it was done. You just do the tonemapping at the end of the shader.


  6. [...] codes: http://filmicgames.com/archives/75 Tags: Graphics, HDR, [...]


  7. It’s been a while since you posted, but I’ve been programming and graphing a lot of tonemapping operators.

    I can’t seem to figure out what the following line does for the curve:
    float3 x = max(0,texColor-0.004);

    I thought maybe it maybe it helped the curve in the blacks, but the graph of the algorithm seems almost the same without it. Also, taking screen shots of my results doesn’t seem to reveal anything different with or without this line. A factor of 0.004 isn’t much after all.


  8. @allingm: The 0.004 sets the value for the black point to give you a little more contrast in the bottom end. The graph will look very close, you will see a noticeable difference in the blacks in your images because the human eye has more precision in the darker areas.


  9. [...] check out how John’s operator [...]


  10. Excellent! I’ve been reading the PPT and didn’t know where exactly all the exposure compensations went. This all makes it clear, including the pow(col,1/2.2) at the end! (after all those pages on gamma ;-) )

    But what does the ‘ExposureBias’ do? Sounds like a magic constant.


  11. [...] I've gotten Uncharted2 tonemapping to work correctly, with a nice link: http://filmicgames.com/archives/75/comment-page-1 I'll see if I can detach the sampling and integration of the exposure. No attachments yet, since I [...]


  12. Hi Rudd,

    Exposure bias is in fact a magic constant. Basically, it just multiplies the whole image by a number before going into the film curve. It’s in there so that when you switch back and forth between linear and filmic then the overall intensity stays about the same.


  13. [...] 2的Filmic Tonemapping。虽然只要改2行代码,但Filmic [...]


  14. Sorry to say but those tone-mapping operators simply don’t work in a real world. They maybe can work for the specific image you used but not in a general way.

    The onlyY way to perform tone-mapping is using an histogram and setting manually the white point and, then, cutting the extremes and apply some non-linear function like a log or a polynomial.


  15. @fdsj: Umm, did you actually read the formulas? The input is implied to be at a reasonable exposure (hence the difference between “exposure correction” and “tonemapping”, which I discuss in a different post). Those formulas have a white point, a black point, and use either a log with a curve or a polynomial.


  16. [...] codes: http://filmicgames.com/archives/75 Share:FacebookEmailPrint Tagged as: HDR, Tone Mapping Leave a comment Comments (0) [...]


  17. John, why in next to last listing finished by linear out:

    float3 retColor = (x*(6.2*x+.5))/(x*(6.2*x+1.7)+0.06);
    return float4(retColor,1);

    and in final listing it finished by gamma-correction:

    float3 retColor = pow(color,1/2.2);
    return float4(retColor,1);

    when it is the same polynom function ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F ?


  18. Hi,

    I’m trying out the GLSL version of it. Unfortunately the image gets extremely saturated/exposed. Am I doing something wrong here:

    //From Filmic Tonemapping, Uncharted: http://filmicgames.com/archives/75

    uniform bool use_tex;
    uniform sampler2D tex;
    uniform bool use_tex_matrix;
    uniform mat4 tex_matrix;
    varying vec4 varying_color;
    varying vec2 varying_texcoord;

    float A = 0.15;
    float B = 0.50;
    float C = 0.10;
    float D = 0.20;
    float E = 0.02;
    float F = 0.30;
    float W = 11.2;
    float exposurebias = 2.0;
    float multiplier = 16.0;
    float power = 0.454;

    vec3 Tonemap(vec3 x)
    {
    return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
    }

    vec3 performTonemap(vec3 tmp)
    {
    vec3 tmpmap;
    vec3 cur;
    vec3 newcur;
    vec3 whitescale;
    tmpmap = multiplier * tmp;
    cur = Tonemap(exposurebias*tmpmap);
    whitescale = 1.0/Tonemap(vec3(W, W, W));
    newcur = cur*whitescale;
    return vec3(pow(newcur.r, power), pow(newcur.g, power), pow(newcur.b, power));
    }

    void main(){
    vec4 tmp=varying_color;
    vec4 coord = vec4(varying_texcoord, 0.0, 1.0);
    if (use_tex_matrix) {
    vec4 sample;
    coord = coord * tex_matrix;
    sample = texture2D(tex, coord.st);
    tmp *= sample;
    }
    else if (use_tex) {
    vec4 sample = texture2D(tex, coord.st);
    tmp *= sample;
    }
    //Everything till here works as a good passthrough pixel shader.
    gl_FragColor=vec4(performTonemap(vec3(tmp.r, tmp.g, tmp.b)), tmp.a);
    }


  19. Hi Guilt, having the same issues — did you figure out a solution?

    I noticed that point about “The input is implied to be at a reasonable exposure”, so instead of our typical “rendering colors between 0 and 1″ we might have to render to a higher range (-1000 .. 1000? who knows?) — but then, I thought that’s what the multiplier=16 would do for our LDR texture already.


  20. Make sure you do gamma correction the right way:

    - if texture are edited in Gimp/Photoshop are most probably in sRGB space, so you must convert in linear color space manually inside the shader or use GL_TEXTURE_SRGB version..

    - if you framebuffer is not sRGB, call glEnable(GL_FRAMEBUFFER_SRGB) (OpenGL 3+) or manually convert via pow func inside the shader.


  21. [...] banding when doing this color space transformation. The technique I’m using is explained here. Here are some examples of the same view using different tone mapping [...]


  22. Thanks for the explaination of tonemapping and putting up the codes :)
    I tried to apply it to Gta4

    [IMG]http://img37.imageshack.us/img37/7038/eflc2012072806470959.png[/IMG]


  23. Hi, everyone .
    I am trying to create the HDR rendering using stage3d of adobe flash.
    The problem I have got is flash only supports 8bits per channel Rgba texture format for frame buffer. Is there any way possible to do the tone mapping properly in this case?
    Thank you very much:)


  24. Hi John,

    This is a very good explanation of the tonemapping process, thanks for that.

    I have a question about the exposure adjustment. What is a good way to correct to exposure of an image, based on the average luminance? Is there any common algorithm?
    And do you limit the adjustment to allow very dark scenes (nighttime) or very bright scenes like snow without getting over or underexposed?

    @Jason I don’t know if you read this, but I suggest taking a look at the RGBE format, it stores a exponent value in the E component and still keeps everything at 8bit.


  25. [...] Habble has a series of three short articles on tone mapping: Filmic Tonemapping Operators, Why Reinhard Desaturates Your Blacks, and Why a Filmic Curve Saturates Your [...]