原文链接:《UE4:Niagara 与 SimulationStage》 | 作者:Feilon
Edition 1.00. Base on UE Ver.4.26
第一章:概述
SimulationStage 是 Niagara 推出的一个新功能,大幅度增加了 Niagara 的能力。使用老版本 UE 的同学是无法正常使用这一功能的,即使是在 4.26 版本,该功能依然处于实验阶段。
SimulationStage 可以说是 Niagara 中的一个比较高级的应用了,也是我们制作能使用 Cascade 制作出来的效果时用不到的功能。之所以一开始就切入这个主题,最主要的一点是它是整个粒子系统执行核心顺序与逻辑的重要分支,能帮助我们更好地理解粒子系统的工作原理。
我们这篇文章还不会涉及太多关于 SimulationStage 中的很多核心组件的使用,这些内容我们下篇再讲,我们先来看看 SimulationStage 在整个粒子系统中的地位,它有哪些特殊的地方,以及我们什么时候应该使用这一功能。
第二章:ParticleUpdate
一般来说,我们粒子系统的核心逻辑都会放到 ParticleUpdate 里,极少数可能在 EmitterUpdate 中,其他都是一些初始化的内容。可以说 Update 是核心,我们需要在这个每帧执行一次的段落中,更新所有粒子的位置、速度、颜色等等各种状态,从而实现效果随时间的演变。通常来说 ParticleUpdate 已经足够我们使用了。
我们这里为了形成对比再强调一下 ParticleUpdate 是怎么执行的。所有位于 ParticleUpdate 栏目下的脚本段落,都会 从上至下对每个粒子分别执行一次 。
具体来说,假如我们有 A、B、C 三个待执行的内容,那么在一个粒子系统中,系统会先对粒子 1 依次执行 ABC ,之后对粒子 2 依次执行 ABC ,直至最后一个粒子执行完毕, ParticleUpdate 才算正式结束,执行从上至下进入下一个大的栏目中。
当然我们的执行或多或少是并行执行的,实际上不是一个一个粒子依次执行ABC,而是一批一批粒子依次执行 ABC 。每一批的粒子中,所有粒子同时从 A 开始执行,互不影响。由于系统内部的调度问题或逻辑条件与计算复杂不同,我们在这个过程中无法控制哪个粒子会先执行完 A 开始执行 B 。本人无法确定每一批的粒子中的其中一个执行完整个 ABC 的流程后,是否会把空闲出来的计算资源直接分配给下一个粒子,还是等待每一批粒子全部执行完 ABC 后再整个更换一批粒子。我们可能给不同的粒子根据调解分配了不同数量的计算,两种模式产生的结果会或多或少影响到这种计算量的不均等带来的时间损耗,不过这不影响我们编写至少正确的执行逻辑。
并行运算的各个粒子之间的运算是无法在同一帧交叉影响的。通常来说每个粒子只能读取其粒子自身的属性,亦或是对应 Emitter 或 System 的全局属性,并修改属于当前粒子自身的属性值。
虽然我们在强大的 Niagara 中能随时获得其他任何一个粒子的属性(通过AttributeReader,具体操作之后再讲),并根据这些属性做出计算,但是由于并行运算的复杂调度,以及我们无法控制每个粒子的先后执行顺序带来的不确定性,实际上我们是缓冲了上一帧的计算结果来进行属性的读取,即整个 ParticleUpdate 过程中发生的所有的使用 AttributeReader 进行的属性读取,都是当前帧的初始值。假如粒子 1 经过 A 运算后,属性发生了改变,粒子 2 想要在 B 运算时获得粒子 1 改变后的属性,是不可能做到的。
这里 多提一嘴 的是,上述获得前一帧粒子属性的限制,只发生在相同 Emitter 下使用 AttributeReader 的属性读取。跨 Emitter 的粒子属性读取,是会在生成代码时自动分析这种依赖关系带来的执行先后顺序,从而保证我们读取到的是当前帧执行后的粒子属性。
到这里我们其实已经可以看到 ParticleUpdate 的一些极限了。每个粒子无法在当前帧实时获取到其他粒子改变后的属性,并执行进一步的操作,而这正是 SimulationStage 带来的突破点。
第三章:SimulationStage
假如我们存在这样的需求,在同一帧下我们希望所有的粒子先同时执行 A ,等所有粒子执行完 A 之后,每个粒子的属性发生了相应改变,我们再利用这些改变后的属性执行 B 。当所有 ABC 执行完毕后,我们才希望更新当前帧的粒子的表征,如位置颜色等,此时我们就必须要用到 SimulationStage 来实现。
我们可以往一个 Emitter 中添加多个 SimulationStage 。概念上讲,每个 SimulationStage 相当于是一个执行的同步等待点。每个 SimulationStage 做着同 ParticleUpdate 类似的事情,即遍历所有粒子同步执行当前 SimulationStage 下的所有脚本,能应用到 ParticleUpdate 中所有可以使用的模块。每个 SimulationStage 必须等待上一个 SimulationStage 异或最前面的 ParticleUpdate 所有粒子执行完毕后,再开始执行当前的 SimulationStage 。我们也可以粗糙地认为 SimulationStage 相当于在 ParticleUpdate 后面追加了 ParticleUpdate2 与 ParticleUpdate3 等等。
假如我们的逻辑不需要这样的等待过程,使用 ParticleUpdate 也能正常完成,那么 SimulationStage 带来的等待必然会产生一些性能上的损失,因此不是在任何的情况下都需要 SimulationStage 来介入。只有在我们的逻辑对当前帧其他粒子的属性或某些中间阶段性的结果有强的顺序依赖时,才需要 SimulationStage 。
引入 SimulationStage 之后还没有解决一个核心的问题,那就是我们读取其他粒子的属性还是只能通过 AttributeReader,而它在 SimulationStage 中读取的同样是上一帧的粒子属性,因为缓存每个阶段所有粒子的属性中间值是不那么高效与优雅的,会浪费很多的资源。这意味着我们要想获得其他粒子的属性,必须创造一个额外的中间容器,来保存我们接下来所需要的中间计算结果。
为了实现这一中间缓存的目的,我们需要这一容器在当前帧的任一 SimulationStage 都是可在任意位置读写的,于是我们的 Grid2D、Grid3D 等等内容就登场了。关于它们的讲解我们会在下一篇文章单独重点展开,此时我们先明白它们存在的意义即可。
而多了这些中间内容,我们可能又不满足于只对所有的粒子进行遍历了,我们的中间内容也是大量数据的集合,而我们使用其进行数据储存后,便会自然而然地诞生针对这些计算上的中间数据做遍历处理的需求。因此,SimulationStage 也追加了 针对这些内容的数据结构进行遍历的功能
最后,我们可能又会有针对某段对数据的处理在同一帧反复执行多次的需求。就算我们可以直接对脚本模块进行复制粘贴,拉出一大长串相同的SimulationStage也过于不美观。因此我们的SimulationStage本身支持了对自身多次的执行。
至此,我们为粒子系统追加SimulationStage的逻辑路线就完整清晰了。
最后额外提一点的是,这种利用当前帧其他粒子中间计算结果的操作,对于没有 SimulationStage 的 Niagara ,也是可以通过跨帧运算的方式来间接实现的。当然这会让粒子的运行本身变慢,毕竟我们无法改变一帧执行一次的大前提。
第四章:SimulationStage 的基础使用方法
概念上我们清楚了,接下来看看最基本的使用方法。
SimulationStage 是以 Emitter 为单位开关的,同一个粒子系统下的多个 Emitter 可以有不同的选择。
使用它首先要打开它。点击任意一个 Emitter 的 EmitterProperties,在右边详情页中点击下拉小箭头展开,就能看到 SimulationStage 栏目下的内容:
第一个选项就是我们打开 SimulationStage 的选项,至于下面的内容都跟 ShaderStage 有关,本人也不清楚其具体是什么,不过既然都被标记为 Deprecated 了,那么我们也不要去管它了。
SimulationStage 目前还必须在 GPU 环境下运行,所以我们还需要在详情页中切换到 GPU 粒子,并设置 Fixed Bounds:
之后我们就能在 Emitter 的卡片中看到在 Add Event Handler 下面多出来的一个 Add Simulation Stage 选项卡。每点击一次 加号 都能创建一个 SimulationStage 。
创建时目前只能选择一个默认的 Generic Simulation Stage ,点击创建后,在 Add Simulation Stage 前面会出现一个新的栏目,默认初始命名都为 None:
然后我们选中 Generic Simulation Stage Setting,可以看到 SimulationStage 的设置选项出现在详情页:
我们先看一个最不重要的设置:Simulation Stage Name,顾名思义它是设置当前 SimulationStage 的名称的,不影响任何运行,但是起一个好的名称能帮助我们理清思路。名称修改后就会反映到 Emitter 的主页上。
然后我们看 Iteration Source ,默认情况下它是 Particles,即我们对所有 Particle 进行遍历,而 Iterations 为 1 说明我们遍历一次。
Iteration Source 可以替换成另一个选项加做 Data Interface,它是我们所有中间数据结构的统称,选中后,后面两项会相应变得可选。其中 Data Interface 中我们可以指定所有在 Emitter 中创建的中间数据结构变量。至于 Emitter Reset Only 则是用来指定当前 SimulationStage 是否为初始化时执行,即只在 Emitter 激活后的第一帧执行唯一一次。
Disable Partial Particle Update 会屏蔽当前 SimulationStage 对除位置外的粒子信息的读写,只能在遍历粒子的情况下使用。本人还没想到该设置可以如何配合 Debug 使用。
创建多个 SimulationStage 后,如果想调整它们之间的执行顺序,可以直接拖动 SimulationStage 的名称栏来改变位置,就如同我们改变一般模块的执行顺序一样,这十分方便。
每个 SimulationStage 中,点击加号都可以追加任意的模块,所有能在 ParticleUpdate 中使用的以及不能使用的,都可以在此处添加,当然具体还是要看模块是否对 SimulationStage 暴露。
第五章:结语
SimulationStage 的使用离不开 Grid2D 等中间数据类型的使用,否则我们总是能用 ParticleUpdate 实现类似的效果。而这些中间数据类型的加入,不仅实现了我们上述所说的,增加了粒子每一帧的逻辑流程的可控性,还使得整个粒子系统彻底脱胎换骨,成为了很多效果与逻辑不可或缺的中间件。而这些,都将是我们下一篇文章会详细展开的。