Universal Render Pipeline環境でのシェーダー最適化対策

## TL;DR
ShaderLabメモリ落ちに苦しんでいる方、アプリケーションのビルド時間が過度に長く、
なんとなくシェーダーが原因ではないかと考え始めた方はとにかく全部読んで損はありません。

## 概要

URPを使うプロジェクトでのメモリによるクラッシュの報告を受け、それの調査に取り掛かった際のやったことを書きます。
主にShaderコンパイルが関わるShaderLabのメモリ・ビルド時間に関わる話。

この記事はURP10.2.2を基準にして作成しています。

## 一般的なシェーダー最適化の話
まずこの辺が整理出来ていなかったのに気づいたのでURP以前にここから整理し始めました。

#### いつも`#pragma multi_compile`よりは先に`#pragma shader_feature`に出来ないか検討する。
- どこにも含まれなかったvariant combinationは自動で外れることになります。(含まれる含まれない判定は、ビルド・もしくはAssetbundleなどの結果物に参照があるかどうかであります。)
- 結果物から外れたvariantはそもそも含まれてないため、コード上で`Shader.EnableKeyword`などを用いて操作する場合は使えない

#### `multi_compile`より`multi_compile_vertex` or `multi_compile_fragment`に出来ないか検討する。

```hlsl
Shader "Hoge/Fuga/Piyo"
{
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: vertex, keywords <no keywords>
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: vertex, keywords _HOGE
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: fragment, keywords <no keywords>
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: fragment, keywords _HOGE
  #pragma multi_compile _ _HOGE

  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: vertex, keywords <no keywords>
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: fragment, keywords <no keywords>
  // Compiled shader: Hoge/Fuga/Piyo, pass: <unnamed>, stage: fragment, keywords _HOGE
  #pragma multi_compile_fragment _ _HOGE

  VertexOutput vert ( VertexInput v)
  {
    ...
  }

  half4 frag (VertexOutput IN) : SV_Target
  {
    ...
    
    #if _HOGE
     hogehoge
    #endif
  }
}
```
  multi_compile/shader_featureのみだとvertex/fragment両方に含まれるので、このキーワードがvertexもしくはfragmentのみで使われることが自明であれば(特に自作するキーワードにつきましてはvertex/fragment両方全部使われることは体感上稀にしかなかったです)、余計な分を減らすことが出来ます。ちょこっと変えるだけで半分に減らすことが出来ると考えると積極的に使いたいですね。

#### シェーダーを二つに分けるという手もある
- ShaderLabは現状ロードすべきのvariantだけではなく、全てのビルド上含まれている該当シェーダーのvariant combinationを全部メモリに載せます。(ただしシェーダーのコンパイル自体は該当するvariantのみが行われます)
- なので、相互同マテリアルに存在しなければならない組み合わせでなければ、multi_compileの代わりにシェーダーを二つに分けることで管理のコストは上がるがhlslファイル分割・関数化・UsePassなりを用いてコードを重複を防ぎながら片方だけをShaderLabに載せるようにすることが出来ます。(やや手間があることではありますが)

#### その他
- ProjectSettings/GraphicsのShaderStrippingを有効活用
- Addressable/Assetbundleを使っているのであれば重複を防ぐ
- IPreprocessShadersを有効活用 (後述)
などなど・・

## 本題

#### URPでのシェーダープロファイル
- Profilerを使う (それはそう)
- Frame Debuggerを使う (それもそう)
- Universal Render Pipeline Assetの設定を変える
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165110/64f9b8c6-00f1-4717-7a05-c17c9bb021ff.png)

