こんにちは!情熱開発部プログラマの倉平です!
9月に入り涼しくなってきたのはありがたいですが、代わりに天候が荒れることが多いですね。
外出先で急なゲリラ豪雨に遭遇することも多々あるはず。
そんな時に限って傘を持ってきていない…。
ずぶぬれ覚悟で移動するか止むまで待つか悩みます。
備えあれば患いなし、めんどくさがらず小傘を持ち歩くのをお勧めします。
さて、今回はUnityでのシェーダーのロードについての話題です。
※本記事ではUnityバージョン 2022.3.44f1を使用しています。
また、URPのサンプルシーンを使用しています。
初回描画時にスパイクが発生?
Unityで制作したゲームのシーン再生時やオブジェクトの描画時に初回だけスパイクが発生することがあります。
これは描画に必要なシェーダーをリアルタイムでロードしているからです。
例)シーンの遷移時に一瞬カクつく
Profilerを見てみましょう
「Shader.ComplineGPUProgram」こちら怪しいですね。
2回目以降の遷移時のProfilerも見てみます。
こちらには「Shader.ComplineGPUProgram」はありませんでした。
どうやらこれが原因ですね。
本件ではこのスパイクを防ぐ方法を説明しようと思います。
と、その前に先ずは描画処理に使用されるShaderVariantについてみていこうと思います。
ShaderVariantとは?
ひとつのシェーダーをベースに異なった環境で効率的に描画処理を行う為に使用される物です。
シェーダーはキーワード設定によって描画処理を変えることが出来ます。
// キーワードを定義
#multi_compile _ TEST_KEY
fixed4 testColor = fixed4(0, 0, 0, 1);
// キーワードが有効なら色を変更
#ifdef TEST_KEY
testColor = fixed4(1, 1, 1, 1);
#endif
このようにシェーダー自体は一つでもキーワードの種類やDifferent Light モード、ライトマップ、シャドウなどの設定によってバリエーションが生まれます。
これらをパスタイプとキーワードのセットで識別出来るようにした物がShaderVariantです。
キーワードが増えるとShaderVariantも増えます。
シェーダーは一つでもバリエーションの数だけ増加する為、シェーダーが増加する一因となっています。
描画を行う際に今まで使用していない組み合わせが発生した場合、
ShaderVariantを生成します。
これがスパイクが発生する原因となります。
ShaderVariantを事前にロードすることで上記を防ぐことが出来ます。
次はロードに必要なShaderVariantCollectioについて説明します。
ShaderVariantCollection?
Collectionと名の付く通り、シェーダーごとのShaderVariantをまとめるクラスです。
ShaderVariantのロード処理を行う機能があります。
こちらを作成し適切な場所でロードを行うことを目指します。
ShaderVariantCollectionを作成してみよう
①自動作成
簡単な作成方法としてUnityEditor上で保持しているShaderVariantを保存する方法があります。
こちらはUnityEditorでゲームをプレイするなりしてShaderVariantを生成して保持させます。
保持しているShaderVariantは「ProjectSettings/Graphics」から確認出来ます。
下にある「Save to asset…」を実行することで保持しているShaderVariantをShaderVariantCollectionでまとめて保存できます。
簡易に作成できますが、プロジェクトによってはEditor上でしか使用しないShaderVariantも含んでいます。
また、組み合わせ抜けが発生する可能性もあるのでご注意ください。
ちなみに隣の「Clear」を押すと保持しているShaderVariantがすべて破棄された状態になります。
②手動作成
UnityEditor上の右クリックメニュー「Create/ShaderVariantCollection」を実行してください。
そうすると空の状態のShaderVariantCollectionが生成されます。
ShaderVariantCollectionを編集してみよう
ShaderVariantCollectionはInspector上で編集出来ます。
「Add shader」を押すと選択ウィンドウが表示されるので、任意のシェーダーを選択してください。
登録されているシェーダーの右となりにある「-」を押すとそのシェーダーを削除できます。
「+」を押すとShaderVariantの追加が出来ます。
ShaderVariant設定ウィンドウが表示されます。
①でライティング状況、②で有効なキーワードを設定、③でShaderVariantを追加が行えます。
おまけ スクリプト上での編集について
public bool Add (ShaderVariantCollection.ShaderVariant variant);
public bool Remove (ShaderVariantCollection.ShaderVariant variant);
上記のAdd関数とRemve関数で追加と削除が行えます。
プロジェクトで使用しているShaderから発生するShaderVariantを計算して追加する機能を作るのも良いかもしれません。
登録されているShaderVariantの総数が増えるとロード時間も増加します。
やたらめったら追加するのではなくゲーム内で実際に使用している物だけを含めることをお勧めします。
ShaderVariantを事前ロードしよう
ShaderVariantCollectionが用意出来たらこちらを使用して事前ロード処理を組み込んでみましょう。
ShaderVariantCollectionにはShaderVariantのロード処理であるWarmup関数があります。
2つのWarmup関数があるのでそれぞれの使用方法と注意点を紹介します。
Warmup関数は同期処理となっている為、終了するまで画面が固まります。
実行する場所はタイトルロゴ中など静止した環境で行うことをお勧めします。
また、DX11とOpneGLは完全にサポートされていますが、DX12、Vulkan、Metal では一部サポートとなっています。
頂点レイアウトやレンダーターゲットの設定が、事前準備に使用するデータと異なる場合、グラフィックスドライバーによる作業が必要な場合がありますのでご注意ください。
こちらはShaderVariantCollection内の全ShaderVariantを全てロードします。
Variant数に応じでロード時間も増加する為、場合によっては完了するまで数秒画面が固まることがあります。
この状態を軽減するにはShaderVariantCollectionを複数に分割して別々のフレームで実行してみてください。
shaderVariantCollection.Warmup();
bool WarmUpProgressively(int variantCount)
引数のvariantCountで指定した数だけShaderVariantをロードします。
戻り値は全てのShaderVariantがロード済みならtrue、それ以外はfalseが返ります。
Warmup()と違いShaderVariantCollectionを分割しなくても少しずつWarmupを行えるので、
毎フレームごとに実行することで画面が固まることが軽減出来ます。
int variantCount = 1;
bool result = shaderVariantCollection.WarmUpProgressively(variantCount);
事前ロードを組み込んでみた
Warmupの実装前後比較です。上が組み込み前、下が組み込み後。
シーン遷移後のカクツキが軽減されていることが分かります。
おまけ Warmupの進捗状況について
Warmupの進捗状況を計算したい場合の例です。
進捗を画面上に表示したい場合などでご活用ください。
/// <summary>
/// Warmupの進捗を返す
/// </summary>
/// <returns>進捗(0~1)</returns>
public bool GetWarmupProgressRate()
{
int variantCount = shaderVariantCollection.variantCount; // variantの総数
int warmedUpCount = shaderVariantCollection.warmedUpVariantCount; // Warmup済みのvariant数
return (float)warmedUpCount / (float)variantCount;
}
まとめ
UnityでのShaderのWarmupいかがでしたでしたか。
プロジェクトごとに適切なShaderVariantCollectionを用意するのは情報の精査等で大変ですが、
効果は大きいので是非とも実装してみてください。
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。