Repository navigation
Setting Up MSDF Fonts
This tutorial assumes you have completed setting up your first project and setting up compiled resource stores, and that you are already familiar with setting up fonts the usual way.
In this tutorial, you will learn how to generate MSDF font atlases and render them with libmsdf:
- Installing the package
- Generating the MSDF atlas
- Adding the font to the MSDF font store
- Using the font in a sprite text
- Troubleshooting
Unlike the bitmap fonts osu!framework ships with, an MSDF font stores glyph shapes as distance fields rather than pre-rasterised pixels. Coverage is reconstructed in the fragment shader, so the same atlas stays sharp at 12px and at 400px. The trade-off is that you generate the atlas with a different tool and register it in a different store.
Add the package to your game project:
dotnet add package kajarlabs.osu.Framework.MsdfTextRendering
An MSDF font is two files: a PNG atlas and a JSON metadata file. Both are produced by msdf-atlas-gen, which you can download from its releases page.
Run it once per weight and style you intend to use:
msdf-atlas-gen -font MyAwesomeFont-Regular.ttf ^
-type msdf ^
-format png ^
-size 48 ^
-pxrange 4 ^
-yorigin top ^
-imageout MyAwesomeFont-Regular.png ^
-json MyAwesomeFont-Regular.json
The flags that matter, and what breaks if you get them wrong:
| Option | Description | If you change it |
|---|---|---|
-type msdf |
Generates a multi-channel signed distance field |
sdf or psdf produce a field the shader cannot read |
-format png |
Stores the atlas as a PNG texture | Other formats are not loaded by the font store |
-size 48 |
Em size used for generation | Lower values lose fine detail; higher values grow the atlas |
-pxrange 4 |
Distance range stored around glyph edges | Too low rounds off sharp corners at large sizes |
-yorigin top |
Top-left origin, matching osu!framework's coordinates | Omitting it renders every glyph upside down |
Warning:
-pxrangeis also written into the JSON asdistanceRange, and the shader reads it from there. Do not edit one without the other.
The output is a pair of files:
MyAwesomeFont-Regular.png
MyAwesomeFont-Regular.json
The JSON contains the glyph metrics, atlas bounds, plane bounds, kerning pairs and the distance range. Both files are required; the store will not load an atlas without its metadata.
File names follow the same pattern osu!framework uses for FontUsage, so the store can resolve a family, a weight and an italic flag to a file:
FontUsage.Family |
FontUsage.Weight |
FontUsage.Italics |
File name |
|---|---|---|---|
| MyAwesomeFont | "Light" |
false |
MyAwesomeFont-Light |
| MyAwesomeFont | "Bold" |
true |
MyAwesomeFont-BoldItalic |
| MyAwesomeFont | null |
false |
MyAwesomeFont |
| MyAwesomeFont | null |
true |
MyAwesomeFont-Italic |
Place the pairs inside your Resources folder, for example in MyAwesomeProject.Game/Resources/Typefaces/:
Resources/
└── Typefaces/
└──MyAwesomeFont
├── MyAwesomeFont-Regular.png
├── MyAwesomeFont-Regular.json
├── MyAwesomeFont-Bold.png
└── MyAwesomeFont-Bold.json
MSDF fonts do not go into the global Fonts store. They live in an MsdfFontStore, which owns the shader and the glyph cache, and which you cache so that MsdfSpriteText can resolve it through dependency injection.
namespace MyAwesomeProject.Game
{
public partial class MyAwesomeGame : osu.Framework.Game
{
private DependencyContainer dependencies = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(parent);
[BackgroundDependencyLoader]
private void load()
{
// Add the DLL resource store of this game project into the global resources store.
Resources.AddStore(new DllResourceStore(@"MyAwesomeProject.dll"));
// Register the shaders and resources libmsdf needs.
MsdfTextRendering.CreateCapabilities(Resources);
// Create the store that resolves and caches MSDF glyphs.
var msdfFonts = new MsdfFontStore(Host.Renderer, Shaders);
// Add one entry per weight/style, by family and weight.
msdfFonts.AddFont(Resources, @"MyAwesomeFont", @"Regular");
msdfFonts.AddFont(Resources, @"MyAwesomeFont", @"Bold");
// Cache it so MsdfSpriteText can find it.
dependencies.CacheAs(msdfFonts);
}
}
}
CreateCapabilities must run before the store is constructed: it is what registers the MSDF shaders with the renderer.
Warning: Certain font names may end up getting mangled by the .NET embedded resource machinery. For instance, numbers may be prefixed with underscores. A safe bet is to stick with letters. See this explanation for more detail.
MsdfSpriteText takes the same FontUsage as SpriteText, so the family, weight and italic flag you pass must match the file names from the table above:
new MsdfSpriteText
{
Text = @"Hello, MSDF!",
Font = new FontUsage(@"MyAwesomeFont", size: 40f, weight: @"Regular"),
}
Because MSDF glyphs are reconstructed per fragment, the same atlas can be scaled up without blurring. Scaling the drawable produces the same result as increasing size:
new MsdfSpriteText
{
Text = @"Still sharp at 8x.",
Font = new FontUsage(@"MyAwesomeFont", size: 40f),
Scale = new Vector2(8),
}
Layout behaviour is unchanged from SpriteText: Truncate, MaxWidth, AllowMultiline, Shadow and fixed-width all work the same way, because libmsdf plugs into the framework's existing text builder rather than replacing it.
| Symptom | Likely cause |
|---|---|
| Every glyph is upside down | The atlas was generated without -yorigin top
|
| Text renders as blank space | Only the PNG or only the JSON was copied into the resource store |
| Corners look rounded when scaled up |
-pxrange too low for the size you are rendering at |
| Glyphs overlap or sit on top of each other | Font metrics missing from the JSON; regenerate the atlas |
| Nothing renders and the log mentions shaders |
CreateCapabilities was not called, or was called after the store |
MSDF is not a free upgrade for every case. At small, static sizes a bitmap font is cheaper and can look crisper, because it was rasterised for exactly that size with hinting. Reach for MSDF when the same typeface has to survive zooming, animated scaling, or a wide range of sizes from one atlas.