文/賈偉昊

0. 牢騷

我發(fā)現(xiàn),每個(gè)月的20+號是我有精力寫博客的時(shí)間……

這次項(xiàng)目算是經(jīng)歷的第一次嚴(yán)格意義上的渠道測試,更換了正式名稱,見了更多玩家,開發(fā)組也經(jīng)歷的更多通宵……評價(jià)和數(shù)據(jù)如何暫時(shí)還未揭曉,趁著沒那么忙,來還欠自己的“文章債務(wù)”。。。

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

7月技術(shù)測試之后,美術(shù)效果的增強(qiáng)也就被逐漸放到更高的優(yōu)先級,天氣系統(tǒng)也就成為了我的工作重點(diǎn)之一。經(jīng)歷整體結(jié)構(gòu)的重構(gòu)和一些效果實(shí)現(xiàn)方式的改變,才有了目前測試在用的這個(gè)版本。本篇文章就以當(dāng)前已經(jīng)實(shí)現(xiàn)的幾種天氣效果為例來聊一下在移動平臺上實(shí)現(xiàn)一套簡單的天氣系統(tǒng)的思路和方法。

1. 綜述

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

那么,在移動端,天氣系統(tǒng)效果簡答來說也就成了美術(shù)做做特效,程序按照需求寫寫掛特效的腳本罷了。的確,在制作各個(gè)天氣的效果的時(shí)候,并沒有用到什么特別的技術(shù)點(diǎn),但整個(gè)實(shí)現(xiàn)天氣系統(tǒng)的過程中,我沒有依賴于美術(shù),而是自己尋找所有需要的資源,編寫邏輯進(jìn)行整合簡化,過程還比較有趣,體會到非常直接的成就感,一些小的細(xì)節(jié)也自己去處理,非常開心。目前實(shí)現(xiàn)的天氣效果包括晴天、多云、陰天、雨天和雪天這幾種比較常見的效果,逐一來進(jìn)行說明。

2. 晴天效果

我們項(xiàng)目中美術(shù)制作的所有場景都是按照晴天的效果來制作的,所以對于程序來說,晴天效果就是沒效果,實(shí)現(xiàn)最簡單,性能最優(yōu),哈哈~(就是注意把其他效果清空不要?dú)埩簟?br />
3. 多云效果

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

092402me881srss1m1rrbz.jpg
多云效果截圖

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

由于是購買的插件,因此貼代碼不太合適,簡單說一下實(shí)現(xiàn)的原理:shader使用Transparent渲染隊(duì)列,在OnWillRenderObject中將一個(gè)平面放到相機(jī)的遠(yuǎn)平面,并且把尺寸縮放成和相機(jī)的遠(yuǎn)平面一樣,這樣就保證它的繪制過程是在最后,用FrameDebugger抓幀看繪制順序和參數(shù)如下圖:

092402vga0c7h0fcf7gh7m.jpg
云陰影的繪制過場截圖

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

這里只說明三個(gè)遇到并解決的小坑:

1.由于云的陰影是飄動的,因此涉及到uv的流動,這個(gè)是根據(jù)時(shí)間來計(jì)算的,最初的時(shí)候這個(gè)時(shí)間直接取了Time.time的值,當(dāng)游戲運(yùn)行一段時(shí)間之后,這個(gè)值就會變得很大,在移動設(shè)備上會導(dǎo)致云的移動出現(xiàn)頓卡的感覺。這也是在很多使用uv流動的過程中很容易出現(xiàn)的一個(gè)問題,通過取余的方式可以保證精度,但是可能會在取余的那一幀出現(xiàn)采樣不連續(xù)的問題。由于我們不會非常長時(shí)間開啟這個(gè)效果,因此這個(gè)問題可以通過在開關(guān)的時(shí)候把時(shí)間參數(shù)重置來規(guī)避。

2.fixed類型在移動設(shè)備上精度問題導(dǎo)致馬賽克。原來Shader中使用了fixed的值,在PC上并沒有問題,安卓設(shè)備上發(fā)現(xiàn)了馬賽克的現(xiàn)象,修改幾個(gè)關(guān)鍵值為float類型可以解決馬賽克問題。

