Unity 2021 LTS におけるシェーダーのビルド時間とメモリ使用量の改善

Unity のスクリプタブルレンダーパイプライン(SRP)で使える機能セットが増え続けるにつれ、ビルド時に処理されコンパイルされるシェーダーのバリアントの量も増えています。さらに、グラフィックス API への対応や、ターゲットプラットフォームの拡大など、SRP の進化はとどまるところを知りません。
シェーダーは、最初のビルド(「クリーン」ビルド)の後にコンパイルされキャッシュされるため、その後のインクリメンタルビルド(「ウォーム」ビルド)が加速されます。通常はクリーンビルドに最も時間がかかりますが、ウォームビルドにかかる時間がプロジェクトの開発やイテレーションにおける問題点となることがよくあります。

この問題に対処するため、Unity の Shader Management チームは、有意義でスケーラブルなソリューションを提供するため、懸命な取り組みを続けています。この取り組みの結果、Unity 2021 LTS 以降のバージョンを使用して作成されたプロジェクトのシェーダービルド時間および実行時のメモリ使用量が大幅に削減されました。
これらの新しい最適化について、影響を受けるバージョン、バックポート、内部テストによる数値などを詳しく読むには、シェーダーバリアントプレフィルタリングと ダイナミックシェーダーローディングのセクションに直接飛んでください。このブログ記事の最後では、プロジェクトのオーサリング、ビルド、実行時の全体にわたって、シェーダーバリアント管理をさらに洗練させるという私たちの将来の計画についても述べています。
Unityのシェーダーシステムに加えられたエキサイティングな改良について掘り下げる前に、この機会に条件付きシェーダーコンパイル、シェーダーバリアント、シェーダーバリアントストリッピングの概念についても簡単に復習しておきましょう。
条件付きシェーダー機能により、開発者やアーティストは、スクリプト、マテリアル設定、プロジェクトおよびグラフィックス設定を使用して、シェーダーの機能を便利に制御および変更することができます。このような条件付き機能は、プロジェクトのオーサリングを簡素化し、オーサリングやメンテナンスを行う必要があるシェーダーの数を最小限に抑えることで、プロジェクトの規模を効率的にスケールさせることができるようになります。

