├── README.md └── Editor └── Fnt2TMPro ├── Fnt2TMPro.cs └── CustomFntParse.cs /README.md: -------------------------------------------------------------------------------- 1 | # fnt2TMPro 2 | Unity tool to convert Bitmap Font to Text Mesh Pro asset 3 | 4 | ## Install 5 | Remember to have TextMeshPro imported into your project. 6 | Download and import all files into your Unity project. 7 | 8 | ## Usage 9 | 10 | ### 1. Create a dummy TMPro font asset 11 | 12 | In Unity, go to **Windows\TextMeshPro\Font Asset Creator**. Fill in these settings as below 13 | 14 | ![font_asset_creator](https://github.com/napbla/fnt2TMPro/blob/readme/images/font_asset_creator.png?raw=true) 15 | 16 | >**Source font file**: choose a true type font that has all your characters in bitmap font. For example Lato, Arial... 17 | 18 | >**Sampling font file**: look at your bitmap font description file (.fnt or .txt), it is the size="" part. It you don't know it just leave it there and the script will patch it automatically. 19 | ![fnt file with size part](https://github.com/napbla/fnt2TMPro/blob/readme/images/fnt_size.png?raw=true) 20 | 21 | >**Padding**: 1 because we don't want to miss any characters in the atlas 22 | 23 | >**Packing Method**: **Fast** (If TMPro can not pack all the characters but your bitmap font can then choose "Optimum") 24 | 25 | >**Atlas Resolution**: Look at your bitmap font texture and fill in. 26 | 27 | >**Character Set**: **Custom Character**, fill in your **Custom Character List** all the characters appeared in your bitmap font 28 | 29 | >**Render Mode**: **RASTER** 30 | 31 | Press **Generate Font Atlas** and **Save as...** to your desired font name. 32 | 33 | ### 2. Patch this dummy font by your bitmap font 34 | 35 | Goto **Windows\Bitmap Font Converter** 36 | 37 | ![bmp font converter](https://github.com/napbla/fnt2TMPro/blob/readme/images/bmp_font_converter.png?raw=true) 38 | 39 | Drag your bitmap font texture to **Font Texture** field 40 | 41 | Drag your text file or fnt file to **Source Font File** field 42 | 43 | Drag your dummy TMPro font asset that you have just created in step 1 to **Destination Font File** field. 44 | 45 | Press **Convert** . Now your dummy font is your bitmap font. 46 | 47 | ## Q&A 48 | 1. What if I do not have the fnt or txt file ? 49 | 50 | >Use ShoeBox , it's free : https://renderhjs.net/shoebox/ 51 | 52 | 2. How to support this project ? 53 | 54 | >You can support me by doing any of these things 55 | 56 | * Star this project 57 | 58 | * Report bugs or fix and create a pull request 59 | -------------------------------------------------------------------------------- /Editor/Fnt2TMPro/Fnt2TMPro.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using UnityEngine.TextCore; 4 | 5 | using TMPro; 6 | using litefeelcustom; 7 | 8 | namespace Fnt2TMPro.EditorUtilities 9 | { 10 | public class Fnt2TMPro : EditorWindow 11 | { 12 | [MenuItem("Window/Bitmap Font Converter")] 13 | 14 | public static void ShowWindow() 15 | { 16 | EditorWindow.GetWindow(typeof(Fnt2TMPro), false, "Bitmap Font Converter"); 17 | } 18 | private Texture2D m_Texture2D; 19 | private TextAsset m_SourceFontFile; 20 | private TMP_FontAsset m_DestinationFontFile; 21 | void PatchGlyph(RawCharacterInfo character, int textureHeight, int textureWidth, ref Glyph g) 22 | { 23 | var scaleH = textureWidth / textureHeight > 1 ? textureWidth / textureHeight : 1; 24 | var scaleW = textureHeight / textureWidth > 1 ? textureHeight / textureWidth : 1; 25 | g.glyphRect = new GlyphRect( 26 | character.X * scaleW, 27 | (textureHeight - character.Y - character.Height) * scaleH, 28 | character.Width * scaleW, 29 | character.Height * scaleH 30 | ); 31 | g.metrics = new GlyphMetrics( 32 | character.Width, 33 | character.Height, 34 | character.Xoffset, 35 | -character.Yoffset, 36 | character.Xadvance 37 | ); 38 | } 39 | void UpdateFont(TMP_FontAsset fontFile) 40 | { 41 | var fontText = m_SourceFontFile.text; 42 | var fnt = FntParse.GetFntParse(ref fontText); 43 | 44 | for (int i = 0; i < fontFile.characterTable.Count; i++) 45 | { 46 | var unicode = fontFile.characterTable[i].unicode; 47 | var glyphIndex = fontFile.characterTable[i].glyphIndex; 48 | for (int j = 0; j < fnt.charInfos.Length; j++) 49 | { 50 | if (unicode == fnt.charInfos[j].index) 51 | { 52 | var glyph = fontFile.glyphLookupTable[glyphIndex]; 53 | PatchGlyph(fnt.rawCharInfos[j], 54 | fnt.textureHeight, 55 | fnt.textureWidth, 56 | ref glyph); 57 | fontFile.glyphLookupTable[glyphIndex] = glyph; 58 | break; 59 | } 60 | } 61 | } 62 | 63 | var newFaceInfo = fontFile.faceInfo; 64 | newFaceInfo.baseline = fnt.lineBaseHeight; 65 | newFaceInfo.lineHeight = fnt.lineHeight; 66 | newFaceInfo.ascentLine = fnt.lineHeight; 67 | newFaceInfo.pointSize = fnt.fontSize; 68 | 69 | var fontType = typeof(TMP_FontAsset); 70 | var faceInfoProperty = fontType.GetProperty("faceInfo"); 71 | faceInfoProperty.SetValue(fontFile, newFaceInfo); 72 | 73 | fontFile.material.SetTexture("_MainTex", m_Texture2D); 74 | fontFile.atlasTextures[0] = m_Texture2D; 75 | } 76 | 77 | void OnGUI() 78 | { 79 | EditorGUILayout.BeginHorizontal(); 80 | m_Texture2D = EditorGUILayout.ObjectField("Font Texture", 81 | m_Texture2D, typeof(Texture2D), false) as Texture2D; 82 | EditorGUILayout.EndHorizontal(); 83 | EditorGUILayout.BeginHorizontal(); 84 | m_SourceFontFile = EditorGUILayout.ObjectField("Source Font File", 85 | m_SourceFontFile, typeof(TextAsset), false) as TextAsset; 86 | EditorGUILayout.EndHorizontal(); 87 | EditorGUILayout.BeginHorizontal(); 88 | m_DestinationFontFile = EditorGUILayout.ObjectField("Destination Font File", 89 | m_DestinationFontFile, typeof(TMP_FontAsset), false) as TMP_FontAsset; 90 | EditorGUILayout.EndHorizontal(); 91 | 92 | if (GUILayout.Button("Convert")) 93 | { 94 | UpdateFont(m_DestinationFontFile); 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /Editor/Fnt2TMPro/CustomFntParse.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Xml; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace litefeelcustom 8 | { 9 | public struct Kerning 10 | { 11 | public int first; 12 | public int second; 13 | public int amount; 14 | } 15 | 16 | public class RawCharacterInfo 17 | { 18 | public int ID, X, Y, Width, Height, Xoffset, Yoffset, Xadvance, Page; 19 | public RawCharacterInfo(int id, int x, int y, int width, int height, int xoffset, int yoffset, int xadvance, int page) 20 | { 21 | ID = id; 22 | X = x; 23 | Y = y; 24 | Width = width; 25 | Height = height; 26 | Xoffset = xoffset; 27 | Yoffset = yoffset; 28 | Xadvance = xadvance; 29 | Page = page; 30 | } 31 | } 32 | 33 | 34 | public class FntParse 35 | { 36 | public int textureWidth; 37 | public int textureHeight; 38 | public string[] textureNames; 39 | 40 | public string fontName; 41 | public int fontSize; 42 | public int lineHeight; 43 | public int lineBaseHeight; 44 | 45 | public CharacterInfo[] charInfos { get; private set; } 46 | 47 | public RawCharacterInfo[] rawCharInfos { get; private set; } 48 | public Kerning[] kernings { get; private set; } 49 | 50 | public static FntParse GetFntParse(ref string text) 51 | { 52 | FntParse parse = null; 53 | if (text.StartsWith("info")) 54 | { 55 | parse = new FntParse(); 56 | parse.DoTextParse(ref text); 57 | } 58 | else if (text.StartsWith("<")) 59 | { 60 | parse = new FntParse(); 61 | parse.DoXMLPase(ref text); 62 | } 63 | return parse; 64 | } 65 | 66 | #region xml 67 | public void DoXMLPase(ref string content) 68 | { 69 | XmlDocument xml = new XmlDocument(); 70 | xml.LoadXml(content); 71 | 72 | XmlNode info = xml.GetElementsByTagName("info")[0]; 73 | XmlNode common = xml.GetElementsByTagName("common")[0]; 74 | XmlNodeList pages = xml.GetElementsByTagName("pages")[0].ChildNodes; 75 | XmlNodeList chars = xml.GetElementsByTagName("chars")[0].ChildNodes; 76 | 77 | 78 | fontName = info.Attributes.GetNamedItem("face").InnerText; 79 | fontSize = ToInt(info, "size"); 80 | 81 | lineHeight = ToInt(common, "lineHeight"); 82 | lineBaseHeight = ToInt(common, "base"); 83 | textureWidth = ToInt(common, "scaleW"); 84 | textureHeight = ToInt(common, "scaleH"); 85 | int pageNum = ToInt(common, "pages"); 86 | textureNames = new string[pageNum]; 87 | 88 | for (int i = 0; i < pageNum; i++) 89 | { 90 | XmlNode page = pages[i]; 91 | int pageId = ToInt(page, "id"); 92 | textureNames[pageId] = page.Attributes.GetNamedItem("file").InnerText; 93 | } 94 | 95 | charInfos = new CharacterInfo[chars.Count]; 96 | rawCharInfos = new RawCharacterInfo[chars.Count]; 97 | 98 | for (int i = 0; i < chars.Count; i++) 99 | { 100 | XmlNode charNode = chars[i]; 101 | charInfos[i] = CreateCharInfo( 102 | ToInt(charNode, "id"), 103 | ToInt(charNode, "x"), 104 | ToInt(charNode, "y"), 105 | ToInt(charNode, "width"), 106 | ToInt(charNode, "height"), 107 | ToInt(charNode, "xoffset"), 108 | ToInt(charNode, "yoffset"), 109 | ToInt(charNode, "xadvance"), 110 | ToInt(charNode, "page")); 111 | rawCharInfos[i] = new RawCharacterInfo( 112 | ToInt(charNode, "id"), 113 | ToInt(charNode, "x"), 114 | ToInt(charNode, "y"), 115 | ToInt(charNode, "width"), 116 | ToInt(charNode, "height"), 117 | ToInt(charNode, "xoffset"), 118 | ToInt(charNode, "yoffset"), 119 | ToInt(charNode, "xadvance"), 120 | ToInt(charNode, "page") 121 | ); 122 | } 123 | 124 | // kernings 125 | XmlNode kerningsNode = xml.GetElementsByTagName("kernings")[0]; 126 | if (kerningsNode != null && kerningsNode.HasChildNodes) 127 | { 128 | XmlNodeList kerns = kerningsNode.ChildNodes; 129 | kernings = new Kerning[kerns.Count]; 130 | for (int i = 0; i < kerns.Count; i++) 131 | { 132 | XmlNode kerningNode = kerns[i]; 133 | kernings[i] = new Kerning(); 134 | kernings[i].first = ToInt(kerningNode, "first"); 135 | kernings[i].second = ToInt(kerningNode, "second"); 136 | kernings[i].amount = ToInt(kerningNode, "amount"); 137 | } 138 | } 139 | } 140 | 141 | 142 | private static int ToInt(XmlNode node, string name) 143 | { 144 | return int.Parse(node.Attributes.GetNamedItem(name).InnerText); 145 | } 146 | #endregion 147 | 148 | #region text 149 | private Regex pattern; 150 | public void DoTextParse(ref string content) 151 | { 152 | // letter=" " // \S+=".+?" 153 | // letter="x" // \S+=".+?" 154 | // letter=""" // \S+=".+?" 155 | // letter="" // \S+ 156 | // char // \S+ 157 | pattern = new Regex(@"\S+="".+?""|\S+"); 158 | string[] lines = content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); 159 | ReadTextInfo(ref lines[0]); 160 | ReadTextCommon(ref lines[1]); 161 | 162 | for (int j = 0; j < textureNames.Length; j++) 163 | { 164 | ReadTextPage(ref lines[j + 2]); 165 | } 166 | 167 | // don't use count of chars, count is incorrect if has space 168 | //ReadTextCharCount(ref lines[3]); 169 | List list = new List(); 170 | List rlist = new List(); 171 | int i = 2 + textureNames.Length; 172 | int l = lines.Length; 173 | for (; i < l; i++) 174 | { 175 | if (!ReadTextChar(i - 4, ref lines[i], ref list, ref rlist)) 176 | break; 177 | } 178 | charInfos = list.ToArray(); 179 | rawCharInfos = rlist.ToArray(); 180 | 181 | // skip empty line 182 | for (; i < l; i++) 183 | { 184 | if (lines[i].Length > 0) 185 | break; 186 | } 187 | 188 | // kernings 189 | if (i < l) 190 | { 191 | int count = 0; 192 | if (ReadTextCount(ref lines[i++], out count)) 193 | { 194 | int start = i; 195 | kernings = new Kerning[count]; 196 | for (; i < l; i++) 197 | { 198 | if (!ReadTextKerning(i - start, ref lines[i], ref list)) 199 | break; 200 | } 201 | }; 202 | } 203 | } 204 | 205 | private void ReadTextInfo(ref string line) 206 | { 207 | string[] keys; 208 | string[] values; 209 | SplitParts(line, out keys, out values); 210 | for (int i = keys.Length - 1; i >= 0; i--) 211 | { 212 | switch (keys[i]) 213 | { 214 | case "face": fontName = values[i]; break; 215 | case "size": fontSize = int.Parse(values[i]); break; 216 | } 217 | } 218 | } 219 | 220 | private void ReadTextCommon(ref string line) 221 | { 222 | string[] keys; 223 | string[] values; 224 | SplitParts(line, out keys, out values); 225 | for (int i = keys.Length - 1; i >= 0; i--) 226 | { 227 | switch (keys[i]) 228 | { 229 | case "lineHeight": lineHeight = int.Parse(values[i]); break; 230 | case "base": lineBaseHeight = int.Parse(values[i]); break; 231 | case "scaleW": textureWidth = int.Parse(values[i]); break; 232 | case "scaleH": textureHeight = int.Parse(values[i]); break; 233 | case "pages": textureNames = new string[int.Parse(values[i])]; break; 234 | } 235 | } 236 | } 237 | 238 | private void ReadTextPage(ref string line) 239 | { 240 | string[] keys; 241 | string[] values; 242 | SplitParts(line, out keys, out values); 243 | string textureName = null; 244 | int pageId = -1; 245 | for (int i = keys.Length - 1; i >= 0; i--) 246 | { 247 | switch (keys[i]) 248 | { 249 | case "file": textureName = values[i]; break; 250 | case "id": pageId = int.Parse(values[i]); break; 251 | } 252 | } 253 | textureNames[pageId] = textureName; 254 | } 255 | 256 | private bool ReadTextCount(ref string line, out int count) 257 | { 258 | string[] keys; 259 | string[] values; 260 | SplitParts(line, out keys, out values); 261 | count = 0; 262 | for (int i = keys.Length - 1; i >= 0; i--) 263 | { 264 | switch (keys[i]) 265 | { 266 | case "count": 267 | count = int.Parse(values[i]); 268 | return true; 269 | } 270 | } 271 | return false; 272 | } 273 | 274 | private bool ReadTextChar(int idx, ref string line, ref List list, ref List rlist) 275 | { 276 | if (!line.StartsWith("char")) return false; 277 | string[] keys; 278 | string[] values; 279 | SplitParts(line, out keys, out values); 280 | int id = 0, x = 0, y = 0, w = 0, h = 0, xo = 0, yo = 0, xadvance = 0; 281 | for (int i = keys.Length - 1; i >= 0; i--) 282 | { 283 | switch (keys[i]) 284 | { 285 | case "id": id = int.Parse(values[i]); break; 286 | case "x": x = int.Parse(values[i]); break; 287 | case "y": y = int.Parse(values[i]); break; 288 | case "width": w = int.Parse(values[i]); break; 289 | case "height": h = int.Parse(values[i]); break; 290 | case "xoffset": xo = int.Parse(values[i]); break; 291 | case "yoffset": yo = int.Parse(values[i]); break; 292 | case "xadvance": xadvance = int.Parse(values[i]); break; 293 | } 294 | } 295 | list.Add(CreateCharInfo(id, x, y, w, h, xo, yo, xadvance)); 296 | rlist.Add(new RawCharacterInfo(id, x, y, w, h, xo, yo, xadvance, 0)); 297 | return true; 298 | } 299 | 300 | private bool ReadTextKerning(int idx, ref string line, ref List list) 301 | { 302 | if (!line.StartsWith("kerning")) return false; 303 | string[] keys; 304 | string[] values; 305 | SplitParts(line, out keys, out values); 306 | Kerning kerning = new Kerning(); 307 | for (int i = keys.Length - 1; i >= 0; i--) 308 | { 309 | switch (keys[i]) 310 | { 311 | case "first": kerning.first = int.Parse(values[i]); break; 312 | case "second": kerning.second = int.Parse(values[i]); break; 313 | case "amount": kerning.amount = int.Parse(values[i]); break; 314 | } 315 | } 316 | kernings[idx] = kerning; 317 | return true; 318 | } 319 | 320 | private bool SplitParts(string line, out string[] keys, out string[] values) 321 | { 322 | MatchCollection parts = pattern.Matches(line); 323 | int count = parts.Count; 324 | keys = new string[count - 1]; 325 | values = new string[count - 1]; 326 | for (int i = count - 2; i >= 0; i--) 327 | { 328 | string part = parts[i + 1].Value; 329 | int pos = part.IndexOf('='); 330 | keys[i] = part.Substring(0, pos); 331 | values[i] = part.Substring(pos + 1).Trim('"'); 332 | } 333 | return true; 334 | } 335 | 336 | #endregion 337 | 338 | private CharacterInfo CreateCharInfo(int id, int x, int y, int w, int h, int xo, int yo, int xadvance, int page = 0) 339 | { 340 | Rect uv = new Rect(); 341 | uv.x = (float)x / textureWidth + page; 342 | uv.y = (float)y / textureHeight; 343 | uv.width = (float)w / textureWidth; 344 | uv.height = (float)h / textureHeight; 345 | uv.y = 1f - uv.y - uv.height; 346 | 347 | Rect vert = new Rect(); 348 | vert.x = xo; 349 | #if UNITY_5_0 || UNITY_5_1 || UNITY_5_2 350 | // unity 5.0 can not support baseline for 351 | vert.y = yo; 352 | #else 353 | vert.y = yo - lineBaseHeight; 354 | #endif 355 | vert.width = w; 356 | vert.height = h; 357 | vert.y = -vert.y; 358 | vert.height = -vert.height; 359 | 360 | CharacterInfo charInfo = new CharacterInfo(); 361 | charInfo.index = id; 362 | 363 | #if UNITY_5_3_OR_NEWER || UNITY_5_3 || UNITY_5_2 364 | charInfo.uvBottomLeft = new Vector2(uv.xMin, uv.yMin); 365 | charInfo.uvBottomRight = new Vector2(uv.xMax, uv.yMin); 366 | charInfo.uvTopLeft = new Vector2(uv.xMin, uv.yMax); 367 | charInfo.uvTopRight = new Vector2(uv.xMax, uv.yMax); 368 | 369 | charInfo.minX = (int)vert.xMin; 370 | charInfo.maxX = (int)vert.xMax; 371 | charInfo.minY = (int)vert.yMax; 372 | charInfo.maxY = (int)vert.yMin; 373 | 374 | charInfo.bearing = (int)vert.x; 375 | charInfo.advance = xadvance; 376 | #else 377 | #pragma warning disable 618 378 | charInfo.uv = uv; 379 | charInfo.vert = vert; 380 | charInfo.width = xadvance; 381 | #pragma warning restore 618 382 | #endif 383 | return charInfo; 384 | } 385 | } 386 | 387 | } 388 | --------------------------------------------------------------------------------