<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:base="https://blag.dsstudio.tech/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
    <title>豆沙工作室</title>
    <link>https://blag.dsstudio.tech/</link>
    <atom:link href="https://blag.dsstudio.tech/feed.xml" rel="self" type="application/rss+xml" />
    <description>一人多役的艺术</description>
    <language>zh</language>
    <follow_challenge>
        <feedId>107427218643910656</feedId>
        <userId>LhUoD4sIRBhBBh14M3JtnBuu2peQk7jy</userId>
    </follow_challenge>
        <item>
<title>一点小小的 AI 震撼</title>
<link>/archives/the-ai-shock.html</link>
<description><![CDATA[<p>也许我是在自己骗自己，也许未来已来。</p><p><div class=more></div><p>最近精神上又受到了一些冲击，虽然相比起真正的苦难而言，这点屁事根本算不上什么；但是痛苦本身毕竟是痛苦，这种东西如果要比烂的话，那么便会否定所有人所正在经历的折磨。这可能是一种更大的不幸。</p><p>苦难催生创作。这种创作是纯粹为了排解，进而带有相当强烈的个人情感——或许我不应该把它叫做创作，而应该把它叫做一种宣泄。我烦扰，于是便拿起笔信手涂抹，不在乎语法是否正确，也不在乎行文是否简洁，如同一匹脱缰的野马在思绪的旷野里惊逃，发疯一般地跑跳转折。</p><p>很久没有像高中那样拿起笔用力地书写了，但我的肌肉显然还没有忘记那种肆意挥洒的感觉，中指上枕笔处留下的老茧依然在默默铭记那段已经燃烧殆尽的岁月。在没有什么娱乐的日子里，我还能逃到自己的脑海里，逃到草稿纸的横线里，用芝麻大小的笔迹去咒骂、去歌唱、去欢笑、去流泪，去用练习英语写作的幌子顶回「你在干什么」的质问。在那三尺的课桌上，探索、深入、挖掘，造访一处处只有自己知道的地方。</p><p>但当我走出高考考场的一刹那，一个个还未解答的问题，一段段没有结局的故事，全部佚散在课本堆里，化作漫天繁星了。或许这是一件好事，高中毕竟是曲折而漫长的行军，重压之下的人容易产生一种宿命感，或是未来的自己所难以理解的怪念头，割舍或许是最好的选择。但现在的我看来，这是绝对的错误：那些没有解答的问题并没有消失，而是静静地在脑海里沉淀了下去。</p><p>当冲击再次来临时，那些原本已经被归档的问题会被解封，一并带着形成这些问题的上下文再次呈现。哦亲爱的老己，好久不见，这才没过几年你怎么看起来这么颓废啊？现在请你带着新的生活的重担，去再次修补带着学业的重担的你的旧魂灵吧！相信你一定能够做到哦！</p><p>我自然是没法回答这些问题，但现在的我可以把它们记录下来，这样当未来的我再次需要寻求答案的时候，我便可以说：这是我们目前的实验过程和探索记录，如果你已经找到了方法，请务必追加。</p><p>我当然还没有考虑用 AI 给自己做心理咨询——窃以为心理咨询的重点是人而不是咨询。</p><p>还记得上面说到的一段段没有结局的故事么？有些想法实在是难以直接以问题的形式写出，因为不加一堆定语的问题无法表现矛盾和冲突；而没头没尾的故事则成了具象化这种思考的绝佳手段。和许多自我意识过剩的小孩子一样，故事的主人公自然永远是「我」的化身。</p><p>于是我便着手拟了一个大纲，但写到第二幕开始的时候，我感觉遇到了困难：现在，作为「异己」的「我」，即将陷于铁蹄之下。「我」正处于水深火热之中，却没有明显的脱身方法。怎么办？怎么办！</p><p>如果是在高中，这个时候我便会接受与自己「和解」的失败：故事没法继续写说明我没有足够的勇气去降神让「异己」获得胜利；但也没有足够的动力下手让「异己」彻底死绝。故事，和我的思绪一样，烂尾在了永恒的矛盾之中。</p><p>从小学开始，我就十分畏惧需要把自己掏心掏肺的东西交给另一个人来批阅的事情。或许是因为小时候真掏心掏肺过一次，然后被父母检查作业的时候痛批了一顿，然后就开始下意识地害怕别人看我的作文：它不够好！它不够好！！我无论怎么写它就是不够好！！！我不知道怎么办！！！！这个「不会写作文」的「病根」<a href=/archives/2112.html>从此就落在了心底</a>。</p><p>但是这一次，我想给「我」完整的一生。</p><p>我把稿纸上的文字，隐去了一些触及真实问题的部分，录入了 ChatGPT. 我告诉它我正在写一个剧本梗概，现在遇到了卡点，让它分析一下之后的剧情如何走更合适。考虑到我也没充钱，而且整段文字还很长，所以我不报什么太大期望。至于为啥不问国内的模型，主要是原文是用英语写的，ChatGPT 的英语语料应该更大一些，或许会有比较好的结果。</p><p>不出一会，ChatGPT 生成了一段回复。它告诉我：现在你的剧情走向大体合理，但是却犯了一个常见错误：外部冲突剧烈（「我」正陷于危险），但是内部冲突却没有任何描写（为什么「我」会陷入到这种危险？是什么内因导致了「我」的沦陷？）；基于此，以下有三种可能的剧情走向来刻画内因……</p><p>同时，它又告诉我：「我」的遭遇发生得太早了，从「被发现」到「即将被干掉」发展得速度太快，不合理。可以考虑这样来递进把握节奏……</p><p>也许是我带着预设来看问题，但是我觉得 ChatGPT 还约摸猜到了我隐去的真实问题是什么，并在第一种剧情走向的可能中写出了直指这个真实问题的发展。</p><p>我越看越感觉到一种难以言表的恐惧。理性上，我知道这是因为这么写通常就只有这么几种发展模式，而我一开始选择的走向一定程度上就透露了我想要的结果，只是陷于不甘和踌躇，我不愿意真的往下推进，而 ChatGPT 只不过是冰冷地给出了最符合我想要的结论罢了；感性上，我觉得「卧槽，它懂我；卧槽，它看透我了；卧槽，那我是谁？」。</p><p>平复心情后，细看之下，ChatGPT 给出的建议自然也是有着这样那样的漏洞，但对于我来说，有一个大方向，有一个需要注意的问题，这就够用了。</p><p>这是不是也算是一种心理咨询？我不知道。但这次，AI 无疑是给了我一个小小的震撼。尽管我已经用 AI 写程序很长时间了，也已经见识到了机器智能水平的快速发展；但是这种触动，我还是第一次亲身体会。因为这是我第一次感觉，写作或许也可以像是编程那样，一点点地自我开始学习，从不断地实践和即时的反馈中，理解更宏伟的概念并掌握更深层的架构。</p><p>但现在，我需要先回去救下那个即将入土的「我」了。</p>]]></description>
<pubDate>Sat, 06 Jun 2026 05:09:32 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/the-ai-shock.html</guid>
</item>

    
        <item>
<title>PreFound:10A - 文本之外，还有（绘）文字</title>
<link>/archives/prefound-10a-the-horrible-emoji-support.html</link>
<description><![CDATA[<p>人类发明这种东西真的不是为了折磨自己么？</p><p><div class=more></div><p>在<a href=/archives/prefound-10-char-by-char.html>第 10 篇</a>中，我们实现了一个打字机效果。但是其中有一个部分我只能很匆匆地略过，就是 Emoji 那一部分。这是因为 Emoji 这玩意真的是一种绝对的痛苦。</p><h2>怎么把 Emoji 还原成可键入的内容？</h2><p>当时给出的答案是使用 GitHub 短码。我的实际实现也确实是使用 GitHub 提供的短码。GitHub 自己维护了一份它所支持的全部短码对应 Emoji 的列表文件：</p><pre><code class="hljs language-text">https://api.github.com/emojis
</code></pre><p>当然，下载下来它是这个样式的：</p><pre><code class="hljs language-json"><span class=hljs-punctuation>{</span>
  <span class=hljs-attr>"+1"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"-1"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"100"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"https://github.githubassets.com/images/icons/emoji/unicode/1f4af.png?v8"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"1234"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"https://github.githubassets.com/images/icons/emoji/unicode/1f522.png?v8"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-comment>/* ... */</span>
<span class=hljs-punctuation>}</span>
</code></pre><p>好吧，虽然但是，我们还是可以从 URL 里面提取出位点信息的，对么？毕竟一个 Emoji 就对应一个位点嘛。</p><h2>不，一个 Emoji 可以是好几个位点</h2><p>首先就是<a href=https://blog.jonnew.com/posts/poo-dot-length-equals-two><code class=prettyprint>"💩".length === 2</code></a>。之前已经受过文本编码毒打的读者可能会意识到：因为 Emoji 大多都是 <code class=prettyprint>U+10000</code> 以上的位点，而许多系统默认内部是采用 UTF-16 编码的，所以这些字符会被拆成两个 Surrogate Pair.</p><p>没关系，我们可以写自己的 UTF-8 解析工具直接上 UCS-4. 对于我们来说，每个字符都是 32 位的，所以就不会用到（希望不会用到）UTF-16 中会出现的扩展支持。这样就可以做到 <code class=prettyprint>{"💩"}.size() == 1</code>.</p><p>但是有些 Emoji 它确实就不是单一位点的，比如 ❤️ 这玩意，它实际上的定义是 <code class=prettyprint>U+2764 U+FE0F</code>. 更痛苦的是：<code class=prettyprint>U+2764</code> 可以是一个独立字符，这个时候它是 <code class=prettyprint>❤</code>. 取决于你在什么设备上来看这个玩意以及你的浏览器的默认设置是什么，你可能看到一个黑色的实心心形字符；或者一颗红心 Emoji! 之所以有这个问题，是因为有些位点是从 Dingbat 位点借调过来的，而后面追加的 VS15 表示「明确使用 Emoji 版本」。</p><p>当然，还有各种旗子。国旗这种玩意起手就是两个位点。Unicode 委员会当初为了不给自己惹太多麻烦，特意没有规定所有的旗子，而是决定划分一块位点 <code class=prettyprint>U+1F1E6 -- U+1F1FF</code> 编码了字母 A-Z 然后说：按 CLDR 里的两字编码可以表示任何国家或者地区，无论它过去现在未来是否存在。至于任意两字组合渲染出来应该是啥样，这事由字体决定。</p><p>那，一个 Emoji 可以是一个或者两个位点。没关系，最多浪费一点 RAM 嘛，视频渲染不缺这几个字节的。</p><h2>不不不，一个 Emoji 可以是好几好几个位点</h2><p>Emoji 是可以通过 ZWJ 字符进行扩展的，而且能扩展多少其实并不一定。比如 <code class=prettyprint>👪</code> 这个玩意，它的内部表示是 <code class=prettyprint>U+1F9D1 U+200D U+1F9D1 U+200D U+1F9D2</code>. 以及，ZWJ 后面还能跟带 Variant Selector 的，比如 <code class=prettyprint>🏃‍➡️</code> 就是 <code class=prettyprint>U+1F3C3 U+200D U+27A1 U+FE0F</code>.</p><p>但是又不是所有的 Emoji 都是通过 ZWJ 扩展的。比如当你打出 👍🏻 的时候，这个玩意是可能携带一个肤色信息的，但肤色信息和 Emoji 之间没有任何字符。然后这个东西还能和 ZWJ 混到一起，比如 <code class=prettyprint>🏃🏿‍➡️</code> 就是 <code class=prettyprint>U+1F3C3 U+1F3FF U+200D U+27A1 U+FE0F</code>.</p><h2>不不不不不，我上面举的例子都是「一个」 Emoji</h2><p>如果你在不支持指定 Emoji 方向的设备上读上面这段文字的话，你可能会纳闷为啥我在对着好几个字符的 Emoji 大呼小叫。这些不都是单独的 Emoji 么？比如 <code class=prettyprint>🏃➡️</code> 不应该是两个 Emoji 么？</p><p>答案是：Emoji 的 ZWJ 序列是可以出现退化情况的。退化的 ZWJ 序列看起来就像是多个 Emoji 依次打出来。这种退化受到字体和平台的影响，在 macOS 下看起来是一个字符的玩意，在 Windows 下看起来就是另一个样子。</p><p>以及不仅仅是 ZWJ 序列可以退化，所有多 Emoji 的序列都可以退化，比如肤色选择就可以退化成 <code class=prettyprint>😀​🏼</code>. 在一个完全不支持多 Emoji 序列的环境下，上面的 <code class=prettyprint>U+1F3C3 U+1F3FF U+200D U+27A1 U+FE0F</code> 会被渲染成三个独立的 Emoji: <code class=prettyprint>🏃​🏿➡️</code>. 而且这个行为是<a href=https://unicode.org/emoji/charts-11.0/emoji-zwj-sequences.html>符合要求</a>的。</p><p>在 SDL 中渲染 Emoji 更是重量级：很多字体是没法完整地渲染所有 Emoji 的，要正常使用 Emoji 的话，需要设置 <a href=https://fonts.google.com/noto/specimen/Noto+Color+Emoji>Noto Color Emoji</a> 为<strong>主字体</strong>以便 SDL_ttf 能够优选 Noto Color Emoji 中的字形，然后把正文字体设置为备用字体才行。</p><h2>怎么界定一个 Emoji 的边界？</h2><p>Unicode 联盟发表了《<a href=https://www.unicode.org/reports/tr51/>UTS #51：Unicode Emoji</a>》来指导我们如何处理这种怪问题。</p><p>好在 Unicode 定义了完整的 Emoji EBNF 表达式，而且它看起来是正则的，这至少可以让我们在有限时间内界定一坨位点中怎么划分出若干个 Emoji. 它能够识别完整的 ZWJ 序列以及各种夹杂着怪东西的序列，而且也并不关心平台是否会单独把每个小部件拆开渲染。</p><h2>但是 GayHub 不在意</h2><p>如果仔细观察 GitHub 提供的结果的话，会发现它的多位点 Emoji 里面省略了 ZWJ 和 VS15 两个字符；比如 <code class=prettyprint>:heart:</code> 对应的 URL 就是 <code class=prettyprint>2764.png</code> 而不是 <code class=prettyprint>2764-fe0f</code>。所以，当我们去查表的时候，我们还得把这俩字符给滤掉才能查到正确的短码。</p><h2>处理非法序列</h2><p>坠痛苦的，可能还得是这件事了。和其他语言文字不同，Emoji 是可以整出字面意义上的非法序列的。毕竟 ZWJ 序列还是太超模了。我能想到的比这玩意更超模的东西是 <a href=https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-18/#G15800>IDS 序列</a>，但赖好那玩意不需要上屏不是 (!).</p><p>非法序列本身自然是没有对应的 GitHub 短码可用的，而且就算是那些合法的序列也不见得有对应的 GitHub 短码。所以我们就得自己想办法。目前想的办法是不处理：无法被转换为合法的字形会直接作为单独的无需提交的 <code class=prettyprint>KeystrokeSequence</code> 打出。当然，更正经的处理方案是为每个存在的 Unicode 位点都设计一个短码，但这个事情实在是太痛苦了（而且按现在的实现还会很低效），目前暂时没有太好的动力去实现。</p><h2>结论：别在你的视频里用 Emoji</h2><p>说实话一开始我觉得中文的那坨子处理应该是挺难的，相对而言 Emoji 应该是很容易处理的。结果发现 Emoji 这玩意的水真的很深。</p><p>为了所有人的身心健康，还是远离这玩意吧。</p><h2>后日谈：这篇文章还炸掉了我的 11ty 渲染管线</h2><p>打开 RSS 流自我陶醉的时候，发现所有包裹在内联代码段里的 Emoji 全变成了 <code class=prettyprint>U+FFFD</code>. 麻中麻啊！</p><p>先怀疑是 minify 过程把 HTML 实体重写了，关掉 minify 不管用；然后是怀疑内联高亮是不是有什么问题，但是 RSS 流里面是不跑内联高亮脚本的，一样有问题。</p><p>最后发现死还是得自己作。之前为了处理内联代码中可能会出现 HTML 字符的问题，以及为了给内联代码套上高亮，写了一个简单的变换函数：</p><pre><code class="hljs language-js">md.<span class=hljs-property>renderer</span>.<span class=hljs-property>rules</span>.<span class=hljs-property>code_inline</span> = <span class=hljs-function>(<span class=hljs-params>tokens, idx</span>) =></span> {
  <span class=hljs-keyword>const</span> token = tokens[idx]
  <span class=hljs-keyword>const</span> text = token.<span class=hljs-property>content</span>.<span class="hljs-title function_">replace</span>(
    <span class=hljs-regexp>/[^0-9A-Za-z ]/g</span>,
    <span class=hljs-function>(<span class=hljs-params>c</span>) =></span> <span class=hljs-string>'&#'</span> + c.<span class="hljs-title function_">charCodeAt</span>(<span class=hljs-number>0</span>) + <span class=hljs-string>';'</span>
  )

  <span class=hljs-keyword>return</span> <span class=hljs-string>`&lt;code class="prettyprint"><span class=hljs-subst>${text}</span>&lt;/code>`</span>
}
</code></pre><p>但是正如同我们刚开头所说，经历过文本编码毒打的人都会意识到 NodeJS 里面，Emoji 可不能 <code class=prettyprint>charCodeAt(0)</code> ——这会拿到一个 Surrogate Pair, 相当于是把字符拦腰斩断了。这就导致了编码出的实体是不合法的。不合法的 HTML 实体在 minify 的过程中会被替换成 <code class=prettyprint>U+FFFD</code>, 最终渲染出来就是带问号的菱形框。</p><p>修的话，还是引入一个外部包来处理 HTML 实体编码的问题吧。导入 <code class=prettyprint>html-entities</code> 包，简单改一下：</p><pre><code class="hljs language-js"><span class=hljs-keyword>import</span> { encode } <span class=hljs-keyword>from</span> <span class=hljs-string>'html-entities'</span>

md.<span class=hljs-property>renderer</span>.<span class=hljs-property>rules</span>.<span class=hljs-property>code_inline</span> = <span class=hljs-function>(<span class=hljs-params>tokens, idx</span>) =></span> {
  <span class=hljs-keyword>const</span> token = tokens[idx]
  <span class=hljs-keyword>const</span> text = <span class="hljs-title function_">encode</span>(token.<span class=hljs-property>content</span>)
  <span class=hljs-keyword>return</span> <span class=hljs-string>`&lt;code class="prettyprint"><span class=hljs-subst>${text}</span>&lt;/code>`</span>
}
</code></pre><p>只能说，有条件的尽量远离 Emoji, 这个东西是真的难对付。</p>]]></description>
<pubDate>Mon, 01 Jun 2026 06:39:55 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-10a-the-horrible-emoji-support.html</guid>
</item>

    
        <item>
