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.
1. Why SDF Atlases
Section titled “1. Why SDF Atlases”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:msdfplus 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 thanmsdf, but cheaper to store (1 channel instead of 3) and immune to a specificmsdffailure mode: complex, multi-shape glyphs — think icon glyphs with overlapping strokes, like Font Awesome’sbiohazard— can produce visible color-seam artifacts inmsdf/mtsdfoutput unless you carefully tune-coloringstrategy/-angle/-errorcorrectionper glyph.softmask/hardmask: Plain coverage masks, no distance field at all — rarely useful here.
2. Installing the Tool
Section titled “2. Installing the Tool”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:
git clone https://github.com/Chlumsky/msdf-atlas-gen.gitcd msdf-atlas-gengit submodule update --init --recursiveset VCPKG_ROOT=C:\path\to\vcpkgcmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%\scripts\buildsystems\vcpkg.cmakecmake --build build --config Release3. 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:
['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):
['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:
./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.pngThe 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.
Flag-by-flag
Section titled “Flag-by-flag”-and: Chains multiple font/charset inputs into a single combined atlas, in the order given. Here, the Anton font contributes two separate-charsblocks (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
-charsper 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 tofire,burst,biohazard, andheartrespectively — 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 theunicodefield in Font Awesome’s publishedmetadata/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) at8columns, 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 large128pxcells — 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 4example in the next section uses64pxcells, an eighth the area, and a proportionally tighter range.
5. Minimal Digit-Only Recipe
Section titled “5. Minimal Digit-Only Recipe”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:
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 makesSpriteIndex → UVa 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-6is 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.
6. Importing into Unreal
Section titled “6. Importing into Unreal”- Import the generated
.pngas a normalTexture2D. - 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. - Set Compression Settings to
UserInterface2D (RGBA)(orGrayscalefor single-channelsdf/psdfoutput) to avoid block compression artifacts distorting the distance field near glyph edges. - Disable mip generation, or ensure your
-pxpaddingis 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. - Assign the imported texture as the base texture on your
SpriteMaterial(seeUMageItCombatTextRendererNiagara::SpriteMaterial), and reconstruct the UV rect for a givenSpriteIndexusing 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.