Unity手游開發札記——移動平台的天氣系統實現

0. 牢騷

我發現,每個月的20+號是我有精力寫博客的時間……

這次項目算是經歷的第一次嚴格意義上的渠道測試,更換了正式名稱,見了更多玩家,開發組也經歷的更多通宵……評價和數據如何暫時還未揭曉,趁著沒那麼忙,來還欠自己的「文章債務」。。。

這篇博客主題是移動平台的天氣系統,做這個系統的主要原因是美術需求——大世界沙盤的動態效果太少了,需要一些動態變化的東西來增加效果。之前也看過一篇博客《Unity3D手游開發日記(7) - 適合移動平台的天氣效果》,作者對於每種天氣效果大致聊了原理,我也挺感興趣,就在今天五一假期(是的,沒看錯,就是半年前的五一……)蹲在家裡花了一天多時間照著文章的思路擼了一個簡單版本,在加上之前積累的多雲的效果,算是我們項目中天氣系統的雛形。過了假期放出來給同事體驗了下,感覺還不錯,然後稍微修改了一些bug就被其他事情擱置下來了,所以7月份測試也沒有放出來。

7月技術測試之後,美術效果的增強也就被逐漸放到更高的優先順序,天氣系統也就成為了我的工作重點之一。經歷整體結構的重構和一些效果實現方式的改變,才有了目前測試在用的這個版本。本篇文章就以當前已經實現的幾種天氣效果為例來聊一下在移動平台上實現一套簡單的天氣系統的思路和方法。

1. 綜述

整體來說,移動平台的性能還不足以支撐端游上完整的一套天氣系統,Unity的Asset Store上有一些不錯的天氣效果實現,也只能看著流流口水,並不敢用,比如這個Weather Maker - Sky, Weather, Fog, Volumetric Light and Dynamic Environment,還有UniStorm。(UniStorm有一個Mobile版本,效果也還不錯,有興趣的同學可以去搜索看下。)

那麼,在移動端,天氣系統效果簡答來說也就成了美術做做特效,程序按照需求寫寫掛特效的腳本罷了。的確,在製作各個天氣的效果的時候,並沒有用到什麼特別的技術點,但整個實現天氣系統的過程中,我沒有依賴於美術,而是自己尋找所有需要的資源,編寫邏輯進行整合簡化,過程還比較有趣,體會到非常直接的成就感,一些小的細節也自己去處理,非常開心。目前實現的天氣效果包括晴天、多雲、陰天、雨天和雪天這幾種比較常見的效果,逐一來進行說明。

2. 晴天效果

我們項目中美術製作的所有場景都是按照晴天的效果來製作的,所以對於程序來說,晴天效果就是沒效果,實現最簡單,性能最優,哈哈~(就是注意把其他效果清空不要殘留……)

3. 多雲效果

先看一下最終實現的多雲效果截圖,動態圖比較容易看出效果,靜態圖感覺比較怪,可以注意主城的模型有一半是被雲遮住了。為了凸顯效果雲陰影的濃度被我故意調整得比較高。

多雲效果截圖

這個效果是之前美術想要的一個內容。如果使用真正移動一個半透的雲模型在空中移動並且產生投影,移動設備上所能支持的shadowmap尺寸無法提供足夠的陰影精度,而直接進行投影的方法又比較難做到在高低不平的山、建築等物體表面計算投影效果。經過調研之後,使用了一個購買的插件Screen Space Cloud Shadow。插件頁面有動態效果視頻,想看動態效果的可以去看下。當時同樣調研了另外一個插件Cloud Shadows,都試玩了下。後者是基於light的cookie的,在當時的unity版本中有些小問題沒有解決掉,而且我自己試驗的cookie在移動設備上有點小問題,所以就沒有選用。Screen Space Cloud Shadow這個插件使用起來比較方便,只需要把prefab丟場景里就好,開關也很簡單,代價就是需要深度圖,場景內所有物件都要繪製兩遍,draw call和面數都會翻倍。這也是整個天氣系統中消耗最大的一塊,因此多雲天氣在最終版本里也只有高配下才會開啟。

