· 5 分钟阅读 · 1888 字
两个动态物体碰撞:等大冲量不等于相同速度变化
质量 1 kg 的圆形物体以 4 m/s 撞向静止的 3 kg 物体。若碰撞回调给双方各做一次“沿法线反射速度”,轻物体变成 −4 m/s,重物体仍然不动,总动量从 +4 变成 −4 kg·m/s。画面已经分开,封闭系统却凭空得到了一次外部作用。
对于恢复系数为一的正碰,正确结果是轻物体 −2 m/s、重物体 +2 m/s。两者得到大小相同、方向相反的冲量,但轻物体改变了 6 m/s,重物体只改变 2 m/s。将反射函数从静态墙面搬到可推动物体时,缺失的正是另一方的有限质量与速度响应。

先约束相对速度,再分配双方变化
本文用独立 C# 工程验证二维、无摩擦、只有平动的瞬时单接触。位置以米计,速度以米每秒计,质量以千克计。接触法线 n 为单位向量,从 A 指向 B;相对法向速度定义为 g = dot(vB-vA,n),因此 g 小于零表示接近。这个方向约定与“墙面朝外”的反射接口不同,不能只替换公式而保留旧法线。
令双方逆质量分别为 wA、wB,恢复系数为 e。向 B 施加 +j n,同时向 A 施加 −j n,速度变为:
vA' = vA - j wA n
vB' = vB + j wB n
g' = g + j (wA + wB)
要求 g' = -e g
因此 j = -(1+e)g / (wA+wB)
冲量单位是 kg·m/s,乘逆质量才得到速度变化。公式的分母不是双方质量之和,而是双方对同一冲量产生的速度响应之和。只在 A 上使用 B 的质量,或者双方各自求一次冲量,都不能保证使用同一接触约束。
固定案例取 n=(1,0),完整输入速度为 A=(4,2)、B=(0,−1)。当 e=1 时,j=6 kg·m/s,结果 A=(−2,2)、B=(2,−1)。无摩擦约束只改变法向分量,两者切向速度分别保持 2 与 −1。下图的箭头只编码 X 方向速度,不把圆形概念物件当作碰撞几何实测。

