In Unity's standard text rendering component, TextMeshPro (TMP), supporting languages with large character counts like Japanese, Chinese, and Korean (CJK) is usually done using Dynamic Font Assets. However, as localizations grow and large volumes of dynamic text are displayed, developers often face a bug where characters suddenly turn into empty boxes (□, known as "tofu") or go completely missing during long play sessions. This article details the internal mechanism of dynamic font atlas exhaustion and how to optimize settings and resolve this bug in production.
1. Root Cause: Memory Exhaustion in the Dynamic Font Atlas
A "Dynamic" TMP Font Asset starts with no glyph images in its texture atlas. Instead, whenever a string is assigned to a text component at runtime, it triggers the following pipeline:
- The text component checks if all characters in the string are already rasterized in the active texture atlas.
- If a new character is detected, TMP loads its glyph data from the source font file (TTF/OTF).
- The character's glyph is rasterized and written onto an empty region of the font atlas texture in memory, and its UV coordinates are registered.
- Subsequent renderings of this character pull directly from the updated texture.
While this approach is highly flexible, the texture atlas size has a fixed limit (defaulting to 512x512 pixels). For multi-byte languages with thousands of unique characters, the free space on the atlas is consumed rapidly as the player encounters more dialogue.
Once the atlas is full and no new glyphs can be written, TMP fails to render any new characters, falling back to the undefined character glyph—the "tofu" (□) or a blank space. This is the root cause of the dynamic atlas exhaustion bug.
2. Solution 1: Optimize Atlas Resolution and Padding
The first step is expanding the atlas bounds to fit more characters and adjusting margins for optimal layout efficiency.
- In the Project window, select your dynamic TMP Font Asset.
- Expand the Generation Settings in the Inspector.
- Increase Atlas Width and Atlas Height. If the default is 512x512, bump it to 1024x1024 for CJK languages, or 2048x2048 for extensive multilingual support.
- Adjust the Padding value. The default is `9`, which keeps a wide safety margin between glyphs but consumes valuable atlas space. For atlases of 1024x1024 or higher, reduce Padding to 5 or 4. This can increase the maximum character capacity by up to 50% (※Be careful: setting padding too low can cause visual artifacts like bleeding or outline artifacts when viewed from a distance).
| Atlas Size | Recommended Padding | Estimated Max CJK Characters |
|---|---|---|
| 512 x 512 | 9 | ~150 to 300 (exhausts easily with basic sentences) |
| 1024 x 1024 | 5 | ~800 to 1,200 (covers common level-1 kanji sets) |
| 2048 x 2048 | 5 | ~3,000 to 4,000 (covers most common gameplay and dialogue texts) |
3. Solution 2: Pre-bake Common Glyphs (Hybrid Static/Dynamic Setup)
Relying 100% on dynamic rasterization can cause performance hiccups (frame spikes) at runtime when a new block of dialogue displays. To resolve this, pre-bake common, high-frequency characters into the atlas using a hybrid approach.
- Open the TMP Font Asset Creator (
Window > TextMeshPro > Font Asset Creator). - Select your font source and set the "Character Set" to "Custom Characters".
- Paste basic characters (alphanumerics, hiragana, katakana, and around 1,000 common kanji) and generate the asset.
- Select the generated font asset and change the Atlas Population Mode to Dynamic.
By combining static baking with dynamic population, common characters are available instantly as high-quality SDF textures, while rare characters or custom player names are loaded dynamically only when they appear, saving CPU spikes and memory.
4. Solution 3: Detect Full Atlas Event and Reset Cache via C#
No matter how large your atlas is, persistent chat input or endless dialogue can eventually fill up the texture buffer. Setting up an automated fallback to clear and rebuild the dynamic cache prevents the tofu bug from ever appearing to the user.
When the atlas is full and fails to add a character, TextMeshPro triggers structural changes. You can hook into TMPro_EventManager.FONT_PROPERTY_WAS_CHANGED to verify glyph counts and clear the cache dynamically during scene transitions or loading states.
using UnityEngine;
using TMPro;
public class TMPFontAtlasManager : MonoBehaviour
{
public TMP_FontAsset targetFontAsset;
[Tooltip("Clear the dynamic cache once character count in the atlas exceeds this threshold")]
public int warningCharacterCount = 2500;
void OnEnable()
{\
// Subscribe to TextMeshPro property change events
TMPro_EventManager.FONT_PROPERTY_WAS_CHANGED.AddListener(OnFontPropertyChanged);
}
void OnDisable()
{
TMPro_EventManager.FONT_PROPERTY_WAS_CHANGED.RemoveListener(OnFontPropertyChanged);
}
private void OnFontPropertyChanged(bool isChanged, Object obj)
{
if (obj == targetFontAsset)
{
CheckAtlasStatus();
}
}
public void CheckAtlasStatus()
{
if (targetFontAsset == null) return;
// Count current glyphs in the dynamic table
int currentGlyphCount = targetFontAsset.glyphTable.Count;
Debug.Log($"[TMP Font Atlas] Current Glyphs in Atlas: {currentGlyphCount} / {targetFontAsset.atlasWidth}x{targetFontAsset.atlasHeight}");
if (currentGlyphCount > warningCharacterCount)
{
Debug.LogWarning("[TMP Font Atlas] Glyphs exceeded threshold. Rebuilding atlas to prevent tofu characters...");
ClearDynamicAtlas();
}
}
/// <summary>
/// Resets the dynamic glyph cache to free up atlas texture space.
/// Note: This triggers UI layout updates. Best called during screen transitions or loading screens.
/// </summary>
public void ClearDynamicAtlas()
{
if (targetFontAsset == null || targetFontAsset.atlasPopulationMode != AtlasPopulationMode.Dynamic) return;
// Clear dynamic texture tables
targetFontAsset.ClearFontAssetData(true);
// Notify all TMP UI components to refresh their materials
TMPro_EventManager.ON_FONT_PROPERTY_CHANGED(true, targetFontAsset);
Debug.Log("[TMP Font Atlas] Dynamic font atlas cache cleared and reset successfully.");
}
}
Deploying this manager lets the system clear the cache in the background during scene transitions, avoiding visual anomalies and keeping the font rendering clean across infinite gameplay hours.