Disclaimer: The following is not an approach I would suggest be used for producing good looking images, but merely an interesting and informative exercise to better understand the problem at hand.
Back to Basics
The last several days I’ve been thinking about the highlight desaturation problem. With display rendering there are a lot of intertwined problem areas, and it’s very easy (at least for me) to get confused. Like earlier when I was thinking about gamut compression and highlight desaturation as one problem. They are two different problems. Related, but not the same.
So how can we simplify and isolate these problem areas? We could ignore “out of display gamut” issues. We could ignore tonemapping or intensity scaling issues.
NaiveDisplayTransform
In an effort to simplify and focus on highlight compression and desaturation, I made a nuke node called NaiveDisplayTransform. As the name suggests, it undertakes a very naive approach to mapping scene-linear to display-linear.
- Maps an input range in scene-linear from
linear_start
tolinear_end
, defined in stops above and below middle gray, to an output range in display linear from 0 todisplay linear end
. - From
display linear end
to 1.0, apply a compression curve to the scene linear values. The compression curve compresses infinity to a value oflimit
stops abovelinear end
. In other words: if limit is 1, all highlight values fromlinear end
to infinity will be compressed into a range of 1 stop. The biggerlimit
is, the more range there will be for the compressed highlight values. - Where highlight values are compressed, desaturate.
The simplest possible display transform would be a linear to linear mapping. We take a range of scene-linear values and remap them to a range of display-linear values. We apply the inverse EOTF. The scene linear values are displayed directly on the display, up until the values clip. For the range that can be displayed, (ignoring spectral emission differences, calibration, and other complications) there should be a 1 to 1 correspondence between scene luminance and display luminance.
Since pictures are easier to understand than words (at least for me), here’s a video demonstration of the concept and the tool.
When Channels Clip
With a simple 1 to 1 mapping, we can focus on how highlight values behave as they approach 100% display emission.
With no highlight compression and no desaturation, hue shifts are introduced for non achromatic colors, because as one component clips, the other components continue to increase. So we need some way to “handle” these components as they increase in brightness, to remove hue shifts as the components approach clipping. To do this we can move the components toward the achromatic axis as their luminance increases.
Here’s a video demo with some interesting plots of what’s going on.
Technical Approach
Amidst an extended conversation with @Troy_James_Sobotka (thanks for your patience with my stupid questions), I got to thinking about the Inverse RGB Ratios approach that we ended up using in the Gamut Mapping VWG. In that approach we were using the inverse rgb ratios to push out of gamut values back towards the achromatic axis.
inverse_rgb_ratio = max(r,g,b) - rgb
If we instead used a constant value that we wanted to “collapse” our components towards, we could do
lin_max = 4.0
inverse_rgb_ratio = lin_max - rgb
If we add inverse_rgb_ratio
to rgb, we will get lin_max
everywhere. But if we modulate inverse_rgb_ratio
by some factor which describes how much we want to desaturate, then we get a simple and effective method of highlight desaturation.
The best way I’ve found so far (and I think there’s better approaches), is to modulate the inverse_rgb_ratio
by the complement of the compression factor. When we use the norm to do the highlight compression, we do
norm = max(r,g,b)
intensity_scale = compress(norm) / norm
scaled_rgb = intensity_scale * rgb
compression_factor = 1 - intensity_scale
intensity_scale
here is kindof like the derivative of the curve: it represents how much the compress function is altering the norm.
Then we can do
lin_max = 4.0
inverse_rgb_ratio = lin_max - c
desaturation_factor = inverse_rgb_ratio * compression_factor
desat_rgb = rgb + desaturation_factor
Here’s a video walkthrough of the technical approach.
Considering how stupid and simple this approach is it actually doesn’t look half bad in my aesthetic opinion.
Here’s a video of how the transform looks on some example test images.
And finally here’s the NaiveDisplayTransform node if you want to play around with it. No blinkscript so it should work fine in Nuke Non-Commercial.
EDIT - since the above link points to a gist that has been updated, here is the original nuke script as it existed at the time of this post: NaiveDisplayTransform_v01.nk (18.3 KB)
Next step for me is seeing how this same approach might be applied with a more traditional sigmoidal curve for input range compression.
Just wanted to share what I’ve been up to in case it’s useful for anyone here.