Forme renders text directly from quadratic Bezier glyph outline data on the GPU without precomputed textures, distance fields, or rasterized atlases. At any size and any scale, text remains sharp. Forme also provides a CPU rasterization path that produces standard MonoGame SpriteFont objects for cases where traditional bitmap fonts are preferred.
According to the Slug creator:
The name Slug comes from the history of typography. A full line of text cast as one piece of hot lead by a Linotype machine was called a slug. The primary function of our software is to lay out and render lines of text.
So to follow in this spirit of naming, Forme was chosen because in traditional letterpress printing, a forme is the complete assembly of individual pieces of type, letters, spacing, and lines, locked together into a single unit, ready for printing.
Where a slug represents a single cast line of text, a forme represents the next step: the composition of many lines into a structured, finalized layout.
The repository is split across three libraries and a content pipeline extension.
Forme is the core font processing library with no dependency on MonoGame or any graphics framework. It reads TrueType and OpenType font files, extracts glyph outlines, builds the curve and band textures required by the Slug algorithm, and handles text layout including word wrapping, alignment, and ellipsis truncation. It can save and load processed font data to a .forme binary file so that the TTF does not need to be reprocessed on every launch.
Forme.MonoGame is the MonoGame-specific rendering layer. It uploads curve and band texture data to the GPU, manages the compiled Slug shaders (one for OpenGL, one for DirectX 11, both embedded in the assembly and selected automatically at runtime), and exposes FormeRenderer for GPU-accelerated text rendering. It also exposes FormeSpriteFont for creating standard SpriteFont objects from TTF data at runtime.
Forme.MonoGame.Content.Pipeline is the MGCB content pipeline extension. It adds importers and processors for TTF and .forme files so that fonts can be compiled as content and loaded through the standard Content.Load<FormeFont>() API.
Install via NuGet.
dotnet add package AristurtleDev.Forme
dotnet add package AristurtleDev.Forme.MonoGame
To use the content pipeline extension with MonoGame, add the reference the following NuGet package in your project and add the dll reference to your Content.mgcb file as usual.
dotnet add package AristurtleDev.Forme.MonoGame.Content.Pipeline
Load from a TTF file at runtime:
using Forme;
byte[] ttfData = File.ReadAllBytes("NotoSans-Regular.ttf");
FormeFont font = FormeFont.FromTtf(ttfData, CharacterSet.Ascii);Save the processed font data to a .forme file to avoid reprocessing:
font.Save("NotoSans-Regular.forme");Load from a previously saved .forme file:
FormeFont font = FormeFont.FromFile("NotoSans-Regular.forme");Load via the MonoGame content pipeline (requires Forme.MonoGame.Content.Pipeline):
FormeFont font = Content.Load<FormeFont>("NotoSans-Regular");Upload the font to the GPU, then use FormeRenderer to draw:
using Forme.MonoGame;
FormeFontDevice fontDevice = new FormeFontDevice(GraphicsDevice, font);
FormeRenderer renderer = new FormeRenderer(GraphicsDevice);
// In Draw():
renderer.Begin();
renderer.DrawString(fontDevice, "Hello, world!", new Vector2(100, 100), Color.White, sizePixels: 32);
renderer.End();Measure text before drawing:
FormeTextBounds bounds = font.MeasureString("Hello, world!", sizePixels: 32);Use TextLayoutOptions for word wrapping, alignment, and ellipsis:
TextLayoutOptions options = new TextLayoutOptions
{
MaxWidth = 400,
Alignment = TextHorizontalAlignment.Center,
EllipsisMode = EllipsisMode.Word
};
FormeTextBounds bounds = font.MeasureString("Hello, world!", sizePixels: 32, options);
IReadOnlyList<GlyphPlacement> glyphs = font.GetGlyphs("Hello, world!", sizePixels: 32, options);Create a standard MonoGame SpriteFont from TTF data at runtime:
using Forme.MonoGame;
byte[] ttfData = File.ReadAllBytes("NotoSans-Regular.ttf");
SpriteFont spriteFont = FormeSpriteFont.Create(GraphicsDevice, ttfData, sizePixels: 24, CharacterSet.Ascii);
// Use with SpriteBatch as normal:
spriteBatch.DrawString(spriteFont, "Hello, world!", new Vector2(100, 100), Color.White);Located in demo/GPURenderingDemo. Demonstrates GPU accelerated text rendering using the Slug algorithm. Both a DesktopGL and a WindowsDX project are included. The DesktopGL project loads fonts through the content pipeline. The WindowsDX project loads fonts at runtime via FormeFont.FromTtf().
The demo shows text at multiple sizes, word-wrapped and aligned text, character and line spacing, all three ellipsis modes, and optional visual overlays for bounding boxes and baselines.
Located in demo/SpriteFontRenderingDemo. Demonstrates creating MonoGame SpriteFont objects from TTF data using FormeSpriteFont.Create(). Both a DesktopGL and a WindowsDX project are included. The demo shows fonts at multiple sizes rendered through the standard SpriteBatch API.
The Slug reference shaders are written for DirectX with full Shader Model 5 features available. MonoGame's OpenGL backend compiles HLSL to GLSL via MojoShader, which only supports Shader Model 3. SM3 has no integer arithmetic, no integer textures, no integer loop variables, and no direct texel load instructions. The Forme shader targets SM3 for OpenGL and SM4 for DirectX 11 from a single .fx source file, gating the profile with #if OPENGL preprocessor blocks.
The following describes what had to change from the reference and why.
The reference CalcRootCode() determines which roots of a quadratic contribute to coverage by extracting the signs of three y-coordinates via asuint() and a packed bit lookup table (0x2E74U >> shift). SM3 has no integer instructions at all, including asuint. Forme replaces this with CalcRootEligibility(), which computes the same sign-combination logic using float comparisons (> 0.0), multiplies, and saturate. The result is semantically identical but expressed entirely in floating-point arithmetic.
The reference band texture is typed Texture2D<uint4> and read with .Load(int3(x, y, 0)), which requires SM4. MojoShader cannot express typed integer textures or direct texel loads at SM3. The band texture is instead formatted as RG32F, storing unsigned 16-bit integers packed as floats. All texture reads use tex2Dlod with computed UV coordinates of the form (float2(tx, ty) + 0.5) / texSize rather than direct integer pixel coordinates. This requires helper functions (FetchBandTexel, FetchCurveTexel) that convert a flat linear texel index into a 2D UV before sampling.
The reference CalcBandLoc() wraps a linear texel offset back to a 2D coordinate using right-shift and bitwise AND, both integer-only operations. Forme uses fmod() and floor() instead to perform the same wrap-around arithmetic in float.
SM3 does not support for loops with integer counters when integer instructions are absent. Forme uses a float loop variable with the [loop] hint and += 1.0 increments.
The reference vertex shader unpacks band texture coordinates from a 32-bit integer stored in tex.z via asuint() and bit shifts. Without integer casting, this is not possible in SM3. Forme instead stores the band texture X and Y coordinates as a single packed float (y * textureWidth + x) passed through tex.z, and unpacks them with fmod and floor in the pixel shader, where bandTexSize is available.
The reference vertex shader applies sub-pixel outward dilation (Lengyel 2017, Section 4) to reduce aliasing at glyph edges. This requires perspective projection to compute the correct half-pixel displacement along the vertex normal using the inverse Jacobian. With an orthographic projection, the intermediate values in the dilation formula diverge and corrupt the glyph geometry. Since MonoGame 2D rendering uses orthographic projection, dilation is skipped. It is a cosmetic refinement and not required for correct coverage rendering.
The reference SolveHorizPoly() falls back to a linear solve when the quadratic coefficient a.y is smaller than 1.0 / 65536.0. This threshold is too tight for GLSL produced by MojoShader. Curves with a.y slightly above that threshold caused near-division-by-zero, producing extreme t values and horizontal banding artifacts. The epsilon is widened to 0.0001, which eliminates the artifacts without affecting any visible curve.
The reference has separate SolveHorizPoly and SolveVertPoly functions. Forme eliminates SolveVertPoly by swapping the x and y components of the control points before passing them to SolveHorizPoly, reducing code duplication.
Slug Algorithm by Eric Lengyel (Terathon Software). GPU antialiased text rendering from quadratic Bezier glyph outlines. Published in the Journal of Computer Graphics Techniques, Vol. 6, No. 2, 2017.
Now in the public domain as of Match 17, 2026, licensed under MIT or Apache. Forme is an independent implementation of this algorithm; Terathon Software is not affiliated with this project.
For license detail on Slug, please see the THIRD_PARTY_NOTICES.
StbTrueTypeSharp by the StbSharp project maintained by Roman Shapiro. C# port of Sean Barrett's stb_truetype.h for reading and parsing TrueType and OpenType font files. Used by the core Forme library to extract glyph metrics and outlines.
Forme is licensed under the MIT License. See LICENSE for the full license text.
