[UE5]改良したUIの上にActorを描画する独自機能(アニメーションできる)

に公開

https://zenn.dev/yoo0n0/articles/97aae0b7a816ad
以前の記事の改良版です。
アニメーション対応しました。

コード以外は変わりません。
なのでコードだけコピペしてコンパイル通せばすぐにアニメーション対応します。

コード

.h

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "StencilWidgetManagerComponent.generated.h"

class UUserWidget;
class UTextureRenderTarget2D;
class UMaterialInterface;
class UMaterialInstanceDynamic;
class FWidgetRenderer;

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class VRM_CAPTURESTUDIO_API UStencilWidgetManagerComponent : public UActorComponent
{
	GENERATED_BODY()
public:
	UStencilWidgetManagerComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
public:

	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
	UFUNCTION(BlueprintPure, Category = "Special UI")
	UUserWidget* GetManagedWidget() const
	{
		return WidgetInstance;
	}

	// 表示/非表示の切り替え
	UFUNCTION(BlueprintCallable, Category = "Special UI")
	void SetUIVisible(bool bInVisible);

	// フェード用の不透明度(0〜1)を設定
	UFUNCTION(BlueprintCallable, Category = "Special UI")
	void SetUIOpacity(float InOpacity);

	UFUNCTION(BlueprintCallable, BlueprintPure)
	UTextureRenderTarget2D* GetUIRenderTarget() const { return UIRenderTarget; }

	// 描画したいWidgetのクラス
	UPROPERTY(EditAnywhere, Category = "SpecialUI")
	TSubclassOf<UUserWidget> WidgetClass;

	// 出力先RenderTarget
	UPROPERTY(EditAnywhere, Category = "SpecialUI")
	UTextureRenderTarget2D* UIRenderTarget;

	// PostProcess用のベースマテリアル
	UPROPERTY(EditAnywhere, Category = "SpecialUI")
	UMaterialInterface* PostProcessMaterial;

	UPROPERTY(EditAnywhere, Category = "SpecialUI")
	FVector2D DesignResolution = FVector2D(1920.0f, 1080.0f);

	UPROPERTY(EditAnywhere, Category = "SpecialUI", meta = (ClampMin = "1.0", UIMin = "1.0"))
	float SuperSampleScale = 1.0f;    // x倍でレンダリング


	// 動画/WindowCaptureなど毎フレーム内容が変わるUIの場合はtrue
	UPROPERTY(EditAnywhere, Category = "SpecialUI")
	bool bAlwaysRedraw = true;

	// WindowCaptureUMGなど「Viewport上に存在してTickされないと更新されない」UI向け:
	// RT描画しつつ、見えない形でViewportに保持してTickだけ回す
	UPROPERTY(EditAnywhere, Category = "SpecialUI")
	bool bKeepWidgetInViewportForTick = true;

	// Viewport保持時の見え方(0だと内部で更新が止まる実装もあるので必要なら0.01などに)
	UPROPERTY(EditAnywhere, Category = "SpecialUI", meta = (ClampMin = "0.0", ClampMax = "1.0"))
	float ViewportHoldOpacity = 0.0f;

	// Viewport保持時は画面外に飛ばして二重表示,前面表示を確実に防ぐ
	UPROPERTY(EditAnywhere, Category = "SpecialUI")
	FVector2D ViewportHoldOffscreenPos = FVector2D(-100000.0f, -100000.0f);

	// RenderTargetへの描画を要求する
	UFUNCTION(BlueprintCallable, Category = "Special UI")
	void MarkDirty();

private:
	UPROPERTY()
	UUserWidget* WidgetInstance;

	UPROPERTY()
	UMaterialInstanceDynamic* PostProcessMID;

	// UIの表示状態
	bool bUIVisible = true;
	// フェード用の現在不透明度
	float UIOpacity = 1.0f;
	// Dirty=true のときだけRTに描画(bAlwaysRedraw=true の場合は毎フレーム描画)
	bool bDirty = true;

	// UMG → RenderTarget 用
	FWidgetRenderer* WidgetRenderer = nullptr;
	void UpdateUIRenderTarget(float DeltaTime);
	void SetupPostProcessOnCamera();
	void RecreateWidget();   // 内部用ヘルパー	
};

