1. 程式人生 > >[Unity優化] Unity CPU性能優化 (難度3 推薦4)

[Unity優化] Unity CPU性能優化 (難度3 推薦4)

難度 sset 依賴關系 目錄 數量 異步 繼續 過多 compare

原文地址:

http://www.cnblogs.com/chwen/p/4396515.html

  前段時間本人轉戰unity手遊,由於作者(Chwen)之前參與端遊開發,有些端遊的經驗可以直接移植到手遊,比如項目框架架構、代碼設計、部分性能分析,而對於移動終端而言,CPU、內存、顯卡甚至電池等硬件因素,以及網絡等條件限制,對移動遊戲開發的優化帶來更大的挑戰。

  這裏就以unity4.5x版本為例,對Unity的優化方案做一個總結,有些是項目遇到的,也有些是看到別人寫的不錯拿來分享,算作一個整理,後期也會持續更新。本優化從CPU、GPU和內存三個方面著手總結,這一篇先從CPU說起,整理一些針對CPU相關的優化建議。

  對CPU的優化主要是從drawcall、物理組件、GC(垃圾回收)、腳本等幾個方面開展。

  1. Drawcall 的優化
  • 什麽是Drawcall?

  Drawcall是CPU向GPU發送繪制命令的接口調用。理論上每一個不同材質的物件需要渲染在屏幕上時,CPU都會調用圖形API ( openGL or Diract3D ) 的Draw接口觸發顯卡進行繪制。

  • 為什麽優化Drawcall?

  Drawcall對硬件和驅動而言,要求大量設置狀態(使用哪些頂點、哪些shader等)和狀態轉換。而Drawcall最大的消耗在於:如果每次drawcall只提交少量的數據將導致CPU瓶頸,CPU無法將GPU填滿。Drawcall對GPU的耗費在於硬件一直等待CPU提交數據,而無法得到有效利用。GPU大量的時間耗費在不斷切換狀態和正確性檢測上。 GPU在Draw Call之間,為了防止前後Draw的依賴關系造成繪制錯誤或者資源競用,一般會在Draw Call後Flush整個流水線,小粒度的Draw Call對GPU流水線來說是個很大的浪費。(這個問題在D3D老版本存在,在新版D3D11中得到改善。)實際上unity官方指出,Drawcall數量的降低並非重點,重點是減少批次的數量,Drawcall優化實際上是對批次數量的優化。
  延伸閱讀:
  · why are draw calls expensive? — Stack Overflow
  · Direct3D Draw函數 異步調用原理解析

  • 如何優化Drawcall?

  在Unity中對Drawcall的優化有以下幾個策略:Drawcall batching,合並打包圖集,減少光照和陰影以及遮擋剔除和視錐剔除等。以下分別談一下各個策略的優缺點。

  1.1. Drawcall Batching

  Unity中對Drawcall的批次有兩種:靜態批次(static batching)和動態批次(dynamic batching)。但不論靜態批次還是動態批次都要求對象的材質是共享的,即不同材質的對象是無法進行批次的。而且要註意的一點:如果在腳本中調用材質時,使用Renderer.material會造成材質的拷貝,而使用Renderer.sharedMaterial來調用則不會拷貝材質。

  1.1.1. 靜態批次 Drawcall static batching

  場景中的多個物件如果是不移動的(包括位置、縮放、旋轉等),並且共享同一材質,比如地形、建築、花盆等,那麽可以選擇采用靜態批次。靜態批次只需要在Inspector勾選static選項即可。靜態批次需要註意的是,unity會將進行批次的多個對象合並成一個大的對象,也會導致內存損耗,有時候要避免太多對象靜態批次造成的內存過高。這也表明,優化並非絕對做好某一方面,而是平衡各個硬件的瓶頸和效率,選擇相對適中的方案。

  1.1.2. 動態批次 Drawcall dynamic batching

  動態批次是運動的物件在unity中也可以進行批次渲染,動態批次不需要手動設置,是unity自動進行的,但是這裏有諸多陷阱和約束,開發者需要遵守一定的限制條件才能享受動態批次的好處。

  根據unity官方文檔描述:

  1) . 動態批次是逐頂點處理的,因此僅對少於900個頂點的mesh有效。如果shader使用了頂點位置,法線和UV那麽僅支持低於300頂點的mesh,而如果shader使用了頂點位置,法線、UV0、UV1和切向量,則之多僅支持180頂點。

  2) . 縮放問題

  縮放對於批次是有影響的,這裏涉及到一個統一縮放和非統一縮放的概念。統一縮放即為三軸同比例縮放,比如(1,1,1),(2,2,2)(5,5,5)... 非統一縮放即為三軸不同比例縮放,如(1,2,1)(2,1,1)(1,2,3)等等。

  Unity對統一縮放的對象是不進行動態批次的,而對非同一縮放的對象是可以進行動態批次的。這裏有點詭異,查閱了一些資料,解釋如下:

  對於非同一縮放的物件,unity將其mesh進行了復制,因此即便是從相同物件進行的非同一縮放的兩個對象是兩份mesh;對統一縮放的對象來說,unity不對mesh進行復制,而是使用同一mesh進行縮放,此時復制mesh來進行批次渲染是不值得的,但是對於非統一縮放的對象,既然已經復制了mesh(不是為了批次,而是其他原因決定復制mesh),那麽進行批次是順帶實現的。

