2020年7月31日 星期五

Mobile 渲染效能分析 (2)

  • 前言
在上一篇,我從硬體角度去分析渲染流程,

並以 Imagination Technologies 的 PowerVR 作為範例,

深入探討如何有效利用裝置機制,讓渲染效能最佳化。

在本篇將從軟體(i.e. Unity3D)視角觀點觀察其渲染機制,

研究效能瓶頸原因,進而找出優化方案。

  • Unity 渲染機制
Unity 對物件的渲染,需要下述三個元件

  1. Mesh : 紀錄物件的形狀,也就是節點資料。
  2. Materail : 描述渲染方法,包含使用的 Shader 和運算所需的參數。
  3. Transform : 決定物件的顯示位置。
Unity 在每呼叫一次 Draw Call 時,GPU 都會從 System memory 去讀取這三個元件資料。



在 Mobile 渲染效能分析 (1) 裡,我提到對於 mobile 而言,

對 System memory 的存取是費時且耗電的一個行為。

因此,如何有效減少 Draw Call 就成了 Mobile渲染優化的一個定石。


Unity 對於減少 Draw Call 的方案,主要有下述三種

  1. Dynamic(Static) Batching
  2. GPU Instancing
  3. SRP Batcher
接下來會針對這三種方案,去一一說明。


  • Dynamic(Static) Batching
既然 Unity 渲染每個物件都會呼叫一次 Draw Call,

那我們可以很直覺地想到如果將所以物件合併成一個之後再渲染,

是不是只需要呼叫一個 Draw Call 就可以了?

這種將物件合併渲染的方法稱作 Dynamic(Static) Batching。

其中 Dynamic 和 Static 的差別只在於物件合併是否再 run-time 時運作。

至於物件要如何合併,

回想我在上面提到物件渲染所需的三個元件 : Mesh、Material 和 Transform。

物件合併就是要把每個物件的的這三個元件按照類別組合再一起。

首先將焦點放在 Mesh 和 Transform 上面,

Mesh 紀錄物件的形狀, Transform 決定物件的顯示位置。

因此,我可以將每個物件的 Mesh 先經過 Transform 的轉換在合併。

這種先轉換座標在合併的好處是我可以省去 Transform 的資料。

如果我將 Mesh 和 Transform 分別合併,我需要儲存所有 Mesh 和 Transform 的記憶體容量。

但我將 Mesh 經 Transform 轉換後在合併,我只需要儲存 Mesh 的記憶體容量(註1)。

Mesh 和 Transform 可以簡單的被合併,但 Material 就沒這麼簡單了。

最主要原因是 Material 包含了 Shader 資料。

試想,你可以簡單的把兩個相異的 float array 合併成一個 float array,

但你要如何把兩段不同的 source code 合併成一個?而且不能有 bug ...

因此 Unity 對於 Dynamic(Static) Batching 的限制是被合併的物件共用相同的 Material,

既然使用了相同的 Material ,就沒有合併的問題。


將物件的 Mesh 合併


從上述的討論可以得知,Dynamic(Static) Batching 是透過找出使用相同 Material 的物件,

將其模型資料合併,來達到減少對 System memory 的存取(i.e. 呼叫 Draw Call)。

這種做法缺點在於需要大量的記憶體空間來儲存合併的模型資料,

對於 Dynamic Batching 來說,甚至要負擔計算合併的開銷,實際上對於效能優化改善有限,

因此在 Unity 5.4 之後提出了 GPU Instancing 作為減少 Draw Call 的另一個方法。


  • GPU Instancing
Dynamic(Static) Batching 提供了一個很好的方向來減少 Draw Call 的呼叫,

但需要大量的記憶體空間來儲存合併後的結果;因此,減少記憶體需求就成了改進的目標。

仔細分析記憶體需求原因是因為儲存了大量的模型節點資料,

所以如果和 Material 一樣,我們限制所有物件共用同一個 Mesh 的話,

是不是能減少對記憶體容量的需求?

答案是可能的,舉個範例來說好了,

我有兩個物件共用一個模型,這個模型的節點數量假設是 1000,

就有 1000 x 3 = 3000 個浮點數(每個節點都是一個三維座標),

