Unity手游開發(fā)札記——移動平臺的天氣系統(tǒng)實現(xiàn)

0. 牢騷

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

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

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

1. 綜述

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

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

2. 晴天效果

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

3. 多云效果

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


多云效果截圖
多云效果截圖

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

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


云陰影的繪制過場截圖
云陰影的繪制過場截圖

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

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

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

4. 陰天

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

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

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

5. 雨天

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

Shader "Shader/Scene/Rain" {
    Properties{
        _RainTex("Main Texture:", 2D) = "white" {}
        _RainIntensity("Intensity of Rain:",Float) = 0.0
        _FallSpeed("Fall Speed of Rain:",Float) = 1
        _ThunderLighting("Thunder Lighting", Color) = (0, 0, 0, 0.5)
    }

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

        Blend SrcAlpha One

        LOD 100
        Cull Off
        ZWRITE Off
        Lighting Off

        Pass{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"

            sampler2D _RainTex;
            float4 _RainTex_ST;
            fixed _FallSpeed;
            fixed _RainIntensity;
            float4 _ThunderLighting;

            struct appdata_t {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f {
                float4 vertex : SV_POSITION;
                float2 texcoord : TEXCOORD0;
            };

            v2f vert(appdata_t v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _RainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed2 UV = i.texcoord;
                float Time = _Time.y;
                fixed vValue = _FallSpeed * Time;

                UV = fixed2(UV.x, UV.y + _FallSpeed * Time);
                fixed4 col = tex2D(_RainTex, UV);
                col.rgb = col.rgb * col.a * _RainIntensity + _ThunderLighting.rgb;
                col.a = 1.0f;
                return col;
            }
            ENDCG
        }
    }
}

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


初版的下雨效果截圖
初版的下雨效果截圖

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

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);
            }
        }
    }
}

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

在這之前我沒怎么玩過粒子系統(tǒng),這里從頭學習制作一個粒子特效,還是挺有趣的。粒子系統(tǒng)可以用比較簡單的方法制作出非??犰诺男Ч?。最終實現(xiàn)效果的截圖如下:


雨天效果截圖
雨天效果截圖

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

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

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

6. 雪天

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

雪天效果截圖
雪天效果截圖

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

7. 風

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

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

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

8. 整合

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

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

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

最終實現(xiàn)的天氣系統(tǒng)類圖
最終實現(xiàn)的天氣系統(tǒng)類圖

9. 總結(jié)

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

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

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

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容