在2018TGDC大會(huì)上,來自Epic Games中國資深技術(shù)工程師王禰先生發(fā)表了《UE4制作多地型游戲的優(yōu)化》主題演講。王禰有近10年的虛幻引擎使用經(jīng)驗(yàn),從console游戲、掌機(jī)到PC端MMO游戲,再到手游,都有過相關(guān)開發(fā)經(jīng)驗(yàn);現(xiàn)在Epic Games China,他作為唯一的引擎技術(shù)專家,參與和幫助了眾多使用UE3和UE4的項(xiàng)目解決各種問題。以下是演講實(shí)錄:

093614ki4fndof5fbrhmwo.jpg
大家好!我是王禰,來自Epic Games,現(xiàn)在在中國區(qū)負(fù)責(zé)引擎技術(shù)支持以及一些針對(duì)中國區(qū)的技術(shù)功能的開發(fā)。我跟解衛(wèi)博以前是多年的同事,他剛才介紹了很多使用虛幻在渲染上的案例,我介紹的更貼近現(xiàn)在主流使用,尤其是國內(nèi)手游比較重度的情況下,我會(huì)介紹一下使用UE4制作大地型游戲的挑戰(zhàn)和優(yōu)化的手段。

這是今天會(huì)講到的總體內(nèi)容,內(nèi)容比較多一些,有些地方會(huì)過得比較快。首先我們來看一下在移動(dòng)設(shè)備上做大地型多人游戲的挑戰(zhàn)。大地型肯定是開放的地圖,視野比較寬,視距比較遠(yuǎn),地圖比較大,在很大的世界里還會(huì)有比較多的風(fēng)格變換,會(huì)導(dǎo)致繪制內(nèi)容的種類比較多,資源的使用、變化比簡單一些的游戲復(fù)雜非常多。

對(duì)于同樣的移動(dòng)硬件來看,優(yōu)化的壓力會(huì)大非常多。我們來看看優(yōu)化分為哪幾部分,主要的優(yōu)化包括有大量的角色需要跟場(chǎng)景發(fā)生交互,角色的動(dòng)畫之類的計(jì)算以及與場(chǎng)景交互的計(jì)算發(fā)生在游戲線程,因此游戲線程承擔(dān)了非常重的優(yōu)化任務(wù)。所以首先我們講游戲線程的優(yōu)化。

引擎里面有一個(gè)東西,我知道這個(gè)是比較偏向于游戲邏輯業(yè)務(wù)的概念,可能一般大家不太認(rèn)為會(huì)在引擎里面實(shí)現(xiàn),我們叫做重要度管理系統(tǒng),大家知道游戲的常規(guī)優(yōu)化手段叫做LOD,不管是面數(shù)、更新頻率,我們都會(huì)根據(jù)在屏幕上所占比進(jìn)行調(diào)整,這是很通用的,延用很久的優(yōu)化手段。

我們?cè)趺礃幼尭鱾€(gè)游戲模塊從游戲邏輯層去修正LOD的計(jì)算?這時(shí)我們引入Significance Manager,我們會(huì)分配針對(duì)每個(gè)平臺(tái)的Bucket,大家可以看到右下示意圖中藍(lán)色的小點(diǎn)代表玩家控制的角色,邊上的小點(diǎn)是別的玩家和交互的動(dòng)態(tài)對(duì)象。我們根據(jù)離主角玩家的距離,在屏幕上的尺寸或者可見性,決定使用什么bucket。例如基于可見性的計(jì)算,雖然離我很近,但是因?yàn)樵谖业谋澈螅赡芎芏鄷r(shí)候我都感受不到,Bucket就可以分得不一樣,通過Bucket我們會(huì)用來控制、修正LOD的各種計(jì)算。這里是一個(gè)例子,我們這個(gè)系統(tǒng)本身用于我們自己比較火熱的游戲《堡壘之夜》,手機(jī)、掌機(jī)、電腦都可以跑,我們兼容所有的平臺(tái)可以聯(lián)機(jī)玩,游戲在不同平臺(tái)上的場(chǎng)景、復(fù)雜程度其實(shí)是一樣的。這種情況下,硬件的計(jì)算能力有非常大的差別,所以我們針對(duì)移動(dòng)平臺(tái)和主機(jī)Bucket也不一樣,除了自身控制的角色給的Bucket比較高,剩下的角色的比較低,主機(jī)有四個(gè),手機(jī)有一個(gè),這個(gè)設(shè)置不僅按平臺(tái)來,也可以按設(shè)備來,移動(dòng)設(shè)備好的和差的硬件計(jì)算能力差很多,我們可以在Device profile指定當(dāng)前這臺(tái)設(shè)備Bucket的規(guī)劃。

剛才是比較全局的系統(tǒng),接下來我們看游戲線程里開銷最大的部分就是我們的動(dòng)畫,動(dòng)畫系統(tǒng)大部分角色是可以定制的,角色會(huì)分為幾個(gè)部分,繪制調(diào)用的數(shù)量、動(dòng)畫骨骼更新、不同部件的不同動(dòng)畫計(jì)算量非常大,針對(duì)Fortnite這樣的游戲有一些特殊的游戲模式,例如50V50,這種情況下,最終在縮圈以后,同屏?xí)霈F(xiàn)超過50甚至80個(gè)角色,每個(gè)角色還分了好幾個(gè)部件,背包、武器都有不同的動(dòng)畫,這個(gè)時(shí)候計(jì)算量非常大,我們需要對(duì)動(dòng)畫做非常大量的優(yōu)化。