(參考 Dynamic Batching and Scale ——unity3d answers )

  3) . 使用了不同的材質,即便實質上是相同的(比如兩個一模一樣的材質),也不會進行批次。

  4) . 擁有lightmap的物件含有額外的材質屬性,比如lightmap偏移和縮放系數等,所以擁有lightmap的物件不能批次。

  5) . 多通道的shader會妨礙批處理操作,接受實時陰影的物件無法批次。

  

  註意: unity渲染是有順序的,渲染排序有可能打斷動態批次。

  例如:

  場景中有物件ABC,假設AB使用同一材質1,C使用材質2.那麽drawcall有可能是2個,也有可能是3個。

  如果順序為:

    1.渲染A,使用材質1

    2.渲染B,使用材質1

    3.渲染C,使用材質2

  那麽drawcall是2個,AB進行了動態批次。

  如果順序為:

    1.渲染A,使用材質1

    2.渲染C,使用材質2

    3.渲染B,使用材質1

  那麽drawcall就是3個,AB的批次被C打斷了。

渲染順序跟什麽有關呢?

首先根據物件到攝像機的距離,進行遠處物件先渲染近處物件後渲染。相同材質的物件盡量在一層,不要讓不同材質的物件進入這一層。如果無法保證這一點,那麽還有一種方法:修改shader中渲染隊列值。即打開shader 將subshader中的tag{}中queue 修改為小於2500的值。

渲染隊列小於等於2500時,unity認為其是不透明的,對於不同材質但z值相同對象,unity不對其進行排序,這樣能保證相同材質的多個對象能是一個批次,不同材質的對象如果進入兩個相同材質的對象之間,不會打破批次;

渲染隊列大於2500時,unity會對不同材質的對象進行排序,此時如果不同材質的對象進入到兩個相同材質的對象之間的話,會使相同材質的對象批次被打破。

  批次先寫到這,其實很多網上都有,不過有些沒深入講解,也有些沒給出解決辦法,我就使用每個方案時遇到的困難給出了自己的解決方案。其實批次還有不少研究的地方,之後想到了會繼續更新。

  延伸閱讀:   

  · Unity -Draw Call Batching

  · Unity Drawcall 優化手記

