シェーダーバリアントの収集、パッキング、コンパイル、最適化のアイデア



An Idea Collecting



ビングユニティShaderVariantCollection.ShaderVariant

https://www.jianshu.com/p/0bd9b16496d0



1.バリアントとは

Unityの公式ドキュメントから説明を引用するには: ShaderVariant

Unityでは、さまざまなライトモード、ライトマップ、シャドウなどを説明するために、多くのシェーダーが内部に複数の「バリアント」を持っています。これらのバリアントは、シェーダーパスタイプとシェーダーキーワードのセットによって識別されます。



Unityのシェーダーリソースには、GPUで実行されるシェーダーコードだけでなく、レンダリング状態、属性の定義、さまざまなレンダリングパイプラインに対応するさまざまなレンダリングステージのシェーダーコードも含まれています。コードの各小さな部分には、異なるレンダリング関数に対応する異なるコンパイルパラメータが含まれる場合もあります。

複数のバリアントを持つシェーダーコードスニペットの中で、最も注目すべき機能は、次のようなプリコンパイルされたスイッチを備えていることです。

#pragma multi_compile_fwdbase // Unity has built-in forward pipeline compilation settings, control lighting, shadow and many other related functions #pragma shader_feature _USE_FEATURE_A // Custom function switch #pragma multi_compile _USE_FUNCTION_A _USE_FUNCTION_B // Custom multi compile option

これらのコンパイルスイッチタグを使用すると、シェーダーコードをほとんど記述できないため、このスケルトンコードにアタッチして、微妙な違いのあるバリアントシェーダーコードを実装できます。もちろん、関数が多ければ多いほど、これらのバリアントの数も指数関数的に増加します。これらの亜種の数を制御する方法も、より多くの経験とスキルを必要とする場合があります。



第二に、なぜバリアントを収集するのか

ゲームの初期化時には、通常、レンダリングに使用するすべてのシェーダーを事前にロードして、ゲームの実行中のタイムリーなロードとコンパイルによって発生するスタッターを減らす必要があります。このとき、Shader.WarmupAllShadersを呼び出して、現在ロードされているメモリをロードできます。 Shaderは、すべてのバリアントを含め、一度にコンパイルされます。

プロジェクトのレンダリング効果が強化されると、シェーダーのバリアントはますます増えています。フルローディングインターフェイスを大まかに呼び出すと、ゲームの開始時間が長くなり、ゲームエクスペリエンスに影響します。

その後、Unityはバリアントコレクションに入りました ShaderVariantCollection 上記の大まかなフルローディングインターフェイスを置き換えて、オンデマンドローディングを実現し、ローディング速度を向上させます。

公式説明の中で最も重要な内容は次のとおりです。

これはシェーダーのプリロード(「ウォームアップ」)に使用されるため、ゲームは起動時に「実際に必要な」シェーダーバリアントがロードされていることを確認でき(またはレベルのロード時に)、ゲームの後半でシェーダーのコンパイルに関連する問題を回避できます。

つまり、バリアントはゲームで実際に使用されるバリアントのセットを記録するため、オンデマンドでそれらをロードすると、ゲームのロード速度を大幅に向上させることができます。

第三に、他のいくつかの理解

公式ドキュメントから、バリアントコレクションがシェーダーのプリロードに使用されていることがわかりますが、パッケージ化およびリリースプロセス中のコンパイル、および実際のコンパイルをAssetBundleにフィルターする方法については触れられていません。

実際にリリースされたゲームパッケージのシェーダーリソースで、必要なバリアントが欠落している場合、レンダリング結果が正しくない可能性があります。 (Unityは、複数のバリアントをロードするときに特定のバリアントを使用する必要があります。マッチング方法では、最適な一致が見つからない場合、キーワードの一致数が最も多い別のバリアントにフォールバックするため、部分的な違いが生じるか、完全に間違っています。結果)。最も厄介なのは、実際のオペレーティングデバイスでバリアントから欠落している正確な情報を確認できないことです。何かがうまくいかない場合、あなたは完全に戻ることができるだけかもしれません。

シェーダーバリアントの損失は通常、AssetBundleとの下請けによって引き起こされます。 Unityは、シーンレンダラーでシェーダーとライティングパラメーターを使用してマテリアルをスキャンすることによって取得された実際のバリアントを内部的に収集します(内部レンダリングパイプラインで使用される実際のシェーダーバリアントを記録する必要があります)。 AssetBundleの更新を実現するために、通常、シェーダーを別のリソースとして別のAssetBundleに配置し、これらのシェーダーを参照する他のマテリアルとシーンをAssetBundleの依存関係としてロードします。 Shaderがそれらを使用するキャリアから分離されると、Unityはパッケージ化時に実際にリリースする必要があるバリアントを完全に考慮することができないため、バリアントの煩わしい損失がランダムに発生します。

