在 GPU 上直接画贝塞尔字形
字形如何变成像素讲到轮廓要被光栅化填到像素网格。问题是这一步在什么时候做、做几次。传统做法是预先把每个字形按某个字号栅格化成一张位图,塞进纹理图集(glyph atlas),渲染时当贴图用——开销小,但放大就糊,每个新字号都得重新生成一张。另一条路是 Slug 的思路:把字形的贝塞尔轮廓原样交给 GPU,在像素着色器(pixel shader)里对每个像素实时求它落在轮廓内 / 外、被覆盖多少——于是任意缩放都锐利,无需任何预栅格化。代价是每个像素都要算一遍曲线覆盖,正好是 GPU 最擅长的并行计算。下面先看缩放对照,再看让这条路真正跑通的关键技巧 Dynamic Dilation。
SDF 是两者之间的折中:不少游戏引擎把字形烤成有向距离场(signed distance field)纹理,放大比普通位图清晰得多、还能廉价做描边 / 发光,但本质仍是固定分辨率的纹理,极端放大、尖角与细节处仍会失真。Slug 这类直接渲染则完全不烤图,把曲线方程留到着色器里现算,从根上没有分辨率上限。
1 · 让直接渲染跑通的关键:Dynamic Dilation
GPU 不会对全屏每个像素都跑一遍字形的着色器——太浪费。它只在字形的一圈包围几何(由控制点张成的多边形)内部光栅化、运行着色器。麻烦在于:光栅化只对中心落在图元内的像素运行着色器。字形边缘那些被曲线部分覆盖的像素,中心常常落在包围多边形外侧一点点——GPU 于是直接跳过它们,字形边缘出现缺口、锯齿。Slug 的解法是在顶点着色器里把包围多边形按需向外扩张(dilate)一小段距离,把这些边缘像素重新纳入;扩张又不能过头,否则 GPU 空跑一堆空像素。拖滑块找那个刚好的扩张量。
2 · 三种字形上屏方式
| 维度 | 位图图集 | SDF 图集 | 直接渲染(Slug) |
|---|---|---|---|
| 存什么 | 某字号的像素 | 到边缘的距离场 | 贝塞尔曲线本身 |
| 缩放 | 放大即糊 | 较好,极端仍失真 | 任意尺寸锐利 |
| 换字号 | 重烤一张 | 多数情况复用 | 无需任何准备 |
| 尖角 / 细节 | 受分辨率限 | 易圆钝 | 精确 |
| 每像素成本 | 一次采样 | 一次采样 | 现算曲线覆盖 |
| 显存 | 每字号一份 | 一份 | 仅曲线数据 |
Slug 把每像素成本换成「无分辨率上限 + 不必预烤」。上面演示用 2D canvas 的矢量填充近似「直接渲染」的锐利效果,真实 Slug 在 GPU 着色器里对每条二次贝塞尔做内 / 外与覆盖率计算,Dynamic Dilation 则发生在顶点着色器。
这项技术现在是公有领域:Slug 的作者 Eric Lengyel 在其满十年时把相关专利贡献到公有领域,任何人都可自由实现该算法,并在 GitHub 上公开了参考着色器。早年它是商业授权、被 Activision / Blizzard / Adobe 等采用的库,如今门槛已不在。
这页接哪里:它和字形如何变成像素是同一件事的两种时机——那页讲浏览器在 CPU 侧把轮廓填进像素并抗锯齿,这页讲把这步搬到 GPU、推迟到运行时按需做;而被渲染的那条干净轮廓从哪来是上游的另一端。
3 · 相关链接
- A Decade of Slug——terathon.com · Eric Lengyel。本页蓝本。GPU 直接从贝塞尔曲线渲染字体的 Slug 算法十年回顾:商业落地、简化历程,核心创新 Dynamic Dilation,以及把专利贡献到公有领域、公开参考着色器的公告。
- GPU-Centered Font Rendering Directly from Glyph Outlines——jcgt.org · Eric Lengyel (2017)。Slug 的原始论文:如何在像素着色器里直接对字形轮廓的二次贝塞尔做内 / 外测试与抗锯齿覆盖,无需预栅格化或距离场。
- msdfgen:Multi-channel signed distance field generator——github.com · Viktor Chlumský。SDF 路线的代表实现:多通道距离场在保留尖角上优于单通道,作为「图集」与「直接渲染」之间折中方案的对照。
- Easy Scalable Text Rendering on the GPU——medium.com · Evan Wallace。用三角形 + 覆盖累加在 GPU 上直接填字形轮廓的另一种思路,有助于理解「现算覆盖」这一类直接渲染方法的共性。