13 / 32 Source、对象、插件与渲染

第 12 课:给画面加特效 —— 滤镜与着色器

# 第 12 课:给画面加特效 —— 滤镜与着色器

嘿,我是小方。 第 11 课我们学会了一件事:画一个源,GPU 就是跑一个像素着色器,而默认那个着色器 PSDrawBare 只有一行 —— return image.Sample(uv),在纹理上采个色,原样返回。 这一课我们就干一件特别有意思的事:在"采样"之后,加工一下再返回。 这就是滤镜的全部秘密。抠绿幕、磨皮、模糊、调色…… 听着高大上,本质都是在那一行后面多写几句数学。这是视频链路里最好玩的一段,而且这一课结束,你能亲手写出自己的第一个滤镜


# 12.1 滤镜的本质:把"原样返回"换成"加工后返回"

📍接上一节:第 11 课那行"采样后原样返回",这一节就在它后面插一段加工——滤镜的全部秘密从这里开始。

直接看核心思想,一张图说清:

滤镜的本质

  • 第 11 课(原样画):像素着色器 c = image.Sample(uv); return c; —— 纹理什么样,画出来什么样,画面不变。
  • 本课(滤镜):c = image.Sample(uv); c.rgb *= brightness; return c; —— 采样之后,改一改(这里是乘个亮度)再返回,画面就变了。

就这么简单。滤镜 = 在像素着色器里,采样之后插一段你自己的加工。 调色、抠绿、模糊、锐化,区别只在"插的那段数学"不同。

接下来,我们看 OBS 里一个真实滤镜是怎么把这件事组织起来的。


# 12.2 滤镜,就是 type = FILTER 的那种 source

📍我们在哪:知道了滤镜的本质,现在看它在"一切皆 source"的体系里是哪一类、靠哪张登记表挂上去。

还记得第 6 课吗?滤镜是 obs_source 四类型之一(OBS_SOURCE_TYPE_FILTER),它挂在一个"宿主源"上,加工宿主产出的画面。翻一个真实滤镜的"登记表"(以"颜色校正"滤镜为例):

滤镜是个 FILTER source

// plugins/obs-filters/color-correction-filter.c
struct obs_source_info color_filter = {
	.id           = "color_filter",
	.type         = OBS_SOURCE_TYPE_FILTER,        // ← 它是个滤镜
	.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_SRGB,
	.create       = color_correction_create,
	.video_render = color_correction_render,       // ← 画面在这画(12.4)
	.update       = color_correction_update,       // 用户改了设置就调
	...
};
1
2
3
4
5
6
7
8
9
10

它的 create 回调,干的就两件关键事(color-correction-filter.c:458):

filter->effect = gs_effect_create_from_file(effect_path, NULL);   // ① 加载你的 .effect 着色器文件
...
filter->gamma_param =
	gs_effect_get_param_by_name(filter->effect, "gamma");        // ② 抓住 shader 里那个参数的「句柄」
1
2
3
4
  • gs_effect_create_from_file —— 把这个滤镜的 .effect 文件(就是那段着色器)加载、编译进 GPU。第 11 课讲过,.effect 会被翻成 D3D/GL/Metal 的原生着色器。
  • gs_effect_get_param_by_name —— 抓住着色器里某个 uniform(比如 gamma)的句柄。为什么要先抓句柄?因为待会儿用户拖滑块,你得能快速把新值塞进这个参数。这个"句柄"就是塞值的把手。

至于"滤镜也是 source"带来的那一切 —— 生死管理(第 7 课)、信号(第 8 课)、那张登记表(第 6 课)—— 滤镜全部白嫖,一行不用自己写。它只需操心三块:create(加载)、update(存参数)、video_render(画)。


# 12.3 关键:滤镜的着色器能"看邻居"

📍承上启下:上一节看的是滤镜的外壳(它是个 source),这一节钻进它的着色器,看模糊/锐化凭什么做得到——关键是"看邻居"。

为什么滤镜能做出"模糊""锐化"这种效果?光改自己这一点的颜色可不够。我们把第 11 课的 PSDrawBare 和一个真实滤镜 sharpness.effect(锐化)的像素着色器摆一起对比:

两个 PS 对比

// 第 11 课:default.effect —— 只采「自己这一点」
float4 PSDrawBare(VertOut v) : TARGET {
	return image.Sample(s, v.uv);     // 1 次采样,原样返回
}

// 本课:sharpness.effect —— 采「自己 + 周围一圈」
float4 E = image.Sample(s, v.uv);     // 中心点
float4 color = 8 * E;                 // 自己 ×8
color -= image.Sample(s,);          // 减去上、下、左、右、四个角……
color -= image.Sample(s,);          //   (一共减 8 个邻居)
...
return saturate(E + color * sharpness);   // saturate=把值夹到 0~1;边缘被强调 → 看起来更锐
1
2
3
4
5
6
7
8
9
10
11
12

