动画旋转加权前,先把四元数放到同一半球
手臂的两个动画样本都朝向同一方向,各占一半权重,混合器却得到了无法归一化的零向量。单独播放任一样本没有问题,出错的前提仅仅是:一条轨道把旋转写成四元数 q,另一条写成 -q。两份数据描述相同的旋转,四个分量却全部相反。给向量加权的代码可以编译,也可以对多数相近样本工作,却不自动满足动画旋转的表示规则。

图中的同向机械臂和球面相对标记用于区分“旋转相同”与“表示相反”;球体只是概念示意,不是四维单位四元数空间的精确投影。下面用一个独立 C# 旋转混合工程重现抵消,再把验证范围收敛到 CPU 数值结果。它没有接入引擎动画图、蒙皮或 GPU,不据此声称某个引擎内置混合器存在故障。
相同朝向也能把四个分量全部抵消
采用 System.Numerics 的 (x,y,z,w) 分量顺序,单位长度四元数表示旋转。固定输入绕正 Z 轴旋转 90°,将 X 轴转向 Y 轴;其四元数近似为 (0,0,0.707107,0.707107)。四个分量全部取负,不改变它对方向向量的旋转作用。不要把四元数取负理解为反向旋转:单位四元数的逆需要取共轭,二者不是同一个操作。
错误实现把 0.5q + 0.5(-q) 交给归一化。此时长度已经是零,补一个归一化调用无法找回原来的旋转。若在零长度时退回单位旋转,手臂反而会从正确的 90° 跳到 0°;静默回退只是让坏数据变成可显示的错误姿态。
对于这一对样本,可以用第一份旋转作为参考,计算每份样本与参考的四维点积。负点积表示它位于参考的另一半球,将其四个分量一起取负,再做加权。这样第二份输入会改写成 q,合成结果仍为 q。这里改变的是参与计算的表示,没有改变输入姿态,也不需要修改资产中的原始关键帧。