条件付きシェーダー機能は、さまざまな方法で実装できる:
- 静的(コンパイル時)分岐
- シェーダーバリアントのコンパイル
- 動的(実行時)分岐
静的分岐は、実行時に分岐に関連するシェーダー実行のオーバーヘッドを回避する一方で、コンパイル時に評価されロックされるため、実行時の制御を提供しない。一方、シェーダー・バリアント・コンパイルは、静的分岐の一形態であり、ランタイム制御を追加するものである。これは可能な限りの静的分岐の組み合わせに対して、固有のシェーダープログラム(バリアント)をコンパイルすることで機能する方法で、実行時に最適な GPU 性能を維持します。
このようなバリアントは、shader_featureおよびmulti_compileシェーダーキーワードによってシェーダー機能を条件付きで宣言および評価することによって作成される。アクティブキーワードと実行時の設定に基づき、正しいシェーダーバリアントが実行時に読み込まれます。追加のシェーダーキーワードを宣言し評価することで、ビルド時間、ファイルサイズ、実行時のメモリ使用量が増加する可能性があります。
同時に、ダイナミック(ユニフォームベース)分岐は、シェーダーバリアントコンパイルのオーバーヘッドを完全に回避し、ビルドを高速化し、ファイルサイズとメモリ使用量の両方を削減します。これにより、開発中によりスムーズで迅速な反復が可能になる。
一方、動的分岐は、シェーダーの複雑さとターゲットデバイスによっては、シェーダーの実行性能に強い影響を与える可能性があります。分岐の一方がもう一方よりはるかに複雑になるような非対称な分岐はパフォーマンスに悪影響を及ぼす可能性があります。これは、より単純なパスを実行しても、より複雑なパスの性能上のペナルティが発生する可能性があるからです。
独自のシェーダーに条件付きシェーダー機能を導入する場合、これらのアプローチとトレードオフに留意する必要があります。より詳細な情報については、シェーダー条件分岐、シェーダー分岐、シェーダーバリアントのドキュメントを参照してください。
シェーダーの処理とコンパイルの時間の増加を緩やかにするために、シェーダーバリアントストリップを利用します。以下の要素をもとに、不要なシェーダーバリアントをコンパイルから除外することを目的としています。
- 含まれるマテリアルと有効になっているキーワード
- プロジェクトとレンダーパイプラインの設定
- スクリプタブルストリッピング
シェーダーバリアントを列挙するとき、Editor は、参照されビルドに含まれるマテリアルで有効になっていないshader_featureで宣言されたキーワードを自動的にフィルタリングします。その結果、これらのキーワードは追加のバリアントを生成しません。
例えば、Clear Coat マテリアル・プロパティがComplex Lit URPシェーダを使用するマテリアルで有効になっていない場合、Clear Coat機能を実装するすべてのシェーダ・バリアントはビルド時に安全に取り除かれます。
一方、multi_compileキーワードは、開発者とプレイヤーに、利用可能なPlayer設定とスクリプトに基づいて、実行時にシェーダーの機能を自由に制御することを促します。裏を返せば、このようなキーワードはシェーダー_featureキーワードと同じ程度にエディターによって自動的に取り除かれることはないということです。そのため、一般的にはより多くのバリアントを作成しています。
スクリプト可能なストリッピングは、実行時には必要ないキーワードや組み合わせによって、ビルド時にシェーダーのバリアントをコンパイルから除外することができるC# APIです。レンダーパイプラインは、プロジェクトのレンダーパイプライン設定とビルドに含まれるクオリティアセットに従って、不要なバリアントを取り除くためにスクリプト可能なストリッピングを利用します。
低品質 高品質 バリアント倍率 メインライト/キャストシャドウ:Off On 2x メインライト/キャストシャドウ:オン オン 1x メインライト/キャストシャドウ:オフ オフ 1倍
エディターのシェーダーバリアントストリッピングの効果を最大にするために、グラフィックス関連の機能およびレンダーパイプラインの設定のうち、実行時に使用しないものをすべて無効にすることを推奨します。シェーダーバリアントストリップの詳細については、公式ドキュメントを参照してください。
シェーダーバリアントストリッピングは、ビルド内のレンダーパイプライン品質アセットなどの要因に基づいて、コンパイルされたシェーダーバリアントの量を大幅に削減します。しかし、現在はシェーダー処理ステージの最後にストリッピングが行われています。コンパイルするしないに関わらず、可能性のあるすべてのバリアントを列挙するだけでも、長い時間がかかることが依然としてあります。
シェーダーバリアント処理(およびプロジェクトビルド)の時間を短縮するため、エンジンに内蔵されたシェーダーバリアントストリッピングに大幅な最適化を導入しました。シェーダーバリアントの事前フィルタリングにより、クリーンビルドとウォームビルドの両方の時間を大幅に短縮しました。
この最適化は、レンダー・パイプラインの設定によって駆動されるプレフィルタリング属性に従って、multi_compileキーワードの早期除外を導入することによって機能する。これにより、潜在的なストリッピングとコンパイルのために列挙されるバリアントの量が減少し、その結果、シェーダの処理時間が短縮されます。
シェーダーバリアントのプリフィルタリングは2023.1.0a14 で初めて導入され、2022.2.0b15と2021.3.15f1 にバックポートされました。


バリアントの事前フィルタリングも同様の原理で、最初の(クリーン)ビルドの時間短縮に貢献します。