3.由于使用了深度圖,因此深度圖的精度對于云的效果影響比較大。我們最初相機(jī)的遠(yuǎn)平攝設(shè)置得非常遠(yuǎn),面又非常近,0.1-1000這樣的值域范圍。在PC上沒有問題,手機(jī)上就有非常明顯的馬賽克,將面和遠(yuǎn)平面都調(diào)整一下,變?yōu)?-300,效果好了很多。(順便再推薦一下在UWA群里推薦過的調(diào)試插件,Hdg Remote Debug - Live Update Tool,可以在電腦上連接移動設(shè)備進(jìn)行實(shí)時(shí)調(diào)試,用于排查和調(diào)試這種問題比頻繁打包要方便很多,節(jié)省太多時(shí)間,已經(jīng)被我默認(rèn)打包進(jìn)了dev版本的工程里。)

4. 陰天

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

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

這里也再推薦一下錢康來一直推薦的將所有的后處理Pass進(jìn)行整合的方案,也就是參考Unity官方的Github實(shí)現(xiàn):Post Processing Stack,Asset Store上也有Post Processing Stack。

5. 雨天

雨天的效果實(shí)現(xiàn)了兩個(gè)版本,最初的版本是基于前文提到的博客里的思路來實(shí)現(xiàn)的,就是掛一個(gè)uv流動的面片在鏡頭前,閃電的效果就是把這個(gè)面片調(diào)整為白色再調(diào)整回來。實(shí)現(xiàn)非常簡單,這里只貼一下Shader代碼好了,因?yàn)闆]有真正在項(xiàng)目中使用,所以只算私貨。
?

  1. Shader Shader/Scene/Rain {
  2. Properties{
  3. _RainTex(Main Texture:, 2D) = white {}
  4. _RainIntensity(Intensity of Rain:,Float) = 0.0
  5. _FallSpeed(Fall Speed of Rain:,Float) = 1
  6. _ThunderLighting(Thunder Lighting, Color) = (0, 0, 0, 0.5)
  7. }
  8. ?
  9. SubShader{
  10. Tags{ Queue = Transparent IgnoreProjector = True RenderType = Transparent }
  11. ?
  12. Blend SrcAlpha One
  13. ?
  14. LOD 100
  15. Cull Off
  16. ZWRITE Off
  17. Lighting Off
  18. ?
  19. Pass{
  20. CGPROGRAM
  21. #pragma vertex vert
  22. #pragma fragment frag
  23. #pragma target 2.0
  24. ?
  25. #include UnityCG.cginc
  26. ?
  27. sampler2D _RainTex;
  28. float4 _RainTex_ST;
  29. fixed _FallSpeed;
  30. fixed _RainIntensity;
  31. float4 _ThunderLighting;
  32. ?
  33. struct appdata_t {
  34. float4 vertex : POSITION;
  35. float2 texcoord : TEXCOORD0;
  36. };
  37. ?
  38. struct v2f {
  39. float4 vertex : SV_POSITION;
  40. float2 texcoord : TEXCOORD0;
  41. };
  42. ?
  43. v2f vert(appdata_t v)
  44. {
  45. v2f o;
  46. o.vertex = UnityObjectToClipPos(v.vertex);
  47. o.texcoord = TRANSFORM_TEX(v.texcoord, _RainTex);
  48. return o;
  49. }
  50. ?
  51. fixed4 frag(v2f i) : SV_Target
  52. {
  53. fixed2 UV = i.texcoord;
  54. float Time = _Time.y;
  55. fixed vValue = _FallSpeed * Time;
  56. ?
  57. UV = fixed2(UV.x, UV.y + _FallSpeed * Time);
  58. fixed4 col = tex2D(_RainTex, UV);
  59. col.rgb = col.rgb * col.a * _RainIntensity + _ThunderLighting.rgb;
  60. col.a = 1.0f;
  61. return col;
  62. }
  63. ENDCG
  64. }
  65. }
  66. }

?

?

初版的下雨效果截圖
這里學(xué)習(xí)到一個(gè)小的技巧是可以使用Unity的AnimationCurve來做一些曲線供游戲邏輯使用,從而做出來一些變化的效果,這里就用曲線控制了雨的濃度和與雷聲配合的閃電效果,C#代碼也貼一下。

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

雨天效果截圖
這里雨的效果包括三個(gè)部分:

1.跟隨相機(jī)移動的一個(gè)產(chǎn)生雨滴的特效,截圖中雨滴不是很密集,但是動起來的效果還是不錯的。這里為了追求效果粒子數(shù)量上限給到了500左右,但是仍然不是非常密集,做不到暴風(fēng)雨的感覺,還需要添加一些面片來做更加密集的雨滴效果。

