// Copyright Epic Games, Inc. All Rights Reserved. #include "MeshVertexPaintTool.h" #include "Engine/Engine.h" #include "Engine/World.h" #include "InteractiveToolManager.h" #include "ToolTargetManager.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 "DynamicSubmesh3.h" #include "Util/BufferUtil.h" #include "Util/ColorConstants.h" #include "Selection/StoredMeshSelectionUtil.h" // GetCurrentGeometrySelectionForTarget #include "Selections/GeometrySelectionUtil.h" #include "Selections/MeshConnectedComponents.h" #include "Selections/MeshFaceSelection.h" #include "Selections/MeshVertexSelection.h" #include "Polygroups/PolygroupUtil.h" #include "Polygon2.h" #include "DynamicMesh/Operations/SplitAttributeWelder.h" #include "MeshDescriptionToDynamicMesh.h" #include "DynamicMeshToMeshDescription.h" #include "TargetInterfaces/MeshDescriptionProvider.h" #include "TargetInterfaces/MeshDescriptionCommitter.h" #include "TargetInterfaces/PrimitiveComponentBackedTarget.h" #include "Changes/MeshVertexChange.h" #include "Changes/MeshPolygroupChange.h" #include "Changes/BasicChanges.h" #include "Sculpting/MeshVertexPaintBrushOps.h" #include "Sculpting/StampFalloffs.h" #include "Sculpting/MeshSculptUtil.h" #include "WeightMapUtil.h" #include "WeightMapTypes.h" #include "CanvasTypes.h" #include "CanvasItem.h" #include "Engine/Engine.h" // for GEngine->GetSmallFont() #include "SceneView.h" #include "Materials/Material.h" #include "Components/MeshComponent.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(MeshVertexPaintTool) using namespace UE::Geometry; #define LOCTEXT_NAMESPACE "UMeshVertexPaintTool" namespace { // probably should be something defined for the whole tool framework... #if WITH_EDITOR static EAsyncExecution VertexPaintToolAsyncExecTarget = EAsyncExecution::LargeThreadPool; #else static EAsyncExecution VertexPaintToolAsyncExecTarget = EAsyncExecution::ThreadPool; #endif } /* * ToolBuilder */ UMeshSurfacePointTool* UMeshVertexPaintToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const { UMeshVertexPaintTool* SculptTool = NewObject(SceneState.ToolManager); SculptTool->SetWorld(SceneState.World); return SculptTool; } void UMeshVertexPaintToolBuilder::InitializeNewTool(UMeshSurfacePointTool* NewTool, const FToolBuilderState& SceneState) const { Super::InitializeNewTool(NewTool, SceneState); UMeshVertexPaintTool* Tool = Cast(NewTool); if (ensure(Tool && Tool->GetTarget())) { UE::Geometry::FGeometrySelection Selection; bool bHaveSelection = UE::Geometry::GetCurrentGeometrySelectionForTarget(SceneState, Tool->GetTarget(), Selection); if (bHaveSelection) { Tool->SetGeometrySelection(MoveTemp(Selection)); } } } bool UMeshVertexPaintToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const { return UMeshSurfacePointMeshEditingToolBuilder::CanBuildTool(SceneState) && SceneState.TargetManager->CountSelectedAndTargetableWithPredicate(SceneState, GetTargetRequirements(), [](UActorComponent& Component) { return Cast(&Component) != nullptr; }) > 0; } const FToolTargetTypeRequirements& UMeshVertexPaintToolBuilder::GetTargetRequirements() const { static FToolTargetTypeRequirements TypeRequirements({ UMaterialProvider::StaticClass(), UMeshDescriptionProvider::StaticClass(), UMeshDescriptionCommitter::StaticClass(), UPrimitiveComponentBackedTarget::StaticClass() }); return TypeRequirements; } /* * Properties */ void UMeshVertexPaintToolActionPropertySet::PostAction(EMeshVertexPaintToolActions Action) { if (ParentTool.IsValid()) { ParentTool->RequestAction(Action); } } /* * Tool */ void UMeshVertexPaintTool::Setup() { // abort for volumes? //if (ActiveColorOverlay == nullptr) //{ // GetToolManager()->PostActiveToolShutdownRequest(this, EToolShutdownType::Cancel, true, // LOCTEXT("NoColorsShutdown", "Target Mesh does not have or support Vertex Colors")); // return; //} UMeshSculptToolBase::Setup(); SetToolDisplayName(LOCTEXT("ToolName", "Paint Vertex Colors")); // 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, &UMeshVertexPaintTool::OnDynamicMeshComponentChanged); FDynamicMesh3* Mesh = GetSculptMesh(); ActiveColorOverlay = (Mesh->Attributes() != nullptr) ? Mesh->Attributes()->PrimaryColors() : nullptr; if (ActiveColorOverlay == nullptr) { if (Mesh->Attributes() == nullptr) { Mesh->EnableAttributes(); } if (Mesh->Attributes()->PrimaryColors() == nullptr) { Mesh->Attributes()->EnablePrimaryColors(); } ActiveColorOverlay = Mesh->Attributes()->PrimaryColors(); if (ActiveColorOverlay == nullptr) { GetToolManager()->PostActiveToolShutdownRequest(this, EToolShutdownType::Cancel, true, LOCTEXT("InvalidColorsMessage", "Target Mesh does not support Vertex Colors")); return; } ActiveColorOverlay->CreateFromPredicate( [](int, int, int) { return false; }, 1.0f); } else { ActiveColorOverlay->SplitVerticesWithPredicate( [](int ElementIdx, int TriID) { return true; }, [this](int ElementIdx, int TriID, float* FillVect) { FVector4f CurValue = ActiveColorOverlay->GetElement(ElementIdx); FillVect[0] = CurValue.X; FillVect[1] = CurValue.Y; FillVect[2] = CurValue.Z; FillVect[3] = CurValue.W; }); } StrokeInitialColorBuffer.SetNum(ActiveColorOverlay->MaxElementID()); StrokeAccumColorBuffer.SetNum(ActiveColorOverlay->MaxElementID()); //Mesh->EnableVertexColors(FVector3f::One()); FAxisAlignedBox3d Bounds = Mesh->GetBounds(true); FilterProperties = NewObject(this); FilterProperties->RestoreProperties(this); FilterProperties->WatchProperty(FilterProperties->MaterialMode, [this](EMeshVertexPaintMaterialMode) { UpdateVertexPaintMaterialMode(); }); FilterProperties->bToolHasSelection = GeometrySelection.IsSet(); SelectionTids.Reset(); TFuture PrecomputeFuture = Async(VertexPaintToolAsyncExecTarget, [&]() { PrecomputeFilterData(); }); TFuture OctreeFuture = Async(VertexPaintToolAsyncExecTarget, [&]() { // 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); }); TFuture AABBTreeFuture = Async(VertexPaintToolAsyncExecTarget, [&]() { AABBTree.SetMesh(Mesh, true); }); // initialize render decomposition TUniquePtr Decomp = MakeUnique(); FMeshRenderDecomposition::BuildChunkedDecomposition(Mesh, &MaterialSet, *Decomp); Decomp->BuildAssociations(Mesh); 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, &UMeshVertexPaintTool::OnPolyLassoFinished); PolygroupLayerProperties = NewObject(this); PolygroupLayerProperties->RestoreProperties(this, TEXT("MeshVertexPaintTool")); PolygroupLayerProperties->InitializeGroupLayers(GetSculptMesh()); PolygroupLayerProperties->WatchProperty(PolygroupLayerProperties->ActiveGroupLayer, [&](FName) { OnSelectedGroupLayerChanged(); }); UpdateActiveGroupLayer(); BasicProperties = NewObject(this); BasicProperties->RestoreProperties(this); BasicProperties->WatchProperty(BasicProperties->SubToolType, [this](EMeshVertexPaintInteractionType NewType) { UpdateSubToolType(NewType); }); BasicProperties->WatchProperty(BasicProperties->ChannelFilter, [this](const FModelingToolsColorChannelFilter&) { OnChannelFilterModified(); }); BasicProperties->WatchProperty(BasicProperties->SecondaryActionType, [this](EMeshVertexPaintSecondaryActionType NewType) { UpdateSecondaryBrushType(NewType); }); BasicProperties->WatchProperty(BasicProperties->bIsErasePressureEnabled, [this](const bool bEnabled) { EraseBrushOpProperties->bIsStrengthPressureEnabled = bEnabled; }); BasicProperties->WatchProperty(BasicProperties->bIsPaintPressureEnabled, [this](const bool bEnabled) { PaintBrushOpProperties->bIsStrengthPressureEnabled = bEnabled; }); InitializeIndicator(); AddToolPropertySource(BasicProperties); AddToolPropertySource(UMeshSculptToolBase::BrushProperties); // initialize our properties UMeshSculptToolBase::BrushProperties->bShowPerBrushProps = false; UMeshSculptToolBase::BrushProperties->bShowFalloff = true; UMeshSculptToolBase::BrushProperties->bShowLazyness = true; UMeshSculptToolBase::BrushProperties->bShowFlowRate = true; UMeshSculptToolBase::BrushProperties->bShowSpacing = true; UMeshSculptToolBase::BrushProperties->BrushFalloffAmount = 0.66f; UMeshSculptToolBase::BrushProperties->BrushSize.bToolSupportsPressureSensitivity = true; CalculateBrushRadius(); PaintBrushOpProperties = NewObject(this); RegisterBrushType((int32)EMeshVertexPaintBrushType::Paint, LOCTEXT("Paint", "Paint"), MakeUnique([this]() { return MakeUnique(); }), PaintBrushOpProperties); // secondary brushes EraseBrushOpProperties = NewObject(this); RegisterSecondaryBrushType((int32)EMeshVertexPaintBrushType::Erase, LOCTEXT("Erase", "Erase"), MakeUnique([this]() { return MakeUnique(); }), EraseBrushOpProperties); RegisterSecondaryBrushType((int32)EMeshVertexPaintBrushType::Soften, LOCTEXT("Soften", "Soften"), MakeUnique>(), NewObject(this)); RegisterSecondaryBrushType((int32)EMeshVertexPaintBrushType::Smooth, LOCTEXT("Smooth", "Smooth"), MakeUnique>(), NewObject(this)); // falloffs RegisterStandardFalloffTypes(); QuickActions = NewObject(this); QuickActions->Initialize(this); UtilityActions = NewObject(this); UtilityActions->Initialize(this); AddToolPropertySource(FilterProperties); AddToolPropertySource(UMeshSculptToolBase::ViewProperties); AddToolPropertySource(QuickActions); AddToolPropertySource(UtilityActions); AddToolPropertySource(PolygroupLayerProperties); AddToolPropertySource(UMeshSculptToolBase::GizmoProperties); SetToolPropertySourceEnabled(UMeshSculptToolBase::GizmoProperties, false); // register watchers BasicProperties->WatchProperty(BasicProperties->PrimaryBrushType, [this](EMeshVertexPaintBrushType NewType) { UpdateBrushType(NewType); }); // must call before updating brush type so that we register all brush properties? UMeshSculptToolBase::OnCompleteSetup(); UpdateBrushType(BasicProperties->PrimaryBrushType); UpdateSecondaryBrushType(BasicProperties->SecondaryActionType); SetPrimaryFalloffType(EMeshSculptFalloffType::Smooth); MeshElementsDisplay = NewObject(this); MeshElementsDisplay->CreateInWorld(DynamicMeshComponent->GetWorld(), DynamicMeshComponent->GetComponentTransform()); if (ensure(MeshElementsDisplay->Settings)) { MeshElementsDisplay->Settings->bShowColorSeams = false; // default color seams to false MeshElementsDisplay->Settings->RestoreProperties(this, TEXT("MeshVertexPaintTool")); AddToolPropertySource(MeshElementsDisplay->Settings); } MeshElementsDisplay->SetMeshAccessFunction([this](UMeshElementsVisualizer::ProcessDynamicMeshFunc ProcessFunc) { const UE::Geometry::FDynamicMesh3* const FullMesh = GetSculptMesh(); if (ShouldFilterTrianglesBySelection()) { // Arguably there should be a way to add a filter to the visualizer, but we've done it this way elsewhere... UE::Geometry::FDynamicSubmesh3 Submesh(FullMesh, SelectionTids.Array()); ProcessFunc(Submesh.GetSubmesh()); } else { ProcessFunc(*FullMesh); } }); FilterProperties->WatchProperty(FilterProperties->bIsolateGeometrySelection, [this](bool) { DynamicMeshComponent->SetSecondaryBuffersVisibility(!ShouldFilterTrianglesBySelection()); MeshElementsDisplay->NotifyMeshChanged(); }); // initialize weightmap list in Utility actions TArray TargetWeightMaps; UE::WeightMaps::FindVertexWeightMaps(UE::ToolTarget::GetMeshDescription(Target), TargetWeightMaps); for (FName Name : TargetWeightMaps) { UtilityActions->WeightMapsList.Add(Name.ToString()); } if (UtilityActions->WeightMapsList.Num() > 0) { UtilityActions->WeightMap = FName(UtilityActions->WeightMapsList[0]); } // initialize LOD names list in Utility actions this->SourceLOD = UE::ToolTarget::GetTargetMeshDescriptionLOD(Target, this->bTargetSupportsLODs); if (bTargetSupportsLODs) { this->AvailableLODs = UE::ToolTarget::GetMeshDescriptionLODs(Target, bTargetSupportsLODs, false, true); this->AvailableLODs.Remove(SourceLOD); } if (this->bTargetSupportsLODs && AvailableLODs.Num() > 0) { for ( int32 k = 0; k < AvailableLODs.Num(); ++k) { EMeshLODIdentifier LOD = AvailableLODs[k]; if (LOD != SourceLOD) { if (LOD == EMeshLODIdentifier::HiResSource) { UtilityActions->LODNamesList.Add(TEXT("HiRes")); } else { UtilityActions->LODNamesList.Add(FString::Printf(TEXT("LOD %d"), static_cast(LOD))); } } } UtilityActions->CopyToLODName = UtilityActions->LODNamesList[0]; } // disable view properties SetViewPropertiesEnabled(false); UpdateWireframeVisibility(false); UpdateFlatShadingSetting(true); UpdateVertexPaintMaterialMode(); // configure panels UpdateSubToolType(BasicProperties->SubToolType); OnChannelFilterModified(); PrecomputeFuture.Wait(); OctreeFuture.Wait(); AABBTreeFuture.Wait(); } void UMeshVertexPaintTool::Shutdown(EToolShutdownType ShutdownType) { if (DynamicMeshComponent != nullptr) { DynamicMeshComponent->OnMeshChanged.Remove(OnDynamicMeshComponentChangedHandle); } if (ensure(MeshElementsDisplay->Settings)) { MeshElementsDisplay->Settings->SaveProperties(this, TEXT("MeshVertexPaintTool")); } MeshElementsDisplay->Disconnect(); BasicProperties->SaveProperties(this); FilterProperties->SaveProperties(this); PolygroupLayerProperties->SaveProperties(this, TEXT("MeshVertexPaintTool")); if (PreviewMeshActor != nullptr) { PreviewMeshActor->Destroy(); PreviewMeshActor = nullptr; } if (ShutdownType == EToolShutdownType::Accept) { // weld colors to remove seams where possible FDynamicMesh3* Mesh = GetSculptMesh(); FDynamicMeshColorOverlay* ColorOverlay = GetActiveColorOverlay(); for (int32 vid : Mesh->VertexIndicesItr()) { FSplitAttributeWelder::WeldSplitColors(vid, *ColorOverlay, FMathd::ZeroTolerance); } Mesh->CompactInPlace(); // to clean up colors we just welded } UMeshSculptToolBase::Shutdown(ShutdownType); } void UMeshVertexPaintTool::CommitResult(UBaseDynamicMeshComponent* Component, bool bModifiedTopology) { GetToolManager()->BeginUndoTransaction(LOCTEXT("VertexPaintToolTransactionName", "Paint Vertex Colors")); Component->ProcessMesh([&](const FDynamicMesh3& CurMesh) { UE::ToolTarget::CommitDynamicMeshUpdate(Target, CurMesh, true); }); GetToolManager()->EndUndoTransaction(); } void UMeshVertexPaintTool::SetGeometrySelection(const UE::Geometry::FGeometrySelection& SelectionIn) { GeometrySelection = SelectionIn; } void UMeshVertexPaintTool::RegisterActions(FInteractiveToolActionSet& ActionSet) { UMeshSculptToolBase::RegisterActions(ActionSet); ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 500, TEXT("PickColorUnderCursor"), LOCTEXT("PickColorUnderCursor", "Pick Color"), LOCTEXT("PickColorUnderCursorTooltip", "Switch the Paint Color to the color currently under the cursor"), EModifierKey::Shift, EKeys::G, [this]() { bPendingPickColor = true; }); ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 501, TEXT("PickEraseColorUnderCursor"), LOCTEXT("PickEraseColorUnderCursor", "Pick Erase Color"), LOCTEXT("PickEraseColorUnderCursorTooltip", "Switch the Erase Color to the color currently under the cursor"), EModifierKey::Control | EModifierKey::Shift, EKeys::G, [this]() { bPendingPickEraseColor = true; }); //ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 502, // 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 + 503, // TEXT("CreateNewGroup"), // LOCTEXT("CreateNewGroup", "New Group"), // LOCTEXT("CreateNewGroupTooltip", "Allocate a new Polygroup and set as Current"), // EModifierKey::Shift, EKeys::Q, // [this]() { AllocateNewGroupAndSetAsCurrentAction(); }); }; TUniquePtr& UMeshVertexPaintTool::GetActiveBrushOp() { if (GetInEraseStroke()) { return SecondaryBrushOp; } else { return PrimaryBrushOp; } } void UMeshVertexPaintTool::OnPropertyModified(UObject* PropertySet, FProperty* Property) { CalculateBrushRadius(); } bool UMeshVertexPaintTool::IsInBrushSubMode() const { return BasicProperties->SubToolType == EMeshVertexPaintInteractionType::Brush || BasicProperties->SubToolType == EMeshVertexPaintInteractionType::Fill || BasicProperties->SubToolType == EMeshVertexPaintInteractionType::GroupFill || BasicProperties->SubToolType == EMeshVertexPaintInteractionType::TriFill; } void UMeshVertexPaintTool::OnBeginStroke(const FRay& WorldRay) { UpdateBrushPosition(WorldRay); if (PaintBrushOpProperties) { PaintBrushOpProperties->Color = BasicProperties->PaintColor; PaintBrushOpProperties->BlendMode = (EVertexColorPaintBrushOpBlendMode)(int32)BasicProperties->BlendMode; PaintBrushOpProperties->bApplyFalloff = (BasicProperties->SubToolType == EMeshVertexPaintInteractionType::Brush); } if (EraseBrushOpProperties) { EraseBrushOpProperties->Color = BasicProperties->EraseColor; EraseBrushOpProperties->BlendMode = EVertexColorPaintBrushOpBlendMode::Lerp; EraseBrushOpProperties->bApplyFalloff = (BasicProperties->SubToolType == EMeshVertexPaintInteractionType::Brush); } TUniquePtr& UseBrushOp = GetActiveBrushOp(); if (GetInSmoothingStroke() && BasicProperties->SecondaryActionType == EMeshVertexPaintSecondaryActionType::Smooth) { UseBrushOp->PropertySet->SetStrength(0.01 + BasicProperties->SmoothStrength / 2.0); } else { UseBrushOp->PropertySet->SetStrength( FMathf::Pow(BasicProperties->Strength, 2.0f)); } UseBrushOp->PropertySet->SetFalloff(UMeshSculptToolBase::BrushProperties->BrushFalloffAmount); // 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 = GetActiveBrushStrength(); LastStamp.TimeStamp = FDateTime::Now(); FSculptBrushOptions SculptOptions; //SculptOptions.bPreserveUVFlow = false; // FilterProperties->bPreserveUVFlow; SculptOptions.ConstantReferencePlane = GetCurrentStrokeReferencePlane(); // to mix colors during a stroke like a proper painting tool, we need to accumulate stroke in a separate buffer // and each frame blend it with the initial color buffer. // (this should be done async after previous stroke ended...except initial buffer needs to be updated on other operations then) if (GetInSmoothingStroke() == false && PaintBrushOpProperties->BlendMode == EVertexColorPaintBrushOpBlendMode::Mix) { FDynamicMeshColorOverlay* ColorOverlay = GetActiveColorOverlay(); int32 NumElements = StrokeAccumColorBuffer.Num(); for (int32 k = 0; k < NumElements; ++k) { StrokeAccumColorBuffer[k] = FVector4f::Zero(); StrokeInitialColorBuffer[k] = ColorOverlay->IsElement(k) ? ColorOverlay->GetElement(k) : FVector4f::One(); } } UseBrushOp->ConfigureOptions(SculptOptions); UseBrushOp->BeginStroke(GetSculptMesh(), LastStamp, VertexROI); AccumulatedTriangleROI.Reset(); // begin change here? or wait for first stamp? BeginChange(); } void UMeshVertexPaintTool::OnEndStroke() { GetActiveBrushOp()->EndStroke(GetSculptMesh(), LastStamp, VertexROI); // close change record EndChange(); } void UMeshVertexPaintTool::OnCancelStroke() { GetActiveBrushOp()->CancelStroke(); ActiveChangeBuilder.Reset(); } void UMeshVertexPaintTool::UpdateROI(const FSculptBrushStamp& BrushStamp) { TRACE_CPUPROFILER_EVENT_SCOPE(MeshVPaint_UpdateROI); 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(); // Theoretically we shouldn't need to do this filtering here since we already filter out // unselected triangles in ApplyVisibilityFilter if needed. But seems odd not to. bool bShouldFilterTrianglesBySelection = ShouldFilterTrianglesBySelection(); // With Lazy Brush, GetBrushTriangleID() may not necessarily return a triangle that contains BrushPos, // so in that case we will try to find the triangle containing BrushPos. Note that this may not // be correct in some geometric cases...potentially we could check if the initial CenterTID // contains the BrushPos point first int32 CenterTID = GetBrushTriangleID(); if (BrushProperties->Lazyness > 0) { double NearDistSqr; IMeshSpatial::FQueryOptions QueryOptions; if (bShouldFilterTrianglesBySelection) { QueryOptions.TriangleFilterF = [this](int32 Tid) { return SelectionTids.Contains(Tid); }; } CenterTID = AABBTree.FindNearestTriangle(BrushPos, NearDistSqr, QueryOptions); } 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 == EMeshVertexPaintBrushAreaType::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 = (BasicProperties->SubToolType == EMeshVertexPaintInteractionType::Fill); bool bGroupFill = (BasicProperties->SubToolType == EMeshVertexPaintInteractionType::GroupFill); if (bVolumetric) { Octree.RangeQuery(BrushBox, [&](int TriIdx) { if (bShouldFilterTrianglesBySelection && !SelectionTids.Contains(TriIdx)) { return; } 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 (bShouldFilterTrianglesBySelection && (!SelectionTids.Contains(t1) || !SelectionTids.Contains(t2))) { return false; } 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) { if (bShouldFilterTrianglesBySelection && (!SelectionTids.Contains(t1) || !SelectionTids.Contains(t2))) { return false; } return CheckEdgeCriteria(t1, t2); }); } else if (bGroupFill) { TArray StartROI; TSet FillGroups; for (int32 tid : TriangleROI) { StartROI.Add(tid); FillGroups.Add(ActiveGroupSet->GetGroup(tid)); } FMeshConnectedComponents::GrowToConnectedTriangles(Mesh, StartROI, TriangleROI, &TempROIBuffer, [&](int t1, int t2) { if (bShouldFilterTrianglesBySelection && (!SelectionTids.Contains(t1) || !SelectionTids.Contains(t2))) { return false; } return (FillGroups.Contains(ActiveGroupSet->GetGroup(t2))); }); } // apply visibility filter if (FilterProperties->VisibilityFilter != EMeshVertexPaintVisibilityType::None) { TArray ResultBuffer; ApplyVisibilityFilter(TriangleROI, TempROIBuffer, ResultBuffer); } // construct ROI vertex set TempVertexSet.Reset(); for (int32 tid : TriangleROI) { FIndex3i Tri = Mesh->GetTriangle(tid); TempVertexSet.Add(Tri.A); TempVertexSet.Add(Tri.B); TempVertexSet.Add(Tri.C); } VertexROI.SetNum(0, EAllowShrinking::No); BufferUtil::AppendElements(VertexROI, TempVertexSet); // construct ROI triangle and group buffers ROITriangleBuffer.Reserve(TriangleROI.Num()); ROITriangleBuffer.SetNum(0, EAllowShrinking::No); for (int32 tid : TriangleROI) { ROITriangleBuffer.Add(tid); } } bool UMeshVertexPaintTool::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.Radius = GetActiveBrushRadius(); CurrentStamp.LocalFrame = GetBrushFrameLocal(); CurrentStamp.Power = GetActiveBrushStrength(); 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 UMeshVertexPaintTool::ApplyStamp() { TRACE_CPUPROFILER_EVENT_SCOPE(MeshVPaint_ApplyStamp); TUniquePtr& UseBrushOp = GetActiveBrushOp(); // yuck FMeshVertexColorBrushOp* VertexColorBrushOp = (FMeshVertexColorBrushOp*)UseBrushOp.Get(); // convert triangle ROI to element IDs, populating ROIElementSet, ROIElementBuffer, ROIColorBuffer { TRACE_CPUPROFILER_EVENT_SCOPE(MeshVPaint_ApplyStamp_ElementROI); InitializeElementROIFromTriangleROI(ROITriangleBuffer, true); } FDynamicMesh3* Mesh = GetSculptMesh(); { TRACE_CPUPROFILER_EVENT_SCOPE(MeshVPaint_ApplyStamp_ApplyStampToVertexColors); VertexColorBrushOp->ApplyStampToVertexColors(Mesh, GetActiveColorOverlay(), StrokeInitialColorBuffer, StrokeAccumColorBuffer, CurrentStamp, ROIElementBuffer, ROIColorBuffer); } bool bUpdated = SyncMeshWithColorBuffer(Mesh); LastStamp = CurrentStamp; LastStamp.TimeStamp = FDateTime::Now(); return bUpdated; } bool UMeshVertexPaintTool::SyncMeshWithColorBuffer(FDynamicMesh3* Mesh) { TRACE_CPUPROFILER_EVENT_SCOPE(MeshVPaint_SyncMeshWithColorBuffer); int NumModified = 0; const int32 NumElems = ROIElementBuffer.Num(); if (FDynamicMeshAttributeSet* AttributeSet = Mesh->Attributes()) { if (FDynamicMeshColorOverlay* ColorAttrib = AttributeSet->PrimaryColors()) { for (int32 k = 0; k < NumElems; ++k) { // check if color has changed? why bother? int32 ElementID = ROIElementBuffer[k]; FVector4f CurColor = ColorAttrib->GetElement(ElementID); FVector4f NewColor = ROIColorBuffer[k]; ApplyChannelFilter(CurColor, NewColor); ActiveChangeBuilder->UpdateValue(ElementID, CurColor, NewColor); ColorAttrib->SetElement(ElementID, NewColor); NumModified++; } } } return (NumModified > 0); } void UMeshVertexPaintTool::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; } FLinearColor SetColor = GetInEraseStroke() ? BasicProperties->EraseColor : BasicProperties->PaintColor; SetTrianglesToVertexColor(FaceSelection.AsSet(), SetColor); } // could get rid of this... void UMeshVertexPaintTool::SetTrianglesToVertexColor(const TSet& Triangles, const FLinearColor& ToColor) { TempROIBuffer.Reset(); for (int32 tid : Triangles) { TempROIBuffer.Add(tid); } SetTrianglesToVertexColor(TempROIBuffer, ToColor); } void UMeshVertexPaintTool::SetTrianglesToVertexColor(const TArray& Triangles, const FLinearColor& ToColor) { FDynamicMesh3* Mesh = GetSculptMesh(); FDynamicMeshColorOverlay* ColorOverlay = GetActiveColorOverlay(); const TArray* UseTriangles = &Triangles; TArray VisibleTriangles; if (HaveVisibilityFilter()) { VisibleTriangles.Reserve(Triangles.Num()); ApplyVisibilityFilter(Triangles, VisibleTriangles); UseTriangles = &VisibleTriangles; } // convert triangle ROI to element IDs, populating ROIElementSet InitializeElementROIFromTriangleROI(*UseTriangles, false); if (ROIElementSet.Num() > 0) { BeginChange(); for (int32 ElementID : ROIElementSet) { FVector4f CurColor = ColorOverlay->GetElement(ElementID); FVector4f NewColor = (FVector4f)ToColor; ApplyChannelFilter(CurColor, NewColor); ActiveChangeBuilder->UpdateValue(ElementID, CurColor, NewColor); ColorOverlay->SetElement(ElementID, NewColor); } DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(*UseTriangles, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } } void UMeshVertexPaintTool::InitializeElementROIFromTriangleROI(const TArray& Triangles, bool bInitializeFlatBuffers) { ROIElementBuffer.Reset(); ROIColorBuffer.Reset(); ROIElementSet.Reset(); FDynamicMesh3* Mesh = GetSculptMesh(); FDynamicMeshColorOverlay* ColorOverlay = GetActiveColorOverlay(); if (BasicProperties->bHardEdges) { for (int32 tid : Triangles) { if (ColorOverlay->IsSetTriangle(tid)) { FIndex3i TriElementIDs = ColorOverlay->GetTriangle(tid); ROIElementSet.Add(TriElementIDs.A); ROIElementSet.Add(TriElementIDs.B); ROIElementSet.Add(TriElementIDs.C); } } } else { // Need to accumulate all Element IDs attached to all vertices of triangles. // Perhaps there is a faster way to do this? TArray TempElementIDs; TempVertexSet.Reset(); for (int32 tid : Triangles) { FIndex3i TriVertIDs = Mesh->GetTriangle(tid); for (int32 j = 0; j < 3; ++j) { int32 vid = TriVertIDs[j]; if (TempVertexSet.Contains(vid) == false) { TempVertexSet.Add(vid); TempElementIDs.Reset(); ColorOverlay->GetVertexElements(vid, TempElementIDs); for (int32 ElementID : TempElementIDs) { ROIElementSet.Add(ElementID); } } } } } if (bInitializeFlatBuffers) { ROIElementBuffer = ROIElementSet.Array(); ROIColorBuffer.SetNum(ROIElementBuffer.Num()); for (int32 k = 0; k < ROIElementBuffer.Num(); ++k) { ROIColorBuffer[k] = ColorOverlay->GetElement(ROIElementBuffer[k]); } } } bool UMeshVertexPaintTool::HaveVisibilityFilter() const { return ShouldFilterTrianglesBySelection() || FilterProperties->VisibilityFilter != EMeshVertexPaintVisibilityType::None; } void UMeshVertexPaintTool::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 UMeshVertexPaintTool::ApplyVisibilityFilter(const TArray& Triangles, TArray& VisibleTriangles) { if (!HaveVisibilityFilter()) { VisibleTriangles = Triangles; return; } FViewCameraState StateOut; GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(StateOut); FVector3d LocalEyePosition(CurTargetTransform.InverseTransformPosition(StateOut.Position)); const FDynamicMesh3* Mesh = GetSculptMesh(); int32 NumTriangles = Triangles.Num(); bool bShouldFilterUnselected = ShouldFilterTrianglesBySelection(); VisibilityFilterBuffer.SetNum(NumTriangles, EAllowShrinking::No); ParallelFor(NumTriangles, [&](int32 idx) { VisibilityFilterBuffer[idx] = true; if (bShouldFilterUnselected && !SelectionTids.Contains(idx)) { VisibilityFilterBuffer[idx] = false; return; } FVector3d Centroid = Mesh->GetTriCentroid(Triangles[idx]); FVector3d FaceNormal = Mesh->GetTriNormal(Triangles[idx]); if (FaceNormal.Dot((Centroid - LocalEyePosition)) > 0) { VisibilityFilterBuffer[idx] = false; return; } if (FilterProperties->VisibilityFilter == EMeshVertexPaintVisibilityType::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 UMeshVertexPaintTool::FindHitSculptMeshTriangleConst(const FRay3d& LocalRay) const { if (!IsInBrushSubMode()) { return IndexConstants::InvalidID; } bool bShouldFilterUnselected = ShouldFilterTrianglesBySelection(); if (GetBrushCanHitBackFaces()) { if (bShouldFilterUnselected) { return Octree.FindNearestHitObject(LocalRay, [this](int32 Tid) { return SelectionTids.Contains(Tid); }); } 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, bShouldFilterUnselected](int TriangleID) { if (bShouldFilterUnselected && !SelectionTids.Contains(TriangleID)) { return false; } FVector3d Normal, Centroid; double Area; Mesh->GetTriInfo(TriangleID, Normal, Area, Centroid); return Normal.Dot((Centroid - LocalEyePosition)) < 0; }); return HitTID; } } int32 UMeshVertexPaintTool::FindHitTargetMeshTriangleConst(const FRay3d& LocalRay) const { check(false); return IndexConstants::InvalidID; } bool UMeshVertexPaintTool::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 UMeshVertexPaintTool::OnUpdateHover(const FInputDeviceRay& DevicePos) { PendingStampType = BasicProperties->PrimaryBrushType; if(ensure(InStroke() == false)) { UpdateBrushPosition(DevicePos.WorldRay); // update strength and falloff for current brush TUniquePtr& UseBrushOp = GetActiveBrushOp(); UseBrushOp->PropertySet->SetStrength( FMathf::Pow(BasicProperties->Strength, 2.0f) ); UseBrushOp->PropertySet->SetFalloff(UMeshSculptToolBase::BrushProperties->BrushFalloffAmount); } return true; } void UMeshVertexPaintTool::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(); if (FilterProperties->bShowHitColor) { FRay3d LocalRay(LocalEyePosition, Normalized(HoverStamp.LocalFrame.Origin - LocalEyePosition)); int CursorHitTID = ShouldFilterTrianglesBySelection() ? Octree.FindNearestHitObject(LocalRay, [this](int32 Tid) {return SelectionTids.Contains(Tid); }) : Octree.FindNearestHitObject(LocalRay); if (Mesh->IsTriangle(CursorHitTID)) { FIntrRay3Triangle3d Intersection = TMeshQueries::RayTriangleIntersection(*GetSculptMesh(), CursorHitTID, LocalRay); FVector3f BaryCoords = (FVector3f)Intersection.TriangleBaryCoords; FVector4f InterpColor; GetActiveColorOverlay()->GetTriBaryInterpolate(CursorHitTID, &BaryCoords.X, &InterpColor.X); FVector2D CursorPixelPos; SceneView->WorldToPixel(HoverStamp.WorldFrame.Origin, CursorPixelPos); FString CursorString = FString::Printf(TEXT("%.3f %.3f %.3f %.3f"), InterpColor.X, InterpColor.Y, InterpColor.Z, InterpColor.W); Canvas->DrawShadowedString(CursorPixelPos.X / (double)DPIScale, CursorPixelPos.Y / (double)DPIScale, *CursorString, UseFont, FLinearColor::White); } } } void UMeshVertexPaintTool::OnTick(float DeltaTime) { TRACE_CPUPROFILER_EVENT_SCOPE(MeshVPaint_OnTick); UMeshSculptToolBase::OnTick(DeltaTime); MeshElementsDisplay->OnTick(DeltaTime); bool bIsLasso = (BasicProperties->SubToolType == EMeshVertexPaintInteractionType::PolyLasso); PolyLassoMechanic->SetIsEnabled(bIsLasso); ConfigureIndicator(FilterProperties->BrushAreaMode == EMeshVertexPaintBrushAreaType::Volumetric); SetIndicatorVisibility(bIsLasso == false); if (bHavePendingAction) { ApplyAction(PendingAction); bHavePendingAction = false; PendingAction = EMeshVertexPaintToolActions::NoAction; } // process the undo update if (bUndoUpdatePending) { // wait for updates WaitForPendingUndoRedoUpdate(); // post rendering update DynamicMeshComponent->FastNotifyTriangleVerticesUpdated(AccumulatedTriangleROI, EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); // ignore stamp and wait for next tick to do anything else bUndoUpdatePending = false; return; } if (bPendingPickColor || bPendingPickEraseColor ) { int32 HitTriangleID = GetBrushTriangleID(); if (HitTriangleID >= 0 && IsStampPending() == false ) { if (GetSculptMesh()->IsTriangle(HitTriangleID)) { if (bPendingPickColor || bPendingPickEraseColor) { if (const FDynamicMeshAttributeSet* AttributeSet = GetSculptMesh()->Attributes()) { if (const FDynamicMeshColorOverlay* ColorAttrib = AttributeSet->PrimaryColors()) { if (ColorAttrib->IsSetTriangle(HitTriangleID)) { FVector4f HitColor; // TODO: use actual hit point here... FVector3f BaryCoords = (1.0f / 3.0f) * FVector3f::One(); ColorAttrib->GetTriBaryInterpolate(HitTriangleID, &BaryCoords.X, &HitColor.X); if (bPendingPickEraseColor) { BasicProperties->EraseColor = HitColor; } else { BasicProperties->PaintColor = HitColor; } NotifyOfPropertyChangeByTool(BasicProperties); } } } } } } bPendingPickColor = bPendingPickEraseColor = false; } auto ExecuteStampOperation = [this](int StampIndex, const FRay& StampRay) { TRACE_CPUPROFILER_EVENT_SCOPE(MeshVPaint_OnTick_InStroke); // update sculpt ROI UpdateROI(CurrentStamp); // append updated ROI to modified region (async) TFuture AccumulateROI = Async(VertexPaintToolAsyncExecTarget, [&]() { AccumulatedTriangleROI.Append(TriangleROI); }); // apply the stamp bool bColorModified = ApplyStamp(); if (bColorModified) { TRACE_CPUPROFILER_EVENT_SCOPE(MeshVPaint_OnTick_UpdateComponent); 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 UMeshVertexPaintTool::ApplyChannelFilter(const FVector4f& CurColor, FVector4f& NewColor) { const FModelingToolsColorChannelFilter& ChannelFilter = BasicProperties->ChannelFilter; if (ChannelFilter.bRed == false) { NewColor.X = CurColor.X; } if (ChannelFilter.bGreen == false) { NewColor.Y = CurColor.Y; } if (ChannelFilter.bBlue == false) { NewColor.Z = CurColor.Z; } if (ChannelFilter.bAlpha == false) { NewColor.W = CurColor.W; } } void UMeshVertexPaintTool::OnChannelFilterModified() { FModelingToolsColorChannelFilter NewFilter = BasicProperties->ChannelFilter; // if we are showing original material then we do not want to filter the colors by default (could be an option) if (FilterProperties->MaterialMode == EMeshVertexPaintMaterialMode::OriginalMaterial) { NewFilter = FModelingToolsColorChannelFilter(); } if (NewFilter.bRed && NewFilter.bGreen && NewFilter.bBlue) { DynamicMeshComponent->ClearVertexColorRemappingFunction(); } else if (NewFilter.bRed || NewFilter.bBlue || NewFilter.bGreen) { DynamicMeshComponent->SetVertexColorRemappingFunction([Filter = NewFilter](FVector4f& Color) { Color.X = (Filter.bRed) ? Color.X : 0; Color.Y = (Filter.bGreen) ? Color.Y : 0; Color.Z = (Filter.bBlue) ? Color.Z : 0; }); } else if (NewFilter.bAlpha) { DynamicMeshComponent->SetVertexColorRemappingFunction([Filter = NewFilter](FVector4f& Color) { Color.X = Color.W; Color.Y = Color.W; Color.Z = Color.W; }); } else { DynamicMeshComponent->ClearVertexColorRemappingFunction(); } } FColor UMeshVertexPaintTool::GetColorForGroup(int32 GroupID) { return LinearColors::SelectFColor(GroupID); } void UMeshVertexPaintTool::UpdateVertexPaintMaterialMode() { if (FilterProperties->MaterialMode == EMeshVertexPaintMaterialMode::OriginalMaterial) { UpdateMaterialMode(EMeshEditingMaterialModes::ExistingMaterial); } else if (FilterProperties->MaterialMode == EMeshVertexPaintMaterialMode::LitVertexColor) { // TODO: add two-sided vertex color support when it becomes available UpdateMaterialMode(EMeshEditingMaterialModes::VertexColor); } else if (FilterProperties->MaterialMode == EMeshVertexPaintMaterialMode::UnlitVertexColor && GEngine && GEngine->VertexColorViewModeMaterial_ColorOnly) { UpdateMaterialMode(EMeshEditingMaterialModes::ExistingMaterial); GetSculptMeshComponent()->SetOverrideRenderMaterial(GEngine->VertexColorViewModeMaterial_ColorOnly); } OnChannelFilterModified(); // channel visibility filtering may be disabled for existing material } void UMeshVertexPaintTool::FloodFillColorAction(FLinearColor Color) { const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); TempROIBuffer.Reset(); TempROIBuffer.Reserve(Mesh->TriangleCount()); for (int32 tid : Mesh->TriangleIndicesItr()) { TempROIBuffer.Add(tid); } SetTrianglesToVertexColor(TempROIBuffer, Color); } void UMeshVertexPaintTool::ApplyCurrentUtilityAction() { switch (UtilityActions->Operation) { case EMeshVertexPaintToolUtilityOperations::BlendAllSeams: BlendAllSeams(); return; case EMeshVertexPaintToolUtilityOperations::FillChannels: FillChannels(); return; case EMeshVertexPaintToolUtilityOperations::InvertChannels: InvertChannels(); return; case EMeshVertexPaintToolUtilityOperations::CopyChannelToChannel: CopyChannelToChannel(); return; case EMeshVertexPaintToolUtilityOperations::SwapChannels: SwapChannels(); return; case EMeshVertexPaintToolUtilityOperations::CopyFromWeightMap: CopyFromWeightMap(); return; case EMeshVertexPaintToolUtilityOperations::CopyToOtherLODs: CopyToOtherLODs(); return; case EMeshVertexPaintToolUtilityOperations::CopyToSingleLOD: CopyToSpecificLOD(); return; } } void UMeshVertexPaintTool::BlendAllSeams() { BeginChange(); TArray TempElementIDs; const FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh(); FDynamicMeshColorOverlay* VertexColors = GetActiveColorOverlay(); for (int32 VertexID : Mesh->VertexIndicesItr()) { TempElementIDs.Reset(); VertexColors->GetVertexElements(VertexID, TempElementIDs); if (TempElementIDs.Num() > 1) { FVector4f SumColor = FVector4f::Zero(); for (int32 ElementID : TempElementIDs) { SumColor += VertexColors->GetElement(ElementID); } SumColor /= (double)TempElementIDs.Num(); for (int32 ElementID : TempElementIDs) { ActiveChangeBuilder->UpdateValue(ElementID, VertexColors->GetElement(ElementID), SumColor); VertexColors->SetElement(ElementID, SumColor); } } } DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshVertexPaintTool::FillChannels() { float FillValue = UtilityActions->SourceValue; FIndex4i Targets = UtilityActions->TargetChannels.AsFlags(); if (Targets == FIndex4i::Zero()) { GetToolManager()->DisplayMessage( LOCTEXT("NoChannelsSelectedMessage", "At least one Target Channel must be selected"), EToolMessageLevel::UserError); return; } BeginChange(); FDynamicMeshColorOverlay* VertexColors = GetActiveColorOverlay(); for (int32 ElementID : VertexColors->ElementIndicesItr()) { FVector4f CurColor = VertexColors->GetElement(ElementID); FVector4f NewColor( Targets[0] ? FillValue : CurColor.X, Targets[1] ? FillValue : CurColor.Y, Targets[2] ? FillValue : CurColor.Z, Targets[3] ? FillValue : CurColor.W); ActiveChangeBuilder->UpdateValue(ElementID, CurColor, NewColor); VertexColors->SetElement(ElementID, NewColor); } DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshVertexPaintTool::InvertChannels() { FIndex4i Targets = UtilityActions->TargetChannels.AsFlags(); if (Targets == FIndex4i::Zero()) { GetToolManager()->DisplayMessage( LOCTEXT("NoChannelsSelectedMessage", "At least one Target Channel must be selected"), EToolMessageLevel::UserError); return; } BeginChange(); FDynamicMeshColorOverlay* VertexColors = GetActiveColorOverlay(); for (int32 ElementID : VertexColors->ElementIndicesItr()) { FVector4f CurColor = VertexColors->GetElement(ElementID); FVector4f NewColor( Targets[0] ? FMathf::Clamp(1.0f-CurColor.X,0,1.0f) : CurColor.X, Targets[1] ? FMathf::Clamp(1.0f-CurColor.Y,0,1.0f) : CurColor.Y, Targets[2] ? FMathf::Clamp(1.0f-CurColor.Z,0,1.0f) : CurColor.Z, Targets[3] ? FMathf::Clamp(1.0f-CurColor.W,0,1.0f) : CurColor.W); ActiveChangeBuilder->UpdateValue(ElementID, CurColor, NewColor); VertexColors->SetElement(ElementID, NewColor); } DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshVertexPaintTool::CopyChannelToChannel() { int32 SourceChannelIndex = VectorUtil::Clamp(static_cast(UtilityActions->SourceChannel), 0, 3); FIndex4i Targets = UtilityActions->TargetChannels.AsFlags(); Targets[SourceChannelIndex] = 0; if (Targets == FIndex4i::Zero()) { GetToolManager()->DisplayMessage( LOCTEXT("NoChannelsSelectedMessage", "At least one Target Channel must be selected"), EToolMessageLevel::UserError); return; } BeginChange(); FDynamicMeshColorOverlay* VertexColors = GetActiveColorOverlay(); for (int32 ElementID : VertexColors->ElementIndicesItr()) { FVector4f CurColor = VertexColors->GetElement(ElementID); FVector4f NewColor = CurColor; float SourceValue = CurColor[SourceChannelIndex]; for (int32 j = 0; j < 4; ++j) { if (Targets[j] != 0) { NewColor[j] = SourceValue; } } ActiveChangeBuilder->UpdateValue(ElementID, CurColor, NewColor); VertexColors->SetElement(ElementID, NewColor); } DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshVertexPaintTool::SwapChannels() { int32 SourceChannelIndex = VectorUtil::Clamp(static_cast(UtilityActions->SourceChannel), 0, 3); int32 DestChannelIndex = VectorUtil::Clamp(static_cast(UtilityActions->TargetChannel), 0, 3); if (SourceChannelIndex == DestChannelIndex) { GetToolManager()->DisplayMessage( LOCTEXT("SameSourceDestMessage", "Source and Destination must be different Channels"), EToolMessageLevel::UserError); return; } BeginChange(); FDynamicMeshColorOverlay* VertexColors = GetActiveColorOverlay(); for (int32 ElementID : VertexColors->ElementIndicesItr()) { FVector4f CurColor = VertexColors->GetElement(ElementID); FVector4f NewColor = CurColor; Swap(NewColor[SourceChannelIndex], NewColor[DestChannelIndex]); ActiveChangeBuilder->UpdateValue(ElementID, CurColor, NewColor); VertexColors->SetElement(ElementID, NewColor); } DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } void UMeshVertexPaintTool::CopyFromWeightMap() { FIndex4i Targets = UtilityActions->TargetChannels.AsFlags(); if (Targets == FIndex4i::Zero()) { GetToolManager()->DisplayMessage( LOCTEXT("NoChannelsSelectedMessage", "At least one Target Channel must be selected"), EToolMessageLevel::UserError); return; } FIndexedWeightMap1f WeightMap; bool bFound = UE::WeightMaps::GetVertexWeightMap(UE::ToolTarget::GetMeshDescription(Target), UtilityActions->WeightMap, WeightMap, 1.0f); if (!bFound) { GetToolManager()->DisplayMessage( LOCTEXT("NoWeightMapMessage", "Selected Weight Map could not be found on the Target Mesh"), EToolMessageLevel::UserError); return; } // todo: range mapping BeginChange(); FDynamicMeshColorOverlay* VertexColors = GetActiveColorOverlay(); for (int32 ElementID : VertexColors->ElementIndicesItr()) { int32 ParentVertex = VertexColors->GetParentVertex(ElementID); float Value = WeightMap.GetValue(ParentVertex); Value = FMathf::Clamp(Value, 0.0f, 1.0f); FVector4f CurColor = VertexColors->GetElement(ElementID); FVector4f NewColor( Targets[0] ? Value : CurColor.X, Targets[1] ? Value : CurColor.Y, Targets[2] ? Value : CurColor.Z, Targets[3] ? Value : CurColor.W); ActiveChangeBuilder->UpdateValue(ElementID, CurColor, NewColor); VertexColors->SetElement(ElementID, NewColor); } DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); EndChange(); } // TODO should be moved to shared location namespace UELocal { static void CopyVertexColorsToLOD( const FDynamicMesh3& FromMesh, const FDynamicMeshColorOverlay& FromColors, const FDynamicMeshAABBTree3& Spatial, FDynamicMesh3& ToMesh, const FVector4f& FallbackColor) { if (ToMesh.HasAttributes() == false) { ToMesh.EnableAttributes(); } if (ToMesh.Attributes()->HasPrimaryColors() == false) { ToMesh.Attributes()->EnablePrimaryColors(); } FDynamicMeshColorOverlay& ToColors = *ToMesh.Attributes()->PrimaryColors(); ToColors.ClearElements(); // maybe should use baking here...? // could be parallel... TArray VertexColors; VertexColors.SetNum(ToMesh.MaxVertexID()); for (int32 vid : ToMesh.VertexIndicesItr()) { FVector3d ToPos = ToMesh.GetVertex(vid); double NearDistSqr; int32 NearestTri = Spatial.FindNearestTriangle(ToPos, NearDistSqr); if (FromColors.IsSetTriangle(NearestTri) == false) { VertexColors[vid] = FallbackColor; break; } FDistPoint3Triangle3d Dist = TMeshQueries::TriangleDistance(FromMesh, NearestTri, ToPos); FVector3f BaryCoords = (FVector3f)Dist.TriangleBaryCoords; FVector4f InterpColor; FromColors.GetTriBaryInterpolate(NearestTri, &BaryCoords.X, &InterpColor.X); VertexColors[vid] = InterpColor; } TArray VertToElemMap; VertToElemMap.SetNum(VertexColors.Num()); for (int32 vid : ToMesh.VertexIndicesItr()) { VertToElemMap[vid] = ToColors.AppendElement(VertexColors[vid]); } for (int32 tid : ToMesh.TriangleIndicesItr()) { FIndex3i TriVerts = ToMesh.GetTriangle(tid); ToColors.SetTriangle(tid, FIndex3i( VertToElemMap[TriVerts.A], VertToElemMap[TriVerts.B], VertToElemMap[TriVerts.C])); } } static void GetMeshDescriptionLODWithCopiedVertexColors( UToolTarget* Target, EMeshLODIdentifier CopyToLOD, FDynamicMesh3& SourceMesh, FDynamicMeshColorOverlay& ColorOverlay, FDynamicMeshAABBTree3& SourceMeshSpatial, FVector4f DefaultColor, FMeshDescription& UpdatedMeshDescriptionOut) { UpdatedMeshDescriptionOut = UE::ToolTarget::GetMeshDescriptionCopy(Target, FGetMeshParameters(true, CopyToLOD)); FDynamicMesh3 LODMesh; FMeshDescriptionToDynamicMesh Converter1; Converter1.bTransformVertexColorsLinearToSRGB = true; Converter1.Convert(&UpdatedMeshDescriptionOut, LODMesh); UELocal::CopyVertexColorsToLOD(SourceMesh, ColorOverlay, SourceMeshSpatial, LODMesh, DefaultColor); FDynamicMeshToMeshDescription Converter2; Converter2.ConversionOptions.bTransformVtxColorsSRGBToLinear = true; Converter2.UpdateVertexColors(&LODMesh, UpdatedMeshDescriptionOut); } } void UMeshVertexPaintTool::CopyToOtherLODs() { // have to do this via MeshDescriptions for now if (bTargetSupportsLODs == false) { GetToolManager()->DisplayMessage( LOCTEXT("NoLODsErrorMessage", "Target Mesh does not support LODs"), EToolMessageLevel::UserError); return; } TArray CopyToLODs = AvailableLODs; if (UtilityActions->bCopyToHiRes == false) { CopyToLODs.Remove(EMeshLODIdentifier::HiResSource); } if (CopyToLODs.Num() == 0) { GetToolManager()->DisplayMessage( LOCTEXT("NoLODsToCopyToMessage", "Target Mesh does not have any other editable LODs"), EToolMessageLevel::UserError); return; } FDynamicMesh3* SourceMesh = GetSculptMesh(); FDynamicMeshColorOverlay* ColorOverlay = GetActiveColorOverlay(); FDynamicMeshAABBTree3 SourceMeshSpatial(SourceMesh, true); TArray LODMeshDescriptions; LODMeshDescriptions.SetNum(CopyToLODs.Num()); for (int32 k = 0; k < CopyToLODs.Num(); ++k) { EMeshLODIdentifier LOD = CopyToLODs[k]; UELocal::GetMeshDescriptionLODWithCopiedVertexColors(Target, LOD, *SourceMesh, *ColorOverlay, SourceMeshSpatial, (FVector4f)BasicProperties->PaintColor, LODMeshDescriptions[k]); } GetToolManager()->BeginUndoTransaction(LOCTEXT("CopyToOtherLODs", "Copy Vertex Colors To LODs")); for (int32 k = 0; k < CopyToLODs.Num(); ++k) { EMeshLODIdentifier LOD = CopyToLODs[k]; UE::ToolTarget::CommitMeshDescriptionUpdate(Target, &LODMeshDescriptions[k], nullptr /*no material set changes*/, FCommitMeshParameters(true, LOD)); } GetToolManager()->EndUndoTransaction(); } void UMeshVertexPaintTool::CopyToSpecificLOD() { // have to do this via MeshDescriptions for now if (bTargetSupportsLODs == false) { GetToolManager()->DisplayMessage( LOCTEXT("NoLODsErrorMessage", "Target Mesh does not support LODs"), EToolMessageLevel::UserError); return; } int32 Index = UtilityActions->LODNamesList.IndexOfByKey(UtilityActions->CopyToLODName); if (Index == INDEX_NONE) { GetToolManager()->DisplayMessage( LOCTEXT("MissingLODMessage", "Could not find selected LOD on Target Mesh"), EToolMessageLevel::UserError); return; } FDynamicMesh3* SourceMesh = GetSculptMesh(); FDynamicMeshColorOverlay* ColorOverlay = GetActiveColorOverlay(); FDynamicMeshAABBTree3 SourceMeshSpatial(SourceMesh, true); FMeshDescription LODMeshDescription; EMeshLODIdentifier LOD = AvailableLODs[Index]; UELocal::GetMeshDescriptionLODWithCopiedVertexColors(Target, LOD, *SourceMesh, *ColorOverlay, SourceMeshSpatial, (FVector4f)BasicProperties->PaintColor, LODMeshDescription); GetToolManager()->BeginUndoTransaction(LOCTEXT("CopyToSpecificLOD", "Copy Vertex Colors To LOD")); UE::ToolTarget::CommitMeshDescriptionUpdate(Target, &LODMeshDescription, nullptr /*no material set changes*/, FCommitMeshParameters(true, LOD)); GetToolManager()->EndUndoTransaction(); } // // Change Tracking // void UMeshVertexPaintTool::BeginChange() { check(ActiveChangeBuilder == nullptr); ActiveChangeBuilder = MakeUnique>(); ActiveChangeBuilder->BeginNewChange(); LongTransactions.Open(LOCTEXT("VertexPaintChange", "Paint Stroke"), GetToolManager()); } void UMeshVertexPaintTool::EndChange() { check(ActiveChangeBuilder); TUniquePtr EditResult = ActiveChangeBuilder->ExtractResult(); ActiveChangeBuilder = nullptr; EditResult->ApplyFunction = [](UObject* Object, const int32& AttribIndex, const TArray& Indices, const TArray& Values) { UMeshVertexPaintTool* Tool = CastChecked(Object); Tool->ExternalUpdateValues(Indices, Values); }; EditResult->RevertFunction = [](UObject* Object, const int32& AttribIndex, const TArray& Indices, const TArray& Values) { UMeshVertexPaintTool* Tool = CastChecked(Object); Tool->ExternalUpdateValues(Indices, Values); }; TUniquePtr> NewChange = MakeUnique>(); NewChange->WrappedChange = MoveTemp(EditResult); NewChange->BeforeModify = [this](bool bRevert) { this->WaitForPendingUndoRedoUpdate(); }; GetToolManager()->EmitObjectChange(this, MoveTemp(NewChange), LOCTEXT("VertexPaintChange", "Paint Stroke")); LongTransactions.Close(GetToolManager()); } void UMeshVertexPaintTool::ExternalUpdateValues(const TArray& ElementIDs, const TArray& NewValues) { DynamicMeshComponent->EditMesh([&](FDynamicMesh3& Mesh) { FDynamicMeshColorOverlay* ColorOverlay = Mesh.Attributes()->PrimaryColors(); int32 N = ElementIDs.Num(); for ( int32 k = 0; k < N; ++k) { int32 ElementID = ElementIDs[k]; ColorOverlay->SetElement(ElementID, NewValues[k]); } }); GetToolManager()->PostInvalidation(); } void UMeshVertexPaintTool::WaitForPendingUndoRedoUpdate() { if (bUndoUpdatePending) { bUndoUpdatePending = false; } } void UMeshVertexPaintTool::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 UMeshVertexPaintTool::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); } }); if (GeometrySelection.IsSet()) { UE::Geometry::EnumerateSelectionTriangles(GeometrySelection.GetValue(), *Mesh, [this, &Mesh](int32 TriangleID) { SelectionTids.Add(TriangleID); }); DynamicMeshComponent->EnableSecondaryTriangleBuffers([this](const UE::Geometry::FDynamicMesh3*, int32 Tid) { return !SelectionTids.Contains(Tid); }); DynamicMeshComponent->SetSecondaryBuffersVisibility(!ShouldFilterTrianglesBySelection()); } } bool UMeshVertexPaintTool::ShouldFilterTrianglesBySelection() const { return FilterProperties->bIsolateGeometrySelection && !SelectionTids.IsEmpty(); } void UMeshVertexPaintTool::OnSelectedGroupLayerChanged() { GetToolManager()->BeginUndoTransaction(LOCTEXT("ChangeActiveGroupLayer", "Change Polygroup Layer")); int32 ActiveLayerIndex = (ActiveGroupSet) ? ActiveGroupSet->GetPolygroupIndex() : -1; UpdateActiveGroupLayer(); int32 NewLayerIndex = (ActiveGroupSet) ? ActiveGroupSet->GetPolygroupIndex() : -1; if (ActiveLayerIndex != NewLayerIndex) { 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 UMeshVertexPaintTool::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); } // update colors DynamicMeshComponent->FastNotifyVertexAttributesUpdated(EMeshRenderAttributeFlags::VertexColors); GetToolManager()->PostInvalidation(); } void UMeshVertexPaintTool::UpdateSubToolType(EMeshVertexPaintInteractionType NewType) { FilterProperties->CurrentSubToolType = BasicProperties->SubToolType; bool bSculptPropsVisible = (NewType != EMeshVertexPaintInteractionType::PolyLasso); SetToolPropertySourceEnabled(UMeshSculptToolBase::BrushProperties, bSculptPropsVisible); //SetToolPropertySourceEnabled(BasicProperties, true); SetBrushOpPropsVisibility(false); } void UMeshVertexPaintTool::UpdateBrushType(EMeshVertexPaintBrushType BrushType) { static const FText BaseMessage = LOCTEXT("OnStartTool", "Hold Shift to Erase. [/] and S/D change Size (+Shift to small-step). Shift+G to pick Paint Color, +Ctrl for Erase Color."); FTextBuilder Builder; Builder.AppendLine(BaseMessage); SetActivePrimaryBrushType((int32)BrushType); SetToolPropertySourceEnabled(GizmoProperties, false); GetToolManager()->DisplayMessage(Builder.ToText(), EToolMessageLevel::UserNotification); } void UMeshVertexPaintTool::UpdateSecondaryBrushType(EMeshVertexPaintSecondaryActionType NewType) { if (NewType == EMeshVertexPaintSecondaryActionType::Erase) { SetActiveSecondaryBrushType((int32)EMeshVertexPaintBrushType::Erase); } else if (NewType == EMeshVertexPaintSecondaryActionType::Soften) { SetActiveSecondaryBrushType((int32)EMeshVertexPaintBrushType::Soften); } else if (NewType == EMeshVertexPaintSecondaryActionType::Smooth) { SetActiveSecondaryBrushType((int32)EMeshVertexPaintBrushType::Smooth); } } void UMeshVertexPaintTool::RequestAction(EMeshVertexPaintToolActions ActionType) { if (!bHavePendingAction) { PendingAction = ActionType; bHavePendingAction = true; } } void UMeshVertexPaintTool::ApplyAction(EMeshVertexPaintToolActions ActionType) { switch (ActionType) { case EMeshVertexPaintToolActions::PaintAll: FloodFillColorAction(BasicProperties->PaintColor); break; case EMeshVertexPaintToolActions::EraseAll: FloodFillColorAction(BasicProperties->EraseColor); break; case EMeshVertexPaintToolActions::FillBlack: FloodFillColorAction(FLinearColor::Black); break; case EMeshVertexPaintToolActions::FillWhite: FloodFillColorAction(FLinearColor::White); break; case EMeshVertexPaintToolActions::ApplyCurrentUtility: ApplyCurrentUtilityAction(); break; } } #undef LOCTEXT_NAMESPACE