从布料到绳索的 Mass-Spring 物理模拟
引言:连续介质的离散化幻觉
在计算机图形学中,我们看到的飘逸裙摆、随风舞动的发丝,本质上是计算机对连续介质力学(Continuum Mechanics) 的一次”离散化幻觉”。真实世界的布料是无数纤维交织的连续体,其行为遵循复杂的偏微分方程(PDE);但在以 60 FPS 运行的实时管线里,直接求解 PDE 是不切实际的算力黑洞。
为了在”物理真实”与”计算效率”之间取得平衡,弹簧质点系统(Mass-Spring System) 应运而生。它将连续的拓扑面/线降维为离散的”点”(承载质量与状态)与”线”(提供约束与作用力)。
本文将系统性地拆解这一体系,从最朴素的胡克定律出发,一路打通经典力学、数值积分、PBD/XPBD、碰撞与摩擦,最终落到 GPU Compute Shader 的工业级并行实现。
一、拓扑学基础与经典力学建模 (Force-Based Formulation)
构建任何物理模拟器的第一步,是建立数学模型。我们需要对几何拓扑做图论级别的拆解,并为每条边赋予明确的物理意义。
1.1 质点网络的拓扑结构 (Topology)
设网格(布料)或曲线(绳索)包含
在 Mass-Spring 模型中,根据节点间拓扑距离的不同,弹簧被严格划分为三个正交的物理维度:
A. 结构弹簧 (Structural Springs)
- 拓扑定义:连接图论距离为 1 的相邻节点。对 2D 布料,即
连接 与 ;对 1D 绳索,即 连接 。 - 物理意义:承担拉伸与压缩刚度 (Tensile/Compressive Stiffness),是维持物体宏观尺寸的核心。结构弹簧失效即对应布料被撕裂的物理现象。
B. 剪切弹簧 (Shear Springs)
- 拓扑定义:仅存在于 2D/3D 网格中,连接对角节点,即
连接 。 - 物理意义:抵抗面内剪切力 (In-plane Shear)。若缺少剪切弹簧,正方形网格在结构弹簧长度不变的情况下,可以坍缩成极度压扁的菱形。
C. 弯曲弹簧 (Bending Springs)
- 拓扑定义:跨越一个节点的连接,距离为 2。即
连接 、 ;绳索中即 连接 。 - 物理意义:抵抗面外弯曲 (Out-of-plane Bending)。丝绸的弯曲弹簧极弱,皮革或粗麻绳的弯曲弹簧则极强。
下面这个交互式拓扑图可以让你直观看到三类弹簧在 5×5 网格上的连接形态:
交互演示 · 三类弹簧的拓扑结构
点击按钮切换不同类型弹簧的可见性。可以观察:仅有结构弹簧时,网格在剪切方向几乎无约束;启用剪切弹簧后,菱形坍缩被禁止;弯曲弹簧则负责抵抗弯折。
进阶补充:现代实现(如 Bridson 等人的工作)往往用 二面角约束 (Dihedral Bending Constraint) 替代距离弯曲弹簧,以解耦”压缩”与”弯曲”两种行为。本文为了行文连贯,沿用经典三类弹簧的简化模型。
1.2 内力场:广义胡克定律的向量推导
考虑通过弹簧连接的两个质点
胡克定律的标量形式为
由”力是势能的负梯度”这一变分原理:
对 L2 范数求导,可得施加在质点
工程陷阱:当
(两质点几乎重合)时,公式产生除零奇异性 (Singularity)。代码实现中必须引入极小值 : float dist = max(length(deltaX), 1e-6f);。
1.3 能量耗散:阻尼力场 (Damping Force)
无阻尼弹簧系统是保守力场,动能与势能无限转换,绳索将永不停歇地抖动。我们需要引入粘性阻尼 (Viscous Damping),模拟材料内部纤维摩擦导致的能量热耗散。
设两点速度
关键设计:只有沿弹簧轴向的相对速度才产生内阻尼。正交方向的相对速度代表系统整体的旋转,不应被阻尼削减,否则会产生”非物理的太空阻滞感”。
记弹簧单位方向向量
1.4 外力场建模 (External Forces)
A. 重力场 (Gravity Field)
最简单的恒力场:
B. 空气动力学阻力 (Aerodynamic Drag)
对高质量布料模拟,风的影响至关重要。空气阻力不仅与速度有关,还与布料的迎风投影面积 (Projected Area) 有关。
设三角形面片法线
注意这里使用
二、数值分析与积分器 (Numerical Integration)
至此我们已构建了二阶常微分方程组 (ODE):
由于函数高度非线性且相互耦合,解析解几乎不可能存在。我们必须依赖离散时间步
2.1 显式积分的死局 (The Curse of Explicit Integrators)
最朴素的前向欧拉法 (Forward Euler):
为什么实时引擎几乎从不直接使用它?
考察一个简谐振子
也就是说,对任何时间步长
为了把发散控制在视觉可接受范围内,必须极大缩小
2.2 隐式欧拉 (Implicit Euler) 的降维打击
为了换取绝对稳定,离线渲染(Maya、Houdini)常用隐式欧拉:
注意公式右边用的是未来时刻的未知受力。它需要通过对力做一阶泰勒展开,构造一个巨大的稀疏矩阵,并使用共轭梯度法 (Conjugate Gradient, CG) 求解线性系统
- 优点:无条件稳定,
可以开得很大,且自带数值阻尼。 - 缺点:每帧求解大型稀疏方程组,计算量与内存压力都极重,移动端与多角色实时场景难以承受。
2.3 实时物理的王者:韦尔莱积分 (Verlet Integration)
韦尔莱算法源于分子动力学,是一种辛积分器 (Symplectic Integrator)——它在长时间积分下能近似保持相空间体积守恒(几何能量守恒)。
对位置进行前向、后向泰勒展开:
两式相加,奇数阶项相互抵消,得到基础 Verlet:
引入隐式速度(由前后位置差表示)和全局阻尼(模拟空气阻滞、提供数值稳定):
其中
优势:计算量与显式欧拉相当,但局部截断误差由
下面这个对比演示同时在两个 Canvas 上跑同一个简谐振子(初始位置
交互演示 · Forward Euler vs Verlet 稳定性对比
同一组初始条件 (x₀=1, v₀=0),两种积分器并行运行 200 步。Δt 越大、k 越大,Forward Euler 的发散越剧烈;Verlet 始终保持有界振荡。
三、工业革命 —— PBD 与 XPBD 架构
2006 年,NVIDIA 的 Matthias Müller 提出了 Position Based Dynamics (PBD),彻底改变了实时柔体物理的格局。Unity 的 Cloth 组件、UE 的 Chaos 引擎、Havok 物理的布料模块,底层皆由此衍生。
3.1 PBD 的哲学:力是表象,约束是本质
PBD 的核心洞见是:计算弹簧力极易爆炸,那我们干脆不要力了。
我们将弹簧视为一个纯粹的几何约束 (Geometric Constraint) 函数:
算法流程被重构为三步:
- 外力预测 (Predict):仅用重力和 Verlet 推算出预测位置
。 - 约束投影 (Project Constraints):发现
之间距离不再是 ,通过 Gauss-Seidel 迭代 把两个点强行推/拉回满足 的位置。 - 更新速度 (Update Velocity):用修正后的最终位置减去上一帧位置,自动得到受约束影响后的新速度。
这套流程的精髓在于:位置永远不会越界,因此系统永远不会爆炸。
下面是 PBD 完整的每帧执行流:
PBD 算法流水线
下面这个交互演示是一根由 12 个粒子组成的绳索,顶端固定。鼠标可以拖动任意粒子,而滑块控制每帧 PBD 求解的迭代次数——你能直观感受到迭代次数对”刚度”的支配关系:迭代越多,绳索越接近刚体;迭代越少,绳索越像橡皮筋:
交互演示 · PBD 距离约束迭代
红色 = 固定点,绿色 = 自由粒子,橙色 = 正在拖动的粒子。注意:迭代 1 次时绳索可被严重拉伸(PBD 此时近似一个软弹簧);迭代 ≥ 10 次时,绳索近似刚体棒——这恰是传统 PBD 刚度依赖迭代次数的根本原因(也是 XPBD 要解决的问题)。
3.2 约束梯度的数学推导
给定标量约束
一阶泰勒展开:
为了让位移在物理上合理(遵守动量守恒、按质量倒数加权),取:
其中
得到 PBD 通用位置修正公式:
代入距离约束
将距离约束
- 计算约束函数的梯度
首先,我们需要分别求约束函数对质点 和 的梯度。
设两质点间的当前距离为 ,两点间的归一化方向向量为 。
- 对
的梯度:
- 对
的梯度(由于 在负号后面):
- 计算分母部分
将梯度代入公式的分母 中。由于 和 都是单位向量,它们的模长平方均为 1:
- 计算标量乘子
将 和分母代入 的公式:
- 得到最终的位移修正量
根据 ,分别代入 和 的权重与梯度,即可得到常用于代码实现中的对称形式:
质点 1 的位移修正:
质点 2 的位移修正:
代码实现中的物理意义总结:
- 方向:
决定了力的作用在两点的连线上。 - 大小:
是当前的形变量(拉伸或压缩量)。 - 权重分配:
确保了位移按照质量的倒数( )进行加权。质量越大的点( 越小),位移修正量越小,完全符合动量守恒的物理直觉。如果某个点是固定点(质量无限大, ),它的位移修正量就会直接变成 0。
3.3 传统 PBD 的致命缺陷与 XPBD 的诞生
PBD 的痛点在于它的”刚度”与真实物理参数解耦:
- 迭代 5 次,布料像丝绸;迭代 20 次,布料像纸板。
- 同样的参数下,网格越细,整体布料越软。
这让美术调参极其痛苦。
XPBD (Extended Position Based Dynamics, 2016) 在约束求解方程中重新引入了真实的柔顺度参数 (Compliance)
XPBD 的拉格朗日乘子增量更新:
如此一来,无论帧率如何变化、迭代多少次,材料的物理刚度表现始终保持恒定。这就是当今 3A 引擎柔体模拟的底层骨架。
四、空间交互 —— 碰撞检测与摩擦力
系统不仅要自洽,还必须与世界交互。碰撞检测分为两个阶段:宽相 (Broad-phase) 与 窄相 (Narrow-phase)。
4.1 宽相加速:空间哈希与 BVH
如果布料有 10000 个顶点,场景有 100 个胶囊体,暴力遍历是
空间哈希 (Spatial Hashing):将 3D 空间划分为均匀网格,用质点坐标计算其所在体素索引,再用大素数哈希函数映射到 1D 数组:
其中
对于动态场景中带层级结构的碰撞体,BVH (Bounding Volume Hierarchy) 是更通用的选择,但构建成本更高。
4.2 窄相碰撞与 SDF (Signed Distance Field)
对于复杂角色(如带盔甲的人物),解析法球/胶囊求交不再适用。现代物理倾向使用 SDF (有向距离场)——一个 3D 体纹理 (Volume Texture),存储空间中每个点到模型表面的最短带符号距离
:质点在模型外部; :质点已穿模进入内部;- 表面法线可直接由 SDF 梯度获得:
。
在 PBD 框架下,SDF 碰撞自然地表示为不等式约束:
其中
4.3 库仑摩擦力模型 (Coulomb Friction)
真实布料不会在角色身上无限滑动。在求解完碰撞约束后,我们已得到碰撞法线
记切向位移:
引入静摩擦系数
- 若
,完全锁死(相当于把切向位移设为零); - 否则按动摩擦衰减:
,或更严格地按 截断。
五、极致工程化 —— Unity GPU Compute Shader 并行架构
理论再丰满,也无法掩盖 CPU 串行执行的羸弱。我们要把成千上万根弹簧扔进 GPU。
5.1 数据竞争噩梦 (The Data Race Nightmare)
GPU 的计算模型是 SIMT (Single Instruction, Multiple Threads)。当数千个线程同时处理不同弹簧时:
- 线程 A 处理弹簧
; - 线程 B 处理弹簧
。
两个线程会在同一周期内读写同一块
5.2 解决方案:图着色 / 红黑排序
为了避免内存冲突,必须将弹簧分组(称为 Pass/Color),保证同一组内任意两条弹簧不共享任何顶点。
- 1D 绳索(最简情形):
- 偶 Pass(Red):求解
- 奇 Pass(Black):求解
- 偶 Pass(Red):求解
- 2D 布料:结构 + 剪切 + 弯曲弹簧的组合通常需要 4 ~ 8 个颜色组 进行分批 Dispatch。颜色数越少,迭代收敛越快;颜色数过多则 Dispatch 调用开销上升,需要折中。
下面这个动画演示了 1D 红黑染色的并行调度过程——红色 Pass 中的 5 条弹簧同时被求解(无任何冲突),然后切换到黑色 Pass:
交互演示 · 红黑图着色并行调度
同一颜色组内的弹簧两两不共享顶点,可以无锁并行求解。GPU 线程组隐式同步后再切换到下一颜色组,即可避免 Data Race。
5.3 工业级 Compute Shader 核心源码
下面是一套经过内存对齐、支持图着色 PBD 的高性能 GPU 核心(HLSL):
HLSL
1 | |
5.4 C# 调度侧逻辑流 (Dispatcher)
Awake():基于 Mesh 拓扑生成Particle[]与多组着色后的Spring[],分别上传到独立的ComputeBuffer。FixedUpdate():- 设置全局参数(
dt、gravity、velocityRetain、stiffness); Dispatch("Integrate")—— 外力预测;- Substep 子迭代循环(增加刚度的关键):
Dispatch("SolveDistanceConstraints")—— 颜色 0(GPU 自然同步);Dispatch("SolveDistanceConstraints")—— 颜色 1;- … 直到所有颜色组都被求解;
Dispatch("CollisionSDF")—— 边界与摩擦处理。
- 设置全局参数(
- 渲染管线对接:将最终的粒子位置 Buffer 直接绑定到自定义 Shader,通过
SV_VertexID在 Vertex Stage 中索引粒子位置进行顶点变换,实现真正的 Zero CPU Overhead 渲染管线(无需 readback)。
5.5 实时布料Demo
最后,我们用 Three.js 在浏览器里复刻一遍上述完整体系:Verlet 积分 + 多次 PBD 距离约束迭代 + 球体 SDF 碰撞 + 鼠标交互。鼠标可以拖拽布料的任意顶点,布料挂在两个角点,自由垂落并与红色球体发生碰撞:
交互演示 · 自由落体、碰撞与空气动力学
玩法:调节风力拉杆,布料会受到基于法线的真实空气动力学升力。点击【剪断/自由落体】可观察布料掉落并包裹球体的精密碰撞过程!
六、高阶视觉化 —— 法线重计算与自阴影
物理对了,但若渲染光影错了,作品依然是失败的。
布料顶点在 GPU 中被移动后,Mesh 原有的法线 (Normals) 与切线 (Tangents) 数据立刻失效。使用旧法线渲染,被风吹起的布料依然呈现”平铺地面”的光照分布,违和感极强。
我们需要再编写一个 Compute Shader Kernel RecalculateNormals:
- 遍历所有三角面片(Triangle ID 作为线程索引);
- 根据三个顶点的新位置
计算面法线: - 利用原子操作(
InterlockedAdd,通常需将法线分量编码为定点整数避免并发写冲突)将面法线累加到顶点法线累加器; - 在第二个 Kernel 中遍历顶点,归一化得到平滑后的顶点法线。
对于高级布料/毛发渲染(如 Kajiya-Kay 或 Marschner 各向异性高光模型),光照对法线与切线的精度极度敏感。法线一旦正确,配合 PBR 光照模型,即可呈现 3A 级次世代的柔体表现。
总结:代码与物理的交响曲
从一根最朴素的弹力系数
计算机图形学的魅力正在于此:我们用最严谨冰冷的数学矩阵,绘制出最具生命感与温度的动态世界。
愿这篇深入到底层的剖析,能成为你在物理渲染道路上的一座指路灯塔。