UnrealC++で画像ファイルからテクスチャアセットを作成する

#はじめに
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">PDFアセットとしてインポートできるようになった！！<a href="https://twitter.com/hashtag/UE4?src=hash&amp;ref_src=twsrc%5Etfw">#UE4</a> <a href="https://t.co/hjLXLuAeQ8">pic.twitter.com/hjLXLuAeQ8</a></p>&mdash; Naotsun (@Naotsun_UE) <a href="https://twitter.com/Naotsun_UE/status/1215929810729332736?ref_src=twsrc%5Etfw">January 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

最近、以前作成したPDFを読み込むプラグインを改良していてUE4のエディタにPDFアセットとしてインポートできる機能を作っていました。
正常にインポートはできますが、PDFアセットの中の各ページのテクスチャがエディタを再起動した際になくなってしまう状態で、読み込んだテクスチャがアセットになっていなかったことが原因でした。
そこでUnrealC++からテクスチャアセットを作る方法を調べたのでご紹介します。

#つくるもの
![1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/d75a9154-db60-0510-21d9-de9332b13a48.png)
タイトルにある通り、画像ファイルからテクスチャアセットを作成するノードを作ります。引数に画像のファイルパス、戻り値にロードしたテクスチャアセットと成功の可否をを持ちます。

#つくってみる
まずは適当なファンクションライブラリを作成してヘッダーで以下のように関数を定義します。

```MyBlueprintFunctionLibrary.h
UFUNCTION(BlueprintCallable, Category = "Test")
static bool LoadTextureAssetFromFile(const FString& FilePath, class UTexture2D* &LoadedTexture);
```
次に実装部分です。まずは以下のヘッダーをインクルードします。

```MyBlueprintFunctionLibrary.cpp
#include "Public/MyBlueprintFunctionLibrary.h"
#include "Engine/Texture2D.h"
#include "Misc/Paths.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "AssetRegistryModule.h"
#include "IImageWrapperModule.h"
#include "IImageWrapper.h"
```
`Texture2D.h`、`FileHelper.h`、`IImageWrapperModule.h`、`IImageWrapper.h`は画像ファイルからテクスチャを作成する際に使用します。
`PackageName.h`と`AssetRegistryModule.h`はテクスチャをアセットとして保存する際に使用します。
`Path.h`はファイルパスを扱うため、インクルードします。

関数の中身を見ていきましょう。

```MyBlueprintFunctionLibrary.cpp
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG);
```
まず画像ファイルからテクスチャを作成するために、`FModuleManager::LoadModuleChecked`関数で`IImageWrapperModule`クラスのインスタンスを取得し、`IImageWrapperModule::CreateImageWrapper`関数で`IImageWrapper`クラスのインスタンスを取得します。
ここで読み込む画像ファイルの拡張子を指定します。今回はjpeg画像を読み込むので`EImageFormat::JPEG`とします。

```MyBlueprintFunctionLibrary.cpp
TArray<uint8> RawFileData;
if (FFileHelper::LoadFileToArray(RawFileData, *FilePath))
{
	const TArray<uint8>* UncompressedRawData = nullptr;
	if (ImageWrapper.IsValid() &&
		ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num()) &&
		ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedRawData)
		)
	{
```
次に画像ファイルのデータを読み込みます。`FFileHelper::LoadFileToArray`関数を使用して画像のデータを読み込みます。テクスチャにするには圧縮されていないデータが必要なので、`IImageWrapper::SetCompressed`関数で圧縮されたデータを指定し、`IImageWrapper::GetRaw`関数で圧縮されていないデータを取得します。

```MyBlueprintFunctionLibrary.cpp
FString Filename = FPaths::GetBaseFilename(FilePath);
int Width = ImageWrapper->GetWidth();
int Height = ImageWrapper->GetHeight();
```
上で説明した箇所のif文の中で、アセット名、画像の縦横のサイズを変数にしておきます。