歴史的に、Unity のランタイムは、シーンとリソースの読み込み時に、すべてのシェーダーオブジェクトをディスクから CPU メモリにあらかじめ読み込んでいました。ほとんどの場合、ビルドされたプロジェクトとシーンには、アプリケーションの実行時の任意の瞬間に必要な数よりも多くのシェーダーバリアントが含まれています。大量のシェーダーを使用するプロジェクトでは、実行時のシェーダーメモリ使用量が多くなることがよくあります。
動的なシェーダー読み込みは、シェーダー読み込みの動作とメモリ使用量をユーザーが細かく制御できるようにすることで、この問題に対処しています。この最適化は、ユーザーが制御するメモリ予算に基づいて、シェーダーのデータチャンクのメモリへのストリーミングや、実行時に不要になったシェーダーデータの排除を可能にします。これにより、メモリ予算に制限のあるプラットフォームでシェーダーメモリの使用量を大幅に削減することができます。
新しいシェーダーバリアント読み込み設定が、エディターのプレーヤー設定からアクセスできるようになりました。これらを使用して、読み込まれるシェーダーチャンクの最大数とシェーダーチャンクごとのサイズ(MB)をオーバーライドします。

以下の C# API が利用可能になったことで、エディタースクリプトを使用して、シェーダーバリアントの読み込み設定を上書きすることができます。
- PlayerSettings.SetDefaultShaderChunkCountと PlayerSettings.SetDefaultShaderChunkSizeInMBを使用して、プロジェクトのデフォルトのシェーダー読み込み設定を上書きします。
- PlayerSettings.SetShaderChunkCountForPlatformおよび PlayerSettings.SetShaderChunkSizeInMBForPlatformを使用して、プラットフォームごとにこれらの設定をオーバーライドできます。
また、実行時にロードされるシェーダーチャンクの最大量をC# APIを使用してオーバーライドすることもできます。 Shader.maximumChunksOverride.これにより、実行時に照会される利用可能なシステムおよびグラフィックスメモリの 合計などの要因に基づいて、シェーダメモリバジェットを上書きすることができます。
動的シェーダー読み込みは2023.1.0a11に 導入され、2022.2.0b10、2022.1.21f1、2021.3.12fにバックポートされました。Universal Render Pipeline(URP)のBoat Attackの場合、シェーダーのランタイムメモリ使用量は、315MiB(デフォルト)から66.8MiB(ダイナミックローディング)へと78.8%削減されました。この最適化についての詳細は公式発表で読むことができる。

ここまでご紹介した重要な変更点以外にも、ユニバーサルレンダーパイプラインのシェーダーバリアント生成とストリッピングの強化に取り組んでいます。また、Unity のシェーダーバリアント管理についても、全般的にさらなる改善を検討しています。最終的な目標は、エンジンの機能拡張を容易にすると同時に、シェーダーのビルドと実行時のオーバーヘッドを最小限に抑えることです。
現在進行中の調査の中には、シェーダーキーワードとシェーダーバリアントコレクションAPIの全体的な改善と同様に、類似したバリアント間でのシェーダーリソースの重複排除が含まれています。シェーダーバリアントの処理と実行時の性能をより柔軟に制御できるようにすることが目的です。
今後の課題として、シェーダーバリアントのトレースと解析のためのエディター内ツールの可能性も探っています。シェーダーバリアントの使用状況について以下のような詳細を提供できるツールの提供を目指しています。
- どのシェーダーとキーワードが最も多くのバリアントを生み出すのか
- コンパイルされているが、実行時には使用されていないバリアントはどれか
- ストリッピングの対象となったが、実行時にリクエストされたバリアントはどれか
これまで私たちは、皆さんからのご意見を、最も有意義なソリューションを優先的に提供する手助けとしてきました。私たちの公開ロードマップをチェックして、あなたのニーズに最も適した機能に投票してください。もし追加で見たい変更があれば、遠慮なく機能リクエストを提出するか、このシェーダー・フォーラムでチームに直接連絡してほしい。
