天天看點

Unity Optimizing graphics performance——CPU部分(四)

接上一篇優化前言優化前言,在接下來的本文中,将詳細的介紹相關CPU優化。此部分包含光照優化以及drawcall等CPU優化細節

首先,為了渲染物體,CPU往往很多事情要去處理,比如:

  1. 計算光照及光照影響的物體
  2. 加載 shader以及加載shader所需的資料與參數
  3. 向GPU驅動程式發送指令,然後準備指令發送給GPU進行渲染,此過程俗稱DrawCall(DC這個概念很重要,在整個優化過程中站很大的比例)

    是以,CPU優化将從這三個方面入手優化。

針對于上面三種情況,每一種對CPU來說都是一種非常負荷大的操作。比如有非常多的可見物體需要渲染,最好是集中在一起,分成一個批次進行渲染,也就是一個Batch。比如,假如現有1000個三角面片,CPU在處理時,相對于處理1000個獨立的三角面片,那麼CPU處理一個有1000個三角面片組成的大網格要來的輕松許多。

同樣這些情況在GPU工作時也是有類似,但是CPU來渲染1000個獨立的面片相對于渲染1個由1000個三角面片組成的大網格,消耗顯而易見的要大的多。這個是因為CPU與GPU的體系架構所決定的。

下面簡單介紹一下CPU與GPU的架構

Unity Optimizing graphics performance——CPU部分(四)

圖1 CPU架構

Unity Optimizing graphics performance——CPU部分(四)

圖2 GPU架構

圖1、2中綠色的是計算單元,橙紅色的是存儲單元,橙黃色的是控制單元。

GPU采用了數量衆多的計算單元和超長的流水線,但隻有非常簡單的控制邏輯并省去了Cache。

CPU不僅被Cache占據了大量空間,而且還有有複雜的控制邏輯和諸多優化電路,相比之下計算能力隻是CPU很小的一部分。簡單來說GPU具有非常強的高并發的資料計算處理能力,而CPU相對于GPU強大之處在其的邏輯計算能力。是以有一種優化政策就是前面那種CPU将1000個面片合并為一個大的Mesh交給GPU處理,而不是分1000次,将1000個面片獨立發給GPU處理。

是以可以從以下幾種方式來減少CPU的工作量:

1. 合并網格,使用Unity的動态或者靜态批處理來合并網格

2. 合并圖集,比如将不同紋理合并打包成一張大的圖集,這樣在渲染時,可以多個物體共享一個材質

3. 盡可能的減少需要多次渲染的物體,比如反射、陰影、逐像素計算的光照

4. 減少可見物體,可以通過調整相機的視椎體

5. 使用遮擋剔除

将這些物體合并,并且針對于整個網格而言,使得每個網格至少合并了幾百個三角面片并且隻使用一個材質。

但是,有一點要注意,如果兩個物體不是共享一個材質,那麼合并這兩個物體并不能帶來性能的優化。最常見的情況是兩個物體所需要的紋理不同,這樣就可能導緻需要多個不同的材質。

是以我們在使用合并網格優化CPU性能時,一定要确認所要合并的對象是否共享相同的材質。

另外,當我們使用前向渲染路徑( Forward rendering path)來實作逐像素點光照計算時,在這種情況時,合并網格并不能帶來性能的提升,下面将對光照性能(Lighting Performance)如何來進行管理。

光照性能(Lighting Performance)

為了達到性能和效果的最佳結合,最好的方式是去建立一種不需要實時計算的光照,比如可以采取烘焙(bake)一張光照貼圖(Lighting Mapping)形式的靜态光而不是每幀去實時計算的光照。雖然烘焙一張Lighting Map的時間僅僅隻比在場景中布置一個實時光源的時間的長一丢丢,但是好處就很明顯,比如:

1. 靜态光照的處理速度往往比實時光照的速度要快很多

