🧐

[Unity][SRP]レンダーパイプラインを自作する その2

2025/03/23に公開

概要

以下の記事の続きです。
https://zenn.dev/nithink/articles/3810a563f2e2d6
UnityのCore RP Library(SRP)を使用して一から独自のレンダーパイプラインを制作する際に、引っかかったこと、気づいたこと、気を付けるべきポイントなどメモも兼ねてまとめています。
バージョンはUnity6000.0で、RenderGraphやnative render pass(以下NRP)を使うことを前提としています。

ShadedWireframeモードとWireframeモードの描画

Sceneウィンドウの右上にはデバッグ用の4つのモードがあります。

  • Shaded Drawモード:Game画面と同じで、デフォルトのモードです。
  • Unlitモード:ライティングが無効なモードです。
  • Shaded Wireframeモード:Shadedモードに、メッシュのワイヤフレームが描画されたモードです。
  • Wireframeモード:メッシュのワイヤフレームだけが描画されたモードです。

自作のレンダーパイプラインではそれぞれのモードが適切に描画されるように実装する必要があります。
ShadedDrawモードはそのままで良いですが、それ以外のモードではそれぞれ専用の対応が必要です。
ちなみにCatlikeCodingでは実装されていません。

Shaded Wireframeモード

専用のRendererListが用意されており、それを描画するだけです。
RendererListの取得は以下のように行います。

RenderGraph.CreateWireOverlayRendererList(Camera);

あとは通常のRasterPassでcmd.DrawRendererListメソッドにより描画するだけです。

Wireframeモード

wireframeモードを選択すると、通常は左画像のような表示になるべきですが、特に対応をしないと右側のように真っ黒になってしまいます。

対応は非常にシンプルで、最終的にBlitするパス(例えばURPならFinalBlitPass)において以下のコードを追加するだけです。

CommandBuffer.SetWireframe(false);

ジオメトリを考慮した正しいscene gridの描画

SceneViewにはgridが表示されていますが、独自のレンダーパイプラインにおいては特定の然るべき対応をしなければ、gridがジオメトリを無視して描画されてしまいます。
例えばURPでは左画像のようにgridが球のジオメトリによって適切に遮蔽されます。しかし然るべき対応を行っていないレンダーパイプラインでは右側のようにgridが常に前面に表示され、表現が破綻してしまいます。

その然るべき対応というのは以下の二点です。

  • backbufferにdepthを書き込む。
  • 最後に指定されるrender targetをbackbuffer(SceneViewRT)にする。

これはgridの描画の処理がSRPでなくエンジン側で行われており、またその描画処理は最後に指定されたrender targetに対して行われるからです。
このことはURPのScriptableRenderer.csのSetEditorTargetの呼び出し部分のコメントから確認できます。

// The editor scene view still relies on some builtin passes (i.e. drawing the scene grid). The builtin
// passes are not explicitly setting RTs and rely on the last active render target being set. Unfortunately
// this does not play nice with the NRP RG path, since we don't use the SetRenderTarget API anymore.
// For this reason, as a workaround, in editor scene view we set explicitly set the RT to SceneViewRT.
// TODO: this will go away once we remove the builtin dependencies and implement the grid in SRP.

なお上記引用コメントのTODOにもあるように、やはりUnityとしてはgridの描画処理をエンジン側でなくSRP側で行うようにしたいようで、もしそうなれば将来的にはこの対応は不要になります。
(先に紹介したOverlayモードのuGUIのように、もしSupportedRenderingFeaturesを通じてエンジン側の描画処理をオフにできるようになり、SRP側で描画処理を実行するコマンドが追加されれば...ということでしょうか。)

gridの描画処理は以下で行われていそうです。これはUnityCsReferenceというUnityのエンジン側のコードです。
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/SceneView/SceneViewGrid.cs

backbufferにdepthを書き込む

ふつうdepthはdepth用attachmentやCameraDepthTextureに書き込んで各種描画(隠面消去やソフトパーティクルなど)に利用するだけで十分であるため、backbufferに書き込むのはcolorだけで、depthまでは書き込まないということが多いかと思います。しかし上記のscene gridの事情からSceneViewやPreviewでは分岐して書き込むようにする必要があります。
なおURPでは、この処理はFinalDepthCopyというパスで行われています。処理としてはCameraDepthTextureからCameraTargetへの深度のコピーとなっています。CameraDepthTextureといえば、生成するかどうかがオプションになっており必ずしも存在するとは限らないのではないかと思われるかもしれませんが、SceneView描画時には必ず生成することになっているようです。

UniversalRenderPipelineAsset.cs
cameraData.requiresDepthTexture |= isSceneViewCamera;

