第 29 课:写你的第一个插件 —— 一个简单的视频滤镜
# 第 29 课:写你的第一个插件 —— 一个简单的视频滤镜
嘿,我是小方。 前面 28 课,我们一直在读 OBS。从这一课起(模块 8),换个姿势:动手写。我们要往 OBS 里加一个你自己的东西 —— 最小、最好上手的一种插件:视频滤镜。 具体造什么?一个"亮度"滤镜:给画面整体调亮/调暗,带一个可拖的滑块。麻雀虽小,五脏俱全 —— 它会把这门课学的东西第一次拼在一起:插件登记(第 6/9 课)、着色器(第 11 课)、滤镜三步(第 12 课)、属性面板(第 27 课)。写完你会发现:所有滤镜,都是这一个形状。 ⚠️ 提醒:OBS 官方不接受"主要由 AI 生成"的代码提交(
CONTRIBUTING.rst)。这一课的代码是学习/演示用的 —— 目的是让你理解滤镜怎么搭,然后写出你自己真正理解的东西,而不是照抄提交上游。
# 29.1 先想清楚:一个 OBS 插件 = 什么?
📍我们在哪:整门课的最后一段。前面都在"读懂 OBS 怎么做",现在要"让 OBS 照你写的做"。第一步,先看清"插件"是个什么东西。

一个 OBS 插件 = 一个"动态库"(.dll / .so / .dylib),OBS 启动时把它加载进来(第 9 课),它在 obs_module_load 里"登记"自己能提供什么 —— 源 / 滤镜 / 编码器 / 输出 / 服务……
最小、最好上手的一种,就是视频滤镜(第 12 课)—— 本质是一个"给画面做点处理的源"。我们这一课造一个**"亮度"滤镜**,拆成 6 块拼图,一块块拼:
① 模块骨架 → ② 登记表(我是滤镜) → ③ create 加载着色器 → ④ 属性(滑块) → ⑤ video_render 三步 → ⑥ .effect 着色器。
下面按这个顺序来。全程我会对着 OBS 自带的 sharpness-filter(锐化滤镜)写 —— 你写的每一行,官方滤镜里都有对应。
# 29.2 拼图 ①:插件骨架 —— 向 OBS "报到"
📍第 9 课的另一端:第 9 课讲"OBS 怎么加载插件";这一节讲"插件这边怎么让自己被加载"。

新建一个 my-brightness-filter.c,先写骨架:
#include <obs-module.h>
OBS_DECLARE_MODULE() // 生成版本导出等样板
OBS_MODULE_USE_DEFAULT_LOCALE("my-brightness", "en-US") // 多语言文本支持
extern struct obs_source_info brightness_filter; // 拼图 ② 在下面定义
bool obs_module_load(void) // ★ OBS 加载这个 .dll 后,会调它
{
obs_register_source(&brightness_filter); // 把滤镜登记进引擎的「源类型表」
return true; // 报到成功
}
2
3
4
5
6
7
8
9
10
11
12
三个东西在起作用:
OBS_DECLARE_MODULE()(libobs/obs-module.h:76):生成一堆必需的样板,其中最关键的是obs_module_ver(:87),它返回LIBOBS_API_VER—— 报告"我针对哪个 libobs API 版本编译的"。版本大版本不匹配,会被直接拒绝加载(第 9 课那道"版本闸门")。obs_module_load(声明在obs-module.h:101):插件的入口。OBS 加载.dll后,用dlsym(第 9 课)找到它、调用它。你在这里obs_register_source把滤镜登记进去。return true= "我准备好了"。返回 false 会让 OBS 卸载这个插件。
这就是第 9 课"插件加载"的完整两端:OBS 那边 dlopen + dlsym + 调 obs_module_load;你这边 OBS_DECLARE_MODULE + obs_module_load + obs_register_source。握手完成。(对照真代码:plugins/obs-filters/obs-filters.c:3-4、:42、:58、:75。)
# 29.3 拼图 ②:登记表 —— 告诉 OBS "我是一个滤镜"
📍又是那张表:第 6 课的
obs_source_info,第 N 次出现。这次填"滤镜"该有的字段。

