unity3d优化收集摘录
Unity3D 對(duì)于移動(dòng)平臺(tái)的支持無(wú)可厚非,但是也有時(shí)候用Unity3D 開發(fā)出來(lái)的應(yīng)用、游戲在移動(dòng)終端上的運(yùn)行有著明顯的效率問(wèn)題,比如卡、畫質(zhì)等各種問(wèn)題。
影響因素:
優(yōu)化方式:
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Unity(或者說(shuō)基本所有圖形引擎)生成一幀畫面的處理過(guò)程大致可以這樣簡(jiǎn)化描述:引擎首先經(jīng)過(guò)簡(jiǎn)單的可見性測(cè)試,確定攝像機(jī)可以看到的物體,然后把這些物體的頂點(diǎn)(包括本地位置、法線、UV等),索引(頂點(diǎn)如何組成三角形),變換(就是物體的位置、旋轉(zhuǎn)、縮放、以及攝像機(jī)位置等),相關(guān)光源,紋理,渲染方式(由材質(zhì)/Shader決定)等數(shù)據(jù)準(zhǔn)備好,然后通知圖形API——或者就簡(jiǎn)單地看作是通知GPU——開始繪制,GPU基于這些數(shù)據(jù),經(jīng)過(guò)一系列運(yùn)算,在屏幕上畫出成千上萬(wàn)的三角形,最終構(gòu)成一幅圖像。
在Unity中,每次引擎準(zhǔn)備數(shù)據(jù)并通知GPU的過(guò)程稱為一次Draw Call。這一過(guò)程是逐個(gè)物體進(jìn)行的,對(duì)于每個(gè)物體,不只GPU的渲染,引擎重新設(shè)置材質(zhì)/Shader也是一項(xiàng)非常耗時(shí)的操作。因此每幀的Draw Call次數(shù)是一項(xiàng)非常重要的性能指標(biāo),對(duì)于iOS來(lái)說(shuō)應(yīng)盡量控制在20次以內(nèi),這個(gè)值可以在編輯器的Statistic窗口看到。
Unity內(nèi)置了Draw Call Batching技術(shù),從名字就可以看出,它的主要目標(biāo)就是在一次Draw Call中批量處理多個(gè)物體。只要物體的變換和材質(zhì)相同,GPU就可以按完全相同的方式進(jìn)行處理,即可以把它們放在一個(gè)Draw Call中。Draw Call Batching技術(shù)的核心就是在可見性測(cè)試之后,檢查所有要繪制的物體的材質(zhì),把相同材質(zhì)的分為一組(一個(gè)Batch),然后把它們組合成一個(gè)物體(統(tǒng)一變換),這樣就可以在一個(gè)Draw Call中處理多個(gè)物體了(實(shí)際上是組合后的一個(gè)物體)。
但Draw Call Batching存在一個(gè)缺陷,就是它需要把一個(gè)Batch中的所有物體組合到一起,相當(dāng)于創(chuàng)建了一個(gè)與這些物體加起來(lái)一樣大的物體,與此同時(shí)就需要分配相應(yīng)大小的內(nèi)存。這不僅會(huì)消耗更多內(nèi)存,還需要消耗CPU時(shí)間。特別是對(duì)于移動(dòng)的物體,每一幀都得重新進(jìn)行組合,這就需要進(jìn)行一些權(quán)衡,否則得不償失。但對(duì)于靜止不動(dòng)的物體來(lái)說(shuō),只需要進(jìn)行一次組合,之后就可以一直使用,效率要高得多。
Unity提供了Dynamic Batching和Static Batching兩種方式。Dynamic Batching是完全自動(dòng)進(jìn)行的,不需要也無(wú)法進(jìn)行任何干預(yù),對(duì)于頂點(diǎn)數(shù)在300以內(nèi)的可移動(dòng)物體,只要使用相同的材質(zhì),就會(huì)組成Batch。Static Batching則需要把靜止的物體標(biāo)記為Static,然后無(wú)論大小,都會(huì)組成Batch。如前文所說(shuō),Static Batching顯然比Dynamic Batching要高效得多,于是,Static Batching功能是收費(fèi)的……
要有效利用Draw Call Batching,首先是盡量減少場(chǎng)景中使用的材質(zhì)數(shù)量,即盡量共享材質(zhì),對(duì)于僅紋理不同的材質(zhì)可以把紋理組合到一張更大的紋理中(稱為Texture Atlasing)。然后是把不會(huì)移動(dòng)的物體標(biāo)記為Static。此外還可以通過(guò)CombineChildren腳本(Standard Assets/Scripts/Unity Scripts/CombineChildren)手動(dòng)把物體組合在一起,但這個(gè)腳本會(huì)影響可見性測(cè)試,因?yàn)榻M合在一起的物體始終會(huì)被看作一個(gè)物體,從而會(huì)增加GPU要處理的幾何體數(shù)量,因此要小心使用。
對(duì)于復(fù)雜的靜態(tài)場(chǎng)景,還可以考慮自行設(shè)計(jì)遮擋剔除算法,減少可見的物體數(shù)量同時(shí)也可以減少Draw Call。
總之,理解Draw Call和Draw Call Batching原理,根據(jù)場(chǎng)景特點(diǎn)設(shè)計(jì)相應(yīng)的方案來(lái)盡量減少Draw Call次數(shù)才是王道,其它方面亦然。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
寫在前面
這一篇是在Digital Tutors的一個(gè)系列教程的基礎(chǔ)上總結(jié)擴(kuò)展而得的~Digital Tutors是一個(gè)非常棒的教程網(wǎng)站,包含了多媒體領(lǐng)域很多方面的資料,非常酷!除此之外,還參考了Unity Cookie中的一個(gè)教程。還有很多其他參考在下面的鏈接中。
這篇文章旨在簡(jiǎn)要地說(shuō)明一下常見的各種優(yōu)化策略。不過(guò)對(duì)每個(gè)基礎(chǔ)有非常深入地講解,需要的童鞋可以自行去相關(guān)資料。
還有一些我認(rèn)為非常好的參考文章:
Performance Optimization for Mobile Devices
4 Ways To Increase Performance of your Unity Game
Unite 2013 Optimizing Unity Games for Mobile Platforms
Unity optimization Tips
影響性能的因素
首先,我們得了解,影響游戲性能的因素哪些,才能對(duì)癥下藥。對(duì)于一個(gè)游戲來(lái)說(shuō),有兩種主要的計(jì)算資源:CPU和GPU。它們會(huì)互相合作,來(lái)讓我們的游戲可以在預(yù)期的幀率和分辨率下工作。CPU負(fù)責(zé)其中的幀率,GPU主要負(fù)責(zé)分辨率相關(guān)的一些東西。
總結(jié)起來(lái),主要的性能瓶頸在于:
- CPU
- 過(guò)多的Draw Calls
- 復(fù)雜的腳本或者物理模擬
- 過(guò)多的Draw Calls
- 頂點(diǎn)處理
- 過(guò)多的頂點(diǎn)
- 過(guò)多的逐頂點(diǎn)計(jì)算
- 過(guò)多的頂點(diǎn)
- 像素(Fragment)處理
- 過(guò)多的fragment,overdraws
- 過(guò)多的逐像素計(jì)算
- 過(guò)多的fragment,overdraws
- 帶寬
- 尺寸很大且未壓縮的紋理
- 分辨率過(guò)高的framebuffer
- 尺寸很大且未壓縮的紋理
對(duì)于CPU來(lái)說(shuō),限制它的主要是游戲中的Draw Calls。那么什么是Draw Call呢?如果你學(xué)過(guò)OpenGL,那么你一定還記得在每次繪圖前,我們都需要先準(zhǔn)備好頂點(diǎn)數(shù)據(jù)(位置、法線、顏色、紋理坐標(biāo)等),然后調(diào)用一系列API把它們放到GPU可以訪問(wèn)到的指定位置,最后,我們需要調(diào)用_glDraw*命令,來(lái)告訴GPU,“嘿,我把東西都準(zhǔn)備好了,你個(gè)懶家伙趕緊出來(lái)干活(渲染)吧!”。而調(diào)用_glDraw*命令的時(shí)候,就是一次Draw Call。那么為什么Draw Call會(huì)成為性能瓶頸呢(而且是CPU的瓶頸)?上面說(shuō)到過(guò),我們想要繪制圖像時(shí),就一定需要調(diào)用Draw Call。例如,一個(gè)場(chǎng)景里有水有樹,我們渲染水的時(shí)候使用的是一個(gè)material以及一個(gè)shader,但渲染樹的時(shí)候就需要一個(gè)完全不同的material和shader,那么就需要CPU重新準(zhǔn)備頂點(diǎn)數(shù)據(jù)、重新設(shè)置shader,而這種工作實(shí)際是非常耗時(shí)的。如果場(chǎng)景中,每一個(gè)物體都使用不同的material、不同的紋理,那么就會(huì)產(chǎn)生太多Draw Call,影響幀率,游戲性能就會(huì)下降。當(dāng)然,這里說(shuō)得很簡(jiǎn)單,更詳細(xì)的請(qǐng)自行谷歌。其他CPU的性能瓶頸還有物理、布料模擬、粒子模擬等,都是計(jì)算量很大的操作。
而對(duì)于GPU來(lái)說(shuō),它負(fù)責(zé)整個(gè)渲染流水線。它會(huì)從處理CPU傳遞過(guò)來(lái)的模型數(shù)據(jù)開始,進(jìn)行Vertex Shader、Fragment Shader等一系列工作,最后輸出屏幕上的每個(gè)像素。因此它的性能瓶頸可能和需要處理的頂點(diǎn)數(shù)目的、屏幕分辨率、顯存等因素有關(guān)。總體包含了頂點(diǎn)和像素兩方面的性能瓶頸。在像素處理中,最常見的性能瓶頸之一是overdraw。Overdraw指的是,我們可能對(duì)屏幕上的像素繪制了多次。
了解了上面基本的內(nèi)容后,下面涉及到的優(yōu)化技術(shù)有:
- 頂點(diǎn)優(yōu)化
- 優(yōu)化幾何體
- 使用LOD(Level of detail)技術(shù)
- 使用遮擋剔除(Occlusion culling)技術(shù)
- 優(yōu)化幾何體
- 像素優(yōu)化
- 控制繪制順序
- 警惕透明物體
- 減少實(shí)時(shí)光照
- 控制繪制順序
- CPU優(yōu)化
- 減少Draw Calls
- 減少Draw Calls
- 帶寬優(yōu)化
- 減少紋理大小
- 利用縮放
- 減少紋理大小
首先是頂點(diǎn)優(yōu)化的部分。
頂點(diǎn)優(yōu)化
優(yōu)化幾何體
這一步主要是為了針對(duì)性能瓶頸中的”頂點(diǎn)處理“一項(xiàng)。這里的幾何體就是指組成場(chǎng)景中對(duì)象的網(wǎng)格結(jié)構(gòu)。
3D游戲制作都由模型制作開始。而在建模時(shí),有一條我們需要記住:盡可能減少模型中三角形的數(shù)目,一些對(duì)于模型沒(méi)有影響、或是肉眼非常難察覺(jué)到區(qū)別的頂點(diǎn)都要盡可能去掉。例如在下面左圖中,正方體內(nèi)部很多頂點(diǎn)都是不需要的,而把這個(gè)模型導(dǎo)入到Unity里就會(huì)是右面的情景: ?
在Game視圖下,我們可以查看場(chǎng)景中的三角形數(shù)目和頂點(diǎn)數(shù)目:
可以看到一個(gè)簡(jiǎn)單的正方形就產(chǎn)生了這么多頂點(diǎn),這是我們不希望看到的。
同時(shí),盡可能重用頂點(diǎn)。在很多三維建模軟件中,都有相應(yīng)的優(yōu)化選項(xiàng),可以自動(dòng)優(yōu)化網(wǎng)格結(jié)構(gòu)。最后優(yōu)化后,一個(gè)正方體可能只剩下8個(gè)頂點(diǎn): ?
它對(duì)應(yīng)的頂點(diǎn)數(shù)和三角形數(shù)目如下:
等等!這里,你可能要問(wèn)了,為什么頂點(diǎn)數(shù)是24,而不是8呢?美術(shù)朋友們經(jīng)常會(huì)遇到這樣的問(wèn)題,就是建模軟件里顯示的模型頂點(diǎn)數(shù)和Unity中的不一樣,通常Unity會(huì)多很多。誰(shuí)才是對(duì)的呢?其實(shí),它們是站在不同的角度上計(jì)算的,都有各自的道理,但我們真正應(yīng)該關(guān)心的是Unity里的數(shù)目。
我們這里簡(jiǎn)單解釋一下。三維軟件里更多地是站在我們?nèi)祟惖慕嵌壤斫忭旤c(diǎn)的,即我們看見的一個(gè)點(diǎn)就是一個(gè)。而Unity是站在GPU的角度上,去計(jì)算頂點(diǎn)數(shù)目的。而在GPU看來(lái),看起來(lái)是一個(gè)的很有可能它要分開處理,從而就產(chǎn)生了額外的頂點(diǎn)。這種將頂點(diǎn)一分為多的原因,主要有兩個(gè):一個(gè)是UV splits,一個(gè)是Smoothing splits。而它們的本質(zhì)其實(shí)都是因?yàn)閷?duì)于GPU來(lái)說(shuō),頂點(diǎn)的每一個(gè)屬性和頂點(diǎn)之間必須是一對(duì)一的關(guān)系。UV splits的產(chǎn)生,是因?yàn)榻r(shí),一個(gè)頂點(diǎn)的UV坐標(biāo)有多個(gè)。例如之前的立方體的例子,由于每個(gè)面都有共同的頂點(diǎn),因此在不同面上,同一個(gè)頂點(diǎn)的UV坐標(biāo)可能發(fā)生改變。這對(duì)于GPU來(lái)說(shuō),這是不可理解的,因此它必須把這個(gè)頂點(diǎn)拆分成兩個(gè)具有不同UV坐標(biāo)的定頂點(diǎn),它才甘心。而Smoothing splits的產(chǎn)生也是類似的,不同的時(shí),這次一個(gè)頂點(diǎn)可能會(huì)對(duì)應(yīng)多個(gè)法線信息或切線信息。這通常是因?yàn)槲覀円獩Q定一個(gè)邊是一條Hard Edge還是Smooth Edge。Hard Edge通常是下面這樣的效果(注意中間的折痕部分): ?
而如果觀察它的頂點(diǎn)法線,就會(huì)發(fā)現(xiàn),折痕處每個(gè)頂點(diǎn)其實(shí)包含了兩個(gè)不同的法線。因此,對(duì)于GPU來(lái)說(shuō),它同樣無(wú)法理解這樣的事情,因此會(huì)把頂點(diǎn)一分為二。而相反,Smooth Edge則是下面的情況: ?
對(duì)于GPU來(lái)說(shuō),它本質(zhì)上只關(guān)心有多少個(gè)頂點(diǎn)。因此,盡可能減少頂點(diǎn)的數(shù)目其實(shí)才是我們真正對(duì)需要關(guān)心的事情。因此,最后一條優(yōu)化建議就是:移除不必要的Hard Edge以及紋理銜接,即避免Smoothing splits和UV?splits。
使用LOD(Level of detail)技術(shù)
LOD技術(shù)有點(diǎn)類似于Mipmap技術(shù),不同的是,LOD是對(duì)模型建立了一個(gè)模型金字塔,根據(jù)攝像機(jī)距離對(duì)象的遠(yuǎn)近,選擇使用不同精度的模型。它的好處是可以在適當(dāng)?shù)臅r(shí)候大量減少需要繪制的頂點(diǎn)數(shù)目。它的缺點(diǎn)同樣是需要占用更多的內(nèi)存,而且如果沒(méi)有調(diào)整好距離的話,可能會(huì)造成模擬的突變。
在Unity中,可以通過(guò)LOD Group來(lái)實(shí)現(xiàn)LOD技術(shù):
?
通過(guò)上面的LOD Group面板,我們可以選擇需要控制的模型以及距離設(shè)置。下面展示了油桶從一個(gè)完整網(wǎng)格到簡(jiǎn)化網(wǎng)格,最后完全被剔除的例子:
????
使用遮擋剔除(Occlusion culling)技術(shù)
遮擋剔除是用來(lái)消除躲在其他物件后面看不到的物件,這代表資源不會(huì)浪費(fèi)在計(jì)算那些看不到的頂點(diǎn)上,進(jìn)而提升性能。關(guān)于遮擋剔除,Unity Taiwan有一個(gè)系列文章大家可以看看(需翻墻):
Unity 4.3 關(guān)于Occlusion Culling : 基本篇
Unity 4.3 關(guān)于Occlusion Culling : 最佳做法
Unity 4.3 關(guān)于Occlusion Culling : 錯(cuò)誤診斷
具體的內(nèi)容大家可以自行查找。
現(xiàn)在我們來(lái)談像素優(yōu)化。
像素優(yōu)化
像素優(yōu)化的重點(diǎn)在于減少overdraw。之前提過(guò),overdraw指的就是一個(gè)像素被繪制了多次。關(guān)鍵在于控制繪制順序。
Unity還提供了查看overdraw的視圖,在Scene視圖的Render Mode->Overdraw。當(dāng)然這里的視圖只是提供了查看物體遮擋的層數(shù)關(guān)系,并不是真正的最終屏幕繪制的overdraw。也就是說(shuō),可以理解為它顯示的是如果沒(méi)有使用任何深度檢驗(yàn)時(shí)的overdraw。這種視圖是通過(guò)把所有對(duì)象都渲染成一個(gè)透明的輪廓,通過(guò)查看透明顏色的累計(jì)程度,來(lái)判斷物體的遮擋。
上圖圖,紅色越是濃重的地方表示overdraw越嚴(yán)重,而且這里涉及的都是透明物體,這意味著性能將會(huì)受到很大影響。
控制繪制順序
需要控制繪制順序,主要原因是為了最大限度的避免overdraws,也就是同一個(gè)位置的像素可以需要被繪制多變。在PC上,資源無(wú)限,為了得到最準(zhǔn)確的渲染結(jié)果,繪制順序可能是從后往前繪制不透明物體,然后再繪制透明物體進(jìn)行混合。但在移動(dòng)平臺(tái)上,這種會(huì)造成大量overdraw的方式顯然是不適合的,我們應(yīng)該盡量從前往后繪制。從前往后繪制之所以可以減少overdraw,都是因?yàn)樯疃葯z驗(yàn)的功勞。
在Unity中,那些Shader中被設(shè)置為“Geometry” 隊(duì)列的對(duì)象總是從前往后繪制的,而其他固定隊(duì)列(如“Transparent”“Overla”等)的物體,則都是從后往前繪制的。這意味這,我們可以盡量把物體的隊(duì)列設(shè)置為“Geometry” 。
而且,我們還可以充分利用Unity的隊(duì)列來(lái)控制繪制順序。例如,對(duì)于天空盒子來(lái)說(shuō),它幾乎覆蓋了所有的像素,而且我們知道它永遠(yuǎn)會(huì)在所有物體的后面,因此它的隊(duì)列可以設(shè)置為“Geometry+1”。這樣,就可以保證不會(huì)因?yàn)樗斐蒾verdraws。
時(shí)刻警惕透明物體
而對(duì)于透明對(duì)象,由于它本身的特性(可以看之前關(guān)于Alpha Test和Alpha Blending的一篇文章)決定如果要得到正確的渲染效果,就必須從后往前渲染(這里不討論使用深度的方法),而且拋棄了深度檢驗(yàn)。這意味著,透明物體幾乎一定會(huì)造成overdraws。如果我們不注意這一點(diǎn),在一些機(jī)器上可能會(huì)造成嚴(yán)重的性能下面。例如,對(duì)于GUI對(duì)象來(lái)說(shuō),它們大多被設(shè)置成了半透明,如果屏幕中GUI占據(jù)的比例太多,而主攝像機(jī)又沒(méi)有進(jìn)行調(diào)整而是投影整個(gè)屏幕,那么GUI就會(huì)造成屏幕的大量overdraws。
因此,如果場(chǎng)景中大面積的透明對(duì)象,或者有很多層覆蓋的多層透明對(duì)象(即便它們每個(gè)的面積可以都不大),或者是透明的粒子效果,在移動(dòng)設(shè)備上也會(huì)造成大量的overdraws。這是應(yīng)該盡量避免的。
對(duì)于上述GUI的這種情況,我們可以盡量減少窗口中GUI所占的面積。如果實(shí)在無(wú)能為力,我們可以把GUI繪制和三維場(chǎng)景的繪制交給不同的攝像機(jī),而其中負(fù)責(zé)三維場(chǎng)景的攝像機(jī)的視角范圍盡量不要和GUI重疊。對(duì)于其他情況,只能說(shuō),盡可能少用。當(dāng)然這樣會(huì)對(duì)游戲的美觀度產(chǎn)生一定影響,因此我們可以在代碼中對(duì)機(jī)器的性能進(jìn)行判斷,例如首先關(guān)閉所有的耗費(fèi)性能的功能,如果發(fā)現(xiàn)這個(gè)機(jī)器表現(xiàn)非常良好,再嘗試開啟一些特效功能。
減少實(shí)時(shí)光照
實(shí)時(shí)光照對(duì)于移動(dòng)平臺(tái)是個(gè)非常昂貴的操作。如果只有一個(gè)平行光還好,但如果場(chǎng)景中包含了太多光源并且使用了很多多Passes的shader,那么很有可能會(huì)造成性能下降。而且在有些機(jī)器上,還要面臨shader失效的風(fēng)險(xiǎn)。例如,一個(gè)場(chǎng)景里如果包含了三個(gè)逐像素的點(diǎn)光源,而且使用了逐像素的shader,那么很有可能將Draw Calls提高了三倍,同時(shí)也會(huì)增加overdraws。這是因?yàn)?#xff0c;對(duì)于逐像素的光源來(lái)說(shuō),被這些光源照亮的物體要被再渲染一次。更糟糕的是,無(wú)論是動(dòng)態(tài)批處理還是動(dòng)態(tài)批處理(其實(shí)文檔中只提到了對(duì)動(dòng)態(tài)批處理的影響,但不知道為什么實(shí)驗(yàn)結(jié)果對(duì)靜態(tài)批處理也沒(méi)有用),對(duì)于這種逐像素的pass都無(wú)法進(jìn)行批處理,也就是說(shuō),它們會(huì)中斷批處理。
例如,下面的場(chǎng)景中,四個(gè)物體都被標(biāo)識(shí)成了“Static”,它們使用的shader都是自帶的Bumped Diffuse。而所有的點(diǎn)光源都被標(biāo)識(shí)成了“Important”,即是逐像素光。可以看到,運(yùn)行后的Draw Calls是23,而非3。這是因?yàn)?#xff0c;只有“Forward Base”的Pass時(shí)發(fā)生了靜態(tài)批處理(這里的動(dòng)態(tài)批處理由于多Pass已經(jīng)完全失效了),節(jié)省了一個(gè)Draw Calls,而后面的“Forward Add” Pass,每一次渲染都是一個(gè)單獨(dú)的Draw Call(而且可以看到Tris和Verts數(shù)目也增加了):
這點(diǎn)正如文檔中說(shuō)的:The draw calls for “additional per-pixel lights” will not be batched。原因我不是很清楚,這里有一個(gè)討論,但里面的意思說(shuō)是對(duì)靜態(tài)批處理沒(méi)有影響,和我這里的結(jié)果不一樣,知道原因的麻煩給我留言,非常感謝。我也在Unity論壇里提問(wèn)里。
我們看到很多成功的移動(dòng)游戲,它們的畫面效果看起來(lái)好像包含了很多光源,但其實(shí)這都是騙人的。
使用Lightmaps
Lightmaps的很常見的一種優(yōu)化策略。它主要用于場(chǎng)景中整體的光照效果。這種技術(shù)主要是提前把場(chǎng)景中的光照信息存儲(chǔ)在一張光照紋理中,然后在運(yùn)行時(shí)刻只需要根據(jù)紋理采樣得到光照信息即可。
當(dāng)然與之配合的還有Light Probes技術(shù)。風(fēng)宇沖有一個(gè)系列文章講過(guò),但是時(shí)間比較久遠(yuǎn),但教程我相信網(wǎng)上有很多。
使用God Rays
場(chǎng)景中很多小型光源效果都是靠這種方法模擬的。它們一般并不是真的光源產(chǎn)生的,很多情況是通過(guò)透明紋理進(jìn)行模擬。具體可以參見之前的文章。
CPU優(yōu)化
減少Draw Calls
批處理(Batching)
這方面的優(yōu)化教程想必是最多的了。最常見的就是通過(guò)批處理(Batching)了。從名字上來(lái)理解,就是一塊處理多個(gè)物體的意思。那么什么樣的物體可以一起處理呢?答案就是使用同一個(gè)材質(zhì)的物體。這是因此,對(duì)于使用同一個(gè)材質(zhì)的物體,它們之間的不同僅僅在于頂點(diǎn)數(shù)據(jù)的差別,即使用的網(wǎng)格不同而已。我們可以把這些頂點(diǎn)數(shù)據(jù)合并在一起,再一起發(fā)送給GPU,就可以完成一次批處理。
Unity中有兩種批處理方式:一種是動(dòng)態(tài)批處理,一種是靜態(tài)批處理。對(duì)于動(dòng)態(tài)批處理來(lái)說(shuō),好消息是一切處理都是自動(dòng)的,不需要我們自己做任何操作,而且物體是可以移動(dòng)的,但壞消息是,限制很多,可能一不小心我們就會(huì)破壞了這種機(jī)制,導(dǎo)致Unity無(wú)法批處理一些使用了相同材質(zhì)的物體。對(duì)于靜態(tài)批處理來(lái)說(shuō),好消息是自由度很高,限制很少,壞消息是可能會(huì)占用更多的內(nèi)存,而且經(jīng)過(guò)靜態(tài)批處理后的所有物體都不可以再移動(dòng)了。
首先來(lái)說(shuō)動(dòng)態(tài)批處理。Unity進(jìn)行動(dòng)態(tài)批處理的條件是,物體使用同一個(gè)材質(zhì)并且滿足一些特定條件。Unity總是在不知不覺(jué)中就為我們做了動(dòng)態(tài)批處理。例如下面的場(chǎng)景:
這個(gè)場(chǎng)景共包含了4個(gè)物體,其中兩個(gè)箱子使用了同一個(gè)材質(zhì)。可以看到,它的Draw Calls現(xiàn)在是3,并且顯示Save by batching是1,也就是說(shuō),Unity靠Batching為我們節(jié)省了1個(gè)Draw Call。下面,我們來(lái)把其中一個(gè)箱子的大小隨便改動(dòng)一下,看看會(huì)發(fā)生什么:
可以發(fā)現(xiàn),Draw Calls變成了4,Save by batching的數(shù)目也變成了0。這是為什么呢?它們明明還是只使用了一個(gè)材質(zhì)啊。原因就是前面提到的那些需要滿足的其他條件。動(dòng)態(tài)批處理雖然自動(dòng)得令人感動(dòng),但它對(duì)模型的要求很多:
- 頂點(diǎn)屬性的最大限制為900,而且未來(lái)有可能會(huì)變。不要依賴這個(gè)數(shù)據(jù)。
- 一般來(lái)說(shuō),那么所有對(duì)象都必須需要使用同一個(gè)縮放尺度(可以是(1, 1, 1)、(1, 2, 3)、(1.5, 1.4, 1.3)等等,但必須都一樣)。但如果是非統(tǒng)一縮放(即每個(gè)維度的縮放尺度不一樣,例如(1, 2, 1)),那么如果所有的物體都使用不同的非統(tǒng)一縮放也是可以批處理的。這個(gè)要求很怪異,為什么批處理會(huì)和縮放有關(guān)呢?這和Unity背后的技術(shù)有關(guān)系,有興趣的可以自行谷歌,比如這里。
- 使用lightmap的物體不會(huì)批處理。多passes的shader會(huì)中斷批處理。接受實(shí)時(shí)陰影的物體也不會(huì)批處理。
上述除了最常見的由于縮放導(dǎo)致破壞批處理的情況,還有就是頂點(diǎn)屬性的限制。例如,在上面的場(chǎng)景中我們添加之前未優(yōu)化后的箱子模型:
可以看到Draw Calls一下子變成了5。這是因?yàn)樾绿砑拥南渥幽P椭?#xff0c;包含了474個(gè)頂點(diǎn),而它使用的頂點(diǎn)屬性有位置、UV坐標(biāo)、法線等信息,使用的總和超過(guò)了900。
動(dòng)態(tài)批處理的條件這么多,一不小心它就不干了,因此Unity提供了另一個(gè)方法,靜態(tài)批處理。接著上面的例子,我們保持修改后的縮放,但把四個(gè)物體的“Static Flag”勾選上:
點(diǎn)擊Static后面的三角下拉框,我們會(huì)看到其實(shí)這一步設(shè)置了很多東西,這里我們想要的只是“Batching static”一項(xiàng)。這時(shí)我們?cè)倏碊raw Calls,恩,還是沒(méi)有變化。但是不要急,我們點(diǎn)擊運(yùn)行,變化出現(xiàn)了:
Draw Calls又回到了3,并且顯示Save by batching是1。這就是得利于靜態(tài)批處理。而且,如果我們?cè)谶\(yùn)行時(shí)刻查看模型的網(wǎng)格,會(huì)發(fā)現(xiàn)它們都變成了一個(gè)名為Combined Mesh (roo: scene)的東西。這個(gè)網(wǎng)格是Unity合并了所有標(biāo)識(shí)為“Static”的物體的結(jié)果,在我們的例子里,就是四個(gè)物體:
你可以要問(wèn)了,這四個(gè)對(duì)象明明不是都使用了一個(gè)材質(zhì),為什么可以合并成一個(gè)呢?如果你仔細(xì)觀察上圖的話,會(huì)發(fā)現(xiàn)里面標(biāo)明了“4 submeshes”,也就是說(shuō),這個(gè)合并后的網(wǎng)格其實(shí)包含了4個(gè)子網(wǎng)格,也就是我們的四個(gè)對(duì)象。對(duì)于合并后后的網(wǎng)格,Unity會(huì)判斷其中使用同一個(gè)材質(zhì)的子網(wǎng)格,然后對(duì)它們進(jìn)行批處理。
但是,我們?cè)偌?xì)心點(diǎn)可以發(fā)現(xiàn),我們的箱子使用的其實(shí)是同一個(gè)網(wǎng)格,但合并后卻變成了兩個(gè)。而且,我們觀察運(yùn)行前后Stats窗口中的“VBO total”,它的大小由241.6KB變成了286.2KB,變大了!還記得靜態(tài)批處理的缺點(diǎn)嗎?就是可能會(huì)占用更多的內(nèi)存。文檔中是這樣寫的:
“Using static batching will require additional memory for storing the combined geometry. If several objects shared the same geometry before static batching, then a copy of geometry will be created for each object, either in the Editor or at runtime. This might not always be a good idea - sometimes you will have to sacrifice rendering performance by avoiding static batching for some objects to keep a smaller memory footprint. For example, marking trees as static in a dense forest level can have serious memory impact.”
也就是說(shuō),如果在靜態(tài)批處理前有一些物體共享了相同的網(wǎng)格(例如這里的兩個(gè)箱子),那么每一個(gè)物體都會(huì)有一個(gè)該網(wǎng)格的復(fù)制品,即一個(gè)網(wǎng)格會(huì)變成多個(gè)網(wǎng)格被發(fā)送給GPU。在上面的例子看來(lái),就是VBO的大小明顯增大了。如果這類使用同一網(wǎng)格的對(duì)象很多,那么這就是一個(gè)問(wèn)題了,這種時(shí)候我們可能需要避免使用靜態(tài)批處理,這意味著犧牲一定的渲染性能。例如,如果在一個(gè)使用了1000個(gè)重復(fù)樹模型的森林中使用靜態(tài)批處理,那么結(jié)果就會(huì)產(chǎn)生1000倍的內(nèi)存,這會(huì)造成嚴(yán)重的內(nèi)存影響。這種時(shí)候,解決方法要么我們可以忍受這種犧牲內(nèi)存換取性能的方法,要么不要使用靜態(tài)批處理,而使用動(dòng)態(tài)批處理(前提是大家使用相同的縮放大小,或者大家都使用不同的非統(tǒng)一縮放大小),或者自己編寫批處理的方法。當(dāng)然,我認(rèn)為最好的還是使用動(dòng)態(tài)批處理來(lái)解決。
有一些小提示可以使用:
- 盡可能選擇靜態(tài)批處理,但得時(shí)刻小心對(duì)內(nèi)存的消耗。
- 如果無(wú)法進(jìn)行靜態(tài)批處理,而要使用動(dòng)態(tài)批處理的話,那么請(qǐng)小心上面提到的各種注意事項(xiàng)。例如:
- 盡可能讓這樣的物體少并且盡可能讓這些物體包含少量的頂點(diǎn)屬性。
- 不要使用統(tǒng)一縮放,或者都使用不同的非統(tǒng)一縮放。
- 盡可能讓這樣的物體少并且盡可能讓這些物體包含少量的頂點(diǎn)屬性。
- 對(duì)于游戲中的小道具,例如可以撿拾的金幣等,可以使用動(dòng)態(tài)批處理。
- 對(duì)于包含動(dòng)畫的這類物體,我們無(wú)法全部使用靜態(tài)批處理,但其中如果有不動(dòng)的部分,可以把這部分標(biāo)識(shí)成“Static”。
一些討論:
How static batching works
Static batching use a ton of memory?
Unity3D draw call optimization
合并紋理(Atlas)
雖然批處理是個(gè)很好的方式,但很容易就打破它的規(guī)定。例如,場(chǎng)景中的物體都使用Diffuse材質(zhì),但它們可能會(huì)使用不同的紋理。因此,盡可能把多張小紋理合并到一張大紋理(Atlas)中是一個(gè)好主意。
利用網(wǎng)格的頂點(diǎn)數(shù)據(jù)
但有時(shí),除了紋理不同外,還有對(duì)于不同的物體,它們?cè)诓馁|(zhì)上還有一些微小的參數(shù)變化,例如顏色不同、某些浮點(diǎn)參數(shù)不同。但鐵定律是,不管是動(dòng)態(tài)批處理還是靜態(tài)批處理,它們的前提都是要使用同一個(gè)材質(zhì)。是同一個(gè),而不是同一種,也就是說(shuō)它們指向的材質(zhì)必須是同一個(gè)實(shí)體。這意味著,只要我們調(diào)整了參數(shù),就會(huì)影響到所有使用這個(gè)材質(zhì)的對(duì)象。那么想要微小的調(diào)整怎么辦呢?由于Unity中的規(guī)定非常死,那么我們只好想些“歪門邪道”,其中一種就是使用網(wǎng)格的頂點(diǎn)數(shù)據(jù)(最常見的就是頂點(diǎn)顏色數(shù)據(jù))。
前面說(shuō)過(guò),經(jīng)過(guò)批處理后的物體會(huì)被處理成一個(gè)VBO發(fā)送給GPU,VBO中的數(shù)據(jù)可以作為輸入傳遞給Vertex Shader,因此我們可以巧妙地對(duì)VBO中的數(shù)據(jù)進(jìn)行控制,從而達(dá)到不同效果的目的。一個(gè)例子是,還是之前的森林,所有的樹使用了同一種材質(zhì),我們希望它們可以通過(guò)動(dòng)態(tài)批處理來(lái)實(shí)現(xiàn),但不同樹的顏色可能不同。這時(shí)我么可以利用網(wǎng)格的頂點(diǎn)數(shù)據(jù)來(lái)調(diào)整。具體方法,可以參見后面會(huì)寫的一篇文章。
但這種方法的缺點(diǎn)就是會(huì)需要更多的內(nèi)存來(lái)存儲(chǔ)這些用于調(diào)整參數(shù)用的頂點(diǎn)數(shù)據(jù)。沒(méi)辦法,永遠(yuǎn)沒(méi)有絕對(duì)完美的方法。
帶寬優(yōu)化
減少紋理大小
之前提到過(guò),使用Texture Atlas可以幫助減少Draw Calls,而這些紋理的大小同樣是一個(gè)需要考慮的問(wèn)題。在這之前要提到一個(gè)問(wèn)題就是,所有紋理的長(zhǎng)寬比最好是正方形,而且長(zhǎng)度值最好是2的整數(shù)冪。這是因?yàn)橛泻芏鄡?yōu)化策略只有在這種時(shí)候才可以發(fā)揮最大效用。
Unity中查看紋理參數(shù)可以通過(guò)紋理的面板:
而調(diào)整參數(shù)可以通過(guò)紋理的Advance面板:
上面各種參數(shù)的說(shuō)明可以參見文檔。其中和優(yōu)化相關(guān)的主要有“Generate Mip Maps”、“Max Size”和“Format”幾個(gè)選項(xiàng)。
“Generate Mip Maps”會(huì)為同一張紋理創(chuàng)建出很多不同大小的小紋理,構(gòu)成一個(gè)紋理金字塔。而在游戲中可以根據(jù)距離物體的遠(yuǎn)近,來(lái)動(dòng)態(tài)選擇使用哪一個(gè)紋理。這是因?yàn)?#xff0c;在距離物體很遠(yuǎn)的時(shí)候,就算我們使用了非常精細(xì)的紋理,但肉眼也是分辨不出來(lái)的,這種時(shí)候完全可以使用更小、更模糊的紋理來(lái)代替,而這大量可以節(jié)省訪問(wèn)的像素的數(shù)目。但它的缺點(diǎn)是,由于需要為每一個(gè)紋理建立一個(gè)圖像金字塔,因此它會(huì)需要占用更多的內(nèi)存。例如上面的例子,在勾選“Generate Mip Maps”前,內(nèi)存占用是0.5M,而勾選了“Generate Mip Maps”后,就變成了0.7M。除了內(nèi)存的占用以外,一些時(shí)候我們也不希望使用Mipmaps,例如GUI紋理等。我們還可以在面板中查看生成的Mip Maps:
Unity中還提供了查看場(chǎng)景中物體的Mip Maps的使用情況。更確切的說(shuō)是,展示了物體理想的紋理大小。其中紅色表示這個(gè)物體可以使用更小的紋理,藍(lán)色表示應(yīng)該使用更大的紋理。
“Max Size”決定了紋理的長(zhǎng)寬值,如果我們使用的紋理本身超過(guò)了這個(gè)最大值,Unity會(huì)對(duì)其進(jìn)行縮小來(lái)滿足這個(gè)條件。這里再重復(fù)一點(diǎn),所有紋理的長(zhǎng)寬比最好是正方形,而且長(zhǎng)度值最好是2的整數(shù)冪。這是因?yàn)橛泻芏鄡?yōu)化策略只有在這種時(shí)候才可以發(fā)揮最大效用。
“Format”負(fù)責(zé)紋理使用的壓縮模式。通常選擇這種自動(dòng)模式就可以了,Unity會(huì)負(fù)責(zé)根據(jù)不同的平臺(tái)來(lái)選擇合適的壓縮模式。而對(duì)于GUI類型的紋理,我們可以根據(jù)對(duì)畫質(zhì)的要求來(lái)選擇是否進(jìn)行壓縮,具體可以參見之前關(guān)于畫質(zhì)的文章。
我們還可以根據(jù)不同的機(jī)器來(lái)選擇使用不同分辨率的紋理,以便讓游戲在某些老機(jī)器上也可以運(yùn)行。
利用縮放
很多時(shí)候分辨率也是造成性能下降的原因,尤其是現(xiàn)在很多國(guó)內(nèi)山寨機(jī),除了分辨率高其他硬件簡(jiǎn)直一塌糊涂,而這恰恰中了游戲性能的兩個(gè)瓶頸:過(guò)大的屏幕分辨率+糟糕的GPU。因此,我們可能需要對(duì)于特定機(jī)器進(jìn)行分辨率的放縮。當(dāng)然,這樣會(huì)造成游戲效果的下降,但性能和畫面之間永遠(yuǎn)是個(gè)需要權(quán)衡的話題。
在Unity中設(shè)置屏幕分辨率可以直接調(diào)用Screen.SetResolution。實(shí)際使用中可能會(huì)遇到一些情況,雨松MOMO有一篇文章講了這種技術(shù),可以去看看。
?
寫在最后
這篇文章是總結(jié)性質(zhì)的,因此對(duì)每種技術(shù)都沒(méi)有進(jìn)行非常詳細(xì)的解釋。強(qiáng)烈建議大家閱讀文章開頭給出的各種鏈接,寫得都很好。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
剛開始寫這篇文章的時(shí)候選了一個(gè)很土的題目。。。《Unity3D優(yōu)化全解析》。因?yàn)檫@是一篇臨時(shí)起意才寫的文章,而且陳述的都是既有的事實(shí),因而給自己“文(dou)學(xué)(bi)”加工留下的余地就少了很多。但又覺(jué)得這塊是不得不提的一個(gè)地方,平時(shí)見到很多人對(duì)此處也給予了忽略了事,需要時(shí)才去網(wǎng)上扒一些只言片語(yǔ)的資料。也恰逢年前,尋思著周末認(rèn)真寫點(diǎn)東西遇到節(jié)假日沒(méi)準(zhǔn)也沒(méi)什么人讀,所以索性就寫了這篇臨時(shí)的文章。題目很土,因?yàn)橛昧酥赶蛐院苊鞔_的“Unity3D”,讓人少了遐(瞎)想的空間,同時(shí)用了“高大全”這樣的構(gòu)詞法,也讓匹夫有成為眾矢之的的可能。。。所以最后還是改成了現(xiàn)在各位看到的題目。話不多說(shuō),下面就開始正文~正所謂“草蛇灰線,伏脈千里”。那咱們首先~~~~~~
看看優(yōu)化需要從哪里著手?
匹夫印象里遇到的童靴,提Unity3D項(xiàng)目?jī)?yōu)化則必提DrawCall,這自然沒(méi)錯(cuò),但也有很不好影響。因?yàn)檫@會(huì)給人一個(gè)錯(cuò)誤的認(rèn)識(shí):所謂的優(yōu)化就是把DrawCall弄的比較低就對(duì)了。
對(duì)優(yōu)化有這種第一印象的人不在少數(shù),drawcall的確是一個(gè)很重要的指標(biāo),但絕非全部。為了讓各位和匹夫能達(dá)成盡可能多的共識(shí),匹夫首先介紹一下本文可能會(huì)涉及到的幾個(gè)概念,之后會(huì)提出優(yōu)化所涉及的三大方面:
- drawcall是啥?其實(shí)就是對(duì)底層圖形程序(比如:OpenGL ES)接口的調(diào)用,以在屏幕上畫出東西。所以,是誰(shuí)去調(diào)用這些接口呢?CPU。
- fragment是啥?經(jīng)常有人說(shuō)vf啥的,vertex我們都知道是頂點(diǎn),那fragment是啥呢?說(shuō)它之前需要先說(shuō)一下像素,像素各位應(yīng)該都知道吧?像素是構(gòu)成數(shù)碼影像的基本單元呀。那fragment呢?是有可能成為像素的東西。啥叫有可能?就是最終會(huì)不會(huì)被畫出來(lái)不一定,是潛在的像素。這會(huì)涉及到誰(shuí)呢?GPU。
- batching是啥?都知道批處理是干嘛的吧?沒(méi)錯(cuò),將批處理之前需要很多次調(diào)用(drawcall)的物體合并,之后只需要調(diào)用一次底層圖形程序的接口就行。聽上去這簡(jiǎn)直就是優(yōu)化的終極方案啊!但是,理想是美好的,世界是殘酷的,一些不足之后我們?cè)偌?xì)聊。
- 內(nèi)存的分配:記住,除了Unity3D自己的內(nèi)存損耗。我們可是還帶著Mono呢啊,還有托管的那一套東西呢。更別說(shuō)你一激動(dòng),又引入了自己的幾個(gè)dll。這些都是內(nèi)存開銷上需要考慮到的。
好啦,文中的幾個(gè)概念提前講清楚了,其實(shí)各位也能看的出來(lái)匹夫接下來(lái)要說(shuō)的匹夫關(guān)注的優(yōu)化時(shí)需要注意的方面:
- CPU方面
- GPU方面
- 內(nèi)存方面
所以,這篇文章也會(huì)按照CPU—->GPU—->內(nèi)存的順序進(jìn)行。
CPU的方面的優(yōu)化:
上文中說(shuō)了,drawcall影響的是CPU的效率,而且也是最知名的一個(gè)優(yōu)化點(diǎn)。但是除了drawcall之外,還有哪些因素也會(huì)影響到CPU的效率呢?讓我們一一列出暫時(shí)能想得到的:
- DrawCalls
- 物理組件(Physics)
- GC(什么?GC不是處理內(nèi)存問(wèn)題的嘛?匹夫你不要騙我啊!不過(guò),匹夫也要提醒一句,GC是用來(lái)處理內(nèi)存的,但是是誰(shuí)使用GC去處理內(nèi)存的呢?)
- 當(dāng)然,還有代碼質(zhì)量
DrawCalls:
前面說(shuō)過(guò)了,DrawCall是CPU調(diào)用底層圖形接口。比如有上千個(gè)物體,每一個(gè)的渲染都需要去調(diào)用一次底層接口,而每一次的調(diào)用CPU都需要做很多工作,那么CPU必然不堪重負(fù)。但是對(duì)于GPU來(lái)說(shuō),圖形處理的工作量是一樣的。所以對(duì)DrawCall的優(yōu)化,主要就是為了盡量解放CPU在調(diào)用圖形接口上的開銷。所以針對(duì)drawcall我們主要的思路就是每個(gè)物體盡量減少渲染次數(shù),多個(gè)物體最好一起渲染。所以,按照這個(gè)思路就有了以下幾個(gè)方案:
Draw Call Batching
首先我們要先理解為何2個(gè)沒(méi)有使用相同材質(zhì)的物體即使使用批處理,也無(wú)法實(shí)現(xiàn)Draw Call數(shù)量的下降和性能上的提升。
因?yàn)楸弧芭幚怼钡?個(gè)物體的網(wǎng)格模型需要使用相同材質(zhì)的目的,在于其紋理是相同的,這樣才可以實(shí)現(xiàn)同時(shí)渲染的目的。因而保證材質(zhì)相同,是為了保證被渲染的紋理相同。
因此,為了將2個(gè)紋理不同的材質(zhì)合二為一,我們就需要進(jìn)行上面列出的第二步,將紋理打包成圖集。具體到合二為一這種情況,就是將2個(gè)紋理合成一個(gè)紋理。這樣我們就可以只用一個(gè)材質(zhì)來(lái)代替之前的2個(gè)材質(zhì)了。
而Draw Call Batching本身,也還會(huì)細(xì)分為2種。
Static Batching 靜態(tài)批處理
看名字,猜使用的情景。
靜態(tài)?那就是不動(dòng)的咯。還有呢?額,聽上去狀態(tài)也不會(huì)改變,沒(méi)有“生命”,比如山山石石,樓房校舍啥的。那和什么比較類似呢?嗯,聰明的各位一定覺(jué)得和場(chǎng)景的屬性很像吧!所以我們的場(chǎng)景似乎就可以采用這種方式來(lái)減少draw call了。
那么寫個(gè)定義:只要這些物體不移動(dòng),并且擁有相同的材質(zhì),靜態(tài)批處理就允許引擎對(duì)任意大小的幾何物體進(jìn)行批處理操作來(lái)降低描繪調(diào)用。
那要如何使用靜態(tài)批來(lái)減少Draw Call呢?你只需要明確指出哪些物體是靜止的,并且在游戲中永遠(yuǎn)不會(huì)移動(dòng)、旋轉(zhuǎn)和縮放。想完成這一步,你只需要在檢測(cè)器(Inspector)中將Static復(fù)選框打勾即可,如下圖所示:
至于效果如何呢?
舉個(gè)例子:新建4個(gè)物體,分別是Cube,Sphere, Capsule, Cylinder,它們有不同的網(wǎng)格模型,但是也有相同的材質(zhì)(Default-Diffuse)。
首先,我們不指定它們是static的。Draw Call的次數(shù)是4次,如圖:
我們現(xiàn)在將它們4個(gè)物體都設(shè)為static,在來(lái)運(yùn)行一下:
如圖,Draw Call的次數(shù)變成了1,而Saved by batching的次數(shù)變成了3。
靜態(tài)批處理的好處很多,其中之一就是與下面要說(shuō)的動(dòng)態(tài)批處理相比,約束要少很多。所以一般推薦的是draw call的靜態(tài)批處理來(lái)減少draw call的次數(shù)。那么接下來(lái),我們就繼續(xù)聊聊draw call的動(dòng)態(tài)批處理。
Dynamic Batching 動(dòng)態(tài)批處理
有陰就有陽(yáng),有靜就有動(dòng),所以聊完了靜態(tài)批處理,肯定跟著就要說(shuō)說(shuō)動(dòng)態(tài)批處理了。首先要明確一點(diǎn),Unity3D的draw call動(dòng)態(tài)批處理機(jī)制是引擎自動(dòng)進(jìn)行的,無(wú)需像靜態(tài)批處理那樣手動(dòng)設(shè)置static。我們舉一個(gè)動(dòng)態(tài)實(shí)例化prefab的例子,如果動(dòng)態(tài)物體共享相同的材質(zhì),則引擎會(huì)自動(dòng)對(duì)draw call優(yōu)化,也就是使用批處理。首先,我們將一個(gè)cube做成prefab,然后再實(shí)例化500次,看看draw call的數(shù)量。
| 12345 | for(int i = 0; i < 500; i++){????GameObject cube;????cube = GameObject.Instantiate(prefab) as GameObject;} |
draw call的數(shù)量:
可以看到draw call的數(shù)量為1,而 saved by batching的數(shù)量是499。而這個(gè)過(guò)程中,我們除了實(shí)例化創(chuàng)建物體之外什么都沒(méi)做。不錯(cuò),unity3d引擎為我們自動(dòng)處理了這種情況。
但是有很多童靴也遇到這種情況,就是我也是從prefab實(shí)例化創(chuàng)建的物體,為何我的draw call依然很高呢?這就是匹夫上文說(shuō)的,draw call的動(dòng)態(tài)批處理存在著很多約束。下面匹夫就演示一下,針對(duì)cube這樣一個(gè)簡(jiǎn)單的物體的創(chuàng)建,如果稍有不慎就會(huì)造成draw call飛漲的情況吧。
我們同樣是創(chuàng)建500個(gè)物體,不同的是其中的100個(gè)物體,每個(gè)物體的大小都不同,也就是Scale不同。
| 1 2 3 4 5 6 7 8 9 | for(int i = 0; i < 500; i++) { ????GameObject cube; ????cube = GameObject.Instantiate(prefab) as GameObject; ????if(i / 100 == 0) ????{ ????????cube.transform.localScale = new Vector3(2 + i, 2 + i, 2 + i); ????} } |
draw call的數(shù)量:
我們看到draw call的數(shù)量上升到了101次,而saved by batching的數(shù)量也下降到了399。各位看官可以看到,僅僅是一個(gè)簡(jiǎn)單的cube的創(chuàng)建,如果scale不同,竟然也不會(huì)去做批處理優(yōu)化。這僅僅是動(dòng)態(tài)批處理機(jī)制的一種約束,那我們總結(jié)一下動(dòng)態(tài)批處理的約束,各位也許也能從中找到為何動(dòng)態(tài)批處理在自己的項(xiàng)目中不起作用的原因:
所以,盡量使用靜態(tài)的批處理。
物理組件
曾幾何時(shí),匹夫在做一個(gè)策略類游戲的時(shí)候需要在單元格上排兵布陣,而要偵測(cè)到哪個(gè)兵站在哪個(gè)格子匹夫選擇使用了射線,由于士兵單位很多,而且為了精確每一幀都會(huì)執(zhí)行檢測(cè),那時(shí)候CPU的負(fù)擔(dān)叫一個(gè)慘不忍睹。后來(lái)匹夫果斷放棄了這種做法,并且對(duì)物理組件產(chǎn)生了心理的陰影。
這里匹夫只提2點(diǎn)匹夫感覺(jué)比較重要的優(yōu)化措施:
1.設(shè)置一個(gè)合適的Fixed Timestep。設(shè)置的位置如圖:
那何謂“合適”呢?首先我們要搞明白Fixed Timestep和物理組件的關(guān)系。物理組件,或者說(shuō)游戲中模擬各種物理效果的組件,最重要的是什么呢?計(jì)算啊。對(duì),需要通過(guò)計(jì)算才能將真實(shí)的物理效果展現(xiàn)在虛擬的游戲中。那么Fixed Timestep這貨就是和物理計(jì)算有關(guān)的啦。所以,若計(jì)算的頻率太高,自然會(huì)影響到CPU的開銷。同時(shí),若計(jì)算頻率達(dá)不到游戲設(shè)計(jì)時(shí)的要求,有會(huì)影響到功能的實(shí)現(xiàn),所以如何抉擇需要各位具體分析,選擇一個(gè)合適的值。
2.就是不要使用網(wǎng)格碰撞器(mesh collider):為啥?因?yàn)閷?shí)在是太復(fù)雜了。網(wǎng)格碰撞器利用一個(gè)網(wǎng)格資源并在其上構(gòu)建碰撞器。對(duì)于復(fù)雜網(wǎng)狀模型上的碰撞檢測(cè),它要比應(yīng)用原型碰撞器精確的多。標(biāo)記為凸起的(Convex )的網(wǎng)格碰撞器才能夠和其他網(wǎng)格碰撞器發(fā)生碰撞。各位上網(wǎng)搜一下mesh collider的圖片,自然就會(huì)明白了。我們的手機(jī)游戲自然無(wú)需這種性價(jià)比不高的東西。
當(dāng)然,從性能優(yōu)化的角度考慮,物理組件能少用還是少用為好。
處理內(nèi)存,卻讓CPU受傷的GC
在CPU的部分聊GC,感覺(jué)是不是怪怪的?其實(shí)小匹夫不這么覺(jué)得,雖然GC是用來(lái)處理內(nèi)存的,但的確增加的是CPU的開銷。因此它的確能達(dá)到釋放內(nèi)存的效果,但代價(jià)更加沉重,會(huì)加重CPU的負(fù)擔(dān),因此對(duì)于GC的優(yōu)化目標(biāo)就是盡量少的觸發(fā)GC。
首先我們要明確所謂的GC是Mono運(yùn)行時(shí)的機(jī)制,而非Unity3D游戲引擎的機(jī)制,所以GC也主要是針對(duì)Mono的對(duì)象來(lái)說(shuō)的,而它管理的也是Mono的托管堆。 搞清楚這一點(diǎn),你也就明白了GC不是用來(lái)處理引擎的assets(紋理啦,音效啦等等)的內(nèi)存釋放的,因?yàn)閁3D引擎也有自己的內(nèi)存堆而不是和Mono一起使用所謂的托管堆。
其次我們要搞清楚什么東西會(huì)被分配到托管堆上?不錯(cuò)咯,就是引用類型咯。比如類的實(shí)例,字符串,數(shù)組等等。而作為int,float,包括結(jié)構(gòu)體struct其實(shí)都是值類型,它們會(huì)被分配在堆棧上而非堆上。所以我們關(guān)注的對(duì)象無(wú)外乎就是類實(shí)例,字符串,數(shù)組這些了。
那么GC什么時(shí)候會(huì)觸發(fā)呢?兩種情況:
所以為了達(dá)到優(yōu)化CPU的目的,我們就不能頻繁的觸發(fā)GC。而上文也說(shuō)了GC處理的是托管堆,而不是Unity3D引擎的那些資源,所以GC的優(yōu)化說(shuō)白了也就是代碼的優(yōu)化。那么匹夫覺(jué)得有以下幾點(diǎn)是需要注意的:
代碼?腳本?
聊到代碼這個(gè)話題,也許有人會(huì)覺(jué)得匹夫多此一舉。因?yàn)榇a質(zhì)量因人而異,很難像上面提到的幾點(diǎn),有一個(gè)明確的評(píng)判標(biāo)準(zhǔn)。也是,公寫公有理,婆寫婆有理。但是匹夫這里要提到的所謂代碼質(zhì)量是基于一個(gè)前提的:Unity3D是用C++寫的,而我們的代碼是用C#作為腳本來(lái)寫的,那么問(wèn)題就來(lái)了~腳本和底層的交互開銷是否需要考慮呢?也就是說(shuō),我們用Unity3D寫游戲的“游戲腳本語(yǔ)言”,也就是C#是由mono運(yùn)行時(shí)托管的。而功能是底層引擎的C++實(shí)現(xiàn)的,“游戲腳本”中的功能實(shí)現(xiàn)都離不開對(duì)底層代碼的調(diào)用。那么這部分的開銷,我們應(yīng)該如何優(yōu)化呢?
1.以物體的Transform組件為例,我們應(yīng)該只訪問(wèn)一次,之后就將它的引用保留,而非每次使用都去訪問(wèn)。這里有人做過(guò)一個(gè)小實(shí)驗(yàn),就是對(duì)比通過(guò)方法GetComponent<Transform>()獲取Transform組件, 通過(guò)MonoBehavor的transform屬性去取,以及保留引用之后再去訪問(wèn)所需要的時(shí)間:
- GetComponent = 619ms
- Monobehaviour = 60ms
- CachedMB = 8ms
- Manual Cache = 3ms
2.如上所述,最好不要頻繁使用GetComponent,尤其是在循環(huán)中。
3.善于使用OnBecameVisible()和OnBecameVisible(),來(lái)控制物體的update()函數(shù)的執(zhí)行以減少開銷。
4.使用內(nèi)建的數(shù)組,比如用Vector3.zero而不是new Vector(0, 0, 0);
5.對(duì)于方法的參數(shù)的優(yōu)化:善于使用ref關(guān)鍵字。值類型的參數(shù),是通過(guò)將實(shí)參的值復(fù)制到形參,來(lái)實(shí)現(xiàn)按值傳遞到方法,也就是我們通常說(shuō)的按值傳遞。復(fù)制嘛,總會(huì)讓人感覺(jué)很笨重。比如Matrix4x4這樣比較復(fù)雜的值類型,如果直接復(fù)制一份新的,反而不如將值類型的引用傳遞給方法作為參數(shù)。
好啦,CPU的部分匹夫覺(jué)得到此就介紹的差不多了。下面就簡(jiǎn)單聊聊其實(shí)匹夫并不是十分熟悉的部分,GPU的優(yōu)化。
GPU的優(yōu)化
GPU與CPU不同,所以側(cè)重點(diǎn)自然也不一樣。GPU的瓶頸主要存在在如下的方面:
那么針對(duì)以上4點(diǎn),其實(shí)仔細(xì)分析我們就可以發(fā)現(xiàn),影響的GPU性能的無(wú)非就是2大方面,一方面是頂點(diǎn)數(shù)量過(guò)多,像素計(jì)算過(guò)于復(fù)雜。另一方面就是GPU的顯存帶寬。那么針鋒相對(duì)的兩方面舉措也就十分明顯了。
減少繪制的數(shù)目
那么第一個(gè)方面的優(yōu)化也就是減少頂點(diǎn)數(shù)量,簡(jiǎn)化復(fù)雜度,具體的舉措就總結(jié)如下了:
- 保持材質(zhì)的數(shù)目盡可能少。這使得Unity更容易進(jìn)行批處理。
- 使用紋理圖集(一張大貼圖里包含了很多子貼圖)來(lái)代替一系列單獨(dú)的小貼圖。它們可以更快地被加載,具有很少的狀態(tài)轉(zhuǎn)換,而且批處理更友好。
- 如果使用了紋理圖集和共享材質(zhì),使用Renderer.sharedMaterial?來(lái)代替Renderer.material?。
- 使用光照紋理(lightmap)而非實(shí)時(shí)燈光。
- 使用LOD,好處就是對(duì)那些離得遠(yuǎn),看不清的物體的細(xì)節(jié)可以忽略。
- 遮擋剔除(Occlusion culling)
- 使用mobile版的shader。因?yàn)楹?jiǎn)單。
優(yōu)化顯存帶寬
第二個(gè)方向呢?壓縮圖片,減小顯存帶寬的壓力。
- OpenGL ES 2.0使用ETC1格式壓縮等等,在打包設(shè)置那里都有。
- 使用mipmap。
MipMap
這里匹夫要著重介紹一下MipMap到底是啥。因?yàn)橛腥苏f(shuō)過(guò)MipMap會(huì)占用內(nèi)存呀,但為何又會(huì)優(yōu)化顯存帶寬呢?那就不得不從MipMap是什么開始聊起。一張圖其實(shí)就能解決這個(gè)疑問(wèn)。
上面是一個(gè)mipmap 如何儲(chǔ)存的例子,左邊的主圖伴有一系列逐層縮小的備份小圖
是不是很一目了然呢?Mipmap中每一個(gè)層級(jí)的小圖都是主圖的一個(gè)特定比例的縮小細(xì)節(jié)的復(fù)制品。因?yàn)榇媪酥鲌D和它的那些縮小的復(fù)制品,所以內(nèi)存占用會(huì)比之前大。但是為何又優(yōu)化了顯存帶寬呢?因?yàn)榭梢愿鶕?jù)實(shí)際情況,選擇適合的小圖來(lái)渲染。所以,雖然會(huì)消耗一些內(nèi)存,但是為了圖片渲染的質(zhì)量(比壓縮要好),這種方式也是推薦的。
內(nèi)存的優(yōu)化
既然要聊Unity3D運(yùn)行時(shí)候的內(nèi)存優(yōu)化,那我們自然首先要知道Unity3D游戲引擎是如何分配內(nèi)存的。大概可以分成三大部分:
第3類不是我們關(guān)注的重點(diǎn),所以接下來(lái)我們會(huì)分別來(lái)看一下Unity3D內(nèi)部?jī)?nèi)存和Mono托管內(nèi)存,最后還將分析一個(gè)官網(wǎng)上Assetbundle的案例來(lái)說(shuō)明內(nèi)存的管理。
Unity3D內(nèi)部?jī)?nèi)存
Unity3D的內(nèi)部?jī)?nèi)存都會(huì)存放一些什么呢?各位想一想,除了用代碼來(lái)驅(qū)動(dòng)邏輯,一個(gè)游戲還需要什么呢?對(duì),各種資源。所以簡(jiǎn)單總結(jié)一下Unity3D內(nèi)部?jī)?nèi)存存放的東西吧:
- 資源:紋理、網(wǎng)格、音頻等等
- GameObject和各種組件。
- 引擎內(nèi)部邏輯需要的內(nèi)存:渲染器,物理系統(tǒng),粒子系統(tǒng)等等
Mono托管內(nèi)存
因?yàn)槲覀兊挠螒蚰_本是用C#寫的,同時(shí)還要跨平臺(tái),所以帶著一個(gè)Mono的托管環(huán)境顯然必須的。那么Mono的托管內(nèi)存自然就不得不放到內(nèi)存的優(yōu)化范疇中進(jìn)行考慮。那么我們所說(shuō)的Mono托管內(nèi)存中存放的東西和Unity3D內(nèi)部?jī)?nèi)存中存放的東西究竟有何不同呢?其實(shí)Mono的內(nèi)存分配就是很傳統(tǒng)的運(yùn)行時(shí)內(nèi)存的分配了:
- 值類型:int型啦,float型啦,結(jié)構(gòu)體struct啦,bool啦之類的。它們都存放在堆棧上(注意額,不是堆所以不涉及GC)。
- 引用類型:其實(shí)可以狹義的理解為各種類的實(shí)例。比如游戲腳本中對(duì)游戲引擎各種控件的封裝。其實(shí)很好理解,C#中肯定要有對(duì)應(yīng)的類去對(duì)應(yīng)游戲引擎中的控件。那么這部分就是C#中的封裝。由于是在堆上分配,所以會(huì)涉及到GC。
而Mono托管堆中的那些封裝的對(duì)象,除了在在Mono托管堆上分配封裝類實(shí)例化之后所需要的內(nèi)存之外,還會(huì)牽扯到其背后對(duì)應(yīng)的游戲引擎內(nèi)部控件在Unity3D內(nèi)部?jī)?nèi)存上的分配。
舉一個(gè)例子:
一個(gè)在.cs腳本中聲明的WWW類型的對(duì)象www,Mono會(huì)在Mono托管堆上為www分配它所需要的內(nèi)存。同時(shí),這個(gè)實(shí)例對(duì)象背后的所代表的引擎資源所需要的內(nèi)存也需要被分配。
一個(gè)WWW實(shí)例背后的資源:
- 壓縮的文件
- 解壓縮所需的緩存
- 解壓縮之后的文件
如圖:
那么下面就舉一個(gè)AssetBundle的例子:
Assetbundle的內(nèi)存處理
以下載Assetbundle為例子,聊一下內(nèi)存的分配。匹夫從官網(wǎng)的手冊(cè)上找到了一個(gè)使用Assetbundle的情景如下:
| 123456789101112131415161718192021 | IEnumerator DownloadAndCache (){????????// Wait for the Caching system to be ready????????while (!Caching.ready)????????????yield return null;????????// Load the AssetBundle file from Cache if it exists with the same version or download and store it in the cache????????using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){????????????yield return www; //WWW是第1部分????????????if (www.error != null)????????????????throw new Exception("WWW download had an error:" + www.error);????????????AssetBundle bundle = www.assetBundle;//AssetBundle是第2部分????????????if (AssetName == "")????????????????Instantiate(bundle.mainAsset);//實(shí)例化是第3部分????????????else????????????????Instantiate(bundle.Load(AssetName));????????????????????// Unload the AssetBundles compressed contents to conserve memory????????????????????bundle.Unload(false);????????} // memory is freed from the web stream (www.Dispose() gets called implicitly)????}} |
內(nèi)存分配的三個(gè)部分匹夫已經(jīng)在代碼中標(biāo)識(shí)了出來(lái):
那就分別解析一下:
| 1 | WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version) |
| 1 | AssetBundle bundle = www.assetBundle; |
| 1 | Instantiate(bundle.mainAsset); |
通過(guò)AssetBundle獲取資源,實(shí)例化對(duì)象
最后各位可能看到了官網(wǎng)中的這個(gè)例子使用了:
| 12 | using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){} |
這種using的用法。這種用法其實(shí)就是為了在使用完Web Stream之后,將內(nèi)存釋放掉的。因?yàn)閃WW也繼承了idispose的接口,所以可以使用using的這種用法。其實(shí)相當(dāng)于最后執(zhí)行了:
| 1 2 | //刪除Web Stream www.Dispose(); |
OK,Web Stream被刪除掉了。那還有誰(shuí)呢?對(duì)Assetbundle。那么使用
| 1 2 | //刪除AssetBundle bundle.Unload(false); |
ok,寫到這里就先打住啦。寫的有點(diǎn)超了。有點(diǎn)趕也有點(diǎn)臨時(shí),日后在補(bǔ)充編輯。
總結(jié)
以上是生活随笔為你收集整理的unity3d优化收集摘录的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: elasticsearch之增删改查与其
- 下一篇: SLM激光切片软件开发