· 5 分钟阅读 · 1995 字
同一姿态混合后消失:四元数加权前必须对齐半球
两段动画采样都让骨骼绕 Z 轴旋转 60°,混合权重各为一半,结果却可能没有可归一化的旋转。固定输入中的两个采样分别是 q=(0,0,0.5,0.8660254) 与 -q=(0,0,-0.5,-0.8660254)。它们表示同一个姿态,直接相加却恰好得到零。若运行时把归一化失败替换成单位旋转,骨骼就会从 60° 突然跳回静止方向。

这个错误来自表示空间。单位四元数对三维旋转是双重覆盖,q 与 -q 的四个分量相反,但对向量的旋转作用相同。动画混合器若把分量当成普通属性相加,就让资产导出器或采样缓存选择的符号参与了姿态决策。问题应在混合入口解决;在输出处补一次单位化,无法从零向量中恢复已被抵消的方向。
混合前先确定共同参照
设单位参考旋转为 r。每个输入先单位化,再检查 dot(r,q):点积为负就翻转四个分量,使所有参与者落在同一个参考半球。随后按非负权重求和,最后单位化。固定案例取单位旋转作为参考,两个点积分别为 +0.8660254 和 -0.8660254;翻转后,两份输入都成为 q。
这里的点积是四维内积,不能用骨骼前向轴的三维点积替代。后者忽略绕前向轴的扭转,无法选择四元数表示。参考也不能取“当前累计和”:累计和会随遍历顺序变化,甚至在尚未对齐时已经接近零。当前实现由调用方传入一份固定参考,同一次混合的所有输入都使用它。
真实探针位于 .tmp/daily-labs/2026-10-31-2000/。调用链是 Program.Main → QuaternionBlend.Blend → Validate,失败对照保留在 UnalignedSum;核心文件 QuaternionBlend.cs 的完整实现如下:
using System.Numerics;
public static class QuaternionBlend
{
public static Quaternion Blend(Quaternion reference, Quaternion[] rotations, float[] weights)
{
Validate(reference);
if (rotations.Length == 0 || rotations.Length != weights.Length)
throw new ArgumentException("Mismatched blend inputs");
reference = Quaternion.Normalize(reference);
double total = 0;
for (int i = 0; i < weights.Length; i++)
{
Validate(rotations[i]);
if (!float.IsFinite(weights[i]) || weights[i] < 0)
throw new ArgumentOutOfRangeException(nameof(weights));
total += weights[i];
}
if (total <= 0) throw new ArgumentException("No active weight");
Quaternion sum = new(0, 0, 0, 0);
for (int i = 0; i < rotations.Length; i++)
{
Quaternion q = Quaternion.Normalize(rotations[i]);
float dot = Quaternion.Dot(reference, q);
if (MathF.Abs(dot) <= 1e-6f)
throw new ArgumentException("Ambiguous hemisphere boundary");
if (dot < 0) q = -q;
sum += q * (float)(weights[i] / total);
}
if (sum.LengthSquared() <= 1e-12f)
throw new ArgumentException("Degenerate weighted rotation");
return Quaternion.Normalize(sum);
}
public static Quaternion UnalignedSum(Quaternion[] rotations, float[] weights)
{
Quaternion sum = new(0, 0, 0, 0);
for (int i = 0; i < rotations.Length; i++) sum += rotations[i] * weights[i];
return sum;
}
private static void Validate(Quaternion q)
{
float length = q.LengthSquared();
if (!float.IsFinite(q.X) || !float.IsFinite(q.Y) || !float.IsFinite(q.Z) ||
!float.IsFinite(q.W) || !float.IsFinite(length) || length <= 1e-12f)
throw new ArgumentOutOfRangeException(nameof(q));
}
}
权重总和使用 double 累加,再把归一化比例乘到输入上,避免两个合法的最大 float 权重相加时先溢出。这样既允许权重不预先合为一,也限制了加权和的分量范围。它只接受非负权重;附加动画中的有符号旋转增量属于另一种组合合同,不能直接复用这个平均器。
输入合法性检查包含权重为零的项。这是刻意选择的严格接口:即使某层当前不贡献结果,非法旋转或处于半球边界的采样仍会被拒绝。若运行时希望关闭层不参与校验,应由上游先删除该项,并让缓存与诊断遵循同一规则,不能把“零权重是否忽略”留成平台差异。

