惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Announcements
Recent Announcements
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog
T
The Blog of Author Tim Ferriss
J
Java Code Geeks
腾讯CDC
D
Docker
G
Google Developers Blog
D
DataBreaches.Net
雷峰网
雷峰网
Blog — PlanetScale
Blog — PlanetScale
S
SegmentFault 最新的问题
The Cloudflare Blog
有赞技术团队
有赞技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Stack Overflow Blog
Stack Overflow Blog
大猫的无限游戏
大猫的无限游戏
量子位
美团技术团队
aimingoo的专栏
aimingoo的专栏
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Engineering at Meta
Engineering at Meta
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

豆沙工作室

不想(二)十七岁 小点阵字 Get an Oscilloscope 杰理 WTS 格式音频转换和打包 [11ty] 增量式地刷新 CDN 缓存 站起来,为你的成果答辩吧 公理还是定理? [11ty] 处理资产文件的 CDN 缓存问题 [11ty] 无障碍设计:正确地标识图和表 停止 Mac 上的时间机器备份 从 Gitea 迁移到 Forgejo 评论里的私信消息 记得清理你的日志文件 招魂 Annihilation [11ty] 使用 Pagefind 实现静态站点的搜索 直面死亡 [11ty] 修复 RSS 里的数学公式 看到了一个神奇的 UA PreFound:10B - 四次击键 一点小小的 AI 震撼 PreFound:10A - 文本之外,还有(绘)文字 PreFound:09 - 色彩运算子 计算机不予申辩 Re-Game 2.0 Addendum C - Press START PreFound:08 - 动效与仿射 PreFound:07 - 图片拼贴报 PreFound:06 - 漫游雪花海 And Trek I Will PreFound:04 - 举起渲染器
PreFound:05 - 写出到屏幕
https://archive.dsstudio.tech/person/ · 2026-05-25 · via 豆沙工作室

在视频里加入文字是一件很奇怪的事情么?

刚刚上期我们启动了 SDL 的渲染流程,并成功地绘制了一些纯色色块输出到视频中。现在我们需要继续添加绘制文字的能力了。

好在(至少现在)我们不需要手动地解析 TrueType/OpenType 字形。FreeTypeHarfBuzz 库已经帮我们完成了解析字形、光栅化以及必要的文字排版的操作。SDL_ttf 库则进一步将整个过程包装成可供 SDL 库使用的形式。目前我们只需要先简单地引入 SDL_ttf 库就可以绘制文字到缓冲区了。

导入

SDL_ttf 库需要单独作为依赖导入:

/* in vcpkg.json */
{
	/* ... */
	"dependencies": [
		/* ... */
		"sdl3-ttf"
	]
	/* ... */
}

也需要单独进行链接:

# in CMakeLists.txt
# ...
find_package(SDL3_ttf CONFIG REQUIRED)
# ...
target_link_libraries(Foundation SDL3_ttf::SDL3_ttf)

以及单独地进行初始化:

// after SDL_Init(.)
if (!TTF_Init()) {
	spdlog::error("failed to initialize sdl_ttf: {}", SDL_GetError());
	return -30;
}

指定字体

要加载一个字体进入程序,只需要调用 TTF_OpenFont(filename, ptsize) 即可。

需要注意到,SDL_ttf 库并不会查询系统内已经安装好的字体。我们必须手动定位字体文件,并指示 SDL_ttf 打开一个具体的字体文件。所以如果像是用 CSS 那样简单地指定 "JetBrains Mono", "Fira Code", "Consolas", "Courier New", monospace 是一定会失败的。

但是「查找系统中安装的字体」这一茬又是心烦:在不同的平台上,这个操作是完全不一样的,御三家各有各的玩法。在 Windows 下,需要通过 GDI 的 EnumFontFamiliesEx 过程来查找字体(或者手动去 C:\Windows\Fonts 文件夹下枚举文件);而在 Linux 下,则需要通过 FontConfig 库来枚举各个安装好的字体——这些字体可能并不总是在 /usr/share/fonts 下,也可能是用户自行配置的路径下;在 macOS 下,则还需要打一个 FFI 到 Objective-C 那边,通过 Cocoa 提供的 NSFontManager 来枚举字体。

那么,解决这个问题的最好方法就是暂时不解决这个问题。我直接提供一个具体的字体文件就可以了!西文的 DejaVu, 中文的文泉驿,以及包含尽可能多字符集的 GNU Unifont 都是可以自由使用的字体。

等会,字体的 ptsize 是什么?怎么换算成像素?

ptsize 是字体的磅数,或者说字号。

SDL_ttf 不确定用的是那套标准,但直觉上应该还是按照桌面排版规则来走的。在桌面排版系统中,1pt 表示 172\frac{1}{72} 英寸,大约 0.35 毫米。如果我们假定像素密度是 72 px/in 的话,那么 1pt 和 1px 就是刚好对应的。

