1.1 从弹跳球写最小引擎


1.1 从弹跳球写最小引擎

本节摘要:最小物理引擎不是框架,而是四条状态更新:加速度来自重力,速度累加速度变化,位置累加速度,碰到地面就把法向速度翻面并乘恢复系数。先把这颗球跑稳,再对照完整引擎补上积分器选择、宽相、接触流形和约束求解。动手优先的意思是:先有可观察的运动,再给运动找理论名字。

学习目标

阅读完本节,你应当能够:

  1. 写出二维弹跳球的状态结构和单步更新
  2. 解释恢复系数、穿透修正、质量倒数各自管什么
  3. 指出这颗球相对真实引擎缺了哪些模块
  4. 用一张对照表决定下一步该先补碰撞还是先补积分

一、先看见球落地

打开空白循环,不要先建“PhysicsWorld”。只要一个球:位置、速度、半径、质量。每步做三件事——加重力、挪位置、若圆心低于地面加半径则把它抬回去并把竖直速度反向。三件事做完,屏幕上就有一个会跳的点。这一跳比任何架构图都值钱:它暴露了时间步、穿透、能量三个后续全部问题的原型。

把球想成台球桌上的一颗球,重力是桌面微微倾斜。你不会先发明一整套关节库再去推这颗球。你先用手把它推出去,看它撞边时怎么弹。引擎开发同一逻辑:最小可观察现象,再把现象里的错误逐条变成模块。

每一步: v ← v + g * dt x ← x + v * dt 若 x.y - r < 地面: x.y ← 地面 + r v.y ← -v.y * 恢复系数

这不是玩具伪代码,这是显式欧拉加最简单的位置投影。游戏里 80% 的“角色落地”早期原型就是这个形状:一个点、一个半空间、一次速度反射。

地面检测用“圆心到平面的有符号距离”。距离大于半径就分开;小于半径就穿透。穿透时两件事都要做:把位置抬到刚好相切,把速度的法向分量按恢复系数缩放。只抬位置不改速度,球会在下一帧继续往下钻,看起来像粘在地上发抖。只改速度不抬位置,球会在地下弹几下才浮上来,模型穿模。

字段 为什么现在就要有 以后谁用
位置 画出来、测地面 约束投影、休眠判定
速度 积分、反弹 冲量、摩擦、CCD 扫掠
半径 球对平面最便宜 换成凸包后变成形状句柄
质量 以后两球对撞要动量 质量倒数进有效质量
恢复系数 弹跳观感 接触约束的目标相对速度

质量现在好像没用,单球撞无限质量地面时,球的质量不出现在公式里。立刻加上是为了下一节两球相撞时你不必回头改结构。习惯上存质量倒数:静止地面倒数是 0,动态体是 1/m,求解器里乘法比除法多,零倒数自然表示无穷质量。

二、把“碰到了”写成数据,而不是 if 迷宫

弹跳球的 if 很快会腐烂。两颗球、一个斜坡、一个活动箱子,分支会爆炸。工业引擎把“碰到了”变成接触记录:法向、穿透深度、接触点、两个刚体索引、摩擦与恢复。求解器只吃接触数组,不关心形状是球还是胶囊。

最小版本可以先只生成一种接触:球对静平面。

Contact: bodyA, bodyB # B 可以是静态地面哨兵 normal # 从 B 指向 A,单位向量 penetration # 正数表示重叠 restitution friction

单步流程变成:积分预测位置 → 生成接触 → 对每条接触把相对速度的法向分量弹开 → 沿法向把穿透分给能移动的那一方。这已经是冲量求解器的漫画版。第 4 章会把“弹开”换成计算冲量标量,再应用到线速度和角速度。

注意顺序:先积分再碰撞。有人喜欢“先检测再移动”,那会把这一帧的运动丢掉。标准实时引擎是预测-校正:积分给出可能穿模的预测,碰撞和约束再把非法部分拧回来。

💡 关键直觉:碰撞检测不负责“看起来对”,它只负责产出接触。看起来对应该由求解器改速度和位置。把两件事混在一个 if 里,后面加摩擦和休眠会改不动。

两球相撞时,反射公式要换成沿法向的相对速度分解。设法向从 B 指向 A,相对速度 vrel = vA - vB,法向分量 vn = dot(vrel, n)。若 vn 已经分离(大于 0)且没有穿透要修,就不要再施加分离冲量,否则会把已经离开的物体再推一把,出现“粘连后弹飞”。恢复系数 e 把碰撞后的目标法向相对速度设为 -e * vn。对心、无摩擦时,冲量大小是 -(1+e)*vn / (invMassA + invMassB)。把这个标量沿 n 加到 A、减到 B,动量守恒自动满足。地面是 invMassB=0 的特例,公式退回“自己弹开”。

不要在这一步引入角速度。旋转是刚体的第二张皮,会把接触点从“球心沿法向”变成“表面上的杠杆臂”,冲量开始产生转矩。球可以先当质点。等质点堆箱子稳定了,再给盒子加方向和惯性张量,否则你分不清是积分炸了还是角冲量写错了。

休眠可以很早加:速度低于阈值连续若干帧,就把物体标睡,跳过积分。地面上的球停住后不睡,会因浮点残差一直微微颤,接触生成每帧都跑。阈值要带单位意识:用米/秒,不要用“像素/帧”。睡下去的物体被新接触碰到时要立刻醒,否则箱子砸到睡着的地板会穿过去。

