版權聲明:本文采用國際知識共享“署名-非商業使用-禁止演繹”協議4.0進行授權許可。轉載請註明作者姓名和文章出處。
喜歡我的博客請記住我的名字:秦元培,我的博客地址是:http://qinyuanpei.com
轉載請註明出處,本文作者:秦元培, 本文出處:http://blog.csdn.net/qinyuanpei/article/details/48262583
各位朋友大家好,我是秦元培,歡迎大家關注我的博客,我的博客地址是http://qinyuanpei.com。
最近開始研究Unity3D遊戲場景優化,每次提及遊戲優化這個話題的時候,我的腦海中都會浮現出《仙劍奇俠傳六》這個讓四路泰坦都光榮隕落的神奇遊戲,作爲一個使用Unity3D引擎進行遊戲開發的仙劍玩家,我曾經天真的以爲,這款使用Unity3D引擎打造的仙劍二十週年獻禮之作,會讓我對《仙劍奇俠傳》這個系列遊戲的未來充滿更多期待,然而當遊戲真正呈現在我眼前的時候,我感受到了在歷代仙劍遊戲中從未有過的尷尬和失望,我尷尬的是Unity3D這樣一個比較強大的遊戲引擎硬生生地被北軟玩成了這個鬼樣子,我失望的是這部遊戲除了劇情和跳跳樂以外並沒有什麼讓人看到希望的東西。
我知道我這樣說會有一堆仙劍玩家指責我說,仙劍本來就是玩劇情的嘛,所以只要劇情好其它的都可以原諒啦。然而我們每一個人都清楚《仙劍奇俠傳》是一個RPG遊戲,它不是每隔三年出一次新番的GAL動漫、不是每隔三年更新一次的言情小說、更不是每隔三年播放一次的偶像電影。兩年前的今天我可以耐着性子玩通關《仙劍奇俠傳五》,但是這一次我真的玩不下去了。當一個遊戲因爲優化問題而獲得《仙劍奇俠傳六:泰坦隕落》稱號的時候,作爲一個玩家我真的不想再爲這個遊戲洗白什麼,雖然我曾經深愛過這個遊戲。所以言歸正傳,作爲一個程序員,我們還是來做點程序員該做的事情,那麼我們今天說什麼呢,我們來說說Unity3D裏的批處理!
一、什麼是批處理?
我們知道Unity3D在屏幕上繪製一個圖形本質上調用OpneGL或者DirectX這樣的API,因此在這個過程中會產生一定程度上的性能消耗。DrawCall是OpenGL中描述繪製次數的一個量,例如一個基本的OpenGL繪製流程是設置顏色->繪圖方式->頂點座標->繪製->結束,在繪製的過程中每幀都會重複這個過程,這就是一次DrawCall,所以當遊戲中的繪製過程變得複雜的時候,就會帶來DrawCall的急劇增加,進而帶來遊戲的性能問題,反映到遊戲表現上就變成了優化問題。那麼在Unity3D中採取了什麼樣的措施來降低DrawCall呢?這就是我們今天要說的批處理,換句話說Unity3D使用了批處理來達到降低DrawCall的目的,批處理希望通過對物體網格的重組來獲得更高的繪製效率,試想以下如果將多個物體合併爲一個物體,那麼在繪製的時候只需要繪製一次就夠了,因此從這個角度上來講這樣做肯定是可以降低DrawCall的,更深刻的一種理解是這裏體現了一種資源循環調用的思想,接觸過Android開發的朋友們一定知道ListView控件可以對其元素進行“緩存”從而提高效率,因爲我們可以發現其實ListView是對列表項進行某種程度上的“複用”從而提高了效率,在Unity3D這裏同樣遵循了這個原理。在Unity3D中進行批處理的一個前提是相同材質的物體可以被合併,如果這些物體使用不同的材質,那麼當我們把這些材質對應的紋理打成“圖集”以後可以對其進行合併,並且在合併的時候應該是用Renderer.sharedMaterial 而非 Renderer.material以保證材質是可以共享的。關於DrawCall的相關細節大家從這裏來了解,博主並未對圖形學領域有過深入的研究,因此就不在這裏班門弄斧了啊,哈哈!
二、Unity3D中批處理的兩種方式
在Unity3D中有靜態批處理和動態批處理兩種方式,下面我們就來分別說說這兩種不同的批處理方式!
靜態批處理
靜態批處理其實大家都是知道的。爲什麼這樣說呢?因爲我們在使用Unity3D的過程中無形中培養了這樣一個習慣,那就是將場景中相對來說“靜態”的物體都勾選Static選項,這在Unity3D中稱爲Static GameObjects,並且因爲這一特性和Lightmapping、Navigation、Off-meshLinks、ReflectionProbe、Occluder and Occludee等內容均有着密切的聯繫,因此說靜態批處理大家都是知道的其實一點都爲過,和場景優化相關的內容博主會在後續的博客中涉及,希望大家能及時關注我的博客更新。靜態批處理允許遊戲引擎儘可能多的去降低繪製任意大小的物體所產生的DrawCall,它會佔用更多的內存資源和更少的CPU資源,因爲它需要額外的內存資源來存儲合併後的幾何結構,如果在靜態批處理之前,如果有幾個對象共享相同的幾何結構,那麼將爲每個對象創建一個幾何圖形,無論是在編輯器還是在運行時。這看起來是個艱難的選擇,你需要在內存性能和渲染性能間做出最爲正確的選擇。在內部,靜態批處理是通過將靜態對象轉換爲世界空間,併爲它們構建一個大的頂點+索引緩衝區。然後,在同一批中,一系列的“便宜”畫調用,一系列的“便宜”,幾乎沒有任何狀態變化之間的。所以在技術上它並不保存“三維的調用”,但它可以節省它們之間的狀態變化(這是昂貴的部分)。使用靜態批處理非常簡單啦,只要勾選物體的Static選項即可!
動態批處理
相對靜態批處理而言,動態批處理的要求更爲嚴格一些,它要求批處理的動態對象具有一定的頂點,所以動態批處理只適用於包含小於900個頂點屬性的網格。如果你的着色器使用頂點位置,法線和單光,然後你可以批處理300個頂點的動態對象;而如果你的着色器使用頂點位置,法線,uv0,UV1和切線,那麼只能處理180個頂點的動態對象。接下來最爲重要的一點,如果動態對象使用的是不同的材質,那麼即使進行了動態批處理從效率上來講並不會有太大的提升。如果動態對象採用的是多維子材質,那麼批處理是無效的。如果動態對象接收實時光影,同樣批處理是無效的。下面展示的是一個將多個物體合併爲一個物體的腳本示例:
[MenuItem("ModelTools/將多個物體合併爲一個物體")]
static void CombineMeshs2()
{
//在編輯器下選中的所有物體
object[] objs=Selection.gameObjects;
if(objs.Length<=0) return;
//網格信息數組
MeshFilter[] meshFilters =new MeshFilter[objs.Length];
//渲染器數組
MeshRenderer[] meshRenderers = new MeshRenderer[objs.Length];
//合併實例數組
CombineInstance[] combines = new CombineInstance[objs.Length];
//材質數組
Material[] mats = new Material[objs.Length];
for (int i = 0; i < objs.Length; i++)
{
//獲取網格信息
meshFilters[i]=((GameObject)objs[i]).GetComponent<MeshFilter>();
//獲取渲染器
meshRenderers[i]=((GameObject)objs[i]).GetComponent<MeshRenderer>();
//獲取材質
mats[i] = meshRenderers[i].sharedMaterial;
//合併實例
combines[i].mesh = meshFilters[i].sharedMesh;
combines[i].transform = meshFilters[i].transform.localToWorldMatrix;
}
//創建新物體
GameObject go = new GameObject();
go.name = "CombinedMesh_" + ((GameObject)objs[0]).name;
//設置網格信息
MeshFilter filter = go.transform.GetComponent<MeshFilter>();
if (filter == null)
filter = go.AddComponent<MeshFilter>();
filter.sharedMesh = new Mesh();
filter.sharedMesh.CombineMeshes(combines,false);
//設置渲染器
MeshRenderer render = go.transform.GetComponent<MeshRenderer>();
if (render == null)
render = go.AddComponent<MeshRenderer>();
//設置材質
render.sharedMaterials = mats;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
這段腳本的核心是CombineMeshes()方法,該方法有三個參數,第一個參數是合併實例的數組,第二個參數是是否對子物體的網格進行合併,第三個參數是是否共享材質,如果希望物體共享材質則第三個參數爲true,否則爲false。在我測試的過程中發現,如果選擇了對子物體的網格進行合併,那麼每個子物體都不能再使用單獨的材質,默認會以第一個材質作爲合併後物體的材質,下面演示的是合併前的多個物體和合並後的一個物體的對比:
三、批處理效率分析
那麼批處理對遊戲效率提升究竟有怎樣的作用呢?我們來看下面幾組測試對比:
1、三個不同的物體使用同一種材質,不做靜態批處理,不做動態批處理:DrawCall爲4、面數爲584、頂點數爲641
2、三個不同的物體使用同一種材質,只做靜態批處理,不做動態批處理:DrawCall爲2、面數爲584、頂點數爲641
3、三個不同的物體使用不同的材質,不做靜態批處理,不做動態批處理:DrawCall爲4、面數爲584、頂點數爲641
4、三個不同的物體使用不同的材質,只做靜態批處理,不做動態批處理:DrawCall爲4、面數爲584、頂點數爲641
5、三個不同的物體使用不同的材質,不做靜態批處理,只做動態批處理:DrawCall爲4、面數爲584、頂點數爲641
6、三個不同的物體使用不同的材質,做靜態批處理,做動態批處理:DrawCall爲4、面數爲584、頂點數爲641
7、三個不同的物體使用同一種材質,不做靜態批處理,只做動態批處理::DrawCall爲4、面數爲584、頂點數爲641
大家可以注意到各組測試結果中,只有第二組的DrawCall降低,這說明只有當不同的物體使用同一種材質時通過批處理可以從一定程度上降低DrawCall,即我們在文章開始提到的儘可能地保證材質共享。昨天下午興沖沖地將遊戲場景裏的某些物體進行了動態批處理,但是實際測試的過程中發現DrawCall非常地不穩定,但是在場景中的某些地方DrawCall卻可以降得非常低,如果靜態批處理和動態批處理都不能對場景產生較好的優化,那麼Unity3D遊戲場景的優化究竟要從哪裏抓起呢?我覺得這是我們每一個人都該用心去探索的地方,畢竟遊戲做出來首先要保證能讓玩家流暢的玩下去吧,一味的強調引擎、強調畫面,卻時常忽略引擎使用者的主觀能動性,希望把一切問題都交給引擎去解決,這樣的思路是錯誤而落後的,仙劍六的問題完全是用不用心的問題,我常常看到有人在公開場合說仙劍以後要換虛幻三,其實按照北軟現在這樣的狀態,給他們一個虛幻四也不過是然並卵。我在知乎上看到了號稱15歲就開發次時代遊戲的高中生妹子,做出個能稱爲DEMO的遊戲就覺得自己可以搞引擎了,更有甚者隨便用DirectX或者OpenGL封裝若干函數就敢說自己會做遊戲引擎了,呵呵,你確定你的遊戲能在別人的電腦或者手機上運行起來嗎?優化的重要性可見一斑。
四、小結
好了,通過今天這篇文章,我們可以整理出以下觀點:
1、如果不同的物體間共享材質,則可以直接通過靜態批處理降低DrawCall
2、動態批處理並不能降低DrawCall、面數和頂點數(我不會告訴你我昨天傻呵呵地合併了好多場景中的模型,結果面數和頂點數並沒有降下來,23333)
3、不管是靜態批處理還是動態批處理都會影響Culiing,這同樣是涉及到場景優化的一個概念,好吧,爲了讓場景的DrawCall降下來我最近可能要研究好多涉及的優化的內容……
那麼今天的內容就是這樣子了,希望對大家學習Unity3D有所幫助,歡迎大家和我交流這些問題來相互促進,畢竟這纔是我寫博客最初的目的嘛,哈哈!
喜歡我的博客請記住我的名字:秦元培,我的博客地址是:http://qinyuanpei.com
轉載請註明出處,本文作者:秦元培, 本文出處:http://blog.csdn.net/qinyuanpei/article/details/48262583