詳しくは後述の「URPにおけるdepth」をご覧ください。

最後に指定されるrender targetをbackbufferにする

URPでは最後のSetRenderTargetというパスによってBuiltInRenderTextureType.CameraTargetをSetRenderTargetでrender targetに指定することで、必ずレンダーパイプラインの最後の描画先がbackbufferになるようにしています。

using (var builder = renderGraph.AddUnsafePass<DummyData>("SetEditorTarget", out var passData, Profiling.setEditorTarget))
{
    builder.AllowPassCulling(false);
    builder.SetRenderFunc((DummyData data, UnsafeGraphContext context) =>
    {
        context.cmd.SetRenderTarget(BuiltinRenderTextureType.CameraTarget,
            RenderBufferLoadAction.Load, RenderBufferStoreAction.Store, // color
            RenderBufferLoadAction.Load, RenderBufferStoreAction.DontCare); // depth
    });
}

SceneViewのbackbufferはCamera.targetTextureかBuiltInRenderTextureType.CameraTargetで、どちらでも良いようです。
このコードではSetRenderTargetという本来RenderGraphで使われないAPIが用いられていますが、以下のようにパスをRasterPassにしてSetRenderAttachmentDepthにしても(少なくとも自分が確認した範囲では)問題ありません。

using IRasterRenderGraphBuilder builder = passParams.RenderGraph.AddRasterRenderPass("SetEditorTarget", out PassData _);
builder.SetRenderAttachment(backbufferColor, 0, AccessFlags.None);
builder.SetRenderAttachmentDepth(backbufferDepth, AccessFlags.None);
builder.AllowPassCulling(false);
builder.SetRenderFunc<PassData>((_, _) => { });

ちなみにURPではSetEditorTargetパスの直前のDrawGizmoPassというパスでbackbufferがSetRenderAttachmentDepthによりrender targetに指定されているため、実はSetEditorTargetパスをコメントアウトしても「最後のrender targetがbackbufferであること」は変わらずgridはそのまま正しく描画されます。

(参考)URPにおけるdepth

この節はURPにおけるdepthの処理について調査した過程で、副産物として得られた情報をまとめたものですが、単体で読んでも毒にも薬にもならないような内容なのでご注意ください。
depthに関する何らかの調査をする際には一助になるかもしれません。

(前提)CameraDepthTexture

CameraDepthTextureはURPでシェーダーからdepthを参照したい場合などに使うグローバルテクスチャです。ソフトパーティクルを実装する際などに使ったことがあるという方も多いと思います。
後述の通り、レンダーパイプライン中で状況によってさまざまな方法で生成されます。
シェーダー内では_CameraDepthTextureを直接参照してもまぁ良いですが、基本的には以下のHLSLファイルをincludeして、その中の関数を使います。
Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl

(前提)depth priming mode

depth priming modeはdepth prepassにするかどうかを選択できる機能で、概要はドキュメント等をご覧ください。
https://docs.unity3d.com/6000.0/Documentation/Manual/urp/urp-universal-renderer.html
要は、depth prepassではDepthOnlyというdepthだけを書き込むシェーダーのパスで事前(opaque描画よりも前)に深度のバッファを作成します。

状況によりかなり変わる

URPでのdepthの処理は、SceneViewかGameViewか、depth prepassかどうかなど状況によってかなり変わるようでした。検証結果を以下にまとめましたのでご覧ください。それぞれの理由についてはまだ解明できていないところも多くあります。
※A:attachment、T:CameraDepthTexture、B:backbuffer
※CameraDepthTextureは全てのケースで生成することになっているとします。

描画先 レンダリングパス depth prepass Tのformat depth prepassによるDepthOnly描画先 CopyDepthでのBlit FinalDepthCopyでのBlit
GameView Forward オフ R32_SFloat - A→T -
GameView Forward オン D32_SFloat_S8_UInt A A→T -
SceneView Forward オフ D32_SFloat_S8_UInt T - T→B
SceneView Forward オン D32_SFloat_S8_UInt A A→T T→B
GameView Deferred - R32_SFloat - A→T -
SceneView Deferred - R32_SFloat A A→T T→B

このようなバッファの調査を行う際にはRenderGraphViwerが便利です。
CopyDepthは表からも分かるようにCameraDepthTextureを作成するためのパスです。opaque描画後にするかtransparent描画後にするかは選択することができます。
SceneViewでは前述のscene gridの事情で最後に必ずBackbufferにコピーされていることが確認できます。

CameraDepthTextureを作る方法