亜種の喪失に対するいくつかのオンラインソリューション:
(1)シェーダーとそれらを使用するマテリアルをAssetBundleにヒットします
(2)エディターで、プロジェクトシーン全体を実行すると、Unityは収集されたすべてのシェーダーとバリアントを記録し、この情報をバリアントのコレクションとして保存し、シェーダーパックと一緒に保存します。

Unityは、プロジェクト設定パネルの下部にあるこの最も重要な機能を非表示にします。

上記の方法はほとんどの場合に機能しますが、次のようになります。

  • 方法(1)では、ライトはマテリアルからバリアントの使用状況の完全な記録を取得できません。また、実際のレンダラーとシーンのグローバルイルミネーションにも関連しています。
  • 方法(2)では、常に行方不明の魚が常に存在し、Unityによって保存されたシェーダーバリアントはすべて一緒になっています。それらが分割されていない場合、パッケージングと下請け戦略はわずかに影響を受けます。

4、私の解決策

1.プロジェクトでのレンダリングに使用されるリソースは、通常3つのカテゴリのみです。
(1)シーン
(2)動的にロードされたモデル、キャラクター、特殊効果など。
(3)タマネギとタマネギの効果

その中で、一般的なUIはUGUIに組み込まれたシェーダーを直接使用しており、バリアントはmulti_compileを介して提供されます。この種のコンパイルスイッチは、このスイッチがマテリアルで使用されているかどうかに関係なく、バリアントがコンパイルされ、実際のマシンパッケージにリリースされることを保証できます。また、UIにはシェーダーが多すぎることはなく、単にそれらを処理します。

したがって、最終的には、シェーダーの2つのユースケースを考慮する必要があります。シーン内の動的ロードとシーン内の静的リソースです。

2.自動シェーダーバリアントコレクターを実装します。手順は次のとおりです。
(1)パッケージ化する必要のある現在のリソースパスを収集します(複数の言語、チャネルなどのプロジェクトリリース設定に従って)
(2)依存関係を通じて動的にロードされたプレハブなどのリソースに依存するマテリアルパスを収集します
(3)新しい空のシーンを開き、ゲームシーンに次のような動的な光源環境を作成します。リアルタイムパラレルライト
(4)ShaderUtil.ClearCurrentShaderVariantCollectionを呼び出して、現在のプロジェクトによって収集されたバリアントをクリアすることを反映します。それらを再度収集する必要があります。
(5)シーンでレンダリングするためのカメラを作成します
(6)シーン内に球のジオメトリの束を作成し、それらをきちんと配置してから、レンダリングカメラをそれらに位置合わせし、それらがすべて表示されることを確認します。
(7)これらのマテリアルリソースをこれらの球ジオメトリにバッチで割り当て、1つのフレームをレンダリングします
(8)レンダリング後、シーンを順番に開き、パノラマカメラの遠近法を設定してレンダリングします
(9)このようにして、基本的にプロジェクトのShaderバリアントが収集され、リフレクション呼び出しShaderUtil.SaveCurrentShaderVariantCollectionがバリアントコレクションリソース全体に保存されます。
(10)自動収集ツールのタスクが完了しました。

3.このバリアントのコレクションで、大丈夫ですか?
いいえ、タスクは半分しか完了していません。一部のカスタムシェーダー、特にUsePassを介してのみ参照されるシェーダーは、マテリアルリソースに表示されないため、Unityがそのバリ​​アントで収集することはできません(これは確かです。実際のプロジェクトでは、複数のパスを持ついくつかのシェーダーを使用しました。アートの使用に開放されていない内部シェーダーを含めるためにUsePassによって提供されます。アートによって直接使用されるシェーダーには実際のコードがありません。これの利点は、より多くを組み合わせる柔軟性があることです。増加することなくマルチパスの組み合わせシェーダーコードの量)。

たとえば、3つのシェーダーがあります。

  • ABC.shader
  • InternalA.shader
  • InternalB.shader

ABCはInternalAとInternalBの内部パスを使用し、ABCには実際のコードスニペットはありません。この場合、Unityによって収集されたバリアントのコレクションはABCに属し、InternalAとInternalBの区別はありません。このUnityエクスポートの結果を直接取得すると、バリアントが失われる可能性があります。

