OBS Studio Scene API 深度解析:场景合成与场景项变换控制(obs_scene_t)
本文基于 OBS Studio 官方 Sphinx API 参考文档 reference-scenes.rst,系统讲解 libobs 场景层 API 的核心对象模型、数据结构、信号机制与完整函数族,并结合 libobs/obs-scene.h、libobs/obs-scene.c、libobs/obs.h 等源码给出实现层面的佐证。读完后你将能够:理解"场景即源"(scene is a source)的对象模型与引用计数规则;熟练运用 obs_sceneitem_t 的位置、旋转、缩放、对齐、边界框与裁剪等变换控制接口;并掌握分组(group)、过渡(transition)、延迟更新(defer)等高级合成机制。
场景是什么:一种特殊的容器型源
官方文档对场景的定义只有一句话,但它是整个 API 的基石:
A scene is a source which contains and renders other sources using specific transforms and/or filtering.
也就是说,场景本身是一种 obs_source_t,只不过它扮演"容器"角色:内部持有若干"场景项"(scene item),每个场景项引用一个普通源,并通过各自的变换矩阵决定该源在场景画布中的位置、旋转、缩放与层级顺序。场景可以像普通源一样被添加到输出、被其他场景嵌套、参与过渡。
API 涉及两类核心对象,均为引用计数对象:
obs_scene_t:A reference-counted scene object(引用计数的场景对象);obs_sceneitem_t:A reference-counted scene item object(引用计数的场景项对象)。
头文件约定为 #include <obs.h>。
从源码结构看,这种"场景即源"并非停留在文档层面。libobs/obs-scene.h 中的 struct obs_scene 第一个成员就是 struct obs_source *source,场景自身还维护了 first_item 双向链表头、id_counter(场景项唯一 ID 分配器)、video_mutex / audio_mutex 两把互斥锁;而 struct obs_scene_item 则同时持有 parent(所属场景)与 source(承载的源)双指针,并用 prev / next 构成场景内层级链表。源文件中的注释"how obs scene!"也点明了这一设计的核心。
核心数据结构:变换信息、裁剪与排序
obs_transform_info:场景项的完整变换描述
obs_transform_info 是批量读写场景项变换信息的聚合结构,文档定义的成员与 libobs/obs.h 中的 struct obs_transform_info 完全一致:
| 成员 | 类型 | 含义 |
|---|---|---|
pos |
struct vec2 |
场景项位置 |
rot |
float |
旋转角度(单位:度) |
scale |
struct vec2 |
缩放系数 |
alignment |
uint32_t |
场景项相对于其位置的锚点对齐方式,取 0 或下列位或组合 |
bounds_type |
enum obs_bounds_type |
边界框类型(见下表) |
bounds_alignment |
uint32_t |
源在边界框内的对齐方式,取值同 alignment |
bounds |
struct vec2 |
边界框尺寸(启用边界框时有效) |
其中 alignment 与 bounds_alignment 均为位标志,取值来自 libobs/obs-defs.h:
#define OBS_ALIGN_CENTER (0)
#define OBS_ALIGN_LEFT (1 << 0)
#define OBS_ALIGN_RIGHT (1 << 1)
#define OBS_ALIGN_TOP (1 << 2)
#define OBS_ALIGN_BOTTOM (1 << 3)
注意 OBS_ALIGN_CENTER 是 0,即"什么都不按位或"就表示中心对齐;OBS_ALIGN_LEFT | OBS_ALIGN_BOTTOM 这类组合则表示锚点定位在左下角。
bounds_type 枚举在 libobs/obs.h 中定义,共 7 种行为:
| 取值 | 行为 |
|---|---|
OBS_BOUNDS_NONE |
无边界框 |
OBS_BOUNDS_STRETCH |
拉伸填满边界框,不保持宽高比 |
OBS_BOUNDS_SCALE_INNER |
按宽高比缩放,完整放进边界框内(inner rectangle) |
OBS_BOUNDS_SCALE_OUTER |
按宽高比缩放,覆盖整个边界框(outer rectangle) |
OBS_BOUNDS_SCALE_TO_WIDTH |
按宽高比缩放至边界框宽度 |
OBS_BOUNDS_SCALE_TO_HEIGHT |
按宽高比缩放至边界框高度 |
OBS_BOUNDS_MAX_ONLY |
仅允许缩小到源的最大尺寸以内,按宽高比限制 |
obs_sceneitem_crop:四向裁剪
obs_sceneitem_crop 描述场景项的裁剪边距,四个成员均为 int:left、top、right、bottom。裁剪在变换之后作用于场景项渲染纹理(源码中对应 struct obs_sceneitem_crop crop 与边界框裁剪 bounds_crop 两套独立字段)。
obs_sceneitem_order_info:带分组的批量排序描述
obs_sceneitem_order_info 用于 obs_scene_reorder_items2,描述"某个场景项应放入哪个分组、排在哪个位置":
| 成员 | 含义 |
|---|---|
group |
该场景项所属分组;不属于任何分组时为 NULL |
item |
指定的场景项 |
场景信号:前端响应的驱动源
场景会发出 9 类信号,供前端(GUI、脚本)同步 UI 状态。这些信号在 libobs/obs-scene.c 的信号表中定义,与文档逐一对应:
| 信号 | 签名 | 触发时机 |
|---|---|---|
item_add |
(ptr scene, ptr item) |
场景项被添加到场景 |
item_remove |
(ptr scene, ptr item) |
场景项被移除 |
reorder |
(ptr scene) |
场景项层级顺序发生变化 |
refresh |
(ptr scene) |
整个场景项列表需要刷新(通常发生在分组结构变化时) |
item_visible |
(ptr scene, ptr item, bool visible) |
场景项可见性状态改变 |
item_locked |
(ptr scene, ptr item, bool locked) |
场景项被锁定/解锁 |
item_select / item_deselect |
(ptr scene, ptr item) |
场景项被选中/取消选中(原文档作者注:这两个信号未来应被替换) |
item_transform |
(ptr scene, ptr item) |
场景项的变换发生变化 |
从源码看,这些信号并非分散触发:obs_sceneitem_select 通过 const char *command = select ? "item_select" : "item_deselect" 统一发出(libobs/obs-scene.c);而 item_transform 信号在每次 update_item_transform 完成矩阵更新后由 signal_parent 发出(libobs/obs-scene.c),这意味着任何会引起矩阵重算的变换写入,最终都会触发一次 item_transform 信号,前端据此重绘变换手柄。
场景通用函数族
创建、引用计数与克隆
| 函数 | 说明 |
|---|---|
obs_scene_t *obs_scene_create(const char *name) |
创建场景。若名称不唯一,libobs 会自动改为唯一名称 |
obs_scene_t *obs_scene_create_private(const char *name) |
创建私有场景:名称不必唯一,也可为 NULL(私有源不会出现在前端源列表中) |
obs_scene_t *obs_scene_duplicate(obs_scene_t *scene, const char *name, enum obs_scene_duplicate_type type) |
克隆场景。可只引用原场景项,或完整复制场景项 |
obs_scene_t *obs_scene_get_ref(obs_scene_t *scene) |
在仍有效时增加引用计数,否则返回 NULL;用 obs_scene_release 释放 |
void obs_scene_release(obs_scene_t *scene) |
释放一次引用 |
obs_source_t *obs_scene_get_source(const obs_scene_t *scene) |
取得场景对应的源上下文,不增加引用 |
obs_scene_t *obs_scene_from_source(const obs_source_t *source) |
从源反查场景上下文;非场景返回 NULL,不增加引用 |
obs_scene_duplicate 的类型参数 enum obs_scene_duplicate_type 定义于 libobs/obs.h,四种克隆语义:
OBS_SCENE_DUP_REFS:复制场景,但场景项仅以引用方式共享(同一份源实例);OBS_SCENE_DUP_COPY:复制场景,且尽可能完整复制每个场景项;OBS_SCENE_DUP_PRIVATE_REFS:引用共享,但新场景为私有源;OBS_SCENE_DUP_PRIVATE_COPY:完整复制,且场景与复制出的源均为私有源。
查找与枚举
| 函数 | 说明 |
|---|---|
obs_sceneitem_t *obs_scene_find_source(obs_scene_t *scene, const char *name) |
按名称查找场景顶层的场景项,找不到返回 NULL |
obs_sceneitem_t *obs_scene_find_source_recursive(obs_scene_t *scene, const char *name) |
同 obs_scene_find_source,但会递归搜索场景内的分组 |
obs_sceneitem_t *obs_scene_find_sceneitem_by_id(obs_scene_t *scene, int64_t id) |
按场景项唯一数值 ID 查找 |
void obs_scene_enum_items(obs_scene_t *scene, bool (*callback)(obs_scene_t*, obs_sceneitem_t*, void*), void *param) |
自最底层场景项到最顶层场景项顺序枚举;回调返回 true 继续、false 终止 |
void obs_scene_prune_sources(obs_scene_t *scene) |
释放场景中所有已被 obs_source_remove 标记删除的源 |
枚举接口的使用有一个关键约定:回调期间拿到的 obs_sceneitem_t * 不保证长期有效,如需在 obs_scene_enum_items 结束后继续持有引用,必须调用 obs_sceneitem_addref()。典型用法如下:
#include <obs.h>
static bool find_item_by_source_name(obs_scene_t *scene, obs_sceneitem_t *item, void *data)
{
const char *target = (const char *)data;
const char *name = obs_source_get_name(obs_sceneitem_get_source(item));
if (name && strcmp(name, target) == 0) {
obs_sceneitem_addref(item); /* 延长生命周期 */
obs_sceneitem_set_visible(item, false);
return false; /* 结束枚举 */
}
return true;
}
obs_scene_t *scene = obs_scene_create("Overlay");
obs_sceneitem_t *result = NULL;
obs_scene_enum_items(scene, find_item_by_source_name, (void *)"camera");
/* 使用 result ... */
obs_scene_release(scene);
另外,文档特别注明脚本场景下可使用 Python 侧的 obs_scene_enum_items(对应 shared/obs-scripting 模块的绑定)。
层级排序
bool obs_scene_reorder_items(obs_scene_t *scene, obs_sceneitem_t * const *item_order, size_t item_order_size):按给定数组顺序重排场景项;bool obs_scene_reorder_items2(obs_scene_t *scene, struct obs_sceneitem_order_info *item_order, size_t item_order_size):重排时同时指定每个场景项的分组归属(即前文obs_sceneitem_order_info的用途)。
两者成功时都会发出 reorder 信号,源码中对应 signal_reorder(scene->first_item)(libobs/obs-scene.c)。
场景项函数族
生命周期与上下文
| 函数 | 说明 |
|---|---|
obs_sceneitem_addref / obs_sceneitem_release |
增加/释放场景项引用 |
void obs_sceneitem_remove(obs_sceneitem_t *item) |
从场景中移除该场景项 |
obs_scene_t *obs_sceneitem_get_scene(const obs_sceneitem_t *item) |
取所属场景,不增加引用 |
obs_source_t *obs_sceneitem_get_source(const obs_sceneitem_t *item) |
取承载源,不增加引用 |
void obs_sceneitem_set_id(obs_sceneitem_t *item) |
手动设置场景项数值 ID。文档明确警告:这是危险函数,正常不应使用,可能导致 OBS 内部错误 |
int64_t obs_sceneitem_get_id(const obs_sceneitem_t *item) |
获取场景项数值 ID |
obs_data_t *obs_sceneitem_get_private_settings(obs_sceneitem_t *item) |
获取场景项私有设置的已增加引用的 obs_data_t,前端可借此把自定义信息随场景项持久化;用 obs_data_release() 释放 |
变换的读写接口
位置、旋转、缩放、对齐均有成对 setter/getter:
obs_sceneitem_set_pos/obs_sceneitem_get_pos(vec2);obs_sceneitem_set_rot/obs_sceneitem_get_rot(角度,float);obs_sceneitem_set_scale/obs_sceneitem_get_scale(vec2);obs_sceneitem_set_alignment/obs_sceneitem_get_alignment(uint32_t,位或组合OBS_ALIGN_*)。
批量接口 obs_sceneitem_set_info2 / obs_sceneitem_get_info2 一次性读写整个 obs_transform_info,且文档注明:该版本还会设置 obs_transform_info 的 crop_to_bounds 成员("30.1 版本新增")。crop_to_bounds 字段确实存在于 libobs/obs.h 的结构定义中,用于控制是否裁剪到边界框。
层级控制三件套:
obs_sceneitem_set_order(item, enum obs_order_movement):相对移动,取值为 libobs/obs.h 中的OBS_ORDER_MOVE_UP/OBS_ORDER_MOVE_DOWN/OBS_ORDER_MOVE_TOP/OBS_ORDER_MOVE_BOTTOM;obs_sceneitem_set_order_position(item, int position):直接改为指定序号;int obs_sceneitem_get_order_position(item):获取当前序号。
边界框三接口
边界框用于把源"拉伸/定位"到指定尺寸的框内,三个接口各自独立:
obs_sceneitem_set_bounds_type/obs_sceneitem_get_bounds_type:设置obs_bounds_type(前述 7 种行为);obs_sceneitem_set_bounds_alignment/obs_sceneitem_get_bounds_alignment:源在框内的对齐方式;obs_sceneitem_set_bounds/obs_sceneitem_get_bounds:边界框宽高(vec2)。
变换矩阵与延迟更新
obs_sceneitem_get_draw_transform(item, matrix4 *transform):取得实际绘制源所用的变换矩阵;obs_sceneitem_get_box_transform(item, matrix4 *transform):取得用于边界框或场景项边缘的变换矩阵。
从源码看,这两个矩阵并非实时计算,而是"脏标记 + 每帧批量更新"模式:每次 setter 通过 do_update_transform 宏把 item->update_transform 置为 true,随后每帧的 update_transforms_and_prune_sources 统一调用 update_item_transform 重算。绘制矩阵的组装顺序在 libobs/obs-scene.c 一目了然:
matrix4_identity(&item->draw_transform);
matrix4_scale3f(&item->draw_transform, &item->draw_transform, scale.x, scale.y, 1.0f);
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));
matrix4_translate3f(&item->draw_transform, &item->draw_transform, position.x, position.y, 0.0f);
即 缩放 → 以锚点为原点平移 → 绕锚点旋转(弧度 RAD(rot))→ 平移到目标位置,这正是 alignment 锚点语义的数学体现。
由此引出两个性能相关的延迟更新接口:
obs_sceneitem_defer_update_begin/obs_sceneitem_defer_update_end:在 begin 与 end 之间可连续调用任意变换函数而不触发内部矩阵更新,直到 end 才被允许更新。适合脚本一次性写入 pos/rot/scale/bounds 等大量属性的场景,避免 N 次矩阵重算与 N 次item_transform信号。obs_sceneitem_defer_group_resize_begin/obs_sceneitem_defer_group_resize_end:针对分组内场景项的同类优化——因为分组自身变换每帧会根据成员自动更新,文档明确说明:若用户在分组内拖拽/缩放成员,不加 defer 保护会导致分组变换每帧被自动覆写。对应源码中obs_scene_item上的defer_group_resize原子计数器与update_group_resize标志(libobs/obs-scene.h)。
选择、可见性、锁定与裁剪
| 接口对 | 行为 |
|---|---|
obs_sceneitem_select / obs_sceneitem_selected |
选中/查询选中态。切换某个项的选中态不影响其他项——支持多选 |
obs_sceneitem_set_visible / obs_sceneitem_visible |
设置/查询可见性,触发 item_visible 信号 |
obs_sceneitem_set_locked / obs_sceneitem_locked |
设置/查询锁定态(前端据此禁止误操作),触发 item_locked 信号 |
obs_sceneitem_set_crop / obs_sceneitem_get_crop |
设置/查询 obs_sceneitem_crop 四向裁剪 |
缩放滤镜与混合方式
obs_sceneitem_set_scale_filter / obs_sceneitem_get_scale_filter 控制场景项的缩放滤镜,文档列出的取值为:OBS_SCALE_DISABLE、OBS_SCALE_POINT、OBS_SCALE_BICUBIC、OBS_SCALE_BILINEAR、OBS_SCALE_LANCZOS(注意 libobs/obs.h 的 enum obs_scale_type 中还存在 OBS_SCALE_AREA,属于同一枚举的成员)。
混合控制分两层:
obs_sceneitem_set_blending_method:OBS_BLEND_METHOD_DEFAULT或OBS_BLEND_METHOD_SRGB_OFF(关闭 sRGB 转换路径);obs_sceneitem_set_blending_mode:共 7 种混合模式——OBS_BLEND_NORMAL、OBS_BLEND_ADDITIVE、OBS_BLEND_SUBTRACT、OBS_BLEND_SCREEN、OBS_BLEND_MULTIPLY、OBS_BLEND_LIGHTEN、OBS_BLEND_DARKEN(定义见 libobs/obs.h)。
变换状态持久化
obs_scene_save_transform_states(scene, bool all_items) 保存场景内场景项的全部变换状态,返回 obs_data_t;all_items 为 false 时只保存当前选中的项。配套接口 obs_scene_load_transform_states(const char *states) 用于回填。这对做"变换历史/撤销栈"类功能很有用。
场景项级过渡(Transition)
文档最后一组函数为单个场景项(而非整个场景切换)配置显隐过渡:
| 函数 | 说明 |
|---|---|
void obs_sceneitem_set_transition(item, bool show, obs_source_t *transition) |
为显示(show=true)或隐藏(show=false)设置过渡源;传 NULL 表示移除过渡 |
obs_source_t *obs_sceneitem_get_transition(item, bool show) |
查询对应的过渡源,未设置时返回 NULL |
void obs_sceneitem_set_transition_duration(item, bool show, uint32_t duration_ms) |
设置过渡时长(毫秒) |
uint32_t obs_sceneitem_get_transition_duration(item, bool show) |
查询过渡时长(毫秒) |
void obs_sceneitem_do_transition(item, bool visible) |
启动显示或隐藏过渡 |
源码侧,struct obs_scene_item 为每个方向各存了一份 show_transition / hide_transition 源指针与 show_transition_duration / hide_transition_duration 时长(libobs/obs-scene.h),与文档描述一一对应。
场景项分组(Group)函数族
分组是场景内最强大的合成工具:一个分组本身就是一个(私有的)子场景,其成员共享统一变换。从源码结构看,obs_scene 结构体有 is_group 标志,分组成员通过链表挂在组场景之下,因此"分组"在实现上就是"场景的递归嵌套"。
创建与查找
| 函数 | 说明 |
|---|---|
obs_scene_add_group(scene, name) |
按名新增分组。不发送 refresh 信号 |
obs_scene_add_group2(scene, name, bool signal) |
同上,signal=true 时发送 refresh 信号 |
obs_scene_insert_group(scene, name, items, count) |
把给定场景项数组"收编"为分组,新分组插入到最顶层场景项位置;不发送 refresh |
obs_scene_insert_group2(scene, name, items, count, signal) |
同上,可指定是否发送 refresh |
obs_scene_get_group(scene, name) |
按名称查找分组场景项,找不到返回 NULL |
obs_scene_t *obs_group_from_source(obs_source_t *source) |
源是分组则返回分组上下文,否则 NULL;不增加引用 |
obs_scene_t *obs_group_or_scene_from_source(obs_source_t *source) |
无论分组还是场景都返回上下文,两者皆非时返回 NULL |
bool obs_sceneitem_is_group(item) |
判断场景项是否为分组 |
obs_scene_t *obs_sceneitem_group_get_scene(item) |
取得分组本身的场景上下文,非分组返回 NULL |
成员管理与解组
| 函数 | 说明 |
|---|---|
void obs_sceneitem_group_add_item(group, item) |
把场景项加入分组 |
void obs_sceneitem_group_remove_item(item) |
把场景项移出分组,该项会被放到主场景中分组之前的位置 |
obs_sceneitem_t *obs_sceneitem_get_group(scene, item) |
返回场景项的父分组;不在分组中返回 NULL |
void obs_sceneitem_group_ungroup(group) |
解散分组,成员放回分组原来所在的位置;不发送 refresh |
void obs_sceneitem_group_ungroup2(group, signal) |
同上,可指定是否发送 refresh |
void obs_sceneitem_group_enum_items(group, callback, param) |
枚举分组内场景项,回调语义与 obs_scene_enum_items 相同(返回 true 继续) |
分组枚举同样遵循"回调结束后需 obs_sceneitem_addref 才能继续持有引用"的约定。配合 obs_sceneitem_defer_group_resize_begin/end,即可安全地批量调整分组内多个成员的变换而不会与分组的每帧自动变换相互干扰。
使用要点小结
- 引用计数纪律:场景与场景项都是引用计数对象;所有
get_source/get_scene/from_source类接口都不增加引用,跨作用域持有一律先addref,结束时release。 - 私有对象策略:内部结构(如分组的子场景)通过
*_create_private与OBS_SCENE_DUP_PRIVATE_*创建,避免污染前端的源列表。 - 批量写入用 defer:脚本或工具连续修改一个(或一组)场景项的变换时,用
defer_update_begin/end、分组场景用defer_group_resize_begin/end包裹,既减少矩阵重算,也避免信号风暴——矩阵本身也是每帧由update_transforms_and_prune_sources统一刷新的。 - 信号驱动前端:任何会引起 UI 变化的 API(增删、排序、显隐、锁定、选中、变换)都有对应场景信号,前端只需订阅即可,无需轮询。
- 危险接口慎入:
obs_sceneitem_set_id被文档明确标注为危险函数,除非你完全理解id_counter分配机制(见 libobs/obs-scene.h 的struct obs_scene::id_counter),否则不要调用。
以上全部函数与数据结构均出自 docs/sphinx/reference-scenes.rst,实现佐证可在 libobs/obs-scene.c、libobs/obs-scene.h、libobs/obs.h 与 libobs/obs-defs.h 中逐条对照。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00