首页
/ OBS Studio Scene API 深度解析:场景合成与场景项变换控制(obs_scene_t)

OBS Studio Scene API 深度解析:场景合成与场景项变换控制(obs_scene_t)

2026-09-06 21:29:09作者:谭伦延

本文基于 OBS Studio 官方 Sphinx API 参考文档 reference-scenes.rst,系统讲解 libobs 场景层 API 的核心对象模型、数据结构、信号机制与完整函数族,并结合 libobs/obs-scene.hlibobs/obs-scene.clibobs/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 边界框尺寸(启用边界框时有效)

其中 alignmentbounds_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 描述场景项的裁剪边距,四个成员均为 intlefttoprightbottom。裁剪在变换之后作用于场景项渲染纹理(源码中对应 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_posvec2);
  • obs_sceneitem_set_rot / obs_sceneitem_get_rot(角度,float);
  • obs_sceneitem_set_scale / obs_sceneitem_get_scalevec2);
  • obs_sceneitem_set_alignment / obs_sceneitem_get_alignmentuint32_t,位或组合 OBS_ALIGN_*)。

批量接口 obs_sceneitem_set_info2 / obs_sceneitem_get_info2 一次性读写整个 obs_transform_info,且文档注明:该版本还会设置 obs_transform_infocrop_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):获取当前序号。

边界框三接口

边界框用于把源"拉伸/定位"到指定尺寸的框内,三个接口各自独立:

  1. obs_sceneitem_set_bounds_type / obs_sceneitem_get_bounds_type:设置 obs_bounds_type(前述 7 种行为);
  2. obs_sceneitem_set_bounds_alignment / obs_sceneitem_get_bounds_alignment:源在框内的对齐方式;
  3. 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_DISABLEOBS_SCALE_POINTOBS_SCALE_BICUBICOBS_SCALE_BILINEAROBS_SCALE_LANCZOS(注意 libobs/obs.henum obs_scale_type 中还存在 OBS_SCALE_AREA,属于同一枚举的成员)。

混合控制分两层:

  1. obs_sceneitem_set_blending_methodOBS_BLEND_METHOD_DEFAULTOBS_BLEND_METHOD_SRGB_OFF(关闭 sRGB 转换路径);
  2. obs_sceneitem_set_blending_mode:共 7 种混合模式——OBS_BLEND_NORMALOBS_BLEND_ADDITIVEOBS_BLEND_SUBTRACTOBS_BLEND_SCREENOBS_BLEND_MULTIPLYOBS_BLEND_LIGHTENOBS_BLEND_DARKEN(定义见 libobs/obs.h)。

变换状态持久化

obs_scene_save_transform_states(scene, bool all_items) 保存场景内场景项的全部变换状态,返回 obs_data_tall_itemsfalse只保存当前选中的项。配套接口 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,即可安全地批量调整分组内多个成员的变换而不会与分组的每帧自动变换相互干扰。

使用要点小结

  1. 引用计数纪律:场景与场景项都是引用计数对象;所有 get_source / get_scene / from_source 类接口都不增加引用,跨作用域持有一律先 addref,结束时 release
  2. 私有对象策略:内部结构(如分组的子场景)通过 *_create_privateOBS_SCENE_DUP_PRIVATE_* 创建,避免污染前端的源列表。
  3. 批量写入用 defer:脚本或工具连续修改一个(或一组)场景项的变换时,用 defer_update_begin/end、分组场景用 defer_group_resize_begin/end 包裹,既减少矩阵重算,也避免信号风暴——矩阵本身也是每帧由 update_transforms_and_prune_sources 统一刷新的。
  4. 信号驱动前端:任何会引起 UI 变化的 API(增删、排序、显隐、锁定、选中、变换)都有对应场景信号,前端只需订阅即可,无需轮询。
  5. 危险接口慎入obs_sceneitem_set_id 被文档明确标注为危险函数,除非你完全理解 id_counter 分配机制(见 libobs/obs-scene.hstruct obs_scene::id_counter),否则不要调用。

以上全部函数与数据结构均出自 docs/sphinx/reference-scenes.rst,实现佐证可在 libobs/obs-scene.clibobs/obs-scene.hlibobs/obs.hlibobs/obs-defs.h 中逐条对照。

登录后查看全文
热门项目推荐
相关项目推荐