欢迎光临散文网 会员登陆 & 注册

Minecraft 1.20 - Modern UI 文本引擎技术规范

2023-07-16 20:59 作者:冰月酱BloCamLimb  | 我要投稿

1. 什么是Modern UI?

本文所指的Modern UI是由我——BloCamLimb(Icyllis Milica)创作的桌面应用开发框架,使用Java编写,包含使用LWJGL与OpenGL 4.5、Vulkan 1.1接口的独立图形渲染引擎,UI组件、窗口系统、事件循环、动画状态机,以及使用ICU库(International Component for Unicode)的国际化支持的富文本布局引擎。

项目在LGPL 3.0许可下开源,代码仓库:https://github.com/BloCamLimb/ModernUI

Modern UI 本身与 Minecraft 并无关联,但我为 Minecraft 制作了 Modern UI 扩展组件,使得Modern UI 能运行在 Minecraft 中,并提供了模组开发API和一些扩展功能;其中最复杂的就是专门为 Minecraft 设计的文本引擎,称为现代文本引擎(Modern Text Engine)。该扩展 Mod 需 Forge 启动,本文将介绍我在设计该引擎的一些技术细节。

该项目同样在LGPL 3.0许可下开源,但代码被移动到了新仓库:https://github.com/BloCamLimb/ModernUI-MC

3. U16字符串到U32字符串

在Java中,字符是无符号16位整数,对应Unicode中的16位码元(code unit)。而Unicode字符集的字符需要用32位整数表示,称为码点(code point)。如果一个字符在BMP平面(Basic Multilingual Plane)上,那么一个码元就表示一个码点;如果一个字符在SMP平面(Supplementary Multilingual Plane)上,那么两个相邻的码元组成一个码点,前一个码元称为高代理,后一个码元称为低代理,这两个码元合称代理对(Surrogate Pair)。所以要想得到Unicode字符串,就得先把输入的U16字符串转成U32字符串。在这里往往要执行一个附加的操作,就是修复无效代理对。如果当前字符是高代理,而下个字符不是低代理,那么就把这个字符替换成U+FFFD(Replacement Character);如果当前字符就是低代理,那么直接将其替换成U+FFFD。早期Modern UI可以单独设置是否要修复无效代理对,在新版中已经默认启动,无法再设置。如下图,英文字母只是示意,代指某个字符:

U16字符串转U32字符串


4. 字形列表

文字渲染的单位是字形(Glyph),想要渲染文字,就要把字符串(Characters)转成字形列表(Glyph List),并且需要知道每个字形在一段文字中的相对位置(x,y)。字形是每个字体文件(或字体家族)定义的,不同字体的字形图像和字形码不一样。

那么一个U32字符就对应一个字形吗?显然没有这么简单。

对于英文和中文,一个码元就可以映射一个字形。对于一部分在辅助平面上的字符,一个码点就可以映射一个字形。然而,如果一个码点后面紧接着一个变种选择符(Variation Selector),那么可以同一个码点可以映射成不同的字形。例如:一个不拥有EMOJI_PRESENTATION属性的字符,其后面必须连接一个Variation Selector 16字符(U+FE0F)才可以渲染成彩色Emoji样式,否则应渲染成黑白Emoji样式。这种码点到字形的映射表存储在TrueType字体文件中;映射表有多种格式,其中UVS格式(Unicode Variation Sequences),支持变种选择功能。

变种选择

支持变种选择符还没结束,有许多语言的文字包含连接字符(Combining)的包围字符(Enclosing),这意味着两个或更多个字符分开渲染与合在一起渲染的结果不同。例如上面提到的U32字符串为"AcDfg",单独渲染[A,c,D,f,g]这五个字符对应的字形是[o,p,q,r,s],合起渲染"Ac"有可能是"mn",合起渲染"AcDfg"有可能是"uvwxy"。你看,原本的ab两个码元,可以渲染成o,可以渲染成m,也可以渲染成u(注:也可能超过两个码元才对应一个字形,见后文提到的字素簇的概念),所以一个字符加一个变种选择符也不能唯一地确定一个字形,它和前后的多个字符有关,我们称其上下文(Context)或语境。如下图,上下文范围和布局范围的单位是码元而非码点:

单独渲染5个字符
不同上下文范围的字符

要想正确得渲染文字,就必须正确地将字符串转为字形列表,这个过程称为文本整形(Text Shaping),它需要提供字符串、字体Strike、上下文范围、布局范围和文字方向。其中上下文范围必须大于等于布局范围。例如上述例子中,渲染[0,3)范围的"abc"的上下文范围不同,字形也不同。

