三维高斯泼溅(3DGS)用大量各向异性高斯分布表达场景,每个高斯点都带有位置、缩放、旋转等属性。其中旋转属性通常以四元数形式存储,用来描述该高斯基元的空间朝向。当我们希望对重建好的模型做局部旋转编辑,例如让桌上的杯子绕自身轴转三十度,就必须直接修改对应高斯点的旋转四元数,而不是仅仅转动相机。理解四元数的运算规则,是把编辑意图正确施加到成千上万个高斯点上的前提。

四元数表示与高斯点旋转属性的底层结构
在3DGS的实现里,每一个高斯点的旋转用一个长度为四的向量表示,分别对应四元数的实部与三个虚部,一般记作 (w, x, y, z)。这个四元数必须是单位长度,因为只有单位四元数才能构成有效的三维旋转,不会产生缩放畸变。原始训练流程在优化时往往会对旋转向量做归一化,但在编辑阶段如果手动改写了数值,就必须自己保证它依旧是单位四元数,否则渲染时协方差矩阵会出错,画面出现拉丝或断裂。
从数学上看,一个单位四元数 q 对三维点 v 的旋转作用写作 q * v * q^{-1},其中 q^{-1} 是共轭除以模长。由于高斯点本身不是孤立的点,而是带有协方差矩阵的形状体,实际渲染时旋转会作用在协方差矩阵上,使其在世界空间正确朝向。因此修改旋转四元数,本质上就是改变这个形状体在世界坐标下的主轴方向。下面这段代码展示了如何从高斯点参数中取出四元数并做基础检查:
import numpy as np
def get_rotation(gaussian_params, idx):
# gaussian_params['rotation'] 形状为 (N, 4)
q = gaussian_params['rotation'][idx]
norm = np.linalg.norm(q)
if abs(norm - 1.0) > 1e-4:
print('警告:该高斯点四元数未归一化,模长为', norm)
return q
q0 = get_rotation(params, 1024)
print('原旋转四元数', q0)
上面的检查逻辑虽简单,却能在调试编辑工具时帮你快速定位因为忘记归一化导致的异常。需要特别提醒,四元数的分量顺序在不同代码库中可能不同,有的把 w 放在最后,阅读开源实现时要先确认内存布局,避免把虚部当实部用。
通过右乘增量四元数实现叠加旋转
假设我们希望把某个高斯点绕世界坐标的Y轴旋转角度 theta,最直观的做法是构造一个对应的增量四元数 dq,然后让新的旋转 q_new = dq * q_old 或者 q_new = q_old * dq,顺序取决于你是想在局部坐标系还是世界坐标系下旋转。对于物体自身绕固定轴自转,通常采用右乘,即 q_new = q_old * dq,这表示在物体当前朝向的局部坐标系里再叠加一次旋转。
构造绕轴 axis 旋转 theta 的四元数时,公式为 (cos(theta/2), axis_x*sin(theta/2), axis_y*sin(theta/2), axis_z*sin(theta/2))。编辑工具中常提供一个滑块控制 theta,每次滑动就重新计算 dq 并作用到选中的高斯点集合上。关键在于,不要直接覆盖原始 q_old,而是保留一份备份,否则多次编辑会累积浮点误差。以下示例演示了批量右乘旋转:
def quat_mul(q1, q2):
w1, x1, y1, z1 = q1
w2, x2, y2, z2 = q2
w = w1*w2 - x1*x2 - y1*y2 - z1*z2
x = w1*x2 + x1*w2 + y1*z2 - z1*y2
y = w1*y2 - x1*z2 + y1*w2 + z1*x2
z = w1*z2 + x1*y2 - y1*x2 + z1*w2
return np.array([w, x, y, z])
def rotate_gaussians(rotations, axis, theta):
axis = axis / np.linalg.norm(axis)
dq = np.array([np.cos(theta/2),
axis[0]*np.sin(theta/2),
axis[1]*np.sin(theta/2),
axis[2]*np.sin(theta/2)])
new_rot = np.zeros_like(rotations)
for i in range(len(rotations)):
q = quat_mul(rotations[i], dq)
new_rot[i] = q / np.linalg.norm(q)
return new_rot
axis_y = np.array([0.0, 1.0, 0.0])
params['rotation'] = rotate_gaussians(params['rotation'], axis_y, np.pi/6)
这段代码的 quat_mul 按照哈密顿乘积展开,没有调用现成库,方便你移植到不支持四元数运算的环境。循环里每次都重新归一化,能防止连续编辑几百次后数值漂移。如果你的高斯点数量在百万级,这种纯Python循环会卡顿,此时应改用PyTorch的向量化运算,把 quat_mul 写成张量操作,在GPU上一次完成所有点的旋转。
局部编辑与绑定骨架两种方案对比
当我们面对的不是一个点而是一块区域,例如一整把椅子的靠背,有两种常见思路。第一种是逐点编辑:用包围盒或语义标签选出相关高斯点,整体施加同一个增量四元数,实现刚体式旋转。它的好处是改动直观、不需要额外结构,缺点是如果选区的点来自多个部件,旋转后可能和未选中的部分穿模,而且每次框选都要遍历属性数组,交互不够流畅。
第二种是绑定骨架方案:在重建出的高斯模型上预先绑定一个简化骨骼,每个高斯点记录自己受哪些骨骼节点影响及权重。编辑时只旋转骨骼节点,再按权重把节点四元数混合并写到高斯点。这种方法适合做角色动画,但前期绑定成本高,且权重计算若不准会让物体表面撕裂。下面的表格列出了两者在典型场景下的差异:
| 对比维度 | 逐点编辑 | 绑定骨架 |
|---|---|---|
| 实现复杂度 | 低,几十行代码 | 高,需蒙皮与权重 |
| 显存占用 | 几乎不增加 | 需存权重矩阵 |
| 交互延迟 | 选区大时明显 | 节点少则流畅 |
| 适用对象 | 静态物件微调 | 可动角色 |
实践中,很多桌面级编辑工具先从逐点编辑做起,验证旋转四元数改写逻辑正确后,再视需求升级到骨架系统。无论哪种方案,核心都离不开前面说的右乘单位四元数与归一化步骤。只要保证每帧写回高斯属性时四元数模长为一,渲染管线就能稳定工作,不必重新跑训练,真正实现所见即所得的旋转编辑。