<!-- title: 倒着追才不爆：解码烟雾模拟的流体算法 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=Q78wvrQ9xsU -->

# 倒着追才不爆：解码烟雾模拟的流体算法

## 导语

如果你看过任何一款带烟雾、爆炸、水流的 3A 游戏或视觉特效，背后几乎都站着同一个人写的一套数学：流体力学。问题是，流体方程（Navier-Stokes）是出了名的"难算还爱爆"——时间步长稍微大一点，整个速度场就数值发散，画面瞬间糊成一团马赛克。1999 年，一位叫 **Jos Stam** 的研究者用一个反直觉的诀窍终结了这场噩梦：**不要顺着流体的方向把物质"推"过去，而是从目标格子"倒着追"回它的源头去采样**。这一招让流体模拟第一次做到"无条件稳定"——你可以塞进任意大的时间步，它都不会炸。

YouTube 创作者 **Sebastian Lague** 的《Coding Adventure: Simulating Smoke》把这个听起来高深的话题做成了 41 分钟的可视化编程冒险：他用 GPU compute shader 从零实现了一套实时烟雾模拟，你能眼睁睁看着数学公式变成翻滚的烟柱。这条视频拿下 62 万播放，频道订阅 300 万+，是目前把"Stable Fluids 算法"讲得最直观的一期。它真正解决的问题是：**让一个普通程序员，在没有数学博士学位的前提下，也能跑通一套工业级的流体求解器**。今天我们就把它拆到底。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | Coding Adventure: Simulating Smoke |
| 频道 | Sebastian Lague（约 300 万订阅） |
| 时长 | 41 分 40 秒 |
| 播放量 | 约 62 万（截至抓取时 620,125） |
| 发布 | 约 8 个月前（2025 年 10 月前后） |
| 核心主题 | 用 GPU 实时模拟烟雾——基于 Jos Stam 的 Stable Fluids 算法 |
| 技术栈 | Unity + Compute Shader + 体积渲染 |
| 理论基石 | Jos Stam《Stable Fluids》(1999) 与《Real-Time Fluid Dynamics for Games》(2003) |

## 核心内容拆解

### 要点一：先把流体"钉"在网格上——Eulerian 思维

视频开篇解决一个根本选择：**怎么表示一团流动的流体**？两条路。一条是 Lagrangian（拉格朗日）——让一堆粒子跟着流体走，粒子在哪流体就在哪（SPH 粒子法就是这类）。另一条是 Eulerian（欧拉）——把一张固定的网格铺满空间，流体从格子里穿过去，我们只记录每个格子当前的流速、密度。

Lague 选择的是后者。**为什么烟雾偏爱网格**？因为烟雾的核心难题是"压力"——烟雾不可压缩，流速必须处处满足"进多少出多少"（散度为零）。在固定网格上，"这个格子的左右邻居是否平衡"是个局部、好算的约束；而在飘忽的粒子上，你要先找到谁是邻居，再做约束，复杂度爆炸。网格法把连续的物理世界离散成一张二维的速度表 `u(x,y)` 和一张密度表 `density(x,y)`，存在 GPU 纹理里，每个像素就是一个采样点。这是后面一切的地基。

> 这是视频原话层面的选择；我的延伸是：这种"把世界钉死在格子上"的取舍，其实是几乎所有实时流体方案的共同基因——游戏里的水面、特效里的火焰、甚至 NVIDIA FleX 的早期实现，都从 Eulerian 网格出发。

### 要点二：Navier-Stokes 被拆成流水线——算子分裂

真正的流体方程长这样（不可压缩 Navier-Stokes）：

$$\frac{\partial \mathbf{u}}{\partial t} = -(\mathbf{u}\cdot\nabla)\mathbf{u} - \frac{1}{\rho}\nabla p + \nu\nabla^2\mathbf{u} + \mathbf{f}, \quad \nabla\cdot\mathbf{u}=0$$

