粒子特效教程 | 多重GPU粒子力场
我们将分享加拿大游戏特效大神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趣味问答,赢取新年礼物 (最后一天)
2月16日18点前,参加Unity趣味问答,赢取限量Unity纪念礼物。了解详情,请点击此处。
Asset Store新春特惠
2月23日前,Asset Store资源商店将进行新春特惠活动,全场资源9折,更有顶级精品资源7折,赶紧选购吧。了解详情,请点击此处。
Obstacle Tower挑战赛正式开启
Unity举办的Obstacle Tower挑战赛现在正式开启,此次比赛将为富有挑战的全新任务训练出最佳性能的代理。了解详情,请点击此处。
了解更多比赛流程,并开始训练自己的代理,请访问:
https://www.unity3d.com/OTC
Unite Shanghai 2019
5月10日-12日上海,Unite大会强势回归。技术门票正在热销中,购票即获指定Asset Store资源商店精品21款资源的5折优惠券。
购票请访问:Unite2019.csdn.net
点击“阅读原文”访问Unity官方中文论坛