A闪的 BLOG 技术与人文
我将尝试借助一些列文章介绍Unity中字体系统的使用方式和各种情况下的处理方案。在实际游戏开发过程中,字体作为一类特殊渲染存在很多时候我们仅是使用,而很少针对字体系统进行深入底层的了解。这也导致很多时候在特殊情况下出现一些问题而无从下手的原因。
Unity自身支持TrueType和OpenType两种字体格式。其原因也非常简单这两种字体格式是目前支持最为广泛的字体格式,与此同时,TrueType格式中的存在潜在专利已经过期,不会出现相关专利问题。通常情况下TrueType字体的文件后缀名为.ttf
,OpenType字体文件后缀名为.otf
。关于这两种字体的更多介绍可以参考TrueType Wiki 和 OpenType Wiki。
Unity底层针对这两种的字体的实现使用的开源库FreeType,我们也不需要在上层业务逻辑中针对字体文件做特殊处理,这部分如果有兴趣可以自己查阅相关资料。本篇文章仅针对TTF格式做一个简单的介绍,以方便后续理解字体文件是如何被渲染到画面中的。
参考
TTF字体文件主要数据分为两大部分,第一部分是当前字体所包含的字符目录索引,文档中称之为The Font Directory。这部分数据中记录了当前字体所支持的字符都有哪些,对应的字符数据在数据流中的具体位置。第二部分则是字符描述数据,这些数据用来描述对应字符“长”成什么样子,其中最重要的是glyf图元数据。
一个字符的图元数据是如何表示的呢?可以简单的理解为通过复杂的点和贝塞尔曲线数据来表示,我们可以通过https://opentype.js.org/ 来查看一个字符文件中字符的绘制信息。
上面的描述并不严谨,你只需要知道TTF字体文件的基本概念即可。
想看一个TTF文件字体所有字符和信息可以使用 https://kekee000.github.io/fonteditor/ 这个在线字体编辑器。
FreeType 是一个可免费使用的用于渲染字体的软件库,Unity借助FreeType解析字体文件,并将查询到的字符对应的glyph转换为bitmap数据,提交给Unity。Unity将其转换为渲染层可用的纹理来进行使用。虽然引擎内部和TextMeshPro已经帮我们完成了这部分操作,但通过 UnityEngine.TextCore.LowLevel 我们依然可以进行一些相对底层的操作。
实际项目中这部分内容没有太多意义,作为学习研究还是有一定意义的。
通过对API推测,UnityEngine.TextCore.LowLevel 中的 `FontEngine` 接口应该是C#对 `FreeType` C库 的桥接层。
我们可以通过一段简单的代码来了解 FontEngine
的相关API使用。
我将一个英文字体文件修改名称为 fontBytes.bytes
放到了 Resources
目录中,方便加载资源。
using UnityEditor;
using UnityEngine;
using UnityEngine.TextCore;
using UnityEngine.TextCore.LowLevel;
using TextAsset = UnityEngine.TextAsset;
public static class FontTest
{
[MenuItem("Test/TestFont")]
public static void Run()
{
var rel = FontEngine.InitializeFontEngine();
Debug.Log($"FontEngine初始化结果:{rel}");
var fontBytes = Resources.Load<TextAsset>("testfont");
rel = FontEngine.LoadFontFace(fontBytes.bytes);
Debug.Log($"FontEngine字体加载结果:{rel}");
var faceInfo = FontEngine.GetFaceInfo();
Debug.Log($"GetFaceInfo:{faceInfo.ToDes()}");
var faces = FontEngine.GetFontFaces();
Debug.Log($"GetFontFaces:{faces.Length}");
var fontNames = FontEngine.GetSystemFontNames();
Debug.Log($"系统字体:{fontNames}");
CheckHasChar('a');
CheckHasChar('中');
FontEngine.UnloadAllFontFaces();
}
private static void CheckHasChar(char character)
{
if (FontEngine.TryGetGlyphIndex(character, out var glyphIndex))
{
Debug.Log($"当前字体文件包含字符:{character}, glyphIndex:{glyphIndex}");
return;
}
Debug.Log($"当前字体文件不包含字符包含字符:{character}");
}
public static string ToDes(this FaceInfo face)
{
var rel = "FaceInfo 详细信息\n" +
$"高线: {face.ascentLine}\n" +
$"基准线: {face.baseline}\n" +
$"大写字母线: {face.capLine}\n" +
$"低线: {face.descentLine}\n" +
$"字体的名称: {face.familyName}\n" +
$"行高表示连续文本行之间的距离: {face.lineHeight}\n" +
$"等分线: {face.meanLine}\n" +
$"用于采样字体的磅值: {face.pointSize}\n" +
$"字体的相对比例: {face.scale}\n" +
$"删除线的位置: {face.strikethroughOffset}\n" +
$"删除线的粗细: {face.strikethroughThickness}\n" +
$"字体的样式名称: {face.styleName}\n" +
$"使用下标的字符位置: {face.subscriptOffset}\n" +
$"下标字符的相对大小/比例: {face.subscriptSize}\n" +
$"使用上标的字符位置: {face.superscriptOffset}\n" +
$"上标字符的相对大小/比例: {face.superscriptSize}\n" +
$"制表符的宽度: {face.tabWidth}\n" +
$"下划线的位置: {face.underlineOffset}\n" +
$"下划线的粗细: {face.underlineThickness}";
return rel;
}
}
运行工具代码,可以看到如下打印:
FontEngine初始化结果:Success
UnityEngine.Debug:Log (object)
FontTest:Run () (at Assets/FontTest/FontTest.cs:17)
FontEngine字体加载结果:Success
UnityEngine.Debug:Log (object)
FontTest:Run () (at Assets/FontTest/FontTest.cs:21)
GetFaceInfo:FaceInfo 详细信息
高线: 1854
基准线: 0
大写字母线: 1409
低线: -434
字体的名称: Liberation Sans
行高表示连续文本行之间的距离: 2355
等分线: 1082
用于采样字体的磅值: 2048
字体的相对比例: 1
删除线的位置: 432.8
删除线的粗细: 150
字体的样式名称: Regular
使用下标的字符位置: -434
下标字符的相对大小/比例: 0.5
使用上标的字符位置: 1854
上标字符的相对大小/比例: 0.5
制表符的宽度: 569
下划线的位置: -292
下划线的粗细: 150
UnityEngine.Debug:Log (object)
FontTest:Run () (at Assets/FontTest/FontTest.cs:24)
GetFontFaces:1
UnityEngine.Debug:Log (object)
FontTest:Run () (at Assets/FontTest/FontTest.cs:27)
系统字体:System.String[]
UnityEngine.Debug:Log (object)
FontTest:Run () (at Assets/FontTest/FontTest.cs:30)
当前字体文件包含字符:a, glyphIndex:68
UnityEngine.Debug:Log (object)
FontTest:CheckHasChar (char) (at Assets/FontTest/FontTest.cs:43)
FontTest:Run () (at Assets/FontTest/FontTest.cs:32)
当前字体文件不包含字符包含字符:中
UnityEngine.Debug:Log (object)
FontTest:CheckHasChar (char) (at Assets/FontTest/FontTest.cs:47)
FontTest:Run () (at Assets/FontTest/FontTest.cs:33)
可以看到通过直接对 .ttf
文件的读取解析得到了字体相关信息,这些信息用来做排版渲染等操作。值得注意的是,英文字体中并不包含中文字符,所以就上面示例中字符“中”并不能在字符文件中找到,这也是为什么我们在Unity中会看到不支持的字体被渲染为一个方块的原因。
Unity是如何将字体文件渲染成一张纹理的呢?
实际上是通过内部API实现的,我们在外部无法访问该API,除非重新修改并生成UnityEngine的DLL文件才可以实现。主要的API有如下2个:
还有一些渲染相关的API,我并没有在TextMeshPro中找到调用的地方,一些API调用但是被废弃了。
这些接口的实现都在原生层实现,具体实现思路无法得知。但唯一可以确定的是,将字形渲染到一张纹理中,是需要一定性能消耗的。
我们可以简单来梳理一下一个文本的渲染流程。实际执行时会更多的细节
当我们使用文本时,选择动态字体,则会执行上面的流程。每次遇到纹理中没有字符的情况下,都会执行这些操作。除去字符纹理在运行时的CPU消耗,我们要存储字体文件,这需要占用一定的内存,。尤其中日韩这三种语言的字体文件通常较大。当遇到的字符越来越多的时候,Texture纹理的数量也会越来越多。
为此,TextMeshPro 提供了静态字体纹理这种方式。也就是将上面运行时所需要的操作在开发时就已经制作好,运行时只需要适应预先渲染好的纹理即可。运行中也不需要动态加载字体文件,从内存和CPU的角度来讲省去了非常多的操作环节。
但静态字体纹理也有一定的局限性。例如,当预先设定的字符集中不包含所需要的字符时,文本显示就会出现方块的问题。我们并没有两全其美的解决方案,只能在一定程度上缓解最糟糕的情况放生,具体的思路我会在后面的文章中逐步分析。
这部分内容没啥太大用处,仅仅是分析代码时的一些总结和发现。
FontEngineError枚举的来源
当我们调用 FontEngine.InitializeFontEngine
这个接口时,会有一个返回值,类型为 TextCore.LowLevel.FontEngineError
。这个枚举类型定义了 14 个错误类型。那么这些错误类型是从哪里来的呢?
他们的原始代码如下:
public enum FontEngineError
{
Success = 0x0,
// Font file structure, type or path related errors.
Invalid_File_Path = 0x1,
Invalid_File_Format = 0x2,
Invalid_File_Structure = 0x3,
Invalid_File = 0x4,
Invalid_Table = 0x8,
// Glyph related errors.
Invalid_Glyph_Index = 0x10,
Invalid_Character_Code = 0x11,
Invalid_Pixel_Size = 0x17,
//
Invalid_Library = 0x21,
// Font face related errors.
Invalid_Face = 0x23,
Invalid_Library_or_Face = 0x29,
// Font atlas generation and glyph rendering related errors.
Atlas_Generation_Cancelled = 0x64,
Invalid_SharedTextureData = 0x65,
// OpenType Layout related errors.
OpenTypeLayoutLookup_Mismatch = 0x74,
// Additional errors codes will be added as necessary to cover new FontEngine features and functionality.
}
这些错误值实际上来源于 FreeType
库的 fterrdef.h
头文件定义。本质上,是将 FreeType
的操作结果值直接传递到了C#层。而 fterrdef.h
中定义的错误类型可不只这些。
fterrdef.h
源码
/****************************************************************************
*
* fterrdef.h
*
* FreeType error codes (specification).
*
* Copyright (C) 2002-2024 by
* David Turner, Robert Wilhelm, and Werner Lemberg.
*
* This file is part of the FreeType project, and may only be used,
* modified, and distributed under the terms of the FreeType project
* license, LICENSE.TXT. By continuing to use, modify, or distribute
* this file you indicate that you have read the license and
* understand and accept it fully.
*
*/
/**************************************************************************
*
* @section:
* error_code_values
*
* @title:
* Error Code Values
*
* @abstract:
* All possible error codes returned by FreeType functions.
*
* @description:
* The list below is taken verbatim from the file `fterrdef.h` (loaded
* automatically by including `FT_FREETYPE_H`). The first argument of the
* `FT_ERROR_DEF_` macro is the error label; by default, the prefix
* `FT_Err_` gets added so that you get error names like
* `FT_Err_Cannot_Open_Resource`. The second argument is the error code,
* and the last argument an error string, which is not used by FreeType.
*
* Within your application you should **only** use error names and
* **never** its numeric values! The latter might (and actually do)
* change in forthcoming FreeType versions.
*
* Macro `FT_NOERRORDEF_` defines `FT_Err_Ok`, which is always zero. See
* the 'Error Enumerations' subsection how to automatically generate a
* list of error strings.
*
*/
/**************************************************************************
*
* @enum:
* FT_Err_XXX
*
*/
/* generic errors */
FT_NOERRORDEF_( Ok, 0x00,
"no error" )
FT_ERRORDEF_( Cannot_Open_Resource, 0x01,
"cannot open resource" )
FT_ERRORDEF_( Unknown_File_Format, 0x02,
"unknown file format" )
FT_ERRORDEF_( Invalid_File_Format, 0x03,
"broken file" )
FT_ERRORDEF_( Invalid_Version, 0x04,
"invalid FreeType version" )
FT_ERRORDEF_( Lower_Module_Version, 0x05,
"module version is too low" )
FT_ERRORDEF_( Invalid_Argument, 0x06,
"invalid argument" )
FT_ERRORDEF_( Unimplemented_Feature, 0x07,
"unimplemented feature" )
FT_ERRORDEF_( Invalid_Table, 0x08,
"broken table" )
FT_ERRORDEF_( Invalid_Offset, 0x09,
"broken offset within table" )
FT_ERRORDEF_( Array_Too_Large, 0x0A,
"array allocation size too large" )
FT_ERRORDEF_( Missing_Module, 0x0B,
"missing module" )
FT_ERRORDEF_( Missing_Property, 0x0C,
"missing property" )
/* glyph/character errors */
FT_ERRORDEF_( Invalid_Glyph_Index, 0x10,
"invalid glyph index" )
FT_ERRORDEF_( Invalid_Character_Code, 0x11,
"invalid character code" )
FT_ERRORDEF_( Invalid_Glyph_Format, 0x12,
"unsupported glyph image format" )
FT_ERRORDEF_( Cannot_Render_Glyph, 0x13,
"cannot render this glyph format" )
FT_ERRORDEF_( Invalid_Outline, 0x14,
"invalid outline" )
FT_ERRORDEF_( Invalid_Composite, 0x15,
"invalid composite glyph" )
FT_ERRORDEF_( Too_Many_Hints, 0x16,
"too many hints" )
FT_ERRORDEF_( Invalid_Pixel_Size, 0x17,
"invalid pixel size" )
FT_ERRORDEF_( Invalid_SVG_Document, 0x18,
"invalid SVG document" )
/* handle errors */
FT_ERRORDEF_( Invalid_Handle, 0x20,
"invalid object handle" )
FT_ERRORDEF_( Invalid_Library_Handle, 0x21,
"invalid library handle" )
FT_ERRORDEF_( Invalid_Driver_Handle, 0x22,
"invalid module handle" )
FT_ERRORDEF_( Invalid_Face_Handle, 0x23,
"invalid face handle" )
FT_ERRORDEF_( Invalid_Size_Handle, 0x24,
"invalid size handle" )
FT_ERRORDEF_( Invalid_Slot_Handle, 0x25,
"invalid glyph slot handle" )
FT_ERRORDEF_( Invalid_CharMap_Handle, 0x26,
"invalid charmap handle" )
FT_ERRORDEF_( Invalid_Cache_Handle, 0x27,
"invalid cache manager handle" )
FT_ERRORDEF_( Invalid_Stream_Handle, 0x28,
"invalid stream handle" )
/* driver errors */
FT_ERRORDEF_( Too_Many_Drivers, 0x30,
"too many modules" )
FT_ERRORDEF_( Too_Many_Extensions, 0x31,
"too many extensions" )
/* memory errors */
FT_ERRORDEF_( Out_Of_Memory, 0x40,
"out of memory" )
FT_ERRORDEF_( Unlisted_Object, 0x41,
"unlisted object" )
/* stream errors */
FT_ERRORDEF_( Cannot_Open_Stream, 0x51,
"cannot open stream" )
FT_ERRORDEF_( Invalid_Stream_Seek, 0x52,
"invalid stream seek" )
FT_ERRORDEF_( Invalid_Stream_Skip, 0x53,
"invalid stream skip" )
FT_ERRORDEF_( Invalid_Stream_Read, 0x54,
"invalid stream read" )
FT_ERRORDEF_( Invalid_Stream_Operation, 0x55,
"invalid stream operation" )
FT_ERRORDEF_( Invalid_Frame_Operation, 0x56,
"invalid frame operation" )
FT_ERRORDEF_( Nested_Frame_Access, 0x57,
"nested frame access" )
FT_ERRORDEF_( Invalid_Frame_Read, 0x58,
"invalid frame read" )
/* raster errors */
FT_ERRORDEF_( Raster_Uninitialized, 0x60,
"raster uninitialized" )
FT_ERRORDEF_( Raster_Corrupted, 0x61,
"raster corrupted" )
FT_ERRORDEF_( Raster_Overflow, 0x62,
"raster overflow" )
FT_ERRORDEF_( Raster_Negative_Height, 0x63,
"negative height while rastering" )
/* cache errors */
FT_ERRORDEF_( Too_Many_Caches, 0x70,
"too many registered caches" )
/* TrueType and SFNT errors */
FT_ERRORDEF_( Invalid_Opcode, 0x80,
"invalid opcode" )
FT_ERRORDEF_( Too_Few_Arguments, 0x81,
"too few arguments" )
FT_ERRORDEF_( Stack_Overflow, 0x82,
"stack overflow" )
FT_ERRORDEF_( Code_Overflow, 0x83,
"code overflow" )
FT_ERRORDEF_( Bad_Argument, 0x84,
"bad argument" )
FT_ERRORDEF_( Divide_By_Zero, 0x85,
"division by zero" )
FT_ERRORDEF_( Invalid_Reference, 0x86,
"invalid reference" )
FT_ERRORDEF_( Debug_OpCode, 0x87,
"found debug opcode" )
FT_ERRORDEF_( ENDF_In_Exec_Stream, 0x88,
"found ENDF opcode in execution stream" )
FT_ERRORDEF_( Nested_DEFS, 0x89,
"nested DEFS" )
FT_ERRORDEF_( Invalid_CodeRange, 0x8A,
"invalid code range" )
FT_ERRORDEF_( Execution_Too_Long, 0x8B,
"execution context too long" )
FT_ERRORDEF_( Too_Many_Function_Defs, 0x8C,
"too many function definitions" )
FT_ERRORDEF_( Too_Many_Instruction_Defs, 0x8D,
"too many instruction definitions" )
FT_ERRORDEF_( Table_Missing, 0x8E,
"SFNT font table missing" )
FT_ERRORDEF_( Horiz_Header_Missing, 0x8F,
"horizontal header (hhea) table missing" )
FT_ERRORDEF_( Locations_Missing, 0x90,
"locations (loca) table missing" )
FT_ERRORDEF_( Name_Table_Missing, 0x91,
"name table missing" )
FT_ERRORDEF_( CMap_Table_Missing, 0x92,
"character map (cmap) table missing" )
FT_ERRORDEF_( Hmtx_Table_Missing, 0x93,
"horizontal metrics (hmtx) table missing" )
FT_ERRORDEF_( Post_Table_Missing, 0x94,
"PostScript (post) table missing" )
FT_ERRORDEF_( Invalid_Horiz_Metrics, 0x95,
"invalid horizontal metrics" )
FT_ERRORDEF_( Invalid_CharMap_Format, 0x96,
"invalid character map (cmap) format" )
FT_ERRORDEF_( Invalid_PPem, 0x97,
"invalid ppem value" )
FT_ERRORDEF_( Invalid_Vert_Metrics, 0x98,
"invalid vertical metrics" )
FT_ERRORDEF_( Could_Not_Find_Context, 0x99,
"could not find context" )
FT_ERRORDEF_( Invalid_Post_Table_Format, 0x9A,
"invalid PostScript (post) table format" )
FT_ERRORDEF_( Invalid_Post_Table, 0x9B,
"invalid PostScript (post) table" )
FT_ERRORDEF_( DEF_In_Glyf_Bytecode, 0x9C,
"found FDEF or IDEF opcode in glyf bytecode" )
FT_ERRORDEF_( Missing_Bitmap, 0x9D,
"missing bitmap in strike" )
FT_ERRORDEF_( Missing_SVG_Hooks, 0x9E,
"SVG hooks have not been set" )
/* CFF, CID, and Type 1 errors */
FT_ERRORDEF_( Syntax_Error, 0xA0,
"opcode syntax error" )
FT_ERRORDEF_( Stack_Underflow, 0xA1,
"argument stack underflow" )
FT_ERRORDEF_( Ignore, 0xA2,
"ignore" )
FT_ERRORDEF_( No_Unicode_Glyph_Name, 0xA3,
"no Unicode glyph name found" )
FT_ERRORDEF_( Glyph_Too_Big, 0xA4,
"glyph too big for hinting" )
/* BDF errors */
FT_ERRORDEF_( Missing_Startfont_Field, 0xB0,
"`STARTFONT' field missing" )
FT_ERRORDEF_( Missing_Font_Field, 0xB1,
"`FONT' field missing" )
FT_ERRORDEF_( Missing_Size_Field, 0xB2,
"`SIZE' field missing" )
FT_ERRORDEF_( Missing_Fontboundingbox_Field, 0xB3,
"`FONTBOUNDINGBOX' field missing" )
FT_ERRORDEF_( Missing_Chars_Field, 0xB4,
"`CHARS' field missing" )
FT_ERRORDEF_( Missing_Startchar_Field, 0xB5,
"`STARTCHAR' field missing" )
FT_ERRORDEF_( Missing_Encoding_Field, 0xB6,
"`ENCODING' field missing" )
FT_ERRORDEF_( Missing_Bbx_Field, 0xB7,
"`BBX' field missing" )
FT_ERRORDEF_( Bbx_Too_Big, 0xB8,
"`BBX' too big" )
FT_ERRORDEF_( Corrupted_Font_Header, 0xB9,
"Font header corrupted or missing fields" )
FT_ERRORDEF_( Corrupted_Font_Glyphs, 0xBA,
"Font glyphs corrupted or missing fields" )
/* */
/* END */
这些差异的根本原因在于,没有定义的错误根本用不到,或者说“绝对不会出现”。例如 0x40
和 0x41
两个内存相关的错误,大概率Unity底层已经进行了处理,不太可能让上层业务逻辑去处理这方面的事情。
还有一些API可以在C#接口和 FreeType
库中找到对应的桥接痕迹,更多的就不再展开赘述了。
弄清楚字体的基本原理,可以帮助我们后续针对不同问题,不用业务情况找到一个比较合理的解决方案。不至于“一通瞎搞”,各种尝试,即使得到了想要的结果,也不知道其中的缘由。
针对本篇文章中出现的一些技术点,我也没有进行太过深入研究。只是将我想要知道一些原理性的东西整理出来,有个大致的脉络,算是“浅尝辄止”。如果你对某一个方面的话题很感兴趣想探讨相关话题,可以发邮件给我。