<title>PreFound:09 - 色彩运算子</title>
<link>/archives/prefound-09-do-the-math.html</link>
<description><![CDATA[<p>相乘就是相乘，翻译成正片叠底到底是为啥啊！</p><p><div class=more></div><h2>渲染到 PNG</h2><p>目前我们一直在输出完整的视频文件。不过在调试过程中，我们通常只会关心某些具体帧的渲染情况，有时甚至可能只是第一帧的渲染结果。</p><p>可以简单地修改一下渲染循环——不需要启动 FFmpeg 编码器，只启动 SDL 并渲染指定时间的帧，之后借助 SDL_image 保存到一个 PNG 文件即可。</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* bufferSurface = <span class=hljs-built_in>SDL_CreateSurfaceFrom</span>(settings.width,
                                            settings.height,
                                            SDL_PIXELFORMAT_RGB24,
                                            sdl.<span class=hljs-built_in>buffer</span>(),
                                            <span class=hljs-number>3</span> * settings.width);
<span class=hljs-keyword>if</span> (bufferSurface == <span class=hljs-literal>nullptr</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to create surface: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
    <span class=hljs-keyword>return</span> <span class=hljs-number>-2</span>;
}

<span class=hljs-keyword>if</span> (!<span class=hljs-built_in>IMG_Save</span>(bufferSurface, outPath.<span class=hljs-built_in>string</span>().<span class=hljs-built_in>c_str</span>())) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to save surface: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
    <span class=hljs-built_in>SDL_DestroySurface</span>(bufferSurface);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-3</span>;
}

<span class=hljs-built_in>SDL_DestroySurface</span>(bufferSurface);
</code></pre><p>得益于我们之前无状态的设计，我们并不需要按顺序渲染每个帧再丢弃不需要的帧，只需要渲染我们关心的帧即可。</p><h2>颜色算术 (Color math)</h2><p>颜色算术控制两个图层混叠的方法。在 Photoshop 等图片处理程序中，它是图层的「混合模式」设置背后的实现。</p><p>当我们要混叠两个图层时，我们可以将其建模为：对于结果图层的每一个像素点，其颜色值是两个图层对应位置的像素通过某个函数计算出的结果。而设置图层的混叠模式就是指定这个函数具体是什么。</p><p>在 SDL 中，颜色算数是通过设置材质的混合模式实现的。比如，要设置图层按照「正常」的透明模式混叠渲染，只需要设置它的混叠模式为 <code class=prettyprint>SDL_BLENDMODE_BLEND</code> 即可：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* texture = <span class=hljs-built_in>render_frame</span>();
<span class=hljs-built_in>SDL_SetTextureBlendMode</span>(texture, SDL_BLENDMODE_BLEND);
</code></pre><p>在 SDL 中，传入 <code class=prettyprint>SDL_RenderTexture</code> 函数的材质为上层材质，即作为「源」；传入 <code class=prettyprint>SDL_SetRendererTarget</code> 的材质为下层材质，即作为「目标」。SDL 的混叠不会生成一个单独的结果材质，而是直接操作下层材质。</p><p>需要注意到，SDL 的颜色运算是在浮点 RGB 色彩空间里进行的，取 255 为 <code class=prettyprint>1.f</code>.</p><p>SDL 原生支持的色彩运算操作有五种：替换、混合、相加、相乘和调制。它们对应以下的色彩运算操作：</p><table><thead><tr><th>混叠模式</th><th>对应参数</th><th>颜色运算</th><th>透明度运算</th></tr></thead><tbody><tr><td>替换</td><td><code class=prettyprint>SDL_BLENDMODE_NONE</code></td><td>目标 = 源</td><td>目标 = 源</td></tr><tr><td>混合</td><td><code class=prettyprint>SDL_BLENDMODE_BLEND</code></td><td>目标 = (源 * 源透明度) + (目标 * (1 - 源透明度))</td><td>目标 = 源透明度 + (目标透明度 * (1 - 源透明度))</td></tr><tr><td>相加</td><td><code class=prettyprint>SDL_BLENDMODE_ADD</code></td><td>目标 = 目标 + (源 * 源透明度)</td><td>目标 = 目标</td></tr><tr><td>相乘</td><td><code class=prettyprint>SDL_BLENDMODE_MUL</code></td><td>目标 = (源 * 目标) + (目标 * (1 - 源透明度))</td><td>目标 = 目标</td></tr><tr><td>调制</td><td><code class=prettyprint>SDL_BLENDMODE_MOD</code></td><td>目标 = 源 * 目标</td><td>目标 = 目标</td></tr></tbody></table><h2>遮罩效果</h2><p>我们就借助「相乘」混合模式来实现一个遮罩效果吧——通过叠加一个特殊的图层上去，使得底部图层只显示一小部分。</p><p>简单地通过一个以透明为底、纯白为顶的图层作为<strong>底层</strong>，叠加我们的需要的图片上去即可。注意到这里选择遮罩作为底层，是因为我们需要让图片不需要可见的部分透明度为 0. 结合上面的表格可以知道，相乘会采用底图的透明度。</p><p>我们同样还可以给遮罩本身加点动画，比如从左到右扫描之类的。</p><video controls width=640><source src=https://static.dsstudio.tech/video/pf/masking.mp4 type=video/mp4></video><h2>自定义混合模式</h2><p>但是如果我们把这个效果应用到文字上，就会发现文字会产生原先没有的不透明底。如果选择「相乘」，则会产生一个白底；而选择「调制」则会产生一个黑色底。</p><video controls width=640><source src=https://static.dsstudio.tech/video/pf/masking-bad-transparency-mul.mp4 type=video/mp4></video><video controls width=640><source src=https://static.dsstudio.tech/video/pf/masking-bad-transparency-mod.mp4 type=video/mp4></video><p>明明文字的底本身是透明的，为什么叠加之后会有这个问题呢？</p><p>答案是：因为白色色块本身的透明度是 1.0, 相乘之后就会导致原本透明的部分不再透明；相乘出来是白色是因为 <code class=prettyprint>(目标 * (1 - 源透明度))</code> 得到了 1.0 白色；调制出来是黑色则是因为 <code class=prettyprint>源 * 目标</code> 得到的是 0.0 黑色。</p><p>所以，如果要正确地处理这个透明部分，我们必须从源上取这个透明度。好在 SDL 支持我们自定义混成方式，即通过 <code class=prettyprint>SDL_ComposeCustomBlendMode</code>.</p><p>这个函数支持我们自定义整个混色流程。设 <code class=prettyprint>srcRGB</code> 为上层颜色，<code class=prettyprint>dstRGB</code> 为底层颜色，<code class=prettyprint>srcA</code> 为上层透明度，<code class=prettyprint>dstA</code> 为底层透明度的话，这个函数相当于是允许我们按照如下公式自由调整混色过程：</p><pre><code class="hljs language-c">dstRGB = colorOperation(srcRGB * srcColorFactor, dstRGB * dstColorFactor);
dstA = alphaOperation(srcA * srcAlphaFactor, dstA * dstAlphaFactor); 
</code></pre><p>其中，<code class=prettyprint>srcColorFactor</code>, <code class=prettyprint>dstColorFactor</code>, <code class=prettyprint>srcAlphaFactor</code>, <code class=prettyprint>dstAlphaFactor</code> 可以设置为如下的值：</p><table><thead><tr><th>程序名</th><th>作用</th></tr></thead><tbody><tr><td><code class=prettyprint>SDL_BLENDFACTOR_ZERO</code></td><td><code class=prettyprint>0</code></td></tr><tr><td><code class=prettyprint>SDL_BLENDFACTOR_ONE</code></td><td><code class=prettyprint>1</code></td></tr><tr><td><code class=prettyprint>SDL_BLENDFACTOR_SRC_COLOR</code></td><td>上层颜色</td></tr><tr><td><code class=prettyprint>SDL_BLENDFACTOR_ONE_MINUS_SRC_COLOR</code></td><td>上层反色</td></tr><tr><td><code class=prettyprint>SDL_BLENDFACTOR_SRC_ALPHA</code></td><td>上层透明度</td></tr><tr><td><code class=prettyprint>SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA</code></td><td>上层透明度取反</td></tr><tr><td><code class=prettyprint>SDL_BLENDFACTOR_DST_COLOR</code></td><td>下层颜色</td></tr><tr><td><code class=prettyprint>SDL_BLENDFACTOR_ONE_MINUS_DST_COLOR</code></td><td>下层反色</td></tr><tr><td><code class=prettyprint>SDL_SDL_BLENDFACTOR_DST_ALPHA</code></td><td>下层透明度</td></tr><tr><td><code class=prettyprint>SDL_BLENDFACTOR_ONE_MINUS_DST_ALPHA</code></td><td>下层透明度取反</td></tr></tbody></table><p>以及 <code class=prettyprint>colorOperation</code> 和 <code class=prettyprint>alphaOperation</code> 支持设置为以下值：</p><table><thead><tr><th>程序名</th><th>作用</th></tr></thead><tbody><tr><td><code class=prettyprint>SDL_BLENDOPERATION_ADD</code></td><td>结果相加</td></tr><tr><td><code class=prettyprint>SDL_BLENDOPERATION_SUBTRACT</code></td><td>结果相减</td></tr><tr><td><code class=prettyprint>SDL_BLENDOPERATION_REV_SUBTRACT</code></td><td>结果相减，但是是右边减左边</td></tr><tr><td><code class=prettyprint>SDL_BLENDOPERATION_MINIMUM</code></td><td>二者取最小值</td></tr><tr><td><code class=prettyprint>SDL_BLENDOPERATION_MAXIMUM</code></td><td>二者取最大值</td></tr></tbody></table><p>并不是所有渲染后端都支持自定义自定义混合模式，比如在软件渲染后端中就完全没法自定义任何混合模式。不过从 SDL 的代码注释里看，Direct3D, OpenGL/OpenGL ES 以及 Vulkan 后端都是完全支持这些模式的，考虑到大多数设备都至少支持其中一个渲染后端，这个方法应该是可以放心用的。</p><p>要模仿「调制」混合模式，我们只需要：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span> blend = <span class=hljs-built_in>SDL_ComposeCustomBlendMode</span>(SDL_BLENDFACTOR_DST_COLOR,
                                        SDL_BLENDFACTOR_ZERO,
                                        SDL_BLENDOPERATION_ADD,
                                        SDL_BLENDFACTOR_DST_ALPHA,
                                        SDL_BLENDFACTOR_ZERO,
                                        SDL_BLENDOPERATION_ADD);
</code></pre><p>这些设置等价于：</p><pre><code class="hljs language-c">目标 = 源 * 目标
目标透明度 = 源透明度 * 目标透明度
</code></pre><p>源透明度乘以目标透明度，只要其中一个是 0 就会得到透明效果了：</p><video controls width=640><source src=https://static.dsstudio.tech/video/pf/masking-proper.mp4 type=video/mp4></video><h2>那，正片叠底？</h2><p>我没有找到可靠来源，只能说这玩意就是这么翻译的。也许这也是和句柄一样是一个从另外的语境中借调过来的词。</p><p>但不管怎么说，这比「颜色增值」要好的多——两个不大于 1 的数相乘，结果一定是不大于这两个数的，「增」到哪去了？而且再怎么说 multiply 也应该翻译成「增殖」吧。</p><h2>下一步</h2><p>下一步我们会探索一个比较困难的内容：打字效果。</p><p>这个东西看起来简单，实际做起来则要困难很多。它需要照顾很多奇妙的小细节，而且我们会给自己上很大的本可以不上的强度。希望我们做的这堆东西能够有一个有趣的结果吧！</p>]]></description>
<pubDate>Sun, 31 May 2026 18:11:13 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-09-do-the-math.html</guid>
</item>

    
        <item>
<title>计算机不予申辩</title>
<link>/archives/computers-dont-argue.html</link>
<description><![CDATA[<p>《<a href="https://www.atariarchives.org/bcc2/showpage.php?page=133">计算机不予申辩 (Computers Don't Argue)</a>》是 Gordon R. Dickson 于 1965 年发表的一篇短篇科幻小说。不同于其他科幻小说的天马行空和平铺直叙，这篇短文全文由人物之间的信件组成。不过，即使仅凭借着几人的书信来往，我们也得以一窥 Dickson 笔下虽然高度信息化但却又充斥着卡夫卡式的阴郁的世界。而这种阴郁，或多或少地在当下也有所体现。</p><p>同样的，翻译因个人恶趣味与原文会有出入。如需参考，请以原文为准。</p><p><div class=more></div><p>来自：黄金屋书店<br> <strong>请勿折叠、穿刺或污损此卡。</strong></p><p>佟怀特 先生 应付：$4.98.</p><p>亲爱的顾客：</p><p>包裹内含您订购的罗伯特·路易斯·史蒂文森所著《绑架》。</p><p>伍德兰街<br> 潘都，密歇根<br> 1956 年 11 月 16 日</p><hr><p>收：<br> 黄金屋书店<br> 曼迪街道 1823 号<br> 芝加哥，伊利诺伊<br></p><p>敬爱的负责人：</p><p>我需要您澄清关于最近您寄来的机读卡的问题，是关于拉迪亚德·吉卜林的《基姆》这本书的。我在打开包裹前就向您邮寄了支票，但我打开包裹之后发现这本书缺了半本内容，所以我把书寄回给您要求退换或者退款。但现在，您却给我寄了一本罗伯特·路易斯·史蒂文森的《绑架》。请您核对此问题！</p><p>包裹内附这本《绑架》向您退还。</p><p>佟怀特敬上</p><hr><p>来自：黄金屋书店<br> <strong>催缴通知</strong><br> <strong>请勿折叠、穿刺或污损此卡。</strong></p><p>佟怀特 先生 应付：$4.98. 物品：罗伯特·路易斯·史蒂文森所著《绑架》</p><p>（如果您已缴清欠款，请忽略此通知）</p><p>伍德兰街<br> 潘都，密歇根<br> 1956 年 11 月 16 日</p><hr><p>收：<br> 黄金屋书店<br> 曼迪街道 1823 号<br> 芝加哥，伊利诺伊<br></p><p>敬爱的负责人：</p><p>你是否读了 1965 年 11 月 16 日我寄出的邮件？你一直在给我发这种我没订购的书的催收卡。而且说实在话，明明是你们公司倒欠我钱。</p><p>佟怀特敬上</p><hr><p>寄：<br> 黄金屋书店<br> 曼迪街道 1823 号<br> 芝加哥，伊利诺伊<br> 1996 年 2 月 1 日</p><p>收：<br> 佟怀特<br> 伍德兰街 437 号<br> 潘都，密西根</p><p>敬爱的佟先生：</p><p>我们已经向您寄送了多次关于您订书的催缴通知。您所欠付的 $4.98 现在已超最晚付款期限。</p><p>虽然我们很愿意为您这样的老客户尽可能延长缴付时间，但很抱歉通知您，如果您未能在回信中及时清缴欠款，我们将会将此事项转交账款催收公司处理。</p><p>塞谬·格瑞姆<br> 账款负责人</p><hr><p>寄：<br> 伍德兰街 437 号<br> 潘都，密西根<br> 1966 年 2 月 5 日</p><p>格瑞姆先生：</p><p>你能不能别一直给我发机读卡和格式邮件？能不能派个活人来沟通？</p><p><u>我</u>不欠你任何钱，但<u>你</u>可是倒欠我不少。应该是我邀催收公司找你问题吧！</p><p>佟怀特</p><hr><p>寄：<br> 联邦账款处理部门<br> 行署路 88 号<br> 芝加哥，伊利诺伊<br> 1966 年 2 月 28 日</p><p>收：<br> 佟怀特先生<br> 伍德兰街 437 号<br> 潘都，密西根</p><p>敬爱的佟先生：</p><p>您关于黄金屋书店的 $4.98 圆欠款及滞纳金现转由我方处理。本金与滞纳金合计为 $6.83 圆，请尽快缴付，否则我们将采取进一步行动。</p><p>何杰克<br> 部门经理</p><hr><p>寄：<br> 联邦账款处理部门<br> 行署路 88 号<br> 芝加哥，伊利诺伊<br> 1966 年 4 月 8 日</p><p>收：<br> 佟怀特先生<br> 伍德兰街 437 号<br> 潘都，密西根</p><p>敬爱的佟先生：</p><p>您似乎认为忽略催收信件是一个好主意。现在，您关于黄金屋书店的欠款本金与滞纳金目前合计为 $7.51 圆。</p><p>如果您未能在 1966 年 4 月 11 日之前缴清欠款，则我们将采取法律措施。</p><p>何伊泽<br> 总经理</p><hr><p>寄：<br> 美普律师事务所<br> 行署路 89 号<br> 芝加哥，伊利诺伊<br> 1966 年 4 月 29 日</p><p>收：<br> 佟怀特先生<br> 伍德兰街 437 号<br> 潘都，密西根</p><p>敬爱的佟先生：</p><p>您关于黄金屋书店欠款清缴事宜现由我方处理。</p><p>现在您的欠款总额为 10.01 元。如果您在 1966 年 5 月 5 日之前缴清，则我方将不再进一步追究。否则，我方将正式提起诉讼。</p><p>诉讼会对您的声誉带来不利影响，请您多考量。</p><p>普和<br> 律师</p><hr><p>收：<br> 美普律师事务所<br> 行署路 89 号<br> 芝加哥，伊利诺伊<br> 1966 年 5 月 4 日</p><p>敬爱的普和先生：</p><p>能见到活人来信可真是太好了，我得向您解释一下这是怎么回事。</p><p>这整件事都很荒谬——我在之前和书店的通信里面已经和它们解释过一万遍了，但那些只会读机读卡的电脑根本就搞不清楚情况。长话短说，我之前订购了一本拉迪亚德·吉卜林的《基姆》，订价是 $4.98. 我收到包裹的时候发现这本书掉了一半书页。因为我之前已经把这本书的支票寄过去了，所以我就把书退回去要求他们要么换货要么退款。但是他们却给我寄了一本罗伯特·路易斯·史蒂文森的《绑架》——这书我从来没订过，但他们又在一直催收我这本书的钱。</p><p>现在我还在等书店他们那边给我结清欠款。希望您能帮我理清。</p><p>佟怀特敬上</p><p>另：《绑架》我收到的时候就直接退回去了，但他们却也一直没承认收到退件。</p><hr><p>寄：<br> 美普律师事务所<br> 行署路 89 号<br> 芝加哥，伊利诺伊<br> 1966 年 5 月 9 日</p><p>收：<br> 佟怀特先生<br> 伍德兰街 437 号<br> 潘都，密西根</p><p>敬爱的佟先生：</p><p>从我这边了解到的情况来看，书店并未收到您所退还的任何物品。</p><p>我认为如果情况确实如您所述，书店方不会敦促我们向您催收款项。</p><p>若三日之内（1966 年 5 月 12 日前）您仍拒绝结清款项，我们将不得不使用必要的法律手段。</p><p>普和<br> 律师</p><hr><p>小额索赔法庭民事判决书抄送<br> 芝加哥，伊利诺伊</p><p>收：<br> 佟怀特先生<br> 伍德兰街 437 号<br> 潘都，密西根</p><p>涉及你方的民事小额索赔案件，判决如下：</p><p>裁定被告佟怀特应于本判决生效之日起偿还本金 4.98 元及违约金（截至 1966 年 5 月 26 日违约金 合计 10.66 元，总计 15.66 元）给黄金屋书店。被告佟怀特应向本院补缴案件受理费……</p><p>本判决于 1966 年 5 月 26 日生效。</p><p>欠款清缴可向本院或借款方进行。若你向借款方偿还欠款，则需要向本院提交来自借款方的欠款缴清通知。</p><p>因最近推行的《法律文书互通法案》，若你非本州公民，则此诉讼会在你所在的常住地自动生成一份副本。你亦可向你所在的常住地法院清缴欠款。</p><hr><p>小额索赔法庭电子文书互通系统<br> 芝加哥，伊利诺伊<br> <strong>请勿折叠、穿刺或污损此卡。</strong></p><p>生效日期：1966 年 5 月 26 日<br> 法条编号：15.66</p><p>被告：佟怀特<br> 常住地：伍德兰街 437 号，潘都，密西根</p><p>卡片抄送：是<br> 收件：毗喀原法庭，潘都，密西根<br> 涉案金额：#941</p><hr><p>寄：<br> 伍德兰街 437 号<br> 潘都，密西根<br> 1966 年 5 月 31 日</p><p>收：<br> 塞谬·格瑞姆<br> 总经办，黄金屋书店<br> 曼迪街道 1823 号<br> 芝加哥，伊利诺伊<br></p><p>格瑞姆：</p><p>这事还有完没完！我明天就动身去芝加哥跟你好好掰扯掰扯到底是谁欠谁的！</p><p>佟怀特</p><hr><p>毗喀原法庭工作备忘录<br></p><p>1966 年 6 月 1 日</p><p>哈利：</p><p>这份芝加哥小额法庭的卡片上面，关于怀特的案子上，法条填的是 15 打头 4 位数，这个应该是归你们刑事法庭管的，已经超出民事范畴了。我已经转发到你的电脑上了。近来工作还顺利么？</p><p>老乔</p><hr><p>嫌疑人记录<br> 潘都，密西根<br> <strong>请勿折叠、穿刺或污损此卡。</strong></p><p>嫌疑人：佟怀特<br> 录入日期：1966 年 5 月 26 日<br> 常住地：伍德兰街 437 号，潘都，密西根<br> 法条：#1566（更正）#1567<br> 罪名：绑架<br> 日期：1965 年 11 月 16 日<br> 备注：在逃，需要立刻拘捕</p><hr><p>警情通信：（潘都，密西根）往（芝加哥，伊利诺伊）：关于佟氏男子（真名不详），根据法院录入信息，于 1965 年 11 月 16 日涉嫌绑架一名名为罗伯特·路易斯·史蒂文森的儿童。根据目前掌握的信息，嫌疑人已逃离潘都伍德兰街 473 住所，请在你的辖区内排查。可供排查的区域：曼迪街道 1823 号黄金屋书店。嫌疑人未携带武器，但仍需警惕。发现后立刻拘捕并通知……</p><hr><p>警情通信：（芝加哥，伊利诺伊）往（潘都，密西根）：关于你方请求拘捕涉嫌绑架的佟氏男子，住潘都伍德兰街 437 号：嫌疑人已于黄金屋书店落网，自报姓名为佟怀特；抓捕时正在试图从书店员工格瑞姆拿 $4.98 块钱，嫌疑人目前羁押待审。</p><hr><p>警情通信：（潘都，密西根）往（芝加哥，伊利诺伊）：关于涉嫌绑架的佟氏男子（化名佟怀特），正抄送法院于 1966 年 5 月 27 日发来的机读卡给你方。</p><hr><p>犯罪记录<br> 芝加哥，伊利诺伊<br> <strong>请勿折叠、穿刺或污损此卡。</strong></p><p>主题：（更正——记录缺损）<br> 法条：#1567<br> 判决编号：#456789<br> 文书记录：缺损<br> 指示：1966 年 6 月 9 日于一号法庭受审，审判长狄暗江</p><hr><p>审判长秘书处<br> 1966 年 6 月 2 日</p><p>托尼：</p><p>我收到了一份周四出庭的通知，但是我看了这份通知好像填得有问题，很多内容是缺失的。</p><p>你能不能帮我找一下相关信息？（#456789 刑院，被告姓佟）比如关于受害者的信息？受害者有没有受伤之类的？</p><p>狄谙江</p><hr><p>1966 年 6 月 3 日<br> 电子记录索引部</p><p>回复：转发：判决编号 #456789 -- 受害者是否受伤？</p><p>马妥尼<br> 电记部</p><hr><p>1966 年 6 月 3 日<br> 收件：国家统计局<br> 部门：电子记录索引部<br> 主题：罗伯特·路易斯·史蒂文森<br> 查询：人物基本信息</p><p>电子记录索引部<br> 犯罪信息档案室<br> 公安部<br> 芝加哥，伊利诺伊</p><hr><p>1966 年 6 月 5 日<br> 收件：电子记录索引部<br> 犯罪信息档案室<br> 公安部<br> 芝加哥，伊利诺伊<br> 主题：你方关于罗伯特·路易斯·史蒂文森的查询（档案号 #189623） 结果：该人已登记死亡，死亡年龄 44 岁。需要更多信息么？</p><p>小卡<br> 信息化办公室<br> 国家统计局</p><hr><p>1966 年 6 月 6 日<br> 收件：国家统计局<br> 部门：电子记录索引部<br> 主题：回复：档案编号 #189623<br> 不需要了，万分感谢。</p><p>电子记录索引部<br> 犯罪信息档案室<br> 公安部<br> 芝加哥，伊利诺伊</p><hr><p>1966 年 6 月 7 日<br> 收件：马妥尼<br> 电子记录索引部<br> 回复：转发：判决编号 #456789 -- 受害者已死亡。</p><p>电记部</p><hr><p>1966 年 6 月 7 日<br> 收件：狄审判长秘书处</p><p>狄谙江同志：</p><p>关于判决编号 #456789, 被绑架的受害者看起来已被谋杀。</p><p>从这起案子莫名缺失的加害者和受害者的背景信息来看，这很有可能是一起恶性涉黑案件。但这仅仅是我个人的猜想，我也拿不准。因为这个史蒂文森我总感觉在哪见过。可能和东岸派有联系，因为我总是会联想到海盗之类的——也许是码头帮之类的家伙。</p><p>以上仅仅是我个人的一些猜想，仅供您个人参考……</p><p>随时乐意效劳……</p><p>祝好，<br> 马妥尼<br> 电记部</p><hr><p>寄：<br> 磊诺麦<br> 诉讼律师<br> 津江路 49 号<br> 芝加哥，伊利诺伊<br> 1966 年 6 月 8 日</p><p>金哥：</p><p>抱歉：这周没法去钓鱼了。法院指派了一个案子给我，说是绑架。</p><p>一般来说这种案子我能撂就会撂的，而且老狄也会放我一马。但是这个案子真是我这辈子见过最离奇的了。</p><p>被告他是因为一个特别搞的一连串的录入错误才落得今天这个下场的。他压根就没犯什么错，而且从他的实际情况来看应该是要反诉芝加哥那个书店巨头的。这种白来的案子不接不行啊。</p><p>现在这个高度信息化的社会能出这种乱子按道理来说是不应该的，但是仔细想想又确实能发生——一个完全无辜的人会被推上被告席！</p><p>案子本身应该没啥需要多谈的了。明天开庭之前我会和老狄见一面，跟他把这事讲清楚就行。客户获释之后我再和他谈谈反诉的事情。</p><p>下周再约钓鱼？</p><p>麦兄敬上</p><hr><p>寄：<br> 磊诺麦<br> 诉讼律师<br> 津江路 49 号<br> 芝加哥，伊利诺伊<br> 1966 年 6 月 10 日</p><p>金哥：</p><p>长话短说——</p><p>下周也约不了了，万分抱歉。</p><p>我说的这个你肯定不信。我那比窦娥还冤的客户刚刚因谋杀罪且情节特别恶劣被判处了死刑，因为法院认为他绑架并谋杀了一个人。</p><p>我已经和老狄谈过了，他也给我说了情况，我差点没晕过去。</p><p>这不是说法庭不信我说这个人是无罪的。没过三分钟老狄就知道这人压根就不应该出现在看守所里。但是——很重要的但是——老狄也没办法。</p><p>老狄说根据电子档案，这个人的有罪判决早就下来了，只是缺了庭审记录——肯定没有庭审记录啊（但我现在来不及给你解释了）！他只能按照目前的记录来办事。也就是说要么选择坐牢，要么给吃花生米。</p><p>因为电子文档里包含了受害者死亡记录，所以只能顶格处罚。先行的法案又因为电子化记录把上诉时间缩短了，说是为了消灭故意延后刑期以及减少给罪犯带来的心理压力。我现在只有五天的时间填上诉，十天的时间上高院。</p><p>上诉肯定是来不及了，我现在去找州长开特赦——之后再处理这个烂摊子。老狄已经先给州长通信了，也表明了这个案子是多荒谬但他也没法处理。我们应该是能要到这个特赦。</p><p>然后我可得狠狠整顿一下这群……</p><p>之后我们再说钓鱼的事情。</p><p>祝好<br> 麦兄</p><hr><p>1966 年 6 月 27 日<br> 收：<br> 磊诺麦<br> 津江路 49 号<br> 芝加哥，伊利诺伊<br></p><p>磊律：</p><p>特赦呢？</p><p>再有五天我就要被送刑场了！</p><p>佟</p><hr><p>1966 年 6 月 29 日<br> 收：<br> 佟怀特（化名佟怀特）<br> 五号监区<br> 伊利诺伊看守所<br> 乔利埃特，伊利诺伊</p><p>怀特：</p><p>州长回办公室后，又立刻被叫去华盛顿参与跨州下水道建设研讨了。</p><p>我现在在他家门外打地铺蹲他了，他一回来我立刻就能搭上话。</p><p>我很清楚现在这个情况绝非儿戏。马艾伦管教会把这封信带给你，然后跟你谈话。你一定要认真听他说什么。还有这封来自你家人的信我也一并附上了。一定要仔细听马管教说了什么！</p><p>磊诺麦敬上</p><hr><p>1966 年 6 月 30 日<br> 收：<br> 磊诺麦<br> 津江路 49 号<br> 芝加哥，伊利诺伊<br></p><p>磊律：（这封信是马管教夹带出来的）</p><p>在我和马管教谈话的时候，他获知州长已经回到伊利诺伊了，明天周五他会到办公室坐班。一定要抓紧时间签出特赦令送到看守所，周六我就要被押送刑场了。</p><p>我婉拒了马管教协助我越狱的想法。毕竟他也没法保证我出逃的时候可以引开所有人的视线，而且一旦有人发现我的小命也会不保。</p><p>但现在事情应该能理顺的。这种疯狂总归要能停下来。</p><p>一切顺利<br> 佟</p><hr><p>伊利诺伊州长令</p><p>我，魏胡丹，作为伊利诺伊州州长，动用特赦权限，于 1966 年 7 月 1 日宣布佟怀特（化名佟怀特）因一系列错误指控而受拘留和错误审判的无辜人，赦免其所负罪名；且同时要求任何羁押胡怀特（化名胡怀特）的单位立即释放胡怀特并提供必要的协助……</p><hr><p>部门协同通信系统<br> <strong>请勿折叠、穿刺或污损此卡。</strong></p><p>无法将文档传递到部门！</p><p>收件：魏胡丹州长<br> 回复：特赦胡怀特 1996-07-01</p><p>错误：你的文档缺失<strong>路由编号</strong>。</p><p>指示：请重新提交你的文档，并附上此卡和 #876 号表格说明为何此文档需要<strong>最高优先</strong>递送等级。#876 号表格必须由你的上级签名。</p><p>文档提交日期：在协同通信办公室复工后即可提交，最早为 1966 年 7 月 5 日（周二）。</p><p>警告：#876 号表格<strong>必须由你的上级签名</strong>，否则你可能因滥用国家通信系统而被起诉。此项目<strong>不设例外</strong>。</p><hr><hr><p>你可能注意到了，有些人名前后不太能对得上——这个和小说原本的写法是一致的。</p>]]></description>
<pubDate>Sat, 30 May 2026 06:46:39 +0000</pubDate>
<dc:creator>Gordon R. Dickson</dc:creator>
<guid>/archives/computers-dont-argue.html</guid>
</item>

    
        <item>
<title>Re-Game 2.0 Addendum C - Press START</title>
<link>/archives/re-game-2-0-addendum-c.html</link>
<description><![CDATA[<p>有些时候，最困难的部分可能只是按下「开始」键。</p><p><div class=more></div><h2>电子阳痿</h2><p>电子阳痿并不是一个新鲜事，但我想可能还是有必要澄清一下我对于这个词的理解：物质的富足没有带动精神的富足。游戏买了但是不玩，面对着一叠尚未开封的游戏但却一点提不起玩的兴趣，就是「电子阳痿」。</p><p>其实老一代人并不会说「电子阳痿」这种话出来，他们只会说「老了玩不动了」——谁没有年轻过呢？谁没有过鲜衣怒马的时候呢？只是当生活的重担压下时，他们或心甘情愿或被逼无奈地放下了曾带领他们进入到另一个世界的钥匙，转而踏进人生的洪流之中了。</p><p>但是他们只是纯粹的没有时间或者没有精力。我能说现在的我没有时间去打机么？似乎并不能，毕竟时间是海绵里的水，多挤挤那总还是会有的；我能说现在的我没有精力去打机么？或许可以，但是精力这种事情很难去量化，而且现在的我只有上班这一件事要忙，还没有更耗费精力的其他社会责任，相比之下，我更应该是精力充沛的那一类人。</p><p>但或许是因为我们现在的生活过于碎片化了，以至于我们无法真正地「投入」到一款作品当中。即使是休息，二十四小时在线的微信和钉钉也会随时要求你临时回到工作岗位。这种「无限待机」的要求，对于上一代人而言只存在于警察医生消防员等特殊职业当中；而对于我们这一代则是一种常态。</p><p>玩游戏，尤其是好游戏，需要大块的连续时间。而碎片化的时间分配使得我们不敢点开游戏，生怕下一秒催命的三连音让血压再提高几个层级。自然而然的，对于精神快餐的追求就成了理所当然。</p><p>规训是强劲的武器，那能不能<a href=https://etw.fm/2025>把工作的思维带入生活</a>，进而达成「玩游戏」这个目标呢？</p><h2>游戏 KPI 和生活 OKR</h2><p>如果给玩游戏制定 KPI, 要求每周必须玩够多少个小时的游戏或者某个游戏要达成某个进度，我真的会去拿起手柄么？</p><p>凭我对我自己的了解，大概率是不会的，因为自己给自己设立的规矩想打破实在是太容易也太没有后果了。而且给一个爱好设置考核目标是让自己彻底放弃这种爱好的绝佳手段。之前看到有人提问「孩子总是贪玩游戏怎么办？」其中一个回答就是按抓学习那样抓打游戏，游戏必须打出成绩，不然就一直打不允许停，打不好就别吃饭，打到有结果为止。这样孩子要么有望进入青训队走正经的电竞路线，要么就自然而然地放弃游戏了。</p><p>当然，你我都很清楚，这样无异于饮鸩止渴。孩子那是想玩游戏么？孩子那是需要一个逃离现实的港湾。没了游戏，他不会自己去学习，而是会找其他逃避现实的方法。真逼急了，那就是能在书桌前干坐一整天。上午书在第几页，晚上书还是在第几页。</p><p>上班本就痛苦，待机本就烦人。我不想在除了工位以外的任何场景看到报表。如果真的给打游戏设置 KPI, 给生活设置 OKR, 我不知道我离高空肘击水泥地还会有多远。</p><h2>按下开始键</h2><p>我想谈谈之前说的「等待戈多」到底是在等待什么。我知道这是一个很矫情的事情，这是一个其他任何「心理正常」的人都不会产生的问题。但是对于这种矫情的忠实记录也是为了未来的自己可以少干一些这种让人脚趾扣地的事情。</p><p>我并不知道我在害怕什么。可能是害怕游戏不如自己脑海里想象得好玩？可能是害怕自己又要投入很长的时间进去然后最终又落得一个无疾而终的结果？可能是害怕自己本来可以把这种时间投入到其他「更有意义」的事情上去？可能是因为心里总是在想着工作上的事情，所以脑子里总是焦虑？可能是因为自己从来没有「玩得尽兴」的体验，因为小时候的自己总是会把作业拖到最后一刻导致总是玩也玩不好学也学不好？</p><p>按下开始键吧，哪怕只有三分钟也好。</p>]]></description>
<pubDate>Wed, 27 May 2026 16:27:18 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/re-game-2-0-addendum-c.html</guid>
</item>

    
        <item>
<title>PreFound:08 - 动效与仿射</title>
<link>/archives/prefound-08-affine-affinity.html</link>
<description><![CDATA[<p>如何让屏幕上元素的移动显得更「自然」？</p><p><div class=more></div><h2>图层系统</h2><p>目前我们仍然是直接操作 SDL 的主材质，虽然方便，但是在后续实现一些效果的时候会比较棘手。比如我们即将开始研究的仿射变换需要针对整个画面进行操作，如果是纯色色块这种没有自己临时图层的生成器就比较难以实现。</p><p>所以，每个生成器都应渲染出一个自己的材质，然后最终再由主循环逐一地将每个生成器生成的中间材质复制到主材质上。</p><p>同时，虽然生成器仍然是操作一个具体的 <code class=prettyprint>SDL_Texture</code>, 但是这个材质的大小可以随着具体的需要改变。自然的，每个材质要复制的位置也是可以不同的。所以，我们定义一个图层 <code class=prettyprint>Layer</code> 为包含原点坐标、图层尺寸和一个可绘制材质的对象。</p><p>为了方便和 SDL 做交互，定义图层的原点始终对应材质的左上角，并定义全局的坐标系为右 +x 下 +y 坐标系。</p><p><a href=https://imgchr.com/i/pmP4FMT><img alt=coordinates.png src=https://s41.ax1x.com/2026/05/27/pmP4FMT.png></a></p><p>图 1 坐标系的定义</p><p>按道理来说，图层应该是有 Z 轴定义的。不过现在为了方便，暂时按照图层创建的次序定义 Z 轴顺序：越晚创建的图层具有越高的 Z 轴值。</p><h2>图层变换算子</h2><p>现在有了图层的概念之后，我们还需要想办法驱动这些图层。</p><p>我们目前设计的架构并不直接支持我们做有状态的渲染。从架构设计上，我们也不应该考虑做有状态的渲染：理想情况下，所有帧的渲染都仅与时间相关，这样即使是非常复杂的场景，也可以通过多机并行渲染来实现短时间内出结果。</p><p>所以，图层变换算子的输入也只有一个：当前相对于片段开始的时间。图层变换算子本身也必须是纯的，不能携带任何非常数参数。</p><p>这个重要限制在之后也会影响许多从原理上应该是「有状态」的变换设计。简要地来说，一旦我们遇到了严格有状态的变换，比如依赖于一些没有解析解的微分方程组的变换，那么这些变换会在外部预先计算好具体的值，然后作为数据加载给变换算子。变换算子仅负责查表，以及进行必要的插值。</p><h3>位置变换</h3><p>一般指定图层位移时，自然的方法是指定一个初始位置 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mn>0</mn></msub></mrow><annotation encoding=application/x-tex>\vec{x}_0</annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:.864em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></eq>、一个结束位置 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mn>1</mn></msub></mrow><annotation encoding=application/x-tex>\vec{x}_1</annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:.864em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></eq>，以及期望这段动画开始 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mi>t</mi><mn>0</mn></msub></mrow><annotation encoding=application/x-tex>t_0</annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:.7651em></span><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></eq> 和持续的时间 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>d</mi></mrow><annotation encoding=application/x-tex>d</annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.6944em></span><span class="mord mathnormal">d</span></span></span></span></eq>。</p><p>不妨考虑函数 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi mathvariant=normal>T</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>↦</mo><mover accent=true><mi>x</mi><mo>⃗</mo></mover></mrow><annotation encoding=application/x-tex> \mathrm{T}(t) \mapsto \vec{x} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathrm">T</span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>↦</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=height:.714em></span><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span></span></span></span></eq> 的输出是时刻 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>t</mi></mrow><annotation encoding=application/x-tex>t</annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.6151em></span><span class="mord mathnormal">t</span></span></span></span></eq> 时，图层所在的位置。考虑最简单的线性插值法，显然的，有：</p><section><eqn><span class=katex-display><span class=katex><span class=katex-mathml><math display=block xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi mathvariant=normal>T</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>=</mo><mrow><mo fence=true>{</mo><mtable columnalign="left left" columnspacing=1em rowspacing=0.36em><mtr><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mi mathvariant=normal>u</mi><mi mathvariant=normal>n</mi><mi mathvariant=normal>d</mi><mi mathvariant=normal>e</mi><mi mathvariant=normal>f</mi><mi mathvariant=normal>i</mi><mi mathvariant=normal>n</mi><mi mathvariant=normal>e</mi><mi mathvariant=normal>d</mi></mrow></mstyle></mtd><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mi>t</mi><mo>&lt;</mo><msub><mi>t</mi><mn>0</mn></msub></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle=false scriptlevel=0><mrow><msub><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mn>0</mn></msub><mo>+</mo><mo stretchy=false>(</mo><mfrac><mrow><mi>t</mi><mo>−</mo><msub><mi>t</mi><mn>0</mn></msub></mrow><mi>d</mi></mfrac><mo stretchy=false>)</mo><mo stretchy=false>(</mo><msub><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mn>1</mn></msub><mo>−</mo><msub><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mn>0</mn></msub><mo stretchy=false>)</mo></mrow></mstyle></mtd><mtd><mstyle displaystyle=false scriptlevel=0><mrow><msub><mi>t</mi><mn>0</mn></msub><mo>≤</mo><mi>t</mi><mo>≤</mo><msub><mi>t</mi><mn>0</mn></msub><mo>+</mo><mi>d</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mi mathvariant=normal>u</mi><mi mathvariant=normal>n</mi><mi mathvariant=normal>d</mi><mi mathvariant=normal>e</mi><mi mathvariant=normal>f</mi><mi mathvariant=normal>i</mi><mi mathvariant=normal>n</mi><mi mathvariant=normal>e</mi><mi mathvariant=normal>d</mi></mrow></mstyle></mtd><mtd><mstyle displaystyle=false scriptlevel=0><mrow><msub><mi>t</mi><mn>0</mn></msub><mo>+</mo><mi>d</mi><mo>&lt;</mo><mi>t</mi></mrow></mstyle></mtd></mtr></mtable></mrow></mrow><annotation encoding=application/x-tex> \mathrm{T}(t) = \begin{cases}\mathrm{undefined} & t &lt; t_0 \\ \vec{x}_0 + (\frac{t - t_0}{d})(\vec{x}_1 - \vec{x}_0) & t_0 \le t \le t_0 + d \\ \mathrm{undefined} & t_0 + d &lt; t \end{cases} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathrm">T</span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-1.91em;height:4.32em></span><span class=minner><span class=mopen><span class="delimsizing mult"><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:2.35em><span style=top:-2.2em><span class=pstrut style=height:3.15em></span><span class="delimsizinginner delim-size4"><span>⎩</span></span></span><span style=top:-2.192em><span class=pstrut style=height:3.15em></span><span style=width:.8889em;height:.316em><svg viewbox="0 0 888.89 316" height=0.316em preserveaspectratio=xMinYMin style=width:.8889em width=0.8889em xmlns=http://www.w3.org/2000/svg><path d="M384 0 H504 V316 H384z M384 0 H504 V316 H384z"/></svg></span></span><span style=top:-3.15em><span class=pstrut style=height:3.15em></span><span class="delimsizinginner delim-size4"><span>⎨</span></span></span><span style=top:-4.292em><span class=pstrut style=height:3.15em></span><span style=width:.8889em;height:.316em><svg viewbox="0 0 888.89 316" height=0.316em preserveaspectratio=xMinYMin style=width:.8889em width=0.8889em xmlns=http://www.w3.org/2000/svg><path d="M384 0 H504 V316 H384z M384 0 H504 V316 H384z"/></svg></span></span><span style=top:-4.6em><span class=pstrut style=height:3.15em></span><span class="delimsizinginner delim-size4"><span>⎧</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:1.85em><span></span></span></span></span></span></span><span class=mord><span class=mtable><span class=col-align-l><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:2.41em><span style=top:-4.41em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord mathrm">undefined</span></span></span></span><span style=top:-2.97em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span><span class=mopen>(</span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.8407em><span style=top:-2.655em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">d</span></span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.4101em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3173em><span style=margin-left:0;margin-right:.0714em;top:-2.357em><span class=pstrut style=height:2.5em></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.143em><span></span></span></span></span></span></span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.345em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class=mclose>)</span><span class=mopen>(</span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mclose>)</span></span></span><span style=top:-1.53em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord mathrm">undefined</span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:1.91em><span></span></span></span></span></span><span class=arraycolsep style=width:1em></span><span class=col-align-l><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:2.41em><span style=top:-4.41em><span class=pstrut style=height:3.008em></span><span class=mord><span class="mord mathnormal">t</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>&lt;</span><span class=mspace style=margin-right:.2778em></span><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span><span style=top:-2.97em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2778em></span><span class=mrel>≤</span><span class=mspace style=margin-right:.2778em></span><span class="mord mathnormal">t</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>≤</span><span class=mspace style=margin-right:.2778em></span><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span><span class="mord mathnormal">d</span></span></span><span style=top:-1.53em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span><span class="mord mathnormal">d</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>&lt;</span><span class=mspace style=margin-right:.2778em></span><span class="mord mathnormal">t</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:1.91em><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></eqn></section><video controls width=640><source src=https://static.dsstudio.tech/video/pf/linear.mp4 type=video/mp4></video><h2>插值函数</h2><p>更一般的，设 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi mathvariant=normal>I</mi><mo stretchy=false>(</mo><mi>x</mi><mo stretchy=false>)</mo><mo>↦</mo><mrow><mo fence=true>[</mo><mn>0</mn><mo separator=true>,</mo><mn>1</mn><mo fence=true>]</mo></mrow></mrow><annotation encoding=application/x-tex> \mathrm{I}(x) \mapsto \left[0, 1\right] </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathrm">I</span><span class=mopen>(</span><span class="mord mathnormal">x</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>↦</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=minner><span class="mopen delimcenter" style=top:0>[</span><span class=mord>0</span><span class=mpunct>,</span><span class=mspace style=margin-right:.1667em></span><span class=mord>1</span><span class="mclose delimcenter" style=top:0>]</span></span></span></span></span></eq>, 且 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>x</mi><mo>∈</mo><mrow><mo fence=true>[</mo><mn>0</mn><mo separator=true>,</mo><mn>1</mn><mo fence=true>]</mo></mrow></mrow><annotation encoding=application/x-tex> x \in \left[0, 1\right] </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.0391em;height:.5782em></span><span class="mord mathnormal">x</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>∈</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=minner><span class="mopen delimcenter" style=top:0>[</span><span class=mord>0</span><span class=mpunct>,</span><span class=mspace style=margin-right:.1667em></span><span class=mord>1</span><span class="mclose delimcenter" style=top:0>]</span></span></span></span></span></eq> 为一个插值函数，那么图层位置变换函数可以定义为：</p><section><eqn><span class=katex-display><span class=katex><span class=katex-mathml><math display=block xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi mathvariant=normal>T</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>=</mo><mrow><mo fence=true>{</mo><mtable columnalign="left left" columnspacing=1em rowspacing=0.36em><mtr><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mi mathvariant=normal>u</mi><mi mathvariant=normal>n</mi><mi mathvariant=normal>d</mi><mi mathvariant=normal>e</mi><mi mathvariant=normal>f</mi><mi mathvariant=normal>i</mi><mi mathvariant=normal>n</mi><mi mathvariant=normal>e</mi><mi mathvariant=normal>d</mi></mrow></mstyle></mtd><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mi>t</mi><mo>&lt;</mo><msub><mi>t</mi><mn>0</mn></msub></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle=false scriptlevel=0><mrow><msub><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mn>0</mn></msub><mo>+</mo><mi>I</mi><mo stretchy=false>(</mo><mfrac><mrow><mi>t</mi><mo>−</mo><msub><mi>t</mi><mn>0</mn></msub></mrow><mi>d</mi></mfrac><mo stretchy=false>)</mo><mo stretchy=false>(</mo><msub><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mn>1</mn></msub><mo>−</mo><msub><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mn>0</mn></msub><mo stretchy=false>)</mo></mrow></mstyle></mtd><mtd><mstyle displaystyle=false scriptlevel=0><mrow><msub><mi>t</mi><mn>0</mn></msub><mo>≤</mo><mi>t</mi><mo>≤</mo><msub><mi>t</mi><mn>0</mn></msub><mo>+</mo><mi>d</mi></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mi mathvariant=normal>u</mi><mi mathvariant=normal>n</mi><mi mathvariant=normal>d</mi><mi mathvariant=normal>e</mi><mi mathvariant=normal>f</mi><mi mathvariant=normal>i</mi><mi mathvariant=normal>n</mi><mi mathvariant=normal>e</mi><mi mathvariant=normal>d</mi></mrow></mstyle></mtd><mtd><mstyle displaystyle=false scriptlevel=0><mrow><msub><mi>t</mi><mn>0</mn></msub><mo>+</mo><mi>d</mi><mo>&lt;</mo><mi>t</mi></mrow></mstyle></mtd></mtr></mtable></mrow></mrow><annotation encoding=application/x-tex> \mathrm{T}(t) = \begin{cases}\mathrm{undefined} & t &lt; t_0 \\ \vec{x}_0 + I(\frac{t - t_0}{d})(\vec{x}_1 - \vec{x}_0) & t_0 \le t \le t_0 + d \\ \mathrm{undefined} & t_0 + d &lt; t \end{cases} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathrm">T</span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-1.91em;height:4.32em></span><span class=minner><span class=mopen><span class="delimsizing mult"><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:2.35em><span style=top:-2.2em><span class=pstrut style=height:3.15em></span><span class="delimsizinginner delim-size4"><span>⎩</span></span></span><span style=top:-2.192em><span class=pstrut style=height:3.15em></span><span style=width:.8889em;height:.316em><svg viewbox="0 0 888.89 316" height=0.316em preserveaspectratio=xMinYMin style=width:.8889em width=0.8889em xmlns=http://www.w3.org/2000/svg><path d="M384 0 H504 V316 H384z M384 0 H504 V316 H384z"/></svg></span></span><span style=top:-3.15em><span class=pstrut style=height:3.15em></span><span class="delimsizinginner delim-size4"><span>⎨</span></span></span><span style=top:-4.292em><span class=pstrut style=height:3.15em></span><span style=width:.8889em;height:.316em><svg viewbox="0 0 888.89 316" height=0.316em preserveaspectratio=xMinYMin style=width:.8889em width=0.8889em xmlns=http://www.w3.org/2000/svg><path d="M384 0 H504 V316 H384z M384 0 H504 V316 H384z"/></svg></span></span><span style=top:-4.6em><span class=pstrut style=height:3.15em></span><span class="delimsizinginner delim-size4"><span>⎧</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:1.85em><span></span></span></span></span></span></span><span class=mord><span class=mtable><span class=col-align-l><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:2.41em><span style=top:-4.41em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord mathrm">undefined</span></span></span></span><span style=top:-2.97em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span><span class="mord mathnormal" style=margin-right:.07847em>I</span><span class=mopen>(</span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.8407em><span style=top:-2.655em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">d</span></span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.4101em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3173em><span style=margin-left:0;margin-right:.0714em;top:-2.357em><span class=pstrut style=height:2.5em></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.143em><span></span></span></span></span></span></span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.345em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class=mclose>)</span><span class=mopen>(</span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mclose>)</span></span></span><span style=top:-1.53em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord mathrm">undefined</span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:1.91em><span></span></span></span></span></span><span class=arraycolsep style=width:1em></span><span class=col-align-l><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:2.41em><span style=top:-4.41em><span class=pstrut style=height:3.008em></span><span class=mord><span class="mord mathnormal">t</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>&lt;</span><span class=mspace style=margin-right:.2778em></span><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span><span style=top:-2.97em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2778em></span><span class=mrel>≤</span><span class=mspace style=margin-right:.2778em></span><span class="mord mathnormal">t</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>≤</span><span class=mspace style=margin-right:.2778em></span><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span><span class="mord mathnormal">d</span></span></span><span style=top:-1.53em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:0;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span><span class="mord mathnormal">d</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>&lt;</span><span class=mspace style=margin-right:.2778em></span><span class="mord mathnormal">t</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:1.91em><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></eqn></section><p>换成人话说就是：我们可以只关心设计一个平滑的、输入输出都是从 0 到 1 的函数作为插值函数，然后把它代换进去就可以了。当然，如果要实现回弹效果的话，输出的值是可以大于 1 或者小于 0 的。</p><p>是时候写点看起来比较自然的动效了。</p><p>现实世界中，一个物体不会突然开始移动，也不会突然停止移动。它应该缓慢加速，再缓慢停止。我们不妨考虑恒外力下从静止开始的运动，此时位移的平方和时间成正比；在恒外力下减速同理。稍微整理一下，可以写出一个符合要求的分段函数：</p><section><eqn><span class=katex-display><span class=katex><span class=katex-mathml><math display=block xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>I</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>=</mo><mrow><mo fence=true>{</mo><mtable columnalign="left left" columnspacing=1em rowspacing=0.36em><mtr><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mn>2</mn><msup><mi>t</mi><mn>2</mn></msup></mrow></mstyle></mtd><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mn>0</mn><mo>≤</mo><mi>t</mi><mo>≤</mo><mn>0.5</mn></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mn>1</mn><mo>−</mo><mo stretchy=false>(</mo><msqrt><mn>2</mn></msqrt><mo stretchy=false>(</mo><mi>x</mi><mo>−</mo><mn>1</mn><mo stretchy=false>)</mo><msup><mo stretchy=false>)</mo><mn>2</mn></msup></mrow></mstyle></mtd><mtd><mstyle displaystyle=false scriptlevel=0><mrow><mn>0.5</mn><mo>&lt;</mo><mi>t</mi><mo>≤</mo><mn>1</mn></mrow></mstyle></mtd></mtr></mtable></mrow></mrow><annotation encoding=application/x-tex> I(t) = \begin{cases} 2t^2 & 0 \le t \le 0.5 \\ 1 - (\sqrt{2}(x - 1))^2 & 0.5 &lt; t \le 1 \end{cases} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathnormal" style=margin-right:.07847em>I</span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-1.25em;height:3em></span><span class=minner><span class="mopen delimcenter" style=top:0><span class="delimsizing size4">{</span></span><span class=mord><span class=mtable><span class=col-align-l><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:1.69em><span style=top:-3.69em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord>2</span><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.8141em><span style=margin-right:.05em;top:-3.063em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span><span style=top:-2.25em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord>1</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span><span class=mopen>(</span><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.9072em><span class=svg-align style=top:-3em><span class=pstrut style=height:3em></span><span class=mord style=padding-left:.833em><span class=mord>2</span></span></span><span style=top:-2.8672em><span class=pstrut style=height:3em></span><span class=hide-tail style=min-width:.853em;height:1.08em><svg preserveaspectratio="xMinYMin slice" viewbox="0 0 400000 1080" height=1.08em width=400em xmlns=http://www.w3.org/2000/svg><path d="M95,702 c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14 c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54 c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10 s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429 c69,-144,104.5,-217.7,106.5,-221 l0 -0 c5.3,-9.3,12,-14,20,-14 H400000v40H845.2724 s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7 c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z M834 80h400000v40h-400000z"/></svg></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.1328em><span></span></span></span></span></span><span class=mopen>(</span><span class="mord mathnormal">x</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span><span class=mord>1</span><span class=mclose>)</span><span class=mclose><span class=mclose>)</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.8141em><span style=margin-right:.05em;top:-3.063em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:1.19em><span></span></span></span></span></span><span class=arraycolsep style=width:1em></span><span class=col-align-l><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:1.69em><span style=top:-3.69em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord>0</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>≤</span><span class=mspace style=margin-right:.2778em></span><span class="mord mathnormal">t</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>≤</span><span class=mspace style=margin-right:.2778em></span><span class=mord>0.5</span></span></span><span style=top:-2.25em><span class=pstrut style=height:3.008em></span><span class=mord><span class=mord>0.5</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>&lt;</span><span class=mspace style=margin-right:.2778em></span><span class="mord mathnormal">t</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>≤</span><span class=mspace style=margin-right:.2778em></span><span class=mord>1</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:1.19em><span></span></span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></eqn></section><video controls width=640><source src=https://static.dsstudio.tech/video/pf/quad.mp4 type=video/mp4></video><p>我们也可以考虑使用三角函数来实现缓入缓出的效果：</p><section><eqn><span class=katex-display><span class=katex><span class=katex-mathml><math display=block xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>I</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>=</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><mo stretchy=false>(</mo><mn>1</mn><mo>−</mo><mrow><mi mathvariant=normal>c</mi><mi mathvariant=normal>o</mi><mi mathvariant=normal>s</mi></mrow><mo stretchy=false>(</mo><mi>π</mi><mrow></mrow><mi>t</mi><mo stretchy=false>)</mo><mo stretchy=false>)</mo></mrow><annotation encoding=application/x-tex> I(t) = \frac{1}{2} (1 - \mathrm{cos}(\pi{}t)) </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathnormal" style=margin-right:.07847em>I</span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.686em;height:2.0074em></span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:1.3214em><span style=top:-2.314em><span class=pstrut style=height:3em></span><span class=mord><span class=mord>2</span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.677em><span class=pstrut style=height:3em></span><span class=mord><span class=mord>1</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.686em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class=mopen>(</span><span class=mord>1</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=mord><span class="mord mathrm">cos</span></span><span class=mopen>(</span><span class="mord mathnormal" style=margin-right:.03588em>π</span><span class=mord></span><span class="mord mathnormal">t</span><span class=mclose>))</span></span></span></span></span></eqn></section><video controls width=640><source src=https://static.dsstudio.tech/video/pf/trig.mp4 type=video/mp4></video><h3>三次贝塞尔曲线</h3><p>一个在设计领域内更常见的插值函数是通过三次贝塞尔曲线生成：</p><section><eqn><span class=katex-display><span class=katex><span class=katex-mathml><math display=block xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mover accent=true><mi>B</mi><mo>⃗</mo></mover><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>=</mo><mo stretchy=false>(</mo><mn>1</mn><mo>−</mo><mi>t</mi><msup><mo stretchy=false>)</mo><mn>3</mn></msup><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>0</mn></msub><mo>+</mo><mn>3</mn><mo stretchy=false>(</mo><mn>1</mn><mo>−</mo><mi>t</mi><msup><mo stretchy=false>)</mo><mn>2</mn></msup><mi>t</mi><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>1</mn></msub><mo>+</mo><mn>3</mn><mo stretchy=false>(</mo><mn>1</mn><mo>−</mo><mi>t</mi><mo stretchy=false>)</mo><msup><mi>t</mi><mn>2</mn></msup><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>2</mn></msub><mo>+</mo><msup><mi>t</mi><mn>3</mn></msup><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>3</mn></msub></mrow><annotation encoding=application/x-tex> \vec{B}(t) = (1 - t)^3\vec{P}_0 + 3(1 - t)^2t\vec{P}_1 + 3(1-t)t^2\vec{P}_2 + t^3\vec{P}_3 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1.2163em></span><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.05017em>B</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=mopen>(</span><span class=mord>1</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1.2163em></span><span class="mord mathnormal">t</span><span class=mclose><span class=mclose>)</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.8641em><span style=margin-right:.05em;top:-3.113em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">3</span></span></span></span></span></span></span></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=mord>3</span><span class=mopen>(</span><span class=mord>1</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1.2163em></span><span class="mord mathnormal">t</span><span class=mclose><span class=mclose>)</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.8641em><span style=margin-right:.05em;top:-3.113em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mord mathnormal">t</span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=mord>3</span><span class=mopen>(</span><span class=mord>1</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1.2163em></span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.8641em><span style=margin-right:.05em;top:-3.113em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.15em;height:1.1163em></span><span class=mord><span class="mord mathnormal">t</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.8641em><span style=margin-right:.05em;top:-3.113em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">3</span></span></span></span></span></span></span></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">3</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></span></eqn></section><p>其中，<eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mrow><mn>0</mn><mo>…</mo><mn>3</mn></mrow></msub></mrow><annotation encoding=application/x-tex> \vec{P}_{0 \dots 3} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:1.1163em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">0</span><span class="minner mtight">…</span><span class="mord mtight">3</span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></eq> 是控制点。一般，<eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>0</mn></msub><mo>=</mo><mo stretchy=false>(</mo><mn>0</mn><mo separator=true>,</mo><mn>0</mn><mo stretchy=false>)</mo></mrow><annotation encoding=application/x-tex> \vec{P}_0 = (0, 0) </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:1.1163em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=mopen>(</span><span class=mord>0</span><span class=mpunct>,</span><span class=mspace style=margin-right:.1667em></span><span class=mord>0</span><span class=mclose>)</span></span></span></span></eq>, <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>3</mn></msub><mo>=</mo><mo stretchy=false>(</mo><mn>1</mn><mo separator=true>,</mo><mn>1</mn><mo stretchy=false>)</mo></mrow><annotation encoding=application/x-tex> \vec{P}_3 = (1, 1) </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:1.1163em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">3</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=mopen>(</span><span class=mord>1</span><span class=mpunct>,</span><span class=mspace style=margin-right:.1667em></span><span class=mord>1</span><span class=mclose>)</span></span></span></span></eq>. <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>1</mn></msub></mrow><annotation encoding=application/x-tex> \vec{P}_1 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:1.1163em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></eq> 和 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>2</mn></msub></mrow><annotation encoding=application/x-tex> \vec{P}_2 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:1.1163em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></eq> 是用户控制的两个点。</p><p>要保证这个贝塞尔曲线可以形成一个函数，则需要 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>1</mn></msub></mrow><annotation encoding=application/x-tex> \vec{P}_1 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:1.1163em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></eq> 和 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mover accent=true><mi>P</mi><mo>⃗</mo></mover><mn>2</mn></msub></mrow><annotation encoding=application/x-tex> \vec{P}_2 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.15em;height:1.1163em></span><span class=mord><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.13889em>P</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3011em><span style=margin-left:-.1389em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.15em><span></span></span></span></span></span></span></span></span></span></eq> 的横轴坐标在 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mo fence=true>[</mo><mn>0</mn><mo separator=true>,</mo><mn>1</mn><mo fence=true>]</mo></mrow><annotation encoding=application/x-tex> \left[0, 1\right] </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=minner><span class="mopen delimcenter" style=top:0>[</span><span class=mord>0</span><span class=mpunct>,</span><span class=mspace style=margin-right:.1667em></span><span class=mord>1</span><span class="mclose delimcenter" style=top:0>]</span></span></span></span></span></eq> 之间。</p><p>贝塞尔曲线本身的输出 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mover accent=true><mi>B</mi><mo>⃗</mo></mover></mrow><annotation encoding=application/x-tex> \vec{B} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.9663em></span><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9663em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.05017em>B</span></span><span style=top:-3.2523em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1522em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span></span></span></span></eq> 是一个点，我们只取 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>y</mi></mrow><annotation encoding=application/x-tex>y</annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.1944em;height:.625em></span><span class="mord mathnormal" style=margin-right:.03588em>y</span></span></span></span></eq> 轴坐标作为输出即可。</p><h2>仿射变换</h2><p>当我们说向量 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mover accent=true><mi>y</mi><mo>⃗</mo></mover></mrow><annotation encoding=application/x-tex> \vec{y} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.1944em;height:.9084em></span><span class="mord accent"><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.03588em>y</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1799em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.1944em><span></span></span></span></span></span></span></span></span></eq> 由向量 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mover accent=true><mi>x</mi><mo>⃗</mo></mover></mrow><annotation encoding=application/x-tex> \vec{x} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.714em></span><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span></span></span></span></eq> 经过仿射变换得出时，我们指存在一个矩阵 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi mathvariant=normal>A</mi></mrow><annotation encoding=application/x-tex> \mathrm{A} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.6833em></span><span class="mord mathrm">A</span></span></span></span></eq> 和向量 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mover accent=true><mi>b</mi><mo>⃗</mo></mover></mrow><annotation encoding=application/x-tex> \vec{b} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.9774em></span><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9774em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">b</span></span><span style=top:-3.2634em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2355em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span></span></span></span></eq> 满足 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mover accent=true><mi>y</mi><mo>⃗</mo></mover></mrow><annotation encoding=application/x-tex> \vec{y} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.1944em;height:.9084em></span><span class="mord accent"><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.03588em>y</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1799em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.1944em><span></span></span></span></span></span></span></span></span></eq> 和 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mover accent=true><mi>x</mi><mo>⃗</mo></mover></mrow><annotation encoding=application/x-tex> \vec{x} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.714em></span><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span></span></span></span></eq> 关系：</p><section><eqn><span class=katex-display><span class=katex><span class=katex-mathml><math display=block xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mover accent=true><mi>y</mi><mo>⃗</mo></mover><mo>=</mo><mi mathvariant=normal>A</mi><mover accent=true><mi>x</mi><mo>⃗</mo></mover><mo>+</mo><mover accent=true><mi>b</mi><mo>⃗</mo></mover></mrow><annotation encoding=application/x-tex> \vec{y} = \mathrm{A}\vec{x} + \vec{b} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.1944em;height:.9084em></span><span class="mord accent"><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal" style=margin-right:.03588em>y</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.1799em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.1944em><span></span></span></span></span></span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.0833em;height:.7973em></span><span class="mord mathrm">A</span><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.714em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">x</span></span><span style=top:-3em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2077em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=height:.9774em></span><span class="mord accent"><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.9774em><span style=top:-3em><span class=pstrut style=height:3em></span><span class="mord mathnormal">b</span></span><span style=top:-3.2634em><span class=pstrut style=height:3em></span><span class=accent-body style=left:-.2355em><span class=overlay style=width:.471em;height:.714em><svg viewbox="0 0 471 714" height=0.714em preserveaspectratio=xMinYMin style=width:.471em width=0.471em xmlns=http://www.w3.org/2000/svg><path d="M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 -1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 -7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 c-16-25.333-24-45-24-59z"/></svg></span></span></span></span></span></span></span></span></span></span></span></eqn></section><p>换成人话说就是：通过仿射变换，可以自由地平移和拉伸图层，但拉伸之后的图层只能是一个平行四边形。是的，矩形是一种特殊的平行四边形。</p><p>在 SDL 中，仿射变换的 API 非常地符合直觉：这个变换是由图像左上角坐标、图像右上角坐标和图像左下角坐标共同定义的。你需要这个材质的这三个点出现在哪里，就可以直接指定到哪里。但是这也意味着，当我们需要做诸如从属关系的时候，需要通过这个信息手动反算出变换矩阵。</p><p>考虑到仿射变换涵盖了平移，故将其也设置为图层的属性之一。在最终层叠所有的图层的时候，就可以通过仿射变换 API 来层叠了：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>SDL_SetRenderTarget</span>(sdl.<span class=hljs-built_in>renderer</span>(), sdl.<span class=hljs-built_in>texture</span>());
<span class=hljs-built_in>SDL_SetRenderDrawColor</span>(sdl.<span class=hljs-built_in>renderer</span>(), <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>);
<span class=hljs-built_in>SDL_RenderClear</span>(sdl.<span class=hljs-built_in>renderer</span>());

<span class=hljs-keyword>for</span> (<span class=hljs-type>const</span> <span class=hljs-keyword>auto</span>* layer : layers) {
    <span class=hljs-keyword>if</span> (!layer-><span class=hljs-built_in>visible</span>() || layer-><span class=hljs-built_in>texture</span>() == <span class=hljs-literal>nullptr</span>) {
        <span class=hljs-keyword>continue</span>;
    }

    <span class=hljs-built_in>SDL_SetTextureBlendMode</span>(sdl.<span class=hljs-built_in>texture</span>(), SDL_BLENDMODE_BLEND);
    <span class=hljs-built_in>SDL_RenderTextureAffine</span>(sdl.<span class=hljs-built_in>renderer</span>(),
                            layer-><span class=hljs-built_in>texture</span>(),
                            <span class=hljs-literal>nullptr</span>,
                            layer-><span class=hljs-built_in>origin</span>(),
                            layer-><span class=hljs-built_in>right</span>(),
                            layer-><span class=hljs-built_in>down</span>());
}

<span class=hljs-built_in>SDL_SetRenderTarget</span>(sdl.<span class=hljs-built_in>renderer</span>(), <span class=hljs-literal>nullptr</span>);
</code></pre><p>同样的，我们也可以设计一个仿射变换的算子。比如弹簧振荡效果：</p><section><eqn><span class=katex-display><span class=katex><span class=katex-mathml><math display=block xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msub><mi>I</mi><mi>f</mi></msub><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>=</mo><mfrac><mrow><mrow><mi mathvariant=normal>s</mi><mi mathvariant=normal>i</mi><mi mathvariant=normal>n</mi></mrow><mo stretchy=false>(</mo><mi>f</mi><mi>π</mi><mrow></mrow><mi>t</mi><mo stretchy=false>)</mo></mrow><mrow><mi>f</mi><mi>π</mi><mrow></mrow><mi>t</mi></mrow></mfrac></mrow><annotation encoding=application/x-tex> I_f(t) = \frac{\mathrm{sin}(f\pi{}t)}{f\pi{}t} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.2861em;height:1.0361em></span><span class=mord><span class="mord mathnormal" style=margin-right:.07847em>I</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3361em><span style=margin-left:-.0785em;margin-right:.05em;top:-2.55em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style=margin-right:.10764em>f</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.2861em><span></span></span></span></span></span></span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.8804em;height:2.3074em></span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:1.427em><span style=top:-2.314em><span class=pstrut style=height:3em></span><span class=mord><span class="mord mathnormal" style=margin-right:.10764em>f</span><span class="mord mathnormal" style=margin-right:.03588em>π</span><span class=mord></span><span class="mord mathnormal">t</span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.677em><span class=pstrut style=height:3em></span><span class=mord><span class=mord><span class="mord mathrm">sin</span></span><span class=mopen>(</span><span class="mord mathnormal" style=margin-right:.10764em>f</span><span class="mord mathnormal" style=margin-right:.03588em>π</span><span class=mord></span><span class="mord mathnormal">t</span><span class=mclose>)</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.8804em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></eqn></section><video controls width=640><source src=https://static.dsstudio.tech/video/pf/spring.mp4 type=video/mp4></video><p>什么？你问为啥它是从左上角为原点缩放的？因为我写到这里的时候发现了一个很头大的事情：图层没有定义过计算原点，所以一旦算子改变了图层的左上角坐标，之后的计算就会叠加在新的坐标上了。而且，又由于算子本身又被设计为可以叠加，所以还不能在算子实例中保存原有的原点——万一用户确实需要它动呢？</p><p>也许变换矩阵确实是一种好文明。</p><h2>自动变换计算</h2><p>那么，能不能把移动和变换结合起来呢？比如，图层在横向移动的时候，会变得更「扁」，而且速度越快越「扁」。</p><p>当然可以！我们甚至可以这样定义：</p><p>由于速度越快越扁，所以我们需要取到插值函数的导数 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><msup><mi>I</mi><mo mathvariant=normal>′</mo></msup><mrow></mrow><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo></mrow><annotation encoding=application/x-tex> I^\prime{}(t) </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1.0019em></span><span class=mord><span class="mord mathnormal" style=margin-right:.07847em>I</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.7519em><span style=margin-right:.05em;top:-3.063em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span><span class=mord></span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span></span></span></span></eq>。好在目前我们设计的函数都是初等函数的组合，其导数很容易求出解析解。一个例外是三次贝塞尔曲线。遇到这种，我们只能取 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>t</mi></mrow><annotation encoding=application/x-tex> t </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.6151em></span><span class="mord mathnormal">t</span></span></span></span></eq> 附近的值做差来近似求出变化量。</p><p>注意到，当我们求速度时，我们实际是在求一个复合函数的导数：<eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>v</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>=</mo><msup><mi>I</mi><mo mathvariant=normal>′</mo></msup><mrow></mrow><mo stretchy=false>(</mo><mfrac><mrow><mi>t</mi><mo>−</mo><msub><mi>t</mi><mn>0</mn></msub></mrow><mi>d</mi></mfrac><mo stretchy=false>)</mo><mo>=</mo><mfrac><mrow><msup><mi>I</mi><mo mathvariant=normal>′</mo></msup><mrow></mrow><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo></mrow><mi>d</mi></mfrac></mrow><annotation encoding=application/x-tex> v(t) = I^\prime{}(\frac{t - t_0}{d}) = \frac{I^\prime{}(t)}{d} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathnormal" style=margin-right:.03588em>v</span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.345em;height:1.1857em></span><span class=mord><span class="mord mathnormal" style=margin-right:.07847em>I</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.7519em><span style=margin-right:.05em;top:-3.063em><span class=pstrut style=height:2.7em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span><span class=mord></span><span class=mopen>(</span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.8407em><span style=top:-2.655em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">d</span></span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.4101em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class="mbin mtight">−</span><span class="mord mtight"><span class="mord mathnormal mtight">t</span><span class=msupsub><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.3173em><span style=margin-left:0;margin-right:.0714em;top:-2.357em><span class=pstrut style=height:2.5em></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">0</span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.143em><span></span></span></span></span></span></span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.345em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class=mclose>)</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.345em;height:1.4095em></span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:1.0645em><span style=top:-2.655em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">d</span></span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.485em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style=margin-right:.07847em>I</span><span class=msupsub><span class=vlist-t><span class=vlist-r><span class=vlist style=height:.8278em><span style=margin-right:.0714em;top:-2.931em><span class=pstrut style=height:2.5em></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span><span class="mord mtight"></span><span class="mopen mtight">(</span><span class="mord mathnormal mtight">t</span><span class="mclose mtight">)</span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.345em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></eq>。</p><p>我们定义一个参数 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>z</mi></mrow><annotation encoding=application/x-tex> z </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.4306em></span><span class="mord mathnormal" style=margin-right:.04398em>z</span></span></span></span></eq> 来限制形变，一般来说这个值会很小，比如 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mn>0.05</mn></mrow><annotation encoding=application/x-tex> 0.05 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=height:.6444em></span><span class=mord>0.05</span></span></span></span></eq>。</p><p>定义「压扁率」为 0 时表示无形变，为 1 时表示完全被压扁，则可以定义「压扁率函数」<eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>S</mi><mo stretchy=false>(</mo><mi>v</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo stretchy=false>)</mo><mo>=</mo><mn>1</mn><mo>−</mo><mfrac><mn>1</mn><mrow><mi mathvariant=normal>∣</mi><mi>v</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo>∗</mo><mi>z</mi><mi mathvariant=normal>∣</mi><mo>+</mo><mn>1</mn></mrow></mfrac></mrow><annotation encoding=application/x-tex> S(v(t)) = 1 - \frac{1}{|v(t) * z| + 1} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathnormal" style=margin-right:.05764em>S</span><span class=mopen>(</span><span class="mord mathnormal" style=margin-right:.03588em>v</span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>))</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.0833em;height:.7278em></span><span class=mord>1</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.52em;height:1.3651em></span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.8451em><span style=top:-2.655em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">∣</span><span class="mord mathnormal mtight" style=margin-right:.03588em>v</span><span class="mopen mtight">(</span><span class="mord mathnormal mtight">t</span><span class="mclose mtight">)</span><span class="mbin mtight">∗</span><span class="mord mathnormal mtight" style=margin-right:.04398em>z</span><span class="mord mtight">∣</span><span class="mbin mtight">+</span><span class="mord mtight">1</span></span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.394em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.52em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></eq>，这个定义方法可以保证速度无论多快，「压扁率」都不会达到 1. 同时可以定义出被压扁的边长度为 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mi>b</mi><mo stretchy=false>(</mo><mn>1</mn><mo>−</mo><mi>S</mi><mo stretchy=false>(</mo><mi>v</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo stretchy=false>)</mo><mo stretchy=false>)</mo></mrow><annotation encoding=application/x-tex> b (1 - S(v(t))) </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathnormal">b</span><span class=mopen>(</span><span class=mord>1</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>−</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class="mord mathnormal" style=margin-right:.05764em>S</span><span class=mopen>(</span><span class="mord mathnormal" style=margin-right:.03588em>v</span><span class=mopen>(</span><span class="mord mathnormal">t</span><span class=mclose>)))</span></span></span></span></eq>.</p><p>而「压扁」这个操作本身应该保持面积，即压扁的矩形的面积应该无形变的矩形的面积相同。则被伸长的边长度为 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mfrac><mi>a</mi><mrow><mn>1</mn><mo>−</mo><mi>S</mi><mo stretchy=false>(</mo><mi>v</mi><mo stretchy=false>(</mo><mi>t</mi><mo stretchy=false>)</mo><mo stretchy=false>)</mo></mrow></mfrac></mrow><annotation encoding=application/x-tex> \frac{a}{1 - S(v(t))} </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.52em;height:1.2154em></span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.6954em><span style=top:-2.655em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span><span class="mbin mtight">−</span><span class="mord mathnormal mtight" style=margin-right:.05764em>S</span><span class="mopen mtight">(</span><span class="mord mathnormal mtight" style=margin-right:.03588em>v</span><span class="mopen mtight">(</span><span class="mord mathnormal mtight">t</span><span class="mclose mtight">))</span></span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.394em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">a</span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.52em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></eq>.</p><p>最后，解算变换以矩形中心进行！</p><video controls width=640><source src=https://static.dsstudio.tech/video/pf/squish.mp4 type=video/mp4></video><p>虽然所有的所有这些操作在正经的非线编软件里应该都是可以做出来的，但亲自动手实现出来还是有那么一番别样的滋味。关键帧系统是一个好东西，不过现在还不太好搞。</p><h2>下一步</h2><p>有了图层的概念，我们就可以做一些色彩运算了——比如说正片叠底。</p>]]></description>
<pubDate>Wed, 27 May 2026 15:55:30 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-08-affine-affinity.html</guid>
</item>

    
        <item>
<title>PreFound:07 - 图片拼贴报</title>
<link>/archives/prefound-07-in-their-image.html</link>
<description><![CDATA[<p>来点图片是正确的、必要的、鼓舞人心的。</p><p><div class=more></div><p><a href=/archives/prefound-06-make-some-noise.html>上期</a>决定添加图片的支持。SDL 作为一个 2D 图形库，对于图片自然有一流的支持，用起来也非常舒心。</p><h2>导入</h2><p>和字体支持一样，SDL 的图片支持也是单独成库的。编辑 <code class=prettyprint>vcpkg.json</code> 引入 <code class=prettyprint>SDL3_image</code> 库：</p><pre><code class="hljs language-json"><span class=hljs-punctuation>{</span>
    <span class=hljs-comment>/* ... */</span>
    <span class=hljs-attr>"dependencies"</span><span class=hljs-punctuation>:</span> <span class=hljs-punctuation>[</span>
        <span class=hljs-comment>/* ... */</span>
        <span class=hljs-punctuation>{</span>
            <span class=hljs-attr>"name"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"sdl3-image"</span><span class=hljs-punctuation>,</span>
            <span class=hljs-attr>"features"</span><span class=hljs-punctuation>:</span> <span class=hljs-punctuation>[</span>
                <span class=hljs-string>"jpeg"</span><span class=hljs-punctuation>,</span>
                <span class=hljs-string>"png"</span>
            <span class=hljs-punctuation>]</span>
        <span class=hljs-punctuation>}</span>
    <span class=hljs-punctuation>]</span>
    <span class=hljs-comment>/* ... */</span>
<span class=hljs-punctuation>}</span>
</code></pre><p>注意到常见的两个图像格式 <code class=prettyprint>.jpg</code> 和 <code class=prettyprint>.png</code> 需要作为功能单独开启。SDL 底层实际会去调用 <code class=prettyprint>libjpeg-turbo</code> 和 <code class=prettyprint>libpng</code> 两个库来处理这两个类型的图像。</p><p>然后加入对应的链接库：</p><pre><code class="hljs language-cmake"><span class=hljs-comment># ...</span>
<span class=hljs-keyword>find_package</span>(SDL3_image CONFIG REQUIRED)
<span class=hljs-comment># ...</span>
<span class=hljs-keyword>target_link_libraries</span>(Foundation
    $&lt;<span class=hljs-keyword>IF</span>:$&lt;TARGET_EXISTS:SDL3_image::SDL3_image-shared>,SDL3_image::SDL3_image-shared,SDL3_image::SDL3_image-static>
)
</code></pre><p>就可以使用了。</p><h2>加载图像</h2><p>和 SDL 2 版本不同，SDL 3 版本的图形库不再需要手动调用 <code class=prettyprint>IMG_Init(.)</code> 来初始化支持了。它会在你首次使用对应的图像类型的时候，自动初始化底层库。</p><p>要加载一个图像，我们只需要执行 <code class=prettyprint>IMG_LoadTexture(.)</code> 就可以将图像加载成为一个纹理。</p><p>计算机图像处理一般到了这个时候，就该抽出一张莱娜图了。不过这回我们还是用点别的吧——</p><blockquote><p>I retired from modeling a long time ago. It's time I retired from tech, too.</p></blockquote><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* image = <span class=hljs-built_in>IMG_LoadTexture</span>(sdl.<span class=hljs-built_in>renderer</span>(), <span class=hljs-string>"assets/image/pepper.png"</span>);
</code></pre><h2>将绘制抽象成类</h2><p>后续我们应该会用到更多的需要引用外部资源，或者有复杂绘制流程。如果都摊开的话未免有点太乱。以图像为例子，我们可以构建一个类来封装它的资源申请、释放和绘制操作：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>class</span> <span class="hljs-title class_">Image</span> {
<span class=hljs-keyword>public</span>:
    <span class=hljs-built_in>Image</span>(SdlContext *sdl, <span class=hljs-type>int</span> x, <span class=hljs-type>int</span> y, <span class=hljs-type>const</span> <span class=hljs-type>char</span>* path) : _sdl(sdl), _x(x), _y(y) {
        _image = <span class=hljs-built_in>IMG_LoadTexture</span>(_sdl-><span class=hljs-built_in>renderer</span>(), path);
    }

    ~<span class=hljs-built_in>Image</span>() {
        <span class=hljs-keyword>if</span> (_image) {
            <span class=hljs-built_in>SDL_DestroyTexture</span>(_image);
        }
    }

    <span class=hljs-function><span class=hljs-type>void</span> <span class=hljs-title>render</span><span class=hljs-params>(SdlContext *sdl)</span> </span>{
        <span class=hljs-keyword>if</span> (_image) {
            _draw_image();
        } <span class=hljs-keyword>else</span> {
            _draw_missing_texture();
        }
    }

<span class=hljs-keyword>private</span>:
    SdlContext* _sdl;
    <span class=hljs-type>int</span> _x, _y;
    SDL_Texture* _image{<span class=hljs-literal>nullptr</span>};

    <span class=hljs-type>void</span> _draw_image() { <span class=hljs-comment>/* ... */</span> }
    <span class=hljs-type>void</span> _draw_missing_texture() { <span class=hljs-comment>/* ... */</span> }
}
</code></pre><p>然后我们就可以这样进行调用：</p><pre><code class="hljs language-cpp"><span class=hljs-function>Image <span class=hljs-title>img</span><span class=hljs-params>(&sdl, <span class=hljs-number>10</span>, <span class=hljs-number>10</span>, <span class=hljs-string>"assets/image/pepper.png"</span>)</span></span>;
img.<span class=hljs-built_in>render</span>(&sdl);
</code></pre><h2>处理资源文件找不到的情况</h2><p>有些时候，图片文件可能恰好找不到或者是损坏的。这会使得 <code class=prettyprint>IMG_LoadTexture(.)</code> 返回一个 <code class=prettyprint>nullptr</code>. 我们在绘制的时候需要处理这种情况。</p><p>业界常用的方法是绘制一个「材质丢失」站位图，一般是洋红和黑色组成的棋盘格。由于没有图片加载的情况下，我们无从知道这个图像具体的宽高，所以就先统一使用 200x200 大小。</p><pre><code class="hljs language-cpp"><span class=hljs-type>void</span> _draw_missing_texture() {
    <span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> y = <span class=hljs-number>0</span>, cy = <span class=hljs-number>0</span>; y &lt; <span class=hljs-number>200</span>; y += <span class=hljs-number>16</span>, cy++) {
        <span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> x = <span class=hljs-number>0</span>, cx = <span class=hljs-number>0</span>; x &lt; <span class=hljs-number>200</span>; x += <span class=hljs-number>16</span>, cx++) {
            <span class=hljs-type>const</span> SDL_FRect rect = {
                    .x = _x + <span class=hljs-built_in>static_cast</span>&lt;<span class=hljs-type>float</span>>(x),
                    .y = _y + <span class=hljs-built_in>static_cast</span>&lt;<span class=hljs-type>float</span>>(y),
                    .w = <span class=hljs-number>16.f</span>,
                    .h = <span class=hljs-number>16.f</span>,
            };

            <span class=hljs-keyword>if</span> ((xc + yc) % <span class=hljs-number>2</span> == <span class=hljs-number>0</span>) {
                <span class=hljs-built_in>SDL_SetRenderDrawColor</span>(_sdl-><span class=hljs-built_in>renderer</span>(), <span class=hljs-number>255</span>, <span class=hljs-number>0</span>, <span class=hljs-number>255</span>, <span class=hljs-number>255</span>);
            } <span class=hljs-keyword>else</span> {
                <span class=hljs-built_in>SDL_SetRenderDrawColor</span>(_sdl-><span class=hljs-built_in>renderer</span>(), <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>255</span>);
            }
            <span class=hljs-built_in>SDL_RenderFillRect</span>(_sdl-><span class=hljs-built_in>renderer</span>(), &rect);
        }
    }
}
</code></pre><p><a href=https://imgchr.com/i/pmCIWX8><img alt=checkerboard.png src=https://s41.ax1x.com/2026/05/26/pmCIWX8.png></a></p><p>图 1 棋盘格</p><h2>绘制图像</h2><p>图像绘制本身非常简单，只是一个纹理复制的事情：</p><pre><code class="hljs language-cpp"><span class=hljs-type>void</span> _draw_image() {
    <span class=hljs-type>const</span> SDL_FRect dstRect = {
        .x = _x,
        .y = _y,
        .w = _image->w,
        .h = _image->h
    };
    <span class=hljs-built_in>SDL_SetRenderTarget</span>(_sdl-><span class=hljs-built_in>renderer</span>(), _sdl-><span class=hljs-built_in>texture</span>());
    <span class=hljs-built_in>SDL_RenderTexture</span>(_sdl-><span class=hljs-built_in>renderer</span>(), _image, <span class=hljs-literal>nullptr</span>, &dstRect);
}
</code></pre><p><a href=https://imgchr.com/i/pmCI40g><img alt=pepper.png src=https://s41.ax1x.com/2026/05/26/pmCI40g.png></a></p><p>图 2 彩椒图案</p><h2>下一步</h2><p>现在，图形绘制原语已经基本齐备。接下来或许可以研究一些动画效果了。</p>]]></description>
<pubDate>Mon, 25 May 2026 17:02:11 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-07-in-their-image.html</guid>
</item>

    
        <item>
<title>PreFound:06 - 漫游雪花海</title>
<link>/archives/prefound-06-make-some-noise.html</link>
<description><![CDATA[<p>我们来试试编码器的能力上限吧！</p><p><div class=more></div><p><a href=/archives/prefound-05-write-that-down.html>上期</a>决定做点会动的东西。单纯地移动色块之类的当然是要做，不过首先，我决定先做一些比较不正经的探索部件——毕竟，用代码生成视频的一个主要玩法就是程序化生成；而没有什么是比一个「随机」更适合作为基线的生成器了。</p><h2>整理框架</h2><p>继续用一个大的 <code class=prettyprint>main</code> 函数处理一切也不是不行，但既然用了 C++, 合适的代码架构还是应该设计一下的。</p><p>简单来说，我们会将所有 SDL 相关的初始化放到一个类中，叫做 <code class=prettyprint>SdlContext</code>; 所有 FFmpeg 相关的初始化和操作放到一个类中，叫做 <code class=prettyprint>FFmpegContext</code>. 这两个类在构造时设置所有必要的内容，析构时清理所有用到的数据结构。</p><p>需要用到的关键配置，比如视频的宽高、帧率、码率、文件信息，整理成一个单独的结构体 <code class=prettyprint>VideoSettings</code>.</p><p>这样，我们只需要把它放到全局上就好啦：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in main.cpp, global scope</span>
<span class=hljs-type>static</span> VideoSettings videoSettings {
    .width = <span class=hljs-number>640</span>,
    .height = <span class=hljs-number>480</span>,
    .fps = <span class=hljs-number>60</span>,
    .bitrate = <span class=hljs-number>10000000</span>,
    .filename = <span class=hljs-string>"output.mkv"</span>
};
<span class=hljs-type>static</span> FFmpegContext ff{videoSettings};
<span class=hljs-type>static</span> SdlContext sdl{videoSettings};
</code></pre><p>要提交一帧数据，只需要调用 <code class=prettyprint>ff.encode_rgb24_frame(.)</code>. 要获取 SDL 渲染好的 buffer, 只需要调用 <code class=prettyprint>sdl.get_render_output()</code>，即：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in main.cpp, main function</span>
<span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> i = <span class=hljs-number>0</span>; i &lt; TOTAL_FRAME_COUNT; i++) {
    sdl.<span class=hljs-built_in>begin_frame</span>();
    <span class=hljs-comment>/* SDL 渲染操作 */</span>
    sdl.<span class=hljs-built_in>end_frame</span>();
    ff.<span class=hljs-built_in>encode_rgb24_frame</span>(sdl.<span class=hljs-built_in>get_renderer_output</span>());
}
</code></pre><p>SDL 渲染操作中，由各个生成器操作 SDL 的渲染目标材质 <code class=prettyprint>sdl.texture()</code> 即可。我们也可以计算一下当前帧的时间 <code class=prettyprint>frameTime = static_cast&lt;float>(i) / static_cast&lt;float>(videoSettings.fps)</code> 传递给生成器作为参考。</p><h2>生成雪花帧</h2><p>雪花帧的生成意外地难。</p><p>我们首先考虑一个朴素的实现：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> y = <span class=hljs-number>0</span>; y &lt; videoSettings.height; y++) {
    <span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> x = <span class=hljs-number>0</span>; x &lt; videoSettings.width; x++) {
        <span class=hljs-type>const</span> <span class=hljs-type>uint8_t</span> r = <span class=hljs-built_in>next_random</span>();
        <span class=hljs-type>const</span> <span class=hljs-type>uint8_t</span> g = <span class=hljs-built_in>next_random</span>();
        <span class=hljs-type>const</span> <span class=hljs-type>uint8_t</span> b = <span class=hljs-built_in>next_random</span>();
        <span class=hljs-built_in>SDL_SetRenderDrawColor</span>(_renderer, r, g, b, <span class=hljs-number>255</span>);
        <span class=hljs-built_in>SDL_RenderPoint</span>(sdl.<span class=hljs-built_in>renderer</span>(), x, y);
    }
}
</code></pre><p>开始运行后，很长时间都没有出现结果。</p><pre><code class="hljs language-text">[2026-05-25 22:42:15.708] [info] Start rendering output.mkv
[2026-05-25 22:42:15.709] [info] selected video codec: h264_videotoolbox (VideoToolbox H.264 Encoder)
</code></pre><p>直到半分钟后——</p><pre><code class="hljs language-text">[2026-05-25 22:42:53.597] [info] rendered 600 frames in 37.763 seconds
</code></pre><p>这回是真的撞上了性能瓶颈。如果给每个像素都生成 3 个随机数的话，每帧就需要 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mn>640</mn><mo>×</mo><mn>480</mn><mo>×</mo><mn>3</mn><mo>=</mo><mn>921600</mn></mrow><annotation encoding=application/x-tex> 640 \times 480 \times 3 = 921600 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.0833em;height:.7278em></span><span class=mord>640</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>×</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.0833em;height:.7278em></span><span class=mord>480</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>×</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=height:.6444em></span><span class=mord>3</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=height:.6444em></span><span class=mord>921600</span></span></span></span></eq> 个随机字节生成；以及每次都需要做 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mn>640</mn><mo>×</mo><mn>480</mn><mo>=</mo><mn>307200</mn></mrow><annotation encoding=application/x-tex> 640 \times 480 = 307200 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.0833em;height:.7278em></span><span class=mord>640</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>×</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=height:.6444em></span><span class=mord>480</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>=</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=height:.6444em></span><span class=mord>307200</span></span></span></span></eq> 次点绘制操作。</p><p>随机数生成部分还好说，毕竟视频并不需要很强的密码学随机，用一个足够好的伪随机数生成器也能出同样的效果；但是这个点绘制是真的够呛。由于 SDL 的渲染架构，我们必须每次 roll 一个颜色然后画一个点，导致这个步骤没法很好地并行；而且这个过程的步骤数还是随着视频分辨率平方增长的。我们需要一个更为高效的方法。</p><p>答案是：预先生成一张 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mn>2048</mn><mo>×</mo><mn>2048</mn></mrow><annotation encoding=application/x-tex> 2048 \times 2048 </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.0833em;height:.7278em></span><span class=mord>2048</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>×</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=height:.6444em></span><span class=mord>2048</span></span></span></span></eq> 的雪花图，然后 GPU 直接从雪花图上复制区块下来就行。</p><p>具体的，我们先创建一个 CPU 渲染的画布（因为需要直接操作底层的 buffer, 比 GPU 画一堆点更快）：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* randomSurface = <span class=hljs-built_in>SDL_CreateSurface</span>(<span class=hljs-number>2048</span>, <span class=hljs-number>2048</span>, SDL_PIXELFORMAT_RGB24);
</code></pre><p>然后，用随机数填充画布：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>SDL_LockSurface</span>(randomSurface);
<span class=hljs-keyword>auto</span>* canvas = <span class=hljs-built_in>static_cast</span>&lt;<span class=hljs-type>uint8_t</span>*>(randomSurface->pixels);
<span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> y = <span class=hljs-number>0</span>; y &lt; <span class=hljs-number>2048</span>; y++) {
    <span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> x = <span class=hljs-number>0</span>; x &lt; <span class=hljs-number>2048</span>; x++) {
        <span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> i = <span class=hljs-number>0</span>; i &lt; <span class=hljs-number>3</span>; i++) {
            canvas[y * <span class=hljs-number>2048</span> * <span class=hljs-number>3</span> + x * <span class=hljs-number>3</span> + i] = <span class=hljs-built_in>next_random</span>();
        }
    }
}
<span class=hljs-built_in>SDL_UnlockSurface</span>(randomSurface);
</code></pre><p>最后，为了能让 GPU 使用它，把它烘焙成一个材质：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* randomTexture = <span class=hljs-built_in>SDL_CreateTextureFromSurface</span>(sdl.<span class=hljs-built_in>renderer</span>(), randomSurface);
</code></pre><p>用完后，记得释放材质和 CPU 画布：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>SDL_DestroyTexture</span>(randomTexture);
<span class=hljs-built_in>SDL_DestroySurface</span>(randomSurface);
</code></pre><h2><code class=prettyprint>next_random</code> 随机数的选择</h2><p>随机数本身在选择上也有讲究：必须挑选一个周期特别特别长的随机数生成器，否则生成出来的材质看起来就一点也不随机了：雪花中会出现一条条纵向的斜线（如图 1）。</p><p><a href=https://imgchr.com/i/pmCWoDJ><img alt=pattern.png src=https://s41.ax1x.com/2026/05/25/pmCWoDJ.png></a></p><p>图 1 由于周期过短形成的纹路</p><p>C++ 中内置了梅森旋转法伪随机发生器，我们可以直接使用：</p><pre><code class="hljs language-cpp"><span class=hljs-type>static</span> std::random_device rnd;
<span class=hljs-type>static</span> std::mt19937 engine{<span class=hljs-built_in>rnd</span>()};
<span class=hljs-function><span class=hljs-type>static</span> std::uniform_int_distribution&lt;<span class=hljs-type>uint8_t</span>> <span class=hljs-title>dist</span><span class=hljs-params>(<span class=hljs-number>0</span>, <span class=hljs-number>255</span>)</span></span>;

<span class=hljs-function><span class=hljs-type>static</span> <span class=hljs-type>uint8_t</span> <span class=hljs-title>next_random</span><span class=hljs-params>()</span> </span>{
    <span class=hljs-keyword>return</span> <span class=hljs-built_in>dist</span>(engine);
}
</code></pre><h2>绘制雪花</h2><p>我们按照时间的流逝，将视窗快速循环地滑过整个材质表面（如图 2），就可以实现动起来的雪花屏效果了：</p><p><a href=https://imgchr.com/i/pmCc7fU><img alt=zigzag.png src=https://s41.ax1x.com/2026/05/25/pmCc7fU.png></a></p><p>图 2 视窗在材质上 Z 字形滑动</p><pre><code class="hljs language-cpp"><span class=hljs-type>const</span> <span class=hljs-type>float</span> NOISE_TRAVEL_FACTOR = <span class=hljs-number>10000.f</span>;
<span class=hljs-type>const</span> <span class=hljs-type>int</span> travel = <span class=hljs-built_in>static_cast</span>&lt;<span class=hljs-type>int</span>>(frameTime * NOISE_TRAVEL_FACTOR);
<span class=hljs-type>const</span> <span class=hljs-type>int</span> maxX = <span class=hljs-number>2048</span> - videoSettings.width;
<span class=hljs-type>const</span> <span class=hljs-type>int</span> maxY = <span class=hljs-number>2048</span> - videoSettings.height;
<span class=hljs-type>const</span> <span class=hljs-type>int</span> horizontalTravel = travel % maxX;
<span class=hljs-type>const</span> <span class=hljs-type>int</span> verticalTravel = ((travel / maxX) * videoSettings.height) % maxY;

<span class=hljs-type>const</span> SDL_FRect srcRect = {
    .x = horizontalTravel,
    .y = verticalTravel,
    .w = videoSettings.width,
    .h = videoSettings.height
};

<span class=hljs-built_in>SDL_SetRenderTarget</span>(sdl.<span class=hljs-built_in>renderer</span>(), sdl.<span class=hljs-built_in>texture</span>());
<span class=hljs-built_in>SDL_RenderTexture</span>(sdl.<span class=hljs-built_in>renderer</span>(), randomTexture, &srcRect, <span class=hljs-literal>nullptr</span>);
</code></pre><p>注意到：如果我们设置的滑动速度 <code class=prettyprint>NOISE_TRAVEL_FACTOR</code> 太慢的话，它看起来就会变成真的「滑动」了。</p><p>这回渲染的速度就比之前快了很多了：</p><pre><code class="hljs language-text">[2026-05-25 22:46:17.333] [info] Start rendering output.mkv
[2026-05-25 22:46:17.333] [info] selected video codec: h264_videotoolbox (VideoToolbox H.264 Encoder)
[2026-05-25 22:46:18.977] [info] rendered 600 frames in 1.523 seconds
</code></pre><p><a href=https://imgchr.com/i/pmCWjgO><img alt=noise.png src=https://s41.ax1x.com/2026/05/25/pmCWjgO.png></a></p><p>图 3 样例输出</p><p><a href=https://imgchr.com/i/pmCWzKe><img alt=blocky-fuzz.png src=https://s41.ax1x.com/2026/05/25/pmCWzKe.png></a></p><p>图 4 注意到编码器已经无法完整地捕捉全部图像信息，进而产生了块状模糊</p><h2>那... 移动色块？</h2><p>即得易见平凡，仿照上例显然。留作习题答案略，读者自证不难。</p><pre><code class="hljs language-cpp"><span class=hljs-type>const</span> <span class=hljs-type>int</span> startX = <span class=hljs-number>10</span>, startY = <span class=hljs-number>10</span>;
<span class=hljs-type>const</span> <span class=hljs-type>int</span> w = <span class=hljs-number>50</span>, h = <span class=hljs-number>50</span>;
<span class=hljs-type>const</span> <span class=hljs-type>int</span> speedX = <span class=hljs-number>4</span>, speedY = <span class=hljs-number>1</span>;
<span class=hljs-type>const</span> <span class=hljs-type>float</span> dX = speedX * frameTime;
<span class=hljs-type>const</span> <span class=hljs-type>float</span> dY = speedY * frameTime;

<span class=hljs-type>const</span> SDL_FRect rect = {
    .x = startX + dX,
    .y = startY + dY,
    .w = w,
    .h = h,
};

<span class=hljs-built_in>SDL_SetRenderTarget</span>(sdl.<span class=hljs-built_in>renderer</span>(), sdl.<span class=hljs-built_in>texture</span>());
<span class=hljs-built_in>SDL_SetRenderDrawColor</span>(<span class=hljs-number>255</span>, <span class=hljs-number>0</span>, <span class=hljs-number>255</span>, <span class=hljs-number>0</span>); <span class=hljs-comment>// 当然是继续用洋红色</span>
<span class=hljs-built_in>SDL_RenderFillRect</span>(sdl.<span class=hljs-built_in>renderer</span>(), &rect);
</code></pre><p>不过，即使是移动色块也是有很多说法的。现在这种线性的移动看起来非常的怪，而且看起来好像还有些卡顿。这个问题我们在之后会专题讨论。</p><h2>下一步</h2><p>也许我们需要考虑一些图像的加载相关的内容了。毕竟，就算是做 PPT 也得能插入图片吧！</p>]]></description>
<pubDate>Mon, 25 May 2026 14:48:49 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-06-make-some-noise.html</guid>
</item>

    
        <item>
<title>PreFound:05 - 写出到屏幕</title>
<link>/archives/prefound-05-write-that-down.html</link>
<description><![CDATA[<p>在视频里加入文字是一件很奇怪的事情么？</p><p><div class=more></div><p><a href=/archives/prefound-04-raise-the-renderer.html>刚刚上期</a>我们启动了 SDL 的渲染流程，并成功地绘制了一些纯色色块输出到视频中。现在我们需要继续添加绘制文字的能力了。</p><p>好在（至少现在）我们不需要手动地解析 TrueType/OpenType 字形。<a href=https://gitlab.freedesktop.org/freetype/freetype/>FreeType</a> 和 <a href=https://github.com/harfbuzz/harfbuzz>HarfBuzz</a> 库已经帮我们完成了解析字形、光栅化以及必要的文字排版的操作。<a href=https://wiki.libsdl.org/SDL3_ttf/FrontPage>SDL_ttf</a> 库则进一步将整个过程包装成可供 SDL 库使用的形式。目前我们只需要先简单地引入 SDL_ttf 库就可以绘制文字到缓冲区了。</p><h2>导入</h2><p>SDL_ttf 库需要单独作为依赖导入：</p><pre><code class="hljs language-json"><span class=hljs-comment>/* in vcpkg.json */</span>
<span class=hljs-punctuation>{</span>
	<span class=hljs-comment>/* ... */</span>
	<span class=hljs-attr>"dependencies"</span><span class=hljs-punctuation>:</span> <span class=hljs-punctuation>[</span>
		<span class=hljs-comment>/* ... */</span>
		<span class=hljs-string>"sdl3-ttf"</span>
	<span class=hljs-punctuation>]</span>
	<span class=hljs-comment>/* ... */</span>
<span class=hljs-punctuation>}</span>
</code></pre><p>也需要单独进行链接：</p><pre><code class="hljs language-cmake"><span class=hljs-comment># in CMakeLists.txt</span>
<span class=hljs-comment># ...</span>
<span class=hljs-keyword>find_package</span>(SDL3_ttf CONFIG REQUIRED)
<span class=hljs-comment># ...</span>
<span class=hljs-keyword>target_link_libraries</span>(Foundation SDL3_ttf::SDL3_ttf)
</code></pre><p>以及单独地进行初始化：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// after SDL_Init(.)</span>
<span class=hljs-keyword>if</span> (!<span class=hljs-built_in>TTF_Init</span>()) {
	spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to initialize sdl_ttf: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
	<span class=hljs-keyword>return</span> <span class=hljs-number>-30</span>;
}
</code></pre><h2>指定字体</h2><p>要加载一个字体进入程序，只需要调用 <code class=prettyprint>TTF_OpenFont(filename, ptsize)</code> 即可。</p><p>需要注意到，SDL_ttf 库并不会查询系统内已经安装好的字体。我们必须手动定位字体文件，并指示 SDL_ttf 打开一个具体的字体文件。所以如果像是用 CSS 那样简单地指定 <code class=prettyprint>"JetBrains Mono", "Fira Code", "Consolas", "Courier New", monospace</code> 是一定会失败的。</p><p>但是「查找系统中安装的字体」这一茬又是心烦：在不同的平台上，这个操作是完全不一样的，御三家各有各的玩法。在 Windows 下，需要通过 GDI 的 <a href=https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-enumfontfamiliesexw><code class=prettyprint>EnumFontFamiliesEx</code></a> 过程来查找字体（或者手动去 <code class=prettyprint>C:\Windows\Fonts</code> 文件夹下枚举文件）；而在 Linux 下，则需要通过 <a href=https://stackoverflow.com/questions/10542832/how-to-use-fontconfig-to-get-font-list-c-c><code class=prettyprint>FontConfig</code></a> 库来枚举各个安装好的字体——这些字体可能并不总是在 <code class=prettyprint>/usr/share/fonts</code> 下，也可能是用户自行配置的路径下；在 macOS 下，则还需要打一个 FFI 到 Objective-C 那边，通过 Cocoa 提供的 <a href=https://stackoverflow.com/questions/1113040/list-of-installed-fonts-os-x-c><code class=prettyprint>NSFontManager</code></a> 来枚举字体。</p><p>那么，解决这个问题的最好方法就是暂时不解决这个问题。我直接提供一个具体的字体文件就可以了！西文的 <a href=https://dejavu-fonts.github.io/>DejaVu</a>, 中文的<a href="http://wenq.org/wqy2/index.cgi?action=browse&id=Home">文泉驿</a>，以及包含尽可能多字符集的 <a href=https://unifoundry.com/unifont/index.html>GNU Unifont</a> 都是可以自由使用的字体。</p><h3>等会，字体的 <code class=prettyprint>ptsize</code> 是什么？怎么换算成像素？</h3><p><code class=prettyprint>ptsize</code> 是字体的磅数，或者说字号。</p><p>SDL_ttf 不确定用的是那套标准，但直觉上应该还是按照桌面排版规则来走的。在桌面排版系统中，1pt 表示 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mfrac><mn>1</mn><mn>72</mn></mfrac></mrow><annotation encoding=application/x-tex>\frac{1}{72}</annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.345em;height:1.1901em></span><span class=mord><span class="mopen nulldelimiter"></span><span class=mfrac><span class="vlist-t vlist-t2"><span class=vlist-r><span class=vlist style=height:.8451em><span style=top:-2.655em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">72</span></span></span></span><span style=top:-3.23em><span class=pstrut style=height:3em></span><span class=frac-line style=border-bottom-width:.04em></span></span><span style=top:-3.394em><span class=pstrut style=height:3em></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span><span class=vlist-s>​</span></span><span class=vlist-r><span class=vlist style=height:.345em><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></eq> 英寸，大约 0.35 毫米。如果我们假定像素密度是 72 px/in 的话，那么 1pt 和 1px 就是刚好对应的。</p><p>不过，当我们说设置字体大小为 16pt 的时候，我们究竟是设置了什么东西成为 16pt?</p><p>和 <code class=prettyprint>em</code>, <code class=prettyprint>ex</code> 以及 <code class=prettyprint>x-height</code> 等描述具体字的尺寸不同。字体的磅数描述的是「铅字本身」的高度。也就是，这套字体在铸成之后，每个小的铅块的高度。在数字排版中，这个概念被继承了下来，用于表述这个字体的「最大」外框的高度。这里的「最大」带着引号，是因为数字字体的样条完全可以突破这个外框继续绘制（虽然最终绘制的效果可能会随着具体的渲染后端而变化）。</p><p>这也意味着一个更为棘手的问题：不同的字体即使调整成同样的磅数，它们的实际高度也可以是不一样的。</p><p><a href=https://imgchr.com/i/pmCM8cd><img alt=different-sizes.png src=https://s41.ax1x.com/2026/05/25/pmCM8cd.png></a></p><p>图 1 Hello, world 在 Calibri, Arial 和宋体下的渲染</p><p>字体大小设置为 16pt 后，光标的高度确实是实打实的 <eq><span class=katex><span class=katex-mathml><math xmlns=http://www.w3.org/1998/Math/MathML><semantics><mrow><mn>4</mn><mi>p</mi><mi>x</mi><mo>+</mo><mn>16</mn><mi>p</mi><mi>t</mi><mi mathvariant=normal>/</mi><mn>72</mn><mi>p</mi><mi>t</mi><mi mathvariant=normal>/</mi><mi>i</mi><mi>n</mi><mo>∗</mo><mn>96</mn><mi>p</mi><mi>x</mi><mi mathvariant=normal>/</mi><mi>i</mi><mi>n</mi><mo>≈</mo><mn>25</mn><mi>p</mi><mi>x</mi></mrow><annotation encoding=application/x-tex> 4px + 16pt / 72pt/in * 96px/in \approx 25px </annotation></semantics></math></span><span aria-hidden=true class=katex-html><span class=base><span class=strut style=vertical-align:-.1944em;height:.8389em></span><span class=mord>4</span><span class="mord mathnormal">p</span><span class="mord mathnormal">x</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>+</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=mord>16</span><span class="mord mathnormal">pt</span><span class=mord>/72</span><span class="mord mathnormal">pt</span><span class=mord>/</span><span class="mord mathnormal">in</span><span class=mspace style=margin-right:.2222em></span><span class=mbin>∗</span><span class=mspace style=margin-right:.2222em></span></span><span class=base><span class=strut style=vertical-align:-.25em;height:1em></span><span class=mord>96</span><span class="mord mathnormal">p</span><span class="mord mathnormal">x</span><span class=mord>/</span><span class="mord mathnormal">in</span><span class=mspace style=margin-right:.2778em></span><span class=mrel>≈</span><span class=mspace style=margin-right:.2778em></span></span><span class=base><span class=strut style=vertical-align:-.1944em;height:.8389em></span><span class=mord>25</span><span class="mord mathnormal">p</span><span class="mord mathnormal">x</span></span></span></span></eq>（笑）。</p><p>算了，先把文字画出来就算成功。</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* font = <span class=hljs-built_in>TTF_OpenFont</span>(<span class=hljs-string>"DejaVuSans.ttf"</span>, <span class=hljs-number>24</span>);
<span class=hljs-keyword>if</span> (!font) {
	spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to load font: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
	<span class=hljs-keyword>return</span> <span class=hljs-number>-31</span>;
}
</code></pre><h2>绘制字形</h2><p>既然之前已经在用 GPU 绘制，那么字形绘制也可以按照 GPU 绘制的方法来：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* engine = <span class=hljs-built_in>TTF_CreateRendererTextEngine</span>(renderer);
<span class=hljs-keyword>if</span> (!engine) {
	spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to create text renderer: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
	<span class=hljs-keyword>return</span> <span class=hljs-number>-32</span>;
}
</code></pre><p>创建要绘制的字符串对象：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* textObj = <span class=hljs-built_in>TTF_CreateText</span>(engine, font, <span class=hljs-string>"Hello, world!"</span>, <span class=hljs-number>0</span>);
<span class=hljs-keyword>if</span> (!textObj) {
	spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to create text object: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
	<span class=hljs-keyword>return</span> <span class=hljs-number>-33</span>;
}
</code></pre><p>然后在渲染循环里提交就可以：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// after SDL_SetRenderTarget(.)</span>
<span class=hljs-comment>// 设置文字的颜色</span>
<span class=hljs-built_in>SDL_SetRenderDrawColor</span>(renderer, <span class=hljs-number>255</span>, <span class=hljs-number>255</span>, <span class=hljs-number>255</span>, <span class=hljs-number>255</span>);
<span class=hljs-comment>// 它会自行调用渲染器执行绘制，我们绘制到画布 (10, 10) 的位置上</span>
<span class=hljs-built_in>TTF_DrawRendererText</span>(textObj, <span class=hljs-number>10</span>, <span class=hljs-number>10</span>);
</code></pre><p>使用完成的对象记得清理：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>TTF_DestroyText</span>(textObj);
<span class=hljs-built_in>TTF_DestroyRendererTextEngine</span>(engine);
<span class=hljs-built_in>TTF_CloseFont</span>(font);
<span class=hljs-built_in>TTF_Quit</span>();
</code></pre><h2>文字描边</h2><p>但是，这样画出来的文字感觉总是缺一点什么。它看起来太「细」了，边缘也过于锐利。要解决这个问题，最简单的方法是做一下文字描边。</p><p>虽然 SDL_ttf 库提供了设置文字描边的接口，但是比较糟心的是：文字描边并不是一个字体的后处理值，而是会被烘焙到字体对象中的一个设定。当你调用 <code class=prettyprint>TTF_SetFontOutline(.)</code> 函数设置字体对象的描边时，实际上就改变了字体光栅化的结果。这个函数相当于是直接沿着样条法线把字体「加粗」了。</p><p>所以，如果要提供描边支持，我们需要创建两个字体。一个是正常的内容，另一个是「加粗」的描边：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span> outlineThickness = <span class=hljs-number>2</span>; <span class=hljs-comment>// 2px 描边</span>
fontOutline = <span class=hljs-built_in>TTF_OpenFont</span>(<span class=hljs-string>"DejaVuSans.ttf"</span>, <span class=hljs-number>24</span>);
<span class=hljs-keyword>if</span> (!fontOutline) {
	spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to open font: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
	<span class=hljs-keyword>return</span> <span class=hljs-number>-34</span>;
}

<span class=hljs-built_in>TTF_SetFontOutline</span>(fontOutline, outlineThickness);
</code></pre><p>这两个文字对象需要各自渲染到自己的材质中（材质对象申请略）：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>SDL_SetRenderTarget</span>(renderer, textTexture);
<span class=hljs-built_in>SDL_SetRenderDrawColor</span>(renderer, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>);
<span class=hljs-built_in>SDL_RenderClear</span>(renderer);
<span class=hljs-built_in>SDL_SetRenderDrawColor</span>(renderer, <span class=hljs-number>255</span>, <span class=hljs-number>255</span>, <span class=hljs-number>255</span>, <span class=hljs-number>255</span>);
<span class=hljs-built_in>TTF_DrawRendererText</span>(textObj, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>);

<span class=hljs-built_in>SDL_SetRenderTarget</span>(renderer, textOutlineTexture);
<span class=hljs-built_in>SDL_SetRenderDrawColor</span>(renderer, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>);
<span class=hljs-built_in>SDL_RenderClear</span>(renderer);
<span class=hljs-built_in>SDL_SetRenderDrawColor</span>(renderer, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>, <span class=hljs-number>255</span>);
<span class=hljs-built_in>TTF_DrawRendererText</span>(textObj, <span class=hljs-number>0</span>, <span class=hljs-number>0</span>);
</code></pre><p>再将文字叠到阴影上：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// 因为带描边和不带描边绘制出来的大小不太一样</span>
<span class=hljs-comment>// 这里需要做一点偏移</span>
<span class=hljs-type>const</span> SDL_FRect cascadeRect = {
	outlineThickness, outlineThickness, textTexture->w, textTexture->h
};
<span class=hljs-built_in>SDL_SetRenderTarget</span>(renderer, textOutlineTexture);
<span class=hljs-built_in>SDL_RenderTexture</span>(renderer, textTexture, <span class=hljs-literal>nullptr</span>, &cascadeRect);
</code></pre><p>偏移的量是参考字<a href=https://gamedev.stackexchange.com/questions/119642/how-to-use-sdl-ttfs-outlines>这篇回答</a>得出的。</p><p>最后再提交到最终渲染：</p><pre><code class="hljs language-cpp"><span class=hljs-type>const</span> SDL_FRect dstRect = {
	<span class=hljs-number>10</span>, <span class=hljs-number>10</span>, textOutlineTexture->w, textOutlineTexture->h
};
<span class=hljs-built_in>SDL_SetRenderTarget</span>(renderer, texture);
<span class=hljs-built_in>SDL_RenderTexture</span>(renderer, textOutlineTexture, <span class=hljs-literal>nullptr</span>, &dstRect);
</code></pre><p>这样就可以实现带黑色描边的白色文字啦！</p><p><a href=https://imgchr.com/i/pmC3hZR><img alt=text-rendering-outlined.png src=https://s41.ax1x.com/2026/05/25/pmC3hZR.png></a></p><p>图 2 测试工程中显示的文字（注意和上述代码运行结果不同）</p><h2>下一步</h2><p>只渲染静态的内容未免有点无聊，该搞一些动态的内容了。以及，现在所有的东西都跑在一个大 <code class=prettyprint>main</code> 里面，需要设计一下怎么把它们拆分出去。</p><p>（按：其实拆分这一茬在我实际开始写 PreFound 库的时候就已经做了。博客中主要是为了记录关键过程，所以采用了大 <code class=prettyprint>main</code> 跑一切的方案。）</p>]]></description>
<pubDate>Mon, 25 May 2026 05:15:04 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-05-write-that-down.html</guid>
</item>

    
        <item>
<title>And Trek I Will</title>
<link>/archives/and-trek-i-will.html</link>
<description><![CDATA[<p>The following is a brief stray thought that has been winding around in my mind. I have to write it down and post it. It is a little confession of the recent disturbances rippling out from my head — the sudden proliferation of tech-related posts is not because I have more spare time, but because a new kind of restless driving force has pushed me forward.</p><p>I don’t know if it’s normal. I don’t know if it’s good for me. I’ll leave the right to judge to you, dear readers. Hopefully, you can decide whether one should pursue.</p><p><div class=more></div><p>Magicks come with sacrifices. The usual sacrifice is time – you spent countless hours studying magicks and practicing them. Those who took particular interest would then lean into more arcane topics. Many of which would not make it alone as the texts grow more obscure and the rituals grow more lengthy and specific. Often, when the task went dire, one would pray to the deities of the craft. Usually, there will be two figures to respond: Turing and von Neumann.</p><p>Those who are gifted heard from both. Their hands guided and their visions firm. They find their way into the magicks like the wind drafting through the forest, natural and elegant.</p><p>Those who are not that favored by the magicks can only hear from one and have to pay more than those lucky few. The Turing's Troop often find themselves gradually losing grip with their identities, lost in layers of abstraction; the von Neumann's Knights would often suffer from hair loss and chronic pain due to weary computations. The two groups practice magicks well, albeit they specialize in only a single field.</p><p>And there are those who are forsaken. Many of them would wisely decide to do something else, and many do find their ways of life without conjuring magicks into their lives. But some are too stubborn to give up. They wanted to practice magicks, even if it would cost more than they could pay.</p><p>It's a path filled with rocks and thorns, but trek they will.</p><p>Burning midnight candles is just the norm in society; I don't see any issue with that. Maybe the leisures can also be optional – I can derive pleasure from learning how magicks work. Friends and family might be important, but I can just take care of those later. Does food and water really matter? I don't even feel like having any of those. “Insane” is a strong word to use, my dear, and I don’t think I’ve done anything that outrageous.</p><p>Too much was offered on the altar. They are barely a person by the late stage. The primal instinct drives them into what can only be described as a human-skinned beast, yearning for more magicks.</p><p>Disfigured, they can also practice magicks. Some of them can even outperform those who were answered.</p><p>Is it a price too much to pay? I don't know, but I do know there must be pride to take.</p><hr><p>“So, you must have taken your share of the pride, then.” The man smirks at me.</p><p>“I was answered before! Otherwise, I won’t even think about magicks and whatnot. Besides, my hands were forced back then.”</p><p>“You don’t even have hands now, buddy.” He could barely hide the laughter. “It must have been quite difficult for you to type on the keyboard.”</p>]]></description>
<pubDate>Sun, 24 May 2026 16:55:07 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/and-trek-i-will.html</guid>
</item>

    
        <item>
<title>PreFound:04 - 举起渲染器</title>
<link>/archives/prefound-04-raise-the-renderer.html</link>
<description><![CDATA[<p>趁热打铁，实现无头渲染！</p><p><div class=more></div><p>由于博客实在不是一个贴代码的好地方，接下来的文章里就只能讨论总体的设计，仅在比较关键的地方列出大体代码结构了。</p><p><a href=/archives/prefound-03-walking-the-bitplane.html>刚刚上期</a>我们完成了将 RGB 图像写入视频流的基本架构，现在我们需要想办法渲染出 RGB 图像了。而说到 2D 渲染，我脑海里出现的最合适的库是 SDL. 当然，如果后面需要支持渲染 3D 模型的话，使用 GLFW 也可以是一个选择。</p><p>SDL 可以在不创建（可见）窗体的情况下创建渲染器，并且支持渲染到材质的操作。这正好符合我们的需要。</p><p>不过，虽然说名义上是「无头」渲染，通过 SDL 渲染图形仍然要创建窗口出来。这意味着哪怕未来会需要在服务器上运行这个程序，服务器上也需要装好 X11 的一套东西才行。</p><p>SDL 目前也在经历 v2/v3 变更。为了这个项目能用得久一点，我选择了使用 SDL3.</p><h2>初始化无窗口 SDL</h2><p>我们只需要用到 SDL 的图形能力，所以只需要初始化图形子系统：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>if</span> (!<span class=hljs-built_in>SDL_Init</span>(SDL_INIT_VIDEO)) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to initialize SDL: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
    <span class=hljs-keyword>return</span> <span class=hljs-number>-20</span>;
}
</code></pre><p>然后就是创建渲染器：</p><pre><code class="hljs language-cpp">SDL_Window *window = <span class=hljs-literal>nullptr</span>;
SDL_Renderer *renderer = <span class=hljs-literal>nullptr</span>;
<span class=hljs-keyword>if</span> (!<span class=hljs-built_in>SDL_CreateWindowAndRenderer</span>(
        <span class=hljs-string>"Preview"</span>, width, height, <span class=hljs-number>0</span>, &window, &renderer)) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to create window: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
    <span class=hljs-keyword>return</span> <span class=hljs-number>-21</span>;
}
</code></pre><p>这之后，我们就可以创建一个用于做为渲染目标的材质了：</p><pre><code class="hljs language-cpp">texture = <span class=hljs-built_in>SDL_CreateTexture</span>(renderer,
                            SDL_PIXELFORMAT_RGBA8888,
                            SDL_TEXTUREACCESS_TARGET,
                            width,
                            height);
<span class=hljs-keyword>if</span> (!texture) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to create texture: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
    <span class=hljs-keyword>return</span> <span class=hljs-number>-22</span>;
}
</code></pre><h2>绘制一些图形</h2><p>和有窗口的 SDL 不同，无头 SDL 模式下完全不需要去实现 <code class=prettyprint>SDL_AppIterate</code> 那一坨东西。拿到渲染器之后就可以直接使用了：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>SDL_SetRenderTarget</span>(renderer, texture);
<span class=hljs-built_in>SDL_SetRenderDrawColor</span>(renderer, <span class=hljs-number>255</span>, <span class=hljs-number>0</span>, <span class=hljs-number>255</span>, <span class=hljs-number>255</span>);
<span class=hljs-built_in>SDL_RenderClear</span>(renderer);
</code></pre><p>同样的，也不需要调用 <code class=prettyprint>SDL_RenderPresent</code> 函数。渲染到材质是立即执行的。</p><h2>拿到渲染结果</h2><p>SDL 的渲染器 (Renderer) 会自动选择最合适的图形后端，这也意味着在有显卡的机器上，这个后端十有八九是 GPU. 要拿到材质的原始 RGB 值，则需要将它「下载」到 CPU 端：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* surface = <span class=hljs-built_in>SDL_RenderReadPixels</span>(renderer, <span class=hljs-literal>nullptr</span>);
<span class=hljs-keyword>if</span> (!surface) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to read pixels: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
    <span class=hljs-keyword>return</span> <span class=hljs-number>-23</span>;
}
</code></pre><p>我们指定的材质格式是 <code class=prettyprint>RGBA8888</code>, 这是为了渲染的时候可以保留透明度信息，以便之后实现多个图层的叠加。当然，现在读取了材质内容，自然是为了将它提交到渲染循环当中，那么就需要将它转换成 RGB24 的格式：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* converted = <span class=hljs-built_in>SDL_ConvertSurface</span>(surface, SDL_PIXELFORMAT_RGB24);
<span class=hljs-keyword>if</span> (!converted) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to convert surface: {}"</span>, <span class=hljs-built_in>SDL_GetError</span>());
    <span class=hljs-built_in>SDL_DestroySurface</span>(surface);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-24</span>;
}
</code></pre><p>然后就可以将 RGB24 数据提交到编码循环里了：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>memcpy</span>(rgbBuffer, converted->pixels, surface->w * surface->h * <span class=hljs-number>3</span>);
</code></pre><p>完成使用后的资源记得及时释放：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>SDL_DestroySurface</span>(converted);
<span class=hljs-built_in>SDL_DestroySurface</span>(surface);
</code></pre><h2>性能的担忧</h2><blockquote><p>提早优化是万恶之源。</p></blockquote><p>我在写这一段实现的时候，我的直觉是这段代码的效率太低了：它需要做两次复制。如果我选择使用 CPU 渲染，我就可以节省从 GPU 复制到 CPU 的操作；如果我选择直接使用 <code class=prettyprint>RGB24</code> 作为绘制格式的话，我就可以再省下转换的操作。</p><p>这两次「贵」的操作可能一开始看起来很浪费，但是仔细思考一下：之后的各个操作，如果用 CPU 硬拉渲染可能会比用 GPU 渲染要慢许多。而且更重要的是，在图层逐渐增多的情况下，大部分操作都是渲染操作，让 GPU 进行大量的渲染显然比 CPU 渲染更为高效。</p><p>并且，从 GPU 复制到 CPU 这个「贵」的操作只有最终的图层合并完成之后才会需要做，而转换操作也只是在最终提交一帧的时候进行。所以整体来看，这并不是什么性能问题。</p><p>以及，如果真的认为有性能问题，那么必须通过性能剖析来验证。任何「直觉」在现代计算机的超高并发+分支预测+乱序执行下都会失效。</p><h2>下一步</h2><p>现在有了 SDL, 我们便可以做一些简单的图形绘制工作了。当然，仅局限于绘制一些色块未免有些无聊。（除非你是真正的大角虫，可以硬用色块砌出动画。）PreFound 当初设计的目标之一是渲染一些编程相关的视频短文 (video essay), 所以想办法绘制一些文字进去是绝对必要的。</p>]]></description>
<pubDate>Sun, 24 May 2026 16:05:38 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-04-raise-the-renderer.html</guid>
</item>

    
        <item>
