【UE4】ウィジェットのサムネイルを撮影するエディタ拡張

2022年7月13日

はじめに

UE4は基本的に各アセットに適したサムネイルレンダラーが実装されており、コンテンツブラウザを開いた時に自動でサムネイルが作成されます。
ですが、ウィジェットブループリント(以下ウィジェットBP)については作成してくれません…。

でもテクスチャや、フリップブックなどは対応しているんですよね…。

(そっちが出来ているのにドウシテ…)

一応、WidgetComponentを使ってウィジェットをレベル上に配置して撮影することも可能なんですが、それを手作業でやっていくのは骨が折れます。
また、サムネイルを撮影した後にアセットの場所を変えたり、リネームするとサムネイルが無くなってしまうという問題もあります。

というわけで、ウィジェット用のサムネイルレンダラーを作ってサムネイル撮影を自動化したいと思います。

検証環境

バージョン: 4.26.2-15973114+++UE4+Release-4.26

サムネイルレンダラー

独自のサムネイルレンダラーを作るには C++のコードが必須です。
今回はエディタ用に C++ソースを追加して UThumbnailRenderer を継承します。

(ランタイム用でも動作しますが、製品に余計なコードが入っちゃいますからね。)

C++プロジェクトで UThumbnailRenderer を検索してみると、UDefaultSizedThumbnailRenderer を継承しているものが多いです。

UDefaultSizedThumbnailRenderer は、サムネイルのデフォルトサイズのプロパティを持っているので、基本的にはこれを派生する方が良いのかな?

エディタ用コードを追加する

エディタ用のコードを追加する方法はコチラを参照してください。
[UE4] モジュールについて

今回は WidgetThumbnailRenderer.h / .cpp という名前でプロジェクトに追加してみます。

.uproject ファイルにエディタ用モジュールを宣言

{
  "FileVersion": 3,
  "EngineAssociation": "4.26",
  "Category": "",
  "Description": "",
  "Modules": [
    {
      "Name": "MyProject",
      "Type": "Runtime",
      "LoadingPhase": "Default",
      "AdditionalDependencies": [
        "UnrealEd"
      ]
    },
    {
      "Name": "MyProjectEd",
      "Type": "Editor",
      "LoadingPhase": "Default"
    }
  ]
}

ゲーム用モジュールのフォルダ構成を真似てエディタ用のコードを追加

以下のような構成になる様に該当するファイルをコピーして、各ファイルのモジュール名を変更します。

  • Source
    • MyProjectEd
      • MyProjectEd.Build.cs
      • MyProjectEd.cpp
      • MyProjectEd.h
      • WidgetThumbnailRenderer.cpp
      • WidgetThumbnailRenderer.h
    • MyProjectEditor.Target.cs

自作レンダラー(WidgetThumbnailRenderer)

※必ずしも動作を保証するものではないのでご了承ください。

WidgetThumbnailRenderer.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"

#include "ThumbnailRendering/DefaultSizedThumbnailRenderer.h"
#include "WidgetThumbnailRenderer.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECTED_API UWidgetThumbnailRenderer : public UDefaultSizedThumbnailRenderer
{
	GENERATED_BODY()
public:

	UWidgetThumbnailRenderer( const FObjectInitializer& ObjectInitializer );

	// Begin UThumbnailRenderer Object
	virtual bool CanVisualizeAsset( UObject* Object ) override;
	virtual void Draw( UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* RenderTarget, FCanvas* Canvas, bool bAdditionalViewFamily ) override;
	// End UThumbnailRenderer Object

private:

	//レイアウト用のウィジェットクラス
	TSubclassOf<UUserWidget> LayoutWidgetClass_;
};

WidgetThumbnailRenderer.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "WidgetThumbnailRenderer.h"

#include "WidgetBlueprint.h"
#include "Blueprint/UserWidget.h"
#include "Components/NamedSlot.h"
#include "Slate/WidgetRenderer.h"
#include "UObject/ConstructorHelpers.h"