一次求解交付双方速度
实现位于 .tmp/daily-labs/2026-10-26-2000/Impulse.cs,调用链为 Program → ContactImpulse.Solve → V.Dot。求解器返回完整的双方结果与冲量;调用方应在同一物理步内应用这对结果,不能让 A 的碰撞回调先改完速度,再让 B 用已经变更的输入重算同一接触。
public readonly record struct V(double X, double Y)
{
public static V operator +(V a, V b) => new(a.X + b.X, a.Y + b.Y);
public static V operator -(V a, V b) => new(a.X - b.X, a.Y - b.Y);
public static V operator *(V a, double s) => new(a.X * s, a.Y * s);
public double Dot(V b) => X * b.X + Y * b.Y;
public bool Finite => double.IsFinite(X) && double.IsFinite(Y);
}
public static class ContactImpulse
{
public static (V A, V B, double J) Solve(
V a, V b, V normal, double inverseA, double inverseB, double restitution)
{
if (!a.Finite || !b.Finite || !normal.Finite ||
Math.Abs(normal.Dot(normal) - 1) > 1e-10 ||
!double.IsFinite(inverseA) || !double.IsFinite(inverseB) ||
inverseA < 0 || inverseB < 0 ||
!double.IsFinite(restitution) || restitution < 0 || restitution > 1)
throw new ArgumentOutOfRangeException(nameof(normal));
double sum = inverseA + inverseB;
double speed = (b - a).Dot(normal);
if (!double.IsFinite(sum) || !double.IsFinite(speed))
throw new ArithmeticException();
if (sum == 0 || speed >= 0)
return (a, b, 0);
double impulse = -(1 + restitution) * speed / sum;
V nextA = a - normal * (impulse * inverseA);
V nextB = b + normal * (impulse * inverseB);
if (!double.IsFinite(impulse) || !nextA.Finite || !nextB.Finite)
throw new ArithmeticException();
return (nextA, nextB, impulse);
}
}
逆质量为零表示求解器不修改该对象的速度,并不表示它的速度输入必须为零。这样可表示静止墙,也可表示由外部轨迹驱动的运动学表面;后者能与动态物体交换能量,不能再要求“双方有限质量动能守恒”。两边逆质量都为零时没有可响应自由度,直接返回原速度,避免除零。
g≥0 时同样返回原值。接触记录仍然存在,并不意味着双方还在接近;如果把已经分离的速度再代入恢复公式,会得到负冲量,把物体拉回去。这里的提前返回也覆盖切向擦过的零法向速度。法线长度、非负逆质量、有限输入与 e∈[0,1] 都在入口校验;中间量或结果溢出则显式失败,不把无穷速度提交给后续积分。
守恒检查要覆盖质量比与接触方向
Program.cs 中的 Check 与 Reject 覆盖固定案例、等质量速度交换、完全非弹性碰撞、静态 B、双方静态、分离、擦过,以及非法法线、负逆质量、非有限输入与超范围恢复系数。错误对照保留“双方独立反射”所得的 −4,与正确总动量 +4 直接比较。
额外 10,000 组使用种子 20261026,随机旋转法线,质量分别落在 [0.1,10) kg,接近速度落在 [0.01,20.01) m/s,恢复系数落在 [0,1)。每组同时验证二维动量、反弹后的相对法向速度,以及动能损失公式:ΔK=(1-e²)g²/[2(wA+wB)]。随机切向速度使测试不会只验证水平直线上的标量公式。
当天在 .NET 9 Release 下执行,$LAB_ROOT 表示本文探针目录:
$ cd $LAB_ROOT
$ dotnet build ImpulseLab.csproj -c Release --nologo
Build succeeded. 0 Warning(s), 0 Error(s)
Time Elapsed 00:00:09.47
exit code: 0
$ dotnet run --project ImpulseLab.csproj -c Release --no-build
passed=21 failed=0 skipped=0 samples=10000 elapsed_ms=78.3754
fixed_input: massA=1 massB=3 vA=(4,2) vB=(0,-1) e=1
output: j=6 vA=(-2,2) vB=(2,-1)
normal_momentum: before=4 after=4 wrong=-4
max_momentum_error=5.859285502108464e-14
max_restitution_error=2.4868995751603507e-14
max_energy_loss_error=1.0231815394945443e-12
exit code: 0
三类最大误差分别以 kg·m/s、m/s 和焦耳计;它们是双精度有限样本范围内的观测值,不能作为引擎单精度接入的容差直接复制。耗时记录只证明检查实际执行,没有测量物理引擎吞吐。e=0 的固定正碰另得到双方同为 1 m/s:相对法向运动消失,总动量仍为 4,法向动能从 8 J 降到 2 J。恢复系数控制能量损失,不控制冲量是否成对。
偏心接触会改变分母
当前接口适合只有平动自由度的圆盘、粒子或受约束游戏对象。一般刚体在偏离质心处受冲量还会旋转,此时接触点速度包含角速度贡献,有效逆质量也包含惯量与力臂项。只把输入换成接触点速度,却仍用 wA+wB 作分母,会低估系统对冲量的响应;必须一起扩展角动量更新和有效质量计算。
接触生成负责提供同一时刻的法线和状态,求解器负责同时改变双方速度,积分器再消费更新后的速度。穿透修正、持续接触的静止阈值、多点迭代与摩擦都不由这条单接触恢复公式承担;已有物理引擎处理接触时也不能再叠加一次本文冲量。轻重物体能否正确互推,验收对象应是成对状态变化及其守恒关系,单看反弹方向正确无法证明响应正确。