Unity Shader入門精要學習筆記 - 第11章 讓畫面動起來
轉自 馮樂樂的 《Unity Shader入門精要》
Unity Shader 中的內置變量
動畫效果往往都是把時間添加到一些變量的計算中,以便在時間變化時畫面也可以隨之變化。Unity Shader 提供了一系列關於時間的內置變量來允許我們方便地在Shader中訪問允許時間,實現各種動畫效果。下表給出了這些內置的時間變量。
紋理動畫
紋理動畫在遊戲中的應用非常廣泛。尤其在各種資源都比較局限的移動平臺上,我們往往會使用紋理動畫來代替復雜的例子系統等模擬各種動畫效果。
最常用的紋理動畫之一就是序列幀動畫。序列幀動畫的原理非常簡單,它像放電影一樣,依次播放一系列關鍵幀圖像,當播放速度達到一定數值時,看起來就是一個連續的動畫。它的有點在於靈活性很強,我們不需要進行任何物理計算就可以得到非常細膩的動畫效果。而它的缺點也很明顯,由於序列幀中每張關鍵幀圖像都不一樣,因此,要制作一張出色的序列幀紋理所需要的美術工程量比較大。
想要實現序列幀動畫,我們先要提供一張包含了關鍵幀圖像的圖像。如下圖所示。
上圖包含了8×8張關鍵幀圖像,它們的大小相同,而且播放順序為從左到右、從上到下、下圖給出了不同時刻播放的不同動畫效果。
為了再Unity實現序列幀動畫,我們做如下準備工作。
1)新建一個場景,去掉天空盒子。
2)新建一個材質,新建一個Shader,並賦給材質
3)新建一個Quad,調整它的位置使其正面朝向攝像機,並把上步材質賦給它
上述序列幀動畫的精髓在於,我們需要在每個時刻計算該時刻下應該播放的關鍵幀的位置,並對該關鍵幀進行紋理采樣。我們修改Shader 代碼。
- Shader "Unity Shaders Book/Chapter 11/Image Sequence Animation" {
- Properties {
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //關鍵幀紋理
- _MainTex ("Image Sequence", 2D) = "white" {}
- //圖像在水平方向上的關鍵幀個數
- _HorizontalAmount ("Horizontal Amount", Float) = 4
- //圖像在垂直方向上的關鍵幀個數
- _VerticalAmount ("Vertical Amount", Float) = 4
- //控制序列幀的播放速度
- _Speed ("Speed", Range(1, 100)) = 30
- }
- SubShader {
- //由於序列幀圖像通常都是透明紋理,我們需要設置Pass的相關狀態,以渲染透明效果
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- fixed4 _Color;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- float _HorizontalAmount;
- float _VerticalAmount;
- float _Speed;
- struct a2v {
- float4 vertex : POSITION;
- float2 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert (a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- return o;
- }
- fixed4 frag (v2f i) : SV_Target {
- //_Time.y 是自該場景後所經過的時間,與速度相乘來得到模擬的時間,再用floor函數取整
- float time = floor(_Time.y * _Speed);
- //獲得行索引
- float row = floor(time / _HorizontalAmount);
- //獲得列索引
- float column = time - row * _HorizontalAmount;
- // half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount);
- // uv.x += column / _HorizontalAmount;
- // uv.y -= row / _VerticalAmount;
- //進行位置偏移
- half2 uv = i.uv + half2(column, -row);
- //進行大小鎖定
- uv.x /= _HorizontalAmount;
- uv.y /= _VerticalAmount;
- //進行采樣
- fixed4 c = tex2D(_MainTex, uv);
- c.rgb *= _Color;
- return c;
- }
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }
很多2D遊戲都使用了不斷滾動的背景來模擬遊戲角色在場景中的穿梭,這些背景往往包含了多個層來模擬一種視覺效果。而這些背景的實現往往就是利用了紋理動畫。我們將實現一個包含了兩層的無限滾動的2D遊戲背景。我們可以得到類似下圖的效果。單擊允許後,我們就可以得到一個無限滾動的背景效果。
為此,我們需要進行如下準備工作。
1)新建一個場景,去掉天空盒子,攝像機投影模式設置為正交投影。
2)新建一個材質,新建一個Shader,賦給材質
3)新建一個Quad,調整大小位置,使它充滿攝像機的視野範圍,然後把第2步的材質拖拽給它。
修改shader 代碼
- Shader "Unity Shaders Book/Chapter 11/Scrolling Background" {
- Properties {
- //第一層(較遠)背景紋理
- _MainTex ("Base Layer (RGB)", 2D) = "white" {}
- //第二層(較近)背景紋理
- _DetailTex ("2nd Layer (RGB)", 2D) = "white" {}
- //第一層滾動速度
- _ScrollX ("Base layer Scroll Speed", Float) = 1.0
- //第二層滾動速度
- _Scroll2X ("2nd layer Scroll Speed", Float) = 1.0
- //控制紋理的整體亮度
- _Multiplier ("Layer Multiplier", Float) = 1
- }
- SubShader {
- Tags { "RenderType"="Opaque" "Queue"="Geometry"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- sampler2D _MainTex;
- sampler2D _DetailTex;
- float4 _MainTex_ST;
- float4 _DetailTex_ST;
- float _ScrollX;
- float _Scroll2X;
- float _Multiplier;
- struct a2v {
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float4 uv : TEXCOORD0;
- };
- v2f vert (a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //計算兩層背景紋理的紋理坐標
- //首先利用了TRANSFORM_TEX 來得到初始的紋理坐標
- //再利用_Time.y變量在水平方向上對紋理坐標進行偏移
- o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y);
- o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y);
- return o;
- }
- fixed4 frag (v2f i) : SV_Target {
- //對紋理進行采樣
- fixed4 firstLayer = tex2D(_MainTex, i.uv.xy);
- fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw);
- //使用第二層紋理的透明通道來混合兩張紋理
- fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a);
- c.rgb *= _Multiplier;
- return c;
- }
- ENDCG
- }
- }
- FallBack "VertexLit"
- }
頂點動畫
河流的模擬是頂點動畫最常見的應用之一。它的原理通常就是使用正弦函數等來模擬水流波動效果。我們將學習如何模擬一個2D的河流效果,我們可以得到類似下圖的效果。
為此,我們需要進行如下準備工作。
1)新建一個場景,去掉天空盒子,攝像機投影模式設置為正交投影。
2)新建一個材質,新建一個Shader,賦給材質
3)在場景中創建多個Water模型,調整它們的位置、大小和方向,把上步的材質賦給它
修改shader代碼:
- Shader "Unity Shaders Book/Chapter 11/Water" {
- Properties {
- //河流紋理
- _MainTex ("Main Tex", 2D) = "white" {}
- //控制整體顏色
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //控制水流波動的幅度
- _Magnitude ("Distortion Magnitude", Float) = 1
- //控制波動頻率
- _Frequency ("Distortion Frequency", Float) = 1
- //用於控制波長的倒數
- _InvWaveLength ("Distortion Inverse Wave Length", Float) = 10
- //河流紋理的移動速度
- _Speed ("Speed", Float) = 0.5
- }
- SubShader {
- // 禁用批處理,因為批處理會合並所有相關的模型,而這些模型各自的模型空間就會丟失
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- //關閉深度寫入,開啟並設置了混合模式,並關閉了剔除功能。這是為了讓水流的每個面都能顯示
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- Cull Off
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- sampler2D _MainTex;
- float4 _MainTex_ST;
- fixed4 _Color;
- float _Magnitude;
- float _Frequency;
- float _InvWaveLength;
- float _Speed;
- struct a2v {
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert(a2v v) {
- v2f o;
- float4 offset;
- offset.yzw = float3(0.0, 0.0, 0.0);
- //只在水平方向上偏移,利用_Frequency 和 內置的_Time.y 來控制正弦函數的頻率
- //為了讓不同的位置具有不同的位移,我們對上述結果加上了模型空間下的位置分量,並乘以_InvWaveLength 來控制波長
- //最後乘以_Magnitude 來控制波動幅度,得到最終的位移。
- offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex + offset);
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- o.uv += float2(0.0, _Time.y * _Speed);
- return o;
- }
- fixed4 frag(v2f i) : SV_Target {
- //這邊只進行紋理采樣再添加顏色控制即可
- fixed4 c = tex2D(_MainTex, i.uv);
- c.rgb *= _Color.rgb;
- return c;
- }
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }
另一種常見的頂點動畫就是廣告牌技術。廣告牌技術會根據視角方向來旋轉一個被紋理著色的多邊形(通常就是簡單的四邊形,這個多邊形就是廣告牌),是的多邊形看起來好像總是面對著攝像機。廣告牌技術被用於很多應該,比如渲染煙霧、運毒、閃光效果等。
廣告牌技術的本質就是構建旋轉矩陣,而我們知道一個變換矩陣需要3個基向量。廣告牌技術使用的基向量通常就是表面法線(normal)、指向上的方向(up)以及指向右的方向(right)。除此之外,我們還需要指定一個錨點。這個錨點在旋轉的過程中是固定不變的,以此來確定多邊形在空間中的位置。
廣告牌技術的難點在於,如何根據需要來構建3個相互正交的基向量。計算過程通常是,我們首先會通過初始計算得到目標的表面法線(例如就是視角方向)和指向上的方向,而兩者往往是不垂直的。但是,兩者其中之一是固定的,例如當模擬草叢時,我們希望廣告牌的法線方向是固定的,即總是指向視角方向,指向上的方向則可以發生變換。我們假設法線方向是固定的,首先,我們根據初始的表面法線和指向上的方向來計算出目標方向的指向右的方向(通過叉積操作):
right = up × normal
對其歸一化後,再由法線方向和指向右的方向計算出正交的指向上的方向即可:
up‘ = normal × right
至此,我們就可以得到用於旋轉的3個正交基了。下圖給出了上述計算過程的圖示。如果指向上的方向是固定的,計算過程也是類似的。
下面,我們將在Unity中實現上面提到的廣告牌技術。我們可以得到類似下圖中的效果。
為此,我們需要做如下準備工作。
1)新建一個場景,去掉天空盒子
2)新建一個材質,新建一個Shader,賦給材質
3)在場景中創建多個Quad,調整位置和大小,把上步材質賦給它們。
更改Shader代碼。
- Shader "Unity Shaders Book/Chapter 11/Billboard" {
- Properties {
- //廣告牌顯示的透明紋理
- _MainTex ("Main Tex", 2D) = "white" {}
- //控制整體顏色
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //調整是固定法線還是固定指向上的方向
- _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1
- }
- SubShader {
- // Need to disable batching because of the vertex animation
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- //這裏關閉了深度寫入,開啟並設置了混合模式,並關閉了剔除功能。這是為了讓廣告牌的每個面都能顯示
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- Cull Off
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cginc"
- sampler2D _MainTex;
- float4 _MainTex_ST;
- fixed4 _Color;
- fixed _VerticalBillboarding;
- struct a2v {
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert (a2v v) {
- v2f o;
- // 選擇模型空間的原點作為廣告牌的錨點,並利用內置變量獲取模型空間下的視角位置
- float3 center = float3(0, 0, 0);
- float3 viewer = mul(_World2Object,float4(_WorldSpaceCameraPos, 1));
- //計算3個正交矢量。首先,我們根據觀察位置和錨點計算目標法線方向,
- //並根據_VerticalBillboarding 屬性來控制垂直方向上的約束度。
- float3 normalDir = viewer - center;
- // 如果 _VerticalBillboarding 等於 1, 意味著法線方向固定為視角方向
- // 如果 _VerticalBillboarding 等於 0, 意味著向上方向固定為(0,1,0)
- normalDir.y =normalDir.y * _VerticalBillboarding;
- //歸一化操作
- normalDir = normalize(normalDir);
- //我們得到了粗略的向上方向。為了防止法線方向和向上方向平行
- //我們對法線方向的y分量進行判斷,以得到合適的向上方向。然後,根據法線方向
- //和粗略的向上方向得到向右方向,並對結果進行歸一化。但由於此時向上的方向還是不
- //準確的,我們又根據準確的法線方向和向右方向得到最後的向上方向
- float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
- float3 rightDir = normalize(cross(upDir, normalDir));
- upDir = normalize(cross(normalDir, rightDir));
- //我們根據原始的位置相對於錨點的偏移量以及3個正交基矢量,以計算得到新的頂點位置。
- float3 centerOffs = v.vertex.xyz - center;
- float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;
- //最後,把模型空間的頂點位置變換到裁剪空間中
- o.pos = mul(UNITY_MATRIX_MVP, float4(localPos, 1));
- o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
- return o;
- }
- fixed4 frag (v2f i) : SV_Target {
- fixed4 c = tex2D (_MainTex, i.uv);
- c.rgb *= _Color.rgb;
- return c;
- }
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }
需要說明的是,在上面的例子中,我們使用的是Unity自帶的Quad來作為廣告牌,而不能使用自帶的Plane。這是因為,我們的代碼是建立在一個豎直擺放的多邊形的基礎上的,也就是說,這個多邊形的頂點結構需要滿足在模型空間下是豎直排列的。只有這樣,我們才能使用v.vertex來計算到正確的相對於中心的位置偏移量。
頂點動畫雖然非常靈活有效,但有些註意事項需要註意。
首先,在之前看到的那樣,如果我們在模型空間下進行了一些頂點動畫,那麽批處理往往就會破壞這種動畫效果。這時,我們可以通過SubShader的DisableBatching標簽來強制取消對該Unity Shader的批處理。然而,取消批處理會帶來一定的性能下降,增加了Draw Call,因此我們應該盡量避免使用模型空間下的一些絕對位置和方向來進行計算。在廣告牌的例子中,為了避免顯示使用模型空間的中心來作為錨點,我們可以利用頂點顏色來存儲每個頂點到錨點的距離值,這種做法在商業遊戲中很常見。
其次,如果我們想要對包含了頂點動畫的物體添加陰影,那麽如果像之前那樣使用內置的Diffuse等包含的陰影Pass來渲染,就得不到正確的陰影效果(這裏指的是無法向其他物體正確地投射陰影)。這是因為,我們講過Unity 的陰影繪制需要調用一個ShadowCaster Pass,而如果直接使用這些內置的ShadowCasterPass,這個Pass中並沒有進行相關的頂點動畫,因此Unity 自定義的ShadowCaster Pass,而這個Pass中,我們將進行統一的頂點變換過程。需要註意的是,在前面的實現中,如果涉及半透明物體我們都把Fallback設置成了Transparent/VertexLit ,而Transparent/VertexLit沒有定義ShadowCaster Pass,因此也就不會產生陰影。
在之前的場景中,我們給出了計算頂點動畫的陰影的一個例子。在這個例子中,我們使用了之前的大部分代碼,模擬一個波動的水流。同時,我們開啟了場景中平行光的陰影效果,並添加了一個平面來接收來自“水流”的陰影。我們還把這個Unity Shader 的Fallback 設置為內置的VertexLit,這樣Unity將根據Fallback最終找到VertexLit 中的ShadowCaster Pass 來渲染陰影。下圖給出了這樣的結果。
可以看出,此時雖然Water模型發生了形變,但它的陰影並沒有產生相應的動畫效果。為了正確繪制變形對象的陰影,我們就需要提供自定義的ShadowCaster Pass。我們新建一個Shader來實現,效果如下圖。
在這個Shader中,我們提供了一個ShadowCaster Pass,相關代碼如下:
- Shader "Unity Shaders Book/Chapter 11/Billboard" {
- Properties {
- //廣告牌顯示的透明紋理
- _MainTex ("Main Tex", 2D) = "white" {}
- //控制整體顏色
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //調整是固定法線還是固定指向上的方向
- _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1
- }
- SubShader {
- // Need to disable batching because of the vertex animation
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- //這裏關閉了深度寫入,開啟並設置了混合模式,並關閉了剔除功能。這是為了讓廣告牌的每個面都能顯示
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- Cull Off
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cginc"
- sampler2D _MainTex;
- float4 _MainTex_ST;
- fixed4 _Color;
- fixed _VerticalBillboarding;
- struct a2v {
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert (a2v v) {
- v2f o;
- // 選擇模型空間的原點作為廣告牌的錨點,並利用內置變量獲取模型空間下的視角位置
- float3 center = float3(0, 0, 0);
- float3 viewer = mul(_World2Object,float4(_WorldSpaceCameraPos, 1));
- //計算3個正交矢量。首先,我們根據觀察位置和錨點計算目標法線方向,
- //並根據_VerticalBillboarding 屬性來控制垂直方向上的約束度。
- float3 normalDir = viewer - center;
- // 如果 _VerticalBillboarding 等於 1, 意味著法線方向固定為視角方向
- // 如果 _VerticalBillboarding 等於 0, 意味著向上方向固定為(0,1,0)
- normalDir.y =normalDir.y * _VerticalBillboarding;
- //歸一化操作
- normalDir = normalize(normalDir);
- //我們得到了粗略的向上方向。為了防止法線方向和向上方向平行
- //我們對法線方向的y分量進行判斷,以得到合適的向上方向。然後,根據法線方向
- //和粗略的向上方向得到向右方向,並對結果進行歸一化。但由於此時向上的方向還是不
- //準確的,我們又根據準確的法線方向和向右方向得到最後的向上方向
- float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
- float3 rightDir = normalize(cross(upDir, normalDir));
- upDir = normalize(cross(normalDir, rightDir));
- //我們根據原始的位置相對於錨點的偏移量以及3個正交基矢量,以計算得到新的頂點位置。
- float3 centerOffs = v.vertex.xyz - center;
- float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;
- //最後,把模型空間的頂點位置變換到裁剪空間中
- o.pos = mul(UNITY_MATRIX_MVP, float4(localPos, 1));
- o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
- return o;
- }
- fixed4 frag (v2f i) : SV_Target {
- fixed4 c = tex2D (_MainTex, i.uv);
- c.rgb *= _Color.rgb;
- return c;
- }
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }
- Pass{
- Tags{"LightMode"="ShadowCaster"}
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #pragma multi_compile_shadowcaster
- #include "UnityCG.cginc"
- float _Magnitude;
- float _Frequency;
- float _InvaWaveLength;
- float _Speed;
- struct a2v{
- float4 vertex : POSITION;
- float4 texcoord : TEXCOORD0;
- };
- struct v2f{
- V2F_SHADOW_CASTER;
- };
- v2f vert(a2v i){
- v2f o;
- float4 offset;
- offset.yzw = float3(0.0,0.0,0.0);
- //計算偏移
- offset.x = sin(_Frequency*_Time.y+v.vertex.x*_InvaWaveLength+
- v.vertex.y*_InvaWaveLength+v.vertex.z*_InvaWaveLength)*_Magnitude;
- //加上偏移
- v.vertex = v.vertex + offset;
- TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
- return o;
- }
- fixed4 frag(v2f i) : SV_Target{
- //使用SHADOW_CASTER_FRAGMENT來讓Unity自動完成陰影投射的部分,把結果輸出到深度圖和陰影映射紋理中
- SHADOW_CASTER_FRAGMENT(i)
- }
- ENDCG
- }
Unity Shader入門精要學習筆記 - 第11章 讓畫面動起來