首页
/ OBS Studio libobs 图形 API 解析:quat 四元数模块的接口、实现原理与旋转插值实践

OBS Studio libobs 图形 API 解析:quat 四元数模块的接口、实现原理与旋转插值实践

2026-09-06 11:43:33作者:傅爽业Veleda

本篇基于 OBS Studio 的 Sphinx API 文档 Quaternion 参考 对应的实际文档文件 reference-libobs-graphics-quat.rst,系统讲解 libobs/graphics 图形数学库中的四元数(quaternion)模块:struct quat 的数据布局、全套组件运算与转换函数、旋转插值算法(slerp/三次插值)的底层实现,以及四元数与矩阵、轴角表示互操作的调用链。读完后你将能够直接在 OBS 插件或效果管线中正确使用四元数表示旋转、进行平滑旋转插值,并理解其实现中规避万向锁(gimbal lock)的设计意图。

1. 模块定位:为什么 libobs 需要四元数

quat 模块属于 OBS Studio 核心库 libobs 的图形(graphics)子系统,是其参考文档体系 reference-libobs-graphics.rst 中 Graphics API Reference 的一个条目,与 vec2/vec3/vec4matrix4axisanggraphics 等模块并列。

头文件 quat.h 顶部注释直接给出了模块的设计动机:

/*
 * Quaternion math
 *
 *   Generally used to represent rotational data more than anything.  Allows
 *   for efficient and correct rotational interpolation without suffering from
 *   things like gimbal lock.
 */

即:四元数主要用于表示旋转数据,它能在避免万向锁的前提下提供高效且正确的旋转插值能力。这在 OBS 的场景过渡(transition)、滤镜(filter)旋转参数随时间平滑变化的场景中是关键能力。

头文件同时以 extern "C" 包裹(quat.h),意味着该模块以 C 接口暴露、可在 C++ 前端代码中直接调用;引入方式为:

#include <graphics/quat.h>

2. struct quat 数据结构:四分量 + SSE 向量的联合布局

API 文档对 struct quat 的定义是:

#include <graphics/quat.h>

struct quat {
	float x;        /* X component */
	float y;        /* Y component */
	float z;        /* Z component */
	float w;        /* W component */
	float ptr[4];   /* Unioned array of all components */
};

对应源码中 quat.h 的实际实现,该结构体是一个 union

struct quat {
	union {
		struct {
			float x, y, z, w;
		};
		float ptr[4];
		__m128 m;
	};
};

从源码结构看,这里有一个文档未列出的重要细节:联合体中除了命名分量 x/y/z/w 和数组视图 ptr[4] 外,还有一个 __m128 m 成员,即把四个 float 当作一条 SSE 向量寄存器。这正是 libobs 数学库性能设计的关键——几乎每个组件运算函数都通过 m 成员做单条 SIMD 指令级的操作。

约定上:(x, y, z) 为旋转轴相关的向量部分,w 为标量部分;单位四元数满足 x*x + y*y + z*z + w*w == 1

3. 组件级运算函数:内联实现与参数说明

API 文档列出的组件级函数全部是 static inline,直接在头文件中展开,无函数调用开销。以下按文档逐条对照源码说明。

3.1 初始化与赋值

文档函数 源码实现 说明
void quat_identity(struct quat *dst) quat.h#L52-L56 设为单位四元数 {0, 0, 0, 1},即"无旋转"。实现为 _mm_setzero_ps() 清零后再写 q->w = 1.0f
void quat_set(struct quat *dst, float x, float y, float z, float w) quat.h#L58-L61 按分量赋值,底层为 _mm_set_ps(x, y, z, w) 一条指令写入四个分量。注意文档中参数列表存在排版笔误(出现两次 y),实际签名为 x, y, z, w 四个参数
void quat_copy(struct quat *dst, const struct quat *v) quat.h#L63-L66 整体拷贝,实现为一次 __m128 寄存器赋值 dst->m = q->m

3.2 加减乘与标量运算