参考旋转要成为混合器的显式输入
$LAB_ROOT/QuaternionBlend.cs 的 QuaternionBlend.Blend 接收参考旋转和 RotationSample 数组。所有样本必须描述同一骨骼、同一父空间下的局部旋转;跨骨骼混合或局部旋转与世界旋转混合,无法靠符号校正修复。以下是完整核心文件。
using System;
using System.Numerics;
public readonly record struct RotationSample(Quaternion Rotation, float Weight);
public static class QuaternionBlend
{
public static Quaternion Blend(Quaternion reference, ReadOnlySpan<RotationSample> samples)
{
RequireUnit(reference);
double x = 0, y = 0, z = 0, w = 0, total = 0;
foreach (var sample in samples)
{
RequireUnit(sample.Rotation);
if (!float.IsFinite(sample.Weight) || sample.Weight < 0)
throw new ArgumentException("Weights must be finite and nonnegative.");
var q = sample.Rotation;
double weight = sample.Weight;
total += weight;
if (Quaternion.Dot(reference, q) < 0) weight = -weight;
x += q.X * weight;
y += q.Y * weight;
z += q.Z * weight;
w += q.W * weight;
}
if (total == 0) throw new ArgumentException("Positive total weight required.");
x /= total; y /= total; z /= total; w /= total;
double length = Math.Sqrt(x * x + y * y + z * z + w * w);
if (!double.IsFinite(length) || length < 1e-6)
throw new ArgumentException("Degenerate rotation blend.");
return new Quaternion((float)(x / length), (float)(y / length),
(float)(z / length), (float)(w / length));
}
private static void RequireUnit(Quaternion q)
{
if (!float.IsFinite(q.X) || !float.IsFinite(q.Y) ||
!float.IsFinite(q.Z) || !float.IsFinite(q.W) ||
Math.Abs((double)q.X * q.X + (double)q.Y * q.Y +
(double)q.Z * q.Z + (double)q.W * q.W - 1) > 1e-4)
throw new ArgumentException("Finite unit quaternion required.");
}
}
非负权重允许相加后再归一化,不要求输入权重预先和为一。实现用 double 累加,除以总权重后检查合成长度,因此 1e-6 阈值衡量的是归一权重下的抵消程度,而不会因为所有权重一起缩小就误报退化。输入单位长度的平方误差容忍度为 1e-4;它是本接口的校验阈值,不是四元数格式的普遍常数。
零权重样本也要满足旋转格式,避免坏资产因当前权重恰好为零而绕过检查。空集合、全零权重、负权重和非有限值均被拒绝。这也明确排除了加法动画中需要另行定义的带符号权重语义。函数只在全部计算完成后返回新旋转,不持有活动骨骼;调用方应在成功返回后替换姿态,异常时采用自己的保持或报错策略。本工程没有实现跨线程姿态发布,不能把这一纯函数边界扩大为完整动画事务。
多轨混合中,不应在遍历过程中用“当前累计结果”不断替换参考,否则符号判断会跟着输入顺序改变。这里所有样本对齐到同一显式参考。在远离半球分界的局部姿态集合内,这个合同容易复核;调用方可以选定稳定的参考姿态,但需要验证参与样本与它之间的角距离,而不能每帧无条件换成当前最大权重轨道。
半球分界不能靠一个小常数解释过去
点积为零时,样本与参考之间对应 180° 的相对旋转,两个符号都位于分界上,没有一边天然更近。当前代码仅在点积小于零时取负,零点积保持原符号。以单位旋转为参考,输入 (0,0,1,0) 及其负值,各占一半,两者点积都为零,最终仍抵消,函数明确抛出异常。
这条回归限制了“换符号不影响结果”的适用范围。靠近分界时,浮点扰动也可能改变取符号的分支;固定参考并不保证所有动画连续。需要覆盖大角度、跨分界的动作时,应在采样与混合层明确路径选择和时间连续性,并记录参考的生命周期。简单把条件改成“小于一个正数”只会搬动分界,不能创造唯一的路径。
$LAB_ROOT/Program.cs 中的 CheckInputs 保留正常与失败断言,Main 执行批量验证并输出 result.json。下面的反例片段可直接放在具备核心类型的测试入口:
var q = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2);
var opposite = new Quaternion(-q.X, -q.Y, -q.Z, -q.W);
var raw = (q + opposite) * 0.5f;
var corrected = QuaternionBlend.Blend(q, [new(q, .5f), new(opposite, .5f)]);
if (raw.LengthSquared() != 0 ||
Vector3.Distance(Vector3.Transform(Vector3.UnitX, corrected), Vector3.UnitY) > 1e-5)
throw new Exception("Antipodal regression failed.");
2026-09-07 使用 .NET 9 Release 执行下列命令。$LAB_ROOT 为本篇研究工程目录;构建退出码为 0,零警告、零错误。测试退出码为 0,通过 12、失败 0、跳过 0,测试检查耗时 46.745 毫秒。耗时仅记录本次运行,不作为性能基准。
cd "$LAB_ROOT"
dotnet build BlendLab.csproj -c Release
dotnet run --project BlendLab.csproj -c Release --no-build
raw_length_squared=0.000000
corrected_rotated_x=0.000000,1.000000,0.000000
t=0.00 nlerp_degrees=0.000000 slerp_degrees=0.000000
t=0.25 nlerp_degrees=27.795773 slerp_degrees=30.000000
t=0.50 nlerp_degrees=60.000000 slerp_degrees=60.000000
t=0.75 nlerp_degrees=92.204229 slerp_degrees=90.000000
t=1.00 nlerp_degrees=120.000006 slerp_degrees=120.000000
passed=12 failed=0 skipped=0 cases=693 elapsed_ms=46.745
693 组样本由绕 Z 轴的 -160° 至 160°、每 10° 一个角度,与 0 至 1、每 0.05 一个权重组成。每组分别翻转第一份、第二份及两份输入的符号,并比较旋转后的 X、Y 两根基向量,误差阈值为 1e-5。第一份输入同时作为参考,因此也覆盖参考整体取负。比较的是旋转效果,不要求四元数分量相同;该网格刻意没有把 ±180° 分界包装成已通过区域。另有端点、普通中点、权重整体缩放与非法数据检查。
27.795773° 说明了另一份合同
符号对齐解决抵消,但加权后归一化仍是归一化线性混合。把终点改成绕 Z 轴 120°,四分之一权重得到约 27.795773°,四分之三得到约 92.204229°;只有中点恰好为 60°。技术图中的对照值由 120° × t 计算,表示本例最短路径匀角速度的解析目标,并非另一套运行时 Slerp 实现的测量结果。
因此,姿态混合与按时间匀速转动不能共用一个未经声明的验收标准。局部、多轨的姿态融合可以选择一次加权归一化,前提是接受其权重到角度的非线性,并把参考分界列入资产或运行时检查。若功能要求两端点之间严格匀角速度,应选择对应的球面插值并单独处理路径;多轨平均、加法旋转和根旋转积分也需要各自定义,不能把这段函数当作通用替代品。
手臂两个相同朝向的样本现在可以稳定融合,因为它们在相加前使用了相容的四元数表示。值得固化在接口里的不是一次零向量回退,而是明确的参考、单位输入与失败结果:表示选择在混合层完成,路径连续性由调用层约束,最终姿态只在有效结果返回后更新。