はじめに
皆さんこんにちは。SGEコアテクのチャン ユービンです。
前回のRenderGraph記事からだいぶ時間が開いてしまいましたが、今回はいよいよFrameBufferFetchの内容に入ります。
今までの記事
- Unity6からRenderGraphを使いこなそう ー 基本機能編
- Unity6からRenderGraphを使いこなそう ー 実装応用編 その1
- Unity6からRenderGraphを使いこなそう ー 実装応用編 その2
- Unity6からRenderGraphを使いこなそう ー 実装応用編 その3 ← 今はここ
- つづく
執筆する時点の環境
- Unity6 (6000.0.38f1)
- URP 17.0.3
- テスト端末:iphone14 ( iOS 18.3 )
FrameBufferFetchとは?
FrameBufferFetchは、フレームバッファを直接GPUのオンチップメモリから取得する機能です。これにより、メインメモリ(VRAM)への不要な読み書きを避け、レンダリング速度を向上させることができます。特に、Tile-BasedRendering GPUを搭載しているモバイル端末では、メモリ帯域幅の削減と処理時間の短縮に大きな効果があります。
従来のUnityでもそれができないわけではありませんが、自前で色々なことをする必要があるため実装は面倒でした。Unity6のRenderGraphが提供しているFrameBufferFetchの機能によって、より手早く実装できるようになりました。
連動記事
シェーダー最適化入門 第2回目 「タイルベースレンダリングのGPUとは?」
Tile-BasedRendering GPUの基本仕組みを解説し、従来のUnityおよびUnity6でフレームバッファへアクセスする方法を紹介しています。先に読んで頂くと本記事のことがより理解しやすくなると思いますので、是非併せて参考にしてみてください。
本記事はRenderGraphでFrameBufferFetchの実装方法と効果に注目して解説します。
FrameBufferFetchの実装方法
ここからはサンプルを見つつ実装コードの解説をします。
サンプル & 解説
パス構成
このサンプルでは、FrameBufferFetchの効果を見やすくするために、これを2回実行しています。
フルコード
C#コード
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Rendering.Universal; public class FrameBufferFetchSampleRendererFeature : ScriptableRendererFeature { [Flags] private enum UseFrameBufferFetchFlag { None = 0, Pass0 = 1 << 0, Pass1 = 1 << 1, Pass2 = 1 << 2, } [SerializeField] private RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; [SerializeField] private Material material; [SerializeField] private UseFrameBufferFetchFlag useFrameBufferFetchFlag = UseFrameBufferFetchFlag.None; private BlogSampleRenderPass _blogSampleRenderPass; public override void Create() { _blogSampleRenderPass = new BlogSampleRenderPass(); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (renderingData.cameraData.isPreviewCamera || !material) { return; } // SceneViewCameraはFrameBufferFetch非対応なので使わないように var useFBFFlag = renderingData.cameraData.isSceneViewCamera ? UseFrameBufferFetchFlag.None : useFrameBufferFetchFlag; _blogSampleRenderPass.Setup(material, useFBFFlag); _blogSampleRenderPass.renderPassEvent = renderPassEvent; renderer.EnqueuePass(_blogSampleRenderPass); } private class BlogSampleRenderPass : ScriptableRenderPass { private Material _material; private UseFrameBufferFetchFlag _useFrameBufferFetchFlag; public void Setup(Material material, UseFrameBufferFetchFlag useFrameBufferFetchFlag) { _material = material; _useFrameBufferFetchFlag = useFrameBufferFetchFlag; } private class PassData { public List<TextureHandle> SourceList; public Material Material; public int PassIndex; public bool UseFBF; } public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { var resourceData = frameData.Get<UniversalResourceData>(); var cameraColorTarget = resourceData.activeColorTexture; // CameraColorTargetと同じフォーマットのRenderTexture作成 var desc = renderGraph.GetTextureDesc(resourceData.activeColorTexture); desc.depthBufferBits = 0; desc.name = "_CustomBlitTempColor_0"; var tempColorTarget0 = renderGraph.CreateTexture(in desc); desc.name = "_CustomBlitTempColor_1"; var tempColorTarget1 = renderGraph.CreateTexture(in desc); // FrameBufferFetchの効果を見やすくするために2回回す for (var i = 0; i < 2; i++) { // Pass0 AddRenderPass(renderGraph, _material, 0, _useFrameBufferFetchFlag.HasFlag(UseFrameBufferFetchFlag.Pass0), new List<TextureHandle>{cameraColorTarget}, tempColorTarget0, "CustomBlitPass_0"); // Pass1 AddRenderPass(renderGraph, _material, 1, _useFrameBufferFetchFlag.HasFlag(UseFrameBufferFetchFlag.Pass1), new List<TextureHandle>{cameraColorTarget}, tempColorTarget1, "CustomBlitPass_1"); // Pass2 AddRenderPass(renderGraph, _material, 2, _useFrameBufferFetchFlag.HasFlag(UseFrameBufferFetchFlag.Pass2), new List<TextureHandle>{tempColorTarget0, tempColorTarget1}, cameraColorTarget, "CustomBlitPass_Merge"); } return; static void AddRenderPass(RenderGraph renderGraph, Material material, int passIndex, bool useFBF, in List<TextureHandle> sourceList, in TextureHandle target, string passName = "CustomBlit") { using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData)) { passData.SourceList = sourceList; passData.Material = material; passData.PassIndex = passIndex + (useFBF ? 3 : 0); passData.UseFBF = useFBF; builder.SetRenderAttachment(target, 0, AccessFlags.Write); if (passData.UseFBF) { for (var i = 0; i < passData.SourceList.Count; i++) { builder.SetInputAttachment(passData.SourceList[i], i, AccessFlags.Read); } } else { foreach (var source in passData.SourceList) { builder.UseTexture(source, AccessFlags.Read); } } builder.SetRenderFunc(static (PassData data, RasterGraphContext ctx) => { RenderPass(data, ctx); }); } } static void RenderPass(PassData data, RasterGraphContext ctx) { var scaleBias = new Vector4(1, 1, 0, 0); if (data.UseFBF) { Blitter.BlitTexture(ctx.cmd, scaleBias, data.Material, data.PassIndex); } else { for (var i = 1; i < data.SourceList.Count; i++) { data.Material.SetTexture($"_BlitTexture{i}", data.SourceList[i]); } Blitter.BlitTexture(ctx.cmd, data.SourceList[0], scaleBias, data.Material, data.PassIndex); } } } } }
Shaderコード
Shader "FrameBufferFetchSample/CustomBlit" { SubShader { ZTest Always ZWrite Off Cull Off HLSLINCLUDE #pragma vertex Vert #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl" // 画面左の2/3を赤とブランドする half3 ProcessColor_Left(half3 color, float2 uv) { if (uv.x < 0.67) { color = lerp(half3(1, 0, 0), color, 0.8); } return color; } // 画面右の2/3を青とブランドする half3 ProcessColor_Right(half3 color, float2 uv) { if (uv.x > 0.33) { color = lerp(half3(0, 0, 1), color, 0.8); } return color; } // 画面を合成する(上半分を左カラー、下半分を右カラー) half3 MergeLeftRightColor(half3 colorL, half3 colorR, float2 uv) { return uv.y < 0.5 ? colorL : colorR; } ENDHLSL Pass // 0 { Name "CustomBlit_0" HLSLPROGRAM #pragma fragment frag half4 frag (Varyings input) : SV_Target { half4 color = FragNearest(input); color.rgb = ProcessColor_Left(color.rgb, input.texcoord); return color; } ENDHLSL } Pass // 1 { Name "CustomBlit_1" HLSLPROGRAM #pragma fragment frag half4 frag (Varyings input) : SV_Target { half4 color = FragNearest(input); color.rgb = ProcessColor_Right(color.rgb, input.texcoord); return color; } ENDHLSL } Pass // 2 { Name "CustomBlit_Merge" HLSLPROGRAM #pragma fragment frag TEXTURE2D_X(_BlitTexture1); half4 frag (Varyings input) : SV_Target { half4 color0 = FragNearest(input); half4 color1 = SAMPLE_TEXTURE2D_X(_BlitTexture1, sampler_PointClamp, input.texcoord); half4 color = half4(0, 0, 0, 1); color.rgb = MergeLeftRightColor(color0.rgb, color1.rgb, input.texcoord); return color; } ENDHLSL } Pass // 3 { Name "CustomBlit_0_FrameBufferFetch" HLSLPROGRAM #pragma fragment frag FRAMEBUFFER_INPUT_HALF(0); half4 frag (Varyings input) : SV_Target { // *3 FramBufferから直接ロードする half4 color = LOAD_FRAMEBUFFER_INPUT(0, input.positionCS.xy); color.rgb = ProcessColor_Left(color.rgb, input.texcoord); return color; } ENDHLSL } Pass // 4 { Name "CustomBlit_1_FrameBufferFetch" HLSLPROGRAM #pragma fragment frag FRAMEBUFFER_INPUT_HALF(0); half4 frag (Varyings input) : SV_Target { half4 color = LOAD_FRAMEBUFFER_INPUT(0, input.positionCS.xy); color.rgb = ProcessColor_Right(color.rgb, input.texcoord); return color; } ENDHLSL } Pass // 5 { Name "CustomBlit_Merge_FrameBufferFetch" HLSLPROGRAM #pragma fragment frag FRAMEBUFFER_INPUT_HALF(0); FRAMEBUFFER_INPUT_HALF(1); half4 frag (Varyings input) : SV_Target { half4 color0 = LOAD_FRAMEBUFFER_INPUT(0, input.positionCS.xy); half4 color1 = LOAD_FRAMEBUFFER_INPUT(1, input.positionCS.xy); half4 color = half4(0, 0, 0, 1); color.rgb = MergeLeftRightColor(color0.rgb, color1.rgb, input.texcoord); return color; } ENDHLSL } Pass // 6 { Name "CustomBlit_0_FrameBufferFetch_MS" HLSLPROGRAM #pragma fragment frag #pragma target 4.5 #pragma require msaatex FRAMEBUFFER_INPUT_HALF_MS(0); half4 frag (Varyings input, uint sampleID : SV_SampleIndex) : SV_Target { half4 color = LOAD_FRAMEBUFFER_INPUT_MS(0, sampleID, input.positionCS.xy); color.rgb = ProcessColor_Left(color.rgb, input.texcoord); return color; } ENDHLSL } Pass // 7 { Name "CustomBlit_1_FrameBufferFetch_MS" HLSLPROGRAM #pragma fragment frag #pragma target 4.5 #pragma require msaatex FRAMEBUFFER_INPUT_HALF_MS(0); half4 frag (Varyings input, uint sampleID : SV_SampleIndex) : SV_Target { half4 color = LOAD_FRAMEBUFFER_INPUT_MS(0, sampleID, input.positionCS.xy); color.rgb = ProcessColor_Right(color.rgb, input.texcoord); return color; } ENDHLSL } Pass // 8 { Name "CustomBlit_Merge_FrameBufferFetch_MS" HLSLPROGRAM #pragma fragment frag #pragma target 4.5 #pragma require msaatex FRAMEBUFFER_INPUT_HALF_MS(0); FRAMEBUFFER_INPUT_HALF_MS(1); half4 frag (Varyings input, uint sampleID : SV_SampleIndex) : SV_Target { half4 color0 = LOAD_FRAMEBUFFER_INPUT_MS(0, sampleID, input.positionCS.xy); half4 color1 = LOAD_FRAMEBUFFER_INPUT_MS(1, sampleID, input.positionCS.xy); half4 color = half4(0, 0, 0, 1); color.rgb = MergeLeftRightColor(color0.rgb, color1.rgb, input.texcoord); return color; } ENDHLSL } } }
C#側に必要な実装
// FrameBufferとして入力するTextureの使用申請
builder.SetInputAttachment(inputTex, index, accessFlags);
通常のRenderTexture使用と同様に、builderを介して事前に使用申請をする必要があります。ただし、builder.UseTexture
の代わりに、builder.SetInputAttachment
を使います。
- inputTex : FrameBuffeリストに入れるTexture
- index : FrameBufferリストに入れるIndex、0 ~ 7に指定可能
- accessFlags : 他の使用申請と同様にRead/Write指定可能
注意事項:
- FrameBufferリストは描画前に詰められるため、0番が空いていると、1番にセットされているTextureが実際には0番になる
- SetInputAttachmentできるのは該当PassのRender Target(SetRenderAttachmentの対象)と解像度が同じのTextureに限る
- 該当PassですでにSetRenderAttachment、UseTextureで使用申請されたTextureはSetInputAttachmentできない
- 該当PassですでにセットされたIndexに異なるTextureをセットしようとすることはできない(上書きではなく、エラーが出る)
Shaderコードの解説
// FRAMEBUFFERの変数宣言マクロ FRAMEBUFFER_INPUT_HALF(index) // MSAA処理が必要な場合は_MS版を使う FRAMEBUFFER_INPUT_HALF_MS(index) // FRAMEBUFFERからロードするマクロ LOAD_FRAMEBUFFER_INPUT(index, pixelPos) // MSAA処理が必要な場合は_MS版を使う LOAD_FRAMEBUFFER_INPUT_MS(index, sampleIdx, pixelPos)
- 変数宣言マクロはデータタイプ別で、以下のバリエーションがある
- FRAMEBUFFER_INPUT_(HALF | FLOAT | INT | UINT)
- MSAA処理が必要な場合は最後に_MSをつける
- ロード時に使うマクロ
- index:読み込もうとするTextureのindex、C#側でセットする時に使ったindex
- pixelPos:uv座標ではなく、フラグメントのpostionCS.xyを直接使うべき
- MS版にはsampleIdxを渡す必要がある
FrameBufferFetch使用前後の比較
UnityのRenderGraphViewer、XCodeのMetal debuggerで比較します。
その2つのツールがまだ知らない方には以下の記事がおすすめです。
- RenderGraphViewer:RenderGraph基本機能編
- Metal debugger:Xcode GPUプロファイリング入門
解説に入る前に少し補足:
- パスの実行タイミングがAfterRenderingPostProcessingに設定されています
- Passのマージ、MemoryLess化などまだ触れたことがない言葉が出てきますが、次回の記事で解説する予定ですので、ここでは処理効率が良くなる、メモリが節約されるという意味で捉えて頂ければと思います
RenderGraphViewer
前画像
後画像
Metal debugger
前画像
後画像
重要な変化を抜粋します。
- GPU Time:6.02ms → 5.66ms
- Textureのメモリ使用量:117.03MiB → 92.78MiB
- メモリ帯域幅使用(処理中一番多いところ):
- Read:23.26 GiB/s → 2.35 GiB/s
- Write:11.52 GiB/s → 2.17 GiB/s
まとめ
Texture読み込みする際にFrameBufferFetchを使用することでより多くのPassがマージ可能になり、一時TextureがMemoryLess化されることで得られるメリットは以下になります。
特にデメリットはありませんが、以下のケースでは使用できない制限があります。
- 読み込もうとするTextureがPassのRenderTargetと解像度が異なる
- 読み込もうとするPixel位置が現在のFragmentと異なる
最後に
今回はFrameBufferFetchの基本概念、使い方、メリットを解説しました。その中でも特にメモリ帯域幅の節約効果が嬉しいので、皆さんも是非FrameBufferFetchを使ってみてください。
次回予告
次回はその裏の仕組みについて解説していきたいと思います。主にRenderGraphのNativeRenderPass/SubPass、MemoryLessなどの内容になります。