布料模拟(Cloth Simulation)

Rust + WebAssembly 用 Verlet 积分和距离约束模拟一块可以拖拽、剪开、撕破的布

🖱️ 拖拽 / 👆 手指 拉扯布料,拉得太猛会撕破  |  切到「剪刀」后划过布料把它剪开

📖 原理说明

游戏里飘动的旗帜、角色的披风,常用的做法是把布看成一张由质点连成的网:每个质点受重力和风力推动自由运动,相邻质点之间有一根「不能拉长」的连接,每一帧把被拉长或压短的连接往回调整几次。这种方法(Jakobsen 2001,Verlet 积分 + 约束松弛)简单又稳定,还能轻松实现剪开和撕破。

🧮算法原理

x新 = x + (x − x上一帧) · 阻尼 + a · Δt²Verlet 积分:速度隐含在两帧位置之差里,不用单独存
Δ = (|pb − pa| − L) / |pb − pa|,两端各移动 Δ/2距离约束:把连接的两端沿连线方向拉回到原长 L
Verlet 积分
每个质点只记住当前位置和上一帧的位置,两者之差就是速度。好处是修改位置(比如约束把质点推回来、撞到地面被截住)会自动反映到速度上,不需要额外处理,所以数值上很稳定,不容易「爆炸」。
约束松弛
一个质点连着四根连接,同时满足所有连接的长度是一个大方程组。这里不去精确求解,而是逐根调整:每根都把两端拉回原长(一端被固定时只移动另一端),整张网重复调整若干遍。遍数越多布越「硬」、越接近不可拉伸;遍数少时布像橡皮一样松软下垂。
撕裂与剪开
每一步检查每根连接的长度,超过原长的 1.9 倍就删掉它。实测最大风力只能把连接拉到约 1.5 倍,而用手猛拉会超过 2 倍,所以风吹不破、猛拉会破。剪刀直接删除鼠标轨迹附近的连接。约束迭代调到 1~2 次时布太松,在强风里也可能被自己扯破。
绘制
四条边都完好的小方格填充颜色,颜色由方格面积相对原来的比例决定:被挤压(褶皱)的地方暗,被拉伸的地方亮并偏红,于是不用光照计算也能看出布的起伏。断开后剩下的零散连接画成线,按张力着色。

🔄Rust 与 JavaScript 的分工

  1. JS每一帧按真实时间累积,每满 1/60 秒调用一次 cloth_step(dt, 迭代次数, 重力, 风力, 撕裂阈值),所以物理速度与屏幕刷新率无关。
  2. RustRust 对 1600 个质点做 Verlet 积分,按设定遍数松弛 3118 根连接,删掉拉断的连接,并把质点限制在画布范围内(碰到地面和墙壁有摩擦)。
  3. JSJS 读取 cloth_positions()、cloth_links()、cloth_stretch() 并绘制。按下鼠标时 cloth_grab 找到最近的质点,拖动时用 cloth_drag 把它固定在指针位置;剪刀模式调用 cloth_cut。

⚡性能要点

  • 实测(桌面 Chromium):50×32 个质点、15 遍约束迭代,每步约 0.5ms;读回位置和连接不到 0.03ms。计算量与「连接数 × 迭代次数」成正比。
  • 绘制(约 1500 个填充四边形)比物理计算更耗时,这部分由 Canvas 2D 完成。

源码crates/simulation/cloth/src/lib.rswww/cloth/index.js