抓住任意一点拖动。把「撕裂」打开再狠狠一拽,布会沿着受力最大的地方裂开。
// 布料 = 三种距离约束叠在一张网格上
// 拉伸:相邻两点(撑住布不被拉长)
// 剪切:格子对角线(防止布像纸箱一样塌成平行四边形)
// 弯曲:隔一格的两点(越硬布越挺,越软越像丝绸)
for (let y = 0; y < rows; y++)
for (let x = 0; x < cols; x++) {
const p = id(x, y);
// 拉伸
if (x + 1 < cols) addEdge(p, id(x + 1, y));
if (y + 1 < rows) addEdge(p, id(x, y + 1));
// 剪切
if (x + 1 < cols && y + 1 < rows) {
addEdge(p, id(x + 1, y + 1));
addEdge(id(x + 1, y), id(x, y + 1));
}
// 弯曲
if (x + 2 < cols) addEdge(p, id(x + 2, y));
if (y + 2 < rows) addEdge(p, id(x, y + 2));
}
// 撕裂:一条边被拉到静止长度的 N 倍就直接删掉
if (tearing && len > e.rest * tearRatio) e.dead = true;抓起来往地上砸。它靠的不是弹簧,是「每个三角形都想守住自己的面积」。边柔度越大越面,面积柔度越大越瘪——两个都拉到底,它就彻底塌成一摊。
// 面积约束:三角形被压扁时把它顶回去
// 这是原仓库四面体「体积约束」SolveVolumes() 的二维版本
// C = A - A0,梯度是每个顶点对面那条边的法向
const alpha = areaCompliance / (dt * dt);
for (const t of tris) {
const [i0, i1, i2] = t.ids;
// ∇0 = ½(y1 - y2, x2 - x1),其余两个轮换
const g0x = 0.5 * (py[i1] - py[i2]), g0y = 0.5 * (px[i2] - px[i1]);
const g1x = 0.5 * (py[i2] - py[i0]), g1y = 0.5 * (px[i0] - px[i2]);
const g2x = 0.5 * (py[i0] - py[i1]), g2y = 0.5 * (px[i1] - px[i0]);
const area = 0.5 * ((px[i1] - px[i0]) * (py[i2] - py[i0])
- (py[i1] - py[i0]) * (px[i2] - px[i0]));
const C = area - t.restArea;
const w = invMass[i0] * (g0x * g0x + g0y * g0y)
+ invMass[i1] * (g1x * g1x + g1y * g1y)
+ invMass[i2] * (g2x * g2x + g2y * g2y);
if (w === 0) continue;
const lambda = -C / (w + alpha);
px[i0] += lambda * invMass[i0] * g0x; // 三个顶点一起往外推
py[i0] += lambda * invMass[i0] * g0y;
/* i1、i2 同理 */
}左边持续吹进来一股烟。按住画面拖动,圆形障碍物就跟着你的手指走,烟会实时绕开它。
// 欧拉流体:不追踪水滴,而是把空间切成格子记录速度
// 对应原仓库 17 Eulerian Fluid Simulator / FluidSim.cs
// ① 投影:让每个格子的净流入为 0(不可压缩),高斯-赛德尔迭代
for (let it = 0; it < numIters; it++)
for (let i = 1; i < numX - 1; i++)
for (let j = 1; j < numY - 1; j++) {
if (s[i][j] === 0) continue; // 障碍物格子
const sx0 = s[i-1][j], sx1 = s[i+1][j];
const sy0 = s[i][j-1], sy1 = s[i][j+1];
const sSum = sx0 + sx1 + sy0 + sy1;
if (sSum === 0) continue;
const div = u[i+1][j] - u[i][j] + v[i][j+1] - v[i][j];
const p = -div / sSum * overRelaxation; // 1.9 收敛快得多
u[i][j] -= sx0 * p; u[i+1][j] += sx1 * p;
v[i][j] -= sy0 * p; v[i][j+1] += sy1 * p;
}
// ② 平流:每个格子回头去上游取值(半拉格朗日,永远稳定)
const x = i * h - dt * velocityU;
const y = j * h - dt * velocityV;
newField[i][j] = sampleField(x, y);绳子、布料、软体的区别,只在于「约束表里装了什么」。主循环一模一样:预测 → 解约束 → 反推速度,一帧切成若干子步反复做。
// 主循环:一帧切成 N 个子步,每个子步只解一遍约束
// 子步数比迭代次数管用得多 —— 这是 XPBD 相对老式 PBD 的关键
const sdt = dt / numSubSteps;
for (let s = 0; s < numSubSteps; s++) {
// ① 预测:只受重力,先自由飞一小步
for (let i = 0; i < n; i++) {
if (invMass[i] === 0) continue;
vy[i] += gravity * sdt;
prevX[i] = px[i]; prevY[i] = py[i];
px[i] += vx[i] * sdt;
py[i] += vy[i] * sdt;
}
// ② 解约束:直接改位置,不算力
solveDistance(sdt);
solveArea(sdt); // 软体才有
// ③ 反推速度:位置变了多少,速度就是多少
for (let i = 0; i < n; i++) {
if (invMass[i] === 0) continue;
vx[i] = (px[i] - prevX[i]) / sdt;
vy[i] = (py[i] - prevY[i]) / sdt;
}
}// XPBD 距离约束:把两个粒子拉回静止长度
// 对应原仓库 ClothSimulationTutorial.cs → SolveStretching()
const alpha = compliance / (dt * dt); // 柔度,0 = 绝对刚硬
for (const e of edges) {
const w0 = invMass[e.a], w1 = invMass[e.b];
const w = w0 + w1;
if (w === 0) continue;
let dx = px[e.a] - px[e.b];
let dy = py[e.a] - py[e.b];
const len = Math.hypot(dx, dy);
if (len === 0) continue;
dx /= len; dy /= len; // 梯度 gradC,模长为 1
const C = len - e.rest; // 约束违反量
const lambda = -C / (w + alpha); // 拉格朗日乘子
px[e.a] += lambda * w0 * dx; // x += lambda * w * gradC
py[e.a] += lambda * w0 * dy;
px[e.b] -= lambda * w1 * dx;
py[e.b] -= lambda * w1 * dy;
}