· 5 分钟阅读 · 1999 字
箱角撞地后弹得过高:接触冲量还要计算转动有效质量
箱子的一个角先碰到地面,接触求解把质心向下的速度全部抵消,又给箱子加上转动。质心停住了,箱角却以比落地前更大的速度向上弹开。问题出在同一份冲量被用于平移和转动,计算冲量大小时却只考虑了平移响应。

把问题缩成一个二维刚体与静态地面的瞬时接触:质量为 2,绕质心的转动惯量为 2/3;质心速度 (0,-3),角速度为零,接触点相对质心的位置 r=(1,0),地面法线 n=(0,1)。长度、质量和时间分别采用米、千克与秒,恢复系数为零。这里不先指定箱体尺寸,质量、惯量和接触力臂就是数值模型的输入。
按质心速度计算,冲量大小是 j=3/(1/2)=6。平移速度因此变成零;但偏心冲量还带来 Δω=j/I=9,接触点的向上速度也变成 9。没有外部驱动的完全非弹性碰撞,竟把动能从 9 增加到 27。这是接触响应方程不一致,可以在接入碰撞检测与渲染之前直接复现。
接触点停住,不要求质心停住
取逆时针角速度为正,perp(x,y)=(-y,x)。r 是世界坐标中从质心指向接触点的向量,n 是地面指向刚体的单位法线。接触点速度和它的法向分量为:
v_contact = v + omega * perp(r)
vn = dot(v_contact, n)
vn<0 才表示接近地面。质心向下运动时,已有转动可能让接触点向上离开;只检查质心速度会在这个时候错误地施加反弹。反过来,即使质心静止,向下扫过地面的角点也可能需要冲量。
法向冲量 J=j*n 同时改变两种速度。令二维叉积 a×b=a.x*b.y-a.y*b.x,则:
Δv = j * inverseMass * n
Δomega = j * inverseInertia * cross(r,n)
Δvn = j * [inverseMass + inverseInertia * cross(r,n)^2]
k = inverseMass + inverseInertia * cross(r,n)^2
j = -(1+e) * vn / k
最后一式让碰后法向速度满足 vn_after=-e*vn_before。k 是冲量到接触速度变化的响应系数,量纲为逆质量;它的倒数才是这个接触方向上的有效质量。力臂以平方进入分母,是因为同一个 r 先把冲量变成角速度,再把角速度变成接触点线速度,两个环节都不能省略。
固定输入中,k=0.5+1.5×1²=2,所以 j 为 1.5。碰后质心仍以 2.25 向下运动,角速度为 2.25;接触点的向上转动速度抵消质心下落速度,恰好得到零。对单个接触瞬间而言,这就是恢复系数为零要求的结果。

