天天看點

2018TGDC王祢:UE4制作多人大地型遊戲的優化

在2018TGDC大會上,來自Epic Games中國資深技術工程師王祢先生發表了《UE4制作多人大地型遊戲的優化》主題演講。王祢有近10年的虛幻引擎使用經驗,從console遊戲、掌機到PC端MMO遊戲,再到手遊,都有過相關開發經驗;現在Epic Games China,他作為唯一的引擎技術專家,參與和幫助了衆多使用UE3和UE4的項目解決各種問題。以下是演講實錄:

2018TGDC王祢:UE4制作多人大地型遊戲的優化

大家好!我是王祢,來自Epic Games,現在在中國區負責引擎技術支援以及一些針對中國區的技術功能的開發。我跟解衛博以前是多年的同僚,他剛才介紹了很多使用虛幻在渲染上的案例,我介紹的更貼近現在主流使用,尤其是國内手遊比較重度的情況下,我會介紹一下使用UE4制作大地型遊戲的挑戰和優化的手段。

這是今天會講到的總體内容,内容比較多一些,有些地方會過得比較快。首先我們來看一下在移動裝置上做大地型多人遊戲的挑戰。大地型肯定是開放的地圖,視野比較寬,視距比較遠,地圖比較大,在很大的世界裡還會有比較多的風格變換,會導緻繪制内容的種類比較多,資源的使用、變化比簡單一些的遊戲複雜非常多。

對于同樣的移動硬體來看,優化的壓力會大非常多。我們來看看優化分為哪幾部分,主要的優化包括有大量的角色需要跟場景發生互動,角色的動畫之類的計算以及與場景互動的計算發生在遊戲線程,是以遊戲線程承擔了非常重的優化任務。是以首先我們講遊戲線程的優化。

引擎裡面有一個東西,我知道這個是比較偏向于遊戲邏輯業務的概念,可能一般大家不太認為會在引擎裡面實作,我們叫做重要度管理系統,大家知道遊戲的正常優化手段叫做LOD,不管是面數、更新頻率,我們都會根據在螢幕上所占比進行調整,這是很通用的,延用很久的優化手段。

我們怎麼樣讓各個遊戲子產品從遊戲邏輯層去修正LOD的計算?這時我們引入Significance Manager,我們會配置設定針對每個平台的Bucket,大家可以看到右下示意圖中藍色的小點代表玩家控制的角色,邊上的小點是别的玩家和互動的動态對象。我們根據離主角玩家的距離,在螢幕上的尺寸或者可見性,決定使用什麼bucket。例如基于可見性的計算,雖然離我很近,但是因為在我的背後,可能很多時候我都感受不到,Bucket就可以分得不一樣,通過Bucket我們會用來控制、修正LOD的各種計算。這裡是一個例子,我們這個系統本身用于我們自己比較火熱的遊戲《堡壘之夜》,手機、掌機、電腦都可以跑,我們相容所有的平台可以聯機玩,遊戲在不同平台上的場景、複雜程度其實是一樣的。這種情況下,硬體的計算能力有非常大的差别,是以我們針對移動平台和主機Bucket也不一樣,除了自身控制的角色給的Bucket比較高,剩下的角色的比較低,主機有四個,手機有一個,這個設定不僅按平台來,也可以按裝置來,移動裝置好的和差的硬體計算能力差很多,我們可以在Device profile指定目前這台裝置Bucket的規劃。

剛才是比較全局的系統,接下來我們看遊戲線程裡開銷最大的部分就是我們的動畫,動畫系統大部分角色是可以定制的,角色會分為幾個部分,繪制調用的數量、動畫骨骼更新、不同部件的不同動畫計算量非常大,針對Fortnite這樣的遊戲有一些特殊的遊戲模式,例如50V50,這種情況下,最終在縮圈以後,同屏會出現超過50甚至80個角色,每個角色還分了好幾個部件,背包、武器都有不同的動畫,這個時候計算量非常大,我們需要對動畫做非常大量的優化。

