[UE4]描画している3Dシーンのテクスチャをコピーして使用する

# はじめに
<font color="OrangeRed">**※UE5での対応については Zi_su さんが記事を書かれています。
下記記事を参照してください**</font>
[UE5 デリゲートを使ったフレームキャプチャをする方法](https://qiita.com/Zi_su/items/50d79c575c19b206976c) - Qiita/Zi_su


- 本記事に関して
    - UE4.23.1にて検証  
    - <font color="OrangeRed">※要エンジン改造</font>
    - DeferredShading + PC(Windows)環境でテストしました
        - 手元の環境でのみ動作確認しております。他プラットフォームや環境で動作しなければごめんなさい
    - 何か既にUE4内に機能あったらごめんなさい

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">画面左が実際のシーン、右がフレームをキャプチャーしたテクスチャ: [UE4]FrameCapture.描画している3Dシーンのテクスチャをコピーして使用する <a href="https://t.co/ffvtb0rfWN">https://t.co/ffvtb0rfWN</a></p>&mdash; com04 (@com04) <a href="https://twitter.com/com04/status/1201174011754901505?ref_src=twsrc%5Etfw">December 1, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
こんな感じのをやります。

UE4には[FFrameGrabber](https://docs.unrealengine.com/en-US/API/Runtime/MovieSceneCapture/FFrameGrabber/index.html)というレンダリングしたシーンをコピーして持ってくるC++機能が有ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58518/393cc45a-871e-a9aa-36c5-bd3de62a907f.png)

- Frame Grabber機能を使って、レンダリング結果（フレームバッファ）をテクスチャ化する方法について - ぼっちプログラマのメモ  
    - [http://pafuhana1213.hatenablog.com/entry/2018/08/27/001512](http://pafuhana1213.hatenablog.com/entry/2018/08/27/001512)
- 【UE4】FrameGrabberを用いたキャプチャの実装方法を読み解いてみる - N煎ログブログ  
    - http://isemito.hatenablog.com/entry/2018/12/09/192026


ただ幾つか問題が考えられます。

- HUDが乗ってしまう
- レンダリングした結果をCPU側にコピーしている  
    - →テクスチャとして使用する際にはGPUに書き戻す必要がある
        - →速度的な問題


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58518/3168e12c-0b9a-fef8-45b7-ad5b22b9eecf.png)
また、シーンをテクスチャ化する他の方法として [SceneCaptureComponent2D](https://docs.unrealengine.com/en-US/Resources/ContentExamples/Reflections/1_7/index.html) で同じカメラ設定にしてキャプチャーする手もあります。
こちらの場合はHUDは乗らないように出来ますが、

- シーン全体を再度レンダリングする負荷がかかる
- Exposure/EyeAdaptationが合わない可能性がある
- 1フレームだとTAAのバッファが足りない

等々こちらも問題があります。

という事でなんとか今レンダリングした結果をコピーして使用します。
なんとかした結果だけ欲しい方は→「さいごに」項を御覧ください

# アプローチ

## HUDが乗ってしまう
[FFrameGrabber](https://docs.unrealengine.com/en-US/API/Runtime/MovieSceneCapture/FFrameGrabber/index.html)は `FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent()` のDelegateで描画イベントをキャッチしています。
これはSlateの描画が終わったタイミングです。（RenderThread)
これと同じ様に、3Dシーンの描画が終わったタイミングにDelegateを追加します。

## レンダリングした結果をCPU側にコピーしている
[FFrameGrabber](https://docs.unrealengine.com/en-US/API/Runtime/MovieSceneCapture/FFrameGrabber/index.html) は数フレーム分キャッシュしており、そのバッファを使えるようになっています。
（あと多分RenderThreadとGameThreadの取り扱いの為？）
ただ今回は1フレーム分だけ使う事前提にGPUでコピーします。  

# 実装
## エンジンを拡張する

１. プロジェクト側からDelegateを介して処理を挟めるようにします 
インターフェイスの `IRendererModule` に定義を追加します（引数は後ほど）

```c++:RendererInterface.h-706行辺り
	/** Evict all data from virtual texture caches*/
	virtual void FlushVirtualTextureCache() = 0;

	// [com] ----
	DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnRender3DFinishDelegate, FRHICommandListImmediate& /*RHICmdList*/, const FTexture2DRHIRef& /*SceneTexture*/, const FIntRect& /*ViewRect*/);
	virtual FOnRender3DFinishDelegate& GetOnRender3DFinishDelegate() = 0;
	// ---- [com]
};
```
２. `FRendererModule` にDelegate変数と定義を実装します

```c++:RendererModule.h-108行辺り
	virtual void LoadPendingVirtualTextureTiles(FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel) override;
	virtual void FlushVirtualTextureCache() override;

	// [com] ----
	virtual FOnRender3DFinishDelegate& GetOnRender3DFinishDelegate() override { return OnRender3DFinishDelegate; }
	// ---- [com]
private:
	TSet<FSceneInterface*> AllocatedScenes;
	FPostOpaqueRenderDelegate PostOpaqueRenderDelegate;
	FPostOpaqueRenderDelegate OverlayRenderDelegate;
	FOnResolvedSceneColor PostResolvedSceneColorCallbacks;

	// [com] ----
	FOnRender3DFinishDelegate OnRender3DFinishDelegate;
	// ---- [com]
```

３. `FDeferredShadingSceneRenderer::Render` 内で描画終了タイミングでBroadcastします
PostProcess終了後 (`GPostProcessing.Process`) に先程のDelegateを投げます

- View.bIsGameView - これをチェックしないとエディター用のViewportまで対象になります
- View.CameraConstrainedViewRect - これが実際の3D描画範囲になります（多分）

```c++:DeferredShadingRenderer.cpp-2053行辺り
	else
	{
		// Release the original reference on the scene render targets
		SceneContext.AdjustGBufferRefCount(RHICmdList, -1);
	}

	// [com] ----
	{
		for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
		{
			FViewInfo& View = Views[ViewIndex];

			// ViewがGame設定の分だけ
			if (View.bIsGameView && View.Family && View.Family->RenderTarget)
			{
				const FTexture2DRHIRef& Target = View.Family->RenderTarget->GetRenderTargetTexture();
				if (Target)
				{
					GetRendererModule().GetOnRender3DFinishDelegate().Broadcast(RHICmdList, Target, View.CameraConstrainedViewRect);
				}
			}
		}
	}
	// ---- [com]

	for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
	{
		ShaderPrint::EndView(Views[ViewIndex]);
	}
```

これで描画終了タイミングでDelegate `OnRender3DFinishDelegate` を投げる様になりました

## プロジェクト側C++
C++で、BPからアクセス出来るComponentを作成します。
GitHubに上げていますので、そちらのコードベースになります。
https://github.com/com04/UnrealSandBox/tree/master/FrameCapture/Project

クラスのインターフェイスは [USceneCaptureComponent](https://docs.unrealengine.com/en-US/API/Runtime/Engine/Components/USceneCaptureComponent2D/index.html) を参考にしてみます。
`bCaptureEveryFrame` 変数で常時キャプチャー、`CaptureScene` で1フレームキャプチャーになっています。

１. DelegateへのBindingします。`UFrameCaptureComponent::SetBinding`
エンジン拡張したDelegateへ関数をつなげます。

```c++:FrameCaptureComponent.cpp
IRendererModule* RendererModule = &FModuleManager::GetModuleChecked<IRendererModule>(TEXT("Renderer"));
OnRender3DFinishHandle = RendererModule->GetOnRender3DFinishDelegate().AddUObject(this, &UFrameCaptureComponent::OnRender3DFinish);
```
２. テクスチャのコピーをします。`UFrameCaptureComponent::OnRender3DFinish`
`FrameGrabber.cpp` - `FViewportSurfaceReader::ResolveRenderTarget` を参考しています。（ほぼそのまま）

３. プロジェクトの `*.Build.cs` に依存モジュールを追加します。
Renderer, RenderCore, RHI辺りが必要です。

```cs
		PrivateDependencyModuleNames.AddRange(new string[] {
			"Renderer",
			"RenderCore",
			"RHI",
		});
```

４. 自身のプロジェクトに組み込む際は、`FrameCaptureComponent.h` のAPI指定は書き換えてください
`CUSTOM4_23_API` の部分

```c++:FrameCaptureComponent.h
UCLASS(hidecategories=(Collision, Object, Physics, SceneComponent), ClassGroup=Rendering, editinlinenew, meta=(BlueprintSpawnableComponent))
class CUSTOM4_23_API UFrameCaptureComponent : public USceneComponent
{
```

## プロジェクト側BP
１. `SceneCaptureComponent2D` と同じようにコンポーネントとして追加、`TextureTarget` を指定すれば使えます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58518/8a7f7567-ea29-cc62-6010-c32bf04923f5.png)

２. デフォルトは常時フレームキャプチャーしていますが、`CaptureEveryFrame` をOFFにして `CaptureScene` ノードを使用すれば1フレームのみキャプチャー出来ます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58518/6f6fb3fd-6c78-2997-30d9-46bf7e27c577.png)

３. 色味が白くなってる時はsRGBの対応が必要ですので適時`Pow 2.2` 等してください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58518/4501c779-e225-3bdd-7a4f-1ed57fe2af04.png)
また、RenderTargetアセットを使用する場合は `RTF_RGBA16f` フォーマットが色味合うと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58518/299d284b-3fec-1124-7c90-a1ee8a1eb1d1.png)



# さいごに
GitHubに纏めました。
https://github.com/com04/UnrealSandBox/tree/master/FrameCapture
エンジンそのままは規模が大きいので.patchと、プロジェクトコードだけ上げています。
RenderThreadとGameThreadの排他制御は適当かもしれません。詳しい人教えて下さい。

`stat GPU` の `FrameCapture` が負荷になります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58518/47682c3c-d95c-c78e-5465-2ad0e93e6c9e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58518/d01daf83-e53c-e952-2b08-5bad3514e605.png)