差别就在这:滤镜的 PS 不止采"自己",还采"周围一圈",再做数学。(这种"看周围一圈像素、加权求和"的操作,有个正经名字,叫卷积 convolution。)这是滤镜真正的本事:

  • 锐化 = 自己 ×8,减掉周围 8 个 → 颜色变化大的地方(边缘)被放大,看着更"锐";
  • 模糊 = 自己和周围一圈取平均 → 细节被抹平;
  • 抠绿(色键) = 看这一点的颜色离"绿"有多近,够近就把它的 alpha 设成 0(透明) → 绿幕没了。

那个 sharpness 是哪来的?它是着色器里的一个 uniform float sharpness —— 用户在 OBS 里拖那个"锐化"滑块,值就被喂进了它。 怎么喂的?下一节就是。


# 12.4 源码深读:滤镜的 video_render 三步走

📍我们在哪:着色器会看邻居了,可它怎么被引擎驱动着一帧帧跑起来?这一节就是滤镜真正"画"的那一步。

到了真正画的时候。你可能担心:"把宿主画面弄成纹理、跑我的着色器、再画出去……这一套我得自己写?" 不用。 引擎给你包好了一对函数 obs_source_process_filter_begin / end,你只管夹在中间设参数。看真实代码(color-correction-filter.c:548):

滤镜 video_render 三步

