Skip to content

Generating the SDF Atlas

The Niagara renderer doesn’t render text — it renders sprites sampled from a signed distance field (SDF) texture atlas, indexed per-digit through User.DamageStyle. This guide covers how to build that atlas with msdf-atlas-gen, the open-source tool this plugin’s own atlases (T_MCT_Default_SDF, and the demo fonts under Content/Examples/VFX/Textures) were generated with.


A distance field encodes, per texel, the distance to the nearest glyph edge instead of raw coverage. This lets a single low-resolution texture stay crisp at any scale, and lets a shader cheaply reconstruct outlines, drop shadows, or glow — exactly the kind of stylized “damage number” look most games want, without shipping a huge pre-rendered texture per font size.

msdf-atlas-gen supports six output types:

  • msdf (default): Multi-channel distance field. Preserves sharp corners, which is usually the right choice for plain text-only atlases (a single display font, digits/letters only).
  • mtsdf: msdf plus a true single-channel SDF packed into the alpha channel. Useful if your material also wants a cheap outline/glow pass sampled from the same texture without a second draw.
  • sdf / psdf: Single-channel distance fields with progressively less corner fidelity than msdf, but cheaper to store (1 channel instead of 3) and immune to a specific msdf failure mode: complex, multi-shape glyphs — think icon glyphs with overlapping strokes, like Font Awesome’s biohazard — can produce visible color-seam artifacts in msdf/mtsdf output unless you carefully tune -coloringstrategy / -angle / -errorcorrection per glyph.
  • softmask / hardmask: Plain coverage masks, no distance field at all — rarely useful here.

Prebuilt Windows binaries are published on the Releases page — download msdf-atlas-gen.exe and skip straight to building an atlas.

To build from source instead, you’ll need CMake and vcpkg:

Terminal window
git clone https://github.com/Chlumsky/msdf-atlas-gen.git
cd msdf-atlas-gen
git submodule update --init --recursive
set VCPKG_ROOT=C:\path\to\vcpkg
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%\scripts\buildsystems\vcpkg.cmake
cmake --build build --config Release

3. Matching the Charset to FindSpriteIndex

Section titled “3. Matching the Charset to FindSpriteIndex”

The default FindSpriteIndex_Implementation on the Niagara renderer maps each displayed digit directly to its numeric value ('0' → sprite 0, '7' → sprite 7, …). For that to line up with your atlas, glyph order in the atlas must be 0-9, in that order — which is exactly what you get if you don’t specify a charset at all, since msdf-atlas-gen defaults to ASCII order and digits sort first among printable characters you’d typically keep.

To be explicit (and to keep only the glyphs you need, for a smaller atlas), pass a charset file:

digits.charset
['0','9']

If you also plan to override FindSpriteIndex to render words like "MISS" or "BLOCK" via ConvertStringToAlphabetIndices, append the lowercase alphabet immediately after the digits, so the letters start at a known, contiguous StartIndex (10, in this example):

digits-and-alphabet.charset
['0','9']
['a','z']
// In your FindSpriteIndex override, letters start right after the 10 digits:
TArray<int32> Indices = UMageItCombatTextRendererNiagara::ConvertStringToAlphabetIndices(TEXT("MISS"), /*StartIndex=*/10);

4. Real-World Recipe: Font + Icons in One Atlas

Section titled “4. Real-World Recipe: Font + Icons in One Atlas”

Here’s an actual production command used to build one of this plugin’s demo atlases — a full alphanumeric display font, a few punctuation marks, and four Font Awesome icon glyphs, all packed into one 1024×1024, 8-column uniform grid:

Terminal window
./msdf-atlas-gen.exe \
-font ./fonts/Anton/Anton-Regular.ttf -chars "['0', '9'], ['A', 'Z']" \
-and -font ./fonts/Anton/Anton-Regular.ttf -chars "'\"' '\!' '-'" \
-and -font "./fonts/fontawesome-free-7.2.0-desktop/otfs/Font Awesome 7 Free-Solid-900.otf" -chars "0xf06d 0xe4dc 0xf780 0xf004" \
-type sdf -format png -dimensions 1024 1024 \
-uniformgrid -uniformcols 8 -uniformcell 128 128 -uniformorigin off \
-pxrange 24 \
-imageout T_CT_AntonExample.png

