粒子特效教程 | GPU粒子力场
我们将继续分享加拿大游戏特效大神Mirza Beig的粒子特效的系列教程,趁着春节假期的空闲,来梳理一下如何学习制作精美的粒子特效吧。
往期教程回顾:
在本教程中,我们将学习使用Unity粒子系统制作球体GPU力场。下图是我们将制作的效果预览。
Part 1:粒子系统
为了能够预览我们的效果, 需要一个用于测试的粒子系统,只需布满白色粒子的平面场即可。
我们创建一个新粒子系统,重置它的Transform组件。在Main模块中,勾选Prewarm,将Start Speed设为0,使Start Size在0.25~ 0.3之间随机取值,Max Particles设为10,000。
将Emission模块的Rate over Time设为2,000。
将Shape设为Box,Scale设为(25, 0, 25)。
现在我们得到了基本的平面场,现在仅需启用自定义顶点流,添加Center流,和之前一样,请无视警告信息,一旦我们使用新的着色器分配新材质,警告会自动消失。
至此,我们的预设阶段就完成了。
Part 2:顶点着色器
使用《伴随Simplex噪声的GPU粒子动画》教程中扩展基础着色器的代码来创建一个新着色器,下面是只修改了部分名称的代码内容。
Shader "Custom/Particles/GPU Force Field Unlit (Tutorial)"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
}
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
#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;
v2f vert(appdata v)
{
v2f o;
float3 vertexOffset = 0;
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;
col *= col.a;
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我们要创建一个球体力场,由于球体由半径和世界空间位置定义,所以我们要添加这二个额外的属性。
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)
}
添加着色器的关联变量。
sampler2D _MainTex;
float4 _MainTex_ST;
float _ForceFieldRadius;
float3 _ForceFieldPosition;
创建一个新函数,它会接收粒子位置或中心点,返回float3值,即用x、y和z定义的位置。我们最终需要顶点和片段部分的结果,所以不必重复编写相同代码,只要将该效果的代码添加到函数中即可。
float3 GetParticleOffset(float3 particleCenter)
{
}
力场的基本逻辑如下:
if (particle is within force field)
{
move particle to edge of force field (radius)
}
我们可以通过检查球体中心和粒子位置间的距离是否小于球体半径,判断粒子位置是否在球体之中。
float distanceToParticle = distance(particleCenter, _ForceFieldPosition);
如果距离小于力场半径,我们会进行处理。
float3 GetParticleOffset(float3 particleCenter)
{
float distanceToParticle = distance(particleCenter, _ForceFieldPosition);
if (distanceToParticle < _ForceFieldRadius)
{
}
}
在if语句中,我们需要获取粒子到力场边缘的距离,并使用半径方向,向外移动该距离的长度。
float distanceToForceFieldRadius = _ForceFieldRadius - distanceToParticle;
float3 directionToParticle = normalize(particleCenter - _ForceFieldPosition);
return directionToParticle * distanceToForceFieldRadius;
如果粒子不在力场内,会返回0,即没有偏移,等价于float3(0.0, 0.0, 0.0),这样我们的偏移计算函数就完成了。
float3 GetParticleOffset(float3 particleCenter)
{
float distanceToParticle = distance(particleCenter, _ForceFieldPosition);
if (distanceToParticle < _ForceFieldRadius)
{
float distanceToForceFieldRadius = _ForceFieldRadius - distanceToParticle;
float3 directionToParticle = normalize(particleCenter - _ForceFieldPosition);
return directionToParticle * distanceToForceFieldRadius;
}
return 0;
}
我们可以在顶点着色器使用该函数,从TEXCOORD流获取粒子中心位置,将位置传入偏移函数,然后使用返回值作为偏移量。
v2f vert(appdata v)
{
v2f o;
float3 particleCenter = float3(v.tc0.zw, v.tc1.x);
float3 vertexOffset = GetParticleOffset3(particleCenter);
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 3:片段着色器
现在给粒子系统添加颜色,类似上一篇教程,我们将基于标准化偏移或位移值来插补颜色。首先添加合适的属性和变量。
材质属性:
_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)
着色器变量:
float _ForceFieldRadius;
float3 _ForceFieldPosition;
float4 _ColourA;
float4 _ColourB;
标准化偏移值是指粒子和力场之间的距离,以力场半径为标准值。如果我们将函数返回类型改为float4,我们可以在xyz中保存偏移值,在w中保存标准化偏移标量。
float4 GetParticleOffset(float3 particleCenter)
{
float distanceToParticle = distance(particleCenter, _ForceFieldPosition);
if (distanceToParticle < _ForceFieldRadius)
{
float distanceToForceFieldRadius = _ForceFieldRadius - distanceToParticle;
float3 directionToParticle = normalize(particleCenter - _ForceFieldPosition);
float4 particleOffset;
particleOffset.xyz = directionToParticle * distanceToForceFieldRadius;
particleOffset.w = distanceToForceFieldRadius / _ForceFieldRadius;
return particleOffset;
}
return 0;
}
然后在片段函数中,只要检索数值并用它插补在二个颜色之间即可。
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 particleOffsetNormalizedLength = GetParticleOffset2(particleCenter).w;
col = lerp(col * _ColourA, col * _ColourB, particleOffsetNormalizedLength);
col *= col.a;
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
现在只要稍作调整,我们就可以看到彩色的粒子系统。
Part 4:优化和扩展功能
在前面部分,我们使着色器代码尽可能简单,但我们可以修改部分代码,从而更好地符合GPU编程时的最佳实践,并添加负半径值的支持。
首先,我们可以通过获取粒子到力场距离和0之间的较大值,从而去掉if语句。因为如果粒子到力场距离大于半径,即粒子在力场外,我们会得到一个负值,负值比0小,因此会得到0。在GPU的超级并行状态时,我们要避免分支结构,以顺利传输数据。
一个小细节是在将半径用作除数时,我们给半径加了一个小数,从而防止在半径为0时出现未定义的行为。
float4 GetParticleOffset(float3 particleCenter)
{
float distanceToParticle = distance(particleCenter, _ForceFieldPosition);
float3 directionToParticle = normalize(particleCenter - _ForceFieldPosition);
float distanceToForceFieldRadius = _ForceFieldRadius - distanceToParticle;
distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0);
float4 particleOffset;
particleOffset.xyz = directionToParticle * distanceToForceFieldRadius;
particleOffset.w = distanceToForceFieldRadius / (_ForceFieldRadius + 0.0001); // 添加小数来避免除数为0,以及在r=0.0时出现未定义的颜色或行为。
return particleOffset;
}
然后,我们会允许使用负半径值,这样不会远离力场中心移动粒子,而是将粒子向粒子中心吸引。我们首先将半径处理为绝对值,将它乘以sign函数,再将结果用于调整偏移方向。
float4 GetParticleOffset(float3 particleCenter)
{
float distanceToParticle = distance(particleCenter, _ForceFieldPosition);
float forceFieldRadiusAbs = abs(_ForceFieldRadius);
float3 directionToParticle = normalize(particleCenter - _ForceFieldPosition);
float distanceToForceFieldRadius = forceFieldRadiusAbs - distanceToParticle;
distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0);
distanceToForceFieldRadius *= sign(_ForceFieldRadius);
float4 particleOffset;
particleOffset.xyz = directionToParticle * distanceToForceFieldRadius;
particleOffset.w = distanceToForceFieldRadius / (_ForceFieldRadius + 0.0001); // 添加小数来避免除数为0,以及在r=0.0时出现未定义的颜色或行为。
return particleOffset;
}
这样,反向力场制作完成。
下面是完整的着色器代码。
Shader "Custom/Particles/GPU Force Field Unlit (Tutorial)"
{
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)
}
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
#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;
float _ForceFieldRadius;
float3 _ForceFieldPosition;
float4 _ColourA;
float4 _ColourB;
float4 GetParticleOffset(float3 particleCenter)
{
float distanceToParticle = distance(particleCenter, _ForceFieldPosition);
float forceFieldRadiusAbs = abs(_ForceFieldRadius);
float3 directionToParticle = normalize(particleCenter - _ForceFieldPosition);
float distanceToForceFieldRadius = forceFieldRadiusAbs - distanceToParticle;
distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0);
distanceToForceFieldRadius *= sign(_ForceFieldRadius);
float4 particleOffset;
particleOffset.xyz = directionToParticle * distanceToForceFieldRadius;
particleOffset.w = distanceToForceFieldRadius / (_ForceFieldRadius + 0.0001); //添加小数来避免除数为0,以及在r=0.0时出现未定义的颜色或行为。
return particleOffset;
}
v2f vert(appdata v)
{
v2f o;
float3 particleCenter = float3(v.tc0.zw, v.tc1.x);
float3 vertexOffset = GetParticleOffset(particleCenter);
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 particleOffsetNormalizedLength = GetParticleOffset(particleCenter).w;
col = lerp(col * _ColourA, col * _ColourB, particleOffsetNormalizedLength);
col *= col.a;
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 5:GPU力场游戏对象
我们可以按现在的样子使用力场,但现在使用它并不是很直观。我们要编写一个小脚本,把力场作为一个游戏对象以便进行控制,这样我们能够轻松地在场景中将力场可视化,像处理普通对象一样进行缩放。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class GPUParticleForceField : MonoBehaviour
{
public Material material;
void LateUpdate()
{
material.SetFloat("_ForceFieldRadius", transform.lossyScale.x);
material.SetVector("_ForceFieldPosition", transform.position);
}
void OnDrawGizmos()
{
Gizmos.DrawWireSphere(transform.position, transform.lossyScale.x);
}
}
下面来详解代码。
ExecuteInEditMode属性允许脚本在编辑器未处于运行模式时执行,这样我们就可以使用脚本并即时查看结果。
我们定义了公开引用,用于指定使用CPU对象的力场材质。然后在LateUpdate函数中设置材质的半径和位置属性,以便应用于对该对象的任何改动和移动。
标量浮点半径和向量位置分别由对象Transform(X轴)的世界坐标大小和位置进行设置。世界坐标大小指总体大小,所以我们将该值除以2来用作半径,否则我们会传入直径作为半径。
我们使用OnDrawGizmos函数以及Transform位置和X轴的世界坐标大小,绘制球体线条来表示球体大小。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class GPUParticleForceField : MonoBehaviour
{
public Material material;
void LateUpdate()
{
material.SetFloat("_ForceFieldRadius", transform.lossyScale.x / 2.0f);
material.SetVector("_ForceFieldPosition", transform.position);
}
void OnDrawGizmos()
{
Gizmos.DrawWireSphere(transform.position, transform.lossyScale.x / 2.0f);
}
}
现在将该脚本添加到游戏对象,并将材质拖到Material栏,我们就得到了下图的效果。
Part 6:粒子噪声
现在我们已经大致完成了,下面对粒子系统进行一些调整,使它获得预览图的效果。
启用Colour over Lifetime模块,应用快速浅入-维持原状-以合适时长淡出的变化效果。
启用Noise模块,将Frequency设为0.15,Scroll Speed设为0.25。
现在我们得了预览图的效果。
小结
本教程结束了,这些都是熟练掌握制作精美粒子特效的基础,希望大家要熟练掌握起来。在下一篇教程中,我们将学习如何添加多个GPU力场的支持,敬请期待。
更多教程文章,尽在Unity官方中文论坛(UnityChina.cn)!
原文来源:mirzabeig.com
推荐阅读
Unite Shanghai 2019
Unite Shanghai 2019技术门票热销中
2019年5月10日-12日上海,Unite大会强势回归。技术门票正在热销中,购票即获指定Asset Store资源商店精品21款资源的5折优惠券。[了解详情...]
大会官网
http://Unite2019.csdn.net
Unite 2019 | Training Day开发者训练营课程曝光
5月10日将举行二场最受开发者欢迎的Training Day开发者训练营活动,了解训练营的精彩课程。
购票地址:Unite2019.csdn.net
点击“阅读原文”访问Unity官方中文论坛