为何四元数整体Slerp与分轴Slerp后相乘结果不一致?
分轴四元数Slerp插值与完整四元数Slerp结果偏差问题
问题背景
我需要将完整四元数拆分为X、Y、Z三个单轴旋转四元数,分别执行Slerp插值后再相乘合并,但结果与直接对完整四元数执行Slerp的结果存在显著偏差。测试验证过拆分后的单轴四元数可以正确重构原完整四元数,但插值后的结果差异明显。由于动画约束必须采用分轴插值方式,特此询问该方案是否可行。
测试代码及数据
var quat_A_full = quaternion.EulerXYZ(ax_rad, ay_rad, az_rad);//-0.3161251f, 0.7749172f, 0.0828281f, 0.5410246f var quat_B_full = quaternion.EulerXYZ(bx_rad, by_rad, bz_rad);//-0.5004139f, 0.5069054f, 0.1151911f, 0.6923611f var quat_A_x = quaternion.RotateX(ax_rad);//0.2424405f, 0f, 0f, 0.9701663f var quat_A_y = quaternion.RotateY(ay_rad);//0f, 0.8527206f, 0f, 0.5223673f var quat_A_z = quaternion.RotateZ(az_rad);//0f, 0f, 0.5134861f, 0.8580979f var quat_B_x = quaternion.RotateX(bx_rad);//-0.7160885f, 0f, 0f, 0.6980096f var quat_B_y = quaternion.RotateY(by_rad);//0f, 0.4602496f, 0f, 0.8877895f var quat_B_z = quaternion.RotateZ(bz_rad);//0f, 0f, -0.3183014f, 0.9479896f var quat_A_full_reconstructed = math.mul(quat_A_z, math.mul(quat_A_y, quat_A_x); // quat_A_full == quat_A_full_reconstructed <-- 结果为TRUE var quat_B_full_reconstructed = math.mul(quat_B_z, math.mul(quat_B_y, quat_B_x); // quat_B_full == quat_B_full_reconstructed <-- 结果也为TRUE // 但插值后结果偏差明显 var result_full = math.slerp(quat_A_full, quat_B_full, animation); var result_parts = math.mul( math.slerp(quat_A_z, quat_B_z, animation), math.mul(math.slerp(quat_A_y, quat_B_y, animation), math.slerp(quat_A_x, quat_B_x, animation)) ); // 两者结果差异显著: // result_full: // (-0.4088441f, 0.6605082f, 0.09956253f, 0.621822f) // 欧拉角(度): [ x: -107.9114542, y: 47.7336353, z: 80.7913939 ] // vs // result_parts: // (-0.2624312f, 0.6446596f, 0.2598185f, 0.6693567f) // 欧拉角(度): [ x: -87.4064322, y: 46.6058942, z: 87.1785867 ]
使用的Unity风格Slerp函数
public static quaternion slerp(quaternion q1, quaternion q2, float t) { float dt = dot(q1, q2); if (dt < 0.0f) { dt = -dt; q2.value = -q2.value; } if (dt < 0.9995f) { float angle = acos(dt); float s = rsqrt(1.0f - dt * dt); // 等价于1.0f / sin(angle) float w1 = sin(angle * (1.0f - t)) * s; float w2 = sin(angle * t) * s; return quaternion(q1.value * w1 + q2.value * w2); } else { // 角度较小时使用线性插值 return nlerp(q1, q2, t); } }
解答
这个方案不可行,核心原因是四元数乘法不满足线性性,分轴插值再合并的逻辑和完整四元数球面插值的本质完全不同:
- 四元数Slerp是在四元数的球面空间上插值两个旋转之间的最短路径,而分轴插值相当于对每个欧拉分量独立插值后再复合旋转,本质是欧拉角空间的插值,天生存在路径不唯一、万向锁等问题,和球面插值的路径完全不一致。
- 旋转的复合顺序会影响最终结果,即使单轴插值的顺序和原四元数构建顺序一致,独立插值后的单轴旋转已经脱离了原旋转的耦合关系,无法还原完整四元数Slerp的中间状态。
如果必须满足分轴动画约束,建议采用两种替代方案:
- 将四元数转换为欧拉角,对每个欧拉分量进行插值(需处理角度周期跨越±180度的情况),再转换回四元数。但要注意万向锁问题可能带来的异常。
- 先对完整四元数执行Slerp,再对结果进行目标轴的旋转约束修正,这种方式既保留了球面插值的平滑性,又能满足约束要求。
内容的提问来源于stack exchange,提问作者Spectraljump
相关产品推荐
相关产品推荐