三、对照完整引擎,决定补丁顺序

把最小球和产品引擎并排,缺口一目了然。缺口不要一起补。积分不稳时加宽相没有意义;接触数据还不存在时写 PGS 求解器是空转。

完整引擎模块 弹跳球里的对应 现在缺什么 建议补丁顺序
时间步进 直接用渲染 dt 固定步、累加器 本节之后立刻做 1.2
积分器 显式欧拉 半隐式、能量行为 第 2 章
形状 一个球 凸包、胶囊、网格 第 3 章窄相
宽相 无,因为只有一对 SAP、网格、BVH 第 3 章宽相
接触缓存 每帧新建 热启动、流形持久化 第 3.3 节
求解器 速度取反 顺序冲量、摩擦锥 第 4 章
关节 铰链、滑移、电机 第 4.3 节
休眠与岛 唤醒图、岛睡眠 第 6.2 节
调试画线 看球 法向、冲量、包围盒 第 7.1 节

补丁顺序之所以把时间步放在积分器前面,是因为欧拉在变化的 dt 下会表现出“有时稳有时炸”,你会被误导去调重力或恢复系数。先把 dt 钉死,爆炸如果还在,才是积分器的锅。

穿透修正有两种常见写法。硬投影:直接改位置,简单,破坏动量,堆箱子时底层会慢慢下沉或抖动。鲍姆加特稳定化:不直接改位置,而在速度层加一项 bias = penetration * beta / dt,让求解器在若干帧内“挤”出穿透。弹跳球阶段用硬投影没问题,箱子一多就要换成偏置速度,否则会看到底层箱子被压进地面再弹出来。偏置系数 beta 通常 0.1 到 0.3,太大变弹簧,太小穿透消除太慢。

恢复系数不要设成 1。数值误差会让能量往上爬,球越跳越高。0.2 到 0.6 适合游戏箱子,0.8 以上要配合更稳的积分和位置修正上限。还有一个“碰撞阈值”:法向相对速度若只是轻微接触(比如已经几乎静止地搁在地面),不要按弹跳处理,否则物体永远睡不着。很多引擎把相对速度低于 1 米每秒的接触当成非弹性,e 当 0 用。

图 弹跳球状态机

图 弹跳球状态机

静态几何用哨兵刚体还是单独的“世界碰撞器”列表?两种都行。哨兵刚体让求解器走同一套双刚体公式,代码少分支;缺点是静态物体也会占刚体槽。大型场景通常刚体表只放动态和运动学物体,静态网格走单独宽相。弹跳球用一个 staticGround 哨兵就够,invMass=0,速度恒 0。

运动学物体是第三种:位置由动画或玩家输入写,物理不积分它,但别的物体可以撞它。电梯、移动平台属于这一类。最小引擎可以暂不实现,但字段上留一个 type:动态、运动学、静态。后面角色控制器会用到运动学胶囊。

⚠️ 常见坑:用像素当单位,重力写成每帧加 0.5。换分辨率或帧率,全世界的“质量感”都变。从第一行就用米和秒,屏幕缩放留给摄像机。

调试输出现在就可以加:画速度向量、画接触法向、打印动能。动能在无阻尼弹跳里应缓慢下降(数值耗散)而不应上升。上升就是时间步或反射写错。第 7 章会把这些画线升级成接触热图,但习惯要在第一颗球时养成。

单位再强调一次。重力取 9.81 米每二次方秒,球半径 0.2 米,dt 1/60 秒,恢复系数 0.4,这样跳两下就会明显衰减,适合观察。若你把半径写成 20“像素”、重力写成 0.5,你其实在另一套匿名单位里调参,以后和渲染、动画对不上。物理世界用米,屏幕用像素,中间只有一个投影系数。

单球稳定后,复制出第二个球,让它们用上面的对心冲量互撞。不要急着加盒子。两球互撞能验证质量倒数、法向方向、分离速度符号。常见符号错误:法向从 A 指向 B 却按“加到 A”来施加冲量,两球会吸在一起。画出法向箭头,箭头必须从接触点指向被推开的那个球。

这一节结束时,你手里应该有:一个固定结构的刚体记录、一个接触记录、一个“积分-生成接触-反射-投影”的函数。它还不是引擎,但它已经能失败——失败的方式(穿地、越跳越高、互撞粘连)会告诉你下一章该看哪。这比先读完线性代数更接近真实工作。

一节小结

  • 最小引擎是四条更新:重力、积分、接触、反射加投影,不是类继承树。
  • 接触是数据:法向、穿透、两个索引;求解器只吃数组。
  • 质量存倒数:静止体为 0,动态体为 1/m,公式更干净。
  • 先钉单位和时间:米和秒,从第一颗球就开始,像素只属于摄像机。
  • 对照表决定补丁顺序:先固定 dt,再换积分器,再上宽相和冲量,不要并行开六道。
  • 休眠要可唤醒:睡着的物体被撞必须立刻参与求解。
  • 动能是第一张体检表:无故上升就停下来查积分和反射,不要继续加功能。

下一节把渲染帧时间从这条循环里拆出去,用累加器喂固定步长,让卡顿不再变成物理爆炸。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U