// Copyright Epic Games, Inc. All Rights Reserved. #include "ConvertToPolygonsTool.h" #include "InteractiveToolManager.h" #include "ToolBuilderUtil.h" #include "ToolSetupUtil.h" #include "ModelingToolTargetUtil.h" #include "Drawing/PreviewGeometryActor.h" #include "PreviewMesh.h" #include "MeshOpPreviewHelpers.h" #include "ModelingOperators.h" #include "DynamicMesh/DynamicMesh3.h" #include "DynamicMesh/MeshNormals.h" #include "DynamicMesh/MeshSharingUtil.h" #include "DynamicMeshEditor.h" #include "DynamicSubmesh3.h" #include "MeshRegionBoundaryLoops.h" #include "Util/ColorConstants.h" #include "Polygroups/PolygroupUtil.h" #include "Util/ColorConstants.h" #include "Selections/GeometrySelectionUtil.h" #include "Selection/StoredMeshSelectionUtil.h" #include "Selection/GeometrySelectionVisualization.h" #include "PropertySets/GeometrySelectionVisualizationProperties.h" #include "ToolTargetManager.h" #include "Polygroups/PolygroupsGenerator.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(ConvertToPolygonsTool) using namespace UE::Geometry; #define LOCTEXT_NAMESPACE "UConvertToPolygonsTool" /* * ToolBuilder */ USingleTargetWithSelectionTool* UConvertToPolygonsToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const { return NewObject(SceneState.ToolManager); } bool UConvertToPolygonsToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const { return USingleTargetWithSelectionToolBuilder::CanBuildTool(SceneState) && SceneState.TargetManager->CountSelectedAndTargetableWithPredicate(SceneState, GetTargetRequirements(), [](UActorComponent& Component) { return !ToolBuilderUtil::IsVolume(Component); }) >= 1; } class FConvertToPolygonsOp : public FDynamicMeshOperator { public: // parameters set by the tool EConvertToPolygonsMode ConversionMode = EConvertToPolygonsMode::FaceNormalDeviation; double AngleTolerance = 0.1f; bool bUseAverageGroupNormal = true; int32 NumPoints = 10; bool bSubdivideExisting = false; FPolygroupsGenerator::EWeightingType WeightingType = FPolygroupsGenerator::EWeightingType::None; FVector3d WeightingCoeffs = FVector3d::One(); bool bRespectUVSeams = false; bool bRespectHardNormals = false; double QuadMetricClamp = 1.0; double QuadAdjacencyWeight = 1.0; int QuadSearchRounds = 1; int32 MinGroupSize = 2; int32 InitialGroupID = 0; bool bCalculateNormals = false; // input mesh TSharedPtr OriginalMesh; // input polygroups, if available TSharedPtr SourcePolyGroups; // result FPolygroupsGenerator Generator; virtual void CalculateResult(FProgressCancel* Progress) override { if ((Progress && Progress->Cancelled()) || OriginalMesh.IsValid() == false) { return; } OriginalMesh->AccessSharedObject([&](const FDynamicMesh3& ReadMesh) { ResultMesh->Copy(ReadMesh, true, true, true, true); }); if (Progress && Progress->Cancelled()) { return; } if (ConversionMode == EConvertToPolygonsMode::CopyFromLayer) { if (!ensure(SourcePolyGroups.IsValid())) return; for (int32 tid : ResultMesh->TriangleIndicesItr()) { ResultMesh->SetTriangleGroup(tid, SourcePolyGroups->GetTriangleGroup(tid)); } return; } Generator = FPolygroupsGenerator(ResultMesh.Get()); Generator.MinGroupSize = MinGroupSize; Generator.InitialGroupID = InitialGroupID; switch (ConversionMode) { case EConvertToPolygonsMode::FromUVIslands: { Generator.FindPolygroupsFromUVIslands(); break; } case EConvertToPolygonsMode::FromNormalSeams: { Generator.FindPolygroupsFromHardNormalSeams(); break; } case EConvertToPolygonsMode::FromMaterialIDs: { Generator.FindPolygroupsFromMaterialIDs(); break; } case EConvertToPolygonsMode::FromConnectedTris: { Generator.FindPolygroupsFromConnectedTris(); break; } case EConvertToPolygonsMode::FaceNormalDeviation: { double DotTolerance = 1.0 - FMathd::Cos(AngleTolerance * FMathd::DegToRad); Generator.FindPolygroupsFromFaceNormals( DotTolerance, bRespectUVSeams, bRespectHardNormals, bUseAverageGroupNormal); break; } case EConvertToPolygonsMode::FindPolygons: { Generator.FindSourceMeshPolygonPolygroups( bRespectUVSeams, bRespectHardNormals, QuadMetricClamp, QuadAdjacencyWeight, FMath::Clamp(QuadSearchRounds, 1, 100) ); break; } case EConvertToPolygonsMode::FromFurthestPointSampling: { if (bSubdivideExisting) { OriginalMesh->AccessSharedObject([&](const FDynamicMesh3& ReadMesh) { FPolygroupSet InputGroups(&ReadMesh); Generator.FindPolygroupsFromFurthestPointSampling(NumPoints, WeightingType, WeightingCoeffs, &InputGroups); }); } else { Generator.FindPolygroupsFromFurthestPointSampling(NumPoints, WeightingType, WeightingCoeffs, nullptr); } break; } default: check(0); } Generator.FindPolygroupEdges(); if (bCalculateNormals && ConversionMode == EConvertToPolygonsMode::FaceNormalDeviation) { if (ResultMesh->HasAttributes() == false) { ResultMesh->EnableAttributes(); } FDynamicMeshNormalOverlay* NormalOverlay = ResultMesh->Attributes()->PrimaryNormals(); NormalOverlay->ClearElements(); FDynamicMeshEditor Editor(ResultMesh.Get()); for (const TArray& Polygon : Generator.FoundPolygroups) { FVector3f Normal = (FVector3f)ResultMesh->GetTriNormal(Polygon[0]); Editor.SetTriangleNormals(Polygon, Normal); } FMeshNormals Normals(ResultMesh.Get()); Normals.RecomputeOverlayNormals(ResultMesh->Attributes()->PrimaryNormals()); Normals.CopyToOverlay(NormalOverlay, false); } } void SetTransform(const FTransformSRT3d& Transform) { ResultTransform = Transform; } }; TUniquePtr UConvertToPolygonsOperatorFactory::MakeNewOperator() { // backpointer used to populate parameters. check(ConvertToPolygonsTool); // Create the actual operator type based on the requested operation TUniquePtr MeshOp = MakeUnique(); // Operator runs on another thread - copy data over that it needs. ConvertToPolygonsTool->UpdateOpParameters(*MeshOp); // give the operator return MeshOp; } /* * Tool */ UConvertToPolygonsTool::UConvertToPolygonsTool() { SetToolDisplayName(LOCTEXT("ConvertToPolygonsToolName", "Generate PolyGroups")); } bool UConvertToPolygonsTool::CanAccept() const { return Super::CanAccept() && (PreviewCompute == nullptr || PreviewCompute->HaveValidResult()); } void UConvertToPolygonsTool::Setup() { UInteractiveTool::Setup(); FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Target); OriginalDynamicMesh = MakeShared(UE::ToolTarget::GetDynamicMeshCopy(Target)); // initialize triangle ROI if one exists SelectionTriangleROI = MakeShared, ESPMode::ThreadSafe>(); TArray TriangleROI; if (HasGeometrySelection()) { const FGeometrySelection& InputSelection = GetGeometrySelection(); UE::Geometry::EnumerateSelectionTriangles(InputSelection, *OriginalDynamicMesh, [&](int32 TriangleID) { SelectionTriangleROI->Add(TriangleID); }); TriangleROI = SelectionTriangleROI->Array(); OriginalSubmesh = MakeShared(OriginalDynamicMesh.Get(), TriangleROI); bUsingSelection = true; } if (bUsingSelection) { ComputeOperatorSharedMesh = MakeShared(&OriginalSubmesh->GetSubmesh()); } else { ComputeOperatorSharedMesh = MakeShared(OriginalDynamicMesh.Get()); } Settings = NewObject(this); Settings->RestoreProperties(this); AddToolPropertySource(Settings); FTransform MeshTransform = (FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target); UE::ToolTarget::HideSourceObject(Target); { // create the operator factory UConvertToPolygonsOperatorFactory* ConvertToPolygonsOperatorFactory = NewObject(this); ConvertToPolygonsOperatorFactory->ConvertToPolygonsTool = this; // set the back pointer PreviewCompute = NewObject(ConvertToPolygonsOperatorFactory); PreviewCompute->Setup(GetTargetWorld(), ConvertToPolygonsOperatorFactory); ToolSetupUtil::ApplyRenderingConfigurationToPreview(PreviewCompute->PreviewMesh, Target); PreviewCompute->SetIsMeshTopologyConstant(true, EMeshRenderAttributeFlags::Positions | EMeshRenderAttributeFlags::VertexNormals); // Give the preview something to display PreviewCompute->PreviewMesh->SetTransform(MeshTransform); PreviewCompute->PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated); PreviewCompute->PreviewMesh->UpdatePreview(OriginalDynamicMesh.Get()); PreviewCompute->ConfigureMaterials(MaterialSet.Materials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager()) ); // show the preview mesh PreviewCompute->SetVisibility(true); // something to capture the polygons from the async task when it is done PreviewCompute->OnOpCompleted.AddLambda([this](const FDynamicMeshOperator* MeshOp) { const FConvertToPolygonsOp* ConvertToPolygonsOp = static_cast(MeshOp); this->PolygonEdges = ConvertToPolygonsOp->Generator.PolygroupEdges; UpdateVisualization(); }); } if (bUsingSelection) { // create the preview object for the unmodified area UnmodifiedAreaPreviewMesh = NewObject(); UnmodifiedAreaPreviewMesh->CreateInWorld(GetTargetWorld(), MeshTransform); ToolSetupUtil::ApplyRenderingConfigurationToPreview(UnmodifiedAreaPreviewMesh, Target); UnmodifiedAreaPreviewMesh->SetMaterials(MaterialSet.Materials); UnmodifiedAreaPreviewMesh->EnableSecondaryTriangleBuffers( [this](const FDynamicMesh3* Mesh, int32 TriangleID) { return SelectionTriangleROI->Contains(TriangleID); }); UnmodifiedAreaPreviewMesh->SetSecondaryBuffersVisibility(false); UnmodifiedAreaPreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::AutoCalculated); UnmodifiedAreaPreviewMesh->UpdatePreview(OriginalDynamicMesh.Get()); } PreviewGeometry = NewObject(this); PreviewGeometry->CreateInWorld(GetTargetWorld(), MeshTransform); Settings->WatchProperty(Settings->ConversionMode, [this](EConvertToPolygonsMode) { OnSettingsModified(); }); Settings->WatchProperty(Settings->bShowGroupColors, [this](bool) { UpdateVisualization(); }); Settings->WatchProperty(Settings->AngleTolerance, [this](float) { OnSettingsModified(); }); Settings->WatchProperty(Settings->bUseAverageGroupNormal, [this](float) { OnSettingsModified(); }); Settings->WatchProperty(Settings->NumPoints, [this](int32) { OnSettingsModified(); }); Settings->WatchProperty(Settings->bSplitExisting, [this](bool) { OnSettingsModified(); }); Settings->WatchProperty(Settings->bNormalWeighted, [this](bool) { OnSettingsModified(); }); Settings->WatchProperty(Settings->NormalWeighting, [this](float) { OnSettingsModified(); }); Settings->WatchProperty(Settings->MinGroupSize, [this](int32) { OnSettingsModified(); }); Settings->WatchProperty(Settings->QuadAdjacencyWeight, [this](float) { OnSettingsModified(); }); Settings->WatchProperty(Settings->QuadMetricClamp, [this](float) { OnSettingsModified(); }); Settings->WatchProperty(Settings->QuadSearchRounds, [this](int) { OnSettingsModified(); }); Settings->WatchProperty(Settings->bRespectUVSeams, [this](int) { OnSettingsModified(); }); Settings->WatchProperty(Settings->bRespectHardNormals, [this](int) { OnSettingsModified(); }); // add group layer picking property set for source groups CopyFromLayerProperties = NewObject(this); CopyFromLayerProperties->InitializeGroupLayers(OriginalDynamicMesh.Get()); CopyFromLayerProperties->WatchProperty(CopyFromLayerProperties->ActiveGroupLayer, [&](FName) { OnSelectedFromGroupLayerChanged(); }); AddToolPropertySource(CopyFromLayerProperties); UpdateFromGroupLayer(); SetToolPropertySourceEnabled(CopyFromLayerProperties, Settings->ConversionMode == EConvertToPolygonsMode::CopyFromLayer); // add picker for output group layer Settings->OptionsList.Reset(); Settings->OptionsList.Add(TEXT("Default")); // always have standard group if (OriginalDynamicMesh->Attributes()) { for (int32 k = 0; k < OriginalDynamicMesh->Attributes()->NumPolygroupLayers(); k++) { FName Name = OriginalDynamicMesh->Attributes()->GetPolygroupLayer(k)->GetName(); Settings->OptionsList.Add(Name.ToString()); } } Settings->OptionsList.Add(TEXT("Create New...")); Settings->WatchProperty(Settings->GroupLayer, [&](FName NewName) { Settings->bShowNewLayerName = (NewName == TEXT("Create New...")); }); if (bUsingSelection) { GeometrySelectionVizProperties = NewObject(this); GeometrySelectionVizProperties->RestoreProperties(this); AddToolPropertySource(GeometrySelectionVizProperties); GeometrySelectionVizProperties->Initialize(this); GeometrySelectionVizProperties->bEnableShowTriangleROIBorder = true; GeometrySelectionVizProperties->SelectionElementType = static_cast(GeometrySelection.ElementType); GeometrySelectionVizProperties->SelectionTopologyType = static_cast(GeometrySelection.TopologyType); GeometrySelectionViz = NewObject(this); GeometrySelectionViz->CreateInWorld(GetTargetWorld(), MeshTransform); UE::Geometry::InitializeGeometrySelectionVisualization( GeometrySelectionViz, GeometrySelectionVizProperties, *OriginalDynamicMesh, GeometrySelection, nullptr, nullptr, &TriangleROI); } // start the compute PreviewCompute->InvalidateResult(); // updates the triangle color visualization UpdateVisualization(); GetToolManager()->DisplayMessage( LOCTEXT("OnStartTool", "Cluster triangles of the Mesh into PolyGroups using various strategies"), EToolMessageLevel::UserNotification); } void UConvertToPolygonsTool::UpdateOpParameters(FConvertToPolygonsOp& ConvertToPolygonsOp) const { ConvertToPolygonsOp.bCalculateNormals = Settings->bCalculateNormals; ConvertToPolygonsOp.ConversionMode = Settings->ConversionMode; ConvertToPolygonsOp.AngleTolerance = Settings->AngleTolerance; ConvertToPolygonsOp.bUseAverageGroupNormal = Settings->bUseAverageGroupNormal; ConvertToPolygonsOp.NumPoints = Settings->NumPoints; ConvertToPolygonsOp.bSubdivideExisting = Settings->bSplitExisting; ConvertToPolygonsOp.WeightingType = (Settings->bNormalWeighted) ? FPolygroupsGenerator::EWeightingType::NormalDeviation : FPolygroupsGenerator::EWeightingType::None; ConvertToPolygonsOp.WeightingCoeffs = FVector3d(Settings->NormalWeighting, 1.0, 1.0); ConvertToPolygonsOp.MinGroupSize = Settings->MinGroupSize; ConvertToPolygonsOp.QuadMetricClamp = Settings->QuadMetricClamp; ConvertToPolygonsOp.QuadAdjacencyWeight = Settings->QuadAdjacencyWeight; ConvertToPolygonsOp.QuadSearchRounds = Settings->QuadSearchRounds; ConvertToPolygonsOp.bRespectUVSeams = Settings->bRespectUVSeams; ConvertToPolygonsOp.bRespectHardNormals = Settings->bRespectHardNormals; ConvertToPolygonsOp.OriginalMesh = ComputeOperatorSharedMesh; if (bUsingSelection) { ConvertToPolygonsOp.InitialGroupID = OriginalDynamicMesh->MaxGroupID(); } if (ActiveFromGroupSet.IsValid()) { ConvertToPolygonsOp.SourcePolyGroups = ActiveFromGroupSet; if (bUsingSelection) { ConvertToPolygonsOp.InitialGroupID = ActiveFromGroupSet->MaxGroupID; } } FTransform LocalToWorld = (FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target); ConvertToPolygonsOp.SetTransform(LocalToWorld); } void UConvertToPolygonsTool::OnShutdown(EToolShutdownType ShutdownType) { Settings->SaveProperties(this); UE::ToolTarget::ShowSourceObject(Target); if (PreviewGeometry) { PreviewGeometry->Disconnect(); PreviewGeometry = nullptr; } if (UnmodifiedAreaPreviewMesh != nullptr) { UnmodifiedAreaPreviewMesh->Disconnect(); UnmodifiedAreaPreviewMesh = nullptr; } if (PreviewCompute) { FDynamicMeshOpResult Result = PreviewCompute->Shutdown(); if (ShutdownType == EToolShutdownType::Accept) { GetToolManager()->BeginUndoTransaction(LOCTEXT("ConvertToPolygonsToolTransactionName", "Find PolyGroups")); FDynamicMesh3* DynamicMeshResult = Result.Mesh.Get(); if (ensure(DynamicMeshResult != nullptr)) { if (Settings->GroupLayer != TEXT("Default")) { FDynamicMesh3 UseResultMesh = *OriginalDynamicMesh; if (UseResultMesh.HasAttributes() == false) { UseResultMesh.EnableAttributes(); } // if we want to write to any layer other than default, we have to find or create it FDynamicMeshPolygroupAttribute* UseAttribLayer = nullptr; if (Settings->GroupLayer == TEXT("Create New...")) { // append new group layer and set it's name int32 TargetLayerIdx = UseResultMesh.Attributes()->NumPolygroupLayers(); UseResultMesh.Attributes()->SetNumPolygroupLayers(TargetLayerIdx + 1); UseAttribLayer = UseResultMesh.Attributes()->GetPolygroupLayer(TargetLayerIdx); FString UseUniqueName = UE::Geometry::MakeUniqueGroupLayerName(UseResultMesh, Settings->NewLayerName); UseAttribLayer->SetName( FName(UseUniqueName) ); } else { UseAttribLayer = UE::Geometry::FindPolygroupLayerByName(UseResultMesh, Settings->GroupLayer); } if (UseAttribLayer) { // copy the generated groups from the Op output mesh (stored in primary groups) to the target layer if (bUsingSelection) { for (int32 tid : *SelectionTriangleROI) { int32 GroupID = DynamicMeshResult->GetTriangleGroup(OriginalSubmesh->MapTriangleToSubmesh(tid)); UseAttribLayer->SetValue(tid, GroupID); } } else { for (int32 tid : UseResultMesh.TriangleIndicesItr()) { UseAttribLayer->SetValue(tid, DynamicMeshResult->GetTriangleGroup(tid)); } } UE::ToolTarget::CommitDynamicMeshUpdate(Target, UseResultMesh, true); } else { // If we can't find or create the layer (which should not be possible) the tool is going to do nothing, // this is the safest option at this point UE_LOG(LogGeometry, Warning, TEXT("Output group layer missing?")); } } else { // todo: have not actually modified topology here, but groups-only update is not supported yet if (bUsingSelection) { for (int32 tid : *SelectionTriangleROI) { int32 GroupID = DynamicMeshResult->GetTriangleGroup(OriginalSubmesh->MapTriangleToSubmesh(tid)); OriginalDynamicMesh->SetTriangleGroup(tid, GroupID); } UE::ToolTarget::CommitDynamicMeshUpdate(Target, *OriginalDynamicMesh, true); } else { UE::ToolTarget::CommitDynamicMeshUpdate(Target, *DynamicMeshResult, true); } } } if (bUsingSelection) { // if the input was a group selection, that selection is no longer valid. But if we output // a triangle selection it should be converted to the group selection FGeometrySelection OutputSelection; for (int32 tid : *SelectionTriangleROI) { OutputSelection.Selection.Add(FGeoSelectionID::MeshTriangle(tid).Encoded()); } UE::Geometry::SetToolOutputGeometrySelectionForTarget(this, Target, OutputSelection); } GetToolManager()->EndUndoTransaction(); } } if (ComputeOperatorSharedMesh.IsValid()) { ComputeOperatorSharedMesh->ReleaseSharedObject(); } Super::OnShutdown(ShutdownType); } void UConvertToPolygonsTool::OnSettingsModified() { SetToolPropertySourceEnabled(CopyFromLayerProperties, Settings->ConversionMode == EConvertToPolygonsMode::CopyFromLayer); PreviewCompute->InvalidateResult(); } void UConvertToPolygonsTool::OnTick(float DeltaTime) { Super::OnTick(DeltaTime); PreviewCompute->Tick(DeltaTime); } void UConvertToPolygonsTool::OnSelectedFromGroupLayerChanged() { UpdateFromGroupLayer(); PreviewCompute->InvalidateResult(); } void UConvertToPolygonsTool::UpdateFromGroupLayer() { ComputeOperatorSharedMesh->AccessSharedObject([&](const FDynamicMesh3& ReadMesh) { if (CopyFromLayerProperties->HasSelectedPolygroup() == false) { ActiveFromGroupSet = MakeShared(&ReadMesh); } else { FName SelectedName = CopyFromLayerProperties->ActiveGroupLayer; const FDynamicMeshPolygroupAttribute* FoundAttrib = UE::Geometry::FindPolygroupLayerByName(ReadMesh, SelectedName); ensureMsgf(FoundAttrib, TEXT("Selected Attribute Not Found! Falling back to Default group layer.")); ActiveFromGroupSet = MakeShared(&ReadMesh, FoundAttrib); } }); } void UConvertToPolygonsTool::UpdateVisualization() { if (!PreviewCompute) { return; } IMaterialProvider* MaterialTarget = Cast(Target); FComponentMaterialSet MaterialSet; if (Settings->bShowGroupColors) { int32 NumMaterials = MaterialTarget->GetNumMaterials(); for (int32 i = 0; i < NumMaterials; ++i) { MaterialSet.Materials.Add(ToolSetupUtil::GetVertexColorMaterial(GetToolManager())); } PreviewCompute->PreviewMesh->SetTriangleColorFunction([this](const FDynamicMesh3* Mesh, int TriangleID) { return LinearColors::SelectFColor(Mesh->GetTriangleGroup(TriangleID)); }, UPreviewMesh::ERenderUpdateMode::FastUpdate); } else { MaterialTarget->GetMaterialSet(MaterialSet); PreviewCompute->PreviewMesh->ClearTriangleColorFunction(UPreviewMesh::ERenderUpdateMode::FastUpdate); } PreviewCompute->ConfigureMaterials(MaterialSet.Materials, ToolSetupUtil::GetDefaultWorkingMaterial(GetToolManager())); FColor GroupLineColor = FColor::Red; float GroupLineThickness = 2.0f; ComputeOperatorSharedMesh->AccessSharedObject([&](const FDynamicMesh3& ReadMesh) { PreviewGeometry->CreateOrUpdateLineSet(TEXT("GroupBorders"), PolygonEdges.Num(), [&](int32 k, TArray& LinesOut) { FVector3d A, B; ReadMesh.GetEdgeV(PolygonEdges[k], A, B); LinesOut.Add(FRenderableLine(A, B, GroupLineColor, GroupLineThickness)); }, 1); }); } #undef LOCTEXT_NAMESPACE