UWidgetThumbnailRenderer::UWidgetThumbnailRenderer( const FObjectInitializer& ObjectInitializer )
	: Super( ObjectInitializer )
{
	//レイアウト用のウィジェットクラスを読み込む(※ConstructorHelpersはコンストラクタ内でしか使えない)
	static ConstructorHelpers::FClassFinder<UUserWidget> LayoutWidgetBPClass( TEXT( "/Game/BP/ThumbLayoutWidget" ) );
	if ( LayoutWidgetBPClass.Class )
	{
		LayoutWidgetClass_ = LayoutWidgetBPClass.Class;
	}
}

bool UWidgetThumbnailRenderer::CanVisualizeAsset( UObject* Object )
{
	//念のため、UUserWidget だけ扱う様にしてみる
	UWidgetBlueprint* Blueprint = Cast<UWidgetBlueprint>( Object );
	if ( Blueprint && Blueprint->GeneratedClass && Blueprint->GeneratedClass->IsChildOf( UUserWidget::StaticClass() ) )
	{
		return true;
	}
	return false;
}

void UWidgetThumbnailRenderer::Draw( UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* RenderTarget, FCanvas* Canvas, bool bAdditionalViewFamily )
{
	//ウィジェットBPからウィジェットを生成する
	UWidgetBlueprint* Blueprint = Cast<UWidgetBlueprint>( Object );
	if ( Blueprint && Blueprint->GeneratedClass && Blueprint->GeneratedClass->IsChildOf( UUserWidget::StaticClass() ) )
	{
		//エディタ用ワールドを取得 ※UNREALED_APIが必要
		UWorld* EditorWorld = GEditor->GetEditorWorldContext().World();

		//レイアウト用ウィジェットの生成
		UUserWidget* LayoutWidget = CreateWidget<UUserWidget>( EditorWorld, TSubclassOf<UUserWidget>( LayoutWidgetClass_ ) );
		if ( IsValid( LayoutWidget ) == false )
		{
			return;
		}

		//レイアウト用ウィジェットの構築(サムネイルに書き込むためにも必要)
		const TSharedRef<SWidget> LayoutWidgetRef = LayoutWidget->TakeWidget();

		//念のため中央に配置する
		LayoutWidget->SetAnchorsInViewport( FAnchors( 0.5f ) );
		LayoutWidget->SetAlignmentInViewport( FVector2D( 0.5f, 0.5f ) );

		//撮影するウィジェットを生成
		UUserWidget* UserWidget = CreateWidget( EditorWorld, TSubclassOf<UUserWidget>( Blueprint->GeneratedClass ) );
		if ( IsValid( UserWidget ) == false )
		{
			return;
		}

		//レイアウト用ウィジェットに撮影したいウィジェットを追加する
		UNamedSlot* NamedSlot = Cast<UNamedSlot>( LayoutWidget->GetWidgetFromName( TEXT( "ThumbTarget" ) ) );
		if ( IsValid( NamedSlot ) == false )
		{
			return;
		}
		NamedSlot->ClearChildren();
		NamedSlot->AddChild( UserWidget );

		//プログラムで非表示にしている可能性があるので変更する(AddChild後の方が安心?)
		UserWidget->SetVisibility( ESlateVisibility::Visible );
		UserWidget->SetRenderOpacity( 1.0f );

		//ウィジェットを描画するコマンド
		FWidgetRenderer* WidgetRenderer = new FWidgetRenderer( true, true );
		check( WidgetRenderer );
		WidgetRenderer->DrawWidget( RenderTarget, LayoutWidgetRef, FVector2D( Width, Height ), FApp::GetDeltaTime() );

		//描画コマンドをフラッシュ(実行する) ※RENDERCORE_APIが必要
		FlushRenderingCommands( false );
		BeginCleanup( WidgetRenderer );
	}
}

コンストラクタ

ここではサムネイル用にレイアウトを調整するためのウィジェットクラスを読み込んでいます。
これは様々な構造のウィジェットを一定のサイズに収めるために用意しました。

アセットのパスはハードコーディングになるので注意が必要です。
パスはエディタ上で右クリック / リファレンスをコピーでクリップボードに保存できます。

ですが、以下のような文字列になっており、そのままだとパスとして使えないので型名の部分と拡張子の部分は除外する必要があります…。

WidgetBlueprint’/Game/BP/ThumbLayoutWidget.ThumbLayoutWidget’

