在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇Create -> Shader -> Unlit Shader。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前texcoord的可用空間大,它會作為餘下部分移動到下一個texcoord,例如texcoord1或texcoord2等。
下圖是相應的設置案例,裡面沒有添加這些頂點流。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的Alpha值,只需添加混合模式即可。
我們可以選擇常用命令,例如:加法(One One命令),Alpha混合(SrcAlpha OneMinusSrcAlpha)和Alpha混合預乘(One OneMinusSrcAlpha)。對黑色背景上紋理(例如默認粒子紋理)的最好選擇是加法和預乘。
我們選擇加法,因為它最直接,在黑暗場景中效果最好,並且和HDR 和 閾值泛光的結合效果很好,因為像素會通過加法互相疊加。
Tags { "RenderType"="Opaque" }
LOD 100
Blend One One // 加法混合
返回到Unity編輯器,我們增大了粒子大小,以突出目前存在的一個顯示問題。雖然粒子通過加法混合清楚地渲染了出來,渲染效果就像液滴或熔岩燈,但它們沒有半透明效果,而且公告牌四邊形的輪廓很清楚。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
通過將基礎粒子系統改為粒子彈簧,我們可以讓效果沒那麼奇怪。在得到正常運行的頂點動畫後,為粒子系統實現粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統的默認Rotation X值為-90,請確保將其重置為0。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
通過將基礎粒子系統改為粒子彈簧,我們可以讓效果沒那麼奇怪。在得到正常運行的頂點動畫後,為粒子系統實現粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統的默認Rotation X值為-90,請確保將其重置為0。
在Main模塊,勾選Prewarm,將Start Lifetime設為4,Start Speed設為0。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
通過將基礎粒子系統改為粒子彈簧,我們可以讓效果沒那麼奇怪。在得到正常運行的頂點動畫後,為粒子系統實現粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統的默認Rotation X值為-90,請確保將其重置為0。
在Main模塊,勾選Prewarm,將Start Lifetime設為4,Start Speed設為0。
完全禁用Shape模塊。因為我們想要完美的點發射器。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
通過將基礎粒子系統改為粒子彈簧,我們可以讓效果沒那麼奇怪。在得到正常運行的頂點動畫後,為粒子系統實現粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統的默認Rotation X值為-90,請確保將其重置為0。
在Main模塊,勾選Prewarm,將Start Lifetime設為4,Start Speed設為0。
完全禁用Shape模塊。因為我們想要完美的點發射器。
最後啟用Velocity over Lifetime模塊,並按下圖進行設置。Linear Y設為2,Orbital Y設為8,Offset X設為1。這樣一來,粒子會上升,繞著本地Y軸,將Space設為Local旋轉,由於旋轉中心在X軸偏移1個單位,它會實現螺旋圖案。
通過修改以上設置,我們可以控制彈簧的外形。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
通過將基礎粒子系統改為粒子彈簧,我們可以讓效果沒那麼奇怪。在得到正常運行的頂點動畫後,為粒子系統實現粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統的默認Rotation X值為-90,請確保將其重置為0。
在Main模塊,勾選Prewarm,將Start Lifetime設為4,Start Speed設為0。
完全禁用Shape模塊。因為我們想要完美的點發射器。
最後啟用Velocity over Lifetime模塊,並按下圖進行設置。Linear Y設為2,Orbital Y設為8,Offset X設為1。這樣一來,粒子會上升,繞著本地Y軸,將Space設為Local旋轉,由於旋轉中心在X軸偏移1個單位,它會實現螺旋圖案。
通過修改以上設置,我們可以控制彈簧的外形。
下圖是得到的效果,彈簧會上下移動。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
通過將基礎粒子系統改為粒子彈簧,我們可以讓效果沒那麼奇怪。在得到正常運行的頂點動畫後,為粒子系統實現粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統的默認Rotation X值為-90,請確保將其重置為0。
在Main模塊,勾選Prewarm,將Start Lifetime設為4,Start Speed設為0。
完全禁用Shape模塊。因為我們想要完美的點發射器。
最後啟用Velocity over Lifetime模塊,並按下圖進行設置。Linear Y設為2,Orbital Y設為8,Offset X設為1。這樣一來,粒子會上升,繞著本地Y軸,將Space設為Local旋轉,由於旋轉中心在X軸偏移1個單位,它會實現螺旋圖案。
通過修改以上設置,我們可以控制彈簧的外形。
下圖是得到的效果,彈簧會上下移動。
如果想要多個彈簧粒子,應該怎樣處理呢?因為該著色器使用和噪聲相同的輸入,無論材質如何,每個彈簧都有相同的動畫效果。我們需要一種方法來根據材質指定偏移。
現在返回到著色器代碼,在主紋理下添加偏移時間的新屬性。我們在此創建了一個數值滑塊,調整範圍在0.0和100.0之間,默認值為0.0,即無偏移。
_MainTex("Texture", 2D) = "white" {}
_TimeOffset("Noise Offset", Range(0, 100)) = 0.0
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
通過將基礎粒子系統改為粒子彈簧,我們可以讓效果沒那麼奇怪。在得到正常運行的頂點動畫後,為粒子系統實現粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統的默認Rotation X值為-90,請確保將其重置為0。
在Main模塊,勾選Prewarm,將Start Lifetime設為4,Start Speed設為0。
完全禁用Shape模塊。因為我們想要完美的點發射器。
最後啟用Velocity over Lifetime模塊,並按下圖進行設置。Linear Y設為2,Orbital Y設為8,Offset X設為1。這樣一來,粒子會上升,繞著本地Y軸,將Space設為Local旋轉,由於旋轉中心在X軸偏移1個單位,它會實現螺旋圖案。
通過修改以上設置,我們可以控制彈簧的外形。
下圖是得到的效果,彈簧會上下移動。
如果想要多個彈簧粒子,應該怎樣處理呢?因為該著色器使用和噪聲相同的輸入,無論材質如何,每個彈簧都有相同的動畫效果。我們需要一種方法來根據材質指定偏移。
現在返回到著色器代碼,在主紋理下添加偏移時間的新屬性。我們在此創建了一個數值滑塊,調整範圍在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相加,將得到的結果存為新變量,以使代碼更清楚,接下來將該變量傳到正弦函數中,使它在函數中與頻率相乘。
float time = _Time.y + _TimeOffset;
float sineOffset = sin(time * sineFrequency) * sineAmplitude;
現在,我們只需要複製粒子系統和材質,然後修改剛添加的偏移屬性即可。創建當前粒子系統的副本,然後確保每個副本有獨特的材質和不同的偏移值,並使用相同的著色器。
我們完成的效果如下圖所示。
在本教程中,我們將學習如何在Unity粒子系統中使用自定義頂點流(Vertex Streams)。頂點流通過粒子系統的Renderer模塊來設置,它可以將額外的單個粒子數據傳遞到著色器。著色器可以使用該數據,為系統中的每個粒子創建各種獨特的效果,所有粒子都會在GPU上以極快的速度處理。最終的效果場景如下圖所示,雖然本文實現的效果不是非常驚豔,但它為後續教程中學習創作精美特效奠定基礎。
Part 1:基礎部分
首先,我們使用Unity模板創建一個簡單的無光粒子著色器。在項目窗口中單擊右鍵,選擇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
// 實現模糊效果
#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
}
}
}
創建新材質,指定著色器,然後設置紋理屬性為默認粒子紋理。
現在我們創建粒子系統,指定該材質到粒子系統Renderer模塊的Materials字段。
我們會得到下圖的效果。
圖中的效果存在一些透明度問題,我們稍後會解決這些問題。
在相同粒子系統的Renderer模塊中,勾選Custom Vertex Streams啟用自定義頂點流,然後單擊右下方的“+”按鈕,添加Lifetime分類中的AgePercent流。
AgePercent是1D值,表示標準化範圍[0.0, 1.0]內粒子的“生命週期”。0.0表示粒子生成時間,0.5表示粒子生命週期終點,1.0表示粒子消逝時間。
我們忽略頂點流與著色器輸入不匹配紅色警告信息,現在將頂點流傳給著色器,我們需要接收並處理頂點流的數據。
在頂點流顯示中,可以注意到數據已被緊湊地打包了。實際2D UV座標位於TEXCOORD0.xy,AgePercent數據位於TEXCOORD0.z。要記住這些信息,以便我們知道在著色器中何處以及如何獲取此數據。
每個texcoord都可以是4D向量,即CG/HLSL代碼中,以[x, y, z, w]形式保存的float4變量。如果我們要添加額外的1D流,它將位於TEXCOORD0.w。如果數據比當前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)中。
現在關注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在被傳遞到片段部分前包含合適的數值,這些“部分”其實是一個.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;
}
最後在片段部分,即給對象上色的像素著色器部分,我們可以利用該數據。下面代碼中,我們根據粒子壽命,使用該數據向紋理(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);
// 根據粒子壽命百分比,將紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
經過修改後,以下是完整的著色器代碼。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 實現模糊效果
#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);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我們得到了下圖的結果,或許它並不驚豔,但這僅只是開始。
Part 2:透明度,深度測試及渲染隊列
繼續下一步前,我們先解決之前出現的問題,從透明度開始。為了從輸入紋理獲取合適的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 //關閉深度測試
如下圖所示,處理方法很有效。
下圖是沒有修改粒子大小時,應該呈現的效果。
雖然可能效果不太明顯,但我們的著色器仍然會在場景中對其它透明對象進行排序,例如精靈。解決這個問題很簡單,只需將材質上的渲染隊列更改為透明層即可。
我們可以添加Queue = Transparent標記從著色器自動執行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:頂點顏色和著色
下面我們來解決頂點流不匹配著色器輸入的警告。
最簡單的方法是用過在編輯器選擇Color流,單擊“+”旁邊的“-”按鈕,移除Color流,這樣問題就解決了。
但是本文想說明,我們應該瞭解真正解決該錯誤,而不是簡單的刪除Color流。
熟悉Unity粒子系統的基礎知識的開發者,應該知道我們可以定義所有粒子初始化時的起始顏色,生命週期顏色和隨速度變化的顏色。在當前著色器中,這些設置沒有任何效果,因為數據是通過COLOR頂點輸入傳遞的。
這些警告實際在告訴我們,著色器中沒有地方接收該數據,即使粒子系統已設置為發送數據。因此當我們移除它時,警告就消失了。如果在著色器中接收Color流,但並不發送該數據,我們也會得到相同的警告。
現在我們更新著色器,以便從粒子系統接收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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色。
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編輯器的粒子系統設置,警告已經消失了。
現在將Main模塊的Start Color設為藍色。
可能你已經明白了,但是這樣做不會改變什麼。因為我們接收了數據,但還沒在片段部分處理數據。
現在修改設置,使粒子系統組件的顏色對紋理顏色進行著色,然後再插補為紅色。
fixed4 frag (v2f i) : SV_Target
{
// 採樣紋理
fixed4 col = tex2D(_MainTex, i.uv);
//讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
現在Unity編輯器中,我們可以看見下圖畫面,和預期一樣,粒子首先會被著色為藍色。
我們已經成功編寫好了粒子著色器,它可以處理自定義頂點流,下面是完整的著色器代碼。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 4:頂點動畫
效果依舊不是很特別,因為現在可以接收並處理粒子系統的輸入顏色,所以我們可以通過將Start Color恢復為純白色,註釋掉將col插補為紅色的代碼,並使用Color over Lifetime模塊在標準化粒子生命週期將紅色過渡為藍色,從而獲得完全相同的結果。
如果我們只是為了製作一個自定義著色器,它應該能實現更多功能。現在我們要實現無法通過修改組件設置或在CPU編程來輕鬆實現的效果。
我們已經學習如何使用自定義頂點流處理渲染粒子的像素。接下來處理它的頂點。在頂點部分中,在修改位置的代碼前,即代碼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,它是未更改的時間正弦。
我們使用正弦波創建了動畫,其中Y偏移被調整為正弦偏移和粒子壽命的乘積。粒子壽命越大,偏移越大,於是得到了下圖效果。
通過將基礎粒子系統改為粒子彈簧,我們可以讓效果沒那麼奇怪。在得到正常運行的頂點動畫後,為粒子系統實現粒子彈簧效果非常簡單。
Part 5:粒子彈簧
首先,新粒子系統的默認Rotation X值為-90,請確保將其重置為0。
在Main模塊,勾選Prewarm,將Start Lifetime設為4,Start Speed設為0。
完全禁用Shape模塊。因為我們想要完美的點發射器。
最後啟用Velocity over Lifetime模塊,並按下圖進行設置。Linear Y設為2,Orbital Y設為8,Offset X設為1。這樣一來,粒子會上升,繞著本地Y軸,將Space設為Local旋轉,由於旋轉中心在X軸偏移1個單位,它會實現螺旋圖案。
通過修改以上設置,我們可以控制彈簧的外形。
下圖是得到的效果,彈簧會上下移動。
如果想要多個彈簧粒子,應該怎樣處理呢?因為該著色器使用和噪聲相同的輸入,無論材質如何,每個彈簧都有相同的動畫效果。我們需要一種方法來根據材質指定偏移。
現在返回到著色器代碼,在主紋理下添加偏移時間的新屬性。我們在此創建了一個數值滑塊,調整範圍在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相加,將得到的結果存為新變量,以使代碼更清楚,接下來將該變量傳到正弦函數中,使它在函數中與頻率相乘。
float time = _Time.y + _TimeOffset;
float sineOffset = sin(time * sineFrequency) * sineAmplitude;
現在,我們只需要複製粒子系統和材質,然後修改剛添加的偏移屬性即可。創建當前粒子系統的副本,然後確保每個副本有獨特的材質和不同的偏移值,並使用相同的著色器。
我們完成的效果如下圖所示。
著色器代碼
完整的著色器代碼如下。
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
// 實現模糊效果
#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);
// 從保存在顏色頂點輸入的粒子系統接收數據,並將該數據用於初始化顏色
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);
// 讓紋理顏色和粒子系統的頂點顏色輸入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根據粒子壽命百分比,從紋理顏色插值為紅色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 應用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}