// Copyright Epic Games, Inc. All Rights Reserved. #include "MirrorTool.h" #include "ModelingObjectsCreationAPI.h" #include "BaseBehaviors/KeyAsModifierInputBehavior.h" #include "BaseBehaviors/SingleClickBehavior.h" #include "CompositionOps/MirrorOp.h" #include "Drawing/MeshDebugDrawing.h" #include "DynamicMeshToMeshDescription.h" #include "InteractiveToolManager.h" #include "MeshDescriptionToDynamicMesh.h" #include "Misc/MessageDialog.h" #include "ToolBuilderUtil.h" #include "ToolSetupUtil.h" #include "TargetInterfaces/MaterialProvider.h" #include "TargetInterfaces/MeshDescriptionCommitter.h" #include "TargetInterfaces/MeshDescriptionProvider.h" #include "TargetInterfaces/PrimitiveComponentBackedTarget.h" #include "ToolTargetManager.h" #include "ExplicitUseGeometryMathTypes.h" // using UE::Geometry::(math types) using namespace UE::Geometry; #define LOCTEXT_NAMESPACE "UMirrorTool" //Tool builder functions const FToolTargetTypeRequirements& UMirrorToolBuilder::GetTargetRequirements() const { static FToolTargetTypeRequirements TypeRequirements({ UMaterialProvider::StaticClass(), UMeshDescriptionCommitter::StaticClass(), UMeshDescriptionProvider::StaticClass(), UPrimitiveComponentBackedTarget::StaticClass() }); return TypeRequirements; } bool UMirrorToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const { return SceneState.TargetManager->CountSelectedAndTargetable(SceneState, GetTargetRequirements()) > 0; } UInteractiveTool* UMirrorToolBuilder::BuildTool(const FToolBuilderState& SceneState) const { UMirrorTool* NewTool = NewObject(SceneState.ToolManager); TArray> Targets = SceneState.TargetManager->BuildAllSelectedTargetable(SceneState, GetTargetRequirements()); NewTool->SetTargets(MoveTemp(Targets)); NewTool->SetWorld(SceneState.World); return NewTool; } // Operator factory TUniquePtr UMirrorOperatorFactory::MakeNewOperator() { TUniquePtr MirrorOp = MakeUnique(); // Set up inputs and settings MirrorOp->OriginalMesh = MirrorTool->MeshesToMirror[ComponentIndex]->GetMesh(); MirrorOp->bAppendToOriginal = MirrorTool->Settings->OperationMode == EMirrorOperationMode::MirrorAndAppend; MirrorOp->bCropFirst = MirrorTool->Settings->bCropAlongMirrorPlaneFirst; MirrorOp->bWeldAlongPlane = MirrorTool->Settings->bWeldVerticesOnMirrorPlane; MirrorOp->bAllowBowtieVertexCreation = MirrorTool->Settings->bAllowBowtieVertexCreation; FTransform LocalToWorld = MirrorTool->TargetComponentInterface(ComponentIndex)->GetWorldTransform(); MirrorOp->SetTransform(LocalToWorld); // We also need WorldToLocal. Threshold the LocalToWorld scaling transform so we can get the inverse. FVector LocalToWorldScale = LocalToWorld.GetScale3D(); for (int i = 0; i < 3; i++) { float DimScale = FMathf::Abs(LocalToWorldScale[i]); float Tolerance = KINDA_SMALL_NUMBER; if (DimScale < Tolerance) { LocalToWorldScale[i] = Tolerance * FMathf::SignNonZero(LocalToWorldScale[i]); } } LocalToWorld.SetScale3D(LocalToWorldScale); UE::Geometry::FTransform3d WorldToLocal = UE::Geometry::FTransform3d(LocalToWorld).Inverse(); // Now we can get the plane parameters in local space. MirrorOp->LocalPlaneOrigin = WorldToLocal.TransformPosition(MirrorTool->MirrorPlaneOrigin);; FVector3d WorldNormal = MirrorTool->MirrorPlaneNormal; MirrorOp->LocalPlaneNormal = WorldToLocal.TransformNormal(MirrorTool->MirrorPlaneNormal); return MirrorOp; } // Tool property functions void UMirrorToolActionPropertySet::PostAction(EMirrorToolAction Action) { if (ParentTool.IsValid()) { ParentTool->RequestAction(Action); } } // Tool itself UMirrorTool::UMirrorTool() { } bool UMirrorTool::CanAccept() const { for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews) { if (!Preview->HaveValidResult()) { return false; } } return Super::CanAccept(); } void UMirrorTool::SetWorld(UWorld* World) { TargetWorld = World; } void UMirrorTool::OnPropertyModified(UObject* PropertySet, FProperty* Property) { // Editing the "show preview" option changes whether we need to be displaying the preview or the original mesh. if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UMirrorToolProperties, bShowPreview))) { for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++) { TargetComponentInterface(ComponentIdx)->SetOwnerVisibility(!Settings->bShowPreview); } for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews) { Preview->SetVisibility(Settings->bShowPreview); } } // Regardless of what changed, update the previews. for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews) { Preview->InvalidateResult(); } } void UMirrorTool::OnTick(float DeltaTime) { // Deal with any buttons that may have been clicked if (PendingAction != EMirrorToolAction::NoAction) { ApplyAction(PendingAction); PendingAction = EMirrorToolAction::NoAction; } if (PlaneMechanic != nullptr) { // Update snapping behavior based on modifier key. PlaneMechanic->SetEnableGridSnaping(Settings->bSnapToWorldGrid ^ bSnappingToggle); PlaneMechanic->Tick(DeltaTime); } for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews) { Preview->Tick(DeltaTime); } } void UMirrorTool::Render(IToolsContextRenderAPI* RenderAPI) { // Have the plane draw itself. PlaneMechanic->Render(RenderAPI); } void UMirrorTool::Setup() { UInteractiveTool::Setup(); SetToolDisplayName(LOCTEXT("ToolName", "Mirror")); GetToolManager()->DisplayMessage( LOCTEXT("OnStartMirrorTool", "Mirror one or more meshes across a plane. Grid snapping behavior is swapped while the shift key is down. The plane can be set by using the preset buttons, moving the gizmo, or ctrl+clicking on a spot on the original mesh."), EToolMessageLevel::UserNotification); // Set up the properties Settings = NewObject(this, TEXT("Mirror Tool Settings")); Settings->RestoreProperties(this); AddToolPropertySource(Settings); ToolActions = NewObject(this); ToolActions->Initialize(this); AddToolPropertySource(ToolActions); CheckAndDisplayWarnings(); // Fill in the MeshesToMirror array with suitably converted meshes. for (int i = 0; i < Targets.Num(); i++) { IPrimitiveComponentBackedTarget* TargetComponent = TargetComponentInterface(i); IMeshDescriptionProvider* TargetMeshProvider = TargetMeshProviderInterface(i); // Convert into dynamic mesh TSharedPtr DynamicMesh = MakeShared(); FMeshDescriptionToDynamicMesh Converter; Converter.Convert(TargetMeshProvider->GetMeshDescription(), *DynamicMesh); // Wrap the dynamic mesh in a replacement change target UDynamicMeshReplacementChangeTarget* WrappedTarget = MeshesToMirror.Add_GetRef(NewObject()); // Set callbacks so previews are invalidated on undo/redo changing the meshes WrappedTarget->SetMesh(DynamicMesh); WrappedTarget->OnMeshChanged.AddLambda([this, i]() { Previews[i]->InvalidateResult(); }); } // Set the visibility of the StaticMeshComponents depending on whether we are showing them or the preview. for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++) { TargetComponentInterface(ComponentIdx)->SetOwnerVisibility(!Settings->bShowPreview); } // Initialize the PreviewMesh and BackgroundCompute objects SetupPreviews(); // Update the bounding box of the meshes. CombinedBounds.Init(); for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++) { FVector ComponentOrigin, ComponentExtents; TargetComponentInterface(ComponentIdx)->GetOwnerActor()->GetActorBounds(false, ComponentOrigin, ComponentExtents); CombinedBounds += FBox::BuildAABB(ComponentOrigin, ComponentExtents); } // Set the initial mirror plane. We want the plane to start in the middle if we're doing a simple // mirror (i.e., not appending, and not cropping). Otherwise, we want the plane to start to one side. MirrorPlaneOrigin = (FVector3d)CombinedBounds.GetCenter(); MirrorPlaneNormal = FVector3d(0, -1, 0); if (Settings->OperationMode == EMirrorOperationMode::MirrorAndAppend || Settings->bCropAlongMirrorPlaneFirst) { MirrorPlaneOrigin.Y = CombinedBounds.Min.Y; } // Set up the mirror plane mechanic, which manages the gizmo PlaneMechanic = NewObject(this); PlaneMechanic->Setup(this); PlaneMechanic->Initialize(TargetWorld, FFrame3d(MirrorPlaneOrigin, MirrorPlaneNormal)); // Have the plane mechanic update things properly PlaneMechanic->OnPlaneChanged.AddLambda([this]() { MirrorPlaneNormal = PlaneMechanic->Plane.Rotation.AxisZ(); MirrorPlaneOrigin = PlaneMechanic->Plane.Origin; for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews) { Preview->InvalidateResult(); } }); // Modify the Ctrl+click set plane behavior to respond to our CtrlClickBehavior property PlaneMechanic->SetPlaneCtrlClickBehaviorTarget->OnClickedPositionFunc = [this](const FHitResult& Hit) { bool bIgnoreNormal = (Settings->CtrlClickBehavior == EMirrorCtrlClickBehavior::Reposition); PlaneMechanic->SetDrawPlaneFromWorldPos((FVector3d)Hit.ImpactPoint, (FVector3d)Hit.ImpactNormal, bIgnoreNormal); }; // Also include the original components in the ctrl+click hit testing even though we made them // invisible, since we want to be able to reposition the plane onto the original mesh. for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++) { IPrimitiveComponentBackedTarget* TargetComponent = TargetComponentInterface(ComponentIdx); PlaneMechanic->SetPlaneCtrlClickBehaviorTarget->InvisibleComponentsToHitTest.Add(TargetComponent->GetOwnerComponent()); } // Add modifier button for snapping UKeyAsModifierInputBehavior* SnapToggleBehavior = NewObject(); SnapToggleBehavior->Initialize(this, SnappingToggleModifierId, FInputDeviceState::IsShiftKeyDown); AddInputBehavior(SnapToggleBehavior); // Start the preview calculations for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews) { Preview->InvalidateResult(); } } void UMirrorTool::SetupPreviews() { // Create a preview (with an op) for each selected component. int32 NumMeshes = MeshesToMirror.Num(); for (int32 PreviewIndex = 0; PreviewIndex < NumMeshes; ++PreviewIndex) { UMirrorOperatorFactory* MirrorOpCreator = NewObject(); MirrorOpCreator->MirrorTool = this; MirrorOpCreator->ComponentIndex = PreviewIndex; UMeshOpPreviewWithBackgroundCompute* Preview = Previews.Add_GetRef( NewObject(MirrorOpCreator, "Preview")); Preview->Setup(TargetWorld, MirrorOpCreator); Preview->PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated); FComponentMaterialSet MaterialSet; TargetMaterialInterface(PreviewIndex)->GetMaterialSet(MaterialSet); Preview->ConfigureMaterials(MaterialSet.Materials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager())); // Set initial preview to unprocessed mesh, so that things don't disappear initially Preview->PreviewMesh->UpdatePreview(MeshesToMirror[PreviewIndex]->GetMesh().Get()); Preview->PreviewMesh->SetTransform(TargetComponentInterface(PreviewIndex)->GetWorldTransform()); Preview->SetVisibility(Settings->bShowPreview); } } void UMirrorTool::CheckAndDisplayWarnings() { // We can have more than one warning, which makes this a bit more work. FText SameSourceWarning; FText NonUniformScaleWarning; // See if any of the selected components have the same source. TArray MapToFirstOccurrences; bool bAnyHaveSameSource = GetMapToSharedSourceData(MapToFirstOccurrences); if (bAnyHaveSameSource) { SameSourceWarning = LOCTEXT("MirrorMultipleAssetsWithSameSource", "WARNING: Multiple meshes in your selection use the same source asset! Only the \"Create New Assets\" save mode is supported."); // We could forcefully set the save mode to CreateNewAssets, but the setting will persist on new invocations // of the tool, which may surprise the user. So, it's up to them to set it. } // See if any of the selected components have a nonuniform scaling transform. IPrimitiveComponentBackedTarget* NonUniformScalingTarget = nullptr; for (int32 i = 0; i < Targets.Num(); ++i) { FVector Scaling = TargetComponentInterface(i)->GetWorldTransform().GetScale3D(); if (Scaling.X != Scaling.Y || Scaling.Y != Scaling.Z) { NonUniformScalingTarget = TargetComponentInterface(i); break; } } if (NonUniformScalingTarget) { NonUniformScaleWarning = FText::Format( LOCTEXT("MirrorNonUniformScaledAsset", "WARNING: The item \"{0}\" has a non-uniform scaling transform. This is not supported because mirroring acts on the underlying mesh, and mirroring is not commutative with non-uniform scaling. Consider deforming the mesh rather than scaling it non-uniformly."), FText::FromString(NonUniformScalingTarget->GetOwnerActor()->GetName())); } if (bAnyHaveSameSource && NonUniformScalingTarget) { // Concatenates the two warnings with an extra line in between. GetToolManager()->DisplayMessage(FText::Format(LOCTEXT("CombinedWarnings", "{0}\n\n{1}"), SameSourceWarning, NonUniformScaleWarning), EToolMessageLevel::UserWarning); } else if (bAnyHaveSameSource) { GetToolManager()->DisplayMessage(SameSourceWarning, EToolMessageLevel::UserWarning); } else if (NonUniformScalingTarget) { GetToolManager()->DisplayMessage(NonUniformScaleWarning, EToolMessageLevel::UserWarning); } } void UMirrorTool::Shutdown(EToolShutdownType ShutdownType) { Settings->SaveProperties(this); PlaneMechanic->Shutdown(); // Restore (unhide) the source meshes for (int32 ComponentIdx = 0; ComponentIdx < Targets.Num(); ComponentIdx++) { TargetComponentInterface(ComponentIdx)->SetOwnerVisibility(true); } // Swap in results, if appropriate if (ShutdownType == EToolShutdownType::Accept) { // Gather results TArray Results; for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews) { Results.Emplace(Preview->Shutdown()); } // Convert to output. This will also edit the selection. GenerateAsset(Results); } else { for (UMeshOpPreviewWithBackgroundCompute* Preview : Previews) { Preview->Cancel(); } } } void UMirrorTool::GenerateAsset(const TArray& Results) { if (Results.Num() == 0) { return; } GetToolManager()->BeginUndoTransaction(LOCTEXT("MirrorToolTransactionName", "Mirror Tool")); ensure(Results.Num() > 0); int32 NumSourceMeshes = MeshesToMirror.Num(); // check if we entirely cut away any meshes bool bWantToDestroy = false; for (int OrigMeshIdx = 0; OrigMeshIdx < NumSourceMeshes; OrigMeshIdx++) { if (Results[OrigMeshIdx].Mesh->TriangleCount() == 0) { bWantToDestroy = true; break; } } // if so ask user what to do if (bWantToDestroy) { FText Title = LOCTEXT("MirrorDestroyTitle", "Delete mesh components?"); EAppReturnType::Type Ret = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("PlaneCutDestroyQuestion", "The mirror plane cropping has entirely cut away at least one mesh. Actually destroy these mesh components?"), &Title); if (Ret == EAppReturnType::No) { bWantToDestroy = false; } } // Properly deal with each result, setting up the selection at the same time. FSelectedOjectsChangeList NewSelection; NewSelection.ModificationType = ESelectedObjectsModificationType::Replace; for (int OrigMeshIdx = 0; OrigMeshIdx < NumSourceMeshes; OrigMeshIdx++) { IPrimitiveComponentBackedTarget* TargetComponent = TargetComponentInterface(OrigMeshIdx); FDynamicMesh3* Mesh = Results[OrigMeshIdx].Mesh.Get(); check(Mesh != nullptr); if (Mesh->TriangleCount() == 0) { if (bWantToDestroy) { TargetComponent->GetOwnerComponent()->DestroyComponent(); } continue; } else if (Settings->SaveMode == EMirrorSaveMode::UpdateAssets) { NewSelection.Actors.Add(TargetComponent->GetOwnerActor()); TargetMeshCommitterInterface(OrigMeshIdx)->CommitMeshDescription([&Mesh](const IMeshDescriptionCommitter::FCommitterParams& CommitParams) { FDynamicMeshToMeshDescription Converter; Converter.Convert(Mesh, *CommitParams.MeshDescriptionOut); }); } else { // Build array of materials from the original. TArray Materials; IMaterialProvider* TargetMaterial = TargetMaterialInterface(OrigMeshIdx); for (int MaterialIdx = 0, NumMaterials = TargetMaterial->GetNumMaterials(); MaterialIdx < NumMaterials; MaterialIdx++) { Materials.Add(TargetMaterial->GetMaterial(MaterialIdx)); } FCreateMeshObjectParams NewMeshObjectParams; NewMeshObjectParams.TargetWorld = TargetWorld; NewMeshObjectParams.Transform = (FTransform)Results[OrigMeshIdx].Transform; NewMeshObjectParams.BaseName = TEXT("Mirror"); NewMeshObjectParams.Materials = Materials; NewMeshObjectParams.SetMesh(Mesh); FCreateMeshObjectResult Result = UE::Modeling::CreateMeshObject(GetToolManager(), MoveTemp(NewMeshObjectParams)); if (Result.IsOK() && Result.NewActor != nullptr) { NewSelection.Actors.Add(Result.NewActor); } // Remove the original actor TargetComponent->GetOwnerComponent()->DestroyComponent(); } } // Update the selection if (NewSelection.Actors.Num() > 0) { GetToolManager()->RequestSelectionChange(NewSelection); } GetToolManager()->EndUndoTransaction(); } // Action support void UMirrorTool::RequestAction(EMirrorToolAction ActionType) { if (PendingAction == EMirrorToolAction::NoAction) { PendingAction = ActionType; } } void UMirrorTool::ApplyAction(EMirrorToolAction ActionType) { FVector3d ShiftedPlaneOrigin = (FVector3d)CombinedBounds.GetCenter(); if (ActionType == EMirrorToolAction::ShiftToCenter) { // We keep the same orientation here PlaneMechanic->SetDrawPlaneFromWorldPos(ShiftedPlaneOrigin, FVector3d(), true); } else { // We still start from the center, but adjust one of the coordinates and set direction. FVector3d DirectionVector; switch (ActionType) { case EMirrorToolAction::Left: ShiftedPlaneOrigin.Y = CombinedBounds.Min.Y; DirectionVector = FVector3d(0, -1.0, 0); break; case EMirrorToolAction::Right: ShiftedPlaneOrigin.Y = CombinedBounds.Max.Y; DirectionVector = FVector3d(0, 1.0, 0); break; case EMirrorToolAction::Up: ShiftedPlaneOrigin.Z = CombinedBounds.Max.Z; DirectionVector = FVector3d(0, 0, 1.0); break; case EMirrorToolAction::Down: ShiftedPlaneOrigin.Z = CombinedBounds.Min.Z; DirectionVector = FVector3d(0, 0, -1.0); break; case EMirrorToolAction::Forward: ShiftedPlaneOrigin.X = CombinedBounds.Max.X; DirectionVector = FVector3d(1.0, 0, 0); break; case EMirrorToolAction::Backward: ShiftedPlaneOrigin.X = CombinedBounds.Min.X; DirectionVector = FVector3d(-1.0, 0, 0); break; } // The user can optionally have the button change the direction only if (Settings->bButtonsOnlyChangeOrientation) { ShiftedPlaneOrigin = MirrorPlaneOrigin; // Keeps the same } PlaneMechanic->SetDrawPlaneFromWorldPos(ShiftedPlaneOrigin, DirectionVector, false); } } void UMirrorTool::OnUpdateModifierState(int ModifierID, bool bIsOn) { if (ModifierID == SnappingToggleModifierId) { bSnappingToggle = bIsOn; } } #undef LOCTEXT_NAMESPACE