Notifications
Article
粒子特效教程 | GPU粒子力场
Published 7 months ago
1.5 K
3
我们将继续分享加拿大游戏特效大神Mirza Beig的粒子特效的系列教程,趁着春节假期的空闲,来梳理一下如何学习制作精美的粒子特效吧。
往期教程回顾:
  • 使用粒子实现Logo消融效果
  • 倒放粒子系统
  • 使用粒子实现Logo显现效果
  • 创建3D均匀粒子网格
  • 创作绚丽的粒子星系(上)
  • 创作绚丽的粒子星系(下)
  • Unity自定义粒子顶点流
  • 伴随Simplex噪声的GPU粒子动画
在本教程中,我们将学习使用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
Unity China
671
Comments
Rs
Rahul sharma
12 days ago
I've also seen something like this here:
0
z
zhaoge
2 months ago
wonderful
0