剛剛我們已經(jīng)說到角色可能分為幾個(gè)部分,有一些不同的策略,引擎提供各種方式,一種是將不同的部位的Mesh合成為一個(gè),這個(gè)模型有一個(gè)問題,材質(zhì)是要合并起來的,你的表情的動(dòng)畫就沒有了,在這個(gè)方案上我們做了一些取舍,最終決定不在Fortnite用這種方式。另一種,身上不需要?jiǎng)赢嫷膭傮w掛件可以方便的掛在角色骨骼的Socket上面,這是比較簡單的方式。還有Master Slave的方式,主體動(dòng)畫是一套完整的骨骼,身上掛載的動(dòng)畫是這個(gè)骨骼的子支,這個(gè)時(shí)候我們可以把這些掛載的部件的動(dòng)畫完全跳過自己的動(dòng)畫更新計(jì)算,完全用Master驅(qū)動(dòng),這樣的骨骼動(dòng)畫直接使用Master的骨骼矩陣,沒有辦法擴(kuò)展,比如Master Skeleton沒有尾巴或是披風(fēng)的骨骼,尾巴或是披風(fēng)的獨(dú)立動(dòng)畫或者物理模擬就沒辦法做。針對(duì)這種情況,我們還有一個(gè)解決方案是Copy Pose,可以把主體的計(jì)算完的骨骼矩陣拷貝給附屬的骨骼矩陣,只要保持目標(biāo)骨骼和原骨骼的層級(jí)結(jié)構(gòu)一致,就可以在目標(biāo)骨骼上增加擴(kuò)展性的骨骼,可以根據(jù)自己的狀態(tài)播自己的動(dòng)畫,也可以模擬物理。這是四種多部件角色setup的方案,無論使用哪一種,都需要對(duì)骨骼模型和骨架設(shè)置LOD,這是下面提到多種優(yōu)化的前提。

第一步比較直觀的是在動(dòng)畫更新的時(shí)候會(huì)有大量的邏輯事件的計(jì)算,我們稱之為Event Graph,這是UE4提供的圖形化的腳本功能,Event Graph是需要經(jīng)過圖形化的腳本虛擬機(jī),這個(gè)調(diào)用在動(dòng)畫邏輯比較復(fù)雜的時(shí)候開銷有點(diǎn)高,我們把在虛擬機(jī)上計(jì)算的Event Grape轉(zhuǎn)到C++,省掉了大量開銷。

再有一個(gè)是Anim Graph,我們根據(jù)當(dāng)前的狀態(tài)選擇不同的骨骼層級(jí),播放哪個(gè)動(dòng)畫,或是經(jīng)過哪些骨骼控制節(jié)點(diǎn),比如說IK、物理模擬最終的POSE的計(jì)算。在這個(gè)計(jì)算中間有一些步驟會(huì)用到數(shù)學(xué)計(jì)算,因?yàn)槭窃贕raph,會(huì)有一些額外的開銷。我們做了一些優(yōu)化,我們把所有這些獨(dú)立計(jì)算的模塊通通納入到一些基礎(chǔ)的骨骼動(dòng)畫混合節(jié)點(diǎn),包括偏移和縮放,這樣可以減少虛擬機(jī)的調(diào)用開銷,我們把這些包含簡單計(jì)算項(xiàng)的動(dòng)畫混合節(jié)點(diǎn)叫做Fast Path節(jié)點(diǎn)(右上角有閃電小圖標(biāo)),骨骼混合的計(jì)算邏輯通通是用Fast Path可以完全消除在虛擬機(jī)上的開銷。

同屏有那么多的角色要做骨骼動(dòng)畫計(jì)算,大家知道移動(dòng)設(shè)備是多核設(shè)備,為了更好的利用多核的定性,我們需要把剛剛這種虛擬機(jī)上的調(diào)用更好的平攤到不同的線程?;谏厦鎯蓚€(gè)優(yōu)化方向,我們不要使用Event Graph,把游戲邏輯更新的部分放在AnimInstanceProxy上,這樣引擎會(huì)自動(dòng)判斷這個(gè)Event Graph是不是可以放在別的線程上更新。如果你用了Fast Path,我們就可以把骨骼的update和evaluation都放到working thread上面去,例如有50個(gè)角色,在任一角色更新開始,就把計(jì)算分到別的線程上面,主線程繼續(xù)往下走。

即使我們能利用多線程,計(jì)算量還是非常大的,我們要減少動(dòng)畫更新的數(shù)據(jù)量,已經(jīng)有些設(shè)置可以幫助動(dòng)畫在不渲染的時(shí)候跳過 Tick pose,也可以通過Singnificance Manager跳過附屬武器、背包的更新,除了自己的主角,別的角色離你遠(yuǎn)一些,信息不更新其實(shí)你是注意不到的。

我們的掉落物會(huì)模擬物理,是骨骼物體。骨骼計(jì)算有一個(gè)問題,是走的Dynamic Path。我們引擎的中的靜態(tài)對(duì)象,會(huì)在加到場(chǎng)景中的時(shí)候就直接排序分組到自己的Drawing Policy,繪制的時(shí)候可以很大程度減少渲染狀態(tài)的切換。而動(dòng)態(tài)的單位,是每一幀在渲染開始的InitViews階段動(dòng)態(tài)獲取到數(shù)據(jù),它和靜態(tài)獲取數(shù)據(jù)的方式不一樣,不會(huì)進(jìn)入到靜態(tài)排序的表里,繪制的效率比較低。針對(duì)這種實(shí)際每一幀渲染數(shù)據(jù)不發(fā)生變化的骨骼物體,我們把這些物體額外加到了一個(gè)StaticRenderPath,加速了這些物體的渲染。

URO(Update Rate Optimization),我們其實(shí)沒有必要對(duì)所有的角色在每一幀都做骨骼計(jì)算。比如畫面中一個(gè)角色的POSE上半身動(dòng)作是怎么樣,下半身動(dòng)作是怎么樣,是否需要融合,什么頻率融合,中間是不是要插值,這些設(shè)置可以非常大程度決定骨骼更新的計(jì)算量。大家可以看到下面的圖,左一是每一幀都更新,左邊二是每四幀更新一次,中間用插值,第三張圖是每十幀更新一次,中間用插值,最后一張圖是每四幀更新一次,不用插值。大家可以看到當(dāng)角色占屏面積比較小,離得比較遠(yuǎn)的時(shí)候其實(shí)是沒有大差別的。

