[Unity][SRP]レンダーパイプラインを自作する その1
概要
UnityのCore RP Library(SRP)を使用して一から独自のレンダーパイプラインを制作する際に、引っかかったこと、気づいたこと、気を付けるべきポイントなどメモも兼ねてまとめています。
実践編以降は他サイトの情報とはなるべく重複しないようにしているため、補助資料としてお役に立てばと思います。
バージョンはUnity6000.0で、RenderGraphやnative render pass(以下NRP)を使うことを前提としています。
筆者自身まだまだ学習の途上にあるので、記事内容は必ずしも最適な手法でなかったり、的外れな言及があったりするかもしれません。
またSRP周りはAPIや仕様の変化が激しく、バージョンが変わると記事の内容が当てはまらなくなることがありますのでご注意ください。
前提編
この記事はSRPやURPの基礎的な知識がある人(例えば下記のCatlikeCodingのCustomSRPを終えた人など)を対象にしていますが、まずこの節では記事内容の前提となる事項をいくつか紹介します。
Core RP Library
ドキュメント:https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@17.0/manual/index.html
リポジトリ:https://github.com/Unity-Technologies/Graphics/tree/master/Packages/com.unity.render-pipelines.core
SRPでレンダーパイプラインを構築する上で必要なライブラリです。
URPやHDRPもこのライブラリを使用して作られています。
BlitでおなじみのBlitterクラス、RTHandle、RenderGraphなどもこれに含まれています。
シェーダーのライブラリもなかなか充実しており、一度は目を通しておくのをおすすめします。(乱数(ハッシュ)、ディザリング、トリプラナーマッピング、ACES等。)
URP
ドキュメント:https://docs.unity3d.com/6000.0/Documentation/Manual/urp/urp-introduction.html
リポジトリ:https://github.com/Unity-Technologies/Graphics/tree/master/Packages/com.unity.render-pipelines.universal
当然ながら、Core RP Libraryの最新かつ正しい使い方のリファレンスとしてURP(HDRPも)以上のものは無いと思います。
しかしSRPやURPは数多くのプラットフォーム・環境に対応する都合や歴史的経緯から、少なくとも初心者にとっては膨大かつ難解なコードになっており、URPやSRPのソースコードからレンダーパイプラインについて学び始めるのはハードルが高いです。(OpenGLやVulkanとかに詳しい人なら可能そうですが...。)まずは下記のCatlikeCodingのチュートリアルから学習するのがおすすめです。
とはいえSRPやURPのコードにはかなり豊富にコメントが付いており、適宜参照することで大変勉強になります。
Catlike Coding Custom SRP
チュートリアル:https://catlikecoding.com/unity/tutorials/custom-srp/
続編:https://catlikecoding.com/unity/custom-srp/
Core RP Libraryで独自のレンダーパイプラインを構築するとても素晴らしいチュートリアルです。
かなりボリューミーかつ難度が高いですが、豊富な知見が得られます。
Windowsだけでなく、モバイル環境も意識されています。
ただし現状はやや旧式なAPIが使われています。
続編ではRenderGraphの対応があるものの、NRPの対応やBlitter、RTHandleなどの使用がありません。そのため、チュートリアルを終えた後はURP等のソースコードを見て、最新の実装方式を学習する必要があります。
なお最近も更新は続いており、今後はNRP含めモダンな方式へと移行される可能性はあります。
RenderGraph
ドキュメント:https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@17.0/manual/render-graph-fundamentals.html
フォーラム:https://discussions.unity.com/t/introduction-of-render-graph-in-the-universal-render-pipeline-urp/930355/241?page=13
URPでの解説記事:https://blog.sge-coretech.com/entry/2024/06/04/171757
最近Core RP Libraryに追加された、レンダーパイプラインを構築する上で用いるフレームワークのようなものです。
リソースの管理を自動で行ってくれたり、裏側で各種最適化を行ってくれたりするという利点があります。
NRPに対応する場合はAPIがかなり異なるので注意が必要です。
Unity6000.0からはURPでも正式に使用されるようになり、RendererFeatureを作成する際などに触れたという方も多いと思います。現状ではURPでRenderGraphを使うかどうかはオプションで切り替えられるようになっています。(compatibility mode)
ちなみにRenderGraphというのは元々レンダーパイプラインを構築する上で用いられるモダンなデザインパターンの一種だそうです。(FrameGraphとも呼ばれるらしいです。)それがUnityのSRPにも導入されたということですね。
そのためShaderGraphやVFX Graphのようなノードベースというニュアンスでの「Graph」とは全く異なります。
この記事ではRenderGraphの基本的な使い方は習得しているという前提の内容になります。
SRPによるレンダーパイプラインの構成
最低限の構成
SRPによるレンダーパイプラインの最低限の構成は以下の通りです。
RenderPipelineAssetクラスとRenderPipelineクラスを継承することで、独自のレンダーパイプラインを作成します。RenderPipelineAssetはScriptableObjectを継承しており、クラス作成後にアセットファイルとして作成してGraphicsSettingsにセットすることで、そのレンダーパイプラインが実行されるようになります。
また、RenderPipelineクラスでは以下のメソッドを通してゲームに存在する全てのカメラのリストが毎フレーム渡されるので、それぞれのカメラの描画処理を記述していきます。
protected override void Render(ScriptableRenderContext context, List<Camera> cameras)
つまりこのRenderメソッドがレンダーパイプラインのエントリーポイント(平たく言えばUpdateメソッドのレンダーパイプライン版)ということになります。
この辺の内容はCatlikeCodingの1章で詳しい解説がありますのでご覧ください。
なおURPではそれぞれUniversalRenderPipelineAssets、UniversalRenderPipelineというクラスがこれらに該当します。
Renderメソッドに渡ってくるカメラというのはシーンに置かれたGameObjectのコンポーネントとしてのカメラだけではなく、SceneView、Preview(例えばMaterialのプレビュー表示)、反射などの描画にあたって内部的に生成されるカメラも含まれます。Camera.CameraTypeを確認することでこうした種別を確認できます。レンダーパイプラインを作る上ではこのようなさまざまなケースでの描画処理を想定し、適切に対応する必要があります。
Renderer
上記の構成を踏まえると、RenderPipeline.Renderメソッドに全ての処理を書いていけばレンダーパイプラインが制作できることが分かりますが、それはUpdateメソッドにアプリの全ての処理を書くようなものなので、実際にはレンダリング専用のクラスを別途作成します。そのようなクラスは一般にRendererと呼ばれます。(MeshRendererやSpriteRendererのようなRendererとは異なる概念です。)
なおURPにおけるRendererはUniversalRendererと2DRendererがあり、2Dか3DかでRendererを分ける形となっています。
このように一つのレンダーパイプライン内に大きく異なる流れがある場合はRendererごと分けて切り替える方式も良いですね。
ちなみに現在はcompatibility modeの都合もあってかRenderGraph対応版の処理はpartialクラスに分離されているので注意が必要です。それぞれUniversalRendererRenderGraph.cs、Renderer2DRenderGraph.csというファイルとなっています。
backbufferとattachment
backbufferとはレンダーパイプラインの最終的な描画先です。とどのつまり、描画したいものをここに全て書き込むことがレンダーパイプラインの目的となります。
実態としては、GameView、RenderTexture、SceneView、Preview(マテリアルのサムネイルなど)、Reflection(ReflectionProbeのリアルタイムモードなど)などがあります。
このようにレンダーパイプラインの開発ではGame画面以外を描画するケースも考慮して実装する必要があります。
このうち、RenderTexture、SceneView、Preview、Reflectionの場合はCamera.targetTextureを通じてbackbufferを取得できます。
GameViewの場合はCamera.targetTextureがnullとなるので、BuiltinRenderTextureType.CameraTargetで取得します。
URPではbackbufferをRTHandleでラップしてRenderGraphにインポートしています。
(以下、URPのUniversalRendererRenderGraph.csより抜粋、やや改変。)
//targetTextureがない(=Game画面への描画である)ならBuiltinRenderTextureType(=backbuffer)を使う。
RenderTargetIdentifier targetColorId = cameraData.targetTexture != null ? new RenderTargetIdentifier(cameraData.targetTexture) : BuiltinRenderTextureType.CameraTarget;
RenderTargetIdentifier targetDepthId = cameraData.targetTexture != null ? new RenderTargetIdentifier(cameraData.targetTexture) : BuiltinRenderTextureType.Depth;
…
//RTHandleにする。
m_TargetColorHandle = RTHandles.Alloc(targetColorId, "Backbuffer color");
m_TargetDepthHandle = RTHandles.Alloc(targetDepthId, "Backbuffer depth");
…
//RenderGraphへインポート。TextureHandleにする。
resourceData.backBufferColor = renderGraph.ImportTexture(m_TargetColorHandle, importInfo, …);
resourceData.backBufferDepth = renderGraph.ImportTexture(m_TargetDepthHandle, importInfoDepth, …);
attachmentとはbackbufferへ描画する前に用いる中間のテクスチャです。VulkanのRenderPassという概念が由来のようです。途中まではattachmentに描画していき、最終的にはattachmentからbackbufferへBlitすることで描画処理が完了します。URPでは、attachmentは色情報用と深度用に分かれており、色情報の方はMRTを想定して配列で定義されています。
実践編
Blitterクラスを使えるようにする
BlitterクラスとはCore RP Libraryにある、いわゆるBlitの処理を行うたくさんのメソッドを備えたユーティリティクラスです。
URPでRendererFeatureを作成する際によく使うので、ご存じの方も多いかと思います。
SRP環境ではcmd.BlitやGraphics.Blitは非推奨なので、代わりにcmd.DrawProcedualなどを使うことになりますが、Blitterの各メソッドはそうした手法のラッパーとなっています。
しかし、独自のSRPでこのBlitterクラスを使える状態にするためにはやや長い道のりがありました。
CatlikeCodingではこの道のりを避けてか、Blitterクラスを使わず自前でcmd.DrawProcedualを使うという形をとっているため、URPの実装を参考にします。
まず、Blitterには初期化メソッド(Initialize)と破棄メソッド(Cleanup)があります。レンダーパイプラインの生成時(RenderPipelineのコンストラクタ)と破棄時(RenderPipeline.Dispose)に合わせてそれぞれを呼びます。
専用のシェーダーの用意
Initializeメソッドには二つのシェーダーを渡す必要があります。
public static void Initialize(Shader blitPS, Shader blitColorAndDepthPS)
これらのシェーダーはMaterialを渡さないBlitメソッドを呼んだ場合に使われるシェーダーで、パスの要件(名前・順序・仕様)はBlitterクラス内のenumで指定されています。
// This enum needs to be in sync with the shader pass names and indices of the Blit.shader in every pipeline.
enum BlitShaderPassNames
{
Nearest = 0,
Bilinear = 1,
NearestQuad = 2,
BilinearQuad = 3,
NearestQuadPadding = 4,
BilinearQuadPadding = 5,
NearestQuadPaddingRepeat = 6,
BilinearQuadPaddingRepeat = 7,
BilinearQuadPaddingOctahedral = 8,
NearestQuadPaddingAlphaBlend = 9,
BilinearQuadPaddingAlphaBlend = 10,
NearestQuadPaddingAlphaBlendRepeat = 11,
BilinearQuadPaddingAlphaBlendRepeat = 12,
BilinearQuadPaddingAlphaBlendOctahedral = 13,
CubeToOctahedral = 14,
CubeToOctahedralLuminance = 15,
CubeToOctahedralAlpha = 16,
CubeToOctahedralRed = 17,
BilinearQuadLuminance = 18,
BilinearQuadAlpha = 19,
BilinearQuadRed = 20,
NearestCubeToOctahedralPadding = 21,
BilinearCubeToOctahedralPadding = 22,
}
enum BlitColorAndDepthPassNames
{
ColorOnly = 0,
ColorAndDepth = 1,
}
なかなか数が多いですが、これらのパスの処理自体はそれぞれ関数として全てCore RP Libraryの以下のHLSLファイルに用意されていますのでご安心ください。
Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl
Packages/com.unity.render-pipelines.core/Runtime/Utilities/BlitColorAndDepth.hlsl
ただしこれらはHLSLファイルなので、それに対応するShaderを作る必要があります。基本的にはShaderLab側で上記順序の通りにパスを用意して、それぞれ#pragma fragmentでHLSL内の該当の関数を呼ぶことになります。ただここには特にオリジナリティを挟む余地はなく単純な手作業になるので、URPにあるShaderLabを流用するのが良いと思います。(これもCore RP Library側に置いてくれても良い気がしますが...)
blitPS:Packages/com.unity.render-pipelines.universal/Shaders/Utils/CoreBlit.shader
blitColorAndDepthPS:Packages/com.unity.render-pipelines.universal/Shaders/Utils/CoreBlitColorAndDepth.shader
ただしこれらのシェーダーにはUniversalPipelineタグが付いているため、コピペして使う場合は該当箇所を消しておく必要がある点に注意が必要です。(そうしないとビルド後にシェーダーエラーとなります。)
Tags{ "RenderPipeline" = "UniversalPipeline" }
ポストエフェクトなどでBlitする際のシェーダーについては、URPを参考にするのが良いかと思います。
つまり、以下をincludeし、頂点シェーダーはこの中のVertを用います。
Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl
RenderPipelineGlobalSettingsの用意
シェーダーを用意してようやくInitializeメソッドを呼ぶと、なんとNullReferenceExceptionが出ます。
該当箇所はInitializeメソッド内の以下の行で、GraphicsSettings.GetRenderPipelineSettings<RenderGraphUtilsResources>()がnullになっていることが原因です。
s_Copy = CoreUtils.CreateEngineMaterial(GraphicsSettings.GetRenderPipelineSettings<RenderGraphUtilsResources>().coreCopyPS);
GraphicsSettings内にRenderGraphUtilsResources型の設定が無いということなので、作成してGraphicsSettings.RegisterRenderPipelineSettingsで登録すれば良いのではないかと思ってしまいますが、RenderGraphUtilsResourcesがinternalであるためそれはできません。
結論としては、別の専用のメソッドを呼び、間接的に登録する必要があります。
まずRenderPipelineGlobalSettingsクラスを継承したクラスを用意します。これはURPのUniversalRenderPipelineGlobalSettingsを参考にします。
実装例を以下に示します。(レンダーパイプライン名は仮でXRPとしています。)
[CreateAssetMenu(menuName = "Rendering/Xrp/XrpGlobalSettings", fileName = "XrpGlobalSettings")]
[SupportedOnRenderPipeline(typeof(XrpAsset))]
public class XrpGlobalSettings : RenderPipelineGlobalSettings
{
[SerializeField] private RenderPipelineGraphicsSettingsContainer _settings = new();
protected override List<IRenderPipelineGraphicsSettings> settingsList => _settings.settingsList;
private void Reset()
{
#if UNITY_EDITOR
EditorGraphicsSettings.PopulateRenderPipelineGraphicsSettings(this);
Initialize();
#endif
}
}
EditorGraphicsSettings.PopulateRenderPipelineGraphicsSettingsメソッドのコメントによれば、IRenderPipelineGraphicsSettingsインターフェイスを実装するもので設定リストを埋めるとあります。
Fills the settingsList with all the available IRenderPipelineGraphicsSettings that are supported for the RenderPipelineGlobalSettings asset.
先ほどのRenderGraphUtilsResourcesクラスはまさにこのIRenderPipelineGraphicsSettingsインターフェイスを実装しており、このメソッドによって登録されるであろうことが分かります。
また、このXrpGlobalSettings自体を生成し、登録する必要があります。これにはRenderPipelineAsset.CreatePipelineメソッドの中で以下のような処理を行うのが良いかと思います。
if (!_globalSettings)
{
_globalSettings = GraphicsSettings.GetSettingsForRenderPipeline<Xrp>() as XrpGlobalSettings;
if(RenderPipelineGlobalSettingsUtils.TryEnsure<XrpGlobalSettings, Xrp>(ref _globalSettings, "Assets/XrpGlobalSettings.asset", true))
{
AssetDatabase.Refresh();
}
}
これでBlitterを使うことができるようになりました。
なお上下反転してしまうケースの解消や、RasterCommandBufferによるBlitについてはNRPの節で取り扱います。
RTHandlesを使う
RTHandleを用いる場合、以下のドキュメントに基づいて初期化や破棄を行う必要があります。
URPではbackbufferやattachmentでそうした手法をとっています。
overlayモードのuGUIの描画処理をSRP管轄下に置く
uGUIのoverlayモードの描画処理は、デフォルトではSRPの処理から独立しています。
このことは独自のSRPでまだ半透明オブジェクトの描画処理を実装していない段階でも描画されることや、FrameDebuggerの表示からも分かります。
しかしURPのFrameDebuggerを見ると、SRPの処理の中に組み込まれています。
このように、overlayモードのuGUIの描画処理をSRP管轄下に置く方法について調査しました。
まともなプロジェクトであればそもそもuGUIをoverlayモードで描画することはほとんどないかと思いますが、基盤開発においては一応さまざまなケースを想定し対応しておくに越したことはありません。
SRPのパスとして描画
URPにはUIScreenSpaceUIPassというクラスがあり、このクラスではoverlayモードのuGUI、UI Toolkit、IMGUI、カーソルといった、画面の前面に出る特殊な要素の描画が行われています。
RenderGraphのパスの種別はRasterPassで、例によってRendererListによる描画ですが、RendererListの作成にはCreateUIOverlayRendererListという専用のメソッドを使います。
passData.rendererList = renderGraph.CreateUIOverlayRendererList(cameraData.camera, UISubset.UIToolkit_UGUI);
builder.UseRendererList(passData.rendererList);
UISubset(flagのenum)によって、描画する要素を選別でき、URPではそれぞれ別々のPassで描画が行われています。
- uGUI(overlay)とUI Toolkit
- IMGUIとカーソル
また、本来overlayモードのuGUIはカメラごとに描画されるのではなく、最終的なGame画面の前面に描画されるという仕様です。このことから、Passの実行タイミングは以下のように条件付ける必要があります。
- カメラの描画対象がGame画面である
- 例えばSceneViewやPreview、RenderTextureなどには描画しない。
- camera.cameraTypeで判別が可能です。
- camera.targetTextureがnullであるかどうかも判別処理に必要です。
- nullでない→RenderTexture、あるいはSceneViewやPreviewへの描画である。
- Game画面を対象とするカメラの中では描画順が最後である
- camera.depthなどで判定が可能です。
このように条件付けをしないとあらぬシチュエーションで変な場所に表示されてしまうことがあるので注意が必要です。
エンジン側の描画処理をオフにする
これでoverlayモードのuGUIをRenderGraphによって描画するようにできましたが、現状ではSRPと独立した当初の描画も別途行われてしまっており、結果的に二重に描画されてしまっているため、そちらはオフにする必要があります。
SupportedRenderingFeatures.active.rendersUIOverlay = true;
描画処理全体を通じてオフで良いので、RenderPipelineを継承したクラスのコンストラクタなどに記述します。
NRP(native render pass)対応
renderGraph.nativeRenderPassEnabledをtrueにするとNRPが有効になります。
しかしNPRを有効にすることで、さまざまなコーディング上の制約を受けることになります。例えばとあるメソッドの特定のオーバーロードだけエラーになったりするようです。
この制約については現状ほとんど情報がなく、NPR対応はやや苦しい道のりとなります。
なおNPRはMetal、Vulkan、DX12で効果を発揮するそうです。さまざまな最適化に加え、一部の環境ではframebuffer fetch(programmable blending)という、ピクセルシェーダーでフレームバッファのピクセルの色を引数として受け取れる機能が有効になります。
下記サイトによればNRPはVulkanのRenderPass/SubPassという機能がベースになっているそうですが、筆者はこの辺の知識が乏しいため、正直あまり釈然とはしていません。(下記サイトはRenderGraphによるNPR対応とは全く別の実装の話になりますのでご注意ください。)
以下は非RenderGraph環境でのNRP対応について書かれたドキュメントなので少し状況が異なりますが、NRP(render pass)そのものについての説明が少しあり、参考になります。
なおURPはNRP対応されていますが、HDRPは対応されていないため、NRPを見据えてコードを参考にする際は注意が必要です。
RenderGraph.nativeRenderPassEnabledには、NRPに関する重要なコメントが記されています。
長いですが重要なので以下に添付します。
Enable the use of the render pass API by the graph instead of traditional SetRenderTarget. This is an advanced feature and users have to be aware of the specific impact it has on rendergraph/graphics APIs below.
When enabled, the render graph try to use render passes and supasses instead of relying on SetRendertarget. It will try to aggressively optimize the number of BeginRenderPass+EndRenderPass calls as well as calls to NextSubPass. This with the aim to maximize the time spent "on chip" on tile based renderers.
The Graph will automatically determine when to break render passes as well as the load and store actions to apply to these render passes. To do this, the graph will analyze the use of textures. E.g. when a texture is used twice in a row as a active render target, the two render graph passes will be merged in a single render pass with two surpasses. On the other hand if a render target is sampled as a texture in a later pass this render target will be stored (and possibly resolved) and the render pass will be broken up.
When setting this setting to true some existing render graph API is no longer valid as it can't express detailed frame information needed to emit native render pases. In particular:
The ImportBackbuffer overload without a RenderTargetInfo argument.
Any AddRenderPass overloads. The more specific AddRasterRenderPass/AddComputePass/AddUnsafePass functions should be used to register passes.
In addition to this, additional validation will be done on the correctness of arguments of existing API that was not previously done. This could lead to new errors when using existing render graph code with nativeRenderPassesEnabled.
Note: that CommandBuffer.BeginRenderPass/EndRenderPass calls are different by design from SetRenderTarget so this could also have effects outside of render graph (e.g. for code relying on the currently active render target as this will not be updated when using render passes).
筆者はまだこの文章を理解しきれたわけではありませんが、要点とコメントを以下に記しておきます。
- オンにすると、RenderGraphは従来のSetRenderTargetの代わりにrender passとsubpassを用いるようになる。
- SetRenderTargetは描画対象を指定する際に用いる従来の方法ですが、NRP環境下では噛み合わないようで、通常使うことはありえないようです。NRP版のBlitterのオーバーロードでは使われなくなっています。
- BeginRenderPass、EndRenderPass、NextSubPassの呼び出し回数を減らし、タイルベースの描画においてGPUの処理時間を削減できる。
- 状況を見て自動的にパスやLoad/Store actionを統合する。
- 例えば同じテクスチャが別のパスで二回続けてrender targetとして用いられた場合は一つのrender pass/二つのsub passに統合する。
- render targetがテクスチャとして後のパスでサンプリングされる場合には、そのrender targetはstoreされる(=StoreAction.Storeになる)。
- RenderGraph自体概ねこのような感じの最適化・自動化が行われるものでしたが、よりネイティブな(NRPの)レベルで行ってくれるようになるということでしょうか。
- オンにすると、いくつかの既存のAPIが無効になり、特に以下の二点に注意が必要。
- ImportedBackbufferメソッドのオーバーロードのうち、RenderTargetInfoを伴わないものが無効になる。
- AddRenderPassメソッドは無効となり、代わりにAddRasterRenderPass/AddComputePass/AddUnsafePassを使わなければならなくなる。
後述します。
- これ以外にも既存のAPIの引数の有効性のチェックが行われるようになり、これまでには出なかったようなエラーが出るようになる。
- 確かにいろいろ挙動が変わってエラーがたくさん出てしまい、大変でした。
- CommandBuffer.BeginRenderPass/EndRenderPassは、SetRenderTargetとはデザインが異なるため、RenderGraphの外側にも影響が及ぶことがある。
Raster/Compute/UnsafePass
旧来の非NRP環境下では、以下のようなAPIを用いるのでした。
renderGraph.AddRenderPass
RenderGraphBuilder
CommandBuffer
NRP環境下では、これらに対してRaster/Compute/Unsafeという三つの種別が導入されます。
つまり、例えばRasterのPassであればAPIは次のようになります。
renderGraph.AddRasterRenderPass
IRasterRenderGraphBuilder
IRasterCommandBuffer
これらは旧来のものと型名が似ていますが全くの別物であり、継承関係もないことに注意が必要です。
Rasterとはレンダリングにおけるrasterization(ポリゴンをピクセルに落とし込む処理)から来ていますが、オブジェクトの描画といった一般的な処理のパスだという理解で良いと思います。
なおComputeはComputeBufferを用いるパスです。
Passの処理は基本的にBuilderやCommandBufferを通じて行われるのでした。IRasterRenderGraphBuilderやIRasterCommandBufferは旧来の型に比べて機能が制限されており、例えばIRasterCommandBufferではCopyやCompute系のメソッドはありません。逆にComputePass版ではRasterPass版の処理はありません。概ね相互排他的になっているということです。
また、RasterPassでは必ずAttachmentの指定をする必要があります。(depthは任意。)
builder.SetRenderAttachment(attachmentColor, 0, AccessFlags.Write);
builder.SetRenderAttachmentDepth(attachmentDepth, AccessFlags.Write);
このようにPassの機能や役割を限定することで、pass mergingという最適化処理を行うことができます。
NRP環境下では必ずこれら三種を用いる必要があり、旧来のAPIを用いるとエラーになるので注意が必要です。ちなみに旧式のAPIと同じような機能でも微妙に名称が異なるものがあります。
例)
テクスチャをReadする
旧:builder.ReadTexture(texture);
新:builder.UseTexture(texture, AccessFlags.Read);
UnsafePassの用途
UnsafePassは旧来のように制約のないPassで実装したい場合に用いる種別です。従来のCommandBufferを用いることができ、Attachmentの指定は必須ではありません。しかし、当然ながら上記の最適化の恩恵にはあずかれないことになります。
なおC#のいわゆるUnsafeとは別物です。あくまでもRenderGraphによるリソース管理下から外れることや、CommandBufferの一部の非推奨なAPIが使えてしまうことを指してunsafeとしています。自由度が高いということは危険でもあるということですね。
用途としては、複数のrender target間でいわゆるping pongなBlitをする場合などが挙げられます。RasterPassでは(MRTの話を別にすれば)一つのPassで一つのAttachmentを指定する(=一つのRenderTargetを指定する)という仕様なので、次々にRenderTargetが切り替わるような実装(例えば任意枚数のping pong)と相性が悪く、UnsafePassを使うことになります。例えばURPのBloom(Blit Bloom Mipmapsパス)ではping pongを行うのでUnsafePassが用いられています。
RenderGraphViewerの表示
NRPが有効な場合、RenderGraphViewerでpass mergingが確認できるようになります。
マージされたpassが青い線で括られます。
また、右下にPass Listが表示されるようになります。
各Passがmergeできていない場合には、その理由を確認できます。
例えば下記画像ではUnsafePassを使っているためmergeできなかったという旨が表示されています。
バッファのクリア
URPではNRP環境下でClearRenderTargetを使うことを避けており(例:UniversalRendererRenderGraph.cs850行)、別の方法でクリアが行われているようです。
例えばRenderGraphのimported resourcesでは、renderGraph.ImportTextureに渡すImportResourceParamsのパラメータを通じてクリアが行われているようです。internal resourcesではTextureDescのパラメータでクリアが行えます。
ただRasterCommandBufferにもClearRenderTargetメソッドがあるので、NPRで使ってはいけないというわけではないのかもしれません?
詳細は調査中です。
RasterCommandBufferによるBlit
BlitterでRasterCommandBufferによるBlitを行おうとすると、以下の点に気づくと思います。
destination(描画先、render target)を指定するオーバーロードがない
ここまでに見たようにNRP環境のRasterPassではSetRenderTargetを用いず、代わりにRenderGraphのSetRenderAttachmentを呼ぶことでrender targetを指定するという方法を取ります。つまりdestinationを指定するオーバーロードが無いのは、Blitするよりも前にSetRenderAttachmentでdestinationを指定していることが前提になるからです。
ほぼ全てのオーバーロードにscaleBiasというVector4のパラメータがある
このパラメータはdynamic scalingの対応や、画面が上下反転してしまう問題の解消と関係しており、適切な値を渡す必要があります。
ただしこの適切な値を導き出す方法はなかなか複雑なので、URPの実装を参考にするのが良いと思います。
URPではRenderingUtils.GetFinalScaleBiasメソッドや、DepthCopyPass.Executeなどでの処理が参考になります。
次回以降
次回以降は以下のようなトピックを扱う予定です。
- ShadedWireframeモードとWireframeモードの描画
- 正しいscene gridの描画
- Volumeフレームワーク対応
- VFX Graph対応
- URPのdepthについて調査
- MSAA対応
Discussion