2. 視覺效果更好,因為可以烘焙出全局的環境光,并且使用這種光照映射可以使得效果更加平滑柔順更加好

在很多情況中,我們可以複雜問題簡單化去取代在場景中布置很多實時光源。比如,當我們需要實作一個邊緣照明效果的時,我們可以不布置一個直接照射相機的光源,我們可以在我們自定義的着色器(Shader)并且在裡面實作這種效果的計算(具體如何實作,将來學習Shader的時候再來補充連結)。

光照——關于前向渲染(Forward Rendering):

動态的逐像素點計算光照(Per-Pixel-Dynamic-Lighting)因為是基于每一個受到光照影響的像素的處理,是以這種方式的效果非常好但是計算有很繁重。是以我們要避免在哪些處理能力比較差的低端裝置上使用逐像素點計算的光照計算,比如移動裝置或者低端GPU的PC裝置上,我們可以使用烘焙的靜态的光源(Lighting Map)去實作光照,而不是使用每幀實時計算光照。

同樣動态逐頂點計算光照(Per-Vertex-Dynamic-Lighting)可以根據每個頂點變換來實作一個非常好的光照效果,但是同樣也需要注意避免多個光照影響一個頂點的情形,這樣會使計算負載增加。

光照——合并網格(Combine Mesh)

在合并網格時,避免合并那些因為距離遠且受到不同逐像素點光照影響的Mesh。當我們在使用逐像素點計算的光照的時候,需要經過多次渲染處理這些像素點才能達到我們所需要的光照效果。如果我們将兩個距離較遠且受到不同關照的網格合并在一起,這樣會增加網格的實際大小。并且在渲染過程中,在這個大Mesh中所有影響到像素點的像素光都需要被考慮在内,是以在渲染通道的數量又增加了,在計算量和記憶體上都增大了開銷。通常,渲染這種合并網格的渲染通道數是渲染每個獨立的網格的所需渲染通道數的總和,是以這種合并網格的方式實際效果不可取。

在我們渲染期間,Unity會找出一個網格周圍的所有光照,然後對這些光照進行計算并找出哪個光照對這個網格影響最大。Unity中的QualitySetting往往被用于更新最終有多少光照是像素光,有多少光照是頂點光。對于每一個光照計算來說,光源離網格有多遠,光照強度有多大,這些名額都很重要,甚至對于某類光照來說,這些名額甚至比其他的因素更重要(此處并不明白是什麼因素有待以後慢慢補充)。因為每一個光照都可以根據品質來設定一種渲染模式(每個Light中的Render Mode),一些設定為不重要的光照,以此一些不重要的光照他的渲染開銷可以降低。

比如:現在有一款駕駛遊戲,玩家在夜間開着頭燈駕駛車輛。這個頭燈是夜間最重要的光源,是以我們将他設定為最重要(Important)的光照,那麼其他的比如汽車尾燈或者其他微弱影響的光源,這類光源并不需要通過像素光來提升實際效果,是以我們可以将其Render Mode設定為NotImportant進而避免在一些沒有實際用處的地方浪費寶貴的渲染資源。

最重要的,優化像素光不僅僅是可以給CPU減負同樣也可以給GPU減負:

1. 可以降低CPU的DrawCall

2. 可以讓GPU減少頂點和像素光栅化的處

與逐點計算的像素光相同的情況還有實時陰影(Realtime Shadows),這個跟上面情況類似。

Draw Call Batching(drawcall的批處理)

在我們需要在螢幕上繪制一個物體的時候,引擎必須要向圖形處理API(比如OpenGL或者Direct3D)發送一個draw call 指令。

從CPU角度來說,Draw calls需要做很多準備來支援圖形處理API來處理每一次圖形調用(也可以了解為CPU為GPU每一幀渲染需要準備很多資料已經發送指令修改GPU渲染狀态),這個可以導緻CPU的消耗。主要是由于在不同的drawcall之間需要進行狀态的切換(比如渲染不同的材質時),因為在這個切換的過程中我們需要對CPU進行資料的收集和驗證并且通知并改變GPU來改變渲染狀态。

