· 6 分钟阅读 · 2123 字
摩擦冲量已经落在圆盘内,为何仍需切向质量块求解
切向冲量长度都等于 1 N·s,两份求解结果却不等价。固定接触的有效逆质量为 K=diag(1,4) kg^-1,自由切向相对速度为 (-2,-2) m/s:把未约束冲量径向缩回圆盘,得到 (0.970143,0.242536);直接求解带圆盘约束的二次目标,得到 (0.922110,0.386927)。后者没有增加摩擦预算,却使局部能量变化从 −1.837121 J 降到 −1.893506 J。
此前《物体斜向滑动时摩擦变强》验证了各向同性摩擦的可行域必须是圆盘,并把局部最优性限定在标量有效质量下。本篇补上它留下的矩阵条件:转动惯量进入接触响应后,材质仍可各向同性,速度对冲量的响应却未必各向相同。继续沿用标量径向投影,保住的是冲量上限,未必是当前接触的最优速度修正。

圆盘来自材质,椭圆来自接触动力学
取接触平面中两个正交单位切线,把切向冲量写成二维向量 J。法向累计冲量 Jn 在本次局部求解中固定,摩擦半径 R=μJn。刚体的质量、转动惯量与接触点力臂共同形成对称切向有效逆质量矩阵 K,它描述 Δv=KJ。即使材质只有一个摩擦系数,两个切线方向的响应仍可能不同,且矩阵可以有非零耦合项。
本探针只求固定法向载荷、正定 K 下的一次局部耗散问题:
min F(J) = 0.5 J^T K J + v_free^T J
subject to |J| <= R
v_free 表示去掉当前接触切向累计冲量贡献后的相对速度。F 是这个冻结局部模型的动能变化,单位为焦耳;常数项不影响极小点。正定 K 保证目标严格凸,因此可行集上的驻点就是唯一最小点。圆盘没有变,变化的是“离未约束解多远”的度量:配方后目标等价于 0.5*(J-J0)^T K (J-J0) 加常数,J0=-K^-1 v_free。
只有 K 是标量乘单位阵时,这份度量才与欧氏距离成比例。一般矩阵下,把 J0 沿射线缩短不等于沿目标等值线找到最先接触圆盘的位置。本例 J0=(2,0.5),径向解虽合法,目标梯度却没有与圆盘法线反向平行,沿边界仍存在继续降低 F 的方向。
用一个乘子闭合两条切线
若 J0 位于圆盘内,直接接受;约束活跃时引入非负乘子 λ,求解:
(K + lambda I) J = -v_free
|J| = R
把 K 放到其正交特征基后,每个分量都除以 k_i+λ。正定特征值使长度随 λ 增大而下降,因而可以先扩大可行侧上界,再二分 λ。固定样本的 λ 为 1.168938 kg^-1,解的两个分量都受同一个乘子约束,而非分别截断。
完整实现位于 .tmp/daily-labs/2026-11-09-2000/Friction.cs,调用链为 FrictionBlock.Solve → At → Norm。这是普通量级双精度输入的独立数值探针,不依赖引擎 API:
public readonly record struct Vec2(double X, double Y);
public readonly record struct TangentMass(double A, double B, double C);
public static class FrictionBlock
{
public static Vec2 Solve(TangentMass k, Vec2 freeVelocity, double radius)
{
if (!double.IsFinite(k.A) || !double.IsFinite(k.B)
|| !double.IsFinite(k.C) || k.A <= 0 || k.C <= 0
|| !double.IsFinite(k.A*k.C-k.B*k.B) || k.A*k.C-k.B*k.B <= 0)
throw new ArgumentException("有效逆质量矩阵必须正定");
if (!double.IsFinite(radius) || radius < 0
|| !double.IsFinite(freeVelocity.X) || !double.IsFinite(freeVelocity.Y))
throw new ArgumentException("速度与半径必须为有效有限值");
if (radius == 0) return new(0, 0);
Vec2 unrestricted = At(k, freeVelocity, 0);
if (Norm(unrestricted) <= radius) return unrestricted;
double low = 0, high = 1;
while (Norm(At(k, freeVelocity, high)) > radius)
{
high *= 2;
if (!double.IsFinite(high)) throw new ArithmeticException("区间扩展溢出");
}
// high 始终位于可行侧,返回值不因停止条件越出圆盘。
for (int i = 0; i < 80; i++)
{
double middle = low + (high-low)*.5;
if (Norm(At(k, freeVelocity, middle)) > radius) low = middle;
else high = middle;
}
return At(k, freeVelocity, high);
}
private static Vec2 At(TangentMass k, Vec2 v, double lambda)
{
double a = k.A+lambda, c = k.C+lambda;
double determinant = a*c-k.B*k.B;
if (!double.IsFinite(determinant) || determinant <= 0)
throw new ArithmeticException("矩阵求解超出数值范围");
Vec2 result = new((-c*v.X+k.B*v.Y)/determinant,
(k.B*v.X-a*v.Y)/determinant);
if (!double.IsFinite(result.X) || !double.IsFinite(result.Y))
throw new ArithmeticException("冲量超出数值范围");
return result;
}
public static double Norm(Vec2 v) => Math.Sqrt(v.X*v.X+v.Y*v.Y);
}
80 次二分用于锁定这个探针的数值结果,并非推荐所有接触逐帧采用的迭代预算。生产实现应按速度残差、允许的冲量误差与实际剖析数据选择停止条件;不能从这次 CPU 执行时间推导引擎性能。奇异矩阵被拒绝,极端量级的平方、行列式与消减误差也需要专门的缩放或分解策略,当前实现不承担任意条件数求解。

