以空间殖民算法驱动 Pyro:循环爆发烟雾球的制作解析
引言
我是一名常驻纽伦堡的3D与动态设计师,为产品展示、广告和品牌传播创作视觉与动画。凭借美术、视觉传达和平面设计的背景,我将艺术方法与技术实验相结合。
几年前,我的Instagram账号被黑客入侵,最终被封禁。有一段时间,我以为自己再也不会创建新账号了。如果多年积累的东西能在一瞬间消失,那为什么还要重新开始?但我又在骗谁呢?我仍然想看看其他创意人在做什么,并分享我自己的作品。
对于回归后的第一条帖子,我想做一些在视觉上与重生–一个全新的开始–相呼应的东西。我不确定这个想法是在我开始之前就有的,还是在过程中浮现的。有时,潜意识会占据主导。
结果是一个类似循环系统的分支结构。它输送的不是血液,而是某种无形且短暂的东西:创造力的力量。
空间殖民求解器的步骤与要素
以下是我让这个效果生动起来所需的内容:
• 分支路径
• 从路径方向推导的速度
• 来自选定发射点的密度
• 使用 Pyro 进行烟雾模拟
• 在 Solaris 和 Karma 中渲染
分支网络
这个设置中的技术部分灵感来自 Entagma 的 VEX in Houdini:太空殖民(Space Colonization)教程,以及 2007 年由 Adam Runions、Brendan Lane 和 Przemyslaw Prusinkiewicz 撰写的论文 用太空殖民算法建模树木。我的实现改编了这一思路:连接已有的点,而不是根据平均吸引方向生成新的生长位置。
首先,我们需要选择分支网络的形状。我使用 Labs Cylinder 几何节点创建了一个简单的圆柱体,并用 Points from Volume 几何节点将其填充。点间距控制着分支能够生长得多密集。
接下来是有意思的部分。我们选择一个或多个点作为起始组,然后重复三个步骤:
- 搜索:活跃点在固定半径内寻找未使用的邻居。
- 连接:每个邻居选择提出连接请求的最近父点。一条线将这一对连接起来。
- 推进:新连接的点成为活跃点;上一代停止搜索。
每个子点只有一个父点,而一个父点可以有多个子点。组会跟踪未使用、活跃和已选中的点;候选父点列表用于解决相互竞争的连接。当不再有可达的未使用点时,生长便会停止。

绿色 – 起点。橙色 – 活动点及其搜索半径。红色 – 此前已连接的点。白色 – 未使用的点。
控制分支
改变搜索距离和候选限制会产生不同的结构。更大的半径允许更长的连接;候选限制则控制每个活跃点可以提出多少个邻居。
以下是相同点布局下分支规则的四种变体。

实现
该循环使用四个 wrangle:INIT、find_neighbours、connect_logic 和 set_groups。INIT 运行一次;其余三个重复执行。
在这里你可以看到,INIT 将起始点标记为活动状态,其余点标记为空闲状态。每个点开始时拥有自己的 __id;后续连接的子级会继承其父级的 ID。

搜索 & 连接
如下面的截图所示,find_neighbours 记录候选父节点并将其建议的邻居标记为选中。此版本请求 Pts Max + 1 个候选,因此显示值为 3 时最多允许四个。

在下面的截图中,你可以看到 connect_logic 选择最近的候选父级,继承其 __id,并创建连接段。

推进与重复
在这里,最后一个 wrangle 节点 set_groups 会遍历 __infected 和 __selected。它让当前的活动点退役,清空它们的父列表,并使选中的点在下一轮中成为活动点。

应用于圆柱体内的点,这会从每个起始点产生扩散的簇。通过 __id 着色可以让我们区分各个簇并分离出单个簇。
下面,你可以看到完整的网络和一个孤立的簇。每个簇继承其起始点的 ID。

结构准备就绪后,Edge Transport 几何节点会测量从起始组沿各分支的距离。我将其存储为 dist,并启用了全局归一化。这样一来,我们就能区分距离根部较近的点与沿分支更远的点。
下图是用颜色可视化的 dist 属性。

准备路径
在这个阶段,两个相连点之间的每一段都是一个独立的图元。PolyPath 将这些段连接成连续的路径。我对它们重新采样,熔合重合点,使用 Group Transfer 恢复起始组,并平滑网络以获得更有机的形状。
处理链以 direction_calc 结束,它写入速度属性,如下所示。

比较相邻的 dist 值可确定向外的方向。在 direction_calc 中,我将指向距离更大的邻居的向量归一化,并将其写入该邻居的 v 属性。
Edge Transport 提供 dist;wrangle 将顺序转换为向外的方向向量,如下图所示。

网络的变体
为了省心,我把它打包进了一个 HDA,并加入了一些额外的控件。分支逻辑是一样的;这个资产只是让下一步更易于管理。
我把 Space Colonization 放进了另一个循环中,每次迭代都选择一个不同的随机起始组。内层循环生长出一个网络。这个外层循环会创建十个变体,每个都单独打包,以便之后选择。
外层循环生成并打包十个网络。

在接下来的两张图片中,你可以看到顶部有十个重叠的网络,底部则是沿 X 轴偏移的网络,以便进行比较。


