LoginSignup
6
8

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-03

概要

OffscreenRendering.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に記載されている手法は理解していることを前提とします。また細かい設定に関する言及は省いているので、サンプルプロジェクトを見ながら追っていただくのが良いかもしれません。

サンプルプロジェクト

方針

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

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

具体的な手順

撮影用マップ更新クラスの作成

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

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

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

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

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

適当な位置に作成したRenderTargetをSceneCapture2Dに指定します。SceneCapture2d位置や画角などは好みに設定しましょう。
2020-06-03_17h18_00.png

表示用ウィジェットの作成

まずレンダーターゲットをUMGで表示するためのマテリアルを作成します。SceneCaptureComponent2Dの設定によりRenderTargetには反転されたアルファが入っているので、OneMinusノードで反転しています。
2020-06-03_17h24_21.png

ウィジェットを作成します。とりあえずBorderとImageを重ねて置いておきます。ImageのBrushに先ほど作成したマテリアルを指定します。
2020-06-03_17h26_44.png

動作確認用マップの作成

最後に動作確認用マップを作成して動作を確認します。ここでは新規にTimeOfDayのマップを作成してDefaultMapという名前で保存しました。
2020-06-03_17h44_33.png

C++クラスのPreviewWorldUpdaterをドラッグ&ドロップで配置します。PreviewWorldIdには撮影用マップを指定してください。
2020-06-03_17h45_46.png

レベルブループリントのBeginPlayでウィジェットを作成します。
2020-06-03_17h47_39.png

動作確認

DefaultMapでプレイすると、ウィジェット上のアンリアルおじさんが歩いていることを確認できます。
OffscreenRendering.gif

あとがき

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

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

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

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

6
8
0

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
6
8