OBS 插件开发实战:5 步写出实时屏幕标注滤镜
【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio
远程上课,你只想在直播画面上随手画个圈强调重点——这个圈得直接落在推流画面上,而不是另一个窗口里。这是典型的屏幕标注问题,也是一个 OBS 插件开发能解决的典型问题:挂一个滤镜,在源画面上叠加 GPU 绘制的笔画。跟着走一遍,就能从零搭出完整链路。
先看看最终效果
给场景里的任意源加一个"屏幕标注"滤镜,右侧出现一个小工具栏:画笔、橡皮、清空、撤销。鼠标在预览窗口上移动就开始画,笔画实时叠加在源画面顶层,随画面一起编码推出去,观众看到的就是你已经圈好重点的屏幕。整个过程不需要切窗口、不需要截图粘贴。
最薄的插件壳
我们先搭个能跑的空壳。OBS 里所有源(包括滤镜)的入口都是obs_source_info结构体,仓库里的核心滤镜就是靠 libobs/obs-source.h 里这个结构体注册的,官方插件文档 里对模块加载流程也有说明。
OBS 滤镜怎么注册
最小骨架长这样:一个结构体 + 一行注册:
static struct obs_source_info screen_annotation = { .id = "screen_annotation", .type = OBS_SOURCE_TYPE_FILTER, .output_flags = OBS_SOURCE_VIDEO, .get_name = annotation_name, .create = annotation_create, .destroy = annotation_destroy, .update = annotation_update, .video_render = annotation_render }; bool obs_module_load(void) { return obs_register_source(&screen_annotation); }回调的时机基本就是全部约定:create在用户把滤镜加到源上时调用,destroy在移除时调用,update在设置面板改动时调用,video_render则是每一帧渲染时调你。注意create/destroy/update里涉及图形资源的地方要包在obs_enter_graphics()/obs_leave_graphics()之间,这是 OBS 对图形 API 调用线程的硬性要求,仓库里每个滤镜(比如plugins/obs-filters/scroll-filter.c)都是这个写法。
让笔尖真正落在画面上
接下来是最短的可用链路:捕获输入 → 归一化坐标 → 存点。OBS 把鼠标事件以struct obs_mouse_event的形式派发给源,里面就三个有用的字段:修饰键、x、y(像素坐标)。我们只需要把每个点转成 0–1 的归一化坐标存下来:
static void append_point(struct annotation_data *a, int x, int y) { float nx = x / (float)a->width; float ny = y / (float)a->height; a->points[a->point_count++] = (point){ nx, ny, 1.0f }; }这里有个值得说清的设计取舍:为什么存 0–1 归一化坐标而不是像素值?因为像素坐标绑死分辨率。用户把基础分辨率从 1080p 改成 1440p,或者换了采集窗口大小,存像素值的笔画就整体漂移了;存归一化坐标则天然跟着画面走,存档、加载、跨设备都不用换算。OBS 自带的滚动滤镜(scroll-filter.c)里累计偏移量用的也是同一套 0–1 思路。
按下左键开一笔,移动时append_point,松开收笔——到这里你已经"能画"了,虽然还谈不上"画得不卡"。
从"能画"到"画得不卡"
CPU 上逐点画线,每帧几百上千个点,帧率直接掉到个位数,你的观众会先于你发现。所以渲染必须走 GPU 加速绘制这条路,而且 OBS 已经把最难的部分铺好了:
- 用
.effect文件而不是裸 GL 代码。OBS 自带一套跨 D3D11/OpenGL/Metal 的着色器抽象,create时用gs_effect_create_from_file加载一个.effect文件,拿好gs_effect_t *和参数句柄就行,三行代码替代一堆glGen*。 - video_render 里两段式合成:先用 OBS 传入的 effect 把原始画面画出来,再把自己的线条画在顶层:
static void annotation_render(void *data, gs_effect_t *effect) { struct annotation_data *a = data; draw_base_image(effect); // 1. 原始源画面 for (size_t i = 0; i < a->stroke_count; i++) draw_stroke(a, &a->strokes[i]); // 2. 叠加笔画 }- 批量绘制,别每笔重传缓冲。关键问题是:每一笔都重新上传整个顶点缓冲,你猜帧率会怎样?正确姿势是把笔画点序列展开成 GPU 上的顶点条带,只有正在书写中的那一笔每帧增量上传新点,已完成的笔画顶点常驻显存不动。一笔画几百个点,代价从"每帧全量上传所有笔画"降到"每帧上传几个点"。
- 线宽和圆角交给着色器。每个线段在片元阶段按距离线段中心的距离做 smoothstep 衰减,就得到抗锯齿的圆头线条,比在 CPU 里算好纹理快得多,也好看得多。
让工具真正好用的那些细节
骨架通了之后,差距在细节。这几项都不难,但缺一个用户就会觉得"糙":
- 压感:数位笔的压感值不要只影响整笔粗细,而是逐点记录——同一个点结构里多存一个压力字段,展开顶点时按点的压力缩放线宽,粗细分界才自然。
- 多笔刷:实线、高亮、矩形其实共用同一条渲染路径,差异只是顶点布局和着色器参数,一个枚举就够。
- 撤销/重做:命令不用设计得多复杂——"新增一笔"是唯一的写操作,撤销就是弹出最后一条笔画引用,重做再压回去。别深拷贝整条轨迹。
- 多场景标注:数据挂在源实例上,场景切换天然隔离;想跨场景共享就给每个场景名挂一份数据,切换时存档/恢复。
- 快捷键:走 OBS 自带的 hotkey 注册,Ctrl+Z 撤销、一键清空,别让工具栏按钮独占交互。
别让标注拖垮你的直播
功能全之后,上线前按 OBS 插件性能优化最常见的三处过一遍:
- 帧率自适应:用
os_gettime_ns()算帧间隔,连续几帧低于 30fps 就切简化模式——关掉纹理笔刷和动态效果,只画纯色线条,恢复后自动切回。 - 内存上限:笔画数组设硬上限,超了丢最旧的一笔;顶点缓冲按笔画增长而不是无限预分配。
- 资源生命周期:每个
gs_*资源创建必对应 destroy 里的一次释放,尤其是update里重建 sampler 时先 destroy 旧的——这是滤镜里最容易泄漏的点,scroll-filter.c 里gs_samplerstate_destroy再 create 的写法值得抄。
| 故障现象 | 可能原因 | 处理 |
|---|---|---|
| 标注内容闪烁 | 资源在 video_render 中间被重建 | 资源创建移出渲染回调,只在 create/update 里动 |
| 笔画多了帧率骤降 | 每帧全量上传顶点缓冲 | 只对当前笔做增量上传 |
| 移除滤镜后显存不降 | destroy 里漏释放 gs 资源 | 每个 create 配对一次 destroy |
编译、丢进 plugins 目录、验证
构建走 OBS 主工程的标准流程,插件作为一个子模块编出来:
git clone https://gitcode.com/GitHub_Trending/ob/obs-studio cd obs-studio && mkdir build && cd build cmake .. && make -j$(nproc) cp -r ../../plugins/screen-annotation $OBS_PLUGIN_DIR/验证顺序:先确认 OBS 日志里模块加载成功(没有obs_register_source的报错),再往任意源上挂滤镜,画一笔看预览和推流画面是否一致,最后连画几分钟观察显存和帧率曲线不漂移,就算过关。
"源纹理 + GPU 批绘叠加"这套架构和标注本身没强绑定:字幕叠加、动态水印、实时画面滤镜,换一套着色器参数和顶点布局就是另一个插件,骨架原样复用。
【免费下载链接】obs-studioOBS Studio - Free and open source software for live streaming and screen recording项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考