14 / 32 Source、对象、插件与渲染

第 13 课:场景与合成

# 第 13 课:场景与合成

嘿,我是小方。 到这儿,你已经会"采集一帧"(第 10 课)、"把一个源画出来、还能加滤镜"(第 11、12 课)。可你打开任何一个直播,画面里从来不止一个源:游戏打底、右下角一个摄像头小窗、底下一行字幕、角落一个 logo…… 它们各自画好之后,是怎么按位置、大小、层级叠成最终那一幅画面的? 这就是"合成",打包它的东西叫"场景(scene)"。这一课我们拆开它,并且揭晓第 6 课埋的最后一个伏笔:场景,它自己居然也是个 source。这是模块 3 的收官。


# 13.1 背景:合成,就是把多个图层叠起来

📍接上一节:上一课我们会画一个源了,可直播画面从来不止一个源——这一节先建立"合成"的直觉:把多个图层摞起来。

先建立直觉。你最终看到的一帧,是好几个源"摞"在一起的结果:

合成的概念

合成(compositing) 这个词,在图形里就是指"把多个图层,按各自的位置、大小、透明度、上下层级,叠成最终一幅"。游戏画面铺满打底(最底层),摄像头缩小放右下角(上面一层),字幕压在下方,logo 盖在角上(最顶层)。

OBS 把"这一组源 + 每个源怎么摆"打包成一个东西,叫场景。你在 OBS 界面左下角切换的那些"场景 1""场景 2",就是它。一个场景,就是一套排布好的画面。

那"一个场景"在代码里长什么样?


# 13.2 数据模型:场景 = 一串"场景项"

📍我们在哪:有了合成的直觉,先看"一个场景"在代码里长什么样——它是一串"场景项"串成的链表。

一个场景(obs_scene)内部,是一串场景项(obs_scene_item) 串成的链表。每个场景项 = 一个源 + 它在画面里怎么摆:

场景数据模型

翻开 struct obs_scene_item(obs-scene.h:30),它记着一个源,加上一堆"怎么摆"的信息:

// libobs/obs-scene.h:30 (节选)
struct obs_scene_item {
	struct obs_scene  *parent;   // 属于哪个场景
	struct obs_source *source;   // 它包的那个源(游戏?摄像头?)
	bool user_visible;           // 界面上那个眼睛图标(用户点的显示/隐藏;下面的循环用的就是它)

	struct vec2 pos;             // 位置 (x, y)  ┐
	struct vec2 scale;           // 缩放         │ 这三样就是「怎么摆」
	float       rot;             // 旋转         ┘
	struct obs_sceneitem_crop crop;  // 裁剪

	struct matrix4 draw_transform;   // ★ 由上面几样算出来的「变换矩阵」(13.5 要用)

	struct obs_scene_item *prev;     // 链表:上一项 ┐
	struct obs_scene_item *next;     //       下一项 ┘ 顺序 = 层级!
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

抓两个重点:

  • source + pos/scale/rot —— 一个场景项,就是"哪个源"加上"它摆在哪、多大、转了没"。你在同一个摄像头源上建两个场景项,就能让它在两个场景里以不同大小出现。(这也印证了第 6 课:源是"蓝图/实例"里的实例,场景项引用的是实例。)
  • prev / next(链表) —— 场景项串成一条链,这条链的顺序,就是图层的上下层级:排在前面的先画(在底层),后面的后画(盖在上面)。你在界面里拖动来源列表调顺序,改的就是这条链。

draw_transform 那个矩阵是干嘛的?先记住它,13.5 见分晓。现在先看一个更妙的东西。


# 13.3 大揭秘:场景,自己也是个 source

📍承上启下:数据模型看完,揭第 6 课埋的最后一个伏笔——场景本身也是一个 source,这决定了后面合成为什么这么简单。

第 6 课我列 source 四类型时,说过 OBS_SOURCE_TYPE_SCENE,还卖了个关子:"场景本身也是一个 source,留到第 13 课讲。"现在揭晓 —— 而且是实锤

翻开 obs-scene.c,你会看到场景注册了一张 obs_source_info 登记表,和第 6 课任何一个源一模一样:

场景也是个 source

// libobs/obs-scene.c:1742
const struct obs_source_info scene_info = {
	.id           = "scene",
	.type         = OBS_SOURCE_TYPE_SCENE,     // ← 四类型之一
	.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_COMPOSITE,
	.get_name     = scene_getname,
	.video_render = scene_video_render,        // ← 合成就发生在这个回调里
	...
};
1
2
3
4
5
6
7
8
9

所以,"创建一个场景"本质就是"创建一个 id 为 "scene" 的 source":

// libobs/obs-scene.c:1794
obs_scene_t *obs_scene_create(const char *name)
{
	return create_id(obs->data.main_canvas, "scene", name);   // 就是第 6 课那条 obs_source_create!
}
1
2
3
4
5

obs_scene 和它底层的那个 obs_source,是两面一体的:obs_scene_get_source(scene) 拿到它的 source,obs_scene_from_source(source) 反过来拿回 scene。

这个设计有多妙?正因为场景是 source:

  • 你能把"一个场景",当成源,放进另一个场景里 → 场景可以嵌套;
  • 转场(第 6 课)就是在两个场景(两个 source)之间做过渡,不用为场景搞特殊机制;
  • 渲染、属性面板、存档…… 引擎对待场景,和对待一个摄像头完全一样,零特殊处理

一个"场景也是 source"的抽象,让"多源合成"这件复杂的事,几乎没给引擎添任何新负担。接下来看合成到底怎么画。


# 13.4 源码深读:合成,就是"逐项画一遍"

📍我们在哪:知道场景是 source 了,现在进它的 video_render,看"合成"这件事在现场到底是怎么一项项画出来的。

场景那个 video_render 回调 scene_video_render(obs-scene.c:1059),就是合成的现场。抽出核心:

合成渲染

// libobs/obs-scene.c:1059 (抽骨架)
static void scene_video_render(void *data, gs_effect_t *effect)
{
	struct obs_scene *scene = data;
	...
	struct obs_scene_item *item = scene->first_item;   // 从链表头开始
	while (item) {
		if (item->user_visible)         // 跳过隐藏的项
			render_item(item);          // ★ 画这一项
		item = item->next;              // 下一项(更上面的层)
	}
	...
}
1
2
3
4
5
6
7
8
9
10
11
12
13

就是顺着链表,把每个可见的场景项挨个画一遍。而 render_item(obs-scene.c:897)画单独一项的核心,只有三行有意义的代码:

gs_matrix_push();                              // 存一下当前坐标状态
gs_matrix_mul(&item->draw_transform);          // ① 设好「这一项摆哪、多大」的矩阵
obs_source_video_render(item->source);         // ② 画它包的那个源(第 11 课那套!)
gs_matrix_pop();                               // 恢复
1
2
3
4

看第 ① 和 ② 行,一切就通了:

  • gs_matrix_mul(&item->draw_transform) —— 把这一项的变换矩阵(13.2 那个 draw_transform)乘进当前坐标系。这就决定了"接下来画的东西,摆在画面的哪个位置、多大、转多少"。
  • obs_source_video_render(item->source) —— 调用这个项包的源的 video_render这不就是第 11、12 课那套嘛 —— 把源变成纹理、跑着色器画出来。摄像头也好、另一个场景也好、加了滤镜的源也好,都从这一句进去。

一句话:合成 = 顺着链表,给每个源"设好它的变换矩阵,然后调它的 video_render"。 场景本身不关心每个源具体怎么画(那是第 11、12 课的事),它只负责摆好位置、按顺序调用。而链表顺序决定谁在上谁在下 —— 先画的被后画的盖住,层级就这么来的。

💡 旁注 D:嵌套场景,是怎么"自动"就画对了的? 13.3 说场景能嵌套(场景 B 当成一个源放进场景 A)。它凭什么不用写任何特殊代码就能工作?看 render_item 那句 obs_source_video_render(item->source):如果这个 item->source 恰好是另一个场景,那这一句调进去的,就是那个场景的 scene_video_render——于是它又会顺着它自己的链表逐项画一遍。这是一次递归:大场景画到"嵌套场景"这一项时,自然地钻进小场景,把小场景的每一项也画出来,画完再回到大场景继续下一项。整个过程没有一行"专门处理嵌套"的代码——因为场景是 source,obs_source_video_render 一视同仁,递归就免费发生了。 这就是 13.3 说的"抽象的妙处"最实在的一次兑现。


# 13.5 你拖动一个源,改的到底是什么

📍承上启下:合成时那句 gs_matrix_mul 用到的矩阵,是你鼠标拖出来的——这一节把"拖动一个源"和第 11 课的 ViewProj 接上。

现在来还 13.2 那个 draw_transform 的债,顺便把第 11~13 课合龙

你在 OBS 里用鼠标拖动、缩放、旋转一个源时,背后是这么一条链:

拖动一个源改了什么