.cpp

#include "StencilWidgetManagerComponent.h"
#include "Blueprint/UserWidget.h"
#include "Blueprint/WidgetBlueprintLibrary.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/Actor.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Slate/WidgetRenderer.h"
//

UStencilWidgetManagerComponent::UStencilWidgetManagerComponent()
{
    PrimaryComponentTick.bCanEverTick = true;
}


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

    // 動く系はViewport上に存在してTickされないと更新されないことが多いので、
    // 必要ならViewportに見えない形で保持しつつRTに描画する。
    if (bKeepWidgetInViewportForTick)
    {
        RecreateWidget();
    }
    else
    {
        // 画面には出さずRT描画専用として生成
        if (WidgetClass)
        {
            if (UWorld* World = GetWorld())
            {
                if (APlayerController* PC = World->GetFirstPlayerController())
                {
                    WidgetInstance = CreateWidget<UUserWidget>(PC, WidgetClass);
                }
            }
        }
    }

    // FWidgetRendererを作成(UMG → RenderTarget)
    if (!WidgetRenderer)
    {
        WidgetRenderer = new FWidgetRenderer(true, true);
    }

    // RenderTargetの解像度設定
    if (UIRenderTarget)
    {
        const int32 TargetX = FMath::RoundToInt(DesignResolution.X * SuperSampleScale);
        const int32 TargetY = FMath::RoundToInt(DesignResolution.Y * SuperSampleScale);

        if (UIRenderTarget->SizeX != TargetX || UIRenderTarget->SizeY != TargetY)
        {
            UIRenderTarget->ResizeTarget(TargetX, TargetY);
        }

        // 黒問題の本質ではないので、まずはデフォルト寄り(必要なら後でtrueに)
        UIRenderTarget->bForceLinearGamma = false;
        UIRenderTarget->UpdateResourceImmediate();
    }

    // PostProcess用MIDを作成してカメラに登録
    if (PostProcessMaterial)
    {
        PostProcessMID = UMaterialInstanceDynamic::Create(PostProcessMaterial, this);
        SetupPostProcessOnCamera();
    }
}

void UStencilWidgetManagerComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    // まずTick停止
    SetComponentTickEnabled(false);

    // Viewport保持していたWidgetを除去
    if (WidgetInstance)
    {
        if (WidgetInstance->IsInViewport())
        {
            WidgetInstance->RemoveFromParent();
        }
        WidgetInstance = nullptr;
    }

    // PostProcessからMIDを外す
    if (PostProcessMID)
    {
        if (AActor* Owner = GetOwner())
        {
            if (UCameraComponent* Camera = Owner->FindComponentByClass<UCameraComponent>())
            {
                Camera->PostProcessSettings.RemoveBlendable(PostProcessMID);
            }
        }
        PostProcessMID = nullptr;
    }

    // WidgetRendererを破棄
    if (WidgetRenderer)
    {
        delete WidgetRenderer;
        WidgetRenderer = nullptr;
    }

    Super::EndPlay(EndPlayReason);
}


void UStencilWidgetManagerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    // 静的UIはDirty時だけ動く系は毎フレーム
    if (!bAlwaysRedraw && !bDirty)
    {
        return;
    }

    UWorld* World = GetWorld();
    if (!World || World->bIsTearingDown)
    {
        return;
    }
    if (!IsValid(WidgetInstance) || !IsValid(UIRenderTarget) || !WidgetRenderer)
    {
        return;
    }

    UpdateUIRenderTarget(DeltaTime);
    bDirty = false;
}

void UStencilWidgetManagerComponent::SetUIVisible(bool bInVisible)
{
    bUIVisible = bInVisible;
    MarkDirty();
}

void UStencilWidgetManagerComponent::SetUIOpacity(float InOpacity)
{
    UIOpacity = FMath::Clamp(InOpacity, 0.0f, 1.0f);

    // WidgetInstanceをViewportに保持している場合、ここ SetRenderOpacity()を触ると
    // Viewportに出てしまう/Stencilより前面に来る原因になる。
    // 実際の不透明度はUpdateUIRenderTarget内でRT描画時だけ適用。
    MarkDirty();
}

