🎲

Unity 6.3: RSUV で描画を軽量化する

に公開

Renderer Shader User Value (RSUV)

https://docs.unity3d.com/6000.3/Documentation/Manual/renderer-shader-user-value.html

Unity 6.3 で追加された Renderer Shader User Value (RSUV) は、SRP 環境で MeshRenderer や SkinnedMeshRenderer に対して設定できる 32 ビットの整数で、マテリアルプロパティのようにシェーダーから参照できます。

C#
uint value = meshRenderer.GetShaderUserValue();
meshRenderer.SetShaderUserValue(value);
HLSL
uint value = unity_RenedrerUserValue;

RSUV は通常のマテリアルプロパティとは GPU への値の渡され方が異なり、特に大量描画系のシチュエーションで描画のパフォーマンスを向上できる場合があります

RSUV が登場した背景

通常、内容の異なるマテリアルは別々のインスタンスとして作成される必要があり、大量にマテリアルインスタンスが作成されるとメモリ消費の観点でパフォーマンスにネガティブな影響があります。これを軽減するために、MaterialPropertyBlock を使ってマテリアルインスタンスを複製せずに一部のプロパティのみをオーバーライドする手法も使われてきました。

Built-in Render Pipeline (BiRP) の頃はマテリアルのインスタンスが変わったり MaterialPropertyBlock を使ったりすると Dynamic Batching が解除され Draw Call が増えるという問題がありました。それに対して SRP で登場した SRP Batcher ではマテリアルインスタンスが異なる際の描画パス発行を効率化し、パフォーマンスが改善されました。しかし一方で SRP Batcher は MaterialPropertyBlock に対応していないため、必然的にマテリアルインスタンスを複製せざるを得ず、依然としてマテリアルのメモリ消費が問題になりやすくなっていました。

これを改善するのが RSUV です。RSUV は通常オブジェクトごとのモデル行列などを GPU に送信するために使われている GPU 定数バッファ UnityPerDraw の一部を利用し、オブジェクトごとに 32 ビットの数値を設定します。これによって共通のマテリアルインスタンスを使いながら、オブジェクトごとに異なるパラメータを渡すことができるというわけです。

さらに RSUV は GPU Resident Drawer と併用することでパフォーマンスを向上できます。GPU Resident Drawer は内部的に BatchRendererGroup によって GPU インスタンシングを行い、マテリアルインスタンス・メッシュ・シェーダーバリアントが同一のオブジェクトをより効率的に描画できます。ここにおいても RSUV を利用することで、GPU インスタンシングを利用しながらオブジェクトごとの見た目にバリエーションを持たせることができます。

SRP Batcher 環境における GPU インスタンシングは BatchRendererGroup を直接利用する方法もありますが、BatchRendererGroup は通常の Renderer とは独立した仕組みで描画する必要があり煩雑でした。GPU Resident Drawer では Renderer を使いながら BatchRendererGroup の機能も利用できる点が魅力ですが、直接 BatchRendererGroup を使う場合と異なりインスタンスごとに異なる値を持たせる方法がこれまで提供されていませんでした。RSUV はちょうどこのギャップを埋める存在といえると思います。

使ってみる

32 ビットのカラーを RSUV に詰めて MeshRenderer にセットします。

C#
Color32 color = /* ... */;
var colorEncoded = (uint)(color.r << 24 | color.g << 16 | color.b << 8 | color.a);
meshRenderer.SetShaderUserValue(colorEncoded);

今回は ShaderGraph から参照します。現状は RSUV を参照するノードが提供されていないようで、Custom Function ノードを使用する必要があります。OutputsVector4Value を追加し、TypeString に設定して、Body に次の内容を入力します。

HLSL
uint c = unity_RendererUserValue;
Value = float4((float)((c >> 24) & 255) * (1.f / 255.f),
                        (float)((c >> 16) & 255) * (1.f / 255.f),
                        (float)((c >> 8) & 255) * (1.f / 255.f),
                        (float)((c >> 0) & 255) * (1.f / 255.f));

32 ビットを超えるデータの渡し方

RSUV は 32 ビット固定ですが、RSUV を GraphicsBuffer へのインデックスとして利用することで、間接的により大きなデータを渡すこともできます。この GraphicsBuffer に対する更新の量や頻度、それにかかる CPU / GPU 時間は大きくなりすぎないように工夫する必要があると思いますが、RSUV のパフォーマンス上の利点を生かしながらさらに柔軟な表現が可能になるでしょう。

パフォーマンス計測

Discussions にも詳細なパフォーマンス計測結果がありますが、自分でも測ってみて、ついでに DX12 のレイヤでどのような API 呼び出しになっているのか調べてみます。とりあえず9万の Cube を配置して、それぞれに異なる方法でオブジェクトの色を設定しました。

  • マテリアルパラメータ (Color) を利用したもの
  • マテリアルパラメータ (Color) を利用しつつ GPU Resident Drawer を有効化したもの
  • RSUV を利用したもの
  • RSUV を利用しつつ GPU Resident Drawerを有効化したもの

GPU 時間

PIX で 1 フレームにかかる GPU 時間を計測しました。

- マテリアル RSUV
GRDなし 13.649 ms 13.587 ms
GRDあり 13.677 ms 3.343 ms

RSUV+GRD 条件が突出して高性能ですが、あとはほぼ変わりないようです。

PIX を確認すると、RSUV+GRD 以外ではオブジェクトごとに描画コマンド DrawIndexedInstanced が発行されています。

RSUV+GRD では 256 インスタンスごとに DrawIndexedInstanced がまとめられており、GPU インスタンシングが効いていることがわかります。

CPU 時間

Profiler で 1 フレームにかかる CPU 時間を計測しました。

- マテリアル RSUV
GRDなし 45 ms 32 ms
GRDあり 35 ms 2.6 ms

ここも主には GPU へのコマンド発行にかかる時間が変化しており、GRD+RSUV 条件の性能の高さが目立ちます。

メモリ

Memory Profiler で Resident Memory を計測しました。

- マテリアル RSUV
GRDなし 2.65 GB 1.20 GB
GRDあり 2.59 GB 1.11 GB

RSUV でマテリアルインスタンスが減った分が顕著に現れてますね。

所感

全体的にはとにかく GPU Resident Drawer というか GPU インスタンシングが強力で、そのパワーを発揮しながら、オブジェクトの見た目にを多様に変化させたい場合に RSUV が役に立ちそうです。非 GRD 環境では実行時間よりは主にマテリアルインスタンスの確保するメモリ消費量の削減を目的として RSUV を利用するのがよさそうです。

Discussion