流体仿真实战
流体仿真实战
时间:2026/04/09
关键词:Eulerian grid、advection、diffusion、projection、pressure solve、ping-pong、texture object、CUDA
核心目标:从工程角度理解一个实时流体模拟在 GPU 上通常由哪些步骤组成,以及每一步为什么这样设计。
1. 先建立问题模型
实时流体仿真通常不是在“跟踪每一个分子”,而是在求解离散化后的场:
- 速度场
u(x, y[, z]) - 压力场
p - 密度/烟雾浓度
d - 温度、外力等附加场
在图形学实时仿真里,最常见的是 Eulerian 网格法:
- 空间被划分成规则网格
- 每个网格单元保存局部状态
- 关注的是“这个位置上的流体状态”,而不是“某个粒子去哪了”
2. 一帧流体模拟通常做什么
一个经典的 2D/3D 实时烟雾模拟步骤,通常长这样:
- 对速度场做平流(advection)
- 对密度/温度做平流
- 加入外力(force / impulse)
- 计算散度(divergence)
- 解压强泊松方程(pressure solve)
- 用压力梯度做投影(projection)
- 得到无散速度场
- 渲染密度或其他可视化结果
这条管线里最关键的思想是:
- 平流负责“把东西搬着走”
- 压力投影负责“让速度场满足不可压缩约束”
3. 为什么很多实时流体用 Stable Fluids
Jos Stam 的 Stable Fluids 思想之所以经典,是因为它强调:
- 数值上更稳定
- 能容忍较大的时间步长
- 非常适合图形实时场景
它的代价通常是:
- 会更粘、更耗散
- 细节可能被抹平
所以工程上通常要在:
- 稳定性
- 速度
- 视觉效果
之间做平衡。
4. 网格、边界与数据布局
4.1 最常见的数据
二维时经常有:
vel_xvel_ypressuredivergencedensity
4.2 常见存储方式
对 GPU 更友好的方式通常是:
- 扁平连续数组
- 纹理/表面对象
- ping-pong 双缓冲
例如二维数组常展平成:
1 | idx = y * width + x; |
4.3 为什么常用 SoA 思路
不同字段通常分开存:
- 一个数组/纹理存
vel_x - 一个数组/纹理存
vel_y - 一个数组/纹理存
pressure
这比把所有字段塞进一个大结构更容易:
- 控制带宽
- 做批量 kernel
- 针对单一字段优化访问
5. 平流(Advection)是什么
直觉上,平流就是:
按速度场把某个量“搬运”到下一个时刻。
对于密度场来说,可以理解成:
- 烟雾随着流速移动
对于速度场本身来说:
- 速度也会被自身流动搬运
5.1 半拉格朗日(Semi-Lagrangian)平流
实时图形里很常见的做法是“反向追踪”:
- 对当前网格点
(x, y) - 沿速度反方向回溯到上一个时刻的位置
- 从旧场中插值取样
公式直觉:
1 | phi_new(x) = phi_old(x - dt * u(x)) |
5.2 为什么它适合 GPU
因为每个网格点都能独立算:
- 回溯位置
- 插值取样
- 写回新值
这非常适合 data-parallel kernel。
6. 为什么纹理对象在流体里常见
流体平流特别需要“按浮点坐标插值采样旧场”。
这正是纹理对象很擅长的事情。
6.1 纹理对象的实际优势
- 支持硬件插值
- 支持边界寻址模式
- 访问接口清晰
- 对二维/三维场很自然
因此在 CUDA 流体项目里,经常会看到:
- 旧场绑定成 texture
- 新场写到 output buffer / surface
6.2 一个常见工程模式
src作为只读纹理dst作为可写 buffer- kernel 结束后交换
src/dst
这就是典型的 ping-pong。
7. Ping-Pong 双缓冲为什么几乎是标配
平流、扩散、压力迭代这类步骤,通常不能一边读旧值一边原地覆盖写新值,否则会污染后续计算。
最常见做法:
A存旧场B存新场- 一步计算后交换
A/B
例如:
1 | advect: vel0 -> vel1 |
双缓冲的代价是多占一份显存,但好处是:
- 读写依赖清晰
- kernel 更容易写对
- 更适合并行
8. 散度(Divergence)为什么要算
不可压缩流体通常要求:
1 | div(u) = 0 |
但经过加力、平流等步骤后,速度场往往不再满足这个条件。
所以需要先计算散度,看看当前速度场“有多不守恒”。
二维中心差分的直觉形式:
1 | div = (u_right - u_left + v_up - v_down) * 0.5 / h |
算出来的散度场会成为后面压力方程的右端项。
9. 压力求解(Pressure Solve)是核心瓶颈之一
为了让速度场重新无散,通常要解一个泊松方程:
1 | ∇²p = div |
这里:
p是压力div是上一阶段算出的散度
9.1 为什么这是难点
因为它不是一个局部一步完成的更新,而是一个全局耦合问题。
实时实现中常用近似迭代法,例如:
- Jacobi
- Gauss-Seidel
- Red-Black Gauss-Seidel
- Multigrid(更复杂也更强)
9.2 为什么很多教程先用 Jacobi
因为它简单、规则、容易并行:
- 每次迭代只读旧压力
- 写新压力
- 适合 GPU 大规模并行
代价是收敛速度不算快。
10. 投影(Projection)在做什么
解出压力场后,用压力梯度修正速度:
1 | u' = u - ∇p |
直觉上就是:
- 从原速度里减掉“造成体积膨胀/压缩”的那部分
投影之后,速度场更接近无散状态,也就更符合不可压缩流体假设。
这一步通常是:
- 读取速度
- 读取相邻压力
- 做梯度差分
- 写回新速度
11. 边界条件不能忽略
很多新手代码“能跑但看起来不对”,问题往往在边界。
常见边界处理包括:
- 固壁边界
- 开放边界
- 周期边界
- 障碍物边界
例如固壁边界时,速度在法向方向通常要满足不穿透约束。
如果边界没处理好,可能会出现:
- 烟雾穿墙
- 边缘数值爆炸
- 压力解不稳定
12. CUDA 上通常如何拆 kernel
一个典型实时流体项目不会把全部逻辑塞进一个 kernel,而是拆成多步:
advect_velocityadvect_densityadd_forcecompute_divergencejacobi_pressureproject_velocityrender_density
这样做的好处:
- 每步逻辑清晰
- 便于调试
- 便于替换某一步算法
代价是:
- kernel launch 次数变多
- 中间结果要反复读写全局内存
所以工程优化通常是在“模块清晰”和“减少中间带宽”之间折中。
13. 一个典型的 CUDA 数据流
可以把一帧想成这样:
1 | velocity_src --advect--> velocity_dst |
其中压力求解阶段往往会迭代很多轮,是核心带宽热点之一。
14. 纹理对象在这里最常见的落点
14.1 平流时
因为需要按浮点坐标回溯并插值取样旧场。
14.2 可视化时
例如把密度场映射到图像。
14.3 有时不用纹理也能做
完全可以手写双线性插值,但纹理对象通常更方便,也更贴近 GPU 图像管线思维。
15. 性能瓶颈通常在哪
15.1 不一定是算术,而是带宽
流体很多步骤都是 stencil / 邻域访问:
- 读周围多个点
- 写一个点
因此常见瓶颈是:
- 全局内存带宽
- 多次中间场读写
15.2 压力迭代轮数
Jacobi 每多迭代一轮,都意味着:
- 再扫一遍整个压力场
所以视觉质量和帧率经常直接在这里博弈。
15.3 边界和条件判断
边界格子处理如果分支太多,也会影响 warp 执行效率。
16. 常见优化方向
16.1 减少全局内存往返
例如:
- 更少的中间缓冲
- 更合理的 kernel 融合
16.2 用 shared memory 做局部邻域缓存
对于 stencil 类计算,局部 tile 常能减少重复全局加载。
16.3 让访问尽量规则
例如:
- 扁平数组
- 合理线程块布局
- 连续内存访问
16.4 区分“视觉上够用”和“数值上更准”
实时图形里很多时候不是求学术最优,而是:
- 足够稳定
- 足够快
- 视觉上自然
17. 为什么这类项目特别适合做综合练习
流体仿真把很多高性能主题都串起来了:
- 数据布局
- 邻域访存
- ping-pong 双缓冲
- GPU kernel 设计
- 纹理对象
- 迭代求解器
- 可视化与物理折中
所以它既是一个图形学题目,也是一个很典型的并行计算工程题。
18. 初学实现建议
如果你第一次做,比较稳的路线是:
- 先做 2D,不要一开始上 3D
- 先做密度平流,再加速度场
- 再补散度、压力、投影
- 压力求解先用 Jacobi 跑通
- 最后再考虑纹理对象、shared memory、性能优化
因为最难的不是“写出一个 kernel”,而是:
- 整条数据流逻辑正确
- 每步边界一致
- 数值上稳定
19. 一页总结
实时流体仿真的工程骨架,可以压缩成下面几句:
- 用规则网格存速度、压力和密度场
- 用平流把量沿速度场搬运
- 用散度和压力解修正速度场
- 用投影得到近似不可压缩流体
- 用 ping-pong 双缓冲保证每一步读写分离
- 用纹理对象和连续布局提高 GPU 访问效率
如果只记一个最核心的工程关系,那就是:
平流负责“搬”,压力投影负责“稳”。
20. 建议继续补充的相关主题
和本篇衔接最紧密的内容:
- CUDA texture / surface object 细节
- Staggered grid(MAC grid)
- Jacobi / Gauss-Seidel / Multigrid 对比
- Vorticity confinement 与视觉细节增强
- 3D 烟雾与体渲染
21. 参考资料
Jos Stam, Stable Fluids
https://www.dgp.toronto.edu/public_user/stam/reality/Research/pdf/GDC03.pdfGPU Gems: Fast Fluid Dynamics Simulation on the GPU
https://developer.nvidia.com/gpugems/gpugems/part-vi-beyond-triangles/chapter-38-fast-fluid-dynamics-simulation-gpu