剛剛我們已經說到角色可能分為幾個部分,有一些不同的政策,引擎提供各種方式,一種是将不同的部位的Mesh合成為一個,這個模型有一個問題,材質是要合并起來的,你的表情的動畫就沒有了,在這個方案上我們做了一些取舍,最終決定不在Fortnite用這種方式。另一種,身上不需要動畫的剛體挂件可以友善的挂在角色骨骼的Socket上面,這是比較簡單的方式。還有Master Slave的方式,主體動畫是一套完整的骨骼,身上挂載的動畫是這個骨骼的子支,這個時候我們可以把這些挂載的部件的動畫完全跳過自己的動畫更新計算,完全用Master驅動,這樣的骨骼動畫直接使用Master的骨骼矩陣,沒有辦法擴充,比如Master Skeleton沒有尾巴或是披風的骨骼,尾巴或是披風的獨立動畫或者實體模拟就沒辦法做。針對這種情況,我們還有一個解決方案是Copy Pose,可以把主體的計算完的骨骼矩陣拷貝給附屬的骨骼矩陣,隻要保持目标骨骼和原骨骼的層級結構一緻,就可以在目标骨骼上增加擴充性的骨骼,可以根據自己的狀态播自己的動畫,也可以模拟實體。這是四種多部件角色setup的方案,無論使用哪一種,都需要對骨骼模型和骨架設定LOD,這是下面提到多種優化的前提。

第一步比較直覺的是在動畫更新的時候會有大量的邏輯事件的計算,我們稱之為Event Graph,這是UE4提供的圖形化的腳本功能,Event Graph是需要經過圖形化的腳本虛拟機,這個調用在動畫邏輯比較複雜的時候開銷有點高,我們把在虛拟機上計算的Event Grape轉到C++,省掉了大量開銷。

再有一個是Anim Graph,我們根據目前的狀态選擇不同的骨骼層級,播放哪個動畫,或是經過哪些骨骼控制節點,比如說IK、實體模拟最終的POSE的計算。在這個計算中間有一些步驟會用到數學計算,因為是在Graph,會有一些額外的開銷。我們做了一些優化,我們把所有這些獨立計算的子產品通通納入到一些基礎的骨骼動畫混合節點,包括偏移和縮放,這樣可以減少虛拟機的調用開銷,我們把這些包含簡單計算項的動畫混合節點叫做Fast Path節點(右上角有閃電小圖示),骨骼混合的計算邏輯通通是用Fast Path可以完全消除在虛拟機上的開銷。

同屏有那麼多的角色要做骨骼動畫計算,大家知道移動裝置是多核裝置,為了更好的利用多核的定性,我們需要把剛剛這種虛拟機上的調用更好的平攤到不同的線程。基于上面兩個優化方向,我們不要使用Event Graph,把遊戲邏輯更新的部分放在AnimInstanceProxy上,這樣引擎會自動判斷這個Event Graph是不是可以放在别的線程上更新。如果你用了Fast Path,我們就可以把骨骼的update和evaluation都放到working thread上面去,例如有50個角色,在任一角色更新開始,就把計算分到别的線程上面,主線程繼續往下走。

即使我們能利用多線程,計算量還是非常大的,我們要減少動畫更新的資料量,已經有些設定可以幫助動畫在不渲染的時候跳過 Tick pose,也可以通過Singnificance Manager跳過附屬武器、背包的更新,除了自己的主角,别的角色離你遠一些,資訊不更新其實你是注意不到的。

我們的掉落物會模拟實體,是骨骼物體。骨骼計算有一個問題,是走的Dynamic Path。我們引擎的中的靜态對象,會在加到場景中的時候就直接排序分組到自己的Drawing Policy,繪制的時候可以很大程度減少渲染狀态的切換。而動态的機關,是每一幀在渲染開始的InitViews階段動态擷取到資料,它和靜态擷取資料的方式不一樣,不會進入到靜态排序的表裡,繪制的效率比較低。針對這種實際每一幀渲染資料不發生變化的骨骼物體,我們把這些物體額外加到了一個StaticRenderPath,加速了這些物體的渲染。

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