直接解这个偏微分方程对实时渲染是灾难。Stam 的工程智慧是**算子分裂（operator splitting）**：别一口气解，把它拆成一串小步骤，每步用最合适的方法处理一个物理项。于是原本吓人的方程，变成了一条流水线：

1. **施力（Force）**——鼠标拖拽、浮力往速度场里注入动量；
2. **扩散（Diffuse）**——黏性让速度向邻居渗透；
3. **投影（Project）**——强制流速"进多少出多少"；
4. **平流（Advect）**——速度场带着自己往前走；
5. 再投影一次。

每一步都是对整张网格的一次 shader pass。这种"分而治之"是把学术数学塞进游戏引擎的标准姿势——视频里你能看到每跑一遍 pipeline，画面就演进一帧。

### 要点三：**倒着追**——整部视频的灵魂（Semi-Lagrangian Advection）

这是本文标题的由来，也是 Stam 1999 年那篇论文真正的封神点。所谓"平流"，就是"速度场如何搬运自己"。

- **朴素做法（向前推）**：从当前格子出发，顺着速度把物质推到下一个格子。听起来自然，但有个致命的 **CFL 条件**——时间步必须小于"一个格子大小除以最大流速"。流速一大或步长一长，一个格子的物质会被推到老远，采样落空，数值瞬间发散，画面"炸了"。
- **Stam 的做法（向后追）**：反过来。想求"下一个时刻格子 A 的速度"，不要从 A 往外推，而是**沿着速度的反方向倒退一步**，找到一个"上游位置 B"，然后把 B 处当前的速度采样回来，赋给 A。

为什么向后追就稳？因为你倒退到的那个位置，一定落在已知数据的范围里——你在"过去"里取值，过去是确定的、已经算好的。**向前推是在预测未知的未来（容易翻车），向后追是在回溯已知的过去（永远安全）**。这就是"无条件稳定"的本质。代价是引入**数值扩散（numerical diffusion）**——流体能量被算法一点点"抹平"，细节会变糊。这是一个工程权衡：用精度换稳定性，换来"可以塞进任意大时间步还实时跑"。

> 视频会直观演示：把时间步开到很大，朴素方法直接花屏崩溃，而向后追法依然稳如老狗地翻滚。这是我强烈建议你亲自看的一分钟。

### 要点四：让烟雾"进多少出多少"——压力投影（Projection）

烟雾（空气）几乎不可压缩，物理上要求每个格子的散度为零：$\nabla\cdot\mathbf{u}=0$。但前面几步算完，速度场早就不满足这个约束了。怎么办？

**投影**分三步：
1. 算出每个格子的散度 $d=\nabla\cdot\mathbf{u}$（净流入/流出量）；
2. 解一个**泊松方程** $\nabla^2 p = d$，求出每个格子的"压力" $p$——直观看，散度为正的地方压力高，需要被压回去；
3. 把压力的梯度从速度里减掉：$\mathbf{u} \leftarrow \mathbf{u} - \nabla p$。减完之后，散度就被"挤"回零，流速重新满足不可压缩条件。

难点在第 2 步——解泊松方程没有解析解，只能迭代逼近。Stam 用的是最朴素的 **Jacobi 迭代**：反复用邻居的平均值更新自己，跑 **20 到 40 次**。在 GPU 上，这就是"把同一个 shader pass 反复执行几十遍"。这也是整个算法最吃算力的地方。Lague 在视频里调试迭代次数的片段，正好说明了"精度 vs 帧率"的现实拉锯。

### 要点五：染料与速度是两套场——被动平流

很多人第一反应是"算出速度不就有烟雾了吗"。**没有**。速度场只描述空气往哪流，而"你看得见的烟"是一团被动的染料/密度。它们是**两张独立的表**：染料自己没有动量，它纯粹被速度场"搬运"。

