让游戏文字任意缩放2026年8月16日 · 阅读需 9 分钟李瑾Dora SSR 开发者 从一份线下发来的核心算法代码,到两次公开 PR,社区伙伴与我一起把 SDF 接进了 Dora。 游戏里的文字,很少老实待在原来的大小 做普通文档时,文字选好字号便会安静地待在那里。游戏里的文字却没有这么听话:标题会放大登场,按钮会弹跳,伤害数字会从角色头顶冒出来,整个 UI 还可能跟着窗口和场景节点一起缩放。 程序员只是想让文字动一动,文字纹理却会在每次放大时认真交出自己的像素边缘。 于是问题来了:TrueType 字体里明明保存的是矢量轮廓,为什么游戏文字放大以后仍会模糊、露出锯齿? 答案是,字体文件保存着矢量轮廓,不等于屏幕上的每一帧都在直接绘制矢量曲线。 游戏引擎通常会先把字符轮廓转换成像素图,再把许多字符装进 glyph atlas(字形图集)。以后显示文字时,GPU 从纹理中找到相应区域便能快速绘制。这个过程叫作栅格化。 它很适合实时渲染,但也有代价:如果一个字最初只生成了有限的像素,再把小图放大,纹理过滤只能柔化过渡,不能凭空找回没有保存的边缘信息。 所以,我们真正想解决的并不是一个“中文小字号为什么更糊”的专门问题,而是: 怎样让游戏文字在不同字号和节点缩放下,仍然保持接近矢量渲染的稳定质量? 中文笔画密集,很适合检验边缘和细节,但这项能力并不只服务中文或小字号。 三个很自然的办法,为什么还不够 第一次看到文字缩放后的质量不理想时,我们很容易想到三个办法。 第一个是换一款更好的字体。 字体当然重要,但换字体改变的是轮廓设计,不会改变引擎烘焙和缩放字形纹理的方式。再好的字体,也可能被一张过小的纹理放大成柔和的马赛克。 第二个是把生成字号或纹理分辨率调大。 这能为放大保留更多细节,却会占据更多图集空间并增加生成、上传成本,而且通常只能照顾一段预先猜好的尺寸范围。 第三个是继续调整抗锯齿和纹理过滤参数。 这些参数可以改善像素边缘的过渡,却不能恢复栅格化时已经丢失的轮廓信息。 这些方法都不是错的,只是没有完整回答“同一份字形怎样覆盖更宽的缩放范围”。这正是 SDF 字体要处理的问题。 SDF 保存的不是颜色,而是离边缘还有多远 SDF 是 Signed Distance Field 的缩写,中文通常叫有符号距离场。 普通字形纹理记录像素的明暗或透明度;SDF 则记录这个位置离字形轮廓还有多远,并用正负区分轮廓内外。 可以把它想成一张围绕海岸线绘制的地形图:海岸是零,向陆地和海洋两边移动时,数值沿不同方向变化。显示文字时,shader 根据距离阈值重新找出边缘,并安排平滑过渡。 纹理保存的不再只是固定字号下的明暗结果,而是关于“边缘在哪里”的可计算信息。文字改变尺寸时,shader 可以重新调整过渡宽度,让同一份字形缓存覆盖更宽的字号和缩放范围。 描边也因此更自然:选取另一段距离区间,就能画出外轮廓,不必另外准备描边图片。 不过,SDF 仍然是一张有限分辨率的距离纹理,不是把字体文件中的全部曲线原样送进了 GPU。它还会受到烘焙尺寸、距离范围、图集空间、纹理采样和 shader 平滑参数影响。 所以更准确的说法是:SDF 能让字形纹理在更宽的尺寸范围内保持接近矢量缩放的质量,但不是无限放大都永远无损。 这项能力,Dora 曾经放下过一次 2018 年,项目说明里曾经明确记录:因为 SDF 渲染中文字符可能很慢,相关字体渲染函数被移除。 中文常用字符多,单个汉字的轮廓也往往比拉丁字母复杂。SDF 还要为字形周围的像素计算到轮廓的距离。当这些计算在新字形进入缓存时完成,速度就可能变成玩家或开发者能够感受到的等待。 对个人维护的开源引擎来说,“理论上可以实现”和“值得长期承担复杂度”不是同一件事。当时放下这项能力,只是因为实现路径还没有给出足够好的交换条件。 2024 年 12 月,社区伙伴“有个小小杜”把一份 SDF 核心算法代码在线下发给我,又和我一起讨论怎样接进 Dora。我负责完成 Label、FontManager、shader、API 和测试的初次整合,核心算法则来自他的创作。 从功能清单看,这件事似乎已经可以打勾。 但开源项目里有一种很容易被发布说明忽略的状态: 代码里已经有了,不等于我们愿意默认把它交给所有用户。 当时的生成速度和实现状态,还没有让我觉得 SDF 真正跨过了可用门槛。 他重新想了一遍:距离场还能怎样生成 接入只是开始。“有个小小杜”随后又把注意力放回生成器本身:距离场已经能算出来了,但能不能用更适合批量计算的方式重写? 最初加入 Dora 的 SdfBuilder 会为字形建立内外两组距离网格,通过前后扫描逐步传播最近边缘,再逐个计算每个像素的距离。PR #38 用新的 sdf_gen2d 实现替换了这套 Builder,并引入 KTM 的向量计算加速类型,把一部分距离结果按四个元素成组计算。 这个改动最打动我的,不只是速度变得可以接受,而是他没有停留在“把现有代码跑快一点”,而是重新组织了问题,让实现更适合继续维护和整合。 后来,他又发现批量计算末尾的一到三个像素没有被正确补完,于是用 PR #47 修正了循环起点。改动最终只有一行,却体现了同一种创作态度:既愿意提出新的算法路径,也愿意回来把不起眼的边角修完整。 可用的算法,还要继续长成引擎能力 社区 PR 让 SDF 生成跨过关键门槛,但还不是字体系统的终点。之后,Dora 继续修正参数,把 NanoVG 的 SDF 文字接入 FontManager,改进缩放平滑,并在 2026 年 7 月让 Label 默认启用 SDF。 当前 Dora 会把 SDF 字形统一按 64 像素的基础尺寸烘焙进图集。创建字号为 24 的 Label 时,不必另生成一套 24 像素字形;Label 使用 24 / 64 的比例计算显示尺寸,其他字号也能复用这套缓存。 非 SDF 字体则按请求字号烘焙;同一字体的 16、24 和 48 像素版本,可能需要分别进入缓存。 只复用缓存还不够。文字变小,边缘过渡太窄容易发硬或闪烁;文字放大,过渡太宽又会发虚。Label 会根据目标字号与 64 像素基础尺寸的比例计算平滑区间,渲染时再结合节点的世界缩放调整。 NanoVG 现在也通过同一个 FontCache 和 FontManager 获取字形、字距与图集区域,并在绘制和边界测量中计入 SDF padding。Label 和矢量绘图界面不必再维护两套互不相识的字体缓存。 从社区算法到缓存、测量、shader 和默认 API,这些工作共同完成后,“支持 SDF”才变成可以日常使用的引擎能力。 我们准备怎样证明它确实更好 文字渲染最终要让眼睛说话。但复活多年前的引擎、工具链和运行环境成本很高,也容易引入其他变量,因此后续素材不会伪装成“旧版与新版复现”。 更可控的方法,是在当前 Dora 中编写同一份真实 UI 演示,只改变 Label 是否启用 SDF: 使用相同的字体、文字、颜色、背景和输出分辨率。 同时创建非 SDF 与 SDF 两组文字。 从创建字号开始,逐步缩小和放大节点。 放入中文、英文和数字混排,观察不同复杂度的字形。 截取原始尺寸、明显放大和明显缩小三个阶段。 这组素材只回答:在当前引擎的相同条件下,普通字形纹理与 SDF 路径面对缩放时分别发生什么。它不代表历史版本的全部表现,也不证明 SDF 在任何倍率下都没有代价,但足以形成可观察、可复现的对照。 开源贡献,不只是发布说明末尾的一行名字 回头看,Dora 的 SDF 字体不是由某个人在某一天完整做成的。项目曾因中文字符的渲染成本放下这条路线;后来“有个小小杜”带来了新的算法思路,我把它接进引擎并继续完成字体系统整合。 每一步解决的都不是同一个问题,却共同决定了最后那一行 sdf = true 能不能放心成为默认值。 这也是我很喜欢的开源协作:贡献不一定要带来全新的大功能。它也可以走进一个“已经能跑”的功能,找到阻挡它被日常使用的门槛,再把门槛推过去。 有时候,一项能力从“已经加入”走到“真正可用”,中间差的正是另一位创作者看待问题的新方法。