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

推荐订阅源

Spread Privacy
Spread Privacy
大猫的无限游戏
大猫的无限游戏
F
Fortinet All Blogs
M
MIT News - Artificial intelligence
G
Google Developers Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Help Net Security
V
Visual Studio Blog
WordPress大学
WordPress大学
博客园 - 司徒正美
TaoSecurity Blog
TaoSecurity Blog
Webroot Blog
Webroot Blog
Hugging Face - Blog
Hugging Face - Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
W
WeLiveSecurity
C
CERT Recently Published Vulnerability Notes
Y
Y Combinator Blog
S
Schneier on Security
Recent Announcements
Recent Announcements
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
宝玉的分享
宝玉的分享
T
Troy Hunt's Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 三生石上(FineUI控件)
Microsoft Azure Blog
Microsoft Azure Blog
N
News and Events Feed by Topic
A
About on SuperTechFans
小众软件
小众软件
K
Kaspersky official blog
Help Net Security
Help Net Security
V2EX - 技术
V2EX - 技术
P
Proofpoint News Feed
S
Secure Thoughts
IT之家
IT之家
云风的 BLOG
云风的 BLOG
N
Netflix TechBlog - Medium
The Register - Security
The Register - Security
I
Intezer
NISL@THU
NISL@THU
GbyAI
GbyAI
P
Privacy & Cybersecurity Law Blog
T
Threatpost
C
Check Point Blog
Forbes - Security
Forbes - Security
C
Cisco Blogs
AI
AI
Recorded Future
Recorded Future
F
Full Disclosure

豆沙工作室

从 Gitea 迁移到 Forgejo 评论里的私信消息 记得清理你的日志文件 招魂 Annihilation [11ty] 使用 Pagefind 实现静态站点的搜索 直面死亡 [11ty] 修复 RSS 里的数学公式 看到了一个神奇的 UA PreFound:10B - 四次击键 一点小小的 AI 震撼 PreFound:10A - 文本之外,还有(绘)文字 PreFound:09 - 色彩运算子 计算机不予申辩 Re-Game 2.0 Addendum C - Press START PreFound:08 - 动效与仿射 PreFound:07 - 图片拼贴报 PreFound:05 - 写出到屏幕 And Trek I Will PreFound:04 - 举起渲染器 PreFound:03 - 跨越比特面 PreFound:02 - 启动编码器 PreFound:01 - 新建文件夹 PreFound:00 - 梦开始的地方 向着无穷远跑去 如何打包一个 Python 项目 只能作为光盘手册存在的书籍 航天飞机和马屁股的关系 Get an iPod 从鸿蒙系统中拉取蓝牙 HCI 日志 第二类擦除 迁移 FreshRSS 的数据库
PreFound:06 - 漫游雪花海
https://archive.dsstudio.tech/person/ · 2026-05-25 · via 豆沙工作室

我们来试试编码器的能力上限吧!

上期决定做点会动的东西。单纯地移动色块之类的当然是要做,不过首先,我决定先做一些比较不正经的探索部件——毕竟,用代码生成视频的一个主要玩法就是程序化生成;而没有什么是比一个「随机」更适合作为基线的生成器了。

整理框架

继续用一个大的 main 函数处理一切也不是不行,但既然用了 C++, 合适的代码架构还是应该设计一下的。

简单来说,我们会将所有 SDL 相关的初始化放到一个类中,叫做 SdlContext; 所有 FFmpeg 相关的初始化和操作放到一个类中,叫做 FFmpegContext. 这两个类在构造时设置所有必要的内容,析构时清理所有用到的数据结构。

需要用到的关键配置,比如视频的宽高、帧率、码率、文件信息,整理成一个单独的结构体 VideoSettings.

这样,我们只需要把它放到全局上就好啦:

// in main.cpp, global scope
static VideoSettings videoSettings {
    .width = 640,
    .height = 480,
    .fps = 60,
    .bitrate = 10000000,
    .filename = "output.mkv"
};
static FFmpegContext ff{videoSettings};
static SdlContext sdl{videoSettings};

要提交一帧数据,只需要调用 ff.encode_rgb24_frame(.). 要获取 SDL 渲染好的 buffer, 只需要调用 sdl.get_render_output(),即:

// in main.cpp, main function
for (int i = 0; i < TOTAL_FRAME_COUNT; i++) {
    sdl.begin_frame();
    /* SDL 渲染操作 */
    sdl.end_frame();
    ff.encode_rgb24_frame(sdl.get_renderer_output());
}

SDL 渲染操作中,由各个生成器操作 SDL 的渲染目标材质 sdl.texture() 即可。我们也可以计算一下当前帧的时间 frameTime = static_cast<float>(i) / static_cast<float>(videoSettings.fps) 传递给生成器作为参考。

生成雪花帧

雪花帧的生成意外地难。

我们首先考虑一个朴素的实现:

for (int y = 0; y < videoSettings.height; y++) {
    for (int x = 0; x < videoSettings.width; x++) {
        const uint8_t r = next_random();
        const uint8_t g = next_random();
        const uint8_t b = next_random();
        SDL_SetRenderDrawColor(_renderer, r, g, b, 255);
        SDL_RenderPoint(sdl.renderer(), x, y);
    }
}

