OBS Studio libobs Effects 特效 API 详解:着色器、Technique 与 Pass 的加载、执行与参数设置机制
【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio
libobs 的 Effects(特效)子系统是 OBS Studio 图形层将 HLSL 着色器文本与 C 运行时参数管理绑定在一起的核心机制:它让开发者可以把顶点着色器、像素着色器、共享函数与 uniform 参数写在同一个.effect文件中,再经由gs_effect_*系列 API 在运行时加载 Technique(技术)、驱动 Pass(通道)执行并注入参数。本文基于仓库中的 Sphinx API 参考文档 Effects (Shaders) 及其对应的源码实现 effect.c、effect-parser.c,完整讲解 Effect/Technique/Pass/Param 四类对象的生命周期、内置.effect文件的真实结构,以及参数上传与缓存的内部原理,帮助你在编写 OBS 插件或滤镜时正确调用这套 API。
一、Effect 是什么:一个文件内的着色器集合
API 文档给出的定义是:
Effects are a single collection of related shaders. They're used for easily writing vertex and pixel shaders together all in the same file in HLSL format.
即一个 Effect 是一组相关着色器的集合,目的是让你可以在同一个文件里用 HLSL 格式一起编写顶点着色器和像素着色器。源码 effect.h 中的注释进一步解释了它的动机:
/* * Effects introduce a means of bundling together shader text into one * file with shared functions and parameters. This is done because often * shaders must be duplicated when you need to alter minor aspects of the code * that cannot be done via constants. Effects allow developers to easily * switch shaders and set constants that can be used between shaders. * * Effects are built via the effect parser, and shaders are automatically * generated for each technique's pass. */翻译过来就是:很多场景下,只是要改变着色器代码的一小部分就必须整段复制着色器;Effect 机制允许开发者在同一个文件里声明共享的函数、结构体、uniform 参数和采样器状态,然后定义多个 Technique,每个 Technique 的一个或多个 Pass 各自引用不同的顶点/像素着色函数。解析器(effect parser)在加载时会自动为每个 Pass 生成独立的着色器文本,并自动把该 Pass 依赖的结构体、函数、参数、采样器一并写入。
这套机制对外暴露的核心类型(均需#include <graphics/graphics.h>):
| 类型 | C 类型 | 含义 |
|---|---|---|
| Effect 对象 | gs_effect_t(struct gs_effect) | 一个 effect 文件对应的整体对象,包含全部参数与 Technique |
| Technique 对象 | gs_technique_t(struct gs_effect_technique) | 一组 Pass 的集合,对应一次完整的绘制策略 |
| Effect 参数对象 | gs_eparam_t(struct gs_effect_param) | 一个 uniform 参数(含 annotation 注解参数) |
从源码结构看,三者的层级关系是gs_effect持有params与techniques两个动态数组,gs_effect_technique持有passes数组,每个gs_effect_pass持有编译好的vertshader/pixelshader以及两个参数映射表(vertshader_params/pixelshader_params),见 effect.h:
struct gs_effect_param { char *name; enum effect_section section; enum gs_shader_param_type type; bool changed; DARRAY(uint8_t) cur_val; // 当前值(字节缓冲) DARRAY(uint8_t) default_val; // 默认值(字节缓冲) gs_effect_t *effect; gs_samplerstate_t *next_sampler; gs_effect_param_array_t annotations; }; struct gs_effect { bool processing; bool cached; char *effect_path, *effect_dir; gs_effect_param_array_t params; DARRAY(struct gs_effect_technique) techniques; struct gs_effect_technique *cur_technique; struct gs_effect_pass *cur_pass; gs_eparam_t *view_proj, *world, *scale; graphics_t *graphics; struct gs_effect *next; // 线程级缓存链表 size_t loop_pass; bool looping; };可以看到参数值是以原始字节(DARRAY(uint8_t))形式缓存的——这解释了后文为什么gs_effect_get_val返回的是“当前值的字节拷贝”,以及为什么gs_effect_set_*系列函数内部统一走“memcpy 到 cur_val + 标记 changed”的路径。
二、真实.effect文件结构:以 libobs 内置文件为例
理解 API 之前,先看仓库中实际使用的 effect 文件。libobs 内置的 21 个.effect文件位于 libobs/data/,包括default.effect、opaque.effect、solid.effect、bicubic_scale.effect、lanczos_scale.effect、format_conversion.effect、deinterlace_*.effect等,分别服务于场景渲染、不透明源绘制、纹理缩放、像素格式转换与隔行扫描消隐。
以 default.effect 为例,它展示了一个典型 effect 文件的完整语法:
#include "color.effect" // 共享函数库(sRGB/HDR 转换) uniform float4x4 ViewProj; // 参数:视图投影矩阵 uniform texture2d image; // 参数:输入纹理 uniform float multiplier; // 参数:音量/不透明度乘子 sampler_state def_sampler { // 采样器状态声明 Filter = Linear; AddressU = Clamp; AddressV = Clamp; }; struct VertInOut { // 共享的顶点输入输出结构体 float4 pos : POSITION; float2 uv : TEXCOORD0; }; VertInOut VSDefault(VertInOut vert_in) // 顶点着色函数 { VertInOut vert_out; vert_out.pos = mul(float4(vert_in.pos.xyz, 1.0), ViewProj); vert_out.uv = vert_in.uv; return vert_out; } float4 PSDrawBare(VertInOut vert_in) : TARGET // 像素着色函数 { return image.Sample(def_sampler, vert_in.uv); } technique Draw // Technique:单 Pass { pass { vertex_shader = VSDefault(vert_in); pixel_shader = PSDrawBare(vert_in); } } technique DrawMultiply // 另一组像素逻辑,复用同一个 VS { pass { vertex_shader = VSDefault(vert_in); pixel_shader = PSDrawMultiply(vert_in); } }其中值得注意的要点:
uniform声明即参数入口:ViewProj、image、multiplier会成为该 effect 的gs_eparam_t参数,运行时通过gs_effect_get_param_by_name拿到后设置。- Technique 与 Pass 的对应:文件里定义了
Draw、DrawAlphaDivide、DrawTonemap、DrawPQ、DrawD65P3等十余个 Technique,每个 Technique 一个 Pass,Pass 内部指定vertex_shader = 函数(参数)与pixel_shader = 函数(参数)。解析器会按 Pass 自动生成着色器。 - 跨文件复用:color.effect 本身不含任何 Technique,只提供
srgb_nonlinear_to_linear、rec709_to_rec2020、reinhard(Reinhard 色调映射)、linear_to_st2084/st2084_to_linear(PQ/HDR)、HLG 等共享 HLSL 函数,供default.effect、opaque.effect 等通过#include "color.effect"引入——这正是 effect 机制“共享函数与参数”的设计目标。
解析逻辑由 effect-parser.c 完成,其头部注释直接点明了工作方式:“effect parser 接收一个 effect 文件,为每个 technique 的每个 pass 转换成独立着色器;它会自动把所有依赖的结构体/函数/参数写入着色器,并为每个 pass 的每个着色器组件构建着色器文本”。
三、创建与销毁:注意缓存语义
API 提供两个创建入口:
gs_effect_t *gs_effect_create_from_file(const char *file, char **error_string); gs_effect_t *gs_effect_create(const char *effect_string, const char *filename, char **error_string);| 参数 | 说明 |
|---|---|
file | effect 文件路径 |
effect_string | 直接传入 effect 的 HLSL 文本 |
filename | effect 字符串对应的(虚拟)文件名,用于路径解析与缓存键 |
error_string | 接收错误信息指针,必须用bfree()释放;传NULL则忽略该参数 |
| 返回值 | 成功返回 effect 对象,失败返回NULL |
一个关键实现细节:gs_effect_create_from_file会先查线程级缓存(graphics.c#L825-L860),同一文件路径第二次创建时直接返回缓存实例:
gs_effect_t *gs_effect_create_from_file(const char *file, char **error_string) { ... effect = find_cached_effect(file); if (effect) return effect; file_string = os_quick_read_utf8_file(file); ... effect = gs_effect_create(file_string, file, error_string); ... }而在gs_effect_create中,只要提供了effect_path,新建的 effect 就会被挂入thread_graphics->first_effect缓存链表并置位cached(graphics.c#L883-L893)。与之配套,销毁函数是这样实现的(effect.c#L30-L36):
void gs_effect_destroy(gs_effect_t *effect) { if (effect) { if (!effect->cached) gs_effect_actually_destroy(effect); } }因此实践中必须记住:凡是经过gs_effect_create_from_file(或带filename的gs_effect_create)创建的 effect 都是缓存实例,调用gs_effect_destroy是空操作;这类 effect 的生命周期由图形上下文管理。只有不携带文件名、完全临时的 effect 字符串才会被gs_effect_destroy真正释放。
OBS 自身就是这样使用的:视频系统初始化时通过obs_find_data_file定位内置文件,逐个调用gs_effect_create_from_file加载default.effect、opaque.effect、solid.effect、bicubic_scale.effect、lanczos_scale.effect等十余个文件,并挂到video->xxx_effect字段上供渲染管线全程使用(obs.c#L505-L550),且 OpenGL 后端会额外加载一份default_rect.effect。
四、Technique 与 Pass 的执行流程
执行一个 effect 的标准流程是:取 technique →begin→ 逐 passbegin_pass/end_pass→end:
gs_technique_t *tech = gs_effect_get_technique(effect, "Draw"); if (tech) { size_t num_passes = gs_technique_begin(tech); // 返回该 technique 的 pass 数 for (size_t i = 0; i < num_passes; i++) { if (gs_technique_begin_pass(tech, i)) { /* 此处进行绘制(draw) */ gs_technique_end_pass(tech); } } gs_technique_end(tech); }各函数职责与实现要点:
gs_effect_get_technique(effect, name):按名称线性查找 technique,未找到返回NULL(effect.c#L38-L50)。gs_effect_get_current_technique(effect):返回当前处于激活状态的 technique,无则返回NULL。gs_technique_begin(tech):把该 technique 设为 effect 的cur_technique并绑定到图形上下文(graphics->cur_effect),返回 pass 数量(effect.c#L101-L110)。gs_technique_begin_pass(tech, idx):核心步骤。它加载该 pass 的顶点/像素着色器(gs_load_vertexshader/gs_load_pixelshader),然后upload_parameters(effect, false)全量上传该 pass 用到的所有 uniform 参数(effect.c#L195-L212)。pass 索引越界返回false。gs_technique_begin_pass_by_name(tech, name):按名称查找 pass 并转调begin_pass,语义相同。gs_technique_end_pass(tech):结束当前 pass。实现上会清空该 pass 全部纹理参数(clear_tex_params把所有GS_SHADER_PARAM_TEXTURE类型的着色器纹理置为NULL,effect.c#L230-L256),避免下一帧绘制意外复用上一帧绑定的纹理。gs_technique_end(tech):结束 technique。调用前必须保证所有已开始的 pass 都已end_pass;它会卸载着色器(gs_load_vertexshader(NULL)等)、清空cur_effect,并把所有 effect 参数的cur_val重置为空、changed置false(effect.c#L112-L135)。
参数上传还有一个增量优化:upload_shader_params(..., changed_only)只上传changed == true的参数;pass 开始时的全量上传之后,reset_params会把已上传参数的changed标志清零,从而后续gs_effect_update_params只推送真正变化的值(effect.c#L137-L193)。
gs_effect_loop:官方推荐的简化写法
对于单 Pass technique(这是绝大多数情况),文档给出的推荐用法是gs_effect_loop辅助函数:
for (gs_effect_loop(effect, "my_technique")) { /* perform drawing here */ [...] }C 中对应的 while 形态在仓库各插件中随处可见,例如 xshm-input.c、gpu-delay.c 的while (gs_effect_loop(effect, "Draw")),Lua 脚本 API 中同样是while obs.gs_effect_loop(effect, "Draw") do ... end(clock-source.lua)。
从 effect.c#L60-L99 的实现可以读到它的完整语义:
- 首次调用时,它检查是否已有 effect 处于激活状态——
gs_get_effect()非空则记录警告gs_effect_loop: An effect is already active并返回false; - 找不到指定名称的 technique 时记录
Technique 'xxx' not found并返回false; - 否则自动执行
gs_technique_begin,进入循环体返回true; - 循环体再次执行完毕后的下一次调用:先
gs_technique_end_pass,再尝试begin_pass(loop_pass++);当 pass 用尽时自动gs_technique_end、复位循环状态并返回false,结束 for 循环。
也就是说gs_effect_loop把 technique 的 begin/end 和 pass 的 begin/end 全部包了进来,返回true的每一次循环迭代内都可以直接绘制。
五、参数访问与设置 API 全表
参数(gs_eparam_t)是 effect 与着色器之间传递数据的唯一通道。以下按 API 文档逐组说明,并结合 effect.c 的实现补充行为细节。
5.1 参数查询
| 函数 | 说明 |
|---|---|
size_t gs_effect_get_num_params(const gs_effect_t *effect) | 返回该 effect 的参数总数 |
gs_eparam_t *gs_effect_get_param_by_idx(effect, size_t param) | 按下标取参数,越界返回NULL |
gs_eparam_t *gs_effect_get_param_by_name(effect, const char *name) | 按名称取参数,未找到返回NULL |
void gs_effect_get_param_info(const gs_eparam_t *param, struct gs_effect_param_info *info) | 查询参数的名称与类型 |
参数类型与元信息结构在 API 文档中定义为:
enum gs_shader_param_type { GS_SHADER_PARAM_UNKNOWN, GS_SHADER_PARAM_BOOL, GS_SHADER_PARAM_FLOAT, GS_SHADER_PARAM_INT, GS_SHADER_PARAM_STRING, GS_SHADER_PARAM_VEC2, GS_SHADER_PARAM_VEC3, GS_SHADER_PARAM_VEC4, GS_SHADER_PARAM_INT2, GS_SHADER_PARAM_INT3, GS_SHADER_PARAM_INT4, GS_SHADER_PARAM_MATRIX4X4, GS_SHADER_PARAM_TEXTURE, }; struct gs_effect_param_info { const char *name; enum gs_shader_param_type type; };实现上gs_effect_get_param_info只填充name与type两个字段(effect.c#L359-L366)。
Annotation(注解)参数:effect 文件中的参数可以携带注解参数(对应 HLSL 的[xxx(...)]属性语法),API 提供:
| 函数 | 说明 |
|---|---|
size_t gs_param_get_num_annotations(const gs_eparam_t *param) | 注解数量 |
gs_eparam_t *gs_param_get_annotation_by_idx(param, size_t annotation) | 按下标取注解参数对象,越界返回NULL |
gs_eparam_t *gs_param_get_annotation_by_name(param, const char *annotation) | 按名称取注解参数对象,未找到返回NULL |
实现见 effect.c#L292-L321:每个gs_effect_param内部持有annotations动态数组,注解本身也是gs_effect_param,因此可以继续对它取值。
5.2 设置参数
所有 set 函数最终都汇聚到内部函数effect_setval_inline(effect.c#L368-L391):把新值 memcpy 进cur_val字节缓冲,并仅在值或大小真正变化时置changed = true——这就是“脏标记”,配合第四节描述的增量上传,未变化的参数不会反复推送到 GPU。
| 函数 | 设置的参数类型 | 实现细节 |
|---|---|---|
gs_effect_set_bool(gs_eparam_t *param, bool val) | BOOL | 以sizeof(int)字节写入 |
gs_effect_set_float(param, float val) | FLOAT | 4 字节 |
gs_effect_set_int(param, int val) | INT | 4 字节 |
gs_effect_set_matrix4(param, const struct matrix4 *val) | MATRIX4X4 | sizeof(struct matrix4) |
gs_effect_set_vec2(param, const struct vec2 *val) | VEC2 | sizeof(struct vec2) |
gs_effect_set_vec3(param, const struct vec3 *val) | VEC3 | 固定按float*3字节写入 |
gs_effect_set_vec4(param, const struct vec4 *val) | VEC4 | sizeof(struct vec4) |
gs_effect_set_color(param, uint32_t argb) | 便捷颜色函数 | 参数为0xAARRGGBB形式的整数颜色值;内部经vec4_from_bgra转成vec4后按 VEC4 写入 |
gs_effect_set_texture(param, gs_texture_t *val) | TEXTURE | 写入{tex, srgb=false} |
gs_effect_set_texture_srgb(param, gs_texture_t *val) | TEXTURE | 同上但srgb=true,即“优先使用 SRGB 视图采样” |
gs_effect_set_val(param, const void *val, size_t size) | 任意 | 手动传原始数据指针与字节数,适合自定义布局 |
gs_effect_set_default(param) | 任意 | 把参数恢复为 effect 文件中声明的默认值(default_val) |
gs_effect_set_next_sampler(param, gs_samplerstate_t *sampler) | 仅 TEXTURE | 为该纹理参数挂一个“下一次使用时的采样器”;实现中只有param->type == GS_SHADER_PARAM_TEXTURE才会生效(effect.c#L547-L556) |
纹理设置的实现(effect.c#L473-L487)表明:纹理参数在cur_val中实际存的是一个gs_shader_texture结构{gs_texture_t *tex; bool srgb;},srgb标志决定 GPU 层是否切换到 SRGB 采样视图。
gs_effect_set_next_sampler的“next”语义对应参数结构中的next_sampler字段:它在下一次该参数被上传到着色器时生效(upload_shader_params中先处理next_sampler再处理值,effect.c#L146-L171),且会被gs_technique_end清空,不会跨 technique 残留。
5.3 读取参数值
| 函数 | 返回值与约定 |
|---|---|
void *gs_effect_get_val(gs_eparam_t *param) | 返回当前值的一份字节拷贝,无当前值时返回NULL;必须用bfree()释放 |
void *gs_effect_get_default_val(gs_eparam_t *param) | 返回默认值的一份字节拷贝,无默认值时返回NULL;同样用bfree()释放 |
size_t gs_effect_get_val_size(gs_eparam_t *param) | 当前值的字节大小 |
size_t gs_effect_get_default_val_size(gs_eparam_t *param) | 默认值的字节大小 |
实现见 effect.c#L494-L540:gs_effect_get_val用bzalloc(size)分配内存后 memcpycur_val。这意味着你无法拿到参数内部的指针引用,而是拿到一个可自由支配的副本,读取前应先调用gs_effect_get_val_size确定字节长度。
5.4 矩阵参数快捷入口
| 函数 | 说明 |
|---|---|
gs_eparam_t *gs_effect_get_viewproj_matrix(const gs_effect_t *effect) | 返回 effect 中视图投影矩阵参数(即viewproj)的对象,供直接gs_effect_set_matrix4 |
gs_eparam_t *gs_effect_get_world_matrix(const gs_effect_t *effect) | 返回世界矩阵参数(即world)的对象 |
从源码结构看,struct gs_effect中预置了view_proj、world、scale三个gs_eparam_t *成员(effect.h#L157),解析阶段会为常用矩阵名建立快捷引用。OBS 渲染管线中场景坐标变换正是通过gs_effect_get_viewproj_matrix拿到该参数后每帧写入matrix4的。
六、在 OBS 插件与脚本中的实际调用方式
API 文档是 libobs 的 C 接口说明,OBS 的代码库本身就是最权威的调用示例集合:
- C 插件:xshm-input.c 中创建 effect 后以
while (gs_effect_loop(effect, "Draw")) { gs_effect_set_texture(image, tex); /* ... 提交顶点与 draw ... */ }的模式渲染 X11 共享内存帧缓冲;decklink-ui-main.cpp 的 DeckLink 输出预览、nvidia-videofx-filter.c 的 NVIDIA 滤镜、transition-fade-to-color.c 的转场都遵循同一套
gs_effect_create_from_file→gs_effect_loop→ 设置参数 → 绘制的流程。 - Lua 脚本:obs-scripting 层把同一批函数暴露为
obs.gs_effect_*,仓库自带的 clock-source.lua 展示了脚本内创建自定义 effect 并用while obs.gs_effect_loop(effect, "Draw")绘制时钟数字的完整例子。 - libobs 自身:如第五节所述,
obs.c初始化时加载的全部内置 effect 文件,就是这套 API 在核心渲染管线中的使用样板。
七、调用流程小结与易错点
把前文串起来,一个完整的最小调用序列是:
#include <graphics/graphics.h> char *error_string = NULL; gs_effect_t *effect = gs_effect_create_from_file("my.effect", &error_string); if (!effect) { /* 处理 error_string(记得 bfree) */ } gs_eparam_t *image = gs_effect_get_param_by_name(effect, "image"); gs_eparam_t *mult = gs_effect_get_param_by_name(effect, "multiplier"); gs_eparam_t *vp = gs_effect_get_viewproj_matrix(effect); for (gs_effect_loop(effect, "Draw")) { gs_effect_set_texture(image, texture); gs_effect_set_float(mult, 1.0f); gs_effect_set_matrix4(vp, &view_proj_matrix); /* 提交顶点缓冲并 gs_draw */ } /* 无需 gs_effect_destroy:来自文件的 effect 为缓存实例,销毁是空操作 */易错点汇总(均有源码依据):
- 不要对文件创建的 effect 依赖
gs_effect_destroy——缓存实例的销毁是 no-op(effect.c#L30-L36); error_string、gs_effect_get_val/gs_effect_get_default_val的返回值都必须bfree();- 同一时刻只允许一个 effect 处于激活状态,嵌套使用
gs_effect_loop会触发警告并直接返回false(effect.c#L69-L73); - pass 必须按
begin_pass → 绘制 → end_pass成对出现,gs_technique_end前所有 pass 须已结束;end_pass会自动解绑纹理,下一帧使用前记得重新gs_effect_set_texture; gs_effect_set_color的入参是0xAARRGGBB整数(内部经 BGRA 转vec4),不要误传0xRRGGBBAA;- 纹理参数想换采样方式时用
gs_effect_set_next_sampler,它只对GS_SHADER_PARAM_TEXTURE类型生效,且仅在下次上传时生效。
掌握以上内容后,你就能按 API 参考文档 中列出的每一个gs_effect_*函数在 OBS 插件、滤镜或脚本中正确加载 effect 文件、驱动 technique/pass、设置和回读全部类型的 uniform 参数,并理解参数脏标记、缓存实例与纹理自动解绑这些底层行为对调试渲染问题的意义。
【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考