本篇教程由作者设定使用 CC BY-NC-SA 协议。
Hi there!这篇文章介绍了 Minecraft 原版和 Modern UI 中的字体是如何工作的。现代化文本引擎为 Minecraft 提供高质量文本渲染、分辨率无关外框字体、彩色表情符号、以及正确的 Unicode 文本处理和布局支持。阅读该文章需要一点 Unicode 和 JSON 知识。
一、何谓“字体”
在 Modern UI 中,我们通常所说的字体(Typeface)是指一组字体家族(Font Family),即字体合集(Font Collection)。字体家族是 TrueType 的概念,和 Word 中的“字体”是一样的,即同一字库的标准体(Regular)、粗体(Bold)、斜体(Italic)和粗斜体(Bold Italic)的合称。一个字体家族必须包含标准体,当缺失粗斜体时,渲染引擎可以自动合成粗斜体。
类似地,在 Minecraft 1.20.1 中,用户可以在资源包 assets/<namespace>/font 下创建 JSON 文件定义“字体”。该文件定义了一个字体集(Font Set),包含一组字形提供器(Glyph Provider)。不难发现,Minecraft 的“字体”类似于 Modern UI 字体合集,而字形提供器类似于字体家族,但只能包含一个 Minecraft 格式的标准体,且不满足 TrueType 和 Unicode 规范。
为什么要定义一组字体而不是一个字体呢?一个 TrueType 字体最多包含65535种字形,而一个字体也不能保证渲染每种语言的文字,所以要指定多个字体以防出现“无法渲染”、“字体丢失”等情况。
二、原版的字体
原版大致有3个字体(字体合集):minecraft:default,minecraft:uniform 和 minecraft:alt。第一个是原版默认字体,渲染又粗又大的字母和数字;第二个是Unicode字体,来自开源字体GNU Unifont;第三个则是附魔台的申必文字。
形如 minecraft:default 的便是字体名,这种命名方式称为资源定位(Resource Location),由命名空间(Namespace)和路径(Path)组成,中间由半角冒号分隔。我们可以在原版资源里找到它们的定义文件,即 assets/minecraft/font 目录下面。
例如 assets/minecraft/font/default.json 就是一个字体定义文件,其内容如下:
{
"providers": [
{
"type": "reference",
"id": "minecraft:include/space"
},
{
"type": "reference",
"id": "minecraft:include/default"
},
{
"type": "reference",
"id": "minecraft:include/unifont"
}
]
}
注意:字体名是由路径删去 font/ 和文件后缀名 .json 得到的,同理也可以由字体名得到字体定义文件的路径。例如 assets/minecraft/font/default.json 的字体名是 minecraft:default,assets/aaa/font/bbb/ccc.json 的字体名是 aaa:bbb/ccc。
上文说到,原版中一个JSON字体定义指定了一组字形提供器,这里 providers 的值是一个JSON数组;每个元素是一个JSON对象,为字形提供器的定义,其中必须包含 type 属性表示提供器的类型,也必须包含该类型的必要属性。在 Minecraft 1.20.1 中,字形提供器有下面几种类型:
1. 位图字体
位图字体(Bitmap Font)提供了一张像素图,这张图被均匀地划分成一个个网格,将指定Unicode码点映射到网格上的字形。这些图像是以 Minecraft 界面比例 等比例放大渲染的,例如界面比例为3,那么图像上的1x1像素将渲染到屏幕上的3x3像素。位图字体不能提供Unicode支持,因为它只有图像信息。
字形提供器定义属性:
type: 字符串,必须为 "bitmap"
file: 字符串,像素图的资源定位,其路径会自动加以 textures/ 前缀
ascent: 整数,字符顶部到基线的像素距离,基线可在 Modern UI 偏好设置修改(现代文本引擎默认基线为7)
chars: JSON数组,表示Unicode码点网格,数组元素为字符串,表示网格的一行
height: 整数(可选),表示每个字形的像素高度,即字符底部到顶部的距离,默认值为8(与现代文本引擎默认字体大小基值一致)
注意:
重复定义的码点会被后者覆盖
网格的行数或列数不能为0,每一行的码点个数必须相等
对于基本多语言平面的字符,一次转义即可;对于补充多语言平面的字符,需要两次转义
空隙必须用空字符(\u0000)补齐
例如 assets/minecraft/font/include/default.json 中便使用了下面的定义:
...
{
"type": "bitmap",
"file": "minecraft:font/ascii.png",
"ascent": 7,
"chars": [
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002a\u002b\u002c\u002d\u002e\u002f",
"\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003a\u003b\u003c\u003d\u003e\u003f",
"\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004a\u004b\u004c\u004d\u004e\u004f",
"\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005a\u005b\u005c\u005d\u005e\u005f",
"\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006a\u006b\u006c\u006d\u006e\u006f",
"\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007a\u007b\u007c\u007d\u007e\u0000",
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u00a3\u0000\u0000\u0192",
"\u0000\u0000\u0000\u0000\u0000\u0000\u00aa\u00ba\u0000\u0000\u00ac\u0000\u0000\u0000\u00ab\u00bb",
"\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510",
"\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567",
"\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580",
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u2205\u2208\u0000",
"\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u0000\u221a\u207f\u00b2\u25a0\u0000"
]
}
...
其中 minecraft:font/ascii.png 指向 assets/minecraft/textures/font/ascii.png 文件,是一张 128x128 的图像。码点网格一共16行16列,即每个字形 8x8 像素,也就是height的默认值8。每个字形的宽度由图像本身决定,为图像最左不透明像素到最右不透明像素的宽度+1。
注意:如果由网格行列数与图像宽高计算出来的每个字形的高度与指定的height不符,那么则会拉伸渲染。
注意:你可以查询Unicode规范来查询每个字符的码点,你也可以使用在线工具。如果你想渲染自定义字符/图标,你可以选择Unicode私用码点,U+E000..U+F8FF。
注意:现代文本引擎不会为位图字体执行 HarfBuzz 文本整形。现代文本引擎会自动将位图字体的字形居中对齐(原版不会),即左右各0.5像素(上面说了宽度+1,1就是两个字符间的间距)。
注意:原版不区分彩色图像和灰度图,统一按彩色图像处理,并会正片叠底。
注意:现代文本引擎不会将位图字体上传至图册,我们建议你自己创建图册,不要一个字符一个字体。
2. TTF字体
TrueType 或 OpenType 字体,后缀名为 .ttf .otf .ttc .otf。ttc是ttf的集合形式,即一个文件包含了整个字体家族。想要正确的Unicode支持,你必须使用TTF字体,因为TTF字体不仅包含的字形图像信息,还包含了布局信息,告诉 HarfBuzz 和 FreeType Font Scaler 如何布局并渲染文本。
字形提供器定义属性:
type: 字符串,必须为 "ttf"
file: 字符串,字体文件资源定位,其路径会自动加以 font/ 前缀
现代文本引擎会忽略其他属性,下面是原因:
原版有size属性指定字体的大小,这是由TTF字体的度规决定,并非EM大小,而且是界面比例为1时的大小。但 现代文本引擎为您提供了分辨率无关,即界面比例无关的字体,所以该选项完全忽略。你可以在 Modern UI 偏好设置修改原版所有TTF字体大小基值,即界面比例为1时的字体大小,例如默认基值为8,界面比例为4时,字体大小为32。而且此大小为EM大小,即“M”这个字母的高度是多少像素。
原版有oversample属性指定超采样倍数。但现代文本引擎为您提供了分辨率无关,即界面比例无关的字体,所以该选项完全忽略。相当于界面比例是多少,就超采样几倍。
原版有shift属性指定偏移量。但 Modern UI 使用TTF字体的度规,并与基线对齐,所以该选项完全忽略。
原版有skip属性指定不渲染哪些字符。但 Modern UI 有 HarfBuzz 文本整形,文字处理变得复杂,而且有专属的字体评选算法,所以该选项完全不适用。
例如 MTR 的 assets/mtr/font/mtr.json 中便使用了下面的定义:
{
"type": "ttf",
"file": "mtr:noto-sans-semibold.ttf",
"size": 12.0,
"oversample": 8.0
}
其中 mtr:noto-sans-semibold.ttf 指向 assets/mtr/font/noto-sans-semibold.ttf 字体文件,其他属性忽略。
注意:由于原版只能指定一个文件,如果你想指定一个字体家族,你必须使用ttc或otc格式,其中包含了标准体、粗体、斜体和粗斜体。否则 Modern UI 将自动合成其他字体,详见 FreeType。
注意:TTF和OTF字体在 Modern UI 也叫做外框字体(Outline Font),也可叫做矢量字体。相较于像素风的位图字体,外框字体可以提供分辨率无关字体,因为它是矢量图。
3. 空格字体
1.19新增,空格字体(Space Font)指定了不渲染任何内容,但提供了 advance 的一组字符。advance 即递进量,一个字形写完后下个字形前进多少像素(注意此处并非字符)。由于原版只有位图字体,所以必须得搞个空格字体来处理空格,这当然不能提供Unicode支持。TTF字体能正确处理非打印字符,而且 Modern UI 会在渲染时忽略隐形字符,不会占用显存。
这里不再赘述它的定义,你可以参考原版的 assets/minecraft/font/include/space.json,原版中 \u0020 即ASCII空格为4像素宽,原版并不能处理其他空格。
4. Unihex字体
以前称作 legacy_unicode 字体,即 GNU Unifont 的点阵图格式。和位图字体类似,只有图像信息,没有布局信息,所以不能提供Unicode支持。
现代文本引擎会完全忽略这种字体,并按照 Modern UI 默认字体处理,也就是偏好设置里的值。
5. 字体引用
1.20新增,即引用一个“字体”(合集)。1.20以前想要在不同“字体”(合集)中重复使用同一个或一组字形提供器,那么就必须重复定义它。这样不仅难以维护一致性,而且每一次定义都会新创建一个字体,同时增加内存和显存的使用。这个的作用就是引用一组字形提供器。
字形提供器定义属性:
type: 字符串,必须为 "reference"
id: 字符串,字体名,如上文所说,assets/minecraft/font/include/default.json 的字体名就是 minecraft:include/default
因此大家看到原版默认字体的定义是这样的:
{
"type": "reference",
"id": "minecraft:include/default"
}
其中 minecraft:include/default 就是原版像素字母和数字字体的实际定义,可被多次引用。
PS:这里需要拓扑排序,而且不能包含闭环(循环引用)。
三、Modern UI 的字体
Modern UI 自身并不像原版一样通过JSON文件定义字体,且 Modern UI 只能使用TTF/OTF字体。Modern UI 会读取操作系统上的所有字体,例如Windows为C:\Windows\Fonts,Linux为/usr/share/fonts等目录,macOS为/Library/Fonts/。同时 Modern UI 会在游戏启动时注册 assets/modernui/font 下的所有字体文件,注册后你可以使用字体家族的英文名来选择该字体。当然在偏好设置中,你可以在首选字体家族下看到这些字体的本地化名,这是在 Minecraft 中的一种独有方式。
注意:SansSerif、Serif 和 Monospaced 为逻辑字体,其对应的字体根据操作系统而定。
注意:Windows上用户安装到系统的字体并不能显示在字体列表中,你可以通过注册字体的方式使用它。
注意:现代文本引擎为 Minecraft 提供了三个字体重定向:minecraft:sans-serif,minecraft:serif 和 minecraft:monospace,分别对应上述三种逻辑字体。其中 minecraft: 可省略,也可换为 modernui:。注意monospace没有d。
既然“字体”是一组字体,或者说是一个列表,那么 Modern UI 如何选择当中的字体?
当执行文本布局时,给定文本和字体合集,Modern UI 会使用专属的字体评选算法从字体合集中选出合适的字体家族,分配到文本的不同部分。这与原版的从上到下使用字体提供器不同,Modern UI 会进行上下文分析,例如:
如果字符后面跟变种选择器(Variation Selector),那么列表中能包含该字符+变种选择器的字体会获得最高优先级
如果变种选择器为VS-16(\uFE0F),且字符为Emoji字符,那么彩色表情符号将获得最高优先级
如果字符为不间断字符或粘滞字符,例如阿拉伯语的单词或者零宽连字符,则一定不会切换字体
等等。总之 Modern UI 能按照 Unicode 规范进行复杂的文本排版。即使你所选的字体中没有任何一个能渲染某字符,Modern UI 也能从系统上所有字体中选择一个能渲染它的字体。所以你会看到 Modern UI 提供了回退(Fallback)字体家族配置。
四、现代文本引擎对默认字体的处理
我不想用原版的英文和数字字体怎么办?
在 3.9.0 中,Modern UI 遵循原版的强制Unicode字体选项,你可以选择开启强制Unicode字体来完全忽略 minecraft:default 字体。这种情况下,minecraft:default 字体会被定向至 minecraft:uniform 字体,而 minecraft:uniform 只包含 unihex 字体,所以会被定向至 Modern UI 默认字体。你也可以直接在原版字体定义上追加TTF字体,因为字体文件定义文件支持字形提供器追加。
但这种做法并不推荐,因为一些模组作者,特别是小游戏地图作者会往 minecraft:default 追加新的字形提供器来渲染一些图标,所以 Modern UI 为您提供了默认字体行为配置,这种情况下你必须关闭强制Unicode字体。
默认字体行为有四个选项,两两组合:
忽略全部:相当于强制Unicode字体,忽略 minecraft:default 下的所有字形提供器
保留ASCII:只保留ASCII字体,即原版渲染又粗又大的字母和数字的字体,额外字体将被忽略,Unicode字体定向至 Modern UI
保留其他:舍弃ASCII字体,保留 minecraft:default 下的其他字体,保证模组和小地图兼容性
保留全部:默认值,和原版一样,渲染又粗又大的字母和数字的字体,保证模组和小地图兼容性,Unicode字体定向至 Modern UI
显然,如果你不想用原版又粗又大的字体,只要将默认字体行为调成保留其他即可。
为什么按上面做了还是能看见原版的英文和数字字体?
有些模组有自定义字体定义文件,如果它加入了 minecraft:include/default 字体的引用,也就是原版的英文和数字字体,那么就肯定会优先使用这个字体。因为 Modern UI,乃至原版的强制Unicode字体都只对原版默认字体 minecraft:default 有效,对其他字体无效,所以没有办法禁用这个字体。你可以请求模组作者添加这类支持,或者直接暴力修改jar文件里的JSON文件(自己翻assets/xxxxx/font/xxx.json)。
我想用原版Unicode字体怎么办?
原版Unicode字体就是GNU Unifont字体,但原版不能提供Unicode支持,你必须通过现代文本引擎加载TTF或OTF字体。你可以前往其官网 https://unifoundry.com/unifont/ 点击 unifont-15.1.04.otf 下载GNU Unifont字体,然后按Ctrl+K打开 Modern UI 偏好设置,向右划至第二页,点开首选字体家族,点击打开字体文件,找到下载的GNU Unifont字体文件即可。
注意:GNU Unifont只有字号为16的倍数时才可渲染像素风,否则边缘会因抗锯齿模糊。你可以选择关闭抗锯齿,你也可以选择将界面比例设为偶数。因为字体大小基值是8,偶数界面比例可以得到16的倍数的字号,例如8*2=16,8*4=32等,这样可以原分辨率渲染。为了节省显存,你还可以在偏好设置中开启固定分辨率,既将分辨率等级固定在2,字号为8*2=16,而不受界面比例影响。
注意:你可能觉得渲染中文和原版没什么两样,但是原版不能正确渲染阿拉伯语、天城文、孟加拉语等复杂语言文字,而 Modern UI 可以正确渲染。同时 Modern UI 字体显存占用远小于原版,所以不推荐为了使用原版字体而关闭现代文本引擎。
如何自定义字体?
如果你想自定义字体给 Minecraft 用,参考上文TTF字体的JSON格式创建新字体,然后使用富文本格式指定字体即可。
例如您新建资源包,您使用 customfont 作为命名空间,创建 assets/customfont/font/customfont1.json 文件,并将 my-font.ttf 放在 assets/customfont/font 里,编辑JSON文件如下:
{
"providers": [
{
"type": "ttf",
"file": "customfont:my-font.ttf"
}
]
}
如上文所说,你的字体名为 customfont:customfont1,在游戏中输入指令 /tellraw @a {"text":"Hello","font":"customfont:customfont1"} 即可使用您的自定义字体。
游戏里文本默认都是使用默认字体,你可以按Ctrl+K打开 Modern UI 偏好设置,向右滑至第二页修改默认字体。
现代文本引擎兼容原版的位图字体,你可以添加自定义位图字体。
为什么开启彩色表情符号后,一个Emoji后加一个英文冒号就不是彩色的了?
这是排版引擎的正常行为,如果您想强制彩色表情符号,您需要添加VS-16(\uFE0F)字符,别问我咋打。你可以执行指令 /modernui text layout "文本" 来查看渲染成了什么字体。
为什么荧光告示牌上的位图字体没有边框?
这是正常的,因为位图字体不是外框字体,没有边框的路径信息。而原版那种拙劣的渲染方式非常卡,现代文本引擎舍弃了该方式。你可以使用TTF字体解决该问题,再说又不是没有像素风的TTF字体。
为什么机械动力翻牌显示器不能布局文本,不能渲染Emoji?
渲染Emoji需要文本布局,而翻牌显示器它本身压根就不进行布局,不支持Unicode和国际化,而是像原版一样一个码点一个码点渲染。Modern UI 虽然添加了兼容代码,但只能保证能使用 Modern UI 的字体和光栅器,渲染质量受分辨率等级影响(界面比例)。
我能加载自定义Emoji字体吗?
不能。Modern UI 并不支持字体中带的SVG或PNG,目前只能使用 Modern UI 自带的 Google Noto Color Emoji,因其开源免费,支持 Unicode 15.0,和你使用的操作系统没有关系。