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

426 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "CSGMeshesTool.h"
#include "MeshDescriptionToDynamicMesh.h"
#include "ToolSetupUtil.h"
#include "ModelingToolTargetUtil.h"
#include "BaseGizmos/TransformProxy.h"
#include "CompositionOps/BooleanMeshesOp.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "TargetInterfaces/MeshDescriptionProvider.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(CSGMeshesTool)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UCSGMeshesTool"
void UCSGMeshesTool::EnableTrimMode()
{
check(OriginalDynamicMeshes.Num() == 0); // must not have been initialized!
bTrimMode = true;
}
void UCSGMeshesTool::SetupProperties()
{
Super::SetupProperties();
if (bTrimMode)
{
TrimProperties = NewObject<UTrimMeshesToolProperties>(this);
TrimProperties->RestoreProperties(this);
AddToolPropertySource(TrimProperties);
TrimProperties->WatchProperty(TrimProperties->WhichMesh, [this](ETrimOperation)
{
UpdateGizmoVisibility();
UpdatePreviewsVisibility();
});
TrimProperties->WatchProperty(TrimProperties->bShowTrimmingMesh, [this](bool)
{
UpdatePreviewsVisibility();
});
TrimProperties->WatchProperty(TrimProperties->ColorOfTrimmingMesh, [this](FLinearColor)
{
UpdatePreviewsMaterial();
});
TrimProperties->WatchProperty(TrimProperties->OpacityOfTrimmingMesh, [this](float)
{
UpdatePreviewsMaterial();
});
SetToolDisplayName(LOCTEXT("TrimMeshesToolName", "Trim"));
GetToolManager()->DisplayMessage(
LOCTEXT("OnStartTrimTool", "Trim one mesh with another. Use the transform gizmos to tweak the positions of the input objects (can help to resolve errors/failures)"),
EToolMessageLevel::UserNotification);
}
else
{
CSGProperties = NewObject<UCSGMeshesToolProperties>(this);
CSGProperties->RestoreProperties(this);
AddToolPropertySource(CSGProperties);
CSGProperties->WatchProperty(CSGProperties->Operation, [this](ECSGOperation)
{
UpdateGizmoVisibility();
UpdatePreviewsVisibility();
});
CSGProperties->WatchProperty(CSGProperties->bShowSubtractedMesh, [this](bool)
{
UpdatePreviewsVisibility();
});
CSGProperties->WatchProperty(CSGProperties->SubtractedMeshColor, [this](FLinearColor)
{
UpdatePreviewsMaterial();
});
CSGProperties->WatchProperty(CSGProperties->SubtractedMeshOpacity, [this](float)
{
UpdatePreviewsMaterial();
});
SetToolDisplayName(LOCTEXT("CSGMeshesToolName", "Mesh Boolean"));
GetToolManager()->DisplayMessage(
LOCTEXT("OnStartTool", "Perform Boolean operations on the input meshes; any interior faces will be removed. Use the transform gizmos to modify the position and orientation of the input objects."),
EToolMessageLevel::UserNotification);
}
}
void UCSGMeshesTool::UpdatePreviewsMaterial()
{
if (!PreviewsGhostMaterial)
{
PreviewsGhostMaterial = ToolSetupUtil::GetSimpleCustomMaterial(GetToolManager(), FLinearColor::Black, .2);
}
FLinearColor Color;
float Opacity;
if (bTrimMode)
{
Color = TrimProperties->ColorOfTrimmingMesh;
Opacity = TrimProperties->OpacityOfTrimmingMesh;
}
else
{
Color = CSGProperties->SubtractedMeshColor;
Opacity = CSGProperties->SubtractedMeshOpacity;
}
PreviewsGhostMaterial->SetVectorParameterValue(TEXT("Color"), Color);
PreviewsGhostMaterial->SetScalarParameterValue(TEXT("Opacity"), Opacity);
}
void UCSGMeshesTool::UpdatePreviewsVisibility()
{
int32 ShowPreviewIdx = -1;
if (bTrimMode && TrimProperties->bShowTrimmingMesh)
{
ShowPreviewIdx = TrimProperties->WhichMesh == ETrimOperation::TrimA ? OriginalMeshPreviews.Num() - 1 : 0;
}
else if (!bTrimMode && CSGProperties->bShowSubtractedMesh)
{
if (CSGProperties->Operation == ECSGOperation::DifferenceAB)
{
ShowPreviewIdx = OriginalMeshPreviews.Num() - 1;
}
else if (CSGProperties->Operation == ECSGOperation::DifferenceBA)
{
ShowPreviewIdx = 0;
}
}
for (int32 MeshIdx = 0; MeshIdx < OriginalMeshPreviews.Num(); MeshIdx++)
{
OriginalMeshPreviews[MeshIdx]->SetVisible(ShowPreviewIdx == MeshIdx);
}
}
int32 UCSGMeshesTool::GetHiddenGizmoIndex() const
{
int32 ParentHiddenIndex = Super::GetHiddenGizmoIndex();
if (ParentHiddenIndex != -1)
{
return ParentHiddenIndex;
}
if (bTrimMode)
{
return TrimProperties->WhichMesh == ETrimOperation::TrimA ? 0 : 1;
}
else if (CSGProperties->Operation == ECSGOperation::DifferenceAB)
{
return 0;
}
else if (CSGProperties->Operation == ECSGOperation::DifferenceBA)
{
return 1;
}
else
{
return -1;
}
}
void UCSGMeshesTool::SaveProperties()
{
Super::SaveProperties();
if (bTrimMode)
{
TrimProperties->SaveProperties(this);
}
else
{
CSGProperties->SaveProperties(this);
}
}
void UCSGMeshesTool::ConvertInputsAndSetPreviewMaterials(bool bSetPreviewMesh)
{
OriginalDynamicMeshes.SetNum(Targets.Num());
FComponentMaterialSet AllMaterialSet;
TMap<UMaterialInterface*, int> KnownMaterials;
TArray<TArray<int>> MaterialRemap; MaterialRemap.SetNum(Targets.Num());
if (bTrimMode || !CSGProperties->bUseFirstMeshMaterials)
{
for (int ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
const FComponentMaterialSet ComponentMaterialSet = UE::ToolTarget::GetMaterialSet(Targets[ComponentIdx]);
for (UMaterialInterface* Mat : ComponentMaterialSet.Materials)
{
int* FoundMatIdx = KnownMaterials.Find(Mat);
int MatIdx;
if (FoundMatIdx)
{
MatIdx = *FoundMatIdx;
}
else
{
MatIdx = AllMaterialSet.Materials.Add(Mat);
KnownMaterials.Add(Mat, MatIdx);
}
MaterialRemap[ComponentIdx].Add(MatIdx);
}
}
}
else
{
AllMaterialSet = UE::ToolTarget::GetMaterialSet(Targets[0]);
for (int MatIdx = 0; MatIdx < AllMaterialSet.Materials.Num(); MatIdx++)
{
MaterialRemap[0].Add(MatIdx);
}
for (int ComponentIdx = 1; ComponentIdx < Targets.Num(); ComponentIdx++)
{
MaterialRemap[ComponentIdx].Init(0, Cast<IMaterialProvider>(Targets[ComponentIdx])->GetNumMaterials());
}
}
UpdatePreviewsMaterial();
for (int ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++)
{
OriginalDynamicMeshes[ComponentIdx] = MakeShared<FDynamicMesh3, ESPMode::ThreadSafe>();
*OriginalDynamicMeshes[ComponentIdx] = UE::ToolTarget::GetDynamicMeshCopy(Targets[ComponentIdx]);
// ensure materials and attributes are always enabled
OriginalDynamicMeshes[ComponentIdx]->EnableAttributes();
OriginalDynamicMeshes[ComponentIdx]->Attributes()->EnableMaterialID();
FDynamicMeshMaterialAttribute* MaterialIDs = OriginalDynamicMeshes[ComponentIdx]->Attributes()->GetMaterialID();
for (int TID : OriginalDynamicMeshes[ComponentIdx]->TriangleIndicesItr())
{
MaterialIDs->SetValue(TID, MaterialRemap[ComponentIdx][MaterialIDs->GetValue(TID)]);
}
if (bSetPreviewMesh)
{
UPreviewMesh* OriginalMeshPreview = OriginalMeshPreviews.Add_GetRef(NewObject<UPreviewMesh>());
OriginalMeshPreview->CreateInWorld(GetTargetWorld(), (FTransform) UE::ToolTarget::GetLocalToWorldTransform(Targets[ComponentIdx]));
OriginalMeshPreview->UpdatePreview(OriginalDynamicMeshes[ComponentIdx].Get());
OriginalMeshPreview->SetMaterial(0, PreviewsGhostMaterial);
OriginalMeshPreview->SetVisible(false);
TransformProxies[ComponentIdx]->AddComponent(OriginalMeshPreview->GetRootComponent());
}
}
Preview->ConfigureMaterials(AllMaterialSet.Materials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager()));
}
void UCSGMeshesTool::SetPreviewCallbacks()
{
DrawnLineSet = NewObject<ULineSetComponent>(Preview->PreviewMesh->GetRootComponent());
DrawnLineSet->SetupAttachment(Preview->PreviewMesh->GetRootComponent());
DrawnLineSet->SetLineMaterial(ToolSetupUtil::GetDefaultLineComponentMaterial(GetToolManager()));
DrawnLineSet->RegisterComponent();
Preview->OnOpCompleted.AddLambda(
[this](const FDynamicMeshOperator* Op)
{
const FBooleanMeshesOp* BooleanOp = (const FBooleanMeshesOp*)(Op);
CreatedBoundaryEdges = BooleanOp->GetCreatedBoundaryEdges();
}
);
Preview->OnMeshUpdated.AddLambda(
[this](const UMeshOpPreviewWithBackgroundCompute*)
{
GetToolManager()->PostInvalidation();
UpdateVisualization();
}
);
}
void UCSGMeshesTool::UpdateVisualization()
{
FColor BoundaryEdgeColor(240, 15, 15);
float BoundaryEdgeThickness = 2.0;
float BoundaryEdgeDepthBias = 2.0f;
const FDynamicMesh3* TargetMesh = Preview->PreviewMesh->GetPreviewDynamicMesh();
FVector3d A, B;
DrawnLineSet->Clear();
if (!bTrimMode && CSGProperties->bShowNewBoundaries)
{
for (int EID : CreatedBoundaryEdges)
{
TargetMesh->GetEdgeV(EID, A, B);
DrawnLineSet->AddLine((FVector)A, (FVector)B, BoundaryEdgeColor, BoundaryEdgeThickness, BoundaryEdgeDepthBias);
}
}
}
TUniquePtr<FDynamicMeshOperator> UCSGMeshesTool::MakeNewOperator()
{
TUniquePtr<FBooleanMeshesOp> BooleanOp = MakeUnique<FBooleanMeshesOp>();
BooleanOp->bTrimMode = bTrimMode;
if (bTrimMode)
{
BooleanOp->WindingThreshold = TrimProperties->WindingThreshold;
BooleanOp->TrimOperation = TrimProperties->WhichMesh;
BooleanOp->TrimSide = TrimProperties->TrimSide;
BooleanOp->bAttemptFixHoles = false;
BooleanOp->bTryCollapseExtraEdges = false;
}
else
{
BooleanOp->WindingThreshold = CSGProperties->WindingThreshold;
BooleanOp->CSGOperation = CSGProperties->Operation;
BooleanOp->bAttemptFixHoles = CSGProperties->bTryFixHoles;
BooleanOp->bTryCollapseExtraEdges = CSGProperties->bTryCollapseEdges;
}
check(OriginalDynamicMeshes.Num() == 2);
check(Targets.Num() == 2);
BooleanOp->Transforms.SetNum(2);
BooleanOp->Meshes.SetNum(2);
for (int Idx = 0; Idx < 2; Idx++)
{
BooleanOp->Meshes[Idx] = OriginalDynamicMeshes[Idx];
BooleanOp->Transforms[Idx] = TransformProxies[Idx]->GetTransform();
}
return BooleanOp;
}
void UCSGMeshesTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCSGMeshesToolProperties, bUseFirstMeshMaterials)))
{
if (!AreAllTargetsValid())
{
GetToolManager()->DisplayMessage(LOCTEXT("InvalidTargets", "Target meshes are no longer valid"), EToolMessageLevel::UserWarning);
return;
}
ConvertInputsAndSetPreviewMaterials(false);
Preview->InvalidateResult();
}
else if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCSGMeshesToolProperties, bShowNewBoundaries)))
{
GetToolManager()->PostInvalidation();
UpdateVisualization();
}
else
{
//TODO: UBaseCreateFromSelectedTool::OnPropertyModified below calls Preview->InvalidateResult() which triggers
//expensive recompute of the boolean operator. We should rethink which code is responsible for invalidating the
//preview and make it consistent since some of the code calls Preview->InvalidateResult() manually.
bool bVisualUpdate = false;
if (bTrimMode)
{
bVisualUpdate = Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UTrimMeshesToolProperties, bShowTrimmingMesh) ||
Property->GetFName() == GET_MEMBER_NAME_CHECKED(UTrimMeshesToolProperties, OpacityOfTrimmingMesh) ||
Property->GetFName() == GET_MEMBER_NAME_CHECKED(UTrimMeshesToolProperties, ColorOfTrimmingMesh) ||
Property->GetFName() == FName("R") ||
Property->GetFName() == FName("G") ||
Property->GetFName() == FName("B") ||
Property->GetFName() == FName("A"));
}
else
{
bVisualUpdate = Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCSGMeshesToolProperties, bShowSubtractedMesh) ||
Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCSGMeshesToolProperties, SubtractedMeshColor) ||
Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCSGMeshesToolProperties, SubtractedMeshOpacity) ||
Property->GetFName() == FName("R") ||
Property->GetFName() == FName("G") ||
Property->GetFName() == FName("B") ||
Property->GetFName() == FName("A"));
}
// If the property that was changed only affects the visuals, we do not need to recalculate the output
// of FBooleanMeshesOp
if (bVisualUpdate == false)
{
Super::OnPropertyModified(PropertySet, Property);
}
}
}
FString UCSGMeshesTool::GetCreatedAssetName() const
{
if (bTrimMode)
{
return TEXT("Trim");
}
else
{
return TEXT("Boolean");
}
}
FText UCSGMeshesTool::GetActionName() const
{
if (bTrimMode)
{
return LOCTEXT("TrimActionName", "Trim Meshes");
}
else
{
return LOCTEXT("BooleanActionName", "Boolean Meshes");
}
}
void UCSGMeshesTool::OnShutdown(EToolShutdownType ShutdownType)
{
Super::OnShutdown(ShutdownType);
for (UPreviewMesh* MeshPreview : OriginalMeshPreviews)
{
MeshPreview->SetVisible(false);
MeshPreview->Disconnect();
MeshPreview = nullptr;
}
}
#undef LOCTEXT_NAMESPACE