using Cryville.Common.Font; using Cryville.Culture; using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; using TMPro; using UnityEngine; using UnityEngine.TextCore; using UnityEngine.TextCore.LowLevel; using AtlasPopulationMode = TMPro.AtlasPopulationMode; namespace Cryville.Common.Unity.UI { [RequireComponent(typeof(TMP_Text))] public class TMPLocalizedText : MonoBehaviour { public static Shader DefaultShader; public static FontMatcher FontMatcher; public static int MaxFallbackCount = 4; static readonly Dictionary _cachedFonts = new(); [SerializeField] Shader m_shader; public TMP_Text Text { get; private set; } void Awake() { Text = GetComponent(); } public void SetText(string text, CultureInfo culture = null) { Text.text = text; SetCulture(culture ?? CultureInfo.CurrentCulture); } void SetCulture(CultureInfo culture) { if (FontMatcher == null) return; string cultureName = culture.Name; if (string.IsNullOrEmpty(cultureName)) cultureName = CultureInfo.CurrentCulture.Name; if (!_cachedFonts.TryGetValue(culture, out var font)) { foreach (var typeface in FontMatcher.MatchLanguage(new LanguageId(cultureName), true)) { try { var ifont = CreateFontAsset(typeface.File.FullName, typeface.IndexInFile); if (m_shader) ifont.material.shader = m_shader; else if (DefaultShader) ifont.material.shader = DefaultShader; if (font == null) { font = ifont; if (MaxFallbackCount <= 0) break; } else { font.fallbackFontAssetTable ??= new List(); font.fallbackFontAssetTable.Add(ifont); if (font.fallbackFontAssetTable.Count >= MaxFallbackCount) break; } } catch (Exception) { } } _cachedFonts.Add(culture, font); } Text.font = font; } static TMP_FontAsset CreateFontAsset(string path, int index) => CreateFontAsset(path, index, 90, 9, GlyphRenderMode.SDFAA, 1024, 1024, AtlasPopulationMode.Dynamic); static readonly Lazy _f_m_Version = new(() => typeof(TMP_FontAsset).GetField("m_Version", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); static readonly Lazy _p_atlasWidth = new(() => typeof(TMP_FontAsset).GetProperty("atlasWidth", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); static readonly Lazy _p_atlasHeight = new(() => typeof(TMP_FontAsset).GetProperty("atlasHeight", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); static readonly Lazy _p_atlasPadding = new(() => typeof(TMP_FontAsset).GetProperty("atlasPadding", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); static readonly Lazy _p_atlasRenderMode = new(() => typeof(TMP_FontAsset).GetProperty("atlasRenderMode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); static readonly Lazy _p_freeGlyphRects = new(() => typeof(TMP_FontAsset).GetProperty("freeGlyphRects", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); static readonly Lazy _p_usedGlyphRects = new(() => typeof(TMP_FontAsset).GetProperty("usedGlyphRects", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); static readonly Lazy _p_ShaderRef_MobileBitmap = new(() => typeof(ShaderUtilities).GetProperty("ShaderRef_MobileBitmap", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)); static readonly Lazy _p_ShaderRef_MobileSDF = new(() => typeof(ShaderUtilities).GetProperty("ShaderRef_MobileSDF", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)); public static TMP_FontAsset CreateFontAsset(string path, int faceIndex, int samplingPointSize, int atlasPadding, GlyphRenderMode renderMode, int atlasWidth, int atlasHeight, AtlasPopulationMode atlasPopulationMode = AtlasPopulationMode.Dynamic, bool enableMultiAtlasSupport = true) { // Initialize FontEngine FontEngine.InitializeFontEngine(); // Load Font Face if (FontEngine.LoadFontFace(path, samplingPointSize, faceIndex) != FontEngineError.Success) { Debug.LogWarning("Unable to load font face at path " + path); return null; } // Create new font asset TMP_FontAsset fontAsset = ScriptableObject.CreateInstance(); _f_m_Version.Value.SetValue(fontAsset, "1.1.0"); fontAsset.faceInfo = FontEngine.GetFaceInfo(); fontAsset.atlasPopulationMode = atlasPopulationMode; _p_atlasWidth.Value.SetValue(fontAsset, atlasWidth); _p_atlasHeight.Value.SetValue(fontAsset, atlasHeight); _p_atlasPadding.Value.SetValue(fontAsset, atlasPadding); _p_atlasRenderMode.Value.SetValue(fontAsset, renderMode); // Initialize array for the font atlas textures. fontAsset.atlasTextures = new Texture2D[1]; // Create and add font atlas texture. var texture = new Texture2D(0, 0, TextureFormat.Alpha8, false); fontAsset.atlasTextures[0] = texture; fontAsset.isMultiAtlasTexturesEnabled = enableMultiAtlasSupport; // Add free rectangle of the size of the texture. int packingModifier; if (((int)renderMode & 0x10) != 0) { packingModifier = 0; // Optimize by adding static ref to shader. var tmp_material = new Material((Shader)_p_ShaderRef_MobileBitmap.Value.GetValue(null)); //tmp_material.name = texture.name + " Material"; tmp_material.SetTexture(ShaderUtilities.ID_MainTex, texture); tmp_material.SetFloat(ShaderUtilities.ID_TextureWidth, atlasWidth); tmp_material.SetFloat(ShaderUtilities.ID_TextureHeight, atlasHeight); fontAsset.material = tmp_material; } else { packingModifier = 1; // Optimize by adding static ref to shader. var tmp_material = new Material((Shader)_p_ShaderRef_MobileSDF.Value.GetValue(null)); //tmp_material.name = texture.name + " Material"; tmp_material.SetTexture(ShaderUtilities.ID_MainTex, texture); tmp_material.SetFloat(ShaderUtilities.ID_TextureWidth, atlasWidth); tmp_material.SetFloat(ShaderUtilities.ID_TextureHeight, atlasHeight); tmp_material.SetFloat(ShaderUtilities.ID_GradientScale, atlasPadding + packingModifier); tmp_material.SetFloat(ShaderUtilities.ID_WeightNormal, fontAsset.normalStyle); tmp_material.SetFloat(ShaderUtilities.ID_WeightBold, fontAsset.boldStyle); fontAsset.material = tmp_material; } _p_freeGlyphRects.Value.SetValue(fontAsset, new List(8) { new(0, 0, atlasWidth - packingModifier, atlasHeight - packingModifier) }); _p_usedGlyphRects.Value.SetValue(fontAsset, new List(8)); // TODO: Consider adding support for extracting glyph positioning data fontAsset.ReadFontAssetDefinition(); return fontAsset; } } }