[TextMeshPro] Dynamically copy a material for a new FontAsset

Hi,

I need to display localized text in multiple languages in my game. I’m using fallback fonts to support different scripts, which kind of works… Some characters are available in the primary font while others require fallback fonts, leading to inconsistent styling within the same text.

This results in a mix of different fonts in a single sentence, causing visual inconsistencies:

Screenshot 2025-01-30 150625


To fix this, I tried to detect which font can render all the characters and apply a tag to enforce consistency. However, when using the tag, TextMeshPro resets the material to the default one of the specified font instead of inheriting the material from the parent:

Is there a way to use the tag while keeping the material of the parent, similar to how fallback fonts behave?

Alternatively, is it possible to programmatically copy and apply the parent material to the new font?

Or is there a way to make TextMeshPro use the fallback font for the entire text if possible instead of only replacing missing characters?

Thanks in advance for any insights!

Ok, I found the way to copy the parent material for the new fontAsset,
Just call the function:

var newMaterial = TMP_MaterialManager.GetFallbackMaterial(originalMaterial, fontAsset.material);

So instead of applying a font tag I just assign the new fontAsset and the new material to the TMP_Text object.

If anyone interested here are my scripts:

The script to search for a system font that can display a text.

using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.TextCore.LowLevel;
using Debug = UnityEngine.Debug;

#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
using System.Linq;
#endif

namespace Utilities
{
    public static class LocalizableTextUtils
    {
        private static readonly List<TMP_FontAsset> DynamicFontAssets = new();

        public static bool EnableLogs = true;
        private static void Log( string txt )
        {
            if( EnableLogs )
                Debug.Log( txt );
        }
        
        public static void ClearDynamicFontAssets()
        {
            if( DynamicFontAssets?.Count > 0 )
            {
                foreach( var fontAsset in DynamicFontAssets )
                {
                    fontAsset.ClearFontAssetData(true);
                }
            }
        }

        public static TMP_FontAsset GetFontAssetForText( string texte )
        {
            if( TMP_Settings.defaultFontAsset.HasCharacters( texte ) )
            {
                Log( $"default font asset can show the text: {texte}" );
                return TMP_Settings.defaultFontAsset;
            }
            
            Log( $"le texte {texte} n'est pas affichable avec la font standard" );
            
            // defaultFontAsset does not support this text, trying DynamicFontAssets
            foreach( var fAsset in DynamicFontAssets )
            {
                if( fAsset.HasCharacters( texte ) )
                {
                    Log( $"the dynamic font {fAsset.name} can show the text: {texte}" );
                    return fAsset;
                }
            }
            
            // check in fallback fonts if any
            foreach( var fAsset in TMP_Settings.fallbackFontAssets )
            {
                if( fAsset.HasCharacters( texte ) )
                {
                    Log( $"the fallbackFont {fAsset.name} can show the text: {texte}" );
                    return fAsset;
                }
            }
            
            // still nothing found, check for devices fonts
            
            // Get paths to OS Fonts
            var fontPaths = Font.GetPathsToOSFonts();
            
            #if UNITY_ANDROID && !UNITY_EDITOR
            // filter fonts NotoSans -Regular on Android
            fontPaths = fontPaths.Where( name => name.Contains( "NotoSans" ) && name.Contains( "-Regular" ) ).ToArray();
            #elif UNITY_IOS && !UNITY_EDITOR
            // TODO filter fonts for iOS (find font list by languages)
            #endif
            
            foreach( var fontPath in fontPaths )
            {
                // Create new font object from one of those paths
                var osFont = new Font(fontPath);
                Log( $"checking device font: {osFont.name} - {fontPath}" );

                var fontok = false;
                if( FontEngine.LoadFontFace( osFont, 20 ) == FontEngineError.Success )
                {
                    fontok = true;
                    // internal lookup table to not check multiple similar characters
                    Dictionary<uint, uint> m_CharacterLookupDictionary = new();
                    foreach( uint unicode in texte )
                    {
                        // Check if character is already contained in the character table.
                        if( !m_CharacterLookupDictionary.TryGetValue( unicode, out var index ) )
                        {
                            FontEngine.TryGetGlyphIndex( unicode, out index );
                            if( index == 0 )
                            {
                                // Special handling for characters with potential alternative glyph representations
                                switch( unicode )
                                {
                                    case 0xA0: // Non Breaking Space <NBSP>
                                        // Use Space
                                        FontEngine.TryGetGlyphIndex( 0x20, out index );
                                        break;
                                    case 0xAD:   // Soft Hyphen <SHY>
                                    case 0x2011: // Non Breaking Hyphen
                                        // Use Hyphen Minus
                                        FontEngine.TryGetGlyphIndex( 0x2D, out index );
                                        break;
                                }
                            }
                            
                            m_CharacterLookupDictionary.Add( unicode, index );
                        }

                        if( index == 0 )
                        {
                            fontok = false;
                            break;
                        }
                    }
                }

                if( fontok )
                {
                    Log( $"the deviceFont font: {osFont.name} can show the text: {texte}" );
                    
                    // Create new dynamic font asset
                    var fontAsset  = TMP_FontAsset.CreateFontAsset(osFont,120,20,GlyphRenderMode.SDFAA,512,512);
                    fontAsset.name = osFont.name;
                    // add it to the dynamic font list
                    DynamicFontAssets.Add( fontAsset );
                    
                    // add it to fallbakFontAssets ?
                    // TMP_Settings.fallbackFontAssets.Add( fontAsset );
                    
                    return fontAsset;
                }
            }

            Log( $"no font found to show the text: {texte}" );
            
            return null;
        }
    }
}

And the one to set the text

using TMPro;
using UnityEngine;
using Utilities;

namespace TextUtils
{
    [RequireComponent(typeof(TMP_Text))]
    public class LocalizableText : MonoBehaviour
    {
        private TMP_Text      tmp;
        private Material      originalMaterial;
        private TMP_FontAsset originalFontAsset;
        private bool          IsInit;
        
        private void Awake()    { Init(); }
        private void OnEnable() { Init(); }

        private void Init()
        {
            if( IsInit )
                return;
            
            tmp = GetComponent<TMP_Text>();
            if( tmp != null )
            {
                originalMaterial  = tmp.fontSharedMaterial;
                originalFontAsset = tmp.font;
                IsInit            = true;
            }
        }
        
        public void SetText( string localizedText, string fallbackText )
        {
            Init();
            if( !IsInit )
            {
                Debug.LogError( "NO TMP_Text found" );
                return;
            }
            
            var mat       = originalMaterial;
            var fontAsset = LocalizableTextUtils.GetFontAssetForText( localizedText );
            if( fontAsset != null )
            {
                if( fontAsset != originalFontAsset )
                    mat = TMP_MaterialManager.GetFallbackMaterial(originalMaterial, fontAsset.material);
                
                tmp.text               = localizedText;
                tmp.fontSharedMaterial = mat;
                tmp.font               = fontAsset;
            }
            else
            {
                tmp.text               = fallbackText;
                tmp.fontSharedMaterial = mat;
                tmp.font               = originalFontAsset;
            }
        }
    }
}