20180122 - Over Exposure Color Shaping


Simple example shadertoy: https://www.shadertoy.com/view/XljBRK


Color Shaping
In Advanced Techniques and Optimization of HDR Color Pipelines (slides on https://gpuopen.com/gdc16-wrapup-presentations/) there is a generalized tonemapper which has the following logic for over-exposure,

// improved crosstalk  maintaining saturation
float tonemappedMaximum; // max(color.r, color.g, color.b)
float3 ratio; // color / tonemappedMaximum
float crosstalk; // controls amount of channel crosstalk
float saturation; // full tonal range saturation control
float crossSaturation; // crosstalk saturation

// wrap crosstalk in transform
ratio = pow(ratio, saturation / crossSaturation);
ratio = lerp(ratio, white, pow(tonemappedMaximum, crosstalk));
ratio = pow(ratio, crossSaturation);

// final color
color = ratio * tonemappedMaximum;

As is this has a disadvantage if source input is lit by pure primaries such as only RED. The RED will quickly desaturate as it over-exposes to white (as there isn't any GREEN or BLUE to choose a path for saturation). In the "real world" one would expect lighting to not be narrow band, and thus this problem wouldn't happen (aka fix by adjusting content). This is arguably less than ideal. And the real art in tonemapping is how color adjusts as it over-exposes.

It is possible to expand {crosstalk, saturation, and crossSaturation} into separate settings for RGB (this is not adding extra cost, simply adjusting some multiply terms). This allows better shaping of color. Specifically how colors adjust in hue as they over-expose.

The best way to understand how it works is to try it. Shadertoy at the time of this posting has a yellow bias on over-exposure. So pure RED transforms towards to ORANGE then YELLOW on it's route to WHTIE. The shadertoy example isn't of a real-world scene so I'd expect in practice different settings would be used.

Shadertoy Source Copy
In case shadertoy ever dies, dumped the shadertoy source right here as well,

// Tonemapper settings
#define CONTRAST 1.4
#define SHOULDER 1.0
#define HDR_MAX 64.0
#define MID_IN 0.18
#define MID_OUT 0.18
#define SATURATION vec3(0.0,0.0,0.0)
#define CROSSTALK vec3(64.0,32.0,128.0)
#define CROSSTALK_SATURATION vec3(4.0,1.0,16.0)
//--------------------------------------------------------------
#define GT_GPU 1
#define GT_GLSL 1
#define GT_SHOULDER 1

//_____________________________/\_______________________________
//==============================================================
//
//
//           [GT] GENERALIZED TONEMAPPER - 20180122
//
//                      by Timothy Lottes
//
//
//--------------------------------------------------------------
// Showing full flexibility of over-exposure color shaping
// Via 3 channel 'crosstalk' and 'crosstalk saturation' terms
//--------------------------------------------------------------
// Based on AMD GDC Presentation:
//  Advanced Techniques and Optimization of HDR Color Pipelines
//  https://gpuopen.com/gdc16-wrapup-presentations/
//--------------------------------------------------------------
// Using math fixup from Bart Wronski (see comments),
//  https://bartwronski.com/2016/09/01/dynamic-range-and-evs/
//--------------------------------------------------------------
// DEFINES
// =======
// GT_CPU - Define to get CPU code (not implemented)
// GT_GPU - Define to get GPU code
// GT_SHOULDER - Define if shoulder is not set to 1.0
// GT_GLSL - Define for OpenGL/Vulkan
// GT_HLSL - Define for DX (not tested)
//==============================================================
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
//_____________________________/\_______________________________
//==============================================================
//
//                          CPU CODE
//
//==============================================================
#ifdef GT_CPU
 // TODO!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#endif
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
//_____________________________/\_______________________________
//==============================================================
//
//                          GPU CODE
//
//==============================================================
#ifdef GT_GPU
//_____________________________/\_______________________________
//==============================================================
//                         PORTABILITY
//==============================================================
 #ifdef GT_GLSL
  #define GtF1 float
  #define GtF2 vec2
  #define GtF3 vec3
  #define GtF4 vec4
  #define GtFractF1 fract
  #define GtLerpF3(x,y,a) mix(x,y,a)
  #define GtRcpF1(x) (1.0/(x))
  #define GtSatF1(x) clamp((x),0.0,1.0)
//--------------------------------------------------------------
  GtF1 GtMax3F1(GtF1 a,GtF1 b,GtF1 c){return max(a,max(b,c));}
 #endif
//==============================================================
 #ifdef GT_HLSL
  #define GtF1 float
  #define GtF2 float2
  #define GtF3 float3
  #define GtF4 float4
  #define GtFractF1 frac
  #define GtLerpF3(x,y,a) lerp(x,y,a)
  #define GtRcpF1(x) (1.0/(x))
  #define GtSatF1(x) saturate(x)
//--------------------------------------------------------------
  GtF1 GtMax3F1(GtF1 a,GtF1 b,GtF1 c){return max(a,max(b,c));}
 #endif
//_____________________________/\_______________________________
//==============================================================
//                     CONSTANT GENERATION
//--------------------------------------------------------------
// GPU version of math to generate constants for tonemapper
// Better to generate constants into a constant buffer first
//==============================================================
 void GtConstants(
  out GtF4 tone0,
  out GtF4 tone1,
  out GtF4 tone2,
  out GtF4 tone3,
  GtF1 contrast,
  GtF1 shoulder,
  GtF1 hdrMax,
  GtF1 midIn,
  GtF1 midOut,
  GtF3 saturation,
  GtF3 crosstalk,
  GtF3 crosstalkSaturation
 ){
  tone0.x=contrast;
  tone0.y=shoulder;
  GtF1 cs=contrast*shoulder;
//--------------------------------------------------------------
  // TODO: Better factor and clean this up!!!!!!!!!!!!!!!!!!!!!!
  GtF1 z0=-pow(midIn,contrast);
  GtF1 z1=pow(hdrMax,cs)*pow(midIn,contrast);
  GtF1 z2=pow(hdrMax,contrast)*pow(midIn,cs)*midOut;
  GtF1 z3=pow(hdrMax,cs)*midOut;
  GtF1 z4=pow(midIn,cs)*midOut;
  tone0.z=-((z0+(midOut*(z1-z2))/(z3-z4))/z4);
//--------------------------------------------------------------
  GtF1 w0=pow(hdrMax,cs)*pow(midIn,contrast);
  GtF1 w1=pow(hdrMax,contrast)*pow(midIn,cs)*midOut;
  GtF1 w2=pow(hdrMax,cs)*midOut;
  GtF1 w3=pow(midIn,cs)*midOut;
  tone0.w=(w0-w1)/(w2-w3);
//--------------------------------------------------------------
  // Saturation base is contrast
  saturation+=contrast;
  tone1.xyz=saturation/crosstalkSaturation;
  tone2.xyz=crosstalk;
  tone3.xyz=crosstalkSaturation;}
//_____________________________/\_______________________________
//==============================================================
//                      APPLY TONEMAPPER
//--------------------------------------------------------------
// Note 'pow(x,y)' is implimented as 'exp2(log2(x)*y)'
//==============================================================
 GtF3 GtFilter(
  // Linear input color
  GtF3 color,
  // Tonemapper constants
  GtF4 tone0,
  GtF4 tone1,
  GtF4 tone2,
  GtF4 tone3
 ){
//--------------------------------------------------------------
  // Peak of all channels
  GtF1 peak=GtMax3F1(color.r,color.g,color.b);
  // Protect against /0
  peak=max(peak,1.0/(256.0*65536.0));
  // Color ratio
  GtF3 ratio=color*GtRcpF1(peak);
//--------------------------------------------------------------
  // Apply tonemapper to peak
  // Contrast adjustment
  peak=pow(peak,tone0.x);
//--------------------------------------------------------------
  // Highlight compression
  #ifdef GT_SHOULDER
   peak=peak/(pow(peak,tone0.y)*tone0.z+tone0.w);
  #else
   // No shoulder adjustment avoids extra pow
   peak=peak/(peak*tone0.z+tone0.w);
  #endif
//--------------------------------------------------------------
  // Convert to non-linear space and saturate
  // Saturation is folded into first transform
  ratio=pow(ratio,tone1.xyz);
  // Move towards white on overexposure
  vec3 white=vec3(1.0,1.0,1.0);     
  ratio=GtLerpF3(ratio,white,pow(GtF3(peak),tone2.xyz));
  // Convert back to linear
  ratio=pow(ratio,tone3.xyz);
//--------------------------------------------------------------
   return ratio*peak;}
//==============================================================
#endif





//_____________________________/\_______________________________
//==============================================================
//
//                 SIMPLE MESSY-CODE TEST SCENE
//
//==============================================================
#define F1 float
#define F2 vec2
#define F3 vec3
#define F4 vec4
//--------------------------------------------------------------
F1 Linear1(F1 c){return(c<=0.04045)?c/12.92:pow((c+0.055)/1.055,2.4);}
F3 Linear3(F3 c){return F3(Linear1(c.r),Linear1(c.g),Linear1(c.b));}
F1 Srgb1(F1 c){return(c<0.0031308?c*12.92:1.055*pow(c,0.41666)-0.055);}
F3 Srgb3(F3 c){return F3(Srgb1(c.r),Srgb1(c.g),Srgb1(c.b));}
F1 Noise(F2 n,F1 x){n+=fract(iTime)*x;return fract(sin(dot(n.xy,F2(12.9898, 78.233)))*43758.5453);}
F1 Dither(F2 n){return (Noise(n,0.07)+Noise(n,0.11)+Noise(n,0.13))/3.0;}
//--------------------------------------------------------------
F3 Hue(F1 n){return clamp(F3(
 abs(fract(n)-0.5)*(-6.0)+2.0,
 abs(fract(n+(1.0/3.0))-0.5)*(-6.0)+2.0,
 abs(fract(n-(1.0/3.0))-0.5)*(-6.0)+2.0),F3(0.0),F3(1.0));}
//==============================================================
void mainImage(out vec4 fragColor,in vec2 fragCoord){
 F2 uv=fragCoord.xy/iResolution.xy;
 uv.y=1.0-uv.y;
 F3 color=Linear3(texture(iChannel0,uv*3.0).rgb);
 color*=(1.0/8.0);
//--------------------------------------------------------------
 #if 1
  // Add colored gradients
  F3 bars=Hue(uv.y/(5.0/8.0))*2.0;   
  color+=bars*bars*(uv.x*uv.x*uv.x*uv.x*uv.x);
 #endif
//--------------------------------------------------------------
 #if 1      
  // Draw swatches
  if(uv.y>7.0/8.0){
   color.b=fract(uv.x*8.0);
   color.r=1.0-fract(uv.y*8.0);
   color.g=floor(uv.x*8.0)/8.0;
   color.rgb=Linear3(color);}
  else if(uv.y>6.0/8.0){
   color.r=fract(uv.x*8.0);
   color.g=1.0-fract(uv.y*8.0);
   color.b=floor(uv.x*8.0)/8.0;
   color.rgb=Linear3(color);}
  else if(uv.y>5.0/8.0){
   color.g=fract(uv.x*8.0);
   color.b=1.0-fract(uv.y*8.0);
   color.r=floor(uv.x*8.0)/8.0;
   color.rgb=Linear3(color);}
 #endif   
//--------------------------------------------------------------
 color*=16.0;
//--------------------------------------------------------------
 #if 1
  // Fade in/out
  color.rgb*=pow(abs(sin(fract(iTime/16.0)*3.14159*2.0)),2.2);    
 #endif
//--------------------------------------------------------------
 // Apply generalized tonemapper
 // In practice GtConstants() would be run on CPU,
 //  or if run on GPU, 
 //  run on one wave and store into constant buffer in prepass
 GtF4 tone0;
 GtF4 tone1;
 GtF4 tone2;
 GtF4 tone3;
 GtConstants(
  // Outputs
  tone0,tone1,tone2,tone3,
  // Inputs
  CONTRAST,SHOULDER,HDR_MAX,MID_IN,MID_OUT,
  SATURATION,CROSSTALK,CROSSTALK_SATURATION);
 color=GtFilter(color,tone0,tone1,tone2,tone3);
//--------------------------------------------------------------
 // Convert back into sRGB.
 color=Srgb3(color);
 color+=(Dither(uv)-0.5)*(16.0/256.0);    
 fragColor=F4(color,1.0);}