在2018TGDC大會上,來自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ù)支持以及一些針對中國區(qū)的技術(shù)功能的開發(fā)。我跟解衛(wèi)博以前是多年的同事,他剛才介紹了很多使用虛幻在渲染上的案例,我介紹的更貼近現(xiàn)在主流使用,尤其是國內(nèi)手游比較重度的情況下,我會介紹一下使用UE4制作大地型游戲的挑戰(zhàn)和優(yōu)化的手段。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Shader code,我們會利用Shared Shader code的功能,將大量靜態(tài)的導(dǎo)致產(chǎn)生的Shader有重復(fù)的去除,將實(shí)際的Shader code存入ShaderLibrary,在每個MaterialInstance對象上只存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ù)的方式來緩解這個問題。另外,Slate altas Size可以小一點(diǎn),可以減少冗余的空掉的貼圖內(nèi)存。GPU Particle不用的時候可以把fx.AllowGPUParticles關(guān)掉,我們會用到兩張128位1024的RT存gpu particle的position和velocity,有將近60兆的大小。另外,F(xiàn)SlateRHIResoureceManage,F(xiàn)renderTargetPool里polling起來的資源,可以適時主動調(diào)釋放的接口,以減少之前用過,之后短期內(nèi)不會用到的資源。

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

還有很多比較散內(nèi)存優(yōu)化點(diǎn),礙于時間關(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)目組也可以定義任意的分組,每個分組里面可以有我們不同的參數(shù)控制,配合有繼承關(guān)系的Device profile系統(tǒng),可以很方便的針對不同的設(shè)備使用不同的scalability設(shè)置,單獨(dú)可使用的設(shè)置項(xiàng)非常多,可能有上千個。

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

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

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

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

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

銳亞教育

銳亞教育,網(wǎng)絡(luò)游戲,網(wǎng)游,新游,游戲,新聞,國內(nèi),全球,評論,資訊,專題,圖片,焦點(diǎn),排行,免費(fèi),私服,魔獸,傳奇,西游,泡泡堂,冒險島,征途,勁舞團(tuán),勁樂團(tuán),公會,外掛,17173,玩家,測試,內(nèi)測,封測,公測,試玩,調(diào)查,flash