  1. 你拖 / 缩 / 转 一个源;
  2. 界面调 obs_sceneitem_set_pos(...) 之类,改这个场景项的 pos / scale / rot;
  3. 引擎把"位移 + 缩放 + 旋转"合成成一个 4×4 矩阵,存进 item->draw_transform;
  4. 合成时(13.4),gs_matrix_mul(&item->draw_transform) 把这个矩阵设进当前坐标系;
  5. 而这个矩阵,正是第 11 课着色器里那个 uniform float4x4 ViewProj —— 顶点着色器 VSDefault 里那句 mul(v.pos, ViewProj),就是拿它把源的四个角摆到画面上正确的位置。

所以,"在 OBS 里拖一个源" = 改它那个 4×4 矩阵 = 改喂给顶点着色器的 ViewProj 第 11 课学的"顶点着色器负责定位",到这一课终于知道那个定位矩阵从哪来、被谁改 —— 就是你的鼠标,经过场景项,变成了矩阵。

但上面第 3 步——"把位移+缩放+旋转合成成一个 4×4 矩阵"——听着还是有点像黑盒。别让它悬着,我们把它当场拆开。


# 13.6 拆开黑盒①:draw_transform 那个矩阵是怎么拼出来的

📍我们在哪:上一节说位移+缩放+旋转"合成成一个矩阵"还是个黑盒,这一节拆开它——五行代码怎么把 pos/scale/rot 拼成那个 4×4。

# 先补一句:4×4 矩阵到底是个啥

如果你没系统学过线性代数,别慌,一句话够用了:

一个 4×4 矩阵,你就把它当成一个"打包好的变换盒子" —— 里面装着"缩放 + 旋转 + 平移"这一整套动作。一个点(顶点)只要"乘一下"这个盒子,这套动作就一次性、全套作用到它身上。

为什么非得用矩阵、而不是老老实实"先加个偏移、再乘个缩放"?两个实在的好处:

  • 可组合:两个矩阵相乘 = 两套变换叠加。所以下面能"一步一步把动作乘进同一个盒子里"。
  • GPU 原生支持:第 11 课那句着色器代码 mul(v.pos, ViewProj),mul 就是"顶点乘矩阵",硬件一条指令就算完。

不用会算矩阵乘法,只要知道它是"一个打包好的变换"就够了。

拆开矩阵黑盒

# 真实代码:五行,把 pos/scale/rot 拼成矩阵

这活儿发生在 update_item_transform(obs-scene.c:601)。函数前半段在算 scale / origin / position 这些数值(处理对齐、裁剪、画布缩放等细节),真正"拼矩阵"的,就是这连续五行(obs-scene.c:657):

// libobs/obs-scene.c:657
matrix4_identity(&item->draw_transform);                                                    // ① 从「白纸」开始
matrix4_scale3f(&item->draw_transform, &item->draw_transform, scale.x, scale.y, 1.0f);      // ② 先按 scale 缩放
matrix4_translate3f(&item->draw_transform, &item->draw_transform, -origin.x, -origin.y, 0.0f); // ③ 移到对齐原点
matrix4_rotate_aa4f(&item->draw_transform, &item->draw_transform, 0.0f, 0.0f, 1.0f, RAD(item->rot)); // ④ 绕 z 轴转 rot 度
matrix4_translate3f(&item->draw_transform, &item->draw_transform, position.x, position.y, 0.0f); // ⑤ 平移到最终位置
1
2
3
4
5
6

一行一行读,你会发现它就是"照着人的直觉,一步步把动作乘进去":

