はじめに
※UE5での対応については Zi_su さんが記事を書かれています。
下記記事を参照してください
UE5 デリゲートを使ったフレームキャプチャをする方法 - Qiita/Zi_su
- 本記事に関して
- UE4.23.1にて検証
- ※要エンジン改造
- DeferredShading + PC(Windows)環境でテストしました
- 手元の環境でのみ動作確認しております。他プラットフォームや環境で動作しなければごめんなさい
- 何か既にUE4内に機能あったらごめんなさい
こんな感じのをやります。画面左が実際のシーン、右がフレームをキャプチャーしたテクスチャ: [UE4]FrameCapture.描画している3Dシーンのテクスチャをコピーして使用する https://t.co/ffvtb0rfWN
— com04 (@com04) December 1, 2019
UE4にはFFrameGrabberというレンダリングしたシーンをコピーして持ってくるC++機能が有ります
- Frame Grabber機能を使って、レンダリング結果(フレームバッファ)をテクスチャ化する方法について - ぼっちプログラマのメモ
- 【UE4】FrameGrabberを用いたキャプチャの実装方法を読み解いてみる - N煎ログブログ
ただ幾つか問題が考えられます。
- HUDが乗ってしまう
- レンダリングした結果をCPU側にコピーしている
- →テクスチャとして使用する際にはGPUに書き戻す必要がある
- →速度的な問題
- →テクスチャとして使用する際にはGPUに書き戻す必要がある
また、シーンをテクスチャ化する他の方法として SceneCaptureComponent2D で同じカメラ設定にしてキャプチャーする手もあります。
こちらの場合はHUDは乗らないように出来ますが、
- シーン全体を再度レンダリングする負荷がかかる
- Exposure/EyeAdaptationが合わない可能性がある
- 1フレームだとTAAのバッファが足りない
等々こちらも問題があります。
という事でなんとか今レンダリングした結果をコピーして使用します。
なんとかした結果だけ欲しい方は→「さいごに」項を御覧ください
アプローチ
HUDが乗ってしまう
FFrameGrabberは FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent()
のDelegateで描画イベントをキャッチしています。
これはSlateの描画が終わったタイミングです。(RenderThread)
これと同じ様に、3Dシーンの描画が終わったタイミングにDelegateを追加します。
レンダリングした結果をCPU側にコピーしている
FFrameGrabber は数フレーム分キャッシュしており、そのバッファを使えるようになっています。
(あと多分RenderThreadとGameThreadの取り扱いの為?)
ただ今回は1フレーム分だけ使う事前提にGPUでコピーします。
実装
エンジンを拡張する
1. プロジェクト側からDelegateを介して処理を挟めるようにします
インターフェイスの IRendererModule
に定義を追加します(引数は後ほど)
/** 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変数と定義を実装します
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描画範囲になります(多分)
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へ関数をつなげます。
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
の部分
UCLASS(hidecategories=(Collision, Object, Physics, SceneComponent), ClassGroup=Rendering, editinlinenew, meta=(BlueprintSpawnableComponent))
class CUSTOM4_23_API UFrameCaptureComponent : public USceneComponent
{
プロジェクト側BP
1. SceneCaptureComponent2D
と同じようにコンポーネントとして追加、TextureTarget
を指定すれば使えます
2. デフォルトは常時フレームキャプチャーしていますが、CaptureEveryFrame
をOFFにして CaptureScene
ノードを使用すれば1フレームのみキャプチャー出来ます
3. 色味が白くなってる時はsRGBの対応が必要ですので適時Pow 2.2
等してください
また、RenderTargetアセットを使用する場合は RTF_RGBA16f
フォーマットが色味合うと思います。
さいごに
GitHubに纏めました。
https://github.com/com04/UnrealSandBox/tree/master/FrameCapture
エンジンそのままは規模が大きいので.patchと、プロジェクトコードだけ上げています。
RenderThreadとGameThreadの排他制御は適当かもしれません。詳しい人教えて下さい。