Search Unity

TextMesh Pro TMP_FontAsset metrics aren't friendly to customization

Discussion in 'UGUI & TextMesh Pro' started by tessellation, Feb 17, 2020.

  1. tessellation

    tessellation

    Joined:
    Aug 11, 2015
    Posts:
    390
    The TMP FontAsset inspector allows you to modify font metrics (FaceInfo) such as baseline, ascentLine, underlineOffset, lineHeight, etc. so that you can manually tune the font metrics as needed. We use this a lot with Chinese, Japanese, and Korean fonts especially.

    We recently upgraded from TMP version 1.0.56 to Unity 2018.4 with TMP package 1.5.0-preview.4 It's nice that the font creation information is preserved when you click [Update Atlas Texture]. However, as before, when you save the new atlas, the font asset metrics (FaceInfo) always get overwritten from the default in the source OTF font.

    To solve this in the past, we wrote a script to preserve the font metrics after regenerating a static font. It even handles the case when the point size changes. For example, say we had created a FontAsset with point size 40 and a manually-tuned line height of 50. After regenerating the atlas with added glyphs, the auto point sizing has to be smaller to make the new glyphs fit: in this example it becomes 35. Since the line height and other metrics are proportional to point size, we wrote a script that fixes up the font metrics after going from 40 to 35. In this case the new line height would be set to 43.75 = 50 * 35 / 40:
    NewMetric = OldMetric * NewPointSize / OldPointSize

    Now after upgrading TMP, while you can still edit the metrics from the UI, it appears as if you're no longer able to modify the FontAsset faceInfo from a custom editor script. Is there an API exposed to do this?

    We would love it if this editor script we wrote to fix the metrics wasn't needed any more; there are two solutions to achieve this. The first and best solution would be if TextMeshPro automatically preserve the metrics when regenerating existing fonts, as described above, when the point size changes and the source font is the same.

    The second (less ideal) solution is if we could keep the point size fixed at, say, 40 and just dynamically create as many smaller atlases as needed to build the font. This is supported in the new 1.5.0 preview! However, we cannot use dynamic fonts for Chinese at runtime because the Noto Sans OTF fonts for CJK would take up 110MB, which is larger than our entire APK right now on Android! What we really need is an "Atlas Population Mode" of "Dynamic In Editor, Static at Runtime" which would ensure that the original OTF file is not referenced in the builds. With this solution, the metrics could also be manually tuned and presumable with Dynamic font generation, the metrics would not get overwritten.

    Thanks so much @Stephan_B
     
  2. tessellation

    tessellation

    Joined:
    Aug 11, 2015
    Posts:
    390
    OK, so after testing this dynamic font idea more, this could work in theory, but the problem right now is that Dynamic multi-atlas fonts don't get baked down to the FontAsset when you switch from Dynamic to Static population mode in the Font Asset Inspector. So all the additional atlases are lost and there's no way to generate multiple atlases statically and no way to generate any static atlases without blowing up the custom font metrics. So we're stuck with one atlas that has to hold all the glyphs and that means whenever we add new glyphs, the point size might have to shrink to fit them all.

    You might be asking - why do we want custom font metrics? What is our use case? Because when we localize using I2 Localization, the TextMeshProUGUI font is changed, depending on which language we're targeting. The UGUI layouts are designed for a certain font size and line spacing, so when we swap fonts, we expect the UI layout to look mostly correct, regardless of the language and font used. In order to make this work, we have to tune the font metrics for several fonts so that the metrics match as closely as they can to the Latin fonts for which the original UI was designed. These metrics, again, are things like line spacing, font scaling, underlines, etc. Being able to adjust the metrics also means that fallback fonts can be intermixed with the parent font if the metrics are closely matched. You can see from this that figuring out what the correct metrics should be is a tedious process and we have created special scenes just for lining up fonts to tune the metrics and make sure they visually match up. This is why we created scripts to properly maintain the metrics so that we don't have to go through this tedious process every time the atlas point size changes.
     
  3. tessellation

    tessellation

    Joined:
    Aug 11, 2015
    Posts:
    390
    Well, good news, turns out I was wrong about this, but I think I did find a minor bug. After switching the Dynamic font back to Static, I noticed that the font's atlas count was still 5 in my debugging script, not the expected 1 (originally I had statically generated just 1 atlas). So I right-clicked on the font asset and selected "Reimport", now all 5 atlas textures appeared. So I guess it's working, there's just a problem with refreshing the project view of the font asset.

    The process of calling ClearFontAssetData(true) and TryAddCharacters(glyphs, true) does not overwrite the font metrics. This is good.
     
    Last edited: Feb 18, 2020
  4. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    I will still look at potential solutions for preserving edited values in the Face Info when the Sampling Point Size changes.
     
  5. tessellation

    tessellation

    Joined:
    Aug 11, 2015
    Posts:
    390
    Thanks, this would be great. It would also be nice to have script/API access to the FaceInfo as well. Maybe a faceInfo property setter?
     
  6. rubeng

    rubeng

    Joined:
    Apr 20, 2013
    Posts:
    60
    Hi, we have the exact same issue, we modified the face info values to make the text look better on our game, and now when we regenerate it gets overwritten, and also we expect to need in the future to do what @tessellation said, and need to force the face info to match different fonts for Chinese, Korean, Japanese

    Is a reasonable workaround to edit the fonts in something like FontForge to change the faceInfo to match our desired values? or does that break anything else? (also not sure what would be the exact matching properties from textmeshpro to fontforge)

    Ideally we would be able to override the values in a sampling point neutral way, and keep that when regenerating the fonts

    Thanks for any information

    Rubén
     
  7. tessellation

    tessellation

    Joined:
    Aug 11, 2015
    Posts:
    390
    In preview.7 and after, you can now write to the font.faceInfo. We use a tool internally that saves the font information prior to regeneration and then adjusts/preserves the metrics afterwards. I'm sharing the class we use for doing this. You just call SavedFontInfo.Save(fontBefore) and then SavedFontInto.Apply(fontAfter).

    Code (CSharp):
    1. [System.Serializable]
    2. class SavedFontInfo
    3. {
    4.     public FaceInfo faceInfo;
    5.     public int glyphCount;
    6.     public int atlasCount;
    7.     public int atlasWidth;
    8.     public int atlasHeight;
    9.     public int atlasPadding;
    10.  
    11.     public void Save(TMP_FontAsset font)
    12.     {
    13.         glyphCount = font.glyphTable.Count;
    14.         atlasWidth = font.atlasWidth;
    15.         atlasHeight = font.atlasHeight;
    16.         atlasPadding = font.atlasPadding;
    17.         atlasCount = font.atlasTextureCount;
    18.  
    19.         faceInfo = CopyFaceInfo(font.faceInfo);
    20.     }
    21.  
    22.     protected static FaceInfo CopyFaceInfo(FaceInfo from, float scale = 1f)
    23.     {
    24.         FaceInfo to = from; // struct is copied!
    25.         if (scale != 1f)
    26.         {
    27.             to.baseline *= scale;
    28.             to.strikethroughOffset *= scale;
    29.             to.strikethroughThickness *= scale;
    30.             to.underlineOffset *= scale;
    31.             to.underlineThickness *= scale;
    32.             to.subscriptOffset *= scale;
    33.             to.subscriptSize *= scale;
    34.             to.superscriptOffset *= scale;
    35.             to.superscriptSize *= scale;
    36.             to.descentLine *= scale;
    37.             to.meanLine *= scale;
    38.             to.capLine *= scale;
    39.             to.ascentLine *= scale;
    40.             to.lineHeight *= scale;
    41.         }
    42.         return to;
    43.     }
    44.  
    45.     public void Apply(TMP_FontAsset font)
    46.     {
    47.         if (font.faceInfo.pointSize == 0 || faceInfo.pointSize == 0)
    48.         {
    49.             Debug.LogError("Zero point size found in font, unable to apply saved font info!", font);
    50.             return;
    51.         }
    52.  
    53.         // By what ratio does the new font point size change?
    54.         float ratio = (float)font.faceInfo.pointSize / (float)faceInfo.pointSize;
    55.         faceInfo.pointSize = font.faceInfo.pointSize; // Adopt the new font point size
    56.         font.faceInfo = CopyFaceInfo(faceInfo, ratio);
    57.  
    58.         atlasWidth = font.atlasWidth;
    59.         atlasHeight = font.atlasHeight;
    60.         atlasPadding = font.atlasPadding;
    61.         glyphCount = font.glyphTable.Count;
    62.         atlasCount = font.atlasTextureCount;
    63.  
    64.         TMPro_EventManager.ON_FONT_PROPERTY_CHANGED(true, font);
    65.         EditorUtility.SetDirty(font);
    66.     }
    67. }
     
    j1mmie and Stephan_B like this.
  8. rubeng

    rubeng

    Joined:
    Apr 20, 2013
    Posts:
    60
    Thanks for the code sample, for the current case we just discarded the faceinfo changes before commiting, but this will help when we have more fonts, would love to test just editting the source fonts outside of unity, but am not sure if that may have any issues.
     
  9. rubeng

    rubeng

    Joined:
    Apr 20, 2013
    Posts:
    60
    Hi, sorry for reviving this old thread but, has there been any improvement with being able to keep the font face customizations when regenerating (officially I mean), also since we started our localization process now we have more fonts to maintain and solving this is getting more important.

    I was unable to find a way to regenerate the font by code, so that we can automate the regeneration when the needed chars change (we started with automatically translated text and now we are getting the real ones).

    I was able to check if the font supports all the needed chars with something like this

    Code (CSharp):
    1.  
    2. var textAssetPath = AssetDatabase.GUIDToAssetPath(fontAsset.creationSettings.referencedTextAssetGUID);
    3. var textAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(textAssetPath);
    4. var charsString = textAsset.text;
    5. List<char> missingCharList;
    6. fontAsset.HasCharacters(charsString, out missingCharList);
    7. if (missingCharList.Count == 0)
    8. {
    9.     Debug.Log($"Font: {fontAsset.name} is valid");
    10. }
    11. else
    12. {
    13.     StringBuilder builder = new StringBuilder();
    14.     builder.AppendLine($"Font: {fontAsset.name} is INVALID");
    15.     foreach (var theChar in missingCharList)
    16.     {
    17.         builder.AppendLine($"Char: {theChar} - u:{((int) theChar).ToString("X4")}");
    18.     }
    19.  
    20.     Debug.LogError(builder.ToString());
    21. }
    22.  
    Thanks for any information

    Rubén