2.跟隨角色移動的地面漣漪。在通常的做法中,雨滴漣漪的制作是用粒子系統(tǒng)的碰撞來做的。當(dāng)粒子產(chǎn)生了碰撞之后就會產(chǎn)生一個(gè)新的粒子效果,這樣可以做到很精準(zhǔn)的感覺,包括落在樹葉上、建筑房頂上等,但是消耗也比較大。我們采用的是比較討巧的方法,角色腳底掛一個(gè)不斷隨機(jī)產(chǎn)生漣漪的粒子特效,在斜坡、橋上等地方會有穿幫的小問題,但是也基本滿足的策劃的需求。

3.與陰天一樣,下雨的時(shí)候會陰暗一些,所以同樣掛了一個(gè)tint color調(diào)色的后處理。

總結(jié):雨的效果花費(fèi)了挺多精力來制作,最終的效果基本滿意。使用特效的方案整體的overdraw沒有那么高,但是為了出效果粒子數(shù)量用得還算比較多,因此在粒子系統(tǒng)上的性能消耗還挺大的。對比之前面片的方案各有優(yōu)劣,只是出了追求高品質(zhì)效果的考慮選擇了效果上限較高的粒子系統(tǒng)來實(shí)現(xiàn)。

6. 雪天

在實(shí)現(xiàn)雨天的效果之后,雪天的效果制作就非常簡單了,霧效果加上一個(gè)和雨滴相似的粒子特效掛在鏡頭前就可以啦。由于雪花生命周期比較長,飄落速度比較慢,粒子數(shù)最多在300左右就可以達(dá)到不錯的效果。實(shí)現(xiàn)的效果截圖如下(這里有一些序列幀動畫之類的小技巧可以優(yōu)化雪片的效果,不過不屬于程序的技術(shù)了,特效同學(xué)應(yīng)該都會的):

092403l9r9bqa9700rz4ba.jpg 雪天效果截圖
也同樣研究了一下《鎮(zhèn)魔曲》中雪花效果的實(shí)現(xiàn),發(fā)現(xiàn)比較討巧的是他們沒有讓一個(gè)雪花是一個(gè)粒子,而是用一張圖來表現(xiàn)幾片雪花的效果,然后大約只需要同時(shí)存在十幾個(gè)粒子就可以做到比較密集的下雪效果。當(dāng)然代價(jià)是仔細(xì)觀察的話會發(fā)現(xiàn)一些重復(fù)感,overdraw也會稍微有些提高,但是粒子數(shù)量降低得會比較多,值得借鑒。(我們美術(shù)同學(xué)嘗試了一個(gè)版本之后告訴我不太滿意,當(dāng)然在看了完全隨機(jī)的效果之后,對于略有重復(fù)的效果自然能感覺出來瑕疵,沒有對比才沒有傷害……)

7. 風(fēng)

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

這里只說幾個(gè)我們移植時(shí)的幾個(gè)修改:

1.使用Shader的全局變量。Shader.SetGlobalXXX一系列的接口就是為這種全局參數(shù)來設(shè)計(jì)的,簡單易用。

2.臨近測試我們美術(shù)比較忙,表示沒時(shí)間對每棵樹的模型去刷頂點(diǎn)色,于是搖擺的幅度控制采用了一個(gè)簡化的方案——由頂點(diǎn)高度和一個(gè)美術(shù)設(shè)定的模型高度的比值來決定,目前只采用的線性差值,效果一般,勉強(qiáng)夠用。

3.GPU Gem中的實(shí)現(xiàn)比較復(fù)雜,考慮了橫向的和縱向的抖動,有不少計(jì)算在里面,這塊可以根據(jù)自己項(xiàng)目的游戲類型和需求來修改和簡化。

8. 整合

把實(shí)現(xiàn)的各個(gè)天氣效果整合成天氣系統(tǒng),由一個(gè)管理器來控制,可以模擬游戲中各個(gè)國家的氣候風(fēng)格,這是最后整合進(jìn)游戲進(jìn)行實(shí)際應(yīng)用的步驟。由于我們大世界和戰(zhàn)斗場景是兩種完全不同的鏡頭方式,因此最終特效掛接的部分實(shí)現(xiàn)了兩套不同的控制邏輯。除此之外,根據(jù)不同國家的特性,也將雨天和雪天統(tǒng)一為了特殊天氣,比如在燕國這樣靠北的國家,就只會下雪,而其他國家則是下雨。這其中有很多繁雜的與游戲業(yè)務(wù)相關(guān)的邏輯就不談了,只聊幾個(gè)實(shí)現(xiàn)過程中比較有感觸的點(diǎn):

