查看原文
其他

粒子特效教程 | GPU粒子力场

Unity Unity官方平台 2022-05-07

我们将继续分享加拿大游戏特效大神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官方中文论坛

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存