由於是購買的插件,因此貼代碼不太合適,簡單說一下實現的原理:shader使用Transparent渲染隊列,在OnWillRenderObject中將一個平面放到相機的遠平面,並且把尺寸縮放成和相機的遠平面一樣,這樣就保證它的繪製過程是在最後,用FrameDebugger抓幀看繪製順序和參數如下圖:

雲陰影的繪製過場截圖

在Shader的frag過程中,根據深度圖和世界空間的攝像機方向射線來計算出陰影應該繪製的濃度。這裡包含了一些magic value,我也有些細節沒有看得特別懂……再加上本身並不是我自己設計的演算法,因此不在這裡詳述了,有興趣想了解的朋友可以自己去購買一份插件,source code include。

這裡只說明三個遇到並解決的小坑:

  1. 由於雲的陰影是飄動的,因此涉及到uv的流動,這個是根據時間來計算的,最初的時候這個時間直接取了Time.time的值,當遊戲運行一段時間之後,這個值就會變得很大,在移動設備上會導致雲的移動出現頓卡的感覺。這也是在很多使用uv流動的過程中很容易出現的一個問題,通過取余的方式可以保證精度,但是可能會在取余的那一幀出現採樣不連續的問題。由於我們不會非常長時間開啟這個效果,因此這個問題可以通過在開關的時候把時間參數重置來規避。
  2. fixed類型在移動設備上精度問題導致馬賽克。原來Shader中使用了fixed的值,在PC上並沒有問題,安卓設備上發現了馬賽克的現象,修改幾個關鍵值為float類型可以解決馬賽克問題。
  3. 由於使用了深度圖,因此深度圖的精度對於雲的效果影響比較大。我們最初相機的遠平攝設置得非常遠,近平面又非常近,0.1-1000這樣的值域範圍。在PC上沒有問題,手機上就有非常明顯的馬賽克,將近平面和遠平面都調整一下,變為1-300,效果好了很多。(順便再推薦一下在UWA群里推薦過的調試插件,Hdg Remote Debug - Live Update Tool,可以在電腦上連接移動設備進行實時調試,用於排查和調試這種問題比頻繁打包要方便很多,節省太多時間,已經被我默認打包進了dev版本的工程里。)

4. 陰天

陰天的效果其實就是天色變暗的感覺,如果是實時光照的話可以通過調暗方向光的亮度或者顏色來處理,但是由於手游上目前大都還是烘焙的,因此比較方便的方案就是通過後處理來實現。

考慮過Color Grading方案,但是感覺稍微有點耗,而且和晝夜系統實現會有些小衝突,最後實現的時候選擇了直接在顏色上乘以一個Tint Color的方案來做,由於我們整合了整個後處理效果棧,因此在開啟別的後處理的情況下,這個tint color的過程消耗非常小,每個像素多一個乘法而已。

這裡也再推薦一下錢康來一直推薦的將所有的後處理Pass進行整合的方案,也就是參考Unity官方的Github實現:Post Processing Stack,Asset Store上也有Post Processing Stack。

5. 雨天

雨天的效果實現了兩個版本,最初的版本是基於前文提到的博客里的思路來實現的,就是掛一個uv流動的面片在鏡頭前,閃電的效果就是把這個面片調整為白色再調整回來。實現非常簡單,這裡只貼一下Shader代碼好了,因為沒有真正在項目中使用,所以只算私貨。

