← 字体里的隐藏开关 / 在 GPU 上直接画贝塞尔字形 待审核 17 / 18
加餐 · GPU 渲染 / 直接画轮廓

在 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 上直接填字形轮廓的另一种思路,有助于理解「现算覆盖」这一类直接渲染方法的共性。