UnrealC++でアセットのエクスポート機能を拡張する

#はじめに
皆さんはアセットのエクスポート機能は使っていますか？テクスチャアセットとかだと比較的使う機会があったりするのかもしれません。
そんなアセットのエクスポート機能ですが、実は結構お手軽に拡張できます。
今回はアセットのサムネイルを画像ファイルに出力するプラグインを見ながらご紹介したいと思います。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ぷちコンでアセットのサムネイルを使いたかったからアセットのサムネイルを画像ファイルに出力する機能作ってみた。<br>エクスポートを拡張する場合はUExporterを継承するだけで作れるからお手軽だった。<a href="https://twitter.com/hashtag/UE4?src=hash&amp;ref_src=twsrc%5Etfw">#UE4</a> <a href="https://t.co/sKKVFGTRQc">pic.twitter.com/sKKVFGTRQc</a></p>&mdash; Naotsun (@Naotsun_UE) <a href="https://twitter.com/Naotsun_UE/status/1370761092968763394?ref_src=twsrc%5Etfw">March 13, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#つくってみる
「はじめに」に埋め込んだtweetにある通り、必要な作業は[`UExporter`](https://docs.unrealengine.com/en-US/API/Runtime/Engine/Exporters/UExporter/index.html)クラスを継承して、出力する部分の処理を作るだけです。

[ThumbnailExporter.h](https://github.com/Naotsun19B/TmbExporterPlugin/blob/master/Plugins/AssetThumbnailExporter/Source/AssetThumbnailExporter/Private/ThumbnailExporter.h)

```ThumbnailExporter.h
// Copyright 2020 Naotsun. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Exporters/Exporter.h"
#include "ThumbnailExporter.generated.h"

/*
 * Class for exporting asset thumbnails.
 */
UCLASS()
class ASSETTHUMBNAILEXPORTER_API UThumbnailExporter : public UExporter
{
	GENERATED_BODY()

public:
	UThumbnailExporter(const FObjectInitializer& ObjectInitializer);

	// UExporter interface.
	virtual bool SupportsObject(UObject* Object) const override;
	virtual bool ExportBinary(UObject* Object, const TCHAR* Type, FArchive& Ar, FFeedbackContext* Warn, int32 FileIndex = 0, uint32 PortFlags = 0) override;
	// End of UExporter interface.
};
```
まずはヘッダーですが、[`UExporter`](https://docs.unrealengine.com/en-US/API/Runtime/Engine/Exporters/UExporter/index.html)クラスを継承して、このプラグインでは画像ファイルへ出力するため`UExporter::ExportBinary`関数をオーバライドします。
![3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/f57d0ef4-cead-46dc-c857-e2394c0b77b9.png)
また、このプラグインでは画像のようにエディタ設定で指定した任意のクラスをサポートするようにしたいため、`UExporter::SupportsObject`関数もオーバライドします。もし単一のアセットのみをサポートするのであればこの関数はオーバライドする必要はありません。

[ThumbnailExporter.cpp](https://github.com/Naotsun19B/TmbExporterPlugin/blob/22fcab33bc0c93a703244cca1e09c642934dcda1/Plugins/AssetThumbnailExporter/Source/AssetThumbnailExporter/Private/ThumbnailExporter.cpp#L35)

```ThumbnailExporter.cpp
UThumbnailExporter::UThumbnailExporter(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	SupportedClass = UObject::StaticClass();
	PreferredFormatIndex = 2;
	BatchExportMode = true;
	CancelBatch = true;

	FormatExtension.Add(TEXT("EXR"));
	FormatDescription.Add(TEXT("Export Thumbnail As EXR"));
	FormatExtension.Add(TEXT("JPEG"));
	FormatDescription.Add(TEXT("Export Thumbnail As JPEG"));
	FormatExtension.Add(TEXT("PNG"));
	FormatDescription.Add(TEXT("Export Thumbnail As PNG"));
}
```
次にソースファイルです。まずはコンストラクタで各種設定を行います。

`SupportedClass`関数はサポートするアセットのクラスです。このプラグインでは複数のアセットを対象としたいため`UObject`のクラスを設定します。単一のアセットをサポートする場合はここでクラスを指定すればOKです。

`PreferredFormatIndex`はコメントを読む限り「追加する拡張子がプルダウンメニュー上のどのインデックスに挿入されるか」を決める変数のようですが、どんな値にしても結果は変わりませんでした。(原因がわかる方はコメントなどで教えて頂けると嬉しいです！)

`BatchExportMode`は複数のアセットを一括でエクスポートできるかを決めます。`CancelBatch`は`BatchExportMode`が`true`の時に`true`にすると一括エクスポート中にキャンセルできるようになります。

`FormatExtension`には追加する拡張子を、`FormatDescription`にはその拡張子の説明を追加します。プルダウンメニューには`FormatDescription`の文字列が表示されます。

[ThumbnailExporter.cpp](https://github.com/Naotsun19B/TmbExporterPlugin/blob/22fcab33bc0c93a703244cca1e09c642934dcda1/Plugins/AssetThumbnailExporter/Source/AssetThumbnailExporter/Private/ThumbnailExporter.cpp#L51)

```ThumbnailExporter.cpp
bool UThumbnailExporter::SupportsObject(UObject* Object) const
{
	auto* Settings = GetDefault<UAssetThumbnailExporterSettings>();
	if (Super::SupportsObject(Object) && IsValid(Settings) && IsValid(Object))
	{
		bool bIsSupportsOrChildClass = false;
		UClass* ObjectClass = Object->GetClass();
		for (const auto& SupportsAssetClass : Settings->SupportsAssetClasses)
		{
			if (SupportsAssetClass == ObjectClass || ObjectClass->IsChildOf(SupportsAssetClass))
			{
				bIsSupportsOrChildClass = true;
				break;
			}
		}

		return (Object->IsAsset() && bIsSupportsOrChildClass);
	}

	return false;
}
```
次に`SupportsObject`関数ですがこのプラグインのような特殊なことをするのでなければ不要かつ、エディタ設定で設定されているクラスかを確かめているだけなのでサラッと流します。
ちなみにベースクラスの`SupportsObject`関数ではコンストラクタで設定した`SupportedClass`及びその子クラスかを判定しています。

[UnrealExporter.cpp](https://github.com/EpicGames/UnrealEngine/blob/5df54b7ef1714f28fb5da319c3e83d96f0bedf08/Engine/Source/Runtime/Engine/Private/UnrealExporter.cpp#L70)

```UnrealExporter.cpp
// Returns whether this exporter supports the specific object
bool UExporter::SupportsObject(UObject* Object) const
{
	return (SupportedClass && Object->IsA(SupportedClass));
}
```
どうでもいいことですが、`UExporter`のようにヘッダーとソースファイルの名前が違うとエクスプローラやgithubで探す時に戸惑うのでちょっと困りますね...w

[ThumbnailExporter.cpp](https://github.com/Naotsun19B/TmbExporterPlugin/blob/22fcab33bc0c93a703244cca1e09c642934dcda1/Plugins/AssetThumbnailExporter/Source/AssetThumbnailExporter/Private/ThumbnailExporter.cpp#L73)

```ThumbnailExporter.cpp
bool UThumbnailExporter::ExportBinary(UObject* Object, const TCHAR* Type, FArchive& Ar, FFeedbackContext* Warn, int32 FileIndex, uint32 PortFlags)
{
	FVector2D ImageSize(512, 512);
	if (auto* Settings = GetDefault<UAssetThumbnailExporterSettings>())
	{
		ImageSize = Settings->ImageSize;
	}

	FObjectThumbnail ObjectThumbnail;
	ThumbnailTools::RenderThumbnail(
		Object,
		ImageSize.X, ImageSize.Y,
		ThumbnailTools::EThumbnailTextureFlushMode::AlwaysFlush, NULL,
		&ObjectThumbnail
	);

	if (ObjectThumbnail.IsEmpty())
	{
		Warn->Log(TEXT("The thumbnail object was empty."));
		return false;
	}

	IImageWrapperModule& ImageWrapperModule = FModuleManager::Get().LoadModuleChecked<IImageWrapperModule>(TEXT("ImageWrapper"));
	TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(ThumbnailExporterInternal::ConvertStringToImageFormat(Type));
	if (!ImageWrapper.IsValid())
	{
		Warn->Log(TEXT("Failed to get ImageWrapper."));
		return false;
	}
	
	const TArray<uint8>& RawImageData = ObjectThumbnail.AccessImageData();

	if (!ImageWrapper->SetRaw(RawImageData.GetData(), RawImageData.GetAllocatedSize(), ImageSize.X, ImageSize.Y, ERGBFormat::BGRA, 8))
	{
		Warn->Log(TEXT("Could not set data to ImageWrapper."));
		return false;
	}

	const TArray64<uint8>& CompressedData = ImageWrapper->GetCompressed();
	if (CompressedData.Num() == 0)
	{
		Warn->Log(TEXT("The compressed data was empty."));
		return false;
	}

	Ar.Serialize((void*)CompressedData.GetData(), CompressedData.GetAllocatedSize());

	return true;
}
```
`IImageWrapperModule`を使った画像出力についてはヒストリアさんの記事や私が以前に書いた記事にあるのでそちらを読んでいただければと思います。
[ImageWrapperModuleを用いて指定の拡張子でスクリーンショットを書き出す方法](https://historia.co.jp/archives/8595/)
[UnrealC++で画像ファイルからテクスチャアセットを作成する](https://qiita.com/Naotsun/items/31b3de4dbcf0ec241398)

最後にアセットからサムネイルのデータを取得する方法ですが、`ThumbnailTools::RenderThumbnail`を使います。必要なヘッダーは"Misc/ObjectThumbnail.h"です。

引数にサムネイルを取得したいオブジェクト、画像サイズ、テクスチャストリームのフラッシュ設定、描画に使用するレンダーターゲットリソース、最後に結果を出力する`FObjectThumbnail`を渡すだけで動きます。
テクスチャストリームのフラッシュ設定ですが、`ThumbnailTools::EThumbnailTextureFlushMode::NeverFlush`にすると出力結果がぼやける可能性があるので特に理由がなければ`ThumbnailTools::EThumbnailTextureFlushMode::AlwaysFlush`にしておきましょう。

そして`FObjectThumbnail`から`TArray<uint8>`型でデータが取得できるのでそれを`IImageWrapperModule`を使って画像ファイルに出力します。

#おわりに
ここ最近誰得記事を量産している気がしますが、「ちょうど今エクスポート機能拡張したかったんだよぉ！！」って方の役に立てれば嬉しいです。あとは13連続でUnrealC++を使った記事なのでBPでできることの記事も書きたいですね。
このプラグインでは画像ファイルへの出力でしたが、テキストファイルへの出力などもできるのでもっといろんなことができるのではないでしょうか。

この記事で紹介したプラグインは以下でダウンロードできます。
https://github.com/Naotsun19B/TmbExporterPlugin