固定参考解决符号,不能替代旋转路径
固定参考还界定了允许混合的姿态范围。当 dot(r,q)=0,该输入相对参考恰好旋转半周,正负表示离参考同样远,单靠点积无法选择连续分支。探针对绝对点积不超过 1e-6 的输入直接拒绝。这个阈值是接口的数值保护带,不是通用动画容差;跨越它的动作需要由动画状态或路径信息决定分支。
在角色局部姿态邻域内,可以让每个骨骼的稳定基准姿态拥有参考。若改用上一帧结果,参考便成为持久状态,重置、传送、回放起点和网络纠正都需要定义它的生命周期。另一种做法是选择当前权重最大的输入作参考,但权重主导者切换时参考也会改变,必须额外验证切换连续性。混合函数只能消费参考,无法替调用方决定这些状态政策。
半球对齐也不使加权单位化变成恒角速度插值。只有对齐后的两输入内积为正时,等权结果才是两者短弧中点;一般权重下,球面上的推进速度并不线性。固定参考甚至不保证任意两个输入之间选择短弧:相对单位参考,绕 Z 的 +170° 与 -170° 都无需翻转,等权结果为单位旋转,而两输入短弧中点在 180°。探针把这个长跨度样本固定为参考政策的边界回归。当前切片适合把多份相近局部旋转融合成一份姿态。若需求是相机以固定角速度转向,或明确指定超过半周的旋转轨迹,应保留对应的插值参数和路径,不能只交付一组无序姿态与权重。
用符号翻转作为回归变换
Program.Main 固定同一姿态的相反表示,RandomRegression 用种子 200031 生成 10,000 组四输入混合,每个轴来自正分量向量的单位化,角度限定在 [-1.2,1.2] 弧度,权重为正。每组先求结果,再随机翻转部分输入的四个分量,重新求值并比较。输入旋转没有改变,输出就不应改变;本次最大分量差为 0。
固定案例的决定性输出为:
{
"passed": 23,
"failed": 0,
"skipped": 0,
"samples": 10000,
"unaligned_length": 0,
"aligned_rotation": [0, 0, 0.5, 0.8660254],
"rotated_x": [0.5, 0.8660254, 0],
"max_component_error": 0
}
这里的 rotated_x 是对运行结果中的三维向量作数组表示。它给出可直接验收的空间效果:局部 X 轴被转向 XY 平面内的 60° 方向。检查同时覆盖相同表示、不同权重、输入归一化、参考符号翻转、中点、零权重,以及负权重、非有限值、空数组、长度不匹配、零旋转和半周歧义的拒绝。
验证在 .NET 9、System.Numerics 的 CPU 环境执行:
dotnet build $WORK_DIR/QuaternionBlendLab.csproj -c Release
dotnet run --project $WORK_DIR/QuaternionBlendLab.csproj -c Release --no-build
构建退出码 0,零警告、零错误,工具报告 5.97 s;运行退出码 0,通过 23、失败 0、跳过 0,内部计时 30.6859 ms。该计时只记录本次验证,不用于推断动画系统吞吐。测试没有接入骨骼层级、根运动或实际播放器,结论限于所示加权旋转函数。
混合入口应交付“同父局部空间的旋转、非负权重、稳定参考及半周处理政策”,然后再把结果送入层级世界变换。仅在资产导出时统一 w≥0 不能覆盖全部连续动画:路径经过 w=0 时仍会发生表示切换。把符号无关性作为混合器的回归性质,同时由姿态状态拥有参考,才能让同一动作不因四元数存储符号不同而被混成静止姿态。