1.漸變需求。天氣效果中所有的控制效果都有不同的漸變細(xì)節(jié)需要處理,比如下雪天氣停止不能突然沒有,而是要有漸漸消失的感覺;天氣由晴天變陰天,也不應(yīng)該突然黑下來,而是要有一個(gè)亮度漸變的過程。這些需要各個(gè)天氣系統(tǒng)針對自己的效果做好差值的處理,這個(gè)過程使用了DoTween來做,代碼實(shí)現(xiàn)非常簡單高效。

2.對于需要跨天氣控制的效果進(jìn)行統(tǒng)一的管理。在最初的版本里,用于表現(xiàn)變暗效果的Tint Color由每一個(gè)天氣進(jìn)行各自的管理和差值,這里就有一些非常惡心的特殊代碼要做處理,比如陰天效果的停止函數(shù)中,當(dāng)進(jìn)入晴天的時(shí)候需要把亮度逐漸調(diào)整到1,如果從陰天進(jìn)入雨天,則不需要做這樣的調(diào)整。天氣效果的控制也就像是一個(gè)狀態(tài)機(jī),在單獨(dú)的狀態(tài)中如果需要考慮變換的前后邏輯,代碼里就需要非常多if-else這樣的邏輯判斷。在迭代的過程中,把這塊的控制抽象成為一個(gè)天氣亮度管理器——BrightnessManager,它負(fù)責(zé)控制亮度并按照設(shè)定的速度在當(dāng)前亮度和目標(biāo)亮度之間做差值,這樣對于任何天氣效果,只需要在開啟的時(shí)候設(shè)置默認(rèn)的亮度值給亮度管理器,其他細(xì)節(jié)都不需要關(guān)心。同樣還有用于風(fēng)的參數(shù)控制的WindManager。

3.效果與實(shí)現(xiàn)邏輯的分離。從表面上看,下雨和下雪是兩個(gè)不同的天氣效果,但是他們在程序的邏輯是有很大相似性的——都是控制特效的掛接和跟隨邏輯。因此從邏輯實(shí)現(xiàn)上這兩個(gè)天氣效果有相同的邏輯,只是數(shù)據(jù)(特效)不同而已。另外下雨的效果有額外的一些漣漪的處理。于是使用面向?qū)ο蟮乃悸烦槿∫粋€(gè)FXWeather的公共父類來做代碼的復(fù)用,方便維護(hù)。

經(jīng)過一些思考和迭代之后,最終C#代碼中的類圖如下所示。

092404jhf01fu818uzh68h.jpg 最終實(shí)現(xiàn)的天氣系統(tǒng)類圖
9. 總結(jié)

回顧整個(gè)天氣系統(tǒng)的實(shí)現(xiàn),其實(shí)沒有特別有難度的東西,只是一些效果的應(yīng)用和業(yè)務(wù)邏輯的編寫。使用面向?qū)ο蟮睦^承和組合,再加上狀態(tài)模式就完成了最后的需求。在效果方面,由于要兼顧移動平臺的性能限制,相比端游的動態(tài)天氣效果做了很多妥協(xié)和簡化,盡量用20%的性能消耗做到60%的表現(xiàn)力,對于真實(shí)感等方面做了不少的妥協(xié)。

當(dāng)然,現(xiàn)在實(shí)現(xiàn)的各種天氣效果還很簡陋,比如下雨還可以添加地面濕滑的材質(zhì)效果,還可以制作暴風(fēng)雨這樣更動感刺激的天氣效果,在沙漠中實(shí)現(xiàn)沙塵暴的感覺等等。這些東西,還需要更多的時(shí)間和精力來填滿缺失的細(xì)節(jié)。

無論如何,希望這篇文章可以給期望增強(qiáng)游戲效果的同學(xué)一些啟發(fā),也同樣期望有更好實(shí)現(xiàn)效果和方法的朋友不吝賜教,給予更多思路和經(jīng)驗(yàn)的分享。

Via : Nexus泛娛樂游戲社群

聲明:游資網(wǎng)登載此文出于傳遞信息之目的,絕不意味著游資網(wǎng)贊同其觀點(diǎn)或證實(shí)其描述。

銳亞教育

銳亞教育,游戲開發(fā)論壇|游戲制作人|游戲策劃|游戲開發(fā)|獨(dú)立游戲|游戲產(chǎn)業(yè)|游戲研發(fā)|游戲運(yùn)營| unity|unity3d|unity3d官網(wǎng)|unity3d 教程|金融帝國3|8k8k8k|mcafee8.5i|游戲蠻牛|蠻牛 unity|蠻牛