如果我使用 Dynamic(Static) Batching 的話我需要 1000 x 3 x 2 = 6000 個浮點數的記憶體需求。

但因為 Mesh 資料是相同的,彼此差別只在 Transform 資料,

因此我只需要合併 Transform 資料,每個 Transform 資料有 16 個浮點數 (= 4x4 Matrix)。

所以總共我需要 1000 x 3 + 16 x 2 = 3032 個浮點數的記憶體需求,節省了將近一半的空間。

這種同時限制共用 Material 和 Mesh ,只合併不同參數資料 (這裡為 Transform)的作法,

就叫做 GPU Instancing。

這裡衍生思考一下,既然能夠合併 Transform 資料,

這表示也能合併其他資料像是 Materail Parameter。

Material 要共用是因為 Shader 無法合併的關係,但不代表 Materail Parameter 無法被合併。

把 Run Shader 想成 Call Function,Materail Parameters 就可以視為 function 的 input arguments。

因此 Materail Parameter 的合併不會影響到渲染結果。



從上面的討論可以得知,GPU Instancing 和 Particle System 相當的吻合,

Particle System 是將同一個物件(通常是個 Billboard)大量渲染, 

符合了 GPU Instancing 對 Material 和 Mesh 共用的限制。

因此大部分的 GPU Instancing 展示都是用 Particle System 來實現。


  • Scriptable Render Pipeline Batcher

在前面提的兩項效能優化方案,都是避免 Draw Call 的大量呼叫,

減少對 System memory 的存取;但兩種方案都對渲染物件有很大的限制。

回過頭來看,為什麼每一次的 Draw Call 都需要去重新讀取 System memory?

這是因為 Unity 為了要讓渲染能夠更彈性化,

能提供一個跨平台非固定(non-constant)的 buffer 儲存渲染參數,所做的一個讓步[1]。

這樣的結果造成了當 Draw Call 發生,必須要從 System memory 重新讀取所有參數,

即使我只改變了某個特定的參數(ex. mesh 或 material parameter)而已。

這時換個角度來思考,如果只更新被變更的參數,減少從 System memory 讀取的資料量,

是不是能夠提供一個更彈性的渲染優化方案?


Scriptable Render Pipeline Batcher(之後簡稱 SRP Batcher),

顧名思義就是針對 Scriptable Render Pipleline 的一個渲染優化技術(註2)。

可以針對物件渲染時,變更的參數資料單獨更新。

這些變更的參數資料可以是 mesh 或 material parameters,但不包含 Shader。

因此,對渲染物件的限制也就只有要求必須使用同一個 Shader(註3)。

比較起前兩個渲染優化方案提供更大的彈性。




  • 結論
物件的渲染最終還是由硬體裝置輸出,因此如何提供硬體一個更有效率的渲染方式便是使用者

需要去琢磨的部分。

對 Mobile 而言, System memory 的存取往往是效能瓶頸的所在,

對此,通常會從兩個方向去優化

  1. 減少對 System memory 的存取量
  2. 減少對 System memory 的存取次數

方向 1 可以藉由減少模型大小 (減面或減節點)和減少貼圖的解析度,

來降低讀取的數量,縮短讀取時間,減少裝置耗能。

方向 2 Unity 提供了 3 個方案協助使用者減少存取次數。


如何應用上述兩個方向,同時不影響畫面美觀,就是遊戲開發人員最需要面對的課題。


  • References



  • 附註
1.這裡要注意的是因為 Unity 的 Mesh 結構有節點容量的限制,因此有可能無法將所有物件合併     成一個。
   而因為這樣的限制, Dynamic batching 對於物件模型的節點數量有著更嚴格的要求,
   詳細要求可以參考 [2]。

2.SRP Batcher 只能在使用 Scriptable render pipleline 的環境下使用,若使用 Unity 內定的
   渲染環境, SRP Batcher 將無法作用。

3.同一個 Shader 指的是物件 material 引用的 shader file 來源必須是同一個,且擁有相同的
   keyword。Unity 的 Variant System 可以透過 keyword 管理,讓 shader 可以在 rum-time 時做到
  類似 Script 的 #if-#else 一樣切換運作內容。

沒有留言:

張貼留言