<title>PreFound:03 - 跨越比特面</title>
<link>/archives/prefound-03-walking-the-bitplane.html</link>
<description><![CDATA[<p>开始千里之行的第二个三分钟。</p><p><div class=more></div><p><a href=/archives/prefound-02-priming-the-codec.html>刚刚上期</a>我们完成了基本的渲染循环的实现，留了两个关键问题：那个警告是从哪来的？以及怎么把 RGB 转换成 YUV420?</p><h2>指定色域</h2><p>警告本身的内容很简单，因为我们没有指定色域，所以它默认使用了 MPEG 色域。</p><p>如果只是需要去掉这一行警告的话，在设置编码器上下文时明确指定色域即可：</p><pre><code class="hljs language-cpp">context->color_range = AVCOL_RANGE_MPEG;
</code></pre><p>比较有趣的是：这个色域是一个窄色域，其明度范围为 <code class=prettyprint>[16, 235]</code>, 色度范围为 <code class=prettyprint>[16-240]</code>. 另一个色域格式是 <code class=prettyprint>AVCOL_RANGE_JPEG</code>, 其范围为 <code class=prettyprint>[0, 255]</code>.</p><p>虽然看似我们应该设置 <code class=prettyprint>AVCOL_RANGE_JPEG</code>, 不过真这么做的话，会发现 VLC 等播放器中的颜色反而比设置为 <code class=prettyprint>AVCOL_RANGE_MPEG</code> 要更为「黯淡」——尤其是后面我们开始从 RGB 图像中复制信息的时候，你会发现黑不够黑而白不够白。</p><p>和色彩相关的问题总是无穷无尽的，比如这玩意真的叫「色域」么？它和 Gamut 的区别是什么？为什么源码里叫它 MPEG/JPEG 而不是像其他程序中称其为 Limited/Full?</p><p>这些问题只能暂时先放下了。我们的目标是用代码程序化地生成视频，这些问题暂时不影响这个主要目标。</p><h2>格式转换</h2><p>尽管我们可以手动写 RGB 到 YUV420 的转换，但这块石头我还是不太想亲手搬的。FFmpeg 库为我们提供了 <code class=prettyprint>swscale</code> 能力，我们可以直接借用。</p><p>分配一个 RGB 的缓冲区：</p><pre><code class="hljs language-cpp"><span class=hljs-type>uint8_t</span> *rgbBuffer = <span class=hljs-keyword>new</span> <span class=hljs-type>uint8_t</span>[context->width * context->height * <span class=hljs-number>3</span>];
</code></pre><p>以及分配一个 <code class=prettyprint>swscale</code> 的上下文：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* swContext = <span class=hljs-built_in>sws_getContext</span>(context->width,
                                    context->height,
                                    AV_PIX_FMT_RGB24,
                                    context->width,
                                    context->height,
                                    context->pix_fmt,
                                    <span class=hljs-number>0</span>,
                                    <span class=hljs-literal>nullptr</span>,
                                    <span class=hljs-literal>nullptr</span>,
                                    <span class=hljs-literal>nullptr</span>);
<span class=hljs-comment>// 源和目标宽高一致；源格式为 RGB24, 目标格式遵循编码器设定，没有额外的 filter</span>
</code></pre><p>有了这个上下文之后，我们就可以通过调用 <code class=prettyprint>sws_scale</code> 函数来做格式之间的转换了。</p><p>先将之前写 YCbCr 空间的部分替换为写 RGB buffer:</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in for-loop</span>
<span class=hljs-comment>// after av_frame_make_writable</span>
<span class=hljs-comment>// 简单的条纹</span>
<span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> y = <span class=hljs-number>0</span>; y &lt; context->height; y++) {
    <span class=hljs-built_in>memset</span>(&rgbBuffer[y * context->width * <span class=hljs-number>3</span>],
            (y + i) & <span class=hljs-number>0xff</span>,
            context->width * <span class=hljs-number>3</span>);
}
</code></pre><p>写完 RGB buffer 后，就可以直接通过 <code class=prettyprint>swscale</code> 转换到 <code class=prettyprint>frame</code> 当中去：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in for-loop</span>
<span class=hljs-type>const</span> <span class=hljs-type>uint8_t</span>* sourceDataPlanes[<span class=hljs-number>1</span>] = {rgbBuffer};
<span class=hljs-type>const</span> <span class=hljs-type>int</span> sourceDataStrides[<span class=hljs-number>1</span>] = {<span class=hljs-number>3</span> * context->width};

err = <span class=hljs-built_in>sws_scale</span>(swContext,
                sourceDataPlanes,
                sourceDataStrides,
                <span class=hljs-number>0</span>,
                context->height,
                frame->data,
                frame->linesize);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to convert frame data: {}"</span>, err);
    <span class=hljs-keyword>continue</span>;
}
</code></pre><p>当然，申请的资源要记得释放：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>sws_freeContext</span>(swContext);
<span class=hljs-keyword>delete</span>[] rgbBuffer;
</code></pre><p>完整的代码在<a href=https://gist.github.com/dousha/daa312e19692e981d94f4d55596a9d16>这里</a>。</p><h2>成果展示</h2><p><a href=https://imgchr.com/i/pm9qsaD><img alt=output.mkv.png src=https://s41.ax1x.com/2026/05/24/pm9qsaD.png></a></p><p>图 1 output.mkv</p><h2>下一步</h2><p>经过这一段的折腾，我们终于搭建好了视频渲染的基础设施。接下来就是想办法开始程序化地填充 RGB Buffer 了。</p><p>程序化填充 RGB Buffer 的方法有很多，比如通过图形库即可实现无头渲染；或者如果我们确实想搞一点非常有趣的效果的话，也可以使用 OpenGL 的无窗口模式进行绘制。</p>]]></description>
<pubDate>Sun, 24 May 2026 13:02:59 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-03-walking-the-bitplane.html</guid>
</item>

    
        <item>
