想象你经营着一家“未来台球厅”,每颗台球都内置芯片,顾客击球后,球的位置、转速、碰撞、弹跳全部由电脑实时计算。球被球杆击中的瞬间,它会前进、旋转、撞到另一颗球后改变方向——顾客只会觉得“手感真棒”,但作为开发者,你看到的是另一幕:一个不断被拆解、计算、再拼装起来的世界。这颗球是刚体,那个镜头是物理引擎,而它背后要回答的问题,正是刚体动力学:如何描述不形变的物体,如何让力改变它的运动,如何检测碰撞、计算反冲,如何用离散的帧模拟连续的时间,以及怎样让整个场景在几十毫秒内算完。本文就用这颗台球做导游,带你走完游戏物理引擎最核心的那条主线。理解它之后,再看 Havok、PhysX 或 Bullet,你会发现它们只是“用了同一套台球规则的不同品牌球桌”。
对 AI 从业者而言,刚体动力学是读懂主流物理引擎、构建仿真环境、训练具身智能与自动驾驶策略的底层能力,是让 AI 从“看得见”走向“摸得着”的关键桥梁。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
平动:F = m*a (牛顿第二定律) 6自由度:3平动(质心位置) + 3转动(绕质心旋转) 碰撞检测:宽相AABB + 窄相SAT 碰撞检测算法对比 转动:tau = I*alpha + omega x (I*omega) (欧拉方程) 冲量公式:J = -(...
刚体状态表达
台球游戏开场前,你要给摄像机一张特殊的“状态表”:每颗球现在的位置、朝哪个方向、有没有在旋转、下一秒大概往哪走。顾客看到的是流畅画面,但在引擎看来,世界是一帧一帧的快照,而快照的核心就是每个刚体的状态。
刚体是理想化的“不会形变的硬球”。现实中的台球碰撞时会有一点点凹陷,游戏中我们忽略这种弹性形变。因此,只要记住位置、朝向、线速度、角速度四个信息,就能完整描述一颗球“在哪儿、往哪儿去、转得多快”。
位置 p 是球心在三维空间中的坐标;朝向 q 常用四元数表示,可以避开欧拉角带来的“万向锁”问题;线速度 v 是球心每秒移动多少米;角速度 ω 是每秒绕自身轴旋转多少弧度。除了状态,物体还有质量 m 和惯量张量 I,它们不随状态改变,是“这台球有多沉、多难被转起来”的天生属性。
有了这张状态表,台球厅的电脑才知道该把球画在哪、往哪个方向滚。但状态不会自己变化,下一步我们要给球施加力——这就是牛顿欧拉方程的舞台。
- 位置向量 p记录球心在世界坐标中的三维位置,这是刚体的平移状态。
- 朝向四元数 q记录球的旋转姿态,四个分量可避免欧拉角的顺序歧义。
- 线速度 v记录球心单位时间内位置的变化快慢。
- 角速度 w记录绕自身旋转轴单位时间转过的角度。
牛顿欧拉方程
当你用球杆击中一颗台球,如果击在正中心,球只会直直滑出去;如果击在边上,球会带着旋转飞出去。区别不在球本身,而在于“力”是作用在质心,还是偏离质心。
物理引擎里的刚体运动由牛顿欧拉方程统治:所有外力合成为 F,F 决定线加速度;所有外力矩合成为 τ,τ 决定角加速度。线运动只关心质心,旋转运动则要抵抗惯量张量 I。惯量张量越大,就越难让物体转起来,就像同样推一个门轴,推门把手比推门中间更容易让门转动。
实际引擎每一物理帧干的事就是:收集重力、弹簧力、推进力等所有外力,累加成 F 和 τ,再用质量和惯量算出加速度,交给后面的积分器去更新速度。无论是台球、汽车还是机器人,这一套“合力到线加速度、合力矩到角加速度”的流程都通用。
球杆的每一次击打,都能拆成“质心平移力 + 改变旋转的力矩”。在代码里,这只是两次向量更新;在玩家眼里,却是台球被“打活”的瞬间。
- 合力求线加速度把刚体受到的合外力除以质量,得到质心加速度。
- 合力矩求角加速度用惯量张量的逆矩阵乘以合力矩,得到角加速度。
- 线速度与角速度不断更新加速度要被积分成速度,速度再被积分成位置。
碰撞检测算法
台球桌上最让裁判紧张的时刻是两球相撞。如果裁判只看最终画面,球可能已经互相穿过;真正的流程是在物理步内提前发现“它们要撞了”。
引擎把碰撞检测分成两阶段:宽相先用粗糙包围盒把不可能的物体成对剔除,比如比较 AABB,或用空间哈希划分区域;窄相再对剩下的候选对做精确计算,得到接触点和法向。这就像台球裁判先扫一眼全场,只盯可能撞上的球对,再弯腰仔细看两个球是否真的碰到了。
对于两颗圆形球,窄相只需计算两球心距离是否小于半径和。对于 3D 凸体,常用 GJK 算法判断是否相交并返回最近点,再生成接触点。宽相追求“快”,窄相追求“准”,两者配合,才能让一个真实场景在上百个物体中快速找到有效碰撞。
拿到接触点和法向后,台球裁判就能判罚:这颗球该弹向哪里。接下来就是碰撞响应的工作。
- 求圆心差向量从第一颗球球心指向第二颗球球心的向量。
- 取向量长度把差向量的长度作为两球之间的距离。
- 与半径和比较若距离小于两半径之和,说明两球发生重叠。
碰撞响应与约束
裁判不能只喊“碰了”,还要决定碰撞如何改变球。现实里两球在极短时间互相推挤,物理引擎不想模拟这种微观形变,它直接用一个“冲量”——力在极短时间内的累积效果——瞬间改变速度。
冲量大小由恢复系数 e 控制:e=1 是完美反弹,e=0 是撞上就粘住。公式里的 v_rel·n 是两球在法线方向上的相对靠近速度。除此之外,切向还要计算摩擦力,让球在碰撞后产生旋转和减速。摩擦与旋转,是台球“会走位”而不是像两块冰一样互滑的关键。
当桌上同时有十几颗球挤在一起时,接触点有几十个,它们彼此影响,不能一次解完。引擎使用接触约束求解器,例如 Projected Gauss-Seidel,迭代多轮,每轮把每个接触当成一个约束来修正速度,逐步逼近一个稳定解。
这一套“冲量 + 迭代约束”就是台球厅裁判手里的秒表:每次碰撞都掐表给一个干脆利落的速度修正。搞定单次碰撞后,剩下的问题是如何把连续时间切成物理帧。
- 计算法向相对速度把两球相对速度投影到接触法线
n上,得到“相互靠近”的速度分量。 - 计算冲量大小 j负号表示推开,
e控制反弹程度,分母让质量小的球更容易被弹开。 - 应用冲量更新速度沿法线方向给两球各加或减一份速度。
数值积分方法
台球厅的高速摄像机并不能真正连续计算,它每隔 1/60 秒拍一张照片。物理引擎做的事,就是根据当前状态和加速度,推算出下一张照片里球应该在哪里。
最直觉的方法是显式欧拉:先更新位置,再更新速度。它简单,但误差会累积,甚至会慢慢把能量“吹”进系统,让台球越弹越高。游戏物理更常用半隐式欧拉:先计算新速度,再用新速度更新位置。位置和速度互相追赶,稳定性显著提升,是 PhysX 和 Havok 等引擎最常用的基础积分器之一。
某些更精密的场景会使用更高阶的 RK4 四阶龙格-库塔法,它对轨道模拟更准确,但计算量更大。工程上还要求固定时间步长,并通过子步拆分来避免帧率抖动导致物理表现忽快忽慢。这是“手感”稳定的幕后功臣。
台球厅的每一帧画面,都是由这样一个“预测公式”推出来的。积分方法决定了球滑得顺不顺、弹得稳不稳。最后一个问题是:桌上有几十上百颗球时,如何让它们全都算得过来?
- 用加速度更新速度把当前加速度乘以步长,加到旧速度上。
- 用新速度更新位置这是半隐式欧拉与显式欧拉的关键差异。
- 继续下一帧把新状态作为下一帧的起点,循环执行积分。
引擎优化选型
台球厅高峰期有几十组客人同时打球,但并不是每张桌子都值得一直盯着。一颗已经停稳的球,不需要每一帧都重算;一颗在角落里睡着的球,更可以在它醒来前彻底忽略。于是引擎学会了“睡眠”。
如果一颗球的线速度、角速度和动能长期低于阈值,引擎就把它标记为睡眠;一旦有约束把它撞醒,再重新激活。更进一步,球与球之间的接触会连成“孤立岛”,不同孤岛之间没有物理联系,可以并行求解。多核 CPU 与 GPU 把这些岛分配到不同线程或批次,极大提升吞吐量。
选型时,PhysX 凭借 Unity/Unreal 生态和 GPU 加速广受欢迎;Havok 以稳定和高效闻名,常出现在大型动作游戏中;Bullet 开源免费,适合科研与定制。无论哪个引擎,底层都跑着上面讲的同一套刚体动力学规则,只是工程实现各有取舍。
从一颗球到一张球桌,再到几十张球桌,刚体动力学像一套分工明确的台球厅管理手册:登记状态、分析受力、判定碰撞、修正速度、逐帧结算,最后让睡着的球休息。读懂这套逻辑,你就拿到了解读 PhysX 与 Havok 的钥匙。
- 计算平移动能质量乘线速度平方的一半,代表球“平移”的能量。
- 计算旋转动能角速度与惯量张量共同贡献,代表球“旋转”的能量。
- 与阈值比较若总动能持续低于阈值,则把刚体标记为睡眠。
AI 应用场景
自动驾驶仿真环境构建
阶段:物理仿真环境构建
解决的问题:真实路测成本高、危险场景难以复现;刚体动力学引擎能够提供廉价、可重复、带真实碰撞反馈的仿真场景。
具体案例:训练无人车在模拟城市中变道时,车辆由多个刚体组成,轮胎与路沿、车辆与护栏之间用碰撞检测和接触约束求解。强化学习策略在仿真器里试错,每一次急刹或碰撞都来自刚体动力学给出的精确反馈。
机器人抓取强化学习
阶段:强化学习经验生成
解决的问题:机械臂夹取物体时,需要视觉感知与物理动作联动;若缺少刚体动力学,物体可能被“手穿模”式抓取,策略学到不真实的动作。
具体案例:使用 PyBullet 这类基于 Bullet 的物理引擎训练机械臂抓取策略。机械臂关节施加力矩,物体在重力下落,被抓取时接触约束提供摩擦与反弹反馈,RL 模型通过数十万次交互学会稳定抓取。
Python 演示
import math class RigidBall: def __init__(self, x, y, vx, vy, radius, mass=None): self.pos = [x, y] self.vel = [vx, vy] self.radius = radius self.mass = mass if mass is not None else radius * radius self.inv_mass = 1.0 / self.mass def step(self, dt, gravity=-10.0, floor=0.0): self.vel[1] += gravity * dt self.pos[0] += self.vel[0] * dt self.pos[1] += self.vel[1] * dt if self.pos[1] - self.radius < floor: self.pos[1] = floor + self.radius self.vel[1] = -self.vel[1] * 0.85 def resolve_collision(a, b, restitution=0.9): dx = b.pos[0] - a.pos[0] dy = b.pos[1] - a.pos[1] dist = math.hypot(dx, dy) min_dist = a.radius + b.radius if dist >= min_dist or dist == 0: return nx = dx / dist ny = dy / dist overlap = min_dist - dist a.pos[0] -= overlap * 0.5 * nx a.pos[1] -= overlap * 0.5 * ny b.pos[0] += overlap * 0.5 * nx b.pos[1] += overlap * 0.5 * ny rvx = b.vel[0] - a.vel[0] rvy = b.vel[1] - a.vel[1] vn = rvx * nx + rvy * ny if vn > 0: return j = -(1 + restitution) * vn / (a.inv_mass + b.inv_mass) a.vel[0] -= j * nx * a.inv_mass a.vel[1] -= j * ny * a.inv_mass b.vel[0] += j * nx * b.inv_mass b.vel[1] += j * ny * b.inv_mass def main(): balls = [RigidBall(-5 + i * 2, 1, i * 0.6, -2, 1, 2) for i in range(5)] for frame in range(10): for b in balls: b.step(0.02) for i in range(len(balls)): for j in range(i + 1, len(balls)): resolve_collision(balls[i], balls[j]) if frame % 2 == 0: print([round(b.pos[0], 2) for b in balls]) if __name__ == "__main__": main()
self.vel[1] += gravity * dt先更新速度,对应半隐式欧拉的第一步。self.pos[1] += self.vel[1] * dt用新速度更新位置,是半隐式欧拉的关键。if self.pos[1] - self.radius < floor碰撞检测:判断球是否越过地面。vn = rvx * nx + rvy * ny计算两球沿法线方向的相对速度。j = -(1 + restitution) * vn / (a.inv_mass + b.inv_mass)按冲量公式求出碰撞冲量大小。b.vel[0] += j * nx * b.inv_mass把冲量按质量倒数分配给被撞球。
常见误区
误区只要算出合力除以质量,就能预测刚体运动,旋转是额外附加的小问题。
正解刚体除了平移,还有旋转。旋转由力矩、惯量张量、角速度共同决定。两颗台球相撞后为什么有的前冲、有的带着旋转溜走,靠的正是牛顿欧拉方程里“力与力矩”两条腿走路。
为何常见:高中物理最常讨论质点的 F=ma,没有接触真实物体的旋转自由度,所以初学者很容易把刚体简化成点。
误区所有物体都应该在每帧对每个三角形做精确碰撞检测,这样才不会有穿模。
正解真实引擎如果全部精确检测,场景中数千个物体会产生天文数字的候选对。正确做法是先做宽相检测,用 AABB 或空间哈希快速筛掉不可能碰撞的物体,再对少数候选对做窄相检测,效率和精度才能兼得。
为何常见:在小场景 demo 中物体很少,宽相优化看起来毫无必要,因此容易被忽略。
误区物理帧率越高、步长越小,物理就一定越准,所以无脑增大计算频率就好。
正解步长缩小确实能降低单步误差,但计算成本线性上升,而且如果帧率不稳定,物体运动时快时慢。工程上更常见的是固定时间步长加子步,并用半隐式欧拉在 60Hz 下获得足够稳定的效果。
为何常见:人们只看到局部截断误差随步长变小,却忽略了工程性能与确定性要求。
误区只要把恢复系数设为 1,球就会永远弹性碰撞,永远不减速。
正解恢复系数只描述单次碰撞法向速度的比值,数值积分本身的离散误差、摩擦阻力以及穿透修正都会引入能量损失。游戏中通常还要加阻尼和角速度衰减,才能让球“停得自然”。
为何常见:公式看起来简洁,但连续时间方程在离散化后不会完美守恒,初学者容易把公式理想化。
误区碰撞是两个物体的事,一次解一组接触点就够了,迭代求解器没有必要。
正解当多颗球挤在一起,一个接触点会通过速度变化影响其他接触点。引擎需要把接触看成全局约束,用迭代法反复修正,让整个接触网络近似收敛。这就像开球瞬间裁判要同时处理十几颗球的连锁碰撞,不可能只处理其中一对。
为何常见:单独看两球碰撞很容易,但多个物体同时接触时,约束会互相耦合,问题复杂度远超直觉。
面试题
Q1. 物理引擎中的“刚体”最核心假设是下列哪一项?
- A.物体的形状永远不会改变← 正确答案
- B.质量会随速度变化
- C.只能沿直线运动
- D.永远不受重力影响
答案:A。刚体忽略形变,用来简化位置、旋转、速度等状态计算。质量保持不变、只沿直线运动、不受重力都不是刚体的定义,而是某些特定模拟的附加条件。
Q2. 下列哪一项通常不属于刚体状态变量?
- A.物体的位置矢量
- B.物体的线速度
- C.物体的贴图颜色← 正确答案
- D.物体的角速度
答案:C。贴图颜色属于渲染信息,不影响物理计算。位置、线速度、角速度才是引擎描述刚体当前运动状态的核心数据。
Q3. 与显式欧拉相比,半隐式欧拉积分的关键改进是什么?
- A.先更新速度再更新位置← 正确答案
- B.先旋转再平移
- C.直接使用解析解
- D.不需要设置时间步长
答案:A。半隐式欧拉用新速度更新位置,使位置与速度互相耦合,稳定性更好;显式欧拉用旧速度更新位置,容易发散。旋转、平移不分先后,解析解通常也不存在。
Q4. 在刚体碰撞冲量公式中,符号 e 表示什么?
- A.摩擦系数
- B.恢复系数← 正确答案
- C.惯性张量
- D.接触点个数
答案:B。恢复系数 e 控制碰撞后法向相对速度与碰撞前之比,e=1 为完全弹性,e=0 为完全非弹性。摩擦系数对应切向力,惯性张量描述旋转惯性。
Q5. 物理引擎中“宽相”检测主要用来做什么?
- A.计算每个接触点的精确法向
- B.较快筛出可能碰撞的候选对← 正确答案
- C.求解所有接触约束的冲量
- D.更新物体的位置和速度
答案:B。宽相用包围盒、空间哈希等廉价方法快速排除明显不会碰撞的物体对。精确法向是窄相的任务,求解冲量属于碰撞响应阶段。
Q6. 当场景中有大量接触点时,游戏引擎通常采用什么策略求解约束?
- A.一次性形成线性方程组并求逆
- B.迭代法逐步逼近可行解← 正确答案
- C.随机挑选一个接触点忽略
- D.只保留质量最大的物体
答案:B。真实引擎常用迭代求解器,每轮修正每个接触约束,反复迭代后近似收敛。一次性求逆在大规模场景中太慢,随机忽略和只保留大物体都会产生错误物理。
Q7. 下列哪些情况通常被用来判断刚体能否进入“睡眠”状态?
- A.线速度大小持续低于阈值← 正确答案
- B.角速度大小持续低于阈值← 正确答案
- C.动能持续低于阈值← 正确答案
- D.物体完全透明不可见
答案:ABC。睡眠机制通过速度、角速度、动能综合判断物体是否长期静止。透明度是视觉属性,与物理状态无关,不用于睡眠判断。
Q8. 下列哪种算法适合两个凸体之间的窄相碰撞检测?
- A.空间网格哈希
- B.AABB 包围盒求交
- C.GJK 算法← 正确答案
- D.光线投射
答案:C。GJK 专门用于两个凸体之间的相交检测,并能返回最近点信息。空间网格哈希和 AABB 包围盒常用于宽相筛选,光线投射则用于射线与物体求交。
Q9. 关于显式欧拉与半隐式欧拉,下列哪些说法正确?
- A.时间步长越小通常越稳定← 正确答案
- B.显式欧拉在弹簧系统中可能逐渐引入能量← 正确答案
- C.半隐式欧拉使用新速度更新位置← 正确答案
- D.时间步长越大求解越准确
答案:ABC。步长越小,数值误差通常越小;显式欧拉在振荡系统中可能“注入”能量,导致越弹越高;半隐式欧拉的核心是先更新速度再更新位置。D 与正确方向相反。
Q10. 关于 PhysX、Havok 等物理引擎的接触约束求解,下列说法哪些符合实际?
- A.把接触与关节统一成约束← 正确答案
- B.使用迭代求解器求冲量← 正确答案
- C.每次迭代尽量修正穿透和相对速度← 正确答案
- D.单次求解就能完全消除穿透
答案:ABC。现代引擎会把接触、关节等统一为约束,用迭代方法逐轮修正速度和穿透。由于约束互相耦合,单次求解无法完全收敛,只能逐步逼近稳定解。