剛才講的這些是針對骨骼動畫更新的優化,其實伴随着骨骼LOD的設定,我們在AnimGraph中可以設定骨骼控制節點從某一級LOD下不計算,比如說IK、實體模拟。

說完動畫的優化,接下來遊戲線程還有大量的Scene Component,Scene Component是指世界中有坐标位置的對象,它的Transform更新都是在遊戲線程中計算。當你大地圖、大場景動态更新對象非常多,同時每個對象身上會挂很多Scene Component的時候,計算量是非常大的。盡管我們會把Scene Component的計算踢到異步線程,但是計算量依然很大。我們做了一些改進,針對一些挂載在人物身上,不是處于激活狀态的Scene Component做了自動的管理。

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

當Scene Component發生位置變化的時候會觸發Overlap的檢查,每一幀有大量運動對象時會産生大量Overlap事件,耗費比較大的開銷。優化的原則是盡可能把不需要産生Overlap的事件關掉,注意引擎預設是打開的。我們對層級結構比較複雜的做了子Component是否打開overlap事件的引用計數,會看自己是不是打開了Overlap事件,以及自己的子對象有沒有打開。這個時候我們在做Overlap檢查的時候可以很快的跳過,這個節點往下都沒有,就不需要再檢查自己的子節點,這在場景的對象結構比較複雜的情況下是可觀的優化。

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

Physics,我們會盡可能的用一些替代的Physics優化實體注冊的對象,有一組對象,比如說邊界,不需要很細緻的碰撞模型,我們可以用簡單的volume來表達實體碰撞對象,減少注冊到實體場景中的對象數量。實體的一個場景會有兩個樹,一個用以做Query,一個用于做Simulation,我們要盡可能保證注冊進去的對象最優化。是以需要盡可能的簡化每個實體對象的複雜度,以及減少整個場景注冊的實體對象數。可以同時以比較小的記憶體開銷打開異步的實體場景,Physics注冊的對象是一樣的,隻不過他會用Shared Shape的方式加到Async Scene裡,這樣在場景做實體模拟的同時,他可以在異步的scene裡做其他的query。

另外我還嘗試過把同樣mesh的不同執行個體對象用Shared shapes減少注冊的實體對象的記憶體開銷,在記憶體敏感的場景下也可以嘗試。還有一個思路是我們可以把實體對象和視覺對象解耦,預設的情況下,引擎的Mesh對象打開碰撞就會注冊實體對象到PhysX Scene,增加了實體場景的複雜度和實體的記憶體占用。是以當你的Mesh加載到記憶體裡,即使不被渲染出來,這些開銷就在了,但是其實很多情況下視覺會看得更遠一些,實際需要實體計算互動的距離在有些遊戲中沒那麼遠,我們可以用一些手段把視覺上對象的實體關掉,把這個實體屬性轉到一些新的Component和Actor上面放到新的Streaming Level裡,用更近的加載解除安裝距離來管理,這樣實際的實體場景複雜度和記憶體占用都會小很多。另外移動端的布料,計算量和網格數量相關,在移動端會不太推薦使用那麼複雜的模拟,引擎也就沒有提供移動端的NvCloth的lib,是以我們一般會用剛體來模拟。

Ticking,也即所有動态邏輯更新的對象,引擎的圖形化腳本可以讓美術策劃和GamePlay程式很友善的在Event Graph做Tick更新,但是需要付出一定虛拟機的調用開銷,當Tick事件觸發的執行隊列非常長,每一幀付出虛拟機的成本就會比較高一些。一個方法是轉到C++,另外一個方法是減低Tick的頻率,更有一些特殊的,例如每一幀隻是視覺上在轉動的風車或是旗幟在飄、樹在擺動,其實可以不需要用骨骼動畫、或者在Tick做旋轉,可以用頂點動畫來做。

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