```MyBlueprintFunctionLibrary.cpp
FString PackagePath = TEXT("/Game/LoadedTexture/");
FString AbsolutePackagePath = FPaths::ProjectContentDir() + TEXT("/LoadedTexture/");

FPackageName::RegisterMountPoint(PackagePath, AbsolutePackagePath);

PackagePath += Filename;

UPackage* Package = CreatePackage(nullptr, *PackagePath);
Package->FullyLoad();
```
次にテクスチャアセットを保存するためにパッケージを作成します。まずは、パッケージの相対パスと絶対パスを定義し、`FPackageName::RegisterMountPoint`関数で作成するテクスチャアセットの出力先を作ります。今回は[Content/LoadedTexture]に保存したいのでこのようになっています。
`PackagePath`にテクスチャアセットの名前を足して、`CreatePackage`関数でパッケージを作成します。
最後に`UPackage::FullyLoad`関数で作成したパッケージをロードしておきます。

```MyBlueprintFunctionLibrary.cpp
FName TextureName = MakeUniqueObjectName(Package, UTexture2D::StaticClass(), FName(*Filename));
UTexture2D* Texture = NewObject<UTexture2D>(Package, TextureName, RF_Public | RF_Standalone);
```
次に`MakeUniqueObjectName`関数を用いて他のアセットと被らない名前を取得します。そして、先程作成したパッケージと名前を指定してテクスチャを作成します。

```MyBlueprintFunctionLibrary.cpp
Texture->PlatformData = new FTexturePlatformData();
Texture->PlatformData->SizeX = Width;
Texture->PlatformData->SizeY = Height;
Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
Texture->NeverStream = false;
```
次に、画像のデータが格納される`FTexturePlatformData`を新しく作成し、テクスチャのサイズやMipMapの設定などを行います。

```MyBlueprintFunctionLibrary.cpp
FTexture2DMipMap* Mip = new FTexture2DMipMap();
Texture->PlatformData->Mips.Add(Mip);
Mip->SizeX = Width;
Mip->SizeY = Height;
Mip->BulkData.Lock(LOCK_READ_WRITE);
uint8* TextureData = (uint8*)Mip->BulkData.Realloc(UncompressedRawData->Num());
FMemory::Memcpy(TextureData, UncompressedRawData->GetData(), UncompressedRawData->Num());
Mip->BulkData.Unlock();
```
次に、画像のピクセルデータが格納される`FTexture2DMipMap`を新しく作成し、こちらでもテクスチャのサイズを指定した後、`BulkData.Lock`でリソースをロック->`FMemory::Memcpy`関数でピクセルデータをコピー->`BulkData.Unlock`でリソースのロックを解除の流れでテクスチャにデータを書き込みます。

```MyBlueprintFunctionLibrary.cpp
Texture->AddToRoot();
Texture->Source.Init(Width, Height, 1, 1, ETextureSourceFormat::TSF_BGRA8, UncompressedRawData->GetData());
Texture->UpdateResource();
```
次に`UTexture2D::UpdateResource`関数で今までの変更を適用するのですが、その前に`FTextureSource::Init`関数を実行します。
**これを忘れるとテクスチャアセットは作れますが、エディタを再起動した際にピクセルデータが失われます。**
また、この時にもRGBのフォーマットが必要になりますが、`IImageWrapper::GetRaw`関数でしたフォーマットと同じフォーマットを指定してください。これを間違えると色が反転したりします。

```MyBlueprintFunctionLibrary.cpp
Package->MarkPackageDirty();
FAssetRegistryModule::AssetCreated(Texture);
LoadedTexture = Texture;

FString PackageFilename = FPackageName::LongPackageNameToFilename(PackagePath, FPackageName::GetAssetPackageExtension());
return UPackage::SavePackage(Package, Texture, RF_Public | RF_Standalone, *PackageFilename, GError, nullptr, true, true, SAVE_NoError);
```
最後に作成したテクスチャをアセットとして保存します。
まず、`FAssetRegistryModule::AssetCreated`関数で新たにアセットが作成されたことを通知します。
次に、`FPackageName::LongPackageNameToFilename`関数でパッケージの拡張子付きにファイルパスを作り'UPackage::SavePackage'関数でテクスチャアセットのパッケージを保存します。