Shader "Shader/Scene/Rain" {n Properties{n _RainTex("Main Texture:", 2D) = "white" {}n _RainIntensity("Intensity of Rain:",Float) = 0.0n _FallSpeed("Fall Speed of Rain:",Float) = 1n _ThunderLighting("Thunder Lighting", Color) = (0, 0, 0, 0.5)n }nn SubShader{n Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }nn Blend SrcAlpha Onenn LOD 100n Cull Offn ZWRITE Offn Lighting Offnn Pass{n CGPROGRAMn #pragma vertex vertn #pragma fragment fragn #pragma target 2.0nn #include "UnityCG.cginc"nn sampler2D _RainTex;n float4 _RainTex_ST;n fixed _FallSpeed;n fixed _RainIntensity;n float4 _ThunderLighting;nn struct appdata_t {n float4 vertex : POSITION;n float2 texcoord : TEXCOORD0;n };nn struct v2f {n float4 vertex : SV_POSITION;n float2 texcoord : TEXCOORD0;n };nn v2f vert(appdata_t v)n {n v2f o;n o.vertex = UnityObjectToClipPos(v.vertex);n o.texcoord = TRANSFORM_TEX(v.texcoord, _RainTex);n return o;n }nn fixed4 frag(v2f i) : SV_Targetn {n fixed2 UV = i.texcoord;n float Time = _Time.y;n fixed vValue = _FallSpeed * Time;nn UV = fixed2(UV.x, UV.y + _FallSpeed * Time);n fixed4 col = tex2D(_RainTex, UV);n col.rgb = col.rgb * col.a * _RainIntensity + _ThunderLighting.rgb;n col.a = 1.0f;n return col;n }n ENDCGn }n }n}n

實現的效果截圖如下圖所示。鏡頭前的面片使用的貼圖也是我從網上找的雨滴雜訊貼圖自己修改的,因此有不連續的問題,截圖中可以看出來,這個也是自己P圖的基本功不夠的原因……

初版的下雨效果截圖

這裡學習到一個小的技巧是可以使用Unity的AnimationCurve來做一些曲線供遊戲邏輯使用,從而做出來一些變化的效果,這裡就用曲線控制了雨的濃度和與雷聲配合的閃電效果,C#代碼也貼一下。

using UnityEngine;nnnamespace ThorFramework.Weathern{n [DisallowMultipleComponent]n public class RainController : MonoBehaviourn {n public AnimationCurve rainCurve;n public AnimationCurve thunderCurve;nn private Color lightingColor = Color.white;n private Material weatherMaterial;n private float startTime = 0.0f;n private AudioSource thunderAudio;nn // Use this for initializationn void Start()n {n MeshRenderer r = gameObject.GetComponent<MeshRenderer>();n if (r != null)n {n weatherMaterial = r.material;n startTime = Time.time;n }n thunderAudio = gameObject.GetComponent<AudioSource>();n }nn void OnEnable()n {n startTime = Time.time;n }nn // Update is called once per framen void Update()n {n float curveTime = Time.time - startTime;n if (weatherMaterial == null)n {n return;n }n if (rainCurve != null)n {n float val = rainCurve.Evaluate(curveTime);n weatherMaterial.SetFloat("_RainIntensity", val);n thunderAudio.volume = 2.0f * val;n }nn if (thunderCurve != null)n {n float val = thunderCurve.Evaluate(curveTime);n weatherMaterial.SetColor("_ThunderLighting", lightingColor*val);n }n }n }n}n

這種實現OverDraw會直接翻倍,但是沒有其他的太多額外消耗,因此性能上還比較節省,大致測試了下對於性能幾乎感受不出來影響,特別是被降低了解析度的情況下。但是最終我們還是沒有採用這種方案,主要原因是這種效果很難做出深度感,就是雨滴真的在空間中有分布的感覺。最終還是用了粒子特效,一個一直掛在相機前的特效,在區域範圍內一直產生垂直墜落的雨滴。

在這之前我沒怎麼玩過粒子系統,這裡從頭學習製作一個粒子特效,還是挺有趣的。粒子系統可以用比較簡單的方法製作出非常酷炫的效果。最終實現效果的截圖如下:

雨天效果截圖

這裡雨的效果包括三個部分:

  1. 跟隨相機移動的一個產生雨滴的特效,截圖中雨滴不是很密集,但是動起來的效果還是不錯的。這裡為了追求效果粒子數量上限給到了500左右,但是仍然不是非常密集,做不到暴風雨的感覺,還需要添加一些面片來做更加密集的雨滴效果。
  2. 跟隨角色移動的地面漣漪。在通常的做法中,雨滴漣漪的製作是用粒子系統的碰撞來做的。當粒子產生了碰撞之後就會產生一個新的粒子效果,這樣可以做到很精準的感覺,包括落在樹葉上、建築房頂上等,但是消耗也比較大。我們採用的是比較討巧的方法,角色腳底掛一個不斷隨機產生漣漪的粒子特效,在斜坡、橋上等地方會有穿幫的小問題,但是也基本滿足的策劃的需求。
  3. 與陰天一樣,下雨的時候會陰暗一些,所以同樣掛了一個tint color調色的後處理。

總結:雨的效果花費了挺多精力來製作,最終的效果基本滿意。使用特效的方案整體的overdraw沒有那麼高,但是為了出效果粒子數量用得還算比較多,因此在粒子系統上的性能消耗還挺大的。對比之前面片的方案各有優劣,只是出了追求高品質效果的考慮選擇了效果上限較高的粒子系統來實現。

6. 雪天

在實現雨天的效果之後,雪天的效果製作就非常簡單了,霧效果加上一個和雨滴相似的粒子特效掛在鏡頭前就可以啦。由於雪花生命周期比較長,飄落速度比較慢,粒子數最多在300左右就可以達到不錯的效果。實現的效果截圖如下(這裡有一些序列幀動畫之類的小技巧可以優化雪片的效果,不過不屬於程序的技術了,特效同學應該都會的):

雪天效果截圖

也同樣研究了一下《鎮魔曲》中雪花效果的實現,發現比較討巧的是他們沒有讓一個雪花是一個粒子,而是用一張圖來表現幾片雪花的效果,然後大約只需要同時存在十幾個粒子就可以做到比較密集的下雪效果。當然代價是仔細觀察的話會發現一些重複感,overdraw也會稍微有些提高,但是粒子數量降低得會比較多,值得借鑒。(我們美術同學嘗試了一個版本之後告訴我不太滿意,當然在看了完全隨機的效果之後,對於略有重複的效果自然能感覺出來瑕疵,沒有對比才沒有傷害……)

7. 風

風不屬於任何一個天氣,而是用於輔助表現其他天氣效果的元素,在我們遊戲中主要能做的表現是樹木的搖擺和一些相應的音效。搖擺的效果採用頂點動畫來實現,已有的實現方案可以參考Unity3D手游開發日記(5) - 適合移動平台的植被隨風擺動這篇文章,網上也有很多實現細節的討論,但比較好的方案追本溯源還是《GPU Gems》中的一篇文章:《Chapter 16. Vegetation Procedural Animation and Shading in Crysis》。它主要描述了在CryEngine中的實現原理,考慮到樹榦和樹葉的不同,使用頂點色來對振幅進行控制,估計很多人都讀過,實現細節可以去參考原文。

這裡只說幾個我們移植時的幾個修改:

  1. 使用Shader的全局變數。Shader.SetGlobalXXX一系列的介面就是為這種全局參數來設計的,簡單易用。
  2. 臨近測試我們美術比較忙,表示沒時間對每棵樹的模型去刷頂點色,於是搖擺的幅度控制採用了一個簡化的方案——由頂點高度和一個美術設定的模型高度的比值來決定,目前只採用的線性差值,效果一般,勉強夠用。
  3. GPU Gem中的實現比較複雜,考慮了橫向的和縱向的抖動,有不少計算在裡面,這塊可以根據自己項目的遊戲類型和需求來修改和簡化。

8. 整合

把實現的各個天氣效果整合成天氣系統,由一個管理器來控制,可以模擬遊戲中各個國家的氣候風格,這是最後整合進遊戲進行實際應用的步驟。由於我們大世界和戰鬥場景是兩種完全不同的鏡頭方式,因此最終特效掛接的部分實現了兩套不同的控制邏輯。除此之外,根據不同國家的特性,也將雨天和雪天統一為了特殊天氣,比如在燕國這樣靠北的國家,就只會下雪,而其他國家則是下雨。這其中有很多繁雜的與遊戲業務相關的邏輯就不談了,只聊幾個實現過程中比較有感觸的點:

  1. 漸變需求。天氣效果中所有的控制效果都有不同的漸變細節需要處理,比如下雪天氣停止不能突然沒有,而是要有漸漸消失的感覺;天氣由晴天變陰天,也不應該突然黑下來,而是要有一個亮度漸變的過程。這些需要各個天氣系統針對自己的效果做好差值的處理,這個過程使用了DoTween來做,代碼實現非常簡單高效。
  2. 對於需要跨天氣控制的效果進行統一的管理。在最初的版本里,用於表現變暗效果的Tint Color由每一個天氣進行各自的管理和差值,這裡就有一些非常噁心的特殊代碼要做處理,比如陰天效果的停止函數中,當進入晴天的時候需要把亮度逐漸調整到1,如果從陰天進入雨天,則不需要做這樣的調整。天氣效果的控制也就像是一個狀態機,在單獨的狀態中如果需要考慮變換的前後邏輯,代碼里就需要非常多if-else這樣的邏輯判斷。在迭代的過程中,把這塊的控制抽象成為一個天氣亮度管理器——BrightnessManager,它負責控制亮度並按照設定的速度在當前亮度和目標亮度之間做差值,這樣對於任何天氣效果,只需要在開啟的時候設置默認的亮度值給亮度管理器,其他細節都不需要關心。同樣還有用於風的參數控制的WindManager。
  3. 效果與實現邏輯的分離。從表面上看,下雨和下雪是兩個不同的天氣效果,但是他們在程序的邏輯是有很大相似性的——都是控制特效的掛接和跟隨邏輯。因此從邏輯實現上這兩個天氣效果有相同的邏輯,只是數據(特效)不同而已。另外下雨的效果有額外的一些漣漪的處理。於是使用面向對象的思路抽取一個FXWeather的公共父類來做代碼的復用,方便維護。

經過一些思考和迭代之後,最終C#代碼中的類圖如下所示。

最終實現的天氣系統類圖

9. 總結

回顧整個天氣系統的實現,其實沒有特別有難度的東西,只是一些效果的應用和業務邏輯的編寫。使用面向對象的繼承和組合,再加上狀態模式就完成了最後的需求。在效果方面,由於要兼顧移動平台的性能限制,相比端游的動態天氣效果做了很多妥協和簡化,盡量用20%的性能消耗做到60%的表現力,對於真實感等方面做了不少的妥協。

當然,現在實現的各種天氣效果還很簡陋,比如下雨還可以添加地面濕滑的材質效果,還可以製作暴風雨這樣更動感刺激的天氣效果,在沙漠中實現沙塵暴的感覺等等。這些東西,還需要更多的時間和精力來填滿缺失的細節。

無論如何,希望這篇文章可以給期望增強遊戲效果的同學一些啟發,也同樣期望有更好實現效果和方法的朋友不吝賜教,給予更多思路和經驗的分享。

2017年9月26日凌晨 於杭州家中

推薦閱讀:

【《Effective C#》提煉總結】提高Unity中C#代碼質量的22條準則
小隨筆:寫一個基於幾何生成方法的描邊效果
【Unity】UGUI系列教程——OSU!Battle!
骨骼與動畫重定向——Unity探索筆記之視覺解決方案(一)

TAG:Unity游戏引擎 |