  • matrix4_identity —— 把 draw_transform 重置成单位矩阵。单位矩阵就是"啥也不改的变换"(相当于数字里的 1),一张干净白纸,从这儿开始往上叠。
  • matrix4_scale3f(..., scale.x, scale.y, 1.0f) —— 先缩放scale.x/scale.y 就是这个源被你拉大/缩小的倍数;z 方向填 1.0f(2D 合成,深度不缩)。
  • matrix4_translate3f(..., -origin.x, -origin.y, ...) —— 平移到对齐原点origin 是根据这个项的对齐方式(居中?左上角?)算出来的锚点;先减掉它,是为了让接下来的旋转绕着正确的中心转,而不是绕左上角瞎转。
  • matrix4_rotate_aa4f(..., 0,0,1, RAD(item->rot)) —— 旋转aa = axis-angle(轴 + 角度),(0,0,1) 表示绕 z 轴(垂直于屏幕那根轴)转;RAD(item->rot) 把你在界面里填的"度数"转成弧度。
  • matrix4_translate3f(..., position.x, position.y, ...) —— 最后平移到它在画布上真正该待的位置。position 就是你拖出来的那个 pos(经过画布坐标换算)。

顺序是有讲究的:先缩放、再旋转、最后平移。 这就是为什么你在 OBS 里旋转一个源,它是绕自己的中心转、而且转完还待在原地——因为先减 origin 把中心挪到了旋转轴上,转完再用 position 送回去。把顺序打乱,旋转就会"甩飞"到别处。

🔧 这里引入了什么"库"? 没有外部库。matrix4_identity / matrix4_scale3f / matrix4_rotate_aa4f / matrix4_translate3f 全是 libobs 自带的数学库(libobs/graphics/matrix4.cmath-defs.h 里的 RAD)。OBS 没有依赖 glm、Eigen 这类第三方数学库,自己写了一套精简的 vec2/vec3/vec4/matrix4/quat——因为图形引擎里这点线代运算,自己维护比拉一个大依赖更划算。

💡 旁注 E:这五行什么时候才跑?(不是每帧都算!) 你可能担心:每合成一帧都要重算一次矩阵,岂不是很费?其实不会update_item_transform 只在你真正改了 pos/scale/rot(或源尺寸变了)时才被调用一次,算完把结果缓存item->draw_transform 里。合成时(13.4)只是 gs_matrix_mul 这个缓存矩阵,不重算。函数开头那句 if (os_atomic_load_long(&item->defer_update) > 0) return;(obs-scene.c:614)还做了"延迟合并"——你拖动过程中一连串的改动,可以先攒着、最后统一算一次。这就是典型的"惰性重算":变了才算,没变就吃缓存,所以拖再快也不卡。


# 13.7 拆开黑盒②:坐标系与 ViewProj 里的"投影"

📍承上启下:矩阵拼好了,可它输出的还是画布像素,离 GPU 要的 −1~1 还差一步"投影"——这一节补上 ViewProj 里的 Proj。

上面矩阵拼好了,item->pos 的单位是画布像素(比如 (1920, 1080) 那套)。可第 11 课说过,GPU 最终要的顶点坐标是 −1~1 的裁剪空间(clip space),跟画布像素完全不是一回事。这中间少了一步"投影"——把画布像素坐标,映射到 GPU 认的 −1~1 方框里。这就是 ViewProj 里那个 Proj(projection,投影) 的来历。

坐标系与投影

干这件事的,是一句 gs_ortho。画整个画布前会先设一次(obs-display.c:218),场景给每个项做离屏渲染时也会设(obs-scene.c:935):

// libobs/obs-display.c:218 —— 每次开画一个画布前
gs_ortho(0.0f, (float)cx, 0.0f, (float)cy, -100.0f, 100.0f);
//        left  right       bottom top      near     far
1
2
3

ortho = orthographic(正交投影)。这一句的意思是:

"我接下来用的坐标,x 从 0cx(画布宽),y 从 0cy(画布高)。你(GPU 底层)帮我把这个范围,线性映射到 −1~1 的裁剪空间去。"

于是坐标就对上了:你在画布像素空间里用 pos=(x,y) 摆源,gs_ortho 负责把 0~19200~1080 这套"人看得懂的像素坐标",换算成"GPU 要的 −1~1"。正交投影的特点是不带近大远小的透视——2D 合成正需要这样,一个源摆哪就是哪、多大就是多大,不会因为"离得远"就变小。

现在可以把第 11 课那个 ViewProj 彻底说透了:

ViewProj = draw_transform(你的定位,画布像素空间) × ortho(投影,画布像素 → −1~1)。