<title>PreFound:02 - 启动编码器</title>
<link>/archives/prefound-02-priming-the-codec.html</link>
<description><![CDATA[<p>当心：前方有龙。</p><p><div class=more></div><p>由于我们是直接在内存中渲染图形并喂给 FFmpeg 进行编码的，我们需要自行控制编码器、编码流和容器写出。FFmpeg 原生提供的用于操作文件的帮助函数我们的使用就相对少一些。</p><h2>查找系统中可用的编码器</h2><p><a href=/archives/prefound-01-wanna-be-starting-something.html>上一篇中</a>我们注意到了虽然 FFmpeg 输出它支持的编码器很多，但系统上并不是所有的编码器都装了的。现在，我们需要筛选出实际可以用的编码器。具体的，我们关心系统上是否存在一个可以进行 H.264 编码的编码器：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>extern</span> <span class=hljs-string>"C"</span> {
<span class=hljs-meta>#<span class=hljs-keyword>include</span> <span class=hljs-string>"libavformat/avformat.h"</span></span>
<span class=hljs-meta>#<span class=hljs-keyword>include</span> <span class=hljs-string>"libavcodec/avcodec.h"</span></span>
<span class=hljs-meta>#<span class=hljs-keyword>include</span> <span class=hljs-string>"libavutil/opt.h"</span></span>
}

<span class=hljs-meta>#<span class=hljs-keyword>include</span> <span class=hljs-string>"spdlog/spdlog.h"</span></span>

<span class=hljs-function><span class=hljs-type>bool</span> <span class=hljs-title>select_video_encoder</span><span class=hljs-params>(<span class=hljs-type>char</span>* buffer, <span class=hljs-type>size_t</span> buffer_size)</span> </span>{
    <span class=hljs-type>const</span> AVCodec* codec;
    <span class=hljs-type>void</span>* handle = <span class=hljs-literal>nullptr</span>;

    <span class=hljs-keyword>while</span> ((codec = <span class=hljs-built_in>av_codec_iterate</span>(&handle))) {
        <span class=hljs-keyword>if</span> (!<span class=hljs-built_in>av_codec_is_encoder</span>(codec) || codec->type != AVMEDIA_TYPE_VIDEO) {
            <span class=hljs-keyword>continue</span>;
        }

        spdlog::<span class=hljs-built_in>debug</span>(
            <span class=hljs-string>"known video codec: {} ({})"</span>, codec->name, codec->long_name);

        <span class=hljs-comment>// find h264</span>
        <span class=hljs-keyword>if</span> (<span class=hljs-built_in>strstr</span>(codec->long_name, <span class=hljs-string>"H.264"</span>) || <span class=hljs-built_in>strstr</span>(codec->name, <span class=hljs-string>"h264"</span>)) {
            spdlog::<span class=hljs-built_in>info</span>(
                <span class=hljs-string>"selected video codec: {} ({})"</span>, codec->name, codec->long_name);
            <span class=hljs-built_in>strncpy</span>(buffer, codec->name, buffer_size);
            <span class=hljs-keyword>return</span> <span class=hljs-literal>true</span>;
        }
    }

    spdlog::<span class=hljs-built_in>warn</span>(<span class=hljs-string>"no suitable video encoder found"</span>);
    <span class=hljs-keyword>return</span> <span class=hljs-literal>false</span>;
}
</code></pre><p>你的系统可能这个函数确实会返回 <code class=prettyprint>false</code>. 但是没关系，可以仿照查找 H.264 编码器的方法继续查找其他编码器（如 <code class=prettyprint>mpeg4</code>, <code class=prettyprint>vp9</code> 或者 <code class=prettyprint>av1</code>）。如果确实是一个都找不着……那可能确实需要开一些 GPL 库才可以。</p><p>在 macOS 下，<code class=prettyprint>h264</code> 编码由 <code class=prettyprint>h264_videotoolbox</code> 实现。这个编码器是默认 FFmpeg 配置下就可用的。</p><p>还有一个方法是使用 <code class=prettyprint>avcodec_find_encoder</code> 方法来查找注册到系统中的编码器，比如：</p><pre><code class="hljs language-cpp"><span class=hljs-type>const</span> <span class=hljs-keyword>auto</span>* codec = <span class=hljs-built_in>avcodec_find_encoder</span>(AV_CODEC_ID_H264);
</code></pre><p>如果对应的 ID 下没有可用的编码器，则该函数返回 <code class=prettyprint>nullptr</code>. 我们可以通过传入可接受的 <code class=prettyprint>AV_CODEC_ID_*</code> 来逐个查找系统中可能的编码器实现，此处不再赘述。</p><p>后文中，我将假定所使用的编码器实现 H.264 编码格式，或者至少支持 YUV420 格式输入。</p><h2>启动编码器</h2><p>首先，根据查找到的可用编码器名，召唤对应的编码器实现：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// include 和函数签名略，就当我们在 main 函数里</span>
<span class=hljs-type>int</span> err = <span class=hljs-number>0</span>;
<span class=hljs-type>char</span> codecName[<span class=hljs-number>64</span>];
<span class=hljs-keyword>if</span> (!<span class=hljs-built_in>select_video_encoder</span>(codecName, <span class=hljs-built_in>sizeof</span>(codecName))) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"no codec available"</span>);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-1</span>;
}

