Unity を検索

スクリプタブルシェーダーバリアントの除去

2018年5月14日 カテゴリ: テクノロジー | 12 分 で読めます
取り上げているトピック
シェア

どのシェーダーバリアントを Unity シェーダーのコンパイラーで処理してプレイヤーデータに含めるかを開発者が制御できるようになったことにより、プレイヤーのビルド時間とデータサイズが大幅に削減されます。

プレイヤーのビルド時間とデータサイズはプロジェクトの複雑性が増すにつれて増加します。これはシェーダーバリアントの数が増えるためです。

2018.2 ベータ版で初公開となったスクリプタブルシェーダーバリアントの除去機能によって、シェーダーバリアントの生成数の管理が可能となり、結果としてプレイヤーのビルド時間とデータサイズの大幅な削減が可能になりました。

この機能を使用すると、無効なコードパスを含むすべてのシェーダーバリアントを除去したり、使用されていない機能のシェーダーバリアントを除去することができます。また「debug」 や「release」などのシェーダービルド設定を、イテレーション時間を長くしたり、メンテナンスを複雑にしたりすることなく作成できます。

本記事は、まず始めにいくつかの用語について解説した上で、シェーダーバリアントの定義に焦点を当て、非常に多数の生成が可能な理由をご説明します。次に、シェーダーバリアントの自動除去機能についてと、スクリプタブルシェーダーバリアントの除去機能がどのように Unity シェーダーパイプラインのアーキテクチャーに実装されているかをご説明します。続いてスクリプタブルシェーダーバリアント除去 API についてのご紹介と『Fountainbleau』デモの結果の解説をお届けし、最後に除去スクリプトを記述するに当たってのヒントをご紹介します。

スクリプタブルシェーダーバリアントの除去について学ぶことは容易なタスクとは言えませんが、チームの生産性の劇的な向上に繋がるかもしれません!

各種の概念

スクリプタブルシェーダーバリアントの除去機能を理解するには、それに関連する様々な概念を正確に理解する必要があります。

  • シェーダーアセット:プロパティ、サブシェーダー、パス、HLSL を含む、完全なソースコードです。
  • シェーダースニペット:単一のシェーダーステージの依存を含む、HLSL 入力コードです。
  • シェーダーステージ:GPU レンダリングパイプライン内の特定のステージです。通常は頂点シェーダーステージとフラグメントシェーダーステージです。
  • シェーダーキーワード:複数のシェーダーにわたるコンパイル時ブランチのプリプロセッサー識別子です。
  • シェーダーキーワードセット:特定のコードパスを識別する、特定のシェーダーキーワードの一式です。
  • シェーダーバリアント:Unity シェーダーコンパイラーによって生成されるプラットフォームごとのシェーダーコードです。特定のグラフィックスのティア/パス/シェーダーキーワードセットなどに関して、特定のシェーダーステージ用に生成されます。
  • ウーバーシェーダー:多数のシェーダーバリアントを生成できるシェーダーソースコードです。

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」セクションに基づいて行うものです。

Graphics 設定のシェーダーバリアント自動除去のオプション

シェーダーバリアントの自動除去は、ビルド時の制約に基づきます。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# コールバック経由で利用可能で、各コールバックはシェーダースニペットごとに実行されます。

スクリプタブルシェーダーバリアント除去 API

例えば、以下のスクリプトでは(開発プレイヤービルド内で使用される「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, IList shaderCompilerData)
    {
        // 開発ではデバッグバリアントを除去しない
        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 秒
HD レンダーパイプラインを使用した『Fontainebleau』フォトグラメトリデモの、標準の PlayStation 4 からのスクリーンショット(解像度 1920x1080)

さらに、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_ORANGE + OP_ADD、COLOR_VIOLET + OP_MUL、COLOR_GREEN + OP_MUL

まず、すべてのキーワードが実際に役立つものであることを確認する必要があります。このシーンでは 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 メンバー関数によって戻された値を命令することで、スクリプトの実行を命令できます。シェーダービルドパイプラインは callbackOrder が増加する順にこのコールバックを実行しますので、最も低いものが最初に、最も高いものが最後になります。

複数のシェーダー除去スクリプトを使用するユースケースのひとつは、スクリプトを目的ごとに分離するものです。例えば以下のようなケースです。

  • スクリプト 1:無効なコードパスを持つすべてのシェーダーバリアントを系統的に除去します。
  • スクリプト 2:すべてのデバッグシェーダーバリアントを除去します。
  • スクリプト 3:コードベース内の、現在のプロジェクトに不要なシェーダーバリアントをすべて除去します。
  • スクリプト 4:除去スクリプトのイテレーション時間を短縮するために、残っているシェーダーバリアントをログしてそれらすべてを除去します。

シェーダーバリアント除去スクリプトの記述手順

シェーダーバリアントの除去は極めて効果的ですが、良い結果を得るためには多くの作業を要します。

  1. プロジェクトビューで、すべてのシェーダーをフィルタリングします。
  2. シェーダーをひとつ選択し、インスペクター内で「Show」をクリックしてそのシェーダーのキーワードとバリアントのリストを開きます。ビルドに常に含まれているキーワードが一覧表示されます。
  3. プロジェクトが使用しているグラフィックス機能を確実に把握してください。
  4. キーワードが使用されているかどうかをすべてのシェーダーステージで確認してください。これらのキーワードを使用しないステージでは必要なバリアントは 1 つだけです。
  5. スクリプト内でシェーダーバリアントを除去します。
  6. ビルドのビジュアルを確認してください。
  7. 各シェーダーに関して、ステップ 2 ~ 6 を繰り返します。

サンプルプロジェクトのダウンロード

本記事で使用されたサンプルプロジェクトはこちらでダウンロード可能です。お使いになるには Unity 2018.2.0b1 以降のバージョンをお使いください。

2018 年のUnite Berlinで、バイナリのデプロイサイズの最適化について詳しくご紹介しています。

6 月 21 日の Jonas Echterhoff の講演で、最終的な完成版ビルドに何を含めるか制御できる新しいツールの全貌が紹介されました。講演の録画はこちらからご覧になれます。

2018年5月14日 カテゴリ: テクノロジー | 12 分 で読めます
取り上げているトピック