emilk / egui

egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native
https://www.egui.rs/
Apache License 2.0
21.57k stars 1.56k forks source link

Blurry Text Rendering When Using a Pixel-style Font #1790

Open zicklag opened 2 years ago

zicklag commented 2 years ago

Describe the bug When using a pixel-style font like Cozette or Ark I notice that the edges of the font get kind of blurred slightly most of the time, though if you position and size the window/widget to just the right spot the edges will be clear.

Here's a screenshot:

fish-fight-text

The effect is subtle, but zooming in makes it more obvious:

image

But the button text happens to line up fine and end up clear:

image

To Reproduce It seems to be reproducible by simply uploading a font such as font like Cozette or Ark and placing a label.

Expected behavior The edges of the font should stay crisply lined up with the screen's pixels

Desktop (please complete the following information):

Additional context

Related to https://github.com/fishfight/punchy/pull/38.


I wonder if this might be due to linear filtering on the font texture. :thinking: It might be simple enough to allow you to specify that a certain font should have it's texture generated with nearest filtering mode. We would have to test to see if switching the filtering mode will fix the issue.

zicklag commented 2 years ago

I just inspected a render with RenderDoc and discovered that the font is actually blurred in the font texture itself, so that rules out linear filtering:

image

I guess that means we need to tweak the rasterization step to somehow line up the texture pixels with the bitmap-style font pixels.

Here's the issue, this is a ttf font that represents a bitmap-like style. So I don't know if we can really tell from the font vectors how to line it up with the pixels while rasterizing. We might have to use an actual bitmap font to get it to line up right.

Unless font hinting is th right solution, like mentioned here: https://github.com/emilk/egui/issues/56#issuecomment-933913761.


Another possible solution could be to support bitmap font formats directly, such as BDF, which seems to be somewhat standard for pixel-style fonts. I made a BDF parser with only 211 lines of code ( and the peg crate ). We would just have to integrate that with the egui font rasterization. I'm just not sure what that looks like in Egui right now, so I'd have to investigate whether or not that would be feasible.

parasyte commented 1 year ago

I was hopeful that the linked PR would fix this, but it does not. I put the CozetteVector.ttf into an egui hello world with and without the patch applied. The difference is only in positioning and relative sizing. Both have the same aliasing artifacts around the edge of the text, no matter what font size is used.

But I suppose it isn't too surprising, because the vector fonts are specifically designed to scale arbitrarily and antialiasing is actually a good thing for this use case. To put it another way, the Cozette README describes the situation:

The vector formats (CozetteVector) are provided as a compatibility feature. They look hideous. They don't contain any glyphs past U+FFFF. Rendering of vectorized bitmap-like fonts is terrible on virtually all operating systems. If Cozette looks awful on your system, you probably have a vector version. Please use the bitmap formats (.otb) if you can.

Issues with vector formats will not be fixed or addressed. Cozette is a bitmap font first and foremost.

