✨
[UE5]改良したUIの上にActorを描画する独自機能(アニメーションできる)
以前の記事の改良版です。
アニメーション対応しました。
コード以外は変わりません。
なのでコードだけコピペしてコンパイル通せばすぐにアニメーション対応します。
コード
.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