所以算法对染料做的是轻量版的同一套：扩散 + 平流（同样用倒着追）。鼠标划过时，既往速度场注入方向（产生气流），又往密度场注入颜色（产生可见烟）。这一刀切得极妙：**物理（速度）和视觉（染料）解耦**，让你可以用一套流体引擎渲染烟雾、墨水、彩色漩涡——只要换染料颜色，运动规律不变。这是为什么 Stam 的框架成了流体特效的通用底座。

### 要点六：找回被算法吃掉的涡旋——涡度增强（Vorticity Confinement）

倒着追虽然稳，但代价是数值扩散会**抹掉细小的漩涡**，烟雾看起来发"软"、不够有动感。补救手段叫**涡度增强**（Vorticity Confinement），由 Fedkiw 等人在 2001 年提出，视频后半段专门做了对比。

原理是：先算每个格子的**旋度（vorticity = ∇×u）**，它衡量局部旋转的强度；然后沿着旋度的梯度方向，人为加一个**与流动垂直的力**，把那些快要被抹平的涡旋重新"撑"起来。打个比方，数值扩散像给烟雾打了柔光滤镜，而涡度增强是给细节做锐化，把卷曲的纹理加回来。开关一开，烟雾立刻有了那种标志性的、富有生命力的翻滚感。这是把"稳定但糊"和"生动但炸"之间，找到的精妙平衡点。

### 要点七：搬到 GPU——纹理与乒乓缓冲

数学讲完了，落地才是程序员的活。Lague 把整张网格存成 GPU 纹理，用 **compute shader** 并行处理——每个线程负责一个格子。所有"读旧值、写新值"的步骤都要面对一个问题：**你不能边读边改同一张表**。解法是**乒乓缓冲（ping-pong / double buffering）**：维护 A、B 两张纹理，这一帧从 A 读、写到 B，下一帧反过来。几十次 Jacobi 迭代就是几十轮 A↔B 交换。染料最后通过**体积渲染（volumetric rendering）**投射成有厚度的烟雾画面，而非一张扁平贴图。这套"纹理即数据、shader 即算子"的范式，正是 GPU 通用计算（GPGPU）最典型的写法，学会它你能套用到任意网格类数值计算。

## 知识结构图

```mermaid
flowchart TD
    INIT["初始化<br/>速度场 u 与染料场 ρ<br/>存为 GPU 纹理"] --> FORCE["① 施力 Force<br/>鼠标/浮力注入速度与染料"]
    FORCE --> DIFF["② 扩散 Diffuse<br/>隐式求解 扩散方程"]
    DIFF --> P1["③ 投影 Project<br/>散度 d=∇·u →<br/>泊松 ∇²p=d → Jacobi 迭代<br/>u = u − ∇p → 无散度"]
    P1 --> ADV{"④ 平流 Advect<br/>半拉格朗日"}
    ADV -->|向前推| BL["CFL 限制 → 易爆裂"]
    ADV -->|向后追| OK["追溯源头采样<br/>无条件稳定<br/>代价: 数值扩散"]
    OK --> P2["⑤ 再次投影<br/>确保 ∇·u = 0"]
    P2 --> DYE["染料场: 同样扩散+平流"]
    P2 --> VOR["⑥ 涡度增强<br/>找回被抹平的涡旋"]
    DYE --> RENDER["体积渲染<br/>输出烟雾画面"]
    VOR --> RENDER
    RENDER --> NEXT["下一帧 ↔ 乒乓缓冲"]
    NEXT -.-> FORCE
    style ADV fill:#ffe4b5
    style OK fill:#d4edda
    style BL fill:#f8d7da
```

## 实战启示

**给游戏/图形程序员的行动指南。** 首先，Stam 的这套实现代码量惊人地小——核心求解器可能只有几百行。你完全可以在一个周末用 WebGL shader 或 Unity compute shader 跑通一个 2D 版本（GitHub 上 `WebGL-Fluid-Simulation` 这类仓库就是活教材）。从那里出发，你能立刻把它接到任意视觉项目里：水墨效果、能量场、魔法烟雾、动态背景。**别被 Navier-Stodes 吓退——算子分裂已经把巨兽拆成了你可以逐个击破的小函数**。