HarfBuzz是一款开源文本整形引擎,Android,X11,Windows,JavaFX,Chromium等内部都使用了HarfBuzz,Modern UI也不例外。HarfBuzz是用C++编写的,意味着不同平台要编译出不同的版本,但Java 11以上自带HarfBuzz,所以不需要额外添加native库,这也是为什么Modern UI在Minecraft 1.16.5上也需要Java 11才能运行的原因。

5. 文本运行?逻辑运行?视觉运行?样式运行?字体运行?

既然HarfBuzz可以正确地将字符串转为字形列表,那我们能直接把整个字符串喂给HarfBuzz吗?显然没有这么简单。我们必须把整个字符串分成多个运行(Run),每个运行又分成多个子运行,每个子运行又可以分成它的子运行。

高级布局引擎

所谓一个运行,就是指一个区间 [start,end) 附加这个区间的信息,这个区间必须是上下文区间。如上图所示,一个长度为10的字符串,首先要计算 BiD i运行决定文字方向;然后计算 Style 运行决定字体样式;然后计算 Font 运行决定使用哪个字体(包含样式和大小);最后计算 Script 运行决定这段文字的字母系统。我们需要把每个 Script Run 进行 HarfBuzz 整形,最后按照视觉顺序连接在一起。每一级运行都有不同的算法。

大部分语言是从左向右读的,例如英语,而有一些语言是从右向左读的,例如阿拉伯语。如果一段文字混合着英语和阿拉伯语,那么我们该如何渲染文字呢?相信你已经注意到了这里有很多顺序:逻辑顺序(Logical Order)、视觉顺序(Visual Order)、LTR(从左到右顺序,Left-to-Right)、RTL(从右到左顺序,Right-to-Left),这便是双向文本分析(Bidirectional Analysis),简称BiDi分析。无论文本是单样式文本还是多样式文本,必须先进行BiDi分析,且要分析的字符串必须是逻辑顺序。

所谓逻辑顺序,就是文字阅读的顺序,同样也是字符串在内存和硬盘中存储的顺序。例如 English Arabic Text 便是逻辑顺序,阿拉伯语的 A 在英语字母 h 的后面。而渲染在屏幕上时,它应该是 English cibarA Text,读 English 时还是按照从左向右读,而 Arabic 是从右向左读,视觉上阿拉伯语的 A 在阿拉伯语的 r 后面。渲染时要将逻辑顺序重排序成视觉顺序,并将 BiDi 文字分成多个 BiDi 段落,每个段落要么是 LTR 方向,要么是 RTL 方向,每个段落也称为 BiDi 运行。我们只需调用 ICU 库中的 Bidi 类便可完成 BiDi 分析,但 BiDi 有四种算法,Modern UI 又在这四种算法的基础上提供了双向文本启发式算法来决定最终的算法。

样式运行是由逻辑顺序上字体样式的转折点决定的,每一个连续的、字体样式不变的区间便是一个样式运行。字体样式指粗、斜体的组合,它会影响字形渲染和字形位置,尽管它们的字形码是相同的(属于同一字体家族或模拟粗、斜体)。

字体运行的计算比较复杂,样式运行中包含了要使用哪个字体,但这个字体指的是一个字体合集,其中包含了多个字体家族,每个字体家族又包含了多个字体文件。我们要从多个字体家族中找到可为当前连续区间渲染字符的、最佳匹配的字体家族,并根据字体样式选择其中的某款最合适的字体,来进行后续的计算。这套算法是由 Modern UI 实现的,也涉及上下文分析。简要概括就是根据连体字、Emoji 组合、字体偏好的语言地区、是否包含当前字符、是否包含变种选择符,尽可能让相邻文字使用同一个字体,并且保证能渲染该字符。如果所选字体不能渲染该字符,Modern UI 则会从系统中找出能渲染它的字体。

FreeType 内容省略。

6. Minecraft 中的多样式文本组件

Minecraft中的文本有四种形式:String、FormattedText、Component、FormattedCharSequence。其中 FormattedText 和 Component 都可以提取出 String,而 Minecraft 中 String 允许包含格式控制码,即ChatFormatting。它由章节符号U+00A7前缀和一个码元组成。码元是一个ASCII字符,不区分大小写,它决定格式类型,分为16个颜色控制码,2个字体样式控制码(粗、斜体),2个效果控制码(下划线、删除线),1个混淆控制码和1个重置控制码。控制码本身不进行布局和渲染,只更改当前字符的样式,控制状态机。因此在Modern UI布局引擎中,有 strip* 概念,意思是剔除控制码之后的字符串(或字符数组)。

Style 是一个包含所有样式信息的类,如文字颜色、粗、斜体、下划线、删除线、混淆字、字体名称、悬浮事件、点击事件和建议(用于补全命令)。对于布局和渲染而言,只需知道前几种。Modern UI可以将字符样式压缩成一个32位int,其中低24位表示文字颜色,25到29位分别表示,粗体、斜体、下划线、删除线、混淆字;第30位表示快速数位替换,31位表示位图替换,32位表示隐式颜色。隐式颜色就是不使用低24位的RGB颜色,而使用参数提供的基样式(基颜色)。

