OBS Studio libobs 图形 API 解析:quat 四元数模块的接口、实现原理与旋转插值实践
本篇基于 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/vec4、matrix4、axisang、graphics 等模块并列。
头文件 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_divf(L95-L98):逐分量除以标量;quat_neg(L116-L122):四分量取负,与quat_inv的区别在于neg会连w一起取负;quat_norm(L140-L144):归一化,点积为 0 时输出全零向量;quat_close(L146-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 vec3 且 w=0,整个过程没有内存拷贝开销。
5. 表示互转:轴角、矩阵、朝向向量
5.1 quat_from_axisang:轴角 → 四元数
文档签名:void quat_from_axisang(struct quat *dst, const struct axisang *aa),输入类型为 axisang.h 中定义的 struct axisang(x/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_quat(axisang.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(及循环下标j、k),先解出ptr[i],再对称填充其余两个分量与w,避免sqrtf(tr+1)接近零时的数值退化。
另外 quat_from_matrix3(quat.h#L154 声明,quat.c#L62-L65 实现)只是把 matrix3 按位重解释为 matrix4 后转发给 quat_from_matrix4。
5.3 quat_get_dir 与 quat_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):由指定"注视方向"构造四元数。算法要点(从源码结构看):- 对
dir归一化并取负(与"前向"取反约定对齐); - 用
close_float(..., EPSILON)判断 XZ 平面分量、Y 分量是否为零,得到xz_valid/yz_valid; - XZ 有效时构造绕 Y 轴旋转
atan2f(new_dir.x, new_dir.z)的四元数xz_rot;Y 有效时构造绕 X 轴旋转asinf(new_dir.y)的四元数yz_rot; - 按有效性组合:两者都有效时用
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_rotate(matrix3.c#L81-L86)则是"先四元数建旋转矩阵、再与已有矩阵相乘"的便捷封装;matrix3_from_axisang(matrix3.c#L45-L50)内部同样先经 quat_from_axisang 中转。这构成了 libobs 中"轴角 ⇄ 四元数 ⇄ 矩阵"的完整转换闭环。
6. 旋转插值:quat_interpolate、quat_get_tangent、quat_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_log 与 quat_exp 这对函数对,文档页面未列出,但它们是四元数"旋转空间线性化"的工具:
quat_log(quat.c#L148-L161):把四元数映射为纯虚四元数(w置 0、轴方向乘以angle/sin(angle)),即对数映射,得到"旋转矢量"表示;quat_exp(quat.c#L163-L172):反向的指数映射,w = cosf(length),轴向量乘以sin(length)/length,长度接近 0 时退化为 1.0f。
两者配合可用于把多个小旋转变量相加后再指数化,实现旋转空间的"线性"组合;quat_from_axisang 本质上也是 exp 的一种特例形式。
8. 实践要点与阅读指引
- 组合旋转用
quat_mul,不要用quat_add:quat_add/sub只是分量线性运算;把两个旋转依次作用,必须乘起来,且顺序敏感(非交换)。 - 插值前保证近单位化:
quat_interpolate的 slerp 支路以acos(q1·q2)求夹角,输入若严重偏离单位长度会导致角度失真,可先用quat_norm(quat.h#L140-L144)规整。 - 朝向类操作优先走
quat_get_dir/quat_set_look_dir:它们与矩阵模块的z轴"前向"约定一致(见 quat.c#L106-L111 的m.z取值)。 - 相关文档入口:
- 本篇对应的 Sphinx 源文件:docs/sphinx/reference-libobs-graphics-quat.rst
- Graphics API Reference 总目录(含 vec2/vec3/vec4/quat/matrix4/axisang/effects 等条目):docs/sphinx/reference-libobs-graphics.rst
- 轴角表示参考:docs/sphinx/reference-libobs-graphics-axisang.rst
- 核心源码文件:
- 结构体与内联运算:libobs/graphics/quat.h
- 乘法、转换与插值实现:libobs/graphics/quat.c
- 轴角结构:libobs/graphics/axisang.h
- 矩阵 ⇄ 四元数互转:libobs/graphics/matrix3.c
总结:OBS Studio 的 quat 模块以 x/y/z/w + ptr[4] + __m128 三重视图的联合结构为底座,用 SSE 内联函数覆盖组件级运算,在 quat.c 中实现四元数乘法、轴角/矩阵互转、look 方向构造,以及以 slerp 为核心、Catmull-Rom 切向量增强的两层级旋转插值。它是 libobs 中所有"随时间平滑变化的旋转"能力的数学基础,配合 matrix3/axisang/vec3 模块即可覆盖 OBS 效果与过渡管线中的完整旋转计算需求。
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