
Editor Utility Widget
This time, I tried out the Editor Utility Widget feature, which became available from UE4.22. Using Editor Utility Widget, you can easily create editor extensions with UMG and Blueprints.
For a basic explanation of Editor Utility Widget, I referred to Okazu-san’s article.
What I Made
I created a feature where you draw a single continuous line by dragging the mouse on the Editor Utility Widget, and then generate a spline mesh along the drawn line.
The resulting spline mesh can be converted into a static mesh by pressing a button.
Editor Scriptingでお絵かきして作ったSplineMeshComponentをそのままスクリプトでStaticMesh化するのに成功した。#UE4 #ue4study pic.twitter.com/sCfMMPdC1J
— ayuma (@ayuma_x) May 30, 2019
Enabling the Editor Scripting Utilities Plugin
Enabling this plugin allows operations like manipulating assets from the Editor Utility Widget.
Drawing Lines on the Widget
I implemented the line drawing part within a single UserWidget.
First, prepare an array of coordinates (Vector2D) for drawing lines in the variables.
Next, empty the coordinate array every time on MouseDown.
In MouseMove, add the mouse coordinates to the coordinate array.
However, processing every MouseMove event results in too many points, so points are stored only if they are more than 5 pixels away from the previously stored point.

Then, call DrawLines in OnPaint to draw the line.
In MouseUp, fire an event indicating that the line drawing is finished.
The Editor Utility Widget monitors this event and proceeds with mesh generation afterwards.

Editor Utility Widget Implementation
Next, create the Editor Utility Widget.
Also place the UserWidget created earlier.

To display a list of Actors with mesh generation functionality in the ComboBox, get the list of Actors and add them when the ComboBox opens.
Also, set the current Actor when the ComboBox selection changes.

Monitor the line drawing finished event of the UserWidget created earlier and pass the array of coordinates along the line to the current Actor.
Spline Mesh Generation
Let’s create the Actor that generates the spline mesh.
Add two SplineComponents and one SceneComponent to be the parent of the SplineMeshComponents. Two SplineComponents are used lavishly for point correction.

Create the function called when the UserWidget’s line drawing finishes.
First, destroy all previously generated SplineMeshComponents and clear the points of the SplineComponent.
Next, add the passed array of points directly to the SplineComponent’s points. 1 pixel is set as 10cm.

Next, extract points at equal intervals along the line of the first SplineComponent and add them to the second SplineComponent.
The reason for creating the second SplineComponent here is that creating points at equal intervals results in a cleaner SplineMesh later.

Finally, create SplineMeshComponents using the point positions and Tangents of the second SplineComponent.
This creates a mesh along the line drawn on the UserWidget.
Converting Spline Mesh to Static Mesh
To convert an Actor containing SplineMeshComponents to a StaticMesh, I usually use the MergeActor feature, so I looked for a way to call this from the Editor Utility Widget.
I found the node below, but its input is StaticMeshActor, so the type doesn’t match.

So, wondering if there was a way, I peeked into the C++ implementation of this node.
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;
}
It turns out that it extracts an array of UPrimitiveComponents from the array of StaticMeshActors passed as arguments and then merges them.
So, referencing this source, I created a function modified to take an Actor as an argument and merge the MeshComponents within the Actor.
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);
}
}
if (allComponents.Num() == 0) // Added check for empty components
{
UE_LOG(LogTemp, Warning, TEXT("MergeStaticMeshComponents failed. No valid StaticMeshComponents found in the actor."));
return false;
}
FVector MergedActorLocation;
TArray<UObject*> CreatedAssets;
const float ScreenAreaSize = TNumericLimits<float>::Max();
// Use the first component's world
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;
}
It extracts an array of UPrimitiveComponents from the Actor passed as an argument, and the subsequent merge process uses the logic from the original source as is.
Calling the completed function from the Editor Utility Widget properly merged the SplineMesh into a StaticMesh.
However, why are the materials also copied??
When manually using Merge Actor, it merges while referencing the original materials, which is the preferred behavior, but I couldn’t figure out how to do that.
It has properly become a static mesh.
Summary
This was my first time using Editor Utility Widget, and it was a lot of fun.
Since you can create editor extensions using familiar UMG and Blueprints, I think anyone can easily automate daily tasks.
I manually converted an Actor containing a large number of SplineMeshComponents to StaticMesh a while ago, so if I had known about this feature then, how much easier it would have been…
Automation not only makes tasks easier but also prevents careless mistakes from manual work, so I think it’s useful in terms of quality as well.
I decided to actively use it more and more for tasks I want to automate in the future.