剛才講的這些是針對(duì)骨骼動(dòng)畫更新的優(yōu)化,其實(shí)伴隨著骨骼LOD的設(shè)置,我們?cè)贏nimGraph中可以設(shè)置骨骼控制節(jié)點(diǎn)從某一級(jí)LOD下不計(jì)算,比如說IK、物理模擬。

說完動(dòng)畫的優(yōu)化,接下來游戲線程還有大量的Scene Component,Scene Component是指世界中有坐標(biāo)位置的對(duì)象,它的Transform更新都是在游戲線程中計(jì)算。當(dāng)你大地圖、大場(chǎng)景動(dòng)態(tài)更新對(duì)象非常多,同時(shí)每個(gè)對(duì)象身上會(huì)掛很多Scene Component的時(shí)候,計(jì)算量是非常大的。盡管我們會(huì)把Scene Component的計(jì)算踢到異步線程,但是計(jì)算量依然很大。我們做了一些改進(jìn),針對(duì)一些掛載在人物身上,不是處于激活狀態(tài)的Scene Component做了自動(dòng)的管理。

打開Auto Manage Attachment,對(duì)于音頻和粒子特效,可以自動(dòng)根據(jù)它是否激活的狀態(tài),決定是否掛在父級(jí)Scene Component。如果Detach掉,它的Transform就不會(huì)再更新。

當(dāng)Scene Component發(fā)生位置變化的時(shí)候會(huì)觸發(fā)Overlap的檢查,每一幀有大量運(yùn)動(dòng)對(duì)象時(shí)會(huì)產(chǎn)生大量Overlap事件,耗費(fèi)比較大的開銷。優(yōu)化的原則是盡可能把不需要產(chǎn)生Overlap的事件關(guān)掉,注意引擎默認(rèn)是打開的。我們對(duì)層級(jí)結(jié)構(gòu)比較復(fù)雜的做了子Component是否打開overlap事件的引用計(jì)數(shù),會(huì)看自己是不是打開了Overlap事件,以及自己的子對(duì)象有沒有打開。這個(gè)時(shí)候我們?cè)谧鯫verlap檢查的時(shí)候可以很快的跳過,這個(gè)節(jié)點(diǎn)往下都沒有,就不需要再檢查自己的子節(jié)點(diǎn),這在場(chǎng)景的對(duì)象結(jié)構(gòu)比較復(fù)雜的情況下是可觀的優(yōu)化。

Character Movement,因?yàn)榻巧容^多,角色的移動(dòng)更新是非常大的游戲線程的計(jì)算,針對(duì)這個(gè)計(jì)算,一部分是角色在移動(dòng)的時(shí)候要檢查新的位置是不是能站立,要做一些掃描,要做一些碰撞,還要找落腳點(diǎn)是不是斜坡,這個(gè)斜坡的斜率是不是角色可以站上的,往前走的高度變化是不是可以超過跨過階梯最大的高度,角色一多計(jì)算量就非常大。所以除了玩家自己控制的角色,需要比較精確的計(jì)算外,其余角色分到的Significance Manager的Bucket我們最終是用了插值,通過網(wǎng)絡(luò)同步過來的位置做簡單的插值來模擬預(yù)測(cè)計(jì)算,在大部分時(shí)候都不容易注意到明顯的差異,只有在幀數(shù)較低或者網(wǎng)絡(luò)帶寬受限比較嚴(yán)重的時(shí)候,對(duì)于落地點(diǎn)會(huì)有顯著的偏差,大家可以對(duì)比看到這兩個(gè)視頻中左邊是預(yù)測(cè)計(jì)算,右邊是插值。

Physics,我們會(huì)盡可能的用一些替代的Physics優(yōu)化物理注冊(cè)的對(duì)象,有一組對(duì)象,比如說邊界,不需要很細(xì)致的碰撞模型,我們可以用簡單的volume來表達(dá)物理碰撞對(duì)象,減少注冊(cè)到物理場(chǎng)景中的對(duì)象數(shù)量。物理的一個(gè)場(chǎng)景會(huì)有兩個(gè)樹,一個(gè)用以做Query,一個(gè)用于做Simulation,我們要盡可能保證注冊(cè)進(jìn)去的對(duì)象最優(yōu)化。因此需要盡可能的簡化每個(gè)物理對(duì)象的復(fù)雜度,以及減少整個(gè)場(chǎng)景注冊(cè)的物理對(duì)象數(shù)。可以同時(shí)以比較小的內(nèi)存開銷打開異步的物理場(chǎng)景,Physics注冊(cè)的對(duì)象是一樣的,只不過他會(huì)用Shared Shape的方式加到Async Scene里,這樣在場(chǎng)景做物理模擬的同時(shí),他可以在異步的scene里做其他的query。

另外我還嘗試過把同樣mesh的不同實(shí)例對(duì)象用Shared shapes減少注冊(cè)的物理對(duì)象的內(nèi)存開銷,在內(nèi)存敏感的場(chǎng)景下也可以嘗試。還有一個(gè)思路是我們可以把物理對(duì)象和視覺對(duì)象解耦,默認(rèn)的情況下,引擎的Mesh對(duì)象打開碰撞就會(huì)注冊(cè)物理對(duì)象到PhysX Scene,增加了物理場(chǎng)景的復(fù)雜度和物理的內(nèi)存占用。因此當(dāng)你的Mesh加載到內(nèi)存里,即使不被渲染出來,這些開銷就在了,但是其實(shí)很多情況下視覺會(huì)看得更遠(yuǎn)一些,實(shí)際需要物理計(jì)算交互的距離在有些游戲中沒那么遠(yuǎn),我們可以用一些手段把視覺上對(duì)象的物理關(guān)掉,把這個(gè)物理屬性轉(zhuǎn)到一些新的Component和Actor上面放到新的Streaming Level里,用更近的加載卸載距離來管理,這樣實(shí)際的物理場(chǎng)景復(fù)雜度和內(nèi)存占用都會(huì)小很多。另外移動(dòng)端的布料,計(jì)算量和網(wǎng)格數(shù)量相關(guān),在移動(dòng)端會(huì)不太推薦使用那么復(fù)雜的模擬,引擎也就沒有提供移動(dòng)端的NvCloth的lib,所以我們一般會(huì)用剛體來模擬。

