search
LoginSignup
5

More than 3 years have passed since last update.

posted at

updated at

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

はじめに

  • 本記事に関して
    • 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

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
What you can do with signing up
5