<span class=hljs-type>const</span> <span class=hljs-keyword>auto</span>* codec = <span class=hljs-built_in>avcodec_find_encoder_by_name</span>(codecName);
<span class=hljs-keyword>if</span> (!codec) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"cannot find codec `{}`"</span>, codecName);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-2</span>;
}
</code></pre><p>接下来需要申请一个编码器上下文：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* context = <span class=hljs-built_in>avcodec_alloc_context3</span>(codec);
<span class=hljs-keyword>if</span> (!context) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to allocate codec context"</span>);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-3</span>;
}
</code></pre><p>编码器上下文用于保存编码器配置，比如比特率、帧率等等信息。假设我们需要输出的视频文件尺寸为 640x480@60fps, 码率为 10Mbps, 则可以这样设置编码器上下文：</p><pre><code class="hljs language-cpp">context->bit_rate = <span class=hljs-number>10000000</span>;
context->width = <span class=hljs-number>640</span>;
context->height = <span class=hljs-number>480</span>;
context->time_base = (AVRational){<span class=hljs-number>1</span>, <span class=hljs-number>60</span>};
context->framerate = (AVRational){<span class=hljs-number>60</span>, <span class=hljs-number>1</span>};
</code></pre><p>一般来说，大部分编码器都需要 YUV420 格式、逐行扫描的输入：</p><pre><code class="hljs language-cpp">context->pix_fmt = AV_PIX_FMT_YUV420P;
</code></pre><p>对于 H.264 这种带有帧间压缩的编码，则同样需要指定帧间压缩参数。比如我们需要每 10 帧生成一个 I 帧，则可以这样配置：</p><pre><code class="hljs language-cpp">context->gop_size = <span class=hljs-number>10</span>;
</code></pre><p>以及，关闭 B 帧生成。这主要是因为如果有 B 帧，后续将 H.264 码流导入视频容器时会需要处理各种比较棘手的情况。</p><pre><code class="hljs language-cpp">context->max_b_frames = <span class=hljs-number>0</span>;
</code></pre><p>对于 H.264 编码，我们还可以设置其使用高质量（缓慢）编码预设：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>if</span> (codec->id == AV_CODEC_ID_H264) {
    <span class=hljs-built_in>av_opt_set</span>(context->priv_data, <span class=hljs-string>"preset"</span>, <span class=hljs-string>"slow"</span>, <span class=hljs-number>0</span>);
}
</code></pre><p>完成上述设置之后，我们就可以尝试启动编码器了：</p><pre><code class="hljs language-cpp">err = <span class=hljs-built_in>avcodec_open2</span>(context, codec, <span class=hljs-literal>nullptr</span>);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to open the codec: {}"</span>, err);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-4</span>;
}
</code></pre><h2>设置基本数据结构</h2><p>创建一个用于承载视频信息的帧：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* frame = <span class=hljs-built_in>av_frame_alloc</span>();
<span class=hljs-keyword>if</span> (!frame) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to allocate a frame"</span>);
    <span class=hljs-built_in>avcodec_free_context</span>(&context);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-5</span>;
}
</code></pre><p>以及一个用于承载编码信息的包：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* packet = <span class=hljs-built_in>av_packet_alloc</span>();
<span class=hljs-keyword>if</span> (!packet) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to allocate a packet"</span>);
    <span class=hljs-built_in>avcodec_free_context</span>(&context);
    <span class=hljs-built_in>av_frame_free</span>(&frame);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-6</span>;
}
</code></pre><p>这两个数据结构在整个编码循环中都可以重复使用，并不需要每个视频帧或者每次编码都创建。</p><p>另一个有趣的地方是 FFmpeg 的许多清理函数都是传入 <code class=prettyprint>T**</code> 而非 <code class=prettyprint>T*</code> 的。在对应的变量被清理后，FFmpeg 会自动将其内容设置为 <code class=prettyprint>nullptr</code>.</p><p>我们同样需要设置帧的大小信息，复用之前的视频信息配置即可：</p><pre><code class="hljs language-cpp">frame->format = context->pix_fmt;
frame->width = context->width;
frame->height = context->height;
</code></pre><p>之后，即可分配帧所需要使用的缓冲区了：</p><pre><code class="hljs language-cpp">err = <span class=hljs-built_in>av_frame_get_buffer</span>(frame, <span class=hljs-number>0</span>);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to allocate frame buffer: {}"</span>, err);
    <span class=hljs-built_in>avcodec_free_context</span>(&context);
    <span class=hljs-built_in>av_frame_free</span>(&frame);
    <span class=hljs-built_in>av_packet_free</span>(&packet);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-7</span>;
}
</code></pre><p>当然，我们会需要保存编码结果。目前我们先打开一个文件用于保存编码结果：</p><pre><code class="hljs language-cpp">FILE *fp = <span class=hljs-built_in>fopen</span>(<span class=hljs-string>"output.h264"</span>, <span class=hljs-string>"wb"</span>);
<span class=hljs-comment>// 错误处理从略</span>
</code></pre><h2>编码循环</h2><p>接下来，我们将生成 300 帧（5 秒钟）的视频数据并编码。</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> i = <span class=hljs-number>0</span>; i &lt; <span class=hljs-number>300</span>; i++) {
    <span class=hljs-comment>// 接下来标记为 in for-loop 的代码发生在这里</span>
}
</code></pre><p>首先，和其他图形操作一样，我们需要先让帧缓冲区进入可写状态：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in for-loop</span>
err = <span class=hljs-built_in>av_frame_make_writable</span>(frame);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"cannot make frame writable: {}"</span>, err);
    <span class=hljs-keyword>break</span>;
}
</code></pre><p>之后，我们就可以操作帧缓冲区中的数据了。如果我们已经有生成好的图形数据，就是在这个步骤填入的。</p><p>YUV420 是一种平坦格式，即它的数据是按 <code class=prettyprint>YYYY...CbCbCbCb...CrCrCrCr...</code> 的顺序存储的。相比而言，RGB888 则是一种交错格式，即它的数据是按 <code class=prettyprint>RGBRGBRGBRGB...</code> 的顺序存储的。</p><p>我们在这里先生成一些测试用的数据：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in for-loop</span>
<span class=hljs-comment>// Y 分量的数据</span>
<span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> y = <span class=hljs-number>0</span>; y &lt; context->height; y++) {
    <span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> x = <span class=hljs-number>0</span>; x &lt; context->width; x++) {
        frame->data[<span class=hljs-number>0</span>][y * frame->linesize[<span class=hljs-number>0</span>] + x] = (x + y + i * <span class=hljs-number>3</span>) & <span class=hljs-number>0xff</span>;
    }
}

