Klyff text renderer
A text renderer intended for games, with high customisability. It uses:
- [
wgpu] for rendering. - [
cosmic_text] for shaping and layout. - [
skrifa] & [swash] for font reading. - [
etagere] for atlas packing. - [
klyff_msdf] for MSDF generation (done entirely on the GPU with wgpu).
Because glyphs are rendered with MSDF, glyphs can be scaled up infinitely and still preserves sharp corners. This is a pure rust reimplementation of this technique, originally invented by Chlumsky: msdfgen.
This crate features two layers: a convenient high level API, and a low level API for more customisability (such as custom shaders).
High level API
The high level API mainly involves [TextRenderer] and [StyledText].
First, create a [TextRenderer] with [TextRenderer::new()] for a basic renderer drawing
only the glyph's body, or [TextRenderer::with_styling()] to draw texts with additional
styling such as shadow, outline or glow. Each styling type is drawn with a separate draw call,
so disable what you don't need in the [Features] parameter.
use ;
// Dependencies
let device: Device = todo!;
let queue: Queue = todo!;
let surface_format: TextureFormat = todo!;
let view: TextureView = todo!;
let mut font_system: FontSystem = todo!;
let = ;
// One-time setup: a renderer for the swapchain format, and an atlas to cache glyphs.
let mut renderer = new;
let mut atlas = new;
// Build a `StyledText` for the given bounding box. Each call to `push_text` adds a run.
let attrs = new.family;
let mut text_builder = new;
text_builder.push_text
let text = text_builder.finish;
// Per frame: prepare uploads glyphs into the atlas and writes vertex buffers.
let mut cmd_encoder = device.create_command_encoder;
renderer.prepare;
// Then issue the draw inside any render pass that targets `surface_format`.
let mut pass = cmd_encoder.begin_render_pass;
renderer.render;
See the full setup at the hello_world and all_features examples.
Features
- Styling
Styling is done through [StyledText]. Each text can hold multiple chunks of text, each with its
own styling. Build styled text with [StyledTextBuilder].
When styling is enabled, each effect is drawn as a separate draw call in back-to-front order (shadow - outer glow - outer stroke - fill - inner glow - inner stroke) to avoid overlap between different glyphs and proper alpha blending.
Beyond this, [TextRenderer] allows you to further modify styling per glyph, and position of
each emitted vertex with [TextRenderer::prepare_with_glyph_transform], which is useful for
text animations.
- Rasterized glyphs
Glyphs that are incompatible with MSDF rendering (such as colored glyphs, bitmap glyphs) are rasterized as is into the atlas, and rendered with a separate pipeline. Currently, styling is not supported for these glyphs (though it is feasible to implement in the future).
- Custom glyphs
[StyledText] supports injecting custom glyphs into text. They are represented with an empty
glyph in the Private Use Area (PUA) defined by a custom font. You can set it up by calling
[setup_custom_glyph_font].
Klyff does not handle rendering custom glyphs, instead it stores the processed glyphs and
their position in a [Vec] which can be read back and rendered later by application code. Both
[TextRenderer] and [MeshEncoder] supports this operation.
- Custom pipeline
TextRenderer supports passing in custom pipeline for MSDF and rasterized glyphs through
[TextRenderer::with_custom_pipeline]. This is intended for use cases such as specifying
depth-stencil state, custom blend ops, or otherwise modifying the pipeline while still using
the built-in shader and pipeline layout.
Modifying the pipeline layout or the shader module may create an invalid pipeline and panic at runtime, so do be careful. If you need to use custom shaders then the low level API is recommended for more flexibility.
Low level API
For renderers that need full shader control, klyff splits the work into composable encoders.
-
[
MeshEncoder] decodes [Text] into the vertex / index buffers, which is then shared across all draw calls. It records a list of [DecodedGlyph] entry, allowing you to attach per-glyph attributes without re-running layout. -
[
MaterialEncoder] walks the decoded glyphs and produces a parallel vertex buffer of [GlyphMaterial] data - drawing instruction consumed by the built-in MSDF shader. One [MaterialEncoder] corresponds to one draw call / one layer (fill, stroke, glow, shadow, …); the layered effects of [TextRenderer] are themselves implemented as multiple [MaterialEncoder]s sharing one [MeshEncoder].
If the built-in [GlyphMaterial] does not cover your effect, the same pattern still applies:
iterate the [MeshEncoder]'s [DecodedGlyph]s, emit your own parallel vertex buffer and bind
it alongside the mesh buffer in your own pipeline.
You can see an example of these patterns in the custom_pipeline example crate.
Glyph atlas
The generated distance fields / rasterized glyphs are cached on a texture atlas. By default
this atlas is generated at runtime as new glyphs are drawn. With the serde feature you can
also generate the atlas offline and save it with [TextureAtlas::bake()], then reload it on
startup with [TextureAtlas::from_baked()].
If you do not want new glyphs to be generated into the atlas during runtime, call
[TextureAtlas::freeze()]; [TextureAtlas::unfreeze()] re-enables generation.
See the prebaked_atlas example for demonstration of this feature.
Versioning
This crate follows semver. In addition to standard rust's API backward compatibility guarantee, any change to vertex buffer layout or bind group layout will be considered a breaking change.