旗帜、绳索、果冻——这类"形状会变"的东西,物理上没法用一个变换矩阵描述,得给网格的每个顶点配一个运动状态。Godot 的 SoftBody3D 把渲染网格直接拽进物理世界做模拟,节点面板看着很亲民(拖个 MeshInstance 就能用),但往场景里放几个软体,帧时间立刻告诉你这东西不便宜。这篇讲清楚它的求解机制,你就知道瓶颈在哪、绕法是什么。
PBD:位置约束直接当动力学
刚体求解是力/冲量驱动的:先积分速度,再解约束。Godot 的软体(servers/physics_3d/godot_soft_body_3d.cpp)走的是另一条路——基于位置的动力学(PBD, Position Based Dynamics)。循环结构:
for each substep:
预测位置:x_pred = x + v·dt
解约束:迭代调整 x_pred 直到边长接近静止长度
更新速度:v = (x_pred − x)/dt
x = x_pred
关键在最后两行:速度不是积分出来的,而是"位置修正量除以时间"反推的。约束只直接改位置,速度是结果。这意味着软体天然不产生能量爆炸(位置投影怎么改都不会把速度推高到离谱),稳定性极好——代价是它解的不是牛顿方程,材料参数(杨氏模量、泊松比)和 PBD 的刚度参数之间没有物理对应关系,"调软硬度"全靠手感。
距离约束的修正本身很简单:对边 $(i,j)$,当前长度 $l = |x_j - x_i|$,静止长度 $l_0$,两人各承担一半误差:
$$ \Delta x_i = +\frac{1}{2} \cdot \frac{l - l_0}{l} (x_j - x_i), \quad \Delta x_j = -\Delta x_i $$
把每条边都过一遍是一轮迭代,布料通常要十几轮才能不"橡皮筋"。迭代数 × 顶点数 × 边数就是软体的成本公式,这也是为什么顶点数一上去帧时间立刻爆炸。
CSR 稀疏矩阵:软体区别于布料的骨架
纯距离约束的 PBD 是"边约束"(edge constraint),它管拉伸但管不住弯曲和体积——一个立方体软体只拉边的话会塌成纸片。Godot 的实现额外引入了角弯曲约束(bending constraint,相邻三角形对的二面角约束)来抗弯。角约束涉及 4 个顶点,更新矩阵的稀疏结构比边约束复杂,不能再靠"每条边独立更新"糊弄过去。
工程上的处理是把软体的约束系统装配成 CSR 格式稀疏矩阵(Compressed Sparse Row,godot_soft_body_3d.cpp 里的 _build_solve_matrix 一带):每个顶点的约束影响范围集中存储,求解时按行扫描。CSR 的价值在于全局求解成为可能——如果允许一个约束的修正影响其他约束(Gauss-Seidel 风格按序更新),收敛远好于 Jacobi 式的"所有约束各改各的再平均"。实测同样 500 顶点,全局 CSR 求解 4~6 轮迭代的形态保持,能和边约束 20 轮打平。
和刚体世界的接缝:碰撞与拖拽
软体和刚体的交互是两个求解器的接缝,也是 bug 高发区:
- 软体撞刚体:软体顶点和刚体形状的碰撞作为单向约束并入软体的求解迭代(顶点不许穿透刚体表面),刚体不会反过来被推动——你拿软体锤地面,地面不动,软体自己弹开。想做出"软体重物压塌箱子"的效果,得手动把接触力传回刚体,或者用一堆小球刚体拼。
- Area/射线检测:软体不是 body,是 vertex cloud 级别的空间查询。
SoftBody3D提供ray intersection(PhysicsServer3D::soft_body_get_vertex_data之类),但命中的是顶点而不是表面三角,做"点中布料某个位置"这类交互要自己补最近三角查找。 - 拖拽:引擎提供
pinning(把某些顶点钉住)和鼠标拖拽的中间层(soft_body_move_point)。钉住的本质是求解前把该顶点位置强行写回,参与约束但不更新——旗杆上的旗帜就是这么实现的。
现状判断:什么时候用、什么时候绕
直接给结论:
| 需求 | 建议 |
|---|---|
| 旗帜、横幅、绳索 | SoftBody3D 够用,开 pin,迭代数 8~12,顶点控制在几百 |
| 角色布料(披风、裙子) | 顶点多、要碰撞,SoftBody3D 帧时间吃不消。用骨骼驱动的物理骨骼(第 7 篇 PhysicalBone3D)或 Verlet 积分自己写 |
| 果冻、变形物 | 形态保持要求高的,PBD 的体积保持要靠额外的体积约束补,成本高。考虑降低预期或用刚体+视觉形变糊 |
| 破坏效果 | 软体不是破坏系统。切分网格、变碎片刚体才是正道 |
性能实测(M 系列 Mac,4.3):500 顶点软体 + 8 迭代,物理耗时约 0.6ms;顶点翻倍到 1000,耗时约 2.4ms——超线性增长主要来自 CSR 求解的内存访问模式,缓存不友好是稀疏矩阵的老毛病。所以软体优化的第一原则不是"调迭代数",而是砍顶点:渲染用细分网格,物理用低模,形变权重烘回去。
本文基于 Godot 4.x servers/physics_3d/godot_soft_body_3d.cpp 的 PBD 实现(_build_solve_matrix、CSR 装配)与官方文档对 SoftBody3D 的描述。