<span class=hljs-comment>// CbCr 分量的数据</span>
<span class=hljs-comment>// 注意到 YUV420 中，色度数据量是明度数据量的一半</span>
<span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> y = <span class=hljs-number>0</span>; y &lt; context->height / <span class=hljs-number>2</span>; y++) {
    <span class=hljs-keyword>for</span> (<span class=hljs-type>int</span> x = <span class=hljs-number>0</span>; x &lt; context->width / <span class=hljs-number>2</span>; x++) {
        frame->data[<span class=hljs-number>1</span>][y * frame->linesize[<span class=hljs-number>1</span>] + x] = (<span class=hljs-number>128</span> + y + i * <span class=hljs-number>2</span>) & <span class=hljs-number>0xff</span>;
        frame->data[<span class=hljs-number>2</span>][y * frame->linesize[<span class=hljs-number>2</span>] + x] = (<span class=hljs-number>64</span> + x + i * <span class=hljs-number>5</span>) & <span class=hljs-number>0xff</span>;
    }
}

<span class=hljs-comment>// 指定这个是第几帧</span>
frame->pts = i;
</code></pre><p>完成帧数据的填充后，就可以提交到编码器进行编码了：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in for-loop</span>
err = <span class=hljs-built_in>avcodec_send_frame</span>(context, frame);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to send the frame to encoder: {}"</span>, err);
    <span class=hljs-keyword>break</span>;
}
</code></pre><p>提交到编码器后，就可以从编码器中取出编码数据并写出文件：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in for-loop</span>
<span class=hljs-keyword>while</span> (err >= <span class=hljs-number>0</span>) {
    err = <span class=hljs-built_in>avcodec_receive_packet</span>(context, packet);
    <span class=hljs-keyword>if</span> (err == <span class=hljs-built_in>AVERROR</span>(EAGAIN) || err == AVERROR_EOF) {
        <span class=hljs-comment>// 继续编码下一帧</span>
        <span class=hljs-keyword>break</span>;
    }

    <span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
        spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to get packet from encoder: {}"</span>, err);
        <span class=hljs-keyword>break</span>;
    }

    <span class=hljs-built_in>fwrite</span>(packet->data, <span class=hljs-number>1</span>, packet->size, fp);
    <span class=hljs-comment>// 包数据操作完成，释放包内的数据引用</span>
    <span class=hljs-built_in>av_packet_unref</span>(packet);
}
</code></pre><h2>结束编码</h2><p>完成所有的视频信息编码后，我们还需要清理一下编码器的状态，并写出编码结尾：</p><pre><code class="hljs language-cpp">err = <span class=hljs-built_in>avcodec_send_frame</span>(context, <span class=hljs-literal>nullptr</span>);
<span class=hljs-keyword>if</span> (err) {
    <span class=hljs-comment>// 错误处理从略</span>
}