(Also consider sub-pixel rendering with ClearType, FreeType, etc. a la #2356)

Support for bitmap fonts seems like the right way forward. Although rasterization of vector fonts can be made a little better for "pixel font" styles by disabling texture filtering/antialiasing, it's more trouble than it is worth.

Sydius commented 11 months ago

I would be very interested in support for bitmap fonts; I'm working on a pixel game that is low resolution by design and I've been struggling to get text to look good.

Sydius commented 11 months ago

I hacked in support for bitmap fonts and it makes a world of difference at low resolutions; much crisper text. Essentially what I did was remove ab_glyph as a dependency of epaint and then fixed whatever broke in font.rs and fonts.rs, since I didn't already know the codebase at all.

I would suggest that, rather than adding native support for BDF or other bitmap font formats into the library directly, it could make more sense to put a layer of abstraction between epaint and ab_glyph such that there's a trait that can be implemented by clients to provide a draw routine to generate the atlas texture, bounding box / kerning information, unique glyph IDs, and pixels per point. Bitmap fonts would set pixels per point to one and ignore the scaling passed in. ab_glyph could then be one implementation of that trait, but games could provide their own if they're storing fonts in some other format, including BDF etc.

The ab_glyph implementation of the aforementioned trait could even be in its own crate, which could be useful given the license issues with the baked in default fonts (something I only realized as I was going through this exercise). I would drop ab_glyph as a dependency, as well as the default fonts, for my own project (I realize there's a default-fonts feature flag, though this doesn't change the license bit of the epaint crate configuration, confusing cargo-about etc.).

parasyte commented 11 months ago

it could make more sense to put a layer of abstraction between epaint and ab_glyph such that there's a trait that can be implemented by clients to provide a draw routine to generate the atlas texture, bounding box / kerning information, unique glyph IDs, and pixels per point.

This is probably good for a hack, but that exposes a lot of internal details to a public API. A very leaky abstraction, and it would make changing implementation details (related to these internals) very difficult to update without breaking compatibility.

The reasoning behind suggesting "something like BDF" is that this is a stable format. Everything behind the abstraction boundary can be replaced and callers would never notice or care. The ideal for abstractions.

Anyway, ab_glyph itself is an implementation detail (and one that should probably be replaced, for reasons).

Sydius commented 11 months ago

It doesn't strike me as particularly leaky, having just hacked it in. All it really needed was a draw routine that returned a list of x,y,v points, which seems super generic and something any font library would likely have to provide, and the ascent/descent/kerning/bounding box info, which, again, is something just about any font API would have to provide. I mentioned that it'd be used to create the atlas texture, but the API itself wouldn't have to know anything about that, or even that there is one, for example.

It's somewhat common for bitmap fonts to be bespoke, so generating a BDF would be a bit of PITA compared to making an adapter that returns much the same info if you're just making it in Aseprite or something. But I won't press the issue. I'm sure there are plenty of uses for a more expansive API than what I'm describing, given all the features of even something as ancient as BDF has that I have ignored.

parasyte commented 11 months ago

I'm talking about abstractions leaking implementation details. :) There's no reason that someone who wants to put text into the UI should have to know anything about texture atlases. Let alone the specific texture format or the need for pixels_per_point at all (ignoring it is most certainly incorrect).

Like I said, probably good enough for just working around this issue, but I would be concerned about any proposal to add it to the public API.

As far as which bitmap font formats to support, I'm indifferent on the matter. It could be "PNG + fixed glyph size" and that would be enough for just about any use case for fixed-width pixel fonts. More opinionated formats have the advantage that they are not limited to fixed-width and provide typographical information that is helpful for the rasterizer (kerning and hinting, etc).

Sydius commented 11 months ago

Right; like I said, I don't think it would need to know that there is a texture atlas, let alone texture formats, though it would need some way of indicating where pixels are of course.

This is a rough sketch of what I mean:

/// `FontProvider` is a trait that can be implemented by the user to provide fonts to `epaint`.
pub trait FontProvider {
    type FontImpl: Font;

    /// Returns the font for the given point `size` and `style`.
    fn font(&self, size: f32, style: TextStyle) -> Self::FontImpl;
}

/// `Font` is a trait that can be implemented by the user to represent a specific, sized font to `epaint`.
pub trait Font {
    type GlyphImpl: Glyph;
    type Iter: Iterator<Item = char>;

    /// Returns the ascent of the font, in points.
    fn ascent(&self) -> f32;

    /// Returns the descent of the font, in points.
    fn descent(&self) -> f32;

    /// Returns the glyph for the given character.
    fn get(&self, cp: char) -> &Self::GlyphImpl;

    /// Returns an iterator over all supported characters.
    fn characters(&self) -> Self::Iter;
}

pub trait Glyph {
    /// Returns the bounding box for this glyph: width, height, x offset, y offset.
    fn bbx(&self) -> (i32, i32, i32, i32);

    /// Returns the amount to advance to the next character.
    fn advancement_width(&self) -> i32;

    /// Draw the glyph using cb(x, y, alpha).
    /// x,y must be within bbx().
    /// alpha is 0.0 to 1.0.
    fn draw(&self, cb: &mut dyn FnMut(u32, u32, f32));
}

The above isn't real, and definitely isn't correct, but the idea would be that any font implementation should be able to easily implement the above primitives, be that ab_glyph or some BDF thing or a PNG+fixed-size. Again, without needing to know about texture formats etc. epaint could then use it to create the texture atlas or do whatever else.

I do think it does leak quite a bit implicitly in the sense that it'd be assumed that draw() results are being cached etc. And I totally get not wanting to pollute the public API. You might consider starting with something akin to this internally as you migrate away from ab_glyph -- that way, you can have both the old and the new during the migration and BDF etc. would be easy to add as a third option. Then, if the API ever stabilizes enough that you're not worried about it, you can reconsider exposing it for others to provide implementations for, similarly to Serde serialization etc.

(as for ignoring pixels_per_point... I'm ignoring scaling entirely, since I'm doing something 'pixel perfect' and cannot tolerate fractional pixels etc., but that's all beside the point (pun intended))

parasyte commented 11 months ago

The above isn't real, and definitely isn't correct, but the idea would be that any font implementation should be able to easily implement the above primitives, be that ab_glyph or some BDF thing or a PNG+fixed-size. Again, without needing to know about texture formats etc. epaint could then use it to create the texture atlas or do whatever else.

Sounds good in theory! But that Glyph::draw() method is kind of frightening. You're asking the font provider to call a function for each pixel that it wants to update. Not only are there optimization concerns, but this kind of API doesn't enforce any kind of behavior on whether or not the callback will even be called, or in what order its arguments will arrive (sequential access vs random access), or whether some will be missing entirely. So not only do we have a potentially slow rendition of a memcpy, but we also have to assume that the buffer may not be fully written by this call. In the worst case, that means an obligatory memset prior to drawing (just to be sure).

I can imagine a slightly different API that instead passes a &mut [Color32] and pixels_per_point (this is important; see below) to the draw method. This still requires zeroing the buffer before egui makes the call, and still allows the implementation to decide on just how much of the buffer to write. And it still has the issue that this buffer needs to be transformed into an internal representation that can become part of an actual texture. But the benefits are enormous (IMO):

Anyway, yes, it's within the realm of possibility. I don't know if it's actually better than just "PNG + size"... At least in that case, a font provider doesn't have to do anything at runtime. It's a much easier API to use; "set it and forget it."

(as for ignoring pixels_per_point... I'm ignoring scaling entirely, since I'm doing something 'pixel perfect' and cannot tolerate fractional pixels etc., but that's all beside the point (pun intended))

The missing piece here is that fonts designed for "low DPI" displays (pixels_per_point = 1.0) look obnoxiously small on "high DPI" displays (2.0). And it gets worse when scaled; Imagine an ultra-super high DPI display with 4.0 physical pixels per logical pixel (per dimension! that's 16 pixels-per-pixel total). Now your font is so tiny that it's completely unreadable, even though it is incredibly sharp and crisp! (I don't think any displays actually exist with 4.0 logical pixel scaling, but it's an illustrative example.)

And vice versa. Fonts designed for 2.0 look obnoxiously large on a 1:1 display...

The right way to handle this is with multiple size variations for your font. Say one for 1.0, another for 1.5, and one for 2.0. Choose the right variant based on the display's pixels_per_point (or the closest approximation) and you're good to go. This unfortunately can't be ignored! UX depends on it.

Sydius commented 11 months ago

Ah, yeah, I assumed once the texture atlas was made, those 'draw' calls would never occur again and thus amortized, but it doesn't matter because your solution is strictly better anyway.

The right way to handle this is with multiple size variations for your font.

Sure, in the normal case; in mine, I'm scaling the entire screen in post-processing afterward along with everything else and there's nothing dynamic about anything (think old school CRPGs). In any case, you're right for the general case, though I'm guessing very few who reach for a bitmap font are going to support more than one size.

As an aside, I happened to take a before and after, as a point of comparison:

image

image

The difference is more noticeable because of that post-processing I'm doing to fake CRT effects; a pretty niche case for sure. I was definitely happy when I saw the difference, though.