Shader Variant Log Levelを変えることで、ビルド時に下記のようにどれぐらいシェーダーが含まれ、どれが外れてどれが含まれたかをログに出してくれます。
[ShaderPreprocessor.cs#L328](https://github.com/Unity-Technologies/Graphics/blob/9901704/com.unity.render-pipelines.universal/Editor/ShaderPreprocessor.cs#L328)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165110/3bb690cb-63a9-76ac-cff3-f92f62897fab.png)
なのでビルド一番最後のログを見ると、全体にどれぐらいVariantCombinationがあり、ビルドにどれぐらい含まれたかが確認出来ます。

また、このオプションを有効にすることでアプリケーション上でシェーダーコンパイルが行われた際ログ上で確認が取れるようになります。

```
Compiled shader: Hidden/Universal Render Pipeline/FallbackError, pass: <unnamed>, stage: vertex, keywords <no keywords>
Compiled shader: Field/PBR/CloudTransparent, pass: Forward, stage: fragment, keywords FOG_EXP2 LIGHTMAP_ON _EmissionTypeNormal _MIXED_LIGHTING_SUBTRACTIVE
```

#### アプリケーションに含まれてしまったUniversal Render Pipelineのシェーダーを排除する
Universal Render PipelineのdefaultシェーダーはLit.shaderとなっており、これが例のStandardシェーダーみたいに、色んなライティング組み合わせが混ざったビルドに含まれるとそれなりにShaderLabのメモリを占有することになります。
昔これを排除するために参照されるマテリアルを頑張って消すなり色んな工夫をしていたのですが、

Unity2018.2からは[IPreprocessShaders.OnProcessShader](https://docs.unity3d.com/ScriptReference/Build.IPreprocessShaders.OnProcessShader.html)という仕組みが用意されており、便利にビルドに含まれるシェーダーを制御することが出来るようになっています。
ビルド・Addressablesに参照がある対象のvariantについて、ここを通してユーザーが操作可能とする仕組みです。
ライト設定やフォグなどの環境については、同じく対象となっているシーンの環境の組み合わせと照らし合わせてくれます。

使用例はURPパッケージ内に結構良い感じのスクリプトがありますので、この辺を参考にすれば良いでしょう。
[ShaderPreprocessor.cs](https://github.com/Unity-Technologies/Graphics/blob/9901704/com.unity.render-pipelines.universal/Editor/ShaderPreprocessor.cs)

今回のプロジェクトでは基本URP用スクリプトはなく、全部自作のスクリプトを使っていたためこの辺は排除することにしました。

```csharp
public class OptimizeShaderPreprocessor : IPreprocessShaders
{
    //IPreprocessShadersの呼び順を制御します。値が小さいほど先に呼ばれます。
    int IOrderedCallback.callbackOrder => default;

    void IPreprocessShaders.OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
    {
        if (shader.name.StartsWith("Universal Render Pipeline", System.StringComparison.OrdinalIgnoreCase))
        {
            //URP命名に引っかかったら対象から全て外す
            data.Clear();
        }
    }
}
```

#### それでもかなりShaderLabが大きい。URPシェーダーを書く際に必ずしも必要と思われるキーワード

URP環境上で新たにシェーダーを書く際に、なんとなくそのままLit.shaderなどからコピペーして持って来る部分があるでしょう。
自分は下記のところを何も考えず持ってきていました。

```
// Universal Pipeline keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE
```
さてと、これをこのまま使うとすると・・？

```
// Universal Pipeline keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS // 1 * 2 = 2
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE // 2 * 2 = 4
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS // 4 * 3 = 12
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS // 12 * 2 = 24
#pragma multi_compile_fragment _ _SHADOWS_SOFT // 24 * 2 = 48
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION // 48 * 2 = 96
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING // 96 * 2 = 192
#pragma multi_compile _ SHADOWS_SHADOWMASK // 192 * 2 = 384
#pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE // 384 * 2 = 768
```

あの・・自分のキーワードを書く前に、もう768個のバリエーションが出来てしまいました。
ここで必要に応じて追加したのを足していくとどうなるでしょう。

```
#pragma multi_compile _ _A // 768 * 2 = 1536
#pragma multi_compile _ _B // 1536 * 2 = 3072
#pragma multi_compile _ _C // 3072 * 2 = 6144
#pragma multi_compile _ _D // 6144 * 2 = 12288
#pragma multi_compile _ _E // 12288 * 2 = 24576
```
32個で済ませるものが 24576個になってしまった。
ある程度はURP側でもこの辺を認識しているのか、これがまるごと全部載ることはなく、[ShaderPreprocessor.cs](https://github.com/Unity-Technologies/Graphics/blob/9901704/com.unity.render-pipelines.universal/Editor/ShaderPreprocessor.cs)
のところで理屈上ありえないパタンなどはある程度防ぐようになってますが、まだまだ不十分で

- 無効の場合の最適化はあるが、有効のみの最適化が網羅出来ていない (後述)
- そもそものURPで必要としているvariant combinationが多すぎて、IShaderPreprocessorで弾くのにも結構な時間を必要とするためビルド時間が伸びている

##### それぞれのパラメータの解説

- Universal Render Pipeline Assetの設定ファイルに応じたキーワード

`#pragma multi_compile _ _MAIN_LIGHT_SHADOWS`
メインライト(EnvironmentでSunとして設定されているライト。なければ一番明るいライト。Directionalに限る)の影を落とすかどうか
`#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE`
ShadowCascadeが設定されているかどうか
`#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS`
Main以外のライトを反映させるかどうか
`#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS`
Main以外のライトの影を落とすかどうか
`#pragma multi_compile_fragment _ _SHADOWS_SOFT`
SoftShadowにするかどうか
`#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING`
ライトマップで影を混合するかどうか
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165110/84b2115a-4a20-0139-3c03-162843947ca5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165110/b7bb209f-0ccb-ce0e-c2d8-e67154fb793d.png)

- その他

`#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION`
SSAOを使うかどうか (RenderFeatureにてSSAOを使わなければ要りません)
`#pragma multi_compile _ SHADOWS_SHADOWMASK`
`#pragma multi_compile _ _MIXED_LIGHTING_SUBTRACTIVE`
LIGHTMAP_SHADOW_MIXINGが設定されている前提で、BakedGIが設定され、かつSubtractive/Shadowmaskが設定されたシーンがある場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165110/5125f042-c3fc-2986-28a5-4cca7d73f744.png)

~~この辺の設定はランタイム中に動的に切り替えたい場合は滅多にないと思われ、ここをmulti_compileとして設定する必要はないでしょう。~~
UniversalRenderPipelineAssetの設定に応じて#defineに書き換えました。

(2021/01/06追記)
- URPCore内部コードにより設定以外の条件で使われるところがちょいちょいあって、それぞれの事情により消せるか消せないかが決まります。
- `_MAIN_LIGHT_SHADOWS`: 現在写せる影が一つもない場合[(リンク)](https://github.com/Unity-Technologies/Graphics/blob/master/com.unity.render-pipelines.universal/Runtime/Passes/MainLightShadowCasterPass.cs#L188)にてオフになるため、
これを常時defineしてしまうと、影がない場合に変な影が写ってしまう問題がありました。
- `_ADDITIONAL_LIGHTS_VERTEX/_ADDITIONAL_LIGHTS`: Main以外のライトが一個もない場合はURP側でオフにするので、常時defineだと余計な計算が走る可能性があります。メモリとのTradeOff?
- `_ADDITIONAL_LIGHT_SHADOWS`: 同じくURP内部で切り替わります。
基本上記以外は場面によって操作して大丈夫かと思います。

```
#if UNITY_HARDWARE_TIER1
    #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#elif UNITY_HARDWARE_TIER2
    #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
    #define _ADDITIONAL_LIGHTS_VERTEX
    #define _ADDITIONAL_LIGHT_SHADOWS
    #define _SHADOWS_SOFT
#elif UNITY_HARDWARE_TIER3
    #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
    #define _MAIN_LIGHT_SHADOWS_CASCADE
    #define _ADDITIONAL_LIGHTS
    #define _ADDITIONAL_LIGHT_SHADOWS
    #define _SHADOWS_SOFT
#endif
```

#### 結果
ランタイム中のShaderLabメモリは400mb -> 4mbになり、ビルド時間の7割を減らした

## 終わりに
UniversalRenderPipelineAssetの設定が変わると再度全シェーダーに対して調整する必要が確かにありますが、それにしても得られるメリットは非常に大きいので、しばらくはこういう感じで設定していこうと思います。
最近はパッケージに変更が激しいので、安定するまでのしばらくは[リポジトリ](https://github.com/Unity-Technologies/Graphics)を注意して確認する必要があるかなーと

(2020/12/31追記) https://forum.unity.com/threads/changes-to-the-graphics-github-repository.1029502/
Graphicsリポジトリにはもうアップデート内容が反映されないとのことなので、これで変更を追うことがさらに難しくなりそうですね・・