libgdx-freetype
npx machina-cli add skill kyu-n/gdx-claude-skills/libgdx-freetype --openclawlibGDX gdx-freetype
Runtime BitmapFont generation from .ttf/.otf files. Use instead of pre-baking fonts with Hiero when you need multiple sizes, dynamic scaling, or user-selectable fonts.
Gradle Dependencies
// core
implementation "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
// desktop
implementation "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop"
// android (all four ABIs)
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-armeabi-v7a"
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-arm64-v8a"
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86"
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86_64"
// iOS
natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-ios"
The platform artifact is gdx-freetype-platform, not gdx-freetype. Missing platform natives causes UnsatisfiedLinkError at runtime.
iOS/RoboVM: Also add to robovm.xml inside <config>:
<forceLinkClasses>
<pattern>com.badlogic.gdx.graphics.g2d.freetype.**</pattern>
</forceLinkClasses>
Without this, RoboVM's AOT compiler strips FreeType classes, causing runtime crashes on iOS.
Basic Usage
FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/myfont.ttf"));
FreeTypeFontParameter parameter = new FreeTypeFontParameter();
parameter.size = (int)(24 * Gdx.graphics.getDensity()); // density-scaled
parameter.color = Color.WHITE;
parameter.borderWidth = 1;
parameter.borderColor = Color.BLACK;
parameter.minFilter = Texture.TextureFilter.Linear;
parameter.magFilter = Texture.TextureFilter.Linear;
parameter.characters = FreeTypeFontGenerator.DEFAULT_CHARS; // ASCII + common punctuation
BitmapFont font = generator.generateFont(parameter);
generator.dispose(); // safe — the font is independent of the generator
// font remains fully usable after generator disposal
FreeType requires an OpenGL context. It bakes glyphs into a texture atlas. It does not work in the headless backend or before create().
FreeTypeFontGenerator is NOT thread-safe. Generating fonts from multiple threads simultaneously crashes. AssetManager handles synchronization internally, but manual multi-threaded generation will fail.
Key Parameter Fields
| Field | Default | Description |
|---|---|---|
size | 16 | Pixel height. Scale with Gdx.graphics.getDensity() for consistent physical size across devices. |
characters | DEFAULT_CHARS | Which glyphs to bake. ASCII by default — add accented chars for European languages, or use incremental for CJK. |
color | Color.WHITE | Font fill color |
borderWidth | 0 | Outline thickness in pixels |
borderColor | Color.BLACK | Outline color |
shadowOffsetX/Y | 0 | Drop shadow offset |
shadowColor | Color(0,0,0,0.75f) | Drop shadow color |
minFilter / magFilter | Nearest | Texture filtering. Use Linear for smooth scaled text. |
hinting | AutoMedium | None, Slight, Medium, Full, AutoSlight, AutoMedium, AutoFull. Default is best for most fonts. Use None for pixel art fonts; Full can cause artifacts with some fonts. |
kerning | true | Adjust spacing between glyph pairs |
mono | false | Monospace rendering (all glyphs same width) |
genMipMaps | false | Generate mipmaps for minification |
incremental | false | Render glyphs on demand instead of all at once — see CJK section |
Disposal Lifecycle
Generator and font are independent. After generateFont(), the font owns its own texture. The generator is no longer needed.
// Generate multiple sizes from one .ttf, then dispose generator
FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("font.ttf"));
FreeTypeFontParameter param = new FreeTypeFontParameter();
param.size = 16;
BitmapFont small = generator.generateFont(param);
param.size = 24;
BitmapFont medium = generator.generateFont(param);
param.size = 48;
BitmapFont large = generator.generateFont(param);
generator.dispose(); // fonts remain usable
// In dispose():
small.dispose();
medium.dispose();
large.dispose();
Exception — incremental mode: When parameter.incremental = true, the generator must stay alive because it renders new glyphs on demand. Dispose the generator only after disposing the font. See CJK section below.
Generated BitmapFonts are managed textures — they auto-reload after Android GL context loss like any other managed resource. Do NOT re-run the generator in resume() to "reload" fonts.
Characters Parameter (i18n)
DEFAULT_CHARS covers basic ASCII. For European languages, add accented characters:
parameter.characters = FreeTypeFontGenerator.DEFAULT_CHARS
+ "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
+ "ŁłŃńŚśŹźŻżŒœ¿¡«»";
Characters not in the set render as missing glyphs. If text looks broken, check this parameter first.
Atlas size: The default atlas page is 1024x1024. Large character sets without incremental mode can fill it, silently dropping glyphs. For large sets, either use incremental = true or provide a larger packer: parameter.packer = new PixmapPacker(2048, 2048, Pixmap.Format.RGBA8888, 1, false);
Incremental Rendering (CJK)
CJK character sets have thousands of glyphs. Baking them all upfront creates enormous textures and long load times. Use incremental mode instead:
FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/NotoSansCJK.ttf"));
FreeTypeFontParameter param = new FreeTypeFontParameter();
param.size = 24;
param.incremental = true; // glyphs rendered on first use
param.characters = ""; // start empty — glyphs added on demand
BitmapFont cjkFont = generator.generateFont(param);
// DO NOT dispose generator — it's needed to render new glyphs
With incremental = true:
- Glyphs are baked into the atlas the first time they're drawn
- The generator MUST stay alive — dispose it only after the font
- Small hitch on first render of each new glyph
- Pre-load common characters during a loading screen to reduce hitches
// Dispose order for incremental fonts
cjkFont.dispose();
generator.dispose(); // AFTER the font
AssetManager Integration
Register the FreeType loaders, then load fonts asynchronously:
AssetManager assets = new AssetManager();
// Register loaders (once, at startup)
assets.setLoader(FreeTypeFontGenerator.class,
new FreeTypeFontGeneratorLoader(assets.getFileHandleResolver()));
assets.setLoader(BitmapFont.class, ".ttf",
new FreetypeFontLoader(assets.getFileHandleResolver()));
// Queue a font for loading
FreetypeFontLoader.FreeTypeFontLoaderParameter params =
new FreetypeFontLoader.FreeTypeFontLoaderParameter();
params.fontFileName = "fonts/myfont.ttf"; // actual .ttf path
params.fontParameters.size = (int)(24 * Gdx.graphics.getDensity());
params.fontParameters.minFilter = Texture.TextureFilter.Linear;
params.fontParameters.magFilter = Texture.TextureFilter.Linear;
assets.load("myfont-24.ttf", BitmapFont.class, params); // key is arbitrary, not a file path
// In render loop:
if (assets.update()) {
BitmapFont font = assets.get("myfont-24.ttf", BitmapFont.class);
}
The asset key (first argument to load()) doesn't need to match the actual .ttf filename, but it must end in .ttf — the FreeType loader is registered for the .ttf extension. A key like "heading-font" without .ttf won't match the loader and silently fails to load.
Density Scaling
Scale font size by screen density so text appears the same physical size across devices:
parameter.size = (int)(24 * Gdx.graphics.getDensity());
// mdpi (1.0) → 24px, xhdpi (2.0) → 48px, xxhdpi (3.0) → 72px
Common Mistakes
- Wrong platform artifact name — The natives artifact is
gdx-freetype-platform, notgdx-freetype. Using the wrong name compiles fine but crashes withUnsatisfiedLinkErrorat runtime. - Disposing generator before generating the font — The generator must be alive when
generateFont()is called. Dispose it after. - Thinking font disposal is tied to generator disposal — They're independent. Disposing the generator does NOT affect already-generated fonts (unless using incremental mode).
- Disposing generator in incremental mode — When
incremental = true, the generator must stay alive to render new glyphs on demand. Dispose the generator only after the font. - Missing
charactersfor i18n —DEFAULT_CHARSis ASCII only. European accented characters (é, ñ, ü) must be added explicitly or they render as missing glyphs. - Not knowing about
incrementalfor CJK — Baking all CJK glyphs upfront creates huge textures. Useparameter.incremental = truefor on-demand rendering. - Suggesting Hiero when runtime generation is needed — Hiero pre-bakes fonts at build time. Use FreeType when sizes must be computed at runtime (density scaling, user preferences).
- Missing platform natives in Gradle — The core
gdx-freetypedependency alone is not enough. Each platform needs itsgdx-freetype-platformnatives or FreeType crashes at runtime. - Missing robovm.xml forceLinkClasses for iOS — RoboVM strips FreeType classes without this entry. App compiles but crashes on iOS.
- Hardcoding pixel size without density scaling —
parameter.size = 24looks different on every device. Use(int)(24 * Gdx.graphics.getDensity())for consistent physical size. - Using FreeType in headless backend — FreeType bakes glyphs into a GL texture atlas. It requires an OpenGL context and does not work in headless mode.
- Re-running generator in
resume()on Android — Generated BitmapFonts are managed textures and auto-reload after GL context loss. Regenerating wastes time and leaks the old font. - AssetManager key without
.ttfextension — The FreeType loader is registered for.ttf. A key like"heading-font"won't match — use"heading-font.ttf"instead.
Source
git clone https://github.com/kyu-n/gdx-claude-skills/blob/master/skills/libgdx-freetype/SKILL.mdView on GitHub Overview
libgdx-freetype enables BitmapFont generation at runtime from .ttf/.otf files using FreeTypeFontGenerator. It supports density-scaled sizes, color, borders, shadows, and i18n glyphs, with incremental glyph rendering for CJK and AssetManager integration via FreeTypeFontLoader. This is essential for multiple sizes, dynamic font selection, and debugging font issues, especially on iOS.
How This Skill Works
Create a FreeTypeFontGenerator from a font file, configure a FreeTypeFontParameter (size, color, border/shadow, characters, etc.), and call generateFont to bake a BitmapFont that uses a texture atlas. After generation, dispose the generator safely while the font remains usable; note that FreeType requires an OpenGL context and is not thread-safe, so use AssetManager for synchronized loading when possible.
When to Use It
- Need multiple font sizes or user-selected fonts at runtime
- Require extended glyphs (i18n) or CJK support with on-demand rendering
- Debugging missing glyphs, blurry fonts, or FreeType issues on iOS
- Integrating font loading with AssetManager via FreeTypeFontLoader
- Maintaining density-consistent font sizes across devices
Quick Start
- Step 1: FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/myfont.ttf"));
- Step 2: FreeTypeFontParameter parameter = new FreeTypeFontParameter(); parameter.size = (int)(24 * Gdx.graphics.getDensity()); parameter.color = Color.WHITE; parameter.characters = FreeTypeFontGenerator.DEFAULT_CHARS;
- Step 3: BitmapFont font = generator.generateFont(parameter); generator.dispose();
Best Practices
- Scale font sizes with device density: parameter.size = (int)(desired * Gdx.graphics.getDensity())
- Specify characters to bake (default ASCII plus accents); enable incremental for CJK if needed
- Tune texture filtering and hinting (minFilter/magFilter, hinting) for readability
- Dispose the FreeTypeFontGenerator after generateFont; the BitmapFont remains usable
- For iOS/ RoboVM, include platform natives and forceLinkClasses to prevent crashes
Example Use Cases
- Desktop UI with multiple font sizes for labels and menus
- Mobile game with density-scaled fonts and outline/border effects
- Internationalized game supporting accented European languages
- CJK-heavy text rendering using incremental glyph loading
- Asynchronous font loading via AssetManager and FreeTypeFontLoader