The plugin’s five demo fonts (Anton, Bangers, Creepster, Luckiest Guy, Press Start 2P) each use this exact same recipe — only the -font path and -imageout filename change. That’s intentional: because every atlas shares the same charset order, -uniformcols, and -uniformcell, a single UV-lookup expression works against any of the five textures, so swapping a damage type’s visual style is just swapping which texture the SpriteMaterial points at — no material or index-math changes needed.

  • -and: Chains multiple font/charset inputs into a single combined atlas, in the order given. Here, the Anton font contributes two separate -chars blocks (alphanumerics, then punctuation), and the Font Awesome font contributes a third block of icon glyphs — all three end up as sequential glyphs in one output image.
  • Multiple -chars per font: -chars "['0', '9'], ['A', 'Z']" and a following -and -font <same font> -chars "'\"' '\!' '-'" are two separate input blocks for the same font file. Splitting them out lets you mix range syntax (['0','9']) and individually-escaped literal characters ('\"', '\!') without one syntax fighting the other in the same string. On Windows shells, " and ! need the backslash escape shown so the shell doesn’t consume them first.
  • 0xf06d 0xe4dc 0xf780 0xf004: Font Awesome glyphs are selected by codepoint, not character, since they live in Unicode Private-Use-Area ranges rather than printable ASCII. These four map to fire, burst, biohazard, and heart respectively — a natural fit for damage/status icons alongside numbers. Find codepoints for other icons via the Font Awesome cheatsheet (each icon’s detail page lists its unicode value) or the unicode field in Font Awesome’s published metadata/icons.json.
  • -type sdf: See the note above — chosen here specifically because the atlas mixes text with complex icon shapes.
  • -dimensions 1024 1024: A fixed atlas size (as opposed to -square2’s auto-sizing). With 43 total glyphs (26 letters + 10 digits + 3 punctuation + 4 icons) at 8 columns, that’s 6 rows × 128px cells = 768px of height actually used, leaving headroom in the fixed 1024×1024 canvas for adding more glyphs later without having to resize (and re-tune every material/index) again.
  • -uniformcell 128 128: Explicit, generous per-glyph cell size — larger than msdf-atlas-gen would auto-pick — so that thick display fonts (Anton, Bangers) and detailed icon glyphs both get enough padding to avoid clipping or bleeding into neighboring cells.
  • -uniformorigin off: Glyphs are fit tightly to their own bounding box within each cell, rather than aligning a shared baseline/origin position across all cells. Since every glyph here is looked up and drawn as an independent, self-contained sprite (not laid out relative to a shared text baseline), maximizing each glyph’s use of its cell reads better than baseline alignment would.
  • -pxrange 24: A wide distance-field range appropriate for the large 128px cells — gives the material plenty of headroom for thick outlines or soft glow before the falloff clips. Scale this roughly with cell size: the smaller -pxrange 4 example in the next section uses 64px cells, an eighth the area, and a proportionally tighter range.

Niagara’s User.DamageStyle array carries a raw sprite index (a float), so your material needs a trivial way to turn SpriteIndex into a UV rect. The simplest option is to force every glyph into an identical cell on a uniform grid, so the material only needs SpriteIndex → (col, row) → UV offset:

Terminal window
msdf-atlas-gen ^
-font "Roboto-Bold.ttf" ^
-charset "digits-and-alphabet.charset" ^
-type msdf ^
-format png ^
-uniformgrid ^
-uniformcols 6 ^
-square2 ^
-size 64 ^
-pxrange 4 ^
-imageout "T_MyDamageFont_SDF.png" ^
-json "T_MyDamageFont_SDF.json"
  • -uniformgrid / -uniformcols 6: Every glyph occupies an identically-sized cell, 6 columns wide (36 glyphs → 6 rows for the digits+alphabet charset above). This is what makes SpriteIndex → UV a two-line material expression instead of a per-glyph lookup table.
  • -square2: Rounds the atlas up to a power-of-two square, which plays nicest with GPU texture sampling and Unreal’s default texture compression/mip pipeline.
  • -size 64: Glyph size in pixels per em — pick this based on the largest on-screen size you expect digits to render at; SDF lets you scale down cleanly, but scaling far past the source size still softens edges.
  • -pxrange 4: Width of the distance field falloff in pixels. Larger values give the shader more room for thick outlines/glow at the cost of atlas resolution; 2-6 is the typical range.
  • -json: Not consumed by the plugin directly, but invaluable for cross-checking glyph-to-cell assignment while building your material, and for debugging index mismatches.

  1. Import the generated .png as a normal Texture2D.
  2. Set Texture Group to UI (or a custom group) and disable sRGB — distance field values are linear data, not color, and sRGB decoding will corrupt them.
  3. Set Compression Settings to UserInterface2D (RGBA) (or Grayscale for single-channel sdf/psdf output) to avoid block compression artifacts distorting the distance field near glyph edges.
  4. Disable mip generation, or ensure your -pxpadding is large enough that neighboring glyphs don’t bleed into each other once mips blend across cell boundaries. For a fixed on-screen digit size (the common case for combat text), disabling mips entirely is simplest and avoids this class of bug altogether.
  5. Assign the imported texture as the base texture on your SpriteMaterial (see UMageItCombatTextRendererNiagara::SpriteMaterial), and reconstruct the UV rect for a given SpriteIndex using the grid dimensions you generated with (-uniformcols, and the row count implied by glyph count ÷ columns).

7. Keeping DigitSize / DigitSpacing in Sync

Section titled “7. Keeping DigitSize / DigitSpacing in Sync”

The renderer’s DigitSize and DigitSpacing properties control on-screen sprite size and the horizontal offset between digits in a multi-digit number — they’re independent of the atlas’s own -size/cell dimensions. Match the aspect ratio of your uniform grid cell when picking DigitSize so glyphs don’t appear stretched, and set DigitSpacing to roughly the visible glyph width (not the full padded cell width) so digits sit close together instead of gapped.