为什么游戏里的“积分”不是高中数学那个积分?
从高中微积分,到 Euler、半隐式 Euler、Verlet 与 RK4:理解游戏物理如何把连续世界离散成一帧一帧的计算。
如果你第一次在游戏开发中看到“积分器(Integrator)”这个词,可能会有一点困惑。
我们在高中或大学微积分里学到的积分,通常长这样:
然后开始求原函数、换元、分部积分。
可到了游戏引擎里,所谓“积分”却常常只是几行代码:
velocity += acceleration * dt;
position += velocity * dt;没有积分号,没有原函数,甚至连微积分都看不见。
但奇怪的是:
这几行代码,确实是在做积分。
更神奇的是,如果把这两行代码交换一下:
position += velocity * dt;
velocity += acceleration * dt;数学意义看起来差不多,实际运行出来的物理世界却可能完全不同。
一个弹簧可能稳定振动,另一个却越振越大,最后直接“爆炸”。
为什么?
这背后其实连接着一整条非常有意思的知识链:
微积分 → 微分方程 → 数值积分 → 游戏物理 → 数值稳定性
理解这件事,也就理解了计算机究竟是如何模拟一个连续世界的。
核心问题
游戏里的“积分”并不是另一种数学积分。
它仍然是积分,只不过现实中的运动通常由连续微分方程描述,而计算机只能在离散时间点上进行有限次计算。
因此游戏开发真正面对的问题是:
怎样用一帧一帧的离散计算,近似连续的积分过程?
1从高中数学的积分开始
假设一个物体沿直线运动。
它的位置记作:
速度就是位置随时间的变化率:
加速度则是速度随时间的变化率:
因此反过来:
而位置则是:
这就是我们熟悉的积分。
如果加速度永远不变:
那么可以直接得到解析解:
以及:
这时候事情非常简单。
问题在于:
真实游戏里的力几乎从来没有这么简单。
比如弹簧:
再考虑阻尼:
根据牛顿第二定律:
于是:
现在得到的就不是一个简单的“求面积问题”了,而是一个:
微分方程。
游戏物理真正需要解决的,正是大量这样的微分方程。
2现实是连续的,游戏却是一帧一帧的
理论上的时间是连续的:
0 ───────────────────────────────→ t但计算机不可能计算:
然后再计算:
一直计算到永远。
游戏只能选择一些离散的时间点:
0 Δt 2Δt 3Δt 4Δt ...
│────│─────│──────│──────│────────→ t例如物理系统每秒更新 50 次:
或者每秒更新 60 次:
于是原本连续的问题:
只能近似成:
整理:
于是:
代码就是:
velocity += acceleration * dt;同理:
可以写成:
代码就是:
position += velocity * dt;看到这里,游戏里的“积分”就已经出现了。
它并没有真的去计算一个漂亮的原函数。
它做的是:
沿着时间轴向前迈一小步,然后再迈一小步,再迈一小步。
只要这些步足够小,我们就希望它最终能够逼近真实的连续运动。
这类方法因此被称为:
Numerical Integration,数值积分。
3最简单的方法:Explicit Euler
最直观的数值积分方法叫做:
显式 Euler 法(Explicit Euler)。
假设当前状态是:
我们使用当前速度更新位置:
然后使用当前加速度更新速度:
代码就是:
position += velocity * dt;
velocity += acceleration * dt;思路非常自然:
我知道“现在”的运动趋势,所以沿着当前趋势往前走一步。
这其实就是把曲线局部看成一条直线。
假设有:
Euler 法就是:
它看了一眼当前斜率,然后说:
下一小段时间,大概就照这个方向走吧。
这就是 Euler 法最核心的思想。
4问题来了:Euler 可能偷偷给系统“充能”
考虑一个最经典的系统:
没有阻尼的理想弹簧。
弹簧力:
于是:
理想情况下,一个弹簧应该永远振荡:
←────●────→它不断在两种能量之间转换。
弹性势能:
动能:
总能量:
如果没有阻尼和外力,理论上:
也就是说:
能量应该守恒。
可是如果用显式 Euler:
position += velocity * dt;
velocity += acceleration * dt;经常会出现一种诡异现象:
小振荡
↓
更大的振荡
↓
更大的振荡
↓
越来越大
↓
💥没人给这个弹簧输入能量。
但数值算法自己创造出了能量。
这就是游戏物理中非常重要的概念:
5数值误差并不只是“位置差了一点”
初学数值计算时,我们很容易认为:
误差不就是算出来的位置差几毫米吗?
并不一定。
在动态系统中,一个很小的误差可能会影响下一步计算,而下一步又继续影响后面的计算。
于是:
尤其对于:
- 弹簧
- 摆
- 轨道运动
- 刚体约束
- 关节
- 布料
这些会不断交换能量的系统,数值积分器对“能量”的处理方式会直接决定整个模拟是否稳定。
于是就出现了一个非常有意思的方法。
6只交换两行代码:Semi-Implicit Euler
把之前的代码:
position += velocity * dt;
velocity += acceleration * dt;交换一下:
velocity += acceleration * dt;
position += velocity * dt;现在得到的就是:
Semi-Implicit Euler
也叫:
Symplectic Euler
中文通常称为:
半隐式 Euler / 辛 Euler。
对应公式:
然后:
注意这里最重要的区别。
显式 Euler 使用:
更新位置。
而半隐式 Euler 使用刚刚得到的新速度:
更新位置。
就是这么小的区别。
但对于经典力学中的许多系统,它的长期行为可能好得多。
7为什么半隐式 Euler 更稳定?
这里真正有趣的地方不是:
半隐式 Euler “误差更小”。
事实上,如果只看方法的阶数,显式 Euler 和半隐式 Euler 都属于一阶方法:
也就是说,把时间步长减半,它们的全局误差大致都会按同一个数量级下降。
真正的区别在于:
它们如何改变系统的几何结构与能量。
对于理想振荡系统,显式 Euler 往往倾向于让能量不断增加。
轨迹可能像这样:
phase space
velocity
↑
│ ╭────────╮
│ ╭──╯ ╰──╮
│ ╭─╯ ╰─╮
───┼────────────────────────────→ position
│理论上的相空间轨迹应该是一条闭合曲线。
但显式 Euler 可能让轨迹不断向外扩张:
意味着系统凭空获得越来越多的能量。
而半隐式 Euler 在许多经典机械系统中具有更好的长期能量行为。
它的能量往往会在真实值附近上下波动,而不是无限制地漂移。
一个非常重要的区别
“稳定”并不等于“精确”。
一个积分器可能每一步都有误差,但长期运行依然保持合理的物理结构。
另一个积分器可能短时间看起来很精确,却逐渐把系统能量推向完全错误的方向。
实时物理尤其在意这种长期稳定性。
这也是为什么游戏物理中不能只问:
哪个方法单步误差最小?
还必须问:
跑 10 秒、100 秒以后,它还像一个物理世界吗?
8为什么叫 Symplectic Euler?
这里会进入一点更深的数学。
经典力学不仅仅描述:
它通常同时描述位置和动量:
这些变量构成所谓的:
Phase Space,相空间。
经典 Hamilton 力学具有一种特殊的几何结构,叫:
Symplectic Structure,辛结构。
辛积分器的重要特点,是在离散模拟中尽量保留这种结构。
它不意味着:
严格成立。
但对于许多 Hamilton 系统,它往往能够避免长期系统性地增加或减少能量。
于是你会看到一种很有意思的现象:
真实系统:
Energy
│ ─────────────────────────
│
└─────────────────────────→ time显式 Euler:
Energy
│ /
│ ___/
│ ___/
│ ___/
│________/
└─────────────────────────→ timeSymplectic Euler:
Energy
│ ~~~~ ~~~~ ~~~~ ~~~~
│ ~~ ~ ~ ~
│────────────────────────
└─────────────────────────→ time它不完美。
但它通常不会一路逃向无穷大。
对于游戏而言,这往往比单纯追求局部精度更重要。
9那为什么不直接用“完美公式”?
你可能会问:
如果我们知道:
为什么不直接计算?
因为这个公式要求:
但真实游戏中的加速度往往取决于当前状态。
例如弹簧:
空气阻力:
或者:
万有引力:
角色可能发生碰撞。
轮胎可能突然接触地面。
一个 Joint 可能突然达到角度限制。
玩家甚至可能在这一帧按下按钮产生新的力。
于是:
也就是说:
加速度本身也在不断变化。
这时候通常不存在一个可以直接塞进代码的漂亮解析公式。
数值积分就是为了处理这种情况。
10Verlet:另一种游戏开发经典积分器
游戏开发里另一个非常著名的方法是:
Verlet Integration。
它尤其常见于:
- 布料
- 绳子
- 粒子
- PBD
- 约束系统
一种常见的位置形式是:
有意思的是:
这里甚至没有显式保存速度。
因为:
本身就隐含了上一小段时间的运动趋势。
于是可以写成:
Vector3 current = position;
position =
position +
(position - previousPosition) +
acceleration * dt * dt;
previousPosition = current;position - previousPosition
可以看成近似的位移趋势。
因此 Verlet 给人的感觉非常漂亮:
物体不知道自己的“速度”是多少。
它只是记得自己上一刻在哪里。
然后根据:
过去的位置 + 当前受到的力
推算下一刻。
这套思想后来与 Position Based Dynamics 等方法产生了非常紧密的联系。
11RK4:既然可以算一次斜率,为什么不多看几次?
Euler 的核心思想是:
看当前的斜率,然后大胆往前走。
RK4,也就是:
Fourth-order Runge–Kutta
采取了更谨慎的方法。
它会在一个时间步内多次估计斜率:
最后综合:
直觉上可以理解成:
相比 Euler:
看看现在
↓
直接走RK4 更像:
看看现在
看看中间
再看看中间
看看未来附近
↓
综合决定怎么走对于很多普通微分方程,它的精度非常优秀。
全局误差可以达到:
听起来似乎已经完胜 Euler。
可现实游戏物理并没有因此全部变成 RK4。
为什么?
12游戏物理追求的并不是“数学考试里的最高精度”
因为一个完整游戏世界里可能存在:
2000 个刚体
500 个 Joint
大量接触点
角色控制器
布料
车辆
破坏系统假设一次物理状态评估需要处理整个世界。
Euler 可能只需要一次。
RK4 却需要多次评估。
成本一下子增加很多。
更麻烦的是,刚体物理存在大量:
- 碰撞
- 摩擦
- 接触约束
- Joint
- 不连续事件
比如一个球:
这一刻:
●
下一刻:
──────── 地面到底在什么时候接触?
一旦发生碰撞,速度可能突然改变:
这种系统并不像一个平滑函数那么优雅。
因此现代实时刚体物理的核心困难,很多时候并不是:
怎样精确积分一个光滑函数?
而是:
怎样稳定地处理大量接触和约束?
这就会进一步进入:
Constraint Solver。
例如:
- Sequential Impulse
- Projected Gauss–Seidel
- XPBD
- Jacobian
- Lagrange Multiplier
于是你会发现:
游戏物理最深的部分,后来甚至已经不再像高中力学。
它逐渐变成了:
线性代数 + 数值分析 + 优化 + 约束求解。
13时间步长可能比积分器本身还重要
即使使用同一种积分器:
velocity += acceleration * dt;
position += velocity * dt;不同的:
dt也可能得到完全不同的结果。
假设:
每一步很小。
我们对真实曲线的逼近通常比较好。
如果:
那么每一步跨越的时间非常大。
局部线性近似就可能完全失效。
可以想象成沿着一条弯路行驶。
如果每隔 1 米重新调整方向:
· · · · · · ·还能比较好地贴着道路。
如果每隔 100 米才调整一次:
●────────────●你可能早就冲出山崖了。
因此很多游戏都会使用:
Fixed Timestep。
例如逻辑上保持:
或者:
而不是直接把渲染帧耗时作为物理步长。
这也是 Unity 中 FixedUpdate 这类概念背后的核心思想之一:
渲染帧率和物理模拟频率,并不是同一件事情。
14为什么 Variable Delta Time 容易制造麻烦?
考虑:
position += velocity * Time.deltaTime;如果游戏稳定在:
60 FPS那么:
但突然掉帧到:
15 FPS那么:
一步突然变成原来的四倍。
这不仅意味着误差变大。
对于弹簧、Joint、约束等系统,甚至可能直接跨过稳定范围。
于是你会看到经典游戏物理事故:
平时:
●
/|
──────┴──────
突然卡顿一帧:
●
/
─────────────┴────────
然后:
💥 NaN因此复杂物理系统通常非常在意:
最大时间步长。
必要时会进行:
Substepping。
例如这一帧:
不要一次模拟:
而是拆成:
也就是:
一个大步
↓
多个小步虽然计算量增加,但稳定性通常明显改善。
15“积分器”和“物理引擎”不是同一个东西
这里还有一个很容易混淆的概念。
一个完整的刚体物理 Step,大概更像:
实际物理引擎当然会复杂得多。
但可以看到:
Integrator 只是整个物理引擎中的一个组成部分。
真正困难的部分还包括:
- Broad Phase
- Narrow Phase
- Contact Manifold
- Friction
- Joint Constraint
- Solver
- CCD
- Sleeping
- Island
- Warm Starting
所以:
“PhysX 使用什么积分器?”
虽然是一个有意义的问题,但它绝不足以解释 PhysX 为什么稳定或者为什么性能好。
现代物理引擎的行为,是整个求解系统共同决定的。
16一个特别重要的认知:模拟不等于现实
很多游戏开发者刚开始学习物理时,会下意识认为:
只要把牛顿公式写对,游戏里的运动就一定是正确的。
实际上:
物理公式正确
和
数值模拟正确
是两个问题。
连续世界可能满足:
但你最终实现的是:
这是另一个数学系统。
它只是希望:
当:
时,它能够逼近原来的连续系统。
因此游戏里的物理并不是现实物理的复制品。
它更像是:
现实物理在有限计算资源下的一个离散模型。
这也是数值分析最迷人的地方之一。
17数学里的“正确”,其实有很多种
当我们评价一个积分器时,可以问很多完全不同的问题。
比如:
精度
一次模拟到底离真实值有多远?
稳定性
误差会不会随着时间不断爆炸?
能量行为
系统会不会凭空产生或损失能量?
计算成本
每一步需要多少次力计算?
时间可逆性
往前模拟再往后模拟,能不能回到原状态?
结构保持
是否保留原系统的一些几何性质?
因此不存在一个简单的:
“最好的积分器”。
不同问题,会需要不同的方法。
| 方法 | 特点 | 常见场景 |
|---|---|---|
| Explicit Euler | 极其简单,但动态系统中可能不稳定 | 教学、简单模拟 |
| Semi-Implicit Euler | 简单、便宜、长期行为通常更适合机械系统 | 实时游戏物理 |
| Verlet | 位置形式自然,约束处理方便 | 布料、绳子、粒子、PBD |
| RK4 | 对光滑 ODE 精度高,但计算成本较高 | 轨道、控制、科学模拟 |
| Implicit Methods | 通常更稳定,但需要求解方程 | 刚性系统、离线模拟 |
这里最有趣的一点是:
“精度最高”并不自动意味着“最适合游戏”。
游戏是实时系统。
它永远需要在:
之间进行权衡。
18从 Euler 还能一路走到现代游戏物理
如果继续往下研究,会发现 Euler 只是入口。
这条路线非常有意思。
因为你可能最开始只是想写:
rigidbody.velocity += gravity * dt;最后却一路进入:
- 线性代数
- 微积分
- 微分方程
- 数值分析
- Hamilton 力学
- 优化算法
- 约束求解
游戏开发中的数学经常就是这样。
一个看似平凡的 API 背后,可能藏着几百年的数学。
19回到最开始的问题
所以:
为什么游戏里的“积分”不像高中数学里的积分?
因为高中数学经常讨论的是:
已知一个函数,求它的积分。
而游戏开发通常面对的是:
已知一个动态系统的变化规律,怎样让计算机一步一步模拟它未来会发生什么?
理论上我们写:
计算机里我们写:
velocity += acceleration * dt;理论上:
计算机里:
position += velocity * dt;微积分描述了:
世界应该怎样连续变化。
数值积分解决的是:
有限速度的计算机,怎样假装自己生活在那个连续世界里。
而 Semi-Implicit Euler 最有趣的地方就在这里:
velocity += acceleration * dt;
position += velocity * dt;它看起来不过是两行平凡代码。
但这两行代码背后连接的,却是:
Newton、Euler、Hamilton、微分方程、相空间、数值稳定性,以及今天每秒运行几十次的游戏物理世界。
有时候,游戏开发里最有意思的数学,就是藏在这些我们每天都在写、却很少继续追问的代码里面。
CONTINUE EXPLORING