前⾔
本⽂旨在介绍⽂字渲染需要了解的基本知识点,以及些许延伸扩展;不涉及到实际代码的编写,不涉及具体算法实现,但中间穿插引⽤的外部⽂档有部分代码实现可供参考。
本文引用的图都是来源网络博文,部分图片并未记录来源。如涉及侵权请联系我。
字体发展历史
- 在 Macintosh 电脑中,使用点阵字体(格式后缀:bdf、pcf、fnt、hbf等)。点阵字体放大后会出现锯齿、模糊的情况。
- 在同一时期,Adobe 发明了基于 PostScript Type 1 的矢量字体格式,但 Type 1 字体是加密的,Adobe 通过售卖字体认证赚取利润,苹果也不得不向 Adobe 购买 Type 1 字体认证。
- 于是,苹果决定设计全新的字体格式,最后在 1991 年发布为 TrueType 格式,同时也包括 Times Roman、Helvetica、Courier 等大量字体。
- 但是 TrueType 字体反响并不好,因为大部分用户已经购买了 Adobe Type 1 字体,没必要再切换。因此,苹果联合微软对抗 Adobe,授权给微软使用 TrueType 字体。1991年,微软在 Windows 3.1 系统上支持了 TrueType,并和 MonoType 公司联合开发了大量著名字体,例如 Arial 等字体。
- 1994 年,微软独自开发了 TrueType Open 字体;1996 年 Adobe 加入开发,兼容了 Type1 字体格式,更名为 OpenType。2007 年,OpenType 被国际标准组织 (ISO)采用。OpenType 常见后缀 otf、ttf、ttc。
- 2023年,Adobe 将停止支持 Type 1 字体创作。
SFNT:SFNT 是苹果在开发 TrueType 字体格式时设计的通⽤字体数据存储结构,SFNT 是 spline font 或者 scalable font 的缩写,TrueType、OpenType、WOFF 等格式都采⽤ SFNT 作为容器。
WOFF:Web Open Font Format,采⽤压缩格式,字体⽂件更⼩,适合⽹⻚使⽤。可以简单理解为 OpenType + 压缩。⼀般⽐ ttf 字体⼩ 40%。WOFF2 是 WOFF 的下⼀代标准,在 WOFF 的基础上提⾼ 30% 的 压缩率。
字体类型
点阵字体 / 图集字体
⽮量字体
如下图所⽰,利⽤直线和⻉塞尔曲线来描述字体。每个字形中存储的是⼀系列控制点,由控制点⽣成字的轮廓,再进⾏填充颜⾊。
轮廓:指由直线、⼆阶⻉塞尔曲线组成的闭合曲线。
点阵 / 图集字体渲染
点阵 / 图集字体的渲染,其实就类似图⽚纹理的渲染。通过⼀定的策略,找到对应⽂字的纹理坐标进⾏渲染即可。
点阵字体已经淘汰,但图集形式的字体,在某些场景下还会⽤到,但存在「放⼤后模糊」等问题。但是,利⽤ SDF 可以实现⽆损放⼤。
有向距离场 SDF (Signed Distance Field)
SDF 中每个像素,不再表⽰字体光栅化后的颜⾊,取⽽代之记录的是从⼀个点到集合边界的的距离值,其值的正负对应该点在集合外部或内部。
在⽂字渲染这个场景下,遍历光栅化后⽂字纹理中所有的像素,将像素分为 in 和 out 两种,对于⽬标纹理的每个像素,找到源纹理中对应的像素,对这个像素求最近的和⾃⼰不是同⼀种像素之间的距离,其中 in 记录为正值,out 记录为负值。最后再将距离映射到 0 - 1 之间。
那么在 0.5 附近就是边界,因此在 0.5 ± delta 区域做平滑,其余 < 0.5 不填充,> 0.5 填充即可。
SDF 的问题以及 MSDF
利⽤ MSDF 的渲染,锐利的直⻆会被渲染为圆⻆。
改进后的 MSDF 可以解决这个问题。
参考:Signed Distance Field与Multi-channel signed distance field
SDF 的其他应⽤
本小节图片来源于博文:有号距离场(SDF)
形变动画
上面的形变动画就是利用如下的 SDF 来实现的。序列帧柔和过渡
上面的动画,利用 SDF 优化效果后效果如下:软阴影
碰撞检测
Ray Marching
OpenType 格式简介
解析⼯具:https://photopea.github.io/Typr.js/
TableDirectory
核⼼字段:
- numberTables 表⽰ TableRecord 总数
- tableRecords 数组记录所有 TableRecord 数据
TableRecord
核⼼字段:
- tableTag,以 4 个 char 进⾏标识不同的表,例如 cmap / loca / glfy
- offset & length
Tables
maxp
maxp 中记录了各种数据的最⼤值,例如简单字形最多⼏个点等等。从表中的数据,可以计算得到解析这个字体最多需要多少内存。head
字体基础 / 全局信息等,version、magicNumber 等hhea
hhea 记录了⽔平排版信息cmap
cmap 存储的是从字符「码位 code point」到 glyphId 的映射。当然 cmap 中不是简单的 map 映射表,相对复杂,不展开。
因同⼀个字符,在不同编码下的码位可能有差异,这点在 cmap 的映射中通过 platformID 和 encodingID 来 区分。loca
loca 中存储的是 offsets 数组,下标就是 glyphId,取出的 offset 是字形数据在 glyf 表中的位置。glyf
glyf 中存储了所有的字形数据。
字形基本信息:
⽮量字体光栅化(CPU)
glyph 字形轮廓
- 通过 cmap 表,完成从 code point 到 glyphId 的映射
- 通过 loca 表,完成从 glyphId 到字形 offset 的映射
- 解析 glyf 表,结合 offset,得到字形数据
- 解析字形数据,得到所有控制点信息,对于每个控制点,都有标记标明是在轮廓线上 in-curve / 线外 off-curve
那么,有了控制点数据之后,就可以从控制点计算得到直线 / 贝塞尔曲线参数。这里的贝塞尔曲线都是二次贝塞尔曲线,因此需要 P0 / P1 / P2 三个点,其中 P1 是线外的控制点。
直线 / ⻉塞尔曲线参数解析逻辑:
按顺序遍历控制点数据,假定 P 是上一个在线内的点。从数组中第一个线内的点开始,记为 P0,P1 和 P2 就是 P0 后面的点。
- 如果 P1 在线内,那么 P0 和 P1 组成直线(下一个循环的 P0 为当前 P1)
- 如果 P1在线外:
- 如果 P2 在线内,那么 P0 & P1 & P2 组成二次贝塞尔曲线,P1 为线外控制点(下一个循环的 P0 为当前 P2)
- 如果 P2 在线外,说明是连续的⼆阶⻉塞尔曲线,P1 和 P2 分别是前后两个⼆次⻉塞尔曲线的控制点,P1 和 P2 的中点是线内的点。那么 P0 & P1-P2 中点 & P1 组成⼆次⻉塞尔曲线(下⼀个循环的 P0 为当前 P1-P2 中点,P1 为当前 P2)
到此,得到了直线、⼆阶⻉塞尔曲线的控制点。
这个时候如果把直线和⼆阶⻉塞尔曲线绘制出来,我们可以得到字形轮廓。
如果需要渲染未填充的字形,那到这⾥就可以缓存起来⽅便之后使⽤了。
但往往还需要考虑如何填充颜⾊。填充算法:
- 种⼦填充算法
- 扫描线算法
⾄此,我们知道了如何从 OpenType ⽮量字体中解析得到⽮量数据,并进⾏光栅化。但如果每次渲染字形都重新光栅化,性能往往⽐较差。并且,⼀个 App / 游戏⽤到的字都相对有限,因此我们可以考虑把光栅化后的字形进⾏缓存。
OpenGL ⽂字渲染代码实现参考:⽂字渲染
光栅化缓存
⼀般会创建⼀个固定⼤⼩的缓存纹理,例如 512 * 512,将光栅化好的字形绘制到纹理中,并记录字形到纹理坐标的映射。当缓存纹理空间不够时,则申请新的缓存纹理继续绘制。当然,也需要配合⼀定的 LRU 等缓存淘汰机制。
同时,如何最⼤化的利⽤缓存纹理的空间,也诞⽣了⼀些算法。(KeyWord:书架算法)
游戏中⼀般会采⽤「预置静态光栅化缓存」和「动态⽣成光栅化缓存」结合的⽅案。
⽮量字体渲染(GPU)
GPU text rendering with vector textures(DEMO)
Paper - GPU-Centered Font Rendering Directly from Glyph Outlines
利⽤ GPU Compute Shader 进⾏实时⽮量渲染,虽然可⾏,但⽤的场景不多。
相⽐于光栅化结合⼀些 trade off 策略,⽮量渲染在效果上优势不⼤,性能也没有光栅化的⽅案好。
常⻅字体渲染库
- FreeType:https://freetype.org/
- Cairo:https://www.cairographics.org/
更多字体相关库可以参考⽂章:⽂字渲染⼀探
MORE
- 字体微调
- 抗锯⻮
- 排版
References
TrueType
FreeType
OpenType® Specification Version 1.9
⽂字渲染的那些事(⼀)字体是如何存储的?
⽂字渲染那些事(⼆)⽂字的形状是怎么表⽰的?
TrueType字体⽂件解析和字体光栅化
TTF⾥都有什么
Korok字体系统设计
⽂字渲染⼀探
Typography for User Interfaces
使⽤Compute Shader计算有向距离场
Signed Distance Field与Multi-channel signed distance field
有号距离场(SDF)
在 WebGL 中渲染⽂字