Unity自定義粒子頂點流
在本教程中,我們將學習如何在Unity粒子系統(tǒng)中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統(tǒng)的Renderer模塊來設置,它可以將額外的單個粒子數(shù)據(jù)傳遞到著色器。著色器可以使用該數(shù)據(jù),為系統(tǒng)中的每個粒子創(chuàng)建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現(xiàn)的效果不是非常驚艷,但它為后續(xù)教程中學習創(chuàng)作精美特效奠定基礎。

Part 1:基礎部分
首先,我們使用Unity模板創(chuàng)建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇Create -> Shader -> Unlit Shader。

我們將該文件命名為Simple Particle Unlit,代碼如下圖所示。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現(xiàn)模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct APPdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (APPdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
創(chuàng)建新材質,指定著色器,然后設置紋理屬性為默認粒子紋理。

現(xiàn)在我們創(chuàng)建粒子系統(tǒng),指定該材質到粒子系統(tǒng)Renderer模塊的Materials字段。

我們會得到下圖的效果。

圖中的效果存在一些透明度問題,我們稍后會解決這些問題。
在相同粒子系統(tǒng)的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然后單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化范圍[0.0, 1.0]內粒子的“生命周期”。0.0表示粒子生成時間,0.5表示粒子生命周期終點,1.0表示粒子消逝時間。

我們忽略頂點流與著色器輸入不匹配紅色警告信息,現(xiàn)在將頂點流傳給著色器,我們需要接收并處理頂點流的數(shù)據(jù)。
在頂點流顯示中,可以注意到數(shù)據(jù)已被緊湊地打包了。實際2D UV坐標位于TEXCOORD0.xy,AgePercent數(shù)據(jù)位于TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數(shù)據(jù)。

每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位于TEXCOORD0.w。如果數(shù)據(jù)比當前texcoord的可用空間大,它會作為余下部分移動到下一個texcoord,例如texcoord1或texcoord2等。
下圖是相應的設置案例,里面沒有添加這些頂點流。

我們可以看到,InverseStartLifetime(1D值)被添加到TEXCOORD0.w,Center(3D值)被添加到TEXCOORD1.xyz,Rotation3D(3D值)一部分被添加到TEXCOORD1 (w)。另一部分被添加到TEXCOORD2 (xy)。(w|xy)表示xy屬于下一個texcoord,即TEXCOORD2,盡管它顯示TEXCOORD1.w|xy。因此Velocity從TEXCOORD2.zw開始,而Rotation3D有一部分存在TEXCOORD1.xy中,Velocity也有一部分存在TEXCOORD3 (x)中。
這可能有點難理解,因為Rotation3D的xyz值存在TEXCOORD1.w(Rotation3D的x)和TEXCOORD2.xy(Rotation3D的yz)中。它類似對Velocity中xyz的處理,Velocity的xyz存在TEXCOORD2.zw (xy)和TEXCOORD3.x (z)中。
現(xiàn)在關注AgePercent,回到我們的自定義著色器,開始進行處理。
該著色器只處理TEXCOORD0的x和y以獲得實際紋理的UV坐標。AgePercent位于TEXCOORD0.z,因此我們需要在頂點輸入和輸出結構,分別為APPdata和v2f將float2改為float3。
查看下面的代碼,了解改動內容。
struct APPdata
{
float4 vertex : POSITION;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
接下來,我們需要在著色器的頂點部分初始化UV,使UV在被傳遞到片段部分前包含合適的數(shù)值,這些“部分”其實是一個.shader文件中的頂點著色器和片段/像素著色器。在柵格化前,先處理頂點操作。
v2f vert (APPdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化當前uv.z(包含粒子的壽命百分比)
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
最后在片段部分,即給對象上色的像素著色器部分,我們可以利用該數(shù)據(jù)。下面代碼中,我們根據(jù)粒子壽命,使用該數(shù)據(jù)向紋理(col)的粒子插補紅色。
fixed4 frag (v2f i) : SV_Target
{
// 采樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據(jù)粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經(jīng)過修改后,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現(xiàn)模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct APPdata
{
float4 vertex : POSITION;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (APPdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化當前uv.z(包含粒子的壽命百分比)
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據(jù)粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它并不驚艷,但這僅只是開始。

Part 2:透明度,深度測試及渲染隊列
繼續(xù)下一步前,我們先解決之前出現(xiàn)的問題,從透明度開始。為了從輸入紋理獲取合適的Alpha值,只需添加混合模式即可。
我們可以選擇常用命令,例如:加法(One One命令),Alpha混合(SrcAlpha OneMinusSrcAlpha)和Alpha混合預乘(One OneMinusSrcAlpha)。對黑色背景上紋理(例如默認粒子紋理)的最好選擇是加法和預乘。
我們選擇加法,因為它最直接,在黑暗場景中效果最好,并且和HDR 和 閾值泛光的結合效果很好,因為像素會通過加法互相疊加。
Tags { "RenderType"="Opaque" }
LOD 100
Blend One One // 加法混合
返回到Unity編輯器,我們增大了粒子大小,以突出目前存在的一個顯示問題。雖然粒子通過加法混合清楚地渲染了出來,渲染效果就像液滴或熔巖燈,但它們沒有半透明效果,而且公告牌四邊形的輪廓很清楚。

為了解決該問題,我們需要禁用深度測試。
Tags { "RenderType"="Opaque" }
LOD 100
Blend One One // 加法混合
ZWrite Off //關閉深度測試
如下圖所示,處理方法很有效。

下圖是沒有修改粒子大小時,應該呈現(xiàn)的效果。

雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。

我們可以添加Queue = Transparent標記從著色器自動執(zhí)行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。

最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。

但是本文想說明,我們應該了解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統(tǒng)的基礎知識的開發(fā)者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命周期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數(shù)據(jù)是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數(shù)據(jù),即使粒子系統(tǒng)已設置為發(fā)送數(shù)據(jù)。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但并不發(fā)送該數(shù)據(jù),我們也會得到相同的警告。
現(xiàn)在我們更新著色器,以便從粒子系統(tǒng)接收Color輸入流。
struct APPdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
然后我們將v2f struct和輸入初始化到頂點部分。
v2f vert (APPdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 從保存在顏色頂點輸入的粒子系統(tǒng)接收數(shù)據(jù),并將該數(shù)據(jù)用于初始化顏色。
o.color = v.color;
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化uv.z(它保存了粒子壽命百分比)
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
返回到Unity編輯器的粒子系統(tǒng)設置,警告已經(jīng)消失了。

現(xiàn)在將Main模塊的Start Color設為藍色。

可能你已經(jīng)明白了,但是這樣做不會改變什么。因為我們接收了數(shù)據(jù),但還沒在片段部分處理數(shù)據(jù)。
現(xiàn)在修改設置,使粒子系統(tǒng)組件的顏色對紋理顏色進行著色,然后再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 采樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統(tǒng)的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據(jù)粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現(xiàn)在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。

我們已經(jīng)成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
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
// 實現(xiàn)模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct APPdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (APPdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 從保存在顏色頂點輸入的粒子系統(tǒng)接收數(shù)據(jù),并將該數(shù)據(jù)用于初始化顏色
o.color = v.color;
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化當前uv.z(包含粒子的壽命百分比)
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
// 讓紋理顏色和粒子系統(tǒng)的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據(jù)粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現(xiàn)在可以接收并處理粒子系統(tǒng)的輸入顏色,所以我們可以通過將Start Color恢復為純白色,注釋掉將col插補為紅色的代碼,并使用Color over Lifetime模塊在標準化粒子生命周期將紅色過渡為藍色,從而獲得完全相同的結果。

如果我們只是為了制作一個自定義著色器,它應該能實現(xiàn)更多功能?,F(xiàn)在我們要實現(xiàn)無法通過修改組件設置或在CPU編程來輕松實現(xiàn)的效果。
我們已經(jīng)學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼o.vertex = UnityObjectToClipPos(v.vertex),添加這部分代碼。
// 奇怪的頂點動畫
float sineFrequency = 5.0;
float sineAmplitude = 4.0;
float sineOffset = sin(_Time.y * sineFrequency) * sineAmplitude;
float agePercent = v.uv.z;
float3 vertexOffset = float3(0, sineOffset * agePercent, 0);
v.vertex.xyz += vertexOffset;
注意:_Time是Unity著色器內置的4D變量,_Time的Y值沒有更改。我們也可以使用_SinTime.w,它是未更改的時間正弦。
我們使用正弦波創(chuàng)建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,于是得到了下圖效果。

通過將基礎粒子系統(tǒng)改為粒子彈簧,我們可以讓效果沒那么奇怪。在得到正常運行的頂點動畫后,為粒子系統(tǒng)實現(xiàn)粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統(tǒng)的默認Rotation X值為-90,請確保將其重置為0。

在Main模塊,勾選Prewarm,將Start Lifetime設為4,Start Speed設為0。

完全禁用Shape模塊。因為我們想要完美的點發(fā)射器。

最后啟用Velocity over Lifetime模塊,并按下圖進行設置。Linear Y設為2,Orbital Y設為8,Offset X設為1。這樣一來,粒子會上升,繞著本地Y軸,將Space設為Local旋轉,由于旋轉中心在X軸偏移1個單位,它會實現(xiàn)螺旋圖案。
通過修改以上設置,我們可以控制彈簧的外形。

下圖是得到的效果,彈簧會上下移動。

如果想要多個彈簧粒子,應該怎樣處理呢?因為該著色器使用和噪聲相同的輸入,無論材質如何,每個彈簧都有相同的動畫效果。我們需要一種方法來根據(jù)材質指定偏移。
現(xiàn)在返回到著色器代碼,在主紋理下添加偏移時間的新屬性。我們在此創(chuàng)建了一個數(shù)值滑塊,調整范圍在0.0和100.0之間,默認值為0.0,即無偏移。
_MainTex("Texture", 2D) = "white" {}
_TimeOffset("Noise Offset", Range(0, 100)) = 0.0

然后,添加時間偏移變量。
sampler2D _MainTex;
float4 _MainTex_ST;
uniform float _TimeOffset;
最后將偏移值與_Time.y相加,將得到的結果存為新變量,以使代碼更清楚,接下來將該變量傳到正弦函數(shù)中,使它在函數(shù)中與頻率相乘。
float time = _Time.y + _TimeOffset;
float sineOffset = sin(time * sineFrequency) * sineAmplitude;
現(xiàn)在,我們只需要復制粒子系統(tǒng)和材質,然后修改剛添加的偏移屬性即可。創(chuàng)建當前粒子系統(tǒng)的副本,然后確保每個副本有獨特的材質和不同的偏移值,并使用相同的著色器。
我們完成的效果如下圖所示。

著色器代碼
完整的著色器代碼如下。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_TimeOffset("Noise Offset", Range(0, 100)) = 0.0
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
LOD 100
Blend One One // 加法混合
ZWrite Off // 關閉深度測試
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現(xiàn)模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct APPdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _TimeOffset;
v2f vert(APPdata v)
{
v2f o;
// 奇怪的定點動畫
float sineFrequency = 5.0;
float sineAmplitude = 4.0;
float time = _Time.y + _TimeOffset;
float sineOffset = sin(time * sineFrequency) * sineAmplitude;
float agePercent = v.uv.z;
float3 vertexOffset = float3(0, sineOffset * agePercent, 0);
v.vertex.xyz += vertexOffset;
o.vertex = UnityObjectToClipPos(v.vertex);
// 從保存在顏色頂點輸入的粒子系統(tǒng)接收數(shù)據(jù),并將該數(shù)據(jù)用于初始化顏色
o.color = v.color;
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化tex coord變量
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//采樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
// 讓紋理顏色和粒子系統(tǒng)的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據(jù)粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
更多課程資源請點擊:https://bycwedu.vipwan.cn/promotion_channels/630597732