函数 实现位置 语义
void quat_add(struct quat *dst, const struct quat *v1, const struct quat *v2) quat.h#L68-L71 逐分量相加,_mm_add_ps(q1->m, q2->m)
void quat_sub(struct quat *dst, const struct quat *v1, const struct quat *v2) quat.h#L73-L76 逐分量相减(dst = v1 - v2),_mm_sub_ps
void quat_mul(struct quat *dst, const struct quat *v1, const struct quat *v2) quat.c#L30-L45 四元数乘积,见下节详解
void quat_addf(struct quat *dst, const struct quat *v, float f) quat.h#L80-L83 每个分量加标量 f_mm_add_ps(q->m, _mm_set1_ps(f))
void quat_subf(struct quat *dst, const struct quat *v, float f) quat.h#L85-L88 每个分量减标量 f
void quat_mulf(struct quat *dst, const struct quat *v, float f) quat.h#L90-L93 每个分量乘标量 f

需要强调:quat_add/quat_sub/标量运算都是分量级的线性代数操作,它们并不构成四元数群意义上的"旋转组合"——组合两个旋转必须使用 quat_mul

从源码结构看,quat.h 中还提供了文档未收录的一组辅助内联函数:

  • quat_divfL95-L98):逐分量除以标量;
  • quat_negL116-L122):四分量取负,与 quat_inv 的区别在于 neg 会连 w 一起取负;
  • quat_normL140-L144):归一化,点积为 0 时输出全零向量;
  • quat_closeL146-L151):以给定 epsilon 判断两个四元数各分量是否接近。

3.3 模长、点积与距离

  • float quat_dot(const struct quat *v1, const struct quat *v2)quat.h#L100-L107):四维点积。实现上用两次 shuffle/add 把 __m128 四分量水平归约为 x,返回 v1·v2。两个单位四元数的点积等于它们夹角余弦的 ±1,这一性质是后面 quat_interpolate(slerp)算法的基础。
  • float quat_len(const struct quat *v)quat.h#L124-L128):sqrtf(quat_dot(v, v)),点积非正时返回 0.0f 防御性处理。
  • float quat_dist(const struct quat *v1, const struct quat *v2)quat.h#L130-L138):先 quat_sub 求差、再对自身做点积开方,即四维欧氏距离。

3.4 quat_inv:共轭逆

static inline void quat_inv(struct quat *dst, const struct quat *q)
{
	dst->x = -q->x;
	dst->y = -q->y;
	dst->z = -q->z;
}

quat.h#L109-L114。实现只取向量部分的负值、保留 w 不变,这是单位四元数的共轭(conjugate),等价于逆旋转。由于没有除以模长,对非单位四元数它只是共轭而非严格逆——使用时应保证输入为(近)单位四元数。

4. quat_mul:四元数乘法的源码实现

quat_mul 是唯一的非内联基础运算,实现在 quat.c#L30-L45

void quat_mul(struct quat *dst, const struct quat *q1, const struct quat *q2)
{
	struct vec3 q1axis, q2axis;
	struct vec3 temp1, temp2;

	quat_vec3(&q1axis, q1);   /* 取 q 的前三分量 x,y,z 作为 vec3 */
	quat_vec3(&q2axis, q2);

	vec3_mulf(&temp1, &q2axis, q1->w);
	vec3_mulf(&temp2, &q1axis, q2->w);
	vec3_add(&temp1, &temp1, &temp2);
	vec3_cross(&temp2, &q1axis, &q2axis);
	vec3_add((struct vec3 *)dst, &temp1, &temp2);

	dst->w = (q1->w * q2->w) - vec3_dot(&q1axis, &q2axis);
}

对照标准四元数乘法公式(记向量部分为 p、q,标量为 s₁、s₂):

结果向量 = s1·q + s2·p + p × q
结果标量 = s1·s2 − p·q