void UStencilWidgetManagerComponent::UpdateUIRenderTarget(float DeltaTime)
{
    UWorld* World = GetWorld();
    if (!World || World->bIsTearingDown)
    {
        return;
    }
    if (!IsValid(WidgetInstance) || !IsValid(UIRenderTarget) || !WidgetRenderer)
    {
        return;
    }

    // Viewport保持している場合は、確実に画面外へ(SetUIOpacity等の操作でも二重表示を防ぐ)
    if (bKeepWidgetInViewportForTick && WidgetInstance->IsInViewport())
    {
        WidgetInstance->SetPositionInViewport(ViewportHoldOffscreenPos, false);
    }

    // RTに描くときだけ見せたい不透明度を一時適用
    const float OldOpacity = WidgetInstance->GetRenderOpacity();
    const float EffectiveOpacity = (bUIVisible ? UIOpacity : 0.0f);
    WidgetInstance->SetRenderOpacity(EffectiveOpacity);

    // レイアウト確定(動的Brush更新やサイズ計算遅れを抑制)
    WidgetInstance->ForceLayoutPrepass();

    const FVector2D DrawSize(UIRenderTarget->SizeX, UIRenderTarget->SizeY);
    TSharedRef<SWidget> SlateWidget = WidgetInstance->TakeWidget();

    // bDeferRenderはfalse推奨(初回黒/更新遅れ対策)
    WidgetRenderer->DrawWidget(
        UIRenderTarget,
        SlateWidget,
        1.0f,
        DrawSize,
        DeltaTime,
        false
    );

    if (PostProcessMID)
    {
        PostProcessMID->SetTextureParameterValue(TEXT("UITexture"), UIRenderTarget);
    }

    // Viewport保持用の不透明度に戻す(表示させない)
    WidgetInstance->SetRenderOpacity(bKeepWidgetInViewportForTick ? ViewportHoldOpacity : OldOpacity);
}

void UStencilWidgetManagerComponent::SetupPostProcessOnCamera()
{
    if (!PostProcessMID)
    {
        return;
    }

    if (AActor* Owner = GetOwner())
    {
        if (UCameraComponent* Camera = Owner->FindComponentByClass<UCameraComponent>())
        {
            Camera->PostProcessSettings.AddBlendable(PostProcessMID, 1.0f);
        }
    }
}

void UStencilWidgetManagerComponent::MarkDirty()
{
    bDirty = true;
}

void UStencilWidgetManagerComponent::RecreateWidget()
{
    // 既存を破棄
    if (WidgetInstance)
    {
        if (WidgetInstance->IsInViewport())
        {
            WidgetInstance->RemoveFromParent();
        }
        WidgetInstance = nullptr;
    }

    if (!WidgetClass)
    {
        return;
    }

    if (UWorld* World = GetWorld())
    {
        if (APlayerController* PC = World->GetFirstPlayerController())
        {
            WidgetInstance = CreateWidget<UUserWidget>(PC, WidgetClass);

            if (WidgetInstance && bKeepWidgetInViewportForTick)
            {
                // Tick確保のためにViewportへ。ただし見えないようにする
                WidgetInstance->AddToViewport(0);

                // 画面外へ移動して二重表示,Stencil前面表示を確実に防ぐ
                WidgetInstance->SetPositionInViewport(ViewportHoldOffscreenPos, false);

                // 保持用の透明度を適用(必要なら0.01などに)
                WidgetInstance->SetRenderOpacity(ViewportHoldOpacity);

                // VisibilityはVisibleのまま(Hidden/Collapsedだと更新が止まる実装がある)
                WidgetInstance->SetVisibility(ESlateVisibility::HitTestInvisible);
            }
        }
    }

    MarkDirty();
}

何が変わったの?

めっっっっっちゃ力技です。
Viewportに置いたWidgetを画面外へ移動させています。
存在するけど見えない状態です。
また、SetUIOpacity()がRenderOpacityを触らなくなりました。
ここが原因で、二重表示問題が生じたので修正しました。

おしまい

結果これで改善されました!(Niagara除く)
やっぱり力。力はすべてを解決する。。。。

Discussion