使用Unity著色器實現精靈(Sprite)塗鴉效果

遊資網發表於2020-01-03
本文將由來自英國的遊戲開發工程師Alan Zucconi分享如何在Unity中使用著色器製作近來流行的精靈塗鴉效果。

精靈塗鴉效果在過去幾年逐漸流行起來,《GoNNER》和《Baba is You》等遊戲大量使用了這種美術效果。

使用Unity著色器實現精靈(Sprite)塗鴉效果

本文將展示在無需繪製多個不同影象的情況下,如何實現精靈塗鴉效果。本文將介紹從Unity著色器程式設計的基礎到所應用的數學原理等所有必要知識。

引言

本文會涉及一些比較高階的話題,包括:反向運動學的數學原理和大氣的瑞利散射效果。但是既對這些內容感興趣,又有理解所需的必備技術知識的開發者其實並不多。

在遊戲開發者Nick Kaman的一則推文中,他展示瞭如何在Unity實現塗鴉效果。

使用Unity著色器實現精靈(Sprite)塗鴉效果

Nick Kaman:我想分享一個在Unity實現“塗鴉”效果的技巧:

我們不必繪製相同精靈的不同幀,我們可以把精靈放到網格,然後使用法線貼圖偏移頂點即可,該法線貼圖會每秒X次大幅進行滾動。





這篇推文獲得大量的點贊和轉發,我們發現,讓即使沒有著色器程式設計知識的人也可以理解的簡單教程是很有必要的。

如果想要製作2D精靈動畫的專業而高效的方法,並且需要完整的藝術級控制功能,你可以使用Doodle Studio 95!資源。

獲取Doodle Studio 95!:

https://fernandoramallo.itch.io/doodle-studio-95

下面是使用Doodle Studio 95!時的動畫圖片。

使用Unity著色器實現精靈(Sprite)塗鴉效果

塗鴉效果的剖析

為了實現塗鴉效果,我們首先需要理解實現原理以及使用了哪些技術。

著色器效果

首先,我們想要塗鴉效果儘可能輕量,不使用任何額外指令碼。我們可以通過著色器實現這種效果,指導Unity在螢幕上渲染3D模型或者平面模型。

精靈著色器

Unity提供了多種著色器的型別,如果使用Unity提供的2D工具,開發者可能想要處理精靈。在這種情況下,你需要使用精靈(Sprite)著色器,它是一種特殊型別的著色器,與Unity的SpriteRenderer相容。此外,你也可以使用較為傳統的Unlit著色器。

頂點替換

在手動繪製精靈時,不會有相同的兩個幀。我們想要通過使精靈進行“搖晃”,模擬出這種效果。使用著色器有一種非常高效的實現方法,該方法需要使用頂點替換功能。這種方法可以修改3D物件的頂點位置。如果隨機變化這些位置,我們就可以實現想要的效果。

對齊時間

手繪動畫通常有較低的幀率,如果我們想要模擬出諸如每秒5幀的畫面,我們需要每秒5次修改精靈的頂點位置。但是,Unity可能會在更高重新整理速率下執行遊戲,可能會有每秒30幀或60幀的幀率。為了確保我們的精靈不以每秒60次的速度發生變化,我們需要處理動畫的時間元件。


擴充套件精靈著色器

如果想要在Unity建立新的著色器,我們可以使用Unlit Shader,儘管它不一定是特定應用程式的最佳選擇。

如果想讓塗鴉著色器完全相容Unity的SpriteRenderer,我們需要擴充套件它的現有精靈著色器。但是,在Unity中無法直接獲取該著色器。

獲取該著色器的方法是:訪問Unity下載存檔頁面,下載正在使用Unity版本的Build in shaders資源包,該Zip壓縮檔案包含特定Unity版本推出的所有著色器原始碼。

下載Build in shaders資源包:

https://unity3d.com/get-unity/download/archive

使用Unity著色器實現精靈(Sprite)塗鴉效果

