软体:把「必须正好」换成「尽量靠近」,绳子为什么就软了
全课骨架:物理模拟每一帧只做三件事 —— ① 对每个物体积分;② 找出接触(谁压着谁、压多深);③ 解约束(按质量分配地纠回来)。 第 3 课讲的是硬约束:接触必须被满足,穿进去就推出来。可绳子、布料、头发不是"撞到了"——它们从来没被要求正好是某个长度, 只是"尽量靠近"。本课是骨架 ① 与 ③ 的组合:把约束放松一点,软体就出来了。 保真度:真机可证(画面与读数都由本页自己的解算器给出;对照物是闭式静平衡与稳定性判据,读者能拿笔复核)。
本页的原理与数值表照常可读(画面只是把同一个数画出来)。
(脚本启动中…)
定点观察:切到「4.1 弹簧力」,把「弹簧刚度 k」从 1000 往右推,直到越过状态行里那条稳定线 3600 → 看左边那条链 → 越线不等于立刻爆:k = 4000 时链会停在一个"伸长 1.8 m 的错误振荡"上(正确值是 0.039 m),再往右到 10000 才是真指数发散(画面会打出面板并自动重置)。切到 4.2 以后,同样把 k_s 推到底,链只会更硬、不会失控。
本帧的读数
(脚本启动中…)
4.1 段 · 先用弹簧把它连起来
本段只增加一样东西:弹簧刚度 k(只有显式弹簧力,PBD/XPBD 都关掉;其余控件与本课各段一致)。 保真度:真机可证。第 3 课的接触是"硬"的 —— 压进去就推出来,没有商量余地。绳子不是这样: 它只是不想被拉长,拉长了就给一份回拉力,公式是最朴素的那个 F = k·(d − L₀)。本段把这条链交给它。
| k (N/m) | 临界步长 √(m/k) (s) | 阶段 | 末步伸长 (m) | 末步 \|v\| 峰值 (m/s) | 算式 mg·N(N−1)/2k |
|---|
算式(稳定性):单根弹簧的角频率 ω = √(k/m),半隐式欧拉对它的稳定条件是 h·ω < 2,即 k < 4m/h² = 14400。
但一根链上最高的那个模态是略小于 2√(k/m)(相邻两颗粒子反相跳动),于是门槛收到 k ≈ m/h² = 3600 附近,
等价地步长必须小于 √(m/k)。实测(N = 6)的翻转点在这条近似线的右侧一点:3800 仍在稳定侧、4000 已失控
—— 因为 N = 6 的链最高模态还没到 2√(k/m)(模态与链长有关),所以精确门槛比 m/h² 略高。近似线写在这里,判读者对错靠实测。
算式(静平衡):第 0 颗锚定 ⇒ 自由粒子 N−1 颗、弹簧 N−1 根;第 s 根承担它下面 N−s 颗的重力,
于是总伸长 = Σ j·mg/k = mg·N(N−1)/(2k)。拿 N = 6、k = 1000 验算:1×9.81×30/(2×1000) = 0.1472 m —— 表① 那一格应当对上。
"越线"不是一件事,是三件事(这是本页实测纠正过来的一处口径,原来写成"超过就炸"):
① 稳定侧停在正确的静平衡上(3800 的伸长 0.0397 贴着算式 0.0387,差 3%);
② 刚越线(k = 4000)不会爆,而是停在一个错误的极限环上 —— 伸长在 0.78~1.83 m 之间摆,而算式只要 0.0368 m,
差 20~50 倍,而且阻尼把它限住了、不会自己变大;
③ 再往右(k ≥ 4500)才是真发散 —— k = 10000 时 0.5 s 就到 5.5e21。
所以画面上的守卫把 ② 也算作失控(见画面里的面板):它虽然有限,但已经完全不是这根绳子的解了。
这条式子长得和 4.4 段一模一样(到那里把 1/k 写成 α 就是了)。
4.2 段 · 不推力,直接改位置
本段只增加一样东西:改用 PBD 的位置投影(刚度换成 k_s,其余控件一致)。 保真度:真机可证。4.1 的麻烦是双重的:要挑 k,还要一直盯着 h·ω < 2 —— 想更硬就得更小心, 而"更硬"恰恰是布料最想要的东西。PBD 换了个问法:别算力了,直接问"这两个点现在差了多少,把它挪回去"。 挪多少由 k_s 控制(1 = 一步到位,0.5 = 只纠一半),而挪的比例还是按质量倒数分配 —— 和第 3 课同一套。
| k_s | 总伸长 (m) | 链长 ÷ 原长 (N·L₀ = 0.60 m) | 备注 |
|---|
算式:PBD 的伸长没有闭式解(它是每步投影之后的残差),但有一个能拿笔验的端点: k_s → 0 时完全不受约束(表② 最后一行,链会一直往下坠、伸长随时间增长)。 而 k_s = 1 那一行实测是 2.7 cm,不是 0 —— 因为一遍扫描是逐条处理的(Gauss-Seidel): 修好第 i 条约束会把第 i−1 条重新推歪,于是每帧重力新造出来的那点长度差,一遍扫不干净。 这和第 3 课表④的迭代问题是同一件事,只是那里的对象是穿透、这里是伸长。 记下这个 2.7 cm —— 4.3 与 4.4 都要用到它(它是一遍扫描的"残差地板")。
4.3 段 · 子步在换什么
本段只增加一样东西:子步数 n_s(k_s 冻回 0.5,其余控件一致)。 保真度:真机可证。4.2 的 k_s 是个 0~1 的数,可它到底是"多软"?它自己说了不算 —— 每步只纠 k_s 份,而"一步"有多长是由步长定的。想把结果调硬,有两条路:调大 k_s,或者把一步拆成 n_s 个更小的步。 本段只动后者,看这两条路是不是同一回事。
| 子步数 n_s | 总伸长 (m) | 等效单遍刚度 1−(1−k_s)^n | 用该刚度单遍跑出的伸长 (m) | 两者之比 |
|---|
算式:单根约束被投影 n 次、每次留 k_s 的残差,剩下的误差是 (1−k_s)^n,所以"等效一次投影" 应当等价于刚度 1 − (1−k_s)^n(k_s = 0.5、n = 2 ⇒ 0.75;n = 4 ⇒ 0.9375)。 表③ 的第 4 列就是把那个等效刚度拿去单遍跑一遍的结果 —— 如果两条路真的一样,它应当与第 2 列相等。 实测并不相等(比值见最后一列),因为子步同时把"每步的下沉量"按 1/n_s 缩小了,而 k_s 没有。 这条差异正是 4.4 段要消灭的东西。
4.4 段 · 想说多硬,就直接说多硬
本段只增加一样东西:把 k_s 换成柔度 α = 1/k(其余控件一致)。 保真度:真机可证。4.3 的结论很别扭:k_s 是"每步纠多少",含义跟着步长跑 —— 美术说"这根绳子柔度 1e-4", 程序却得先问"你每秒跑几帧"。XPBD 把这件事扳回来了:直接用柔度 α = 1/k(单位 m/N,和 4.1 的 k 互为倒数), 在投影式子里带上 α̃ = α / h_s² —— 于是 h_s 变了,α̃ 自动补偿。
| α = 1/k (m/N) | 算式 mg·N(N−1)/2·α (m) | 实测 n_s = 1 (m) | 实测 n_s = 4 (m) | 实测 n_s = 8 (m) | n_s = 8 ÷ 算式 |
|---|
算式:静平衡下第 s 根弹簧承担它下面 N−s 颗的重力 ⇒ 它的伸长是 (N−s)·mg·α,
Σ 起来就是 总伸长 = mg·N(N−1)/2·α(和 4.1 那条同形,因为 α 就是 1/k)。
本段的看点是第 3、4、5 列:三档子步数给出的是同一个数 —— 最后一列的比值实测 0.990 / 1.000 / 1.004 / 1.002,
也就是说 α 换了子步(等价于换了步长)也照样兑现。这正是 k_s 做不到的事:
表③ 里同样的"换子步",结果差了 18 倍;这里只差 0.4%。
唯一还有残差的是 n_s = 1 那一列里 α 最小的两行(0.0176 / 0.0069,对着算式 0.0147 / 0.0015)——
那是表② 里那个"一遍扫描的地板"(约 2.7 cm 量级)盖过了柔度本身;子步一多就消失(n_s = 4 起就贴着算式)。
所以准确的说法是:α 定的是目标值,子步只决定你离它多近。
课末:第二视角 —— 一条链横竖各来一遍,就是布
本页左边那条链是一维的:只连相邻两颗,为的是每个数都能拿笔算。右边那块布用的是同一个解算器, 只是把同一件事在网格上做了两遍 —— 横着连一遍(每行的邻居)、竖着连一遍(每列的邻居), 约束条数从 N−1 变成约 2N(8×6 的网格是 82 条),解算器一行都不用改。 所以"布料是怎么出来的"这个问题,答案不是另写一个布料求解器,而是把"必须正好"放松成"尽量靠近",然后在网格上多连几遍。 下一课把镜头挪到关节与骨骼:点约束、铰约束、motor —— 以及 ragdoll 为什么会抖成面条。