重慶分公司,新征程啟航
為企業提供網站建設、域名注冊、服務器等服務
為企業提供網站建設、域名注冊、服務器等服務
這篇文章將為大家詳細講解有關Unity在如何使用Fast Shadow Receiver優化渲染效率,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
成都創新互聯從2013年開始,先為平江等服務建站,平江等地企業,進行企業商務咨詢服務。為平江企業網站制作PC+手機+微官網三網同步一站式服務解決您的所有建站問題。
關于Unity中的動態陰影,已經有挺多帖子聊過這個話題了,
無論是最簡單的基于Planar投影的方案還是稍微“老式”一些的Projector的方案,乃至目前比較主流的ShadowMap的方案其實都各有優劣和對應的應用場景,它們之間的原理和差異不是本文的重點,有興趣的同學也可以很容易地找到相關的論文或者博客來看。
我們項目本著不要重復造輪子的想法,一直堅持使用Unity原生的ShadowMap的方案來做動態陰影。而且UWA也做過一些動態陰影方案效率的對比,自己的輪子能做得比有源碼的官方好的并不多,更何況我們這種地表有起伏,高配需要支持多角色動態陰影的“大型”MMORPG游戲,ShadowMap已經是最適合的方案了。
然而!人生總會有然而,否則就太平淡無味了不是?……
大約1個多月前,我發現了這個問題——《Unity中靜態合批與Shadowmap的宏設置沖突問題》,簡單來說,靜態合批首先對場景物體進行了排序,保證結果正確,但是當引入了動態陰影之后,會去修改物體接受陰影的宏(這也是一種優化,因為有采樣和陰影計算的消耗,所以關閉掉宏著色器的效率更高),導致原本排序好的物體無法正常進行合批,因為著色器的宏不一樣了,從而導致之前靜態合批之后理論上可以做到很低的Batch數值增加了很多,使得場景渲染的效率大幅下降。
這個問題在想清楚原因之后,在依然想要使用Unity的ShadowMap的前提下感覺是沒有什么特別簡單的優化方案的,于是就暫時擱置下來,直到上周的時候對游戲各個效果對于幀率的影響在真機上做了一個定量的測試之后,才發現問題遠比想象中的嚴重……
各個效果對于幀率影響的定量測試結果
上面的測試是在中配機型小米Max2上進行的,可以看出陰影的開關與否導致一幀的時間消耗有9.5ms左右的差異,是所有效果中影響最大的!而ShadowMap自身渲染消耗不應該有這么大的差異才對,觀察了下Batch數量的差異,單純場景的Batch數量大約會從25增加到150左右,這有點超出我們之前制定的美術規范了。
在中配效果下,我們只有主角自己開啟了動態陰影,因此最初的一個想法就是引入另外一套陰影繪制方案,比如Dynamic Shadow Projector,來專門針對主角進行陰影的繪制。雖然我個人很不喜歡同時使用兩套技術方案,但目前看起來這似乎是在不降低效果的前提下唯一的選擇了。
This simple Unity asset provides a few components to render a shadow onto a render texture so that the render texture can be used with Blob Shadow Projector. Blob Shadow Projector is usually used for dropping a round blurry shadow which is not suitable for a skinned mesh object. This asset enables a projector to drop a dynamic shadow which is perfect for skinned mesh objects.
Dynamic Shadow Projector插件的原理比較簡單,將角色的陰影繪制到一張rt上,然后使用Unity的Projector組件將這張rt作為繪制輸入,再繪制一遍接受陰影的物體。陰影的rt是每幀更新的,也就做到了可以讓帶有動畫的角色陰影是實時變化的。
試用了一下,還是比較簡單易上手的,幾個組件正確設置之后就可以看到效果了,由于是針對單個角色的,因此使用比較小的rt就可以做到比shadowmap更加精細的效果,但是如果想讓一個projector處理多個角色,一旦擴大projector的范圍,陰影效果質量的下降就比shadowmap的方法還要厲害。
128*128的rt只投影一個Cube的情況下rt的使用率和陰影質量
512*512的rt投影三個Cube的情況下rt的使用率和陰影質量
上面兩張圖分別給出了模擬使用一個Projector針對單個角色進行投影和多個角色進行投影的效果對比圖,在下面的那張圖中,三個Cube的距離相隔并不遠,但是即使使用了512*512的rt,明顯可以看到其陰影已經有了鋸齒感,距離更大的時候鋸齒更加嚴重。
那我為什么糾結于一定想要使用一個Projector來進行多個角色的動態陰影繪制呢?因為對于每一個Projector來說,繪制陰影的時候都需要把接受陰影的模型完整重回一遍,從下面抓幀的截圖可以看出,三個Cube分別使用三個不同的Projector,地表平面需要繪制三遍。這其實就是Projector的方法不太適合移動設備上多個物體都需要進行動態陰影繪制的原因。
多個Projector的時候接收陰影的地表繪制抓幀截圖
我們的地表使用了Terrain制作,轉為Mesh之后的三角形數量一般在大幾千的水平,多遍繪制對于整體面數的增加還是很可觀的,雖然在我們的中配下只有主角接受動態陰影,只需要多一遍地表模型的繪制,拿一次Draw Call和幾千面的消耗換取100+次Batch的減少,理論上已經夠劃算了,但是我還有些不太甘心,于是想嘗試下Dynamic Shadow Projector推薦配合“服用”的Fast Shadow Receiver插件。
Fast Shadow Receiver插件是很久前我就關注過的一個插件,錢康來在他的博客里也有提到。我一直保持一個敬而遠之的心態,一是因為從經驗上來說ShadowMap沒有接受陰影方需要重繪的問題,只是宏的改變,效率應該挺高的(沒想到影響了Static Batching);二是對于運行時對mesh進行暴力重建一直心存懷疑,擔心其對于CPU和內存的額外壓力。
購買了插件,將其引入我自己本地的項目工程,玩了玩Demo之后,嘗試將其和Dynamic Shadow Projector結合一起使用。和AssetStore上對于這個插件的評論一樣,這個插件的文檔的確有些晦澀,大約玩了三四個小時的時間才正式在游戲中跑通整個流程,過程不詳述了,幾個小坑記錄一下:
可能是官方被吐槽文檔太難讀,所以做了一套Wizard,一步步走教你怎么配置,然而我按照步驟做完之后并沒有得到正確的結果,反而因為Wizard隱藏了背后的部分設置步驟導致我無法正確理解過程,從而難以排查原因。而且Wizard是針對特定的需求,未必是我自己想要的效果。最終我還是按照Demo工程里的組件逐個對照配置實現的效果。
LayerMask設定需要注意,為了優化效率,Projector組件上有Igore Layers的設定,在Draw Target Object上,也有Layer Mask的設定用于標識要繪制的節點下哪些Layer會被繪制,最終的ShadowReceiver組件也會屬于某一個Layer,比如默認的Default。這幾個Layer如果設定有問題,會導致最終沒有影子被繪制出來。我因為這里的失誤多花了1個小時的時間調試各種參數,如果你在使用中遇到了奇怪的問題,可以把自己設置的各種Layer梳理一遍,保證邏輯上的正確性。我當時的問題之一是把ShadowReceiver所在的GameObject歸入到了Default Layer,而Projector又Igore掉了Default Layer,導致結果不正確。
Fast Shadow Receiver的插件制作者估計沒有經受過中國美術的洗禮,除了文檔晦澀之外,代碼中對于容錯的兼容考慮得也不周全……我們場景中有幾千個物體,在最初測試的時候沒有花心思標記所有的地表接受陰影的物體,索性將所有物體都進行標注,結果MeshTree的生成一直存在問題,查了下是因為我們場景中存在一個Mesh對象為miss狀態的GameObject導致的,做一下兼容就好了,當然根本上也要美術去修復掉mesh miss的問題。
總之,經過一系列的嘗試,最終在我們自己的工程內使用正式的美術資源跑通了整個流程,也對于Fast Shadow Receiver的原理有了更深的理解:它使用Mesh Tree這樣一個繼承自Scriptable的類在離線階段來預計算并存儲需要接受陰影的地表網格信息,并且提供BinaryMeshTree、OctMeshTree和TerrainMeshTree三種類型來應對不同的場景。運行時,它提供MeshShadowReceiver這樣的組件,根據Projector的設定實時計算出來接受陰影的地方需要覆蓋的那些面片,生成一個新的網格作為陰影接收者的網格對象進行渲染,從而做到可以將原本幾千面的模型只需要幾十個面就可以繪制出來,因為畢竟需要繪制動態陰影的只有鏡頭前的部分區域。
Fast Shadow Receiver的Demo中的示例截圖
在最初的設想中是針對單獨的主角使用Projector方式的動態陰影,然后用Fast Shadow Receiver進行優化,在Demo中看到Fast Shadow Receiver支持ShadowMap的方案時也沒有多想。后來在和同事討論這個問題的時候聊到Projector的動態陰影方案和ShadowMap的動態陰影方案的優劣,被問到兩種方案是不是有可能做一個結合,然后想起了在Demo中看到了使用Fast Shadow Receiver來優化ShadowMap的例子。正好也在糾結我們抽離式的戰斗中在中等配置下的效果,如果使用Projector,需要多幾張rt的繪制是否合算,那如果可以用Fast Shadow Receiver結合之前的Shadow Map方案,對于目前結構的改動是最小的,也不必引入第二套動態陰影的產生方案,只相當于用新的插件在中配下解決場景靜態合批的問題,這似乎是非常理想的一個方案。
沿著這個思路,學習了一下Fast Shadow Receiver中關于ShadowMap的例子,看上去也非常簡單。在理解了原理的情況下,只是讓場景內的其他Render組件的Receive Shadow屬性都更改為false,然后只讓Fast Shadow Receiver生成的那樣一個面片讀取生成的ShadowMap進行陰影的繪制即可,這樣額外增加1個Draw Call和幾十個面的渲染消耗,就可以做到和之前相似的效果,中高配置的切換邏輯也更加簡潔。
我們先來看一下最后經過修改敲定下來的制作步驟,然后再聊一些其中的設計細節。
統一將場景中的Mesh相關的組件放置到同一個GameObject下。這一條原本沒有一條硬性的規定,完全看場編同學自覺,其實整理之后Unity中的Hierarchy面板也會更加干凈整潔;
場景Mesh統一放入ArtRoot根節點下
標記接受陰影的物體。這一步是一個有點瑣碎的工作,需要美術標記出來哪些物體是接收陰影的,BinaryMeshTree是根據這些標記出來的物體來進行網格的預處理的。標記的物體過少會出現應當接受陰影的物體沒有陰影效果,而過多會導致BinaryMeshTree的數據內容過多,加載變慢、檢索速度降低,內存占用也會很多。由于我們目前只在中配下使用,所以對于這部分只要求地表和表現明顯的物體加入到標記中。Fast Shadow Receiver只支持Layer和RenderType的過濾方式,在我們場景中有些物體已經被標記過了其他有邏輯意義的Layer,因此我針對這點進行了改造,增加了Tag的過濾,和Mask Layer取或的方式來進行處理,并且為美術提供了方便的快捷鍵進行快速標注。我自己測試,我們游戲內的場景,標注加上驗證需要的耗時大約也就半個小時到2個小時不等。
提供FastReceiver的Tag進行標注
創建BinaryMeshTree。我們最終選擇使用BinaryMeshTree這種結構,它和OctMeshTree的區別見下圖。其實這個步驟還需要更多的測試來做對比,因為官方也明說small和large的界限具體是什么。
兩種不同的MeshTree對比
創建BinaryMeshTree的過程也很簡單,插件提供了右鍵Create菜單的支持:
創建BinaryMeshTree
生成Mesh Tree。在標注完接收陰影的物體之后,就可以選中創建好的BinaryMeshTree,填寫其Root Object為場景的根節點,設置好Layer進行build。我們建議美術檢查最后創建完畢之后給出的build信息中對于內存的占用要小于2M,這是一個編輯幾個場景之后的經驗值而已,還需要更多驗證。
Mesh Tree生成時Layer的配置
Build之后的Mesh Tree信息統計
配置Projector和Mesh Tree信息。這部分為了簡化美術的配置工作,大部分的配置邏輯都寫在了代碼中,只需要美術復制一份prefab出來,將新創建的Mesh Tree信息設置正確即可。需要注意這份prefab是不保留在場景內的,編輯完畢Apply后會從場景中刪除掉。
創建BinaryMeshTree
這里一共只使用了兩個組件,一個是圖中LightProjector對象上的LightProjector組件,用于設置陰影使用的方向光對象以及一些Projector的參數,比如跟隨的Target對象,擴展的Bound范圍等;另外一個是MeshShadowReceiver組件,關聯Mesh Tree數據,場景渲染物體的根節點和Projecter對象,一些Fast Shadow Receiver的裁剪、更新方式等屬性也可以在這里進行設置。
在資源根節點上添加Shadow Receiver Controller組件,并進行配置。這一組件是我們自己實現的,用于控制Fast Shadow Receiver的開關,它會根據游戲配置在場景加載、游戲配置切換等邏輯中對Fast Shadow Receiver進行設置。并且基于這一組件實現對于Mesh Tree的懶加載功能。
Shadow Receiver Controller組件配置
在游戲運行狀態下進行測試。上述配置完畢之后,就可以在游戲邏輯的中等配置下看到優化后的陰影效果了,可以跑跑游戲進行測試。
大部分細節已經在上述步驟中描述了,這里再說明以下幾個地方:
a) Projector和MeshShadowReceiver組件是不默認放在場景里的。這是由于當地表物體較多的時候,Mesh Tree的加載是有時間消耗的(遇到過一個測試例子,Mesh Tree的大小有18M左右,在PC上需要5s以上的情況,具體原因沒有細查),也會有額外的內存消耗,因此這里一方面建議美術確保這個文件不會特別大,另一方面通過Lazy Load的方式,在需要的時候才加載,來保證在高配和低配的情況下,不需要任何額外的CPU和內存開銷。
b) 為美術提供更多便利的工具來標記信息。由于標記地表是一個相對瑣碎的工作,驗證標記是否合理也是一個件需要花費很多時間和精力的事情,除了前面提到的快捷鍵可以一鍵標注,還推薦通過Layer的顯隱功能,以及我們自己開發的Tag顯隱功能進行快速檢查和問題定位。
Unity原生的Layer過濾功能
使用同樣的測試方式,對比優化前后的游戲運行幀率和時間消耗:
優化前后的性能消耗對比
可以看到,使用Fast Shadow Receiver在小米 Max2上有大約7.2ms的性能提升,幀率從26上升到33,這其中有Batch數量降低的功勞,應該也有場景物體不需要采樣ShadowMap貼圖帶來的渲染性能提升,更加具體的數據就沒有去測試了。剩余的1.5ms的時間消耗包括了ShadowMap的繪制以及Fast Shadow Receiver的更新消耗,這是后續的優化對象,但這次優化已經有很大的提升了,中配下整體效率提升了20%,已經是難得的“神級優化”了。當然,這建立在場景通過關閉Shadow接收的宏能夠降低較大Batch數量的前提下。
這次優化的收益是很大的,但它也不全是一種無損優化,需要付出的代價有這么幾點:
美術工作量。需要美術同學針對場景進行地表接收陰影物體的標注,雖然提供了快捷的工具,但是依然需要花費一些時間成本。
部分物體不再會受到動態陰影的影響。在之前基于ShadowMap的方案中,幾乎所有的物體都可以標記為接收陰影,而且可以保證效果的正確性,但是目前這種方案如果要做到這點會導致Mesh Tree對于內存的占用較多,對于外部的大世界場景也不適應,因此會有出現一些小石頭等物體不會接收角色陰影的問題,這是一些效果的降低,但目前看是可以接受的范圍內。
和靜態陰影的融合與ShadowMap的方案不同。ShadowMap的方案是在場景繪制的時候進行處理的,一次像素著色的過程中會采樣lightmap和shadowmap兩張貼圖,這就可以判斷出該像素點是否在靜態陰影之中,這樣可以做到比如在屋檐下或者樹蔭下這樣的靜態陰影中,角色的實時陰影可以和靜態陰影做一個較好的融合,如下圖所示。
基于ShadowMap的方案動態陰影和靜態陰影的融合效果
而使用Fast Shadow Receiver方案之后,就比較難做融合的效果,除非在新生成的mesh中保存之前mesh的uv2信息以及使用的lightmap貼圖信息,再做一次lightmap的采樣。但這比較麻煩,性價比也不高,于是在靜態隱形中的角色動態陰影的效果就變成了如下圖所示的樣子。
使用Fast Shadow Receiver方案的效果
除了這些之外的代價就是程序這邊花費了大約半個多星期的時間來學習和集成這套方案,但是從優化結果上看,還是收獲很大,非常值得的~
由于我們是類似回合制的抽離式戰斗方式,即玩家進入戰斗后整場戰斗都會發生在一小塊固定區域內,這里其實對于ShadowMap結合Fast Shadow Receiver的方案是一個非常合適的應用場景——只需要在進入戰斗前生成一次陰影接收的面片,整場戰斗中都不需要對其進行修改和變動!
我們將LightProjector的Target鎖定為戰斗的中心區域點,然后通過修改Bound的方式擴大其投射范圍到整個戰場。前面已經討論過基于Projector的動態陰影方案的一個問題是當projector較大的時候rt的使用率較低,導致陰影質量驟降的問題,但因為我們使用的是ShadowMap的陰影方案,因此擴大Projector的范圍并不會影響陰影精度,也不需要處理多個Projector帶來rt數量、draw call增加等問題。
戰場中多個角色公用一個LightProjector的方案
Fast Shadow Receiver這種通過CPU的實時計算來換取GPU的渲染性能的方案,正好解決了我們場景靜態合批被動態陰影打斷的問題,大大提升了我們游戲在中配下的幀率,是近期所做的優化中效果最為顯著的一個了,因此也記錄一下詳細的過程在這里分享出來。
對于這個插件的感覺,在這一周的逐漸熟悉、應用、修改的過程中,也從心存懷疑到由衷贊嘆。目前針對這個插件的魔改還不多,除了前面提到的增加Tag的支持、建立Mesh Tree的時候缺少一些對于資源的錯誤兼容之外,只修改了部分Component的默認參數,更加適合我們項目的設定,讓美術和程序可以更加方便地使用。它在運行時對于內存的分配和CPU的性能消耗也讓我們滿意,因此在這里也幫這個插件做一下廣告——別被它的文檔和使用過程嚇到,用好之后,你的游戲效率可以獲得很大的提升~
至于未來,當中配下的效果和效率都被驗證可以接受之后,可能考慮優化一些它的效果,將它也應用到高配下,當然,對于貼花等需要處理高低不平地面效果的地方,也可以考慮使用這個插件進行效率的優化。
PS:從Fast Shadow Receiver的啟發來思考場景靜態合批被打斷的問題,其實另外一個思路是自己來做哪些物體需要被接受陰影的判斷。Unity內部肯定也是有這樣的判定邏輯來設置各個場景Render的宏,由于Shadow的距離設定較大,Unity的判定范圍也過廣,導致了雖然我們在中配下只有角色渲染陰影,但是接收陰影的物體數量過多,從而導致Batch被頻繁打斷的問題。仿照Fast Shadow Receiver,使用一個跟隨角色的投影,和場景物體相交來判斷有哪些物體需要被設置為接收陰影,由于角色腳下的物體可能只會有幾個,因此Batch的數量也只會增加幾個。目前沒有沿著這個思路來做的原因之一也是地表物體的面數實在是有點多,Fast Shadow Receiver對于面數的降低也是我們想要的優化之一。
關于Unity在如何使用Fast Shadow Receiver優化渲染效率就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。