【Unity Shader】(十) ------ UV動畫原理及簡易實現


筆者使用的是 Unity 2018.2.0f2 + VS2017,建議讀者使用與 Unity 2018 相近的版本,避免一些因為版本不一致而出現的問題。

 

【Unity Shader】(三) ------ 光照模型原理及漫反射和高光反射的實現
【Unity Shader】(四) ------ 紋理之法線紋理、單張紋理及遮罩紋理的實現
【Unity Shader】(五) ------ 透明效果之半透明效果的實現及原理
【Unity Shader】(六) ------ 復雜的光照(上)
【Unity Shader】(七) ------ 復雜的光照(下)
【Unity Shader】(八) ------ 高級紋理之立方體紋理及光線反射、折射的實現
【Unity Shader】(九) ------ 高級紋理之渲染紋理及鏡子與玻璃效果的實現

前言

純粹的靜態美景宛如一張漂亮的貼圖,而在游戲中,這種沒有一點動畫的情況往往是十分無趣且讓人感到別扭的。所以本文會介紹一些簡單的UV動畫。

 

一. 時間變量

在我們寫游戲邏輯時,涉及到隨時間移動或旋轉這種動作時,我們一般都會使用 Time.time 這個變量,同樣,在 Unity Shader 中,我們需要實現一些動畫時,也需要時間變量。下圖是 Unity 內置的時間變量

名稱 類別 作用
_Time  float4  t 是從場景加載開始時經歷的時間,(t/20 , t , 2t , 3t)
_SinTime  float4  t 是時間的正弦值,(t/8 , t/4 , t/2 ,t)
_CosTime  float4  t 是時間的余弦值,(t/8 , t/4 , t/2 ,t)
unity_DeltaTime  float4  dt 是時間增量,(dt , 1/dt , smoothDt, 1/smoothDT)

 

比如我們使用 _Time.y 時,就相當於 _Time 的 t 變量,即會記錄場景加載后經歷的時間。下面我們使用它來實現一些效果

 

二. 序列幀動畫

 

序列幀動畫是一種十分常見的動畫,它就像播放電影一樣,把一連串的關鍵幀圖像以一定的速度播放出來,看起來就是一段連續的動畫。而它的優缺點也十分明顯:

  • 靈活性強,不需要進行物理上的計算,比如光照,陰影等計算
  • 制作序列幀的美術工作量大

本文以制作一個火焰效果為例。我們需要用到一張序列幀圖像,讀者可以在本文末端下載,也可以使用自己的圖像,先看一下我們要實現的效果

 

2.1 准備工作

(1)創建一個場景,這次為了效果明顯,我們去掉天空盒子

(2)創建一個 Quad,一個 Material,一個 shader,命名為 SequenceAnimation

(3)准備一張序列幀圖像,這里筆者使用的是一張包含了 4 x 4 張關鍵幀的圖像

這 16 張關鍵幀圖像的大小相同,我們要實現的是讓它們從左到右,從上到下播放。所以我們要做的就很簡單了,只需要在播放時記錄下應該播放的關鍵幀的位置(UV坐標),然后進行采樣就行了。

 

2.2 Shader 實現

序列幀圖像往往被當成是一個半透明對象,所以我們以對待半透明對象的方法來對待它。如果對半透明原理及實現方法不熟悉的讀者可以翻看這篇博文 【Unity Shader】(五) ------ 透明效果之半透明效果的實現及原理

 

I.定義 Properties 塊

1     Properties
2  { 3 _Color ("Color", Color) = (1,1,1,1) 4 _MainTex ("Sequence Image", 2D) = "white" {} 5 _Speed("Speed", Range(1,100)) = 50 6 _HorizontalAmount ("Horizontal Amount",float) = 4 7 _VerticalAmount ("Vertical Amount",float) = 4 8 }
 

MainTex 對應着我們准備的序列幀圖像,Speed 代表播放速度,HorizontalAmount 和 VerticalAmount 代表着圖像在水平方向和豎直方向包含的關鍵幀圖像個數。

 

II.定義 Tags

1 Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}

序列幀圖像一般都是透明紋理,所以這里我們設置為 Transparent

 

III. 定義相關屬性與做出聲明

 1             Tags{"LightMode" = "ForwardBase"}
 2  ZWrite Off 3  Blend SrcAlpha OneMinusSrcAlpha 4 5  CGPROGRAM 6 #pragma multi_compile_fwdbase 7 #include "UnityCG.cginc" 8 #pragma vertex vert 9 #pragma fragment frag 10 11  fixed4 _Color; 12  sampler2D _MainTex; 13  float4 _MainTex_ST; 14 float _Speed; 15 float _HorizontalAmount; 16 float _VerticalAmount;