FormattedText 是一个接口,可包含多个样式化文本,通过 visit 函数遍历其内容,该函数接受一个 Style 作为基样式,每个内容的样式会叠加在此样式上,遍历可得到所有的 String 和 该条目所使用的的 Style。当然,这里的 String 也可以包含控制码,所以 Style 并不是最终的 Style。Component 则是 FormattedText 的实现,每个 Component 可以按照逻辑顺序包含多个子 Component,每个 Component 包含一个 String 和 Style,其中 String 和 Style 可变。

Modern UI 在每次处理 FormattedText 时,都会遍历其内容,构建剔除控制码之后的字符数组(单位为码元),以及一个int数组存储字符样式(压缩方法见上)和一个Object数组存储每个字符所使用的字体名称。构建会使用预分配的缓冲区提高性能,这些缓冲区存放在一个共享的 LookupKey 中,用于从缓存中查找之前的布局结果。如果Cache Miss,便会执行文字布局。

FormattedCharSequence 是一个函数式接口,表示深度处理的字符串,其接收一个 Sink 得到每个码点和 Style。但是 Minecraft 会通过 FormattedBidiReorder 将 FormattedText 处理成排序后的 FormattedCharSequence。如果 Minecraft 直接用处理好的 FormattedCharSequence 调用渲染,那 Modern UI 得不到完整的按照逻辑顺序的字符数组,则不能完成 BiDi 分析和其他布局操作,因此 Modern UI 使用了诸多优化来尽可能禁止原版或其他 Mod 创建 FormattedCharSequence。

7. 禁止重排

为了得到 FormattedText 而不是 FormattedCharSequence,Modern UI 创建了一个新类 FormattedTextWrapper 实现 FormattedCharSequence,并持有原始 FormattedText 的引用。如下:

这样我们可以通过 instanceof 来得到原本的 FormattedText。这里的 accept 只是一个备用方案,如果原版直接把多个 FormattedCharSequence composite 在一起,那么内部会调用accept,这里的实现是不进行重排,直接遍历 FormattedText 按照逻辑顺序传输码点,这种情况下 Modern UI 会使用最慢的布局方式,如原版的聊天栏。

原版有几个类中都有输入一个 FormattedText 返回一个 FormattedCharSequence 的方法,因此 Modern UI 直接通过 Mixin 重写该方法,直接返回 FormattedTextWrapper。这里有 FormattedBidiReorder、Language 和 ClientLanguage。此外 Font 类的 bidirectionalShaping 也直接返回原始字符串。

在处理格式控制码时,需要根据码元得到对应的 ChatFormatting,然后应用到 Style 中,每次遍历都要大量的这种操作。原版的 ChatFormatting#getByCode(char) 是非常蠢的:

不难看出,Mojang 这段代码性能不佳。toString()和values()会创建新的对象,toLowerCase()不是特别快,还要遍历长度为22的数组。因为格式控制码全是ASCII字符,Modern UI 做出了改良,直接创建一个长度为128的引用数组,以大小写的char code作为索引,直接查表一次得到 ChatFormatting,比原版不知道快到哪里去了:


8. 文字度量与 Minecraft 坐标系

Modern UI 之所以能提供高质量渲染,是因为 Modern UI 渲染和布局能1比1精确到像素网格,不像原版一样直接拉伸放大,1x1变成2x2、变成3x3,依然是像素化的。换句话说,Modern UI 使用的单位是屏幕空间(或者叫设备空间)上的像素,因此在不同GUI比例(界面比例)下,Modern UI 会重新创建文本布局,重新计算字形位置,重新渲染新的、在设备空间下的字形图像。但 Minecraft GUI 中的坐标可不在设备空间下,无论是2D还是3D世界,Minecraft 的文字都使用相同的单位,因此不同 GUI 比例下仅仅是使用了一个缩放系数为 GUI 比例正交投影矩阵。所以 Modern UI 在 Minecraft 中使用的坐标,必须在原本的基础上做该变换的逆变换,我通常称这个坐标系为 Minecraft GUI 缩放坐标系(Minecraft GUI scaled coordinates),它与屏幕空间相差一个缩放变换,我通常称这个变换过程为归一化。

未完待续

9. SDF 文字渲染

未完待续

10. 快速数位替换

未完待续

11. Unicode 换行算法

未完待续

12. 高覆盖率纹理图册生成

未完待续

13. 彩色 Emoji 处理

未完待续

Minecraft 1.20 - Modern UI 文本引擎技术规范的评论 (共 条)

分享到微博请遵守国家法律