表を見ると、CameraDepthTextureの作られ方は2通りあることが分かります。
一つはDepthOnlyパスで直接CameraDepthTextureへオブジェクトの描画ごとにdepthを書き込む方法(3行目)です。この場合はフラグメントシェーダーでpositionCS.zの値を書き込みます。
もう一つはdepth attachmentをCameraDepthTextureにコピーする方法(A→T)です。この場合はZWriteで書き込まれたdepthがそのままCameraDepthTextureにコピーされることになります。

フォーマット

R32_SFloatは色のバッファのフォーマットで、D32_SFloat_S8_UIntはdepthのバッファのフォーマットです。
CameraDepthTextureは上記表の通り状況によりフォーマットが変わりますが、depth attachmentは常にD32_SFloat_S8_UIntです。

ちなみにR32_SFloatは色のフォーマット扱いなので、SetRenderAttachmentDepthでの指定ができません。行おうとすると以下のエラーが出ます。

InvalidOperationException: Trying to SetRenderAttachmentDepth on a texture that has a color format R32_SFloat. Use a texture with a depth format instead.

逆に、D32_SFloat_S8_UIntはdepthのフォーマット扱いなので、SetRenderAttachmentでの指定ができません。行おうとすると以下のエラーが出ます。

InvalidOperationException: Trying to SetRenderAttachment on a texture that has a depth format. Use a texture with a color format instead.

D32_SFloat_S8_UIntからR32_SFloat、つまりdepthからカラーへのBlitの場合は、SetRenderAttachmentだけで行えます。

コピー

depthのバッファをコピーする際のシェーダーは以下です。
Packages/com.unity.render-pipelines.universal/Shaders/Utils/CopyDepth.shader
フラグメントシェーダーの出力先が_OUTPUT_DEPTHキーワードによって分岐するようになっており、結果的にはCopyDepthパスの処理によりR32_SFloat(色のフォーマット)のコピーならSV_Targetに、D32_SFloat_S8_UInt(depthのフォーマット)ならSV_Depthに出力するようになっています。

#if defined(_OUTPUT_DEPTH)
float frag(Varyings input) : SV_Depth
#else
float frag(Varyings input) : SV_Target
#endif

MSAA周りのロジックが複雑なのですが、これは今後MSAAのトピックとしてまとめて調査・記事化しようと思います。

表3行目のケースとscene grid

Forwardでdepth prepassがオフのSceneView(3行目)ではCameraDepthTextureに対して直接DepthOnlyで深度が書き込まれます。(depth prepassオフなのにdepth prepassパスが実行されるんですね...。)その後はZWriteによりdepthが書き込まれるAttachmentがbackbufferにコピーされるという流れはなく、CameraDepthTextureがbackbufferにコピーされるという流れであるため、この場合scene gridが適切に描画される条件は、シェーダーがZWriteを行うことではなく、DepthOnlyパスを持っていることになります。
言い換えると、URPのSceneViewでは、Forwardの場合、シェーダーがDepthOnlyパスを持っていなければscene gridが適切に描画されません。

CameraCaptureBridge対応

CameraCaptureBridgeとはレンダーパイプラインの処理の外から画面の像を取得したい場合に使う機能です。例えばスクリーンショットの保存機能の実装などで用います。CameraCaptureBridgeはレンダーパイプライン側で実装対応していなければ使うことができない機能です。
既にURPでの詳細な解説記事があり、実装対応にあたってはそちらが参考になります。
https://siguma-sig.hatenablog.com/entry/2023/02/24/232233
URPではなぜか2DRendererのみ実装されていないため、使うにはRendererFeatureを使って実装対応する必要があります。

CatlikeCoding Custom SRPとUnity6000

前回述べた通り、この記事はCatlikeCodingのCustomSRPのチュートリアルを終えた人を対象にしています。
https://catlikecoding.com/unity/tutorials/custom-srp/
https://catlikecoding.com/unity/custom-srp/
現在Custom SRPはUnity2022.3から始まります。もしUnity6000から始めようとするとさまざまな問題に直面することになりますが、どうしてもそうしたい場合は事前に以下の章に軽く目を通しておくと良いと思います。
https://catlikecoding.com/unity/custom-srp/4-0-0/
これは現時点での最後の章で、Unity6000.0への移行についての内容です。
Cameraのインスペクタを表示するとCommandBufferの警告が出る問題の解消や、各種API(主にRenderGraph関係)の変更についての解説などがあります。

最後に

今後は小さなトピックは引き続き「レンダーパイプラインを自作する そのn」にまとめ、大きめのトピックは個別の記事としてリリースすることにしようと思います。
なお個別のトピックとしてはVolumeフレームワーク、VFX Graph対応、ScreenSpaceLensFlareなどを予定しています。

Discussion