[UE4]現在のマップ上に3Dモデルを配置せずにUMG上に3Dモデルを表示する

# 概要
![OffscreenRendering.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/4fa933fb-4705-2f14-d06c-7a233bdc2677.gif)
- UE4.25
- C++必須
- エンジン改造は無し

ゲームのUIを製作する際、UI上に3Dモデルを表示したいケースが度々あると思います。やり方を検索すると、マップ上に配置したアクターをSceneCaptureComponent2Dで撮影してテクスチャに書き出し、それをUMGに表示するやり方がヒットします。
( https://engineunreal.wordpress.com/2015/04/21/render-3d-objects-in-umg-widget-hud/ など )

ゲームプレイ画面には表示したくないものを現在のマップ上に配置しなければならないのは少し気持ち悪いです。ゲームプレイ画面を撮影しているカメラには映らないようなフラグがあればよいのですが、そういったシステムもありません。

そこでこの記事では、別のマップに配置した3Dモデルアクターを撮影してUMGに表示するということをやってみました。

以下、上記URLに記載されている手法は理解していることを前提とします。また細かい設定に関する言及は省いているので、サンプルプロジェクトを見ながら追っていただくのが良いかもしれません。

# サンプルプロジェクト
https://github.com/AegisShingo/UE4_OffscreenRendering

# 方針
3DモデルアクターをSceneCaptureComponent2Dで撮影してRenderTargetTextureに書き出し、それをUMGに表示するという仕組みは他のやり方と同様です。

UMGに表示したい3Dモデルアクターを配置した撮影用マップ作成し、それをPrimaryAssetとしてロードします。ロードが完了したらマップ上のアクターに対して直接Tickなどを呼び出し、RenderTargetTextureを更新します。

# 具体的な手順
## 撮影用マップ更新クラスの作成
まず、撮影用マップをロードして直接更新を行うアクタークラスをC++で作成します。

BeginPlayで対象のマップを非同期ロードし、ロード完了時にマップをGamePreviewタイプのワールドとして初期化します。パッケージ時とPIE時でロードしたマップが初期化済みかどうかが変わるので、フラグをみて初期化されていなかったらInitWorld()します。

Tickで全アクターと全コンポネントに対して直接Tickを呼び出します。またワールドのキャプチャ関連の更新関数を呼び出します。正直ここの実装が安全かどうか自信がないです。

EndPlayで諸々の後片付けをします。

```c++:PreviewWorldUpdater.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PreviewWorldUpdater.generated.h"

UCLASS()
class OFFSCREENRENDERING_API APreviewWorldUpdater : public AActor
{
	GENERATED_BODY()
	
public:	
	APreviewWorldUpdater();

protected:
	virtual void BeginPlay() override;

	// ロード完了コールバック
	void HandleLoadCompleted();

	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

public:	
	virtual void Tick(float DeltaTime) override;

protected:
	// プレビューするマップのアセットID
	UPROPERTY(EditAnywhere)
		FPrimaryAssetId PreviewWorldId;

	// プレビューするマップ
	UPROPERTY()
		UWorld* PreviewWorld;

	// 非同期ロードハンドル
	TSharedPtr<struct FStreamableHandle> LoadHandle;
};
```

```c++:PreviewWorldUpdater.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "PreviewWorldUpdater.h"
#include "Engine/AssetManager.h"
#include "Engine/StreamableManager.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Components/SkyLightComponent.h"
#include "Components/ReflectionCaptureComponent.h"

APreviewWorldUpdater::APreviewWorldUpdater()
{
	PrimaryActorTick.bCanEverTick = true;
}

void APreviewWorldUpdater::BeginPlay()
{
	Super::BeginPlay();

	// PreviewWorldを非同期ロード
	if (PreviewWorldId.IsValid())
	{
		if (UAssetManager* AssetManager = UAssetManager::GetIfValid())
		{
			LoadHandle = AssetManager->LoadPrimaryAsset(PreviewWorldId);

			if (LoadHandle.IsValid())
			{
				if (!LoadHandle->HasLoadCompleted())
				{
					LoadHandle->BindCompleteDelegate(FStreamableDelegate::CreateUObject(this, &ThisClass::HandleLoadCompleted));
				}
				else
				{
					HandleLoadCompleted();
				}
			}
		}
	}
}

void APreviewWorldUpdater::HandleLoadCompleted()
{
	// ロードしたPreviewWorldを受け取る
	UObject* AssetLoaded = LoadHandle->GetLoadedAsset();
	PreviewWorld = Cast<UWorld>(AssetLoaded);
	LoadHandle.Reset();

	// PreviewWorldを初期化
	if (IsValid(PreviewWorld))
	{
		PreviewWorld->WorldType = EWorldType::GamePreview;
		PreviewWorld->SetGameInstance(GetGameInstance());

		FWorldContext& WorldContext = GEngine->CreateNewWorldContext(PreviewWorld->WorldType);
		WorldContext.SetCurrentWorld(PreviewWorld);
		WorldContext.OwningGameInstance = GetGameInstance();

		if (!PreviewWorld->bIsWorldInitialized)
		{
			PreviewWorld->InitWorld(UWorld::InitializationValues()
				.AllowAudioPlayback(false)
				.RequiresHitProxies(false)
				.CreatePhysicsScene(false)
				.CreateNavigation(false)
				.CreateAISystem(false)
				.ShouldSimulatePhysics(false)
				.SetTransactional(false)
				.CreateFXSystem(false));
		}

		FURL URL;
		PreviewWorld->InitializeActorsForPlay(URL);
		PreviewWorld->BeginPlay();
	}
}

void APreviewWorldUpdater::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);

	// PreviewWorldの後処理
	if (IsValid(PreviewWorld))
	{
		// 最後に念のため遅延シーンキャプチャの更新を呼んで、内部のリストをクリアする
		USceneCaptureComponent::UpdateDeferredCaptures(PreviewWorld->Scene);

		GEngine->DestroyWorldContext(PreviewWorld);
		PreviewWorld->DestroyWorld(false);
		PreviewWorld = nullptr;
	}
}

void APreviewWorldUpdater::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// PreviewWorldのTickを呼ぶとcheckに引っかかるので、仕方なくアクターのTickをそれぞれ呼び出すことにする
	if (IsValid(PreviewWorld))
	{
		for (FConstLevelIterator LevelItr = PreviewWorld->GetLevelIterator(); LevelItr; ++LevelItr)
		{
			for (AActor* Actor : (*LevelItr)->Actors)
			{
				if (IsValid(Actor))
				{
					if (Actor->PrimaryActorTick.bCanEverTick)
					{
						Actor->TickActor(DeltaTime, ELevelTick::LEVELTICK_All, Actor->PrimaryActorTick);
					}
					for (UActorComponent* Component : Actor->GetComponents())
					{
						if (IsValid(Component) && Component->PrimaryComponentTick.bCanEverTick)
						{
							Component->TickComponent(DeltaTime, ELevelTick::LEVELTICK_All, &Component->PrimaryComponentTick);
						}
					}
				}
			}
		}

		// キャプチャの更新
		USkyLightComponent::UpdateSkyCaptureContents(PreviewWorld);
		UReflectionCaptureComponent::UpdateReflectionCaptureContents(PreviewWorld);
		USceneCaptureComponent::UpdateDeferredCaptures(PreviewWorld->Scene);
	}
}
```

## 撮影用マップの作成
つぎに撮影用のマップを作成します。ここでは名前をPreviewWorldとしています。デフォルトのPrimaryAsset検索パスに入るように、/Game/Maps/以下に配置します。それ以外のパスに配置する際はプロジェクト設定でPrimaryAsset検索パスを追加しましょう。

平行光源、SceneCapture2Dアクター、アンリアルおじさんだけとりあえず置いておきます。アンリアルおじさんのアニメーションにはThirdPersonWalkを指定しておきます。
![2020-06-03_17h15_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/9cabafa0-53e5-301a-b371-cbb8fca45404.png)

適当な位置に作成したRenderTargetをSceneCapture2Dに指定します。SceneCapture2d位置や画角などは好みに設定しましょう。
![2020-06-03_17h18_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/d6b6a12f-c4be-3c32-65dd-2d9a48ae2299.png)

## 表示用ウィジェットの作成
まずレンダーターゲットをUMGで表示するためのマテリアルを作成します。SceneCaptureComponent2Dの設定によりRenderTargetには反転されたアルファが入っているので、OneMinusノードで反転しています。
![2020-06-03_17h24_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/af5cc3b5-3d88-1297-c86a-2be3a7081631.png)

ウィジェットを作成します。とりあえずBorderとImageを重ねて置いておきます。ImageのBrushに先ほど作成したマテリアルを指定します。
![2020-06-03_17h26_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/b1dc13c1-448e-97c2-57cf-7def8bbc49da.png)

## 動作確認用マップの作成
最後に動作確認用マップを作成して動作を確認します。ここでは新規にTimeOfDayのマップを作成してDefaultMapという名前で保存しました。
![2020-06-03_17h44_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/8f0f9134-4478-088a-c1fc-2e97392f6114.png)

C++クラスのPreviewWorldUpdaterをドラッグ＆ドロップで配置します。PreviewWorldIdには撮影用マップを指定してください。
![2020-06-03_17h45_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/39520a10-dc27-acf8-9221-f32ca88d1304.png)

レベルブループリントのBeginPlayでウィジェットを作成します。
![2020-06-03_17h47_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/d5482bf4-a1e7-b096-1988-ccddb7664218.png)

# 動作確認
DefaultMapでプレイすると、ウィジェット上のアンリアルおじさんが歩いていることを確認できます。
![OffscreenRendering.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593615/4fa933fb-4705-2f14-d06c-7a233bdc2677.gif)

# あとがき
UMG上に3Dモデルを表示するということを、現在のマップの3Dモデルを配置しないで実現しました。

InitWorldの引数はひとまず片っ端からfalseにしてみましたが、必要に応じて変更する必要があるかと思います。別マップ更新の際にアクターやコンポネントのTickを呼び出していますが、本当はTickグループに応じて適切なタイミングでTickするべきです。キャプチャ関係の更新関数も呼び出していますが、タイミングに問題がないかどうか自身が持てません。また、行いたい表現によっては他の更新関数も呼び出す必要があるかと思います。

本当はEngineクラスを独自のものに切り替えて、それぞれ適切なタイミングで実行されるようにするべきなのだろうと思います。今回は簡易的に行うために一つのアクターで更新を完結させてしまいました。

ちょっと無理やり対応した感じがするので、リスクを考えると製品で採用するかは難しいなというのが個人的な所感です。自分の備忘もかねて記事とさせていただきました。より良い方法をご存じの方や思いついた方は教えていただけるととてもうれしいです。