游戏中的数学 07:插值参数与旋转插值
上一章已保证姿态端点为单位四元数,本章在该前提下定义插值参数与角速度误差。插值器只负责从两个已知端点求中间值,不拥有时间源或网络真值。若将 Lerp 同时承担平滑与计时职责,帧率、缓冲时间和旋转速度会形成隐式耦合。验证固定 t∈[0,1],对标量曲线和姿态各采样 101 点,并以相邻角步长判断速度稳定性。
// $WORK_DIR/.tmp/game-math-lab/Program.cs · Chapter07
var q0 = Quaternion.Identity;
var q1 = Quaternion.CreateFromAxisAngle(
Vector3.UnitY, (float)Math.PI);
for (var i = 0; i <= 100; i++)
{
var t = i / 100.0;
linear.Add(new P(t, t));
smooth.Add(new P(t, t * t * (3 - 2 * t)));
nAngles.Add(QuatAngle(Quaternion.Normalize(
Quaternion.Lerp(q0, q1, (float)t))));
sAngles.Add(QuatAngle(
Quaternion.Slerp(q0, q1, (float)t)));
}
参数定义
Lerp(a,b,t)=a+(b-a)t 定义值域中的直线;InverseLerp 将实际值恢复为参数。SmoothStep(t)=t²(3-2t) 改变参数推进曲线,但不处理时间抖动、丢包或状态回滚。参数由时间系统或快照缓冲提供,插值器保持无状态,从而对同一输入产生相同结果。
边界缺少数据时,恢复策略仍由上游持有:网络层选择保持、外推或等待,动画层按片段规则收尾,插值器不推断缺失状态。回放、客户端预测校正和编辑器预览因此可以共享同一个纯函数,而不共享隐藏计时器。
// Program.cs · 参数函数与旋转角读取
static bool TryInverseLerp(
double a, double b, double value, out double t)
{
var span = b - a;
if (Math.Abs(span) < 1e-9) { t = 0; return false; }
t = (value - a) / span;
return true;
}
static double QuatAngle(Quaternion q) =>
2 * Math.Acos(
Math.Clamp(Math.Abs(q.W), 0, 1));
static double[] Differences(
IReadOnlyList<double> values) =>
Enumerable.Range(1, values.Count - 1)
.Select(i => values[i] - values[i - 1])
.ToArray();

角速度误差
位置可以线性插值;单位四元数在等角速度约束下使用 Slerp。180° 样本将差异放大到可测量尺度:Slerp 相邻角步长的标准差为 2.9627e-7 rad,接近浮点噪声;归一化 Lerp 为 6.4299e-3 rad。端点一致不等于运动合同一致,算法选择取决于允许的角速度误差。
// Program.cs · Chapter07:角步长统计
var nSteps = Differences(nAngles);
var sSteps = Differences(sAngles);
var nStd = Std(nSteps);
var sStd = Std(sSteps);
static double Std(IEnumerable<double> source)
{
var values = source.ToArray();
var mean = values.Average();
return Math.Sqrt(values.Sum(x =>
(x - mean) * (x - mean)) / values.Length);
}
metric value unit
samples_per_path 101 points
nlerp_angular_step_std 0.006429866664812347 rad
slerp_angular_step_std 0.000000296269524928 rad
adjacent_steps_per_path 100 steps
acceptance_ratio slerp < nlerp * 0.01 boolean
result PASS state
退化条件
Nlerp 能够保持正确端点,但不保证大角度区间中的等角速度。断言要求 Slerp 的角步长标准差低于 Nlerp 的百分之一,将视觉差异转换为可回归的速度合同。TryInverseLerp(10,10,15) 固定返回 false,避免零区间生成无穷或 NaN。
逐帧执行 current=Lerp(current,target,k) 时,起点持续变化,k 不再描述整段路径,抵达时间因而依赖帧数。确定时长的运动保存固定起点、终点和开始 Tick,并由统一时钟计算 t。视觉阻尼可以使用递进 Lerp,但其状态限于渲染层,不反馈到逻辑姿态。
// Program.cs · Chapter07:回归门槛
Check(
sStd < nStd * .01,
"slerp angular steps are effectively constant");
Check(TryInverseLerp(10, 20, 15, out var t)
&& Math.Abs(t - .5) < 1e-9,
"inverse lerp recovers parameter");
Check(!TryInverseLerp(10, 10, 15, out _),
"zero interval fails explicitly");
WriteData(7, new
{
samples = 101,
nlerp_step_std_rad = nStd,
slerp_step_std_rad = sStd,
smoothstep_midpoint = .5
});

实验结果
输出包含 101 个采样点和两种角步长标准差,可用于判断速度合同是否成立。该实验未测量 CPU 成本,因此不支持插值器性能结论;性能选择仍需针对实际骨骼数量、动画层数和目标平台建立独立基准。批处理成本超出预算时,可先按可见性与重要度分层,再评估降低插值精度的影响。
$ cd $WORK_DIR
$ dotnet run --project .tmp/game-math-lab/GameMathLab.csproj -- --out .tmp/game-math-lab/out
CH07 PASS samples=101 nlerp_step_std=6.430E-003rad slerp_step_std=2.963E-007rad
TOTAL pass=15 fail=0 skip=0 chapters=15 elapsed_ms=148.7
exit_code=0
{
"samples": 101,
"nlerp_step_std_rad": 0.006429866664812347,
"slerp_step_std_rad": 2.9626952492849927E-07,
"smoothstep_midpoint": 0.5
}
算法选择
UI 数值、短距离位置和小角度姿态可以优先使用 Lerp/Nlerp;相机轨道、长弧旋转和要求可预测角速度的动画使用 Slerp。选择条件记录在调用边界,而非隐藏在通用“平滑组件”中。网络快照间隔变化时,由缓冲器计算 t 后再调用插值器,避免渲染帧率进入模拟状态。
| 场景 | 默认选择 | 升级触发条件 |
|---|---|---|
| UI 数值、位置 | Lerp | 需要非线性时间曲线 |
| 小角度姿态 | Nlerp | 角速度误差超预算 |
| 长弧姿态 | Slerp | 批量成本超出预算 |
弧长参数化
本章定义的 t 仅表示端点间的参数位置。下一章将其扩展到三次 Bezier;由于等参数不对应等路程,运动系统还需要弧长表将期望路程反查为曲线参数。