源码正是这一公式的直接实现:temp1 = s1*q2vec + s2*q1vec,再叠加叉积 q1vec × q2vec 得到结果向量;w 由标量之积减去向量点积得到。由于 quat_vec3 辅助函数(quat.c#L24-L28)直接把四元数 __m128 前三分量"借位"成 struct vec3w=0,整个过程没有内存拷贝开销。

5. 表示互转:轴角、矩阵、朝向向量

5.1 quat_from_axisang:轴角 → 四元数

文档签名:void quat_from_axisang(struct quat *dst, const struct axisang *aa),输入类型为 axisang.h 中定义的 struct axisangx/y/z 为旋转轴,w 为角度,弧度制)。实现(quat.c#L47-L56):

void quat_from_axisang(struct quat *dst, const struct axisang *aa)
{
	float halfa = aa->w * 0.5f;
	float sine = sinf(halfa);

	dst->x = aa->x * sine;
	dst->y = aa->y * sine;
	dst->z = aa->z * sine;
	dst->w = cosf(halfa);
}

即经典公式 q = (axis·sin(θ/2), cos(θ/2))。反向转换由同库的 axisang_from_quataxisang.h#L61,详见 Axis Angle 参考文档)完成。

5.2 quat_from_matrix4:矩阵 → 四元数

文档描述其"提取矩阵的旋转属性转换为四元数"。实现位于 quat.c#L67-L104,是标准的"特征值最大分支"算法,按数值稳定性选择两条路径:

  • 主对角线迹 tr = m->x.x + m->y.y + m->z.z > 0:先解出 w = 0.5·sqrtf(tr + 1),再用 0.5 / four_d 由矩阵反对称元素交叉差解出 x/y/z
  • 否则:找出主对角线最大元素所在下标 i(及循环下标 jk),先解出 ptr[i],再对称填充其余两个分量与 w,避免 sqrtf(tr+1) 接近零时的数值退化。

另外 quat_from_matrix3quat.h#L154 声明,quat.c#L62-L65 实现)只是把 matrix3 按位重解释为 matrix4 后转发给 quat_from_matrix4

5.3 quat_get_dirquat_set_look_dir:朝向向量互转

  • void quat_get_dir(struct vec3 *dst, const struct quat *q)quat.c#L106-L111):先经 matrix3_from_quat 把四元数转成旋转矩阵,再取矩阵 z 轴作为该四元数所代表的"前向"方向。
  • void quat_set_look_dir(struct quat *dst, const struct vec3 *dir)quat.c#L113-L146):由指定"注视方向"构造四元数。算法要点(从源码结构看):
    1. dir 归一化并取负(与"前向"取反约定对齐);
    2. close_float(..., EPSILON) 判断 XZ 平面分量、Y 分量是否为零,得到 xz_valid / yz_valid
    3. XZ 有效时构造绕 Y 轴旋转 atan2f(new_dir.x, new_dir.z) 的四元数 xz_rot;Y 有效时构造绕 X 轴旋转 asinf(new_dir.y) 的四元数 yz_rot
    4. 按有效性组合:两者都有效时用 quat_mul 复合两个旋转,否则取有效者。

这一实现本质上把"look at"分解为 yaw × pitch 两次基本旋转,并用有效性检查规避了朝向轴与旋转轴平行时 atan2f/asinf 的退化情形。

5.4 与矩阵模块的双向协作

四元数不是孤立模块,matrix3.c 提供了反向路径 matrix3_from_quat

void matrix3_from_quat(struct matrix3 *dst, const struct quat *q)
{
	float norm = quat_dot(q, q);
	float s = (norm > 0.0f) ? (2.0f / norm) : 0.0f;
	/* 由 xx/yy/zz/xy/xz/yz/wx/wy/wz 组合出旋转矩阵三行 */
}

它使用 s = 2 / |q|² 因子,因此不要求输入是单位四元数,非归一化输入也能得到正确的旋转矩阵(只是精度受输入缩放影响)。配套的 matrix3_rotatematrix3.c#L81-L86)则是"先四元数建旋转矩阵、再与已有矩阵相乘"的便捷封装;matrix3_from_axisangmatrix3.c#L45-L50)内部同样先经 quat_from_axisang 中转。这构成了 libobs 中"轴角 ⇄ 四元数 ⇄ 矩阵"的完整转换闭环。

6. 旋转插值:quat_interpolatequat_get_tangentquat_interpolate_cubic

这是本模块对 OBS 场景过渡/动画系统最有价值的部分。

6.1 quat_interpolate:球面线性插值(slerp)

文档签名:void quat_interpolate(struct quat *dst, const struct quat *q1, const struct quat *q2, float t)t 取值范围 0.0f..1.0f。实现(quat.c#L174-L195):

void quat_interpolate(struct quat *dst, const struct quat *q1, const struct quat *q2, float t)
{
	float dot = quat_dot(q1, q2);
	float anglef = acosf(dot);
	float sine, sinei, sinet, sineti;
	struct quat temp;

	if (anglef >= EPSILON) {
		sine = sinf(anglef);
		sinei = 1 / sine;
		sinet = sinf(anglef * t) * sinei;
		sineti = sinf(anglef * (1.0f - t)) * sinei;

		quat_mulf(&temp, q1, sineti);
		quat_mulf(dst, q2, sinet);
		quat_add(dst, &temp, dst);
	} else {
		quat_sub(&temp, q2, q1);
		quat_mulf(&temp, &temp, t);
		quat_add(dst, &temp, q1);
	}
}

结构上分两支:

  • 正常分支:两四元数夹角 anglef = acos(q1·q2) 不小于 EPSILON 时,按标准 slerp 公式 dst = q1·sin(θ(1−t))/sinθ + q2·sin(θt)/sinθ 计算,权重在单位球面上沿最短大圆弧线分配;
  • 退化分支:夹角极小时(sine 接近 0,公式数值不稳定),退化为普通线性插值 dst = q1 + (q2 − q1)·t

slerp 保证插值结果落在单位四元数球面上且角速度恒定,这正是头文件注释所说"efficient and correct rotational interpolation"的落地。

6.2 quat_get_tangent:三次插值的切向量

void quat_get_tangent(struct quat *dst, const struct quat *prev, const struct quat *q,
		      const struct quat *next)
{
	struct quat temp;

	quat_sub(&temp, q, prev);
	quat_add(&temp, &temp, next);
	quat_sub(&temp, &temp, q);
	quat_mulf(dst, &temp, 0.5f);
}

quat.c#L197-L205。展开即 dst = 0.5·(next − prev)q 先被加又被减、抵消),是 Catmull-Rom 样条的中心差分切向量,用于描述关键帧 q 处旋转变化的"斜率"。

6.3 quat_interpolate_cubic:三次 Hermite 插值

void quat_interpolate_cubic(struct quat *dst, const struct quat *q1, const struct quat *q2,
			    const struct quat *m1, const struct quat *m2, float t)
{
	struct quat temp1, temp2;

	quat_interpolate(&temp1, q1, q2, t);
	quat_interpolate(&temp2, m1, m2, t);
	quat_interpolate(dst, &temp1, &temp2, 2.0f * (1.0f - t) * t);
}

quat.c#L207-L215。它复用 slerp 完成三次 Hermite 样条:先用 quat_interpolate 分别在位置 (q1, q2) 与切向 (m1, m2) 之间插值,再以 2t(1−t) 为混合权重把两者复合。使用时,m1/m2 通常由相邻关键帧通过 quat_get_tangent 预先算出,从而在多关键帧旋转序列间获得 C¹ 连续的平滑过渡。

7. 文档未收录但对插值体系重要的函数:quat_log / quat_exp

从源码结构看,quat.h#L160-L161 还声明了 quat_logquat_exp 这对函数对,文档页面未列出,但它们是四元数"旋转空间线性化"的工具:

  • quat_logquat.c#L148-L161):把四元数映射为纯虚四元数(w 置 0、轴方向乘以 angle/sin(angle)),即对数映射,得到"旋转矢量"表示;
  • quat_expquat.c#L163-L172):反向的指数映射,w = cosf(length),轴向量乘以 sin(length)/length,长度接近 0 时退化为 1.0f。

两者配合可用于把多个小旋转变量相加后再指数化,实现旋转空间的"线性"组合;quat_from_axisang 本质上也是 exp 的一种特例形式。

8. 实践要点与阅读指引

  1. 组合旋转用 quat_mul,不要用 quat_addquat_add/sub 只是分量线性运算;把两个旋转依次作用,必须乘起来,且顺序敏感(非交换)。
  2. 插值前保证近单位化quat_interpolate 的 slerp 支路以 acos(q1·q2) 求夹角,输入若严重偏离单位长度会导致角度失真,可先用 quat_normquat.h#L140-L144)规整。
  3. 朝向类操作优先走 quat_get_dir / quat_set_look_dir:它们与矩阵模块的 z 轴"前向"约定一致(见 quat.c#L106-L111m.z 取值)。
  4. 相关文档入口
  5. 核心源码文件

总结:OBS Studio 的 quat 模块以 x/y/z/w + ptr[4] + __m128 三重视图的联合结构为底座,用 SSE 内联函数覆盖组件级运算,在 quat.c 中实现四元数乘法、轴角/矩阵互转、look 方向构造,以及以 slerp 为核心、Catmull-Rom 切向量增强的两层级旋转插值。它是 libobs 中所有"随时间平滑变化的旋转"能力的数学基础,配合 matrix3/axisang/vec3 模块即可覆盖 OBS 效果与过渡管线中的完整旋转计算需求。

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