Files
UnrealEngine/Engine/Plugins/Runtime/MeshModelingToolset/Source/MeshModelingTools/Private/MeshVertexPaintTool.cpp
2025-05-18 13:04:45 +08:00

2187 lines
69 KiB
C++

// 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<UMeshVertexPaintTool>(SceneState.ToolManager);
SculptTool->SetWorld(SceneState.World);
return SculptTool;
}
void UMeshVertexPaintToolBuilder::InitializeNewTool(UMeshSurfacePointTool* NewTool, const FToolBuilderState& SceneState) const
{
Super::InitializeNewTool(NewTool, SceneState);
UMeshVertexPaintTool* Tool = Cast<UMeshVertexPaintTool>(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<UMeshComponent>(&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<AInternalToolFrameworkActor>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo);
DynamicMeshComponent = NewObject<UDynamicMeshComponent>(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<UVertexPaintBrushFilterProperties>(this);
FilterProperties->RestoreProperties(this);
FilterProperties->WatchProperty(FilterProperties->MaterialMode, [this](EMeshVertexPaintMaterialMode) { UpdateVertexPaintMaterialMode(); });
FilterProperties->bToolHasSelection = GeometrySelection.IsSet();
SelectionTids.Reset();
TFuture<void> PrecomputeFuture = Async(VertexPaintToolAsyncExecTarget, [&]()
{
PrecomputeFilterData();
});
TFuture<void> 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<void> AABBTreeFuture = Async(VertexPaintToolAsyncExecTarget, [&]()
{
AABBTree.SetMesh(Mesh, true);
});
// initialize render decomposition
TUniquePtr<FMeshRenderDecomposition> Decomp = MakeUnique<FMeshRenderDecomposition>();
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<UPolyLassoMarqueeMechanic>(this);
PolyLassoMechanic->Setup(this);
PolyLassoMechanic->SetIsEnabled(false);
PolyLassoMechanic->SpacingTolerance = 10.0f;
PolyLassoMechanic->OnDrawPolyLassoFinished.AddUObject(this, &UMeshVertexPaintTool::OnPolyLassoFinished);
PolygroupLayerProperties = NewObject<UPolygroupLayersProperties>(this);
PolygroupLayerProperties->RestoreProperties(this, TEXT("MeshVertexPaintTool"));
PolygroupLayerProperties->InitializeGroupLayers(GetSculptMesh());
PolygroupLayerProperties->WatchProperty(PolygroupLayerProperties->ActiveGroupLayer, [&](FName) { OnSelectedGroupLayerChanged(); });
UpdateActiveGroupLayer();
BasicProperties = NewObject<UVertexPaintBasicProperties>(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<UVertexColorPaintBrushOpProps>(this);
RegisterBrushType((int32)EMeshVertexPaintBrushType::Paint, LOCTEXT("Paint", "Paint"),
MakeUnique<FLambdaMeshSculptBrushOpFactory>([this]() { return MakeUnique<UE::Geometry::FVertexPaintBrushOp>(); }),
PaintBrushOpProperties);
// secondary brushes
EraseBrushOpProperties = NewObject<UVertexColorPaintBrushOpProps>(this);
RegisterSecondaryBrushType((int32)EMeshVertexPaintBrushType::Erase, LOCTEXT("Erase", "Erase"),
MakeUnique<FLambdaMeshSculptBrushOpFactory>([this]() { return MakeUnique<UE::Geometry::FVertexPaintBrushOp>(); }),
EraseBrushOpProperties);
RegisterSecondaryBrushType((int32)EMeshVertexPaintBrushType::Soften, LOCTEXT("Soften", "Soften"),
MakeUnique<TBasicMeshSculptBrushOpFactory<FVertexColorSoftenBrushOp>>(),
NewObject<UVertexColorSoftenBrushOpProps>(this));
RegisterSecondaryBrushType((int32)EMeshVertexPaintBrushType::Smooth, LOCTEXT("Smooth", "Smooth"),
MakeUnique<TBasicMeshSculptBrushOpFactory<FVertexColorSmoothBrushOp>>(),
NewObject<UVertexColorSmoothBrushOpProps>(this));
// falloffs
RegisterStandardFalloffTypes();
QuickActions = NewObject<UMeshVertexPaintToolQuickActions>(this);
QuickActions->Initialize(this);
UtilityActions = NewObject<UMeshVertexPaintToolUtilityActions>(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<UMeshElementsVisualizer>(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<FName> 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<int32>(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<FMeshSculptBrushOp>& 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<FMeshSculptBrushOp>& 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<int32> 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<int32> 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<int32> StartROI;
TSet<int32> 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<int32> 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<FMeshSculptBrushOp>& 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<FMeshSculptBrushOp>& 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<FVector2D> 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<int32>& Triangles, const FLinearColor& ToColor)
{
TempROIBuffer.Reset();
for (int32 tid : Triangles)
{
TempROIBuffer.Add(tid);
}
SetTrianglesToVertexColor(TempROIBuffer, ToColor);
}
void UMeshVertexPaintTool::SetTrianglesToVertexColor(const TArray<int32>& Triangles, const FLinearColor& ToColor)
{
FDynamicMesh3* Mesh = GetSculptMesh();
FDynamicMeshColorOverlay* ColorOverlay = GetActiveColorOverlay();
const TArray<int32>* UseTriangles = &Triangles;
TArray<int32> 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<int32>& 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<int> 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<int32>& Triangles, TArray<int32>& ROIBuffer, TArray<int32>& 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<int32>& Triangles, TArray<int32>& 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<FMeshSculptBrushOp>& 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<FMeshSculptBrushOp>& 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<FDynamicMesh3>::RayTriangleIntersection(*GetSculptMesh(), CursorHitTID, LocalRay);
FVector3f BaryCoords = (FVector3f)Intersection.TriangleBaryCoords;
FVector4f InterpColor;
GetActiveColorOverlay()->GetTriBaryInterpolate<float>(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<float>(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<void> 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<int32> 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<int32>(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<int32>(UtilityActions->SourceChannel), 0, 3);
int32 DestChannelIndex = VectorUtil::Clamp(static_cast<int32>(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<FVector4f> 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<FDynamicMesh3>::TriangleDistance(FromMesh, NearestTri, ToPos);
FVector3f BaryCoords = (FVector3f)Dist.TriangleBaryCoords;
FVector4f InterpColor;
FromColors.GetTriBaryInterpolate<float>(NearestTri, &BaryCoords.X, &InterpColor.X);
VertexColors[vid] = InterpColor;
}
TArray<int32> 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<EMeshLODIdentifier> 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<FMeshDescription> 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<TIndexedValuesChangeBuilder<FVector4f, FMeshVertexColorPaintChange>>();
ActiveChangeBuilder->BeginNewChange();
LongTransactions.Open(LOCTEXT("VertexPaintChange", "Paint Stroke"), GetToolManager());
}
void UMeshVertexPaintTool::EndChange()
{
check(ActiveChangeBuilder);
TUniquePtr<FMeshVertexColorPaintChange> EditResult = ActiveChangeBuilder->ExtractResult();
ActiveChangeBuilder = nullptr;
EditResult->ApplyFunction = [](UObject* Object, const int32& AttribIndex, const TArray<int32>& Indices, const TArray<FVector4f>& Values)
{
UMeshVertexPaintTool* Tool = CastChecked<UMeshVertexPaintTool>(Object);
Tool->ExternalUpdateValues(Indices, Values);
};
EditResult->RevertFunction = [](UObject* Object, const int32& AttribIndex, const TArray<int32>& Indices, const TArray<FVector4f>& Values)
{
UMeshVertexPaintTool* Tool = CastChecked<UMeshVertexPaintTool>(Object);
Tool->ExternalUpdateValues(Indices, Values);
};
TUniquePtr<TWrappedToolCommandChange<FMeshVertexColorPaintChange>> NewChange = MakeUnique<TWrappedToolCommandChange<FMeshVertexColorPaintChange>>();
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<int32>& ElementIDs, const TArray<FVector4f>& 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<TSimpleValueLambdaChange<int32>> GroupLayerChange = MakeUnique<TSimpleValueLambdaChange<int32>>();
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<UE::Geometry::FPolygroupSet>(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<UE::Geometry::FPolygroupSet>(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