Search Unity

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

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

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

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

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

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

各種の概念

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

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

Unity では、ウーバーシェーダーは ShaderLab サブシェーダー、パス、シェーダータイプ、そして #pragma multi_compile および #pragma shader_feature プリプロセッサーディレクティブによって管理されます。

シェーダーバリアントの生成数のカウント

スクリプタブルシェーダーバリアントの除去機能を使用するには、シェーダーバリアントとは何か、またシェーダーバリアントがシェーダービルドパイプラインによってどのように生成されるかを明確に理解する必要があります。生成されるシェーダーバリアントの数は、ビルド時間とプレイヤーシェーダーバリアントのデータサイズに正比例します。シェーダーバリアントは、シェーダービルドパイプラインの出力のひとつです。

シェーダーキーワードは、シェーダーバリアントを生成させる要素のひとつです。シェーダーキーワードの不用意な使用は、シェーダーバリアントの数の爆発的な増加に繋がりやすく、この場合ビルド時間が極度に長くなってしまいます。

以下の簡単なシェーダーは、シェーダーバリアントの生成数をカウントするものです。シェーダーバリアントがどのように生成されるかをご確認いただけます。

プロジェクト内のシェーダーバリアントの合計数は決定性で、以下の公式によって決定されます。

以下の自明な 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」設定に関連付けられるすべてのシェーダーバリアントの除去を行えます。

シェーダーバリアントのコンパイルのスケジュール設定の直前に 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 からのスクリーンショット(解像度 1920×1080)

さらに、Unity 2018.2 のライトウェイトレンダーパイプラインには除去スクリプトを自動でフィードする UI が搭載されています。これは最高 98% のシェーダーバリアントを自動除去でき、特にモバイルプロジェクトにおいて非常に有益であると予測されます。

プロジェクトのためのシェーダーバリアント除去

もうひとつのユースケースは、特定のプロジェクトにおいて使用されていないレンダーパイプラインのすべてのレンダリング機能を除去するスクリプトです。ライトウェイトレンダーパイプラインで弊社の内部的なテスト用デモを使用した所、プロジェクト全体に以下の結果がもたらされました。

除去なし 除去あり
プレイヤーデータのシェーダーバリアント数 31080 7056
ディスク上のプレイヤーのデータサイズ 121 116
プレイヤーのビルド時間 839 秒 286 秒

ご覧の通り、スクリプタブルシェーダーバリアントの除去機能を使用することで非常に大きな効果を得ることが可能です。また、除去スクリプトにさらに手を加えれば、これより大きな結果がもたらされることも期待できます。

ライトウェイトレンダーパイプラインのデモのスクリーンショット

シェーダーバリアント除去コード記述のためのヒント

シェーダーコードの設計を改善する

シェーダーバリアント数の爆発的な上昇は簡単に発生し得るもので、これによってコンパイル時間とプレイヤーのデータサイズが維持不可能なものになる場合があります。スクリプタブルシェーダーバリアントの除去機能は、この問題に対処する一助となりますが、より関連性の高いシェーダーバリアントを生成するためには、シェーダーキーワードをどのように使用しているか再評価する必要があります。使用されていないキーワードをエディター内で確認したい場合は #pragma skip_variants が使用できます。

例えば、ShaderStripping/Color シェーダー内では、前処理ディレクティブが以下のコードで宣言されます。

このアプローチでは、色のキーワードとオペレーターキーワードのすべての組み合わせが生成されます。

以下のシーンをレンダーしたいと仮定しましょう。

COLOR_ORANGE + OP_ADD、COLOR_VIOLET + OP_MUL、COLOR_GREEN + OP_MUL

まず、すべてのキーワードが実際に役立つものであることを確認する必要があります。このシーンでは COLOR_GRAY と OP_SUB が一切使用されていません。もしこれらのキーワードが一切使用されていないことが保証できれば、これらは削除するべきです。

次に、単一のコードパスを効果的に生成するキーワードを組み合わせます。この例では、「add」オペレーションは常に「orange」色と共にしか使用されていません。したがって、これらを単一のキーワード内で組み合わせて以下のようにコードをリファクタリングできます。

もちろん、キーワードのリファクタリングが可能でないこともあります。その場合にも、スクリプタブルシェーダーバリアントの除去が役に立ちます!

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 の講演で、最終的な完成版ビルドに何を含めるか制御できる新しいツールの全貌が紹介されました。講演の録画はこちらからご覧になれます。

20 コメント

コメントの配信登録

コメント受付を終了しました。

  1. This looks like a very nice thing to have, but isn’t this what #pragma shader_feature is supposed to take care of for you? All the examples in the post uses multi_compile, but according to the manual the whole point of using shader_feature instead is that unused features are allegedly stripped at build time.

    It’d be great with an example in the context of #pragma shader_feature, or at least an explanation why we need to set up shader stripping manually instead of using shader_feature directives.

  2. François Dujardin

    5月 20, 2018 10:59 am

    Great post! Thanks!
    There’s a little typo about shader variant count example: 2x3x4=24, not 12.

  3. How about also having some more UI-enabled options too (like saying I am not going to use lights of type x/y/z if you can not know the answer) and display the shader counts before/after, so not everything requires editor scripts or at least you can do a little more [quickly] without scripting?

    1. See section “Shader variants stripping for a render pipeline”, this is being done for scriptable render pipelines.

  4. Vladislav Ivanchuk

    5月 14, 2018 9:42 pm

    Thanks, it really nice feature!!!

    What about iOS Metal shader compilation time? I mean metal shader compiler really slow, base shareds take 50-300 ms to compile on iPhone 6

    Do you think about asynk compile make possible?

    1. The gains using scriptable shader variants stripping is proportional to the number of shader variants compiled. If compiling a single shader variants is really slow, like on Metal, the gains will be particularly significant.

  5. How would you possibly ever determine
    A) all the possible keywords being generated for 31,000 shaders.
    B) which ones you actually do and don’t use?

    That sounds like an incredibly daunting task. Is there no way the compiler can make these determinations automatically?

    1. Yes this is a huge task and this is why the blog post is so detailed. The big issue is that the actual used shader variants depends on the actual C# execution.

      It’s not necessary to strip everything. The C# API allows logging all the shader variants at build time. This is a good start to remove shader variants that seems obviously unused.

  6. Don’t you think pseudocode would have been a better choice than sigma notation – considering your audience?

  7. I’m interested in a Shader (Lit) for Terran and a new system for creating Terran… one question: When?
    UPD: Sorry for my English.

  8. Robert Cummings

    5月 14, 2018 4:10 pm

    Nice! very helpful. I understand this isn’t in 1.1.5 HD preview, but 1.1.6?

  9. Helpful post! (The link of the example project is dead btw)

    1. Yes, this is platform independent

    2. Euhm… something went wrong, I’ll fix it on Tuesday.

  10. Project link at the end of blog post doesn’t work.

    1. Euhm… something went wrong, I’ll fix it on Tuesday.

  11. Could you take advantage of this on WebGL?

    1. Yes, this is platform independent