**给所有程序员的认知升级。** 这期视频最值得带走的，是"倒着追"背后的思维方式：**当一个正向过程（向前推）不稳定、易发散时，逆向建模（向后追、在已知域里取值）往往能让它无条件收敛**。这种思路在编程里到处都是：渲染里的反向光线追踪、强化学习里从目标倒推的策略梯度、甚至编译器里从语法树叶子向上归约。**遇到"一步推太远就崩"的系统，先问自己：能不能从终点倒着采样？**

**做技术决策时记住这个权衡公式**：稳定性往往要靠牺牲精度（数值扩散）来换，而精度（保留细节）往往要靠追加开销（涡度增强、更多 Jacobi 迭代）来赎回。**没有免费的午餐，只有清晰的取舍**。能把这个权衡讲清楚、并给出参数旋钮（迭代次数、时间步、涡度强度）的工程师，就是团队里写流体/数值代码最靠谱的那个。

**最后，重视可解释的可视化。** Lague 之所以能把抽象方程讲得人人都懂，靠的是每一步都配实时画面。你下次写复杂的数值算法或物理逻辑时，**先花一小时把中间状态可视化出来**——一个会动的调试视图，胜过一千行日志。

## 延伸学习路径

1. **Jos Stam《Stable Fluids》(1999, SIGGRAPH)** —— 算法的原始论文，倒着追与投影的数学源头。建议先读。
2. **Jos Stam《Real-Time Fluid Dynamics for Games》(2003, GDC)** —— 作者自己写的简化版教程，代码量极小，是绝大多数实时实现的直接蓝本，初学者从这里入手最快。
3. **Mike Ash《Fluid Simulation for Dummies》** —— 把 Stam 2003 论文逐行翻译成 C++ 的传奇博客，公认的"从零写流体"第一站。
4. **Pavel Dobryakov WebGL-Fluid-Simulation（GitHub）** —— 浏览器里直接跑的开源实现，代码清晰、效果惊艳，照着改一遍胜过读十遍论文。
5. **Sebastian Lague 频道其他《Coding Adventure》** —— 同系列的"蚂蚁模拟""光线追踪""音频分析"，都是把硬核数学变成可视化冒险的范本，能学到"如何把复杂系统讲清楚"这件事本身。

## 一句话总结

**它把流体模拟最难、最吓人的一步——保证不爆炸——拆成了"倒着追"这一个反直觉却无条件稳定的诀窍，让你不用数学博士也能跑通一套工业级实时烟雾求解器。**

## 参考来源

1. [Coding Adventure: Simulating Smoke — Sebastian Lague](https://www.youtube.com/watch?v=Q78wvrQ9xsU) — 本次深度解读的核心视频
2. [Jos Stam《Stable Fluids》(1999, SIGGRAPH)](https://www.dgp.toronto.edu/public_user/stam/reality/Research/pdf/stable_fluids.pdf) — 半拉格朗日平流与压力投影的原始论文
3. [Jos Stam《Real-Time Fluid Dynamics for Games》(2003)](https://www.dgp.toronto.edu/public_user/stam/reality/Research/pdf/GDC03.pdf) — 作者的简化版实时实现教程
4. [Mike Ash — Fluid Simulation for Dummies](https://mikeash.com/pyblog/fluid-simulation-for-dummies.html) — 把 Stam 论文逐行落成代码的经典博客
5. [PavelDoGreat/WebGL-Fluid-Simulation (GitHub)](https://github.com/PavelDoGreat/WebGL-Fluid-Simulation) — 可直接在浏览器运行的流体模拟开源实现

---
> 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵**爱卫生**（微信ID: gprshome201101）维护，聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。
> 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
