This morning I read about foot implementing gamma-correct font rendering, and while all those words sound familiar, I still didn’t know exactly what “gamma correcting” meant in this context. This drove me into a reading journey to better understand the topic, and the following is a short summary of what I’ve learnt
Facts
When rendering fonts, especially at small sizes or on high-resolution screens, the edges of the characters are often anti-aliased to make them look smooth. Anti-aliasing works by blending the font colour with the background colour at the edges.
Human vision is non-linear: our eyes are much more sensitive to changes in dark tones than in lighter tones. E.g.: The difference between 10% and 20% brightness is much more noticeable to us than the difference between 80% and 90% brightness.1
To account for our non-linear vision, RGB values are stored using a non-linear scale called gamma space. Gamma space allocates a larger range to dark tones, making images look more natural to our eyes.
We also represent colours in non-linear space due to technical reason related to ancient CRTs for which we use non-linear space, but that’s mostly historical baggage.
Gamma space
When storing or displaying an image, pixel values are often raised to a power (1/2.2) to account for the non-linear response of displays.
To perform accurate calculations on colours we first need to convert them from a non-linear space into a linear space. To do this, pixel values are raised to the inverse power (2.2).
This value (2.2) is called gamma. I’ll continue using 2.2 below to keep things simple.
Problem
Colour values are represented in non-linear scale, so if we perform calculations on them without first converting them into a linear scale, we get inaccurate results.
This results in incorrect blending of colours, where edges of text might be too dark, appear less sharp, or be inconsistent across different background. Contrast might also not be as good as expected.
Example
Let’s say we’re blending black and white with 50% coverage. Our colours are values between 0 and 1. Often these are represented by integers between 0 and 255, but the mathematics are easier to explain with values between 0 and 1.
Example operating in non-linear space
If we operate with non-linear colour values, we blend by applying the following operation:
Keep in mind that we’re using a non-linear scale, so 0.5 is not the “halfway” value between 0 and 1.
Example operating in linear space
If we operate by first transforming the colours into linear space, the result is entirely different. This process is called “gamma correction”.
First convert the values from non-linear space into linear space. For these particular values (0 and 1), they are the same, but this is not true for any other value.
Second, perform the blending operation, in linear space:
Finally, transform the result back into the original non-linear space:
The difference is far from trivial. For a visual reference, the following two rectangles have their background colour set to 0.5 and 0.73 respectively.
It's darker than "mid way".
E.g.: is has "gamma correction" applied.
It's "mid way" between black and white.
The first of the above rectangles is much close to black, and you might even notice that text on it has less contrast than ideal.
If you want to see some visual examples of blending text with proper gamma correction, see the sample screenshot in the PR which implements this feature for foot. The text is noticeably sharper.
Performance
There’s a lot of maths behind applying gamma correction. The above example does this for a single channel, but real usages have three channels (red, green, blue).
Fortunately, GPUs are designed for exactly this, and applying gamma correction using the GPU has an insignificant overhead.
Typically the conversion is between 8-bit encoded channels, so it’s possible to implement the translation lookup tables, which is a lot more performant that doing the actual arithmetic.
In fact, at night I sometimes adjust the screen brightness from 3% to 5% and it seems like a big difference. During the day, I adjust the brightness from 80% and 90% and the difference is very little. ↩︎