{
  "lead": "用 Jos Stam 提出的「稳定流体」（Stable Fluids）算法实时求解不可压缩流体的 Navier-Stokes 方程。整个画面是一个 N×N 的网格，每个格子保存速度和染料浓度；每一帧先更新速度场，再让染料随速度场流动。这个算法最大的特点是无论时间步长多大都不会数值爆炸，因此常被用在游戏和交互特效里。",
  "formulas": [
    [
      "∂u/∂t = −(u·∇)u + ν∇²u − ∇p + f",
      "动量方程：平流 + 粘性扩散 − 压力梯度 + 外力"
    ],
    [
      "∇·u = 0",
      "不可压缩：速度场无散度"
    ]
  ],
  "terms": [
    [
      "扩散（Diffusion）",
      "粘度让速度在相邻格子之间逐渐均匀化，染料也会慢慢晕开。这里用隐式方法求解：解线性方程组 <code>(1 + 4a)·x − a·Σ邻居 = x₀</code>，采用 20 次 Gauss-Seidel 迭代。隐式方法对任意步长都稳定。"
    ],
    [
      "平流（Advection）",
      "每个格子的新值，是把这个格子「沿速度场倒着追溯 dt 时间」后，在出发点上双线性插值得到的旧值。这叫半拉格朗日法：只做插值、不做外推，数值永远不会超出原来的范围，所以不会爆炸。"
    ],
    [
      "投影（Projection）",
      "扩散和平流之后，速度场一般会有「源」和「汇」（散度不为零），这不符合不可压缩流体的性质。算法先解压力的泊松方程 <code>∇²p = ∇·u</code>，再从速度中减去压力梯度 <code>u ← u − ∇p</code>，得到无散度的速度场。这一步正是产生漩涡的关键。"
    ],
    [
      "边界条件",
      "网格外面有一圈边界格子。在垂直的墙上，水平速度取相反数；在水平的墙上，垂直速度取相反数，流体因此会被墙壁反弹，而不是穿墙而过。"
    ]
  ],
  "flow": [
    [
      "js",
      "JS 记录鼠标拖拽的方向和速度，调用 <code>fluid_add_velocity()</code> 和 <code>fluid_add_density()</code> 在拖拽位置注入力和染料；开启「自动喷射」时，底部两个喷口会持续注入。"
    ],
    [
      "rust",
      "每一帧调用 <code>fluid_step(dt, 粘度, 扩散, 消散)</code>：先处理速度（扩散 → 投影 → 平流 → 投影），再处理染料（扩散 → 平流），最后按消散系数衰减染料。"
    ],
    [
      "rust",
      "<code>fluid_render()</code> 把 N×N 的染料浓度映射成颜色（深色 → 蓝 → 橙 → 浅黄），返回 RGBA 字节数组。"
    ],
    [
      "js",
      "JS 把字节数组写入 N×N 的小 <code>ImageData</code>，再开启平滑缩放，绘制到 512×512 的画布上。"
    ]
  ],
  "perf": [
    "每一步要做 3 次线性求解（速度扩散 2 次，染料扩散 1 次），投影里还有 2 次，每次 20 轮迭代，128×128 网格一帧大约是 5 × 20 × 16,384 ≈ 164 万次格子更新。",
    "计算量与网格格子数成正比：把分辨率从 128 调到 192，格子数是原来的 2.25 倍，底部显示的求解耗时也会相应上升。",
    "所有场都用 <code>f32</code> 的一维数组存储，按行连续排列，内层循环顺序访问内存，对缓存友好。"
  ],
  "src": [
    [
      "crates/simulation/fluid/src/lib.rs",
      ""
    ],
    [
      "www/fluid/index.js",
      ""
    ]
  ]
}