开始运行后,很长时间都没有出现结果。

[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)

直到半分钟后——

[2026-05-25 22:42:53.597] [info] rendered 600 frames in 37.763 seconds

这回是真的撞上了性能瓶颈。如果给每个像素都生成 3 个随机数的话,每帧就需要 640×480×3=921600 640 \times 480 \times 3 = 921600 个随机字节生成;以及每次都需要做 640×480=307200 640 \times 480 = 307200 次点绘制操作。

随机数生成部分还好说,毕竟视频并不需要很强的密码学随机,用一个足够好的伪随机数生成器也能出同样的效果;但是这个点绘制是真的够呛。由于 SDL 的渲染架构,我们必须每次 roll 一个颜色然后画一个点,导致这个步骤没法很好地并行;而且这个过程的步骤数还是随着视频分辨率平方增长的。我们需要一个更为高效的方法。

答案是:预先生成一张 2048×2048 2048 \times 2048 的雪花图,然后 GPU 直接从雪花图上复制区块下来就行。

具体的,我们先创建一个 CPU 渲染的画布(因为需要直接操作底层的 buffer, 比 GPU 画一堆点更快):

auto* randomSurface = SDL_CreateSurface(2048, 2048, SDL_PIXELFORMAT_RGB24);

然后,用随机数填充画布:

SDL_LockSurface(randomSurface);
auto* canvas = static_cast<uint8_t*>(randomSurface->pixels);
for (int y = 0; y < 2048; y++) {
    for (int x = 0; x < 2048; x++) {
        for (int i = 0; i < 3; i++) {
            canvas[y * 2048 * 3 + x * 3 + i] = next_random();
        }
    }
}
SDL_UnlockSurface(randomSurface);

最后,为了能让 GPU 使用它,把它烘焙成一个材质:

auto* randomTexture = SDL_CreateTextureFromSurface(sdl.renderer(), randomSurface);

用完后,记得释放材质和 CPU 画布:

SDL_DestroyTexture(randomTexture);
SDL_DestroySurface(randomSurface);

next_random 随机数的选择

随机数本身在选择上也有讲究:必须挑选一个周期特别特别长的随机数生成器,否则生成出来的材质看起来就一点也不随机了:雪花中会出现一条条纵向的斜线(如图 1)。

pattern.png

图 1 由于周期过短形成的纹路

C++ 中内置了梅森旋转法伪随机发生器,我们可以直接使用:

static std::random_device rnd;
static std::mt19937 engine{rnd()};
static std::uniform_int_distribution<uint8_t> dist(0, 255);

static uint8_t next_random() {
    return dist(engine);
}

绘制雪花

我们按照时间的流逝,将视窗快速循环地滑过整个材质表面(如图 2),就可以实现动起来的雪花屏效果了:

zigzag.png

图 2 视窗在材质上 Z 字形滑动

const float NOISE_TRAVEL_FACTOR = 10000.f;
const int travel = static_cast<int>(frameTime * NOISE_TRAVEL_FACTOR);
const int maxX = 2048 - videoSettings.width;
const int maxY = 2048 - videoSettings.height;
const int horizontalTravel = travel % maxX;
const int verticalTravel = ((travel / maxX) * videoSettings.height) % maxY;

const SDL_FRect srcRect = {
    .x = horizontalTravel,
    .y = verticalTravel,
    .w = videoSettings.width,
    .h = videoSettings.height
};

SDL_SetRenderTarget(sdl.renderer(), sdl.texture());
SDL_RenderTexture(sdl.renderer(), randomTexture, &srcRect, nullptr);

注意到:如果我们设置的滑动速度 NOISE_TRAVEL_FACTOR 太慢的话,它看起来就会变成真的「滑动」了。

这回渲染的速度就比之前快了很多了:

[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

noise.png

图 3 样例输出

blocky-fuzz.png

图 4 注意到编码器已经无法完整地捕捉全部图像信息,进而产生了块状模糊

那... 移动色块?

即得易见平凡,仿照上例显然。留作习题答案略,读者自证不难。

const int startX = 10, startY = 10;
const int w = 50, h = 50;
const int speedX = 4, speedY = 1;
const float dX = speedX * frameTime;
const float dY = speedY * frameTime;

const SDL_FRect rect = {
    .x = startX + dX,
    .y = startY + dY,
    .w = w,
    .h = h,
};

SDL_SetRenderTarget(sdl.renderer(), sdl.texture());
SDL_SetRenderDrawColor(255, 0, 255, 0); // 当然是继续用洋红色
SDL_RenderFillRect(sdl.renderer(), &rect);

不过,即使是移动色块也是有很多说法的。现在这种线性的移动看起来非常的怪,而且看起来好像还有些卡顿。这个问题我们在之后会专题讨论。

下一步

也许我们需要考虑一些图像的加载相关的内容了。毕竟,就算是做 PPT 也得能插入图片吧!