Part 3:Simplex噪声(顶点动画)

这一部分是本教程的重点。我们将在此使用Keijiro Takahashi的HLSL实现方法在Unity实现Simplex噪声。

请访问GitHub下载代码库,它包含多个不同的噪声函数,但我们只需要3D Simplex噪声,即SimplexNoise3D.hlsl。

Download

为了能够在着色器中使用噪声函数,我们需要通过include引用该代码。

#include "UnityCG.cginc"
#include "SimplexNoise3D.hlsl"

我们将添加一些参数,用来控制噪声速度,即在3D空间中滚动偏移的速度、频率、振幅和负范围限定。我们将这些参数作为属性添加到着色器中。

_MainTex ("Texture", 2D) = "white" {}
 
_NoiseSpeedX("Noise Speed X", Range(0 , 100)) = 0.0
_NoiseSpeedY("Noise Speed Y", Range(0 , 100)) = 0.0
_NoiseSpeedZ("Noise Speed Z", Range(0 , 100)) = 1.0
 
_NoiseFrequency("Noise Frequency", Range(0 , 1)) = 0.1
_NoiseAmplitude("Noise Amplitude", Range(0 , 10)) = 2.0
  
_NoiseAbs("Noise Abs", Range(0 , 1)) = 1.0

我们还需要添加匹配的着色器变量。

sampler2D _MainTex;
float4 _MainTex_ST; 
 
float _NoiseSpeedX;
float _NoiseSpeedY;
float _NoiseSpeedZ;
 
float _NoiseFrequency;
float _NoiseAmplitude;
 
float _NoiseAbs;

现在我们可以处理顶点部分即在添加偏移前,从而为粒子设置动画。请记住,粒子的中心向量在TEXCOORD0和TEXCOORD1中混合。
「U3D」伴随Simplex噪声的GPU粒子动画

中心向量会转换为存在tc0.zw的中心X和Y,以及存在tc0.x的Z。你可能会想,如果我们要像之前教程那样偏移顶点,为什么我们还要传入Center顶点流?

这是因为粒子顶点会根据粒子的世界空间位置在最初被偏移,然后用作噪声函数的输入。随着大量粒子在足够大的区域中扩散,我们可以看到平滑的噪声分布,并使它随着时间变化。

我们不能直接传入顶点位置,因为粒子是带有至少三个顶点的多边形,每个顶点都是空间中的不同点。如果我们使用了顶点,那么我们要根据顶点获取不同的偏移,这样在网格变形时,会得到非常奇怪的粒子。

这不是我们想要的效果,我们希望整个粒子进行移动,这意味着要以相同的偏移来一起移动它的所有顶点,所以我们使用粒子的中心位置。

float3 particleCenter = float3(v.tc0.zw, v.tc1.x);

我们可以通过time * speedZ计算出3D噪声偏移。虽然我们可以为所有轴添加单独的噪声速度,但我们不想让噪声在X轴和Y轴变化。

float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);

现在我们可以计算单维噪声值。首先传入particleCenter,添加偏移,然后将结果乘以频率属性。

float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);

snoise函数会提供范围在[-1.0, 1.0]的数值。因为我们想要将该数值重映射为[0.0, 1.0]的范围,然后在范围间进行混合,所以我们可以添加代码来实现这个过程,代码首先会重新映射,然后使用lerp函数来进行混合。

float noise01 = (noise + 1.0) / 2.0;
float noiseRemap = lerp(noise, noise01, _NoiseAbs);

顶点偏移会在世界空间Y中计算noiseRemap * _NoiseAmplitude 。

float3 vertexOffset = float3(0.0, noiseRemap * _NoiseAmplitude, 0.0);
v.vertex.xyz += vertexOffset;

顶点动画的处理到这就完成了,下一部分我们将会根据噪声值设置颜色的变化。我们返回到Unity中查看结果,下面的示例效果可以通过调整粒子系统Main模块的Start Color来实现。
「U3D」伴随Simplex噪声的GPU粒子动画

Part 4:Simplex噪声(片段/像素颜色动画)

我们已经拥有代码来根据噪声制作顶点动画,所以这部分差不多就要完成了。首先添加二个额外属性和全局变量,用于根据重新映射的[0.0, 1.0]范围噪声值来处理插补的颜色。

材质属性会添加到着色器文件的顶部,,HDR标签能确保我们可以大幅增大进入HDR范围的强度,从而允许泛光等后期处理效果能和着色器搭配使用。

[HDR] _ColourA("Color A", Color) = (0,0,0,0)
[HDR] _ColourB("Color B", Color) = (1,1,1,1)

着色器变量添加在噪声变量下。

float4 _ColourA;
float4 _ColourB;

最后在片段部分,在将col乘以输入顶点颜色的代码和将col乘以结合Alpha值的代码之间,添加下面的代码。

col *= i.color;
 
float3 particleCenter = float3(i.tc0.zw, i.tc1.x);
float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);
 
float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);
float noise01 = (noise + 1.0) / 2.0;
 
col = lerp(col * _ColourA, col * _ColourB, noise01);
col *= col.a;

你可以注意到,大部分代码是从顶点部分复制粘贴得来,保存脚本,使用i.tc0和i.tc1来从输入获取流,并使用lerp函数来在颜色间插补。

这些代码只是少量修改了顶点动画部分的代码,下面是得到的效果。
「U3D」伴随Simplex噪声的GPU粒子动画

这样我们就处理好着色器了,在下一部分,我们将使用现有的代码实现预览图像。下面是完整的着色器代码。

Shader "Unlit/Simplex Noise Particle Unlit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
 
        _NoiseSpeedX("Noise Speed X", Range(0 , 100)) = 0.0
        _NoiseSpeedY("Noise Speed Y", Range(0 , 100)) = 0.0
        _NoiseSpeedZ("Noise Speed Z", Range(0 , 100)) = 1.0
 
        _NoiseFrequency("Noise Frequency", Range(0 , 1)) = 0.1
        _NoiseAmplitude("Noise Amplitude", Range(0 , 10)) = 2.0
 
        _NoiseAbs("Noise Abs", Range(0 , 1)) = 1.0
 
        [HDR] _ColourA("Color A", Color) = (0,0,0,0)
        [HDR] _ColourB("Color B", Color) = (1,1,1,1)
    }
 
    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"
            #include "SimplexNoise3D.hlsl"
 
            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 _NoiseSpeedX;
            float _NoiseSpeedY;
            float _NoiseSpeedZ;
 
            float _NoiseFrequency;
            float _NoiseAmplitude;
 
            float _NoiseAbs;
            float4 _ColourA;
            float4 _ColourB;
 
            v2f vert (appdata v)
            {
                v2f o;
 
                float3 particleCenter = float3(v.tc0.zw, v.tc1.x);
                float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);
 
                float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);
 
                float noise01 = (noise + 1.0) / 2.0;
                float noiseRemap = lerp(noise, noise01, _NoiseAbs);
 
                float3 vertexOffset = float3(0.0, noiseRemap * _NoiseAmplitude, 0.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;
  
                float3 particleCenter = float3(i.tc0.zw, i.tc1.x);
                float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);
 
                float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);
                float noise01 = (noise + 1.0) / 2.0;
 
                col = lerp(col * _ColourA, col * _ColourB, noise01); 
                col *= col.a;
 
                // 应用模糊效果
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}