由於是半透明物體,所以我們關閉深度寫入並開啟混合。定義與 Properties 塊中想匹配的屬性

 

IV. 定義輸入輸出結構體

 1             struct a2v
 2  { 3  float4 vertex : POSITION; 4  float4 texcoord : TEXCOORD0; 5  }; 6 7 struct v2f 8  { 9  float4 pos : SV_POSITION; 10  float2 uv : TEXCOORD0; 11 };

 

這個 shader 中我們主要是計算關鍵幀的位置和紋理采樣,所以輸入輸出結構體我們不需要太復雜

 

V. 定義頂點着色器

1             v2f vert(a2v v)
2  { 3  v2f o; 4 o.pos = UnityObjectToClipPos(v.vertex); 5 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 6 return o; 7 }

 

 我們使用 TRANSFORM_TEX 來得到最終的紋理坐標。我們可以在 UnityCG.cginc 找到 TRANSFORM_TEX 的定義

1 // Transforms 2D UV by scale/bias property
2 #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

name##_ST.xy 代表縮放,name##_ST.zw 代表偏移,這里的 name##_ST 就是我們定義的 _MainTex_ST

 

VI. 定義片元着色器

 1             fixed4 frag(v2f i) : SV_Target
 2  { 3 float time = floor(_Time.y * _Speed); 4 float row = floor(time / _HorizontalAmount); 5 float colum = time - row * _HorizontalAmount; 6 7 half2 uv = i.uv + half2(colum, -row); 8 uv.x /= _HorizontalAmount; 9 uv.y /= _VerticalAmount; 10 11 fixed4 c = tex2D(_MainTex, uv); 12 c.rgb += _Color; 13 return c; 14 }

 

(1)定義時間變量,記錄場景經歷的時間,當然要記得乘上播放速度。其中 floor 函數是一個向下取整的函數,我們可以在MSDN上找到它的定義

(2)計算行列索引值。我們使用的序列幀圖像是包含 n x n 張關鍵幀紋理的圖像,所以可以把它當做 n x n 的數組。而行列索引值的計算也很好理解。

  • 時間 / 行個數 = 行索引
  • 時間 - 行個數 * 行索引  =  時間 % 行個數 ,即余數就是列索引

(3)利用索引值得到真正的采樣坐標。

  • 在原先的 UV 加上一個由第2步求得的行列索引構建成的 half2 ,代表偏移。這個偏移值會隨着時間而改變
  • 在采樣之前要先進行等分,實際上相當於 UV原點 + 偏移量(行索引 / 行等分個數 , 列索引 / 列等分個數)

(4)最后進行采樣並加上主顏色即可

 

 疑惑點:

  • 隨着時間的增長,變量 time 不是會變得越來越大嗎,同時 row 也會越來越大,當 row 很大的時候,采樣不會出錯嗎?
  • 進行偏移時,為什么加的是 half2(colum,-row),而不是 half2(row,colum)?

 

解答點:

(1)

  • 隨着時間增長,row 會越來越大,所以為了限制 UV 在可采樣范圍內,我們需要把序列幀圖像 Wrap Mode 設置為 Repeat,如下圖。
  • 在 Repeat 模式下,當 UV 值超過 1 時,會舍棄整數值,使用小數部分進行采樣,這樣就會形成紋理重復或者說循環的效果
  • 可能有的讀者想到使用 % 求余操作,如果只是單純的求余有可能會導致部分少數的關鍵幀沒有被采集到,因為在 uv 坐標數值上映射不到一些關鍵幀的位置。當然讀者可以自行實現一下。查看效果。

(2)

  • 進行偏移時使用的是 half2(colum,-row) 是因為:對 x 軸進行偏移時,我們使用列索引來進行操作,對 y 軸進行偏移時,我們使用行索引來進行操作,所以是 (colum,row)。
  • 之所以 row 取負,是因為在 Unity 中進行采樣時,豎直方向即 y 軸的坐標順序是(從下往上遞增),而我們所期待的播放順序是(從上往下遞增),兩者相反,所以這里的 row 取負

 

VII. 最后關閉 FallBack 或者 Fallback "Transparent/VertexLit" 均可

 

