mooman219 / fontdue

The fastest font renderer in the world, written in pure rust.
Apache License 2.0
1.44k stars 72 forks source link

Support Coverage Maps at Different Gamma Values #118

Open overdrivenpotato opened 2 years ago

overdrivenpotato commented 2 years ago

PR #115 fixes issue #114 by converting the output u8 values to sRGB after fontdue creates a coverage vector. However, when working with linear u8 values, a nonlinear function like sRGB maps u8 -> u8 values with some precision loss. When rendering text, it is also sometimes preferable to use an empirically determined gamma value like 1.45, which has the same precision issue.

This can probably be fixed by exposing a rasterization method that outputs Vec<f32>, or alternatively via a more clever API with the use of a Gamma trait in order to avoid the memory overhead of f32 values.

mooman219 commented 2 years ago

When hand writing the vectorization, I did some AB testing with built in gamma correction which went largely unnoticeable. People that did notice it, preferred it without.

However, when working with linear u8 values, a nonlinear function like sRGB maps u8 -> u8 values with some precision loss.

Gamma correction processes work on images already, which typically have 8 bit channels, so I'm not convinced the 0.3% max precision loss is perceptible.


If you can provide convincing material (Like a PR) showing the legibility of output is noticeably improved by the small precision improvement without a significant cost to the performance, then we can use that to substantiate this issue.

overdrivenpotato commented 2 years ago

If you're rendering fontdue with some kind of linear blending (e.g. on a GPU), it looks fine. But if you're doing blending in a non-linear space it tends to look pretty bad in some cases:

gammatest

The top text in the above picture has a gamma adjustment, while the bottom does not (you might have to click through the image to view it in full quality). The blending is occurring in sRGB space. It's up to you whether you think the bottom text looks bad; to me it looks broken.

So naturally you can use a u8 -> u8 mapping, but this is worse than 0.3% error. For example, a value 2/255 gets mapped to 22/255 from sRGB to linear, while 3/255 becomes 28/255. So you lose the values 23, 24, 25, 26, 27, resulting in visible artifacts. And a performance problem now is that you now have to incur an entire other loop over the generated data to do that mapping, requiring more memory loads and stores.

What about adding a method like Font::rasterize_gamma<G: Gamma>(...), with a Gamma trait like:

trait Gamma {
    fn to_u8(linear: f32) -> u8;
}

(SIMD arguments left out for sake of example). This could be entirely inlined, avoiding both precision errors and the extra cost of a round trip through memory for every pixel. It would be trivial to implement simple truncation or rounding to keep the current behaviour.