Unity3D遊戲遊戲化之DrawCall優化過程分析詳解

在最近,使用U3D開發的遊戲核心部分功能即將完成,中間由於各種歷史原因,導致項目存在比較大的問題,這些問題在最後,恐怕只能通過一次徹底的重構來解決

現在的遊戲跑起來會有接近130-170個左右的DrawCall,遊戲運行起來明顯感覺到卡,而經過一天的優化,DrawCall成功縮減到30-70個,這個效果是非常顯著的,並且這個優化並沒有通過將現有的資源打包圖集來實現,圖集都是原有的圖集,如果從全局的角度對圖集再進行一次優化,那麼DrawCall還可以再減少十幾個

本次優化的重點包括:層級關係和特效

對於U3D,我是一個菜鳥,對於U3D的一些東西是一知半解,例如DrawCall,我得到的是一些並不完全正確的信息,例如將N個紋理打包成一個圖集,這個圖集就只會產生一個DrawCall,如果不打成圖集,那麼就會有N個DrawCall,這個觀點在很多人的認識裏都是正確的,因爲可以通過簡單的操作來驗證,但嚴格來說,這個觀點是錯誤的,因爲它還受層級關係影響!

渲染順序

U3D的渲染是有順序的,U3D的渲染順序是由我們控制的,控制好U3D的渲染順序,你才能控制好DrawCall

一個DrawCall,表示U3D使用這個材質/紋理,來進行一次渲染,那麼這次渲染假設有3個對象,那麼當3個對象都使用這一個材質/紋理的時候,就會產生一次DrawCall,可以理解爲一次將紋理輸送到屏幕上的過程,(實際上引擎大多會使用如雙緩衝,緩存這類的手段來優化這個過程,但在這裏我們只需要這樣子認識就可以了),假設3個對象使用不同的材質/紋理,那麼無疑會產生3個DrawCall

接下來我們的3個對象使用2個材質,A和B使用材質1,C使用材質2,這時候來看,應該是有2個DrawCall,或者3個DrawCall。應該是2個DrawCall啊,爲什麼會有3個DrawCall???而且是有時候2個,有時候3個。我們按照上面的DrawCall分析流程來分析一下:

1.渲染A,使用材質1
2.渲染B,使用材質1
3.渲染C,使用材質2

在這種情況下是2個DrawCall,在下面這種情況下,則是3個DrawCall

1.渲染A,使用材質1
2.渲染C,使用材質2
3.渲染B,使用材質1

因爲我們沒有控制好渲染順序(或者說沒有去特意控制),所以導致了額外的DrawCall,因爲A和B不是一次性渲染完的,而是被C打斷了,所以導致材質1被分爲兩次渲染

那麼是什麼在控制這個渲染順序呢?首先在多個相機的情況下,U3D會根據相機的深度順序進行渲染,在每個相機中,它會根據你距離相機的距離,由遠到近進行渲染,在UI相機中,還會根據你UI對象的深度進行渲染

那麼我們要做的就是,對要渲染的對象進行一次規劃,正確地排列好它們,規則是,按照Z軸或者深度,對空間進行劃分,然後確定好每個對象的Z軸和深度,讓使用同一個材質的東西,儘量保持在這個空間內,不要讓其他材質的對象進入這個空間,否則就會打斷這個空間的渲染順序

在這個基礎上,更細的規則有:

  • 場景中的東西,我們使用Z軸來進行空間的劃分,例如背景層,特效層1,人物層,特效層2
  • NGUI中的東西,我們統一使用Depth來進行空間的劃分
  • 人物模型,當人物模型只是用一個材質,DrawCall只有1,但是用了2個以上的材質,DrawCall就會暴增(或許對材質的RenderQueue進行規劃也可以使DrawCall只有2個,但這個要拆分好才行),3D人物處於複雜3D場景中的時候,我們的空間規則難免被破壞,這只能在設計的時候儘量去避免這種情況了
  • 使用了多個材質的特效,在動畫的過程中,往往會引起DrawCall的波動,在視覺效果可以接受的範圍內,可以將特效也進行空間劃分,假設這個特效是2D顯示,那麼可以使用Z軸來劃分空間

打包圖集

每個材質/紋理的渲染一定是會產生DrawCall的,這個DrawCall只能通過打包圖集來進行優化