UI,如果遊戲HUD有大量的UI對象,它的位置計算會比較複雜,在遊戲線程的計算量就會比較大,可以多利用我們新出的SlatLayoutCaching和Invalidation Box來Cache Prepass減少widget transform更新的計算,這些Cache可以把計算的位置和大小記錄下來,有一些可以把頂點Buffer Cache下來。另外,我們也需要盡量讓UI的Widget可以Batching起來。引擎的一些布局空間會自動幫你布局子控件,例如Horizental和Vertical Box,Grid等,這時候子控件是在同一層上,引擎會優先Batch起來。當使用比較靈活的Canvas Panel時,會導緻引擎預設的行為會把每個加入的子空間的Implicit Zorder自動增一,這時候如果你确定這些子Widget不重疊,其實可以手動控制這個ZOrder。當然Batch的前提還是你用了同樣的材質和貼圖。那麼如果做一個背包界面,裡有很多不同東西的圖示,我們又希望這些圖示有一些特效,我們可以用同一個材質,這隻同一個Texture Altas,針對每個子控件設定不同的Vertex Color,在Vertex Shader裡通過VC的值做為uv來使得這些子控件可以被Batching起來。

音頻和特效,音頻是比較大的開銷,我們之前的堡壘之夜又是從主機到移動端相容的項目,為了優化音頻在移動端的開銷,我們增加了做了很多設定,使得在移動端不同的裝置可以設定不同的SoundCue并發的數量,以及SoundSource的數量。其中SoundSource預設在移動端上總數是16個,主機上可能是32個。簡單說明一下什麼是SoundCue,這就是原始的SoundWave資源拿過來做一些實時處理封裝後的音頻資源,例如可以在多個SoundWave中做一些随機、拼接,以及一些聲音效果的實時處理,這些處理效果對計算量要求比較大,我們可以針對不同的硬體裝置做一些LOD的設定,比如說在比較差的CPU移動裝置上,可以把Reverb,EQ等關掉,或者減少随機的Wave的數量等。

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

Level Streaming,為什麼用Level Streaming?其實道理很簡單,因為場景非常大時,我們不可能把所有的場景加載到記憶體裡面,這時候我們可以把地圖拆得非常碎,每次隻加載視距内的一小部分,使得記憶體的占用變得比較低。這樣一來場景在記憶體裡的東西比較小,場景周遊的開銷也會比較小。同時也可以在設計上增加場景可使用的物件的種類,豐富了場景的複雜度。整個Level Streaming總共分為三個步驟:

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

伺服器,其實剛才針對用戶端的優化,都會惠及伺服器的優化。在新版本中,我們加入了Replication Graph,在集中的類裡做了ServerReplicateActors的計算,總體思路就是減少PerConnection,PerActor的relevancy以及priority的計算量,通過把Net Actor注冊到以空間位置劃分的grid中,每次針對目前Connection隻檢查所在Grid内對象的資訊來大大降低整個Replication的計算量。另外,對于不同Connection見的部分對象,我們也會Cache下來需要replicate的資料結果針對别的connection複用。這個改動優化使得在我們的項目中我們伺服器的整個CPU用以做replication的開銷降到原先的1/4。

另外一些伺服器優化手段有,這是降低所有對象Net relevancy distance的距離;把以移動的RPC包做優化,如果連續的幾個移動方向和速度是一緻的,可以把幾個移動RPC包合并起來隻發一個,減少網絡帶寬的占用和包的序列化等計算量。

伺服器我們也可以關掉大量動畫的計算,隻在播一些特殊動畫的蒙太奇的時候才會打開動畫的更新。在Server上也可以把一些隻關注渲染視覺和實際遊戲邏輯計算沒有關系的Component在Server上去掉。

