首页
/ OBS Studio libobs vec2 二维向量 API 全解:结构定义、25 个函数与图形管线中的实际应用

OBS Studio libobs vec2 二维向量 API 全解:结构定义、25 个函数与图形管线中的实际应用

2026-09-06 12:06:37作者:柏廷章Berta

本文围绕 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 数学模块的典型设计(vec3vec4 同理),要点有三:

  1. 双命名方式:通过匿名 union,同一个 struct vec2 既能以 v.x / v.y 的语义化方式访问,也能以 v.ptr[0] / v.ptr[1] 的下标方式遍历。下标访问在需要循环处理分量、或把分量作为连续内存块传给 GPU 时非常方便。
  2. 纯 32 位浮点:两个分量均为 float,结构体本身只有 8 字节,可零开销地按值拷贝、赋值,也可直接 memcpy 进顶点缓冲或 uniform 数据。
  3. C 兼容:头文件以 extern "C" 包裹,函数全部导出 C ABI(见下文 EXPORT 说明),这意味着 C 插件(如 plugins/obs-ffmpeg 这类纯 C 模块)与 C++ 插件都能直接使用。

头文件顶部还引入了 util/c99defs.h<math.h>,前者提供跨平台的 EXPORT 等导出宏,后者为 sqrtffabsf 等单精度数学函数提供支持。

基础操作: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.ccalculate_bounds_data 中可以看到实际用法——分组项的缩放通过 vec2_mulf(scale, scale, mul) 逐分量放大,边界宽/高分别对应 bounds.x / widthbounds.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」,等价于欧氏距离公式 (x1x2)2+(y1y2)2\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}

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);
	}
}

两个实现细节值得注意:

  1. 零向量保护:当 len == 0.0f 时函数直接返回,不写 dst。这意味着如果传入零向量,dst 的内容保持不变(调用方应自行初始化 dst),而不会产生 0/0 的 NaN。这是与许多数学库不同的约定——某些库会返回零向量或 NaN,使用时要意识到 dst 不会被覆盖。
  2. 一次除法优化:实现不是逐分量除以 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_vec2effect.ceffect_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);

分组/对齐逻辑同样基于 vec2add_alignmentobs-scene.c#L325)按对齐标志调整位置向量,pos_from_absolute / pos_to_absoluteobs-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.cset_vec2 内部把 xy 两个分量以子对象形式写入 obs_data_t。这保证了场景项的 pos_relscalevec2 属性能随配置文件(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,而 absfloorceilclosenorm 五个函数以 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

相关文档与源码位置

适用前提:以上接口与行为以当前仓库中 libobs/graphics/vec2.h 的实现为准,属于 libobs 公共 C ABI,适用于任何链接 libobs 的插件或独立程序开发场景。

登录后查看全文
热门项目推荐
相关项目推荐