製作圖集一般遵循幾個規則:

  • 從功能角度進行劃分,例如UI可以劃分爲公共部分,以及每個具體的界面,功能上,顯示上密切相關的圖片打包到一起
  • 不要一股腦把所有東西打包到一個圖集裏,特別是那些不可能同時出現的東西,它們就不應該在一個圖集裏,這樣的圖集意義不大,減少不了DrawCall,並且一個你不需要顯示的圖片,會一直佔用你的內存,這讓我非常不爽
  • 注意控制圖集的大小,不要讓圖集太大,一個超級大圖集的DrawCall消耗或許頂的上十幾個小圖集的消耗

字符圖集,在使用BMFont或者其他工具生成圖片字的時候,我們往往是直接導入一大串文字,然後直接生成圖片,但實際上這上面的操作也有優化空間,例如BMFont生成的圖片大小,是可以設置的,有兩個規則,一個規則是導出的圖片儘量小,另一個是導出的圖片儘量少,默認的大小應該是512×512,假設你生成的圖片256×256就可以容納,那麼多做一個操作你可以節省這麼多空間,另外當你輸入多幾個字,就導致增加一張圖片時,例如1024變成2048,那麼你可以考慮使用3張512的圖片,這樣也會節省空間

經過精心劃分的圖集在加上精心規劃的渲染順序,DrawCall會有一個質的優化

特效清理

U3D提供了非常便捷的方法讓我們很輕易地使用美術給過來的特效,懶惰的U3D程序猿會直接放入U3D,甚至不去看這是個什麼特效,我們的特效一般都是一瞬間的事情,例如技能特效,或者其他什麼特效,那麼特效播放完,這個特效我們就看不到了,但假設這個特效在播放結束的時候,沒有將自身的Active屬性設置爲false,那麼它就會繼續佔用你的DrawCall,消耗你設備的計算能力,所以程序需要保證當一個特效播放完之後,能夠被消耗,或者設置爲非激活的狀態,可以使用一些公共方法來完成特效播放完之後的清理工作(自己實現2個靜態函數,一個播放完銷燬,一個播放完設置未激活)


在屏幕上渲染物體,引擎需要發出一個繪製調用來訪問圖形API(iOS系統中爲OpenGL ES)。每個繪製調用需要進行大量的工作來訪問圖形API,從而導致了CPU方面顯著的性能開銷。
 
Unity在運行時可以將一些物體進行合併,從而用一個繪製調用來渲染他們。這一操作,我們稱之爲“批處理”。一般來說,Unity批處理的物體越多,你就會得到越好的渲染性能。
 
Unity中內建的批處理機制所達到的效果要明顯強於使用幾何建模工具(或使用Standard Assets包中的CombineChildren腳本)的批處理效果。這是因爲,Unity引擎的批處理操作是在物體的可視裁剪操作之後進行的。Unity先對每個物體進行裁剪,然後再進行批處理,這樣可以使渲染的幾何總量在批處理前後保持不變。但是,使用幾何建模工具來拼合物體,會妨礙引擎對其進行有效的裁剪操作,從而導致引擎需要渲染更多的幾何面片。
 
材質
只有擁有相同材質的物體纔可以進行批處理。因此,如果你想要得到良好的批處理效果,你需要在程序中儘可能地複用材質和物體。
 
如果你的兩個材質僅僅是紋理不同,那麼你可以通過 紋理拼合 操作來將這兩張紋理拼合成一張大的紋理。一旦紋理拼合在一起,你就可以使用這個單一材質來替代之前的兩個材質了。
 
如果你需要通過腳本來訪問複用材質屬性,那麼值得注意的是改變Renderer.material將會造成一份材質的拷貝。因此,你應該使用Renderer.sharedMaterial來保證材質的共享狀態。
 

動態批處理
如果動態物體共用着相同的材質,那麼Unity會自動對這些物體進行批處理。
動態批處理操作是自動完成的,並不需要你進行額外的操作。
 
Tips:
提醒:
1、       批處理動態物體需要在每個頂點上進行一定的開銷,所以動態批處理僅支持小於900頂點的網格物體。
 
