klyff 0.1.1

Text rendering library for games with MSDF support
Documentation
klyff-0.1.1 has been yanked.

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 klyff::{
    EncoderContext, Rect, StyledTextBuilder, TextRenderer, TextStyle, TextureAtlas,
    TextureAtlasDescriptor,
    cosmic_text::{Attrs, Family, FontSystem, Metrics},
};

// Dependencies
let device: wgpu::Device = todo!("Obtain a device from wgpu");
let queue: wgpu::Queue = todo!("Obtain a device from wgpu");
let surface_format: wgpu::TextureFormat = todo!("Define your surface format");
let view: wgpu::TextureView = todo!("Obtain your render target");
let mut font_system: FontSystem = todo!("Obtain a font system from cosmic_text");
let (width, height) = (800u32, 600u32);

// One-time setup: a renderer for the swapchain format, and an atlas to cache glyphs.
let mut renderer = TextRenderer::new(&device, surface_format);
let mut atlas = TextureAtlas::new(&device, TextureAtlasDescriptor::default());

// Build a `StyledText` for the given bounding box. Each call to `push_text` adds a run.
let attrs = Attrs::new().family(Family::Name("Open Sans"));
let mut text_builder = StyledTextBuilder::new(
    Rect::from_xywh(0.0, 0.0, width as f32, height as f32),
    &mut font_system,
    Metrics::new(48.0, 60.0));
text_builder.push_text("Hello, world!", &attrs, TextStyle::default())
let text = text_builder.finish(&mut font_system, &attrs);

// Per frame: prepare uploads glyphs into the atlas and writes vertex buffers.
let mut cmd_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
renderer.prepare(
    EncoderContext {
        atlas: &mut atlas,
        device: &device,
        queue: &queue,
        cmd_encoder: &mut cmd_encoder,
        font_system: &mut font_system,
    },
    (width, height),
    std::slice::from_ref(&text),
);

// Then issue the draw inside any render pass that targets `surface_format`.
let mut pass = cmd_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
    label: None,
    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
        view: &view,
        resolve_target: None,
        depth_slice: None,
        ops: wgpu::Operations {
            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
            store: wgpu::StoreOp::Store,
        },
    })],
    depth_stencil_attachment: None,
    occlusion_query_set: None,
    timestamp_writes: None,
});
renderer.render(&mut pass, &atlas);

See the full setup at the hello_world and all_features examples.

Features

  1. 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.

  1. 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).

  1. 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.

  1. 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.