UE4 アセットを一括修正してみる

# 概要
UnrealEngine のエディターでのアセット修正についてのメモです。
サンプルとしてEditorUtilityWidgetをつかったテクスチャアセットのパラメータを変更するツールを作成してみます。
関連事項として外部アプリケーションの実行も試してみました。

## 環境
Windows10
Visual Studio 2017
UnrealEngine 4.24

## 参考
以下を参考にさせて頂きました、ありがとうございます。

[UnrealEngine : アセットレジストリ](https://docs.unrealengine.com/ja/Programming/Assets/Registry/index.html)
[第004回　UE4のアセットを一括修正する](http://www.cc2.co.jp/blog/?p=18138)
[[UE4] 独自に用意したデータアセットをより便利に活用する方法](https://historia.co.jp/archives/13665/)
[UE4 Editorから外部アプリケーションの実行](http://papersloth.hatenablog.com/entry/ac201702)


# アセットレジストリについて
エディタ上に存在するアセット情報を自動で収集しているコンテンツブラウザが使っているシステム。コードから利用することも可能です。

```cpp:.cpp
#include "AssetRegistryModule.h"
#include "AssetRegistryHelpers.h"

FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AssetData;

// クラスを指定してアセットリストを取得する
const UClass* Class = UStaticMesh::StaticClass();
AssetRegistryModule.Get().GetAssetsByClass(Class, AssetData);
```

`FARFilter` を使って名前やパスなどの条件からアセットリストを取得することもできます。


# EditorUtilityWidgetについて
エディター上で動作するUMGです。
C++を使う場合はエディタモジュールで書かないとパッケージ時にエラーになります。


# テクスチャアセットを一括で変換してみる
テスクチャアセットのパラメータを一括変換するツールを作成してみます。
内容はテスクチャアセットの `Adjustments` パラメータをいくつか書き換えます。

## C++ コード
UEditorUtilityWidget を継承したクラスを作成します。
UMGでのイベントも用意し、起動時(`NativePreConstruct`)に登録します。


```cpp:MyAssetChanger.h
#include "CoreMinimal.h"
#include "EditorUtilityWidget.h"
#include "MyAssetChanger.generated.h"

UCLASS()
class APPEDITOR_API UMyAssetChanger : public UEditorUtilityWidget
{
	GENERATED_BODY()
	
public:
	UMyAssetChanger(){}

protected:
	virtual void NativePreConstruct() override;

	// 対象アセットリストを取得する
	void GetAssetList(TArray<FAssetData>& _AssetList) const;
	// アセット書き換え
	bool ChangeAsset(FAssetData& _AssetData);

	// チェックボックスイベント
	UFUNCTION(BlueprintCallable)
	void ChangeCheckBox(bool bChange);

	// ボタンクリックイベント
	UFUNCTION(BlueprintCallable)
	void ClickedButton();

private:
	// セーブフラグ
	bool	bIsSave = false;
};

```

`NativePreConstruct` でWidgetのボタンとチェックボックスのイベントをバインドさせています。
`GetAssetList` で対象アセットリストを取得しています。
`ChangeAsset` で実際のアセットに対する変更処理をしています。


```cpp:MyAssetChanger.cpp
#include "MyAssetChanger.h"
#include "AssetRegistryModule.h"
#include "FileHelpers.h"
#include "Components/Button.h"
#include "Components/CheckBox.h"

// 開始前処理
void UMyAssetChanger::NativePreConstruct()
{
	Super::NativePreConstruct();

	UButton* Button = Cast<UButton>(GetWidgetFromName("Button0"));
	if( ::IsValid(Button) && !Button->OnClicked.IsBound() ){
		Button->OnClicked.AddDynamic(this, &UMyAssetChanger::ClickedButton);
	}
	
	UCheckBox* CheckBox = Cast<UCheckBox>(GetWidgetFromName("CheckBox0"));
	if( ::IsValid(CheckBox) && !CheckBox->OnCheckStateChanged.IsBound() ){
		CheckBox->OnCheckStateChanged.AddDynamic(this, &UMyAssetChanger::ChangeCheckBox );
	}

}

// 対象アセットリストを取得する
void UMyAssetChanger::GetAssetList(TArray<FAssetData>& _AssetList) const
{
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(FName("AssetRegistry"));
	IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
	
	
	FARFilter Filter;
	// 対象パス（Contentフォルダ全体を対象にする）
	Filter.PackagePaths.Add(TEXT("/Game/"));
	Filter.bRecursivePaths = true;
	
	// 対象クラス指定
	Filter.ClassNames.Add(UTexture2D::StaticClass()->GetFName());

	AssetRegistry.GetAssets(Filter, _AssetList);

}

// アセット書き換え
bool UMyAssetChanger::ChangeAsset(FAssetData&	_AssetData)
{
	
	if (UTexture2D* _Texture = Cast<UTexture2D>(_AssetData.GetAsset())){
		// パラメータを変更する
		_Texture->AdjustBrightness = 1.5f;
		_Texture->AdjustSaturation = 0.9f;
		_Texture->AdjustRGBCurve = 2.0f;
		
		// 編集フラグ設定
		_Texture->MarkPackageDirty();
		
		return(true);
	}
	
	return(false);
}

// チェックボックスイベント
void UMyAssetChanger::ChangeCheckBox(bool bChange)
{	
	bIsSave = bChange;
}

// ボタンクリックイベント
void UMyAssetChanger::ClickedButton()
{
	TArray<FAssetData>	_AssetList;
	TArray<UPackage*>	_PackagesToSave;
		
	// アセットリストの取得
	GetAssetList(_AssetList);
	
	// アセット変更
	for(auto _AssetData : _AssetList){
		if( ChangeAsset(_AssetData) ){
			// 変更したパッケージ追加
			_PackagesToSave.Add(_AssetData.GetPackage());
		}
	}
	
	if( bIsSave ){
		// 編集ファイルを保存
		FEditorFileUtils::PromptForCheckoutAndSave( _PackagesToSave, true, true );
	}

}

```

ファイルの保存には `FEditorFileUtils` を使用しています。
`PromptForCheckoutAndSave` の第2引数はtrue時に編集フラグを立てたものだけセーブします、第3引数はtrue時に確認ウィンドウが表示されます。

```cpp:FileHelpers.h
static EPromptReturnCode PromptForCheckoutAndSave( const TArray<UPackage*>& PackagesToSave, bool bCheckDirty, bool bPromptToSave, TArray<UPackage*>* OutFailedPackages = NULL, bool bAlreadyCheckedOut = false, bool bCanBeDeclined = true );
```



## BPコード：EditorUtilityWidget
コンテンツブラウザ右クリックから[Editor Utilities] -> [Editor Utility Widget]を作成。

![create_EUW.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/c3b72bd9-2da7-6ac9-8789-e0271f630d90.jpeg)

作成したアセットを開いて、[ファイル] -> [ブループリントの親を変更] を選択し、先のC++クラスに差し替えます。

次にボタンとチェックボックスを用意したWidgetを作成する、イベントグラフは何もしない。

![MyAssetChangerBP.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/77252d09-d516-268b-f69f-a59946dc9018.jpeg)


## 実行結果
作成したEditorWidget を右クリック -> [Run Editor Utility Widget]を実行。
![run_EUW.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/a388818b-c27d-f899-b690-5dfdfc3fe9b3.jpeg)

ボタンをクリックすればテクスチャからパラメータ変更処理がかかります。チェックボックスをONにすれば、セーブ処理を行います。

![run_tool.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/a9768da1-3a27-4433-d280-7fe54e61c455.jpeg)


テクスチャビューワでパラメータが変更されているのが確認できます。
![tex_param.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/dc5fb778-1993-ccb5-da7d-f76501e8471a.jpeg)


# その他
その他アセット修正関連で使えそうな事項です。

## 外部アプリケーションを実行する
外部アプリケーション呼び出しには `FPlatformProcess::ExecProcess` か `FPlatformProcess::CreateProcess` を使用することで可能です。

以下 `svn.exe` という外部ツールをコマンドライン実行する例です。

```cpp:.cpp
#include "FileHelpers.h"
#include "Paths.h"

// 外部ツールパスをワークディレクトリから辿る
FString _ExePath = FPaths::ConvertRelativePathToFull( FPlatformProcess::GetCurrentWorkingDirectory().Append(FString(TEXT("\\..\\"))) ) + FString(TEXT("svn.exe"));

int32	_Code;
FString	_Output;

// 実行
auto _Result = FPlatformProcess::ExecProcess(*_ExePath, *(FString(TEXT("--version")) ), &_Code, &_Output, nullptr);
if (!_Result){
	UE_LOG(LogTemp, Error, TEXT("Error!"));
	return;
}

// 出力結果
if (!_Output.IsEmpty()){
	UE_LOG(LogTemp, Log, TEXT("%s"), *_Output);
}

```

## テキストファイル出力
テキストファイル出力でログ出力やアセット変換用のバッチファイルなどを作成することができます。
以下、アセットデータのパスを出力するコード例。

```cpp:.cpp
#include "FileHelpers.h"
#include "Misc/FileHelper.h"
#include "Paths.h"

// アセットデータからパスを取得
FString _FilePath = _AssetData.ObjectPath.ToString();

FString _OutputFilePath = FPaths::ConvertRelativePathToFull(FPaths::GameSavedDir()) + TEXT("/MyLog.txt");
FString _FileContent = FString::Printf(TEXT("%s\n"), *_FilePath );

// ファイル出力
FFileHelper::SaveStringToFile(_FileContent, *_OutputFilePath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), EFileWrite::FILEWRITE_None);

```


## 対象ディレクトリを絞り込む
コンテンツフォルダの名前を条件にしてみるコード例。

```cpp:.cpp
IFileManager& _FileMgr = IFileManager::Get();
		
// "Tex"というフォルダを探す
TArray<FString> _DirNames;
_FileMgr.FindFilesRecursive(_DirNames, *FPaths::ProjectContentDir(), TEXT("Tex"), true, true);

// フィルター
FARFilter Filter;
Filter.bRecursivePaths = true;		// サブフォルダも検索

// 検索したディレクトリ名を書き換える
for(FString _Str : _DirNames){
	FString	_LeftStr;
	FString	_RightStr;
	_Str.Split(TEXT("Content"), &_LeftStr, &_RightStr);
	
	// "Content"から"Game"に置き換え
	FString _Dir(TEXT("/Game"));
	_Dir += _RightStr;
	
	// 対象パス追加
	Filter.PackagePaths.Add( *_Dir );
}

// 対象クラス指定
Filter.ClassNames.Add(UTexture2D::StaticClass()->GetFName());

```


# まとめ
サンプル程度のパラメータ変更は
`[アセットアクション]` -> `[プロパティマトリクスで一斉編集]`
を使った方が早いでしょうが、対象アセットが複数フォルダに散っている場合や、条件が細かいケースなどは使えない（使いにくい）ため、そのような場合に有用かと思います。
更にコンボボックスやツリーなどUMGの機能を使えば、より柔軟な作りにできるかと思います。