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