Gamma is a blight, a curse, and utterly annoying. Ever since somebody told me that RGB colours need to be Gamma corrected, RGB colours were spoilt for me. Gamma does to digital colour what kerning does to typography.
This post is an attempt to get even with Gamma.
Lost Innocence
Because, you see, only once I became fully aware of Gamma, things really started to fall apart. In my pre-gamma-aware innocence, I must have done some things right.
Let me show what I mean:
Here, I generate a linear gradient using a GLSL fragment shader. Say, drawing a full-screen quad using this shader code snippet:
|
|
And voila - a perceptually linear gradient.
But now, let’s get clever about this: We notice that we’re actually drawing to an sRGB monitor (most desktop monitors are, nowadays), so we should probably use an sRGB image for the swapchain (these are the most common 8bpc swapchain image formats in Vulkan). Thus we somehow need to convert our RGB colours to sRGB. The most elegant way to do this is to use an image attachment with an sRGB format, which (at least in Vulkan) does the conversion automatically and precisely on texel save.
If we draw the same shader as before, but now into an sRGB swapchain the swapchain image’s non-linear rRGB encoding should correct the sRGB monitor gamma. The gradient’s brightness values (as measured by a brigthness meter) should now increase linearly on-screen. They do. That should look like a linear gradient, right?
Wrong.
Instead, we get this:
This doesn’t look linear: shades don’t seem to claim equal space. Instead, it looks as if dark shades are too bright, while bright shades wash out. Here’s what I’d expected to see:
The difference is subtle, but look at both gradients and ask yourself: which seems to have more detail? Which is more balanced?
Even though the first gradient is more linear in terms of physical brightness, the second one looks more linear.
I find this counter-intuitive. But where intuition fails, ratio may help; and there is indeed a rational explanation: Visual perception is non-linear.
And You See The Ones In Darkness/Those In Brightness Drop From Sight
The human eye can distinguish more contrast detail in darker shades.
You don’t have to take my word for it; instead take those of Dr. Charles Poynton – the person who gave HDTV square pixels and the number 1080. Here is a diagram of how our perception tends to respond to changes in lightness, which I found in his dissertation:
CIE Lightness, when defined as a (relative) curve of just noticeable differences, fits very nicely a power function with exponent (0.42), with a small linear bit below relative luminance of about 1%.
This is fine. Nature. It probably helped our ancestors to survive or something. And it does explain why our physically linear gradient looked too bright in dark areas. Let’s draw a diagram:
in black: the biases
in red: the signal at this point in the chain
Check the Bias
Whenever we want to draw a perceptually linear gradient, we must remember to pay our dues to evolution, and factor in this perceptual bias.
If you want the appearance of a linear gradient, you must display a non-linear gradient, one that tunes down darker parts. Effectively, you want to apply the inverse of the non-linearity that is introduced by perception. Confusingly, this may be done automatically for you if you forget to do any gamma correction:
Two-Penny Gamma Correction
If you have an sRGB monitor and you innocently don’t do any sRGB correction (by rendering into a linear RGB framebuffer such as FORMAT_R8G8B8A8_UNORM for example), linear gradient values will get biased by the monitor’s gamma response alone – the result will be a gradient that looks “about linear”.
It looks “about linear” because what happens is that while the monitor will “gamma” the gradient, your eye will “de-gamma” the gradient again – and since these two non-linear effects on the signal (monitor, eye) are almost inverses of each other, we get a linear perceived signal at the end.
And here’s a cool thing: This was by design!
"The nonlinearity of a CRT is very nearly the inverse of the lightness sensitivity of human vision. The nonlinearity causes a CRT’s response to be roughly perceptually uniform. Far from being a defect, this feature is highly desirable."
Why is this desirable?
A big reason for encoding images in sRGB is that, because of sRGB’s perceptual nature, we get much better perceptual luminance contrast resolution out of 8bits per channel. Instead of wasting bits on high brightnesses where our eye has trouble noticing change, we spend most of the bit-budget where it counts: on darker shades.
sRGB is an elegant form of perceptual compression: images at 8-bits-per-channel and below (for reasons of encoding efficiency) really want to be encoded as sRGB.
And if the monitor can display these sRGB images directly and natively (because the hardware applies the inverse of the sRGB gamma transform) – that’s just a perfect match…
Practical applications for rendering using Vulkan
In Vulkan, if you can use an sRGB format for your swapchain image, then you don’t have to manually correct for sRGB gamma. your pixels will be automatically stored in non-linear sRGB, and displayed linearly on-screen. this is great for encoding the highest amount of perceptual colour contrast detail using limited amount of bits (sRGB formats are usually about 8 bit per channel).
When image format names contain the suffix *_SRGB the sRGB gamma transform is applied transparently on every texel read and texel write . This is useful, because we can only meaningfully blend color in linear space, blending in non-linear space would not be (physically) correct. The specs on Khronos Data Formats have some great documentation on this topic.
But this means that you need to undo the effect of the implicit sRGB gamma transform on texel write if you want to render a perceptually linear gradient while using an sRGB image backing. The function that the vulkan driver applies for you on texel write is called the srgb_oetf, short for “sRGB optical-electrical transfer function”.
To neutralise this function, you must apply the inverse_srgb_oetf, that’s the srgb_eotf “sRGB electo-optical transfer function”, just before you store the texel.
Here is how this would look like using our schematic from before:
Draw a perceptually linear gradient into an sRGB swapchain image
|
|
And voila:
Further reading:
Bonus
Thank you sticking around past the end credits. Here’s an extra bit of information that might come in handy at a cgi pub quiz one day:
Did you ever wonder what the “s” in RGB stands for? Me neither – until now; I assumed it stood for super. The sad reality is more humble: Apparently it stands for Standard. “Standard RGB”. What a standard.
RSS:
Find out first about new posts by subscribing to the RSS Feed