好了,看完大量遊戲線程的優化手段,接下來我們來看看渲染線程,渲染線程的第一個開銷取決于場景的複雜度,即使實際繪制出來的内容很少,但是場景周遊的開銷卻是正比于場景在記憶體裡的Primitive數量的。如果我這個周遊時間很長,那麼實際繪制調用發出的時間就會比較晚。這個時候,我們就要利用好Streaming Level來最小化Scene Tranversal的開銷。另外,動态的對象每一幀重新擷取要繪制的渲染資料,也會有不小的開銷,同時也會降低靜态對象的渲染狀态排序的優勢。這也是上面提到過的加入了特殊的Static Render Path的優化手段的原因。

場景周遊後的大頭是是Culling,包括預計算的Precomputed visibility Volume,場景針對每個場景的可見性,不是特别大的地圖比較适用,在runtime幾乎沒有開銷,tradeoff是離線計算的時間和一部分記憶體。然後是并行的視錐體裁剪和基于距離的裁剪,都是很正常的Culling手段。移動端的occlusion是比較頭痛的問題,我們在支援ES3.1的裝置上,使用了Hardware occlusion query,在3.1以下的裝置我們提供了一個Software occlusion的解決方案。當然要注意這并不是萬能的,有些情況下還多了繪制的三角形面數及大量bounds transform的CPU開銷,卻沒有實際occlude掉什麼對象。

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

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

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

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

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

Loading,加載,當量啟用streaming level異步加載以後,如果遊戲邏輯發生了阻塞加載,由于引擎并不知道加載資料的依賴性,是以會導緻引擎Flush異步線程,造成卡頓。其中普通遊戲邏輯觸發的加載我們可以比較容易的察覺并改正,但是另一個情況是在網絡同步的時候,當伺服器第一次同步回來一個新的Actor時,用戶端會建立Actor Channel,并需要實際Spawn Actor,可能會依賴阻塞加載的資料,進而導緻flush造成卡頓,我們可以通過打開net.AllowAsyncLoadingEnabled,使得觸發的加載變成一個異步加載,并且這個Actor Channel的建立過程,也會加入一個pending的隊列,等到加載資源都到了以後的那幀才可以實際的建立。

Compile Shader,由于ogl es沒有固定的shadercache标準,引擎提供了ShaderCache,在新版本中改進成了ShaderPipelineCache的功能,該系統可以在離線環境下先跑一遍遊戲,在這個過程中用到的Shader,繪制的狀态記錄都會在Log檔案中。Runtime的時候,我們會先讀log,分一些批次預先Compile完以減少runtime發生compile的情況。另外,一旦compile,可以配合另一個ProgramBinaryCache的功能,引擎會把link完的program儲存下來,以後再需要加載Shader的時候,如果發現這個link program存在,會直接加載program。這樣不但能省去compile和link的過程,還跳過了shader code的加載過程和節省了記憶體。除了compile,這個cache系統還會做warmup,也就是預先繪制,以減少第一次使用的額外開銷。

Spawning,降低spawn的開銷一個是減少每個components的數量,再者,盡可能用C++的Component。如果你是BP components,引擎項目設定中有一個選項,可以在cook的時候把components的序列化,初始化的結果存下來,spawn的時候直接拿這個資料做執行個體化就行了。然後Component注冊到遊戲線程可以做分時。當然最正常的減少spawn卡頓的方法還是做pooling,如果有大量同類型Actor的Spawn,建議這樣做。

