原文链接:《UE4:Niagara 中的 Ribbon》 | 作者:Feilon
Edition 1.00. Base on UE Ver.4.26
第一章:概述
我们到目前还没有讲 Niagara 是如何把粒子显示出来的。
有趣的是,粒子系统的运算部分与显示部分是可以完全解耦的,这意味着一个粒子系统可以没有任何的视觉表现,而只作为一个纯粹的计算器来运作,并与外界交互。
把计算得到的结果写入 RenderTarget 或其他引擎资产,又或是触发某个外部的蓝图事件是不同形式的交互方式,但粒子系统本身的初衷还是一个批处理特效系统,视觉表现层面还是很重要的。
我们这里会简单介绍一下粒子系统是怎么知道该在什么位置渲染什么内容,以及重点讲一下一个特殊的渲染方式Ribbon。
第二章:赋予数据意义
我们在粒子系统的整个计算过程中,会给各个命名空间创建很多的变量,其中几个最关键的如 Position、Color等,几乎是每个效果都需要的。然而,这也只是我们给这个变量起的名字,起名字这个过程不会让引擎知道这就是位置,那就是颜色,引擎只知道这是一个 float3 与 float4 。引擎不知道应该在这个位置渲染一个这个颜色的粒子。
那我们是不是应该规定引擎,去 Hardcode 识别被我们命名为 Position 或 Color 的变量,作为位置与颜色的标记吶?原理上是可行的,但这样做会给我们的粒子系统加上很多的枷锁,变得难以扩展,变得不够灵活。
首先 Position 这种单词本身不是起名字的统一方式,可能会有人不使用官方的模块,而自己定义位置变量的名称为 Pos 甚至是 P,有时人会想给变量标号,有时想把 Position 的值同时给 Color ,而无需额外声明一个 Color 变量。总之需求的灵活性还是很强的,我们不能简单地要求用户必须按照八股文的方式来定义变量。
需求是不定的,但渲染必要的输入往往是不会有太多的变化的,Position、Size 等等这些都会需要,那么我们不过在渲染的之后指定每个需求的输入可以从哪个变量获得。这就是 Niagara 的 Render 的设计思路。通过这种人为设置的绑定,我们可以把任意变量与任意渲染输入绑定,以达到最大的可控性。当然便捷性也不可以丢,默认情况下这些输入还是会绑定到通用的名称上。假如我们没有声明这些名称的变量,则给一个默认值输入,由此也能把计算空间节约下来:
如图我们的每个渲染必要的输入都绑定到了一个 Particles 命名空间下的默认类型上,我们可以更换绑定,只要数据类型是对应的即可。
之所以绑定到了 Particles 命名空间的变量上,是因为一般我们的每个粒子会有不同的位置等输入。而这里也可以绑定到 Emitter 或 System 命名空间中的变量上,这样该 Render 全部的粒子都会有相同的渲染输入。
绑定到输入上往往还只是第一步,因为我们最终要渲染到屏幕上,而对于目前的渲染管线结构,一个渲染到屏幕上的物体需要有一个材质来决定它的 VS 与 PS 等的计算方式。我们往往希望材质中也能用到我们粒子上的任意一个参数。这个通过上面列表中的 Material Parameter Bindings 可以实现,只要在材质中声明一个相同数据类型的参数。当然系统本身已经帮我们提供了几个默认的信息获取接口,比如 Particle Color 等,这些都能在材质的节点中直接找到。
第三章:Sprite Render
目前 Niagara 的 Render 有 5 种,分别是 Sprite、Ribbon、Light、Mesh 与 Component。用的最多的就是 Sprite,也是最基础的 Render ,所以我们先讲下这个 Render。
一般的渲染方式,我们希望每个粒子对应一个渲染实体,最简单的实体就是一个面片,这就是 Sprite。复杂的实体我们可以使用 Mesh。
Sprite Render 的设置没有多少容易混淆的地方,所以我们只讲几个最主要的设置:
首先是 Material,决定了我们每个粒子需要什么材质,每个粒子的材质都能通过一些节点获得该粒子带有的变量,这个我们不多讲。Material User Param Binding 则可以让我们选择 User Exposed 命名空间中我们设置的 Material Interface 材质,这给了我们通过蓝图设置与改变粒子材质的可能。
然后看 Source Mode ,默认情况下是 Particles,这个设置表示我们要以什么为单位生成实体。由于 Render 是在 Emitter 下的,所以能选择的也不多,要么是最常用的 Particles ,即每个粒子生成一个对应实体。要么是 Emitter ,即对每个 Emitter 生成一个实体。然而 Render 在某个特定的 Emitter 下,这意味着 SourceMode 为 Emitter 时仅能生成一个实体。
选择了 SourceMode 为 Emitter 后,粒子的渲染输入绑定的所有变量会默认变成 Emitter 命名空间下的变量,并不可选择 Particles 命名空间下的变量,这也不难理解,因为 Emitter 无法读 Particles 的信息,毕竟无法指定到底读取哪个粒子。
Alignment 下有三个选项,分别是 Unaligned、Velocity Aligned、Custom Alignment,这是三种决定面片朝向的设置方式,与下面的 Facing Mode 的设置是相辅相成的。结合在一起,它们有不同的优先度。
首先我们来看一下上面的渲染输入中有几个是决定 Sprite 的旋转属性的,有 SpriteRotation、SpriteFacing、SpriteAlignment。SpriteFacing 决定了我们的面片的朝向向量,但是仅仅固定了朝向的面片还有一个额外的旋转自由度,因此需要另一个变量确定面片的旋转属性,这个变量可以是 SpriteRotation ,它通过一个角度给定我们的旋转方向,当 SpriteRotation 为 0 时,材质的 U 方向始终朝向我们视野中朝上的位置。而 SpriteAlignment 通过另一种方式给定了旋转方向,即满足 SpriteFacing 的前提下,尽可能保证材质中的 U 方向与 SpriteAlignment 定义的 Vector 夹角最小。SpriteFacing 与 SpriteAlignment 相等时,夹角必定会是 90 度,此时会触发粒子失效的条件,粒子会消失。
我们所进行的设置,一是选择 SpriteFacing 的向量是我们自定义的还是某个特殊向量,二是决定我们采用 SpriteRotation、SpriteAlignment 还是某个特殊向量来决定 Sprite 的旋转。
有了这些原理的基础,我们就能更好地理解 Alignment 与 FacingMode 的设置了。
首先 Alignment 决定我们的旋转角度设置,其中 Unaligned 意味着我们使用 SpriteRotation 来决定旋转,Velocity Aligned 意味着我们将使用 Velocity 来覆盖 SpriteAlignment 的值作为旋转的设置,而Custom Alignment 则是使用 SpriteAlignment 设置的 Vector 来作为旋转的设置。
Facing Mode 决定我们的面片朝向的设置,Custom Facing Vector 使用 SpriteFacing 来作为朝向的设置,其他的设置都是使用面向摄像机的 Vector 来覆盖 SpriteFacing 作为朝向的设置。这是最常用的设置,免去了我们手动计算面向摄像机朝向的麻烦,而其中又有几个变种。区别最大的是 Face Camera 与 Face Camera Position ,前者为指定 SpriteFacing 为 CameraVector:
后者则是指定 SpriteFacing 为 CameraPosition – SpritePosition,二者的区别是固定 Camera 的位置而旋转 Camera 时,前者会旋转 Sprite 而后者不会。这个差别可以很容易地从图中看出来:
Face Camera Distance Blend 是使用上面说到的两个 FaceCamera 的模式做一个生硬的 LOD ,并在中间层做一个过渡,近处是 Face Camera Position ,远处是 Face Camera,中间段有一个线性过渡区。设置需要修改下面两个参数:
小于 Min Facing Camera Blend Distance 的是 Face Camera Position ,大于 Max Facing Camera Blend Distance 的是 Face Camera ,而中间是二者的线性混合,如图:
如果 Min 比 Max 值大,那么会认为 Max 与 Min 相等,取 Min 的值做一个生硬的过渡。
还有一个 Face Camera Plane ,跟 Face Camera 相同,虽然它们有不同的意义,但是实验中没发现什么不同,有发现的可以跟我沟通。
另外前面说到 SpriteFacing 与 SpriteAlignment 同时设置时会优先考虑 SpriteFacing ,但在 Face Camera 的任何一个模式下,都会优先考虑 Velocity Alignment 或 Custom Alignment ,再考虑 SpriteFacing 与 CameraVector 夹角最小。这里有个优先级的问题需要注意。
最后额外提一个可能比较常用的设置,即 RenderVisibility,当 Particles 命名空间下的 VisibilityTag 与该值相同时粒子才会被渲染,我们可以通过这个值的变化方便地设置粒子的可见性,而无需销毁我们可能希望日后再次出现的粒子。这个值还能通过我们添加不同 RenderVisibility 的 Render 的方式,来做一个动态的 Render 的切换,并且几乎没有任何消耗。由此做一个手动的 LOD 也是可行的。
其它的设置意义比较直观,我们这里就不提了。
第四章:Ribbon Render
Ribbon 是一种特殊的渲染方式,与其他渲染模式有很大的不同。首先它并不是以每个粒子为单位产生的,而是在渲染粒子与粒子之间的连接线的。连接线的形状可以我们自己来定义,由此可以很方便地制作一些线条类的效果。
首先我们能看到的是默认的材质有所不同。其实材质本身是没有不同的,这个只是帮我们了解一下一个被拉成长带子的线条的 UV 在三维空间中的朝向。默认情况下,U 方向沿着线条的长度,而 V 方向则沿着线条的宽度。
FacingMode 与 Sprite 的设置有很大的不同。首先 Ribbon 本质上还是一个被拉长了的面片,所以设置其面向的方向是很自然的。朝向 Screen 则意味着连线的面片会尽可能地朝着屏幕,但是由于我们的粒子分布可能导致连线呈现各种各样的弯曲形状,全部位置的 Normal 与 CameraVector 平行在数学上时不可能做到的,我们最多只能求得我们面向的始终是条带面片的同一个正反面。为此转折处可能会出现面的翻转,以在保证条带自身连续的前提下,面向我们的始终是同一个正反面,极端情况下如图:
设置为 Custom 时,会以我们在 Particle s命名空间下定义的 RibbonFacing 作为朝向,统一整个条带面片以同一个正反面朝向该方位,并在必要位置翻转。CustomSideVector 则是将 RibbonFacing 定义的向量作为与条带面片平行的向量,来求得实际的面片朝向。
默认情况下,整个条带的起始位置为 U=0 的位置,末尾为 U=1 的位置,使用默认材质时可以看到条带在末尾逐渐消失。然而整个材质在条带上的分布、方向乃至条带曲面的生成细节都可以通过设置修改,但这些不是我们这里讲述的重点内容。我们主要还是来看看与 Sprite 不同的特殊的粒子属性变量的配置,以生成一个最简单的条带。
首先我们来思考一下条带生成的基本规则。我们还是希望能尽可能地保有设置上的灵活性,所以我们希望能够人为地指定哪个粒子与哪个粒子之间可以生成一个条带,以及条带连接的方向。
我们的计算本身还是基于粒子的,而不会额外酝酿一个 Ribbon 命名空间。这意味着每一个 Ribbon 需要使用 Particles 下的数据进行计算。这也不难理解,我们画个曲线总还是要指定连接空间中的哪个点,再决定他们的切线方向与朝向,这些点其实就对应着我们的粒子。那么我们在粒子的属性中指定他们连接的另一个粒子的编号,就可以确定这条连线了。
点与点之间的线条状态肯定是使用点上的属性通过某种算法拟合出来的,我们无需关心这些中间线条的状态,而只需注意在点上进行一些配置,包括线条宽度、颜色等。
由此我们得到了上面这些必要的变量。我们只看与 Ribbon 相关的几个变量。
首先从简单的开始。RibbonTwist 决定了我们设置好 RibbonFacing 后,产生的进一步的旋转。比如我们Facing Mode 设置成 Screen 时,再设置 RibbonTwist 为 1.57(注意是弧度),此时面片将与我们垂直,我们就看不到条带了。RibbonWidth 是条带的宽度,是条带路过该粒子时的宽度,中间的宽度将通过两头的粒子的宽度设置来线性拟合。
然后最重要的是 RibbonID 与 RibbonLinkOrder ,它们决定了我们的连接方式,如果设置不好这两个变量是看不到条带的。
一般的想法是,我们指定每个粒子连接的其他粒子的编号,我们有 Execution Index 与 Niagara ID 来帮助我们实现这个想法。然而不幸的是这两个东西都并不稳定,不利于我们控制条带的稳定存在。为此我们需要另一种替代方式指定条带的连接方式。
我们可以换一个思路,先决定哪些粒子在同一个条带上,再决定这些粒子在这些条带上的连接顺序。一旦把所有的粒子按照条带预先划分,我们便可以使用一个线性参数来通过比大小的方式决定哪个粒子连接哪个粒子。
划分条带的变量就是 RibbonID ,相同 RibbonID 的粒子会被连接在一起,而 RibbonLinkOrder 则用来决定我们的连接顺序,0 是 Ribbon 的开始位置,值越大越靠后。额外增加一个中间值的粒子可能会大幅度改变条带连接的形状,但这种程度的自由 DIY 正是我们需要的。
虽然我们牺牲了一些条带定制的自由度,比如一个粒子连接多个粒子的情况,但是我们获得了一个更加清晰的条带描述方式,而这些特殊情况大多可以通过一些粒子的冗余来实现,因此不是太大的问题。
到这里我们就能正常地显示一个 Ribbon 了。复杂的 Ribbon 设置还涉及到材质的 UV 控制以及条带的形状控制,但这不是我们希望在这里讲述的。
第五章:小结
我们这篇文章只讲一些最基础的内容,主要用来了解一下 Ribbon 参数的设置原理,好让我们从 0 开始 DIY 我们的Ribbon,而不是简单地套用一些官方的模块。
Ribbon 还是很好用的,很多效果使用 Ribbon 能增彩许多。