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

推荐订阅源

P
Privacy International News Feed
WordPress大学
WordPress大学
Security Latest
Security Latest
Cyberwarzone
Cyberwarzone
K
Kaspersky official blog
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
G
GRAHAM CLULEY
N
News | PayPal Newsroom
Apple Machine Learning Research
Apple Machine Learning Research
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog
美团技术团队
J
Java Code Geeks
I
Intezer
The Cloudflare Blog
SecWiki News
SecWiki News
S
Secure Thoughts
Microsoft Azure Blog
Microsoft Azure Blog
V2EX - 技术
V2EX - 技术
C
Cyber Attacks, Cyber Crime and Cyber Security
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Spread Privacy
Spread Privacy
D
DataBreaches.Net
S
Security Affairs
Help Net Security
Help Net Security
S
Securelist
F
Full Disclosure
C
Check Point Blog
F
Fortinet All Blogs
Know Your Adversary
Know Your Adversary
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
阮一峰的网络日志
阮一峰的网络日志
The Register - Security
The Register - Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
人人都是产品经理
人人都是产品经理
博客园_首页
G
Google Developers Blog
Google Online Security Blog
Google Online Security Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
H
Help Net Security
酷 壳 – CoolShell
酷 壳 – CoolShell
I
InfoQ
Application and Cybersecurity Blog
Application and Cybersecurity Blog
H
Hacker News: Front Page
L
LINUX DO - 热门话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LangChain Blog

豆沙工作室

停止 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:03 - 跨越比特面 PreFound:02 - 启动编码器 PreFound:01 - 新建文件夹 PreFound:00 - 梦开始的地方 向着无穷远跑去 如何打包一个 Python 项目 只能作为光盘手册存在的书籍 航天飞机和马屁股的关系 Get an iPod 从鸿蒙系统中拉取蓝牙 HCI 日志 第二类擦除 迁移 FreshRSS 的数据库
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 跑一切的方案。)