URPのレンダリングパイプラインにおいて、実際に画面へ表示されるのはBackBufferです。しかしBackBufferは書き込み専用で、読み込むことができません。そのためポストプロセスのように途中結果を入力として読む処理がある場合、カメラはいったん中間テクスチャへ描画され、最後にBackBufferへBlitされる仕組みになっています。この中間テクスチャからBackBufferへのBlitを担当するのが、URPのFinalBlitPassです。ただのコピーではなく、画面へ出す直前に必要な最終処理もまとめて担当しています。
「中間テクスチャをBackBufferへ出力する」役割を持つパスは、実はURPに3種類あり、条件によってどれか1つだけが選ばれます。
ポストプロセスが有効であれば追加されるパスですが、以下の条件を満たす場合にFinalBlitの機能も兼ねます。
ポストプロセスが有効で、かつUberPostでは処理しきれない「最終工程」がある場合に追加されるパスです。
上2つのどちらもBackBufferを書いていない場合の、最後の砦です。実際に出るのは主に次のケースです。
表3.1: 最終出力を担当するパスと条件
| 最終出力を担当するパス | 条件 |
|---|---|
| UberPost | ポストプロセス有効、FinalPost不要、後続のカスタムパスもキャプチャも無し |
| FinalPost | ポストプロセス有効、かつFXAA / Linear以外のアップスケール / TAA + CAS のいずれか |
| FinalBlitPass | 上2つがBackBufferを書いておらず、中間テクスチャがある場合 |
| いずれも無し | 中間テクスチャがない、またはカメラスタック上の最後のカメラではない |
URPのポストプロセスを使う時や通常のカスタムポストプロセスを作る時もFinalBlitPassを気にする必要がありません。ただし「最終工程」に相当するカスタムパスを入れた場合、URPのFinalBlitPassは余計なBlitを1回挟むだけの無駄なパスになってしまいます。そこでUnity 6.3から、カスタムパス側から「今からアクティブターゲットがBackBufferになる」と宣言し、URPのFinalBlitPassを省略できるようになりました。
リスト3.1: BackBufferへの切り替え
1 2 | var resourceData = frameData.Get<UniversalResourceData>(); resourceData.SwitchActiveTexturesToBackbuffer(); |
実装は単純です(Runtime/FrameData/UniversalResourceData.cs)。
リスト3.2: SwitchActiveTexturesToBackbufferの実装
1 2 3 4 5 | public void SwitchActiveTexturesToBackbuffer() { activeColorID = UniversalResourceData.ActiveID.BackBuffer; activeDepthID = UniversalResourceData.ActiveID.BackBuffer; } |
frameData上のIDを2つ書き換えるだけで、BlitもコピーもRenderPassの追加も一切行いません。このAPIは「転送処理」ではなく、URPに対する宣言です。activeColorTexture / activeDepthTextureはこのIDを見て返すTextureHandleを切り替えるだけのswitchになっています。
リスト3.3: activeColorTextureの切り替え
1 2 3 4 5 6 7 8 9 10 11 12 | public TextureHandle activeColorTexture { get { switch (activeColorID) { case ActiveID.Camera: return cameraColor; case ActiveID.BackBuffer: return backBufferColor; ... } } } |
SwitchActiveTexturesToBackbuffer()実行後、URPのFinalBlitPassは生成されなくなります。
カスタムFXAAポストプロセスを例に、前後の比較をします。かつての典型的なフローは3回のBlitが必要でした。
リスト3.4: 従来のFXAAフロー
1 | CameraColor → [FXAA] → FXAABuffer → [Blit] → CameraColor → [URP FinalBlit] → BackBuffer |
Unity 6.0以降はCameraColorを直接差し替えられるので2回に減りますが、FXAABufferという中間テクスチャは必要で、FinalBlitPassも引き続き作られます。
リスト3.5: Unity 6.0のFXAAフロー
1 | CameraColor → [FXAA] → FXAABuffer → [URP FinalBlit] → BackBuffer |
Unity 6.3でBackBuffer切り替えを使うと、1回で済みます。中間テクスチャも不要です。
リスト3.6: Unity 6.3のFXAAフロー
1 | CameraColor → [FXAA] → BackBuffer |
コードは次のようになります。なお以降のコードは、要点を示すために一部を抜粋したものです。そのままでは動作しません。
リスト3.7: BackBufferへ直接出力するカスタムFXAA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | private class PassData { public TextureHandle Source; public TextureHandle Target; public Material FXAAMaterial; } public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { var cameraData = frameData.Get<UniversalCameraData>(); var resourceData = frameData.Get<UniversalResourceData>(); // 切り替え前にCameraColorを取得しておく var cameraColor = resourceData.activeColorTexture; // 以降のactiveColorTextureはBackBufferColorを返す // これでURPのFinalBlitPassが生成されなくなる resourceData.SwitchActiveTexturesToBackbuffer(); var backBufferColor = resourceData.activeColorTexture; // LinearToSRGBConversionが必要かどうかの判定 var requireSrgbConversion = cameraData.targetTexture == null && Display.main.requiresSrgbBlitToBackbuffer; CoreUtils.SetKeyword(_fxaaMaterial, ShaderKeywordStrings.LinearToSRGBConversion, requireSrgbConversion); using (var builder = renderGraph.AddRasterRenderPass<PassData>("Custom FXAA Pass", out var passData)) { passData.Source = cameraColor; passData.Target = backBufferColor; passData.FXAAMaterial = _fxaaMaterial; builder.SetRenderAttachment(backBufferColor, 0, AccessFlags.WriteAll); builder.UseTexture(passData.Source, AccessFlags.Read); builder.SetRenderFunc(static (PassData data, RasterGraphContext context) => { // SourceとTargetのUV原点が異なる場合のみY反転が必要 var yFlip = context.GetTextureUVOrigin(data.Source) != context.GetTextureUVOrigin(data.Target); var scaleBias = yFlip ? new Vector4(1, -1, 0, 1) : new Vector4(1, 1, 0, 0); Blitter.BlitTexture(context.cmd, data.Source, scaleBias, data.FXAAMaterial, 0); }); } } |
Unity 6.0ではFinalBlitPassが常に作成されていました。
Unity 6.3でBackBuffer切り替えを使うと、1回のBlitでFXAA処理が完了しBackBufferへ直接出力されます。
SwitchActiveTexturesToBackbuffer()は内部のテクスチャIDを書き換えるだけのAPIです。裏を返せば、辻褄を合わせる責任はすべて呼び出し側にあります。
このAPIを呼んだ瞬間から、以降のすべてのパスのactiveColorTexture / activeDepthTextureはBackBufferを指します。しかしBackBufferはReadできないため、activeColorTexture / activeDepthTextureを読み込む後続処理はすべて破綻します。またcameraData.resolveFinalTarget == false(後続のカメラがある)場合も切り替えてはいけません。OverlayカメラはCameraColorに積み重ねて描画する設計のため、途中のカメラがBackBufferへ直接書くと後続カメラの描画結果と混ざるか上書きされて消えます。したがって呼び出しは、最後のカメラの、最終出力を行うカスタムパスの直前に限定してください。
FinalBlitPassを省略するということは、FinalBlitPassが担っていた最終処理も一緒に消えるということです。カスタムパス側で肩代わりする必要があります。
SourceとTargetのUV原点が一致しない場合、反転しないと画面が上下逆になります。Unity 6.3で追加されたGetTextureUVOrigin()で両者を比較して判定します。このAPIはRasterGraphContextなど実行時のGraphContextのみが持つため、RecordRenderGraphの記録時ではなくSetRenderFunc内で呼ぶ必要があります。
リスト3.8: Y反転の判定
1 2 | var yFlip = context.GetTextureUVOrigin(data.Source) != context.GetTextureUVOrigin(data.Target); var scaleBias = yFlip ? new Vector4(1, -1, 0, 1) : new Vector4(1, 1, 0, 0); |
なおSystemInfo.graphicsUVStartsAtTopはグラフィックスAPIのUV規約を示すだけで、「今回のBlitで反転が必要か」の判定には使えません。
cameraData.requireSrgbConversionを見てLinearToSRGBConversionキーワードを設定する必要があり、Shader側でそのキーワードに応じた変換処理を実装する必要があります。
リスト3.9: sRGB変換キーワードの設定
1 2 | var requireSrgbConversion = cameraData.targetTexture == null && Display.main.requiresSrgbBlitToBackbuffer; cmd.SetKeyword(ShaderGlobalKeywords.LinearToSRGBConversion, data.requireSrgbConversion); |
リスト3.10: sRGB変換
1 2 3 | #ifdef _LINEAR_TO_SRGB_CONVERSION color = LinearToSRGB(color); #endif |
Viewportは通常全画面に設定されているが、全画面以外に設定したい場合、CameraのViewportを設定する必要があります。
リスト3.11: Viewportの設定
1 2 | var targetViewPort = new Rect(0, 0, targetWidth, targetHeight); cmd.SetViewport(targetViewPort); |
HDRディスプレイ出力時のエンコードや、cameraData.rendersOverlayUIがtrueのときのオーバーレイUI合成もFinalBlitPassの仕事でした。HDR出力対応が必要なタイトルでは、HDROutputUtilsを使って同等の処理を実装する必要があります。
要件が多いように見えますが、プロジェクトで実際に使う機能だけ実装すれば十分です。SDR固定・Viewport全画面・オーバーレイUI未使用のタイトルであれば、実質必要なのはY反転とsRGB変換だけになります。
フルスクリーンのBlit1回は、高解像度・モバイル環境では無視できないコストです。カスタムポストプロセスを持つプロジェクトなら、検討する価値は十分にあります。