Closed bdon closed 12 months ago
Generally speaking, and this is only my personal opinion, increasing to 48 feels like a patch and not a real solution, I would prefer to have solution that really solves this problem, preferably even one that allows using regular font files instead of this pre-generated glyphs. Again, my personal opinion.
@HarelM sure, I agree at a high level a solution that does not pre-rendered SDFs is better, for example:
But even if you have a non-pregenerated glyph solution, the rendering and WebGL components of MapLibre will always need to deal with signed distance fields. Any move away from SDFs would have a major impact on the user experience and styling capabilities of MapLibre. So an enhancement to the SDF pipeline for higher res textures is complementary to the solution you mentioned, not an alternative.
Other hardware-accelerated text display systems like video games use SDFs, but they can load fonts on disk and have higher VRAM budgets, so are not subject to the same constraints. A reasonable alternative to this proposal is to keep 24pt fonts but use multi-channel SDFs:
https://github.com/Chlumsky/msdfgen (MIT license) https://github.com/heremaps/harp.gl/tree/master/%40here/harp-text-canvas HarpGL map renderer (Apache License)
I think moving all of MapLibre to MSDFs is viable, but it would also require re-generating all font stacks, would triple the TinySDF cost and triple VRAM usage even if all glyph textures in the atlas are 24pt - you'd then need red, green and blue channels.
This is a great write-up @bdon! The Mapbox GL JS equivalent of this is one of the few things I worked on post-fork, so I think I need to stay a little bit at arms-length from this, but just to share a few bits of perspective from that effort:
Thanks for this nice summary @bdon, I could not have written it better. Your proposal seems to be the right choice from my point of view. It is an incremental improvement of our text rendering stack and the risk of doing something wrong is very small.
I've made an interactive comparison of my branch vs maplibre 3.3.0:
https://bdon.github.io/maplibre-cjk/
Video:
https://github.com/maplibre/maplibre-gl-js/assets/77501/5de37a03-15a3-4595-84fb-e9865e8f2f03
It turns out this is easier than I expected to implement, without having to modify any shaders or vertex buffer layouts, only code that touches JavaScript. I haven't tested 100% of all cases or looked closely at the typographic metrics to make sure the spacing is the same between old and new code.
Thanks for making a live demonstration. It is really eye-opening!
I've updated the demo to align the new glyphs pixel-perfect with the old ones, so visual styling should remain the same with a minor version bump. (At least on Apple devices, need to test others)
https://bdon.github.io/maplibre-cjk/
I've also fixed text-writing-mode: vertical
cases.
Another comparison showing a particularly bad case that is fixed now:
https://github.com/maplibre/maplibre-gl-js/assets/77501/1af981ee-6461-4c28-8b1f-6b4a704ee928
Looks great! Feel free to open a PR if you want the tests to run on every change. If this is a small increment towards a wider solution and improves the current situation without changing a lot of code, let's push it forward.
Released in 3.4.1
User Story
As a viewer of the map in Chinese or Japanese language, the quality of rendered text should look as good as Latin text and other scripts.
Rationale
The drawback to the SDF approach is that it isn't the format fonts are commonly stored in. Fonts are defined as a collection of Bézier curves in a .TTF or .OTF file. In order to display them as SDFs they need to be "baked" into a texture via preprocessing with a program like font-maker.
The conversion from .TTF to SDFs requires the choice of a specific font size for rasterization into a bitmap via FreeType. Early on in GL JS development this was determined to be a 24 point font as a compromise between text sharpness and SDF bitmap size. SDF bitmaps are then stored in ranges of 256 glyphs and served over HTTP. A "font stack" is 256 files of 256 glyphs each, covering the entire 65,536 codepoint Basic Multilingual Plane
SDF 24 point font is good enough to render most Latin fonts with a small amount of rounding. The issue arises when rendering glyphs with more internal detail. These are common where CJK is used with "traditional" scripts - mainly Japan, Taiwan, Hong Kong and Macau. From the MapLibre example page I screenshotted a few[^1]:
Later on in GL JS development, it was decided to special case Chinese, Japanese and Korean glyphs to ignore the above code path, because the amount of network traffic required impacted map performance. Natural CJK text has poor locality over the space of UTF-8 codepoints, so a single map tile could result in several, even dozens of SDF files requested. The alternative
localIdeographs
code path renders text via mapbox/tinysdf (BSD) and can be controlled by the localIdeographFontFamily option.TinySDF skips over the FreeType pre-baking step by using the browser's Canvas API to draw text to a canvas, and then converts that canvas into an SDF. This reduces network requests for CJK glyphs to 0, but limits the display of CJK text to only the basic fonts built into the browser (Sans Serif for Gothic style, Serif for Ming/Song style)
TinySDF takes the 24 point font size as as parameter, this is hardcoded in GL JS to 24pt
Because TinySDF takes any font size as a parameter, we can increase the rendering resolution; most obvious would be doubling it to 48pt instead of 24. You can see the difference on the mapbox/TinySDF demo page (BSD).
so shaders like symbol_sdf.vertex.glsl need to be updated:edit: see below, no shader modifications necessaryhttps://github.com/maplibre/maplibre-gl-js/issues/1051 https://github.com/maplibre/maplibre-gl-js/issues/1002 https://github.com/maplibre/maplibre-style-spec/discussions/174
Impact
Alternatives
[^1]: Note the display of U+FA10 塚 may look unusual, this is my browser's locale setting and is not relevant to this issue.