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
に出来ないか検討する。
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でのシェーダープロファイル
Shader Variant Log Levelを変えることで、ビルド時に下記のようにどれぐらいシェーダーが含まれ、どれが外れてどれが含まれたかをログに出してくれます。
ShaderPreprocessor.cs#L328
なのでビルド一番最後のログを見ると、全体にどれぐらい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という仕組みが用意されており、便利にビルドに含まれるシェーダーを制御することが出来るようになっています。
ビルド・Addressablesに参照がある対象のvariantについて、ここを通してユーザーが操作可能とする仕組みです。
ライト設定やフォグなどの環境については、同じく対象となっているシーンの環境の組み合わせと照らし合わせてくれます。
使用例はURPパッケージ内に結構良い感じのスクリプトがありますので、この辺を参考にすれば良いでしょう。
ShaderPreprocessor.cs
今回のプロジェクトでは基本URP用スクリプトはなく、全部自作のスクリプトを使っていたためこの辺は排除することにしました。
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
のところで理屈上ありえないパタンなどはある程度防ぐようになってますが、まだまだ不十分で
- 無効の場合の最適化はあるが、有効のみの最適化が網羅出来ていない (後述)
- そもそもの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
ライトマップで影を混合するかどうか
- その他
#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が設定されたシーンがある場合
この辺の設定はランタイム中に動的に切り替えたい場合は滅多にないと思われ、ここをmulti_compileとして設定する必要はないでしょう。
UniversalRenderPipelineAssetの設定に応じて#defineに書き換えました。
(2021/01/06追記)
- URPCore内部コードにより設定以外の条件で使われるところがちょいちょいあって、それぞれの事情により消せるか消せないかが決まります。
-
_MAIN_LIGHT_SHADOWS
: 現在写せる影が一つもない場合(リンク)にてオフになるため、
これを常時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の設定が変わると再度全シェーダーに対して調整する必要が確かにありますが、それにしても得られるメリットは非常に大きいので、しばらくはこういう感じで設定していこうと思います。
最近はパッケージに変更が激しいので、安定するまでのしばらくはリポジトリを注意して確認する必要があるかなーと
(2020/12/31追記) https://forum.unity.com/threads/changes-to-the-graphics-github-repository.1029502/
Graphicsリポジトリにはもうアップデート内容が反映されないとのことなので、これで変更を追うことがさらに難しくなりそうですね・・