Workflow for TextMesh Pro sprite glyphs?
I have a folder with sprites which I want to use as glyphs in TextMesh Pro textboxes. From time to time, new sprites are added to that folder. I want to organize a workflow in such way that every time I add a sprite called xxx, I could use it as <sprite name="xxx"> in TextMesh.
I tried to create a Sprite Atlas from folder, but I cannot find any way to export the Sprite Atlas into a texture. Currently, I need to place all new sprites manually onto a big texture and re-create the entire TextMesh Sprite Asset file. How can I improve my workflow? (1 - automatically convert an atlas into a texture [Sprite Asset is not created if an atlas is selected, it wants specifically a texture], 2 - refresh the Sprite Asset file so it reflects changes in an underlying texture)
Answer by androniq · May 09, 2020 at 06:57 AM
Finally came out with this solution.
 using System.IO;
 using System.Linq;
 using TMPro;
 using UnityEngine;
 using UnityEngine.TextCore;
 
 namespace Assets.Scripts.Utilities
 {
     public class Glypher : MonoBehaviour
     {
         public int GlyphWidth = 32;
         public int GlyphHeight = 32;
         public int GlyphsInRow = 8;
 
         public TMP_SpriteAsset Asset;
 
         public void RepackGlyphs()
         {
             Debug.Log("Repacking glyphs...");            
             var path = Path.Combine(Directory.GetCurrentDirectory(), @"Assets\Resources\Glyphs");
             var outputPath = Path.Combine(Directory.GetCurrentDirectory(), @"Assets\Resources\GlyphBuild\texture.png");
             var files = Directory.EnumerateFiles(path, "*.png").ToList();
             var rowNumber = (files.Count - 1) / GlyphsInRow + 1;
             var completeWidth = GlyphsInRow * GlyphWidth;
             var completeHeight = GlyphHeight * rowNumber;
             var image = new Texture2D(completeWidth, completeHeight, TextureFormat.BGRA32, false);
             var yIndex = rowNumber - 1;
             var xIndex = 0;
             Asset?.spriteGlyphTable.Clear();
             Asset?.spriteCharacterTable.Clear();
             uint index = 0;
             foreach (var glyph in files)
             {
                 var texture = new Texture2D(GlyphWidth, GlyphHeight);
                 using (var file = new FileStream(glyph, FileMode.Open))
                 {
                     using (var memstream = new MemoryStream())
                     {
                         file.CopyTo(memstream);
                         var tbytes = memstream.ToArray();
                         texture.LoadImage(tbytes);
                     }
                 }
                 var pixels = texture.GetPixels32();
                 image.SetPixels32(xIndex * GlyphWidth, yIndex * GlyphHeight, GlyphWidth, GlyphHeight, pixels);
 
                 var spriteGlyph = new TMP_SpriteGlyph
                 {
                     glyphRect = new GlyphRect { width = GlyphWidth, height = GlyphHeight - 1, x = xIndex * GlyphWidth, y = yIndex * GlyphHeight },
                     metrics = new GlyphMetrics { width = GlyphWidth, height = GlyphHeight - 1, horizontalBearingY = 28, horizontalBearingX = 0, horizontalAdvance = 32 },
                     index = index
                 };
                 Asset?.spriteGlyphTable.Add(spriteGlyph);
 
                 var spriteCharacter = new TMP_SpriteCharacter(0, spriteGlyph)
                 {
                     name = Path.GetFileNameWithoutExtension(glyph),
                     glyphIndex = index++
                 };
                 Asset?.spriteCharacterTable.Add(spriteCharacter);
 
                 xIndex++;
                 if (xIndex >= GlyphsInRow)
                 {
                     xIndex = 0;
                     yIndex--;
                 }
             }
 
             var bytes = image.EncodeToPNG();
             using (var stream = new FileStream(outputPath, FileMode.Create))
             {
                 stream.Write(bytes, 0, bytes.Length);
             }
 
             if (Asset == null)
                 Debug.LogWarning("Right-click the created texture, Create > TextMeshPro > Sprite Asset and specify the resulting asset in Glypher and in TMP Settings, then repack glyphs again");
             else
                 Debug.Log("Repacking glyphs done!");
         }
     }
 }
 
 
               Then, I have a custom editor to run this script from a button click:
 using UnityEditor;
 using UnityEngine;
 
 namespace Assets.Scripts.Utilities
 {
     [CustomEditor(typeof(Glypher))]
     public class GlypherEditor : Editor
     {
         public override void OnInspectorGUI()
         {
             DrawDefaultInspector();
 
             var glypher = (Glypher)target;
             if (GUILayout.Button("Repack glyphs"))
             {
                 glypher.RepackGlyphs();
             }
         }
     }
 }
 
 
               Finally, I create the sprite asset manually - but only for the first time. Then I specify this asset in Glypher component properties (I have a separate GameObject for devtime utilities, and this GameObject can be safely removed on game startup) AND in TMP Settings (possibly there's a way to do that programmatically also, but since it's a one-time action, I didn't investigate this possibility). Every time new pngs are added to Assets\Resources\Glyphs folder, I then click Repack Glyphs on Glypher inspector, and my glyph table gets updated. Names of the glyphs are taken from file names. A few notes:
This code relies completely on assumption that all your glyphs are the same size. If it's not 32*32, you have to specify it in GlyphWidth&GlyphHeight properties of the Glypher.
Possibly a SpriteAtlas can be used to pack textures more efficiently, but again - I didn't run into limitations yet, so didn't check this possibility.
For the first run you may leave texture property unset - it will create the texture and instruct you of further actions in the log.
Hope it's helpful for anyone else.
Your answer