mooman219 / fontdue

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

Freely zoomable text #108

Closed therocode closed 2 years ago

therocode commented 2 years ago

Hi, I'm using fontdue in an application that has a freely zoomable/pannable canvas that can contain freely sized/positioned items. Items can have both text and geometry, and when the user pans/zooms around the effective appearence of the geometry and text on screen can range from tiny (not bigger than a few pixels) to extremely large.

This comes with a bunch of challenges some of which I've solved, but for this issue there's one that I seem to have hit a dead end with, and I'm not sure if this is in scope for fontdue.

There seems to me like there's two main approaches to this:

  1. Setup LayoutSettings and append text in Screen Space numbers. This means that as you zoom/pan around the camera, the text needs to be re-rasterized and layout be recalculated.
  2. Setup LayoutSettings and append text in World Space numbers. This means that only when the items themselves are moved/scaled does the text need to be re-rasterized and layout be recalculated.

I have tried both but running into issues with both. With 1: On the positive side, raster operations are in screen space always so the text always looks crisp and correct. Metrics width/height are in the effective screen space values as well and text is displayed on screen "how fontdue intended". The issue with this is the layouting part. Zooming the camera means giving the layouter new size/px to work with and even if they are scaled proportionally, it seems like inaccuracies in the layouter add up to create different word-wrap results and the result is extremely jittery: image (this is the https://github.com/mooman219/storm.git repo, the text example. Modified to proportionally scale the Layout max_width and SIZE (px) proportionally over time.

With 2: On the positive side, layouting/positioning happens in world space so the camera becomes irrelevant to this. Camera can be freely panned/zoomed without any jitter as the text glyphs are just zoomed in the same way as other geometry. This eliminates the jitter. The issue with this approach is of course that as you zoom in/out on glyphs the px is the same so you get pixly text as soon as you zoom in past the size of what the glyphs are made to be displayed at.

Semantically, it seems like an application like this needs "layouting in world space values" and "px/raster in screen space values" so I tried to do this by using approach #2 as a base, doing the layouting but before passing the glyph to my texture atlas cache (or the fontdue rasterizer in case none is cached) I do let effective_px = convert(glyph.key.px) where convert is a function that applies the camera zoom factor so that I can get the correct crispness of what the glyph is currently displayed at. However, here I hit the final roadblock: I still need the metrics.width/metrics.height in world-space values to properly size the glyph, but these are directly tied to the rastered glyph and hence the effective_px which is a screen-space value. It also does not give a good visual result to simply scale these back to world-space since they are in whole pixels which are hard integer steps, so when I tried this I got an effect like this:

image

The sizes of some glyphs (e and j here) are off, making them either stretch below the line, or appearing ugly. I assume what happens here is that the layouting still thinks in world space and is small but the rasterizing is screen space and is big so the pixel-sized metrics.width/height values become enlarged and their "snapping grid" of 1 pixel is now a "snapping grid" of scale factor pixels, so this approach does not seem to work either.

So, after all that. I feel I am either overlooking a way to use fontdue for this that exists already, or this is not feasible in fontdue and additional enhancements would be needed. Perhaps a native way to apply a scale-factor on the rasterization step alone? Or maybe a f32 version of metrics.width that wouldn't be floored to the pixel grid? Perhaps it's not within the scope, although I feel that freely-zoomable world space text isn't all too uncommon of a use case.

mooman219 commented 2 years ago

1: You get jitter because eventually the layout utility needs to resolve to a whole pixel. Some glyphs are resolved from say some x position of 1.01 to 1, while others were rounded from like 1.51 to 2. When you apply a smooth scaling, glyphs are going to snap to the grid at different times, appearing chaotic. The Font API does let you raster at these subpixel offsets, but I decided on snapping because text looks better out of the box this way

2: I'd honestly just not use fontdue here. It was designed as a screen space API, and as soon as you misalign it to the screen, all bets are off on quality. Have you considered using pre baked SDFs + SDF shader? I think you'll get better quality results with less work if you want text in world at weird non uniform transforms.

2b: yeah you totally can, the width and height are actually just computed from the bounding box, which is in the original f32 subpixel units

therocode commented 2 years ago

Thanks for the reply.

1: That makes sense, thanks for the explanation.

2: Yeah, I have looked at other alternatives but I haven't found an existing SDF or similar solution that comes along with layouting included + a good API for metrics and the like, like fontdue has. If I go down that route I suspect I'll have to implement those aspects myself, which can be okay.

2b: Oh, this is interesting, I'm having initially improved results from using the OutlineBounds, but there's still something off but I can move forward a bit at least to see if I can reach an acceptable level of a solution. Thanks 👍

And thanks for all your work on fontdue, it's a solid library!

therocode commented 2 years ago

This issue is resolved as far as I'm concerned, happy for it to be closed