GC,主要分為兩步,先是引用分析,然後分析完标記可以destruct的對象會在這時開始發出BeginDestroy,而實際的Destroy會分幀去做,因為有些對象渲染線程的資源還在通路,不能當場删掉,是以隻是發出一個render fence,渲染線程回收掉,我們才在下一幀主線程purge的階段把對象删掉。在整個GC過程中最費的,是引用分析,因為這個必須在目前這幀做完,新版本中我們把标記和引用分析都做了多線程并行,利用所有的核計算,可以比較好的提高引用分析的效率。還有一種手段是可以跳過大量的常駐記憶體的對象,我這裡列了一個參數,MaxObjectNotConsideredByGC,設定這個參數範圍内的對象是不會在引用分析的時候做檢測的。再有一點是Clustering,一組對象永遠是共生的,可以規劃在Clustering裡面,這樣的場景下GC效率可能提升十幾倍。最後新版本中,我們把BeginDestroy也放到的發生GC的後一幀去做。

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

渲染分辨率,我們可以逐裝置的通過MobileContentScaleFactor設定BackBuffer的分辨率。我們也可以通過r.ScreenPercentage把單獨的3D的分辨率改小。改分辨率是顯而易見提升GPU的手段,因為大部分時候我們都是pixel shader bound。當然,帶寬也是很大的因素,引擎還可以靈活的設定SceneColor的格式,預設HDR下我們使用FP16的RGBA,在有些項目裡我們可以用r.Mobile.SceneColorFormat來調整成R11G11B10或者RGBE的方式減少帶寬的占用。當然要注意,移動端有些特性一來DepthBuffer,而支援DepthStencil fetch擴充的裝置并不算太多,是以引擎預設會把Depth存到SceneColor的A通道,是以采用R11G11B10這樣的格式,可能就會使得某些依賴讀回深度的feature發生問題。

材質,也就是shader複雜度,我們可以設定Quality Switch使用不同複雜度的材質針對裝置做優化。也可以直接使用fully rough,non metal之類的材質優化選項。當然濫用的話會使得最終生成的shader permutation的分裂數量很多,需要注意一下。

Shadow,主要分為兩種。Modulate shadow我們已經不太适用,不過因為是單對象一個shadow volume,是以可以設定的shadow map使用率和精度比較高一些,在某些角色展示場景中可能比較有用;CSM是全場景的動态shadow,非全動态光照時,移動端預設隻對動态對象投射。可以通過Device Profile控制,例如可以在低端裝置上沒有shadow,中等的裝置上可以不做PCF filtering,好的裝置上才開filtering做多次采樣。

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

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

後處理,可以根據不同的裝置做不同功能的開關。

Mask,在移動硬體上比較費的原因是因為如果寫depth時,某個像素發生clip/discard,硬體的earlyz就會失效,導緻overdraw。一個方案是開啟prepass畫mask,basepass做z equel;還有一個是引擎的LOD transition,在發生LOD時,不是直接換模型,會把兩個LOD模型都畫一下,通過一個dither的mask慢慢的漸變過去,這個時候可以采用類似于mask的行為,我們可以把LOD的結果dither的結果畫到Stencil,在BasePass時做stenciltest減少不必要的discard。

接下來我們講講記憶體。

記憶體我們針對不同的裝置,獨立于其他的優化選項,單獨有一組Bucket設定,可以針對不同裝置的可用記憶體決定自己使用的Memory Bucket設定。

除了Streaming Level,引擎還有一個内建的很強大的功能是Texture Streaming,剛才已經介紹過一些,IOS上的實作利用了Apple的GL擴充,安卓有些裝置沒有擴充,我們可以做完整的貼圖資源抛棄和重新的建立。在cpu上根據物件bounds的螢幕尺寸×材質中用到的對應貼圖的uv scale系數×一個可以由美術tweak的scalar值來決定實際貼圖送出的mip數,可以用r.Streaming.PoolSize在不同裝置上很友善設定全局的貼圖資源的記憶體Budget。

Shader code,我們會利用Shared Shader code的功能,将大量靜态的分裂導緻産生的Shader有重複的去除,将實際的Shader code存入ShaderLibrary,在每個MaterialInstance對象上隻存ShaderCode的GUID,大大減小了實際的ShaderCode大小。在有些項目裡可以減掉80%。另外,不使用的rendering功能一定要在項目設定中關掉,可以大大減少shader分裂的組合數量。

