SDF vs. MSDF vs. Slug:GPU 文字渲染
摘要
AlphaPixel 发布了 Slughorn,这是对 Eric Lengyel 提出的 Slug GPU 文字渲染技术的 C++20 实现(于 2026 年 3 月奉献至公有领域),并对比了 SDF、MSDF、纹理图集与 Slug 这几种方案,以在 GPU 上以任意缩放级别渲染清晰的矢量字形。
暂无内容
查看缓存全文
缓存时间: 2026/09/30 17:03
# SDF vs MSDF vs Slug:GPU 文本渲染 | AlphaPixel
来源:https://alphapixeldev.com/sdf-vs-msdf-vs-slug-vs-rive-gpu-text-rendering/
## 如何在 SDF、MSDF、Slug、纹理图集或 Rive 之间选择文本渲染算法?
文本看上去很简单,直到你需要自己把它画出来。一个字母不是一张图片,而是一组轮廓:由直线和贝塞尔曲线组成的闭合环,按照填充规则来填充。在 CPU 上把字形栅格化成位图,是一个已经解决的问题。而在 GPU 上,在任意尺寸、任意 3D 变换下清晰地渲染,还要应付每帧都在变化的文字,就不是了。大多数引擎通过预先将字形烘焙到纹理中来回避这个难题,同时默默忍受其中的妥协。
2017 年,Eric Lengyel 发布了一个名为 Slug 的算法,不再回避这个问题。它在片元着色器中直接从轮廓渲染字形,不需要纹理图集,也不需要逐帧细分。Lengyel 在 2019 年为它申请了专利,并于 2026 年 3 月 17 日将该专利贡献到公有领域。这就是我们构建 Slughorn(https://alphapixeldev.com/slughorn/)的原因——我们用 C++20 实现了 Slug 技术——也是我们现在可以谈论它如何工作、以及在哪些场景下取胜的原因。
这是一次关于 GPU 文本渲染实际工作方式的导览,从位图图集一直到 Slug,以及每种方法各自适合的位置。
## 字形为什么难
每一种可缩放字体都把每个字形存储为矢量轮廓。TrueType 使用二次贝塞尔曲线,OpenType 配合 CFF 使用三次贝塞尔曲线,两者都混合了直线段。字母内部由填充规则决定,通常是非零环绕规则:从像素发射一条射线,统计轮廓穿过它的次数,如果环绕数非零,该像素就位于字形内部。
字母 a 以字体轮廓的形式呈现,曲线上点标记为实心点,贝塞尔控制点标记为空心圆,说明字形是由曲线而非像素定义的。
字形是一组轮廓,而不是像素:实心点是曲线上的点,空心圆是贝塞尔控制点。
渲染器必须同时完成三件事并把它们做得很快:正确填充内部、产生干净的抗锯齿边缘,还要在菜单里 8 像素高的小字和透视旋转广告牌上铺满屏幕时都保持锐利。在 CPU 上,你只需在目标尺寸下把每个字形栅格化一次就大功告成。在 GPU 上,你想每帧绘制数千个字形,任意缩放,最好完全不需要重新栅格化。正是这个约束,决定了下面每种技术的权衡。
## 方法一:纹理图集(位图字形)
最古老、至今也最常见的方法。将每个字形栅格化一次,以一种尺寸存入一张共享纹理(即图集),然后把屏幕上的每个字符画成一个带纹理的四边形,采样它所在的槽位。
它速度快、易于移植,只要有纹理单元就能运行。这就是它无处不在的原因。
问题一放大就出现了。超过烘焙尺寸继续放大,字形就会变成模糊或块状的像素,因为你是在放大一张位图。缩小它则会产生闪烁和细笔画丢失,除非你烘焙了 mipmap 层级。你想要清晰的每种尺寸都需要另建一套图集。每种语言都是另一个难题:拉丁文字的图集很小,但中文、日文和韩文有数万个字形,在多个尺寸下烘焙全部字形会是一场内存灾难。而且位图不知道自己正被透视观看,贴在 3D 表面上的文字看起来很软。
字母 a 以三种方式呈现:从小型纹理图集放大呈现的块状像素、放大后带模糊过滤的效果,以及从轮廓渲染出的清晰结果。
放大已烘焙的图集字形(左、中)与从轮廓渲染(右)的对比。
## 方法二:有向距离场(SDF)
Valve 引入的解决方案,支撑了整个行业十年。Chris Green 2007 年的 SIGGRAPH 工作《Improved Alpha-Tested Magnification for Vector Textures and Special Effects》存储的不是字形的像素,而是有向距离场:每个纹素保存到最近边缘的距离,内部为正,外部为负。在着色器中你对该场进行采样,并在零处进行阈值判定。由于距离场平滑插值,你可以把一张很小的 SDF 纹理放大很多倍,仍然得到干净的边缘;通过软化阈值还能获得廉价的抗锯齿效果。
一张小纹理,合理范围内分辨率无关,一个廉价的着色器。在很长一段时间里,这是清晰 UI 文字和游戏 HUD 的默认选择,在受限硬件上至今仍然如此。
但 SDF 仍然是在固定分辨率下烘焙、采样的纹理,而且它会"撒谎"。尖锐的角在距离场中是一个不连续点,双线性插值会把它磨圆。字母上的每一个硬角——"A"的顶点、"K"的缺口——都会被软化。放大到一定程度,或者字形小到距离场只有几纹素宽时,细笔画就会断裂,细节就会糊掉。
字母 a 作为有向距离场的可视化——一种在字形边缘处归零的平滑渐变,旁边是着色器从中恢复出的清晰黑白结果。
有向距离场(左)与着色器从中恢复出的清晰边缘(右)。
## 方法三:多通道有向距离场(MSDF)
Viktor Chlumsky 的工作,出自他 2015 年的论文和 2018 年的论文《Improved Corners with Multi-Channel Signed Distance Fields》,解决了角的问题。MSDF 不是存储一个距离通道,而是三个,分别放在红、绿、蓝中,每个编码到不同边缘子集的距离,并且经过精心选择,使尖锐的角得以保留。在着色器中你对三个通道取中值。这个中值技巧能几乎完美地重建角落,因此 MSDF 字形在那些会把 SDF 磨成一团的放大倍率下依然清晰。
MSDF 是当前许多团队的最佳平衡点,Chlumsky 的 msdfgen 采用 MIT 许可,已被广泛采用。如果你需要清晰的可缩放文本,并且愿意烘焙图集,这是一个优秀的选择。
但它仍然是一张图集,仍然带有图集带来的代价。你提前在选定的分辨率下烘焙每个字形,因此动态文本或用户提供的文本,以及 CJK 这样巨大的字形集合,仍然意味着需要烘焙管线和内存预算。生成成本比纯 SDF 更高。在非常小的尺寸下,你采样的纹素仍然太少,撑不住精细细节;在极端缩小的情况下,你仍然要和混叠作斗争。三通道查找比单通道消耗更多带宽。MSDF 提高了质量的上限,但它仍然使用图集。
字母 A 分别由单通道有向距离场渲染(角被磨圆)与多通道有向距离场渲染(角保持尖锐)进行对比。
SDF 会把尖角磨圆;MSDF 则保留它们。图片:Viktor Chlumsky / msdfgen(MIT)。
## 方法四:细分与覆盖(Loop-Blinn、NV_path_rendering、Pathfinder、Rive)
另一类方法完全跳过纹理,把轮廓转换成 GPU 可以栅格化的几何体。
- Loop-Blinn 把曲线三角形喂给 GPU,用逐像素丢弃着色器只保留每条二次贝塞尔曲线的内部,再结合模板缓冲区来解决环绕数问题。很优雅,但没有那些牺牲质量的技巧,可靠的抗锯齿确实很难做到。
- 模板后覆盖(stencil-then-cover),以 NVIDIA 的 NV_path_rendering 扩展形式暴露,第一遍把路径画进模板缓冲区,第二遍进行覆盖。质量很高,但依赖厂商扩展和特定的硬件路径。
- Pathfinder 把边缘细分成微小三角形,逐像素计算有向梯形面积,并在计算着色器通道中累积覆盖率。基于分块(tile-based),在现代 GPU 上很快。
- Rive 的渲染器于 2024 年开源,将抗锯齿矢量路径化简为互不重叠的三角形补丁,并通过一条带有像素局部存储的大规模并行管线进行栅格化,在动画矢量美术上达到了 120 fps。
这一类方法是真正分辨率无关的,对于会动的、精心设计的矢量图形来说,往往是正确的答案。Rive 尤其是为会动的美术作品而构建的。代价在于细分本身——几何体发生变化时必须重新细分——复杂字形导致的几何体膨胀,干净的解析式抗锯齿的难度,以及某些情况下对特定硬件特性或扩展的依赖。
字母 a 内部填充了蓝色三角形网格,中间的字腔留空,说明细分方法如何把字形轮廓变成 GPU 栅格化的三角形。
细分方法把轮廓转换成 GPU 栅格化的三角形。
## 方法五:Slug,直接从轮廓渲染
Slug 跳过图集和逐帧细分,把字形保存为一组二次贝塞尔曲线和线段,存放在一个很小的 GPU 缓冲区中。与此同时,Slug 还构建了一个轻量级的逐字形加速结构,把字形划分为水平条带,这样一个像素只需要考虑它附近的少数几条曲线,而无需遍历整个轮廓。
然后它在片元着色器中*直接*解决覆盖率问题。对每个像素,它实际上是投射一条射线,找出这条射线与附近贝塞尔曲线的交叉位置,然后统计这些交叉来计算环绕数,进而得到覆盖率。最难的部分,也是 Lengyel 的独门秘方,是他称为*根的合法性*(root eligibility)的判据:一条精确的规则,规定哪些曲线-射线交点应该被计入,使得在曲线相交的共享端点处环绕数计算是精确的——而朴素的方法在这些地方会产生产生裂缝或重复计数。因为着色器是在解析地求解曲线方程,而不是对烘焙的场进行采样,所以它在任意尺度下都能产生精确的覆盖率和干净的抗锯齿效果。
字母 a 上有一条水平线穿过它,标出了四个编号交点,说明如何通过统计射线的交叉次数来判断像素是否位于字形内部。
Slug 的核心思想:对每个像素,投射一条射线,并统计它穿过轮廓的次数。
没有烘焙分辨率,所以同一个字形在 6 像素或 6000 像素下都同样锐利;在任意的 2D 和 3D 变换(包括透视)下也保持锐利,因为覆盖率是在变换之后逐像素计算的。没有图集,所以十万个 CJK 字形的代价只是一套字体大小的轮廓数据,而不是像视频一样大的图集。文字可以每帧变化而没有烘焙成本,这正是实时数据、用户输入和本地化内容所需要的。而且所有这一切都发生在一次普通的片元着色器绘制调用中,不需要任何厂商扩展。
当你无法预测文字会如何被观看时,这一点至关重要。图集、SDF 和 MSDF 都是提前烘焙了固定分辨率,这悄悄假设了屏幕上文字尺寸和观看角度有一个有界的范围。当文字平面与相机之间的关系事先未知时——自由的 3D 相机、任意缩放、特写、或者陡峭的离轴掠射角——这些烘焙的近似就会失效:放大超过烘焙分辨率后,图集会模糊,SDF 和 MSDF 则会变圆、糊化;在斜视角下,采样的场会产生混叠。你无法为每一种可能的视图预分配足够的分辨率,否则存储会爆炸。Slug 在变换之后逐像素解析地计算覆盖率,因此无论观察者有多近、多远或多倾斜,它都保持精确,没有任何烘焙,也没有上限可以撞上。细分是唯一共享这一特性的另一类方法,但它要为此付出细分成本和更困难的抗锯齿代价。这就是为什么在交互式 3D、AR 与 VR、飞行漫游、动态 HUD,以及 CAD 或数字孪生导航等场景下,Slughorn 是应当首选的方案——在这些场景中,你无法事先决定观察者会靠得多近、角度有多偏。
## 直观比较差异
我们用 Slughorn(我们的 osgSlug 集成)渲染了同一个大写字母 R,并与你实际会考虑的其他方案并列比较:单通道 SDF、MSDF、Rive 的渲染器,以及 osgText 的位图及其从位图派生的 SDF。每个基于纹理的方案都获得了相同的预算,即每 em 64 个纹素,因此区分开它们的是技术本身,而不是分辨率。
六个面板在正常尺寸下正面渲染同一个大写字母 R。Slughorn、Rive 和 MSDF 都清晰地复现了轮廓,单通道 SDF 仅有略微磨圆的角,只有 osgText 位图面板看起来是软的。
在各自烘焙尺寸下正面观察时,六个方案中有五个几乎完全相同:Slughorn、Rive 和 MSDF 都复现了轮廓,单通道 SDF 只在笔画末端的脚部略有磨圆。异类是 osgText 位图,一张 64 px/em 的图片被放大了约四倍,它无法恢复从未存储过的细节。
把字形倾斜成透视视角,情况就不同了。
同一个大写字母 R 以六个面板呈现,倾斜成掠射式 3D 透视。Slughorn 和距离场面板保持了形状和干净的边缘,osgText 位图随着字形远去而模糊,而 Rive 的 R 被扭曲成了错误的形状。
在掠射透视下,Slughorn 和三个距离场面板都能让 R 稳稳保持原位、边缘干净,因为它们在字形自身的平面内逐像素计算覆盖率,所以投影对它们没有任何成本。osgText 位图随着远去而模糊。Rive 的 R 形状是错的:它的渲染器只接受 2D 仿射变换,而透视投影不是仿射的,因此它最多只能给出一个中心精确、向边缘逐渐漂移的近似。
现在放大到单条边缘的特写。
六个面板对大写字母 R 单条边缘的极端特写。Slughorn 和 Rive 保持了笔直干净的边缘,距离场面板出现了小缺口,osgText 位图已经溶解成平滑的灰色渐变。
在极端放大下,只有基于曲线的渲染器——Slughorn 和 Rive——仍然能产生笔直、干净的边缘,因为两者都是从实际轮廓出发的,无论显示尺寸多大(Rive 在这种视图下是通过每帧重新细分实现的)。距离场面板出现了缺口,因为在存储的采样点之间插值已经无法匹配真实的曲线。osgText 位图已经溶解成单一的灰色渐变,因为它的每个纹素现在覆盖了面板中很大一部分区域。
关于公平性有一点说明,因为技术读者一定会问。这里所有基于纹理的方法都使用了相同的每 em 64 个纹素,给它们更多纹素只会把这些瑕疵推后,而不能消除它们。SDF 和 MSDF 面板使用的是默认烘焙设置,而 MSDF 的误差修正选项确实可以柔化最后一张图中的部分缺口。Rive 在这些视图下已经处于最佳状态,即每帧通过相机渲染,这比它通常嵌入 3D 场景的方式要慷慨得多——通常它会被画到一张纹理上再贴到表面,这样虽然避免了变形,但会像位图一样变糊。
## 正面对比
| 关注点 | 纹理图集 | SDF | MSDF | 细分 / Rive | Slug |
| --- | --- | --- | --- | --- | --- |
| 任意尺寸下锐利 | 否 | 部分 | 大体上 | 是 | 是 |
| 尖锐的角 | 烘焙尺寸下是 | 否 | 是 | 是 | 是 |
| 极小尺寸 | 需按尺寸烘焙 | 较弱 | 更好 | 好 | 好 |
| 大字形集合(CJK)内存 | 极高 | 高 | 高 | 低 | 中等 |
| 动态 / 变化文本 | 需重新烘焙 | 需重新烘焙 | 需重新烘焙 | 需重新细分 | 免费 |
| 3D 与透视 | 模糊 | 尚可 | 尚可 | 好 | 优秀 |
| 高度动画化矢量美术 | 否 | 否 | 否 | 优秀(Rive) | 好 |
| 低端 / 老旧 GPU 上运行 | 优秀 | 优秀 | 好 | 视情况而定 | 需要较强的着色器阶段 |
| 实现复杂度 | 低 | 低 | 中等 | 高 | 中到高 |
浅绿色标记表示 Slug 是最佳或并列最佳的选择。
## 那么你该用哪一个
没有唯一的赢家,每项工作都有合适的工具。
- **在需要文本在任意尺度下、在 3D 与透视(VR/AR/xR)下都保持锐利时,选用 Slug(Slughorn)**;当你拥有巨大或动态的字形集合时;当文字不断变化时;或者当你需要把混合矢量 UI 渲染进实时管线时。这正是GIS(https://alphapixeldev.com/geospatial-development/)地...
相似文章
Show HN:一个ASCII 3D渲染引擎
GlyphCSS是一个JavaScript库,它使用ASCII字符在DOM中渲染带纹理的3D网格,支持多种3D格式,并与原生JS、React和Vue集成。
为我的离线渲染器制作一个着色语言
作者详细介绍了为其离线CPU渲染器SORT创建的自定义着色语言库——微型着色语言(TSL),并解释了其动机,包括学习、灵活性、Apple Silicon支持以及相比使用OSL减少依赖等。
SDL_GPU 最小化、单头文件、高性能的2D图形绘制库
SDL_gp 是一个为 SDL3 设计的最小化、高性能的2D图形绘制库,从 sokol_gp 移植而来,提供了简单的资源管理系统。
在混合Blackwell/Ada集群上对vLLM、SGLang和llama.cpp进行基准测试
本文在混合Blackwell/Ada GPU集群上对vLLM、SGLang和llama.cpp进行长上下文预填充基准测试,发现vLLM在异构设置上显著优于其他引擎,而SGLang由于FP4支持限制,在使用Ada显卡时会崩溃。
Fable创建了新颖的4D splat格式
一种名为.splat4d的新型4D高斯splat格式,具有可调误差边界,对原始splat的压缩比为16-58倍,并支持动态场景的原生HTTP Range流式传输,代码和演示已提供。