2018TGDC王禰:UE4製作多人大地型遊戲的優化
在2018TGDC大會上,來自Epic Games中國資深技術工程師王禰先生髮表了《UE4製作多人大地型遊戲的優化》主題演講。王禰有近10年的虛幻引擎使用經驗,從console遊戲、掌機到PC端MMO遊戲,再到手遊,都有過相關開發經驗;現在Epic Games China,他作為唯一的引擎技術專家,參與和幫助了眾多使用UE3和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
Physics
另外我還嘗試過把同樣mesh的不同例項物件用Shared shapes減少註冊的物理物件的記憶體開銷,在記憶體敏感的場景下也可以嘗試。還有一個思路是我們可以把物理物件和視覺物件解耦,預設的情況下,引擎的Mesh物件開啟碰撞就會註冊物理物件到PhysX Scene,增加了物理場景的複雜度和物理的記憶體佔用。因此當你的Mesh載入到記憶體裡,即使不被渲染出來,這些開銷就在了,但是其實很多情況下視覺會看得更遠一些,實際需要物理計算互動的距離在有些遊戲中沒那麼遠,我們可以用一些手段把視覺上物件的物理關掉,把這個物理屬性轉到一些新的Component和Actor上面放到新的Streaming Level裡,用更近的載入解除安裝距離來管理,這樣實際的物理場景複雜度和記憶體佔用都會小很多。另外移動端的布料,計算量和網格數量相關,在移動端會不太推薦使用那麼複雜的模擬,引擎也就沒有提供移動端的NvCloth的lib,所以我們一般會用剛體來模擬。
Ticking,
引擎還有個功能較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工具輔助檢查記憶體的使用狀況,以及查詢記憶體洩露和優化的方案。
今天要講的就是這些,謝謝大家。