LoginSignup
16
7

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

Last updated at Posted at 2019-12-01

はじめに

※UE5での対応については Zi_su さんが記事を書かれています。
下記記事を参照してください

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

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

UE4にはFFrameGrabberというレンダリングしたシーンをコピーして持ってくるC++機能が有ります
image.png

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

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

image.png
また、シーンをテクスチャ化する他の方法として SceneCaptureComponent2D で同じカメラ設定にしてキャプチャーする手もあります。
こちらの場合はHUDは乗らないように出来ますが、

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

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

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

アプローチ

HUDが乗ってしまう

FFrameGrabberFSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent() のDelegateで描画イベントをキャッチしています。
これはSlateの描画が終わったタイミングです。(RenderThread)
これと同じ様に、3Dシーンの描画が終わったタイミングにDelegateを追加します。

レンダリングした結果をCPU側にコピーしている

FFrameGrabber は数フレーム分キャッシュしており、そのバッファを使えるようになっています。
(あと多分RenderThreadとGameThreadの取り扱いの為?)
ただ今回は1フレーム分だけ使う事前提にGPUでコピーします。

実装

エンジンを拡張する

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

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]
};

2. FRendererModule にDelegate変数と定義を実装します

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]

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

  • View.bIsGameView - これをチェックしないとエディター用のViewportまで対象になります
  • View.CameraConstrainedViewRect - これが実際の3D描画範囲になります(多分)
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 を参考にしてみます。
bCaptureEveryFrame 変数で常時キャプチャー、CaptureScene で1フレームキャプチャーになっています。

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

FrameCaptureComponent.cpp
IRendererModule* RendererModule = &FModuleManager::GetModuleChecked<IRendererModule>(TEXT("Renderer"));
OnRender3DFinishHandle = RendererModule->GetOnRender3DFinishDelegate().AddUObject(this, &UFrameCaptureComponent::OnRender3DFinish);

2. テクスチャのコピーをします。UFrameCaptureComponent::OnRender3DFinish
FrameGrabber.cpp - FViewportSurfaceReader::ResolveRenderTarget を参考しています。(ほぼそのまま)

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

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

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

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

プロジェクト側BP

1. SceneCaptureComponent2D と同じようにコンポーネントとして追加、TextureTarget を指定すれば使えます
image.png

2. デフォルトは常時フレームキャプチャーしていますが、CaptureEveryFrame をOFFにして CaptureScene ノードを使用すれば1フレームのみキャプチャー出来ます
image.png

3. 色味が白くなってる時はsRGBの対応が必要ですので適時Pow 2.2 等してください
image.png
また、RenderTargetアセットを使用する場合は RTF_RGBA16f フォーマットが色味合うと思います。
image.png

さいごに

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

stat GPUFrameCapture が負荷になります
image.png
image.png

16
7
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
7