Editor Utility Widget
今回UE4.22から利用できるようになった、Editor Utility Widgetという機能を試してみました。 Editor Utility Widgetを利用するとUMGとブループリントでエディタ拡張が簡単に作れます。
Editor Utility Widgetの基本的な説明はおかずさんの記事を参考にしました。
[UE4]エディタ上で動作するツール・エディタ拡張をUMGで簡単に作れる Editor Utility Widget について - Qiita
つくったもの
Editor Utility Widget上をマウスドラッグすることで線を一筆書きで書き、 書き終わった線に沿ってスプラインメッシュを生成する機能を作りました。
できたスプラインメッシュは、ボタンを押すとスタティックメッシュにできます。
Editor Scriptingでお絵かきして作ったSplineMeshComponentをそのままスクリプトでStaticMesh化するのに成功した。#UE4 #ue4study pic.twitter.com/sCfMMPdC1J
— ayuma (@ayuma_x) May 30, 2019
Editor Scripting Utilities Pluginの有効化
このプラグインを有効にすると、Editor Utility Widgetからアセットの操作などができるようになります。
Widget上で線を引く
線を引く部分はUserWidgetを1つ作ってその中で実装しました。
まず、変数に線を引くための座標(Vector2D)の配列を用意します。
次にMouseDownで毎回座標配列を空っぽに。
MouseMoveでは座標の配列にマウスの座標をいれます。
ただし、すべてのMouseMoveイベントで処理すると点の数が多すぎるので前回格納した点から5pixel以上離れた場合のみ点を格納しています。
あとはOnPaintでDrawLinesを呼べば線が引かれます。
MouseUpでは線が引かれ終わったことを示すイベントを発火しています。
このイベントをEditor Utility Widgetが監視してメッシュ生成をこの後していきます。
Editor Utility Widgetの実装
次にEditor Utility Widgetを作成します。
先ほど作ったUserWidgetも配置します。
ComboBoxにメッシュ生成機能をもったActorの一覧を出したいので、 ComboBoxが開いたタイミングでActorの一覧を取得し追加しています。
またComboBoxの選択が変わった時にカレントのActorを設定しています。
先ほど作ったUserWidgetの線の引き終わりイベントを監視して、カレントのActorに線に沿った座標の配列を渡します。
スプラインメッシュ生成
スプラインメッシュを生成するActorを作っていきます。
SplineComponentを2つと、SplineMeshComponentの親になるSceneComponentを1つ追加しておきます。 SplineComponentは点の補正をするために贅沢に2つ使ってます。
UserWidgetの線が引き終わったタイミングで呼ばれる関数を作成していきます。
まずは、直前に生成されていたSplineMeshComponentを全て破棄し、SplineComponentの点も空にします。
次に渡されて点の配列をそのままSplineComponentの点に追加していきます。 1pixelを10cmとして設定しています。
次に1つ目のSplineComponentのラインに沿って等間隔の点を取り出し2つ目のSplineComponentへ点を追加していきます。
ここで2つ目のSplineComponentを作っているのは、等間隔に点を作ったほうがこの後作成するSplineMeshがきれいになるからです。
最後に2つ目のSplineComponentの点の位置とTangentを使ってSplineMeshComponentを作っていきます。
これでUserWidget上に描いた線に沿ったメッシュが出来上がります。
スプラインメッシュをスタティックメッシュに変換
SplineMeshComponentを含むActorをStaticMeshにするには、いつもMergeActorの機能を使っているのでこれをEditor Utility Widgetから呼べないか探してみました。
すると以下のノードが見つかりましたが、これはインプットがStaticMeshActorになっているので型が合いません。
なので何とかならないか、このノードのC++実装をのぞいてみました。
bool UEditorLevelLibrary::MergeStaticMeshActors(const TArray<AStaticMeshActor*>& ActorsToMerge, const FEditorScriptingMergeStaticMeshActorsOptions& MergeOptions, AStaticMeshActor*& OutMergedActor)
{
TGuardValue<bool> UnattendedScriptGuard(GIsRunningUnattendedScript, true);
OutMergedActor = nullptr;
if (!EditorScriptingUtils::CheckIfInEditorAndPIE())
{
return false;
}
FString FailureReason;
FString PackageName = EditorScriptingUtils::ConvertAnyPathToLongPackagePath(MergeOptions.BasePackageName, FailureReason);
if (PackageName.IsEmpty())
{
UE_LOG(LogEditorScripting, Error, TEXT("MergeStaticMeshActors. Failed to convert the BasePackageName. %s"), *FailureReason);
return false;
}
TArray<AStaticMeshActor*> AllActors;
TArray<UPrimitiveComponent*> AllComponents;
FVector PivotLocation;
if (!InternalEditorLevelLibrary::FindValidActorAndComponents(ActorsToMerge, AllActors, AllComponents, PivotLocation, FailureReason))
{
UE_LOG(LogEditorScripting, Error, TEXT("MergeStaticMeshActors failed. %s"), *FailureReason);
return false;
}
//
// See MeshMergingTool.cpp
//
const IMeshMergeUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshMergeModule>("MeshMergeUtilities").GetUtilities();
FVector MergedActorLocation;
TArray<UObject*> CreatedAssets;
const float ScreenAreaSize = TNumericLimits<float>::Max();
MeshUtilities.MergeComponentsToStaticMesh(AllComponents, AllActors[0]->GetWorld(), MergeOptions.MeshMergingSettings, nullptr, nullptr, PackageName, CreatedAssets, MergedActorLocation, ScreenAreaSize, true);
UStaticMesh* MergedMesh = nullptr;
if (!CreatedAssets.FindItemByClass(&MergedMesh))
{
UE_LOG(LogEditorScripting, Error, TEXT("MergeStaticMeshActors failed. No mesh was created."));
return false;
}
FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
for (UObject* Obj : CreatedAssets)
{
AssetRegistry.AssetCreated(Obj);
}
//Also notify the content browser that the new assets exists
if (!IsRunningCommandlet())
{
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
ContentBrowserModule.Get().SyncBrowserToAssets(CreatedAssets, true);
}
// Place new mesh in the world
if (MergeOptions.bSpawnMergedActor)
{
FActorSpawnParameters Params;
Params.OverrideLevel = AllActors[0]->GetLevel();
OutMergedActor = AllActors[0]->GetWorld()->SpawnActor<AStaticMeshActor>(MergedActorLocation, FRotator::ZeroRotator, Params);
if (!OutMergedActor)
{
UE_LOG(LogEditorScripting, Error, TEXT("MergeStaticMeshActors failed. Internal error while creating the merged actor."));
return false;
}
OutMergedActor->GetStaticMeshComponent()->SetStaticMesh(MergedMesh);
OutMergedActor->SetActorLabel(MergeOptions.NewActorLabel);
AllActors[0]->GetWorld()->UpdateCullDistanceVolumes(OutMergedActor, OutMergedActor->GetStaticMeshComponent());
}
// Remove source actors
if (MergeOptions.bDestroySourceActors)
{
UWorld* World = AllActors[0]->GetWorld();
for (AActor* Actor : AllActors)
{
GEditor->Layers->DisassociateActorFromLayers(Actor);
World->EditorDestroyActor(Actor, true);
}
}
//Select newly created actor
GEditor->SelectNone(false, true, false);
GEditor->SelectActor(OutMergedActor, true, false);
GEditor->NoteSelectionChange();
return true;
}
すると引数で渡されたStaticMeshActorの配列から、UPrimitiveComponentの配列を取り出してその後マージしていることが分かります。
なので、このソースを参考に引数をActorに改造してActor内のMeshComponentをマージする関数を作成しました。
bool UEditorUtilExtention::MergeStaticMeshComponents(const AActor* Actor, const FString& PackageName, const FMeshMergingSettings& MergeOptions)
{
const IMeshMergeUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshMergeModule>("MeshMergeUtilities").GetUtilities();
TInlineComponentArray<UStaticMeshComponent*> ComponentArray;
Actor->GetComponents<UStaticMeshComponent>(ComponentArray);
TArray<UPrimitiveComponent*> allComponents;
bool bActorIsValid = false;
for (UStaticMeshComponent* MeshCmp : ComponentArray)
{
if (MeshCmp->GetStaticMesh() && MeshCmp->GetStaticMesh()->RenderData.IsValid())
{
allComponents.Add(MeshCmp);
}
}
FVector MergedActorLocation;
TArray<UObject*> CreatedAssets;
const float ScreenAreaSize = TNumericLimits<float>::Max();
MeshUtilities.MergeComponentsToStaticMesh(allComponents, allComponents[0]->GetOwner()->GetWorld(), MergeOptions, nullptr, nullptr, PackageName, CreatedAssets, MergedActorLocation, ScreenAreaSize, true);
UStaticMesh* MergedMesh = nullptr;
if (!CreatedAssets.FindItemByClass(&MergedMesh))
{
UE_LOG(LogTemp, Error, TEXT("MergeStaticMeshComponents failed. No mesh was created."));
return false;
}
FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
for (UObject* Obj : CreatedAssets)
{
AssetRegistry.AssetCreated(Obj);
}
//Also notify the content browser that the new assets exists
if (!IsRunningCommandlet())
{
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
ContentBrowserModule.Get().SyncBrowserToAssets(CreatedAssets, true);
}
return true;
}
引数で渡されたActorからUPrimitiveComponentの配列を取り出し、その後のマージの処理は元のソースのロジックをそのまま使っています。
完成した関数をEditor Utility Widgetから呼ぶとちゃんとSplineMeshがStaticMeshにマージされました。
ただ、マテリアルもコピーされちゃうのは何故だろう??
手動でMerge Actorしたときは元のマテリアルを参照したままマージしてくれてるので、そっちの挙動の方がうれしいのですがちょっとやり方が分かりませんでした。
ちゃんとスタティックメッシュになっています。
まとめ
今回Editor Utility Widgetを初めて使ってみましたが、とても楽しかったです。
慣れ親しんだUMGとブループリントを使ってエディタ拡張が作れるので、日々の作業の自動化が誰でも簡単に作れると思います。
私はちょっと前に大量のSplineMeshComponentを含むActorを手動でStaticMeshにした事があるので、その時にこの機能をしっていればどれだけ楽だったか。。。
自動化はただ作業が楽になるだけでなく、手作業によるケアレスミスも防げるので品質面でも有用だと思います。
今後も自動化したい作業にはどんどん積極的に使っていこうと思いました。