// 你的私有数据:每个滤镜实例一份(仿 sharpness-filter.c:6)
struct brightness_data {
obs_source_t *context; // 指回这个滤镜实例
gs_effect_t *effect; // 加载好的着色器
gs_eparam_t *bright_param; // 着色器里 "brightness" 参数的「把手」
float brightness; // 当前亮度值
};
struct obs_source_info brightness_filter = {
.id = "my_brightness_filter",
.type = OBS_SOURCE_TYPE_FILTER, // ★ 我是滤镜(不是输入源)
.output_flags = OBS_SOURCE_VIDEO, // ★ 我处理视频
.get_name = bright_get_name,
.create = bright_create,
.destroy = bright_destroy,
.update = bright_update,
.video_render = bright_render, // ★ 每帧干活
.get_properties = bright_props,
.get_defaults = bright_defaults,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这和第 6 课的输入源、第 12 课的滤镜,是同一张 obs_source_info。 差别只在:type = OBS_SOURCE_TYPE_FILTER(而非 INPUT),而且只填 video_render(不管采集/video_tick 那些)。
引擎根本不认识"亮度"这个概念 —— 它只会照着这张表:该建实例时调 create、该画时调 video_render、设置变了调 update……"登记表 + 回调",这门课见了无数次的老规矩,你现在自己在用它。(对照:sharpness-filter.c:145-156。)
填好这张表,OBS 的"滤镜"列表里就多了你这一项。剩下的,是把那几个回调实现出来。
# 29.4 拼图 ③:create / destroy —— 加载着色器、抓参数把手
📍建实例:引擎第一次用你的滤镜时调
create。它要做的:开内存、加载着色器、拿到参数的"把手"。

static void *bright_create(obs_data_t *settings, obs_source_t *context)
{
struct brightness_data *data = bzalloc(sizeof(*data)); // ① 开一块私有数据
data->context = context;
char *path = obs_module_file("brightness.effect"); // ② 找到 .effect 文件
obs_enter_graphics(); // ③ 进 GPU 上下文
data->effect = gs_effect_create_from_file(path, NULL); // ④ 编译加载着色器
if (data->effect)
data->bright_param = // ⑤ 抓参数「把手」
gs_effect_get_param_by_name(data->effect, "brightness");
obs_leave_graphics();
bfree(path); // ⑥ 收尾
if (!data->effect) { bright_destroy(data); return NULL; }
bright_update(data, settings); // 应用初始设置
return data;
}
static void bright_destroy(void *d)
{
struct brightness_data *data = d;
obs_enter_graphics();
gs_effect_destroy(data->effect); // 毁着色器,也要在 graphics 上下文里
obs_leave_graphics();
bfree(data); // 释放私有数据(和 create 对称)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
两个关键词:
.effect文件 = 着色器代码(拼图 ⑥,写"画面怎么处理")。obs_module_file帮你在插件的data/文件夹里找到它(obs-module.h:155)。- 参数把手(
gs_eparam_t *)= 指向着色器里那个uniform的"遥控器"。create时抓一次,以后每帧用它给着色器塞新值(比每帧按名字查快)。
🔧 一条铁律:凡是碰 GPU 资源(建/毁着色器),都要夹在
obs_enter_graphics()/obs_leave_graphics()之间(第 11 课)—— 因为 GPU 操作必须在图形上下文里做。(对照:sharpness-filter.c:44-71;destroy在:31-42。)
# 29.5 拼图 ④:属性 —— 给用户一个能拖的滑块
📍第 27 课的兑现:你只"声明"有个滑块,OBS 的
properties-view自动把对话框画出来。你一行 Qt 都不用写。

// 声明有哪些控件 → OBS 据此自动画界面(第 27 课)
static obs_properties_t *bright_props(void *data)
{
obs_properties_t *props = obs_properties_create();
obs_properties_add_float_slider(props, "brightness",
obs_module_text("Brightness"), -1.0, 1.0, 0.01); // 名字、范围、步长
UNUSED_PARAMETER(data);
return props;
}
// 默认值:没设过时用 0.0(不变亮也不变暗)
static void bright_defaults(obs_data_t *s)
{
obs_data_set_default_double(s, "brightness", 0.0);
}
// 设置变了 → 把新值从 obs_data 读出来,存进私有 data,供 render 用
static void bright_update(void *d, obs_data_t *s)
{
struct brightness_data *data = d;
data->brightness = (float)obs_data_get_double(s, "brightness");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
三个回调的分工(和第 27 课对上):
get_properties:声明"有个叫brightness的浮点滑块,范围 -1~1" —— OBS 据此画界面;get_defaults:没设过时用0.0;update:用户一改,把新值从obs_data读出来,存进你的私有data,供render用。
用户拖动滑块 → bright_update 被调 → 值进了 data->brightness。你没写一行 Qt 代码,滑块却自己出现了 —— 这就是第 27 课"数据驱动 UI"的好处。(对照:sharpness-filter.c:110-124。)
# 29.6 拼图 ⑤:video_render —— 每帧真正干活(三步)
📍第 12 课的三步,现在是你的滤镜在做:begin 准备、塞参数、end 画出来。

static void bright_render(void *d, gs_effect_t *effect)
{
struct brightness_data *data = d;
UNUSED_PARAMETER(effect);
// ① begin:把「上一层的画面」准备成一张着色器能采样的纹理
if (!obs_source_process_filter_begin(data->context, GS_RGBA,
OBS_ALLOW_DIRECT_RENDERING))
return;
// ② 塞参数:把亮度值,通过「把手」塞进着色器的 uniform
gs_effect_set_float(data->bright_param, data->brightness);
// ③ end:用你的 .effect 把这一帧画出来(GPU 执行)
obs_source_process_filter_end(data->context, data->effect, 0, 0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
就这三步:
obs_source_process_filter_begin(libobs/obs.h:1469):引擎给的"夹子"的前半 —— 它替你把"上一层画面"变成着色器能采样的纹理;gs_effect_set_float:通过create时抓的那个把手,把brightness值塞进着色器;obs_source_process_filter_end(libobs/obs.h:1483):夹子的后半 —— 用你的.effect把结果画到目标。
中间"怎么处理像素"全在 .effect 里。你在 C 这边要做的,只是"把这一帧需要的参数塞进去"。 数据流:上一层画面 → 你的着色器 → 下一层。和第 12 课一模一样的三步 —— 只不过这次,是你写的滤镜。(真代码 sharpness-filter.c:73-108 还多了色彩空间/HDR 处理,那是进阶,先不管。)
# 29.7 拼图 ⑥:.effect 着色器 —— 采样 → 算 → 输出
📍滤镜的灵魂:真正"处理像素"的地方,在 GPU 上对每个像素并行跑(第 11 课)。

新建 data/brightness.effect:
uniform float4x4 ViewProj; // 定位矩阵(第 13 课)
uniform texture2d image; // ★ 上一层的画面(滤镜的输入)
uniform float brightness; // ★ C 那边 gs_effect_set_float 塞进来的值
sampler_state def_sampler { Filter = Linear; AddressU = Clamp; AddressV = Clamp; };
struct VertData { float4 pos : POSITION; float2 uv : TEXCOORD0; };
VertData VSDefault(VertData v) // 顶点着色器:定位(套路,几乎不动)
{
VertData o;
o.pos = mul(float4(v.pos.xyz, 1.0), ViewProj);
o.uv = v.uv;
return o;
}
float4 PSBrightness(VertData v) : TARGET // 像素着色器:算一个像素
{
float4 c = image.Sample(def_sampler, v.uv); // ① 采样这个像素
c.rgb += brightness; // ② 加亮度(核心一行!)
return c; // ③ 输出
}
technique Draw {
pass { vertex_shader = VSDefault(v); pixel_shader = PSBrightness(v); }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
看懂这段:
image= 上一层画面(第 12 课那个"过滤器的输入");brightness= 你在 C 里gs_effect_set_float塞进来的那个值,两边靠"名字"对上;- 核心就一行:
c.rgb += brightness—— 每个像素的颜色加一点,正 = 变亮,负 = 变暗; - GPU 对"每个像素"同时跑这段(第 11 课),所以飞快。
一个滤镜的"灵魂",就是这个像素着色器里那几行数学。换一行,就是另一个滤镜:
- 灰度:
float g = dot(c.rgb, float3(0.299,0.587,0.114)); c.rgb = g; - 反色:
c.rgb = 1.0 - c.rgb; - 染色:
c.rgb *= float3(1.0, 0.8, 0.8);
(对照真着色器结构:plugins/obs-filters/data/sharpness.effect,uniform 在 :4、VSDefault 在 :30、像素着色器 image.Sample 在 :48、technique Draw 在 :69。)
# 29.8 拼图收尾:编译 + 装上,让 OBS 认得它
📍从代码到能用:一个 CMake、一次编译、把产物放对地方,重启 OBS。

CMakeLists.txt(仿 plugins/obs-filters/CMakeLists.txt):
add_library(my-brightness MODULE) # ★ 编成动态库(MODULE)
target_sources(my-brightness PRIVATE my-brightness-filter.c)
target_link_libraries(my-brightness PRIVATE OBS::libobs) # ★ 链接 libobs
# data/brightness.effect 会被 OBS 的 CMake 助手自动装到插件旁边
2
3
4
一个插件 = 一个 MODULE 共享库(链接 OBS::libobs)+ 一个 data/ 文件夹(放 .effect)。 编好、装好后,磁盘上是这样:
obs-plugins/64bit/my-brightness.dll ← 你的插件本体
data/obs-plugins/my-brightness/brightness.effect ← 你的着色器
2
OBS 启动时扫这个目录、加载所有 .dll(第 9 课),obs_module_file("brightness.effect") 就能在运行时找到那个着色器。
三步跑起来:① cmake 配置 + 编译 → 得到 .dll;② 把 .dll 和 data/ 放进 OBS 插件目录;③ 重启 OBS。
然后:右键任意源 →"滤镜"→ 加号 → 你会看到"my_brightness_filter"那个名字! 加上它,拖动"亮度"滑块,画面实时变亮变暗 —— 你的第一个插件,活了。
💡 不想折腾编译 libobs? 官方有个
obs-plugintemplate模板仓库,已经配好 CMake、能拉预编译的 libobs 依赖,照着它起步最省心 —— 把上面几个函数填进去就行。
# 29.9 全景:你刚做的这个滤镜,长什么样

6 块拼图,就是"每一个 OBS 滤镜"的完整形状:
| 拼图 | 内容 | 来自 |
|---|---|---|
| ① 模块骨架 | OBS_DECLARE_MODULE / obs_module_load / obs_register_source | 第 9 课 |
| ② 登记表 | obs_source_info,type=FILTER,video | 第 6 课 |
| ③ create | 加载 .effect、抓参数把手 | 第 11 课 |
| ④ 属性 | float_slider / update / defaults | 第 27 课 |
| ⑤ render | begin → 塞参数 → end(三步) | 第 12 课 |
⑥ .effect | 采样→算→输出,c.rgb += brightness | 第 11/12 课 |
恭喜 —— 你把这门课学的东西,第一次"反过来用"了:不再是"读懂 OBS 怎么做",而是"让 OBS 照你写的做"。所有滤镜(官方的、第三方的)都是这 6 块 —— 你现在能看懂 plugins/obs-filters/ 里任何一个,也能改出你自己的。
# 29.10 本课小结
- 插件 = 动态库 + 登记:一个
.dll/.so,OBS 启动时加载(第 9 课),在obs_module_load里obs_register_*登记自己提供的源/滤镜/编码器/输出/服务。最小的一种 = 视频滤镜。 - ① 骨架:
OBS_DECLARE_MODULE()(生成obs_module_ver报告 API 版本,obs-module.h:76/:87)+OBS_MODULE_USE_DEFAULT_LOCALE+obs_module_load(入口,:101)里obs_register_source(&info)+return true。第 9 课加载机制的另一端。 - ② 登记表:
obs_source_info(和第 6/12 课同一张),type=OBS_SOURCE_TYPE_FILTER+output_flags=OBS_SOURCE_VIDEO+ 回调create/destroy/update/video_render/get_properties/get_defaults。引擎照表调,不认识"亮度"本身。 - ③ create/destroy:
bzalloc私有数据 →obs_module_file(".effect")找着色器 →obs_enter_graphics+gs_effect_create_from_file加载 →gs_effect_get_param_by_name抓参数把手 →obs_leave_graphics。destroy 对称(gs_effect_destroy+bfree)。碰 GPU 必须夹在 graphics 上下文里(第 11 课)。 - ④ 属性:
get_properties用obs_properties_add_float_slider声明滑块 → OBS 自动画界面(第 27 课);get_defaults给默认值;update把obs_data的值读进私有数据。一行 Qt 都不写。 - ⑤ video_render:三步 ——
obs_source_process_filter_begin(ctx, GS_RGBA, ALLOW_DIRECT)(obs.h:1469)→gs_effect_set_float(把手, 值)→obs_source_process_filter_end(ctx, effect, 0, 0)(obs.h:1483)。第 12 课的三步,你自己的滤镜版。 - ⑥ .effect:
uniform image(上层画面)+uniform brightness(C 塞进来,靠名字对上)+ 像素着色器image.Sample → c.rgb += brightness → return(核心就一行)+technique Draw。GPU 逐像素并行(第 11 课)。换那行数学 = 换一个滤镜。 - 编译装上:CMake
add_library(MODULE)+target_link_libraries(OBS::libobs)+data/(自动装)→.dll+.effect放进插件目录 → 重启 → 右键源→滤镜→加。或用官方obs-plugintemplate起步。
# 29.11 动手 / 观察(本课作业)
- 看懂一个真滤镜:打开
plugins/obs-filters/sharpness-filter.c,对照本课 6 块拼图,找出它的obs_source_info(:145)、create(:44)、render(:73)、.effect(data/sharpness.effect)。 - 改一行做新滤镜:把
.effect的像素着色器核心行换成"灰度"或"反色"(29.7 给了),体会"换一行 = 换一个滤镜"。 - 加第二个参数:给亮度滤镜再加一个"对比度"滑块 —— C 里加一个
float_slider+ 一个参数把手,.effect里加一行c.rgb = (c.rgb - 0.5) * contrast + 0.5;。 - 想清数据流:用自己的话说清 ——
image、brightness这两个uniform,分别是谁、什么时候、怎么送进着色器的? - (进阶)真的跑起来:去 GitHub 找官方
obs-plugintemplate,照它配好环境,把本课的亮度滤镜填进去、编译、装上、在 OBS 里加上它。
# 下一课预告
写 C + 编译,门槛还是有点高。如果你只想快速试个想法,或者干脆不想碰 C++/编译 —— 有没有更轻的路?有。OBS 内置了脚本引擎,你能用 Lua / Python 写扩展,几行代码就能加个自动化、加个热键、加个小工具,改完即刻生效,不用编译。
第 30 课:不写 C++ 也能扩展 —— Lua / Python 脚本 —— 我们看 OBS 的脚本系统(基于 obs-scripting + obs-frontend-api,第 27 课):它怎么把 libobs 的 C API 暴露给脚本语言(SWIG),你怎么写一个几十行的脚本工具。下节课见。
📁 本课配图:
imgs/29-01~imgs/29-09📌 源码锚点(本课以 OBS 自带sharpness-filter为参照,你写的每段都有对应): 模块libobs/obs-module.h:76(OBS_DECLARE_MODULE)、:87(obs_module_ver→LIBOBS_API_VER)、:101(obs_module_load 声明)、:116(OBS_MODULE_USE_DEFAULT_LOCALE)、:155(obs_module_file);plugins/obs-filters/obs-filters.c:3-4(宏)、:42(obs_module_load)、:58(obs_register_source)、:75(return true); 登记表 + 回调plugins/obs-filters/sharpness-filter.c:6(私有数据 struct)、:145-156(obs_source_info)、:44-71(create:bzalloc/obs_module_file/gs_effect_create_from_file/gs_effect_get_param_by_name)、:31-42(destroy)、:23-29(update:obs_data_get_double)、:110-119(properties:obs_properties_add_float_slider)、:121-124(defaults:obs_data_set_default_double)、:73-108(render); 滤镜三步 APIlibobs/obs.h:1469(obs_source_process_filter_begin)、:1483(obs_source_process_filter_end);gs_effect_set_float/gs_effect_get_param_by_name(graphics/graphics.h); 着色器plugins/obs-filters/data/sharpness.effect:4(uniforms)、:30(VSDefault)、:48(image.Sample)、:69(technique Draw); 构建plugins/obs-filters/CMakeLists.txt:3(add_library MODULE)、:32(target_link_libraries OBS::libobs);data/自动安装cmake/windows/helpers.cmake:302(target_install_resources)。 注:本课示例代码为教学演示,非可直接提交上游的产品代码;OBSCONTRIBUTING.rst不接受主要由 AI 生成的提交。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言