OBS Studio 渲染图形子系统:Effect 文件语法与视频渲染管线实战指南
本文基于 OBS Studio 官方文档 Rendering Graphics 展开,系统讲解 libobs 自研图形子系统的设计理念、graphics context 线程模型、effect 文件(uniform/sampler/语义/technique)的完整语法,以及视频源与滤镜的渲染流程。读完本文,你将能够独立编写 libobs effect 文件、在 video_render 回调中正确使用 effect 参数并实现自定义视频滤镜,并可对照仓库内 default.effect 与 color-key-filter.c 等真实源码验证每个知识点。
图形子系统的设计初衷:为什么要"自研"
libobs 拥有一个自研的可编程图形子系统,统一封装了 Direct3D 11 与 OpenGL 两种后端。文档原文明确给出了设计动机:
Libobs has a custom-made programmable graphics subsystem that wraps both Direct3D 11 and OpenGL. The reason why it was designed with a custom graphics subsystem was to accommodate custom capture features only available on specific operating systems. (libobs 拥有一个自研的可编程图形子系统,封装了 Direct3D 11 和 OpenGL。之所以设计自研图形子系统,是为了适配仅在特定操作系统上可用的自定义采集功能。)
作者还附了一条坦诚的备注:回顾来看本可以基于 ANGLE 之类的方案,但为了满足特定用例仍选择了自研。
从当前仓库的源码结构看,这套子系统已经演化出三个后端实现:
- libobs-d3d11/ —— Direct3D 11 后端(含 d3d11-shaderprocessor.cpp 等);
- libobs-opengl/ —— OpenGL 后端(含 gl-shaderparser.c 等);
- libobs-metal/ —— 面向 macOS 的 Metal 后端。
文档主体只提到 D3D 11 与 OpenGL,仓库中 Metal 后端属于文档之外的后续扩展,读者按文档语义理解 D3D/OpenGL 双后端即可。而文档所说的"effect"正是这个子系统的核心概念:libobs 中所有视频对象(source)的渲染都依赖 effect。effect 的作用是把相关联的 vertex/pixel shader 打包进同一个文件,便于共享函数与参数、快速切换 shader。effect 的核心数据结构定义在 libobs/graphics/effect.h 中,包括 gs_effect(持有 params 参数数组与 techniques 数组)、gs_effect_technique(持有若干 pass)与 gs_effect_pass(持有顶点/像素 shader 及其参数映射):
// libobs/graphics/effect.h
struct gs_effect_technique {
char *name;
enum effect_section section;
struct gs_effect *effect;
DARRAY(struct gs_effect_pass) passes;
};
struct gs_effect {
bool processing;
bool cached;
char *effect_path, *effect_dir;
gs_effect_param_array_t params; // 所有 uniform 参数
DARRAY(struct gs_effect_technique) techniques; // 所有 technique
struct gs_effect_technique *cur_technique;
struct gs_effect_pass *cur_pass;
gs_eparam_t *view_proj, *world, *scale;
graphics_t *graphics;
...
};
从源码注释可以印证文档的说法:"Effects are built via the effect parser, and shaders are automatically generated for each technique's pass"(effect 由 effect parser 构建,每个 technique 的 pass 会自动生成对应的 shader),对应的解析器位于 libobs/graphics/effect-parser.c。
Graphics Context:进入图形上下文的规则
使用任何图形函数之前,当前线程必须已进入 graphics context;而 graphics context 同一时刻只能被一个线程使用。文档给出了明确的进入/离开方式:
- 进入:
obs_enter_graphics() - 离开:
obs_leave_graphics()
这两个函数的实现在 libobs/obs.c 中非常简短,本质是对 graphics_t 上下文的进入/离开转发:
void obs_enter_graphics(void)
{
if (obs->video.graphics)
gs_enter_context(obs->video.graphics);
}
void obs_leave_graphics(void)
{
if (obs->video.graphics)
gs_leave_context();
}
函数声明位于 libobs/obs.h。值得注意的是源码中的判空保护:视频尚未初始化(obs->video.graphics 为空)时调用是安全的空操作。
文档同时列出了自动处于 graphics context 中的回调,在这些回调里无需手动 enter/leave:
obs_source_info.video_render—— 每个 source 的渲染回调(同步视频源即在此绘制);obs_display_add_draw_callback()的绘制回调参数;obs_add_main_render_callback()注册的主渲染回调。
这条规则的实际意义是:绝大多数插件开发者的绘图代码写在 video_render 里,天然满足上下文约束;只有当你需要在非渲染线程(例如自己的 worker 线程)中操作纹理时才需要显式 obs_enter_graphics() / obs_leave_graphics(),并注意同一时间只能有一个线程持有该上下文。
Effect 文件语法:与 HLSL Effect 的三个差异
effect 文件的语法与 Direct3D 11 HLSL effect 文件几乎完全一致,只有以下三处差异:
| 差异点 | libobs effect 写法 | D3D 11 HLSL 写法 |
|---|---|---|
| 采样器状态关键字 | sampler_state |
sampler |
| 位置语义 | POSITION |
SV_Position |
| 目标语义 | TARGET |
SV_Target |
(文档附注:作者自述可能遗漏了个别的差异项。)
创建 Effect:参数(Uniform)
文档建议编写 effect 时先确定 uniform(参数)。支持的 uniform 类型如下表:
| 类别 | 类型 |
|---|---|
| 浮点数 | float、float2、float3、float4 |
| 矩阵 | float3x3、float4x4 |
| 整数 | int、int2、int3、int4 |
| 布尔 | bool |
| 纹理 | texture2d、texture_cube |
获取参数的两个 API:
gs_effect_get_param_by_name()—— 按名字获取;gs_effect_get_param_by_idx()—— 按索引获取。
设置参数的函数族:
gs_effect_set_bool()gs_effect_set_float()gs_effect_set_int()gs_effect_set_matrix4()gs_effect_set_vec2()gs_effect_set_vec3()gs_effect_set_vec4()gs_effect_set_texture()gs_effect_set_texture_srgb()
两个"通用"参数:ViewProj 与 image
文档特别指出,effect 可能会被期望具备两个通用参数:
- ViewProj(
float4x4):主视图/投影矩阵的乘积,顶点着色器用它把模型空间坐标变换到裁剪空间; - image(
texture2d):主纹理。obs_source_draw()、gs_draw_sprite()、gs_draw_quadf()以及obs_source_process_filter_end()等函数都约定使用该参数名传入"当前要绘制/处理的纹理"。
effect 文件中声明参数的示例:
uniform float4x4 ViewProj;
uniform texture2d image;
uniform float4 my_color_param;
uniform float my_float_param;
Uniform 默认值
参数可以带默认值;多元素类型(向量、矩阵)的默认值按数组方式书写。文档给出的示例:
uniform float4x4 my_matrix = {1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0};
uniform float4 my_float4 = {1.0, 0.5, 0.25, 0.0};
uniform float my_float = 4.0;
uniform int my_int = 5;
在源码层面,每个 uniform 由 struct gs_effect_param 承载(见 libobs/graphics/effect.h),其中的 cur_val(当前值)与 default_val(默认值)正是"可覆盖的默认参数"机制的落地位置,changed 标志位用于标记该参数是否被运行时修改过。
Effect 采样器状态(sampler_state)
如果 effect 使用了纹理,就应该定义采样器状态。采样器状态包含以下子参数:
Filter —— 过滤方式,可选值:
AnisotropyPointLinearMIN_MAG_POINT_MIP_LINEARMIN_POINT_MAG_LINEAR_MIP_POINTMIN_POINT_MAG_MIP_LINEARMIN_LINEAR_MAG_MIP_POINTMIN_LINEAR_MAG_POINT_MIP_LINEARMIN_MAG_LINEAR_MIP_POINT
AddressU / AddressV —— 纹理坐标越出 0.0..1.0 区间时的采样行为,可选值:
Wrap或RepeatClamp或NoneMirrorBorder(使用 BorderColor 填充颜色)MirrorOnce
BorderColor —— 使用 Border 寻址模式时的边界颜色,写成 AARRGGBB 格式的十六进制数。例如 7FFF0000 表示 alpha 为 127、红色 255、绿色与蓝色为 0。若未使用 Border 寻址,此值被忽略。
文档给出的采样器状态写法示例:
sampler_state defaultSampler {
Filter = Linear;
AddressU = Border;
AddressV = Border;
BorderColor = 7FFF0000;
};
该采样器使用线性过滤,对越界的纹理坐标采用边界寻址,边界颜色为上述半透明红色。
仓库中最标准的样例是 libobs/data/default.effect,所有基础渲染都基于它:
sampler_state def_sampler {
Filter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
在像素着色器中使用采样器
采样器状态的使用方式与 HLSL 形式一致:声明 texture2d 与 sampler_state 后,在像素着色器中调用 Sample。文档示例:
uniform texture2d image;
sampler_state defaultSampler {
Filter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
float4 MyPixelShaderFunc(VertInOut vert_in) : TARGET
{
return image.Sample(def_sampler, vert_in.uv);
}
顶点/像素语义(Semantics)
需要为输入/输出定义顶点语义结构。顶点分量可用的语义:
- COLOR —— 颜色值(
float4); - POSITION —— 位置值(
float4); - NORMAL —— 法线值(
float4); - TANGENT —— 切线值(
float4); - TEXCOORD[0..7] —— 纹理坐标值(
float2、float3或float4)。
顶点语义结构示例:
struct VertexIn {
float4 my_position : POSITION;
float2 my_texcoord : TEXCOORD0;
};
规则要点(文档原文归纳):
- 该语义结构作为顶点 shader 入口函数的参数,并作为顶点 shader 的返回值类型;
- 顶点 shader 允许返回与输入不同的语义集合,但顶点 shader 的返回类型必须与像素 shader 的参数类型匹配;
- 顶点 shader 函数参数中出现的语义,决定了顶点缓冲区必须提供对应内容——例如声明了 POSITION 与 TEXCOORD0,则顶点缓冲至少要包含位置缓冲和纹理坐标缓冲;
- 像素 shader 必须以 TARGET 语义返回一个
float4(RGBA)。
Technique 与 Pass
Technique 用于按 pass 定义每对顶点/像素 shader 入口函数;一个 technique 可以有多个 pass 或自定义 pass 设置。文档中作者给出了非常实用的建议:
如今多 pass 已经没什么必要,GPU 足够强大,可以在同一个 shader 中完成全部操作。命名 pass 对自定义绘制布局或许有些用处,但你也可以直接把它写成独立的 technique。因此建议直接忽略多余 pass 功能。
一个重要的命名约定:如果你在为视频源编写 effect 滤镜,通常把 pass 命名为 Draw——因为 obs_source_process_filter_end() 会自动调用名为 Draw 的 technique;如需指定其他 technique,可使用 obs_source_process_filter_tech_end() 按名字显式选择。
此外,pass 中顶点/像素 shader 函数的第一个参数必须始终是其顶点语义结构的名称。文档给出的完整 technique 示例(可直接照抄作为骨架):
uniform float4x4 ViewProj;
uniform texture2d image;
struct VertInOut {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
};
VertInOut MyVertexShaderFunc(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 MyPixelShaderFunc(VertInOut vert_in) : TARGET
{
return image.Sample(def_sampler, vert_in.uv);
}
technique Draw
{
pass
{
vertex_shader = MyVertexShaderFunc(vert_in);
pixel_shader = MyPixelShaderFunc(vert_in);
}
};
对照仓库中的 default.effect,可以看到真实项目里一个 effect 文件可以同时定义多个像素着色器(PSDrawBare、PSDrawAlphaDivide、PSDrawAlphaDivideTonemap、PSDrawAlphaDivideR10L 等),再分别挂到不同 technique 上,分别服务"裸绘制""alpha 分离""HDR tone map""10bit PQ 编码"等不同渲染路径。
使用 Effect:gs_effect_loop
文档推荐的 effect 使用方式:
for (gs_effect_loop(effect, "technique")) {
[draw calls go here]
}
该循环会自动处理给定 technique 名下 effect 及其 shader 的加载/卸载。从 libobs/graphics/effect.h 的 gs_effect 结构可以看到支撑这一机制的字段:cur_technique、cur_pass、loop_pass 与 looping 正是 gs_effect_loop 迭代 pass 状态机的内部状态。
渲染视频源(Video Sources)
文档对同步视频源的渲染路径给出明确描述:
- 同步视频源在自己的
obs_source_info.video_render回调中渲染; - source 可以选择自定义绘制(
OBS_SOURCE_CUSTOM_DRAW输出能力标志),也可以不用; - 不使用自定义绘制时,推荐用
obs_source_draw()绘制单张纹理; - 使用自定义绘制时,source 需自行完成渲染并自行管理 effect。
obs_source_draw() 的原型见 libobs/obs.h:
EXPORT void obs_source_draw(gs_texture_t *image, int x, int y,
uint32_t cx, uint32_t cy, bool flip);
此外,libobs 自带一组默认/标准 effect,可通过 obs_get_base_effect() 获取;既可以直接用这些 effect 渲染,也可以用 gs_effect_create_from_file() 创建自定义 effect 再渲染。
当前仓库中 obs_get_base_effect() 支持的枚举完整列于 libobs/obs.h:
enum obs_base_effect {
OBS_EFFECT_DEFAULT, /**< RGB/YUV */
OBS_EFFECT_DEFAULT_RECT, /**< RGB/YUV (using texture_rect) */
OBS_EFFECT_OPAQUE, /**< RGB/YUV (alpha set to 1.0) */
OBS_EFFECT_SOLID, /**< RGB/YUV (solid color only) */
OBS_EFFECT_BICUBIC, /**< Bicubic downscale */
OBS_EFFECT_LANCZOS, /**< Lanczos downscale */
OBS_EFFECT_BILINEAR_LOWRES, /**< Bilinear low resolution downscale */
OBS_EFFECT_PREMULTIPLIED_ALPHA, /**< Premultiplied alpha */
OBS_EFFECT_REPEAT, /**< RGB/YUV (repeating) */
OBS_EFFECT_AREA, /**< Area rescale */
};
EXPORT gs_effect_t *obs_get_base_effect(enum obs_base_effect effect);
实现位于 libobs/obs.c 的 obs_get_base_effect()。这些基础 effect 对应的源文件就放在 libobs/data/ 目录下,共 21 个 .effect 文件(如 default.effect、default_rect.effect、bicubic_scale.effect、lanczos_scale.effect、solid.effect、deinterlace_*.effect 等),与上述枚举一一对应,读者可逐一打开对照效果实现。
渲染视频效果滤镜(Video Effect Filters)
文档指出:大多数视频效果滤镜,就是在 video_render 回调中为已有图像叠加一层处理 shader。此时的标准流程是:
- 滤镜拥有自己的 effect(用
gs_effect_create_from_file()创建); - 调用
obs_source_process_filter_begin()开始处理; - 在自己的 effect 上设置参数(
gs_effect_set_*系列); - 调用
obs_source_process_filter_end()(或obs_source_process_filter_tech_end()指定 technique)完成渲染。
文档以色度键(color key)滤镜为例给出了完整渲染函数:
static void color_key_render(void *data, gs_effect_t *effect)
{
struct color_key_filter_data *filter = data;
if (!obs_source_process_filter_begin(filter->context, GS_RGBA,
OBS_ALLOW_DIRECT_RENDERING))
return;
gs_effect_set_vec4(filter->color_param, &filter->color);
gs_effect_set_float(filter->contrast_param, filter->contrast);
gs_effect_set_float(filter->brightness_param, filter->brightness);
gs_effect_set_float(filter->gamma_param, filter->gamma);
gs_effect_set_vec4(filter->key_color_param, &filter->key_color);
gs_effect_set_float(filter->similarity_param, filter->similarity);
gs_effect_set_float(filter->smoothness_param, filter->smoothness);
obs_source_process_filter_end(filter->context, filter->effect, 0, 0);
UNUSED_PARAMETER(effect);
}
这段示例并非纸上谈兵——它几乎逐行对应仓库中的真实实现。plugins/obs-filters/color-key-filter.c 中的 color_key_render_v1():
static void color_key_render_v1(void *data, gs_effect_t *effect)
{
struct color_key_filter_data *filter = data;
if (!obs_source_process_filter_begin(filter->context, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING))
return;
gs_effect_set_vec4(filter->color_param, &filter->color);
gs_effect_set_float(filter->contrast_param, filter->contrast);
gs_effect_set_float(filter->brightness_param, filter->brightness);
gs_effect_set_float(filter->gamma_param, filter->gamma);
gs_effect_set_vec4(filter->key_color_param, &filter->key_color);
gs_effect_set_float(filter->similarity_param, filter->similarity);
gs_effect_set_float(filter->smoothness_param, filter->smoothness);
obs_source_process_filter_end(filter->context, filter->effect, 0, 0);
UNUSED_PARAMETER(effect);
}
从该文件的源码结构看,v1 版本通过 obs_module_file("color_key_filter.effect") 定位 effect 文件创建滤镜;仓库中还实现了 color_key_render_v2(),它会先用 obs_source_get_color_space() 在 GS_CS_SRGB / GS_CS_SRGB_16F / GS_CS_709_EXTENDED 之间按优先级选取源色彩空间,再据此选择不同的处理路径——这是在"begin → set params → end"基础流程之上的色彩空间增强用法,供需要处理宽色域素材的滤镜参考。
obs_source_process_filter_begin() / obs_source_process_filter_end() 的实现位于 libobs/obs-source.c,其中 obs_source_process_filter_begin 默认以 GS_CS_SRGB 色彩空间委托给 obs_source_process_filter_begin_with_color_space();而 obs_source_process_filter_end() 在滤镜没有自定义 effect 时,会回退到 obs_get_base_effect(OBS_EFFECT_DEFAULT) 完成透传绘制(见 libobs/obs-source.c),这也解释了为什么"滤镜不处理时图像依然能正常走管线"。
小结:一张对照表串起整条管线
| 阶段 | 关键 API / 概念 | 仓库参考位置 |
|---|---|---|
| 线程进入图形上下文 | obs_enter_graphics() / obs_leave_graphics() |
libobs/obs.c |
| 自动处于上下文的回调 | video_render、display 绘制回调、主渲染回调 |
docs/sphinx/graphics.rst |
| 编写 effect | uniform / sampler_state / 语义 / technique(D3D11 语法三处差异) | libobs/data/default.effect |
| 参数读写 | gs_effect_get_param_by_name/idx()、gs_effect_set_*() |
libobs/graphics/effect.h |
| 使用 effect | gs_effect_loop(effect, "technique") |
libobs/graphics/effect.h |
| 源绘制 | obs_source_draw()、OBS_SOURCE_CUSTOM_DRAW、obs_get_base_effect() |
libobs/obs.h |
| 滤镜流程 | obs_source_process_filter_begin/end()(或 _tech_end() 指定 technique) |
plugins/obs-filters/color-key-filter.c、libobs/obs-source.c |
掌握以上内容后,编写 OBS 视频滤镜的完整路径就是:创建 effect 文件(参数 → 采样器 → 语义结构 → 命名 Draw 的 technique)→ 在 create 回调中用 gs_effect_create_from_file() 载入并用 gs_effect_get_param_by_name() 取参 → 在 video_render 回调中按 begin → set params → end 三步完成每帧处理。文中所有示例均可在对应源码路径下直接验证,适用前提是基于当前仓库版本的 libobs API 开发插件。
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 StartedRust0622
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