You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#ROBOMERGE-AUTHOR: michael.balzer #ROBOMERGE-SOURCE: CL 18227685 in //UE5/Release-5.0/... via CL 18229350 #ROBOMERGE-BOT: STARSHIP (Release-Engine-Staging -> Release-Engine-Test) (v895-18170469) #ROBOMERGE[STARSHIP]: UE5-Main [CL 18231457 by michael balzer in ue5-release-engine-test branch]
871 lines
30 KiB
C++
871 lines
30 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "BspConversionTool.h"
|
|
|
|
#include "ModelingObjectsCreationAPI.h"
|
|
#include "Selection/ToolSelectionUtil.h"
|
|
#include "AssetSelection.h"
|
|
#include "ComponentSourceInterfaces.h"
|
|
#include "DynamicMesh/DynamicMesh3.h"
|
|
#include "Editor.h"
|
|
#include "Editor/EditorEngine.h"
|
|
#include "Engine/StaticMeshActor.h"
|
|
#include "InteractiveToolManager.h"
|
|
#include "MeshDescriptionToDynamicMesh.h"
|
|
#include "DynamicMesh/MeshTransforms.h"
|
|
#include "Model.h"
|
|
#include "Operations/MeshBoolean.h"
|
|
#include "StaticMeshAttributes.h"
|
|
#include "ToolBuilderUtil.h"
|
|
#include "Tools/EditorComponentSourceFactory.h"
|
|
#include "ToolSetupUtil.h"
|
|
#include "Engine/Selection.h"
|
|
|
|
#include "Logging/MessageLog.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Logging/TokenizedMessage.h"
|
|
|
|
using namespace UE::Geometry;
|
|
|
|
#define LOCTEXT_NAMESPACE "UBspConversionTool"
|
|
|
|
// Forward declarations of local functions
|
|
void ConvertBrushesToDynamicMesh(TArray<ABrush*>& BrushesToConvert, FDynamicMesh3& OutputMesh,
|
|
TArray<UMaterialInterface*>& OutputMaterials);
|
|
void ApplyStaticMeshBooleanOperation(
|
|
FDynamicMesh3& MeshA, const UE::Geometry::FTransform3d& TransformA, const TArray<UMaterialInterface*>& MaterialsA,
|
|
FDynamicMesh3& MeshB, const UE::Geometry::FTransform3d& TransformB, const TArray<UMaterialInterface*>& MaterialsB,
|
|
FDynamicMesh3& OutputMesh, UE::Geometry::FTransform3d& OutputTransform, TArray<UMaterialInterface*>& OutputMaterials,
|
|
FMeshBoolean::EBooleanOp Operation);
|
|
FText GetBrushGeometryErrorMessage(ABrush* Brush);
|
|
|
|
// Element stored in CachedBrushes: the resulting dynamic mesh and the materials array
|
|
typedef TPair<TSharedPtr<const FDynamicMesh3>, TSharedPtr<const TArray<UMaterialInterface*>>> FCachedResult;
|
|
|
|
|
|
// Tool builder functions
|
|
|
|
bool UBspConversionToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
|
|
{
|
|
// We allow the tool to be built even if nothing is selected because the tool has a "select all" option.
|
|
// We just need to be able to get the place to save assets.
|
|
return true;
|
|
}
|
|
|
|
UInteractiveTool* UBspConversionToolBuilder::BuildTool(const FToolBuilderState& SceneState) const
|
|
{
|
|
UBspConversionTool* NewTool = NewObject<UBspConversionTool>(SceneState.ToolManager);
|
|
NewTool->SetWorld(SceneState.World);
|
|
return NewTool;
|
|
}
|
|
|
|
|
|
// Tool property functions
|
|
|
|
void UBspConversionToolActionPropertySet::PostAction(EBspConversionToolAction Action)
|
|
{
|
|
if (ParentTool.IsValid())
|
|
{
|
|
ParentTool->RequestAction(Action);
|
|
}
|
|
}
|
|
|
|
|
|
// Tool itself
|
|
|
|
UBspConversionTool::UBspConversionTool()
|
|
{
|
|
}
|
|
|
|
void UBspConversionTool::RegisterActions(FInteractiveToolActionSet& ActionSet)
|
|
{
|
|
}
|
|
|
|
void UBspConversionTool::Render(IToolsContextRenderAPI* RenderAPI)
|
|
{
|
|
}
|
|
|
|
bool UBspConversionTool::CanAccept() const
|
|
{
|
|
// We precompute this value and update it at various editor events
|
|
return bCanAccept;
|
|
}
|
|
|
|
void UBspConversionTool::Setup()
|
|
{
|
|
UInteractiveTool::Setup();
|
|
|
|
// Link in the tool properties and actions that will be displayed in the side panel
|
|
Settings = NewObject<UBspConversionToolProperties>();
|
|
Settings->RestoreProperties(this);
|
|
AddToolPropertySource(Settings);
|
|
|
|
ToolActions = NewObject<UBspConversionToolActionPropertySet>(this);
|
|
ToolActions->Initialize(this);
|
|
AddToolPropertySource(ToolActions);
|
|
|
|
SetToolDisplayName(LOCTEXT("ToolName", "Convert BSP"));
|
|
// Give a description to put in the side panel
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("OnStartTool", "Convert geometry brushes (also known as BSP brushes) into a single static mesh. \"Convert then Combine\" first converts the individual brushes and performs boolean operations on the resulting meshes (this requires individual brushes to have manifold geometry. \"Combine then Convert\" is the old functionality, combining brushes, then converting them."),
|
|
EToolMessageLevel::UserNotification);
|
|
|
|
// We write out an empty warning message to make the sidebar look unchanged if we write out a warning message and then
|
|
// change it to be an empty string to clear it.
|
|
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning);
|
|
|
|
// See if we have valid targets selected
|
|
bCanAccept = AtLeastOneValidConversionTarget();
|
|
|
|
// Set up the preview mesh
|
|
PreviewMesh = NewObject<UPreviewMesh>(this, "Preview Mesh");
|
|
PreviewMesh->CreateInWorld(TargetWorld, FTransform::Identity);
|
|
|
|
// The material is set to have opacity 0, so that only the wireframe shows.
|
|
PreviewMesh->SetOverrideRenderMaterial(ToolSetupUtil::GetSimpleCustomMaterial(GetToolManager(), FLinearColor(), 0.0f));
|
|
PreviewMesh->EnableWireframe(Settings->bShowPreview);
|
|
|
|
// Register with any editor events that will require us to recompute the preview or update bCanAccept
|
|
USelection::SelectionChangedEvent.AddUObject(this, &UBspConversionTool::OnEditorSelectionChanged);
|
|
GEngine->OnLevelActorListChanged().AddUObject(this, &UBspConversionTool::OnEditorLevelActorListChanged);
|
|
GEngine->OnActorMoved().AddUObject(this, &UBspConversionTool::OnEditorActorMoved);
|
|
|
|
// Do some precomputing if we show a preview.
|
|
if (bCanAccept && Settings->bShowPreview)
|
|
{
|
|
CompareAndUpdateConversionTargets();
|
|
|
|
FText ErrorMessage;
|
|
// When we do the actual computation, we may find new errors that prevent us from accepting
|
|
bCanAccept = ComputeAndUpdatePreviewMesh(&ErrorMessage);
|
|
|
|
if (!ErrorMessage.IsEmpty())
|
|
{
|
|
GetToolManager()->DisplayMessage(ErrorMessage, EToolMessageLevel::UserWarning);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if there is at least one valid conversion actor selected (an explicitly selected brush, or a volume if
|
|
* those are included).
|
|
*
|
|
* Notes:
|
|
* - An explictly selected subtractive brush is valid, as users may want to delete subtractive brushes through the tool.
|
|
* - Even with an additive brush, a resulting mesh could still end up empty, for instance if a brush was fully inside a subtractive one.
|
|
* - Does not check if targets have valid geometry for the "convert then combine" path (for mesh boolean operations),
|
|
* as that currently requires doing an actual conversion.
|
|
*/
|
|
bool UBspConversionTool::AtLeastOneValidConversionTarget() const
|
|
{
|
|
for (FSelectionIterator Iter(*GEditor->GetSelectedActors()); Iter; ++Iter)
|
|
{
|
|
ABrush* Brush = Cast<ABrush>(*Iter);
|
|
if (IsValidConversionTarget(Brush))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the passed in pointer is a valid convertible brush. Safe to call with nullptr.
|
|
* Considers Settings->bIncludeVolumes.
|
|
*/
|
|
bool UBspConversionTool::IsValidConversionTarget(const ABrush* Brush) const
|
|
{
|
|
return IsValid(Brush)
|
|
&& (!Brush->IsVolumeBrush() || Settings->bIncludeVolumes)
|
|
&& (Brush->BrushType == EBrushType::Brush_Add || Brush->BrushType == EBrushType::Brush_Subtract);
|
|
}
|
|
|
|
void UBspConversionTool::Shutdown(EToolShutdownType ShutdownType)
|
|
{
|
|
if (ShutdownType == EToolShutdownType::Accept)
|
|
{
|
|
GetToolManager()->BeginUndoTransaction(LOCTEXT("BspConversionToolTransactionName", "BSP Conversion"));
|
|
|
|
// If settings had been set to show a preview, then we would have already generated a preview mesh
|
|
// that we could copy the result from. Thus, we only need to generate here if the settings were set
|
|
// to not use a preview.
|
|
if (!Settings->bShowPreview)
|
|
{
|
|
CompareAndUpdateConversionTargets();
|
|
FText ErrorMessage;
|
|
if (!ComputeAndUpdatePreviewMesh(&ErrorMessage))
|
|
{
|
|
// We're closing the tool, so it's too late to just display a side panel warning. Give a pop up.
|
|
FMessageDialog::Open(EAppMsgType::Ok, ErrorMessage);
|
|
|
|
// Don't delete any brushes if we failed
|
|
BrushesToDelete.Empty();
|
|
}
|
|
}
|
|
|
|
// We only need to output something if the result wasn't empty.
|
|
if (PreviewMesh->GetMesh()->VertexCount() > 0)
|
|
{
|
|
TArray<UMaterialInterface*> Materials;
|
|
PreviewMesh->GetMaterials(Materials);
|
|
|
|
FCreateMeshObjectParams NewMeshObjectParams;
|
|
NewMeshObjectParams.TargetWorld = TargetWorld;
|
|
NewMeshObjectParams.Transform = PreviewMesh->GetTransform();
|
|
NewMeshObjectParams.BaseName = TEXT("BspMesh");
|
|
NewMeshObjectParams.Materials = Materials;
|
|
NewMeshObjectParams.SetMesh(PreviewMesh->GetMesh());
|
|
FCreateMeshObjectResult Result = UE::Modeling::CreateMeshObject(GetToolManager(), MoveTemp(NewMeshObjectParams));
|
|
if (Result.IsOK() && Result.NewActor != nullptr)
|
|
{
|
|
ToolSelectionUtil::SetNewActorSelection(GetToolManager(), Result.NewActor);
|
|
}
|
|
}
|
|
|
|
// Delete brushes that we marked for deletion. This may need to happen even if the resulting mesh
|
|
// was empty, for instance if everything was inside a subtractive brush.
|
|
for (ABrush* Brush : BrushesToDelete)
|
|
{
|
|
TargetWorld->EditorDestroyActor(Brush, true);
|
|
}
|
|
GEditor->RebuildAlteredBSP();
|
|
|
|
GetToolManager()->EndUndoTransaction();
|
|
}
|
|
|
|
// Remove the preview mesh
|
|
PreviewMesh->SetVisible(false);
|
|
PreviewMesh->Disconnect();
|
|
PreviewMesh = nullptr;
|
|
|
|
// Deregister all the callbacks we registered
|
|
USelection::SelectionChangedEvent.RemoveAll(this);
|
|
GEngine->OnLevelActorListChanged().RemoveAll(this);
|
|
GEngine->OnActorMoved().RemoveAll(this);
|
|
|
|
// Empty cached data
|
|
CachedBrushes.Empty();
|
|
|
|
Settings->SaveProperties(this);
|
|
}
|
|
|
|
// Conversion functions
|
|
|
|
/**
|
|
* Updates the targets that the conversion functions operate on (BrushesToConvert, BrushForPivot),
|
|
* based on level composition, current selection, and settings. Also checks if this is a change
|
|
* from the previous value of BrushesToConvert.
|
|
*
|
|
* @return true if BrushesToConvert changed.
|
|
*/
|
|
bool UBspConversionTool::CompareAndUpdateConversionTargets()
|
|
{
|
|
TArray<ABrush*> PreviousBrushesToConvert = BrushesToConvert;
|
|
BrushesToConvert.Empty();
|
|
BrushesToDelete.Empty();
|
|
|
|
// One thing we need to check is whether we have just a single additive brush,
|
|
// in which case we want to keep the pivot the same (and we will set BrushForPivot)
|
|
int NumAdditiveBrushes = 0;
|
|
|
|
// The order of brush composition is determined by their order in the ULevel, and may be different from
|
|
// their order of selection. We make BrushesToConvert contain selected brushes in the proper order.
|
|
TArray<AActor*>& LevelActorsInCompositionOrder = TargetWorld->GetCurrentLevel()->Actors;
|
|
for (int i = 0; i < LevelActorsInCompositionOrder.Num(); ++i)
|
|
{
|
|
ABrush* Brush = Cast<ABrush>(LevelActorsInCompositionOrder[i]);
|
|
if (IsValidConversionTarget(Brush)
|
|
&& (Brush->IsSelected() || (!Settings->bExplicitSubtractiveBrushSelection && Brush->BrushType == EBrushType::Brush_Subtract)))
|
|
{
|
|
BrushesToConvert.Add(Brush);
|
|
|
|
if (Brush->BrushType == EBrushType::Brush_Add)
|
|
{
|
|
++NumAdditiveBrushes;
|
|
BrushForPivot = Brush;
|
|
|
|
// Additive volume brushes may not be deleted, depending on settings
|
|
if (!Brush->IsVolumeBrush() || Settings->bRemoveConvertedVolumes)
|
|
{
|
|
BrushesToDelete.Add(Brush);
|
|
}
|
|
}
|
|
// Subtractive brushes only get deleted if they were explicitly selected and the settings allow it
|
|
else if (Settings->bRemoveConvertedSubtractiveBrushes && Brush->IsSelected())
|
|
{
|
|
BrushesToDelete.Add(Brush);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (NumAdditiveBrushes != 1)
|
|
{
|
|
BrushForPivot = nullptr;
|
|
}
|
|
|
|
return BrushesToConvert != PreviousBrushesToConvert;
|
|
}
|
|
|
|
/**
|
|
* Performs conversion of BrushesToConvert and stores the result in the preview mesh.
|
|
*
|
|
* @param OutErrorMessage Error message to fill if there is a problem.
|
|
* @return true if successful.
|
|
*/
|
|
bool UBspConversionTool::ComputeAndUpdatePreviewMesh(FText* OutErrorMessage)
|
|
{
|
|
if (Settings->ConversionMode == EBspConversionMode::CombineFirst)
|
|
{
|
|
return CombineThenConvert(OutErrorMessage);
|
|
}
|
|
else
|
|
{
|
|
return ConvertThenCombine(OutErrorMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The easy conversion path, where we just use the existing bsp conversion function to
|
|
* convert everything that was selected. Operates on non-manifold geometry. Doesn't write
|
|
* out an error message. Returns true unless BrushesToConvert was empty.
|
|
*
|
|
* BrushesToConvert must be initialized.
|
|
*/
|
|
bool UBspConversionTool::CombineThenConvert(FText*)
|
|
{
|
|
if (BrushesToConvert.Num() == 0)
|
|
{
|
|
PreviewMesh->ClearPreview();
|
|
return false;
|
|
}
|
|
|
|
FDynamicMesh3 OutputMesh;
|
|
TArray<UMaterialInterface*> OutputMaterials;
|
|
ConvertBrushesToDynamicMesh(BrushesToConvert, OutputMesh, OutputMaterials);
|
|
|
|
// The created mesh is built with its pivot at the origin, so we need to reset it. If there was
|
|
// just one additive brush, we keep the pivot the same, otherwise we center it.
|
|
FVector3d NewPivot;
|
|
if (BrushForPivot)
|
|
{
|
|
NewPivot = (FVector3d)BrushForPivot->GetTransform().GetLocation();
|
|
}
|
|
else
|
|
{
|
|
NewPivot = OutputMesh.GetBounds().Center();
|
|
}
|
|
|
|
MeshTransforms::Translate(OutputMesh, -NewPivot);
|
|
UE::Geometry::FTransform3d Transform = UE::Geometry::FTransform3d::Identity();
|
|
Transform.SetTranslation(NewPivot);
|
|
|
|
PreviewMesh->UpdatePreview(&OutputMesh);
|
|
PreviewMesh->SetTransform((FTransform)Transform);
|
|
PreviewMesh->SetMaterials(OutputMaterials);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The more complicated conversion path, where we convert brushes individually and use static mesh boolean
|
|
* operations to combine them. Boolean operations fail when geometry is non-manifold, which happens in
|
|
* the case of stair brushes, mainly, due to them not being properly closed.
|
|
* The preview gets cleared in the case of an error.
|
|
*
|
|
* @param ErrorMessage Place to write out error message if a conversion results in invalid geometry.
|
|
* @return false if BrushesToConvert is empty or a conversion gives invalid geometry, true otherwise.
|
|
*/
|
|
bool UBspConversionTool::ConvertThenCombine(FText *ErrorMessage)
|
|
{
|
|
if (BrushesToConvert.Num() == 0)
|
|
{
|
|
PreviewMesh->ClearPreview();
|
|
return false;
|
|
}
|
|
|
|
// We'll need temporary space to store meshes as we build them up. The input and output meshes,
|
|
// transforms, and material arrays will swap after we merge in each new mesh.
|
|
FDynamicMesh3 MeshStorage[2];
|
|
UE::Geometry::FTransform3d TransformStorage[2];
|
|
TArray<UMaterialInterface*> MaterialArraysStorage[2];
|
|
|
|
FDynamicMesh3* InputMesh = &MeshStorage[0];
|
|
FDynamicMesh3* OutputMesh = &MeshStorage[1];
|
|
|
|
UE::Geometry::FTransform3d* InputTransform = &TransformStorage[0];
|
|
UE::Geometry::FTransform3d* OutputTransform = &TransformStorage[1];
|
|
|
|
TArray<UMaterialInterface*>* InputMaterials = &MaterialArraysStorage[0];
|
|
TArray<UMaterialInterface*>* OutputMaterials = &MaterialArraysStorage[1];
|
|
|
|
// The individual mesh to merge in is dependent only on that brush, and therefore can be
|
|
// cached. Unfortunately, we need to be able to modify the mesh while doing boolean operations
|
|
// to update the material references of the triangles to a common material set, so we need to
|
|
// create copies of the cached mesh.
|
|
FDynamicMesh3 NextMesh;
|
|
|
|
// The transform when a brush is first converted is always the same
|
|
UE::Geometry::FTransform3d NextTransform = UE::Geometry::FTransform3d::Identity();
|
|
|
|
// We could actually point to cached materials since we don't change these, but for consistency,
|
|
// we'll have a local copy.
|
|
TArray<UMaterialInterface*> NextMaterials;
|
|
|
|
// This will point to the next bsp brush that we're considering
|
|
ABrush* NextBrush = BrushesToConvert[0];
|
|
|
|
// When converting individual brushes, we pass them to ConvertBrushesToDynamicMesh as a
|
|
// one-element array.
|
|
TArray<ABrush*> WrappedBrush = { NextBrush };
|
|
|
|
// Convert the first brush unless it is subtractive, in which case the mesh will stay empty.
|
|
// This isn't just an optimization- we also need to avoid caching this brush, or trying to use a cached
|
|
// additive version.
|
|
if (NextBrush->BrushType != EBrushType::Brush_Subtract)
|
|
{
|
|
if (Settings->bCacheBrushes && CachedBrushes.Contains(NextBrush))
|
|
{
|
|
OutputMesh->Copy(*CachedBrushes[NextBrush].Key);
|
|
*OutputMaterials = *CachedBrushes[NextBrush].Value;
|
|
}
|
|
else
|
|
{
|
|
ConvertBrushesToDynamicMesh(WrappedBrush, *OutputMesh, *OutputMaterials);
|
|
|
|
if (Settings->bCacheBrushes)
|
|
{
|
|
CachedBrushes.Add(NextBrush, FCachedResult(MakeShared<const FDynamicMesh3>(*OutputMesh),
|
|
MakeShared<const TArray<UMaterialInterface*>>(*OutputMaterials)));
|
|
}
|
|
}
|
|
*OutputTransform = UE::Geometry::FTransform3d::Identity(); // Always identity when first converted
|
|
|
|
// Check validity for the purposes of boolean operations. We'll do this after each brush conversion.
|
|
if (!OutputMesh->CheckValidity(FDynamicMesh3::FValidityOptions(), EValidityCheckFailMode::ReturnOnly))
|
|
{
|
|
PreviewMesh->ClearPreview();
|
|
if (ErrorMessage)
|
|
{
|
|
*ErrorMessage = GetBrushGeometryErrorMessage(NextBrush);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Now convert the other meshes one at a time and apply them using boolean mesh operations
|
|
for (int i = 1; i < BrushesToConvert.Num(); ++i)
|
|
{
|
|
NextBrush = BrushesToConvert[i];
|
|
|
|
Swap(OutputMesh, InputMesh);
|
|
Swap(OutputTransform, InputTransform);
|
|
Swap(OutputMaterials, InputMaterials);
|
|
OutputMesh->Clear();
|
|
|
|
FMeshBoolean::EBooleanOp Operation = FMeshBoolean::EBooleanOp::Union;
|
|
bool IsSubtractiveBrush = NextBrush->BrushType == EBrushType::Brush_Subtract;
|
|
if (IsSubtractiveBrush)
|
|
{
|
|
Operation = FMeshBoolean::EBooleanOp::Difference;
|
|
|
|
// We need a solid mesh to subtract, so the conversion code needs to operate on an additive brush.
|
|
// However, we're going to undo this once we're done because the brush may persist after the conversion,
|
|
// depending on the settings.
|
|
// Despite the undoing, we need to call Modify now because it gets called in the conversion code, which
|
|
// would cause the changed property to be saved.
|
|
NextBrush->Modify();
|
|
NextBrush->BrushType = EBrushType::Brush_Add;
|
|
}
|
|
|
|
// Convert the next brush
|
|
if (Settings->bCacheBrushes && CachedBrushes.Contains(NextBrush))
|
|
{
|
|
NextMesh.Copy(*CachedBrushes[NextBrush].Key);
|
|
NextMaterials = *CachedBrushes[NextBrush].Value;
|
|
}
|
|
else
|
|
{
|
|
WrappedBrush[0] = NextBrush;
|
|
|
|
// Get the result
|
|
ConvertBrushesToDynamicMesh(WrappedBrush, NextMesh, NextMaterials);
|
|
|
|
if (Settings->bCacheBrushes)
|
|
{
|
|
CachedBrushes.Add(NextBrush, FCachedResult(MakeShared<const FDynamicMesh3>(NextMesh),
|
|
MakeShared<const TArray<UMaterialInterface*>>(NextMaterials)));
|
|
}
|
|
}
|
|
|
|
// See if converted geometry is valid
|
|
if (!NextMesh.CheckValidity(FDynamicMesh3::FValidityOptions(), EValidityCheckFailMode::ReturnOnly))
|
|
{
|
|
PreviewMesh->ClearPreview();
|
|
if (ErrorMessage)
|
|
{
|
|
*ErrorMessage = GetBrushGeometryErrorMessage(NextBrush);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Apply the boolean operation
|
|
if (IsSubtractiveBrush)
|
|
{
|
|
ApplyStaticMeshBooleanOperation(
|
|
*InputMesh, *InputTransform, *InputMaterials,
|
|
NextMesh, NextTransform, NextMaterials,
|
|
*OutputMesh, *OutputTransform, *OutputMaterials, Operation);
|
|
|
|
// Undo the change in brush type that we did before.
|
|
NextBrush->BrushType = EBrushType::Brush_Subtract;
|
|
}
|
|
else
|
|
{
|
|
// For union, we actually swap the order of meshes in hopes of better lining up
|
|
// with BSP brush priority in coplanar places (brushes added later have priority,
|
|
// whereas for our boolean operations, first mesh has priority)
|
|
ApplyStaticMeshBooleanOperation(
|
|
NextMesh, NextTransform, NextMaterials,
|
|
*InputMesh, *InputTransform, *InputMaterials,
|
|
*OutputMesh, *OutputTransform, *OutputMaterials, Operation);
|
|
}
|
|
}//end converting other brushes
|
|
|
|
// If there was only a single additive brush, we should keep its pivot in the original location.
|
|
if (BrushForPivot)
|
|
{
|
|
FVector3d BrushTranslation = (FVector3d)BrushForPivot->GetTransform().GetLocation();
|
|
MeshTransforms::Translate(*OutputMesh, OutputTransform->GetTranslation() - BrushTranslation);
|
|
OutputTransform->SetTranslation(BrushTranslation);
|
|
}
|
|
// Otherwise, the pivot set by boolean operations is appropriate.
|
|
|
|
PreviewMesh->UpdatePreview(OutputMesh);
|
|
PreviewMesh->SetTransform((FTransform)(*OutputTransform));
|
|
PreviewMesh->SetMaterials(*OutputMaterials);
|
|
|
|
return true;
|
|
}
|
|
|
|
FText GetBrushGeometryErrorMessage(ABrush* Brush)
|
|
{
|
|
return FText::Format(LOCTEXT("ConvertThenCombineInvalidGeometryError", "The brush \"{0}\" has invalid geometry to be used with mesh boolean operations, so the \"Convert, then Combine\" mode cannot be used. Try using \"Combine, then Convert\" to convert, then use MeshInspector to see problematic areas."),
|
|
FText::FromString(Brush->GetName()));
|
|
}
|
|
|
|
/** Uses our existing conversion functions to convert brushes to a single DynamicMesh. */
|
|
void ConvertBrushesToDynamicMesh(TArray<ABrush*>& BrushesToConvert, FDynamicMesh3& OutputMesh, TArray<UMaterialInterface*>& OutputMaterials)
|
|
{
|
|
// Have the editor rebuild a model composed only of selected brushes into a temporary UModel object, and make
|
|
// sure its polygons get built.
|
|
// Even though it's temporary, we're not allowed to make a UModel on the stack
|
|
UModel* TempModel = NewObject<UModel>();
|
|
TempModel->Initialize(nullptr);
|
|
GEditor->RebuildModelFromBrushes(BrushesToConvert, TempModel);
|
|
GEditor->bspBuildFPolys(TempModel, true, 0); // SurfLinks parameter doesn't matter
|
|
|
|
// Prep some output variables
|
|
FMeshDescription MeshDescription;
|
|
FStaticMeshAttributes StaticMeshAttributes(MeshDescription);
|
|
StaticMeshAttributes.Register();
|
|
|
|
TArray<FStaticMaterial> Materials;
|
|
|
|
// Do the actual conversion using our old conversion function
|
|
GetBrushMesh(nullptr, TempModel, MeshDescription, Materials);
|
|
|
|
// Get a list of material interfaces out of the list of materials that we got
|
|
OutputMaterials.Empty();
|
|
for (FStaticMaterial Material : Materials)
|
|
{
|
|
OutputMaterials.Add(Material.MaterialInterface);
|
|
}
|
|
|
|
// Turn the mesh description into a DynamicMesh
|
|
OutputMesh.Clear();
|
|
FMeshDescriptionToDynamicMesh Converter;
|
|
Converter.Convert(&MeshDescription, OutputMesh);
|
|
}
|
|
|
|
void ApplyStaticMeshBooleanOperation(
|
|
FDynamicMesh3& MeshA, const UE::Geometry::FTransform3d& TransformA, const TArray<UMaterialInterface*>& MaterialsA,
|
|
FDynamicMesh3& MeshB, const UE::Geometry::FTransform3d& TransformB, const TArray<UMaterialInterface*>& MaterialsB,
|
|
FDynamicMesh3& OutputMesh, UE::Geometry::FTransform3d& OutputTransform, TArray<UMaterialInterface*>& OutputMaterials,
|
|
FMeshBoolean::EBooleanOp Operation)
|
|
{
|
|
// These need to be enabled on both meshes to deal with materials properly. This is relevant, for
|
|
// instance, if the first brush in the composition list was empty.
|
|
MeshA.EnableAttributes();
|
|
MeshA.Attributes()->EnableMaterialID();
|
|
MeshB.EnableAttributes();
|
|
MeshB.Attributes()->EnableMaterialID();
|
|
|
|
|
|
// We'll need to combine all the materials into the output, but not duplicate them across the two
|
|
// meshes. This will keep track of the materials that we have seen.
|
|
TMap<UMaterialInterface*, int> SeenMaterials;
|
|
|
|
// Start with the materials from the first mesh.
|
|
OutputMaterials = MaterialsA;
|
|
for (int i = 0; i < OutputMaterials.Num(); ++i)
|
|
{
|
|
SeenMaterials.Add(OutputMaterials[i], i);
|
|
}
|
|
|
|
// Add any new materials from mesh B, and remap any materials that we already have in that mesh. MaterialBRemap
|
|
// maps old material indices to new ones.
|
|
TArray<int> MaterialBRemap;
|
|
for (UMaterialInterface* Mat : MaterialsB)
|
|
{
|
|
int NewMatIndex;
|
|
int* FoundMatIdx = SeenMaterials.Find(Mat);
|
|
if (FoundMatIdx)
|
|
{
|
|
NewMatIndex = *FoundMatIdx;
|
|
}
|
|
else
|
|
{
|
|
NewMatIndex = OutputMaterials.Add(Mat);
|
|
SeenMaterials.Add(Mat, NewMatIndex);
|
|
}
|
|
MaterialBRemap.Add(NewMatIndex);
|
|
}
|
|
|
|
// Apply the remapping of material indices to mesh B.
|
|
FDynamicMeshMaterialAttribute* MaterialIDs = MeshB.Attributes()->GetMaterialID();
|
|
for (int TID : MeshB.TriangleIndicesItr())
|
|
{
|
|
MaterialIDs->SetValue(TID, MaterialBRemap[MaterialIDs->GetValue(TID)]);
|
|
}
|
|
|
|
// Perform the actual boolean operation.
|
|
FMeshBoolean BooleanOperation(&MeshA, TransformA, &MeshB, TransformB, &OutputMesh, Operation);
|
|
BooleanOperation.bSimplifyAlongNewEdges = true;
|
|
BooleanOperation.Compute();
|
|
OutputTransform = BooleanOperation.ResultTransform;
|
|
}
|
|
|
|
|
|
// Button support
|
|
|
|
void UBspConversionTool::RequestAction(EBspConversionToolAction ActionType)
|
|
{
|
|
if (PendingAction == EBspConversionToolAction::NoAction)
|
|
{
|
|
PendingAction = ActionType;
|
|
}
|
|
}
|
|
|
|
void UBspConversionTool::OnTick(float DeltaTime)
|
|
{
|
|
if (PendingAction != EBspConversionToolAction::NoAction)
|
|
{
|
|
ApplyAction(PendingAction);
|
|
PendingAction = EBspConversionToolAction::NoAction;
|
|
}
|
|
}
|
|
|
|
void UBspConversionTool::ApplyAction(EBspConversionToolAction ActionType)
|
|
{
|
|
switch (ActionType)
|
|
{
|
|
case EBspConversionToolAction::SelectAllValidBrushes:
|
|
{
|
|
FSelectedOjectsChangeList NewSelection;
|
|
NewSelection.ModificationType = ESelectedObjectsModificationType::Replace;
|
|
|
|
// Accumulate all actors in the current level that are valid brushes
|
|
for (auto Iter(TargetWorld->GetCurrentLevel()->Actors.CreateConstIterator()); Iter; ++Iter)
|
|
{
|
|
ABrush* Brush = Cast<ABrush>(*Iter);
|
|
if (IsValidConversionTarget(Brush))
|
|
{
|
|
NewSelection.Actors.Add(Brush);
|
|
}
|
|
}
|
|
|
|
GetToolManager()->RequestSelectionChange(NewSelection);
|
|
}
|
|
break;
|
|
case EBspConversionToolAction::DeselectVolumes:
|
|
{
|
|
FSelectedOjectsChangeList NewSelection;
|
|
NewSelection.ModificationType = ESelectedObjectsModificationType::Replace;
|
|
|
|
// Out of the current selection, keep anything except volume brushes
|
|
for (FSelectionIterator Iter(*GEditor->GetSelectedActors()); Iter; ++Iter)
|
|
{
|
|
AActor* Actor = Cast<AActor>(*Iter);
|
|
if (!(Cast<ABrush>(Actor) && Cast<ABrush>(Actor)->IsVolumeBrush()))
|
|
{
|
|
NewSelection.Actors.Add(Actor);
|
|
}
|
|
}
|
|
|
|
GetToolManager()->RequestSelectionChange(NewSelection);
|
|
}
|
|
break;
|
|
case EBspConversionToolAction::DeselectNonValid:
|
|
{
|
|
// Normally, "GEditor->SelectNone(true, false, false)" would deselect all brushes, but
|
|
// it does not deselect volumes, which we want to do depending on the settings.
|
|
|
|
FSelectedOjectsChangeList NewSelection;
|
|
NewSelection.ModificationType = ESelectedObjectsModificationType::Replace;
|
|
|
|
// From current selection, select everything that is valid given current settings
|
|
for (FSelectionIterator Iter(*GEditor->GetSelectedActors()); Iter; ++Iter)
|
|
{
|
|
ABrush* Brush = Cast<ABrush>(*Iter);
|
|
if (IsValidConversionTarget(Brush))
|
|
{
|
|
NewSelection.Actors.Add(Brush);
|
|
}
|
|
}
|
|
|
|
GetToolManager()->RequestSelectionChange(NewSelection);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Callback functions
|
|
|
|
// We need to keep track of the following:
|
|
// - BrushesToConvert needs to contain relevant brushes given settings and selection
|
|
// - PreviewMesh may need updating
|
|
// - CachedBrushes must not contain any outdated conversions.
|
|
|
|
/**
|
|
* This is the primary event we'll be responding to. It affects whether we can accept
|
|
* or not, and the preview, if it is being shown. Does not affect cached brushes.
|
|
*/
|
|
void UBspConversionTool::OnEditorSelectionChanged(UObject* NewSelection)
|
|
{
|
|
USelection* Selection = Cast<USelection>(NewSelection);
|
|
if (!Selection)
|
|
{
|
|
// Clear things
|
|
bCanAccept = false;
|
|
PreviewMesh->ClearPreview();
|
|
BrushesToConvert.Empty();
|
|
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning); // This "clears" it
|
|
return;
|
|
}
|
|
|
|
// Update whether the tool can accept safely
|
|
bCanAccept = AtLeastOneValidConversionTarget();
|
|
|
|
// Update preview. This may change a true bCanAccept to false if there are errors
|
|
if (!bCanAccept)
|
|
{
|
|
// If there isn't anything selected yet, clear things
|
|
PreviewMesh->ClearPreview();
|
|
BrushesToConvert.Empty();
|
|
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning);
|
|
}
|
|
else if (Settings->bShowPreview && CompareAndUpdateConversionTargets())
|
|
{
|
|
FText ErrorMessage;
|
|
bCanAccept = ComputeAndUpdatePreviewMesh(&ErrorMessage);
|
|
|
|
// We do this regardless of success because it resets the error message if ErrorMessage is empty.
|
|
GetToolManager()->DisplayMessage(ErrorMessage, EToolMessageLevel::UserWarning);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Changes to the actor list may change the order of brush composition, or add or remove implicitly included
|
|
* subtractive brushes. Should not affect cached brushes.
|
|
*/
|
|
void UBspConversionTool::OnEditorLevelActorListChanged()
|
|
{
|
|
// Safest thing to do is to do the same things as OnEditorSelectionChanged, even though bCanAccept should
|
|
// only change in very weird cases (such as removing an implicit subtractive stair brush).
|
|
|
|
// Update whether the tool can accept safely
|
|
bCanAccept = AtLeastOneValidConversionTarget();
|
|
|
|
// Update preview. This may change a true bCanAccept to false if there are errors
|
|
if (!bCanAccept)
|
|
{
|
|
// If there isn't anything selected yet, clear things
|
|
PreviewMesh->ClearPreview();
|
|
BrushesToConvert.Empty();
|
|
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning);
|
|
}
|
|
else if (Settings->bShowPreview && CompareAndUpdateConversionTargets())
|
|
{
|
|
FText ErrorMessage;
|
|
bCanAccept = ComputeAndUpdatePreviewMesh(&ErrorMessage);
|
|
|
|
// We do this regardless of success because it resets the error message if ErrorMessage is empty.
|
|
GetToolManager()->DisplayMessage(ErrorMessage, EToolMessageLevel::UserWarning);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actor movements include any transform changes, and change both the preview and the
|
|
* cached brushes. It shouldn't affect BrushesToConvert or bCanAccept.
|
|
*/
|
|
void UBspConversionTool::OnEditorActorMoved(AActor* Actor)
|
|
{
|
|
// Make sure it was a brush that got moved
|
|
ABrush* Brush = Cast<ABrush>(Actor);
|
|
if (!Brush)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Remove cached version of brush
|
|
if (CachedBrushes.Contains(Brush))
|
|
{
|
|
CachedBrushes.Remove(Brush);
|
|
}
|
|
|
|
// Update preview if this is a relevant brush
|
|
if (bCanAccept && Settings->bShowPreview && BrushesToConvert.Contains(Brush))
|
|
{
|
|
FText ErrorMessage;
|
|
bCanAccept = ComputeAndUpdatePreviewMesh();
|
|
GetToolManager()->DisplayMessage(ErrorMessage, EToolMessageLevel::UserWarning);
|
|
}
|
|
}
|
|
|
|
void UBspConversionTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
|
|
{
|
|
// There are a few properties that are only relevant on accept, and don't change anything now
|
|
if (Property && (Property->GetFName() == GET_MEMBER_NAME_CHECKED(UBspConversionToolProperties, bRemoveConvertedSubtractiveBrushes)
|
|
|| Property->GetFName() == GET_MEMBER_NAME_CHECKED(UBspConversionToolProperties, bRemoveConvertedVolumes)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// For the most part, it ends up being too messy to consider all properties individually. It
|
|
// is safer to reset the state (though only clear cached brushes if we have to).
|
|
|
|
if (!Settings->bCacheBrushes)
|
|
{
|
|
CachedBrushes.Empty();
|
|
}
|
|
|
|
bCanAccept = AtLeastOneValidConversionTarget();
|
|
|
|
// Clear preview
|
|
PreviewMesh->ClearPreview();
|
|
BrushesToConvert.Empty();
|
|
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning); // This "clears" it
|
|
|
|
if (Settings->bShowPreview && bCanAccept)
|
|
{
|
|
CompareAndUpdateConversionTargets();
|
|
FText ErrorMessage;
|
|
bCanAccept = ComputeAndUpdatePreviewMesh(&ErrorMessage);
|
|
GetToolManager()->DisplayMessage(ErrorMessage, EToolMessageLevel::UserWarning);
|
|
}
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|