精靈涂鴉效果在過去幾年逐漸流行起來,《GoNNER》和《Baba is You》等游戲大量使用了這種美術(shù)效果。
本文將展示在無需繪制多個不同圖像的情況下,如何實現(xiàn)精靈涂鴉效果。本文將介紹從Unity著色器編程的基礎到所應用的數(shù)學原理等所有必要知識。
引言 本文會涉及一些比較高級的話題,包括:反向運動學的數(shù)學原理和大氣的瑞利散射效果。但是既對這些內(nèi)容感興趣,又有理解所需的必備技術(shù)知識的開發(fā)者其實并不多。 在游戲開發(fā)者Nick Kaman的一則推文中,他展示了如何在Unity實現(xiàn)涂鴉效果。 Nick Kaman:我想分享一個在Unity實現(xiàn)“涂鴉”效果的技巧: 我們不必繪制相同精靈的不同幀,我們可以把精靈放到網(wǎng)格,然后使用法線貼圖偏移頂點即可,該法線貼圖會每秒X次大幅進行滾動。 這篇推文獲得大量的點贊和轉(zhuǎn)發(fā),我們發(fā)現(xiàn),讓即使沒有著色器編程知識的人也可以理解的簡單教程是很有必要的。 如果想要制作2D精靈動畫的專業(yè)而高效的方法,并且需要完整的藝術(shù)級控制功能,你可以使用Doodle Studio 95!資源。 獲取Doodle Studio 95!: https://fernandoramallo.itch.io/doodle-studio-95 下面是使用Doodle Studio 95!時的動畫圖片。 涂鴉效果的剖析 為了實現(xiàn)涂鴉效果,我們首先需要理解實現(xiàn)原理以及使用了哪些技術(shù)。 著色器效果 首先,我們想要涂鴉效果盡可能輕量,不使用任何額外腳本。我們可以通過著色器實現(xiàn)這種效果,指導Unity在屏幕上渲染3D模型或者平面模型 精靈著色器 Unity提供了多種著色器的類型,如果使用Unity提供的2D工具,開發(fā)者可能想要處理精靈。在這種情況下,你需要使用精靈(Sprite)著色器,它是一種特殊類型的著色器,與Unity的SpriteRenderer兼容。此外,你也可以使用較為傳統(tǒng)的Unlit著色器。 頂點替換 在手動繪制精靈時,不會有相同的兩個幀。我們想要通過使精靈進行“搖晃”,模擬出這種效果。使用著色器有一種非常高效的實現(xiàn)方法,該方法需要使用頂點替換功能。這種方法可以修改3D對象的頂點位置。如果隨機變化這些位置,我們就可以實現(xiàn)想要的效果。 對齊時間 手繪動畫通常有較低的幀率,如果我們想要模擬出諸如每秒5幀的畫面,我們需要每秒5次修改精靈的頂點位置。但是,Unity可能會在更高刷新速率下運行游戲,可能會有每秒30幀或60幀的幀率。為了確保我們的精靈不以每秒60次的速度發(fā)生變化,我們需要處理動畫的時間組件。 擴展精靈著色器 如果想要在Unity創(chuàng)建新的著色器,我們可以使用Unlit Shader,盡管它不一定是特定應用程序的最佳選擇。 如果想讓涂鴉著色器完全兼容Unity的SpriteRenderer,我們需要擴展它的現(xiàn)有精靈著色器。但是,在Unity中無法直接獲取該著色器。 獲取該著色器的方法是:訪問Unity下載存檔頁面,下載正在使用Unity版本的Build in shaders資源包,該Zip壓縮文件包含特定Unity版本推出的所有著色器源代碼。 下載Build in shaders資源包: https://unity3d.com/get-unity/download/archive 下載完成后,提取文件,然后在builtin_shaders-2018.1.6f1\DefaultResourcesExtra文件夾內(nèi)找到Sprites-Diffuse.shader文件,它就是我們在本文中需要使用的文件。 如果Sprites-Diffuse文件不是默認的精靈著色器,該怎么辦? 在創(chuàng)建新的精靈時,默認材質(zhì)使用的著色器名為Sprites-Default.shader,而不是Sprites-Diffuse.shader。 兩者的區(qū)別在于:前者是無光著色器,而后者會對場景的光線做出反應。由于Unity的實現(xiàn)方法,相對無光著色器,漫反射著色器可以更簡單地進行編輯。 頂點替換功能 在Sprites-Diffuse.shader文件中,有一個稱為vert的函數(shù),它就是之前提到的頂點函數(shù)。它的名稱并不重要,只要它符合#pragma指令的“vertex: ”部分內(nèi)的名稱即可。 #pragma surface surf Lambert vertex:vert nofog nolightmap nodynlightmap keepalpha noinstancing 簡單來說,頂點函數(shù)會在3D模型的每個頂點調(diào)用,并決定如何在2D屏幕空間進行映射。對于本文而言,我們僅對理解如何替換對象感興趣。 參數(shù)appdata_full v包含名為vertex的字段,該字段包含對象空間中每個頂點的3D位置,修改它的數(shù)值會移動頂點。 例如:下面的代碼會使用該著色器把對象沿著X軸平移一個單位。 void vert (inout appdata_full v, out Input o) { v.vertex = UnityFlipSprite(v.vertex, _Flip); v.vertex.x += 1; #if defined(PIXELSNAP_ON) v.vertex = UnityPixelSnap (v.vertex); #endif UNITY_INITIALIZE_OUTPUT(Input, o); o.color = v.color * _Color * _RendererColor; } 默認情況下,使用Unity制作的2D游戲僅處理X軸和Y軸,因此我們需要修改v.vertex.xy,從而在2D平面上移動精靈。 什么是對象空間? 結(jié)構(gòu)appdata_full的vertex字段包含著色器在對象空間處理的當前頂點位置,如果對象處于游戲世界的中心點即(0,0,0)坐標,它就是該對象未經(jīng)過縮放和旋轉(zhuǎn)時頂點的位置。 相對地,在世界空間表示的頂點會反映頂點在Unity場景內(nèi)的實際位置。 為什么對象不會以每幀1米的速度移動? 如果對C#腳本的Update方法內(nèi)transform.position的x部分加1,我們會看到對象以每幀1個單位速度飛行,換算的速度約為每小時216千米。 發(fā)生這種情況是因為C#對位置的改動會改變位置本身。在頂點函數(shù)中,這種情況不會發(fā)生,著色器僅會改變模型的視覺效果,但不會更新或改變模型上已保存的頂點,因此給v.vertex.x添加+1僅會每次移動對象1米的距離。 別忘了以Tight類型導入精靈。 該效果會替換精靈上的頂點。傳統(tǒng)情況下,精靈會作為四邊形(即下圖左側(cè))導入Unity。這意味著精靈僅有4個頂點。如果是這樣,只有這些頂點可以進行移動,從而會減少涂鴉效果的總體強度。 為了實現(xiàn)更為緊密和逼真的扭曲效果,我們應該確保精靈以Mesh Type設為Tight的情況進行導入,這樣會把精靈包裝為凸面外殼(即下圖右側(cè))。 這樣做會提高頂點的數(shù)量,雖然這不總是理想的選擇,但卻是我們所需要的。 隨機的替換效果 涂鴉效果會隨機改變每個頂點的位置。在著色器采樣隨機數(shù)字是一件需要技巧的事,這是由于GPU的無狀態(tài)架構(gòu),它使模擬大多數(shù)庫使用的相同算法變得更加困難和低效。 Nick Kaman提供的方法是使用噪聲紋理,該紋理在采樣時會得到隨機的感覺。對我們的情況而言,這種方法可能不是最高效的方法,因為它會加倍著色器必須執(zhí)行的紋理查詢次數(shù)。 因此,許多著色器需要使用比較模糊和混亂的函數(shù),即使它們的效果是確定的,而且在我們看來沒有任何模式。 由于函數(shù)必須是無狀態(tài)的,每個隨機數(shù)必須通過其自帶的種子代碼來生成。這種方法的效果很好,因為每個頂點的位置都應該是獨特的。我們可以使用它關(guān)聯(lián)每個頂點的隨機數(shù),我們會在后面討論這種隨機函數(shù)的實現(xiàn)方法,現(xiàn)在我們把該函數(shù)稱為random3。 我們可以使用random3函數(shù)生成每個頂點隨機的替換效果。在下面例子中,隨機數(shù)會通過_NoiseScale屬性調(diào)整,這樣可以控制替換效果的強度。 void vert (inout appdata_full v, out Input o) { ... float2 noise = random3(v.vertex.xyz).xy * _NoiseScale; v.vertex.xy += noise; ... } 現(xiàn)在我們要編寫random3函數(shù)的代碼。 著色器內(nèi)的隨機效果 著色器中最常用和最具標志性的偽隨機函數(shù)來自W.J.J. Rey在1998年發(fā)表的論文。 float rand(float2 co) { return fract(sin(dot(co.xy ,float2(12.9898,78.233))) * 43758.5453); } 該函數(shù)是確定性的,也就是說它不是真正具有隨機效果,但是它的行為非常不規(guī)律,使它看起來完全是隨機的,這類函數(shù)被稱為偽隨機函數(shù)。對于本教程,我使用了Nikita Miropolskiy編寫的高級函數(shù)。 添加時間 通過使用已經(jīng)編寫好的代碼,我們現(xiàn)在可以實現(xiàn)每個點都會在每幀替換相同的次數(shù)。這樣會實現(xiàn)搖擺的精靈,而不是涂鴉效果。 為了解決該問題,我們需要找到隨時間改變效果的方法,最簡單的方法是使用頂點位置和當前時間來生成隨機數(shù)。 在這種情況下,我們添加了以秒為單位的當前時間值_Time.y到頂點位置。 float time = float3(_Time.y, 0, 0); float2 noise = random3(v.vertex.xyz + time).xy * _NoiseScale; v.vertex.xy += noise; 更高級的效果需要更復雜的方法來集成時間到計算方程式中,但由于我們只想實現(xiàn)間隔的隨機效果,因此添加兩個數(shù)值就足夠了。 對齊時間 添加_Time.y的主要問題是:它會造成精靈在每幀都發(fā)生變化。這是不理想的效果,因為大多數(shù)手繪的動畫都有較低的幀率 時間組件不應該有連續(xù)的效果,而是應該變得離散化,這意味著如果我們想實現(xiàn)每秒5幀,它應該僅在每秒改變5次。使用熟悉術(shù)語的話說,那就是:時間應該“對齊”為一秒的五分之一。因此,可以使用的數(shù)值應該為:0/5 = 0,1/5 = 0.2,2/5 = 0.4,3/5 = 0.6,4/5 = 0.8,5/5 = 1 ,以此類推。 下面的函數(shù)會接收數(shù)值x,對齊到Snap值的整數(shù)倍數(shù)。 inline float snap (float x, float snap) { return snap * round(x / snap); } 因此,我們可以更新為以下代碼: float time = snap(_Time.y, _NoiseSnap); float2 noise = random3(v.vertex.xyz + float3(time, 0.0, 0.0) ).xy * _NoiseScale; v.vertex.xy += noise; 大功告成,最后的效果如下圖所示。