流场:让粒子自己成流
全课骨架:特效 = 一大堆各自独立的小东西,每一个只做三件事 —— 按自己的速度挪一步、
按自己的寿命决定还剩多少不透明、最后按「从远到近」的顺序盖到画面上。
第 1 课把这三件事讲完了,可它的粒子是"各飞各的":每颗粒子在出生时就领到一份自己的速度,
然后一直照着那份速度走。真实的烟不是这样 —— 它是被空气推着走的。
这一课就换掉"速度从哪来"这一件事:不再问"这颗粒子要往哪飞",而是问
"它现在所在的那个位置,空气往哪吹"。保真度:画面走 CPU 软光栅(示意 · 等价重现);
读数真机可证 —— 散度、Stokes 环量、ψ 的漂移、源汇、量阶都是本页真算出来的数:
散度在旋度场上是 逐字抵消的 0、ψ 沿流线守恒到积分器的阶、Σ curl·dA 与 ∮ v·dl
逐位相等。读者能拿笔复核。
这台设备拿不到 canvas 2D 上下文,活例跑不了。
本页的原理与数值表照常可读(画面只是把同一个数画出来)。
分段:
速度来源:
100 %
|
场:
2 px
|
1.0
耦合方式:
1.0
|
(脚本启动中…)
定点观察:先点「8.2 场不能有源汇」,再在「取旋度 / 直接用噪声」两档之间切 →
盯住画面上粒子的分布,以及下方读数里的「全场 max|div v|」 →
直接用噪声时粒子会被吸成几坨、旋度场下不会 —— 而这两档的速度大小是同一个量级(已归一)。
本帧的读数
(脚本启动中…)
8.1 速度从哪来:自带,还是每处空间给
本段只增加一样东西:速度的来源(其余控件与上一课一致:同一份积分、同一批戳记、同一个寿命)。
上一课的粒子在出生时就领到一份速度,之后大体照着它走 —— 整团看起来是"一堆各飞各的点"。
真实烟不是这样:它自己不会飞,是周围那口气在推它。所以这一段把"速度"换成一个
每处空间都给一份的函数 v(x, y):粒子只做一件事 —— 低头看看"我现在在的位置,风往哪吹"。
定点观察:把「速度来源」在两档之间切 → 看画面里粒子是朝各个方向散开、还是被同一只手推成一片 →
同一份引擎、同一份积分,只换了"速度从哪来",观感就从"喷泉"变成"烟"。
★ 注意:为了让这件事看得干净,本课把上一课的浮力与阻力关掉了 —— 自带档因此退化成直线。
表① 五个采样点上,势场 ψ 的四个倍频逐项摊开:ψ = Σk Ak·sin(akuk + φk)·cos(bkwk)
(每项都能拿笔算:两次三角函数、一次乘法)。末列是页内用中心差分算出的 |v|
| 点 (X, Y) | 第 1 倍频 | 第 2 倍频 | 第 3 倍频 | 第 4 倍频 | ψ = 和 | |v| (px/s) |
8.2 为什么"随便一个噪声场"会把粒子堆成一坨
本段只增加一样东西:场的无散性(直接拿噪声当速度 → 取它的旋度;场强、来源沿用上一段)。
上一段说"每处空间给一个速度",可它没说清这个速度该怎么给。最省事的给法是拿两路噪声当 (vₓ, v_y) ——
可那样的场里到处是"源"和"汇":粒子会被吸进汇里堆成一坨、从源里喷出来散掉。
要治它只要一件事:别直接给速度,给一个势场 ψ,再取它的旋度。
因为 ∂²ψ/∂x∂y − ∂²ψ/∂y∂x ≡ 0,旋度场的散度是逐字抵消的 0 —— 不是"很小",是恒等。
定点观察:把「取旋度 / 直接用噪声」切一下 → 先看画面上的灰色短线标记(汇 = 四条朝内的短线、
源 = 四条朝外的短线;切到旋度档它们就消失),再看读数里的「全场 max|div v|」与「材料面积比」 →
直接噪声档下粒子会被那些"汇"吸着走,一撮粒子占的面积会塌掉(180 步后中位 0.092 = 塌 91%);
旋度档下同一撮的面积稳稳不动(30 步中位 1.0005、四分位距 0.005)。
★ 两档的速度大小是同一个量级(已归一)—— 差别不是"谁快谁慢",是"有没有源汇"。
★ 一句如实说明:光看"粒子堆得紧不紧"其实是分不出来的(实测两档的填充率只差 1.15 倍),
这条判据必须靠读数,或者靠画面上那组源汇标记。ε 也有个甜点(见判据⑤)。
表② 散度对照:div v = ∂vₓ/∂x + ∂v_y/∂y,两侧都用同一个 ε 的中心差分。
旋度场那一列是逐字抵消的 0(剩下的是浮点加法顺序,1e−16 量级);直接噪声场那一列是 O(1)。
★ 单看第一个倍频还能写成闭式:div = −ab·cos(ax)sin(by) + ab·cos(ax)sin(by) ≡ 0(两项逐字抵消)
| 点 (X, Y) | 旋度场 div v | 直接噪声场 div v | 倍数 |
8.3 让场自己动起来:涡旋、卷曲与"看不见的手"
本段只增加一样东西:时间维(静态场 → 随时间演化的场;无散性、场强沿用上一段)。
上一段治好了源汇,可那个场是死的:粒子沿固定的等值线走,走完就没了 —— 看着像"几股固定的环流",
不像烟。真实的流里,涡自己会翻滚、变形、互相推挤。做法很省:让每个倍频的相位随时间走,
φk(t) = φk + ωk·t —— 场还是那个场,只是它也在动。
代价是:ψ 从此不再是运动的守恒量(它显含 t)—— 这一句留给 8.4 用。
定点观察:把「演化速度 ω」从 0 推上去 → 看涡旋开始自己滚动、粒子轨迹从闭合回路变成翻滚的卷 →
静态场只能让粒子沿固定等值线走;ω > 0 之后同一个点上的速度也会变,流才开始像活的。
表③ 最后一列是 ω = 0 的对照:那一列四行完全相同,这一列每行都不一样。
表③ 同一个点 P3 (1.60, 1.10) 在四档时间上的 ψ 与 v。第 3 列是本段(ω = 1.0),
第 4 列是 ω = 0 的对照(四行必须完全相同,它就是"静态场"的定义);v 用页内的中心差分算
| t (s) | ωt (rad) | ψ(ω = 1.0) | ψ(ω = 0,对照) | vₓ (px/s) | v_y (px/s) |
8.4 场是"速度"还是"力":两件很容易混的事
本段只增加一样东西:耦合方式(v ← 场 直接设定 / v ← v + 场·g·dt;其余控件沿用上一段)。
前三段都把"场 = 速度"当成天经地义 —— 好像风一吹,粒子的速度立刻就变成风速。可空气不是这么干的:
它推粒子,粒子有惯性,加速需要时间。这两条实现看起来只差一行:
p.vx = v[0] 与 p.vx += v[0]*g*dt,但它们的几何后果完全不同 ——
这件事有一个非常干净的量能分开它们:ψ 沿轨迹的漂移。
定点观察:把「场 = 速度 / 场 = 力」切一下 → 盯住读数里的
「ψ 的相对漂移」与画面里粒子的轨迹形状 → 把场当速度用时粒子严格贴着 ψ 的等值线走
(漂移是积分器的阶,不是模型的错);当力用时它会滞后、甩出等值线。
★ 这条只在静态场(ω = 0)下成立 —— 场自己会动时 ψ 显含 t,它就不再是守恒量(这是 8.3 留下的账)。
表④ 同一颗粒子(起手速度 0、静态场 ω = 0、ε = 2 px)在两种耦合下走 60 / 120 / 240 步:
位置、ψ、以及 ψ 的相对漂移。场 = 速度那一列是判据(漂移 1e−4 量级,且它随 差分步长 ε 按 ε² 缩,
见判据⑤ —— 不是随 h 缩);场 = 力那一列是对照物(1.6e−1,差三个数量级,因为它换了物理)
| 耦合 | 步数 | x (px) | y (px) | ψ | |Δψ| / |ψ₀| |
这一课的位置
四段只做了一件事:把"速度从哪来"换掉 —— 自带 → 每处空间给 → 给一个无散场 → 让场自己动 →
再把"场 = 速度 / 场 = 力"分开。粒子引擎、戳记形状、排序与混合全部沿用第 1 课,一行没改。
本页那一格"如实说明":画面是 CPU 软光栅逐点采样、逐层混合写进帧缓冲的(示意 · 等价重现);
真机上流场通常在 compute 里更新、粒子是实例化点精灵,本页不声称复现那两条内部路径。
本课题还没做的三课(留在 stub,不在站首页占行):软粒子(粒子不能有硬边)、
后处理链(bloom:光为什么要溢出来)、屏幕空间(SSAO 与 SSR)。
本页用到的做法,出处在哪
- 旋度噪声(无散流场) —— 原始论文是 Bridson, Hourihan, Nordenstam,
Curl-Noise for Procedural Fluid Flow, SIGGRAPH 2007:
cs.ubc.ca/~rbridson/docs/bridson-siggraph2007-curlnoise.pdf。
论文的要点正是本页 8.2 段讲的那一条:对势场取旋度 ⇒ 恒无散("no sources or sinks are possible")。
- 引擎里的对应模块 —— Unreal 的 Curl Noise Force(旋度噪声力)与 Vector Noise Force:
Particle Update Group reference。
前者注入的就是"无散的速度场",后者是"普通噪声" —— 两个模块名恰好就是本页 8.2 的 A/B。
- 粒子形状与旋转 —— 直接沿用第 1 课那套(确定性哈希戳记 = 内置 flipbook):
Unreal 的 Sprite Smoke 教程
的 Sub UV + Sprite Rotation 两件套。本课不重造。
- 本页故意没做的:软粒子(深度淡出)留给第 3 课 —— 它要一张场景深度,而本页没有场景几何;
体积/网格流体(真解 N-S 方程)不在本课题范围内,本页只做"给定速度场"这一步。