Editor Utility Widgetを使ってお絵かきして動的メッシュ生成機能を作った


/images/2019/05/30/234740/20190530234709.png

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 Utilities Pluginの有効化

このプラグインを有効にすると、Editor Utility Widgetからアセットの操作などができるようになります。

/images/2019/05/30/234740/20190530234123.png

Widget上で線を引く

線を引く部分はUserWidgetを1つ作ってその中で実装しました。

まず、変数に線を引くための座標(Vector2D)の配列を用意します。

/images/2019/05/30/234740/20190530230810.png

次にMouseDownで毎回座標配列を空っぽに。

/images/2019/05/30/234740/20190530230738.png

MouseMoveでは座標の配列にマウスの座標をいれます。

ただし、すべてのMouseMoveイベントで処理すると点の数が多すぎるので前回格納した点から5pixel以上離れた場合のみ点を格納しています。

/images/2019/05/30/234740/20190530230956.png

あとはOnPaintでDrawLinesを呼べば線が引かれます。

/images/2019/05/30/234740/20190530231131.png

MouseUpでは線が引かれ終わったことを示すイベントを発火しています。

このイベントをEditor Utility Widgetが監視してメッシュ生成をこの後していきます。

/images/2019/05/30/234740/20190530231259.png

Editor Utility Widgetの実装

次にEditor Utility Widgetを作成します。

先ほど作ったUserWidgetも配置します。

/images/2019/05/30/234740/20190530231520.png

ComboBoxにメッシュ生成機能をもったActorの一覧を出したいので、 ComboBoxが開いたタイミングでActorの一覧を取得し追加しています。

またComboBoxの選択が変わった時にカレントのActorを設定しています。

/images/2019/05/30/234740/20190530231727.png

先ほど作ったUserWidgetの線の引き終わりイベントを監視して、カレントのActorに線に沿った座標の配列を渡します。

/images/2019/05/30/234740/20190530231813.png

スプラインメッシュ生成

スプラインメッシュを生成するActorを作っていきます。

SplineComponentを2つと、SplineMeshComponentの親になるSceneComponentを1つ追加しておきます。 SplineComponentは点の補正をするために贅沢に2つ使ってます。

/images/2019/05/30/234740/20190530232107.png

UserWidgetの線が引き終わったタイミングで呼ばれる関数を作成していきます。

まずは、直前に生成されていたSplineMeshComponentを全て破棄し、SplineComponentの点も空にします。

/images/2019/05/30/234740/20190530232243.png

次に渡されて点の配列をそのままSplineComponentの点に追加していきます。 1pixelを10cmとして設定しています。

/images/2019/05/30/234740/20190530232409.png

次に1つ目のSplineComponentのラインに沿って等間隔の点を取り出し2つ目のSplineComponentへ点を追加していきます。

ここで2つ目のSplineComponentを作っているのは、等間隔に点を作ったほうがこの後作成するSplineMeshがきれいになるからです。

/images/2019/05/30/234740/20190530232549.png

最後に2つ目のSplineComponentの点の位置とTangentを使ってSplineMeshComponentを作っていきます。

これでUserWidget上に描いた線に沿ったメッシュが出来上がります。

/images/2019/05/30/234740/20190530232744.png

スプラインメッシュをスタティックメッシュに変換

SplineMeshComponentを含むActorをStaticMeshにするには、いつもMergeActorの機能を使っているのでこれをEditor Utility Widgetから呼べないか探してみました。

すると以下のノードが見つかりましたが、これはインプットがStaticMeshActorになっているので型が合いません。

/images/2019/05/30/234740/20190530232947.png

なので何とかならないか、このノードの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にマージされました。

/images/2019/05/30/234740/20190530233704.png

ただ、マテリアルもコピーされちゃうのは何故だろう??

手動でMerge Actorしたときは元のマテリアルを参照したままマージしてくれてるので、そっちの挙動の方がうれしいのですがちょっとやり方が分かりませんでした。

/images/2019/05/30/234740/20190530233817.png

ちゃんとスタティックメッシュになっています。

/images/2019/05/30/234740/20190530233948.png

まとめ

今回Editor Utility Widgetを初めて使ってみましたが、とても楽しかったです。

慣れ親しんだUMGとブループリントを使ってエディタ拡張が作れるので、日々の作業の自動化が誰でも簡単に作れると思います。

私はちょっと前に大量のSplineMeshComponentを含むActorを手動でStaticMeshにした事があるので、その時にこの機能をしっていればどれだけ楽だったか。。。

自動化はただ作業が楽になるだけでなく、手作業によるケアレスミスも防げるので品質面でも有用だと思います。

今後も自動化したい作業にはどんどん積極的に使っていこうと思いました。

UE4 

See also