2、     如果你的着色器使用頂點位置,法線和UV值三種屬性,那麼你只能批處理300頂點以下的物體;如果你的着色器需要使用頂點位置,法線,UV0,UV1和切向量,那你只
            能批處理180頂點以下的物體。
            請注意:屬性數量的限制可能會在將來進行改變。
 
4、      不要使用縮放尺度(scale)。分別擁有縮放尺度(1,1,1)和(2,2,2)的兩個物體將不會進行批處理。
 
5、      統一縮放尺度的物體不會與非統一縮放尺度的物體進行批處理。
          使用縮放尺度(1,1,1)和 (1,2,1)的兩個物體將不會進行批處理,但是使用縮放尺度(1,2,1)和(1,3,1)的兩個物體將可以進行批處理。
 
6、      使用不同材質的實例化物體(instance)將會導致批處理失敗。
 
7、      擁有lightmap的物體含有額外(隱藏)的材質屬性,比如:lightmap的偏移和縮放係數等。所以,擁有lightmap的物體將不會進行批處理(除非他們指向lightmap的同一
           部分)。
 
8、     多通道的shader會妨礙批處理操作。比如,幾乎unity中所有的着色器在前向渲染中都支持多個光源,併爲它們有效地開闢多個通道。
 
9、      預設體的實例會自動地使用相同的網格模型和材質。
 
Static Batching
靜態批處理
 
相對而言,靜態批處理操作允許引擎對任意大小的幾何物體進行批處理操作來降低繪製調用(只要這些物體不移動,並且擁有相同的材質)。因此,靜態批處理比動態批處理更加有效,你應該儘量低使用它,因爲它需要更少的CPU開銷。
 
爲了更好地使用靜態批處理,你需要明確指出哪些物體是靜止的,並且在遊戲中永遠不會移動、旋轉和縮放。想完成這一步,你只需要在檢測器(Inspector)中將Static複選框打勾即可,如下圖所示:

 
使用靜態批處理操作需要額外的內存開銷來儲存合併後的幾何數據。在靜態批處理之前,如果一些物體共用了同樣的幾何數據,那麼引擎會在編輯以及運行狀態對每個物體創建一個幾何數據的備份。這並不總是一個好的想法,因爲有時候,你將不得不犧牲一點渲染性能來防止一些物體的靜態批處理,從而保持較少的內存開銷。比如,將濃密森裏中樹設爲Static,會導致嚴重的內存開銷。
 
靜態批處理目前只支持Unity iOS Advanced。


********************************************************************************************************************************************************************

unity3D 對於移動平臺的支持無可厚非,但是也有時候用Unity3D 開發出來的應用、遊戲在移動終端上的運行有着明顯的效率問題,比如卡、畫質等各種問題。自己在做遊戲開發的時候偶有所得。對於主要影響性能的因素做個總結。

主要因素有:

        1.      Saved by batching 值過大   ---- > 這個值主要是針對Mesh的批處理,這個值越高,應用就越卡   

        2.     Drawcall 值過大 ---- >  Drawcall 值過大,所需要的 GPU 的處理性能較高,從而導致CPU的計算時間過長,於是就卡了

        3.     點、面過多           ---- > 點、面過多,GPU 根據不同面的效果展開計算,並且CPU計算的數據也多,所以效果出來了,但是卡巴斯基

由於 Saved by batching 和 Drawcall 值過大所引起的卡的問題我所做的優化方式有:

        1.    對於模型 :Mesh 合併,有個不錯的插件(DrawCall Minimizer   --->  直接上Asset Store 下載即可,免費的,而且有文檔,很容易上手)

        2.    對於UI  :  儘量避免使用Unity3D自帶的 GUI 換用 NGUI或者EZGUI;因爲這兩個UI插件對於UI中的圖片處理是將UI圖片放置在一個 Atlas 中,一個 Atlas 對應一個Drawcall

        3.   對於燈光: 可以使用 Unity3D 自帶的  Lightmapping 插件來烘焙場景中的燈光效果到物體材質上 

        4.  對於場景: 可以使用 Unity3D 自帶的 Occlusion Culling 插件把靜止不動的場景元素烘焙出來

        4.   對於特效:儘量把材質紋理合並

對於Unity3D 在移動終端上支持的Drawcall 數到底多少,主要是跟機子性能有關的,當然也不是說值小性能就一定沒問題(本人親測,也有17就卡的,主要是模型材質紋理過大所引起的),目前我做的是70左右的,還OK,挺正常的

 

