OBS Studio libobs vec2 二维向量 API 全解:结构定义、25 个函数与图形管线中的实际应用
本文围绕 OBS Studio 中 libobs/graphics/vec2.h 提供的二维向量(vec2)类型展开,完整梳理其结构体内存布局、25 个 API 函数的签名与语义,并结合 libobs 图形子系统(顶点坐标、着色器参数)、场景项变换(位置/缩放)与设置数据(obs_data)中的真实调用点,说明 vec2 在 OBS 渲染管线中的实际用途。读完本文,你可以直接在自己的插件或着色器代码中正确使用全部 vec2_* 接口,并理解 vec2_norm 对零向量的处理、vec2_close 的浮点比较精度等边界行为。
结构体定义:union 布局让命名访问与数组访问并存
vec2 的完整定义位于 vec2.h:
#include <graphics/vec2.h>
struct vec2 {
union {
struct {
float x, y;
};
float ptr[2];
};
};
这是 libobs 数学模块的典型设计(vec3、vec4 同理),要点有三:
- 双命名方式:通过匿名 union,同一个
struct vec2既能以v.x/v.y的语义化方式访问,也能以v.ptr[0]/v.ptr[1]的下标方式遍历。下标访问在需要循环处理分量、或把分量作为连续内存块传给 GPU 时非常方便。 - 纯 32 位浮点:两个分量均为
float,结构体本身只有 8 字节,可零开销地按值拷贝、赋值,也可直接memcpy进顶点缓冲或 uniform 数据。 - C 兼容:头文件以
extern "C"包裹,函数全部导出 C ABI(见下文EXPORT说明),这意味着 C 插件(如plugins/obs-ffmpeg这类纯 C 模块)与 C++ 插件都能直接使用。
头文件顶部还引入了 util/c99defs.h 与 <math.h>,前者提供跨平台的 EXPORT 等导出宏,后者为 sqrtf、fabsf 等单精度数学函数提供支持。
基础操作:zero、set、copy
这三个函数全部是 static inline,实现在头文件内(vec2.h#L36-L52),调用时不会产生函数调用开销:
void vec2_zero(struct vec2 *dst); // 将向量清零:x = 0.0f, y = 0.0f
void vec2_set(struct vec2 *dst, float x, float y);
void vec2_copy(struct vec2 *dst, const struct vec2 *v);
典型用法:
struct vec2 origin;
vec2_zero(&origin); // {0.0f, 0.0f},场景项默认原点
vec2_set(&origin, 10.0f, 20.0f);
struct vec2 other;
vec2_copy(&other, &origin); // {10.0f, 20.0f}
值得注意的约定:几乎全部 vec2_* 函数采用「第一个参数为输出目标 dst」的风格,且 dst 允许与任一输入参数相同(例如 vec2_add(&v, &v, &other) 是合法的),这与 SIMD 库(如 SIMDEx)的写法一致,便于就地运算。
两向量运算:add、sub、mul、div
void vec2_add(struct vec2 *dst, const struct vec2 *v1, const struct vec2 *v2);
void vec2_sub(struct vec2 *dst, const struct vec2 *v1, const struct vec2 *v2);
void vec2_mul(struct vec2 *dst, const struct vec2 *v1, const struct vec2 *v2);
void vec2_div(struct vec2 *dst, const struct vec2 *v1, const struct vec2 *v2);
四个函数均为逐分量(Hadamard)运算,实现见 vec2.h#L54-L72:
static inline void vec2_mul(struct vec2 *dst, const struct vec2 *v1, const struct vec2 *v2)
{
vec2_set(dst, v1->x * v2->x, v1->y * v2->y);
}
即 mul 不是点积或叉积,而是 (v1.x * v2.x, v1.y * v2.y)。在 OBS 场景系统中,这种逐分量乘法正是用来做各向缩放的:源宽、高分别乘不同的缩放系数。在 obs-scene.c 的 calculate_bounds_data 中可以看到实际用法——分组项的缩放通过 vec2_mulf(scale, scale, mul) 逐分量放大,边界宽/高分别对应 bounds.x / width、bounds.y / height 独立缩放。
div 同样是逐分量相除,调用方需自行保证除数分量非零(浮点除零会得到 inf/nan 而非断言)。
标量运算:addf、subf、mulf、divf 与取反 neg
void vec2_addf(struct vec2 *dst, const struct vec2 *v, float f); // 每个分量 + f
void vec2_subf(struct vec2 *dst, const struct vec2 *v, float f); // 每个分量 - f
void vec2_mulf(struct vec2 *dst, const struct vec2 *v, float f); // 每个分量 * f
void vec2_divf(struct vec2 *dst, const struct vec2 *v, float f); // 每个分量 / f
void vec2_neg(struct vec2 *dst, const struct vec2 *v); // 分量取反
实现见 vec2.h#L74-L97,全部委托给 vec2_set:
static inline void vec2_mulf(struct vec2 *dst, const struct vec2 *v, float f)
{
vec2_set(dst, v->x * f, v->y * f);
}
mulf 使用频率最高——它是「等比缩放」的核心原语。例如 obs-scene.c 中计算场景项的画布缩放 item_canvas_scale 时,就是用 vec2_mulf(dst, &item->scale, scale_factor) 把项的缩放向量乘上缩放因子。neg 则用于求方向向量反向,如 vec2.c 相关逻辑中由两向量差得到朝向后再取反。
度量与几何:dot、len、dist
float vec2_dot(const struct vec2 *v1, const struct vec2 *v2);
float vec2_len(const struct vec2 *v);
float vec2_dist(const struct vec2 *v1, const struct vec2 *v2);
实现见 vec2.h#L99-L114:
static inline float vec2_dot(const struct vec2 *v1, const struct vec2 *v2)
{
return v1->x * v2->x + v1->y * v2->y;
}
static inline float vec2_len(const struct vec2 *v)
{
return sqrtf(v->x * v->x + v->y * v->y);
}
static inline float vec2_dist(const struct vec2 *v1, const struct vec2 *v2)
{
struct vec2 temp;
vec2_sub(&temp, v1, v2);
return vec2_len(&temp);
}
三个要点:
vec2_dot返回标量,可用于求夹角余弦(配合归一化向量)或判断两个方向是否正交(dot ≈ 0)。vec2_len使用单精度sqrtf而非sqrt,与float分量匹配,避免不必要的 double 转换。vec2_dist的实现是「先vec2_sub到临时变量再vec2_len」,等价于欧氏距离公式 。
vec2_len 还有一个被复用的内部角色:vec2_norm 内部直接调用它(见下文)。
分量级 clamp 与取整:minf、min、maxf、max、abs、floor、ceil
这一组共 7 个函数,前 4 个是内联实现,后 3 个是导出函数,用于把向量的每个分量限制在某个范围内,或按取整规则变形:
void vec2_minf(struct vec2 *dst, const struct vec2 *v, float val);
void vec2_min(struct vec2 *dst, const struct vec2 *v, const struct vec2 *min_v);
void vec2_maxf(struct vec2 *dst, const struct vec2 *v, float val);
void vec2_max(struct vec2 *dst, const struct vec2 *v, const struct vec2 *max_v);
void vec2_abs(struct vec2 *dst, const struct vec2 *v);
void vec2_floor(struct vec2 *dst, const struct vec2 *v);
void vec2_ceil(struct vec2 *dst, const struct vec2 *v);
clamp 系列的内联实现(vec2.h#L116-L138)本质上是逐分量三目运算:
static inline void vec2_min(struct vec2 *dst, const struct vec2 *v, const struct vec2 *min_v)
{
dst->x = (v->x < min_v->x) ? v->x : min_v->x;
dst->y = (v->y < min_v->y) ? v->y : min_v->y;
}
命名容易踩坑,需要特别强调语义方向:
vec2_minf(dst, v, val)是把v的分量限制在不超过val的下界之上,即dst = max(v, val)的效果(分量小于val时被抬到val)。vec2_maxf(dst, v, val)则是把分量限制在不超过val的上界之下,即dst = min(v, val)。
也就是说函数名里的 min/max 指的是「取的是较小的那个候选值对下界约束」这类底层含义,实际效果与直觉的 clamp 方向相反。一个常见的混淆点,写代码时建议直接按上述实现逐分量确认。两个向量版本 vec2_min / vec2_max 允许上下界本身也是向量(逐分量可不同),这在处理矩形边界(如把拖拽坐标限制在画布宽高内,宽、高上界不同)时非常有用。
abs / floor / ceil 三个函数不是内联的,而是以 EXPORT 形式声明在头文件(vec2.h#L140-L142)、实现在 vec2.c:
void vec2_abs(struct vec2 *dst, const struct vec2 *v)
{
vec2_set(dst, fabsf(v->x), fabsf(v->y));
}
void vec2_floor(struct vec2 *dst, const struct vec2 *v)
{
vec2_set(dst, floorf(v->x), floorf(v->y));
}
void vec2_ceil(struct vec2 *dst, const struct vec2 *v)
{
vec2_set(dst, ceilf(v->x), ceilf(v->y));
}
floor/ceil 常用于把浮点坐标取整到像素网格,避免亚像素模糊。
浮点相等比较:vec2_close 与 epsilon
int vec2_close(const struct vec2 *v1, const struct vec2 *v2, float epsilon);
实现位于 vec2.c#L38-L41:
int vec2_close(const struct vec2 *v1, const struct vec2 *v2, float epsilon)
{
return close_float(v1->x, v2->x, epsilon) && close_float(v1->y, v2->y, epsilon);
}
其中的 close_float 定义在 math-defs.h#L38-L41:
static inline bool close_float(float f1, float f2, float precision)
{
return fabsf(f1 - f2) <= precision;
}
即两个向量的每个分量差值的绝对值都不超过 epsilon 时返回真,这是绝对误差比较(不是相对误差)。math-defs.h 同时提供了三档常用精度宏,供调用方选择 epsilon 取值:
#define LARGE_EPSILON 1e-2f
#define EPSILON 1e-4f
#define TINY_EPSILON 1e-5f
返回类型是 int 而非 bool(C ABI 习惯),调用方可直接写 if (vec2_close(&a, &b, EPSILON))。在测试与断言场景中,这是比较两个场景项位置是否「基本相同」的标准手段,避免浮点直接 == 带来的误判。
归一化:vec2_norm 的零向量保护
void vec2_norm(struct vec2 *dst, const struct vec2 *v);
实现位于 vec2.c#L43-L51:
void vec2_norm(struct vec2 *dst, const struct vec2 *v)
{
float len = vec2_len(v);
if (len > 0.0f) {
len = 1.0f / len;
vec2_mulf(dst, v, len);
}
}
两个实现细节值得注意:
- 零向量保护:当
len == 0.0f时函数直接返回,不写 dst。这意味着如果传入零向量,dst的内容保持不变(调用方应自行初始化 dst),而不会产生0/0的 NaN。这是与许多数学库不同的约定——某些库会返回零向量或 NaN,使用时要意识到 dst 不会被覆盖。 - 一次除法优化:实现不是逐分量除以
len,而是先算1.0f / len再用vec2_mulf乘进去,把两次除法换成一次除法一次乘法,在热路径上有意义。
归一化后的向量模长为 1,常用于方向计算:例如把「当前点到目标点的位移向量」归一化后,配合 vec2_mulf 乘以速度值即可实现匀速移动(math-extra 模块中的 torque 平滑函数也是围绕这类向量运算构建的,见 math-extra.h)。
vec2 在 libobs 图形子系统中的应用
理解了 API 之后,看看 vec2 在 OBS 图形管线里到底扮演什么角色。
1. 顶点与纹理坐标
graphics.h 提供了直接以 vec2 传递顶点坐标和纹理坐标的接口:
EXPORT void gs_vertex2v(const struct vec2 *v);
EXPORT void gs_texcoord2v(const struct vec2 *v, int unit);
EXPORT void gs_shader_set_vec2(gs_sparam_t *param, const struct vec2 *val);
EXPORT void gs_effect_set_vec2(gs_eparam_t *param, const struct vec2 *val);
在 graphics.c 的立即模式顶点数据构建中,绘制矩形时直接用 vec2_set 写入四角纹理坐标:
struct vec2 *tvarray = data->tvarray[0].array;
vec2_set(tvarray, start_u, start_v);
vec2_set(tvarray + 1, end_u, start_v);
vec2_set(tvarray + 2, start_u, end_v);
vec2_set(tvarray + 3, end_u, end_v);
这里 tvarray + 1 的指针算术也依赖了 vec2 恰好是 8 字节连续浮点数组的布局。而 gs_effect_set_vec2(effect.c 中 effect_setval_inline(param, val, sizeof(struct vec2)))则说明 vec2 可以直接按值灌入效果着色器的 uniform——OBS 自带的大量 .effect 文件(位于 libobs/data/)中的 vec2 类型 uniform 就是走这条路径的。
2. 场景项的位置与缩放
场景系统大量使用 vec2 表示位置、缩放和原点。以 obs-scene.c 为例,场景项的位置直接从设置数据中读入 vec2:
obs_data_get_vec2(item_data, "pos_rel", &item->pos);
分组/对齐逻辑同样基于 vec2:add_alignment(obs-scene.c#L325)按对齐标志调整位置向量,pos_from_absolute / pos_to_absolute(obs-scene.c#L365-L382)在绝对坐标与相对父项的坐标间换算。绘制时的基础分辨率也通过 gs_effect_set_vec2 传入着色器(obs-scene.c#L779-L788),包括 base_res 及其倒数 base_res_i,供 shader 做像素级换算。
3. 设置数据的序列化
vec2 不仅是运行时类型,还是一等公民的设置数据类型。obs-data.h 提供了成对的读写接口:
EXPORT void obs_data_set_vec2(obs_data_t *data, const char *name, const struct vec2 *val);
EXPORT void obs_data_get_vec2(obs_data_t *data, const char *name, struct vec2 *val);
// 以及 obs_data_set_default_vec2 / obs_data_get_default_vec2 等变体
实现位于 obs-data.c,set_vec2 内部把 x、y 两个分量以子对象形式写入 obs_data_t。这保证了场景项的 pos_rel、scale 等 vec2 属性能随配置文件(basic.ini)持久化——这正是 OBS 里你在场景中移动、缩放过的元素重启后仍然保持原位的技术基础。
4. 与极坐标的互转
vec2 还参与球面/极坐标换算,见 math-extra.h#L39-L40:
EXPORT void norm_to_polar(struct vec2 *dst, const struct vec3 *norm);
EXPORT void polar_to_norm(struct vec3 *dst, const struct vec2 *polar);
前者把一个 3D 单位法向量分解为 (azimuth, elevation) 形式的 vec2 极角,后者反向重建。这组接口体现了 vec2 在 libobs 数学体系中「平面坐标 + 角度对」的双重角色。
内联函数与导出函数的划分
阅读源码时会发现一个规律:vec2.h 中大多数函数是 static inline,而 abs、floor、ceil、close、norm 五个函数以 EXPORT 声明(vec2.h#L140-L144)、实现在 vec2.c 中编译进 libobs。从源码结构看,这一划分与调用频率有关:高频的算术运算走内联避免调用开销,涉及库内工具(close_float)或复合逻辑的函数则以导出符号形式提供,方便在 C 编译单元间共享且保持 C ABI 稳定。对插件开发者而言这意味着两者调用方式完全一致,无需关心划分细节。
API 速查表
| 函数 | 原型 | 说明 |
|---|---|---|
vec2_zero |
void vec2_zero(struct vec2 *dst) |
清零,{0, 0} |
vec2_set |
void vec2_set(struct vec2 *dst, float x, float y) |
逐分量赋值 |
vec2_copy |
void vec2_copy(struct vec2 *dst, const struct vec2 *v) |
整体拷贝 |
vec2_add / vec2_sub / vec2_mul / vec2_div |
void (…)(struct vec2 *dst, const struct vec2 *v1, const struct vec2 *v2) |
逐分量加减乘除 |
vec2_addf / vec2_subf / vec2_mulf / vec2_divf |
void (…)(struct vec2 *dst, const struct vec2 *v, float f) |
逐分量与标量运算 |
vec2_neg |
void vec2_neg(struct vec2 *dst, const struct vec2 *v) |
分量取反 |
vec2_dot |
float vec2_dot(const struct vec2 *v1, const struct vec2 *v2) |
点积 |
vec2_len |
float vec2_len(const struct vec2 *v) |
模长(sqrtf 实现) |
vec2_dist |
float vec2_dist(const struct vec2 *v1, const struct vec2 *v2) |
两点欧氏距离 |
vec2_minf / vec2_maxf |
void (…)(struct vec2 *dst, const struct vec2 *v, float val) |
分量级标量 clamp(注意方向语义) |
vec2_min / vec2_max |
void (…)(struct vec2 *dst, const struct vec2 *v, const struct vec2 *min_v) |
分量级向量 clamp |
vec2_abs / vec2_floor / vec2_ceil |
void (…)(struct vec2 *dst, const struct vec2 *v) |
逐分量取绝对值 / 向下取整 / 向上取整(导出函数,实现在 vec2.c) |
vec2_close |
int vec2_close(const struct vec2 *v1, const struct vec2 *v2, float epsilon) |
每分量绝对差 ≤ epsilon 则返回真 |
vec2_norm |
void vec2_norm(struct vec2 *dst, const struct vec2 *v) |
归一化;零向量时不写 dst |
相关文档与源码位置
- 本文档对应的 Sphinx 源文件:reference-libobs-graphics-vec2.rst
- 类型与内联实现:vec2.h、vec2.c
- 浮点比较与精度宏:math-defs.h
- 极坐标互转:math-extra.h、math-extra.c
- 同族类型文档:
vec3见 reference-libobs-graphics-vec3.rst,vec4见 reference-libobs-graphics-vec4.rst,完整图形模块索引见 reference-libobs-graphics.rst
适用前提:以上接口与行为以当前仓库中 libobs/graphics/vec2.h 的实现为准,属于 libobs 公共 C ABI,适用于任何链接 libobs 的插件或独立程序开发场景。
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 StartedRust0624
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