首页 › programming › 倒着追才不爆:解码烟雾模拟的流体算法

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

导语

如果你看过任何一款带烟雾、爆炸、水流的 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):

$$\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 年那篇论文真正的封神点。所谓"平流",就是"速度场如何搬运自己"。

为什么向后追就稳?因为你倒退到的那个位置,一定落在已知数据的范围里——你在"过去"里取值,过去是确定的、已经算好的。向前推是在预测未知的未来(容易翻车),向后追是在回溯已知的过去(永远安全)。这就是"无条件稳定"的本质。代价是引入数值扩散(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)最典型的写法,学会它你能套用到任意网格类数值计算。

知识结构图

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 — 本次深度解读的核心视频
  2. Jos Stam《Stable Fluids》(1999, SIGGRAPH) — 半拉格朗日平流与压力投影的原始论文
  3. Jos Stam《Real-Time Fluid Dynamics for Games》(2003) — 作者的简化版实时实现教程
  4. Mike Ash — Fluid Simulation for Dummies — 把 Stam 论文逐行落成代码的经典博客
  5. PavelDoGreat/WebGL-Fluid-Simulation (GitHub) — 可直接在浏览器运行的流体模拟开源实现

本文由「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学通信知识星球海报-扫码加入