由於點、面過多所導致的性能問題,最好用簡模,用四面體來做複雜的模型,但是面、點也別太多,至於Unity3D 到底支持多少點、面的說法各異,我也搞不懂,總之少些肯定OK

 

 

 

檢測方式:

一,Unity3D 渲染統計窗口

Game視窗的Stats去查看渲染統計的信息:

1、FPS

fps其實就是 frames per second,也就是每一秒遊戲執行的幀數,這個數值越小,說明遊戲越卡。

 

2、Draw calls

batching之後渲染mesh的數量,和當前渲染到的網格的材質球數量有關。

 

3、Saved by batching 

渲染的批處理數量,這是引擎將多個對象的繪製進行合併從而減少GPU的開銷;

很多GUI插件的一個好處就是合併多個對象的渲染,從而降低DrawCalls ,保證遊戲幀數。

 

4、Tris 當前繪製的三角面數

 

5、Verts 當前繪製的頂點數

 

6、Used Textures 當前幀用於渲染的圖片佔用內存大小

 

7、Render Textures 渲染的圖片佔用內存大小,也就是當然渲染的物體的材質上的紋理總內存佔用

 

8、VRAM usage 顯存的使用情況,VRAM總大小取決於你的顯卡的顯存

 

9、VBO Total 渲染過程中上載到圖形卡的網格的數量,這裏注意一點就是縮放的物體可能需要額外的開銷。

 

10、Visible Skinned Meshes 蒙皮網格的渲染數量

 

11、Animations 播放動畫的數量

注意事項:

1,運行時儘量減少 Tris 和 Draw Calls

預覽的時候,可點開 Stats,查看圖形渲染的開銷情況。特別注意 Tris 和 Draw Calls 這兩個參數。

一般來說,要做到:

Tris 保持在 7.5k 以下,有待考證。

Draw Calls 保持在 20 以下,有待考證。

2,FPS,每一秒遊戲執行的幀數,這個數值越小,說明遊戲越卡。

3,Render Textures 渲染的圖片佔用內存大小。

4,VRAM usage 顯存的使用情況,VRAM總大小取決於你的顯卡的顯存。

 

二,代碼優化

1. 儘量避免每幀處理

比如:

function Update() { DoSomeThing(); }

可改爲每5幀處理一次:

function Update() { if(Time.frameCount % 5 == 0) { DoSomeThing(); } }

2. 定時重複處理用 InvokeRepeating 函數實現

比如,啓動0.5秒後每隔1秒執行一次 DoSomeThing 函數:

 

function Start() { InvokeRepeating("DoSomeThing", 0.5, 1.0); }

 

3. 優化 Update, FixedUpdate, LateUpdate 等每幀處理的函數

函數裏面的變量儘量在頭部聲明。

比如:

function Update() { var pos: Vector3 = transform.position; }

可改爲

private var pos: Vector3; function Update(){ pos = transform.position; }

 

4. 主動回收垃圾

給某個 GameObject 綁上以下的代碼:

function Update() { if(Time.frameCount % 50 == 0) { System.GC.Collect(); } }

 

5. 優化數學計算

比如,如果可以避免使用浮點型(float),儘量使用整形(int),儘量少用複雜的數學函數比如 Sin 和 Cos 等等

 

6,減少固定增量時間

將固定增量時間值設定在0.04-0.067區間(即,每秒15-25幀)。您可以通過Edit->Project Settings->Time來改變這個值。這樣做降低了FixedUpdate函數被調用的頻率以及物理引擎執行碰撞檢測與剛體更新的頻率。如果您使用了較低的固定增量時間,並且在主角身上使用了剛體部件,那麼您可以啓用插值辦法來平滑剛體組件。

7,減少GetComponent的調用

使用 GetComponent或內置組件訪問器會產生明顯的開銷。您可以通過一次獲取組件的引用來避免開銷,並將該引用分配給一個變量(有時稱爲"緩存"的引用)。例如,如果您使用如下的代碼:

function Update () {

transform.Translate(0, 1, 0);

 

}

通過下面的更改您將獲得更好的性能:

 

var myTransform : Transform;

function Awake () {

myTransform = transform;

}

function Update () {

myTransform.Translate(0, 1, 0);

}

 

8,避免分配內存

您應該避免分配新對象,除非你真的需要,因爲他們不再在使用時,會增加垃圾回收系統的開銷。您可以經常重複使用數組和其他對象,而不是分配新的數組或對象。這樣做好處則是儘量減少垃圾的回收工作。同時,在某些可能的情況下,您也可以使用結構(struct)來代替類(class)。這是因爲,結構變量主要存放在棧區而非堆區。因爲棧的分配較快,並且不調用垃圾回收操作,所以當結構變量比較小時可以提升程序的運行性能。但是當結構體較大時,雖然它仍可避免分配/回收的開銷,而它由於"傳值"操作也會導致單獨的開銷,實際上它可能比等效對象類的效率還要低。

 

9,使用iOS腳本調用優化功能

UnityEngine 命名空間中的函數的大多數是在 C/c + +中實現的。從Mono的腳本調用 C/C++函數也存在着一定的性能開銷。您可以使用iOS腳本調用優化功能(菜單:Edit->Project Settings->Player)讓每幀節省1-4毫秒。此設置的選項有:

Slow and Safe – Mono內部默認的處理異常的調用

 

Fast and Exceptions Unsupported –一個快速執行的Mono內部調用。不過,它並不支持異常,因此應謹慎使用。它對於不需要顯式地處理異常(也不需要對異常進行處理)的應用程序來說,是一個理想的候選項。

 

10,

優化垃圾回收

 

如上文所述,您應該儘量避免分配操作。但是,考慮到它們是不能完全杜絕的,所以我們提供兩種方法來讓您儘量減少它們在遊戲運行時的使用:

如果堆比較小,則進行快速而頻繁的垃圾回收

這一策略比較適合運行時間較長的遊戲,其中幀率是否平滑過渡是主要的考慮因素。像這樣的遊戲通常會頻繁地分配小塊內存,但這些小塊內存只是暫時地被使用。如果在iOS系統上使用該策略,那麼一個典型的堆大小是大約 200 KB,這樣在iPhone 3G設備上,垃圾回收操作將耗時大約 5毫秒。如果堆大小增加到1 MB時,該回收操作將耗時大約 7ms。因此,在普通幀的間隔期進行垃圾回收有時候是一個不錯的選擇。通常,這種做法會讓回收操作執行的更加頻繁(有些回收操作並不是嚴格必須進行的),但它們可以快速處理並且對遊戲的影響很小:

if (Time.frameCount % 30 == 0)

{

System.GC.Collect();

}

 

但是,您應該小心地使用這種技術,並且通過檢查Profiler來確保這種操作確實可以降低您遊戲的垃圾回收時間

如果堆比較大,則進行緩慢且不頻繁的垃圾回收

這一策略適合於那些內存分配 (和回收)相對不頻繁,並且可以在遊戲停頓期間進行處理的遊戲。如果堆足夠大,但還沒有大到被系統關掉的話,這種方法是比較適用的。但是,Mono運行時會儘可能地避免堆的自動擴大。因此,您需要通過在啓動過程中預分配一些空間來手動擴展堆(ie,你實例化一個純粹影響內存管理器分配的"無用"對象):

 

function Start() {

 

var tmp = new System.Object[1024];

 

// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks

 

for (var i : int = 0; i < 1024; i++)

 

tmp[i] = new byte[1024];

 

// release reference

 

tmp = null;

 

}

 

遊戲中的暫停是用來對堆內存進行回收,而一個足夠大的堆應該不會在遊戲的暫停與暫停之間被完全佔滿。所以,當這種遊戲暫停發生時,您可以顯式請求一次垃圾回收:

 

System.GC.Collect();

 

另外,您應該謹慎地使用這一策略並時刻關注Profiler的統計結果,而不是假定它已經達到了您想要的效果。

 

三,模型

1,壓縮 Mesh

導入 3D 模型之後,在不影響顯示效果的前提下,最好打開 Mesh Compression。

Off, Low, Medium, High 這幾個選項,可酌情選取。

2,避免大量使用 Unity 自帶的 Sphere 等內建 Mesh

Unity 內建的 Mesh,多邊形的數量比較大,如果物體不要求特別圓滑,可導入其他的簡單3D模型代替。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章