Filmic Tonemapping Operators

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.

comments powered by Disqus