30 / 32 全链路、插件与脚本扩展

第 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;                                  // 报到成功
}
1
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,
};
1
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。它要做的:开内存、加载着色器、拿到参数的"把手"。

create/destroy

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 对称)
}
1
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");
}
1
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 画出来。

video_render 三步

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);
}
1
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 课)。

.effect 着色器

新建 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); }
}
1
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:4VSDefault:30、像素着色器 image.Sample:48technique 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 助手自动装到插件旁边
1
2
3
4

一个插件 = 一个 MODULE 共享库(链接 OBS::libobs)+ 一个 data/ 文件夹(放 .effect)。 编好、装好后,磁盘上是这样:

obs-plugins/64bit/my-brightness.dll               ← 你的插件本体
data/obs-plugins/my-brightness/brightness.effect  ← 你的着色器
1
2

OBS 启动时扫这个目录、加载所有 .dll(第 9 课),obs_module_file("brightness.effect") 就能在运行时找到那个着色器。

三步跑起来:① cmake 配置 + 编译 → 得到 .dll;② 把 .dlldata/ 放进 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_loadobs_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_propertiesobs_properties_add_float_slider 声明滑块 → OBS 自动画界面(第 27 课);get_defaults 给默认值;updateobs_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 动手 / 观察(本课作业)

  1. 看懂一个真滤镜:打开 plugins/obs-filters/sharpness-filter.c,对照本课 6 块拼图,找出它的 obs_source_info(:145)、create(:44)、render(:73)、.effect(data/sharpness.effect)。
  2. 改一行做新滤镜:把 .effect 的像素着色器核心行换成"灰度"或"反色"(29.7 给了),体会"换一行 = 换一个滤镜"。
  3. 加第二个参数:给亮度滤镜再加一个"对比度"滑块 —— C 里加一个 float_slider + 一个参数把手,.effect 里加一行 c.rgb = (c.rgb - 0.5) * contrast + 0.5;
  4. 想清数据流:用自己的话说清 —— imagebrightness 这两个 uniform,分别是谁、什么时候、怎么送进着色器的?
  5. (进阶)真的跑起来:去 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); 滤镜三步 API libobs/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)。 注:本课示例代码为教学演示,非可直接提交上游的产品代码;OBS CONTRIBUTING.rst 不接受主要由 AI 生成的提交。

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>