Unityによってエクスポートされたバリアントのセットを1つずつ分散したリソースに分割する必要があります。まず、それに関連付けられているシェーダーバリアントセットを作成できます。第二に、粒状の分割のパッケージングを容易にすることもできます。

4.続行します
続行する前に、いくつかの準備をしてください。
(1)リフレクションShaderUtil.OpenShaderCombinations(shader、usedBySceneOnly = true)を介して、Unityによって生成されたLibrary /ParsedCombinations-xxx.shaderファイルを開くことができます。テキスト分析を通じて、すべての有効な組み込み、Shader_features、これら3つのカテゴリのキーワードのmulti_compiles、およびコードスニペットタグを取得できます。
(2)ShaderVariantCollection内のShaderの各バリアントセットをリフレクションメソッドで読み取ります
(3)後でこの情報を繰り返し取得できるように、キャッシュ作業を行います。

次の論理ステートメントをより明確にするために、疑似コードで表現されています。

// Start to split the total set and create independent variant sets for all shaders ShaderVariantCollection unityVAC // The total set exported by unity foreach ( curSVC in unityVAC ) { // The current subset of shader in the collection var cur_shader = curSVC.shader // All variants of the current shader var cur_shaderVariants = curSVC.variants // Create a new independent variant set for the current var va = new ShaderVariantCollection() // Try to copy these variants to the new variant set foreach ( cur_v in cur_shaderVariants ) { try { var realSV = new ShaderVariantCollection.ShaderVariant( cur_v.shader, cur_v.passType, cur_v.keywords ) va.Add( realSV ) } catch ( ... ) { // Explain that this variant does not belong to the specified pass type of the currently created shader // Here, the variants collected by unity are generally dependent } } Save( va ) // Get dependencies, come in through UsePass, Fallback // Create or update variant sets for child shaders in sequence var child_shaders = GetDependencies( GetAssetPath( cur_shader ) ) foreach ( child_shader in child_shaders ) { // The dependent shader can be relied on multiple times by different shaders, here we should pay attention to the cache var child_va = TryGet_New_ShaderVariantCollection( child_shader ) // Pass the variants in order to test whether they belong to dependent child_shader foreach ( cur_v in cur_shaderVariants ) { var _keywords = copy( cur_v.keywords ) // This variant may contain keywords that do not belong to child_shader // By parsing the ParsedCombinations file provided earlier, exclude them RemoveInvalidKeyword( _keywords, child_shader ) try { var realSV = new ShaderVariantCollection.ShaderVariant( child_shader, cur_v.passType, _keywords ) // Pay attention to deduplication if ( !child_va.Contains( realSV ) ) { child_va.Add( realSV ) } } catch ( ... ) { // ... } } } // Save all the shader variants that are dependent on it // ... // There are some problems here: // 1. Since the slave pass of the variant cannot be obtained intact, //(Neither passName nor passIndex) // So it is not possible to accurately create variants for dependencies, so as long as the variants in the above code are legal, they should be used // 2. A shader can be used directly by the material, and the variants collected by Unity, // It may also be referenced by other Shaders, can the variants generated by the referenced part be collected by unity, // Further testing and verification are required }

このように、上記のいくつかのトスの後、私は最も完全なバリアント使用記録を作成し、トスの次の段階を開始したいと思っています。

5.コンパイル時間の最適化
バリアントセットを作成した後、シェーダーをパッケージ化したときに、シェーダーが長時間コンパイルされたままであることがわかりました。multi_compileの数の見積もりを追加しても、バリアントセットの宣言の数を超えすぎていました。 。このことから、公式文書のバリアントコレクションの説明テキストから、バリアントセットは、シェーダーバリアントの使用サブセットをプリロードして指定するためにのみ使用できると推測しました。コンパイルに関しては、それは別のリソース処理段階であり、自分でフィルターで除外する必要があります。

Unity 2018.2では、プログラム可能なシェーダーバリアント削除パイプラインが導入されています:IPreprocessShaders.OnProcessShader、このインターフェイスを使用すると、Unityがシェーダーをコンパイルするときにコールバック通知を受信できます。独自のシェーダーバリアント削除ロジックを実装して、コンパイル時間をさらに短縮できます。

プロジェクトに複数のIPreprocessShadersインターフェイスオブジェクトを実装できます。 Unityは、シェーダーをコンパイルしてコールバックインターフェイスを実行するときに、これらのプロセッサインスタンスを独自に作成します。これらのコールバックで着信パラメータを除外する必要があります。

