UE5 デリゲートを使ったフレームキャプチャをする方法

# 概要
UE5アドベントカレンダーシリーズ6の18日目です。

動作環境
UE5.3.2

ゲーム開発していると現在レンダリングしている画面をキャプチャしてレンダーターゲットテクスチャとして使いたいときがよくあります。

UnrealEngineにはキャプチャする様々な方法がありますが重かったり情報が古くなったりしているので改めて紹介します

# 従来の方法
検索してよく出てくる方法をまとめます。

## SceneCapture2Dを使ったキャプチャ
検索して真っ先に出てくる方法だと思います。
一番手軽ですが、ランタイムで全画面をキャプチャするのに使うには非常に重く、キャプチャ対象を絞ってキャラのみにするなど用途に合わせた使い方が必要になります

## FrameGrabberを使う
FrameGrabberを使ってキャプチャする方法もあります。

https://pafuhana1213.hatenablog.com/entry/2018/08/27/001512

紹介されている通りUIも含めたキャプチャになってしまうので困ることもあります。
具体的にはポーズやメインメニューを開いたりしたとき、背景をキャプチャした絵で止めて処理負荷対策をするときなどです。

## エンジン改造をしてキャプチャする
2019年のアドベントカレンダーで紹介されているエンジン改造の方法もあります。

https://qiita.com/com04/items/5ea9e569443031708664

エンジン改造はビルドも必要になり、難易度も高いです。
2019年当時は4.23.1ということで、記事執筆時点の最新5.3.2(記事執筆時点)ではソースコードも変わり参考にするのも難しいです。

# デリゲートを使った方法
現在では3Dレンダリングの最後に好きな描画コマンドを付け足すことができるデリゲートが公開されています。
こちらを使ってUIレンダリング前の段階を任意のレンダーターゲットに描き込むことでキャプチャすることができます

## デリゲートを確認
Engine.hに以下のデリゲートが追加されています
```c#
	UE_DEPRECATED(5.0, "Please use GetPreRenderDelegateEx().")
	FPreRenderDelegate& GetPreRenderDelegate() { return PreRenderDelegate; }
	FPreRenderDelegateEx& GetPreRenderDelegateEx() { return PreRenderDelegateEx; }
	UE_DEPRECATED(5.0, "Please use GetPostRenderDelegateEx().")
	FPostRenderDelegate& GetPostRenderDelegate() { return PostRenderDelegate; }
	FPostRenderDelegateEx& GetPostRenderDelegateEx() { return PostRenderDelegateEx; }
 ```
呼び出しタイミングは
FDeferredShadingSceneRenderer::Render(FRDGBuilder& GraphBuilder)の末尾
RenderFinish呼び出し前

FMobileShadingRenderer::REnderの末尾
RenderFinish呼び出し前

FMobileSceneRenderer::RenderHitProxiesの最終行
FDeferrredShadingSceneRenderer::RenderHitProxiesの末尾

となっています。

デリゲートの定義は
```c#
DECLARE_MULTICAST_DELEGATE_OneParam(FPostRenderDelegateEx, class FRDGBuilder&);
```
となっているので、任意のFRDGBuilderを構築して渡せばなにか実行できそうです

適当にGameInstanceSubsystemのクラスを作成しInitializeでデリゲートに処理を渡して動作を確認します
```c#
UCLASS()
class BACKBUFFERCAPTURE_API UHogeSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()
public:
	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	void Render(FRDGBuilder& GraphBuilder);
};
```

```c#
#include "HogeSubsystem.h"
#include "RenderGraphEvent.h"
#include "RenderGraphUtils.h"
DECLARE_GPU_STAT(HogeSubSystemRender);

void UHogeSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);
	GEngine->GetPostRenderDelegateEx().AddUObject(this, &UHogeSubsystem::Render);
}

void UHogeSubsystem::Render(FRDGBuilder& GraphBuilder)
{
	AddPass(GraphBuilder, RDG_EVENT_NAME("UHogeSubsystem::Render"), [](FRHICommandListImmediate& RHICmdList)
	{
		
	});
}
```

エディタ再生しRenderDocでキャプチャしてイベントを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157391/1c10d0b9-3408-7587-de59-f0eaecbb473a.png)
Sceneのイベント内で、RenderFinishの手前で実行されています。
この後にSlate UIのイベントが実行されてくるのでUIの描画が行われる前のタイミングです

## 追加したパスでキャプチャする
イベントの実行タイミングは意図通りのものだったので、追加したパスで現在のバックバッファをレンダーターゲットに描き込めばよさそうです

キャプチャするソースコードは、com04さんの記事を元にUE5.3に合わせてAPIの変更に対応しました。
厳密に動作検証しているわけではないので、不具合あるかもしれません。

https://github.com/zi-su/BackBufferCapture/tree/main
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157391/3666eb2b-1594-ecec-43f0-c4c2d6d52b68.gif)

# おわりに
日々エンジン更新されているので検索して出てきた情報が古かった場合には、最新ドキュメントとソースコードを確認してもっと賢い方法や楽な方法がないか調べるようにしていきたいですね。