Ticking,也即所有動(dòng)態(tài)邏輯更新的對(duì)象,引擎的圖形化腳本可以讓美術(shù)策劃和GamePlay程序很方便的在Event Graph做Tick更新,但是需要付出一定虛擬機(jī)的調(diào)用開銷,當(dāng)Tick事件觸發(fā)的執(zhí)行隊(duì)列非常長,每一幀付出虛擬機(jī)的成本就會(huì)比較高一些。一個(gè)方法是轉(zhuǎn)到C++,另外一個(gè)方法是減低Tick的頻率,更有一些特殊的,例如每一幀只是視覺上在轉(zhuǎn)動(dòng)的風(fēng)車或是旗幟在飄、樹在擺動(dòng),其實(shí)可以不需要用骨骼動(dòng)畫、或者在Tick做旋轉(zhuǎn),可以用頂點(diǎn)動(dòng)畫來做。

引擎還有個(gè)功能較TextureStreaming,這個(gè)系統(tǒng)會(huì)在游戲線程計(jì)算用到貼圖的精度,用以決定更新給渲染線程的資源的精度再提交給GPU,對(duì)于這個(gè)每幀分析畫面貼圖Wanted Mip的計(jì)算量每幀還是占比較多的,游戲線程吃緊的情況下可以降低Texture Streaming的分析計(jì)算的頻率。

UI,如果游戲HUD有大量的UI對(duì)象,它的位置計(jì)算會(huì)比較復(fù)雜,在游戲線程的計(jì)算量就會(huì)比較大,可以多利用我們新出的SlatLayoutCaching和Invalidation Box來Cache Prepass減少widget transform更新的計(jì)算,這些Cache可以把計(jì)算的位置和大小記錄下來,有一些可以把頂點(diǎn)Buffer Cache下來。另外,我們也需要盡量讓UI的Widget可以Batching起來。引擎的一些布局空間會(huì)自動(dòng)幫你布局子控件,例如Horizental和Vertical Box,Grid等,這時(shí)候子控件是在同一層上,引擎會(huì)優(yōu)先Batch起來。當(dāng)使用比較靈活的Canvas Panel時(shí),會(huì)導(dǎo)致引擎默認(rèn)的行為會(huì)把每個(gè)加入的子空間的Implicit Zorder自動(dòng)增一,這時(shí)候如果你確定這些子Widget不重疊,其實(shí)可以手動(dòng)控制這個(gè)ZOrder。當(dāng)然Batch的前提還是你用了同樣的材質(zhì)和貼圖。那么如果做一個(gè)背包界面,里有很多不同東西的圖標(biāo),我們又希望這些圖標(biāo)有一些特效,我們可以用同一個(gè)材質(zhì),這只同一個(gè)Texture Altas,針對(duì)每個(gè)子控件設(shè)置不同的Vertex Color,在Vertex Shader里通過VC的值做為uv來使得這些子控件可以被Batching起來。

音頻和特效,音頻是比較大的開銷,我們之前的堡壘之夜又是從主機(jī)到移動(dòng)端兼容的項(xiàng)目,為了優(yōu)化音頻在移動(dòng)端的開銷,我們?cè)黾恿俗隽撕芏嘣O(shè)置,使得在移動(dòng)端不同的設(shè)備可以設(shè)置不同的SoundCue并發(fā)的數(shù)量,以及SoundSource的數(shù)量。其中SoundSource默認(rèn)在移動(dòng)端上總數(shù)是16個(gè),主機(jī)上可能是32個(gè)。簡單說明一下什么是SoundCue,這就是原始的SoundWave資源拿過來做一些實(shí)時(shí)處理封裝后的音頻資源,例如可以在多個(gè)SoundWave中做一些隨機(jī)、拼接,以及一些聲音效果的實(shí)時(shí)處理,這些處理效果對(duì)計(jì)算量要求比較大,我們可以針對(duì)不同的硬件設(shè)備做一些LOD的設(shè)置,比如說在比較差的CPU移動(dòng)設(shè)備上,可以把Reverb,EQ等關(guān)掉,或者減少隨機(jī)的Wave的數(shù)量等。

Particle比較顯著的開銷是Overdraw,我們?cè)赑C上有自動(dòng)把貼圖的Alpha切割出八面體,減少Overdraw的功能,但是這個(gè)功能之前在移動(dòng)端無法使用,最近我發(fā)現(xiàn)其實(shí)只要支持SRV的設(shè)備,是完全可以用這個(gè)功能的,移動(dòng)端上也可以打開。另外,所有的半透也可以以獨(dú)立的RenderPass以低分辨率繪制在upscale回來以減少overdraw帶來的大量的fragment的開銷。

Level Streaming,為什么用Level Streaming?其實(shí)道理很簡單,因?yàn)閳?chǎng)景非常大時(shí),我們不可能把所有的場(chǎng)景加載到內(nèi)存里面,這時(shí)候我們可以把地圖拆得非常碎,每次只加載視距內(nèi)的一小部分,使得內(nèi)存的占用變得比較低。這樣一來場(chǎng)景在內(nèi)存里的東西比較小,場(chǎng)景遍歷的開銷也會(huì)比較小。同時(shí)也可以在設(shè)計(jì)上增加場(chǎng)景可使用的物件的種類,豐富了場(chǎng)景的復(fù)雜度。整個(gè)Level Streaming總共分為三個(gè)步驟:

IO,這一步我們是放在Worker thread做的。第二個(gè)步驟是反序列化,在啟用Event Driven Loader后,IO和Deserialization可以并行,其中反序列化也可以由打開s.AsyncLoadingThreadEnabled放到異步的ALT去做。最后一步是Postload,這個(gè)有很多時(shí)候需要對(duì)游戲線程注冊(cè)對(duì)象,需要在主線程做,在引擎里可以用Time Slice的方式分幀異步來做,同時(shí),對(duì)于PostLoad中某些不影響游戲線程的行為,我們也挪到了ALT里,很大提升了Level Streaming的效益。

服務(wù)器,其實(shí)剛才針對(duì)客戶端的優(yōu)化,都會(huì)惠及服務(wù)器的優(yōu)化。在新版本中,我們加入了Replication Graph,在集中的類里做了ServerReplicateActors的計(jì)算,總體思路就是減少PerConnection,PerActor的relevancy以及priority的計(jì)算量,通過把Net Actor注冊(cè)到以空間位置劃分的grid中,每次針對(duì)當(dāng)前Connection只檢查所在Grid內(nèi)對(duì)象的信息來大大降低整個(gè)Replication的計(jì)算量。另外,對(duì)于不同Connection見的部分對(duì)象,我們也會(huì)Cache下來需要replicate的數(shù)據(jù)結(jié)果針對(duì)別的connection復(fù)用。這個(gè)改動(dòng)優(yōu)化使得在我們的項(xiàng)目中我們服務(wù)器的整個(gè)CPU用以做replication的開銷降到原先的1/4。

另外一些服務(wù)器優(yōu)化手段有,這是降低所有對(duì)象Net relevancy distance的距離;把以移動(dòng)的RPC包做優(yōu)化,如果連續(xù)的幾個(gè)移動(dòng)方向和速度是一致的,可以把幾個(gè)移動(dòng)RPC包合并起來只發(fā)一個(gè),減少網(wǎng)絡(luò)帶寬的占用和包的序列化等計(jì)算量。

服務(wù)器我們也可以關(guān)掉大量動(dòng)畫的計(jì)算,只在播一些特殊動(dòng)畫的蒙太奇的時(shí)候才會(huì)打開動(dòng)畫的更新。在Server上也可以把一些只關(guān)注渲染視覺和實(shí)際游戲邏輯計(jì)算沒有關(guān)系的Component在Server上去掉。

好了,看完大量游戲線程的優(yōu)化手段,接下來我們來看看渲染線程,渲染線程的第一個(gè)開銷取決于場(chǎng)景的復(fù)雜度,即使實(shí)際繪制出來的內(nèi)容很少,但是場(chǎng)景遍歷的開銷卻是正比于場(chǎng)景在內(nèi)存里的Primitive數(shù)量的。如果我這個(gè)遍歷時(shí)間很長,那么實(shí)際繪制調(diào)用發(fā)出的時(shí)間就會(huì)比較晚。這個(gè)時(shí)候,我們就要利用好Streaming Level來最小化Scene Tranversal的開銷。另外,動(dòng)態(tài)的對(duì)象每一幀重新獲取要繪制的渲染數(shù)據(jù),也會(huì)有不小的開銷,同時(shí)也會(huì)降低靜態(tài)對(duì)象的渲染狀態(tài)排序的優(yōu)勢(shì)。這也是上面提到過的加入了特殊的Static Render Path的優(yōu)化手段的原因。

場(chǎng)景遍歷后的大頭是是Culling,包括預(yù)計(jì)算的Precomputed visibility Volume,場(chǎng)景針對(duì)每個(gè)場(chǎng)景的可見性,不是特別大的地圖比較適用,在runtime幾乎沒有開銷,tradeoff是離線計(jì)算的時(shí)間和一部分內(nèi)存。然后是并行的視錐體裁剪和基于距離的裁剪,都是很常規(guī)的Culling手段。移動(dòng)端的occlusion是比較頭痛的問題,我們?cè)谥С諩S3.1的設(shè)備上,使用了Hardware occlusion query,在3.1以下的設(shè)備我們提供了一個(gè)Software occlusion的解決方案。當(dāng)然要注意這并不是萬能的,有些情況下還多了繪制的三角形面數(shù)及大量bounds transform的CPU開銷,卻沒有實(shí)際occlude掉什么對(duì)象。

剔除完就到了最終頭的開銷來源:Draw Calls,減少DC的手段多種多樣,譬如引擎提供了刷foliage的工具,對(duì)于石頭、樹之類大量復(fù)用的對(duì)象,用這種方式刷出的HISCM,會(huì)做gpu instancing大大減少DC數(shù)。然后一個(gè)有用的方案是HLOD,可以把一組Mesh甚至是一個(gè)關(guān)卡合并成一個(gè)Proxy Mesh,在最低級(jí)LOD后,可以切換到這個(gè)合并的Mesh,大大的減少遠(yuǎn)處物件的Draw Call并依然保持很遠(yuǎn)的視距。HLOD依然可以做多級(jí)的LOD幫助進(jìn)一步減少DrawCall和減少面數(shù),這些工具都是引擎內(nèi)建,可以很方便部署自動(dòng)化。

Dynamic Instancing,我們有一些特殊的方案,針對(duì)騰訊的Studio也做了一些整合,接下來的引擎版本會(huì)有非常大的渲染pipeline的重構(gòu),會(huì)對(duì)這個(gè)有更天然支持,甚至支持帶光照烘焙的Dynamic Instancing,在光照?qǐng)D計(jì)算的時(shí)候就把可以instancing到一起的對(duì)象優(yōu)先并到一張光照?qǐng)D上。