下載完成後,提取檔案,然後在builtin_shaders-2018.1.6f1\DefaultResourcesExtra資料夾內找到Sprites-Diffuse.shader檔案,它就是我們在本文中需要使用的檔案。

使用Unity著色器實現精靈(Sprite)塗鴉效果

丨如果Sprites-Diffuse檔案不是預設的精靈著色器,該怎麼辦?

在建立新的精靈時,預設材質使用的著色器名為Sprites-Default.shader,而不是Sprites-Diffuse.shader。

兩者的區別在於:前者是無光著色器,而後者會對場景的光線做出反應。由於Unity的實現方法,相對無光著色器,漫反射著色器可以更簡單地進行編輯。

頂點替換功能

在Sprites-Diffuse.shader檔案中,有一個稱為vert的函式,它就是之前提到的頂點函式。它的名稱並不重要,只要它符合#pragma指令的“vertex: ”部分內的名稱即可。
#pragma surface surf Lambert vertex:vert nofog nolightmap nodynlightmap keepalpha noinstancing

簡單來說,頂點函式會在3D模型的每個頂點呼叫,並決定如何在2D螢幕空間進行對映。對於本文而言,我們僅對理解如何替換物件感興趣。

引數appdata_full v包含名為vertex的欄位,該欄位包含物件空間中每個頂點的3D位置,修改它的數值會移動頂點。

例如:下面的程式碼會使用該著色器把物件沿著X軸平移一個單位。

  1. void vert (inout appdata_full v, out Input o)

  2. {

  3.            v.vertex = UnityFlipSprite(v.vertex, _Flip);


  4.            v.vertex.x += 1;


  5.            #if defined(PIXELSNAP_ON)

  6.            v.vertex = UnityPixelSnap (v.vertex);

  7.            #endif


  8.            UNITY_INITIALIZE_OUTPUT(Input, o);

  9.            o.color = v.color * _Color * _RendererColor;

  10. }
複製程式碼

預設情況下,使用Unity製作的2D遊戲僅處理X軸和Y軸,因此我們需要修改v.vertex.xy,從而在2D平面上移動精靈。

丨什麼是物件空間?

結構appdata_full的vertex欄位包含著色器在物件空間處理的當前頂點位置,如果物件處於遊戲世界的中心點即(0,0,0)座標,它就是該物件未經過縮放和旋轉時頂點的位置。

相對地,在世界空間表示的頂點會反映頂點在Unity場景內的實際位置。

丨為什麼物件不會以每幀1米的速度移動?

如果對C#指令碼的Update方法內transform.position的x部分加1,我們會看到物件以每幀1個單位速度飛行,換算的速度約為每小時216千米。

發生這種情況是因為C#對位置的改動會改變位置本身。在頂點函式中,這種情況不會發生,著色器僅會改變模型的視覺效果,但不會更新或改變模型上已儲存的頂點,因此給v.vertex.x新增+1僅會每次移動物件1米的距離。

丨別忘了以Tight型別匯入精靈。

該效果會替換精靈上的頂點。傳統情況下,精靈會作為四邊形(即下圖左側)匯入Unity。這意味著精靈僅有4個頂點。如果是這樣,只有這些頂點可以進行移動,從而會減少塗鴉效果的總體強度。

為了實現更為緊密和逼真的扭曲效果,我們應該確保精靈以Mesh Type設為Tight的情況進行匯入,這樣會把精靈包裝為凸面外殼(即下圖右側)。

使用Unity著色器實現精靈(Sprite)塗鴉效果

這樣做會提高頂點的數量,雖然這不總是理想的選擇,但卻是我們所需要的。

隨機的替換效果

塗鴉效果會隨機改變每個頂點的位置。在著色器取樣隨機數字是一件需要技巧的事,這是由於GPU的無狀態架構,它使模擬大多數庫使用的相同演算法變得更加困難和低效。

