CA.unityはサイバーエージェントが運営するUnityをテーマにした勉強会です。サイバーエージェントのサービス開発者と社外の開発者を交えて、Unityに関する知見を共有する場となります。CA.unity #9では、ユニティ・テクノロジーズ・ジャパン株式会社様とサイバーエージェントが登壇いたします。
※CA.unityは、Unity Technologiesまたはその関連会社がスポンサーとなっているものでも、提携しているものでもありません。Unityは、米国およびその他の国におけるUnity Technologiesまたはその関連会社の商標または登録商標です。
本記事は、2025年01月08(水)に開催した「CA.unity #9」において発表された「Unity 6シェーダーWarmupガイド」に対して、社内の生成AI議事録ツール「コエログ」を活用して書き起こし、登壇者本人が監修役として加筆修正しました。
ブーシェ ロビン晃(ユニティ・テクノロジーズ・ジャパン株式会社 パートナーエンジニア)
エンジニアとしてモバイル・VR・コンソールのゲーム開発に従事後、2020年にユニティ・テクノロジーズ・ジャパン株式会社入社。Unityを使用するデベロッパー向けにコンサルティング・サポートを行っている。
皆さん、こんにちは。本日は「Unity6 シェーダー Warmupガイド」という題名で、主にモバイル環境でのシェーダーWarmupの現状と、Unity6でシェーダーWarmupのフローを大きく改善する新機能の追加についてご紹介させていただきます。
皆さん、はじめまして。Unity Technologies Japanのロビンと申します。パートナーエンジニアとして、Unityをご利用いただいているお客様の技術サポートを担当しております。どうぞよろしくお願いいたします。
ではさっそく始めます。皆さんシェーダーWarmupやってますか?
よくある光景として、オブジェクトを初めて描画するときにこのような大きなスパイクが発生するのを見たことがあると思います。これは、初めてオブジェクトを描画する際に、そのオブジェクトを描画するためのシェーダーをコンパイルしているときに発生するスパイクです。これを回避するために、シェーダーWarmupを実施するのですよね、皆さん。
どうやってシェーダーWarmupを実施しているかというと、多くの方は、昔からあるShaderVariantCollection.WarmUpというAPIを利用しているのではないかと思います。WarmupしたいシェーダーをShaderVariantCollectionというアセットにまとめ、それを使ってWarmupする形になっています。コードも比較的シンプルで、分かりやすく使いやすいものとなっています。
ただし、このShader Variant Collection WarmupのAPIのマニュアルを見ると、下に注意書きが記載されています。こちらをご存知の方もいらっしゃるかもしれません。
警告が記載されており、「この方法はDirectX11とOpenGLでサポートされています。DirectX12、Vulkan、およびMetalは、頂点データレイアウトの設定がプリウォームに使用したデータと異なる場合に・・・」と書かれております。解説が続きますが、これだけを読んでも何のことやら分からないかもしれません。とりあえず、DirectX12、Vulkan、およびMetalでは、このAPIに何らかの問題や注意事項があることは伝わるかと思います。
ということで、結論を申し上げると、実はVulkan、Metal、およびDirectX12の環境では、このShaderVariantCollection.WarmUp APIは機能しません。呼び出しても意味がないのです。そのため、AndroidやiOSでこれを使ってシェーダーWarmupを行っているつもりであっても、シェーダーコンパイルのスパイクがなぜか解消されない、という悩みが多く寄せられています。
となれば、VulkanとMetalではそもそもどうやってシェーダーWarmupをすれば良いのか、という疑問が残ります。Unity Manualには、Shader Loadingの手法を解説する専用のページがあり、ここにVulkanとMetal環境への言及があります。MetalまたはVulkan用にビルドする場合、正確な頂点データレイアウトとレンダー状態が分かる場合にのみ、シェーダーバリアントの正確なGPU表現を作成できる、と書かれています。
これも何のこっちゃって話なのですが、ここで重要なのは「正確な頂点データレイアウト」という点です。ここから、その点について説明していきます。
では、実際にVulkanとMetal環境でどのようにシェーダーWarmupを実施するのか、見ていきましょう。
まず最初に、正確な頂点データレイアウトとは何かという話です。MetalではShader Warmupを行う場合、実際にオブジェクトが描画される際の頂点データレイアウトを把握し、それをWarmup実行時に指定する必要があります。ここでいう頂点データとは、頂点ポジション、Normal、UV座標、頂点カラーなどが含まれる、実際に頂点データに渡される情報のことです。
頂点シェーダーを作成する際、私自身は構造体にそれらすべてが含まれている形で書いていますが、そのことを指しています。この頂点データレイアウトをシェーダーWarmup時に正確に指定しなければなりません。
もし間違った頂点データでWarmupを行った場合、実際に描画される際に正しいデータで再度Shader Compilerが走ってしまい、結果として再びスパイクが発生することになります。そのため、昔からあるShaderVariantCollection.WarmupのAPIでは正確な頂点データレイアウトを指定できないため、VulkanやMetal環境では、Shaderコンパイルが結局もう一度実行されてしまう結果となります。
先ほどのShaderVariantCollectionのAPIでは、頂点データレイアウトを指定する方法がなかったため、Unity2021.3より追加された(現在はエキスペリメンタルですが)新しいShaderWarmupというAPIがあります。このAPIでは、Warmup時に頂点データを指定できるようになっています。
頂点データをどうやって指定するかというと、Vertex Attribute Descriptorという構造体を定義します。 これには主に3つの情報が必要です。一つ目はAttributeと呼ばれ、これはデータの種類、例えば頂点ポジションやUV、頂点カラーといったデータの種類を指定します。 二つ目はFormatで、これはデータの型を指定します。例えば、Float32、Float16、Unorm8などが該当します。
最後はDimensionで、これはデータの次元を指定し、2、3、4などで表現されます。例えば、Dimensionが3の場合はXYZ、Dimensionが4の場合はXYZWとなります。 例えば、頂点シェーダーで頂点ポジションがFloat32でDimensionが3の場合、Shader CodeではFloat3という定義になります。
よく頂点シェーダーのAttributeでポジションをFloat3で表現することがあるのと同じです。
こちらが実際にこのAPIを使って、頂点データレイアウトを指定してWarmupを実施しているコードです。 この例では、ポジションがfloat32でDimensionが3、ノーマルもfloat32でDimensionが3、カラーはUNormの8でDimensionが4(RGBA)、UVはfloat32でDimensionが2、つまりXとYだけで指定しています。
最後に、その頂点データレイアウトを使用してWarmupを行っています。
これだけ見ると簡単ですが、そもそも頂点データはどこから取得できるのでしょうかという疑問が残りますね。
正確な頂点データをシェーダーWarmupで指定する場合、WarmupしたいShaderで描画されるすべてのオブジェクト、つまり描画しているMeshの頂点データレイアウトをあらかじめすべて把握しておく必要があります。 なお、頂点データレイアウトの組み合わせ自体は、プロジェクトの規模にもよりますが、だいたい20パターン以内に収まるケースが多いです。
具体的に頂点データはどこから確認できるのかというと、まずはMeshの確認方法です。MeshをInspectorで開くと、「Vertex」という項目があり、ここで頂点データのレイアウトを確認できます。たとえば、このメッシュではポジション、ノーマル、タンジェント、UV0が並べられており、ポジションはfloat32×3であることが確認できます。
次はParticle Systemの場合です。Particle SystemもシェーダーWarmupの対象になるため、できればParticle SystemのシェーダーもWarmupしたいというケースが多いと思われます。しかし、Particle Systemはランタイムで独自に描画用のメッシュを生成しているため、MeshのようにInspectorですぐに確認することはできません。その代わり、Particle SystemのInspector内のRendererセクションにある「Custom Vertex Stream」という項目を開くと、使用される頂点データを確認することができます。
例えば、このParticle SystemではPosition、Normal、Tangentはなく、ColorとUVが確認できます。Particle Systemはパーティクルの色を頂点カラーで処理しているため、Colorは必ず入ります。
描画するシェーダーのコードも、実際に行っている頂点データも確認する必要があります。例えば、この例ではシェーダー側の頂点シェーダーで使用される頂点データレイアウトは、左側に示す通りPosition、Normal、UV0だけになっています。しかし、描画対象のMeshにはTangentが含まれている場合があります。シェーダーにはTangentが存在しないのに対し、MeshにはTangentのデータが含まれている場合、この場合どちらが正しいかというと、シェーダー側の定義が正しいと判断されます。したがって、Tangentが含まれていない頂点データレイアウトでWarmupを実施する必要があります。逆に、MeshのInspectorに表示されている通りのTangentが含まれている頂点データでShader Warmupを行うと、失敗してしまうため、注意が必要です。
ここまで来ると、チェックする項目が非常に多くなり、確認漏れが発生しそうで心配になるかと思います。そこで、確実に正確なデータを取得するためには、デバイス上で実行中にGPUデバッガーを使用する方法があります。GPUデバッガーといえば、例えばRenderDocのようなツールがあり、使ったことがある方も多いかと思います。
GPUデバッガーではフレームキャプチャーを取得でき、フレームキャプチャーでパイプラインステートを見ると、各オブジェクトを描画している際にGPU上で処理されている頂点データを実際に確認することができます。これを使うと、実行中のGPU上の描画状態を見ながら、確実に正確な頂点データを取得できるのですが、その代わり、Warmup対象のオブジェクトの描画をすべて地道に確認する必要があるため、非常に手間がかかります。
はい、まあめんどくさいんですよね。GPUデバッガーを使えると言っても、なぜこんな手間をかけなければならないのか、という感じになりますよね。
しかし、これがUnity 2022.3までの、VulkanとMetalでのShader Warmupの現状です。こんなに面倒なため、みなさんが実際に行っているのは、シェーダーWarmupのAPIを使わず、WarmupしたいShaderをあらかじめ自分で描画しておくことで、Shaderコンパイルを力技で完了させてしまうという方法です。
はい、ここからが明るい話なんですが、Unity6からGraphics State CollectionというAPIが追加されました。最後にこれを紹介させていただきます。
GraphicsStateCollection APIを使うと、実行中のデバイス上で描画のPSOを記録することができます。つまり、描画されているシェーダーと頂点データを自動的にトラッキングし、そのデータをアセットファイルとして保存できるようになります。これにより、GraphicsStateというアセットに記録されたシェーダーバリアントと頂点データを利用して、シェーダーWarmupを行うことが可能になります。
では、実際にこれを使って記録するフローを見ていきましょう。使い方は非常にシンプルです。まず、GraphicsStateCollectionのBeginTraceというメソッドを呼び出して記録を開始します。記録を開始したら、通常通りゲームを実行します。この間、様々な描画が行われる中で、描画されているシェーダーバリアントと頂点データが記録されていきます。
記録したいゲームパートを終えたら、EndTraceのメソッドを呼び出して記録を終了します。
最後に、SaveToFileのメソッドを呼び出して、記録結果をGraphicsStateAssetに保存します。これだけで記録は完了です。
GraphicStateはこのようにアセットとして追加・配置できる形で提供され、Inspectorで開いて確認することもできます。
このGraphicsStateアセットを元に、GraphicsStateCollectionのWarmupというアセットを利用して、シェーダーWarmupを実施することが可能です。先ほども述べたように、これはデバイス上で実際の描画状態を記録したデータを用いているため、確実に正確な頂点データレイアウトでWarmupを行うことができます。
これは実際にGraphicsStateCollectionを利用している構造ですが、ご覧の通り非常にシンプルです。記録もファイル保存もWarmupも、ほぼ全てのステップが1行で済むため、とても簡単に利用できます。
はい、なのでGraphicsStateCollectionを使うと、すべてのプラットフォームで正確なシェーダーWarmupが非常に簡単に実施できるようになります。特にAndroidとiOSでは大きな恩恵があると思いますので、ぜひUnity6での利用をご検討ください。
本セッションは以上となります。ご清聴、ありがとうございました。