另外一個(gè)和DrawCall開銷息息相關(guān)的是渲染狀態(tài)切換的數(shù)量,引擎里有個(gè)接近的概念叫Drawing Policies,剛才說靜態(tài)的對(duì)象我們會(huì)按Drawing Policies分組排序,現(xiàn)在的版本中,我們針對(duì)這個(gè)分組排序的規(guī)則做了一些改進(jìn),可以更好的減少渲染線程的渲染繪制調(diào)用的狀態(tài)切換,同時(shí)也一定程度兼顧gpu的overdraw。剛才說到的新的mesh draw command pipeline要到今年年底,明年年初才上線,在目前的測(cè)試場(chǎng)景中,對(duì)于渲染線程的優(yōu)化,可能有近十倍的改善,當(dāng)然最終在移動(dòng)端上表現(xiàn)如何還不能下定論。整個(gè)新管線的思路是盡可能使得渲染線程在cpu端沒有什么開銷的,場(chǎng)景資源管理等的開銷都在GPU上。

RHI Thread,在OpenGL ES上,GraphicAPI的調(diào)用必須和glcontext在一個(gè)線程,于是,我們把所有的gl command都enqueue到了一個(gè)叫RHI Thread的線程,這樣一來,實(shí)際渲染驅(qū)動(dòng)的開銷和引擎渲染線程的工作就可以有一部分并行化,減少整個(gè)渲染的frame time,以及變向降低渲染線程所在核的主頻,這樣可能在部分設(shè)備上還能減少一些功耗開銷。

講完渲染線程,我們來看看Hitches,卡頓主要分為四塊。

Loading,加載,當(dāng)量啟用streaming level異步加載以后,如果游戲邏輯發(fā)生了阻塞加載,由于引擎并不知道加載數(shù)據(jù)的依賴性,所以會(huì)導(dǎo)致引擎Flush異步線程,造成卡頓。其中普通游戲邏輯觸發(fā)的加載我們可以比較容易的察覺并改正,但是另一個(gè)情況是在網(wǎng)絡(luò)同步的時(shí)候,當(dāng)服務(wù)器第一次同步回來一個(gè)新的Actor時(shí),客戶端會(huì)創(chuàng)建Actor Channel,并需要實(shí)際Spawn Actor,可能會(huì)依賴阻塞加載的數(shù)據(jù),進(jìn)而導(dǎo)致flush造成卡頓,我們可以通過打開net.AllowAsyncLoadingEnabled,使得觸發(fā)的加載變成一個(gè)異步加載,并且這個(gè)Actor Channel的創(chuàng)建過程,也會(huì)加入一個(gè)pending的隊(duì)列,等到加載資源都到了以后的那幀才可以實(shí)際的創(chuàng)建。

Compile Shader,由于ogl es沒有固定的shadercache標(biāo)準(zhǔn),引擎提供了ShaderCache,在新版本中改進(jìn)成了ShaderPipelineCache的功能,該系統(tǒng)可以在離線環(huán)境下先跑一遍游戲,在這個(gè)過程中用到的Shader,繪制的狀態(tài)記錄都會(huì)在Log文件中。Runtime的時(shí)候,我們會(huì)先讀log,分一些批次預(yù)先Compile完以減少runtime發(fā)生compile的情況。另外,一旦compile,可以配合另一個(gè)ProgramBinaryCache的功能,引擎會(huì)把link完的program保存下來,以后再需要加載Shader的時(shí)候,如果發(fā)現(xiàn)這個(gè)link program存在,會(huì)直接加載program。這樣不但能省去compile和link的過程,還跳過了shader code的加載過程和節(jié)省了內(nèi)存。除了compile,這個(gè)cache系統(tǒng)還會(huì)做warmup,也就是預(yù)先繪制,以減少第一次使用的額外開銷。

Spawning,降低spawn的開銷一個(gè)是減少每個(gè)components的數(shù)量,再者,盡可能用C++的Component。如果你是BP components,引擎項(xiàng)目設(shè)置中有一個(gè)選項(xiàng),可以在cook的時(shí)候把components的序列化,初始化的結(jié)果存下來,spawn的時(shí)候直接拿這個(gè)數(shù)據(jù)做實(shí)例化就行了。然后Component注冊(cè)到游戲線程可以做分時(shí)。當(dāng)然最常規(guī)的減少spawn卡頓的方法還是做pooling,如果有大量同類型Actor的Spawn,建議這樣做。

GC,主要分為兩步,先是引用分析,然后分析完標(biāo)記可以destruct的對(duì)象會(huì)在這時(shí)開始發(fā)出BeginDestroy,而實(shí)際的Destroy會(huì)分幀去做,因?yàn)橛行?duì)象渲染線程的資源還在訪問,不能當(dāng)場(chǎng)刪掉,所以只是發(fā)出一個(gè)render fence,渲染線程回收掉,我們才在下一幀主線程purge的階段把對(duì)象刪掉。在整個(gè)GC過程中最費(fèi)的,是引用分析,因?yàn)檫@個(gè)必須在當(dāng)前這幀做完,新版本中我們把標(biāo)記和引用分析都做了多線程并行,利用所有的核計(jì)算,可以比較好的提高引用分析的效率。還有一種手段是可以跳過大量的常駐內(nèi)存的對(duì)象,我這里列了一個(gè)參數(shù),MaxObjectNotConsideredByGC,設(shè)置這個(gè)參數(shù)范圍內(nèi)的對(duì)象是不會(huì)在引用分析的時(shí)候做檢測(cè)的。再有一點(diǎn)是Clustering,一組對(duì)象永遠(yuǎn)是共生的,可以規(guī)劃在Clustering里面,這樣的場(chǎng)景下GC效率可能提升十幾倍。最后新版本中,我們把BeginDestroy也放到的發(fā)生GC的后一幀去做。

解析來我們快速的過一下GPU。