让求解入口同时拿到惯量与接触力臂
研究目录 .tmp/daily-labs/2026-11-03-2000/ 中,ContactImpulse.Solve 接收同一刚体状态、接触点力臂、单位法线与恢复系数,返回完整新速度和冲量。Body 中的逆惯量绕质心定义;二维里它是标量,不能直接替代三维世界空间逆惯量张量。
public readonly record struct V2(double X, double Y)
{
public static V2 operator +(V2 a, V2 b) => new(a.X+b.X, a.Y+b.Y);
public static V2 operator *(V2 a, double s) => new(a.X*s, a.Y*s);
public double Dot(V2 b) => X*b.X+Y*b.Y;
public double Cross(V2 b) => X*b.Y-Y*b.X;
public V2 Perp => new(-Y, X);
public bool Finite => double.IsFinite(X) && double.IsFinite(Y);
}
public readonly record struct Body(V2 Velocity, double Omega,
double InverseMass, double InverseInertia);
public readonly record struct Hit(Body State, double Impulse);
public static class ContactImpulse
{
public static double NormalSpeed(Body b, V2 r, V2 n) =>
(b.Velocity + r.Perp*b.Omega).Dot(n);
public static Hit Solve(Body b, V2 r, V2 n, double restitution)
{
if (!b.Velocity.Finite || !r.Finite || !n.Finite ||
!double.IsFinite(b.Omega) || !double.IsFinite(b.InverseMass) ||
!double.IsFinite(b.InverseInertia) || b.InverseMass < 0 ||
b.InverseInertia < 0 || !double.IsFinite(restitution) ||
restitution < 0 || restitution > 1 || Math.Abs(n.Dot(n)-1) > 1e-10)
throw new ArgumentException("Invalid contact input");
double lever = r.Cross(n);
double k = b.InverseMass + b.InverseInertia*lever*lever;
double vn = NormalSpeed(b, r, n);
if (!double.IsFinite(k) || !double.IsFinite(vn))
throw new ArgumentException("Contact arithmetic overflow");
if (vn >= 0 || k == 0) return new(b, 0);
double j = -(1+restitution)*vn/k;
var next = b with {
Velocity = b.Velocity + n*(j*b.InverseMass),
Omega = b.Omega + j*b.InverseInertia*lever
};
if (!double.IsFinite(j) || !next.Velocity.Finite || !double.IsFinite(next.Omega))
throw new ArgumentException("Impulse arithmetic overflow");
return new(next, j);
}
}
当前接口把静态地面固定为零速度。逆质量与逆惯量分别允许为零,因而也能表达锁定平移或锁定转动;两项共同使 k 为零时,没有有限冲量能改变接触速度,直接返回原状态。参数非法或算术溢出则拒绝输入,避免把 NaN 带进后续积分。NormalSpeed 是已验证输入下的计算辅助函数,独立调用它不会执行入口检查。
质心正下方的接触若满足 cross(r,n)=0,转动项消失,公式退回普通线性冲量。这个条件比“r 不为零”更准确:有力臂但冲量作用线经过质心,仍不会产生转矩。引擎的接触数据需要交付世界空间 r,而不是模型局部顶点,也不能把碰撞法线长度意外带进 k。
用接触速度和动能约束回归
本次 2026-11-03 的 .NET 9 Release 运行包含 Check 固定回归、Reject 输入拒绝和 10,000 组种子为 11032000 的随机检查。固定样本覆盖偏心、中心、恢复系数为 1、分离、静止、纯转动接近、锁定转动与零有效质量;非法样本覆盖非单位法线、负逆质量、负逆惯量、非有限数、非法恢复系数和溢出。
cd "$WORK_DIR"
dotnet build .tmp/daily-labs/2026-11-03-2000/ContactLab.csproj -c Release --no-restore
dotnet run --project .tmp/daily-labs/2026-11-03-2000/ContactLab.csproj -c Release --no-build
build: exit=0, warnings=0, errors=0
run: exit=0, passed=26, failed=0, skipped=0
elapsed_ms=44.1176
edge: j=1.5, vy=-2.25, omega=2.25, contact_vn=0
center: j=6, vy=0, omega=0
wrong: j=6, vy=0, omega=9, contact_vn=9
energy: before=9, after=6.75, wrong=27
samples=10000, random_failures=0
max_normal_speed_error=5.329070518200751e-15
max_rotated_frame_impulse_error=2.1316282072803006e-14
max_energy_increase=0
随机输入使用正逆质量 [0.1,2.1)、正逆惯量 [0.1,3.1),质心各速度分量 [-4,4)、角速度 [-3,3)、r 各分量 [-2,2)、恢复系数 [0,1)。检查接近样本达到恢复速度目标,分离样本保持原状态;再将速度、力臂和法线一起旋转,检查冲量大小不变。所有样本同时检查动能不增加,三项容差均为 1e-10。这些数据验证的是双精度代数内核,不是引擎稳定性或性能测量。
动能约束还可以直接推导。对于接近且 k>0 的样本,单次冲量造成的动能变化为 ΔK=j*vn+j²*k/2;代入正确 j 后得到 -(1-e²)*vn²/(2k)。当 e 在 [0,1] 内,它不会为这个静态接触凭空增加动能。固定样本 e 为零,能量减少 2.25,与实际运行一致;e 为 1 的回归则验证动能守恒。
一份冲量同时改变平移与旋转,求解分母也必须包含二者的接触响应。接入完整引擎时,多个接触点会通过同一刚体相互影响,需要联立或迭代求解;穿透修正、摩擦、静摩擦、持续接触缓存与时间积分也各有状态。这里的单点公式给出了局部速度合同,不能靠逐点独立套用就保证箱子落地后稳定静止。若对面也可运动,还必须先用双方接触速度形成相对 vn,再把双方线性与转动响应加入 k。