不过,当我们说设置字体大小为 16pt 的时候,我们究竟是设置了什么东西成为 16pt?

em, ex 以及 x-height 等描述具体字的尺寸不同。字体的磅数描述的是「铅字本身」的高度。也就是,这套字体在铸成之后,每个小的铅块的高度。在数字排版中,这个概念被继承了下来,用于表述这个字体的「最大」外框的高度。这里的「最大」带着引号,是因为数字字体的样条完全可以突破这个外框继续绘制(虽然最终绘制的效果可能会随着具体的渲染后端而变化)。

这也意味着一个更为棘手的问题:不同的字体即使调整成同样的磅数,它们的实际高度也可以是不一样的。

different-sizes.png

图 1 Hello, world 在 Calibri, Arial 和宋体下的渲染

字体大小设置为 16pt 后,光标的高度确实是实打实的 4px+16pt/72pt/in∗96px/in≈25px 4px + 16pt / 72pt/in * 96px/in \approx 25px (笑)。

算了,先把文字画出来就算成功。

auto* font = TTF_OpenFont("DejaVuSans.ttf", 24);
if (!font) {
	spdlog::error("failed to load font: {}", SDL_GetError());
	return -31;
}

绘制字形

既然之前已经在用 GPU 绘制,那么字形绘制也可以按照 GPU 绘制的方法来:

auto* engine = TTF_CreateRendererTextEngine(renderer);
if (!engine) {
	spdlog::error("failed to create text renderer: {}", SDL_GetError());
	return -32;
}

创建要绘制的字符串对象:

auto* textObj = TTF_CreateText(engine, font, "Hello, world!", 0);
if (!textObj) {
	spdlog::error("failed to create text object: {}", SDL_GetError());
	return -33;
}

然后在渲染循环里提交就可以:

// after SDL_SetRenderTarget(.)
// 设置文字的颜色
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
// 它会自行调用渲染器执行绘制,我们绘制到画布 (10, 10) 的位置上
TTF_DrawRendererText(textObj, 10, 10);

使用完成的对象记得清理:

TTF_DestroyText(textObj);
TTF_DestroyRendererTextEngine(engine);
TTF_CloseFont(font);
TTF_Quit();

文字描边

但是,这样画出来的文字感觉总是缺一点什么。它看起来太「细」了,边缘也过于锐利。要解决这个问题,最简单的方法是做一下文字描边。

虽然 SDL_ttf 库提供了设置文字描边的接口,但是比较糟心的是:文字描边并不是一个字体的后处理值,而是会被烘焙到字体对象中的一个设定。当你调用 TTF_SetFontOutline(.) 函数设置字体对象的描边时,实际上就改变了字体光栅化的结果。这个函数相当于是直接沿着样条法线把字体「加粗」了。

所以,如果要提供描边支持,我们需要创建两个字体。一个是正常的内容,另一个是「加粗」的描边:

auto outlineThickness = 2; // 2px 描边
fontOutline = TTF_OpenFont("DejaVuSans.ttf", 24);
if (!fontOutline) {
	spdlog::error("failed to open font: {}", SDL_GetError());
	return -34;
}

TTF_SetFontOutline(fontOutline, outlineThickness);

这两个文字对象需要各自渲染到自己的材质中(材质对象申请略):

SDL_SetRenderTarget(renderer, textTexture);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
TTF_DrawRendererText(textObj, 0, 0);

SDL_SetRenderTarget(renderer, textOutlineTexture);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
TTF_DrawRendererText(textObj, 0, 0);

再将文字叠到阴影上:

// 因为带描边和不带描边绘制出来的大小不太一样
// 这里需要做一点偏移
const SDL_FRect cascadeRect = {
	outlineThickness, outlineThickness, textTexture->w, textTexture->h
};
SDL_SetRenderTarget(renderer, textOutlineTexture);
SDL_RenderTexture(renderer, textTexture, nullptr, &cascadeRect);

偏移的量是参考字这篇回答得出的。

最后再提交到最终渲染:

const SDL_FRect dstRect = {
	10, 10, textOutlineTexture->w, textOutlineTexture->h
};
SDL_SetRenderTarget(renderer, texture);
SDL_RenderTexture(renderer, textOutlineTexture, nullptr, &dstRect);

这样就可以实现带黑色描边的白色文字啦!

text-rendering-outlined.png

图 2 测试工程中显示的文字(注意和上述代码运行结果不同)

下一步

只渲染静态的内容未免有点无聊,该搞一些动态的内容了。以及,现在所有的东西都跑在一个大 main 里面,需要设计一下怎么把它们拆分出去。

(按:其实拆分这一茬在我实际开始写 PreFound 库的时候就已经做了。博客中主要是为了记录关键过程,所以采用了大 main 跑一切的方案。)