これで関数が出来上がりです。if文の関係上少しわかりにくくなってしまったので以下に全文を掲載します。

```MyBlueprintFunctionLibrary.cpp
bool UMyBlueprintFunctionLibrary::LoadTextureAssetFromFile(const FString& FilePath, class UTexture2D* &LoadedTexture)
{
	IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
	TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG);

	// 画像データを読み込む
	TArray<uint8> RawFileData;
	if (FFileHelper::LoadFileToArray(RawFileData, *FilePath))
	{
		// 非圧縮の画像データを取得
		const TArray<uint8>* UncompressedRawData = nullptr;
		if (ImageWrapper.IsValid() &&
			ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num()) &&
			ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedRawData)
			)
		{
			FString Filename = FPaths::GetBaseFilename(FilePath);
			int Width = ImageWrapper->GetWidth();
			int Height = ImageWrapper->GetHeight();

			// パッケージを作成
			FString PackagePath(TEXT("/Game/LoadedTexture/"));
			FString AbsolutePackagePath = FPaths::ProjectContentDir() + TEXT("/LoadedTexture/");

			FPackageName::RegisterMountPoint(PackagePath, AbsolutePackagePath);

			PackagePath += Filename;

			UPackage* Package = CreatePackage(nullptr, *PackagePath);
			Package->FullyLoad();

			// テクスチャを作成
			FName TextureName = MakeUniqueObjectName(Package, UTexture2D::StaticClass(), FName(*Filename));
			UTexture2D* Texture = NewObject<UTexture2D>(Package, TextureName, RF_Public | RF_Standalone);

			// テクスチャの設定
			Texture->PlatformData = new FTexturePlatformData();
			Texture->PlatformData->SizeX = Width;
			Texture->PlatformData->SizeY = Height;
			Texture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
			Texture->NeverStream = false;

			// ピクセルデータをテクスチャに書き込む
			FTexture2DMipMap* Mip = new FTexture2DMipMap();
			Texture->PlatformData->Mips.Add(Mip);
			Mip->SizeX = Width;
			Mip->SizeY = Height;
			Mip->BulkData.Lock(LOCK_READ_WRITE);
			uint8* TextureData = (uint8*)Mip->BulkData.Realloc(UncompressedRawData->Num());
			FMemory::Memcpy(TextureData, UncompressedRawData->GetData(), UncompressedRawData->Num());
			Mip->BulkData.Unlock();

			// テクスチャを更新
			Texture->AddToRoot();
			Texture->Source.Init(Width, Height, 1, 1, ETextureSourceFormat::TSF_BGRA8, UncompressedRawData->GetData());
			Texture->UpdateResource();

			// パッケージを保存
			Package->MarkPackageDirty();
			FAssetRegistryModule::AssetCreated(Texture);
			LoadedTexture = Texture;

			FString PackageFilename = FPackageName::LongPackageNameToFilename(PackagePath, FPackageName::GetAssetPackageExtension());
			return UPackage::SavePackage(Package, Texture, RF_Public | RF_Standalone, *PackageFilename, GError, nullptr, true, true, SAVE_NoError);
		}
	}

	return false;
}
```
#おわりに
画像ファイルからテクスチャを作るものについては、いくつかサンプルがありましたが、テクスチャアセットにするものがほとんどなく大変でした。特に、`FTextureSource::Init`関数が無くてアセットはできるのにエディタを再起動するとデータだけ無くなる状態になりがちなので気を付けましょう。
内容的に画像が少なく文字が多めになってしまい退屈な記事になってしましたが、これからテクスチャアセットを作ろうとしている方のお役に立てたら嬉しいです。

この記事で紹介したプロジェクトは以下でダウンロードできます。
https://github.com/Naotsun19B/CreateTextureAsset