渲染分辨率,我們可以逐設(shè)備的通過MobileContentScaleFactor設(shè)置BackBuffer的分辨率。我們也可以通過r.ScreenPercentage把單獨(dú)的3D的分辨率改小。改分辨率是顯而易見提升GPU的手段,因?yàn)榇蟛糠謺r(shí)候我們都是pixel shader bound。當(dāng)然,帶寬也是很大的因素,引擎還可以靈活的設(shè)置SceneColor的格式,默認(rèn)HDR下我們使用FP16的RGBA,在有些項(xiàng)目里我們可以用r.Mobile.SceneColorFormat來調(diào)整成R11G11B10或者RGBE的方式減少帶寬的占用。當(dāng)然要注意,移動(dòng)端有些特性一來DepthBuffer,而支持DepthStencil fetch擴(kuò)展的設(shè)備并不算太多,所以引擎默認(rèn)會(huì)把Depth存到SceneColor的A通道,所以采用R11G11B10這樣的格式,可能就會(huì)使得某些依賴讀回深度的feature發(fā)生問題。

材質(zhì),也就是shader復(fù)雜度,我們可以設(shè)置Quality Switch使用不同復(fù)雜度的材質(zhì)針對(duì)設(shè)備做優(yōu)化。也可以直接使用fully rough,non metal之類的材質(zhì)優(yōu)化選項(xiàng)。當(dāng)然濫用的話會(huì)使得最終生成的shader permutation的數(shù)量很多,需要注意一下。

Shadow,主要分為兩種。Modulate shadow我們已經(jīng)不太適用,不過因?yàn)槭菃螌?duì)象一個(gè)shadow volume,所以可以設(shè)置的shadow map利用率和精度比較高一些,在某些角色展示場(chǎng)景中可能比較有用;CSM是全場(chǎng)景的動(dòng)態(tài)shadow,非全動(dòng)態(tài)光照時(shí),移動(dòng)端默認(rèn)只對(duì)動(dòng)態(tài)對(duì)象投射。可以通過Device Profile控制,例如可以在低端設(shè)備上沒有shadow,中等的設(shè)備上可以不做PCF filtering,好的設(shè)備上才開filtering做多次采樣。

Landscape,我們?cè)诮诎姹局幸沧隽艘恍└倪M(jìn),不同層LOD的計(jì)算以前是根據(jù)距離,現(xiàn)在改成根據(jù)屏幕占比,頂點(diǎn)shader的計(jì)算量會(huì)小很多。另外現(xiàn)在新的版本中移動(dòng)端的材質(zhì)不再受三層的限制,當(dāng)然三層的時(shí)候,兩個(gè)weightmap和normal共享一張貼圖,依然是比較優(yōu)化的情況。地形本來占屏范圍就廣,采樣多的話pixel shader開銷很高,所以還是盡量推薦使用三層以內(nèi)的混合。

Base Pass pixel shader,效果上我們做了一些改進(jìn),sky light和refleciton的計(jì)算都做了修正,Specular換成了GGX,以前GGX在半精度的情況下,NoH接近1時(shí)會(huì)有比較大誤差,我們做了一些改進(jìn)。另外,在MobileBasePassPixelShader中的各個(gè)模塊,項(xiàng)目組也可以根據(jù)需要去除不需要的,例如IBL或者lightmap或者shadowmap的部分。

后處理,可以根據(jù)不同的設(shè)備做不同功能的開關(guān)。

Mask,在移動(dòng)硬件上比較費(fèi)的原因是因?yàn)槿绻麑慸epth時(shí),某個(gè)像素發(fā)生clip/discard,硬件的earlyz就會(huì)失效,導(dǎo)致overdraw。一個(gè)方案是開啟prepass畫mask,basepass做z equel;還有一個(gè)是引擎的LOD transition,在發(fā)生LOD時(shí),不是直接換模型,會(huì)把兩個(gè)LOD模型都畫一下,通過一個(gè)dither的mask慢慢的漸變過去,這個(gè)時(shí)候可以采用類似于mask的行為,我們可以把LOD的結(jié)果dither的結(jié)果畫到Stencil,在BasePass時(shí)做stenciltest減少不必要的discard。

接下來我們講講內(nèi)存。

內(nèi)存我們針對(duì)不同的設(shè)備,獨(dú)立于其他的優(yōu)化選項(xiàng),單獨(dú)有一組Bucket設(shè)置,可以針對(duì)不同設(shè)備的可用內(nèi)存決定自己使用的Memory Bucket設(shè)置。

除了Streaming Level,引擎還有一個(gè)內(nèi)建的很強(qiáng)大的功能是Texture Streaming,剛才已經(jīng)介紹過一些,IOS上的實(shí)現(xiàn)利用了Apple的GL擴(kuò)展,安卓有些設(shè)備沒有擴(kuò)展,我們可以做完整的貼圖資源拋棄和重新的創(chuàng)建。在cpu上根據(jù)物件bounds的屏幕尺寸×材質(zhì)中用到的對(duì)應(yīng)貼圖的uv scale系數(shù)×一個(gè)可以由美術(shù)tweak的scalar值來決定實(shí)際貼圖提交的mip數(shù),可以用r.Streaming.PoolSize在不同設(shè)備上很方便設(shè)置全局的貼圖資源的內(nèi)存Budget。

Shader code,我們會(huì)利用Shared Shader code的功能,將大量靜態(tài)的導(dǎo)致產(chǎn)生的Shader有重復(fù)的去除,將實(shí)際的Shader code存入ShaderLibrary,在每個(gè)MaterialInstance對(duì)象上只存ShaderCode的GUID,大大減小了實(shí)際的ShaderCode大小。在有些項(xiàng)目里可以減掉80%。另外,不使用的rendering功能一定要在項(xiàng)目設(shè)置中關(guān)掉,可以大大減少shader的組合數(shù)量。