在动画过程中,我一次选择一个网络。在不同变体之间切换会改变提供给烟雾的方向,使运动更具动态感、更少重复。
Blast 几何节点中的这个表达式会以递增的速率在不同网络之间切换。关键帧本可以对时间提供更多控制,但这个表达式对这件作品来说效果很好。


我将选定的网络解包,并从起始组中在分支簇的中心处选择随机点。这些是主要的发射器:烟雾可以从它们那里沿着分支向各个方向扩散。网络中的几个额外点会产生较小的爆发。它们的随机种子每两帧改变一次。

我将小球复制到这些点上,然后用“从体积生成点”(Points from Volume)填充它们,以便为每个发射器提供一定的体积。
这些是为烟雾模拟提供输入的动画发射点。

从路径到体积场
我使用 Pyro Source 为发射点添加密度和温度,将 Particle Separation 链接到 Points from Volume 中的同一参数。经过反复试验,我为这个设置选定了 Density Scale 为 10。

Volume Rasterize Attributes 将点属性中的密度和温度转换为体积场。下面的设置定义了它们的分辨率以及每个点的覆盖范围。

对于速度,我将另一个 Volume Rasterize Attributes 节点直接连接到解包后的路径,并将 Attributes 设置为 v。这会沿着整个网络栅格化方向向量。

我将密度、温度和 v 合并,然后将结果连接到 Pyro Solver 的第一个输入。
为了容纳烟雾,我将原始圆柱体加厚成壳,并用 VDB from Polygons 将其转换为有符号距离场。我将该场命名为 collision,并连接到 Pyro 的第二个输入。VDB from Polygons 中的 Preserve Holes 保持中空内部开放。碰撞体使用较大的体素尺寸,因为它实际上不需要太多细节。

模拟与缓存烟雾
性能
我使用了 Minimal OpenCL 解算器来利用我的 GPU。它适合这个设置,因为烟雾保持在固定的容器内。

动画源需要一些准备时间:Minimal OpenCL 在模拟前会缓存源帧范围。在这种情况下,更快的模拟值得那最初的等待。
碰撞 VDB 是静态的,所以我将 Collision Frame Range 设置为 Static Frame。下面的 Collision SDF 参数标识了解算器读取的场。

速度源(Velocity sourcing)在这里影响最大。使用 Add 时,传入的速度会与现有流场累积叠加,分支就会变得不那么清晰。Copy 会在每次更新时替换源区域内的速度,不断强化这些路径。Turbulence 仍然作用于模拟,但流场一旦离开这些区域,就可以自由偏离。
我将 density 和 temperature 保留为默认的源操作,并禁用 Flame。源的 Frame Range 覆盖整个动画。
下面你可以看到左侧点击 Add 时会发生什么。而在右侧,你可以看到 Copy 的效果:它让分支流保持清晰明确。

在“场”选项卡中,我也禁用了“火焰”,并将其他设置保留为默认值。
在“形状”选项卡中,我禁用了“浮力”,因为我不希望烟雾上升或下降。我启用了“湍流”,并调整其缩放和强度,直到流动有足够的变化,同时分支仍然可见。


在 Output 设置中,我保留 density 用于渲染,保留 velocity 用于运动模糊。Minimal OpenCL 输出 Houdini 体积,其中 velocity 被拆分为 X、Y 和 Z 三个标量场。


我用 Convert VDB 将场转换为 VDB,然后使用 VDB Vector from Scalar 将三个速度分量组合起来。

VDB Resample 将速度场降低到一半分辨率,这对于此镜头中的运动模糊来说已经足够。最后,Primitive 节点将密度设置为 16 位存储,以减小缓存大小。



在 Solaris 和 Karma 中渲染
模拟缓存完成后,我进入 Solaris。这里的设置很简单:一个 Scene Import 引入体积,另一个导入摄像机。

我通过 Material Library 为体积指定材质。两盏盘灯通过 Parent Constraint 节点附加到第一台摄像机上,使其在摄像机移动时相对于该摄像机的位置保持一致。
在这里,材质与灯光协同工作。穿过多个相互重叠分支的光线比穿过一缕纤细烟雾的光线被吸收得更强烈,因此同一个着色器可以在整个体积中产生截然不同的颜色和对比度。
在 Material Library 内部,我使用 Karma Volume 着色器。MtlX Geometry Property Value 节点用于读取密度场。

将密度输入 Karma Ramp Const 让我可以控制在不同密度级别下哪些颜色被吸收。这会产生颜色变化,并有助于突出更密集的分支。我让散射保持无色,并分别调整散射和吸收乘数来控制效果。
运动模糊 & 渲染设置
缓存的速度场提供运动模糊。渲染几何体设置会在体积上启用速度模糊,并且运动模糊也在 Karma 渲染设置中启用。


最后润色
对于最终渲染,我使用了128个路径追踪采样,并启用OIDN降噪。这为这个镜头在质量和渲染时间之间取得了良好的平衡。景深已禁用。
我在Adobe Audition中使用来自Freesound的采样制作了音频。声音设计不是我的专长,但我偶尔喜欢做这个。它能让视觉画面栩栩如生,并能完全改变人们对作品的感受。
差不多就是这些了。我总是乐于接受新想法,所以如果你有项目想法,让我们取得联系!
喜欢这篇内容吗?