积分器与稳定性:同一个球,为什么换个写法就越弹越高
全课骨架:物理模拟每一帧只做三件事 —— ① 对每个物体积分;② 找出接触(谁压着谁、压多深);③ 解约束(按质量分配地纠回来)。
本课只讲 ① 积分。保真度:真机可证(画面与读数都由本页自己的积分给出,对照物是闭式解与守恒量,读者能拿笔复核)。
这台设备拿不到 canvas 2D 上下文,活例跑不了。
本页的原理与数值表照常可读(画面只是把同一个数画出来)。
定点观察:把「步长 h」拉到最大 → 看两条能量曲线 → 显式欧拉每一跳都比上一跳更高(这不是 bug,是它的固有行为);半隐式只在一条带里来回。
本帧的读数
(脚本启动中…)
1.2 段 · 步长一大就爆
本段只增加一样东西:步长 h(其余控件与 1.1 完全一致)。保真度:真机可证。自检面板在页底。
定点观察:把「步长 h」拉到最大 → 看表②「触地前 E」那一列 → 显式那一列逐跳单调上升(越弹越高):
能量不是"少算了一点",是被系统地加上去了 —— 这就是显式欧拉的固有行为,跟显卡、浏览器都无关。
把中间量摊开(件三:每张表都能拿笔验算)
表① 自由落体:本页积分 vs 闭式解(h = 1/120 s、起跳速度 6 m/s)
| t (s) | 本页积分 y (m) | 闭式解 y (m) | 差 (m) |
算式:y = y₀ + v₀·sin(0.9)·t − ½g t²(y₀ = 5 m、v₀ = 6 m/s、g = 9.81)—— 拿笔算一行就能核对本页。
差额那一列随 h 线性缩小(一阶),这正是 1.1 段的结论。
表② 每一跳的能量:显式欧拉 vs 半隐式(同 h、同初值:v₀ = 2 m/s)
| 第 n 跳 | 触地前 E (J) | 触地后 E (J) | Eₙ÷Eₙ₋₁(显式) | Eₙ÷Eₙ₋₁(半隐式) |
算式:E = ½m|v|² + mgy(m = 1 kg)—— 每行都能用那一刻的 v 与 y 手算。
实测:显式那一列每一行都 > 1(每次触地净长约 2%);半隐式每一行都 < 1(每次净掉约 1%)——两者都不是精确守恒,
但方向相反、且半隐式的偏差小一半。真正的差别要看「触地前 E」那一列:显式逐跳单调上升(越弹越高),半隐式只在一条窄带里起伏。
1.3 段 · 只差两行的顺序
本段只增加一样东西:积分器的写法(步长 h 冻回 1.2 里最糟的那一档,其余控件一致)。保真度:真机可证。
定点观察:两个按钮来回切 → 看表② 的两列 → 显式每次触地净长 +2%、半隐式每次净掉 −1%:
整段只差「推位置」用的是旧速度还是新速度这一处 —— 同一台机器、同一个 h、同一个初值,结果的定性完全相反。
1.4 段 · 步子迈小一点:一帧当 8 帧用
本段只增加一样东西:每帧的子步数 n(积分器冻回 1.3 里的半隐式、步长档位保持当前值,其余控件一致)。
保真度:真机可证。上一段解决了「写法错会越弹越高」,但偏差本身还在 —— 一阶方法里它是 ∝ h 的。
想把它压下去只有两条路:换一个更高阶的积分器(1.5 之后),或者把 h 拆小。本段先做后者,因为它最便宜、也最常用。
定点观察:把「每帧子步」从 1 拉到 8 → 看表③「max |y − y_闭式|」那一列 → n 每翻一倍,偏差约砍半:
一阶方法的偏差 ∝ h —— 这不是个估算,是个能在表里逐行验证的等比数列。
表③ 子步数 n 与「同一段时间内的最大偏差」(自由落体、显式欧拉、T = 0.6 s、v₀ = 6 m/s)
| 子步数 n | 子步长 hh (s) | max |y − y_闭式| (m) | 偏差 ÷ 上一档 |
算式:y_闭式 = y₀ + v₀·sin(0.9)·t − ½g t²,max 在 t ∈ (0, 0.6] 上取。
一阶方法的偏差 ∝ h:n 翻倍 ⇒ hh 减半 ⇒ 偏差约减半。表③最后一列应当接近 0.5,越接近越说明这一课没白讲。
1.5 段 · 帧率抖,物理不抖
本段只增加一样东西:把「推一步用的 h」与「这一帧的墙钟时长」解耦(积分器、子步、初值与上一段完全一致)。
保真度:真机可证。1.1 ~ 1.4 都假设 h 是个常数 —— 但浏览器每一帧回来的间隔并不恒定(performance.now() 之差在抖)。
如果直接用「上一帧到这一帧的墙钟」当 h,物理参数就成了帧率的函数:同一台机器、同一个球,60 fps 与 47 fps 跑出来的轨迹不一样。
本段把这事钉死:物理只认固定 h,墙钟的零头丢进一个累加器。
定点观察:把页面放后台再切回来(墙钟会跳一下) → 看自检面板「确定性」那一行 → 同初值两次运行逐位相同 = true:
物理时刻被锁在 h 的整数倍上,墙钟怎么抖都进不来。这一行就是「确定性」三个字在工程上的含义。
表④ 墙钟抖 vs 物理不抖:同一段时间(T = 0.6 s)用两套 Δt 序列喂同一个累加器
| 墙钟 Δt 序列 | 物理步数 | 末态 y (m) | 末态 v_y (m/s) | 两条序列末态差 |Δy| |
算式:acc += Δt;while (acc ≥ h) { acc −= h; 推一步 }。
两条 Δt 序列的构造是「首 +δ、尾 −δ、其余不动」(δ = 0.3h),浮点相加的总和严格等于 T = 0.6 ——
因此物理步数一样、末态逐位相同(不是「几乎相同」)。表④ 的最后一列应当精确等于 0。
反例(拿笔就能验证):如果把抖动序列写成「先生成再除以总和归一」,浮点求和顺序会让总墙钟差 1 ulp,
在 0.6 s / h = 72 的边界上足以差出整整一步,末态偏差 0.12 m 量级 —— 这不是物理错,是「总墙钟没对齐」。
课末:这条链走到哪了
1.1 给出闭式解当对照物;1.2 看见显式欧拉的固有行为;1.3 把它拆到只差两行;1.4 把偏差压到 ∝ h;1.5 把帧率抖隔离在物理之外。
至此骨架①「积分」收齐。下一课讲骨架②:当两个物体不再只是「各跑各的」,怎么知道谁压着谁、压多深 —— 接触检测。