一例:

/// A processor that simply excludes Unity's built-in variant compilation class BuiltinShaderPreprocessor : IPreprocessShaders { static ShaderKeyword[] s_uselessKeywords public int callbackOrder { get {return 0} // You can specify the order of callbacks between multiple processors } static BuiltinShaderPreprocessor() { s_uselessKeywords = new ShaderKeyword[] { new ShaderKeyword( 'DIRLIGHTMAP_COMBINED' ), new ShaderKeyword( 'LIGHTMAP_SHADOW_MIXING' ), new ShaderKeyword( 'SHADOWS_SCREEN' ), } } public void OnProcessShader( Shader shader, ShaderSnippetData snippet, IList data ) { for ( int i = data.Count - 1 i >= 0 --i ) { for ( int j = 0 j

より詳細で正確なコンパイル除外ロジック(不完全なコードフラグメント)を作成する必要があります。

class ShaderPreprocessor : IPreprocessShaders { public void OnProcessShader( Shader shader, ShaderSnippetData snippet, IList data ) { // Skip processing system shader, no processing // return // Read the variant set corresponding to shader: // In the previous step, we created an independent compilation set for each shader used // Get the compilation information of the specified shader var comb = ShaderUtils.ParseShaderCombinations( shader, true ) // Skip some shaders that completely use other shaders and do not contain the shaders themselves, do not process // return // Reverse traversal, which facilitates the delete operation for ( int i = data.Count - 1 i >= 0 --i ) { // List of variant keywords in the current compilation unit var _keywords = data[ i ].shaderKeywordSet.GetShaderKeywords() // Only remove the cases with keywords to reduce code complexity // In fact, keyword-free variants may also be discarded, simply discarding this culling operation will not add too much compilation burden if ( _keywords.Length > 0 ) { var keywordList = new HashSet() for ( int j = 0 j <_keywords.Length ++j ) { var name = _keywords[ j ].GetKeywordName() fullKeywords.Add( name ) if ( snippetCombinations.multi_compiles != null ) { if ( Array.IndexOf( snippetCombinations.multi_compiles, name ) 0 ) { // Show that the keyword of this variant can be removed from the compilation // Further judgment: // Whether the variant formed by this keyword sequence appears in the variant set resource we stored in advance // When traversing the variant set that has been used, pay attention to the multi_compile item // The keyword is removed, in the disorder comparison, if it can match completely, it means that the current compilation // The shader variant may be used, otherwise it will be removed // ... var matched = false // Iterate through all the variants collected from the project for ( int n = 0 n < rawVariants.Count ++n ) { var variant = rawVariants[ n ] var matchCount = -1 var mismatchCount = 0 var skipCount = 0 if ( variant.shader == shader && variant.passType == snippet.passType ) { matchCount = 0 // Need to explain: // When searching for matching variants, the multi_compiles keyword needs to be excluded // The snippetCombinations data comes from manual parsing ParsedCombinations-XXX.shader // If you call ShaderUtil.GetShaderVariantEntries directly, the memory may explode because the number of full variants is too large for ( var m = 0 m < variant.keywords.Length ++m ) { var keyword = variant.keywords[ m ] if ( Array.IndexOf( snippetCombinations.multi_compiles, keyword ) = 0 && mismatchCount == 0 && matchCount + skipCount == keywordList.Count ) { matched = true break } } if ( !matched ) { data.RemoveAt( i ) } } } } } }

6.終了
上記の一連の操作の後、シェーダーバリアントの収集プロセスとコンパイル時間が最適化されます。ただし、プロセス全体を実装するプロセスでは、Unityでは一般的に使用されない多くのエディターAPIが使用されます。一部のプロセスで取得された情報が不完全なため、最終的な結果は検出できないエラーになる可能性があります。この方法はまた、さらなる研究と改善が必要です。

表紙画像ソース: 素晴らしいシェーダー
CG / ShaderLabで記述されたUnity用のシェーダーのコレクション。


著者のLuJianの貢献に感謝し、これはYuhuTechnologyの646番目の記事です。転送と共有へようこそ。著者の許可なしに転載しないでください。独自の洞察や発見があれば、一緒に話し合うために私達に連絡してください。 (QQグループ:793972859)

著者のホームページ: https://github.com/lujian101 、作者はU Sparkleイベントの参加者でもあり、UWAは、より多くの開発者がU Sparkle開発者プログラムに参加することを歓迎します。この段階では、よりエキサイティングになります。