1.2. 合並圖集

  其實合並圖集也是利用了Unity的Drawcall batching。將多個紋理進行打包成圖集是為了減少材質,這樣多個對象共享一個材質,並進而使用同一個紋理和shader,觸發unity的動態批次。圖集打包工具有很多,Asset store中也可以搜到不少,比如Texture Packer Free 、 DrawCall Optimizer(收費) Mesh Baker Free 等等都可以將貼圖打包合並。

  但是合並圖集也有缺點,合並貼圖時應該註意選擇同時出現在屏幕的對象貼圖進行合並。如果不能做到這一點,那麽合並圖集可能起到反作用,即渲染一個對象需要加載過多無用貼圖,造成內存占用率升高。我的項目這個方案也是采用之後又棄用的,因為歸類同時出現在屏幕的貼圖並非易事!

  1.3. 光照和陰影

  實時光照和陰影可能增加Drawcall,帶有光源計算的shader材質會因為光照產生多個Drawcall。使用燈光會打斷Drawcall batching,盡量使用烘焙燈光貼圖等技巧來實現燈光效果。

  延伸閱讀:

    · Forward Rendering Path Details

    · Light Troubleshooting and Performance

  1.4. 遮擋剔除、視錐剔除

  這兩個Unity提供的剔除方案,出視野之後應剔除對象渲染。

  2. 物理組件

  3. GC 

  GC是unity自動回收內存垃圾的回收器,這雖沒有內存泄漏的風險,但是過多的垃圾回收會讓CPU高負荷。這裏就要避免不必要的內存申請和釋放。可以在某個腳本定時清理垃圾,如void update(){if(Time.framecount %5 ==0)System.GC.collect();}

  有以下幾點需要註意:

  3.1. 字符串的拼接會產生臨時字符串內存,移除代碼中的字符串拼接,改用string.format,或stringbuilder,這沒測。

  3.2. 用for代替foreach,foreach每次叠代產生24字節垃圾內存。100次循環就是2.4kB.

  3.3. 對象標簽tag比較采用comparetag,不要用tag=="mytag"這樣。

  3.4. 使用對象池。對象克隆也是調用new,因此對於可以循環利用的對象要采用對象池,比如子彈、特效、寶石等等。

  對象池使用時要註意一點:如果對象上掛了腳本,那麽數據需要每次進行初始化或對象回收的時候進行重置,否則下次再利用的對象腳本可能存留上次的數據,那極有可能出bug。

  3.5. 盡量使用struct而非class,因為struct是棧區,class是堆區。

  GC涉及到內存,詳細內容會在內存篇展開。

4. 腳本

  腳本中如果在update 函數中調用了Getcomponent等接口,最好將組件緩存。

  在update中的處理如果允許,盡量隔多幀處理一次。

  如:將update() {DoSth();} 修改為:update() { if(Time.framecount %5 == 0) DoSth();}

  其實腳本的優化主要就是針對update中復雜和耗費的邏輯進行優化,其次對於遊戲進行中的卡頓,可以修改邏輯,使用預加載方式,將遊戲進行中的對象在開始前一次性加載到內存。

 Unity官方給出的一些優化建議:

  1.PC平臺的話保持場景中顯示的頂點數少於200K~3M,移動設備的話少於10W,一切取決於你的目標GPU與CPU。   2.如果你用U3D自帶的SHADER,在表現不差的情況下選擇Mobile或Unlit目錄下的。它們更高效。   3.盡可能共用材質。   4.將不需要移動的物體設為Static,讓引擎可以進行其批處理。   5.盡可能不用燈光。   6.動態燈光更加不要了。   7.嘗試用壓縮貼圖格式,或用16位代替32位。   8.如果不需要別用霧效(fog)   9.嘗試用OcclusionCulling,在房間過道多遮擋物體多的場景非常有用。若不當反而會增加負擔。   10.用天空盒去“褪去”遠處的物體。   11.shader中用貼圖混合的方式去代替多重通道計算。   12.shader中註意float/half/fixed的使用。   13.shader中不要用復雜的計算pow,sin,cos,tan,log等。   14.shader中越少Fragment越好。   15.註意是否有多余的動畫腳本,模型自動導入到U3D會有動畫腳本,大量的話會嚴重影響消耗CPU計算。   16.註意碰撞體的碰撞層,不必要的碰撞檢測請舍去。

[Unity優化] Unity CPU性能優化 (難度3 推薦4)