文章比較長(zhǎng),但是滿滿的是干貨。
一、移動(dòng)平臺(tái)硬件架構(gòu)
移動(dòng)平臺(tái)無(wú)論是Android 還是 IOS 用的都是統(tǒng)一內(nèi)存架構(gòu),GPU和CPU共享一個(gè)物理內(nèi)存,通常我們有“顯存”和“內(nèi)存”兩種叫法,可以認(rèn)為是這塊物理內(nèi)存的所有者不同,當(dāng)這段映射到cpu,就是通常意義上的內(nèi)存;當(dāng)映射到gpu,就是通常意義上的顯存。并且同一段物理內(nèi)存同一時(shí)刻只會(huì)映射到一個(gè)device。
即使是在同一物理內(nèi)存上 ,之前的openGL ES規(guī)范中CPU和GPU之間的內(nèi)存是不能共享的,vertex和texture的buffer是需要拷貝的。后面出來(lái)的vulkan 與IOS的metal 可以共享內(nèi)存。
了解了移動(dòng)平臺(tái)的硬件架構(gòu),就知道了 1)CPU 2) 帶寬 3) GPU 4) 內(nèi)存 都有可能成為移動(dòng)平臺(tái)3D應(yīng)用性能瓶頸。
二、移動(dòng)平臺(tái)3D應(yīng)用的畫(huà)面渲染過(guò)程
1、CPU通過(guò)調(diào)用繪制命令(稱(chēng)為一次Draw Call)來(lái)告訴GPU開(kāi)始進(jìn)行一個(gè)渲染過(guò)程的。一個(gè)Draw Call命令會(huì)指向本次繪制需要渲染的信息,這些信息包括:頂點(diǎn)數(shù)據(jù)、紋理數(shù)據(jù)、shader參數(shù)(光照模型、法線方向、光照方向等)等,簡(jiǎn)單地說(shuō)就 畫(huà)什么,用什么畫(huà),怎么畫(huà)。
2、GPU接收到Draw Call命令之后就會(huì)開(kāi)始進(jìn)行一次單元渲染,關(guān)于GPU的單元渲染的過(guò)程是這樣的(簡(jiǎn)單示意圖):
1)從顯存中取出拷貝的頂點(diǎn)數(shù)據(jù)和光照模型。
2)通過(guò)頂點(diǎn)處理器(Vertex Processor)對(duì)頂點(diǎn)數(shù)據(jù)進(jìn)行一系列的變換和光照處理,包括裁剪處理。tips: 簡(jiǎn)單的想想,游戲中的各個(gè)物體的坐標(biāo)都是參照游戲中的世界坐標(biāo)系的,而實(shí)際顯示的畫(huà)面是玩家視角或者攝像機(jī)視角,這中間就有許多坐標(biāo)系的轉(zhuǎn)換。這些活就需要頂點(diǎn)處理器來(lái)做,最終我們得到了我們所需要視角的畫(huà)面。
3)到這一步,畫(huà)面還只是一些多邊形,而實(shí)際顯示在屏幕上的是一個(gè)個(gè)像素,這里就需要(光柵處理器)Rasterizer進(jìn)行光柵化(Rasterization),從而將畫(huà)面變成一個(gè)像素圖,把所有的頂點(diǎn)對(duì)應(yīng)到一個(gè)一個(gè)的像素位置。
4)對(duì)這些像素進(jìn)行上色,通過(guò)片元處理器(Fragment Processor)中的像素著色器(Pixel Shader)按照shader光照模型,根據(jù)紋理對(duì)應(yīng)位置顏色,計(jì)算元顏色,再經(jīng)過(guò)深度計(jì)算、透明測(cè)試計(jì)算出每個(gè)像素的最終顏色。
5)把結(jié)果輸出到圖像緩存中,全部完成后拿去顯示。
三、Unity3D應(yīng)用性能優(yōu)化之CPU
CPU的優(yōu)化非常重要,CPU的表現(xiàn)直接決定了VR應(yīng)用的幀率,應(yīng)用的耗電量,發(fā)熱量。我們來(lái)看看相比于普通的app,VR應(yīng)用的CPU都承擔(dān)了什么責(zé)任:a、業(yè)務(wù)邏輯 b、網(wǎng)絡(luò)通信 c、I/O操作 d、drawcall e、physic邏輯 f、GC內(nèi)存回收 g、垂直同步等待。
業(yè)務(wù)邏輯、網(wǎng)絡(luò)通信、I/O操作
這一塊的優(yōu)化和普通的app差不多。
關(guān)于業(yè)務(wù)邏輯:有些不同的是Unity腳本中有一類(lèi)update方法(Update、FixedUpdate、OnGUI等),這一類(lèi)方法是在每幀刷新的時(shí)候調(diào)用的,是比較影響每幀耗時(shí)的,為了嚴(yán)格控制這一部分的執(zhí)行時(shí)間,需要注意的以下幾點(diǎn):
a、盡量不要再Update函數(shù)中做復(fù)雜計(jì)算,如有需要,可以隔N幀計(jì)算一次,對(duì)于純數(shù)學(xué)計(jì)算,可以開(kāi)辟新線程來(lái)計(jì)算(Unity 為什么一般避免使用多線程, 實(shí)際上大多數(shù)游戲引擎也都是單線程的, 因?yàn)榇蠖鄶?shù)游戲引擎是主循環(huán)結(jié)構(gòu), 邏輯更新和畫(huà)面更新的時(shí)間點(diǎn)要求有確定性, 如果在邏輯更新和畫(huà)面更新中引入多線程, 就需要做同步而這加大了游戲的開(kāi)發(fā)難度。UnityEngine絕大多數(shù)類(lèi)是不支持子線程的,所以一般只有純數(shù)學(xué)計(jì)算才會(huì)用到子線程去計(jì)算。)
b、關(guān)閉所有在update類(lèi)中執(zhí)行l(wèi)og的打印操作(Unity中一次log打印有時(shí)長(zhǎng)達(dá)7ms,Profiler數(shù)據(jù))。
c、不在update類(lèi)方法調(diào)用Getcomponent、SendMessage、FindWithTag這幾個(gè)耗時(shí)較長(zhǎng)的方法。
d、不在update類(lèi)方法中使用臨時(shí)變量。
關(guān)于網(wǎng)絡(luò)通信、I/O操作:這些普通app的優(yōu)化和注意點(diǎn)沒(méi)有什么很大區(qū)別,有一點(diǎn)是,Unity工程中使用了資源動(dòng)態(tài)加載,有些資源是保存在服務(wù)器端的,在有必要的時(shí)候才會(huì)通過(guò)網(wǎng)絡(luò)load下去加載。這個(gè)資源動(dòng)態(tài)加載需要注意一個(gè)問(wèn)題:由于網(wǎng)絡(luò)通信過(guò)程,CPU總是處于等待的狀態(tài),一般資源下載是多線程同時(shí)操作,為了盡快上屏顯示資源(在這個(gè)工程中是一些圖片和英雄的3D模型),但是資源有可能是在同一個(gè)幀周期中下載完畢的,如果直接加載的話,可能會(huì)出現(xiàn)Camera瞬時(shí)渲染過(guò)多三角形面,造成渲染時(shí)間(Camera.Render()函數(shù)執(zhí)行時(shí)間)過(guò)長(zhǎng),,卡頓的現(xiàn)象。所以這里要注意,網(wǎng)絡(luò)下載可以多線程多任務(wù)同時(shí)下載,但是在Unity主線程,要避免出現(xiàn)同時(shí)加載大型模型和大紋理的情況,最好使用隊(duì)列的方式,保證一幀只渲染一個(gè)3D模型。
關(guān)于GC
為什么要把GC放在CPU這一部分?雖然GC是用來(lái)處理內(nèi)存回收的,但是卻增加了CPU的開(kāi)銷(xiāo)(GC一次開(kāi)銷(xiāo)可長(zhǎng)可短,有時(shí)長(zhǎng)達(dá)100ms)。因此對(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ì)被分配到托管堆上?對(duì),就是引用類(lèi)型。引用類(lèi)型包括:用戶(hù)自定義的類(lèi),接口,委托,數(shù)組,字符串,Object.而值類(lèi)型包括:幾種基本數(shù)據(jù)類(lèi)型(如:int,float,bool等),結(jié)構(gòu)體,枚舉,空類(lèi)型。所以GC的優(yōu)化也就是代碼的優(yōu)化。
那么GC什么時(shí)候會(huì)觸發(fā)呢??jī)煞N情況:
a、當(dāng)我們的堆的內(nèi)存不足時(shí),會(huì)自動(dòng)調(diào)用GC來(lái)回收內(nèi)存。
b、手動(dòng)的調(diào)用GC,用System.GC.Collect(),一般情況下,不建議手動(dòng)去手動(dòng)進(jìn)行內(nèi)存回收,因?yàn)槿菀壮霈F(xiàn)問(wèn)題。
檢查整個(gè)工程代碼,關(guān)于減少GC這一方面的優(yōu)化經(jīng)驗(yàn)總結(jié)大概如下:
1、字符串連接的處理。因?yàn)閷蓚€(gè)字符串連接的過(guò)程,其實(shí)是生成一個(gè)新的字符串的過(guò)程。而之前的舊的字符串自然而然就成為了垃圾。而作為引用類(lèi)型的字符串,其空間是在堆上分配的,被棄置的舊的字符串的空間會(huì)被GC當(dāng)做垃圾回收,可以使用StringBuilder來(lái)解決(注意:C#沒(méi)有StringBuffer,Java里才有!!String 在進(jìn)行運(yùn)算時(shí)(如賦值、拼接等)會(huì)產(chǎn)生一個(gè)新的實(shí)例,而 StringBuilder 則不會(huì)。所以在大量字符串拼接或頻繁對(duì)某一字符串進(jìn)行操作時(shí)最好使用 StringBuilder,不要使用 String)。
2、盡量不要使用foreach,而是使用for。foreach其實(shí)會(huì)涉及到迭代器的使用,而據(jù)傳說(shuō)每一次循環(huán)所產(chǎn)生的迭代器會(huì)帶來(lái)24 Bytes的垃圾。那么循環(huán)10次就是240Bytes。新版本的unity已經(jīng)修復(fù)該問(wèn)題了。
3、不要直接訪問(wèn)gameobject的tag屬性。比如if (go.tag ==“human”)最好換成if (go.CompareTag (“human”))。因?yàn)樵L問(wèn)物體的tag屬性(每次Object.name也會(huì)分配39B的堆內(nèi)存.)會(huì)在堆上額外的分配空間。如果在循環(huán)中這么處理,留下的垃圾就可想而知了。
4、不要實(shí)例化(Instantiate)和(Destroy)對(duì)象,事先建好對(duì)象池,以實(shí)現(xiàn)空間的重復(fù)利用。
5、在某些可能的情況下,可以使用結(jié)構(gòu)(struct)來(lái)代替類(lèi)(class)。這是因?yàn)椋Y(jié)構(gòu)變量主要存放在棧區(qū)而非堆區(qū)。因?yàn)闂5姆峙漭^快,并且不調(diào)用垃圾回收操作,所以當(dāng)結(jié)構(gòu)變量比較小時(shí)可以提升程序的運(yùn)行性能。但是當(dāng)結(jié)構(gòu)體較大時(shí),雖然它仍可避免分配/回收的開(kāi)銷(xiāo),而它由于”傳值”操作也會(huì)導(dǎo)致單獨(dú)的開(kāi)銷(xiāo),實(shí)際上它可能比等效對(duì)象類(lèi)的效率還要低。所以要注意選擇。
6、場(chǎng)景切換時(shí),可以主動(dòng)進(jìn)行垃圾回收(調(diào)用System.GC.Collect()),從而及時(shí)去除游戲中已經(jīng)不必要地內(nèi)存占用。
Draw Call 的優(yōu)化
前面說(shuō)過(guò)了,DrawCall是CPU調(diào)用底層圖形接口的操作。比如有上千個(gè)物體,每一個(gè)的渲染都需要去調(diào)用一次底層接口,而每一次的調(diào)用CPU都需要做很多工作,那么CPU必然不堪重負(fù)。但是對(duì)于GPU來(lái)說(shuō),圖形處理的工作量是一樣的。
我們先來(lái)看看Draw Call對(duì)CPU的消耗大概是一個(gè)什么級(jí)別的量:
NVIDIA 在 GDC 曾提出,25K batchs/sec 會(huì)吃滿 1GHz 的 CPU,100%的使用率。有一個(gè)公式可以和清楚得計(jì)算出在給定的CPU資源 與 幀率的情況下,最多能有多少個(gè)DrawCall。
DrawCall_Num = 25K * CPU_Frame * CPU_Percentage / Framerate。
DrawCall_Num : DrawCall數(shù)量
CPU_Frame : CPU 工作頻率(GHz單位)
CPU_Percentage:CPU 分配在DrawCall這件事情上的時(shí)間率(百分比)
Framerate:希望的游戲幀率
比如說(shuō)我們使用一個(gè)高通820,工作頻率在2GHz上,分配10%的CPU時(shí)間給DrawCall上,并且我們VR要求60幀,那么一幀最多能有83個(gè)DrawCall(由于雙camera的存在,單眼DrawCall只能保證在41個(gè)以?xún)?nèi))。其實(shí),google官方的建議是單眼DrawCall不多于50個(gè)。
所以對(duì)DrawCall的優(yōu)化,主要就是為了盡量解放CPU在調(diào)用圖形接口上的開(kāi)銷(xiāo)。所以針對(duì)drawcall我們主要的思路就是每個(gè)物體盡量減少渲染次數(shù),多個(gè)物體最好一起渲染。那么DrawCall次數(shù)的優(yōu)化有哪些方案呢?
DC Batching(DC批處理)
batch即批處理,DrawCall batching即DC的批處理,即把多次DrawCall合并成一次DrawCall的方案。
Dynamic Batching 動(dòng)態(tài)批處理
Unity引擎對(duì)于使用相同材質(zhì)的物體會(huì)自動(dòng)進(jìn)行批處理,相同材質(zhì)意味著shader完全一樣,這一部分主要是要注意那些破壞這一特性的人為因素,比如說(shuō):
1、批處理動(dòng)態(tài)物體需要在每個(gè)頂點(diǎn)上進(jìn)行一定的開(kāi)銷(xiāo),所以動(dòng)態(tài)批處理僅支持小于900頂點(diǎn)的網(wǎng)格物體,如果你的著色器使用頂點(diǎn)位置,法線和UV值三種屬性,那么你只能批處理300頂點(diǎn)以下的物體(如果在這基礎(chǔ)上還使用了UV2,則只能批處理180頂點(diǎn)以下的物體);請(qǐng)注意:屬性數(shù)量的限制可能會(huì)在將來(lái)進(jìn)行改變。
2、使用不同的縮放比例的物體,unity將無(wú)法對(duì)這些物體進(jìn)行批處理。比如(1,1,1)和(1,2,2)就不會(huì)動(dòng)態(tài)批處理,但是(1,1,1)和(2,2,2)會(huì)動(dòng)態(tài)批處理。
最新的官方說(shuō)明是鏡像不會(huì)動(dòng)態(tài)合批,更多關(guān)于動(dòng)態(tài)合批與靜態(tài)合批可以參考筆者之前的一篇文章Unity動(dòng)態(tài)合批(Dynamic Batching)與靜態(tài)合批(Static Batching)
3、擁有l(wèi)ightmap的物體含有額外(隱藏)的材質(zhì)屬性,比如:lightmap的偏移和縮放系數(shù)等。所以,擁有l(wèi)ightmap的物體將不會(huì)進(jìn)行批處理(除非他們指向lightmap的同一部分)。接受實(shí)時(shí)陰影的物體也不會(huì)批處理。
4、多通道的shader會(huì)中斷批處理操作(為了達(dá)到特殊的渲染目的,可能某個(gè)物體要多遍渲染.這是就要多個(gè)通道)。
5、在腳本中動(dòng)態(tài)地指定了物體的材質(zhì),也不會(huì)進(jìn)行批處理。
Static Batching 靜態(tài)批處理
動(dòng)態(tài)批處理雖然是自動(dòng)的,但是限制非常多,不小心就會(huì)打破批處理,所以u(píng)nity在專(zhuān)業(yè)版中還提供了靜態(tài)批處理,靜態(tài)批處理要求是想批處理的物體一定是static的,靜態(tài)的,不會(huì)改變位置和旋轉(zhuǎn)角度以及縮放的,且必須材質(zhì)一致。其原理是把物體的網(wǎng)格進(jìn)行合并,變成一個(gè)靜態(tài)的更大的網(wǎng)格物體,再使用一個(gè)統(tǒng)一的材質(zhì)進(jìn)行渲染。
知道了它的原理,它的某些坑就比較清晰了:
1、在一個(gè)平行光、環(huán)境光下,沒(méi)有問(wèn)題,但是如果你使用了多個(gè)平行光,點(diǎn)光源,聚光燈這種復(fù)雜的光源去照射物體,那么靜態(tài)批處理就會(huì)被打斷。(項(xiàng)目中就遇到過(guò),因?yàn)閮蛇呌袃膳庞⑿勰P停詧?chǎng)景中使用了兩個(gè)不同平行光,場(chǎng)景中勾選的static物體并沒(méi)有被合并drawcall,經(jīng)過(guò)一番折磨才找到原因)。
2、如果靜態(tài)批處理前有一些物體共享了相同的網(wǎng)格,那么每一個(gè)物體都會(huì)有一個(gè)該網(wǎng)格的復(fù)制品(本來(lái)unity只會(huì)保留一份,但是靜態(tài)批處理會(huì)生成新的一個(gè)大網(wǎng)格,所以會(huì)保留所有物體的網(wǎng)格,最后合并),即一個(gè)網(wǎng)格會(huì)變成多個(gè)網(wǎng)格被發(fā)送給GPU。這樣會(huì)造成內(nèi)存的使用變大,需要注意這個(gè)問(wèn)題,但是一般場(chǎng)景中使用相同網(wǎng)格的物體會(huì)比較少。
3、對(duì)于那些shader相同,紋理不同導(dǎo)致的不同材質(zhì)無(wú)法進(jìn)行批處理的物體(比如項(xiàng)目中的場(chǎng)景環(huán)境,基座,地面,其實(shí)都使用了unity自帶的standard shader)可以通過(guò)紋理合并的方法來(lái)使得它們可以被靜態(tài)批處理。這就引發(fā)了下面的事情:
BUS總線帶寬
CPU完成一次DrawCall,除了需要發(fā)一個(gè)DrawCall的命令之外,還需要把內(nèi)存中頂點(diǎn)數(shù)據(jù)、紋理貼圖、shader參數(shù)通過(guò)bus總線拷貝到內(nèi)存分配給GPU的顯存之中,注意這是拷貝,不是指針傳遞,速度不快。如果一次drawcall傳遞的數(shù)據(jù)過(guò)大,帶寬成為了瓶頸,那就會(huì)大大影響效率(其它的DrawCall無(wú)法出發(fā),GPU又處于閑置)。這種情況最有可能出現(xiàn)在為了減少DrawCall,瘋狂的合并紋理上。在項(xiàng)目中,UI的DrawCall調(diào)用占了很大一部分,也會(huì)最難優(yōu)化的,為了減少drawcall ,我們把UI模塊的靜態(tài)部分(一些UI的底板,背景等不會(huì)發(fā)生變化的)全部合并成了一個(gè)紋理,最后導(dǎo)致了DrawCall下降了,但是幀率卻也下降了,內(nèi)存使用也增加了,原因就是這個(gè)。在項(xiàng)目中,不會(huì)同時(shí)出現(xiàn)的元素不要打包到一起,保證單張合并紋理不大于10241024一般就不會(huì)有問(wèn)題了(王者榮耀最大紋理限制在了256256)。
DrawCall的優(yōu)化大概就是這些,優(yōu)化的目標(biāo)其實(shí)是往一個(gè)目標(biāo)上靠,cpu的DrawCall命令剛剛好能被GPU消化,不要讓CPU等待(帶寬限制),也不要讓GPU閑置。如果即使做到了這個(gè),應(yīng)用幀率還是上不去,那么就只能去削減場(chǎng)景,做有損優(yōu)化了。
Physics
Unity內(nèi)置NVIDIA PhysX物理引擎,來(lái)模擬物理世界的一些效果,比如說(shuō)重力、阻力、彈性、碰撞這些,其中使用了一些內(nèi)置的組件來(lái)實(shí)現(xiàn)這些模擬,用的比較多的如:剛體(Rigidbody) 各種碰撞器(Collider) 恒力 (Constant Force) 物理材質(zhì)(Physic Material)鉸鏈關(guān)節(jié)(Hinge Joint)彈簧關(guān)節(jié)(Spring Joint)。
unity除了提供了一些重要的組件之外,在unity腳本中的生命周期中提供了一個(gè)專(zhuān)門(mén)為物理計(jì)算的刷新方法:
FixedUpdate()。FixedUpdate跟Update的區(qū)別在于,這兩個(gè)函數(shù)處于不同的“幀循環(huán)”中,F(xiàn)ixedUpdate處于Physics循環(huán)中,而Update不是。所以這兩個(gè)函數(shù)的使用也有了不同。Update的執(zhí)行受場(chǎng)景GameObject的渲染的影響,三角形的數(shù)量越多,渲染所需要的時(shí)間也就越長(zhǎng)。FixedUpate的執(zhí)行則不受這些影響。所以,Update每個(gè)渲染幀之間的間隔是不相等的,而Fixedupdate在每個(gè)渲染幀之間的時(shí)間間隔是相等的。由于關(guān)系到物理模擬,所以一般涉及到物理組件,都需要放在Fixedupdate中進(jìn)行計(jì)算。那么關(guān)于physics,一般的優(yōu)化手段都有哪些呢?下面是一些經(jīng)驗(yàn)及總結(jié):
1、將物理模擬時(shí)間步間隔設(shè)置到合適的大小。 Fixed Timestep是和物理計(jì)算有關(guān)的,所以若計(jì)算的頻率太高,自然會(huì)影響到CPU的開(kāi)銷(xiāo)。同時(shí),若計(jì)算頻率達(dá)不到軟件設(shè)計(jì)時(shí)的要求,有會(huì)影響到功能的實(shí)現(xiàn),所以如何抉擇需要具體分析,選擇一個(gè)合適的值,一般大于16ms,小于30ms。可以通過(guò)Edit->Project Settings->Time來(lái)改變這個(gè)值。
2、謹(jǐn)慎使用網(wǎng)格碰撞器(Mesh Collider),過(guò)于消耗性能,一般使用更簡(jiǎn)單的碰撞器,或者使用基本幾何碰撞器合并的組合碰撞器。在這個(gè)項(xiàng)目中,把所有的網(wǎng)格碰撞體都拋棄了,都換成了box collider。
3、真實(shí)的物理(剛體)很消耗,不要輕易使用,盡量使用自己的代碼(數(shù)學(xué)計(jì)算)模仿假的物理。
4、最小化碰撞檢測(cè)請(qǐng)求(例如ray casts和sphere checks),盡量從每次檢查中獲得更多信息。
項(xiàng)目中涉及到物體的組件很少,關(guān)于physic的優(yōu)化肯定還有很多可以說(shuō)的,需要再去學(xué)習(xí)了。
VSync
簡(jiǎn)單地說(shuō),這是CPU優(yōu)化的最直接的一個(gè)方法。
科普:VSync垂直同步又稱(chēng)場(chǎng)同步(Vertical Hold),垂直同步信號(hào)決定了CRT從屏幕頂部畫(huà)到底部,再返回原始位置的時(shí)間。從CRT顯示器的顯示原理來(lái)看,單個(gè)像素組成了水平掃描線,水平掃描線在垂直方向的堆積形成了完整的畫(huà)面。顯示器的刷新率受顯卡DAC控制,顯卡DAC完成一幀的掃描后就會(huì)產(chǎn)生一個(gè)垂直同步信號(hào)(決定于屏幕的刷新率)。我們平時(shí)所說(shuō)的打開(kāi)垂直同步指的是將該信號(hào)送入顯卡3D圖形處理部分,從而讓顯卡在生成3D圖形時(shí)受垂直同步信號(hào)的制約(注意是制約)。
如果我們選擇等待垂直同步信號(hào)(也就是我們平時(shí)所說(shuō)的垂直同步打開(kāi)),那么在游戲中或許強(qiáng)勁的顯卡迅速的繪制完一屏的圖像,但是沒(méi)有垂直同步信號(hào)的到達(dá),顯卡無(wú)法繪制下一屏,只有等垂直同步的信號(hào)到達(dá),才可以繪制。這樣FPS自然要受到操作系統(tǒng)刷新率運(yùn)行值的制約。而如果我們選擇不等待垂直同步信號(hào)(也就是我們平時(shí)所說(shuō)的關(guān)閉垂直同步),那么游戲中作完一屏畫(huà)面,顯卡和顯示器無(wú)需等待垂直同步信號(hào)就可以開(kāi)始下一屏圖像的繪制,自然可以完全發(fā)揮顯卡的實(shí)力。但是不要忘記,正是因?yàn)榇怪蓖降拇嬖冢拍苁沟糜螒蜻M(jìn)程和顯示器刷新率同步,使得畫(huà)面更加平滑和穩(wěn)定。
取消了垂直同步信號(hào),固然可以換來(lái)更快的幀率,但是在圖像的連續(xù)性上勢(shì)必打折扣。
四、Unity3D應(yīng)用性能優(yōu)化之GPU
一般人說(shuō)DC的優(yōu)化占了unity3D軟件優(yōu)化的三分天下,那么GPU的優(yōu)化也占了三分天下。在了解GPU優(yōu)化都有哪些著手點(diǎn)之前,我們先了解一下GPU在3D軟件渲染中做了啥事:
頂點(diǎn)著色器
GPU接收頂點(diǎn)數(shù)據(jù)作為輸入傳遞給頂點(diǎn)著色器。頂點(diǎn)著色器的處理單元是頂點(diǎn),輸入進(jìn)來(lái)的每個(gè)頂點(diǎn)都會(huì)調(diào)用一次頂點(diǎn)著色器。(頂點(diǎn)著色器本身不可以創(chuàng)建或銷(xiāo)毀任何頂點(diǎn),并無(wú)法得到頂點(diǎn)與頂點(diǎn)之間的關(guān)系)。頂點(diǎn)著色器是完全可編程的,它主要完成的工作有:坐標(biāo)變換和逐頂點(diǎn)光照。 坐標(biāo)變換:就是對(duì)頂點(diǎn)的坐標(biāo)進(jìn)行某種變換—把頂點(diǎn)坐標(biāo)從模型空間轉(zhuǎn)換到齊次裁剪空間。頂點(diǎn)的多少直接決定了三角形面的多少,也直接決定了GPU的渲染流水線的工作量,所以減少頂點(diǎn)數(shù)是一個(gè)比較重要的優(yōu)化點(diǎn)。那么減少頂點(diǎn)怎么操作呢,又有哪些途徑?
1、優(yōu)化基本幾何體
3D軟件都是從模型制作開(kāi)始,在設(shè)計(jì)師建模的時(shí)候就要想到應(yīng)該盡可能地減少頂點(diǎn)數(shù),一些對(duì)于模型沒(méi)有影響、或是肉眼非常難察覺(jué)到區(qū)別的頂點(diǎn)都要盡可能去掉。比如在項(xiàng)目中,對(duì)于用戶(hù)背后的環(huán)境模型,一些樹(shù)木和石頭,視頻背面永遠(yuǎn)無(wú)法看見(jiàn)的神廟,能削減的都已經(jīng)削減了。
2、使用LOD(Level of detail)技術(shù)
LOD技術(shù)有點(diǎn)類(lèi)似于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ì)造成模擬的突變。
3、使用遮擋剔除(Occlusion culling)技術(shù)
遮擋剔除是用來(lái)消除躲在其他物件后面看不到的物件,這代表資源不會(huì)浪費(fèi)在計(jì)算那些看不到的頂點(diǎn)上,進(jìn)而提升性能。剛才神廟后面的剔除就屬于手動(dòng)的遮擋剔除。
遮擋剔除是一個(gè)PRO版才有的功能, 當(dāng)一個(gè)物體被其他物體遮擋住而不在攝像機(jī)的可視范圍內(nèi)時(shí)不對(duì)其進(jìn)行渲染。遮擋剔除在3D圖形計(jì)算中并不是自動(dòng)進(jìn)行的。因?yàn)樵诮^大多數(shù)情況下離 camera 最遠(yuǎn)的物體首先被渲染,靠近攝像機(jī)的物體后渲染并覆蓋先前渲染的物體(這被稱(chēng)為重復(fù)渲染”overdraw”). 遮擋剔除不同于視錐體剔除. 視錐體剔除只是不渲染攝像機(jī)視角范圍外的物體而對(duì)于被其他物體遮擋但依然在視角范圍內(nèi)的物體,則不會(huì)被剔除. 注意當(dāng)你使用遮擋剔除時(shí),視錐體剔除(Frustum Culling)依然有效。
中間操作
1、曲面細(xì)分著色器:是一個(gè)可選的著色器,主要用于細(xì)分圖元。
2、幾何著色器:是一個(gè)可選的著色器,可用于執(zhí)行逐圖元的著色操作,或者被用于產(chǎn)生更多的圖元。
3、裁剪:這一階段是可配置的。目的是把那些不在視野內(nèi)的頂點(diǎn)裁剪掉,并剔除某些三角形圖元的面片。部分在視野內(nèi)的圖元需要做裁剪處理,在裁剪邊緣產(chǎn)生新的頂點(diǎn)和三角形進(jìn)行處理。
4、屏幕映射:這一階段是可配置和編程的,負(fù)責(zé)把每個(gè)圖元的坐標(biāo)(三維坐標(biāo)系)轉(zhuǎn)換成屏幕坐標(biāo)(二維坐標(biāo)系)。
5、三角形設(shè)置:開(kāi)始進(jìn)入光柵化階段,不再是數(shù)學(xué)上點(diǎn)了,而會(huì)把所有的點(diǎn)都映射到屏幕的具體像素坐標(biāo)上,計(jì)算每條邊上的像素坐標(biāo)而得到三角形邊界的表示方式即為三角形設(shè)置。
6、三角形遍歷:這一階段會(huì)檢查每個(gè)像素是否被一個(gè)三角風(fēng)格所覆蓋。如果覆蓋的話,就會(huì)生成一個(gè)片元(一個(gè)片元并不是真正意義上的像素,而是包含了很多狀態(tài)的集合,這些狀態(tài)用于計(jì)算每個(gè)像素的最終顏色。
這些狀態(tài)包括了屏幕坐標(biāo)、深度信息,及從幾何階段輸出的頂點(diǎn)信息,如法線和紋理坐標(biāo)等。),這樣一個(gè)查找哪些像素被三角形覆蓋的過(guò)程就是三角形遍歷。
片元著色器
片元著色器的輸入就是上一階段對(duì)頂點(diǎn)信息插值得到的結(jié)果,更具體點(diǎn)說(shuō),是根據(jù)從頂點(diǎn)著色器中輸出的數(shù)據(jù)插值得到的。而這一階段的輸出是一個(gè)或者多個(gè)顏色值。這一階段可以完成很多重要的渲染技術(shù),如紋理采樣,但是它的局限在于,它僅可以影響單個(gè)片元。片元著色器是比較花時(shí)間的,因?yàn)樗亲罱K顏色的計(jì)算者,在某些情況下,例如復(fù)雜燈光環(huán)境下,片元著色器會(huì)出現(xiàn)GPU流水線主要的拖后腿的存在。為了讓片元著色器的計(jì)算更加快,我們需要從很多方面進(jìn)行提前的優(yōu)化:
1、盡量減少overdraw
片元著色器最容易拖后腿的情況就是,overdraw!和Android app的開(kāi)發(fā)一樣,就是同一個(gè)像素點(diǎn)繪制了多次,某些情況會(huì)造成計(jì)算力的浪費(fèi),增加耗電量。前面提到的遮擋剔除有減少overdraw非常有用。在PC上,資源無(wú)限,為了得到最準(zhǔn)確的渲染結(jié)果,繪制順序可能是從后往前繪制不透明物體,然后再繪制透明物體進(jìn)行混合。但是在移動(dòng)平臺(tái)上,對(duì)于不透明物體,我們可以設(shè)置從前往后繪制,對(duì)于有透明通道的物體(很多UI紋理就是含有透明通道的),再設(shè)置從后往前繪制。unity中shader設(shè)置為“Geometry” 隊(duì)列的對(duì)象總是從前往后繪制的,而其他固定隊(duì)(如“Transparent”“Overla”等)的物體,則都是從后往前繪制的。這意味這,我們可以盡量把物體的隊(duì)列設(shè)置為“Geometry” 。對(duì)于GUI,尤其要注意和設(shè)計(jì)師商量,能用不透明的設(shè)計(jì)就用不透明的,對(duì)于粒子效果,也要注意不要引入透明值,多半情況下,移動(dòng)平臺(tái)的粒子效果透明值沒(méi)有作用。
2、減少實(shí)時(shí)光照
移動(dòng)平臺(tái)的最大敵人。一個(gè)場(chǎng)景里如果包含了三個(gè)逐像素的點(diǎn)光源,而且使用了逐像素的shader,那么很有可能將Draw Calls提高了三倍,同時(shí)也會(huì)增加overdraws。這是因?yàn)椋瑢?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ì)中斷批處理。
所以當(dāng)你需要光照效果時(shí),可以使用Lightmaps,提前烘焙好,提前把場(chǎng)景中的光照信息存儲(chǔ)在一張光照紋理中,然后在運(yùn)行時(shí)刻只需要根據(jù)紋理采樣得到光照信息即可。當(dāng)你需要金屬性強(qiáng)(鏡面)的效果,可以使用Light Probes。當(dāng)你需要一束光的時(shí)候,可以使用體積光去模擬這個(gè)效果。
3、不要使用動(dòng)態(tài)陰影
動(dòng)態(tài)陰影很酷,但是對(duì)于片元著色器來(lái)說(shuō)是災(zāi)難,陰影計(jì)算是三角投影計(jì)算,非常耗性能。如果想要陰影,可以使用 a、簡(jiǎn)單的使用一個(gè)帶陰影的貼圖 b、烘焙場(chǎng)景,拿到lightmaps c、創(chuàng)建投影生成器的方法 d、使用ShadowMap的方法(目前還沒(méi)有研究)。
4、盡量使用簡(jiǎn)單的shader
a、建議盡量實(shí)用Unity自帶mobile版本的(built-in)Shader,這些大大提高了頂點(diǎn)處理的性能。當(dāng)然也會(huì)有一些限制。
b、自己寫(xiě)的shader請(qǐng)注意復(fù)雜操作符計(jì)算,類(lèi)似pow,exp,log,cos,sin,tan等都是很耗時(shí)的計(jì)算,最多只用一次在每個(gè)像素點(diǎn)的計(jì)算,還有有些除法運(yùn)算盡量該能乘法運(yùn)算等。
c、避免透明度測(cè)試著色器,因?yàn)檫@個(gè)非常耗時(shí),使用透明度混合的版本來(lái)代替。
d、浮點(diǎn)類(lèi)型運(yùn)算:精度越低的浮點(diǎn)計(jì)算越快。
e、不要在Shader中添加不必要的Pass.
五、Unity3D應(yīng)用性能優(yōu)化之內(nèi)存
unity中有兩類(lèi)內(nèi)存,一個(gè)是Mono托管的內(nèi)存(相當(dāng)于DVM的內(nèi)存),一個(gè)是Unity3D使用的資源類(lèi)類(lèi)型的內(nèi)存(Texture、Mesh這種)。
Mono內(nèi)存
1、盡量不要?jiǎng)討B(tài)的Instantiate和Destroy Object,使用Object Pool。
2、不要?jiǎng)討B(tài)的產(chǎn)生字符串,使用字符串的直接拼接,使用System.Text.StringBuilder代替。
3、Cache一些東西,在update里面盡量避免search,如GameObject.FindWithTag(“”)、GetComponent這樣的調(diào)用,可以在Start中預(yù)先存起來(lái)。
4、盡量減少函數(shù)調(diào)用棧,用x = (x > 0 ? x : -x);代替x = Mathf.Abs(x)。
5、定時(shí)重復(fù)處理用 InvokeRepeating 函數(shù)實(shí)現(xiàn)。
6、減少GetComponent的調(diào)用,使用 GetComponent或內(nèi)置組件訪問(wèn)器會(huì)產(chǎn)生明顯的開(kāi)銷(xiāo)。您可以通過(guò)一次獲取組件的引用來(lái)避免開(kāi)銷(xiāo),并將該引用分配給一個(gè)變量(transform用的最多)。
7、使用內(nèi)置數(shù)組,內(nèi)置數(shù)組是非常快的。ArrayList或Array類(lèi)很容易使用,你能輕易添加元件。但是他們有完全不同的速度。 內(nèi)置數(shù)組有固定長(zhǎng)度,并且大多時(shí)候你會(huì)事先知道最大長(zhǎng)度然后填充它。內(nèi)置數(shù)組最好的一點(diǎn)是他們直接嵌入結(jié)構(gòu)數(shù)據(jù)類(lèi)型在一個(gè)緊密的緩存里,而不需要任何額外 類(lèi)型信息或其他開(kāi)銷(xiāo)。因此,在緩存中遍歷它是非常容易的,因?yàn)槊總€(gè)元素都是對(duì)齊的。
Unity3D類(lèi)的內(nèi)存
這類(lèi)內(nèi)存包括
1、AssetBundle
Unity3D 里有兩種動(dòng)態(tài)加載機(jī)制:一個(gè)是Resources.Load,另外一個(gè)通過(guò)AssetBundle,其實(shí)兩者區(qū)別不大。 Resources.Load就是從一個(gè)缺省打進(jìn)程序包里的AssetBundle里加載資源,而一般AssetBundle文件需要你自己創(chuàng)建,運(yùn)行時(shí) 動(dòng)態(tài)加載,可以指定路徑和來(lái)源的。
AssetBundle運(yùn)行時(shí)加載:
(1)來(lái)自文件就用CreateFromFile(注意這種方法只能用于standalone程序,就不提了)。
(2)也可以來(lái)自Memory,用CreateFromMemory(byte[]),這個(gè)byte[]可以來(lái)自文件讀取的緩沖,www的下載或者其他可能的方式。其實(shí)WWW的assetBundle就是內(nèi)部數(shù)據(jù)讀取完后自動(dòng)創(chuàng)建了一個(gè)assetBundle而已,Create完以后,等于把硬盤(pán)或者網(wǎng)絡(luò)的一個(gè)文件讀到內(nèi)存一個(gè)區(qū)域,這時(shí)候只是個(gè)AssetBundle內(nèi)存鏡像數(shù)據(jù)塊,還沒(méi)有Assets的概念。
下圖是AssetBundle的加載卸載示意圖:
AssetBundle是如何加載的呢?用AssetBundle.Load(同Resources.Load) 這才會(huì)從AssetBundle的內(nèi)存鏡像里讀取并創(chuàng)建一個(gè)Asset對(duì)象,創(chuàng)建Asset對(duì)象同時(shí)也會(huì)分配相應(yīng)內(nèi)存用于存放(反序列化)。異步讀取用AssetBundle.LoadAsync,也可以一次讀取多個(gè)用AssetBundle.LoadAll。
AssetBundle如何釋放呢?
AssetBundle.Unload(flase)是釋放AssetBundle文件的內(nèi)存鏡像,不包含Load創(chuàng)建的Asset內(nèi)存對(duì)象。
AssetBundle.Unload(true)是釋放那個(gè)AssetBundle文件內(nèi)存鏡像和并銷(xiāo)毀所有用Load創(chuàng)建的Asset內(nèi)存對(duì)象。
2、Texture
對(duì)于IOS選擇使用 PVRTC壓縮格式的,對(duì)于Android選擇ETC壓縮格式的,紋理可以節(jié)省大量?jī)?nèi)存和讀取速度快,但是會(huì)有所降低圖像的質(zhì)量。
2D紋理如果沒(méi)有必要不要使用mimap(會(huì)約增加33%的內(nèi)存開(kāi)銷(xiāo)),曾經(jīng)在IOS上吃過(guò)虧。3D模型的紋理一般是需要mimap的,但是如果確定了3D模型距離攝像機(jī)的距離,在GPU分析器上確定了unity使用的紋理,就可以保留,關(guān)閉mimap(比如項(xiàng)目中的avatar)。
3、Mesh
有Mesh合并和Mesh壓縮(坑比較多,不建議使用)。
4、Particle
粒子效果只要記住使用之后及時(shí)釋放銷(xiāo)毀就行。