粒子效果
球状粒子从模型表面生成(模型的各个位置),拥有与最近模型表面相同的颜色、金属度和粗糙度;生成的一段时间后,开始受到重力影响,下坠,与地面产生碰撞。
Niagara蓝图部分
蓝图部分可以粗略提供的信息有:首先这是一个GPU粒子系统;创建了一个变量暴露给Niagara系统外;有三个新的模块——Skeletal Mesh Location,Sample GBuffer Attributes和Camera Offset,从字面上理解分别是依照骨架网格体位置作为粒子的位置,获取GBuffer中存储的属性值和设定相机的偏移。其具体作用还需后面具体分析。此外,还有一些负责力学解算的部分——Gravity Force重力,Collision碰撞等。
效果实现分析
在进入Niagara蓝图之前,让我们先看一下这个人物骨骼模型的材质。我这边截取了其中跟我们这节内容比较相关的一部分,主要是对材质的BaseColor、Metallic和Roughness进行的设置和编辑(这些属性都会写入到GBuffer中)。
首先是左下角到BreakOutFloat3Components。这部分是用骨架网格体的位置(局部空间内)减去局部空间内bounding box(想象一个盒子将模型包住,同时保证这个盒子体积最小)中位置最小的点(即x,y,z值最小),再分别除以bounding box的长、宽、高,从而得到一个比值(将值限定到了0~1,总体可以理解成一个三维空间上的UV),break出来的三个通道RGB其实对应x、y、z坐标轴。
这个值的R通道作为Step节点的X值。Step节点的作用是将X的值(0.5)与Y值作比较,如果Y<X,输出1,相反则会输出0,这个输出会作为模型的Roughness粗糙度值。这里所有R通道小于0.5的像素点(模型局部空间内的,这里X轴向右,故而是模型的左半边)的Roughness为1,即粗糙拉满;大于0.5,即模型的右半边,粗糙度为0,非常光滑。
在右下角橘黄色部分,拿到前面输出的B通道(与局部坐标z值相关,即纵向,范围01),乘以8(范围扩大到08),然后输入Floor节点(相当于取整操作,比如输入的是5.1则取5),然后除以8(再将取值化为了0~1),这样就实现了分层(将连续的值转化为了离散的值),这个值最后作为了材质的BaseColor(当然中间还有一些节点,但是对最终效果影响不大)也就是最终效果图中我们看到的一层一层颜色的效果。
如此,我们便知道了GBuffer中的这些奇奇怪怪的颜色、粗糙度数据是怎么搞出来的了。
回到Niagara System中。欲获得粒子生成于骨架网格体表面的效果,我们需要在Niagara系统中拿到这个网格体表面的数据。这里创建了一个Skeletal Mesh类型的属性,并暴露出去,以作为获取Skeletal Mesh数据的接口。
在Particle Spawn阶段,用Skeletal Mesh Location去读将上面拿到的Skeletal Mesh类型的属性,并配置各种采样规则。Camera Offset则是根据粒子和相机之间的向量(方向)以及值的小大来作一点粒子的偏移,使距离相机近的部分有相对更大的粒子密度。
Sample GBuffer Attributes,采样GBuffer中的属性。看到右上角的铅笔就知道我们可以到Scratch中对它进行编辑。从使用来看,其上有三个可配置选项:Snap Particle To Sampled Depth移动粒子到采样的深度,Kill Particles If Too Far From Sampled Depth如果粒子距离采样深度太远就销毁掉,Sampled Depth Threshold采样深度阈值。还是进入编辑界面看看脚本是怎样的原理吧。
这个脚本还是比较清晰的,右侧Map Set主要写入或者重写了几项属性:Particles.Color粒子颜色,Particles.Position粒子位置,Data Instance.Alive粒子的生死,Particles.DynamicMaterialParameter动态材质参数(粒子和材质互动的接口参数之一)。
Particles.Color是通过节点Decode Base Color从GBuffer里根据屏幕UV(粒子世界位置转到屏幕UV上,参考GPU中顶点着色器的工作流)扒出一个颜色值(还需要补充一个alpha通道值),再写入/覆盖粒子颜色值。
同样的道理,Particles.DynamicMaterialParameter是也是通过解码节点(Decode节点),分别将深度、粗糙度、金属度写入进去(第四个通道没有存储任何数据,为0)。
其中的自定义节点(HLSL片段)我们先将他的输入理清楚:In_SamplePos读的是粒子的位置,GbufferDepth读的是GBuffer里的深度值。我用注释解释了一下下面这段代码片段。
理解了代码,后面就比较好理解了,其实就是两个条件判断:如果勾选了Snap Particles To Sampled Depth,就将粒子移到模型上的对应位置;只有当粒子到粒子在模型上对应点的距离小于我们之前设定的阈值,且勾选了Kill Particles If Too Far From Sampled Depth,才会保留粒子的存活状态,否则直接销毁该粒子。
float Depth = float(0);
//重置缓存数据
ProjectedDepth = float(0);
WorldPosOnDepth = In_SamplePos;
#if GPU_SIMULATION
float3 CameraPosWS = View.WorldCameraOrigin.xyz;
float3 CameraFWD = View.ViewForward.xyz;
//相机位置-粒子位置,然后归一化得到 粒子到相机的单位向量
float3 CamMinusSamplePos = normalize(CameraPosWS - In_SamplePos);
//粒子到相机单位向量 点乘 相机的朝向(单位向量) 得到两者夹角的余弦值
Depth = dot(CamMinusSamplePos, CameraFWD);
//用GBuffer中的深度值(是模型上对应像素的深度值) 除以 该余弦值 得到 粒子到模型上对应像素位置的距离(想象一条射线从相机出发,穿过粒子打到模型上一个某个点,相机到这个某个点的距离)
Depth = GbufferDepth / Depth;
//相机到这某个“点”的向量
CamMinusSamplePos *= Depth;
//这个某个“点”的世界位置
WorldPosOnDepth = CamMinusSamplePos + CameraPosWS;
//粒子位置到粒子该去的位置之间的距离
ProjectedDepth = length(In_SamplePos - WorldPosOnDepth);
#endif
OK,经过了属性的重写,下面就是力的解算了。之前我们讲解过,这里就不作阐述了。我们直接看我们重写的那些属性如何表现。来到Mesh Renderer网格体渲染其中绑定部分。颜色、位置的数据流动自不必说。那么Particles.DynamicMaterialParameter是如何表现出来的呢?我们进入到使用的材质中。
Particle Color是材质中接粒子属性的接口,而Dynamic Parameter是接Particles.DynamicMaterialParameter的接口,这里使用了Param2和Param3(我们之前构建DynamicMaterialParameter时这里个通道分别传入了GBuffer中获取到的粗糙度和金属度)。
总结
其实从Niagara蓝图的角度看,这个例子并不复杂,但是其中涉及到的图形学相关的计算和转化还是比较多的。要掌握这部分的内容,需要对GBuffer、深度、各种坐标空间、顶点和片元着色器有一定的了解才行。希望我的文字解释得足够清晰。