热启动后的速度不能再充当自由速度
接口中的自由速度必须与求解器记账方式一致。如果物体速度已经包含旧累计冲量 J_old 的效果,应先构造 v_free=v_current-K*J_old。局部块求解返回新的完整累计量,实际施加到刚体上的仍是 J_new-J_old。否则,拿已修正的速度求完整冲量,再覆盖累计量,会把旧贡献当成不存在。
Vec2 freeVelocity = new(
currentVelocity.X - k.A*oldImpulse.X - k.B*oldImpulse.Y,
currentVelocity.Y - k.B*oldImpulse.X - k.C*oldImpulse.Y);
Vec2 next = FrictionBlock.Solve(k, freeVelocity, radius);
Vec2 delta = new(next.X-oldImpulse.X, next.Y-oldImpulse.Y);
// 调用方按接触雅可比将 delta 分别施加到两个刚体,并保存 next。
这份冻结模型把法向累计冲量、接触基与 K 都视为本轮输入。法向载荷随后变化时,需要重新求解相应半径;切线基跨帧变化时,热启动向量还必须先回到同一世界方向再重新投影。局部目标值更低不代表完整接触岛已经收敛,也不覆盖静动摩擦切换、多个接触点之间的耦合或法向与切向联合求解。
预算检查之外,还要检查驻点
Program.cs 的 objective-improvement 固定上述反例,要求矩阵解比径向解至少降低 0.05 J;stationarity-x/y 检查梯度与约束法线的关系。内部解、零半径、零速度、非零矩阵耦合、非法半径、奇异和非正定矩阵各有回归。另用固定种子生成 10,000 组矩阵、速度与半径,每组检查可行性、驻点残差和旋转等价;对约束活跃样本额外枚举 256 个边界角度,确认数值解的目标值不高于这些独立候选。
2026-11-09 使用 .NET SDK 9.0.202 执行,$WORK_DIR 指向探针目录:
dotnet build $WORK_DIR/FrictionLab.csproj -c Release
dotnet run --project $WORK_DIR/FrictionLab.csproj -c Release --no-build
build_exit=0 warnings=0 errors=0 build_elapsed_s=5.68
run_exit=0 passed=21 failed=0 skipped=0
samples=10000 boundary_angles=256 elapsed_ms=301.2997
max_budget_excess=0
max_kkt_residual=3.4684476073050936e-15
max_rotation_error=1.5710734600032811e-15
max_dense_objective_gap=0
密集角度枚举提供独立可行候选,不能单独证明连续域最优;这里的最优性依据是正定目标、凸圆盘和满足容差的驻点条件。随机矩阵的特征值范围为 [0.5,4.5),半径为 [0.1,2.1),数据只验证这组输入范围。零载荷返回零冲量;奇异 K 需要调用层改变求解路径,不能靠给行列式加任意小量悄悄改变动力学。
对轻量角色或只保留标量接触响应的近似系统,径向投影仍是与其模型一致的选择。转动响应使两切向形成矩阵后,评审标准则应从“有没有超出圆盘”增加到“是否在当前质量度量下满足局部驻点”。先明确采用何种接触模型,再决定是否承担块求解成本;调低摩擦系数只会收紧预算,不能修复度量选错造成的方向误差。