<span class=hljs-keyword>while</span> (err >= <span class=hljs-number>0</span>) {
    err = <span class=hljs-built_in>avcodec_receive_packet</span>(context, packet);
    <span class=hljs-keyword>if</span> (err == <span class=hljs-built_in>AVERROR</span>(EAGAIN) || err == AVERROR_EOF) {
        <span class=hljs-comment>// 继续编码下一帧</span>
        <span class=hljs-keyword>break</span>;
    }

    <span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
        spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to get packet from encoder: {}"</span>, err);
        <span class=hljs-keyword>break</span>;
    }

    <span class=hljs-built_in>fwrite</span>(packet->data, <span class=hljs-number>1</span>, packet->size, fp);
    <span class=hljs-comment>// 包数据操作完成，释放包内的数据引用</span>
    <span class=hljs-built_in>av_packet_unref</span>(packet);
}
</code></pre><p>然后关闭文件和各个使用到的数据结构：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>fclose</span>(fp);
<span class=hljs-built_in>avcodec_free_context</span>(&context);
<span class=hljs-built_in>av_frame_free</span>(&frame);
<span class=hljs-built_in>av_packet_free</span>(&packet);
</code></pre><h2>成果展示……？</h2><p>真是绕了好大一圈啊！但你可能发现，我们输出的文件名并不是 <code class=prettyprint>output.mp4</code>, 而是 <code class=prettyprint>output.h264</code>. 输出的文件似乎也没法直接双击播放。如果使用 <code class=prettyprint>file</code> 命令查看文件类型的话，会发现它并不是我们熟悉的文件格式：</p><pre><code class="hljs language-text">output.h264: JVT NAL sequence, H.264 video @ L 30
</code></pre><p>通过 <code class=prettyprint>ffplay</code> 程序，我们还是可以把图像放出来的。执行 <code class=prettyprint>ffplay output.h264</code>, 可以看到形似图 1 的画面：</p><p><a href=https://imgchr.com/i/pm9T0AJ><img alt=output.h264.png src=https://s41.ax1x.com/2026/05/24/pm9T0AJ.png></a></p><p>图 1 output.h264</p><p>但是 <code class=prettyprint>ffplay</code> 输出的是 25 帧，和我们指定的 60 帧并不一样。</p><p>要解决这个问题，我们需要将编码器的输出灌到一个视频容器中。</p><h2>创建容器对象</h2><p>在启动编码器之前，我们需要先准备好操作视频容器所需要的数据结构。</p><p>这里我们使用 Matroska 容器作为视频容器对象：</p><pre><code class="hljs language-cpp">AVFormatContext* format = <span class=hljs-literal>nullptr</span>;
err = <span class=hljs-built_in>avformat_alloc_output_context2</span>(&format, <span class=hljs-literal>nullptr</span>, <span class=hljs-string>"matroska"</span>, <span class=hljs-literal>nullptr</span>);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to allocate output context: {}"</span>, err);
    <span class=hljs-comment>// ...</span>
    <span class=hljs-keyword>return</span> <span class=hljs-number>-10</span>;
}
</code></pre><p>一个视频容器中，各个组成部分的数据由对应的流表示。我们创建一个流用于承载视频流信息：</p><pre><code class="hljs language-cpp"><span class=hljs-keyword>auto</span>* stream = <span class=hljs-built_in>avformat_new_stream</span>(format, <span class=hljs-literal>nullptr</span>);
<span class=hljs-keyword>if</span> (!stream) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to allocate stream"</span>);
    <span class=hljs-comment>// ...</span>
    <span class=hljs-keyword>return</span> <span class=hljs-number>-11</span>;
}

stream->id = <span class=hljs-built_in>static_cast</span>&lt;<span class=hljs-type>int</span>>(format->nb_streams - <span class=hljs-number>1</span>);
</code></pre><p>有了视频容器和流之后，我们就可以先打开文件了：</p><pre><code class="hljs language-cpp">err = <span class=hljs-built_in>avio_open</span>(&format->pb, <span class=hljs-string>"output.mkv"</span>, AVIO_FLAG_WRITE);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to open output file: {}"</span>, err);
    <span class=hljs-comment>// ...</span>
    <span class=hljs-keyword>return</span> <span class=hljs-number>-12</span>;
}
</code></pre><p>由于 Matroska 需要从编码器中读取数据，我们需要给编码器头设置 <code class=prettyprint>AV_CODEC_FLAG_GLOBAL_HEADER</code> 以便元数据可可以取到：</p><pre><code class="hljs language-cpp">context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
err = <span class=hljs-built_in>avcodec_open2</span>(context, codec, <span class=hljs-literal>nullptr</span>);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to open the codec: {}"</span>, err);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-4</span>;
}
</code></pre><p>启动编码器后，我们将编码器信息复制给流，以便它能正确地写出对应的元信息：</p><pre><code class="hljs language-cpp">err = <span class=hljs-built_in>avcodec_parameters_from_context</span>(stream->codecpar, context);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to copy codec context: {}"</span>, err);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-13</span>;
}
</code></pre><p>完成上述设置之后，即可先写出视频容器的头信息：</p><pre><code class="hljs language-cpp">err = <span class=hljs-built_in>avformat_write_header</span>(format, <span class=hljs-literal>nullptr</span>);
<span class=hljs-keyword>if</span> (err &lt; <span class=hljs-number>0</span>) {
    spdlog::<span class=hljs-built_in>error</span>(<span class=hljs-string>"failed to write header: {}"</span>, err);
    <span class=hljs-keyword>return</span> <span class=hljs-number>-14</span>;
}
</code></pre><p>在编码循环和结尾中，写文件操作现在由写流替换：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>av_packet_rescale_ts</span>(packet, context->time_base, stream->time_base);
packet->stream_index = stream->index;
<span class=hljs-built_in>av_interleaved_write_frame</span>(format, packet);
<span class=hljs-built_in>av_packet_unref</span>(packet);
</code></pre><p>结尾也要让视频容器写出尾部信息：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>av_write_trailer</span>(format);
<span class=hljs-built_in>avformat_flush</span>(format);
</code></pre><p>并加入对应数据结构的清理：</p><pre><code class="hljs language-cpp"><span class=hljs-built_in>avio_close</span>(format->pb);
<span class=hljs-built_in>avformat_free_context</span>(format);
</code></pre><p>完整的代码可以在<a href=https://gist.github.com/dousha/34b1b21cfbb0df5dd5b83f41009f65e7>这里</a>获取。</p><h2>成果展示！</h2><p>这回生成的 <code class=prettyprint>output.mkv</code> 文件终于是可以正常播放了：</p><p><a href=https://imgchr.com/i/pm97saQ><img alt=output.mkv.png src=https://s41.ax1x.com/2026/05/24/pm97saQ.png></a></p><p>图 2 output.mkv</p><h2>更多的问题</h2><p>你可能注意控制台会输出一行并不来自我们的警告：</p><pre><code class="hljs language-text">[h264_videotoolbox @ 0x715010000] Color range not set for yuv420p. Using MPEG range.
</code></pre><p>以及，我们如果用程序生成图像的话，一般使用的是 RGB 格式；但现在写视频帧却需要使用 YUV420 格式。这个格式转换需要我们手动进行么？</p><p>今天已经烧够好多个三分钟了，这些问题之后再考虑吧！</p>]]></description>
<pubDate>Sun, 24 May 2026 11:06:47 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-02-priming-the-codec.html</guid>
</item>

    
        <item>
<title>PreFound:01 - 新建文件夹</title>
<link>/archives/prefound-01-wanna-be-starting-something.html</link>
<description><![CDATA[<p>开始千里之行的第一个三分钟。</p><p><div class=more></div><p>为了省去自己手动装各种环境的麻烦，我决定使用 CMake + <a href=https://vcpkg.io/en/index.html><code class=prettyprint>vcpkg</code></a> 组合拳。</p><p>注意：默认情况下，<code class=prettyprint>vcpkg</code> 会尽可能将库静态链接到你的程序中，除非这个库显式地不支持这么做（比如 Qt）。如果你的程序不计划开源，那么则需要首先禁用掉 FFmpeg 中的所有 GPL 库（这包括 <code class=prettyprint>h264</code> 和 <code class=prettyprint>opus</code> 等常用模块），然后要求 <code class=prettyprint>vcpkg</code> 切换到动态链接模式。由于 <code class=prettyprint>vcpkg</code> 的整体设计，单独给 FFmpeg 指定动态链接是非常困难的，所以你的其他库也会变成动态链接模式。如果你是程序写了一半才开始引入这个库，那么这么做可能炸掉你之前写的程序。如果你有闭源的需求，则最好不要通过 <code class=prettyprint>vcpkg</code> 调用 FFmpeg.</p><p>简单地起一个 <code class=prettyprint>vcpkg.json</code> 文件：</p><pre><code class="hljs language-json"><span class=hljs-comment>/* in file vcpkg.json */</span>
<span class=hljs-punctuation>{</span>
  <span class=hljs-attr>"name"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"foundation"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"version"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"0.0.1"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"dependencies"</span><span class=hljs-punctuation>:</span> <span class=hljs-punctuation>[</span>
    <span class=hljs-string>"fmt"</span><span class=hljs-punctuation>,</span>
    <span class=hljs-string>"spdlog"</span><span class=hljs-punctuation>,</span>
    <span class=hljs-string>"ffmpeg"</span><span class=hljs-punctuation>,</span>
    <span class=hljs-string>"ffmpeg-bin2c"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-punctuation>]</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"builtin-baseline"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"2b65c20fc66eda893aa15a15a453c3cf09500b19"</span>
<span class=hljs-punctuation>}</span>
</code></pre><p>其中 <code class=prettyprint>fmt</code> 和 <code class=prettyprint>spdlog</code> 是日志库，如果你需要使用其他日志库的话可以不加这个，同时修改 CMake 和代码中关于 <code class=prettyprint>spdlog</code> 日志库的调用即可。</p><p>然后配置 CMake 使用 <code class=prettyprint>vcpkg</code> 管理依赖：</p><pre><code class="hljs language-json"><span class=hljs-comment>/* in file CMakePresets.json */</span>
<span class=hljs-punctuation>{</span>
  <span class=hljs-attr>"version"</span><span class=hljs-punctuation>:</span> <span class=hljs-number>4</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"configurePresets"</span><span class=hljs-punctuation>:</span> <span class=hljs-punctuation>[</span>
    <span class=hljs-punctuation>{</span>
      <span class=hljs-attr>"name"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"default"</span><span class=hljs-punctuation>,</span>
      <span class=hljs-attr>"generator"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"Ninja"</span><span class=hljs-punctuation>,</span>
      <span class=hljs-attr>"binaryDir"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"${sourceDir}/build"</span><span class=hljs-punctuation>,</span>
      <span class=hljs-attr>"cacheVariables"</span><span class=hljs-punctuation>:</span> <span class=hljs-punctuation>{</span>
        <span class=hljs-attr>"CMAKE_TOOLCHAIN_FILE"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"</span>
      <span class=hljs-punctuation>}</span>
    <span class=hljs-punctuation>}</span>
  <span class=hljs-punctuation>]</span>
<span class=hljs-punctuation>}</span>
</code></pre><p>指定项目需要使用 FFmpeg 相关的内容：</p><pre><code class="hljs language-cmake"><span class=hljs-comment># in file CMakeLists.txt</span>
<span class=hljs-keyword>cmake_minimum_required</span>(VERSION <span class=hljs-number>3.21</span>)
<span class=hljs-keyword>project</span>(Foundation LANGUAGES C CXX)

<span class=hljs-keyword>find_package</span>(spdlog CONFIG REQUIRED)
<span class=hljs-keyword>find_package</span>(FFMPEG REQUIRED)

<span class=hljs-keyword>add_executable</span>(Foundation main.cpp)
<span class=hljs-keyword>target_include_directories</span>(Foundation PRIVATE <span class=hljs-variable>${FFMPEG_INCLUDE_DIRS}</span>)
<span class=hljs-keyword>target_link_libraries</span>(Foundation PRIVATE spdlog::spdlog <span class=hljs-variable>${FFMPEG_LIBRARIES}</span>)
</code></pre><p>加载 CMake 工程时，<code class=prettyprint>vcpkg</code> 会自动下载对应的源码包、编译并提供给 CMake 使用。首次加载 CMake 工程时由于要跑整个 FFmpeg 库的编译，速度会很慢（以及由于基本上都是在从 GitHub 拉取源码，为了保证编译速度，科学上网是必要的）。后续编译成功后加载速度就会恢复正常。</p><p>我们来枚举一下当前 FFmpeg 支持的编解码器来验证安装是没问题的：</p><pre><code class="hljs language-cpp"><span class=hljs-comment>// in file main.cpp</span>
<span class=hljs-keyword>extern</span> <span class=hljs-string>"C"</span> {
<span class=hljs-meta>#<span class=hljs-keyword>include</span> <span class=hljs-string>"libavformat/avformat.h"</span></span>
<span class=hljs-meta>#<span class=hljs-keyword>include</span> <span class=hljs-string>"libavcodec/avcodec.h"</span></span>
<span class=hljs-meta>#<span class=hljs-keyword>include</span> <span class=hljs-string>"libavutil/opt.h"</span></span>
}

<span class=hljs-meta>#<span class=hljs-keyword>include</span> <span class=hljs-string>"spdlog/spdlog.h"</span></span>

<span class=hljs-function><span class=hljs-type>int</span> <span class=hljs-title>main</span><span class=hljs-params>()</span> </span>{
    <span class=hljs-type>const</span> AVCodec *codec = <span class=hljs-literal>nullptr</span>;
    <span class=hljs-type>void</span> *handle = <span class=hljs-literal>nullptr</span>;

    <span class=hljs-keyword>while</span> ((codec = <span class=hljs-built_in>av_codec_iterate</span>(&handle))) {
        spdlog::<span class=hljs-built_in>info</span>(<span class=hljs-string>"Found codec: {} ({})"</span>, codec->name, codec->long_name);
    }

    <span class=hljs-keyword>return</span> <span class=hljs-number>0</span>;
}
</code></pre><p>注意到：当引用 FFmpeg 的头文件时，必须使用 <code class=prettyprint>extern "C"</code> 包裹，否则 C++ 编译器会认为头文件里的函数是 C++ 函数（即经过名称修饰的函数），链接时会报找不到符号错误。</p><p>程序编译运行后，会输出所有 FFmpeg 支持的编解码器格式，形如：</p><pre><code class="hljs language-text">（前略，非常多行）
[2026-05-24 16:28:03.915] [info] Found codec: vnull (null video)
[2026-05-24 16:28:03.915] [info] Found codec: anull (null audio)
</code></pre><p>观察输出，不难发现一些熟悉的身影，比如 <code class=prettyprint>mpeg4</code>, <code class=prettyprint>h264</code>, <code class=prettyprint>aac</code>, <code class=prettyprint>opus</code> 等等。不过，并不是所有的编码器都参与了编译。默认情况下，<code class=prettyprint>ffmpeg</code> 依赖项目仅编译 LGPL 或更宽松授权的编码器，而 <code class=prettyprint>h264</code> 和 <code class=prettyprint>opus</code> 编码器都是 GPL 授权的。</p><p>要启用更严格授权的编码器或者非开放授权的编码器，则需要指示 <code class=prettyprint>vcpkg</code> 打开对应的功能。比如，想要 FFmpeg 可以使用 NVENC 后端，则需要额外打开 <code class=prettyprint>nvcodec</code> 功能：</p><pre><code class="hljs language-json"><span class=hljs-comment>/* in vcpkg.json */</span>
<span class=hljs-punctuation>{</span>
  <span class=hljs-attr>"name"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"foundation"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"version"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"0.0.1"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"dependencies"</span><span class=hljs-punctuation>:</span> <span class=hljs-punctuation>[</span>
    <span class=hljs-string>"fmt"</span><span class=hljs-punctuation>,</span>
    <span class=hljs-string>"spdlog"</span><span class=hljs-punctuation>,</span>
    <span class=hljs-punctuation>{</span>
      <span class=hljs-attr>"name"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"ffmpeg"</span><span class=hljs-punctuation>,</span>
      <span class=hljs-attr>"features"</span><span class=hljs-punctuation>:</span> <span class=hljs-punctuation>[</span>
        <span class=hljs-string>"avcodec"</span><span class=hljs-punctuation>,</span>
        <span class=hljs-string>"avdevice"</span><span class=hljs-punctuation>,</span>
        <span class=hljs-string>"avfilter"</span><span class=hljs-punctuation>,</span>
        <span class=hljs-string>"avformat"</span><span class=hljs-punctuation>,</span>
        <span class=hljs-string>"swresample"</span><span class=hljs-punctuation>,</span>
        <span class=hljs-string>"swscale"</span><span class=hljs-punctuation>,</span>

        <span class=hljs-string>"nvcodec"</span>
      <span class=hljs-punctuation>]</span>
    <span class=hljs-punctuation>}</span><span class=hljs-punctuation>,</span>
    <span class=hljs-string>"ffmpeg-bin2c"</span><span class=hljs-punctuation>,</span>
  <span class=hljs-punctuation>]</span><span class=hljs-punctuation>,</span>
  <span class=hljs-attr>"builtin-baseline"</span><span class=hljs-punctuation>:</span> <span class=hljs-string>"2b65c20fc66eda893aa15a15a453c3cf09500b19"</span>
<span class=hljs-punctuation>}</span>
</code></pre>]]></description>
<pubDate>Sun, 24 May 2026 08:58:47 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-01-wanna-be-starting-something.html</guid>
</item>

    
        <item>