VIII. 完整代碼

 1 Shader "Unity/01-SequenceAnimation" 
 2 {
 3  Properties 4  { 5 _Color ("Color", Color) = (1,1,1,1) 6 _MainTex ("Sequence Image", 2D) = "white" {} 7 _Speed("Speed", Range(1,100)) = 50 8 _HorizontalAmount ("Horizontal Amount",float) = 4 9 _VerticalAmount ("Vertical Amount",float) = 4 10  } 11  SubShader 12  { 13 Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"} 14 15  Pass 16  { 17 Tags{"LightMode" = "ForwardBase"} 18  ZWrite Off 19  Blend SrcAlpha OneMinusSrcAlpha 20 21  CGPROGRAM 22 #pragma multi_compile_fwdbase 23 #include "UnityCG.cginc" 24 #pragma vertex vert 25 #pragma fragment frag 26 27  fixed4 _Color; 28  sampler2D _MainTex; 29  float4 _MainTex_ST; 30 float _Speed; 31 float _HorizontalAmount; 32 float _VerticalAmount; 33 34 struct a2v 35  { 36  float4 vertex : POSITION; 37  float4 texcoord : TEXCOORD0; 38  }; 39 40 struct v2f 41  { 42  float4 pos : SV_POSITION; 43  float2 uv : TEXCOORD0; 44  }; 45 46  v2f vert(a2v v) 47  { 48  v2f o; 49 o.pos = UnityObjectToClipPos(v.vertex); 50 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 51 return o; 52  } 53 54  fixed4 frag(v2f i) : SV_Target 55  { 56 float time = floor(_Time.y * _Speed); 57 float row = floor(time _HorizontalAmount); 58 59 float colum = time - row * _HorizontalAmount; 60 61 half2 uv = i.uv + half2(colum,-row); 62 uv.x /= _HorizontalAmount; 63 uv.y /= _VerticalAmount; 64 65 fixed4 c = tex2D(_MainTex, uv); 66 c.rgb += _Color; 67 return c; 68  } 69 70 71  ENDCG 72 73  } 74 75  } 76 Fallback "Transparent/VertexLit" 77 }

 

IX. 保存,回到 Unity,把准備好的序列幀圖像賦予 MainTex 查看效果

 

 

2.3 總結

序列幀動畫是一種很常見的應用,讀者也許使用 UI 制作過序列幀動畫,而本文則是側重於 shader 的實現。原理也是十分的簡單,只是對正確的 UV 坐標做紋理采樣。不過需要注意一些細節之處,比如行列索引的相關計算,只要明白這一點,相信讀者能十分輕松地理解本例。

 

 

三. 背景滾動

在筆者的童年時,曾玩過紅白機,里面的游戲許多都是一些橫版過關的游戲。在這種 2D 型游戲中,我們可以發現有許多場景中背景一直在滾動,營造了一種主角在移動的感覺。而在現今的 2D 游戲中,這種滾動的背景依舊是我們常用的,所以此處我們來介紹這種效果的 shader 實現。

先看一下我們要實現的效果:

 

實現這個效果我們使用了兩張圖像,讀者可以在本文末端下載,也可以使用自行准備的圖像

 

3.1 准備工作

(1)創建一個場景,去掉天空盒子

(2)創建一個 Quad,一個 Material,一個 shader,命名為 ScrollingBackground,Quad 最好調整為充滿屏幕

(3)准備兩張圖像,一張 “遠景”(Far),一張 “近景”(Near)

 

3.2 shader 實現

 

I. 定義 Properties 塊

1     Properties {
2         _Color ("Color", Color) = (1,1,1,1) 3 _MainTex ("FarLayer ", 2D) = "white" {} 4 _DetailTex("NearLayer ", 2D) = "white" {} 5 _ScrollX ("Far layer scroll Speed",Float) = 1.0 6 _Scroll2X("Near layer scroll Speed",Float) = 1.0 7 _Multiplier ("Layer Multiplier",Float) = 1.0 8 }

_MainTex 代表遠景圖,這里我使用的是一張純背景色的圖像;_DetailTex 代表近景圖,這里我使用的是一張有樓宇的圖像;兩個 _Scroll 代表了兩張圖像的滾動速度。_Multiplier 代表了紋理整體亮度,這個如果覺得沒必要可以不寫。

 

II. 定義相關屬性和做出聲明

 1 Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
 2  Pass 3  { 4 Tags{"LightMode" = "ForwardBase"} 5  ZWrite Off 6  Blend SrcAlpha OneMinusSrcAlpha 7 8  CGPROGRAM 9 #include "UnityCG.cginc" 10 #pragma multi_compile_fwdbase 11 #pragma vertex vert 12 #pragma fragment frag 13 14  fixed4 _Color; 15  sampler2D _MainTex; 16  float4 _MainTex_ST; 17  sampler2D _DetailTex; 18  float4 _DetailTex_ST; 19 float _ScrollX; 20 float _Scroll2X; 21 float _Multiplier;

 我們同樣把它當做透明物體看待,關閉深度寫入和開啟混合,再定義相匹配的變量

 

III. 定義輸入輸出結構體

 1             struct a2v
 2  { 3  float4 vertex : POSITION; 4  float4 texcoord : TEXCOORD0; 5  }; 6 7 struct v2f 8  { 9  float4 pos : SV_POSITION; 10  float4 uv : TEXCOORD0; 11 };

這里只是簡單的處理圖片采樣,所以輸入輸出結構體比較簡單

 

IV. 定義頂點着色器

1     v2f vert(a2v v)
2  { 3  v2f o; 4 o.pos = UnityObjectToClipPos(v.vertex); 5 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); 6 o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); 7 return o; 8 }

 

我們使用一個插值寄存器存儲兩張紋理的坐標,兩張紋理都進行了同樣的操作:先回復到正確的紋理坐標,再在水平方向上進行偏移 。我們使用了 frac 函數進行偏移,有關 frac 函數的定義,我們可以在 MSDN 上找到

 

 

 

這個函數會返回參數 x 的小數部分,相當於在 0 ~ 1 之間循環,紋理會在水平方向上循環偏移

 

V. 定義片元着色器

1     fixed4 frag(v2f i) : SV_Target
2  { 3 fixed4 firstLayer = tex2D(_MainTex,i.uv.xy); 4 fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw); 5 fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); 6 c.rgb *= _Multiplier; 7 c.rgb *= _Color.rgb; 8 return c; 9 }

