日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

unity3d优化收集摘录

發(fā)布時間:2023/12/8 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 unity3d优化收集摘录 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

  Unity3D 對于移動平臺的支持無可厚非,但是也有時候用Unity3D 開發(fā)出來的應用、游戲在移動終端上的運行有著明顯的效率問題,比如卡、畫質等各種問題。

  影響因素:

  • Drawcall 值過大,所需要的 GPU 的處理性能較高,從而導致CPU的計算時間過長,于是就卡了。
  • 點、面過多,GPU 根據(jù)不同面的效果展開計算,并且CPU計算的數(shù)據(jù)也多,所以效果出來了。
  •   優(yōu)化方式:

  • 對于模型:Mesh 合并,有個不錯的插件(DrawCall Minimizer???--->??直接上Asset Store 下載即可,免費的,而且有文檔,很容易上手)。
  • 對于UI:?盡量避免使用Unity3D自帶的 GUI 換用 NGUI或者EZGUI,因為這兩個UI插件對于UI中的圖片處理是將UI圖片放置在一個 Atlas 中,一個 Atlas 對應一個Drawcall。
  • 對于燈光:可以使用 Unity3D 自帶的??Lightmapping?插件來烘焙場景中的燈光效果到物體材質上。
  • 對于場景:可以使用 Unity3D 自帶的 Occlusion?Culling 插件把靜止不動的場景元素烘焙出來。
  • 對于特效:盡量把材質紋理合并。  
  •   在屏幕上渲染物體,引擎需要發(fā)出一個繪制調用來訪問圖形API(iOS系統(tǒng)中為OpenGL ES)。每個繪制調用需要進行大量的工作來訪問圖形API,從而導致了CPU方面顯著的性能開銷。Unity在運行時可以將一些物體進行合并,從而用一個繪制調用來渲染他們。這一操作,我們稱之為“批處理”。一般來說,Unity批處理的物體越多,你就會得到越好的渲染性能。?   Unity中內建的批處理機制所達到的效果要明顯強于使用幾何建模工具(或使用Standard Assets包中的CombineChildren腳本)的批處理效果。這是因為,Unity引擎的批處理操作是在物體的可視裁剪操作之后進行的。Unity先對每個物體進行裁剪,然后再進行批處理,這樣可以使渲染的幾何總量在批處理前后保持不變。但是,使用幾何建模工具來拼合物體,會妨礙引擎對其進行有效的裁剪操作,從而導致引擎需要渲染更多的幾何面片。   材質。只有擁有相同材質的物體才可以進行批處理。因此,如果你想要得到良好的批處理效果,你需要在程序中盡可能地復用材質和物體。如果你的兩個材質僅僅是紋理不同,那么你可以通過紋理拼合操作來將這兩張紋理拼合成一張大的紋理。一旦紋理拼合在一起,你就可以使用這個單一材質來替代之前的兩個材質了。如果你需要通過腳本來訪問復用材質屬性,那么值得注意的是改變Renderer.material將會造成一份材質的拷貝。因此,你應該使用Renderer.sharedMaterial來保證材質的共享狀態(tài)。?   動態(tài)批處理。如果動態(tài)物體共用著相同的材質,那么Unity會自動對這些物體進行批處理。動態(tài)批處理操作是自動完成的,并不需要你進行額外的操作。
  • 批處理動態(tài)物體需要在每個頂點上進行一定的開銷,所以動態(tài)批處理僅支持小于900頂點的網(wǎng)格物體。?
  • 如果你的著色器使用頂點位置,法線和UV值三種屬性,那么你只能批處理300頂點以下的物體;如果你的著色器需要使用頂點位置,法線,UV0,UV1和切向量,那你只能批處理180頂點以下的物體。請注意:屬性數(shù)量的限制可能會在將來進行改變。
  • 不要使用縮放尺度(scale)。分別擁有縮放尺度(1,1,1)和(2,2,2)的兩個物體將不會進行批處理。
  • 統(tǒng)一縮放尺度的物體不會與非統(tǒng)一縮放尺度的物體進行批處理。使用縮放尺度(1,1,1)和 (1,2,1)的兩個物體將不會進行批處理,但是使用縮放尺度(1,2,1)和(1,3,1)的兩個物體將可以進行批處理。
  • 使用不同材質的實例化物體(instance)將會導致批處理失敗。
  • 擁有l(wèi)ightmap的物體含有額外(隱藏)的材質屬性,比如:lightmap的偏移和縮放系數(shù)等。所以,擁有l(wèi)ightmap的物體將不會進行批處理(除非他們指向lightmap的同一部分)。
  • 多通道的shader會妨礙批處理操作。比如,幾乎unity中所有的著色器在前向渲染中都支持多個光源,并為它們有效地開辟多個通道。
  • 預設體的實例會自動地使用相同的網(wǎng)格模型和材質。
  •   靜態(tài)批處理。相對而言,靜態(tài)批處理操作允許引擎對任意大小的幾何物體進行批處理操作來降低繪制調用。因此,靜態(tài)批處理比動態(tài)批處理更加有效,你應該盡量低使用它,因為它需要更少的CPU開銷。為了更好地使用靜態(tài)批處理,你需要明確指出哪些物體是靜止的,并且在游戲中永遠不會移動、旋轉和縮放。想完成這一步,你只需要在檢測器(Inspector)中將Static復選框打勾即可。使用靜態(tài)批處理操作需要額外的內存開銷來儲存合并后的幾何數(shù)據(jù)。在靜態(tài)批處理之前,如果一些物體共用了同樣的幾何數(shù)據(jù),那么引擎會在編輯以及運行狀態(tài)對每個物體創(chuàng)建一個幾何數(shù)據(jù)的備份。這并不總是一個好的想法,因為有時候,你將不得不犧牲一點渲染性能來防止一些物體的靜態(tài)批處理,從而保持較少的內存開銷。比如,將濃密森里中樹設為Static,會導致嚴重的內存開銷。靜態(tài)批處理目前只支持Unity iOS Advanced。
    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    Unity(或者說基本所有圖形引擎)生成一幀畫面的處理過程大致可以這樣簡化描述:引擎首先經(jīng)過簡單的可見性測試,確定攝像機可以看到的物體,然后把這些物體的頂點(包括本地位置、法線、UV等),索引(頂點如何組成三角形),變換(就是物體的位置、旋轉、縮放、以及攝像機位置等),相關光源,紋理,渲染方式(由材質/Shader決定)等數(shù)據(jù)準備好,然后通知圖形API——或者就簡單地看作是通知GPU——開始繪制,GPU基于這些數(shù)據(jù),經(jīng)過一系列運算,在屏幕上畫出成千上萬的三角形,最終構成一幅圖像。

    在Unity中,每次引擎準備數(shù)據(jù)并通知GPU的過程稱為一次Draw Call。這一過程是逐個物體進行的,對于每個物體,不只GPU的渲染,引擎重新設置材質/Shader也是一項非常耗時的操作。因此每幀的Draw Call次數(shù)是一項非常重要的性能指標,對于iOS來說應盡量控制在20次以內,這個值可以在編輯器的Statistic窗口看到。

    Unity內置了Draw Call Batching技術,從名字就可以看出,它的主要目標就是在一次Draw Call中批量處理多個物體。只要物體的變換和材質相同,GPU就可以按完全相同的方式進行處理,即可以把它們放在一個Draw Call中。Draw Call Batching技術的核心就是在可見性測試之后,檢查所有要繪制的物體的材質,把相同材質的分為一組(一個Batch),然后把它們組合成一個物體(統(tǒng)一變換),這樣就可以在一個Draw Call中處理多個物體了(實際上是組合后的一個物體)。

    但Draw Call Batching存在一個缺陷,就是它需要把一個Batch中的所有物體組合到一起,相當于創(chuàng)建了一個與這些物體加起來一樣大的物體,與此同時就需要分配相應大小的內存。這不僅會消耗更多內存,還需要消耗CPU時間。特別是對于移動的物體,每一幀都得重新進行組合,這就需要進行一些權衡,否則得不償失。但對于靜止不動的物體來說,只需要進行一次組合,之后就可以一直使用,效率要高得多。

    Unity提供了Dynamic Batching和Static Batching兩種方式。Dynamic Batching是完全自動進行的,不需要也無法進行任何干預,對于頂點數(shù)在300以內的可移動物體,只要使用相同的材質,就會組成Batch。Static Batching則需要把靜止的物體標記為Static,然后無論大小,都會組成Batch。如前文所說,Static Batching顯然比Dynamic Batching要高效得多,于是,Static Batching功能是收費的……

    要有效利用Draw Call Batching,首先是盡量減少場景中使用的材質數(shù)量,即盡量共享材質,對于僅紋理不同的材質可以把紋理組合到一張更大的紋理中(稱為Texture Atlasing)。然后是把不會移動的物體標記為Static。此外還可以通過CombineChildren腳本(Standard Assets/Scripts/Unity Scripts/CombineChildren)手動把物體組合在一起,但這個腳本會影響可見性測試,因為組合在一起的物體始終會被看作一個物體,從而會增加GPU要處理的幾何體數(shù)量,因此要小心使用。

    對于復雜的靜態(tài)場景,還可以考慮自行設計遮擋剔除算法,減少可見的物體數(shù)量同時也可以減少Draw Call。

    總之,理解Draw Call和Draw Call Batching原理,根據(jù)場景特點設計相應的方案來盡量減少Draw Call次數(shù)才是王道,其它方面亦然。

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    寫在前面


    這一篇是在Digital Tutors的一個系列教程的基礎上總結擴展而得的~Digital Tutors是一個非常棒的教程網(wǎng)站,包含了多媒體領域很多方面的資料,非常酷!除此之外,還參考了Unity Cookie中的一個教程。還有很多其他參考在下面的鏈接中。


    這篇文章旨在簡要地說明一下常見的各種優(yōu)化策略。不過對每個基礎有非常深入地講解,需要的童鞋可以自行去相關資料。


    還有一些我認為非常好的參考文章:
    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



    影響性能的因素


    首先,我們得了解,影響游戲性能的因素哪些,才能對癥下藥。對于一個游戲來說,有兩種主要的計算資源:CPU和GPU。它們會互相合作,來讓我們的游戲可以在預期的幀率和分辨率下工作。CPU負責其中的幀率,GPU主要負責分辨率相關的一些東西。


    總結起來,主要的性能瓶頸在于:

    • CPU
      • 過多的Draw Calls
      • 復雜的腳本或者物理模擬

    • 頂點處理
      • 過多的頂點
      • 過多的逐頂點計算

    • 像素(Fragment)處理
      • 過多的fragment,overdraws
      • 過多的逐像素計算

    • 帶寬
      • 尺寸很大且未壓縮的紋理
      • 分辨率過高的framebuffer


    對于CPU來說,限制它的主要是游戲中的Draw Calls。那么什么是Draw Call呢?如果你學過OpenGL,那么你一定還記得在每次繪圖前,我們都需要先準備好頂點數(shù)據(jù)(位置、法線、顏色、紋理坐標等),然后調用一系列API把它們放到GPU可以訪問到的指定位置,最后,我們需要調用_glDraw*命令,來告訴GPU,“嘿,我把東西都準備好了,你個懶家伙趕緊出來干活(渲染)吧!”。而調用_glDraw*命令的時候,就是一次Draw Call。那么為什么Draw Call會成為性能瓶頸呢(而且是CPU的瓶頸)?上面說到過,我們想要繪制圖像時,就一定需要調用Draw Call。例如,一個場景里有水有樹,我們渲染水的時候使用的是一個material以及一個shader,但渲染樹的時候就需要一個完全不同的material和shader,那么就需要CPU重新準備頂點數(shù)據(jù)、重新設置shader,而這種工作實際是非常耗時的。如果場景中,每一個物體都使用不同的material、不同的紋理,那么就會產(chǎn)生太多Draw Call,影響幀率,游戲性能就會下降。當然,這里說得很簡單,更詳細的請自行谷歌。其他CPU的性能瓶頸還有物理、布料模擬、粒子模擬等,都是計算量很大的操作。


    而對于GPU來說,它負責整個渲染流水線。它會從處理CPU傳遞過來的模型數(shù)據(jù)開始,進行Vertex Shader、Fragment Shader等一系列工作,最后輸出屏幕上的每個像素。因此它的性能瓶頸可能和需要處理的頂點數(shù)目的、屏幕分辨率、顯存等因素有關。總體包含了頂點和像素兩方面的性能瓶頸。在像素處理中,最常見的性能瓶頸之一是overdraw。Overdraw指的是,我們可能對屏幕上的像素繪制了多次。


    了解了上面基本的內容后,下面涉及到的優(yōu)化技術有:

    • 頂點優(yōu)化
      • 優(yōu)化幾何體
      • 使用LOD(Level of detail)技術
      • 使用遮擋剔除(Occlusion culling)技術

    • 像素優(yōu)化
      • 控制繪制順序
      • 警惕透明物體
      • 減少實時光照

    • CPU優(yōu)化
      • 減少Draw Calls

    • 帶寬優(yōu)化
      • 減少紋理大小
      • 利用縮放

    首先是頂點優(yōu)化的部分。

    頂點優(yōu)化

    優(yōu)化幾何體


    這一步主要是為了針對性能瓶頸中的”頂點處理“一項。這里的幾何體就是指組成場景中對象的網(wǎng)格結構。
    3D游戲制作都由模型制作開始。而在建模時,有一條我們需要記住:盡可能減少模型中三角形的數(shù)目,一些對于模型沒有影響、或是肉眼非常難察覺到區(qū)別的頂點都要盡可能去掉。例如在下面左圖中,正方體內部很多頂點都是不需要的,而把這個模型導入到Unity里就會是右面的情景: ?

    在Game視圖下,我們可以查看場景中的三角形數(shù)目和頂點數(shù)目:

    可以看到一個簡單的正方形就產(chǎn)生了這么多頂點,這是我們不希望看到的。
    同時,盡可能重用頂點。在很多三維建模軟件中,都有相應的優(yōu)化選項,可以自動優(yōu)化網(wǎng)格結構。最后優(yōu)化后,一個正方體可能只剩下8個頂點: ?

    它對應的頂點數(shù)和三角形數(shù)目如下:


    等等!這里,你可能要問了,為什么頂點數(shù)是24,而不是8呢?美術朋友們經(jīng)常會遇到這樣的問題,就是建模軟件里顯示的模型頂點數(shù)和Unity中的不一樣,通常Unity會多很多。誰才是對的呢?其實,它們是站在不同的角度上計算的,都有各自的道理,但我們真正應該關心的是Unity里的數(shù)目。
    我們這里簡單解釋一下。三維軟件里更多地是站在我們人類的角度理解頂點的,即我們看見的一個點就是一個。而Unity是站在GPU的角度上,去計算頂點數(shù)目的。而在GPU看來,看起來是一個的很有可能它要分開處理,從而就產(chǎn)生了額外的頂點。這種將頂點一分為多的原因,主要有兩個:一個是UV splits,一個是Smoothing splits。而它們的本質其實都是因為對于GPU來說,頂點的每一個屬性和頂點之間必須是一對一的關系。UV splits的產(chǎn)生,是因為建模時,一個頂點的UV坐標有多個。例如之前的立方體的例子,由于每個面都有共同的頂點,因此在不同面上,同一個頂點的UV坐標可能發(fā)生改變。這對于GPU來說,這是不可理解的,因此它必須把這個頂點拆分成兩個具有不同UV坐標的定頂點,它才甘心。而Smoothing splits的產(chǎn)生也是類似的,不同的時,這次一個頂點可能會對應多個法線信息或切線信息。這通常是因為我們要決定一個邊是一條Hard Edge還是Smooth Edge。Hard Edge通常是下面這樣的效果(注意中間的折痕部分): ?

    而如果觀察它的頂點法線,就會發(fā)現(xiàn),折痕處每個頂點其實包含了兩個不同的法線。因此,對于GPU來說,它同樣無法理解這樣的事情,因此會把頂點一分為二。而相反,Smooth Edge則是下面的情況: ?

    對于GPU來說,它本質上只關心有多少個頂點。因此,盡可能減少頂點的數(shù)目其實才是我們真正對需要關心的事情。因此,最后一條優(yōu)化建議就是:移除不必要的Hard Edge以及紋理銜接,即避免Smoothing splits和UV?splits

    使用LOD(Level of detail)技術


    LOD技術有點類似于Mipmap技術,不同的是,LOD是對模型建立了一個模型金字塔,根據(jù)攝像機距離對象的遠近,選擇使用不同精度的模型。它的好處是可以在適當?shù)臅r候大量減少需要繪制的頂點數(shù)目。它的缺點同樣是需要占用更多的內存,而且如果沒有調整好距離的話,可能會造成模擬的突變。
    在Unity中,可以通過LOD Group來實現(xiàn)LOD技術:
    ?


    通過上面的LOD Group面板,我們可以選擇需要控制的模型以及距離設置。下面展示了油桶從一個完整網(wǎng)格到簡化網(wǎng)格,最后完全被剔除的例子:
    ????



    使用遮擋剔除(Occlusion culling)技術


    遮擋剔除是用來消除躲在其他物件后面看不到的物件,這代表資源不會浪費在計算那些看不到的頂點上,進而提升性能。關于遮擋剔除,Unity Taiwan有一個系列文章大家可以看看(需翻墻):
    Unity 4.3 關于Occlusion Culling : 基本篇

    Unity 4.3 關于Occlusion Culling : 最佳做法

    Unity 4.3 關于Occlusion Culling : 錯誤診斷


    具體的內容大家可以自行查找。

    現(xiàn)在我們來談像素優(yōu)化。

    像素優(yōu)化


    像素優(yōu)化的重點在于減少overdraw。之前提過,overdraw指的就是一個像素被繪制了多次。關鍵在于控制繪制順序。
    Unity還提供了查看overdraw的視圖,在Scene視圖的Render Mode->Overdraw。當然這里的視圖只是提供了查看物體遮擋的層數(shù)關系,并不是真正的最終屏幕繪制的overdraw。也就是說,可以理解為它顯示的是如果沒有使用任何深度檢驗時的overdraw。這種視圖是通過把所有對象都渲染成一個透明的輪廓,通過查看透明顏色的累計程度,來判斷物體的遮擋。


    上圖圖,紅色越是濃重的地方表示overdraw越嚴重,而且這里涉及的都是透明物體,這意味著性能將會受到很大影響。

    控制繪制順序


    需要控制繪制順序,主要原因是為了最大限度的避免overdraws,也就是同一個位置的像素可以需要被繪制多變。在PC上,資源無限,為了得到最準確的渲染結果,繪制順序可能是從后往前繪制不透明物體,然后再繪制透明物體進行混合。但在移動平臺上,這種會造成大量overdraw的方式顯然是不適合的,我們應該盡量從前往后繪制。從前往后繪制之所以可以減少overdraw,都是因為深度檢驗的功勞。
    在Unity中,那些Shader中被設置為“Geometry” 隊列的對象總是從前往后繪制的,而其他固定隊列(如“Transparent”“Overla”等)的物體,則都是從后往前繪制的。這意味這,我們可以盡量把物體的隊列設置為“Geometry” 。
    而且,我們還可以充分利用Unity的隊列來控制繪制順序。例如,對于天空盒子來說,它幾乎覆蓋了所有的像素,而且我們知道它永遠會在所有物體的后面,因此它的隊列可以設置為“Geometry+1”。這樣,就可以保證不會因為它而造成overdraws。

    時刻警惕透明物體


    而對于透明對象,由于它本身的特性(可以看之前關于Alpha Test和Alpha Blending的一篇文章)決定如果要得到正確的渲染效果,就必須從后往前渲染(這里不討論使用深度的方法),而且拋棄了深度檢驗。這意味著,透明物體幾乎一定會造成overdraws。如果我們不注意這一點,在一些機器上可能會造成嚴重的性能下面。例如,對于GUI對象來說,它們大多被設置成了半透明,如果屏幕中GUI占據(jù)的比例太多,而主攝像機又沒有進行調整而是投影整個屏幕,那么GUI就會造成屏幕的大量overdraws。

    因此,如果場景中大面積的透明對象,或者有很多層覆蓋的多層透明對象(即便它們每個的面積可以都不大),或者是透明的粒子效果,在移動設備上也會造成大量的overdraws。這是應該盡量避免的。
    對于上述GUI的這種情況,我們可以盡量減少窗口中GUI所占的面積。如果實在無能為力,我們可以把GUI繪制和三維場景的繪制交給不同的攝像機,而其中負責三維場景的攝像機的視角范圍盡量不要和GUI重疊。對于其他情況,只能說,盡可能少用。當然這樣會對游戲的美觀度產(chǎn)生一定影響,因此我們可以在代碼中對機器的性能進行判斷,例如首先關閉所有的耗費性能的功能,如果發(fā)現(xiàn)這個機器表現(xiàn)非常良好,再嘗試開啟一些特效功能。

    減少實時光照


    實時光照對于移動平臺是個非常昂貴的操作。如果只有一個平行光還好,但如果場景中包含了太多光源并且使用了很多多Passes的shader,那么很有可能會造成性能下降。而且在有些機器上,還要面臨shader失效的風險。例如,一個場景里如果包含了三個逐像素的點光源,而且使用了逐像素的shader,那么很有可能將Draw Calls提高了三倍,同時也會增加overdraws。這是因為,對于逐像素的光源來說,被這些光源照亮的物體要被再渲染一次。更糟糕的是,無論是動態(tài)批處理還是動態(tài)批處理(其實文檔中只提到了對動態(tài)批處理的影響,但不知道為什么實驗結果對靜態(tài)批處理也沒有用),對于這種逐像素的pass都無法進行批處理,也就是說,它們會中斷批處理。
    例如,下面的場景中,四個物體都被標識成了“Static”,它們使用的shader都是自帶的Bumped Diffuse。而所有的點光源都被標識成了“Important”,即是逐像素光。可以看到,運行后的Draw Calls是23,而非3。這是因為,只有“Forward Base”的Pass時發(fā)生了靜態(tài)批處理(這里的動態(tài)批處理由于多Pass已經(jīng)完全失效了),節(jié)省了一個Draw Calls,而后面的“Forward Add” Pass,每一次渲染都是一個單獨的Draw Call(而且可以看到Tris和Verts數(shù)目也增加了):



    這點正如文檔中說的:The draw calls for “additional per-pixel lights” will not be batched。原因我不是很清楚,這里有一個討論,但里面的意思說是對靜態(tài)批處理沒有影響,和我這里的結果不一樣,知道原因的麻煩給我留言,非常感謝。我也在Unity論壇里提問里。

    我們看到很多成功的移動游戲,它們的畫面效果看起來好像包含了很多光源,但其實這都是騙人的。

    使用Lightmaps


    Lightmaps的很常見的一種優(yōu)化策略。它主要用于場景中整體的光照效果。這種技術主要是提前把場景中的光照信息存儲在一張光照紋理中,然后在運行時刻只需要根據(jù)紋理采樣得到光照信息即可。
    當然與之配合的還有Light Probes技術。風宇沖有一個系列文章講過,但是時間比較久遠,但教程我相信網(wǎng)上有很多。

    使用God Rays


    場景中很多小型光源效果都是靠這種方法模擬的。它們一般并不是真的光源產(chǎn)生的,很多情況是通過透明紋理進行模擬。具體可以參見之前的文章。


    CPU優(yōu)化

    減少Draw Calls


    批處理(Batching)


    這方面的優(yōu)化教程想必是最多的了。最常見的就是通過批處理(Batching)了。從名字上來理解,就是一塊處理多個物體的意思。那么什么樣的物體可以一起處理呢?答案就是使用同一個材質的物體。這是因此,對于使用同一個材質的物體,它們之間的不同僅僅在于頂點數(shù)據(jù)的差別,即使用的網(wǎng)格不同而已。我們可以把這些頂點數(shù)據(jù)合并在一起,再一起發(fā)送給GPU,就可以完成一次批處理。
    Unity中有兩種批處理方式:一種是動態(tài)批處理,一種是靜態(tài)批處理。對于動態(tài)批處理來說,好消息是一切處理都是自動的,不需要我們自己做任何操作,而且物體是可以移動的,但壞消息是,限制很多,可能一不小心我們就會破壞了這種機制,導致Unity無法批處理一些使用了相同材質的物體。對于靜態(tài)批處理來說,好消息是自由度很高,限制很少,壞消息是可能會占用更多的內存,而且經(jīng)過靜態(tài)批處理后的所有物體都不可以再移動了。
    首先來說動態(tài)批處理。Unity進行動態(tài)批處理的條件是,物體使用同一個材質并且滿足一些特定條件。Unity總是在不知不覺中就為我們做了動態(tài)批處理。例如下面的場景:



    這個場景共包含了4個物體,其中兩個箱子使用了同一個材質。可以看到,它的Draw Calls現(xiàn)在是3,并且顯示Save by batching是1,也就是說,Unity靠Batching為我們節(jié)省了1個Draw Call。下面,我們來把其中一個箱子的大小隨便改動一下,看看會發(fā)生什么:


    可以發(fā)現(xiàn),Draw Calls變成了4,Save by batching的數(shù)目也變成了0。這是為什么呢?它們明明還是只使用了一個材質啊。原因就是前面提到的那些需要滿足的其他條件。動態(tài)批處理雖然自動得令人感動,但它對模型的要求很多:
    • 頂點屬性的最大限制為900,而且未來有可能會變。不要依賴這個數(shù)據(jù)。

    • 一般來說,那么所有對象都必須需要使用同一個縮放尺度(可以是(1, 1, 1)、(1, 2, 3)、(1.5, 1.4, 1.3)等等,但必須都一樣)。但如果是非統(tǒng)一縮放(即每個維度的縮放尺度不一樣,例如(1, 2, 1)),那么如果所有的物體都使用不同的非統(tǒng)一縮放也是可以批處理的。這個要求很怪異,為什么批處理會和縮放有關呢?這和Unity背后的技術有關系,有興趣的可以自行谷歌,比如這里。

    • 使用lightmap的物體不會批處理。多passes的shader會中斷批處理。接受實時陰影的物體也不會批處理。

    上述除了最常見的由于縮放導致破壞批處理的情況,還有就是頂點屬性的限制。例如,在上面的場景中我們添加之前未優(yōu)化后的箱子模型:


    可以看到Draw Calls一下子變成了5。這是因為新添加的箱子模型中,包含了474個頂點,而它使用的頂點屬性有位置、UV坐標、法線等信息,使用的總和超過了900。

    動態(tài)批處理的條件這么多,一不小心它就不干了,因此Unity提供了另一個方法,靜態(tài)批處理。接著上面的例子,我們保持修改后的縮放,但把四個物體的“Static Flag”勾選上:

    點擊Static后面的三角下拉框,我們會看到其實這一步設置了很多東西,這里我們想要的只是“Batching static”一項。這時我們再看Draw Calls,恩,還是沒有變化。但是不要急,我們點擊運行,變化出現(xiàn)了:


    Draw Calls又回到了3,并且顯示Save by batching是1。這就是得利于靜態(tài)批處理。而且,如果我們在運行時刻查看模型的網(wǎng)格,會發(fā)現(xiàn)它們都變成了一個名為Combined Mesh (roo: scene)的東西。這個網(wǎng)格是Unity合并了所有標識為“Static”的物體的結果,在我們的例子里,就是四個物體:



    你可以要問了,這四個對象明明不是都使用了一個材質,為什么可以合并成一個呢?如果你仔細觀察上圖的話,會發(fā)現(xiàn)里面標明了“4 submeshes”,也就是說,這個合并后的網(wǎng)格其實包含了4個子網(wǎng)格,也就是我們的四個對象。對于合并后后的網(wǎng)格,Unity會判斷其中使用同一個材質的子網(wǎng)格,然后對它們進行批處理。
    但是,我們再細心點可以發(fā)現(xiàn),我們的箱子使用的其實是同一個網(wǎng)格,但合并后卻變成了兩個。而且,我們觀察運行前后Stats窗口中的“VBO total”,它的大小由241.6KB變成了286.2KB,變大了!還記得靜態(tà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.”


    也就是說,如果在靜態(tài)批處理前有一些物體共享了相同的網(wǎng)格(例如這里的兩個箱子),那么每一個物體都會有一個該網(wǎng)格的復制品,即一個網(wǎng)格會變成多個網(wǎng)格被發(fā)送給GPU。在上面的例子看來,就是VBO的大小明顯增大了。如果這類使用同一網(wǎng)格的對象很多,那么這就是一個問題了,這種時候我們可能需要避免使用靜態(tài)批處理,這意味著犧牲一定的渲染性能。例如,如果在一個使用了1000個重復樹模型的森林中使用靜態(tài)批處理,那么結果就會產(chǎn)生1000倍的內存,這會造成嚴重的內存影響。這種時候,解決方法要么我們可以忍受這種犧牲內存換取性能的方法,要么不要使用靜態(tài)批處理,而使用動態(tài)批處理(前提是大家使用相同的縮放大小,或者大家都使用不同的非統(tǒng)一縮放大小),或者自己編寫批處理的方法。當然,我認為最好的還是使用動態(tài)批處理來解決。
    有一些小提示可以使用:
    • 盡可能選擇靜態(tài)批處理,但得時刻小心對內存的消耗。

    • 如果無法進行靜態(tài)批處理,而要使用動態(tài)批處理的話,那么請小心上面提到的各種注意事項。例如:

      • 盡可能讓這樣的物體少并且盡可能讓這些物體包含少量的頂點屬性。

      • 不要使用統(tǒng)一縮放,或者都使用不同的非統(tǒng)一縮放。

    • 對于游戲中的小道具,例如可以撿拾的金幣等,可以使用動態(tài)批處理。

    • 對于包含動畫的這類物體,我們無法全部使用靜態(tài)批處理,但其中如果有不動的部分,可以把這部分標識成“Static”。


    一些討論:
    How static batching works

    Static batching use a ton of memory?

    Unity3D draw call optimization

    合并紋理(Atlas)


    雖然批處理是個很好的方式,但很容易就打破它的規(guī)定。例如,場景中的物體都使用Diffuse材質,但它們可能會使用不同的紋理。因此,盡可能把多張小紋理合并到一張大紋理(Atlas)中是一個好主意。

    利用網(wǎng)格的頂點數(shù)據(jù)


    但有時,除了紋理不同外,還有對于不同的物體,它們在材質上還有一些微小的參數(shù)變化,例如顏色不同、某些浮點參數(shù)不同。但鐵定律是,不管是動態(tài)批處理還是靜態(tài)批處理,它們的前提都是要使用同一個材質。是同一個,而不是同一種,也就是說它們指向的材質必須是同一個實體。這意味著,只要我們調整了參數(shù),就會影響到所有使用這個材質的對象。那么想要微小的調整怎么辦呢?由于Unity中的規(guī)定非常死,那么我們只好想些“歪門邪道”,其中一種就是使用網(wǎng)格的頂點數(shù)據(jù)(最常見的就是頂點顏色數(shù)據(jù))。
    前面說過,經(jīng)過批處理后的物體會被處理成一個VBO發(fā)送給GPU,VBO中的數(shù)據(jù)可以作為輸入傳遞給Vertex Shader,因此我們可以巧妙地對VBO中的數(shù)據(jù)進行控制,從而達到不同效果的目的。一個例子是,還是之前的森林,所有的樹使用了同一種材質,我們希望它們可以通過動態(tài)批處理來實現(xiàn),但不同樹的顏色可能不同。這時我么可以利用網(wǎng)格的頂點數(shù)據(jù)來調整。具體方法,可以參見后面會寫的一篇文章。
    但這種方法的缺點就是會需要更多的內存來存儲這些用于調整參數(shù)用的頂點數(shù)據(jù)。沒辦法,永遠沒有絕對完美的方法。


    帶寬優(yōu)化

    減少紋理大小


    之前提到過,使用Texture Atlas可以幫助減少Draw Calls,而這些紋理的大小同樣是一個需要考慮的問題。在這之前要提到一個問題就是,所有紋理的長寬比最好是正方形,而且長度值最好是2的整數(shù)冪。這是因為有很多優(yōu)化策略只有在這種時候才可以發(fā)揮最大效用。
    Unity中查看紋理參數(shù)可以通過紋理的面板:


    而調整參數(shù)可以通過紋理的Advance面板:



    上面各種參數(shù)的說明可以參見文檔。其中和優(yōu)化相關的主要有“Generate Mip Maps”、“Max Size”和“Format”幾個選項。
    “Generate Mip Maps”會為同一張紋理創(chuàng)建出很多不同大小的小紋理,構成一個紋理金字塔。而在游戲中可以根據(jù)距離物體的遠近,來動態(tài)選擇使用哪一個紋理。這是因為,在距離物體很遠的時候,就算我們使用了非常精細的紋理,但肉眼也是分辨不出來的,這種時候完全可以使用更小、更模糊的紋理來代替,而這大量可以節(jié)省訪問的像素的數(shù)目。但它的缺點是,由于需要為每一個紋理建立一個圖像金字塔,因此它會需要占用更多的內存。例如上面的例子,在勾選“Generate Mip Maps”前,內存占用是0.5M,而勾選了“Generate Mip Maps”后,就變成了0.7M。除了內存的占用以外,一些時候我們也不希望使用Mipmaps,例如GUI紋理等。我們還可以在面板中查看生成的Mip Maps:



    Unity中還提供了查看場景中物體的Mip Maps的使用情況。更確切的說是,展示了物體理想的紋理大小。其中紅色表示這個物體可以使用更小的紋理,藍色表示應該使用更大的紋理。


    “Max Size”決定了紋理的長寬值,如果我們使用的紋理本身超過了這個最大值,Unity會對其進行縮小來滿足這個條件。這里再重復一點,所有紋理的長寬比最好是正方形,而且長度值最好是2的整數(shù)冪。這是因為有很多優(yōu)化策略只有在這種時候才可以發(fā)揮最大效用。
    “Format”負責紋理使用的壓縮模式。通常選擇這種自動模式就可以了,Unity會負責根據(jù)不同的平臺來選擇合適的壓縮模式。而對于GUI類型的紋理,我們可以根據(jù)對畫質的要求來選擇是否進行壓縮,具體可以參見之前關于畫質的文章。


    我們還可以根據(jù)不同的機器來選擇使用不同分辨率的紋理,以便讓游戲在某些老機器上也可以運行。

    利用縮放


    很多時候分辨率也是造成性能下降的原因,尤其是現(xiàn)在很多國內山寨機,除了分辨率高其他硬件簡直一塌糊涂,而這恰恰中了游戲性能的兩個瓶頸:過大的屏幕分辨率+糟糕的GPU。因此,我們可能需要對于特定機器進行分辨率的放縮。當然,這樣會造成游戲效果的下降,但性能和畫面之間永遠是個需要權衡的話題。
    在Unity中設置屏幕分辨率可以直接調用Screen.SetResolution。實際使用中可能會遇到一些情況,雨松MOMO有一篇文章講了這種技術,可以去看看。
    ?

    寫在最后


    這篇文章是總結性質的,因此對每種技術都沒有進行非常詳細的解釋。強烈建議大家閱讀文章開頭給出的各種鏈接,寫得都很好。

    ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    剛開始寫這篇文章的時候選了一個很土的題目。。。《Unity3D優(yōu)化全解析》。因為這是一篇臨時起意才寫的文章,而且陳述的都是既有的事實,因而給自己“文(dou)學(bi)”加工留下的余地就少了很多。但又覺得這塊是不得不提的一個地方,平時見到很多人對此處也給予了忽略了事,需要時才去網(wǎng)上扒一些只言片語的資料。也恰逢年前,尋思著周末認真寫點東西遇到節(jié)假日沒準也沒什么人讀,所以索性就寫了這篇臨時的文章。題目很土,因為用了指向性很明確的“Unity3D”,讓人少了遐(瞎)想的空間,同時用了“高大全”這樣的構詞法,也讓匹夫有成為眾矢之的的可能。。。所以最后還是改成了現(xiàn)在各位看到的題目。話不多說,下面就開始正文~正所謂“草蛇灰線,伏脈千里”。那咱們首先~~~~~~

    看看優(yōu)化需要從哪里著手?

    匹夫印象里遇到的童靴,提Unity3D項目優(yōu)化則必提DrawCall,這自然沒錯,但也有很不好影響。因為這會給人一個錯誤的認識:所謂的優(yōu)化就是把DrawCall弄的比較低就對了。

    對優(yōu)化有這種第一印象的人不在少數(shù),drawcall的確是一個很重要的指標,但絕非全部。為了讓各位和匹夫能達成盡可能多的共識,匹夫首先介紹一下本文可能會涉及到的幾個概念,之后會提出優(yōu)化所涉及的三大方面:

    • drawcall是啥?其實就是對底層圖形程序(比如:OpenGL ES)接口的調用,以在屏幕上畫出東西。所以,是誰去調用這些接口呢?CPU。
    • fragment是啥?經(jīng)常有人說vf啥的,vertex我們都知道是頂點,那fragment是啥呢?說它之前需要先說一下像素,像素各位應該都知道吧?像素是構成數(shù)碼影像的基本單元呀。那fragment呢?是有可能成為像素的東西。啥叫有可能?就是最終會不會被畫出來不一定,是潛在的像素。這會涉及到誰呢?GPU。
    • batching是啥?都知道批處理是干嘛的吧?沒錯,將批處理之前需要很多次調用(drawcall)的物體合并,之后只需要調用一次底層圖形程序的接口就行。聽上去這簡直就是優(yōu)化的終極方案啊!但是,理想是美好的,世界是殘酷的,一些不足之后我們再細聊。
    • 內存的分配:記住,除了Unity3D自己的內存損耗。我們可是還帶著Mono呢啊,還有托管的那一套東西呢。更別說你一激動,又引入了自己的幾個dll。這些都是內存開銷上需要考慮到的。

    好啦,文中的幾個概念提前講清楚了,其實各位也能看的出來匹夫接下來要說的匹夫關注的優(yōu)化時需要注意的方面:

    • CPU方面
    • GPU方面
    • 內存方面

    所以,這篇文章也會按照CPU—->GPU—->內存的順序進行。

    CPU的方面的優(yōu)化:

    上文中說了,drawcall影響的是CPU的效率,而且也是最知名的一個優(yōu)化點。但是除了drawcall之外,還有哪些因素也會影響到CPU的效率呢?讓我們一一列出暫時能想得到的:

    • DrawCalls
    • 物理組件(Physics)
    • GC(什么?GC不是處理內存問題的嘛?匹夫你不要騙我啊!不過,匹夫也要提醒一句,GC是用來處理內存的,但是是誰使用GC去處理內存的呢?)
    • 當然,還有代碼質量

    DrawCalls:

    前面說過了,DrawCall是CPU調用底層圖形接口。比如有上千個物體,每一個的渲染都需要去調用一次底層接口,而每一次的調用CPU都需要做很多工作,那么CPU必然不堪重負。但是對于GPU來說,圖形處理的工作量是一樣的。所以對DrawCall的優(yōu)化,主要就是為了盡量解放CPU在調用圖形接口上的開銷。所以針對drawcall我們主要的思路就是每個物體盡量減少渲染次數(shù),多個物體最好一起渲染。所以,按照這個思路就有了以下幾個方案:

  • 使用Draw Call Batching,也就是描繪調用批處理。Unity在運行時可以將一些物體進行合并,從而用一個描繪調用來渲染他們。具體下面會介紹。
  • 通過把紋理打包成圖集來盡量減少材質的使用。
  • 盡量少的使用反光啦,陰影啦之類的,因為那會使物體多次渲染。
  • Draw Call Batching

    首先我們要先理解為何2個沒有使用相同材質的物體即使使用批處理,也無法實現(xiàn)Draw Call數(shù)量的下降和性能上的提升。

    因為被“批處理”的2個物體的網(wǎng)格模型需要使用相同材質的目的,在于其紋理是相同的,這樣才可以實現(xiàn)同時渲染的目的。因而保證材質相同,是為了保證被渲染的紋理相同。

    因此,為了將2個紋理不同的材質合二為一,我們就需要進行上面列出的第二步,將紋理打包成圖集。具體到合二為一這種情況,就是將2個紋理合成一個紋理。這樣我們就可以只用一個材質來代替之前的2個材質了。

    而Draw Call Batching本身,也還會細分為2種。

    Static Batching 靜態(tài)批處理

    看名字,猜使用的情景。

    靜態(tài)?那就是不動的咯。還有呢?額,聽上去狀態(tài)也不會改變,沒有“生命”,比如山山石石,樓房校舍啥的。那和什么比較類似呢?嗯,聰明的各位一定覺得和場景的屬性很像吧!所以我們的場景似乎就可以采用這種方式來減少draw call了。

    那么寫個定義:只要這些物體不移動,并且擁有相同的材質,靜態(tài)批處理就允許引擎對任意大小的幾何物體進行批處理操作來降低描繪調用。

    那要如何使用靜態(tài)批來減少Draw Call呢?你只需要明確指出哪些物體是靜止的,并且在游戲中永遠不會移動、旋轉和縮放。想完成這一步,你只需要在檢測器(Inspector)中將Static復選框打勾即可,如下圖所示:

    至于效果如何呢?

    舉個例子:新建4個物體,分別是Cube,Sphere, Capsule, Cylinder,它們有不同的網(wǎng)格模型,但是也有相同的材質(Default-Diffuse)。

    首先,我們不指定它們是static的。Draw Call的次數(shù)是4次,如圖:

    我們現(xiàn)在將它們4個物體都設為static,在來運行一下:

    如圖,Draw Call的次數(shù)變成了1,而Saved by batching的次數(shù)變成了3。

    靜態(tài)批處理的好處很多,其中之一就是與下面要說的動態(tài)批處理相比,約束要少很多。所以一般推薦的是draw call的靜態(tài)批處理來減少draw call的次數(shù)。那么接下來,我們就繼續(xù)聊聊draw call的動態(tài)批處理。

    Dynamic Batching 動態(tài)批處理

    有陰就有陽,有靜就有動,所以聊完了靜態(tài)批處理,肯定跟著就要說說動態(tài)批處理了。首先要明確一點,Unity3D的draw call動態(tài)批處理機制是引擎自動進行的,無需像靜態(tài)批處理那樣手動設置static。我們舉一個動態(tài)實例化prefab的例子,如果動態(tài)物體共享相同的材質,則引擎會自動對draw call優(yōu)化,也就是使用批處理。首先,我們將一個cube做成prefab,然后再實例化500次,看看draw call的數(shù)量。

    12345for(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。而這個過程中,我們除了實例化創(chuàng)建物體之外什么都沒做。不錯,unity3d引擎為我們自動處理了這種情況。

    但是有很多童靴也遇到這種情況,就是我也是從prefab實例化創(chuàng)建的物體,為何我的draw call依然很高呢?這就是匹夫上文說的,draw call的動態(tài)批處理存在著很多約束。下面匹夫就演示一下,針對cube這樣一個簡單的物體的創(chuàng)建,如果稍有不慎就會造成draw call飛漲的情況吧。

    我們同樣是創(chuàng)建500個物體,不同的是其中的100個物體,每個物體的大小都不同,也就是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。各位看官可以看到,僅僅是一個簡單的cube的創(chuàng)建,如果scale不同,竟然也不會去做批處理優(yōu)化。這僅僅是動態(tài)批處理機制的一種約束,那我們總結一下動態(tài)批處理的約束,各位也許也能從中找到為何動態(tài)批處理在自己的項目中不起作用的原因:

  • 批處理動態(tài)物體需要在每個頂點上進行一定的開銷,所以動態(tài)批處理僅支持小于900頂點的網(wǎng)格物體。
  • 如果你的著色器使用頂點位置,法線和UV值三種屬性,那么你只能批處理300頂點以下的物體;如果你的著色器需要使用頂點位置,法線,UV0,UV1和切向量,那你只能批處理180頂點以下的物體。
  • 不要使用縮放。分別擁有縮放大小(1,1,1) 和(2,2,2)的兩個物體將不會進行批處理。
  • 統(tǒng)一縮放的物體不會與非統(tǒng)一縮放的物體進行批處理。
  • 使用縮放尺度(1,1,1) 和 (1,2,1)的兩個物體將不會進行批處理,但是使用縮放尺度(1,2,1) 和(1,3,1)的兩個物體將可以進行批處理。
  • 使用不同材質的實例化物體(instance)將會導致批處理失敗。
  • 擁有l(wèi)ightmap的物體含有額外(隱藏)的材質屬性,比如:lightmap的偏移和縮放系數(shù)等。所以,擁有l(wèi)ightmap的物體將不會進行批處理(除非他們指向lightmap的同一部分)。
  • 多通道的shader會妨礙批處理操作。比如,幾乎unity中所有的著色器在前向渲染中都支持多個光源,并為它們有效地開辟多個通道。
  • 預設體的實例會自動地使用相同的網(wǎng)格模型和材質。
  • 所以,盡量使用靜態(tài)的批處理。

    物理組件

    曾幾何時,匹夫在做一個策略類游戲的時候需要在單元格上排兵布陣,而要偵測到哪個兵站在哪個格子匹夫選擇使用了射線,由于士兵單位很多,而且為了精確每一幀都會執(zhí)行檢測,那時候CPU的負擔叫一個慘不忍睹。后來匹夫果斷放棄了這種做法,并且對物理組件產(chǎn)生了心理的陰影。

    這里匹夫只提2點匹夫感覺比較重要的優(yōu)化措施:

    1.設置一個合適的Fixed Timestep。設置的位置如圖:

    那何謂“合適”呢?首先我們要搞明白Fixed Timestep和物理組件的關系。物理組件,或者說游戲中模擬各種物理效果的組件,最重要的是什么呢?計算啊。對,需要通過計算才能將真實的物理效果展現(xiàn)在虛擬的游戲中。那么Fixed Timestep這貨就是和物理計算有關的啦。所以,若計算的頻率太高,自然會影響到CPU的開銷。同時,若計算頻率達不到游戲設計時的要求,有會影響到功能的實現(xiàn),所以如何抉擇需要各位具體分析,選擇一個合適的值。

    2.就是不要使用網(wǎng)格碰撞器(mesh collider):為啥?因為實在是太復雜了。網(wǎng)格碰撞器利用一個網(wǎng)格資源并在其上構建碰撞器。對于復雜網(wǎng)狀模型上的碰撞檢測,它要比應用原型碰撞器精確的多。標記為凸起的(Convex )的網(wǎng)格碰撞器才能夠和其他網(wǎng)格碰撞器發(fā)生碰撞。各位上網(wǎng)搜一下mesh collider的圖片,自然就會明白了。我們的手機游戲自然無需這種性價比不高的東西。

    當然,從性能優(yōu)化的角度考慮,物理組件能少用還是少用為好。

    處理內存,卻讓CPU受傷的GC

    在CPU的部分聊GC,感覺是不是怪怪的?其實小匹夫不這么覺得,雖然GC是用來處理內存的,但的確增加的是CPU的開銷。因此它的確能達到釋放內存的效果,但代價更加沉重,會加重CPU的負擔,因此對于GC的優(yōu)化目標就是盡量少的觸發(fā)GC。

    首先我們要明確所謂的GC是Mono運行時的機制,而非Unity3D游戲引擎的機制,所以GC也主要是針對Mono的對象來說的,而它管理的也是Mono的托管堆。 搞清楚這一點,你也就明白了GC不是用來處理引擎的assets(紋理啦,音效啦等等)的內存釋放的,因為U3D引擎也有自己的內存堆而不是和Mono一起使用所謂的托管堆。

    其次我們要搞清楚什么東西會被分配到托管堆上?不錯咯,就是引用類型咯。比如類的實例,字符串,數(shù)組等等。而作為int,float,包括結構體struct其實都是值類型,它們會被分配在堆棧上而非堆上。所以我們關注的對象無外乎就是類實例,字符串,數(shù)組這些了。

    那么GC什么時候會觸發(fā)呢?兩種情況:

  • 首先當然是我們的堆的內存不足時,會自動調用GC。
  • 其次呢,作為編程人員,我們自己也可以手動的調用GC。
  • 所以為了達到優(yōu)化CPU的目的,我們就不能頻繁的觸發(fā)GC。而上文也說了GC處理的是托管堆,而不是Unity3D引擎的那些資源,所以GC的優(yōu)化說白了也就是代碼的優(yōu)化。那么匹夫覺得有以下幾點是需要注意的:

  • 字符串連接的處理。因為將兩個字符串連接的過程,其實是生成一個新的字符串的過程。而之前的舊的字符串自然而然就成為了垃圾。而作為引用類型的字符串,其空間是在堆上分配的,被棄置的舊的字符串的空間會被GC當做垃圾回收。
  • 盡量不要使用foreach,而是使用for。foreach其實會涉及到迭代器的使用,而據(jù)傳說每一次循環(huán)所產(chǎn)生的迭代器會帶來24 Bytes的垃圾。那么循環(huán)10次就是240Bytes。
  • 不要直接訪問gameobject的tag屬性。比如if (go.tag == “human”)最好換成if (go.CompareTag (“human”))。因為訪問物體的tag屬性會在堆上額外的分配空間。如果在循環(huán)中這么處理,留下的垃圾就可想而知了。
  • 使用“池”,以實現(xiàn)空間的重復利用。
  • 最好不用LINQ的命令,因為它們會分配臨時的空間,同樣也是GC收集的目標。而且我很討厭LINQ的一點就是它有可能在某些情況下無法很好的進行AOT編譯。比如“OrderBy”會生成內部的泛型類“OrderedEnumerable”。這在AOT編譯時是無法進行的,因為它只是在OrderBy的方法中才使用。所以如果你使用了OrderBy,那么在IOS平臺上也許會報錯。
  • 代碼?腳本?

    聊到代碼這個話題,也許有人會覺得匹夫多此一舉。因為代碼質量因人而異,很難像上面提到的幾點,有一個明確的評判標準。也是,公寫公有理,婆寫婆有理。但是匹夫這里要提到的所謂代碼質量是基于一個前提的:Unity3D是用C++寫的,而我們的代碼是用C#作為腳本來寫的,那么問題就來了~腳本和底層的交互開銷是否需要考慮呢?也就是說,我們用Unity3D寫游戲的“游戲腳本語言”,也就是C#是由mono運行時托管的。而功能是底層引擎的C++實現(xiàn)的,“游戲腳本”中的功能實現(xiàn)都離不開對底層代碼的調用。那么這部分的開銷,我們應該如何優(yōu)化呢?

    1.以物體的Transform組件為例,我們應該只訪問一次,之后就將它的引用保留,而非每次使用都去訪問。這里有人做過一個小實驗,就是對比通過方法GetComponent<Transform>()獲取Transform組件, 通過MonoBehavor的transform屬性去取,以及保留引用之后再去訪問所需要的時間:

    • GetComponent = 619ms
    • Monobehaviour = 60ms
    • CachedMB = 8ms
    • Manual Cache = 3ms

    2.如上所述,最好不要頻繁使用GetComponent,尤其是在循環(huán)中。

    3.善于使用OnBecameVisible()和OnBecameVisible(),來控制物體的update()函數(shù)的執(zhí)行以減少開銷。

    4.使用內建的數(shù)組,比如用Vector3.zero而不是new Vector(0, 0, 0);

    5.對于方法的參數(shù)的優(yōu)化:善于使用ref關鍵字。值類型的參數(shù),是通過將實參的值復制到形參,來實現(xiàn)按值傳遞到方法,也就是我們通常說的按值傳遞。復制嘛,總會讓人感覺很笨重。比如Matrix4x4這樣比較復雜的值類型,如果直接復制一份新的,反而不如將值類型的引用傳遞給方法作為參數(shù)。

    好啦,CPU的部分匹夫覺得到此就介紹的差不多了。下面就簡單聊聊其實匹夫并不是十分熟悉的部分,GPU的優(yōu)化。

    GPU的優(yōu)化

    GPU與CPU不同,所以側重點自然也不一樣。GPU的瓶頸主要存在在如下的方面:

  • 填充率,可以簡單的理解為圖形處理單元每秒渲染的像素數(shù)量。
  • 像素的復雜度,比如動態(tài)陰影,光照,復雜的shader等等
  • 幾何體的復雜度(頂點數(shù)量)
  • 當然還有GPU的顯存帶寬
  • 那么針對以上4點,其實仔細分析我們就可以發(fā)現(xiàn),影響的GPU性能的無非就是2大方面,一方面是頂點數(shù)量過多,像素計算過于復雜。另一方面就是GPU的顯存帶寬。那么針鋒相對的兩方面舉措也就十分明顯了。

  • 少頂點數(shù)量,簡化計算復雜度。
  • 縮圖片,以適應顯存帶寬。
  • 減少繪制的數(shù)目

    那么第一個方面的優(yōu)化也就是減少頂點數(shù)量,簡化復雜度,具體的舉措就總結如下了:

    • 保持材質的數(shù)目盡可能少。這使得Unity更容易進行批處理。
    • 使用紋理圖集(一張大貼圖里包含了很多子貼圖)來代替一系列單獨的小貼圖。它們可以更快地被加載,具有很少的狀態(tài)轉換,而且批處理更友好。
    • 如果使用了紋理圖集和共享材質,使用Renderer.sharedMaterial?來代替Renderer.material?。
    • 使用光照紋理(lightmap)而非實時燈光。
    • 使用LOD,好處就是對那些離得遠,看不清的物體的細節(jié)可以忽略。
    • 遮擋剔除(Occlusion culling)
    • 使用mobile版的shader。因為簡單。

    優(yōu)化顯存帶寬

    第二個方向呢?壓縮圖片,減小顯存帶寬的壓力。

    • OpenGL ES 2.0使用ETC1格式壓縮等等,在打包設置那里都有。
    • 使用mipmap。

    MipMap

    這里匹夫要著重介紹一下MipMap到底是啥。因為有人說過MipMap會占用內存呀,但為何又會優(yōu)化顯存帶寬呢?那就不得不從MipMap是什么開始聊起。一張圖其實就能解決這個疑問。

    上面是一個mipmap 如何儲存的例子,左邊的主圖伴有一系列逐層縮小的備份小圖

    是不是很一目了然呢?Mipmap中每一個層級的小圖都是主圖的一個特定比例的縮小細節(jié)的復制品。因為存了主圖和它的那些縮小的復制品,所以內存占用會比之前大。但是為何又優(yōu)化了顯存帶寬呢?因為可以根據(jù)實際情況,選擇適合的小圖來渲染。所以,雖然會消耗一些內存,但是為了圖片渲染的質量(比壓縮要好),這種方式也是推薦的。

    內存的優(yōu)化

    既然要聊Unity3D運行時候的內存優(yōu)化,那我們自然首先要知道Unity3D游戲引擎是如何分配內存的。大概可以分成三大部分:

  • Unity3D內部的內存
  • Mono的托管內存
  • 若干我們自己引入的DLL或者第三方DLL所需要的內存。
  • 第3類不是我們關注的重點,所以接下來我們會分別來看一下Unity3D內部內存和Mono托管內存,最后還將分析一個官網(wǎng)上Assetbundle的案例來說明內存的管理。

    Unity3D內部內存

    Unity3D的內部內存都會存放一些什么呢?各位想一想,除了用代碼來驅動邏輯,一個游戲還需要什么呢?對,各種資源。所以簡單總結一下Unity3D內部內存存放的東西吧:

    • 資源:紋理、網(wǎng)格、音頻等等
    • GameObject和各種組件。
    • 引擎內部邏輯需要的內存:渲染器,物理系統(tǒng),粒子系統(tǒng)等等

    Mono托管內存

    因為我們的游戲腳本是用C#寫的,同時還要跨平臺,所以帶著一個Mono的托管環(huán)境顯然必須的。那么Mono的托管內存自然就不得不放到內存的優(yōu)化范疇中進行考慮。那么我們所說的Mono托管內存中存放的東西和Unity3D內部內存中存放的東西究竟有何不同呢?其實Mono的內存分配就是很傳統(tǒng)的運行時內存的分配了:

    • 值類型:int型啦,float型啦,結構體struct啦,bool啦之類的。它們都存放在堆棧上(注意額,不是堆所以不涉及GC)。
    • 引用類型:其實可以狹義的理解為各種類的實例。比如游戲腳本中對游戲引擎各種控件的封裝。其實很好理解,C#中肯定要有對應的類去對應游戲引擎中的控件。那么這部分就是C#中的封裝。由于是在堆上分配,所以會涉及到GC。

    而Mono托管堆中的那些封裝的對象,除了在在Mono托管堆上分配封裝類實例化之后所需要的內存之外,還會牽扯到其背后對應的游戲引擎內部控件在Unity3D內部內存上的分配。

    舉一個例子:

    一個在.cs腳本中聲明的WWW類型的對象www,Mono會在Mono托管堆上為www分配它所需要的內存。同時,這個實例對象背后的所代表的引擎資源所需要的內存也需要被分配。

    一個WWW實例背后的資源:

    • 壓縮的文件
    • 解壓縮所需的緩存
    • 解壓縮之后的文件

    如圖:

    那么下面就舉一個AssetBundle的例子:

    Assetbundle的內存處理

    以下載Assetbundle為例子,聊一下內存的分配。匹夫從官網(wǎng)的手冊上找到了一個使用Assetbundle的情景如下:

    123456789101112131415161718192021IEnumerator 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(&quot;WWW download had an error:&quot; + www.error);????????????AssetBundle bundle = www.assetBundle;//AssetBundle是第2部分????????????if (AssetName == &quot;&quot;)????????????????Instantiate(bundle.mainAsset);//實例化是第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)????}}

    內存分配的三個部分匹夫已經(jīng)在代碼中標識了出來:

  • Web Stream:包括了壓縮的文件,解壓所需的緩存,以及解壓后的文件。
  • AssetBundle:Web Stream中的文件的映射,或者說引用。
  • 實例化之后的對象就是引擎的各種資源文件了,會在內存中創(chuàng)建出來。
  • 那就分別解析一下:

    1 WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)

  • 將壓縮的文件讀入內存中
  • 創(chuàng)建解壓所需的緩存
  • 將文件解壓,解壓后的文件進入內存
  • 關閉掉為解壓創(chuàng)建的緩存
  • 1AssetBundle bundle = www.assetBundle;

  • AssetBundle此時相當于一個橋梁,從Web Stream解壓后的文件到最后實例化創(chuàng)建的對象之間的橋梁。
  • 所以AssetBundle實質上是Web Stream解壓后的文件中各個對象的映射。而非真實的對象。
  • 實際的資源還存在Web Stream中,所以此時要保留Web Stream。
  • 1 Instantiate(bundle.mainAsset);

    通過AssetBundle獲取資源,實例化對象

    最后各位可能看到了官網(wǎng)中的這個例子使用了:

    12using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){}

    這種using的用法。這種用法其實就是為了在使用完Web Stream之后,將內存釋放掉的。因為WWW也繼承了idispose的接口,所以可以使用using的這種用法。其實相當于最后執(zhí)行了:

    1 2 //刪除Web Stream www.Dispose();

    OK,Web Stream被刪除掉了。那還有誰呢?對Assetbundle。那么使用

    1 2 //刪除AssetBundle bundle.Unload(false);

    ok,寫到這里就先打住啦。寫的有點超了。有點趕也有點臨時,日后在補充編輯。



    總結

    以上是生活随笔為你收集整理的unity3d优化收集摘录的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內容還不錯,歡迎將生活随笔推薦給好友。