<title>PreFound:00 - 梦开始的地方</title>
<link>/archives/prefound-00-introduction.html</link>
<description><![CDATA[<p>当我在说「编译视频」的时候，我说的就是这个！</p><p><div class=more></div><p>「通过代码生成视频」这件事并不是没人搞过。<a href=https://github.com/3b1b/manim>Manim</a> 可以说是这个领域的标杆了。既然已经有了一个实现，为什么还要重复造轮子呢？</p><p>一方面是因为我个人不是特别喜欢使用 Python; 另一方面则是因为我想试一下实现这个想法有多困难——我知道它肯定很困难，但究竟有多困难？哪些恐怖的问题在等着我？借用费曼的话来说——</p><blockquote><p>What I cannot create, I do not understand.</p></blockquote><p>我想学会解那些已经被解决过的问题。</p><hr><p><code class=prettyprint>PresentationFoundation</code>, 或者简称 <code class=prettyprint>PreFound</code>, 是一个用于程序化生成视频的 C++ 程序。选择 C++ 并不是出于性能考虑，而是这样我可以搬起足够多的石头狠狠地往自己的脚上砸个痛快。而且，我们应该会做很多疯狂操作，而 C++ 这种既能用一些现代语法省去麻烦又足够贴近底层允许我干各种恐怖操作的语言可以说是天选了。</p><p>当然，实现一个视频编码器是另一种恐怖操作。和 Manim 一样，将帧编码成视频流的操作还是得 <a href=https://ffmpeg.org/>FFmpeg</a> 代劳。在这之上，或许可以做一些有趣的东西出来。</p>]]></description>
<pubDate>Thu, 21 May 2026 11:21:53 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/prefound-00-introduction.html</guid>
</item>

    
        <item>
<title>向着无穷远跑去</title>
<link>/archives/there-are-infinities-ahead.html</link>
<description><![CDATA[<p>之前不知在哪看到过一个非常有趣的说法：小时候的我们可以拿起画笔肆意涂鸦，觉得很快乐；但长大后的我们却连拿起画笔的勇气都没有。我们创作的能力并非因为成长而丢失，而是我们意识到：我们的创作能力并没有跟随着审美的提升而提升；在社会的规训下，我们便放弃了创作。</p><p><div class=more></div><h2>「全才」的阴霾</h2><p>你相信一个智力基本正常的人可以学会任何事情么？</p><p>我很愿意相信这个问题的答案是「是」，或许是因为这至少能给我一点慰藉：如果你希望学会做什么事情，那么什么时候开始都不晚；以及如果真的学不会……那我都智障了你就不能让让我么？</p><p>但是「可以」和「需要」之间，又牵扯着说不清道不明的关系。如果你有学会一切的权力，那么是否意味着你也有学会一切的义务？如果答案是「是」，那么名为「全才」的阴霾便会笼罩大地。</p><p>「全才」这个名号实在是太诱人了。一个人，什么问题都可以回答，什么事情都可以做到，这不厉害么！</p><h2>「还是差点意思」</h2><p>不过人的精力终究是有限的，一个什么都想学会的人大概率最后得到的结果是什么都学不会。许多领域可以被欣赏的部分只是冰山露出海面上的小尖尖；而哪怕仅仅只是想要入门，也要深潜个几十米不可。</p><p>但没关系，一个智力基本正常的人，总归是可以学会任何事情，不是么？只要我愿意投入足够多的精力去学去练，那最终我也一定能成。</p><p>不过无论怎样努力，你会发现自己总归是「差那么点意思」。毕竟我只是一个半路启程的非科班，就算其他什么条件都一样，也没法追赶上自出生起就浸淫在行业氛围里的人：他们练的时间可比我的三分钟热度要长得多得多。</p><p>那得挨多少打才能成角啊！我又不是专业的，能不能放我一马？</p><h2>「业余」不是低水平的借口</h2><p>社会的规训，从这里便开始体现。</p><p>「业余」仅仅意味着「不以此为谋生手段」，它可不是「菜」的免死金牌。还记得开头中「审美的提升」么？提升了审美的可并不只有你一个人哦。只要动手，那么大家就都在同一个要求之下，不会因为开始的时间不同或者目标不同而有不同的评判基准。</p><p>有些时候我们会用时间来表示距离，比如半小时的车程。我看着其他人，感觉和他们差了不止十年或者二十年的距离。而我还有几个十年可以用来追赶其他人？可能是一点都不剩了吧。</p><p>这么一算，每个方向，都是无穷远。</p><h2>「我总得会点什么，不然就不会有人喜欢我」</h2><p>如果我回答「否」，认为即使是一个智力基本正常的人，也仍然会有一些领域是无法学会的，我能不能就此放过自己？</p><p>就当是一种奇怪的想法吧！本来理工科的成绩就不怎么样，再没点人文素养，人生是否就显得有些失败了？</p><p>也许应该会一种乐器；也许应该会画画；也许应该会摄影；也许应该会剪视频；也许——</p><p>诚实地来讲，人不会真的去做完全没理由的事情。再大公无私的人，总归是有一些私心。而我的私心很简单：我只想你能喜欢我。也许我会弹奏乐器，就会有人能喜欢我；也许我可以画画，就会有人能喜欢我；也许我会摄影……</p><p>从事人文社科工作的人会更容易被「喜欢」么？我只知道从事理工工作的人大部分时间都是无名的牛马。</p><h2>AI 不是我的救世主</h2><p>你可能会问：现在已经有 AI 了，为什么不直接用 AI 做你需要的东西呢？</p><p>先按下 AI 创作背后所蕴含的版权纷争不表。我个人并不看好 AI 创作，更确切地说，我认为 AI 是个人能力的催化剂，它只能给你做乘法，没法给你做加法。</p><p>我接受用 AI 编程是因为我会编程，AI 只是帮我把字码出来，如果它犯错我能够察觉并纠偏；而我并不会作曲，也不会画画，AI 生成的内容即使有这样那样的缺陷，我也很难给出合适的修改方向。</p><p>而更头大的事情是，就算我想改，也很难上手去改：现有的 AI 创作工具几乎都是直接文字到产物，没有中间过程；曲没有谱，图没有层，视频没有工程。以 AI 编程类比的话，就是直接从提示词到可执行文件，中间不输出源码，想要修改就只能修改提示词然后再次抽奖。</p><p>况且，AI 编程至少还能产出一些我之前没有意识到的新手法，或者告诉我这个东西业界的通行方案是怎样的，我还可以继续从中学习；AI 创作就只是无助地看着这个黑盒吐出一个又一个的怪东西。也许对于大多数用户来说，他们的需求确实就是「一句话给我输出能直接用的玩意」，但对于我来说这绝非够用。</p><h2>我手写我心</h2><p>你有 AI 创作羞耻么？</p><p>我已经用 AI 直接莽了一堆公司的项目了，真的就是指标书写好一遍过，端到端测试跑通就是大成功。我不觉得这种事有什么羞耻不羞耻的。AI 要用，大方地用，狠狠地用，自行车站起来蹬到冒烟！</p><p>但那毕竟是公司项目，是需要追寻短平快的商业物件，是一个没有什么特征的、标准化的、流水线上的产物。简单的 CRUD 的东西，换谁来写都一样，尽早写完尽早解放。</p><p>但是博客文章，直到现在，我还只是用 Grammarly 这类工具来修一下英语的语法问题；其他的内容都是湿件直出的。</p><p>我并不是认为 AI 没法写出我要写的内容。我当然相信如果我斥巨资买两张高端显卡，召唤一下<a href=https://modelscope.cn/models/Qwen/Qwen3.5-397B-A17B>千问 3.5 满血版</a>来阅读我的每一篇文章，它便能用我的行文习惯来写更多的内容。它会比我写得更快，调研资料更完整，也会更少出现一些词不达意的情况。</p><p>但这不又成为了另外一种形式的「代写」么？在这个<a href=/archives/3350.html>并不追求什么利益</a>的博客上，要再搞一个代写未免也太破坏自我陶醉的气氛了。我的作品终究应该体现的是我的想法、我的意志，以及我的缺憾。</p><p>有些时候我会怀疑，创作欣赏的不是成品本身，而是成品所携带的独属于创作者本身的缺憾。</p><h2>向着无穷远跑去</h2><p>三分钟热度，就有三分钟的改进。只要我还能拿出三分钟的精神，那么便要继续再走三分钟的路。</p>]]></description>
<pubDate>Sun, 17 May 2026 16:08:25 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/there-are-infinities-ahead.html</guid>
</item>

    
        <item>
<title>如何打包一个 Python 项目</title>
<link>/archives/the-python-packing-pains.html</link>
<description><![CDATA[<p>In Python, "There should be one-- and preferably only one --obvious way to do it." When the Way does not align with yours, welp.</p><p><div class=more></div><p>从其他开发框架迁移过来，使用如下的项目目录结构应该还是蛮常见的：</p><pre><code class="hljs language-text">project/
|_ src/   - 源码
|_ test/  - 测试
|_ ...    - 项目的其他配置文件
</code></pre><p>但这样的一个代码组织会带来一些反直觉的问题。当我们建立了两个文件，需要相互引用的时候，这个代码应该怎么写呢？</p><pre><code class="hljs language-text">project/
|_ src/
   |_ __init__.py
   |_ module.py
   |_ main.py
</code></pre><pre><code class="hljs language-python"><span class=hljs-comment># in src/module.py</span>

<span class=hljs-keyword>def</span> <span class="hljs-title function_">hello</span>():
    <span class=hljs-built_in>print</span>(<span class=hljs-string>'Hello'</span>)
</code></pre><p>直觉上，可以这么写：</p><pre><code class="hljs language-python"><span class=hljs-comment># in src/main.py</span>
<span class=hljs-keyword>from</span> module <span class=hljs-keyword>import</span> hello

<span class=hljs-keyword>if</span> __name__ == <span class=hljs-string>'__main__'</span>:
    hello()
</code></pre><p>然后如果真的运行的话，它也能用：</p><pre><code class="hljs language-text">python src/main.py

Hello
</code></pre><p>但是如果你恰好装了一个名字叫 <code class=prettyprint>module</code> 的包的话，这个玩意就崩掉了：</p><pre><code class="hljs language-text">python src/main.py

Traceback (most recent call last):
  File "/.../src/main.py", line 1, in &lt;module>
    from module import hello
ImportError: cannot import name 'hello' from 'module' (/.../site-packages/module/__init__.py)
</code></pre><p>好在 Python 可以指定从相对位置引入：</p><pre><code class="hljs language-python"><span class=hljs-comment># in src/main.py</span>
<span class=hljs-keyword>from</span> .module <span class=hljs-keyword>import</span> hello
</code></pre><p>但真这么写的话，你就会发现直接调脚本调不动了：</p><pre><code class="hljs language-text">python src/main.py

Traceback (most recent call last):
  File "/.../src/main.py", line 1, in &lt;module>
    from .module import hello
ImportError: attempted relative import with no known parent package
</code></pre><p>需要按照模块的方式来调用才行：</p><pre><code class="hljs language-text">python -m src.main

Hello
</code></pre><p>简单研究了一下，为了让这两种调用方式都可行，需要写成这样的一个模式：</p><pre><code class="hljs language-python"><span class=hljs-comment># in src/main.py</span>
<span class=hljs-keyword>from</span> src.module <span class=hljs-keyword>import</span> hello
</code></pre><p>在你完成脚本调试之后，可能会希望把它打包成一个命令行工具。简单用 <code class=prettyprint>setuptools</code> 起一个打包过程即可：</p><pre><code class="hljs language-toml"><span class=hljs-comment># in pyproject.toml</span>
<span class=hljs-section>[project]</span>
<span class=hljs-attr>name</span> = <span class=hljs-string>"my-tool"</span>
<span class=hljs-attr>version</span> = <span class=hljs-string>"0.0.1"</span>
<span class=hljs-attr>dependencies</span> = [
    <span class=hljs-comment># ...</span>
]
<span class=hljs-attr>requires-python</span> = <span class=hljs-string>">= 3.11"</span>

<span class=hljs-section>[project.scripts]</span>
<span class=hljs-attr>my-tool</span> = <span class=hljs-string>'src.main:main'</span>

<span class=hljs-section>[build-system]</span>
<span class=hljs-attr>requires</span> = [<span class=hljs-string>"setuptools>=61.0"</span>]
<span class=hljs-attr>build-backend</span> = <span class=hljs-string>"setuptools.build_meta"</span>
</code></pre><p>不过，真这么打包的话，会发现 <code class=prettyprint>setuptools</code> 打包的根是在 <code class=prettyprint>src/</code> 下的。<code class=prettyprint>pip install .</code> 之后，它反而又报错了：</p><pre><code class="hljs language-text">my-tool

Traceback (most recent call last):
  File "/.../.venv/bin/my-tool", line 3, in &lt;module>
    from src.main import main
ModuleNotFoundError: No module named 'src'
</code></pre><p>如果在打包工具里去掉 <code class=prettyprint>src.</code> 的前缀呢？</p><pre><code class="hljs language-toml"><span class=hljs-comment># in pyproject.toml</span>
<span class=hljs-comment># ...</span>
<span class=hljs-section>[project.scripts]</span>
<span class=hljs-attr>my-tool</span> = <span class=hljs-string>'main:main'</span>
</code></pre><p>这样做虽然 <code class=prettyprint>my-tool</code> 脚本可以正常运行了，但是会炸掉剩下东西的引用：</p><pre><code class="hljs language-text">my-tool

Traceback (most recent call last):
  File "/.../.venv/bin/my-tool", line 3, in &lt;module>
    from main import main
  File "/.../site-packages/main.py", line 1, in &lt;module>
    from src.module import hello
ModuleNotFoundError: No module named 'src'
</code></pre><p>要让程序可以正常运行，需要告诉 <code class=prettyprint>setuptools</code> 将 <code class=prettyprint>src/</code> 作为打包目标：</p><pre><code class="hljs language-toml"><span class=hljs-comment># in pyproject.toml</span>
<span class=hljs-comment># ...</span>
<span class=hljs-section>[tool.setuptools]</span>
<span class=hljs-attr>packages</span> = [<span class=hljs-string>"src"</span>]
</code></pre><p>这样最终打出来的包才是正常可用的。</p><p>不过你可能会立刻意识到：这个包在 <code class=prettyprint>site-packages</code> 里的名字是 <code class=prettyprint>src</code>. 如果这个包要发布的话，叫这个名字肯定是不行的。所以最好的方法是采用这样的一个结构：</p><pre><code class="hljs language-text">project
|_ my_tool &lt;- 你的包名，取代原有的 src
|  |_ __init__.py
|  |_ ...
|_ test
</code></pre><p>然后对应地指定 <code class=prettyprint>setuptools</code> 使用同样的包名：</p><pre><code class="hljs language-toml"><span class=hljs-comment># in pyproject.toml</span>
<span class=hljs-comment># ...</span>
<span class=hljs-section>[project.scripts]</span>
<span class=hljs-attr>my-tool</span> = <span class=hljs-string>"my_tool.main:main"</span>

<span class=hljs-section>[tool.setuptools]</span>
<span class=hljs-attr>packages</span> = [<span class=hljs-string>"my_tool"</span>]
</code></pre>]]></description>
<pubDate>Sun, 26 Apr 2026 14:10:53 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/the-python-packing-pains.html</guid>
</item>

    
        <item>
<title>只能作为光盘手册存在的书籍</title>
<link>/archives/books-that-are-manuals-of-a-disc.html</link>
<description><![CDATA[<p>最近（一年之内）我购买了《<a href="https://item.taobao.com/item.htm?id=666167202310&mi_id=0000LUCXW7Fk58zQIyDk9w7L_aavyEgmCXBakxJI6z3tnn4&skuId=5940363294108">来自中年玩家的十二封信</a>》，哦不，是《游戏人讲解73》这张光盘。</p><p><div class=more></div><p>当然，我在这里并不是要讨论这本书——抱歉，这张光碟本身的内容；我更感兴趣的是怎么给这份材料写一份引用。</p><p>我明白这是发行的一种妥协，尽管书本身的内容理论上没有任何不妥，但靠着既有的音像公司出版，以光盘加附带手册的形式发行这份资料，似乎比单出一本书要容易或者要便宜。</p><p>但这么做却打了我一个措手不及：和其他带着完整信息的资料不同，我没有办法只凭手头的东西给这样的数据添加引用条目。<a href=/pages/4633>引用格式</a>中一眼能够确定的就是 <code class=prettyprint>V/CD</code> 的作品类型信息和「广东嘉应音像出版有限公司」的出版方信息，剩下的都很难办。</p><p>首先需要确定这张光盘的作者是谁。打开<a href=https://pdc.capub.cn/>国家版本数据中心</a>，输入光盘上的 ISBN 号码查询，得到的结果是：</p><table><thead><tr><th>键</th><th>值</th></tr></thead><tbody><tr><td>类型</td><td>音像电子</td></tr><tr><td>选题名称</td><td>游戏人讲解73</td></tr><tr><td>ISBN</td><td>978-7-88553-357-1</td></tr><tr><td>出版单位</td><td>广东嘉应音像出版有限公司</td></tr><tr><td>著作权人</td><td></td></tr></tbody></table><p>这可不是我忘填了著作权人，而是它确实是空白。那么根据规则，这份数据「来自某个实体，如公司、政府或其他团体，使用实体名称」，得到的结果是 <code class=prettyprint>UCG</code>. 我们同时也拿到了这份光盘的正式名称：《游戏人讲解73》。</p><p>接下来是确定版本信息。光盘本身和「手册」上都没有任何出版时间相关的信息。好在光盘内容的开场词中，提到了这是 UCG 2022 年的开年之作，那么就只能精确到年了。</p><p>由于之前从未引用过离线的音视频文件，定位信息需要打一个补丁。目前决定通过时间戳来作为定位信息，毕竟书是这样定位的，影音光盘这样定位应该问题也不大。</p><p>对于附赠「手册」的引用信息相对而言就好填一些，至少作者名称那一栏除了 <code class=prettyprint>UCG</code> 以外，还可以写一下是哪十二位同志撰的稿；而且作品名称也可以直接写成<code class=prettyprint>来自中年玩家的十二封信</code>。出版信息、版本信息和 ISBN 号则继承自光盘本身；定位信息则沿袭之前的格式放页码即可。</p><hr><p>之后我们或许会需要引用更为复杂的数据，比如游戏软件本身。尽管现在的引用格式允许将其作为「可执行过程」引用，但是我们能接受「无版本信息」以及「非公开获取资料」么？数字发行的内容或许可以按在线数据整理，那卡带又该怎么办呢？</p><p>希望之后我们能想出来一个好的答案吧。</p>]]></description>
<pubDate>Mon, 06 Apr 2026 17:48:22 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/books-that-are-manuals-of-a-disc.html</guid>
</item>

    
        <item>
<title>航天飞机和马屁股的关系</title>
<link>/archives/the-space-shuttle-and-a-horses-arse.html</link>
<description><![CDATA[<p>这篇文章是由 Art 发表在 Physics Forums 的一篇帖子的翻译，其内容因个人恶趣味与原文有所出入。如需引用或考证请<a href=https://www.physicsforums.com/threads/the-link-between-a-horses-arse-and-the-space-shuttle.162604/>查看原文</a>。当然，原文本身其实没必要花心思去考证，因为<a href=https://www.snopes.com/fact-check/railroad-gauge-chariots/>它其实经不起推敲</a>，大伙看一乐就好。</p><hr><p>你是否听过这句话：“我们一直就是这么办事的”？</p><p>美国国标指定铁轨轨距为 4 英尺又 8.5 英寸。这个数为什么有零有整的？因为英国的铁路标准就是这样的。</p><p>那为什么英国铁路又选了这么一个奇怪的数呢？因为建造铁路的这批人之前是建有轨电车的，所以他们就直接沿袭了有轨电车的标准。</p><p>那有轨电车的标准又是从哪来的呢？答案是建造有轨电车的人使用了造马车的工具，而马车的轮距就是这么宽。</p><p>那马车的轮距又是怎么得出的呢？马车的轮距受制于铺设的车辙的间距。如果马车的轮距和车辙对不上的话，轮子就很容易被压坏，或者干脆就无法通行。</p><p><a href=https://imgchr.com/i/peCoTQs><img alt=peCoTQs.jpg src=https://s41.ax1x.com/2026/03/06/peCoTQs.jpg></a></p><p>图 1 铺设的车辙<sup class=footnote-ref><a href=#fn1 id=fnref1>[1]</a></sup></p><p>那么，这路又是谁铺的呢？是罗马帝国铺设的。罗马帝国是首个在欧洲（以及英格兰）建设了长距离公路的政权，这些公路铺好之后就一直在用；而其车辙标准则承袭自罗马战车的轮距。为了能在公路上通行，所有人就纷纷效仿采用了相同的轮距。</p><p>所以说，美国铁路的 4 英尺又 8.5 英寸的规矩来自于罗马帝国的战车轮距的尺寸。</p><p>当你拿到一份标准，纳闷这狗屁标准是那个马屁精拍脑袋的馊主意的时候，你可能一点没错——因为罗马战车的车辙宽度就是为了能正好塞下两个马屁股定下的！</p><p>哦对了，还有大标题的事情。航天飞机在发射台上准备发射的时候，你会看到有两个大火箭连接在主油箱的两侧。这两个大火箭叫固体火箭推进器 (SRB).</p><p>这些推进器是在犹他州的 Thiokol 公司制造的。设计这个推进器的工程师当然希望能把它做得更大一些以便携带更多燃料，但是由于推进器需要通过铁路运输，而隧道的大小又是照着铁路的宽度挖的，所以这个推进器就不得不按照铁路的承载要求进行设计——也就是约摸两个马屁股的宽度。</p><p>所以说，航天飞机——这个可以称得上是世界上最先进的交通系统——它的主要设计指标受制于两千年前的一匹马的屁股有多宽。</p><hr class=footnotes-sep><section class=footnotes><ol class=footnotes-list><li class=footnote-item id=fn1><p>By Dvortygirl - Own work, CC BY-SA 4.0, <a href="https://commons.wikimedia.org/w/index.php?curid=3490257">https://commons.wikimedia.org/w/index.php?curid=3490257</a> <a class=footnote-backref href=#fnref1>↩︎</a></p></li></ol></section>]]></description>
<pubDate>Sat, 28 Mar 2026 10:34:30 +0000</pubDate>
<dc:creator>Art (www.physicsforums.com)</dc:creator>
<guid>/archives/the-space-shuttle-and-a-horses-arse.html</guid>
</item>

    
        <item>
<title>Get an iPod</title>
<link>/archives/get-an-ipod.html</link>
<description><![CDATA[<p>关于一种可爱，一种怀旧，一种对小时候的自己的补偿，和一次对自己数字生活的质询。</p><p><div class=more></div><p>最近心血来潮，在某鱼上面买了一颗 iPod nano 6. 一方面是因为果子全家桶我现在算是集齐一整套了，所以新的电子小垃圾还是选择苹果是一个几乎自然的事情——这个「不用苹果」的 flag 现在是狠狠地被扯了下来；另一方面是两张毛爷爷这个价位，很难买到其他看起来做工精良而且音质合格的产品了。我个人比较喜欢小而轻薄形态的电子产品，而目前市面上其他的播放器要么是一块砖头，要么是看起来就让人感觉遗憾的形制。</p><p><a href=https://imgchr.com/i/pZY90Cn><img alt=ipod-nano-6.jpg src=https://s41.ax1x.com/2025/12/27/pZY90Cn.jpg></a></p><p>iPod nano 6 是整个 iPod 系列中尺寸最小的，也是唯二没有转轮的。没有转轮的 iPod 还能算是 iPod 么？我不知道。但我知道的是这个尺寸、这个形态、这个做工在我看来简直是完美！它几乎可以用「可爱」来形容。要知道，很少有电子产品可以是「可爱」而不「矫揉造作」的。</p><p>也许我也不可避免地陷入到了玫瑰色的过去当中，我的审美还停留在乔布斯时代的苹果，还停留在那个许下了美好未来诺言的时代中。当我把玩着 iOS 4 风格的界面的时候，听着耳机里播着的 Owl City 的 Firefiles, 我感觉自己又回到了 2010 年。</p><blockquote><p>I'd like to make myself believe<br> That planet Earth turns slowly</p></blockquote><p>和那时候的孩子们一样，我也曾经想过拥有一台 iPhone, 或者 iPod touch. 但也和那时候的大部分孩子一样，这个想法最终是停留在了想法阶段，因为这东西真不是靠饿几天就能从饭钱里扣出来的，而我的学业表现也还没能好到让父母愿意花费几个月工资搞一个「对学习一点帮助都没有」又死贵的玩意回来。</p><p>或许这种对于小形制的偏爱来自于那个小小的我。对于那个还在初中摸爬滚打的我，也许一部 iTouch 可以把我从受欺负的泥潭里拯救出来——或者陷入得更深。但现在的我总算是有能力给自己拿下这个想了十几年的潮品，这又何尝不是一种「延迟满足」。</p><p>但说了这么多，有一个关键问题仍然需要解释：马上都 6202 年了，买一个什么功能都没有只能放音乐的 MP3 播放器，是否也是一种罩着怀旧的面纱、打着童年的借口的消费主义作祟？</p><p>和 iPod touch 不同，iPod nano 有且只有一个主要功能：放音乐。它没有办法装任何软件进去，而且 6 代 nano 也没有视频播放能力。它也没有办法联网，没有 WiFi 更不能插卡，所以你如果需要传音乐进去，只能把它插到电脑上，然后用 iTunes 推数据进去；是的，你还必须用 iTunes, 它还不能直接拖文件进去。就连放音乐，它对音频格式的支持也很有限：AAC, ALAC, MP3, WAV. 是的，FLAC 是不支持的，你还得手动把 FLAC 转换成 ALAC 才行。智能推荐之类的高级功能自然更是没有。音质来说，iPod nano 也绝非什么 HiFi 播放器，它的耳机孔的驱动能力最多只能带动 32 欧姆的耳机。也许唯一的救赎也就是它的 24 小时连续播放的长续航，但长续航本身很多也要归功于缺少射频组件和外放，换其他更便宜的播放器也能实现。</p><p>按照现在的标准来说，这就是教科书级的「电子垃圾」。两百块干点什么不好？</p><p>我最近开始重新购买 CD.</p><p>是的，购买了 CD 意味着我需要买光驱，然后需要自己换碟或者自己把光盘里的数据拷出来，如果想要通过网络放音乐的话还得自己架设私有的音频流媒体服务器，自己想办法打洞，还没有海量曲库能随意切换，也没有智能推荐之类的功能。隔壁玩黑胶的哪怕家里连唱机都没有都还能显摆一下自己「走在时代前沿」，当一把潮哥潮姐；而 CD 甚至连这种社交属性都没有。一张正版碟花大几十块钱，一张海淘碟更是上百，买两三张就够你订一年的流媒体会员了，何必呢？</p><blockquote><p>Does one need to smell flowers more quickly, more efficiently?</p><p>-- MecklesFrog, <a href="https://www.youtube.com/watch?v=a8iOJkqYNpc">The Importance of Inconvenience</a></p></blockquote><p>选择 CD 并不是因为 Spotify 之流不香，而是因为我觉得，有些时候真正的「拥有」我喜爱的内容是一个更好的选择；而且我实在是信不过「购买数字单曲」这种事情——毕竟如果我购买了这个单曲，但它还是锁在一层 DRM 后面，没有密钥的我，不还是等于从平台无限期地赁了这首歌出来么？现在可没有多少平台再提供无 DRM 版本的无损质量音乐购买了。</p><p>购买了 CD 的我，可以把它塞到任何一台电脑的光驱里，然后就能直接播放<sup class=footnote-ref><a href=#fn1 id=fnref1>[1]</a></sup>。我不需要登录什么账号，也不需要从谁那获取密钥，也不需要下载特定的播放器。更有意思的是，我可以直接把光盘内容无损地复制到我的电脑里，再复制到我的其他播放器或者刻录到其他光盘中。只要我不和其他人分享复制的内容，这么做是完全合情合理合法的。不会有一天，因为平台没有续期版权，或者艺人和平台闹掰，或者更干脆的被封杀，我就再也听不到这首歌了。</p><p>iPod, 或者说任何一个纯粹的音乐播放器，也正好契合了这种「朴素的拥有」的概念。更确切地说，这种纯粹的播放器带来的是一种始终如一的态度：你所喜爱的东西不会随着外界的变化而变化，这首歌无论何时都是一样的好听。而现在的我们正缺这种对于稳定的许诺。</p><hr class=footnotes-sep><section class=footnotes><ol class=footnotes-list><li class=footnote-item id=fn1><p>除了某索的部分型号碟片，但某索的这种操作严格意义上是不遵循 CD 标准的，所以不算数。 <a class=footnote-backref href=#fnref1>↩︎</a></p></li></ol></section>]]></description>
<pubDate>Sun, 28 Dec 2025 09:22:02 +0000</pubDate>
<dc:creator>dousha99</dc:creator>
<guid>/archives/get-an-ipod.html</guid>
</item>

    </channel>
</rss>