RHI,UI的貼圖比較大,由于預設情況下貼圖資源被CDO(Class Default Object)引用住無法GC掉,可以用弱引用技術的方式來緩解這個問題。另外,Slate altas Size可以小一點,可以減少備援的空掉的貼圖記憶體。GPU Particle不用的時候可以把fx.AllowGPUParticles關掉,我們會用到兩張128位1024的RT存gpu particle的position和velocity,有将近60兆的大小。另外,FSlateRHIResoureceManage,FrenderTargetPool裡polling起來的資源,可以适時主動調釋放的接口,以減少之前用過,之後短期内不會用到的資源。

另外,近期我們還發現在使用UniformBuffer的時候,在一些gles的驅動裡會有非常可觀的記憶體開銷,是以我們現在改成了在ES3也會用pack過的UniformArray的形式。

還有很多比較散記憶體優化點,礙于時間關系,這裡就不展開細說了,例如在clang下TCHAR是4位元組的,我們改成了二位元組,也把相關的字元串函數做了一些自己的實作。

最後,我們簡單看一些引擎關于适配和疊代的設定手段。

這是引擎大量依賴的scalability系統,引擎所有可以控制的屬性,都可以放到Scalability Group,引擎内建了一些分組,我列在這裡了,項目組也可以定義任意的分組,每個分組裡面可以有我們不同的參數控制,配合有繼承關系的Device profile系統,可以很友善的針對不同的裝置使用不同的scalability設定,單獨可使用的設定項非常多,可能有上千個。    

下面的這個Device Profile的例子是iPhoneX,大家可以看到iPhoneX的設定是繼承自IOS高配的并做了一些override,而ios高配又繼承自IOS,而IOS繼承自移動裝置的Profile,一個項目可以适配任意多的硬體和平台。不同的Device Profile的選擇依靠不同平台的Selector,安卓上可以根據正規表達式或者嚴格比對等方案去比對SoC,GPU Family,Device Module或者GL Version等。

再來我們看下項目Iterating的步驟,資料轉換過程我們叫做Cook,cook分為兩種方式,一種是你裝置跑起來的時候,裝置上是沒有資源的,裝置的資源通路不是通路本地,而是通路網絡磁盤,編輯器的一個commandlet會作為server端持續提供你要通路的資料,這個資料如果沒有經過轉換會先阻塞的cook完再發過去,疊代的時候非常有用,叫cook on the fly。還有一個是把資源全部轉化完發到手機上,在不-iterate時,即使資源不改,也會先都load出來再save回去做檢查。項目大了會用很久,如果資源變化了,在DDC(Derived Data Cache)中找不到,需要發生資源轉換的過程,則會更慢。當用了-iterate後就會跳過這個步驟,但是有時候依然會load+save,是因為ini檔案發生了變動,引擎不知道這個變動會不會影響cook結果,隻能重新load/save,這時候引擎有一些優化選項,可以讓你配置一些特殊的字段告訴引擎,當這些字段發生變化時cook也會不做檢查,例如項目版本号之類的字段。當疊代測試的時候隻要改變啟動指令行參數的時候,可以push一個UE4Commandline.txt檔案到裝置上,就可以免除重新打包的時間。

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

Profiling方面,gpu上細節的profiling主要靠移動gpu廠商工具;另外引擎有大量的内建的工具,例如常用的stat系列的指令以及showflag系列指令可以快速幫忙定位問題,cpu的profiling,引擎有自帶的工具,近期還加入了第三方工具framepro的支援,可以以很小的overhead做基于namedevent的profiling。我們也正在和騰訊合作,在做一些新的Profiling工具供大家使用。關于記憶體的profiling,引擎也有一些Memreport和llm的指令和對應的Memory Profiler工具輔助檢查記憶體的使用狀況,以及查找記憶體洩露和優化的方案。

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