Notifications
Article
粒子特效教程 | 多重GPU粒子力场
Published a month ago
128
0
我们将分享加拿大游戏特效大神Mirza Beig的粒子特效的系列教程,该系列教程将帮助你了解如何使用粒子系统制作精美的特效。
上一篇教程《GPU粒子力场》中,我们制作了一个自定义着色器,它能接收表示球体位移影响因子即力场的位置和半径,基于标准化偏移值来移动粒子顶点并给粒子着色。
本篇教程中,我们将用该着色器制作一个特别版本,它能通过使用数组来支持多个力场。下图的预览效果由本教程制作的效果和《创建3D均匀粒子网格》的均匀粒子网格结合而成。

Part 1:顶点着色器

创建上一篇教程中着色器文件的副本,然后修改文件名。
下面的代码中,只是在Field后添加了“s”,使Field一词变为复数形式。
Shader "Custom/Particles/GPU Force Fields Unlit (Tutorial)"
然后删除材质的半径和位置属性。我们不再需要这二个属性,因为我们会用C#脚本,将它们直接指定到着色器力场数组中。
Properties { _MainTex("Texture", 2D) = "white" {} _ForceFieldRadius("Force Field Radius", Float) = 4.0 _ForceFieldPosition("Force Field Position", Vector) = (0.0, 0.0, 0.0, 0.0) [HDR] _ColourA("Color A", Color) = (0.0, 0.0, 0.0, 0.0) [HDR] _ColourB("Color B", Color) = (1.0, 1.0, 1.0, 1.0) }
我们还将删除了半径和位置的变量,将它们替换为力场数量和紧凑的力场数组。我们会用xyz保存每个力场的位置,用w保存半径。我们会用力场数量来结束迭代每个力场的循环,这样就不必处理整个数组。
本文中我们使用的数组大小为8,你可以按喜好使用更大的数值,例如:64。
sampler2D _MainTex; float4 _MainTex_ST; float _ForceFieldRadius; float3 _ForceFieldPosition; int _ForceFieldCount; float4 _ForceFields[8]; float4 _ColourA; float4 _ColourB;
拿一个数组举例,第一个力场的位置和半径会分别定义为_ForceField[0].xyz _ForceField[0].w。
现在我们可以编写用于计算和返回粒子偏移的函数。首先添加第二个参数“forceField”,因为我们没有表示力场半径和位置的全局变量,所以要在循环调用该函数的过程中,传入每个球体的信息。
float4 GetParticleOffset(float3 particleCenter, float4 forceField)
在函数顶部创建二个新变量,用于获取半径和位置。
float forceFieldRadius = forceField.w; float3 forceFieldPosition = forceField.xyz;
这便是我们要修改的内容,现在可以将函数中的_ForceFieldRadius和_ForceFieldPosition替换为刚创建的二个新变量。在下面代码第12行的max函数中把0.0改为一个小数,以避免顶点在多重力场中消失。
其它代码保持不变,代码如下。
float4 GetParticleOffset(float3 particleCenter, float4 forceField) { float forceFieldRadius = forceField.w; float3 forceFieldPosition = forceField.xyz; float distanceToParticle = distance(particleCenter, forceFieldPosition); float forceFieldRadiusAbs = abs(forceFieldRadius); float3 directionToParticle = normalize(particleCenter - forceFieldPosition); float distanceToForceFieldRadius = forceFieldRadiusAbs - distanceToParticle; distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0001); distanceToForceFieldRadius *= sign(forceFieldRadius); float4 particleOffset; particleOffset.xyz = directionToParticle * distanceToForceFieldRadius; particleOffset.w = distanceToForceFieldRadius / (forceFieldRadius + 0.0001); //添加小数来避免被除数为0,以及在r=0.0时出现未定义的颜色或行为。 return particleOffset; }
修改偏移函数并实现数组后,为了完成多重力场的基本支持,我们只需要使用循环代码更新顶点着色器即可。
遍历整个数组长度比用变量力场数提早结束的速度更快。这是因为编译器默认会先尝试展开循环。这种情况下,我们必须在对应C#脚本中每帧创建一个新数组。
数组受限于力场数量,这样脚本会忽略不活动力场,或把这些力场的半径设为0,使它们不对任何粒子造成影响,这也是避免常量分配的较优解决方案。
v2f vert(appdata v) { v2f o; float3 particleCenter = float3(v.tc0.zw, v.tc1.x); float3 vertexOffset = 0.0; for (int i = 0; i < _ForceFieldCount; i++) { vertexOffset += GetParticleOffset(particleCenter, _ForceFields[i]).xyz; } v.vertex.xyz += vertexOffset; o.vertex = UnityObjectToClipPos(v.vertex); // 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色 o.color = v.color; o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex); // 初始化tex coord变量 o.tc0.zw = v.tc0.zw; o.tc1 = v.tc1; UNITY_TRANSFER_FOG(o,o.vertex); return o; }
稍后我们将对顶点着色器进行一些修改,但现在我们先处理片段着色器。

Part 2:片段着色器

片段着色器所更新的代码行为第12-19行。由于多重力场会影响粒子的偏移,所以我们将从整个循环获取最大标准化偏移并使用它。
fixed4 frag(v2f i) : SV_Target { // 采样纹理 fixed4 col = tex2D(_MainTex, i.tc0); //让纹理颜色和粒子系统的顶点颜色输入相乘 col *= i.color; float3 particleCenter = float3(i.tc0.zw, i.tc1.x); float maxNormalizedOffset = 0.0; for (int i = 0; i < _ForceFieldCount; i++) { maxNormalizedOffset = max(maxNormalizedOffset, GetParticleOffset(particleCenter, _ForceFields[i]).w); } col = lerp(col * _ColourA, col * _ColourB, maxNormalizedOffset); col *= col.a; // 应用模糊效果 UNITY_APPLY_FOG(i.fogCoord, col); return col; }
由于没有C#脚本填充力场数组,因此我们目前无法看到任何实际变化。

Part 3:GPU力场游戏对象

设置粒子系统的方法是把任何附加了该组件的游戏对象的任何子Transform都视为作力场对象。然后我们可以从父对象动态添加或移除Transform,来创建或销毁力场。
下面详解该脚本和上一片教程中单力场脚本之间的差异。
首先我们在第9行有一个常量内部变量,用来指定与着色器中的数组长度匹配的最大力场数量。
在第11行有Vector4类型的forceFields数组,我们会在Start()函数中将其初始化为最大长度,并指定为着色器中的等价变量。着色器数组此时没有初始化为它的长度,直到受到外部脚本的设置,所以这是我们立即执行此操作的原因。
每一帧我们都用着色器中的子对象数量更新力场数量,然后用循环来提取它们的位置和半径,这些信息会被指定到数组内当前迭代的力场向量中。当更新循环完成后,只要将数据复制到着色器数组即可。
最后,我们在OnDrawGizmos函数中循环处理数组,这样能可视化力场为球体。
下面是完整的C#脚本。
using System.Collections; using System.Collections.Generic; using UnityEngine; [ExecuteInEditMode] public class GPUParticleForceFields : MonoBehaviour { public Material material; const int MAX_FORCE_FIELDS = 8; // 确保该数值匹配着色器全局数组大小。 Vector4[] forceFields; void Start() { // 需要设置为支持的最大数组长度,因为它会决定着色器上的实际大小。 forceFields = new Vector4[MAX_FORCE_FIELDS]; material.SetVectorArray("_ForceFields", forceFields); } void LateUpdate() { material.SetInt("_ForceFieldCount", transform.childCount); for (int i = 0; i < transform.childCount; i++) { Transform childTransform = transform.GetChild(i); forceFields[i] = new Vector4( childTransform.position.x, childTransform.position.y, childTransform.position.z, childTransform.lossyScale.x / 2.0f); } material.SetVectorArray("_ForceFields", forceFields); } void OnDrawGizmos() { for (int i = 0; i < transform.childCount; i++) { Transform childTransform = transform.GetChild(i); float radius = childTransform.lossyScale.x / 2.0f; Gizmos.DrawWireSphere(childTransform.position, radius); } } }
现在我们可以随意进行调整。

Part 4:均匀半径

我们将为着色器添加一个可选功能,以帮助缓解加法偏移混合的问题,使力场的混合效果更好,该功能适用于处理一些特别情况。
我们会通过静态开关来控制是否让着色器使用该功能,所以我们需要在材质属性中添加开关和均匀半径。
请注意,这里使用了范围滑块,因为这样在编辑器中更容易调整,你也可以使用常规的数值属性或较大的滑块范围。
Properties { _MainTex("Texture", 2D) = "white" {} [Toggle(_USEUNIFORMRADIUS_ON)] _UseUniformRadius("Use Uniform Radius", Float) = 0.0 _UniformRadius("Uniform Radius", Range(-10.0, 10.0)) = 1.0 [HDR] _ColourA("Color A", Color) = (0.0, 0.0, 0.0, 0.0) [HDR] _ColourB("Color B", Color) = (1.0, 1.0, 1.0, 1.0) }
我们需要定义开关的关键字。
CGPROGRAM #pragma vertex vert #pragma fragment frag // 实现模糊效果 #pragma multi_compile_fog #pragma shader_feature _USEUNIFORMRADIUS_ON #include "UnityCG.cginc"
并且定义均匀半径变量。
sampler2D _MainTex; float4 _MainTex_ST; int _ForceFieldCount; float4 _ForceFields[8]; float _UniformRadius; float4 _ColourA; float4 _ColourB;
我们处理混合的方法是应用均匀半径,然后使用所有力场中最小的偏移值。为了从函数获取最小偏移距离,我们将inout关键字和第一行参数一起使用,以便我们可以传入数值并修改原始数值。
该函数用于更新输入变量,具体方法是将输入变量与当前距离作对比,使它总是最小值。
float4 GetParticleOffset(float3 particleCenter, float4 forceField, inout float minDistanceToParticle) { float forceFieldRadius; float3 forceFieldPosition = forceField.xyz; #ifdef _USEUNIFORMRADIUS_ON forceFieldRadius = _UniformRadius + 0.0001; #else forceFieldRadius = forceField.w; #endif float distanceToParticle = distance(particleCenter, forceFieldPosition); float forceFieldRadiusAbs = abs(forceFieldRadius); float3 directionToParticle = normalize(particleCenter - forceFieldPosition); float distanceToForceFieldRadius = forceFieldRadiusAbs - distanceToParticle; distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0001); distanceToForceFieldRadius *= sign(forceFieldRadius); float4 particleOffset; particleOffset.xyz = directionToParticle * distanceToForceFieldRadius; particleOffset.w = distanceToForceFieldRadius / (forceFieldRadius + 0.0001); // Add small value to prevent divide by zero and undefined colour/behaviour at r = 0.0. minDistanceToParticle = min(minDistanceToParticle, distanceToParticle); return particleOffset; }
我们可以修改顶点着色器部分,使脚本在均匀半径开关打开时,持续更新并使用最小距离。下面代码的红色行是改动的着色器代码。
我们将最小距离变量初始化为一个较大数值,这样后续迭代能保证返回较小数值。我们在代码中使用了99999.0。
v2f o; float3 particleCenter = float3(v.tc0.zw, v.tc1.x); float minDistanceToParticle = 99999.0; float3 vertexOffset = 0.0; for (int i = 0; i < _ForceFieldCount; i++) { vertexOffset += GetParticleOffset(particleCenter, _ForceFields[i], minDistanceToParticle).xyz; } #ifdef _USEUNIFORMRADIUS_ON float3 normalizedVertexOffset = normalize(vertexOffset); float uniformRadiusAbs = abs(_UniformRadius); float minDistanceToUniformRadius = max(uniformRadiusAbs - minDistanceToParticle, 0.0); uniformRadiusAbs *= sign(_UniformRadius); vertexOffset = normalizedVertexOffset * minDistanceToUniformRadius; #endif v.vertex.xyz += vertexOffset; o.vertex = UnityObjectToClipPos(v.vertex);
我们也需要对片段部分进行类似的改动。
col *= i.color; float3 particleCenter = float3(i.tc0.zw, i.tc1.x); float minDistanceToParticle = 99999.0; float maxNormalizedOffset = 0.0; for (int i = 0; i < _ForceFieldCount; i++) { maxNormalizedOffset = max(maxNormalizedOffset, GetParticleOffset(particleCenter, _ForceFields[i], minDistanceToParticle).w); } col = lerp(col * _ColourA, col * _ColourB, maxNormalizedOffset); col *= col.a;
着色器现在可以正常使用,我们可以在编辑器看到选项开关。
下面是Use Uniform Radius选项开启和关闭时的不同效果。
下面我们只需要更新C#组件,就可以生成并绘制均匀球体。
void OnDrawGizmos() { bool useUniformRadius = material.GetFloat("_UseUniformRadius") == 1.0f ? true : false; float uniformRadius = material.GetFloat("_UniformRadius"); for (int i = 0; i < transform.childCount; i++) { Transform childTransform = transform.GetChild(i); float radius = useUniformRadius ? uniformRadius : (childTransform.lossyScale.x / 2.0f); Gizmos.DrawWireSphere(childTransform.position, radius); } }
这样就实现了我们想要的效果。

着色器代码

下面是完整的着色器代码。
Shader "Custom/Particles/GPU Force Fields Unlit (Tutorial)" { Properties { _MainTex("Texture", 2D) = "white" {} [Toggle(_USEUNIFORMRADIUS_ON)] _UseUniformRadius("Use Uniform Radius", Float) = 0.0 _UniformRadius("Uniform Radius", Range(-10.0, 10.0)) = 1.0 [HDR] _ColourA("Color A", Color) = (0.0, 0.0, 0.0, 0.0) [HDR] _ColourB("Color B", Color) = (1.0, 1.0, 1.0, 1.0) } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Opaque" } LOD 100 Blend One One // 加法混合 ZWrite Off //关闭深度测试 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag //实现模糊效果 #pragma multi_compile_fog #pragma shader_feature _USEUNIFORMRADIUS_ON #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; fixed4 color : COLOR; float4 tc0 : TEXCOORD0; float4 tc1 : TEXCOORD1; }; struct v2f { float4 tc0 : TEXCOORD0; float4 tc1 : TEXCOORD1; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; fixed4 color : COLOR; }; sampler2D _MainTex; float4 _MainTex_ST; int _ForceFieldCount; float4 _ForceFields[8]; float _UniformRadius; float4 _ColourA; float4 _ColourB; float4 GetParticleOffset(float3 particleCenter, float4 forceField, inout float minDistanceToParticle) { float forceFieldRadius; float3 forceFieldPosition = forceField.xyz; #ifdef _USEUNIFORMRADIUS_ON forceFieldRadius = _UniformRadius + 0.0001; #else forceFieldRadius = forceField.w; #endif float distanceToParticle = distance(particleCenter, forceFieldPosition); float forceFieldRadiusAbs = abs(forceFieldRadius); float3 directionToParticle = normalize(particleCenter - forceFieldPosition); float distanceToForceFieldRadius = forceFieldRadiusAbs - distanceToParticle; distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0001); distanceToForceFieldRadius *= sign(forceFieldRadius); float4 particleOffset; particleOffset.xyz = directionToParticle * distanceToForceFieldRadius; particleOffset.w = distanceToForceFieldRadius / (forceFieldRadius + 0.0001); //添加小数来避免被除数为0,以及在r=0.0时出现未定义的颜色或行为。 minDistanceToParticle = min(minDistanceToParticle, distanceToParticle); return particleOffset; } v2f vert(appdata v) { v2f o; float3 particleCenter = float3(v.tc0.zw, v.tc1.x); float minDistanceToParticle = 99999.0; float3 vertexOffset = 0.0; for (int i = 0; i < _ForceFieldCount; i++) { vertexOffset += GetParticleOffset(particleCenter, _ForceFields[i], minDistanceToParticle).xyz; } #ifdef _USEUNIFORMRADIUS_ON float3 normalizedVertexOffset = normalize(vertexOffset); float uniformRadiusAbs = abs(_UniformRadius); float minDistanceToUniformRadius = max(uniformRadiusAbs - minDistanceToParticle, 0.0); uniformRadiusAbs *= sign(_UniformRadius); vertexOffset = normalizedVertexOffset * minDistanceToUniformRadius; #endif v.vertex.xyz += vertexOffset; o.vertex = UnityObjectToClipPos(v.vertex); //从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色 o.color = v.color; o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex); // 初始化tex coord变量 o.tc0.zw = v.tc0.zw; o.tc1 = v.tc1; UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag(v2f i) : SV_Target { // 采样纹理 fixed4 col = tex2D(_MainTex, i.tc0); // 让纹理颜色和粒子系统的顶点颜色输入相乘 col *= i.color; float3 particleCenter = float3(i.tc0.zw, i.tc1.x); float minDistanceToParticle = 99999.0; float maxNormalizedOffset = 0.0; for (int i = 0; i < _ForceFieldCount; i++) { maxNormalizedOffset = max(maxNormalizedOffset, GetParticleOffset(particleCenter, _ForceFields[i], minDistanceToParticle).w); } col = lerp(col * _ColourA, col * _ColourB, maxNormalizedOffset); col *= col.a; // 应用模糊效果 UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }

小结

实现多重GPU粒子力场就介绍到这里,希望大家学以致用,牢牢掌握这些制作粒子特效的基础,从而制作出精美的特效。
更多Unity教程,尽在Unity官方中文论坛(UnityChina.cn)。
原文来源:mirzabeig.com

Unity China
492
Comments