/Game/BP/ThumbLayoutWidget

CanVisualizeAsset

このアセットがビジュアル化できるか?(描画可能か?)を返すメソッドです。
今回は念のため UUserWidget 派生かどうかチェックしています。

なお、このメソッドで trueを返すアセットは、右クリックのコンテキストメニュー / アセットアクション にサムネイルのキャプチャが表示されないみたいです。

通常はサムネイルのキャプチャがある
ビジュアル化を許可するとキャプチャが無くなる

おそらく、ビジュアル化できるものは自動でサムネイルが作られるはずなので、キャプチャする必要が無いという考えなのかなと思います。

Draw

これがカスタマイズのメインになる描画処理です。
コンテンツブラウザ内にサムネイルを表示している間は繰り返し呼ばれます。

このメソッド内でやっていることは以下の通りです。

  1. レイアウト用のウィジェットと、撮影したいウィジェットを生成しています。
  2. 撮影したいウィジェットを、レイアウト用ウィジェットのNamedSlotに追加します。
  3. 構築されたウィジェットをFWidgetRendererを使ってレンダーターゲットに描き込みます。

依存するモジュール名をMyProjectEd.Build.csに追記

このままコンパイルしても不明なシンボルがあるとされて失敗します。
これは UWidgetThumbnailRenderer クラス内でほかのモジュールに存在するシンボル(関数)を参照している為です。

これを解決するには、参照したいモジュール名を [エディタ用モジュール].Build.cs 追記します。

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class MyProjectEd: ModuleRules
{
	public MyProjectEd(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

        // WidgetThumbnailRenderer にリンクするモジュール
        PrivateDependencyModuleNames.AddRange(
            new string[]
            {
                //UMG関連
                "Slate",
                "SlateCore",
                "UMG",
                "UMGEditor",

                //エディタモジュール(UNREALED_API)
                "UnrealEd",

                //描画モジュール(RENDERCORE_API)
                "RenderCore",
            }
		);
	}
}

これでコンパイルしましょう。

レイアウト用のウィジェットについて

先にコードについて解説しましたが、レイアウト用のウィジェットはこんな感じです。

NamedWidget(ThumbTarget)にウィジェットを追加する

撮影したいウィジェットの比率を維持しながらサムネイルのサイズに収まる様にする構造です。
撮影したいウィジェットは NamedSlot に追加します。

サムネイルレンダラーをエディタに登録する

サムネイルレンダラーのコードをエディタに登録する必要があるのでiniファイルを編集します。

記述方法はエンジンのエディタ設定(Engine/Config/BaseEditor.ini)の ThumbnailManagerセクションを参考にします。
【UE4】iniファイルの読み込み優先度やコンフィグについてあれこれ

今回はプロジェクトにレンダラーのコードを追加するので、プロジェクト用のエディタ設定([ProjectDirectory]/Config/DefaultEngine.ini)に記述しました。

※.iniファイルは必要に応じて追加してください。
※.iniファイルの変更はエディタの起動時に反映されます。

具体的にはこんな感じです。

[/Script/UnrealEd.ThumbnailManager]
+RenderableThumbnailTypes=(ClassNeedingThumbnailName="/Script/UMGEditor.WidgetBlueprint",RendererClassName="/Script/MyProjectEd.WidgetThumbnailRenderer")

ClassNeedingThumbnailName にはウィジェットBPのC++クラス名の WidgetBlueprint を記述します。
エンジンソースを検索しないと、こういう情報が見つからないので大変ですねw

RendererClassName には今回追加したクラス名を記述します。

その手前のパスの様な文字列は BaseEditor.ini の書き方をマネして動作確認しました。

検証結果

ウィジェットBPがあるフォルダを開いた時、自動でサムネイルが更新されるものとそうじゃないものがあるみたいです。

こうなってしまう原因はわからないのですが、どうもエディタがアセットを参照し始めるとサムネイルレンダラーが作られる様です。
なので該当のアセットを右クリックしたり、BPを開いたりすると作成されます。

ということで、なんとか動作しました。

最後にアセットを保存しましょう。そうしないとサムネイルが残らない様です。
(アセットにサムネイルのパスでも格納されているのだろうか?)

参考資料

以上です