Nick Kaman提供的方法是使用噪聲紋理,該紋理在取樣時會得到隨機的感覺。對我們的情況而言,這種方法可能不是最高效的方法,因為它會加倍著色器必須執行的紋理查詢次數。

因此,許多著色器需要使用比較模糊和混亂的函式,即使它們的效果是確定的,而且在我們看來沒有任何模式。

由於函式必須是無狀態的,每個隨機數必須通過其自帶的種子程式碼來生成。這種方法的效果很好,因為每個頂點的位置都應該是獨特的。我們可以使用它關聯每個頂點的隨機數,我們會在後面討論這種隨機函式的實現方法,現在我們把該函式稱為random3。

我們可以使用random3函式生成每個頂點隨機的替換效果。在下面例子中,隨機數會通過_NoiseScale屬性調整,這樣可以控制替換效果的強度。

  1. void vert (inout appdata_full v, out Input o)

  2. {

  3.            ...

  4.            float2 noise = random3(v.vertex.xyz).xy * _NoiseScale;

  5.            v.vertex.xy += noise;

  6.            ...

  7. }
複製程式碼

現在我們要編寫random3函式的程式碼。

使用Unity著色器實現精靈(Sprite)塗鴉效果

著色器內的隨機效果

著色器中最常用和最具標誌性的偽隨機函式來自W.J.J. Rey在1998年發表的論文。

  1. float rand(float2 co)

  2. {

  3.     return fract(sin(dot(co.xy ,float2(12.9898,78.233))) * 43758.5453);

  4. }
複製程式碼

該函式是確定性的,也就是說它不是真正具有隨機效果,但是它的行為非常不規律,使它看起來完全是隨機的,這類函式被稱為偽隨機函式。對於本教程,我使用了Nikita Miropolskiy編寫的高階函式。

新增時間

通過使用已經編寫好的程式碼,我們現在可以實現每個點都會在每幀替換相同的次數。這樣會實現搖擺的精靈,而不是塗鴉效果。

為了解決該問題,我們需要找到隨時間改變效果的方法,最簡單的方法是使用頂點位置和當前時間來生成隨機數。

在這種情況下,我們新增了以秒為單位的當前時間值_Time.y到頂點位置。

  1. float time = float3(_Time.y, 0, 0);

  2. float2 noise = random3(v.vertex.xyz + time).xy * _NoiseScale;

  3. v.vertex.xy += noise;
複製程式碼

更高階的效果需要更復雜的方法來整合時間到計算方程式中,但由於我們只想實現間隔的隨機效果,因此新增兩個數值就足夠了。

使用Unity著色器實現精靈(Sprite)塗鴉效果

對齊時間

新增_Time.y的主要問題是:它會造成精靈在每幀都發生變化。這是不理想的效果,因為大多數手繪的動畫都有較低的幀率。

時間元件不應該有連續的效果,而是應該變得離散化,這意味著如果我們想實現每秒5幀,它應該僅在每秒改變5次。使用熟悉術語的話說,那就是:時間應該“對齊”為一秒的五分之一。因此,可以使用的數值應該為:0/5 = 0,1/5 = 0.2,2/5 = 0.4,3/5 = 0.6,4/5 = 0.8,5/5 = 1 ,以此類推。

下面的函式會接收數值x,對齊到Snap值的整數倍數。

  1. inline float snap (float x, float snap)

  2. {

  3.            return snap * round(x / snap);

  4. }

複製程式碼

因此,我們可以更新為以下程式碼:
  1. float time = snap(_Time.y, _NoiseSnap);

  2. float2 noise = random3(v.vertex.xyz + float3(time, 0.0, 0.0) ).xy * _NoiseScale;

  3. v.vertex.xy += noise;
複製程式碼
大功告成,最後的效果如下圖所示。

使用Unity著色器實現精靈(Sprite)塗鴉效果

小結

如何使用Unity著色器實現精靈塗鴉效果為大家介紹到這裡,喜歡自定義著色器的朋友們不妨一試。

相關文章