  • draw_transform(13.6 那五行拼的)——负责"这个源摆在画布的哪、多大、转多少",输出还在画布像素坐标系;
  • ortho(这一句 gs_ortho)——负责把画布像素坐标投影到 GPU 的 −1~1;
  • 两者一乘,就是喂给顶点着色器的 ViewProj。第 11 课 VSDefault 里那句 mul(v.pos, ViewProj),一步就把源的顶点从"本地坐标",经过"画布定位 + 投影",直接送到了 GPU 的裁剪空间。

第 11 课我们说"顶点着色器负责定位",当时 ViewProj 是个黑盒;这一课,它被拆成了**定位(draw_transform)+ 投影(ortho)**两半,两半都能在源码里指到具体那一行。


# 13.8 拆开黑盒③:这幅合成好的画,到底画到哪去了

📍收束:定位和投影都讲透了,最后一个悬念——这幅合成好的画落到哪儿去了?答案是一张纹理,它就是交给下一课的成品。

最后一个悬念:scene_video_render 把所有项都画完了,这幅合成好的画面,画到哪儿去了?画到屏幕上了吗?

并不是。 它画进的是一张纹理(render target,渲染目标)——你可以理解成"一块显存里的画布"。合成的成果先落在这张纹理上,然后一路给预览显示、一路送去编码

合成好的画去哪了

顺着调用链看,合成是被谁"启动"的?根子在 obs_view_render(obs-view.c:118):

// libobs/obs-view.c:118 (抽核心)
void obs_view_render(obs_view_t *view)
{
	...
	for (size_t i = 0; i < MAX_CHANNELS; i++) {
		struct obs_source *source = view->channels[i];
		if (source && !source->removed)
			obs_source_video_render(source);   // ← 画这个通道的源
	}
	...
}
1
2
3
4
5
6
7
8
9
10
11

view 的某个"通道(channel)"上挂着当前场景那个 source。引擎的视频线程每帧调一次 obs_view_renderobs_source_video_render(当前场景) → 场景的 video_render 回调,也就是 scene_video_render(13.4)→ 逐项画。这就把"每帧产出一幅画"这件事,和"合成"接上了。

而这一整趟画下来,像素落在了 libobs 提前设好的渲染目标上(第 11 课末尾讲过 render target 的概念)。这幅"合成好的一帧",就是后面第 14 课要按帧率定时取走、送进编码器的那个"成品"。

所以闭环了:合成的输出不是屏幕,而是一张纹理;这张纹理,就是流水线里"采集→合成"这半程交出去的、送给"编码"那半程的一帧成品。 第 13 课末尾那个"下一站:编码"的接口,就在这张纹理上。


# 13.9 模块 3 收官:一帧画面的前半生

📍收束:视频链路的前半段全连起来了,这一节回头看这一路,把底座"一切皆 source"再点一次题。

到这一课,视频链路的前半段全连起来了。回头看看这一路:

视频链路前半段回顾

  • 采集(第 10 课):摄像头/屏幕 → obs_source_frame;
  • 上显存(第 11 课):帧 → GPU 纹理;
  • 画 / 滤镜(第 11、12 课):着色器把源画出来、还能加特效;
  • 合成(第 13 课):多个源,按各自的矩阵和层级,叠成最终一幅画面。

而贯穿这四步的,是同一把钥匙 —— 一切皆 Source(第 6 课):

  • 采集源、滤镜、场景…… 全是 source;合成时,场景只是"按矩阵和顺序,逐个调它们的 video_render";
  • 场景自己也是 source,于是能嵌套、能被转场、能当成更大画面里的一块。

一个朴素到有点"狡猾"的抽象(万物都是 source),配上一套统一的渲染(都是纹理 + 着色器),就把"多源、多层、可嵌套、可加特效的实时合成"这么复杂的事,撑了起来。这就是读到模块 3 末尾,你该有的那个"啊,原来如此"的畅快。


# 13.10 本课小结

  • 合成 = 把多个源,按位置/大小/层级,叠成最终一幅;打包这套排布的东西叫场景
  • 数据模型:obs_scene 持有一串 obs_scene_item(obs-scene.h:30/99)。每个场景项 = 一个 source + 变换(pos/scale/rot)+ user_visible(眼睛图标),用 prev/next 串成链表;链表顺序 = 图层上下层级
  • 场景也是 source(第 6 课伏笔闭合):scene_info(obs-scene.c:1742,id="scene"、type=SCENE)是一张正常的登记表;obs_scene_create 就是 obs_source_create 一个 "scene" 源(:1794)。于是场景能嵌套、能被转场、被引擎一视同仁。嵌套之所以免费:obs_source_video_render(item->source) 遇到嵌套场景就递归进它的 scene_video_render(旁注 D)。
  • 合成渲染(scene_video_render,obs-scene.c:1059):顺着链表,对每个可见项 render_item —— gs_matrix_mul(&item->draw_transform) 设变换,再 obs_source_video_render(item->source) 画它的源(第 11 课那套)。
  • 拖动一个源 = 改 item->pos/scale/rot → 算出 draw_transform 4×4 矩阵 → 合成时 gs_matrix_mul 设进去 → 它就是第 11 课着色器里的 ViewProj。第 11~13 课在此合龙。
  • draw_transform 不是黑盒:update_item_transform(obs-scene.c:657)五行——identity → scale → translate(-origin) → rotate → translate(position)——把 pos/scale/rot 拼成一个 4×4 矩阵。用的是 libobs 自带数学库(无第三方依赖);且惰性重算,变了才算、否则吃缓存(旁注 E)。
  • ViewProj 拆成两半:ViewProj = draw_transform(定位,画布像素空间)× ortho(投影,画布像素 → −1~1)。gs_ortho(0,cx,0,cy,...)(obs-display.c:218)负责把画布像素坐标映射到 GPU 的裁剪空间。
  • 合成画到哪:obs_view_render(obs-view.c:118)每帧调 obs_source_video_render(当前场景) → 合成结果落在一张**纹理(render target)**上,一路给预览、一路给编码。这张纹理就是交给第 14 课的"一帧成品"。
  • 模块 3 收官:采集 → 上显存 → 画/滤镜 → 合成,前半段全连起来,底座是"一切皆 source"。

# 13.11 动手 / 观察(本课作业)

  1. 看层级 = 链表顺序:OBS 里往一个场景加三个源,在"来源"列表里上下拖动它们,观察谁盖住谁。你拖的,就是 13.2 那条 prev/next 链的顺序。
  2. 看变换 = 矩阵:选中一个源,拖动/缩放/旋转它,或按 Ctrl+E 打开"编辑变换"看到 pos/scale/rot 的具体数值 —— 它们仨,就是被合成成 draw_transform 矩阵、喂给 ViewProj 的。
  3. 验证"场景是 source":新建两个场景,在场景 A 里,"添加来源"时你会发现能把场景 B 作为一个来源加进来 —— 这就是 13.3 说的嵌套,只有"场景是 source"才做得到。
  4. 翻源码:打开 libobs/obs-scene.c,找到 scene_video_render(:1059),确认那个 while (item) { ... item = item->next; } 循环;再找到 scene_info(:1742),确认场景真的是用 obs_source_info 注册的。
  5. 数一数拼矩阵那五行:跳到 update_item_transform(obs-scene.c:601),对着 13.6 把 identity → scale → translate(-origin) → rotate → translate(position) 这连续五行找出来;再想想:如果把 ③ 的 translate(-origin) 删掉,旋转一个源时它会绕着哪儿转?(答案:绕左上角,而不是中心。)
  6. 找投影那一句:在 obs-display.c:218 找到 gs_ortho(0, cx, 0, cy, -100, 100),它就是把画布像素坐标投影到 GPU −1~1 的那一步;cx/cy 正是你的画布分辨率(如 1920/1080)。这就是 ViewProj 里"Proj"的真身。

# 下一课预告

这一课我们把一堆源"合成"成了一幅画面。但有两个问题还没答:第一,这幅画布本身是怎么定时产出"一帧成品"、送进后面的编码的?第二,你从"场景 A"切到"场景 B"时,那个淡入淡出、滑动的过渡,是怎么做出来的?

第 14 课:从合成结果到一帧成品 —— 视频管线与转场 —— 我们会看 libobs/media-io/video-io 那条视频输出管线怎么按帧率定时取帧,以及**转场(transition)**这种"夹在两个场景之间的 source"是怎么把两幅画面混合过渡的。模块 3 真正的收尾,把"画面"交到"编码"手里。下节课见。


📁 本课配图:imgs/13-01 ~ imgs/13-09 📌 源码锚点:libobs/obs-scene.h:30(obs_scene_item)、:99(obs_scene)、libobs/obs-scene.c:601(update_item_transform)、:657(拼 draw_transform 的五行)、:897(render_item)、:935(gs_ortho 逐项离屏)、:964(gs_matrix_mul draw_transform)、:980(obs_source_video_render)、:1059(scene_video_render 合成循环)、:1742(scene_info —— 场景即 source)、:1794(obs_scene_create)、libobs/obs-view.c:118(obs_view_render —— 每帧启动合成)、libobs/obs-display.c:218(gs_ortho 画布投影)

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>