片元着色器比較簡單,主要是對兩張紋理采樣,然后進行混合

 

VI. 最后關閉 FallBack 或者 Fallback "VertexLit" 均可

 

VII. 完整代碼

 1 Shader "Unity/02-ScrollingBackground" {
 2  Properties { 3 _Color ("Color", Color) = (1,1,1,1) 4 _MainTex ("FarLayer ", 2D) = "white" {} 5 _DetailTex("NearLayer ", 2D) = "white" {} 6 _ScrollX ("Far layer scroll Speed",Float) = 1.0 7 _Scroll2X("Near layer scroll Speed",Float) = 1.0 8 _Multiplier ("Layer Multiplier",Float) = 1.0 9  } 10  SubShader 11  { 12 Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"} 13  Pass 14  { 15 Tags{"LightMode" = "ForwardBase"} 16  ZWrite Off 17  Blend SrcAlpha OneMinusSrcAlpha 18 19  CGPROGRAM 20 #include "UnityCG.cginc" 21 #pragma multi_compile_fwdbase 22 #pragma vertex vert 23 #pragma fragment frag 24 25  fixed4 _Color; 26  sampler2D _MainTex; 27  float4 _MainTex_ST; 28  sampler2D _DetailTex; 29  float4 _DetailTex_ST; 30 float _ScrollX; 31 float _Scroll2X; 32 float _Multiplier; 33 34 35 struct a2v 36  { 37  float4 vertex : POSITION; 38  float4 texcoord : TEXCOORD0; 39  }; 40 41 struct v2f 42  { 43  float4 pos : SV_POSITION; 44  float4 uv : TEXCOORD0; 45  }; 46 47  v2f vert(a2v v) 48  { 49  v2f o; 50 o.pos = UnityObjectToClipPos(v.vertex); 51 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); 52 o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); 53 return o; 54  } 55 56  fixed4 frag(v2f i) : SV_Target 57  { 58 fixed4 firstLayer = tex2D(_MainTex,i.uv.xy); 59 fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw); 60 fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); 61 c.rgb *= _Multiplier; 62 c.rgb *= _Color.rgb; 63 return c; 64  } 65 66  ENDCG 67  } 68  } 69 FallBack "VertexLit" 70 }

 

 VIII. 回到 Unity ,把准備好的圖像賦予 shader ,查看效果

 

 

3.3 總結

背景滾動是十分常用的技術,實現起來也是比較簡單,只是對紋理坐標進行水平上的循環偏移,然后進行采樣即可,關於視覺效果,讀者則可以按照自己喜歡進行調參。 

 

四. 總結

本文介紹了兩種紋理動畫,在實現上思路相似,都是對 UV 值進行偏移修改,然后對紋理進行采樣。紋理動畫實現起來是比較簡單的,與之相關的另外一種動畫,稱為頂點動畫,我們將在下一篇博文中介紹這種動畫效果並列出值得注意的事項。

雖然紋理動畫並不復雜,但其仍然是我們常用的技術實現。本文篇幅不多,希望能對讀者學 UV 動畫這一知識點有所幫助。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM