← ALL ARTICLES

刚体物理求解

全库目录221
本文目录13

实时物理不是逐帧“算出真实世界”。它在固定时间步内完成积分、碰撞检测和约束求解,并接受可控误差。稳定性、可重复性和玩法手感通常比绝对物理精度更重要。

刚体状态与积分

刚体至少保存位置、旋转、线速度、角速度、质量和惯性。力改变速度,速度再改变位置。半隐式欧拉先更新速度再更新位置:

vt+1=vt+aΔt,xt+1=xt+vt+1Δtv_{t+1}=v_t+a\Delta t,\qquad x_{t+1}=x_t+v_{t+1}\Delta t

它比显式欧拉更适合常见实时系统,但时间步过大仍会增加能量误差。固定 Delta Time 能让求解行为更稳定。渲染帧与物理帧不同步时,表现层可以对前后两个物理状态插值。

积分稳定性

对无阻尼线性弹簧,显式欧拉会使离散能量增长。半隐式欧拉先更新速度,再用新速度推进位置;在适用的哈密顿系统中,这是辛欧拉积分。对角频率为 ω\omega 的线性振子,步长满足 0<ωΔt<20<\omega\Delta t<2 时,轨迹保持有界。过大的步长、碰撞与非线性刚性约束仍可能引起失稳,需要子步和约束求解配合。

两种方法都是一阶积分。在线性振子的稳定步长范围内,半隐式欧拉有相位误差,原系统能量通常围绕一个水平有界振荡,并非逐步精确守恒。例如单位质量、角频率1、初始位置1与速度0,步长0.1得到速度−0.1、位置0.99,能量由0.5变为0.49505。显式欧拉在同一无阻尼模型中持续增能。其他受力、接触与约束需分别检查其离散能量行为。

固定时间步与累加器

渲染帧率波动时,常将物理时间步固定,减少步长变化带来的积分和约束差异;输入仍需按同一逻辑时间采样。标准做法是累加器模式:把渲染帧的真实耗时存入累加器,每次循环按固定步长消费,余量留给插值:

csharp
accumulator += deltaTime;
while (accumulator >= fixedStep)
{
    PhysicsStep(fixedStep);
    accumulator -= fixedStep;
}
// 渲染时按 accumulator / fixedStep 在两个物理状态间插值

要防“死亡螺旋”:物理耗时超过固定步长时,累加器持续增长、每帧要跑的物理步越来越多、帧时间进一步恶化。可设置单帧最大物理步数;达到上限后保留积压会继续追帧,丢弃超额时间则让模拟相对墙钟变慢。两种策略都要明确时间账本,权威联网模拟还须定义失步与追帧规则,不能静默丢失逻辑Tick。

动态刚体由力和约束驱动;运动学刚体由游戏逻辑给出目标运动,但仍可参与接触;静态几何不移动。直接修改动态物体变换会绕过速度和连续碰撞语义,应明确使用 Teleport 还是物理驱动。

接触生成

碰撞检测

粗筛用 AABB、扫掠与裁剪(Sweep and Prune)、Grid 或 BVH 快速找出可能相交的物体对。精确碰撞检测再对球、盒、胶囊、凸包或三角网格求准确接触。

离散检测只比较两个时间点,快速小物体可能穿过薄墙。连续碰撞检测会计算扫掠体或碰撞时间 TOI,但成本更高,也不能自动解决所有高速旋转和复杂网格问题。常见策略是只给关键高速物体启用 CCD,并限制最大速度和时间步。

精确碰撞检测的两类主力算法

凸体相交检测有两个常用框架。分离轴测试(SAT)枚举候选分离轴——两个凸体的面法线与边方向叉积——只要找到一条轴上两投影不重叠,两体必不相交。它概念直接,适合盒与凸多面体;胶囊等曲面形状还需对应的解析测试或支撑映射,轴数量随形状复杂度增长。

GJK 用闵可夫斯基差把“两凸体是否相交”变成“原点是否在差集内”,通过支撑函数迭代逼近,不需要枚举特征。距离不为零时还能给出最近点对;已穿透时需要 EPA 等后续算法求穿透深度与方向。实践中两者常配合:GJK 判相交,SAT 或 EPA 求接触信息。

接触流形通常包含法线、穿透深度和若干接触点。点太少会不稳定,点太多会增加求解成本。持久化接触可以复用上一帧的冲量作为热启动,让堆叠更快收敛。

2D 圆与 AABB

圆心到 AABB 的最近点可以通过逐轴 Clamp 求出。输入位置处于同一个二维世界空间,AABB 用中心与半尺寸表示:

cpp
Collision HitCircleAabb(Vec2 center, float radius,
                        Vec2 boxCenter, Vec2 halfExtent)
{
    Vec2 closest = boxCenter + Clamp(center - boxCenter,
                                    -halfExtent, halfExtent);
    Vec2 delta = center - closest;
    float distance2 = Dot(delta, delta);
    if (distance2 > radius * radius) return {};

    if (distance2 <= 1e-12f) {
        Vec2 local = center - boxCenter;
        float dx = halfExtent.x - Abs(local.x);
        float dy = halfExtent.y - Abs(local.y);
        bool exitX = dx <= dy; // 等距时固定选X,保持方向确定
        Vec2 normal = exitX ? Vec2(local.x >= 0 ? 1 : -1, 0)
                            : Vec2(0, local.y >= 0 ? 1 : -1);
        float toFace = exitX ? dx : dy;
        return { true, normal, radius + toFace, center + normal * toFace };
    }
    float distance = Sqrt(distance2);
    return { true, delta / distance, radius - distance, closest };
}

返回的法线从盒子指向圆,穿透深度用于位置修正。圆心位于盒内时,最近点等于圆心,delta 无法提供方向,因此要选择最近边界的出射法线,穿透修正距离为圆半径加圆心到该面的距离。代码分别计算到X、Y边界的距离,再选择最近出射面,等距时固定选X轴。半尺寸和半径要求非负;距离平方的零阈值按场景长度单位设定,近边界输入还需与接触容差统一。位置修正后只移除速度沿法线且指向障碍物的分量;直接把整个速度反向会破坏切向运动。可用四个边、四个角和圆心完全位于盒内的测试验证方向与穿透值。

接触响应

冲量与约束

碰撞法线方向的相对速度决定是否需要冲量。恢复系数控制反弹,摩擦约束限制切向相对运动。约束可以统一写成雅可比矩阵对速度的限制,再通过迭代求解拉格朗日乘子。

顺序冲量求解器会逐条修正约束并重复多轮。迭代次数越高通常越稳定,但不是线性换取精度。质量差异过大、很长的关节链和不合理的穿透修正都可能让系统抖动。

位置误差不能一次强行清零,否则会注入额外能量。Baumgarte Stabilization、Split Impulse 或位置级求解会把修正分散到多轮。Sleep 可以停止几乎静止的刚体,减少成本;唤醒规则必须覆盖接触、外力和父级运动。

穿透修正的三种取向

Baumgarte 稳定化把穿透误差折成一个微小的速度偏差,让物体在几帧内“弹回”正确位置。实现最简单,但修正速度会与真实速度耦合,参数过大时物体表现出不自然的弹性。

Split Impulse 把修正分成两个速度通道:位置修正用单独的伪速度推进,不进入真实动量,因此可以避免把这部分修正直接加入真实动量。额外工作取决于求解器如何组织伪速度约束,不能统一计作两轮。

位置级直接投影(PBD 一系)先在位置层面修正穿透;典型PBD随后根据位置差重建速度。有限迭代下仍可能残留穿透。对不改变刚体运动的内部约束,按质量加权的梯度投影可以保持线动量和角动量;能量、碰撞恢复与有限步长误差需要另外分析,不能由位置投影自动保证。

三者分别改变速度偏差、修正速度通道或位置约束。项目按堆叠、关节链和布料等目标场景比较穿透、反弹、收敛与耗时,再确定求解组合。

法向有效质量与累计冲量

以下所有向量和逆惯量张量均在世界空间求值,法线n归一化且从刚体A指向B,接触相对速度为 vRel=(vB+omegaB×rB)-(vA+omegaA×rA),法向速度 vn=dot(vRel,n) 为负表示靠近。单位法向冲量既改变平移,也在偏离质心的位置产生转动,因而分母包含转动惯量:

K=mA−1+mB−1+(rA×n)TIA−1(rA×n)+(rB×n)TIB−1(rB×n)K=m_A^{-1}+m_B^{-1}+(r_A\times n)^T I_A^{-1}(r_A\times n)+(r_B\times n)^T I_B^{-1}(r_B\times n)

对正在靠近的孤立接触,vn<0、K>0时可取 j=-(1+e)*vn/K,其中普通非增能反弹取恢复系数e在[0,1]。已经分离时取j=0,两端都静态导致K=0时跳过;穿透稳定化与持续接触另按累计约束处理。A施加 -j*n,B施加 j*n,角速度使用各自的 I^-1*(r×impulse) 更新。静态物体逆质量与逆惯量均为零。

堆叠迭代求解时,保存累计法向冲量,把新累计值夹到非负,再只施加与旧累计值的差。这样后续迭代可以减小先前过大的冲量,同时保持接触不能产生拉力。切向摩擦上限依赖这份累计法向冲量,热启动也必须匹配同一接触身份与时间步策略。

查询、层与事件

射线查询、形状扫掠和重叠查询是玩法常用的场景查询。碰撞层和遮罩应表达清楚的交互矩阵,避免每个脚本自行过滤。物理回调通常发生在求解阶段,回调里立即创建或销毁物体容易造成重入,适合把结果排队给后续逻辑阶段处理。

确定性与验证

浮点运算顺序、多线程调度、平台数学库和接触排序都会影响结果。需要确定性时,应固定时间步、输入顺序、随机种子、数据布局和求解顺序,并用状态哈希检测分歧。仅设置相同 Delta Time 不足以保证跨平台确定。

测试可从斜面静止、箱体堆叠、质量悬殊、快速弹丸、关节链和暂停恢复开始。记录最大穿透、能量变化、活跃刚体数和求解耗时,比只看画面“差不多”可靠。

本机机制检查:15组 CPU 输入检查最小平移修正后圆与盒分离,包括圆心在盒内的情况。检查代码见 reference.py,运行 python 知识库/examples/mechanisms/reference.py。引擎画面、GPU捕获与性能另外验证。

实现与证据

现有 reference.py只验证圆盒分离的15组输入。法向有效质量与累计冲量是求解推导,未运行完整刚体求解器、跨平台确定性与性能场景。

相关主题

参考资料