Unity引擎為此提出了兩種解決方案來解決DrawCall的問題:

1. Dynamic Batching(動态批處理): 對于在那些小的零散的Mesh,CPU将會對Mesh的頂點進行轉換,動态的将他們組合在一起,并且通知GPU将他們一次繪制出來。

2. Static Batching(靜态批處理):将一些不會移動并且不會發生改變的物體通過Static合并成一張大的網格,這樣可以使他們渲染起來更快。

與手動的合并GameObjects比起來,使用Unity内置的合并方式好處更多。但是值得注意的是,GameObjects還是可以單獨進行裁剪的。

但是Unity内置的合并方式也有缺點:

1. 靜态批處理,可能導緻更多的記憶體消耗,因為需要在記憶體中組成一個大的網格鏡像

2. 動态批處理,會導緻更多CPU的開銷,因為需要去重新計算每個網格的頂點位置并重新計算生成一張大的網格資料

注意:動态批處理與graphics job設定不相容(Player Settings中的graphics jobs決定Unity是利用主程序、渲染程序或者工作程序進行渲染任務。在可以設定的平台上,graphics jobs可以帶來較為可觀的性能改進)。如果設定了graphics jobs,在Standalone平台上,動态批處理是被禁用的,将不支援。

Material set-up for batching(為了批處理關于材質的設定)

1. 隻有當物體之間共享一個相同的材質,物體才能被批處理。是以如果我們想獲得好的批處理效果,我們可以在不同物體之間盡可能多的使用共享材質。

2. 如果有兩個一模一樣的材質,但是他們使用的是不同紋理,我們可以講兩個不同的紋理合并一個大的紋理,通常稱作紋理集(這裡引入了紋理集的概念,有興趣可以查一下相關的資料),如果這些所需都在一個圖集中,那麼我們可以将這兩個材質使用一個來代替。

3. 如果我們需要在腳本中通路共享材質屬性,這裡非常要注意一點,我們如果通過Renderer.Material來更改的話,會在記憶體中重新建立一個Material的副本,會帶來記憶體的消耗。但是我們可以通過修改Renderer.sharedMaterial來修改,這樣依然可以保持兩個物體共享一個材質(不過這種情況貌似會将動态修改的儲存下來,暫時不确定)。

4. 陰影投射(Shadow Caster)經常會被批處理合并在一起渲染,即使他們使用的材質不同也同樣會進行批處理。在Unity中陰影投射哪怕是使用了不同的材質都會進行動态批處理,隻需要在陰影通道處理時這些材質所需要的值是一樣的就可以進行批處理。比如:我們建立很多條闆箱會使用很多不同的材質和紋理,但是我們渲染的投射的陰影所需要的紋理前面的材質并不相關,是以這種情況我們可以進行批處理。(官方對這一塊的描述比較複雜,簡單來說,就是我們在渲染陰影時,如果陰影效果相同,同一個材質,也可以使用同一個材質然後搭配一張合并了不同紋理的紋理集)

Dynamic Batching(動态批處理)

Unity會自動的進行批處理,将GameObjects自動移動到一個相同的Drawcall來進行渲染,隻要他們滿足他們使用的相同的材質或者滿足其他的條件。動态批處理是Unity自動完成的,并不需要我們額外去做。不過需要注意集中打斷動态批處理的情況:

1. 動态批處理網格的時候因為是逐頂點進行處理的,是以在這方面是有一定的消耗。是以Unity限制動态批處理隻适用于合并後的網格頂點在900以下(比如兩個網格雖然使用了相同材質和紋理,但是兩個網格頂點總數超過了900此種情況就不會進行批處理)。但是如果除了頂點坐标以外我們還使用了法線以及UV坐标,那麼這個900就要除以3變成300了,如果我們在着色器(Shader)中使用了頂點坐标、法線、UV0、UV1以及切線,那麼這個總數就隻能是180了。但是随着硬體裝置性能的提升未來這個數值肯定會發生改變的。

