// Copyright Epic Games, Inc. All Rights Reserved. #include "MeshGroupPaintTool.h" #include "Engine/Engine.h" #include "Engine/World.h" #include "InteractiveToolManager.h" #include "InteractiveGizmoManager.h" #include "Drawing/MeshElementsVisualizer.h" #include "Async/ParallelFor.h" #include "Async/Async.h" #include "SceneView.h" #include "ToolSetupUtil.h" #include "ModelingToolTargetUtil.h" #include "MeshWeights.h" #include "DynamicMesh/MeshNormals.h" #include "DynamicMesh/MeshIndexUtil.h" #include "Util/BufferUtil.h" #include "Util/ColorConstants.h" #include "Selections/MeshConnectedComponents.h" #include "Selections/MeshFaceSelection.h" #include "Selections/MeshVertexSelection.h" #include "Polygroups/PolygroupUtil.h" #include "Polygon2.h" #include "Changes/MeshVertexChange.h" #include "Changes/MeshPolygroupChange.h" #include "Changes/BasicChanges.h" #include "Sculpting/MeshGroupPaintBrushOps.h" #include "Sculpting/StampFalloffs.h" #include "Sculpting/MeshSculptUtil.h" #include "CanvasTypes.h" #include "CanvasItem.h" #include "Engine/Engine.h" // for GEngine->GetSmallFont() #include "SceneView.h" #include "ToolTargetManager.h" #include "ToolBuilderUtil.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(MeshGroupPaintTool) using namespace UE::Geometry; #define LOCTEXT_NAMESPACE "UMeshGroupPaintTool" namespace { // probably should be something defined for the whole tool framework... #if WITH_EDITOR static EAsyncExecution GroupPaintToolAsyncExecTarget = EAsyncExecution::LargeThreadPool; #else static EAsyncExecution GroupPaintToolAsyncExecTarget = EAsyncExecution::ThreadPool; #endif } /* * ToolBuilder */ UMeshSurfacePointTool* UMeshGroupPaintToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const { UMeshGroupPaintTool* SculptTool = NewObject(SceneState.ToolManager); SculptTool->SetWorld(SceneState.World); return SculptTool; } bool UMeshGroupPaintToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const { return UMeshSurfacePointMeshEditingToolBuilder::CanBuildTool(SceneState) && SceneState.TargetManager->CountSelectedAndTargetableWithPredicate(SceneState, GetTargetRequirements(), [](UActorComponent& Component) { return !ToolBuilderUtil::IsVolume(Component); }) >= 1; } /* * Properties */ void UMeshGroupPaintToolActionPropertySet::PostAction(EMeshGroupPaintToolActions Action) { if (ParentTool.IsValid()) { ParentTool->RequestAction(Action); } } /* * Tool */ void UMeshGroupPaintTool::Setup() { UMeshSculptToolBase::Setup(); SetToolDisplayName(LOCTEXT("ToolName", "Paint PolyGroups")); // create dynamic mesh component to use for live preview FActorSpawnParameters SpawnInfo; PreviewMeshActor = TargetWorld->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo); DynamicMeshComponent = NewObject(PreviewMeshActor); InitializeSculptMeshComponent(DynamicMeshComponent, PreviewMeshActor); // assign materials FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Target); for (int k = 0; k < MaterialSet.Materials.Num(); ++k) { DynamicMeshComponent->SetMaterial(k, MaterialSet.Materials[k]); } DynamicMeshComponent->SetInvalidateProxyOnChangeEnabled(false); OnDynamicMeshComponentChangedHandle = DynamicMeshComponent->OnMeshChanged.AddUObject(this, &UMeshGroupPaintTool::OnDynamicMeshComponentChanged); FDynamicMesh3* Mesh = GetSculptMesh(); Mesh->EnableVertexColors(FVector3f::One()); FAxisAlignedBox3d Bounds = Mesh->GetBounds(true); TFuture PrecomputeFuture = Async(GroupPaintToolAsyncExecTarget, [&]() { PrecomputeFilterData(); }); TFuture OctreeFuture = Async(GroupPaintToolAsyncExecTarget, [&]() { // initialize dynamic octree if (Mesh->TriangleCount() > 100000) { Octree.RootDimension = Bounds.MaxDim() / 10.0; Octree.SetMaxTreeDepth(4); } else { Octree.RootDimension = Bounds.MaxDim(); Octree.SetMaxTreeDepth(8); } Octree.Initialize(Mesh); //Octree.CheckValidity(EValidityCheckFailMode::Check, true, true); //FDynamicMeshOctree3::FStatistics Stats; //Octree.ComputeStatistics(Stats); //UE_LOG(LogTemp, Warning, TEXT("Octree Stats: %s"), *Stats.ToString()); }); // initialize render decomposition TUniquePtr Decomp = MakeUnique(); FMeshRenderDecomposition::BuildChunkedDecomposition(Mesh, &MaterialSet, *Decomp); Decomp->BuildAssociations(Mesh); //UE_LOG(LogTemp, Warning, TEXT("Decomposition has %d groups"), Decomp->Num()); DynamicMeshComponent->SetExternalDecomposition(MoveTemp(Decomp)); // initialize brush radius range interval, brush properties UMeshSculptToolBase::InitializeBrushSizeRange(Bounds); // Set up control points mechanic PolyLassoMechanic = NewObject(this); PolyLassoMechanic->Setup(this); PolyLassoMechanic->SetIsEnabled(false); PolyLassoMechanic->SpacingTolerance = 10.0f; PolyLassoMechanic->OnDrawPolyLassoFinished.AddUObject(this, &UMeshGroupPaintTool::OnPolyLassoFinished); PolygroupLayerProperties = NewObject(this); PolygroupLayerProperties->RestoreProperties(this, TEXT("MeshGroupPaintTool")); PolygroupLayerProperties->InitializeGroupLayers(GetSculptMesh()); PolygroupLayerProperties->WatchProperty(PolygroupLayerProperties->ActiveGroupLayer, [&](FName) { OnSelectedGroupLayerChanged(); }); UpdateActiveGroupLayer(); AddToolPropertySource(PolygroupLayerProperties); // initialize other properties FilterProperties = NewObject(this); FilterProperties->WatchProperty(FilterProperties->SubToolType, [this](EMeshGroupPaintInteractionType NewType) { UpdateSubToolType(NewType); }); FilterProperties->WatchProperty(FilterProperties->BrushSize, [this](float NewSize) { UMeshSculptToolBase::BrushProperties->BrushSize.AdaptiveSize = NewSize; CalculateBrushRadius(); }); FilterProperties->WatchProperty(FilterProperties->bHitBackFaces, [this](bool bNewValue) { UMeshSculptToolBase::BrushProperties->bHitBackFaces = bNewValue; }); FilterProperties->RestoreProperties(this); FilterProperties->BrushSize = UMeshSculptToolBase::BrushProperties->BrushSize.AdaptiveSize; FilterProperties->bHitBackFaces = UMeshSculptToolBase::BrushProperties->bHitBackFaces; FilterProperties->SetGroup = ActiveGroupSet->MaxGroupID; AddToolPropertySource(FilterProperties); InitializeIndicator(); // initialize our properties AddToolPropertySource(UMeshSculptToolBase::BrushProperties); UMeshSculptToolBase::BrushProperties->bShowPerBrushProps = false; UMeshSculptToolBase::BrushProperties->bShowFalloff = false; UMeshSculptToolBase::BrushProperties->bShowLazyness = false; CalculateBrushRadius(); PaintBrushOpProperties = NewObject(this); RegisterBrushType((int32)EMeshGroupPaintBrushType::Paint, LOCTEXT("Paint", "Paint"), MakeUnique([this]() { return MakeUnique(); }), PaintBrushOpProperties); // secondary brushes EraseBrushOpProperties = NewObject(this); EraseBrushOpProperties->GetCurrentGroupLambda = [this]() { return PaintBrushOpProperties->GetGroup(); }; RegisterSecondaryBrushType((int32)EMeshGroupPaintBrushType::Erase, LOCTEXT("Erase", "Erase"), MakeUnique>(), EraseBrushOpProperties); AddToolPropertySource(UMeshSculptToolBase::ViewProperties); AddToolPropertySource(UMeshSculptToolBase::GizmoProperties); SetToolPropertySourceEnabled(UMeshSculptToolBase::GizmoProperties, false); // register watchers FilterProperties->WatchProperty( FilterProperties->PrimaryBrushType, [this](EMeshGroupPaintBrushType NewType) { UpdateBrushType(NewType); }); // must call before updating brush type so that we register all brush properties? UMeshSculptToolBase::OnCompleteSetup(); UpdateBrushType(FilterProperties->PrimaryBrushType); SetActiveSecondaryBrushType((int32)EMeshGroupPaintBrushType::Erase); FreezeActions = NewObject(this); FreezeActions->Initialize(this); AddToolPropertySource(FreezeActions); MeshElementsDisplay = NewObject(this); MeshElementsDisplay->CreateInWorld(DynamicMeshComponent->GetWorld(), DynamicMeshComponent->GetComponentTransform()); if (ensure(MeshElementsDisplay->Settings)) { MeshElementsDisplay->Settings->RestoreProperties(this, TEXT("MeshGroupPaintTool")); AddToolPropertySource(MeshElementsDisplay->Settings); } MeshElementsDisplay->SetMeshAccessFunction([this](UMeshElementsVisualizer::ProcessDynamicMeshFunc ProcessFunc) { ProcessFunc(*GetSculptMesh()); }); // force colors update... ? DynamicMeshComponent->SetTriangleColorFunction([this](const FDynamicMesh3* Mesh, int TriangleID) { return GetColorForGroup(ActiveGroupSet->GetGroup(TriangleID)); }); // disable view properties SetViewPropertiesEnabled(false); UpdateMaterialMode(EMeshEditingMaterialModes::VertexColor); UpdateWireframeVisibility(false); UpdateFlatShadingSetting(true); // configure panels UpdateSubToolType(FilterProperties->SubToolType); PrecomputeFuture.Wait(); OctreeFuture.Wait(); } void UMeshGroupPaintTool::Shutdown(EToolShutdownType ShutdownType) { if (DynamicMeshComponent != nullptr) { DynamicMeshComponent->OnMeshChanged.Remove(OnDynamicMeshComponentChangedHandle); } if (ensure(MeshElementsDisplay->Settings)) { MeshElementsDisplay->Settings->SaveProperties(this, TEXT("MeshGroupPaintTool")); } MeshElementsDisplay->Disconnect(); FilterProperties->SaveProperties(this); PolygroupLayerProperties->SaveProperties(this, TEXT("MeshGroupPaintTool")); if (PreviewMeshActor != nullptr) { PreviewMeshActor->Destroy(); PreviewMeshActor = nullptr; } UMeshSculptToolBase::Shutdown(ShutdownType); } void UMeshGroupPaintTool::CommitResult(UBaseDynamicMeshComponent* Component, bool bModifiedTopology) { GetToolManager()->BeginUndoTransaction(LOCTEXT("GroupPaintToolTransactionName", "Paint Groups")); Component->ProcessMesh([&](const FDynamicMesh3& CurMesh) { UE::ToolTarget::CommitDynamicMeshUpdate(Target, CurMesh, true); }); GetToolManager()->EndUndoTransaction(); } void UMeshGroupPaintTool::RegisterActions(FInteractiveToolActionSet& ActionSet) { UMeshSculptToolBase::RegisterActions(ActionSet); ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 500, TEXT("PickGroupColorUnderCursor"), LOCTEXT("PickGroupColorUnderCursor", "Pick PolyGroup"), LOCTEXT("PickGroupColorUnderCursorTooltip", "Switch the active PolyGroup to the group currently under the cursor"), EModifierKey::Shift, EKeys::G, [this]() { bPendingPickGroup = true; }); ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 501, TEXT("ToggleFrozenGroup"), LOCTEXT("ToggleFrozenGroup", "Toggle Group Frozen State"), LOCTEXT("ToggleFrozenGroupTooltip", "Toggle Group Frozen State"), EModifierKey::Shift, EKeys::F, [this]() { bPendingToggleFreezeGroup = true; }); ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 502, TEXT("CreateNewGroup"), LOCTEXT("CreateNewGroup", "New Group"), LOCTEXT("CreateNewGroupTooltip", "Allocate a new Polygroup and set as Current"), EModifierKey::Shift, EKeys::Q, [this]() { AllocateNewGroupAndSetAsCurrentAction(); }); }; TUniquePtr& UMeshGroupPaintTool::GetActiveBrushOp() { if (GetInEraseStroke()) { return SecondaryBrushOp; } else { return PrimaryBrushOp; } } void UMeshGroupPaintTool::IncreaseBrushRadiusAction() { Super::IncreaseBrushRadiusAction(); FilterProperties->BrushSize = BrushProperties->BrushSize.AdaptiveSize; NotifyOfPropertyChangeByTool(FilterProperties); } void UMeshGroupPaintTool::DecreaseBrushRadiusAction() { Super::DecreaseBrushRadiusAction(); FilterProperties->BrushSize = BrushProperties->BrushSize.AdaptiveSize; NotifyOfPropertyChangeByTool(FilterProperties); } void UMeshGroupPaintTool::IncreaseBrushRadiusSmallStepAction() { Super::IncreaseBrushRadiusSmallStepAction(); FilterProperties->BrushSize = BrushProperties->BrushSize.AdaptiveSize; NotifyOfPropertyChangeByTool(FilterProperties); } void UMeshGroupPaintTool::DecreaseBrushRadiusSmallStepAction() { Super::DecreaseBrushRadiusSmallStepAction(); FilterProperties->BrushSize = BrushProperties->BrushSize.AdaptiveSize; NotifyOfPropertyChangeByTool(FilterProperties); } bool UMeshGroupPaintTool::IsInBrushSubMode() const { return FilterProperties->SubToolType == EMeshGroupPaintInteractionType::Brush || FilterProperties->SubToolType == EMeshGroupPaintInteractionType::Fill || FilterProperties->SubToolType == EMeshGroupPaintInteractionType::GroupFill; } void UMeshGroupPaintTool::OnBeginStroke(const FRay& WorldRay) { UpdateBrushPosition(WorldRay); if (PaintBrushOpProperties) { PaintBrushOpProperties->Group = FilterProperties->SetGroup; PaintBrushOpProperties->bOnlyPaintUngrouped = FilterProperties->bOnlySetUngrouped; } if (EraseBrushOpProperties) { EraseBrushOpProperties->Group = FilterProperties->EraseGroup; EraseBrushOpProperties->bOnlyEraseCurrent = FilterProperties->bOnlyEraseCurrent; } // initialize first "Last Stamp", so that we can assume all stamps in stroke have a valid previous stamp LastStamp.WorldFrame = GetBrushFrameWorld(); LastStamp.LocalFrame = GetBrushFrameLocal(); LastStamp.Radius = GetCurrentBrushRadius(); LastStamp.Falloff = GetCurrentBrushFalloff(); LastStamp.Direction = GetInInvertStroke() ? -1.0 : 1.0; LastStamp.Depth = GetCurrentBrushDepth(); LastStamp.Power = GetActivePressure() * GetCurrentBrushStrength(); LastStamp.TimeStamp = FDateTime::Now(); FSculptBrushOptions SculptOptions; //SculptOptions.bPreserveUVFlow = false; // FilterProperties->bPreserveUVFlow; SculptOptions.ConstantReferencePlane = GetCurrentStrokeReferencePlane(); TUniquePtr& UseBrushOp = GetActiveBrushOp(); UseBrushOp->ConfigureOptions(SculptOptions); UseBrushOp->BeginStroke(GetSculptMesh(), LastStamp, VertexROI); AccumulatedTriangleROI.Reset(); // begin change here? or wait for first stamp? BeginChange(); } void UMeshGroupPaintTool::OnEndStroke() { GetActiveBrushOp()->EndStroke(GetSculptMesh(), LastStamp, VertexROI); // close change record EndChange(); } void UMeshGroupPaintTool::OnCancelStroke() { GetActiveBrushOp()->CancelStroke(); ActiveGroupEditBuilder.Reset(); bDrawGroupsDataValid = false; } void UMeshGroupPaintTool::UpdateROI(const FSculptBrushStamp& BrushStamp) { SCOPE_CYCLE_COUNTER(GroupPaintTool_UpdateROI); int32 SetGroupID = GetInEraseStroke() ? FilterProperties->EraseGroup : FilterProperties->SetGroup; const FVector3d& BrushPos = BrushStamp.LocalFrame.Origin; const FDynamicMesh3* Mesh = GetSculptMesh(); float RadiusSqr = GetCurrentBrushRadius() * GetCurrentBrushRadius(); FAxisAlignedBox3d BrushBox( BrushPos - GetCurrentBrushRadius() * FVector3d::One(), BrushPos + GetCurrentBrushRadius() * FVector3d::One()); TriangleROI.Reset(); int32 CenterTID = GetBrushTriangleID(); if (Mesh->IsTriangle(CenterTID)) { TriangleROI.Add(CenterTID); } FVector3d CenterNormal = Mesh->IsTriangle(CenterTID) ? TriNormals[CenterTID] : FVector3d::One(); // One so that normal check always passes bool bVolumetric = (FilterProperties->BrushAreaMode == EMeshGroupPaintBrushAreaType::Volumetric); bool bUseAngleThreshold = (bVolumetric == false) && (FilterProperties->AngleThreshold < 180.0f); double DotAngleThreshold = FMathd::Cos(FilterProperties->AngleThreshold * FMathd::DegToRad); bool bStopAtUVSeams = FilterProperties->bUVSeams; bool bStopAtNormalSeams = FilterProperties->bNormalSeams; auto CheckEdgeCriteria = [&](int32 t1, int32 t2) -> bool { if (bUseAngleThreshold == false || CenterNormal.Dot(TriNormals[t2]) > DotAngleThreshold) { int32 eid = Mesh->FindEdgeFromTriPair(t1, t2); if (bStopAtUVSeams == false || UVSeamEdges[eid] == false) { if (bStopAtNormalSeams == false || NormalSeamEdges[eid] == false) { return true; } } } return false; }; bool bFill = (FilterProperties->SubToolType == EMeshGroupPaintInteractionType::Fill); bool bGroupFill = (FilterProperties->SubToolType == EMeshGroupPaintInteractionType::GroupFill); if (bVolumetric) { Octree.RangeQuery(BrushBox, [&](int TriIdx) { if ((Mesh->GetTriCentroid(TriIdx) - BrushPos).SquaredLength() < RadiusSqr) { TriangleROI.Add(TriIdx); } }); } else { if (Mesh->IsTriangle(CenterTID)) { TArray StartROI; StartROI.Add(CenterTID); FMeshConnectedComponents::GrowToConnectedTriangles(Mesh, StartROI, TriangleROI, &TempROIBuffer, [&](int t1, int t2) { if ((Mesh->GetTriCentroid(t2) - BrushPos).SquaredLength() < RadiusSqr) { return CheckEdgeCriteria(t1, t2); } return false; }); } } if (bFill) { TArray StartROI; for (int32 tid : TriangleROI) { StartROI.Add(tid); } FMeshConnectedComponents::GrowToConnectedTriangles(Mesh, StartROI, TriangleROI, &TempROIBuffer, [&](int t1, int t2) { return CheckEdgeCriteria(t1, t2); }); } else if (bGroupFill) { TArray StartROI; TSet FillGroups; for (int32 tid : TriangleROI) { if (ActiveGroupSet->GetGroup(tid) != SetGroupID) { StartROI.Add(tid); FillGroups.Add(ActiveGroupSet->GetGroup(tid)); } } FMeshConnectedComponents::GrowToConnectedTriangles(Mesh, StartROI, TriangleROI, &TempROIBuffer, [&](int t1, int t2) { return (FillGroups.Contains(ActiveGroupSet->GetGroup(t2))); }); } // apply visibility filter if (FilterProperties->VisibilityFilter != EMeshGroupPaintVisibilityType::None) { TArray ResultBuffer; ApplyVisibilityFilter(TriangleROI, TempROIBuffer, ResultBuffer); } // construct ROI vertex set VertexSetBuffer.Reset(); for (int32 tid : TriangleROI) { FIndex3i Tri = Mesh->GetTriangle(tid); VertexSetBuffer.Add(Tri.A); VertexSetBuffer.Add(Tri.B); VertexSetBuffer.Add(Tri.C); } VertexROI.SetNum(0, EAllowShrinking::No); BufferUtil::AppendElements(VertexROI, VertexSetBuffer); // construct ROI triangle and group buffers ROITriangleBuffer.Reserve(TriangleROI.Num()); ROITriangleBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : TriangleROI) { ROITriangleBuffer.Add(tid); } ROIGroupBuffer.SetNum(ROITriangleBuffer.Num(), EAllowShrinking::No); } bool UMeshGroupPaintTool::UpdateStampPosition(const FRay& WorldRay) { CalculateBrushRadius(); TUniquePtr& UseBrushOp = GetActiveBrushOp(); ESculptBrushOpTargetType TargetType = UseBrushOp->GetBrushTargetType(); switch (TargetType) { case ESculptBrushOpTargetType::SculptMesh: case ESculptBrushOpTargetType::TargetMesh: UpdateBrushPositionOnSculptMesh(WorldRay, true); break; case ESculptBrushOpTargetType::ActivePlane: check(false); UpdateBrushPositionOnActivePlane(WorldRay); break; } if (UseBrushOp->GetAlignStampToView()) { AlignBrushToView(); } CurrentStamp = LastStamp; CurrentStamp.DeltaTime = FMathd::Min((FDateTime::Now() - LastStamp.TimeStamp).GetTotalSeconds(), 1.0); CurrentStamp.WorldFrame = GetBrushFrameWorld(); CurrentStamp.LocalFrame = GetBrushFrameLocal(); CurrentStamp.Power = GetActivePressure() * GetCurrentBrushStrength(); CurrentStamp.PrevLocalFrame = LastStamp.LocalFrame; CurrentStamp.PrevWorldFrame = LastStamp.WorldFrame; FVector3d MoveDelta = CurrentStamp.LocalFrame.Origin - CurrentStamp.PrevLocalFrame.Origin; if (UseBrushOp->IgnoreZeroMovements() && MoveDelta.SquaredLength() < FMathd::ZeroTolerance) { return false; } return true; } bool UMeshGroupPaintTool::ApplyStamp() { SCOPE_CYCLE_COUNTER(GroupPaintToolApplyStamp); TUniquePtr& UseBrushOp = GetActiveBrushOp(); // yuck FMeshTriangleGroupEditBrushOp* GroupBrushOp = (FMeshTriangleGroupEditBrushOp*)UseBrushOp.Get(); FDynamicMesh3* Mesh = GetSculptMesh(); GroupBrushOp->ApplyStampByTriangles(Mesh, CurrentStamp, ROITriangleBuffer, ROIGroupBuffer); bool bUpdated = SyncMeshWithGroupBuffer(Mesh); LastStamp = CurrentStamp; LastStamp.TimeStamp = FDateTime::Now(); return bUpdated; } bool UMeshGroupPaintTool::SyncMeshWithGroupBuffer(FDynamicMesh3* Mesh) { int NumModified = 0; const int32 NumT = ROITriangleBuffer.Num(); // change update could be async here if we collected array of and dispatched independenlty for ( int32 k = 0; k < NumT; ++k) { int TriIdx = ROITriangleBuffer[k]; int32 CurGroupID = ActiveGroupSet->GetGroup(TriIdx); if (FrozenGroups.Contains(CurGroupID)) // skip frozen groups { continue; } if (ROIGroupBuffer[k] != CurGroupID) { ActiveGroupEditBuilder->SaveTriangle(TriIdx, CurGroupID, ROIGroupBuffer[k]); ActiveGroupSet->SetGroup(TriIdx, ROIGroupBuffer[k], *Mesh); //ActiveVertexChange->UpdateVertexColor(VertIdx, OrigColor, NewColor); NumModified++; } } return (NumModified > 0); } void UMeshGroupPaintTool::OnPolyLassoFinished(const FCameraPolyLasso& Lasso, bool bCanceled) { // construct polyline TArray Polyline = Lasso.Polyline; int32 N = Polyline.Num(); if (N < 2) { return; } // Try to clip polyline to be closed, or closed-enough for winding evaluation to work. // If that returns false, the polyline is "too open". In that case we will extend // outwards from the endpoints and then try to create a closed very large polygon if (UPolyLassoMarqueeMechanic::ApproximateSelfClipPolyline(Polyline) == false) { FVector2d StartDirOut = UE::Geometry::Normalized(Polyline[0] - Polyline[1]); FLine2d StartLine(Polyline[0], StartDirOut); FVector2d EndDirOut = UE::Geometry::Normalized(Polyline[N - 1] - Polyline[N - 2]); FLine2d EndLine(Polyline[N - 1], EndDirOut); // if we did not intersect, we are in ambiguous territory. Check if a segment along either end-direction // intersects the polyline. If it does, we have something like a spiral and will be OK. // If not, make a closed polygon by interpolating outwards from each endpoint, and then in perp-directions. FPolygon2d Polygon(Polyline); float PerpSign = Polygon.IsClockwise() ? -1.0 : 1.0; Polyline.Insert(StartLine.PointAt(10000.0f), 0); Polyline.Insert(Polyline[0] + 1000 * PerpSign * UE::Geometry::PerpCW(StartDirOut), 0); Polyline.Add(EndLine.PointAt(10000.0f)); Polyline.Add(Polyline.Last() + 1000 * PerpSign * UE::Geometry::PerpCW(EndDirOut)); FVector2d StartPos = Polyline[0]; Polyline.Add(StartPos); // close polyline (cannot use Polyline[0] in case Add resizes!) } N = Polyline.Num(); // project each mesh vertex to view plane and evaluate winding integral of polyline const FDynamicMesh3* Mesh = GetSculptMesh(); TempROIBuffer.SetNum(Mesh->MaxVertexID()); ParallelFor(Mesh->MaxVertexID(), [&](int32 vid) { if (Mesh->IsVertex(vid)) { FVector3d WorldPos = CurTargetTransform.TransformPosition(Mesh->GetVertex(vid)); FVector2d PlanePos = (FVector2d)Lasso.GetProjectedPoint((FVector)WorldPos); double WindingSum = 0; FVector2d a = Polyline[0] - PlanePos, b = FVector2d::Zero(); for (int32 i = 1; i < N; ++i) { b = Polyline[i] - PlanePos; WindingSum += (double)FMathd::Atan2(a.X*b.Y - a.Y*b.X, a.X*b.X + a.Y*b.Y); a = b; } WindingSum /= FMathd::TwoPi; bool bInside = FMathd::Abs(WindingSum) > 0.3; TempROIBuffer[vid] = bInside ? 1 : 0; } else { TempROIBuffer[vid] = -1; } }); // convert to vertex selection, and then select fully-enclosed faces FMeshVertexSelection VertexSelection(Mesh); VertexSelection.SelectByVertexID([&](int32 vid) { return TempROIBuffer[vid] == 1; }); FMeshFaceSelection FaceSelection(Mesh, VertexSelection, FilterProperties->MinTriVertCount); if (FaceSelection.Num() == 0) { return; } int32 SetGroupID = GetInEraseStroke() ? FilterProperties->EraseGroup : FilterProperties->SetGroup; SetTrianglesToGroupID(FaceSelection.AsSet(), SetGroupID, GetInEraseStroke()); } void UMeshGroupPaintTool::SetTrianglesToGroupID(const TSet& Triangles, int32 ToGroupID, bool bIsErase) { BeginChange(); TempROIBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : Triangles) { int32 CurGroupID = ActiveGroupSet->GetGroup(tid); if (CurGroupID == ToGroupID || FrozenGroups.Contains(CurGroupID)) // skip frozen groups { continue; } if (bIsErase == false && FilterProperties->bOnlySetUngrouped && CurGroupID != 0) { continue; } if (bIsErase && FilterProperties->bOnlyEraseCurrent && CurGroupID != FilterProperties->SetGroup) { continue; } TempROIBuffer.Add(tid); } if (HaveVisibilityFilter()) { TArray VisibleTriangles; VisibleTriangles.Reserve(TempROIBuffer.Num()); ApplyVisibilityFilter(TempROIBuffer, VisibleTriangles); TempROIBuffer = MoveTemp(VisibleTriangles); } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); for (int32 tid : TempROIBuffer) { ActiveGroupSet->SetGroup(tid, ToGroupID, *GetSculptMesh()); } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } bool UMeshGroupPaintTool::HaveVisibilityFilter() const { return FilterProperties->VisibilityFilter != EMeshGroupPaintVisibilityType::None; } void UMeshGroupPaintTool::ApplyVisibilityFilter(TSet& Triangles, TArray& ROIBuffer, TArray& OutputBuffer) { ROIBuffer.SetNum(0, EAllowShrinking::No); ROIBuffer.Reserve(Triangles.Num()); for (int32 tid : Triangles) { ROIBuffer.Add(tid); } OutputBuffer.Reset(); ApplyVisibilityFilter(TempROIBuffer, OutputBuffer); Triangles.Reset(); for (int32 tid : OutputBuffer) { TriangleROI.Add(tid); } } void UMeshGroupPaintTool::ApplyVisibilityFilter(const TArray& Triangles, TArray& VisibleTriangles) { if (!HaveVisibilityFilter()) { VisibleTriangles = Triangles; return; } FViewCameraState StateOut; GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(StateOut); FTransform3d LocalToWorld = UE::ToolTarget::GetLocalToWorldTransform(Target); FVector3d LocalEyePosition(LocalToWorld.InverseTransformPosition(StateOut.Position)); const FDynamicMesh3* Mesh = GetSculptMesh(); int32 NumTriangles = Triangles.Num(); VisibilityFilterBuffer.SetNum(NumTriangles, EAllowShrinking::No); ParallelFor(NumTriangles, [&](int32 idx) { VisibilityFilterBuffer[idx] = true; FVector3d Centroid = Mesh->GetTriCentroid(Triangles[idx]); FVector3d FaceNormal = Mesh->GetTriNormal(Triangles[idx]); if (FaceNormal.Dot((Centroid - LocalEyePosition)) > 0) { VisibilityFilterBuffer[idx] = false; } if (FilterProperties->VisibilityFilter == EMeshGroupPaintVisibilityType::Unoccluded) { int32 HitTID = Octree.FindNearestHitObject(FRay3d(LocalEyePosition, UE::Geometry::Normalized(Centroid - LocalEyePosition))); if (HitTID != Triangles[idx]) { VisibilityFilterBuffer[idx] = false; } } }); VisibleTriangles.Reset(); for (int32 k = 0; k < NumTriangles; ++k) { if (VisibilityFilterBuffer[k]) { VisibleTriangles.Add(Triangles[k]); } } } int32 UMeshGroupPaintTool::FindHitSculptMeshTriangleConst(const FRay3d& LocalRay) const { if (!IsInBrushSubMode()) { return IndexConstants::InvalidID; } if (GetBrushCanHitBackFaces()) { return Octree.FindNearestHitObject(LocalRay); } else { const FDynamicMesh3* Mesh = GetSculptMesh(); FViewCameraState StateOut; GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(StateOut); FVector3d LocalEyePosition(CurTargetTransform.InverseTransformPosition((FVector3d)StateOut.Position)); int HitTID = Octree.FindNearestHitObject(LocalRay, [this, Mesh, &LocalEyePosition](int TriangleID) { FVector3d Normal, Centroid; double Area; Mesh->GetTriInfo(TriangleID, Normal, Area, Centroid); return Normal.Dot((Centroid - LocalEyePosition)) < 0; }); return HitTID; } } int32 UMeshGroupPaintTool::FindHitTargetMeshTriangleConst(const FRay3d& LocalRay) const { check(false); return IndexConstants::InvalidID; } bool UMeshGroupPaintTool::UpdateBrushPosition(const FRay& WorldRay) { TUniquePtr& UseBrushOp = GetActiveBrushOp(); bool bHit = false; ESculptBrushOpTargetType TargetType = UseBrushOp->GetBrushTargetType(); switch (TargetType) { case ESculptBrushOpTargetType::SculptMesh: case ESculptBrushOpTargetType::TargetMesh: bHit = UpdateBrushPositionOnSculptMesh(WorldRay, false); break; case ESculptBrushOpTargetType::ActivePlane: check(false); bHit = UpdateBrushPositionOnSculptMesh(WorldRay, false); break; } if (bHit && UseBrushOp->GetAlignStampToView()) { AlignBrushToView(); } return bHit; } bool UMeshGroupPaintTool::OnUpdateHover(const FInputDeviceRay& DevicePos) { PendingStampType = FilterProperties->PrimaryBrushType; if(ensure(InStroke() == false)) { UpdateBrushPosition(DevicePos.WorldRay); } return true; } void UMeshGroupPaintTool::DrawHUD(FCanvas* Canvas, IToolsContextRenderAPI* RenderAPI) { if (PolyLassoMechanic) { // because the actual group change is deferred until mouse release, color the lasso to let the user know whether it will erase PolyLassoMechanic->LineColor = GetInEraseStroke() ? FLinearColor::Red : FLinearColor::Green; PolyLassoMechanic->DrawHUD(Canvas, RenderAPI); } float DPIScale = Canvas->GetDPIScale(); UFont* UseFont = GEngine->GetSmallFont(); FViewCameraState CamState = RenderAPI->GetCameraState(); const FSceneView* SceneView = RenderAPI->GetSceneView(); FVector3d LocalEyePosition(CurTargetTransform.InverseTransformPosition((FVector3d)CamState.Position)); FDynamicMesh3* Mesh = GetSculptMesh(); // draw the group number for the group under the cursor if (FilterProperties->bShowHitGroup) { int CursorHitTID = Octree.FindNearestHitObject(FRay3d(LocalEyePosition, Normalized(HoverStamp.LocalFrame.Origin - LocalEyePosition))); if (Mesh->IsTriangle(CursorHitTID)) { int32 GroupID = ActiveGroupSet->GetTriangleGroup(CursorHitTID); FVector2D CursorPixelPos; SceneView->WorldToPixel(HoverStamp.WorldFrame.Origin, CursorPixelPos); FString CursorString = FString::Printf(TEXT("%d"), GroupID); Canvas->DrawShadowedString(CursorPixelPos.X / (double)DPIScale, CursorPixelPos.Y / (double)DPIScale, *CursorString, UseFont, FLinearColor::White); } } // draw the group number for all groups if (FilterProperties->bShowAllGroups) { if (bDrawGroupsDataValid == false) { GroupVisualizationCache.UpdateGroupInfo_ConnectedComponents(*Mesh, *ActiveGroupSet); bDrawGroupsDataValid = true; } for (const FGroupVisualizationCache::FGroupInfo& GroupInfo : GroupVisualizationCache) { FRay3d EyeRay; EyeRay.Origin = LocalEyePosition; EyeRay.Direction = Normalized(GroupInfo.Center - LocalEyePosition); int HitTID = Octree.FindNearestHitObject(EyeRay); if (HitTID == GroupInfo.CenterTris.A || HitTID == GroupInfo.CenterTris.B) { FVector2D PixelPos; SceneView->WorldToPixel(CurTargetTransform.TransformPosition(GroupInfo.Center), PixelPos); FString String = FString::Printf(TEXT("%d"), GroupInfo.GroupID); Canvas->DrawShadowedString(PixelPos.X / (double)DPIScale, PixelPos.Y / (double)DPIScale, *String, UseFont, FLinearColor::White); } } } } void UMeshGroupPaintTool::OnTick(float DeltaTime) { UMeshSculptToolBase::OnTick(DeltaTime); MeshElementsDisplay->OnTick(DeltaTime); bool bIsLasso = (FilterProperties->SubToolType == EMeshGroupPaintInteractionType::PolyLasso); PolyLassoMechanic->SetIsEnabled(bIsLasso); ConfigureIndicator(FilterProperties->BrushAreaMode == EMeshGroupPaintBrushAreaType::Volumetric); SetIndicatorVisibility(bIsLasso == false); if (bHavePendingAction) { ApplyAction(PendingAction); bHavePendingAction = false; PendingAction = EMeshGroupPaintToolActions::NoAction; } SCOPE_CYCLE_COUNTER(GroupPaintToolTick); // process the undo update if (bUndoUpdatePending) { // wait for updates WaitForPendingUndoRedoUpdate(); // post rendering update DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(AccumulatedTriangleROI, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); bDrawGroupsDataValid = false; // ignore stamp and wait for next tick to do anything else bUndoUpdatePending = false; return; } if (bPendingPickGroup || bPendingToggleFreezeGroup) { if (GetBrushTriangleID() >= 0 && IsStampPending() == false ) { if (GetSculptMesh()->IsTriangle(GetBrushTriangleID())) { int32 HitGroupID = ActiveGroupSet->GetGroup(GetBrushTriangleID()); if (bPendingPickGroup) { FilterProperties->SetGroup = HitGroupID; NotifyOfPropertyChangeByTool(FilterProperties); } else if (bPendingToggleFreezeGroup) { ToggleFrozenGroup(HitGroupID); } } } bPendingPickGroup = bPendingToggleFreezeGroup = false; } auto ExecuteStampOperation = [this](int StampIndex, const FRay& StampRay) { SCOPE_CYCLE_COUNTER(GroupPaintTool_Tick_ApplyStampBlock); // update sculpt ROI UpdateROI(CurrentStamp); // append updated ROI to modified region (async) TFuture AccumulateROI = Async(GroupPaintToolAsyncExecTarget, [&]() { AccumulatedTriangleROI.Append(TriangleROI); }); // apply the stamp bool bGroupsModified = ApplyStamp(); if (bGroupsModified) { SCOPE_CYCLE_COUNTER(GroupPaintTool_Tick_UpdateMeshBlock); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TriangleROI, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); } // we don't really need to wait for these to happen to end Tick()... AccumulateROI.Wait(); }; if (IsInBrushSubMode()) { ProcessPerTickStamps( [this](const FRay& StampRay) -> bool { return UpdateStampPosition(StampRay); }, ExecuteStampOperation); } } void UMeshGroupPaintTool::AllocateNewGroupAndSetAsCurrentAction() { FilterProperties->SetGroup = ActiveGroupSet->MaxGroupID; NotifyOfPropertyChangeByTool(FilterProperties); } FColor UMeshGroupPaintTool::GetColorForGroup(int32 GroupID) { FColor Color = LinearColors::SelectFColor(GroupID); if (FrozenGroups.Contains(GroupID)) { int32 GrayValue = (Color.R + Color.G + Color.B) / 3; Color.R = Color.G = Color.B = FMath::Clamp(GrayValue, 0, 255); } return Color; } void UMeshGroupPaintTool::ToggleFrozenGroup(int32 FreezeGroupID) { if (FreezeGroupID == 0) return; TArray InitialFrozenGroups = FrozenGroups; if (FrozenGroups.Contains(FreezeGroupID)) { FrozenGroups.Remove(FreezeGroupID); } else { FrozenGroups.Add(FreezeGroupID); } const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); TempROIBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : Mesh->TriangleIndicesItr()) { int32 TriGroupID = ActiveGroupSet->GetGroup(tid); if (TriGroupID == FreezeGroupID) { TempROIBuffer.Add(tid); } } EmitFrozenGroupsChange(InitialFrozenGroups, FrozenGroups, LOCTEXT("ToggleFrozenGroupChange", "Toggle Frozen Group")); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); } void UMeshGroupPaintTool::FreezeOtherGroups(int32 KeepGroupID) { TArray InitialFrozenGroups = FrozenGroups; FrozenGroups.Reset(); const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); TempROIBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : Mesh->TriangleIndicesItr()) { int32 GroupID = ActiveGroupSet->GetGroup(tid); if ( GroupID != 0 && GroupID != KeepGroupID) { FrozenGroups.AddUnique(ActiveGroupSet->GetGroup(tid)); TempROIBuffer.Add(tid); } } EmitFrozenGroupsChange(InitialFrozenGroups, FrozenGroups, LOCTEXT("FreezeOtherGroups", "Freeze Other Groups")); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); } void UMeshGroupPaintTool::ClearAllFrozenGroups() { TArray InitialFrozenGroups = FrozenGroups; const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); TempROIBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : Mesh->TriangleIndicesItr()) { if ( FrozenGroups.Contains(ActiveGroupSet->GetGroup(tid)) ) { TempROIBuffer.Add(tid); } } FrozenGroups.Reset(); EmitFrozenGroupsChange(InitialFrozenGroups, FrozenGroups, LOCTEXT("ClearAllFrozenGroups", "Clear Frozen Groups")); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); } void UMeshGroupPaintTool::EmitFrozenGroupsChange(const TArray& FromGroups, const TArray& ToGroups, const FText& ChangeText) { if (FromGroups != ToGroups) { TUniquePtr>> FrozenGroupsChange = MakeUnique>>(); FrozenGroupsChange->FromValue = FromGroups; FrozenGroupsChange->ToValue = ToGroups; FrozenGroupsChange->ValueChangeFunc = [this](UObject*, const TArray& FromGroups, const TArray& ToGroups, bool) { FrozenGroups = ToGroups; DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); }; GetToolManager()->EmitObjectChange(this, MoveTemp(FrozenGroupsChange), ChangeText); } } void UMeshGroupPaintTool::GrowCurrentGroupAction() { BeginChange(); int32 CurrentGroupID = FilterProperties->SetGroup; const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); FMeshFaceSelection InitialSelection(Mesh); InitialSelection.Select([&](int32 tid) { return ActiveGroupSet->GetGroup(tid) == CurrentGroupID; }); FMeshFaceSelection ExpandSelection(InitialSelection); ExpandSelection.ExpandToOneRingNeighbours([&](int32 tid) { return FrozenGroups.Contains(ActiveGroupSet->GetGroup(tid)) == false; }); TempROIBuffer.SetNum(0, EAllowShrinking::No); ExpandSelection.SetDifference(InitialSelection, TempROIBuffer); ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); for (int32 tid : TempROIBuffer) { ActiveGroupSet->SetGroup(tid, CurrentGroupID, *GetSculptMesh()); } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshGroupPaintTool::ShrinkCurrentGroupAction() { BeginChange(); int32 CurrentGroupID = FilterProperties->SetGroup; const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); FMeshFaceSelection InitialSelection(Mesh); InitialSelection.Select([&](int32 tid) { return ActiveGroupSet->GetGroup(tid) == CurrentGroupID; }); FMeshFaceSelection ContractSelection(InitialSelection); ContractSelection.ContractBorderByOneRingNeighbours(); TempROIBuffer.SetNum(0, EAllowShrinking::No); InitialSelection.SetDifference(ContractSelection, TempROIBuffer); ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); for (int32 tid : TempROIBuffer) { // todo: could probably guess boundary groups here... ActiveGroupSet->SetGroup(tid, 0, *GetSculptMesh()); } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshGroupPaintTool::ClearCurrentGroupAction() { BeginChange(); int32 CurrentGroupID = FilterProperties->SetGroup; const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); TempROIBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : Mesh->TriangleIndicesItr()) { if (ActiveGroupSet->GetGroup(tid) == CurrentGroupID) { TempROIBuffer.Add(tid); } } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); for (int32 tid : TempROIBuffer) { ActiveGroupSet->SetGroup(tid, 0, *GetSculptMesh()); } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshGroupPaintTool::FloodFillCurrentGroupAction() { BeginChange(); int32 SetGroupID = FilterProperties->SetGroup; const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); TempROIBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : Mesh->TriangleIndicesItr()) { int32 GroupID = ActiveGroupSet->GetGroup(tid); if (GroupID == 0 && GroupID != SetGroupID && FrozenGroups.Contains(GroupID) == false) { TempROIBuffer.Add(tid); } } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); for (int32 tid : TempROIBuffer) { ActiveGroupSet->SetGroup(tid, SetGroupID, *GetSculptMesh()); } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshGroupPaintTool::ClearAllGroupsAction() { BeginChange(); const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); TempROIBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : Mesh->TriangleIndicesItr()) { if (ActiveGroupSet->GetGroup(tid) != 0) { TempROIBuffer.Add(tid); } } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); for (int32 tid : TempROIBuffer) { ActiveGroupSet->SetGroup(tid, 0, *GetSculptMesh()); } ActiveGroupEditBuilder->SaveTriangles(TempROIBuffer); DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(TempROIBuffer, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } // // Change Tracking // void UMeshGroupPaintTool::BeginChange() { check(ActiveGroupEditBuilder == nullptr); ActiveGroupEditBuilder = MakeUnique(ActiveGroupSet.Get()); LongTransactions.Open(LOCTEXT("GroupPaintChange", "Group Stroke"), GetToolManager()); } void UMeshGroupPaintTool::EndChange() { check(ActiveGroupEditBuilder); TUniquePtr EditResult = ActiveGroupEditBuilder->ExtractResult(); ActiveGroupEditBuilder = nullptr; TUniquePtr> NewChange = MakeUnique>(); NewChange->WrappedChange = MakeUnique(MoveTemp(EditResult)); NewChange->BeforeModify = [this](bool bRevert) { this->WaitForPendingUndoRedoUpdate(); }; GetToolManager()->EmitObjectChange(DynamicMeshComponent, MoveTemp(NewChange), LOCTEXT("GroupPaintChange", "Group Stroke")); LongTransactions.Close(GetToolManager()); // debug groups are invalid now bDrawGroupsDataValid = false; } void UMeshGroupPaintTool::WaitForPendingUndoRedoUpdate() { if (bUndoUpdatePending) { bUndoUpdatePending = false; } } void UMeshGroupPaintTool::OnDynamicMeshComponentChanged() { // update octree FDynamicMesh3* Mesh = GetSculptMesh(); // make sure any previous async computations are done, and update the undo ROI if (bUndoUpdatePending) { // we should never hit this anymore, because of pre-change calling WaitForPendingUndoRedoUpdate() WaitForPendingUndoRedoUpdate(); // TODO: do we need to read from mesh change here?? //UE::Geometry::VertexToTriangleOneRing(Mesh, Change->Vertices, AccumulatedTriangleROI); } else { // TODO: do we need to read from mesh change here?? //UE::Geometry::VertexToTriangleOneRing(Mesh, Change->Vertices, AccumulatedTriangleROI); } // note that we have a pending update bUndoUpdatePending = true; } void UMeshGroupPaintTool::PrecomputeFilterData() { const FDynamicMesh3* Mesh = GetSculptMesh(); TriNormals.SetNum(Mesh->MaxTriangleID()); ParallelFor(Mesh->MaxTriangleID(), [&](int32 tid) { if (Mesh->IsTriangle(tid)) { TriNormals[tid] = Mesh->GetTriNormal(tid); } }); const FDynamicMeshNormalOverlay* Normals = Mesh->Attributes()->PrimaryNormals(); const FDynamicMeshUVOverlay* UVs = Mesh->Attributes()->PrimaryUV(); UVSeamEdges.SetNum(Mesh->MaxEdgeID()); NormalSeamEdges.SetNum(Mesh->MaxEdgeID()); ParallelFor(Mesh->MaxEdgeID(), [&](int32 eid) { if (Mesh->IsEdge(eid)) { UVSeamEdges[eid] = UVs->IsSeamEdge(eid); NormalSeamEdges[eid] = Normals->IsSeamEdge(eid); } }); } void UMeshGroupPaintTool::OnSelectedGroupLayerChanged() { GetToolManager()->BeginUndoTransaction(LOCTEXT("ChangeActiveGroupLayer", "Change Polygroup Layer")); TArray InitialFrozenGroups = FrozenGroups; int32 ActiveLayerIndex = (ActiveGroupSet) ? ActiveGroupSet->GetPolygroupIndex() : -1; UpdateActiveGroupLayer(); int32 NewLayerIndex = (ActiveGroupSet) ? ActiveGroupSet->GetPolygroupIndex() : -1; if (ActiveLayerIndex != NewLayerIndex) { // clear frozen groups EmitFrozenGroupsChange(InitialFrozenGroups, FrozenGroups, LOCTEXT("ClearAllFrozenGroups", "Clear Frozen Groups")); TUniquePtr> GroupLayerChange = MakeUnique>(); GroupLayerChange->FromValue = ActiveLayerIndex; GroupLayerChange->ToValue = NewLayerIndex; GroupLayerChange->ValueChangeFunc = [this](UObject*, int32 FromIndex, int32 ToIndex, bool) { this->PolygroupLayerProperties->SetSelectedFromPolygroupIndex(ToIndex); this->PolygroupLayerProperties->SilentUpdateWatched(); // to prevent OnSelectedGroupLayerChanged() from being called immediately this->UpdateActiveGroupLayer(); }; GetToolManager()->EmitObjectChange(this, MoveTemp(GroupLayerChange), LOCTEXT("ChangeActiveGroupLayer", "Change Polygroup Layer")); } GetToolManager()->EndUndoTransaction(); } void UMeshGroupPaintTool::UpdateActiveGroupLayer() { if (PolygroupLayerProperties->HasSelectedPolygroup() == false) { ActiveGroupSet = MakeUnique(GetSculptMesh()); } else { FName SelectedName = PolygroupLayerProperties->ActiveGroupLayer; FDynamicMeshPolygroupAttribute* FoundAttrib = UE::Geometry::FindPolygroupLayerByName(*GetSculptMesh(), SelectedName); ensureMsgf(FoundAttrib, TEXT("Selected Attribute Not Found! Falling back to Default group layer.")); ActiveGroupSet = MakeUnique(GetSculptMesh(), FoundAttrib); } // need to reset everything here... FrozenGroups.Reset(); // update colors DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); } void UMeshGroupPaintTool::UpdateSubToolType(EMeshGroupPaintInteractionType NewType) { // Currenly we mirror base-brush properties in UGroupPaintBrushFilterProperties, so we never // want to show both //bool bSculptPropsVisible = (NewType == EMeshGroupPaintInteractionType::Brush); //SetToolPropertySourceEnabled(UMeshSculptToolBase::BrushProperties, bSculptPropsVisible); SetToolPropertySourceEnabled(UMeshSculptToolBase::BrushProperties, false); SetToolPropertySourceEnabled(FilterProperties, true); SetBrushOpPropsVisibility(false); } void UMeshGroupPaintTool::UpdateBrushType(EMeshGroupPaintBrushType BrushType) { static const FText BaseMessage = LOCTEXT("OnStartTool", "Hold Shift to Erase. [/] and S/D change Size (+Shift to small-step). Shift+Q for New Group, Shift+G to pick Group, Shift+F to Freeze Group."); FTextBuilder Builder; Builder.AppendLine(BaseMessage); SetActivePrimaryBrushType((int32)BrushType); SetToolPropertySourceEnabled(GizmoProperties, false); GetToolManager()->DisplayMessage(Builder.ToText(), EToolMessageLevel::UserNotification); } void UMeshGroupPaintTool::RequestAction(EMeshGroupPaintToolActions ActionType) { if (!bHavePendingAction) { PendingAction = ActionType; bHavePendingAction = true; } } void UMeshGroupPaintTool::ApplyAction(EMeshGroupPaintToolActions ActionType) { switch (ActionType) { case EMeshGroupPaintToolActions::ClearFrozen: ClearAllFrozenGroups(); break; case EMeshGroupPaintToolActions::FreezeCurrent: ToggleFrozenGroup(FilterProperties->SetGroup); break; case EMeshGroupPaintToolActions::FreezeOthers: FreezeOtherGroups(FilterProperties->SetGroup); break; case EMeshGroupPaintToolActions::GrowCurrent: GrowCurrentGroupAction(); break; case EMeshGroupPaintToolActions::ShrinkCurrent: ShrinkCurrentGroupAction(); break; case EMeshGroupPaintToolActions::ClearCurrent: ClearCurrentGroupAction(); break; case EMeshGroupPaintToolActions::FloodFillCurrent: FloodFillCurrentGroupAction(); break; case EMeshGroupPaintToolActions::ClearAll: ClearAllGroupsAction(); break; } } #undef LOCTEXT_NAMESPACE