どのシェーダーバリアントを Unity シェーダーのコンパイラーで処理してプレイヤーデータに含めるかを開発者が制御できるようになったことにより、プレイヤーのビルド時間とデータサイズが大幅に削減されます。
プレイヤーのビルド時間とデータサイズはプロジェクトの複雑性が増すにつれて増加します。これはシェーダーバリアントの数が増えるためです。
2018.2 ベータ版で初公開となったスクリプタブルシェーダーバリアントの除去機能によって、シェーダーバリアントの生成数の管理が可能となり、結果としてプレイヤーのビルド時間とデータサイズの大幅な削減が可能になりました。
この機能を使用すると、無効なコードパスを含むすべてのシェーダーバリアントを除去したり、使用されていない機能のシェーダーバリアントを除去することができます。また「debug」 や「release」などのシェーダービルド設定を、イテレーション時間を長くしたり、メンテナンスを複雑にしたりすることなく作成できます。
本記事は、まず始めにいくつかの用語について解説した上で、シェーダーバリアントの定義に焦点を当て、非常に多数の生成が可能な理由をご説明します。次に、シェーダーバリアントの自動除去機能についてと、スクリプタブルシェーダーバリアントの除去機能がどのように Unity シェーダーパイプラインのアーキテクチャーに実装されているかをご説明します。続いてスクリプタブルシェーダーバリアント除去 API についてのご紹介と『Fountainbleau』デモの結果の解説をお届けし、最後に除去スクリプトを記述するに当たってのヒントをご紹介します。
スクリプタブルシェーダーバリアントの除去について学ぶことは容易なタスクとは言えませんが、チームの生産性の劇的な向上に繋がるかもしれません!
スクリプタブルシェーダーバリアントの除去機能を理解するには、それに関連する様々な概念を正確に理解する必要があります。
Unity では、ウーバーシェーダーは ShaderLab サブシェーダー、パス、シェーダータイプ、そして #pragma multi_compile および #pragma shader_feature プリプロセッサーディレクティブによって管理されます。
スクリプタブルシェーダーバリアントの除去機能を使用するには、シェーダーバリアントとは何か、またシェーダーバリアントがシェーダービルドパイプラインによってどのように生成されるかを明確に理解する必要があります。生成されるシェーダーバリアントの数は、ビルド時間とプレイヤーシェーダーバリアントのデータサイズに正比例します。シェーダーバリアントは、シェーダービルドパイプラインの出力のひとつです。
シェーダーキーワードは、シェーダーバリアントを生成させる要素のひとつです。シェーダーキーワードの不用意な使用は、シェーダーバリアントの数の爆発的な増加に繋がりやすく、この場合ビルド時間が極度に長くなってしまいます。
以下の簡単なシェーダーは、シェーダーバリアントの生成数をカウントするものです。シェーダーバリアントがどのように生成されるかをご確認いただけます。
Shader "ShaderVariantsStripping" { SubShader { Pass { Name "ShaderVariantsStripping/Pass" CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY #pragma multi_compile OP_ADD OP_MUL OP_SUB struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 get_color() { #if defined(COLOR_ORANGE) return fixed4(1.0, 0.5, 0.0, 1.0); #elif defined(COLOR_VIOLET) return fixed4(0.8, 0.2, 0.8, 1.0); #elif defined(COLOR_GREEN) return fixed4(0.5, 0.9, 0.3, 1.0); #elif defined(COLOR_GRAY) return fixed4(0.5, 0.9, 0.3, 1.0); #else #error "Unknown 'color' keyword" #endif } fixed4 frag (v2f i) : SV_Target { fixed4 diffuse = tex2D(_MainTex, i.uv); fixed4 color = get_color(); #if defined(OP_ADD) return diffuse + color; #elif defined(OP_MUL) return diffuse * color; #elif defined(OP_SUB) return diffuse - color; #else #error "Unknown 'op' keyword" #endif } ENDCG } } }
プロジェクト内のシェーダーバリアントの合計数は決定性で、以下の公式によって決定されます。
以下の自明な ShaderVariantStripping の例が、この公式に明確さをもたらしています。これは、以下のように公式を単純化する単一のシェーダーです。
これに類似して、このシェーダーは単一のサブシェーダーと単一のパスを持っているので、公式が以下のようにさらに単純化されます。
この公式内のキーワードは、プラットフォームとシェーダーキーワードの両方を参照します。グラフィックスのティアとは、プラットフォームキーワードのセットの特定の組み合わせです。
ShaderVariantStripping/Pass は、2 つのマルチコンパイルディレクティブを持っています。1 つ目のディレクティブは 4 つのキーワード(COLOR_ORANGE、COLOR_VIOLET、COLOR_GREEN、COLOR_GRAY)を定義し、2 つ目のディレクティブは 3 つのキーワード(OP_ADD、OP_MUL、OP_SUB)を定義します。最後にこのパスは 2 つのシェーダーステージ(頂点シェーダーステージとフラグメントシェーダーステージ)を定義します。
このシェーダーバリアントの合計は、単一の対応グラフィックス API 用のものです。しかし、プロジェクト内の対応グラフィックス API のそれぞれに関して、専用のシェーダーバリアントのセットが必要です。例えば、OpenGL ES 3 と Vulkan の両方に対応する Android Player をビルドする場合、シェーダーバリアントのセットが 2 つ必要です。結果として、プレイヤービルド時間とシェーダーのデータサイズは、対応のグラフィックス API の数に正比例することになります。
Unity におけるシェーダーコンパイルパイプラインはブラックボックスで、multi_compile や shader_feature などのバリアントの前処理命令の収集前にシェーダースニペットを抽出するために、プロジェクト内の各シェーダーがパースされます。これのため、コンパイルパラメーターのリストがシェーダーバリアントごとに 1 つ生成されます。
これらのコンパイルパラメーターには、シェーダースニペット、グラフィックスのティア、シェーダータイプ、シェーダーキーワードセット、パスタイプおよびパスの名前が含まれます。設定されたコンパイルパラメーターのそれぞれは単一のシェーダーバリアントの生成に使用されます。
上記の理由により、Unity は自動的にシェーダーバリアントを除去するパスを 2 つのヒューリスティックに基づいて実行します。1 つ目は、除去をプロジェクト設定に基づいて行うものです。例えば、Virtual Reality Supported が無効になっている場合は、VR シェーダーバリアントが系統的に除去されます。2 つ目は、自動除去を Graphics 設定 の「Shader Stripping」セクションに基づいて行うものです。
シェーダーバリアントの自動除去は、ビルド時の制約に基づきます。Unity はビルド時に必要なシェーダーバリアントのみを自動で選択することはできません。なぜなら、これらのシェーダーバリアントはランタイムの C# の実行に依存するからです。例えば、ある C# スクリプトがポイントライトを追加するが、ビルド時にポイントライトがない場合は、シェーダービルドパイプラインは、ポイントライトシェーディングを行うシェーダーバリアントがプレイヤーによって必要とされることを把握する術がありません。
以下は、自動除去される、有効なキーワードを含むシェーダーバリアントのリストです。
ライトマップモード: LIGHTMAP_ON、DIRLIGHTMAP_COMBINED、DYNAMICLIGHTMAP_ON、LIGHTMAP_SHADOW_MIXING、SHADOWS_SHADOWMASK
フォグモード:FOG_LINEAR、FOG_EXP、FOG_EXP2
インスタンシングバリアント: INSTANCING_ON
さらに、Virtual Reality support が無効になっている場合、以下の有効な組み込みのキーワードを持つシェーダーバリアントが除去されます。
STEREO_INSTANCING_ON、STEREO_MULTIVIEW_ON、STEREO_CUBEMAP_RENDER_ON、UNITY_SINGLE_PASS_STEREO
自動除去が行われる時、シェーダービルドパイプラインは、残りのコンパイルパラメーターセットを使用してシェーダーバリアントの同時コンパイルのスケジュールを設定します。プラットフォームの持つ CPU コアスレッドの数と同数のコンパイルが同時に実行されます。
以下は、このプロセスを視覚的に示した図です。
Unity 2018.2 ベータ版では、シェーダーパイプラインアーキテクチャーの、シェーダーバリアントのコンパイルのスケジュール設定の直前に新しいステージが追加されており、シェーダーバリアントのコンパイルをユーザーが制御できるようになっています。この新しいステージはユーザーコードで C# コールバック経由で利用可能で、各コールバックはシェーダースニペットごとに実行されます。
例えば、以下のスクリプトでは(開発プレイヤービルド内で使用される「DEBUG」キーワードによって識別される)「DEBUG」設定に関連付けられるすべてのシェーダーバリアントの除去を行えます。
using System.Collections.Generic; using UnityEditor; using UnityEditor.Build; using UnityEditor.Rendering; using UnityEngine; using UnityEngine.Rendering; // デバッグビルド設定の除去の簡単な例 class ShaderDebugBuildProcessor : IPreprocessShaders { ShaderKeyword m_KeywordDebug; public ShaderDebugBuildProcessor() { m_KeywordDebug = new ShaderKeyword("DEBUG"); } // 複数のコールバックを実装可能です。 // 最初に実行されるのは、callbackOrder が最も小さい数を戻すものです。 public int callbackOrder { get { return 0; } } public void OnProcessShader( Shader shader, ShaderSnippetData snippet, IListshaderCompilerData) { // 開発ではデバッグバリアントを除去しない if (EditorUserBuildSettings.development) return; for (int i = 0; i
シェーダーバリアントのコンパイルのスケジュール設定の直前に OnProcessShader が呼び出されます。
Shader インスタンス、ShaderSnippetData インスタンス、ShaderCompilerData インスタンスの各組み合わせが、シェーダーコンパイラーによって生成される単一のシェーダーバリアントの識別子です。そのシェーダーバリアントの除去は、それを ShaderCompilerData リストから削除するだけで行えます。
シェーダーコンパイラーが生成すべきすべてのシェーダーバリアントがこのコールバック内に表示されます。シェーダーバリアントの除去を行う際はまず、プロジェクトの役に立たない、削除すべきバリアントはどれかを明確にする必要があります。
スクリプタブルシェーダーバリアントの除去のユースケースのひとつは、無効なシェーダーバリアントを、シェーダーキーワードの各種組み合わせに基づいて系統的に除去するというものです。
HD レンダーパイプラインに含まれるシェーダーバリアント除去スクリプトは、HD レンダーパイプラインを使用したプロジェクトのビルド時間とサイズを系統的に削減します。このスクリプトは以下のシェーダーに適用可能です。
HDRenderPipeline/Lit
HDRenderPipeline/LitTessellation
HDRenderPipeline/LayeredLit
HDRenderPipeline/LayeredLitTessellation
このスクリプトは以下の結果をもたらします。
除去なし | 除去あり | |
プレイヤーデータのシェーダーバリアント数 | 24350 (100%) | 12122 (49.8%) |
ディスク上のプレイヤーのデータサイズ | 511 MB | 151 MB |
プレイヤーのビルド時間 | 4864 秒 | 1356 秒 |
さらに、Unity 2018.2 のライトウェイトレンダーパイプラインには除去スクリプトを自動でフィードする UI が搭載されています。これは最高 98% のシェーダーバリアントを自動除去でき、特にモバイルプロジェクトにおいて非常に有益であると予測されます。
もうひとつのユースケースは、特定のプロジェクトにおいて使用されていないレンダーパイプラインのすべてのレンダリング機能を除去するスクリプトです。ライトウェイトレンダーパイプラインで弊社の内部的なテスト用デモを使用した所、プロジェクト全体に以下の結果がもたらされました。
除去なし | 除去あり | |
プレイヤーデータのシェーダーバリアント数 | 31080 | 7056 |
ディスク上のプレイヤーのデータサイズ | 121 | 116 |
プレイヤーのビルド時間 | 839 秒 | 286 秒 |
ご覧の通り、スクリプタブルシェーダーバリアントの除去機能を使用することで非常に大きな効果を得ることが可能です。また、除去スクリプトにさらに手を加えれば、これより大きな結果がもたらされることも期待できます。
シェーダーバリアント数の爆発的な上昇は簡単に発生し得るもので、これによってコンパイル時間とプレイヤーのデータサイズが維持不可能なものになる場合があります。スクリプタブルシェーダーバリアントの除去機能は、この問題に対処する一助となりますが、より関連性の高いシェーダーバリアントを生成するためには、シェーダーキーワードをどのように使用しているか再評価する必要があります。使用されていないキーワードをエディター内で確認したい場合は #pragma skip_variants が使用できます。
例えば、ShaderStripping/Color シェーダー内では、前処理ディレクティブが以下のコードで宣言されます。
#pragma multi_compile COLOR_ORANGE COLOR_VIOLET COLOR_GREEN COLOR_GRAY // 色のキーワード #pragma multi_compile OP_ADD OP_MUL OP_SUB // オペレーターキーワード
このアプローチでは、色のキーワードとオペレーターキーワードのすべての組み合わせが生成されます。
以下のシーンをレンダーしたいと仮定しましょう。
まず、すべてのキーワードが実際に役立つものであることを確認する必要があります。このシーンでは COLOR_GRAY と OP_SUB が一切使用されていません。もしこれらのキーワードが一切使用されていないことが保証できれば、これらは削除するべきです。
次に、単一のコードパスを効果的に生成するキーワードを組み合わせます。この例では、「add」オペレーションは常に「orange」色と共にしか使用されていません。したがって、これらを単一のキーワード内で組み合わせて以下のようにコードをリファクタリングできます。
#pragma multi_compile ADD_COLOR_ORANGE MUL_COLOR_VIOLET MUL_COLOR_GREEN #if defined(ADD_COLOR_ORANGE) #define COLOR_ORANGE #define OP_ADD #elif defined(MUL_COLOR_VIOLET) #define COLOR_VIOLET #define OP_MUL #elif defined(MUL_COLOR_GREEN) #define COLOR_GREEN #define OP_MUL #endif
もちろん、キーワードのリファクタリングが可能でないこともあります。その場合にも、スクリプタブルシェーダーバリアントの除去が役に立ちます!
各スニペットに関して、すべてのシェーダーバリアント除去スクリプトが実行されます。callbackOrder メンバー関数によって戻された値を命令することで、スクリプトの実行を命令できます。シェーダービルドパイプラインは callbackOrder が増加する順にこのコールバックを実行しますので、最も低いものが最初に、最も高いものが最後になります。
複数のシェーダー除去スクリプトを使用するユースケースのひとつは、スクリプトを目的ごとに分離するものです。例えば以下のようなケースです。
シェーダーバリアントの除去は極めて効果的ですが、良い結果を得るためには多くの作業を要します。
本記事で使用されたサンプルプロジェクトはこちらでダウンロード可能です。お使いになるには Unity 2018.2.0b1 以降のバージョンをお使いください。
6 月 21 日の Jonas Echterhoff の講演で、最終的な完成版ビルドに何を含めるか制御できる新しいツールの全貌が紹介されました。講演の録画はこちらからご覧になれます。