请选择 进入手机版 | 继续访问电脑版

查看: 227|回复: 1

[技术] 粒子特效教程 | GPU粒子力场

[复制链接]

1278

主题

1985

帖子

2万

贡献

管理员

Rank: 9Rank: 9Rank: 9

积分
26195
QQ
发表于 2019-2-9 04:57:14 | 显示全部楼层 |阅读模式
我们将继续分享加拿大游戏特效大神Mirza Beig的粒子特效的系列教程,趁着春节假期的空闲,来梳理一下如何学习制作精美的粒子特效吧。

往期教程回顾:
1、使用粒子实现Logo消融效果
2、倒放粒子系统
3、使用粒子实现Logo显现效果
4、创建3D均匀粒子网格
5、创作绚丽的粒子星系(上)
6、创作绚丽的粒子星系(下)
7、Unity自定义粒子顶点流
8、伴随Simplex噪声的GPU粒子动画

在本教程中,我们将学习使用Unity粒子系统制作球体GPU力场。下图是我们将制作的效果预览。

01.gif


Part 1:粒子系统
为了能够预览我们的效果, 需要一个用于测试的粒子系统,只需布满白色粒子的平面场即可。

02.png


我们创建一个新粒子系统,重置它的Transform组件。在Main模块中,勾选Prewarm,将Start Speed设为0,使Start Size在0.25~ 0.3之间随机取值,Max Particles设为10,000。

03.png


将Emission模块的Rate over Time设为2,000。

04.png


将Shape设为Box,Scale设为(25, 0, 25)。

05.png


现在我们得到了基本的平面场,现在仅需启用自定义顶点流,添加Center流,和之前一样,请无视警告信息,一旦我们使用新的着色器分配新材质,警告会自动消失。

至此,我们的预设阶段就完成了。

06.png


Part 2:顶点着色
使用《伴随Simplex噪声的GPU粒子动画》教程中扩展基础着色器的代码来创建一个新着色器,下面是只修改了部分名称的代码内容。
[C#] 纯文本查看 复制代码
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
        }
    }
}


我们要创建一个球体力场,由于球体由半径和世界空间位置定义,所以我们要添加这二个额外的属性。
[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)
}


添加着色器的关联变量。
[C#] 纯文本查看 复制代码
sampler2D _MainTex;
float4 _MainTex_ST;

float _ForceFieldRadius;
float3 _ForceFieldPosition;


创建一个新函数,它会接收粒子位置或中心点,返回float3值,即用x、y和z定义的位置。我们最终需要顶点和片段部分的结果,所以不必重复编写相同代码,只要将该效果的代码添加到函数中即可。
[C#] 纯文本查看 复制代码
float3 GetParticleOffset(float3 particleCenter)
{

}


力场的基本逻辑如下:
[C#] 纯文本查看 复制代码
if (particle is within force field)
{
    move particle to edge of force field (radius)
}


我们可以通过检查球体中心和粒子位置间的距离是否小于球体半径,判断粒子位置是否在球体之中。
[C#] 纯文本查看 复制代码
float distanceToParticle = distance(particleCenter, _ForceFieldPosition);



如果距离小于力场半径,我们会进行处理。
[C#] 纯文本查看 复制代码
float3 GetParticleOffset(float3 particleCenter)
{
    float distanceToParticle = distance(particleCenter, _ForceFieldPosition);

    if (distanceToParticle < _ForceFieldRadius)
    {

    }
}


在if语句中,我们需要获取粒子到力场边缘的距离,并使用半径方向,向外移动该距离的长度。
[C#] 纯文本查看 复制代码
float distanceToForceFieldRadius = _ForceFieldRadius - distanceToParticle;
float3 directionToParticle = normalize(particleCenter - _ForceFieldPosition);

return directionToParticle * distanceToForceFieldRadius;


如果粒子不在力场内,会返回0,即没有偏移,等价于float3(0.0, 0.0, 0.0),这样我们的偏移计算函数就完成了。
[C#] 纯文本查看 复制代码
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流获取粒子中心位置,将位置传入偏移函数,然后使用返回值作为偏移量。
[C#] 纯文本查看 复制代码
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;
}


使用该着色器创建新材质,并将其指定给粒子系统。现在我们应该可以进行如下操作。

07.gif


Part 3:片段着色器
现在给粒子系统添加颜色,类似上一篇教程,我们将基于标准化偏移或位移值来插补颜色。首先添加合适的属性和变量。

材质属性:
[C#] 纯文本查看 复制代码
_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)


着色器变量:
[C#] 纯文本查看 复制代码
float _ForceFieldRadius;
float3 _ForceFieldPosition;

float4 _ColourA;
float4 _ColourB;


标准化偏移值是指粒子和力场之间的距离,以力场半径为标准值。如果我们将函数返回类型改为float4,我们可以在xyz中保存偏移值,在w中保存标准化偏移标量。
[C#] 纯文本查看 复制代码
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;
}

然后在片段函数中,只要检索数值并用它插补在二个颜色之间即可。
[C#] 纯文本查看 复制代码
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;
}


现在只要稍作调整,我们就可以看到彩色的粒子系统。

08.gif


Part 4:优化和扩展功能
在前面部分,我们使着色器代码尽可能简单,但我们可以修改部分代码,从而更好地符合GPU编程时的最佳实践,并添加负半径值的支持。

首先,我们可以通过获取粒子到力场距离和0之间的较大值,从而去掉if语句。因为如果粒子到力场距离大于半径,即粒子在力场外,我们会得到一个负值,负值比0小,因此会得到0。在GPU的超级并行状态时,我们要避免分支结构,以顺利传输数据。

一个小细节是在将半径用作除数时,我们给半径加了一个小数,从而防止在半径为0时出现未定义的行为。
[C#] 纯文本查看 复制代码
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函数,再将结果用于调整偏移方向。
[C#] 纯文本查看 复制代码
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;
}


这样,反向力场制作完成。

09.gif


下面是完整的着色器代码。
[C#] 纯文本查看 复制代码
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力场游戏对象
我们可以按现在的样子使用力场,但现在使用它并不是很直观。我们要编写一个小脚本,把力场作为一个游戏对象以便进行控制,这样我们能够轻松地在场景中将力场可视化,像处理普通对象一样进行缩放。
[C#] 纯文本查看 复制代码
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轴的世界坐标大小,绘制球体线条来表示球体大小。
[C#] 纯文本查看 复制代码
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栏,我们就得到了下图的效果。

10.gif


Part 6:粒子噪声
现在我们已经大致完成了,下面对粒子系统进行一些调整,使它获得预览图的效果。

启用Colour over Lifetime模块,应用快速浅入-维持原状-以合适时长淡出的变化效果。

11.png


启用Noise模块,将Frequency设为0.15,Scroll Speed设为0.25。

12.png


现在我们得了预览图的效果。



小结
本教程结束了,这些都是熟练掌握制作精美粒子特效的基础,希望大家要熟练掌握起来。在下一篇教程中,我们将学习如何添加多个GPU力场的支持,敬请期待。

更多教程文章,尽在Unity官方中文论坛(UnityChina.cn)!


1

主题

24

帖子

245

贡献

初级UU族—3级

Rank: 3Rank: 3

积分
245
发表于 2019-2-9 16:50:29 | 显示全部楼层
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表