Unity 6.3 新增了名為 Renderer Shader User Value(RSUV)的功能。RSUV 是可針對每個 Renderer 元件設定的 32 位元整數值,也可以在著色器中參照。Unity 6000.3.0f1 以後,MeshRenderer、SkinnedMeshRenderer 可使用 RSUV;Unity 6000.5.0f1、6000.4.4f1、6000.3.13f1 以後也支援 SpriteRenderer*1、SpriteShapeRenderer、TilemapRenderer。用法很簡單,只要以 Renderer.SetShaderUserValue() 對 Renderer 設定 RSUV,並在著色器中透過 unity_RendererUserValue 參照即可。
uint value = /* ... */; meshRenderer.SetShaderUserValue(value); value = meshRenderer.GetShaderUserValue();
uint value = unity_RendererUserValue;
此範例的效果同樣可以用材質參數實現。不過使用 RSUV 時,尤其在大量繪製的情境下,記憶體配置量與影格率都有可能比材質更好。本章將說明 RSUV 出現的背景與有效的使用方式,並介紹效能測量結果。
首先回顧一下為何需要 RSUV。在 Unity 中同時顯示大量物件時的效能一直是令人頭痛的問題。大量繪製的效能問題主要有三點:「切換材質的 CPU 負載」、「DrawCall 增加」、「複製材質造成的記憶體配置」。
繪製多個持有不同材質的物件時,每次繪製影格都必須逐一收集各物件的材質資料並送往 GPU,此時的 CPU 處理成本很高。SRP Batcher 解決了這個問題,它將材質資料常駐於 GPU 記憶體中,只要著色器相同,就能以相對較低的成本繪製多個持有不同材質的物件。
GPU Instancing 是將多個擁有相同著色器、相同網格、相同材質的物件以一次 DrawCall 繪製的方法。它的適用條件比 SRP Batcher 嚴格,但在繪製大量物件時能發揮超越 SRP Batcher 的效果。Unity 6 推出 GPU Resident Drawer 後,即使是使用一般 MeshRenderer 的繪製,只要符合條件也會自動進行 GPU Instancing。
最後一個問題是:想在使用 SRP Batcher 或 GPU Instancing 的同時,也讓材質帶有變化的情境。Unity 很早就有 MaterialPropertyBlock 這項功能,能夠不複製材質實例而只覆寫部分參數,但它與 SRP Batcher 和 GPU Resident Drawer 並不相容。RSUV 解決了這個問題,可以在使用 SRP Batcher 和 GPU Resident Drawer 的同時,不透過材質就將每個物件各自不同的 32 位元參數傳給著色器。
RSUV 是同時解決上述三個問題所需的最後一塊拼圖。既能以 SRP Batcher 壓低切換材質的成本,又能以 GPU Resident Drawer 進行高效率的 GPU Instancing,同時還能讓每個實例呈現不同的繪製變化。即使在 GPU Resident Drawer 的自動 Instancing 無法生效的情況下,也能避免複製材質實例,獲得抑制記憶體消耗的好處。
實際來看 RSUV 的用法。RSUV 在 C# / HLSL 中以 32 位元整數(uint)表示,因此若要傳遞例如 32 位元的顏色(RGBA 4 個 channel 各 8 位元),需如下以位元運算進行打包。
Color32 color = /* ... */; var colorEncoded = (uint)(color.r << 24 | color.g << 16 | color.b << 8 | color.a); meshRenderer.SetShaderUserValue(colorEncoded);
接著是著色器端的實作,此處假設使用 ShaderGraph。ShaderGraph 並未提供取得 RSUV 的專用節點,因此使用 Custom Function 節點,依下列方式設定(圖 5.1)並輸入程式碼。
圖 5.1: 節點的設定
uint rsuv = unity_RendererUserValue; Value = float4((float)((rsuv >> 24) & 255) * (1.f / 255.f), (float)((rsuv >> 16) & 255) * (1.f / 255.f), (float)((rsuv >> 8) & 255) * (1.f / 255.f), (float)((rsuv >> 0) & 255) * (1.f / 255.f));
連接節點,確認已能接收到 RSUV。
為了確認 RSUV 在大量繪製的情境下能發揮效果,以下列條件測量了記憶體配置量與影格率。
圖 5.2: 基準測試畫面
首先比較各種參數設定方法的記憶體配置量(表 5.1)。
表 5.1: 記憶體增量
| 條件 | 記憶體增量 |
|---|---|
| 每個 Renderer 一個材質 | 15.79 MB |
| MaterialPropertyBlock | 3.32 MB |
| RSUV | 1.66 KB |
由於不會複製材質,記憶體配置量理所當然地大幅減少。接著來看影格率。首先是每影格參數不變的情況(表 5.2)。
表 5.2: 每影格參數不變時的影格率(FPS)
| 條件 | 無 GPU Resident Drawer | 有 GPU Resident Drawer |
|---|---|---|
| 每個 Renderer 一個材質 | 33.8 | 38.8 |
| RSUV | 49.9 | 59.8 |
| MaterialPropertyBlock | 28.9 | 28.9 |
由此可知,RSUV 的效能高於每個 Renderer 一個材質的做法,而且搭配 GPU Resident Drawer 後效果更為明顯。單獨使用 RSUV 即可削減切換材質的成本,再加上能以 GPU Resident Drawer 進行 GPU Instancing,渲染執行緒上發出繪製命令的負載也得以減輕。接著來看每影格參數會變化的情況(表 5.3)。
表 5.3: 每影格參數變化時的影格率(FPS)
| 條件 | 無 GPU Resident Drawer | 有 GPU Resident Drawer |
|---|---|---|
| 每個 Renderer 一個材質 | 11.1 | 8.1 |
| RSUV | 48.0 | 60.0 |
| MaterialPropertyBlock | 26.1 | 25.2 |
與沒有參數更新的情況相比,每個 Renderer 一個材質的做法效能大幅下降,而 RSUV 幾乎沒有劣化。變更 1 個材質參數就必須將整個材質重新上傳至 GPU,相對地,RSUV 是作為每影格、每個 Renderer 都一定會送往 GPU 的緩衝區的一部分傳送*2,因此更新 RSUV 所額外產生的成本可以壓得很低。有趣的是,大量更新材質參數與 GPU Resident Drawer 似乎搭配不佳,效能反而比停用 GPU Resident Drawer 時更低。如上所述,可以看出 RSUV 在「參數更新」與「發出繪製命令」兩方面都較為有利。特別是在 RSUV 使 GPU Instancing 成為可能的情況下(例如存在大量著色器與網格相同的物件),RSUV 的效果會大幅提升。
RSUV 雖然有助於提升效能,但只能傳遞 32 位元以內的值是它的一大弱點。以下介紹克服此限制的技巧。要用 RSUV 傳遞 32 位元放不下的資料,有效的做法是將目標資料存放在另一個 GraphicsBuffer 中,RSUV 則傳遞該 GraphicsBuffer 中的索引。這個方法會因上傳 GraphicsBuffer 而產生額外成本,因此對 GraphicsBuffer 的更新頻率與大小可能需要有一定的考量。
[StructLayout(LayoutKind.Sequential)] struct VariantGpuData { public Vector4 baseColor; public Vector4 surface; } private static readonly int VariantBufferId = Shader.PropertyToID("_RsuvVariantBuffer"); // 上傳並配置 GraphicsBuffer var buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, count, UnsafeUtility.SizeOf<VariantGpuData>()); var cpuBuffer = new VariantGpuData[count]; FillCpuBufferValues(cpuBuffer); buffer.SetData(cpuBuffer, 0, 0, count); Shader.SetGlobalBuffer(VariantBufferId, buffer); // 設定 RSUV renderer.SetShaderUserValue(index); // ... // 不再需要時釋放 buffer.Dispose();
struct RsuvVariantRecord { float4 baseColor; float4 surface; }; StructuredBuffer<RsuvVariantRecord> _RsuvVariantBuffer; // 存取 RsuvVariantRecord record = _RsuvVariantBuffer[unity_RendererUserValue];
本章介紹了 RSUV 出現的背景、效能測量,以及基本與進階的用法。RSUV 與材質相比雖有 32 位元的限制,但視情況能為效能帶來大幅改善。請看清 RSUV 能發揮效果的情境,並善加運用。
[*1] SpriteRenderer 的 RSUV 取得與設定需要 2D Module。
[*2] 僅限未進行 Instancing 的情況。進行 Instancing 時,會與其他實例參數一起透過 GraphicsBuffer 傳遞,且只在整體參數變更、GraphicsBuffer 變為 dirty 的時間點才上傳。