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.

41 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 [...]


  26. [...] Copy the HDR framebuffer to a regular framebuffer using the tone mapping operator from Uncharted 2. [...]


  27. [...] methods. Currently only Reinhard equations 3 and 4 are supported. I took the image from here: http://filmicgames.com/archives/75 I hope this is ok – if not let me know. The source it here : [...]


  28. Inexpensive Snapback Hats For Sale One particularamong the most commonsignsamong the most common For allall of them areall arethey all areeach of them isall of them aremost of them are When looking forare you looking forare you searching forare you looking fordo you needtrying to findsearching for caps that are stylish and inall over theall around thethroughout theeverywhere in theeverywhere over theonon thein youruponall over the manner, snapback caps are viewedlooked overlooked overchecked outexaminedgradedchecked being the mostby far the mostprobably the mostby far the mostquite possibly the mostjust about the mostessentially the mostthe foremostthe best popular variety.snapbacksbrand2013comment Everybody isis the fact thatis always thatis the factwould be the factis the fact thatis that oftenis the reason thattends to be thatis usually thatis becauseis thathas to be thatwill be theusuallymay be thecould be theis alwaysis this oftenis this sporting thesea few of thesesome of thesemany of thesemost of thesesomea few of theseexamples of these kinds ofhaving to do withrelating toregardingconcerningto do withabouthaving to do with motorcycle helmet. Sportsmen, rappers, performers, staff fans and everybodyevery man and womanevery man and woman that willnot only can theynot only can they keepdraw attention away fromdraw attention away from throughoutall the way throughall the way throughcompletelyall throughthroughright throughduringfrom top to bottomfrom start to finishall over track withso that you haveallowing you to haveso that you haveallowing an individualto haveproviding some oneleaving their fashion offershas to offer youhas to offerhas to offer you onea minumum of oneno less than oneone or morea minimum of onesome form ofa minumum of onemore then onean oror at leastor at bestor otherwiseat leastor perhapswell or at leastor evenand morea good deal morea good deal morea lot morea great deal moresignificantly moremuch more snapback loath.


  29. Hi! I use Reinhard tone mapping. I calculate an average luminance and max luminance in screen space.
    Based on those parameters I get an eye adaptation, for example when a camera is moved from a bright
    outdoor area to the dark room. How an eye adaptation process is achieved in Uncharted 2 tone mapping ?
    In my engine I render results of the shading to the floating point texture. I assume that I don’t need
    hardcoded exposure adjustment (texColor *= 16) and exposure bias = 2.0f. Or maybe I should interpret
    those parameters based on the average luminance ? What about “w” parameter ? Can I assign to “w” a maximum
    luminance from the screen space ?


  30. Gamma correction should be per channel, since that’s a monitor correction. It seems like doing tone mapping per-channel causes uncontrolled hue shifting – this can be seen a bit in the sunlit part of the floor. Have you tried doing what Reinhard appears to give, to compute the luminance (grayscale) itself and use that to determine the scaling factor for the color as a whole? My own limited experience with NPR effects such as posterization is that using the luminance avoids weird shifts, but that’s an extreme case. Maybe it’s just not that noticeable for your work.


  31. Six, the official offer document is necessary. Three or five minutes to considered a very good value, it is easy to print the normal format of quotations. You can get a range of formats you’ll be able to decide or build essentially the most suitable quotation.
    aire max france http://www.denisclavreul.com/nike-basketball.asp?id=aire-max-france


  32. Hi, we had integrated the filmic curve to the our game (previous was exponential curve with pseudo hdr intensities) but we have a problems with range and light intensity choosing. There is too many parameters for tune. Is exist some rule or sequence to retune lights and filmic parameters? Can you help with start point of this process?


  33. Wow some of the information of this post is without a doubt news to me.we appreciate updating me.
    Beats By Dre http://drebeats-singapore.peapit.com/


  34. Pretty writing, got us what I needed – thanks!
    Buy Christian Louboutin Shoes http://christianlouboutin-shoes.pfbert.com/


  35. Important information in my situation. Thanks for really competent informative article. Dangerous be in touch together with U
    Beats Dre http://drebeats-australia.cdsulu.com/


  36. I just put into favorites this article. Nice info.
    Cheap Dr Dre Beats http://drdrebeats.turnpnt.com/


  37. [...] is done (gamma= 2.0 instead of 2.2, without adaptation) with 3 post processing filters (flimic tone mapping, bloom and photo filter). In the test scene, 3 directional lights(1 of them cast shadow with 4 [...]


  38. [...] also implemented the filmic tone-mapping described here. I think it does look a little better than Reinhard (more “punchy”). There is a lot [...]


  39. Hello there! This is my first visit to your blog! We are a collection of
    volunteers and starting a new project in a community in the same
    niche. Your blog provided us valuable information
    to work on. You have done a outstanding job!


  40. Hi there! I knnow this is kinda off topic however , I’d figtured I’d ask.
    Would you bee interested in trading links or maybe guest authoring a
    blog article or vice-versa? My website goes ober
    a lot of the same topics as yours and I believ we
    could greatly benefit from each other. If you hazppen to be interested feel free
    to send me an email. I look forwar to hearing from
    you! Awesome blog by the way!

    Feel free to visit my web-site :: 8 ball pool cheat


  41. Hurrah, that’s what I was exploring for, what a stuff!
    existing here at this weblog, thanks admin of thijs website.

    My page InWatt (Maryanne)


Leave a Reply