static void color_correction_filter_render(void *data, gs_effect_t *effect)
{
	struct ..._data *filter = data;

	// ① 准备:引擎把「宿主源」先画进一张纹理,并设成你 effect 的 image 输入
	if (!obs_source_process_filter_begin(filter->context, GS_RGBA, OBS_ALLOW_DIRECT_RENDERING))
		return;

	// ② 把 update 里存好的 UI 参数,喂进 shader 的那个 uniform(用上 12.2 抓的句柄)
	gs_effect_set_float(filter->gamma_param, filter->gamma);

	// ③ 用你的 effect 跑一遍那张纹理,把加工后的画面画出去
	obs_source_process_filter_end(filter->context, filter->effect, 0, 0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

三步,逐句讲:

  • obs_source_process_filter_begin —— 这一句里,引擎替你干了重活:把你的"宿主源"(比如那个摄像头)先渲染进一张纹理,然后把这张纹理设成你 .effect 里那个 uniform texture2d image 的输入。换句话说,它把"上一环的画面"打包成了你着色器的原料。
  • gs_effect_set_float(filter->gamma_param, filter->gamma) —— 把参数塞进去。filter->gamma_param 是 12.2 抓的句柄,filter->gammaupdate 里存的、用户拖滑块的值。12.3 那个 sharpness 进着色器,就是这么进去的。 这一步,把"界面 → obs_data → 着色器"接通了(回想第 7 课的 obs_data)。
  • obs_source_process_filter_end —— 真正开画:用你的 effect、以那张纹理为输入,跑一遍第 11 课那套渲染管线(VS 摆位置 → PS 逐像素加工),输出加工后的画面。

beginend 之间,你只负责"设参数";搬纹理、跑管线、画出去这些重活,引擎全替你扛了。 一个 GPU 滤镜的全部代码,核心就这五六行。


# 12.5 动手:写你的第一个滤镜

📍收束:前四节的理论攒齐了,这一节把它落成能跑的代码——你的第一个滤镜。

理论齐了,真刀真枪写一个。我们做个最简单的亮度滤镜:把第 11 课的 default.effect 改一行就行。

最小亮度滤镜

uniform float4x4 ViewProj;     // 定位矩阵(照抄)
uniform texture2d image;       // 宿主画面(引擎喂进来)
uniform float brightness;      // ★ 你的参数:用户拖的滑块

sampler_state s { Filter = Linear; AddressU = Clamp; AddressV = Clamp; };

VertInOut VSDefault(VertInOut v) {       // 顶点着色器(照抄)
	v.pos = mul(float4(v.pos.xyz, 1.0), ViewProj);
	return v;
}

float4 PSBrightness(VertInOut v) : TARGET {     // ★ 你的像素着色器
	float4 c = image.Sample(s, v.uv);            //   采样原色
	c.rgb *= brightness;                         //   ★ 加工:乘个亮度
	return c;                                    //   返回
}

technique Draw {
	pass {
		vertex_shader = VSDefault(vert_in);
		pixel_shader  = PSBrightness(vert_in);   // ← 换成自己的 PS
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

跟第 11 课的 default.effect 一对照,你会发现只多了两处:加了个 uniform float brightness(参数),在 PS 里多了一行 c.rgb *= brightness(加工)。配上 12.2/12.4 那套 C 代码(create 加载它、抓 brightness 句柄,rendergs_effect_set_float 喂值),一个能用的亮度滤镜就成了。

想换个效果?把 c.rgb *= brightness 那一行换成别的数学就行:

  • c.rgb = 1.0 - c.rgb; → 反色(底片效果);
  • c.rgb = dot(c.rgb, float3(0.299, 0.587, 0.114)); → 黑白(dot=向量点积/加权求和;还记得第 3 课算亮度的那个加权吗?就是它);
  • 采周围一圈取平均 → 模糊。

滤镜的世界,就是从这一行开始的。


# 12.6 回到第 6 课:滤镜可以一条链串下去

📍收束:单个滤镜讲透了,最后把第 6 课那道"滤镜能串多少个"的悬念收掉——因为滤镜也是 source,链条天然成立。

最后,把第 6 课那道悬而未决的思考题彻底落地。一个源可以挂好几个滤镜,它们排成一条链:

滤镜链

摄像头 → 色键(抠绿)→ 模糊 → 调色 → 最终画面,一个滤镜的输出,就是下一个滤镜的输入。每一段"滤镜 N"内部,都是 12.4 那套 begin → 设参数 → end 各跑一遍 —— 前一个 end 画出的纹理,正好成了后一个 begin 的输入。

因为**"滤镜也是 source"**(第 6 课的核心),这条链理论上能一直接下去,你甚至能给滤镜再挂滤镜。第 6 课结尾我让你想的那道题,答案就在这:统一抽象(一切皆 source)+ 统一渲染(都是跑着色器),让"串联任意多个加工"变得天然成立。


# 12.7 本课小结

  • 滤镜的本质:在像素着色器里,把"采样后原样返回"换成"采样后加工再返回"(c.rgb *= ...)。调色/抠绿/模糊/锐化,区别只在那段数学。
  • 滤镜是 type = FILTER 的 source(第 6 课),挂在宿主源上;只需写 create / update / video_render 三块,其余(生死、信号、登记表)全白嫖。
  • create:gs_effect_create_from_file 加载 .effect;gs_effect_get_param_by_name 抓参数句柄(color-correction-filter.c:458)。
  • 滤镜着色器能"看邻居":PSDrawBare 只采 1 点;sharpness.effect 采中心 + 8 个邻居做卷积 —— 这是模糊/锐化/抠绿的根基。参数(sharpness)是 uniform,由 video_render 喂进来。
  • video_render 三步(color-correction-filter.c:548):obs_source_process_filter_begin(引擎把宿主画成纹理、设为输入)→ gs_effect_set_float(喂 UI 参数,接通"界面→obs_data→着色器")→ obs_source_process_filter_end(跑你的 effect 画出)。重活全在引擎。
  • 动手:亮度滤镜 = default.effect + 一个 brightness 参数 + 一行 c.rgb *= brightness。换那行数学就是换效果。
  • 滤镜链:一个的输出是下一个的输入;每段都是 begin→set→end;"滤镜也是 source" 让链条天然成立(第 6 课闭合)。

# 12.8 动手 / 观察(本课作业)

  1. 在界面里挂滤镜看链条:OBS 里右键一个源 →「滤镜」,加一个"颜色校正",再加一个"锐化"。拖它们的顺序 —— 你在调的,就是 12.6 那条链的次序(谁先加工)。
  2. 翻一个真滤镜的三块:打开 plugins/obs-filters/color-correction-filter.c,对照本课找到 create(gs_effect_create_from_file)、render(process_filter_begin/end)、update。再打开它的 data/color_correction_filter.effect,看 PS 里对 c 做了哪些数学。
  3. 读懂"看邻居":打开 plugins/obs-filters/data/sharpness.effect,确认它的 PS 真的采了 9 个点。想一想:如果把"减邻居"改成"和邻居取平均",会得到什么效果?
  4. (进阶)设计一个滤镜:照 12.5 的最小骨架,想好你那行加工写什么(反色?黑白?某个通道加强?),写出对应的 PSxxx

# 下一课预告

我们已经会"采集一帧"(第 10 课)、"画一个源 + 加滤镜"(第 11、12 课)。可一个直播画面通常有好几个源:游戏打底、摄像头小窗、字幕、logo…… 它们各自画好之后,是怎么按位置、大小、层级叠成最终一幅画面的?那个"画布"到底是什么?拖动、缩放一个源,背后又改了什么?

第 13 课:场景与合成 —— 我们会认识 obs_sceneobs_sceneitem,看 OBS 怎么把多个源组合成一个场景、怎么把它们一层层叠加渲染出最终画面。还有那个最妙的设计:场景,它自己也是一个 source(第 6 课埋的最后一个伏笔)。模块 3 的收官,下节课见。


📁 本课配图:imgs/12-01 ~ imgs/12-06 📌 源码锚点:plugins/obs-filters/color-correction-filter.c(.type = FILTER)、:458(gs_effect_create_from_file)、:462(gs_effect_get_param_by_name)、:548(render)、:552(process_filter_begin)、:556(gs_effect_set_float)、:562(process_filter_end)、plugins/obs-filters/data/sharpness.effect(锐化着色器)、libobs/data/default.effect(对照)

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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