RHI,UI的貼圖比較大,由于默認(rèn)情況下貼圖資源被CDO(Class Default Object)引用住無法GC掉,可以用弱引用技術(shù)的方式來緩解這個(gè)問題。另外,Slate altas Size可以小一點(diǎn),可以減少冗余的空掉的貼圖內(nèi)存。GPU Particle不用的時(shí)候可以把fx.AllowGPUParticles關(guān)掉,我們會(huì)用到兩張128位1024的RT存gpu particle的position和velocity,有將近60兆的大小。另外,F(xiàn)SlateRHIResoureceManage,F(xiàn)renderTargetPool里polling起來的資源,可以適時(shí)主動(dòng)調(diào)釋放的接口,以減少之前用過,之后短期內(nèi)不會(huì)用到的資源。

另外,近期我們還發(fā)現(xiàn)在使用UniformBuffer的時(shí)候,在一些gles的驅(qū)動(dòng)里會(huì)有非??捎^的內(nèi)存開銷,因此我們現(xiàn)在改成了在ES3也會(huì)用pack過的UniformArray的形式。

還有很多比較散內(nèi)存優(yōu)化點(diǎn),礙于時(shí)間關(guān)系,這里就不展開細(xì)說了,例如在clang下TCHAR是4字節(jié)的,我們改成了二字節(jié),也把相關(guān)的字符串函數(shù)做了一些自己的實(shí)現(xiàn)。

最后,我們簡單看一些引擎關(guān)于適配和迭代的設(shè)置手段。

這是引擎大量依賴的scalability系統(tǒng),引擎所有可以控制的屬性,都可以放到Scalability Group,引擎內(nèi)建了一些分組,我列在這里了,項(xiàng)目組也可以定義任意的分組,每個(gè)分組里面可以有我們不同的參數(shù)控制,配合有繼承關(guān)系的Device profile系統(tǒng),可以很方便的針對(duì)不同的設(shè)備使用不同的scalability設(shè)置,單獨(dú)可使用的設(shè)置項(xiàng)非常多,可能有上千個(gè)。

下面的這個(gè)Device Profile的例子是iPhoneX,大家可以看到iPhoneX的設(shè)置是繼承自IOS高配的并做了一些override,而ios高配又繼承自IOS,而IOS繼承自移動(dòng)設(shè)備的Profile,一個(gè)項(xiàng)目可以適配任意多的硬件和平臺(tái)。不同的Device Profile的選擇依靠不同平臺(tái)的Selector,安卓上可以根據(jù)正則表達(dá)式或者嚴(yán)格匹配等方案去匹配SoC,GPU Family,Device Module或者GL Version等。

再來我們看下項(xiàng)目Iterating的步驟,數(shù)據(jù)轉(zhuǎn)換過程我們叫做Cook,cook分為兩種方式,一種是你設(shè)備跑起來的時(shí)候,設(shè)備上是沒有資源的,設(shè)備的資源訪問不是訪問本地,而是訪問網(wǎng)絡(luò)磁盤,編輯器的一個(gè)commandlet會(huì)作為server端持續(xù)提供你要訪問的數(shù)據(jù),這個(gè)數(shù)據(jù)如果沒有經(jīng)過轉(zhuǎn)換會(huì)先阻塞的cook完再發(fā)過去,迭代的時(shí)候非常有用,叫cook on the fly。還有一個(gè)是把資源全部轉(zhuǎn)化完發(fā)到手機(jī)上,在不-iterate時(shí),即使資源不改,也會(huì)先都load出來再save回去做檢查。項(xiàng)目大了會(huì)用很久,如果資源變化了,在DDC(Derived Data Cache)中找不到,需要發(fā)生資源轉(zhuǎn)換的過程,則會(huì)更慢。當(dāng)用了-iterate后就會(huì)跳過這個(gè)步驟,但是有時(shí)候依然會(huì)load+save,是因?yàn)閕ni文件發(fā)生了變動(dòng),引擎不知道這個(gè)變動(dòng)會(huì)不會(huì)影響cook結(jié)果,只能重新load/save,這時(shí)候引擎有一些優(yōu)化選項(xiàng),可以讓你配置一些特殊的字段告訴引擎,當(dāng)這些字段發(fā)生變化時(shí)cook也會(huì)不做檢查,例如項(xiàng)目版本號(hào)之類的字段。當(dāng)?shù)鷾y(cè)試的時(shí)候只要改變啟動(dòng)命令行參數(shù)的時(shí)候,可以push一個(gè)UE4Commandline.txt文件到設(shè)備上,就可以免除重新打包的時(shí)間。

Debug沒什么好說的,新版本中,為了加速迭代,我們開始使用Android Studio做debug,可以同時(shí)debug native和java代碼。當(dāng)native代碼改動(dòng)后,可以在vs里編譯,UBT會(huì)自動(dòng)更新build.gradle,使得Android Studio會(huì)自動(dòng)識(shí)別并更新,改完后直接去android studio中啟動(dòng)就能debug了,不需要再打包了。

Profiling方面,gpu上細(xì)節(jié)的profiling主要靠移動(dòng)gpu廠商工具;另外引擎有大量的內(nèi)建的工具,例如常用的stat系列的命令以及showflag系列命令可以快速幫忙定位問題,cpu的profiling,引擎有自帶的工具,近期還加入了第三方工具framepro的支持,可以以很小的overhead做基于namedevent的profiling。我們也正在和騰訊合作,在做一些新的Profiling工具供大家使用。關(guān)于內(nèi)存的profiling,引擎也有一些Memreport和llm的命令和對(duì)應(yīng)的Memory Profiler工具輔助檢查內(nèi)存的使用狀況,以及查找內(nèi)存泄露和優(yōu)化的方案。

今天要講的就是這些,謝謝大家。


銳亞教育

銳亞教育,游戲論壇|地下城守護(hù)者3|機(jī)核網(wǎng)|織田non|Everspace|血與冰淇淋|cha研|永遠(yuǎn)忠誠|游戲開發(fā)論壇|游戲制作人|游戲策劃|游戲開發(fā)|獨(dú)立游戲|游戲產(chǎn)業(yè)|游戲研發(fā)|游戲運(yùn)營|