第3章 URPのFinalBlitPassを省略する方法

チャン ユービンZHANG YUBING

株式会社サイバーエージェント SGEコア技術本部

3.1 FinalBlitPassとは

URPのレンダリングパイプラインにおいて、実際に画面へ表示されるのはBackBufferです。しかしBackBufferは書き込み専用で、読み込むことができません。そのためポストプロセスのように途中結果を入力として読む処理がある場合、カメラはいったん中間テクスチャへ描画され、最後にBackBufferへBlitされる仕組みになっています。この中間テクスチャからBackBufferへのBlitを担当するのが、URPのFinalBlitPassです。ただのコピーではなく、画面へ出す直前に必要な最終処理もまとめて担当しています。

  • カメラのViewportを考慮した転送
  • Linear→sRGB変換
  • HDR Output時のエンコードとオーバーレイUIの合成
  • UV原点が異なる場合のY反転
  • スケーリング時のフィルタ切り替え

3.1.1 FinalBlitの機能を持つ3種類のFinalBlitPass

「中間テクスチャをBackBufferへ出力する」役割を持つパスは、実はURPに3種類あり、条件によってどれか1つだけが選ばれます。

UberPost

ポストプロセスが有効であれば追加されるパスですが、以下の条件を満たす場合にFinalBlitの機能も兼ねます。

  • カメラスタックの最後のカメラであること
  • 後述のFinalPostが不要であること
  • AfterRenderingPostProcessing以降に実行されるカスタムパスが無いこと

FinalPost

ポストプロセスが有効で、かつUberPostでは処理しきれない「最終工程」がある場合に追加されるパスです。

  1. FXAA
  2. Linear以外のアップスケール … Point / FSR / STPなど。
  3. TAA + CAS … RCASシャープニングを独立パスで実行する必要がある

FinalBlitPass

上2つのどちらもBackBufferを書いていない場合の、最後の砦です。実際に出るのは主に次のケースです。

  • ポストプロセスが無効で、かつ中間テクスチャが存在する
  • ポストプロセスは有効だが、その後ろにカスタムパスがある
  • ポストプロセスは有効だが、Camera.AddCommandBufferAsyncなどのキャプチャ処理がある

3種類の関係を整理

表3.1: 最終出力を担当するパスと条件

最終出力を担当するパス条件
UberPostポストプロセス有効、FinalPost不要、後続のカスタムパスもキャプチャも無し
FinalPostポストプロセス有効、かつFXAA / Linear以外のアップスケール / TAA + CAS のいずれか
FinalBlitPass上2つがBackBufferを書いておらず、中間テクスチャがある場合
いずれも無し中間テクスチャがない、またはカメラスタック上の最後のカメラではない

3.2 FinalBlitPassを省略する方法

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は生成されなくなります。

3.2.1 使用例:カスタムFXAA

カスタム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へ直接出力されます。

3.3 URPのFinalBlitを省略する時の注意事項

SwitchActiveTexturesToBackbuffer()は内部のテクスチャIDを書き換えるだけのAPIです。裏を返せば、辻褄を合わせる責任はすべて呼び出し側にあります。

3.3.1 カメラの最後にだけSwitchActiveTexturesToBackbuffer()を呼び出すこと

このAPIを呼んだ瞬間から、以降のすべてのパスのactiveColorTexture / activeDepthTextureはBackBufferを指します。しかしBackBufferはReadできないため、activeColorTexture / activeDepthTextureを読み込む後続処理はすべて破綻します。またcameraData.resolveFinalTarget == false(後続のカメラがある)場合も切り替えてはいけません。OverlayカメラはCameraColorに積み重ねて描画する設計のため、途中のカメラがBackBufferへ直接書くと後続カメラの描画結果と混ざるか上書きされて消えます。したがって呼び出しは、最後のカメラの、最終出力を行うカスタムパスの直前に限定してください。

3.3.2 最後に実行するカスタムパスにFinalBlitの機能を実装すること

FinalBlitPassを省略するということは、FinalBlitPassが担っていた最終処理も一緒に消えるということです。カスタムパス側で肩代わりする必要があります。

Y反転

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で反転が必要か」の判定には使えません。

sRGB変換

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設定

Viewportは通常全画面に設定されているが、全画面以外に設定したい場合、CameraのViewportを設定する必要があります。

リスト3.11: Viewportの設定

1
2
var targetViewPort = new Rect(0, 0, targetWidth, targetHeight);
cmd.SetViewport(targetViewPort);

HDR OutputとオーバーレイUI合成

HDRディスプレイ出力時のエンコードや、cameraData.rendersOverlayUIがtrueのときのオーバーレイUI合成もFinalBlitPassの仕事でした。HDR出力対応が必要なタイトルでは、HDROutputUtilsを使って同等の処理を実装する必要があります。

要件が多いように見えますが、プロジェクトで実際に使う機能だけ実装すれば十分です。SDR固定・Viewport全画面・オーバーレイUI未使用のタイトルであれば、実質必要なのはY反転とsRGB変換だけになります。

3.4 まとめ

  • URPでBackBufferへの最終出力を担当するパスはUberPost / FinalPost / FinalBlitPassの3種類あり、条件によってどれか1つだけが選ばれる
  • カスタムパスをAfterRenderingPostProcessing以降に注入するとhasPassesAfterPostProcessingがtrueになり、FinalBlitPassがほぼ必ず追加される
  • Unity 6.3のSwitchActiveTexturesToBackbuffer()を使うと、カスタムパスが直接BackBufferへ出力でき、このFinalBlitPassを省略できる
  • ただしこのAPIはカメラのActiveColor/Depth TextureのIDを書き換える宣言にすぎない。最後のカメラの最終パスでのみ呼ぶことと、Y反転・sRGB変換・ViewportなどFinalBlitPassが担っていた処理を自前で持つことが前提になる

フルスクリーンのBlit1回は、高解像度・モバイル環境では無視できないコストです。カスタムポストプロセスを持つプロジェクトなら、検討する価値は十分にあります。

3.5 参考資料