2. 縮放會打斷批處理,比如相同物體才場景中進行了複制,其中一個的scale是(1,1,1),一個的scale是 (-1,-1,-1),這種情況下,Unity是不會進行動态批處理的。

3. 我們在處理使用 lightmap 的物體的時候如果有額外渲染參數設定也會打斷批處理:比如在 lightmap 包含了 lightmap index 和 offset/scale。簡單的來說使用動态光照的物體,在用lightmap時如果需要批處理,那麼必須使用完全一樣的lightmap location。(此處暫時不是很了解,回頭學習光照的時候會重新補充,暫時标記)

4. 如果Shader含有多個pass(渲染流水線),同樣也會打斷批處理。

1)在Unity中幾乎所有Shader在使用Forward Render Path時都支援多個光照效果,能非常有有效的為他們提供渲染管線來處理。是以這種per-pixel-light逐像素點計算的光照将不會批處理

2)如果使用Deferred Render Path(延時渲染),動态批處理将會被禁用,因為延時渲染會固定産生兩次Drawcall,來繪制物體兩次。

綜上,動态批處理的工作原理是将能合并的Mesh的通過CPU計算然後産生一張全新的Mesh,這種處理所帶來的消耗遠遠小于多次DrawCall。一次Drawcall 所需要的資源資料往往取決很多方面,最主要的是給 graphic api 使用。比如,在控制台或者像Apple Metal這類現代graphicAPI,drawcall對CPU的消耗往往要小得多,是以動态批處理并不能帶來多大的性能提升。

Static Batching(靜态批處理)

靜态批處理允許引擎去為了渲染那些規定使用Share Material和不發生移動的幾何體而減少drawcall,這個比動态批處理更有效,靜态批處理不需要去動态的計算頂點重新生成新的Mesh,不過會産生多餘的記憶體消耗。因為被靜态批處理的對象無論在運作時還是在編輯時都會在記憶體中建立一個鏡像,是以有時為了控制記憶體消耗,不得不犧牲渲染性能,避免對某些遊戲物體進行靜态批處理。例如,如果對那種非常密集的樹林進行批處理,那樣會對記憶體造成很大的消耗。

靜态批處理的内部原理:

靜态批處理通過将Static标記遊戲對象轉換到世界空間并為它們建構一個大的頂點和索引緩沖區。然後,對于同一批中可見的GameObjects,進行一系列簡單的drawcall,這樣中間沒有産生過多的且複雜的狀态變化。

從技術來說,這樣做不是減少了 3DAPI的drawcall ,而是減少了渲染過程中狀态的變化。不過靜态批處理也有限制,在大多數圖形API中隻能處理 64k的vertex 和 64的 indices(OpenGL ES 是48k,macos上是32k)。

Tips:

一般情況下,Unity隻對 Mesh Renderers、Trail Renderers、Line Renderers、Particle Systems和Sprite Renderers這幾類進行批處理。對于skinned Meshes、Cloth或者渲染元件不能被批處理

Renderer隻會與其他采用相同方式的Renderer進行批處理。

半透明的Shader往往需要物體的渲染順序是從前到後的方式來渲染,這樣可以不影響透明物體的渲染。因為這順序要求嚴格,是以如果要進行批處理的話,必須要遵守這個渲染順序。這種做法的話,相對于批處理不透明的物體,半透明的物體能進行批處理的次數明顯減少

另外手動合并一些相鄰的網格也是一種不錯的draw call batch。比如一個有着很多抽屜的碗櫥,通常可以在3D模組化的程式中或者使用Mesh.CombineMeshes來合并成一個新網格。

大緻關于Unity的CPU優化方案都集中于此,後面還會補充一下Occlusion Culling*強調内容*

繼續閱讀