Unity のスクリプタブルレンダーパイプライン(SRP)で使える機能セットが増え続けるにつれ、ビルド時に処理されコンパイルされるシェーダーのバリアントの量も増えています。さらに、グラフィックス API への対応や、ターゲットプラットフォームの拡大など、SRP の進化はとどまるところを知りません。
シェーダーは、最初のビルド(「クリーン」ビルド)の後にコンパイルされキャッシュされるため、その後のインクリメンタルビルド(「ウォーム」ビルド)が加速されます。通常はクリーンビルドに最も時間がかかりますが、ウォームビルドにかかる時間がプロジェクトの開発やイテレーションにおける問題点となることがよくあります。
この問題に対処するため、Unity の Shader Management チームは、有意義でスケーラブルなソリューションを提供するため、懸命な取り組みを続けています。この取り組みの結果、Unity 2021 LTS 以降のバージョンを使用して作成されたプロジェクトのシェーダービルド時間および実行時のメモリ使用量が大幅に削減されました。
これらの新しい最適化について、影響を受けるバージョン、バックポート、内部テストで得られた数値などの詳細に関心がある方は、この後をスキップして、シェーダーバリアント の事前フィルタリングおよび動的なシェーダー読み込みのセクションをお読みください。このブログ記事の最後では、プロジェクトのオーサリング、ビルド、実行時の全体にわたって、シェーダーバリアント管理をさらに洗練させるという私たちの将来の計画についても述べています。
Unity のシェーダーシステムに施されたエキサイティングな改良を掘り下げる前に、条件付きシェーダーコンパイル、シェーダーバリアント、シェーダーバリアントストリッピングの概念も簡単に復習しておきましょう。
条件付きシェーダー機能により、開発者やアーティストはスクリプト、マテリアル設定、プロジェクトおよびグラフィックス設定を使用して、シェーダーの機能を簡単に制御および変更することができます。このような条件付き機能は、プロジェクトのオーサリングを簡素化し、オーサリングやメンテナンスを行う必要があるシェーダーの数を最小限に抑えることで、プロジェクトの規模を効率的にスケールさせることができるようになります。
条件付きシェーダー機能は、さまざまな方法で実装することができます。
静的分岐によって、実行時に分岐に関連するシェーダー実行のオーバーヘッドを回避できますが、コンパイル時に評価・固定され、実行時の制御は行いません。一方、シェーダーバリアントのコンパイルは静的分岐の一種で、実行時の制御が追加されています。これは可能な限りの静的分岐の組み合わせに対して、固有のシェーダープログラム(バリアント)をコンパイルすることで機能する方法で、実行時に最適な GPU 性能を維持します。
このようなバリアントは、shader_feature および multi_compile のシェーダーキーワードによって、シェーダー機能を条件付きで宣言し、評価することによって作成されます。アクティブキーワードと実行時の設定に基づき、正しいシェーダーバリアントが実行時に読み込まれます。追加のシェーダーキーワードを宣言し評価することで、ビルド時間、ファイルサイズ、実行時のメモリ使用量が増加する可能性があります。
同時に、動的(ユニフォーム変数ベース)分岐は、シェーダーバリアントのコンパイルのオーバーヘッドを完全に回避し、ビルドの高速化、ファイルサイズとメモリ使用量の削減の両方を実現します。これにより、開発時のイテレーションをよりスムーズに、より速く行うことができます。
一方、動的分岐は、シェーダーの複雑さとターゲットデバイスによっては、シェーダーの実行性能に強い影響を与える可能性があります。分岐の一方がもう一方よりはるかに複雑になるような非対称な分岐はパフォーマンスに悪影響を及ぼす可能性があります。これは、より単純なパスを実行しても、より複雑なパスの性能上のペナルティが発生する可能性があるからです。
独自のシェーダーに条件付きシェーダー機能を導入する場合、これらのアプローチとトレードオフに留意する必要があります。より詳細な情報については、シェーダー条件分岐、シェーダー分岐、シェーダーバリアントのドキュメントをご覧ください。
シェーダーの処理とコンパイルの時間の増加を緩やかにするために、シェーダーバリアントストリップを利用します。以下の要素をもとに、不要なシェーダーバリアントをコンパイルから除外することを目的としています。
シェーダーバリアントを列挙するとき、エディターは shader_feature で宣言されたキーワードのうち、参照されているマテリアルやビルドに含まれるマテリアルで有効になっていないものを自動的に除外します。その結果、これらのキーワードは追加のバリアントを生成することはありません。
たとえば、クリアコートのマテリアルプロパティが Complex Lit URP シェーダーを使用するどのマテリアルでも有効でない場合、クリアコート機能を実装するすべてのシェーダーバリアントはビルド時に安全にストリップされます。
一方、multi_compile キーワードは、開発者とプレイヤーが利用可能なプレイヤー設定とスクリプトに基づいて、実行時にシェーダーの機能を自由に制御できるように指示します。裏を返せば、このようなキーワードは shader_feature キーワードと同じ程度にエディターによって自動的にストリッピングされることはない、ということです。そのため、一般的にはより多くのバリアントを作成しています。
スクリプタブルストリッピングは C# API で、実行時には必要ないキーワードと組み合わせにより、ビルド時にシェーダーのバリアントをコンパイルから除外することができます。レンダーパイプラインは、ビルドに含まれるプロジェクトのレンダーパイプライン設定と Quality Assets に従って、不要なバリアントのストリッピングを行うために、スクリプタブルストリッピングを利用します。
低品質 | 高品質 | バリアントの係数 | |
---|---|---|---|
メインライト/キャストシャドウ: | オフ | オン | 2 倍 |
メインライト/キャストシャドウ: | オン | オン | 1 倍 |
メインライト/キャストシャドウ | オフ | オフ | 1 倍 |
エディターのシェーダーバリアントストリッピングの効果を最大にするために、グラフィックス関連の機能およびレンダーパイプラインの設定のうち、実行時に使用しないものをすべて無効にすることを推奨します。シェーダーバリアントストリッピングの詳細については、公式ドキュメントを参照してください。
シェーダーバリアントストリッピングは、ビルドの Render Pipeline Quality Assets のような要素に基づいて、コンパイルされたシェーダーバリアントの量を大幅に削減します。しかし、現在はシェーダー処理ステージの最後にストリッピングが行われています。コンパイルするしないに関わらず、可能性のあるすべてのバリアントを列挙するだけでも、長い時間がかかることが依然としてあります。
シェーダーバリアント処理(およびプロジェクトビルド)の時間を短縮するため、エンジンに内蔵されたシェーダーバリアントストリッピングに大幅な最適化を導入しました。シェーダーバリアントの事前フィルタリングにより、クリーンビルドとウォームビルドの両方の時間を大幅に短縮しました。
レンダーパイプライン設定によって駆動される事前フィルタリング属性に従い、multi_compile キーワードの早期除外を導入することによって、この最適化が機能します。これにより、潜在的なストリッピングとコンパイルのために列挙されるバリアントの量が減り、その結果、シェーダーの処理時間が短縮されます。最も劇的な例では、ウォームビルド時間が 最大で 90% 短縮されています。
シェーダーバリアントの事前フィルタリングは Unity 2023.1.0a14 で初めて搭載され、バージョン 2022.2.0b15 と 2021.3.15f1 にバックポートされています。
バリアントの事前フィルタリングも同様の原理で、最初の(クリーン)ビルドの時間短縮に貢献します。
歴史的に、Unity のランタイムは、シーンとリソースの読み込み時に、すべてのシェーダーオブジェクトをディスクから CPU メモリにあらかじめ読み込んでいました。ほとんどの場合、ビルドされたプロジェクトとシーンには、アプリケーションの実行時の任意の瞬間に必要な数よりも多くのシェーダーバリアントが含まれています。大量のシェーダーを使用するプロジェクトでは、実行時にシェーダーメモリの使用量が多くなることがよくあります。
動的なシェーダー読み込みは、シェーダー読み込みの動作とメモリ使用量をユーザーが細かく制御できるようにすることで、この問題に対処しています。この最適化は、ユーザーが制御するメモリ予算に基づいて、シェーダーのデータチャンクのメモリへのストリーミングや、実行時に不要になったシェーダーデータの排除を可能にします。これにより、メモリ予算に制限のあるプラットフォームでシェーダーメモリの使用量を大幅に削減することができます。
新しいシェーダーバリアント読み込み設定は、エディターのプレイヤー設定からアクセスできるようになり ました。これらを使用して、読み込まれるシェーダーチャンクの最大数とシェーダーチャンクごとのサイズ(MB)をオーバーライドします。
以下の C# API が利用可能になったことで、エディタースクリプトを使用して、シェーダーバリアントの読み込み設定を上書きすることができます。
Shader.maximumChunksOverride を介して C# API を使用し、実行時に読み込まれるシェーダーチャンクの最大量をオーバーライドすることも可能です。これにより、実行時に照会される利用可能なシステムの合計およびグラフィックスメモリなどの要因に基づいて、シェーダーのメモリ予算をオーバーライドすることができます。
動的なシェーダー読み込みは Unity 2023.1.0a11 で初めて搭載され、バージョン 2022.2.0b10、2022.1.21f1、 および 2021.3.12f にバックポートされています。ユニバーサルレンダーパイプライン(URP)の Boat Attack の場合、シェーダーの実行時のメモリ使用量が 315 MiB(デフォルト)から 66.8 MiB(動的読み込み)へと 78.8% 削減されていることが確認され ました。この最適化については、公式のアナウンスで詳しく説明されています。
ここまでご紹介した重要な変更点以外にも、ユニバーサルレンダーパイプラインのシェーダーバリアント生成とストリッピングの強化に取り組んでいます。また、Unity のシェーダーバリアント管理についても、全般的にさらなる改善を検討しています。最終的な目標は、エンジンの機能拡張を容易にすると同時に、シェーダーのビルドと実行時のオーバーヘッドを最小限に抑えることです。
現在研究を進めている事柄として、類似のバリアント間のシェーダーリソースの重複排除や、シェーダーキーワードの全体的な改善、Shader Variant Collection API などがあります。シェーダーバリアントの処理と実行時の性能をより柔軟に制御できるようにすることが目的です。
今後の課題として、シェーダーバリアントのトレースと解析のためのエディター内ツールの可能性も探っています。シェーダーバリアントの使用状況について以下のような詳細を提供できるツールの提供を目指しています。
これまで私たちは、皆さんからのご意見を、最も有意義なソリューションを優先的に提供する手助けとしてきました。公開ロードマップをご確認の上、皆さんのニーズに最も合った機能にご投票ください。もし追加で変更してほしいところがあれば、機能リクエストを出すか、こちらのシェーダーフォーラムで直接チームに連絡してください。