Files
UnrealEngine/Engine/Plugins/Runtime/MeshModelingToolset/Source/MeshModelingTools/Private/CombineMeshesTool.cpp
2025-05-18 13:04:45 +08:00

571 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombineMeshesTool.h"
#include "InteractiveToolManager.h"
#include "ToolBuilderUtil.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "DynamicMeshEditor.h"
#include "DynamicMesh/MeshTransforms.h"
#include "ModelingObjectsCreationAPI.h"
#include "Selection/ToolSelectionUtil.h"
#include "Physics/ComponentCollisionUtil.h"
#include "ShapeApproximation/SimpleShapeSet3.h"
#include "TargetInterfaces/MaterialProvider.h"
#include "TargetInterfaces/DynamicMeshProvider.h"
#include "TargetInterfaces/DynamicMeshCommitter.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
#include "ToolTargetManager.h"
#include "ModelingToolTargetUtil.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(CombineMeshesTool)
#if WITH_EDITOR
#include "Misc/ScopedSlowTask.h"
#endif
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UCombineMeshesTool"
namespace CombineMeshesToolLocals
{
void SetNewMaterialID(int32 ComponentIdx, FDynamicMeshMaterialAttribute* MatAttrib, int32 TID,
TArray<TArray<int32>>& MaterialIDRemaps, TArray<UMaterialInterface*>& AllMaterials)
{
int MatID = MatAttrib->GetValue(TID);
if (!ensure(MatID >= 0))
{
return;
}
if (MatID >= MaterialIDRemaps[ComponentIdx].Num())
{
UE_LOG(LogGeometry, Warning, TEXT("UCombineMeshesTool: Component %d had at least one material ID (%d) "
"that was not in its material list."), ComponentIdx, MatID);
// There are different things we could do here. It's worth noting that out of bounds material indices
// are handled differently in static meshes and dynamic mesh components, and depend in part on how we
// got to that state. So trying to preserve a specific behavior is not practical, and probably not
// expected if the user is not giving us valid data to begin with.
// The route we go is to give a separate nullptr material slot to each out of bounds ID. This will give
// the user a chance to preserve their material assignments while fixing the issue by assigning materials to
// the slots created in the output (at least, unless they pass through this tool again, at which point any
// nullptr-pointing IDs will be collapsed to point to the same nullptr slot, due to the way we create the
// combined material list for in-bounds IDs).
int32 NumElementsToAdd = MatID - MaterialIDRemaps[ComponentIdx].Num() + 1;
for (int32 i = 0; i < NumElementsToAdd; ++i)
{
MaterialIDRemaps[ComponentIdx].Add(AllMaterials.Num());
AllMaterials.Add(nullptr);
}
checkSlow(MaterialIDRemaps[ComponentIdx].Num() == MatID + 1);
}
MatAttrib->SetValue(TID, MaterialIDRemaps[ComponentIdx][MatID]);
}
}
/*
* ToolBuilder
*/
bool UCombineMeshesToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
{
return (bIsDuplicateTool) ?
(SceneState.TargetManager->CountSelectedAndTargetable(SceneState, GetTargetRequirements()) == 1)
: (SceneState.TargetManager->CountSelectedAndTargetable(SceneState, GetTargetRequirements()) > 1);
}
UMultiSelectionMeshEditingTool* UCombineMeshesToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
{
UCombineMeshesTool* NewTool = NewObject<UCombineMeshesTool>(SceneState.ToolManager);
NewTool->SetDuplicateMode(bIsDuplicateTool);
return NewTool;
}
const FToolTargetTypeRequirements& UCombineMeshesToolBuilder::GetTargetRequirements() const
{
static FToolTargetTypeRequirements CombineTypeRequirements({
UMaterialProvider::StaticClass(),
UDynamicMeshProvider::StaticClass(),
UPrimitiveComponentBackedTarget::StaticClass(),
UDynamicMeshCommitter::StaticClass()
});
static FToolTargetTypeRequirements DuplicateTypeRequirements({
UMaterialProvider::StaticClass(),
UDynamicMeshProvider::StaticClass(),
UPrimitiveComponentBackedTarget::StaticClass()
});
return bIsDuplicateTool ? DuplicateTypeRequirements : CombineTypeRequirements;
}
/*
* Tool
*/
void UCombineMeshesTool::SetDuplicateMode(bool bDuplicateModeIn)
{
this->bDuplicateMode = bDuplicateModeIn;
}
void UCombineMeshesTool::Setup()
{
UInteractiveTool::Setup();
BasicProperties = NewObject<UCombineMeshesToolProperties>(this);
AddToolPropertySource(BasicProperties);
BasicProperties->RestoreProperties(this);
BasicProperties->bIsDuplicateMode = this->bDuplicateMode;
OutputTypeProperties = NewObject<UCreateMeshObjectTypeProperties>(this);
OutputTypeProperties->InitializeDefaultWithAuto();
OutputTypeProperties->OutputType = UCreateMeshObjectTypeProperties::AutoIdentifier;
OutputTypeProperties->RestoreProperties(this, TEXT("OutputTypeFromInputTool"));
OutputTypeProperties->WatchProperty(OutputTypeProperties->OutputType, [this](FString) { OutputTypeProperties->UpdatePropertyVisibility(); });
AddToolPropertySource(OutputTypeProperties);
BasicProperties->WatchProperty(BasicProperties->OutputWriteTo, [&](EBaseCreateFromSelectedTargetType NewType)
{
if (NewType == EBaseCreateFromSelectedTargetType::NewObject)
{
BasicProperties->OutputExistingName = TEXT("");
SetToolPropertySourceEnabled(OutputTypeProperties, true);
}
else
{
int32 Index = (BasicProperties->OutputWriteTo == EBaseCreateFromSelectedTargetType::FirstInputObject) ? 0 : Targets.Num() - 1;
BasicProperties->OutputExistingName = UE::Modeling::GetComponentAssetBaseName(UE::ToolTarget::GetTargetComponent(Targets[Index]), false);
SetToolPropertySourceEnabled(OutputTypeProperties, false);
}
});
SetToolPropertySourceEnabled(OutputTypeProperties, BasicProperties->OutputWriteTo == EBaseCreateFromSelectedTargetType::NewObject);
if (bDuplicateMode)
{
SetToolDisplayName(LOCTEXT("DuplicateMeshesToolName", "Duplicate"));
BasicProperties->OutputNewName = UE::Modeling::GetComponentAssetBaseName(UE::ToolTarget::GetTargetComponent(Targets[0]));
}
else
{
SetToolDisplayName(LOCTEXT("CombineMeshesToolName", "Append"));
BasicProperties->OutputNewName = FString("Combined");
}
HandleSourceProperties = bDuplicateMode
? static_cast<UOnAcceptHandleSourcesPropertiesBase*>(NewObject<UOnAcceptHandleSourcesPropertiesSingle>(this))
: static_cast<UOnAcceptHandleSourcesPropertiesBase*>(NewObject<UOnAcceptHandleSourcesProperties>(this));
AddToolPropertySource(HandleSourceProperties);
HandleSourceProperties->RestoreProperties(this);
if (bDuplicateMode)
{
GetToolManager()->DisplayMessage(
LOCTEXT("OnStartToolDuplicate", "This tool duplicates a single input object to create new objects, and optionally replaces the input object."),
EToolMessageLevel::UserNotification);
}
else
{
GetToolManager()->DisplayMessage(
LOCTEXT("OnStartToolCombine", "This tool appends multiple input object to create new objects, and optionally replaces the one of the input objects."),
EToolMessageLevel::UserNotification);
}
}
void UCombineMeshesTool::OnShutdown(EToolShutdownType ShutdownType)
{
BasicProperties->SaveProperties(this);
OutputTypeProperties->SaveProperties(this, TEXT("OutputTypeFromInputTool"));
HandleSourceProperties->SaveProperties(this);
if (ShutdownType == EToolShutdownType::Accept)
{
if (bDuplicateMode || BasicProperties->OutputWriteTo == EBaseCreateFromSelectedTargetType::NewObject)
{
CreateNewAsset();
}
else
{
UpdateExistingAsset();
}
}
}
void UCombineMeshesTool::CreateNewAsset()
{
using namespace CombineMeshesToolLocals;
// Make sure meshes are available before we open transaction. This is to avoid potential stability issues related
// to creation/load of meshes inside a transaction, for assets that possibly do not have bulk data currently loaded.
static FGetMeshParameters GetMeshParams;
GetMeshParams.bWantMeshTangents = true;
TArray<FDynamicMesh3> InputMeshes;
InputMeshes.Reserve(Targets.Num());
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
InputMeshes.Add(UE::ToolTarget::GetDynamicMeshCopy(Targets[ComponentIdx], GetMeshParams));
}
GetToolManager()->BeginUndoTransaction( bDuplicateMode ?
LOCTEXT("DuplicateMeshToolTransactionName", "Duplicate Mesh") :
LOCTEXT("CombineMeshesToolTransactionName", "Merge Meshes"));
FBox Box(ForceInit);
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
Box += UE::ToolTarget::GetTargetComponent(Targets[ComponentIdx])->Bounds.GetBox();
}
TArray<UMaterialInterface*> AllMaterials;
TArray<TArray<int32>> MaterialIDRemaps;
BuildCombinedMaterialSet(AllMaterials, MaterialIDRemaps);
FDynamicMesh3 AccumulateDMesh;
AccumulateDMesh.EnableTriangleGroups();
AccumulateDMesh.EnableAttributes();
AccumulateDMesh.Attributes()->EnableTangents();
AccumulateDMesh.Attributes()->EnableMaterialID();
AccumulateDMesh.Attributes()->EnablePrimaryColors();
constexpr bool bCenterPivot = false;
FVector3d Origin = FVector3d::ZeroVector;
if (bCenterPivot)
{
// Place the pivot at the bounding box center
Origin = Box.GetCenter();
}
else if (!Targets.IsEmpty())
{
// Use the average pivot
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
Origin += UE::ToolTarget::GetLocalToWorldTransform(Targets[ComponentIdx]).TransformPosition(FVector3d::ZeroVector);
}
Origin /= Targets.Num();
}
FTransform3d AccumToWorld(Origin);
FTransform3d ToAccum(-Origin);
FSimpleShapeSet3d SimpleCollision;
UE::Geometry::FComponentCollisionSettings CollisionSettings;
{
#if WITH_EDITOR
FScopedSlowTask SlowTask(Targets.Num()+1,
bDuplicateMode ?
LOCTEXT("DuplicateMeshBuild", "Building duplicate mesh ...") :
LOCTEXT("CombineMeshesBuild", "Building merged mesh ..."));
SlowTask.MakeDialog();
#endif
bool bNeedColorAttr = false;
int MatIndexBase = 0;
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
#if WITH_EDITOR
SlowTask.EnterProgressFrame(1);
#endif
UPrimitiveComponent* PrimitiveComponent = UE::ToolTarget::GetTargetComponent(Targets[ComponentIdx]);
FDynamicMesh3& ComponentDMesh = InputMeshes[ComponentIdx];
bNeedColorAttr = bNeedColorAttr || (ComponentDMesh.HasAttributes() && ComponentDMesh.Attributes()->HasPrimaryColors());
if (ComponentDMesh.HasAttributes())
{
AccumulateDMesh.Attributes()->EnableMatchingAttributes(*ComponentDMesh.Attributes(), false);
}
// update material IDs to account for combined material set
if (FDynamicMeshMaterialAttribute* MatAttrib = ComponentDMesh.Attributes()->GetMaterialID())
{
for (int TID : ComponentDMesh.TriangleIndicesItr())
{
SetNewMaterialID(ComponentIdx, MatAttrib, TID, MaterialIDRemaps, AllMaterials);
}
}
FDynamicMeshEditor Editor(&AccumulateDMesh);
FMeshIndexMappings IndexMapping;
if (bDuplicateMode) // no transform if duplicating
{
Editor.AppendMesh(&ComponentDMesh, IndexMapping);
if (UE::Geometry::ComponentTypeSupportsCollision(PrimitiveComponent))
{
CollisionSettings = UE::Geometry::GetCollisionSettings(PrimitiveComponent);
UE::Geometry::AppendSimpleCollision(PrimitiveComponent, &SimpleCollision, FTransform3d::Identity);
}
}
else
{
FTransformSRT3d XF = (UE::ToolTarget::GetLocalToWorldTransform(Targets[ComponentIdx]) * ToAccum);
if (XF.GetDeterminant() < 0)
{
ComponentDMesh.ReverseOrientation(false);
}
Editor.AppendMesh(&ComponentDMesh, IndexMapping,
[&XF](int Unused, const FVector3d P) { return XF.TransformPosition(P); },
[&XF](int Unused, const FVector3d N) { return XF.TransformNormal(N); });
if (UE::Geometry::ComponentTypeSupportsCollision(PrimitiveComponent))
{
UE::Geometry::AppendSimpleCollision(PrimitiveComponent, &SimpleCollision, XF);
}
}
FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Targets[ComponentIdx]);
MatIndexBase += MaterialSet.Materials.Num();
}
if (!bNeedColorAttr)
{
AccumulateDMesh.Attributes()->DisablePrimaryColors();
}
#if WITH_EDITOR
SlowTask.EnterProgressFrame(1);
#endif
if (bDuplicateMode)
{
// TODO: will need to refactor this when we support duplicating multiple
check(Targets.Num() == 1);
AccumToWorld = (FTransform3d)UE::ToolTarget::GetLocalToWorldTransform(Targets[0]);
}
// max len explicitly enforced here, would ideally notify user
FString UseBaseName = BasicProperties->OutputNewName.Left(250);
if (UseBaseName.IsEmpty())
{
UseBaseName = (bDuplicateMode) ? TEXT("Duplicate") : TEXT("Merge");
}
FCreateMeshObjectParams NewMeshObjectParams;
NewMeshObjectParams.TargetWorld = GetTargetWorld();
NewMeshObjectParams.Transform = (FTransform)AccumToWorld;
NewMeshObjectParams.BaseName = UseBaseName;
NewMeshObjectParams.Materials = AllMaterials;
NewMeshObjectParams.SetMesh(&AccumulateDMesh);
if (OutputTypeProperties->OutputType == UCreateMeshObjectTypeProperties::AutoIdentifier)
{
UE::ToolTarget::ConfigureCreateMeshObjectParams(Targets[0], NewMeshObjectParams);
}
else
{
OutputTypeProperties->ConfigureCreateMeshObjectParams(NewMeshObjectParams);
}
FCreateMeshObjectResult Result = UE::Modeling::CreateMeshObject(GetToolManager(), MoveTemp(NewMeshObjectParams));
if (Result.IsOK() && Result.NewActor != nullptr)
{
// if any inputs have Simple Collision geometry we will forward it to new mesh.
if (UE::Geometry::ComponentTypeSupportsCollision(Result.NewComponent) && SimpleCollision.TotalElementsNum() > 0)
{
UE::Geometry::SetSimpleCollision(Result.NewComponent, &SimpleCollision, CollisionSettings);
}
// select the new actor
ToolSelectionUtil::SetNewActorSelection(GetToolManager(), Result.NewActor);
}
}
TArray<AActor*> Actors;
for (int32 Idx = 0; Idx < Targets.Num(); Idx++)
{
Actors.Add(UE::ToolTarget::GetTargetActor(Targets[Idx]));
}
HandleSourceProperties->ApplyMethod(Actors, GetToolManager());
GetToolManager()->EndUndoTransaction();
}
void UCombineMeshesTool::UpdateExistingAsset()
{
using namespace CombineMeshesToolLocals;
// Make sure meshes are available before we open transaction. This is to avoid potential stability issues related
// to creation/load of meshes inside a transaction, for assets that possibly do not have bulk data currently loaded.
static FGetMeshParameters GetMeshParams;
GetMeshParams.bWantMeshTangents = true;
TArray<FDynamicMesh3> InputMeshes;
InputMeshes.Reserve(Targets.Num());
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
InputMeshes.Add(UE::ToolTarget::GetDynamicMeshCopy(Targets[ComponentIdx], GetMeshParams));
}
check(!bDuplicateMode);
GetToolManager()->BeginUndoTransaction(LOCTEXT("CombineMeshesToolTransactionName", "Merge Meshes"));
AActor* SkipActor = nullptr;
TArray<UMaterialInterface*> AllMaterials;
TArray<TArray<int32>> MaterialIDRemaps;
BuildCombinedMaterialSet(AllMaterials, MaterialIDRemaps);
FDynamicMesh3 AccumulateDMesh;
AccumulateDMesh.EnableTriangleGroups();
AccumulateDMesh.EnableAttributes();
AccumulateDMesh.Attributes()->EnableTangents();
AccumulateDMesh.Attributes()->EnableMaterialID();
AccumulateDMesh.Attributes()->EnablePrimaryColors();
int32 SkipIndex = (BasicProperties->OutputWriteTo == EBaseCreateFromSelectedTargetType::FirstInputObject) ? 0 : (Targets.Num() - 1);
UPrimitiveComponent* UpdateComponent = UE::ToolTarget::GetTargetComponent(Targets[SkipIndex]);
SkipActor = UE::ToolTarget::GetTargetActor(Targets[SkipIndex]);
FTransform3d TargetToWorld = UE::ToolTarget::GetLocalToWorldTransform(Targets[SkipIndex]);
FSimpleShapeSet3d SimpleCollision;
UE::Geometry::FComponentCollisionSettings CollisionSettings;
bool bOutputComponentSupportsCollision = UE::Geometry::ComponentTypeSupportsCollision(UpdateComponent);
if (bOutputComponentSupportsCollision)
{
CollisionSettings = UE::Geometry::GetCollisionSettings(UpdateComponent);
}
TArray<FTransform3d> Transforms;
Transforms.SetNum(2);
{
#if WITH_EDITOR
FScopedSlowTask SlowTask(Targets.Num()+1,
bDuplicateMode ?
LOCTEXT("DuplicateMeshBuild", "Building duplicate mesh ...") :
LOCTEXT("CombineMeshesBuild", "Building merged mesh ..."));
SlowTask.MakeDialog();
#endif
bool bNeedColorAttr = false;
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
#if WITH_EDITOR
SlowTask.EnterProgressFrame(1);
#endif
UPrimitiveComponent* PrimitiveComponent = UE::ToolTarget::GetTargetComponent(Targets[ComponentIdx]);
FDynamicMesh3& ComponentDMesh = InputMeshes[ComponentIdx];
bNeedColorAttr = bNeedColorAttr || (ComponentDMesh.HasAttributes() && ComponentDMesh.Attributes()->HasPrimaryColors());
// update material IDs to account for combined material set
FDynamicMeshMaterialAttribute* MatAttrib = ComponentDMesh.Attributes()->GetMaterialID();
for (int TID : ComponentDMesh.TriangleIndicesItr())
{
SetNewMaterialID(ComponentIdx, MatAttrib, TID, MaterialIDRemaps, AllMaterials);
}
if (ComponentIdx != SkipIndex)
{
FTransform3d ComponentToWorld = (FTransform3d)UE::ToolTarget::GetLocalToWorldTransform(Targets[ComponentIdx]);
MeshTransforms::ApplyTransform(ComponentDMesh, ComponentToWorld, true);
MeshTransforms::ApplyTransformInverse(ComponentDMesh, TargetToWorld, true);
Transforms[0] = ComponentToWorld;
if (TargetToWorld.GetRotation().IsIdentity() || TargetToWorld.GetScale3D().IsUniform())
{
// Inverse can be represented by a single FTransform3d
Transforms[1] = TargetToWorld.Inverse();
}
else
{
// Separate inverse into a rotation+translation part and a scale part
FQuat4d WorldToTargetR = TargetToWorld.GetRotation().Inverse();
FTransform3d WorldToTargetRT(WorldToTargetR, WorldToTargetR * (-TargetToWorld.GetTranslation()), FVector3d::One());
FTransform3d WorldToTargetS = FTransform3d::Identity;
WorldToTargetS.SetScale3D(FTransform3d::GetSafeScaleReciprocal(TargetToWorld.GetScale3D()));
Transforms[1] = WorldToTargetRT;
Transforms.Add(WorldToTargetS);
}
if (bOutputComponentSupportsCollision && UE::Geometry::ComponentTypeSupportsCollision(PrimitiveComponent))
{
UE::Geometry::AppendSimpleCollision(PrimitiveComponent, &SimpleCollision, Transforms);
}
}
else
{
if (bOutputComponentSupportsCollision && UE::Geometry::ComponentTypeSupportsCollision(PrimitiveComponent))
{
UE::Geometry::AppendSimpleCollision(PrimitiveComponent, &SimpleCollision, FTransform3d::Identity);
}
}
FDynamicMeshEditor Editor(&AccumulateDMesh);
FMeshIndexMappings IndexMapping;
Editor.AppendMesh(&ComponentDMesh, IndexMapping);
}
if (!bNeedColorAttr)
{
AccumulateDMesh.Attributes()->DisablePrimaryColors();
}
#if WITH_EDITOR
SlowTask.EnterProgressFrame(1);
#endif
FComponentMaterialSet NewMaterialSet;
NewMaterialSet.Materials = AllMaterials;
UE::ToolTarget::CommitDynamicMeshUpdate(Targets[SkipIndex], AccumulateDMesh, true, FConversionToMeshDescriptionOptions(), &NewMaterialSet);
// CommitDynamicMeshUpdate updates the materials for the underlying asset. However,
// it does not update the component itself, so address that now.
UE::ToolTarget::CommitMaterialSetUpdate(Targets[SkipIndex], NewMaterialSet, false);
if (bOutputComponentSupportsCollision)
{
UE::Geometry::SetSimpleCollision(UpdateComponent, &SimpleCollision, CollisionSettings);
}
// select the new actor
ToolSelectionUtil::SetNewActorSelection(GetToolManager(), SkipActor);
}
TArray<AActor*> Actors;
for (int Idx = 0; Idx < Targets.Num(); Idx++)
{
AActor* Actor = UE::ToolTarget::GetTargetActor(Targets[Idx]);
Actors.Add(Actor);
}
HandleSourceProperties->ApplyMethod(Actors, GetToolManager(), SkipActor);
GetToolManager()->EndUndoTransaction();
}
void UCombineMeshesTool::BuildCombinedMaterialSet(TArray<UMaterialInterface*>& NewMaterialsOut, TArray<TArray<int32>>& MaterialIDRemapsOut)
{
NewMaterialsOut.Reset();
TMap<UMaterialInterface*, int> KnownMaterials;
MaterialIDRemapsOut.SetNum(Targets.Num());
for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Targets[ComponentIdx]);
int32 NumMaterials = MaterialSet.Materials.Num();
for (int MaterialIdx = 0; MaterialIdx < NumMaterials; MaterialIdx++)
{
UMaterialInterface* Mat = MaterialSet.Materials[MaterialIdx];
int32 NewMaterialIdx = 0;
if (KnownMaterials.Contains(Mat) == false)
{
NewMaterialIdx = NewMaterialsOut.Num();
KnownMaterials.Add(Mat, NewMaterialIdx);
NewMaterialsOut.Add(Mat);
}
else
{
NewMaterialIdx = KnownMaterials[Mat];
}
MaterialIDRemapsOut[ComponentIdx].Add(NewMaterialIdx);
}
}
}
#undef LOCTEXT_NAMESPACE