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

2855 lines
93 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DynamicMeshSculptTool.h"
#include "Containers/Map.h"
#include "Async/Async.h"
#include "Engine/World.h"
#include "InteractiveToolManager.h"
#include "InteractiveGizmoManager.h"
#include "ToolBuilderUtil.h"
#include "SubRegionRemesher.h"
#include "ProjectionTargets.h"
#include "MeshConstraints.h"
#include "MeshConstraintsUtil.h"
#include "MeshWeights.h"
#include "DynamicMesh/MeshNormals.h"
#include "DynamicMesh/MeshIndexUtil.h"
#include "Drawing/MeshDebugDrawing.h"
#include "PreviewMesh.h"
#include "ToolSetupUtil.h"
#include "ToolSceneQueriesUtil.h"
#include "ModelingToolTargetUtil.h"
#include "Changes/MeshVertexChange.h"
#include "Changes/MeshChange.h"
#include "DynamicMesh/DynamicMeshChangeTracker.h"
#include "ToolDataVisualizer.h"
#include "Components/PrimitiveComponent.h"
#include "Generators/SphereGenerator.h"
#include "Sculpting/KelvinletBrushOp.h"
#include "InteractiveGizmoManager.h"
#include "BaseBehaviors/TwoAxisPropertyEditBehavior.h"
#include "BaseGizmos/GizmoComponents.h"
#include "BaseGizmos/TransformGizmoUtil.h"
#include "UObject/ObjectMacros.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "TargetInterfaces/DynamicMeshCommitter.h"
#include "TargetInterfaces/DynamicMeshProvider.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(DynamicMeshSculptTool)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UDynamicMeshSculptTool"
namespace
{
// probably should be something defined for the whole tool framework...
#if WITH_EDITOR
static EAsyncExecution DynamicSculptToolAsyncExecTarget = EAsyncExecution::LargeThreadPool;
#else
static EAsyncExecution DynamicSculptToolAsyncExecTarget = EAsyncExecution::ThreadPool;
#endif
}
/*
* ToolBuilder
*/
UMeshSurfacePointTool* UDynamicMeshSculptToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
{
UDynamicMeshSculptTool* SculptTool = NewObject<UDynamicMeshSculptTool>(SceneState.ToolManager);
SculptTool->SetEnableRemeshing(this->bEnableRemeshing);
SculptTool->SetWorld(SceneState.World);
return SculptTool;
}
const FToolTargetTypeRequirements& UDynamicMeshSculptToolBuilder::GetTargetRequirements() const
{
static FToolTargetTypeRequirements TypeRequirements({
UMaterialProvider::StaticClass(),
UDynamicMeshCommitter::StaticClass(),
UDynamicMeshProvider::StaticClass(),
UPrimitiveComponentBackedTarget::StaticClass()
});
return TypeRequirements;
}
void UDynamicSculptToolActions::DiscardAttributes()
{
ParentTool->DiscardAttributes();
}
/*
* Tool
*/
UDynamicMeshSculptTool::UDynamicMeshSculptTool()
{
// initialize parameters
bEnableRemeshing = true;
}
void UDynamicMeshSculptTool::SetWorld(UWorld* World)
{
this->TargetWorld = World;
}
namespace
{
const FString BrushIndicatorGizmoType = TEXT("BrushIndicatorGizmoType");
}
void UDynamicMeshSculptTool::Setup()
{
UMeshSurfacePointTool::Setup();
SetToolDisplayName(LOCTEXT("ToolName", "DynaSculpt"));
// create dynamic mesh component to use for live preview
FActorSpawnParameters SpawnInfo;
PreviewMeshActor = TargetWorld->SpawnActor<AInternalToolFrameworkActor>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo);
DynamicMeshComponent = NewObject<UOctreeDynamicMeshComponent>(PreviewMeshActor);
DynamicMeshComponent->SetShadowsEnabled(false);
DynamicMeshComponent->SetupAttachment(PreviewMeshActor->GetRootComponent());
DynamicMeshComponent->SetWorldTransform((FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target));
DynamicMeshComponent->RegisterComponent();
ToolSetupUtil::ApplyRenderingConfigurationToPreview(DynamicMeshComponent, Target);
// initialize from LOD-0 MeshDescription
DynamicMeshComponent->SetMesh(UE::ToolTarget::GetDynamicMeshCopy(Target));
// transform mesh to world space because handling scaling inside brush is a mess
// Note: this transform does not include translation ( so only the 3x3 transform)
InitialTargetTransform = UE::ToolTarget::GetLocalToWorldTransform(Target);
// clamp scaling because if we allow zero-scale we cannot invert this transform on Accept
InitialTargetTransform.ClampMinimumScale(0.01);
FVector3d Translation = InitialTargetTransform.GetTranslation();
InitialTargetTransform.SetTranslation(FVector3d::Zero());
DynamicMeshComponent->ApplyTransform(InitialTargetTransform, false);
// since we moved to World coords there is not a current transform anymore.
CurTargetTransform = FTransformSRT3d(Translation);
DynamicMeshComponent->SetWorldTransform((FTransform)CurTargetTransform);
// copy material if there is one
FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Target);
if (MaterialSet.Materials.Num() > 0)
{
DynamicMeshComponent->SetMaterial(0, MaterialSet.Materials[0]);
}
OnDynamicMeshComponentChangedHandle = DynamicMeshComponent->OnMeshChanged.Add(
FSimpleMulticastDelegate::FDelegate::CreateUObject(this, &UDynamicMeshSculptTool::OnDynamicMeshComponentChanged));
// do we always want to keep vertex normals updated? Perhaps should discard vertex normals before baking?
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FMeshNormals::QuickComputeVertexNormals(*Mesh);
// switch to vertex normals for testing
//DynamicMeshComponent->GetMesh()->DiscardAttributes();
// initialize target mesh
UpdateTarget();
bTargetDirty = false;
PendingTargetUpdate.Wait();
// initialize brush radius range interval, brush properties
FAxisAlignedBox3d Bounds = DynamicMeshComponent->GetMesh()->GetBounds(true);
double MaxDimension = Bounds.MaxDim();
BrushRelativeSizeRange = FInterval1d(MaxDimension*0.01, MaxDimension);
BrushProperties = NewObject<UDynamicMeshBrushProperties>(this);
BrushProperties->BrushSize.InitializeWorldSizeRange(
TInterval<float>((float)BrushRelativeSizeRange.Min, (float)BrushRelativeSizeRange.Max));
CalculateBrushRadius();
// initialize other properties
SculptProperties = NewObject<UDynamicMeshBrushSculptProperties>(this);
KelvinBrushProperties = NewObject<UKelvinBrushProperties>(this);
RemeshProperties = NewObject<UBrushRemeshProperties>(this);
RemeshProperties->RestoreProperties(this);
InitialEdgeLength = EstimateIntialSafeTargetLength(*Mesh, 5000);
// hide input Component
UE::ToolTarget::HideSourceObject(Target);
// init state flags flags
bInDrag = false;
bHaveRemeshed = false;
bRemeshPending = false;
bStampPending = false;
ActiveVertexChange = nullptr;
// register and spawn brush indicator gizmo
GetToolManager()->GetPairedGizmoManager()->RegisterGizmoType(BrushIndicatorGizmoType, NewObject<UBrushStampIndicatorBuilder>());
BrushIndicator = GetToolManager()->GetPairedGizmoManager()->CreateGizmo<UBrushStampIndicator>(BrushIndicatorGizmoType, FString(), this);
BrushIndicatorMesh = MakeDefaultSphereMesh(this, TargetWorld);
BrushIndicator->AttachedComponent = BrushIndicatorMesh->GetRootComponent();
BrushIndicator->LineThickness = 1.0;
BrushIndicator->bDrawIndicatorLines = true;
BrushIndicator->bDrawRadiusCircle = false;
BrushIndicator->LineColor = FLinearColor(0.9f, 0.4f, 0.4f);
// initialize our properties
AddToolPropertySource(BrushProperties);
AddToolPropertySource(SculptProperties);
// add brush-specific properties
SculptMaxBrushProperties = NewObject<USculptMaxBrushProperties>();
SculptMaxBrushProperties->RestoreProperties(this);
AddToolPropertySource(SculptMaxBrushProperties);
AddToolPropertySource(KelvinBrushProperties);
KelvinBrushProperties->RestoreProperties(this);
GizmoProperties = NewObject<UFixedPlaneBrushProperties>();
GizmoProperties->RestoreProperties(this);
AddToolPropertySource(GizmoProperties);
GizmoProperties->RecenterGizmoIfFar(CurTargetTransform.TransformPosition(Bounds.Center()), Bounds.MaxDim());
if (this->bEnableRemeshing)
{
SculptProperties->bIsRemeshingEnabled = true;
AddToolPropertySource(RemeshProperties);
SculptToolActions = NewObject<UDynamicSculptToolActions>();
SculptToolActions->Initialize(this);
AddToolPropertySource(SculptToolActions);
}
BrushProperties->RestoreProperties(this);
CalculateBrushRadius();
SculptProperties->RestoreProperties(this);
// disable tool-specific properties
SetToolPropertySourceEnabled(GizmoProperties, false);
SetToolPropertySourceEnabled(SculptMaxBrushProperties, false);
SetToolPropertySourceEnabled(KelvinBrushProperties, false);
ViewProperties = NewObject<UMeshEditingViewProperties>();
ViewProperties->RestoreProperties(this);
AddToolPropertySource(ViewProperties);
// register watchers
ShowWireframeWatcher.Initialize(
[this]() { return ViewProperties->bShowWireframe; },
[this](bool bNewValue) { DynamicMeshComponent->bExplicitShowWireframe = bNewValue; }, ViewProperties->bShowWireframe);
MaterialModeWatcher.Initialize(
[this]() { return ViewProperties->MaterialMode; },
[this](EMeshEditingMaterialModes NewMode) { UpdateMaterialMode(NewMode); }, EMeshEditingMaterialModes::ExistingMaterial);
CustomMaterialWatcher.Initialize(
[this]() { return ViewProperties->CustomMaterial; },
[this](TWeakObjectPtr<UMaterialInterface> NewMaterial) { UpdateCustomMaterial(NewMaterial); }, ViewProperties->CustomMaterial);
FlatShadingWatcher.Initialize(
[this]() { return ViewProperties->bFlatShading; },
[this](bool bNewValue) { UpdateFlatShadingSetting(bNewValue); }, ViewProperties->bFlatShading);
ColorWatcher.Initialize(
[this]() { return ViewProperties->Color; },
[this](FLinearColor NewColor) { UpdateColorSetting(NewColor); }, ViewProperties->Color);
ImageWatcher.Initialize(
[this]() { return ViewProperties->Image; },
[this](UTexture2D* NewImage) { UpdateImageSetting(NewImage); }, ViewProperties->Image);
TransparentColorWatcher.Initialize(
[this]() { return ViewProperties->TransparentMaterialColor; },
[this](FLinearColor NewColor) { UpdateColorSetting(NewColor); }, ViewProperties->TransparentMaterialColor);
OpacityWatcher.Initialize(
[this]() { return ViewProperties->Opacity; },
[this](double Opacity) { UpdateOpacitySetting(Opacity); }, ViewProperties->Opacity);
TwoSidedWatcher.Initialize(
[this]() { return ViewProperties->bTwoSided; },
[this](bool bOn) { UpdateTwoSidedSetting(bOn); }, ViewProperties->bTwoSided);
BrushTypeWatcher.Initialize(
[this]() { return SculptProperties->PrimaryBrushType; },
[this](EDynamicMeshSculptBrushType NewBrushType) { UpdateBrushType(NewBrushType); }, SculptProperties->PrimaryBrushType);
GizmoPositionWatcher.Initialize(
[this]() { return GizmoProperties->Position; },
[this](FVector NewPosition) { UpdateGizmoFromProperties(); }, GizmoProperties->Position);
GizmoRotationWatcher.Initialize(
[this]() { return GizmoProperties->Rotation; },
[this](FQuat NewRotation) { UpdateGizmoFromProperties(); }, GizmoProperties->Rotation);
DynamicMeshComponent->bExplicitShowWireframe = ViewProperties->bShowWireframe;
// create proxy for plane gizmo, but not gizmo itself, as it only appears in FixedPlane brush mode
// listen for changes to the proxy and update the plane when that happens
PlaneTransformProxy = NewObject<UTransformProxy>(this);
PlaneTransformProxy->OnTransformChanged.AddUObject(this, &UDynamicMeshSculptTool::PlaneTransformChanged);
if (bEnableRemeshing)
{
PrecomputeRemeshInfo();
if (bHaveUVSeams)
{
GetToolManager()->DisplayMessage(
LOCTEXT("UVSeamWarning", "This mesh has UV seams which may limit remeshing. Consider clearing the UV layers using \"Discard Attributes\" or the Remesh Tool."),
EToolMessageLevel::UserWarning);
}
else if (bHaveNormalSeams)
{
GetToolManager()->DisplayMessage(
LOCTEXT("NormalSeamWarning", "This mesh has Hard Normal seams which may limit remeshing. Consider clearing Hard Normals using \"Discard Attributes,\" or the Remesh or Normals Tool."),
EToolMessageLevel::UserWarning);
}
}
UpdateBrushType(SculptProperties->PrimaryBrushType);
// add input behavior to click-drag while holding hotkey to adjust brush size and strength
{
BrushEditBehavior = NewObject<ULocalTwoAxisPropertyEditInputBehavior>(this);
BrushEditBehavior->HorizontalProperty.GetValueFunc = [this]()
{
if (BrushProperties->BrushSize.SizeType == EBrushToolSizeType::Adaptive)
{
return BrushProperties->BrushSize.AdaptiveSize;
}
else
{
return BrushProperties->BrushSize.WorldRadius;
}
};
BrushEditBehavior->HorizontalProperty.SetValueFunc = [this](float NewValue)
{
if (BrushProperties->BrushSize.SizeType == EBrushToolSizeType::Adaptive)
{
BrushProperties->BrushSize.AdaptiveSize = FMath::Clamp(NewValue, 0.f, 1.f);
}
else
{
BrushProperties->BrushSize.WorldRadius = FMath::Max(NewValue, 0.01f);
}
#if WITH_EDITOR
FPropertyChangedEvent PropertyChangedEvent(UDynamicMeshBrushProperties::StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(UDynamicMeshBrushProperties, BrushSize)));
BrushProperties->PostEditChangeProperty(PropertyChangedEvent);
#endif
};
BrushEditBehavior->HorizontalProperty.MutateDeltaFunc = [this](float Delta)
{
// Scale delta if brush size is in world units.
return Delta * (BrushProperties->BrushSize.SizeType == EBrushToolSizeType::World ? (CameraState.Position - LastBrushPosWorld).Length() : 1.f);
};
BrushEditBehavior->HorizontalProperty.Name = LOCTEXT("BrushRadius", "Radius");
BrushEditBehavior->HorizontalProperty.bEnabled = true;
BrushEditBehavior->VerticalProperty.GetValueFunc = [this]()
{
return SculptProperties->PrimaryBrushSpeed;
};
BrushEditBehavior->VerticalProperty.SetValueFunc = [this](float NewValue)
{
SculptProperties->PrimaryBrushSpeed = FMath::Clamp(NewValue, 0.f, 1.f);
#if WITH_EDITOR
FPropertyChangedEvent PropertyChangedEvent(UDynamicMeshBrushSculptProperties::StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(UDynamicMeshBrushSculptProperties, PrimaryBrushSpeed)));
BrushProperties->PostEditChangeProperty(PropertyChangedEvent);
#endif
};
BrushEditBehavior->VerticalProperty.Name = LOCTEXT("BrushStrength", "Strength");
BrushEditBehavior->VerticalProperty.bEnabled = true;
BrushEditBehavior->OnDragUpdated.AddWeakLambda(this, [this]()
{
CalculateBrushRadius();
NotifyOfPropertyChangeByTool(BrushProperties);
});
BrushEditBehavior->Initialize();
AddInputBehavior(BrushEditBehavior.Get());
}
}
void UDynamicMeshSculptTool::Shutdown(EToolShutdownType ShutdownType)
{
LongTransactions.CloseAll(GetToolManager());
if (ShutdownType == EToolShutdownType::Accept && AreAllTargetsValid() == false)
{
UE_LOG(LogTemp, Error, TEXT("Tool Target has become Invalid (possibly it has been Force Deleted). Aborting Tool."));
ShutdownType = EToolShutdownType::Cancel;
}
BrushIndicatorMesh->Disconnect();
BrushIndicatorMesh = nullptr;
GetToolManager()->GetPairedGizmoManager()->DestroyAllGizmosByOwner(this);
BrushIndicator = nullptr;
GetToolManager()->GetPairedGizmoManager()->DeregisterGizmoType(BrushIndicatorGizmoType);
if (DynamicMeshComponent != nullptr)
{
DynamicMeshComponent->OnMeshChanged.Remove(OnDynamicMeshComponentChangedHandle);
UE::ToolTarget::ShowSourceObject(Target);
if (ShutdownType == EToolShutdownType::Accept)
{
// safe to do this here because we are about to destroy componeont
DynamicMeshComponent->ApplyTransform(InitialTargetTransform, true);
// this block bakes the modified DynamicMeshComponent back into the StaticMeshComponent inside an undo transaction
GetToolManager()->BeginUndoTransaction(LOCTEXT("SculptMeshToolTransactionName", "Sculpt Mesh"));
DynamicMeshComponent->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
{
UE::ToolTarget::CommitDynamicMeshUpdate(Target, ReadMesh, bHaveRemeshed);
});
GetToolManager()->EndUndoTransaction();
}
DynamicMeshComponent->UnregisterComponent();
DynamicMeshComponent->DestroyComponent();
DynamicMeshComponent = nullptr;
}
if (PreviewMeshActor != nullptr)
{
PreviewMeshActor->Destroy();
PreviewMeshActor = nullptr;
}
BrushProperties->SaveProperties(this);
SculptProperties->SaveProperties(this);
KelvinBrushProperties->SaveProperties(this);
ViewProperties->SaveProperties(this);
GizmoProperties->SaveProperties(this);
SculptMaxBrushProperties->SaveProperties(this);
RemeshProperties->SaveProperties(this);
}
void UDynamicMeshSculptTool::OnDynamicMeshComponentChanged()
{
bNormalUpdatePending = true;
bTargetDirty = true;
}
void UDynamicMeshSculptTool::OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
CalculateBrushRadius();
}
FBox UDynamicMeshSculptTool::GetWorldSpaceFocusBox()
{
if (LastBrushTriangleID == INDEX_NONE)
{
return Super::GetWorldSpaceFocusBox();
}
FVector Center = LastBrushPosWorld;
double Size = CurrentBrushRadius;
return FBox(Center - FVector(Size), Center + FVector(Size));
}
bool UDynamicMeshSculptTool::HitTest(const FRay& Ray, FHitResult& OutHit)
{
FRay3d LocalRay(CurTargetTransform.InverseTransformPosition((FVector3d)Ray.Origin),
CurTargetTransform.InverseTransformVector((FVector3d)Ray.Direction));
UE::Geometry::Normalize(LocalRay.Direction);
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int HitTID = FindHitSculptMeshTriangle(LocalRay);
if (HitTID != IndexConstants::InvalidID)
{
FTriangle3d Triangle;
Mesh->GetTriVertices(HitTID, Triangle.V[0], Triangle.V[1], Triangle.V[2]);
FIntrRay3Triangle3d Query(LocalRay, Triangle);
Query.Find();
OutHit.FaceIndex = HitTID;
OutHit.Distance = Query.RayParameter;
OutHit.Normal = (FVector)CurTargetTransform.TransformNormal(Mesh->GetTriNormal(HitTID));
OutHit.ImpactPoint = (FVector)CurTargetTransform.TransformPosition(LocalRay.PointAt(Query.RayParameter));
return true;
}
return false;
}
void UDynamicMeshSculptTool::OnBeginDrag(const FRay& Ray)
{
bSmoothing = GetShiftToggle();
bInvert = GetCtrlToggle();
FHitResult OutHit;
if (HitTest(Ray, OutHit))
{
BrushStartCenterWorld = (FVector3d)Ray.PointAt(OutHit.Distance) + (double)BrushProperties->Depth*CurrentBrushRadius*(FVector3d)Ray.Direction;
bInDrag = true;
ActiveDragPlane = FFrame3d(BrushStartCenterWorld, -(FVector3d)Ray.Direction);
ActiveDragPlane.RayPlaneIntersection((FVector3d)Ray.Origin, (FVector3d)Ray.Direction, 2, LastHitPosWorld);
LastBrushPosWorld = LastHitPosWorld;
LastBrushPosNormalWorld = ActiveDragPlane.Z();
LastBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastHitPosWorld);
LastSmoothBrushPosLocal = LastBrushPosLocal;
BeginChange(bEnableRemeshing == false);
UpdateROI(LastBrushPosLocal);
if (SculptProperties->PrimaryBrushType == EDynamicMeshSculptBrushType::Plane)
{
ActiveFixedBrushPlane = ComputeROIBrushPlane(LastBrushPosLocal, false, false);
}
else if (SculptProperties->PrimaryBrushType == EDynamicMeshSculptBrushType::PlaneViewAligned)
{
AlignBrushToView();
ActiveFixedBrushPlane = ComputeROIBrushPlane(LastBrushPosLocal, false, true);
}
// apply initial stamp
PendingStampRay = Ray;
bStampPending = true;
}
}
void UDynamicMeshSculptTool::UpdateROI(const FVector3d& BrushPos)
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateROI);
float RadiusSqr = CurrentBrushRadius * CurrentBrushRadius;
FAxisAlignedBox3d BrushBox(
BrushPos - CurrentBrushRadius * FVector3d::One(),
BrushPos + CurrentBrushRadius * FVector3d::One());
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FDynamicMeshOctree3* Octree = DynamicMeshComponent->GetOctree();
// find set of triangles in brush bounding box
UpdateROITriBuffer.Reset();
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateROI_1RangeQuery);
Octree->ParallelRangeQuery(BrushBox, UpdateROITriBuffer);
}
// collect set of vertices inside brush sphere, from that box
VertexROIBuilder.Initialize(Mesh->MaxVertexID());
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateROI_2Collect);
for (int32 TriIdx : UpdateROITriBuffer)
{
FIndex3i TriV = Mesh->GetTriangle(TriIdx);
for (int j = 0; j < 3; ++j)
{
if (VertexROIBuilder.Contains(TriV[j]) == false)
{
//const FVector3d& Position = Mesh->GetVertexRef(TriV[j]);
FVector3d Position = Mesh->GetVertex(TriV[j]);
if (DistanceSquared(BrushPos, Position) < RadiusSqr)
{
VertexROIBuilder.Add(TriV[j]);
}
}
}
}
VertexROI.Reset();
VertexROIBuilder.SwapValuesWith(VertexROI);
}
// find triangle ROI as full one-rings of all vertices (this is surprisingly expensive...)
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateROI_3TriangleROI);
TriangleROIBuilder.Initialize(Mesh->MaxTriangleID());
for (int32 vid : VertexROI)
{
Mesh->EnumerateVertexEdges(vid, [&](int32 eid)
{
FDynamicMesh3::FEdge Edge = Mesh->GetEdge(eid);
TriangleROIBuilder.Add(Edge.Tri.A);
if (Edge.Tri.B != IndexConstants::InvalidID) TriangleROIBuilder.Add(Edge.Tri.B);
});
}
TriangleROI.Reset();
TriangleROIBuilder.Collect(TriangleROI);
}
}
void UDynamicMeshSculptTool::OnUpdateDrag(const FRay& WorldRay)
{
if (bInDrag)
{
PendingStampRay = WorldRay;
bStampPending = true;
}
}
void UDynamicMeshSculptTool::CalculateBrushRadius()
{
CurrentBrushRadius = BrushProperties->BrushSize.GetWorldRadius();
}
void UDynamicMeshSculptTool::ApplyStamp(const FRay& WorldRay)
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_ApplyStamp);
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FDynamicMeshOctree3* Octree = DynamicMeshComponent->GetOctree();
// update brush type history. apologies for convoluted logic.
StampTimestamp++;
if (LastStampType != PendingStampType)
{
if (BrushTypeHistoryIndex != BrushTypeHistory.Num() - 1)
{
if (LastStampType != EDynamicMeshSculptBrushType::LastValue)
{
BrushTypeHistory.Add(LastStampType);
}
BrushTypeHistoryIndex = BrushTypeHistory.Num()-1;
}
LastStampType = PendingStampType;
if (BrushTypeHistory.Num() == 0 || BrushTypeHistory[BrushTypeHistory.Num()-1] != PendingStampType)
{
BrushTypeHistory.Add(PendingStampType);
BrushTypeHistoryIndex = BrushTypeHistory.Num() - 1;
}
}
CalculateBrushRadius();
TFuture<void> DirtyOctreeFuture = Async(DynamicSculptToolAsyncExecTarget, [&]()
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_ApplyStamp_DirtyOctree);
Octree->NotifyPendingModification(TriangleROI);
});
TFuture<void> SaveROIFuture = Async(DynamicSculptToolAsyncExecTarget, [&]()
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_ApplyStamp_SaveActiveROI);
SaveActiveROI();
});
// TODO:
// - we can begin Octree->RemoveTriangles below as soon as we have (1) finished
// marking dirty box in DirtyOctreeFuture and (2) updated brush position. Unfortunately
// right now that happens inside each brush func :(
EDynamicMeshSculptBrushType ApplyBrushType = (bSmoothing) ?
EDynamicMeshSculptBrushType::Smooth : SculptProperties->PrimaryBrushType;
bool bBrushApplied = false;
switch (ApplyBrushType)
{
case EDynamicMeshSculptBrushType::Offset:
bBrushApplied = ApplyOffsetBrush(WorldRay, false);
break;
case EDynamicMeshSculptBrushType::SculptView:
bBrushApplied = ApplyOffsetBrush(WorldRay, true);
break;
case EDynamicMeshSculptBrushType::SculptMax:
bBrushApplied = ApplySculptMaxBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::Move:
bBrushApplied = ApplyMoveBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::PullKelvin:
bBrushApplied = ApplyPullKelvinBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::PullSharpKelvin:
bBrushApplied = ApplyPullSharpKelvinBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::Smooth:
bBrushApplied = ApplySmoothBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::Pinch:
bBrushApplied = ApplyPinchBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::TwistKelvin:
bBrushApplied = ApplyTwistKelvinBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::Inflate:
bBrushApplied = ApplyInflateBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::ScaleKelvin:
bBrushApplied = ApplyScaleKelvinBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::Flatten:
bBrushApplied = ApplyFlattenBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::Plane:
bBrushApplied = ApplyPlaneBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::PlaneViewAligned:
bBrushApplied = ApplyPlaneBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::FixedPlane:
bBrushApplied = ApplyFixedPlaneBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::Resample:
bBrushApplied = ApplyResampleBrush(WorldRay);
break;
case EDynamicMeshSculptBrushType::LastValue:
break;
}
// wait for ROI to finish saving before we update positions
SaveROIFuture.Wait();
DirtyOctreeFuture.Wait();
// we are going to reinsert these later
TFuture<void> OctreeRemoveFuture = Async(DynamicSculptToolAsyncExecTarget, [&]()
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_ApplyStamp_OctreeRemove);
Octree->RemoveTriangles(TriangleROI, false); // already marked dirty above
});
// Update the mesh positions to match those in the position buffer
if (bBrushApplied)
{
SyncMeshWithPositionBuffer(Mesh);
}
// we don't stricty have to wait here, we could return this future
OctreeRemoveFuture.Wait();
}
double UDynamicMeshSculptTool::CalculateBrushFalloff(double Distance)
{
double f = FMathd::Clamp(1.0 - BrushProperties->BrushFalloffAmount, 0.0, 1.0);
double d = Distance / CurrentBrushRadius;
double w = 1;
if (d > f)
{
d = FMathd::Clamp((d - f) / (1.0 - f), 0.0, 1.0);
w = (1.0 - d * d);
w = w * w * w;
}
return w;
}
void UDynamicMeshSculptTool::SyncMeshWithPositionBuffer(FDynamicMesh3* Mesh)
{
const int NumV = ROIPositionBuffer.Num();
checkSlow(VertexROI.Num() <= NumV);
// only if remeshing is disabled?
if (ActiveVertexChange != nullptr)
{
for (int k = 0; k < NumV; ++k)
{
int VertIdx = VertexROI[k];
ActiveVertexChange->UpdateVertex(VertIdx, Mesh->GetVertex(VertIdx), ROIPositionBuffer[k]);
}
}
ParallelFor(NumV, [&](int32 k)
{
int VertIdx = VertexROI[k];
const FVector3d& NewPos = ROIPositionBuffer[k];
Mesh->SetVertex(VertIdx, NewPos, false);
});
Mesh->UpdateChangeStamps(true, false);
}
bool UDynamicMeshSculptTool::ApplySmoothBrush(const FRay& WorldRay)
{
bool bHit = UpdateBrushPositionOnSculptMesh(WorldRay, true);
if (bHit == false)
{
return false;
}
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [this, Mesh, NewBrushPosLocal](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
double Falloff = CalculateBrushFalloff(Distance(OrigPos, NewBrushPosLocal));
FVector3d SmoothedPos = (SculptProperties->bPreserveUVFlow) ?
FMeshWeights::CotanCentroidSafe(*Mesh, VertIdx, 10.0) : FMeshWeights::UniformCentroid(*Mesh, VertIdx);
FVector3d NewPos = UE::Geometry::Lerp(OrigPos, SmoothedPos, Falloff*SculptProperties->SmoothBrushSpeed);
ROIPositionBuffer[k] = NewPos;
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyMoveBrush(const FRay& WorldRay)
{
UpdateBrushPositionOnActivePlane(WorldRay);
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d MoveVec = NewBrushPosLocal - LastBrushPosLocal;
if (MoveVec.SquaredLength() <= 0)
{
LastBrushPosLocal = NewBrushPosLocal;
return false;
}
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [this, Mesh, NewBrushPosLocal, MoveVec](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
double PrevDist = (OrigPos - LastBrushPosLocal).Length();
double NewDist = (OrigPos - NewBrushPosLocal).Length();
double UseDist = FMath::Min(PrevDist, NewDist);
double Falloff = CalculateBrushFalloff(UseDist) * ActivePressure;
FVector3d NewPos = OrigPos + Falloff * MoveVec;
ROIPositionBuffer[k] = NewPos;
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyOffsetBrush(const FRay& WorldRay, bool bUseViewDirection)
{
UpdateBrushPositionOnTargetMesh(WorldRay, bUseViewDirection);
if (bUseViewDirection)
{
AlignBrushToView();
}
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d LocalNormal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
double Direction = (bInvert) ? -1.0 : 1.0;
double UseSpeed = 0.5 * Direction * FMathd::Sqrt(CurrentBrushRadius) * (SculptProperties->PrimaryBrushSpeed) * ActivePressure;
double MaxOffset = CurrentBrushRadius;
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [&](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
FVector3d BasePos, BaseNormal;
if (GetTargetMeshNearest(OrigPos, (double)(4 * CurrentBrushRadius), BasePos, BaseNormal) == false)
{
ROIPositionBuffer[k] = OrigPos;
}
else
{
FVector3d MoveVec = (bUseViewDirection) ? (UseSpeed*LocalNormal) : (UseSpeed*BaseNormal);
double Falloff = CalculateBrushFalloff(Distance(OrigPos, NewBrushPosLocal));
FVector3d NewPos = OrigPos + Falloff * MoveVec;
ROIPositionBuffer[k] = NewPos;
}
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplySculptMaxBrush(const FRay& WorldRay)
{
UpdateBrushPositionOnTargetMesh(WorldRay, true);
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
double Direction = (bInvert) ? -1.0 : 1.0;
double UseSpeed = Direction * FMathd::Sqrt(CurrentBrushRadius) * (SculptProperties->PrimaryBrushSpeed) * ActivePressure;
double MaxOffset = CurrentBrushRadius * SculptMaxBrushProperties->MaxHeight;
if (SculptMaxBrushProperties->bFreezeCurrentHeight && SculptMaxFixedHeight >= 0)
{
MaxOffset = SculptMaxFixedHeight;
}
SculptMaxFixedHeight = MaxOffset;
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [this, Mesh, NewBrushPosLocal, UseSpeed, MaxOffset](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
FVector3d BasePos, BaseNormal;
if (GetTargetMeshNearest(OrigPos, (double)(2 * CurrentBrushRadius), BasePos, BaseNormal) == false)
{
ROIPositionBuffer[k] = OrigPos;
}
else
{
FVector3d MoveVec = UseSpeed * BaseNormal;
double Falloff = CalculateBrushFalloff(Distance(OrigPos, NewBrushPosLocal));
FVector3d NewPos = OrigPos + Falloff * MoveVec;
FVector3d DeltaPos = NewPos - BasePos;
if (DeltaPos.SquaredLength() > MaxOffset*MaxOffset)
{
UE::Geometry::Normalize(DeltaPos);
NewPos = BasePos + MaxOffset * DeltaPos;
}
ROIPositionBuffer[k] = NewPos;
}
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyPinchBrush(const FRay& WorldRay)
{
UpdateBrushPositionOnTargetMesh(WorldRay, true);
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d BrushNormalLocal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
FVector3d OffsetBrushPosLocal = NewBrushPosLocal - BrushProperties->Depth * CurrentBrushRadius * BrushNormalLocal;
// hardcoded lazybrush...
FVector3d NewSmoothBrushPosLocal = (0.75f)*LastSmoothBrushPosLocal + (0.25f)*NewBrushPosLocal;
double Direction = (bInvert) ? -1.0 : 1.0;
double UseSpeed = Direction * FMathd::Sqrt(CurrentBrushRadius) * (SculptProperties->PrimaryBrushSpeed*0.05) * ActivePressure;
FVector3d MotionVec = NewSmoothBrushPosLocal - LastSmoothBrushPosLocal;
bool bHaveMotion = (MotionVec.Length() > FMathf::ZeroTolerance);
UE::Geometry::Normalize(MotionVec);
FLine3d MoveLine(LastSmoothBrushPosLocal, MotionVec);
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [this, Mesh, NewBrushPosLocal, OffsetBrushPosLocal, bHaveMotion, MotionVec, UseSpeed](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
FVector3d Delta = OffsetBrushPosLocal - OrigPos;
FVector3d MoveVec = UseSpeed * Delta;
// pinch uses 1/x falloff, shifted so that
double Dist = Distance(OrigPos, NewBrushPosLocal);
double NormalizedDistance = Dist / CurrentBrushRadius + FMathf::ZeroTolerance;
double Falloff = (1.0/NormalizedDistance) - 1.0;
Falloff = FMathd::Clamp(Falloff, 0.0, 1.0);
if (bHaveMotion && Falloff < 0.8f)
{
double AnglePower = 1.0 - FMathd::Abs(UE::Geometry::Normalized(MoveVec).Dot(MotionVec));
Falloff *= AnglePower;
}
FVector3d NewPos = OrigPos + Falloff * MoveVec;
ROIPositionBuffer[k] = NewPos;
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
LastSmoothBrushPosLocal = NewSmoothBrushPosLocal;
return true;
}
FFrame3d UDynamicMeshSculptTool::ComputeROIBrushPlane(const FVector3d& BrushCenter, bool bIgnoreDepth, bool bViewAligned)
{
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FVector3d AverageNormal(0, 0, 0);
FVector3d AveragePos(0, 0, 0);
double WeightSum = 0;
for (int TriID : TriangleROI)
{
FVector3d Centroid = Mesh->GetTriCentroid(TriID);
double Weight = CalculateBrushFalloff(Distance(BrushCenter, Centroid));
AverageNormal += Weight * Mesh->GetTriNormal(TriID);
AveragePos += Weight * Centroid;
WeightSum += Weight;
}
UE::Geometry::Normalize(AverageNormal);
AveragePos /= WeightSum;
if (bViewAligned)
{
AverageNormal = -(FVector3d)CameraState.Forward();
}
FFrame3d Result = FFrame3d(AveragePos, AverageNormal);
if (bIgnoreDepth == false)
{
Result.Origin -= BrushProperties->Depth * CurrentBrushRadius * Result.Z();
}
return Result;
}
bool UDynamicMeshSculptTool::ApplyPlaneBrush(const FRay& WorldRay)
{
bool bHit = UpdateBrushPositionOnSculptMesh(WorldRay, true);
if (bHit == false)
{
return false;
}
static const double PlaneSigns[3] = { 0, -1, 1 };
double PlaneSign = PlaneSigns[0];
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d BrushNormalLocal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
double UseSpeed = FMathd::Sqrt(CurrentBrushRadius) * FMathd::Sqrt(SculptProperties->PrimaryBrushSpeed) * 0.05 * ActivePressure;
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [&](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
FVector3d PlanePos = ActiveFixedBrushPlane.ToPlane(OrigPos, 2);
FVector3d Delta = PlanePos - OrigPos;
double Dot = Delta.Dot(ActiveFixedBrushPlane.Z());
FVector3d NewPos = OrigPos;
if (Dot * PlaneSign >= 0)
{
FVector3d MoveVec = UseSpeed * Delta;
double Falloff = CalculateBrushFalloff(Distance(OrigPos, NewBrushPosLocal));
NewPos = OrigPos + Falloff * MoveVec;
}
ROIPositionBuffer[k] = NewPos;
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyFixedPlaneBrush(const FRay& WorldRay)
{
bool bHit = UpdateBrushPositionOnSculptMesh(WorldRay, true);
if (bHit == false)
{
return false;
}
static const double PlaneSigns[3] = { 0, -1, 1 };
double PlaneSign = PlaneSigns[0];
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d BrushNormalLocal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
double UseSpeed = CurrentBrushRadius * FMathd::Sqrt(SculptProperties->PrimaryBrushSpeed) * 0.1 * ActivePressure;
FFrame3d FixedPlaneLocal(
CurTargetTransform.InverseTransformPosition((FVector3d)GizmoProperties->Position),
CurTargetTransform.GetRotation().Inverse() * (FQuaterniond)GizmoProperties->Rotation);
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [&](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
FVector3d PlanePos = FixedPlaneLocal.ToPlane(OrigPos, 2);
FVector3d Delta = PlanePos - OrigPos;
double Dot = Delta.Dot(FixedPlaneLocal.Z());
FVector3d NewPos = OrigPos;
if (Dot * PlaneSign >= 0)
{
double MaxDist = UE::Geometry::Normalize(Delta);
double Falloff = CalculateBrushFalloff(Distance(OrigPos, NewBrushPosLocal));
FVector3d MoveVec = Falloff * UseSpeed * Delta;
NewPos = (MoveVec.SquaredLength() > MaxDist* MaxDist) ?
PlanePos : OrigPos + Falloff * MoveVec;
}
ROIPositionBuffer[k] = NewPos;
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyFlattenBrush(const FRay& WorldRay)
{
bool bHit = UpdateBrushPositionOnSculptMesh(WorldRay, true);
if (bHit == false)
{
return false;
}
static const double PlaneSigns[3] = { 0, -1, 1 };
double PlaneSign = PlaneSigns[0];
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d BrushNormalLocal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
double UseSpeed = FMathd::Sqrt(CurrentBrushRadius) * FMathd::Sqrt(SculptProperties->PrimaryBrushSpeed) * 0.05 * ActivePressure;
FFrame3d StampFlattenPlane = ComputeROIBrushPlane(NewBrushPosLocal, true, false);
//StampFlattenPlane.Origin -= BrushProperties->Depth * CurrentBrushRadius * StampFlattenPlane.Z();
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [&](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
FVector3d PlanePos = StampFlattenPlane.ToPlane(OrigPos, 2);
FVector3d Delta = PlanePos - OrigPos;
double Dot = Delta.Dot(StampFlattenPlane.Z());
FVector3d NewPos = OrigPos;
if (Dot * PlaneSign >= 0)
{
double MaxDist = UE::Geometry::Normalize(Delta);
double Falloff = CalculateBrushFalloff(Distance(OrigPos, NewBrushPosLocal));
FVector3d MoveVec = Falloff * UseSpeed * Delta;
NewPos = (MoveVec.SquaredLength() > MaxDist*MaxDist) ?
PlanePos : OrigPos + Falloff * MoveVec;
}
ROIPositionBuffer[k] = NewPos;
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyInflateBrush(const FRay& WorldRay)
{
bool bHit = UpdateBrushPositionOnSculptMesh(WorldRay, true);
if (bHit == false)
{
return false;
}
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
double Direction = (bInvert) ? -1.0 : 1.0;
double UseSpeed = Direction * CurrentBrushRadius * SculptProperties->PrimaryBrushSpeed * 0.05 * ActivePressure;
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
// calculate vertex normals
ParallelFor(VertexROI.Num(), [this, Mesh](int Index) {
int VertIdx = VertexROI[Index];
FVector3d Normal = FMeshNormals::ComputeVertexNormal(*Mesh, VertIdx);
Mesh->SetVertexNormal(VertIdx, (FVector3f)Normal);
});
ParallelFor(VertexROI.Num(), [this, Mesh, UseSpeed, NewBrushPosLocal](int k)
{
int VertIdx = VertexROI[k];
FVector3d OrigPos = Mesh->GetVertex(VertIdx);
FVector3d Normal = (FVector3d)Mesh->GetVertexNormal(VertIdx);
FVector3d MoveVec = UseSpeed * Normal;
double Falloff = CalculateBrushFalloff(Distance(OrigPos, NewBrushPosLocal));
FVector3d NewPos = OrigPos + Falloff*MoveVec;
ROIPositionBuffer[k] = NewPos;
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyResampleBrush(const FRay& WorldRay)
{
UpdateBrushPositionOnTargetMesh(WorldRay, true);
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d LocalNormal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
double UseSpeed = FMathd::Sqrt(CurrentBrushRadius) * (SculptProperties->PrimaryBrushSpeed) * ActivePressure;
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int NumV = VertexROI.Num();
ROIPositionBuffer.SetNum(NumV, EAllowShrinking::No);
ParallelFor(NumV, [&](int k)
{
ROIPositionBuffer[k] = Mesh->GetVertex(VertexROI[k]);
});
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyPullKelvinBrush(const FRay& WorldRay)
{
UpdateBrushPositionOnActivePlane(WorldRay);
FVector3d BrushNormalLocal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d MoveVec = NewBrushPosLocal - LastBrushPosLocal;
if (MoveVec.SquaredLength() <= 0)
{
LastBrushPosLocal = NewBrushPosLocal;
return false;
}
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FKelvinletBrushOp KelvinBrushOp(*Mesh);
const EKelvinletBrushMode KelvinMode = EKelvinletBrushMode::PullKelvinlet;
FKelvinletBrushOp::FKelvinletBrushOpProperties KelvinletBrushOpProperties(KelvinMode, *KelvinBrushProperties, CurrentBrushRadius, BrushProperties->BrushFalloffAmount);
KelvinletBrushOpProperties.Direction = FVector(MoveVec.X, MoveVec.Y, MoveVec.Z); //FVector(BrushNormalLocal.X, BrushNormalLocal.Y, BrushNormalLocal.Z);
KelvinletBrushOpProperties.Size *= 0.6;
FMatrix ToBrush; ToBrush.SetIdentity(); ToBrush.SetOrigin(-FVector(NewBrushPosLocal.X, NewBrushPosLocal.Y, NewBrushPosLocal.Z)); // ToBrush.
KelvinBrushOp.ApplyBrush(KelvinletBrushOpProperties, ToBrush, VertexROI, ROIPositionBuffer);
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyPullSharpKelvinBrush(const FRay& WorldRay)
{
UpdateBrushPositionOnActivePlane(WorldRay);
FVector3d BrushNormalLocal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d MoveVec = NewBrushPosLocal - LastBrushPosLocal;
if (MoveVec.SquaredLength() <= 0)
{
LastBrushPosLocal = NewBrushPosLocal;
return false;
}
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FKelvinletBrushOp KelvinBrushOp(*Mesh);
const EKelvinletBrushMode KelvinMode = EKelvinletBrushMode::SharpPullKelvinlet;
FKelvinletBrushOp::FKelvinletBrushOpProperties KelvinletBrushOpProperties(KelvinMode, *KelvinBrushProperties, CurrentBrushRadius, BrushProperties->BrushFalloffAmount);
KelvinletBrushOpProperties.Direction = FVector(MoveVec.X, MoveVec.Y, MoveVec.Z); //FVector(BrushNormalLocal.X, BrushNormalLocal.Y, BrushNormalLocal.Z);
KelvinletBrushOpProperties.Size *= 0.6;
FMatrix ToBrush; ToBrush.SetIdentity(); ToBrush.SetOrigin(-FVector(NewBrushPosLocal.X, NewBrushPosLocal.Y, NewBrushPosLocal.Z)); // ToBrush.
KelvinBrushOp.ApplyBrush(KelvinletBrushOpProperties, ToBrush, VertexROI, ROIPositionBuffer);
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyTwistKelvinBrush(const FRay& WorldRay)
{
UpdateBrushPositionOnTargetMesh(WorldRay, true);
FVector3d BrushNormalLocal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
double Direction = (bInvert) ? -1.0 : 1.0;
double UseSpeed = Direction * FMathd::Sqrt(CurrentBrushRadius) * (SculptProperties->PrimaryBrushSpeed) * ActivePressure;
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FKelvinletBrushOp KelvinBrushOp(*Mesh);
FKelvinletBrushOp::FKelvinletBrushOpProperties KelvinletBrushOpProperties(EKelvinletBrushMode::TwistKelvinlet, *KelvinBrushProperties, CurrentBrushRadius, BrushProperties->BrushFalloffAmount);
KelvinletBrushOpProperties.Direction = UseSpeed * FVector(BrushNormalLocal.X, BrushNormalLocal.Y, BrushNormalLocal.Z); // twist about local normal
KelvinletBrushOpProperties.Size *= 0.35; // reduce the core size of this brush.
FMatrix ToBrush; ToBrush.SetIdentity(); ToBrush.SetOrigin(-FVector(NewBrushPosLocal.X, NewBrushPosLocal.Y, NewBrushPosLocal.Z)); // ToBrush.
KelvinBrushOp.ApplyBrush(KelvinletBrushOpProperties, ToBrush, VertexROI, ROIPositionBuffer);
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::ApplyScaleKelvinBrush(const FRay& WorldRay)
{
UpdateBrushPositionOnSculptMesh(WorldRay, true);
FVector3d NewBrushPosLocal = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
FVector3d BrushNormalLocal = CurTargetTransform.InverseTransformNormal(LastBrushPosNormalWorld);
FVector3d OffsetBrushPosLocal = NewBrushPosLocal - BrushProperties->Depth * CurrentBrushRadius * BrushNormalLocal;
double Direction = (bInvert) ? -1.0 : 1.0;
double UseSpeed = Direction * FMath::Sqrt(CurrentBrushRadius) * SculptProperties->PrimaryBrushSpeed * 0.025 * ActivePressure; ;
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FKelvinletBrushOp KelvinBrushOp(*Mesh);
FKelvinletBrushOp::FKelvinletBrushOpProperties KelvinletBrushOpProperties(EKelvinletBrushMode::ScaleKelvinlet, *KelvinBrushProperties, CurrentBrushRadius, BrushProperties->BrushFalloffAmount);
KelvinletBrushOpProperties.Direction = FVector(UseSpeed, 0., 0.); // it is a bit iffy, but we only use the first component for the scale
KelvinletBrushOpProperties.Size *= 0.35;
FMatrix ToBrush; ToBrush.SetIdentity(); ToBrush.SetOrigin(-FVector(OffsetBrushPosLocal.X, OffsetBrushPosLocal.Y, OffsetBrushPosLocal.Z)); // ToBrush.
KelvinBrushOp.ApplyBrush(KelvinletBrushOpProperties, ToBrush, VertexROI, ROIPositionBuffer);
ScheduleRemeshPass();
LastBrushPosLocal = NewBrushPosLocal;
return true;
}
bool UDynamicMeshSculptTool::IsHitTriangleBackFacing(int32 TriangleID, const FDynamicMesh3* QueryMesh)
{
if (TriangleID != IndexConstants::InvalidID)
{
FViewCameraState StateOut;
GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(StateOut);
FVector3d LocalEyePosition(CurTargetTransform.InverseTransformPosition((FVector3d)StateOut.Position));
FVector3d Normal, Centroid;
double Area;
QueryMesh->GetTriInfo(TriangleID, Normal, Area, Centroid);
return (Normal.Dot((Centroid - LocalEyePosition)) >= 0);
}
return false;
}
int UDynamicMeshSculptTool::FindHitSculptMeshTriangle(const FRay3d& LocalRay)
{
int32 HitTID = DynamicMeshComponent->GetOctree()->FindNearestHitObject(LocalRay);
if (BrushProperties->bHitBackFaces == false && IsHitTriangleBackFacing(HitTID, DynamicMeshComponent->GetMesh()) )
{
HitTID = IndexConstants::InvalidID;
}
return HitTID;
}
int UDynamicMeshSculptTool::FindHitTargetMeshTriangle(const FRay3d& LocalRay)
{
PendingTargetUpdate.Wait();
int32 HitTID = BrushTargetMeshSpatial.FindNearestHitTriangle(LocalRay);
if (BrushProperties->bHitBackFaces == false && IsHitTriangleBackFacing(HitTID, &BrushTargetMesh))
{
HitTID = IndexConstants::InvalidID;
}
return HitTID;
}
bool UDynamicMeshSculptTool::UpdateBrushPositionOnActivePlane(const FRay& WorldRay)
{
FVector3d NewHitPosWorld;
ActiveDragPlane.RayPlaneIntersection((FVector3d)WorldRay.Origin, (FVector3d)WorldRay.Direction, 2, NewHitPosWorld);
LastBrushPosWorld = NewHitPosWorld;
LastBrushPosNormalWorld = ActiveDragPlane.Z();
return true;
}
bool UDynamicMeshSculptTool::UpdateBrushPositionOnTargetMesh(const FRay& WorldRay, bool bFallbackToViewPlane)
{
PendingTargetUpdate.Wait();
FRay3d LocalRay(CurTargetTransform.InverseTransformPosition((FVector3d)WorldRay.Origin),
CurTargetTransform.InverseTransformVector((FVector3d)WorldRay.Direction));
UE::Geometry::Normalize(LocalRay.Direction);
const FDynamicMesh3* TargetMesh = BrushTargetMeshSpatial.GetMesh();
int HitTID = FindHitTargetMeshTriangle(LocalRay);
if (HitTID != IndexConstants::InvalidID)
{
FTriangle3d Triangle;
TargetMesh->GetTriVertices(HitTID, Triangle.V[0], Triangle.V[1], Triangle.V[2]);
FIntrRay3Triangle3d Query(LocalRay, Triangle);
Query.Find();
LastBrushPosNormalWorld = CurTargetTransform.TransformNormal(TargetMesh->GetTriNormal(HitTID));
LastBrushPosWorld = CurTargetTransform.TransformPosition(LocalRay.PointAt(Query.RayParameter));
return true;
}
if (bFallbackToViewPlane)
{
FFrame3d BrushPlane(LastBrushPosWorld, (FVector3d)CameraState.Forward());
FVector3d NewHitPosWorld;
BrushPlane.RayPlaneIntersection((FVector3d)WorldRay.Origin, (FVector3d)WorldRay.Direction, 2, NewHitPosWorld);
LastBrushPosWorld = NewHitPosWorld;
LastBrushPosNormalWorld = ActiveDragPlane.Z();
return true;
}
return false;
}
bool UDynamicMeshSculptTool::UpdateBrushPositionOnSculptMesh(const FRay& WorldRay, bool bFallbackToViewPlane)
{
FRay3d LocalRay(CurTargetTransform.InverseTransformPosition((FVector3d)WorldRay.Origin),
CurTargetTransform.InverseTransformVector((FVector3d)WorldRay.Direction));
UE::Geometry::Normalize(LocalRay.Direction);
int HitTID = FindHitSculptMeshTriangle(LocalRay);
if (HitTID != IndexConstants::InvalidID)
{
const FDynamicMesh3* SculptMesh = DynamicMeshComponent->GetMesh();
FTriangle3d Triangle;
SculptMesh->GetTriVertices(HitTID, Triangle.V[0], Triangle.V[1], Triangle.V[2]);
FIntrRay3Triangle3d Query(LocalRay, Triangle);
Query.Find();
LastBrushPosNormalWorld = CurTargetTransform.TransformNormal(SculptMesh->GetTriNormal(HitTID));
LastBrushPosWorld = CurTargetTransform.TransformPosition(LocalRay.PointAt(Query.RayParameter));
LastBrushTriangleID = HitTID;
return true;
}
if (bFallbackToViewPlane)
{
FFrame3d BrushPlane(LastBrushPosWorld, (FVector3d)CameraState.Forward());
FVector3d NewHitPosWorld;
BrushPlane.RayPlaneIntersection((FVector3d)WorldRay.Origin, (FVector3d)WorldRay.Direction, 2, NewHitPosWorld);
LastBrushPosWorld = NewHitPosWorld;
LastBrushPosNormalWorld = ActiveDragPlane.Z();
LastBrushTriangleID = -1;
return true;
}
return false;
}
void UDynamicMeshSculptTool::AlignBrushToView()
{
LastBrushPosNormalWorld = -(FVector3d)CameraState.Forward();
}
bool UDynamicMeshSculptTool::UpdateBrushPosition(const FRay& WorldRay)
{
// This is an unfortunate hack necessary because we haven't refactored brushes properly yet
if (bSmoothing)
{
return UpdateBrushPositionOnSculptMesh(WorldRay, false);
}
bool bHit = false;
switch (SculptProperties->PrimaryBrushType)
{
case EDynamicMeshSculptBrushType::Offset:
case EDynamicMeshSculptBrushType::SculptMax:
case EDynamicMeshSculptBrushType::Pinch:
case EDynamicMeshSculptBrushType::Resample:
bHit = UpdateBrushPositionOnTargetMesh(WorldRay, false);
break;
case EDynamicMeshSculptBrushType::SculptView:
case EDynamicMeshSculptBrushType::PlaneViewAligned:
bHit = UpdateBrushPositionOnTargetMesh(WorldRay, false);
AlignBrushToView();
break;
case EDynamicMeshSculptBrushType::Move:
//return UpdateBrushPositionOnActivePlane(WorldRay);
bHit = UpdateBrushPositionOnSculptMesh(WorldRay, false);
break;
case EDynamicMeshSculptBrushType::Smooth:
case EDynamicMeshSculptBrushType::Inflate:
case EDynamicMeshSculptBrushType::Flatten:
case EDynamicMeshSculptBrushType::Plane:
case EDynamicMeshSculptBrushType::FixedPlane:
case EDynamicMeshSculptBrushType::PullSharpKelvin:
case EDynamicMeshSculptBrushType::PullKelvin:
case EDynamicMeshSculptBrushType::TwistKelvin:
case EDynamicMeshSculptBrushType::ScaleKelvin:
bHit = UpdateBrushPositionOnSculptMesh(WorldRay, false);
break;
default:
UE_LOG(LogTemp, Warning, TEXT("UDynamicMeshSculptTool: unknown brush type in UpdateBrushPosition"));
bHit = UpdateBrushPositionOnSculptMesh(WorldRay, false);
break;
}
return bHit;
}
void UDynamicMeshSculptTool::OnEndDrag(const FRay& Ray)
{
bInDrag = false;
// cancel these! otherwise change record could become invalid
bStampPending = false;
bRemeshPending = false;
// update spatial
bTargetDirty = true;
// close change record
EndChange();
// destroy active remesher. Should we do this every stroke?? need to do it on undo/redo...
if (ActiveRemesher)
{
ActiveRemesher = nullptr;
}
}
void UDynamicMeshSculptTool::OnCancelDrag()
{
bInDrag = false;
bStampPending = false;
bRemeshPending = false;
CancelChange();
// destroy active remesher
ActiveRemesher = nullptr;
}
FInputRayHit UDynamicMeshSculptTool::BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos)
{
return UMeshSurfacePointTool::BeginHoverSequenceHitTest(PressPos);
}
bool UDynamicMeshSculptTool::OnUpdateHover(const FInputDeviceRay& DevicePos)
{
// 4.26 HOTFIX: update LastWorldRay position so that we have it for updating WorkPlane position
UMeshSurfacePointTool::LastWorldRay = DevicePos.WorldRay;
PendingStampType = SculptProperties->PrimaryBrushType;
if (bInDrag)
{
FVector3d NewHitPosWorld;
ActiveDragPlane.RayPlaneIntersection((FVector3d)DevicePos.WorldRay.Origin, (FVector3d)DevicePos.WorldRay.Direction, 2, NewHitPosWorld);
LastBrushPosWorld = NewHitPosWorld;
LastBrushPosNormalWorld = ActiveDragPlane.Z();
}
else
{
UpdateBrushPosition(DevicePos.WorldRay);
//FHitResult OutHit;
//if (HitTest(DevicePos.WorldRay, OutHit))
//{
// LastBrushPosWorld = DevicePos.WorldRay.PointAt(OutHit.Distance + BrushProperties->Depth*CurrentBrushRadius);
// LastBrushPosNormalWorld = OutHit.Normal;
//}
}
return true;
}
void UDynamicMeshSculptTool::Render(IToolsContextRenderAPI* RenderAPI)
{
UMeshSurfacePointTool::Render(RenderAPI);
// Cache here for usage during interaction, should probably happen in ::Tick() or elsewhere
GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(CameraState);
FViewCameraState RenderCameraState = RenderAPI->GetCameraState();
constexpr float Falloff = 0.5f;
constexpr float Strength = 1.0f;
BrushIndicator->Update(
(float)this->CurrentBrushRadius,
(FVector)this->LastBrushPosWorld,
(FVector)this->LastBrushPosNormalWorld,
Falloff,
Strength);
if (BrushIndicatorMaterial)
{
double FixedDimScale = ToolSceneQueriesUtil::CalculateDimensionFromVisualAngleD(RenderCameraState, LastBrushPosWorld, 1.5f);
BrushIndicatorMaterial->SetScalarParameterValue(TEXT("FalloffWidth"), FixedDimScale);
if (BrushEditBehavior.IsValid())
{
FLinearColor IndicatorColor;
if (BrushEditBehavior->IsEditing())
{
IndicatorColor = FLinearColor::White;
}
else
{
BrushIndicatorMaterial->GetVectorParameterDefaultValue(TEXT("IndicatorColor"), IndicatorColor);
}
BrushIndicatorMaterial->SetVectorParameterValue(TEXT("IndicatorColor"), IndicatorColor);
}
}
if (SculptProperties->PrimaryBrushType == EDynamicMeshSculptBrushType::FixedPlane)
{
FPrimitiveDrawInterface* PDI = RenderAPI->GetPrimitiveDrawInterface();
FColor GridColor(128, 128, 128, 32);
float GridThickness = 0.5f*RenderCameraState.GetPDIScalingFactor();
int NumGridLines = 10;
FFrame3d DrawFrame(GizmoProperties->Position, GizmoProperties->Rotation);
MeshDebugDraw::DrawSimpleFixedScreenAreaGrid(RenderCameraState, DrawFrame, NumGridLines, 45.0, GridThickness, GridColor, false, PDI, FTransform::Identity);
}
}
void UDynamicMeshSculptTool::DrawHUD(FCanvas* Canvas, IToolsContextRenderAPI* RenderAPI)
{
Super::DrawHUD(Canvas, RenderAPI);
if (BrushEditBehavior.IsValid())
{
BrushEditBehavior->DrawHUD(Canvas, RenderAPI);
}
}
void UDynamicMeshSculptTool::OnTick(float DeltaTime)
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_OnTick);
ActivePressure = GetCurrentDevicePressure();
// Allow a tick to pass between application of brush stamps. If we do not do this then on large stamps
// that take a significant fraction of a second to compute, frames will be skipped and the editor will appear
// frozen, but when the user releases the mouse, sculpting has clearly happened
static int TICK_SKIP_HACK = 0;
if (TICK_SKIP_HACK++ % 2 == 0)
{
return;
}
ShowWireframeWatcher.CheckAndUpdate();
MaterialModeWatcher.CheckAndUpdate();
CustomMaterialWatcher.CheckAndUpdate();
FlatShadingWatcher.CheckAndUpdate();
ColorWatcher.CheckAndUpdate();
TransparentColorWatcher.CheckAndUpdate();
OpacityWatcher.CheckAndUpdate();
TwoSidedWatcher.CheckAndUpdate();
ImageWatcher.CheckAndUpdate();
BrushTypeWatcher.CheckAndUpdate();
GizmoPositionWatcher.CheckAndUpdate();
GizmoRotationWatcher.CheckAndUpdate();
bool bGizmoVisible = (SculptProperties->PrimaryBrushType == EDynamicMeshSculptBrushType::FixedPlane)
&& (GizmoProperties->bShowGizmo);
UpdateFixedPlaneGizmoVisibility(bGizmoVisible);
GizmoProperties->bPropertySetEnabled = (SculptProperties->PrimaryBrushType == EDynamicMeshSculptBrushType::FixedPlane);
if (PendingWorkPlaneUpdate != EPendingWorkPlaneUpdate::NoUpdatePending)
{
// raycast into scene and current sculpt and place plane at closest hit point
FRay CursorWorldRay = UMeshSurfacePointTool::LastWorldRay;
FHitResult Result;
bool bWorldHit = ToolSceneQueriesUtil::FindNearestVisibleObjectHit(this, Result, CursorWorldRay);
FRay3d LocalRay(CurTargetTransform.InverseTransformPosition((FVector3d)CursorWorldRay.Origin),
CurTargetTransform.InverseTransformVector((FVector3d)CursorWorldRay.Direction));
UE::Geometry::Normalize(LocalRay.Direction);
bool bObjectHit = (FindHitSculptMeshTriangle(LocalRay) != IndexConstants::InvalidID);
if (bWorldHit &&
(bObjectHit == false || (CursorWorldRay.GetParameter(Result.ImpactPoint) < CursorWorldRay.GetParameter((FVector)LastBrushPosWorld))))
{
SetFixedSculptPlaneFromWorldPos(Result.ImpactPoint, Result.ImpactNormal, PendingWorkPlaneUpdate);
}
else
{
SetFixedSculptPlaneFromWorldPos((FVector)LastBrushPosWorld, (FVector)LastBrushPosNormalWorld, PendingWorkPlaneUpdate);
}
PendingWorkPlaneUpdate = EPendingWorkPlaneUpdate::NoUpdatePending;
}
// if user changed to not-frozen, we need to update the target
if (bCachedFreezeTarget != SculptProperties->bFreezeTarget)
{
UpdateTarget();
PendingTargetUpdate.Wait();
}
bool bMeshModified = false;
bool bMeshShapeModified = false;
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FDynamicMeshOctree3* Octree = DynamicMeshComponent->GetOctree();
//Octree->CheckValidity(EValidityCheckFailMode::Check, false, true);
//
// Apply stamp
//
bool bROIUpdatePending = false;
bool bOctreeUpdatePending = false;
TFuture<void> InitializeRemesher;
TFuture<void> PrecomputeRemeshROI;
if (bStampPending)
{
// if we don't have an active remesher for this brush stroke, create one
if (ActiveRemesher == nullptr)
{
InitializeRemesher = Async(DynamicSculptToolAsyncExecTarget, [&]()
{
InitializeActiveRemesher();
});
}
// initialize ROI for current brush position
// TODO: does this break move brush??
UpdateBrushPosition(PendingStampRay);
FVector3d BrushPos = CurTargetTransform.InverseTransformPosition(LastBrushPosWorld);
UpdateROI(BrushPos);
// once we know ROI we can speculatively start initializing remesh ROI
PrecomputeRemeshROI = Async(DynamicSculptToolAsyncExecTarget, [&]()
{
// make sure our remesher is initialized
InitializeRemesher.Wait();
// initialize the ROI
PrecomputeRemesherROI();
});
// apply brush stamp to ROI
ApplyStamp(PendingStampRay);
bStampPending = (bInDrag) ? true : false;
bNormalUpdatePending = true;
bROIUpdatePending = true;
bMeshModified = true;
bMeshShapeModified = true;
bOctreeUpdatePending = true;
if (bRemeshPending)
{
check(bInDrag == true); // this would break undo otherwise!
// make sure our remesher is initialized
InitializeRemesher.Wait();
// make sure our ROI is computed
PrecomputeRemeshROI.Wait();
// remesh the ROI (this removes all ROI triangles from octree)
if (ActiveRemesher)
{
RemeshROIPass_ActiveRemesher(true);
}
else
{
check(false); // broken now
RemeshROIPass();
}
// accumulate new triangles into TriangleROI
for (int32 tid : RemeshFinalTriangleROI)
{
TriangleROI.Add(tid);
}
bMeshModified = true;
bMeshShapeModified = true;
bRemeshPending = false;
bNormalUpdatePending = true;
bROIUpdatePending = true;
bOctreeUpdatePending = true;
bHaveRemeshed = true;
}
}
check(bRemeshPending == false); // should never happen...
// launch octree update that inserts/reinserts all triangles in TriangleROI
TFuture<void> UpdateOctreeFuture;
if (bOctreeUpdatePending)
{
// reinsert new ROI into octree
UpdateOctreeFuture = Async(DynamicSculptToolAsyncExecTarget, [&]() {
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_OctreeReinsert);
Octree->ReinsertTriangles(TriangleROI);
//Octree->CheckValidity(EValidityCheckFailMode::Check, false, true);
});
bOctreeUpdatePending = false;
}
//Octree->CheckValidity(EValidityCheckFailMode::Check, false, true);
if (bNormalUpdatePending)
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateNormals);
if (Mesh->HasAttributes() && Mesh->Attributes()->PrimaryNormals() != nullptr)
{
RecalculateNormals_Overlay(TriangleROI);
}
else
{
RecalculateNormals_PerVertex(TriangleROI);
}
bNormalUpdatePending = false;
bMeshModified = true;
}
// next steps need to wait for octree update to finish
UpdateOctreeFuture.Wait();
// launch async target update task
if (bTargetDirty)
{
UpdateTarget();
bTargetDirty = false;
}
// update render data
if (bMeshModified)
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateRenderMesh);
DynamicMeshComponent->NotifyMeshUpdated();
GetToolManager()->PostInvalidation();
bMeshModified = false;
}
// Allow futures to finish in case bRemeshPending == false
InitializeRemesher.Wait();
PrecomputeRemeshROI.Wait();
}
void UDynamicMeshSculptTool::PrecomputeRemeshInfo()
{
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
// check if we have any open boundary edges
bHaveMeshBoundaries = false;
for (int eid : Mesh->EdgeIndicesItr())
{
if (Mesh->IsBoundaryEdge(eid))
{
bHaveMeshBoundaries = true;
break;
}
}
// check if we have any UV seams
bHaveUVSeams = false;
bHaveNormalSeams = false;
if (Mesh->HasAttributes())
{
FDynamicMeshAttributeSet* Attribs = Mesh->Attributes();
for (int k = 0; k < Attribs->NumUVLayers(); ++k)
{
bHaveUVSeams = bHaveUVSeams || Attribs->GetUVLayer(k)->HasInteriorSeamEdges();
}
bHaveNormalSeams = Attribs->PrimaryNormals()->HasInteriorSeamEdges();
}
}
void UDynamicMeshSculptTool::ScheduleRemeshPass()
{
if (bEnableRemeshing && RemeshProperties != nullptr && RemeshProperties->bEnableRemeshing)
{
bRemeshPending = true;
}
}
/**
* This is an internal class we will use to just hold onto a FSubRegionRemesher instance
* between brush stamps. Perhaps does not need to exist.
*/
class FPersistentStampRemesher
{
public:
FDynamicMesh3* Mesh;
TUniquePtr<FSubRegionRemesher> Remesher;
FPersistentStampRemesher(FDynamicMesh3* MeshIn)
{
this->Mesh = MeshIn;
Remesher = MakeUnique<FSubRegionRemesher>(MeshIn);
}
};
/*
Split Collapse Vertices Pinned Flip
Fixed FALSE FALSE TRUE FALSE
Refine TRUE FALSE TRUE FALSE
Free TRUE TRUE FALSE FALSE
Ignore TRUE TRUE FALSE TRUE
*/
void UDynamicMeshSculptTool::ConfigureRemesher(FSubRegionRemesher& Remesher)
{
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
const double SizeRange = 5;
double LengthMultiplier = (RemeshProperties->TriangleSize >= 0) ?
FMathd::Lerp(1.0, 5.0, FMathd::Pow( (double)RemeshProperties->TriangleSize / SizeRange, 2.0) )
: FMathd::Lerp(0.25, 1.0, 1.0 - FMathd::Pow(FMathd::Abs((double)RemeshProperties->TriangleSize) / SizeRange, 2.0) );
double TargetEdgeLength = LengthMultiplier * InitialEdgeLength;
Remesher.SetTargetEdgeLength(TargetEdgeLength);
double DetailT = (double)(RemeshProperties->PreserveDetail) / 5.0;
double UseSmoothing = RemeshProperties->SmoothingStrength * 0.25;
UseSmoothing *= FMathd::Lerp(1.0, 0.25, DetailT);
Remesher.SmoothSpeedT = UseSmoothing;
// this is a temporary tweak for Pinch brush. Remesh params should be per-brush!
if (SculptProperties->PrimaryBrushType == EDynamicMeshSculptBrushType::Pinch && bSmoothing == false)
{
Remesher.MinEdgeLength = TargetEdgeLength * 0.1;
Remesher.CustomSmoothSpeedF = [this, UseSmoothing](const FDynamicMesh3& Mesh, int vID)
{
FVector3d Pos = Mesh.GetVertex(vID);
double Falloff = CalculateBrushFalloff(Distance(Pos, (FVector3d)LastBrushPosLocal));
return (1.0f - Falloff) * UseSmoothing;
};
}
else if (bSmoothing && SculptProperties->bDetailPreservingSmooth)
{
// this is the case where we don't want remeshing in smoothing
Remesher.MaxEdgeLength = 3 * InitialEdgeLength;
Remesher.MinEdgeLength = InitialEdgeLength * 0.05;
}
else
{
if (RemeshProperties->PreserveDetail > 0)
{
Remesher.MinEdgeLength *= FMathd::Lerp(1.0, 0.1, DetailT);
Remesher.CustomSmoothSpeedF = [this, UseSmoothing, DetailT](const FDynamicMesh3& Mesh, int vID)
{
FVector3d Pos = Mesh.GetVertex(vID);
double FalloffT = 1.0 - CalculateBrushFalloff(Distance(Pos, (FVector3d)LastBrushPosLocal));
FalloffT = FMathd::Lerp(1.0, FalloffT, DetailT);
return FalloffT * UseSmoothing;
};
}
}
if (SculptProperties->bPreserveUVFlow)
{
Remesher.SmoothType = FRemesher::ESmoothTypes::MeanValue;
Remesher.FlipMetric = FRemesher::EFlipMetric::MinEdgeLength;
}
else
{
Remesher.SmoothType = FRemesher::ESmoothTypes::Uniform;
Remesher.FlipMetric = FRemesher::EFlipMetric::OptimalValence;
}
Remesher.bEnableCollapses = RemeshProperties->bCollapses;
Remesher.bEnableFlips = RemeshProperties->bFlips;
Remesher.bEnableSplits = RemeshProperties->bSplits;
Remesher.bPreventNormalFlips = RemeshProperties->bPreventNormalFlips;
FMeshConstraints Constraints;
bool bConstraintAllowSplits = true;
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_Configure_Constraints);
FMeshConstraintsUtil::ConstrainAllBoundariesAndSeams(Constraints, *Mesh,
(EEdgeRefineFlags)RemeshProperties->MeshBoundaryConstraint,
(EEdgeRefineFlags)RemeshProperties->GroupBoundaryConstraint,
(EEdgeRefineFlags)RemeshProperties->MaterialBoundaryConstraint,
bConstraintAllowSplits, !RemeshProperties->bPreserveSharpEdges);
Remesher.SetExternalConstraints(MoveTemp(Constraints));
}
}
void UDynamicMeshSculptTool::InitializeRemesherROI(FSubRegionRemesher& Remesher)
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_Configure_InitializeROI);
Remesher.SetInitialVertexROI(this->VertexROI);
Remesher.InitializeFromVertexROI();
}
void UDynamicMeshSculptTool::InitializeActiveRemesher()
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_InitializeActiveRemesher);
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
ActiveRemesher = MakeShared<FPersistentStampRemesher>(Mesh);
ConfigureRemesher(* ActiveRemesher->Remesher);
}
void UDynamicMeshSculptTool::PrecomputeRemesherROI()
{
FSubRegionRemesher& Remesher = *ActiveRemesher->Remesher;
Remesher.Reset();
InitializeRemesherROI(Remesher);
}
void UDynamicMeshSculptTool::RemeshROIPass_ActiveRemesher(bool bHasPrecomputedROI)
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROIActive);
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FDynamicMeshOctree3* Octree = DynamicMeshComponent->GetOctree();
FSubRegionRemesher& Remesher = *ActiveRemesher->Remesher;
if (bHasPrecomputedROI == false)
{
PrecomputeRemesherROI();
}
// remove initial triangles
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_CopyROIStep);
RemeshRemovedTriangles = Remesher.GetCurrentTriangleROI();
}
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_OctreeRemoveStep);
Octree->RemoveTriangles(RemeshRemovedTriangles, true);
}
if (ActiveMeshChange != nullptr)
{
Remesher.SetMeshChangeTracker(ActiveMeshChange);
}
bool bIsUniformSmooth = (Remesher.SmoothType == FRemesher::ESmoothTypes::Uniform);
for (int k = 0; k < RemeshProperties->Iterations; ++k)
{
if ((bIsUniformSmooth == false) && (k > 1))
{
Remesher.bEnableFlips = false;
}
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_UpdateStep);
Remesher.UpdateROI();
if (ActiveMeshChange != nullptr)
{
// [TODO] would like to only save vertices here, as triangles will be saved by Remesher as necessary.
// However currently FDynamicMeshChangeTracker cannot independently save vertices, only vertices
// that are part of saved triangles will be included in the output FDynamicMeshChange
Remesher.SaveActiveROI(ActiveMeshChange);
//ActiveMeshChange->VerifySaveState(); // useful for debugging
}
Remesher.BeginTrackRemovedTrisInPass();
}
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_RemeshStep);
Remesher.BasicRemeshPass();
}
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_OctreeStep);
const TSet<int32>& TrisRemovedInPass = Remesher.EndTrackRemovedTrisInPass();
Octree->RemoveTriangles(TrisRemovedInPass);
for (int32 tid : TrisRemovedInPass)
{
RemeshRemovedTriangles.Add(tid);
}
}
}
//UE_LOG(LogTemp, Warning, TEXT("Triangle Count %d after update"), Mesh->TriangleCount());
RemeshFinalTriangleROI = Remesher.ExtractFinalTriangleROI();
}
void UDynamicMeshSculptTool::RemeshROIPass()
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI);
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FDynamicMeshOctree3* Octree = DynamicMeshComponent->GetOctree();
FSubRegionRemesher Remesher(Mesh);
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_Configure);
ConfigureRemesher(Remesher);
InitializeRemesherROI(Remesher);
}
// remove initial triangles
RemeshRemovedTriangles = Remesher.GetCurrentTriangleROI();
Octree->RemoveTriangles(RemeshRemovedTriangles);
if (ActiveMeshChange != nullptr)
{
Remesher.SetMeshChangeTracker(ActiveMeshChange);
}
bool bIsUniformSmooth = (Remesher.SmoothType == FRemesher::ESmoothTypes::Uniform);
for (int k = 0; k < 5; ++k)
{
if ( ( bIsUniformSmooth == false ) && ( k > 1 ) )
{
Remesher.bEnableFlips = false;
}
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_UpdateStep);
Remesher.UpdateROI();
if (ActiveMeshChange != nullptr)
{
// [TODO] would like to only save vertices here, as triangles will be saved by Remesher as necessary.
// However currently FDynamicMeshChangeTracker cannot independently save vertices, only vertices
// that are part of saved triangles will be included in the output FDynamicMeshChange
Remesher.SaveActiveROI(ActiveMeshChange);
//ActiveMeshChange->VerifySaveState(); // useful for debugging
}
Remesher.BeginTrackRemovedTrisInPass();
}
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_RemeshROI_RemeshStep);
Remesher.BasicRemeshPass();
}
{
const TSet<int32>& TrisRemovedInPass = Remesher.EndTrackRemovedTrisInPass();
Octree->RemoveTriangles(TrisRemovedInPass);
for (int32 tid : TrisRemovedInPass)
{
RemeshRemovedTriangles.Add(tid);
}
}
}
//UE_LOG(LogTemp, Warning, TEXT("Triangle Count %d after update"), Mesh->TriangleCount());
RemeshFinalTriangleROI = Remesher.ExtractFinalTriangleROI();
}
void UDynamicMeshSculptTool::RecalculateNormals_PerVertex(const TSet<int32>& Triangles)
{
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
int MaxVertexID = Mesh->MaxVertexID();
if (NormalsVertexFlags.Num() < MaxVertexID)
{
NormalsVertexFlags.Init(false, MaxVertexID * 2);
}
{
NormalsBuffer.Reset();
for (int TriangleID : Triangles)
{
if (Mesh->IsTriangle(TriangleID))
{
FIndex3i TriV = Mesh->GetTriangle(TriangleID);
for (int j = 0; j < 3; ++j)
{
int vid = TriV[j];
if (NormalsVertexFlags[vid] == false)
{
NormalsBuffer.Add(vid);
NormalsVertexFlags[vid] = true;
}
}
}
}
}
{
ParallelFor(NormalsBuffer.Num(), [&](int k) {
int vid = NormalsBuffer[k];
FVector3d NewNormal = FMeshNormals::ComputeVertexNormal(*Mesh, vid);
Mesh->SetVertexNormal(vid, (FVector3f)NewNormal);
NormalsVertexFlags[vid] = false;
});
}
}
void UDynamicMeshSculptTool::RecalculateNormals_Overlay(const TSet<int32>& Triangles)
{
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
FDynamicMeshNormalOverlay* Normals = Mesh->HasAttributes() ? Mesh->Attributes()->PrimaryNormals() : nullptr;
check(Normals != nullptr);
int MaxElementID = Normals->MaxElementID();
if (NormalsVertexFlags.Num() < MaxElementID)
{
NormalsVertexFlags.Init(false, MaxElementID * 2);
}
{
NormalsBuffer.Reset();
for (int TriangleID : Triangles)
{
if (Mesh->IsTriangle(TriangleID))
{
FIndex3i TriElems = Normals->GetTriangle(TriangleID);
if (TriElems.A == FDynamicMesh3::InvalidID)
{
continue;
}
for (int j = 0; j < 3; ++j)
{
int elemid = TriElems[j];
if (NormalsVertexFlags[elemid] == false)
{
NormalsBuffer.Add(elemid);
NormalsVertexFlags[elemid] = true;
}
}
}
}
}
{
ParallelFor(NormalsBuffer.Num(), [&](int k) {
int elemid = NormalsBuffer[k];
FVector3d NewNormal = FMeshNormals::ComputeOverlayNormal(*Mesh, Normals, elemid);
Normals->SetElement(elemid, (FVector3f)NewNormal);
NormalsVertexFlags[elemid] = false;
});
}
}
void UDynamicMeshSculptTool::UpdateTarget()
{
if (SculptProperties != nullptr )
{
bCachedFreezeTarget = SculptProperties->bFreezeTarget;
if (SculptProperties->bFreezeTarget)
{
return; // do not update frozen target
}
}
// Allow any in-progress update to finish before starting a new one
PendingTargetUpdate.Wait();
// TODO: could have a second set of target meshes, and swap between them while we update the other one
PendingTargetUpdate = Async(DynamicSculptToolAsyncExecTarget, [this]()
{
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateTarget);
BrushTargetMesh.Copy(*DynamicMeshComponent->GetMesh(), false, false, false, false);
TFuture<void> TargetSpatialUpdate = Async(DynamicSculptToolAsyncExecTarget, [this]() {
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateTarget_Spatial);
BrushTargetMeshSpatial.SetMesh(&BrushTargetMesh, true);
});
TFuture<void> TargetNormalsUpdate = Async(DynamicSculptToolAsyncExecTarget, [this]() {
TRACE_CPUPROFILER_EVENT_SCOPE(DynamicMeshSculptTool_UpdateTarget_Normals);
BrushTargetNormals.SetMesh(&BrushTargetMesh);
BrushTargetNormals.ComputeVertexNormals();
});
TargetSpatialUpdate.Wait();
TargetNormalsUpdate.Wait();
});
}
bool UDynamicMeshSculptTool::GetTargetMeshNearest(const FVector3d& Position, double SearchRadius, FVector3d& TargetPosOut, FVector3d& TargetNormalOut)
{
PendingTargetUpdate.Wait();
double fDistSqr;
int NearTID = BrushTargetMeshSpatial.FindNearestTriangle(Position, fDistSqr, SearchRadius);
if (NearTID <= 0)
{
return false;
}
FTriangle3d Triangle;
BrushTargetMesh.GetTriVertices(NearTID, Triangle.V[0], Triangle.V[1], Triangle.V[2]);
FDistPoint3Triangle3d Query(Position, Triangle);
Query.Get();
FIndex3i Tri = BrushTargetMesh.GetTriangle(NearTID);
TargetNormalOut =
Query.TriangleBaryCoords.X*BrushTargetNormals[Tri.A]
+ Query.TriangleBaryCoords.Y*BrushTargetNormals[Tri.B]
+ Query.TriangleBaryCoords.Z*BrushTargetNormals[Tri.C];
UE::Geometry::Normalize(TargetNormalOut);
TargetPosOut = Query.ClosestTrianglePoint;
return true;
}
double UDynamicMeshSculptTool::EstimateIntialSafeTargetLength(const FDynamicMesh3& Mesh, int MinTargetTriCount)
{
double AreaSum = 0;
for (int tid : Mesh.TriangleIndicesItr())
{
AreaSum += Mesh.GetTriArea(tid);
}
int TriCount = Mesh.TriangleCount();
double TargetTriArea = 1.0;
if (TriCount < MinTargetTriCount)
{
TargetTriArea = AreaSum / (double)MinTargetTriCount;
}
else
{
TargetTriArea = AreaSum / (double)TriCount;
}
double EdgeLen = TriangleUtil::EquilateralEdgeLengthForArea(TargetTriArea);
return (double)FMath::RoundToInt(EdgeLen*100.0) / 100.0;
}
UPreviewMesh* UDynamicMeshSculptTool::MakeDefaultSphereMesh(UObject* Parent, UWorld* World, int Resolution /*= 32*/)
{
UPreviewMesh* SphereMesh = NewObject<UPreviewMesh>(Parent);
SphereMesh->CreateInWorld(World, FTransform::Identity);
FSphereGenerator SphereGen;
SphereGen.NumPhi = SphereGen.NumTheta = Resolution;
SphereGen.Generate();
FDynamicMesh3 Mesh(&SphereGen);
SphereMesh->UpdatePreview(&Mesh);
BrushIndicatorMaterial = ToolSetupUtil::GetDefaultBrushVolumeMaterial(GetToolManager());
if (BrushIndicatorMaterial)
{
SphereMesh->SetMaterial(BrushIndicatorMaterial);
}
// make sure raytracing is disabled on the brush indicator
Cast<UDynamicMeshComponent>(SphereMesh->GetRootComponent())->SetEnableRaytracing(false);
SphereMesh->SetShadowsEnabled(false);
return SphereMesh;
}
void UDynamicMeshSculptTool::IncreaseBrushRadiusAction()
{
BrushProperties->BrushSize.IncreaseRadius(false);
NotifyOfPropertyChangeByTool(BrushProperties);
CalculateBrushRadius();
}
void UDynamicMeshSculptTool::DecreaseBrushRadiusAction()
{
BrushProperties->BrushSize.DecreaseRadius(false);
NotifyOfPropertyChangeByTool(BrushProperties);
CalculateBrushRadius();
}
void UDynamicMeshSculptTool::IncreaseBrushRadiusSmallStepAction()
{
BrushProperties->BrushSize.IncreaseRadius(true);
NotifyOfPropertyChangeByTool(BrushProperties);
CalculateBrushRadius();
}
void UDynamicMeshSculptTool::DecreaseBrushRadiusSmallStepAction()
{
BrushProperties->BrushSize.DecreaseRadius(true);
NotifyOfPropertyChangeByTool(BrushProperties);
CalculateBrushRadius();
}
void UDynamicMeshSculptTool::IncreaseBrushSpeedAction()
{
SculptProperties->PrimaryBrushSpeed = FMath::Clamp(SculptProperties->PrimaryBrushSpeed + 0.05f, 0.0f, 1.0f);
NotifyOfPropertyChangeByTool(SculptProperties);
}
void UDynamicMeshSculptTool::DecreaseBrushSpeedAction()
{
SculptProperties->PrimaryBrushSpeed = FMath::Clamp(SculptProperties->PrimaryBrushSpeed - 0.05f, 0.0f, 1.0f);
NotifyOfPropertyChangeByTool(SculptProperties);
}
void UDynamicMeshSculptTool::NextHistoryBrushModeAction()
{
int MaxHistory = BrushTypeHistory.Num() - 1;
if (BrushTypeHistoryIndex < MaxHistory)
{
BrushTypeHistoryIndex++;
SculptProperties->PrimaryBrushType = BrushTypeHistory[BrushTypeHistoryIndex];
LastStampType = SculptProperties->PrimaryBrushType;
}
}
void UDynamicMeshSculptTool::PreviousHistoryBrushModeAction()
{
if (BrushTypeHistoryIndex > 0)
{
BrushTypeHistoryIndex--;
SculptProperties->PrimaryBrushType = BrushTypeHistory[BrushTypeHistoryIndex];
LastStampType = SculptProperties->PrimaryBrushType;
}
}
void UDynamicMeshSculptTool::RegisterActions(FInteractiveToolActionSet& ActionSet)
{
ActionSet.RegisterAction(this, (int32)EStandardToolActions::IncreaseBrushSize,
TEXT("SculptIncreaseRadius"),
LOCTEXT("SculptIncreaseRadius", "Increase Sculpt Radius"),
LOCTEXT("SculptIncreaseRadiusTooltip", "Increase radius of sculpting brush"),
EModifierKey::None, EKeys::RightBracket,
[this]() { IncreaseBrushRadiusAction(); } );
ActionSet.RegisterAction(this, (int32)EStandardToolActions::DecreaseBrushSize,
TEXT("SculptDecreaseRadius"),
LOCTEXT("SculptDecreaseRadius", "Decrease Sculpt Radius"),
LOCTEXT("SculptDecreaseRadiusTooltip", "Decrease radius of sculpting brush"),
EModifierKey::None, EKeys::LeftBracket,
[this]() { DecreaseBrushRadiusAction(); } );
//ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 10,
// TEXT("NextBrushHistoryState"),
// LOCTEXT("SculptNextBrushHistoryState", "Next Brush History State"),
// LOCTEXT("SculptSculptNextBrushHistoryStateTooltip", "Cycle to next Brush History state"),
// EModifierKey::Shift, EKeys::Q,
// [this]() { NextHistoryBrushModeAction(); });
//ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 11,
// TEXT("PreviousBrushHistoryState"),
// LOCTEXT("SculptPreviousBrushHistoryState", "Previous Brush History State"),
// LOCTEXT("SculptPreviousBrushHistoryStateTooltip", "Cycle to previous Brush History state"),
// EModifierKey::Shift, EKeys::A,
// [this]() { PreviousHistoryBrushModeAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 50,
TEXT("SculptIncreaseSize"),
LOCTEXT("SculptIncreaseSize", "Increase Size"),
LOCTEXT("SculptIncreaseSizeTooltip", "Increase Brush Size"),
EModifierKey::None, EKeys::D,
[this]() { IncreaseBrushRadiusAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 51,
TEXT("SculptDecreaseSize"),
LOCTEXT("SculptDecreaseSize", "Decrease Size"),
LOCTEXT("SculptDecreaseSizeTooltip", "Decrease Brush Size"),
EModifierKey::None, EKeys::S,
[this]() { DecreaseBrushRadiusAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 52,
TEXT("SculptIncreaseSizeSmallStep"),
LOCTEXT("SculptIncreaseSize", "Increase Size"),
LOCTEXT("SculptIncreaseSizeTooltip", "Increase Brush Size"),
EModifierKey::Shift, EKeys::D,
[this]() { IncreaseBrushRadiusSmallStepAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 53,
TEXT("SculptDecreaseSizeSmallStemp"),
LOCTEXT("SculptDecreaseSize", "Decrease Size"),
LOCTEXT("SculptDecreaseSizeTooltip", "Decrease Brush Size"),
EModifierKey::Shift, EKeys::S,
[this]() { DecreaseBrushRadiusSmallStepAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 60,
TEXT("SculptIncreaseSpeed"),
LOCTEXT("SculptIncreaseSpeed", "Increase Speed"),
LOCTEXT("SculptIncreaseSpeedTooltip", "Increase Brush Speed"),
EModifierKey::None, EKeys::E,
[this]() { IncreaseBrushSpeedAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 61,
TEXT("SculptDecreaseSpeed"),
LOCTEXT("SculptDecreaseSpeed", "Decrease Speed"),
LOCTEXT("SculptDecreaseSpeedTooltip", "Decrease Brush Speed"),
EModifierKey::None, EKeys::W,
[this]() { DecreaseBrushSpeedAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::ToggleWireframe,
TEXT("ToggleWireframe"),
LOCTEXT("ToggleWireframe", "Toggle Wireframe"),
LOCTEXT("ToggleWireframeTooltip", "Toggle visibility of wireframe overlay"),
EModifierKey::Alt, EKeys::W,
[this]() { ViewProperties->bShowWireframe = !ViewProperties->bShowWireframe; });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 100,
TEXT("SetSculptWorkSurfacePosNormal"),
LOCTEXT("SetSculptWorkSurfacePosNormal", "Reorient Work Surface"),
LOCTEXT("SetSculptWorkSurfacePosNormalTooltip", "Move the Sculpting Work Plane/Surface to Position and Normal of World hit point under cursor"),
EModifierKey::Shift, EKeys::T,
[this]() { PendingWorkPlaneUpdate = EPendingWorkPlaneUpdate::MoveToHitPositionNormal; });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 101,
TEXT("SetSculptWorkSurfacePos"),
LOCTEXT("SetSculptWorkSurfacePos", "Reposition Work Surface"),
LOCTEXT("SetSculptWorkSurfacePosTooltip", "Move the Sculpting Work Plane/Surface to World hit point under cursor (keep current Orientation)"),
EModifierKey::None, EKeys::T,
[this]() { PendingWorkPlaneUpdate = EPendingWorkPlaneUpdate::MoveToHitPosition; });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 102,
TEXT("SetSculptWorkSurfaceView"),
LOCTEXT("SetSculptWorkSurfaceView", "View-Align Work Surface"),
LOCTEXT("SetSculptWorkSurfaceViewTooltip", "Move the Sculpting Work Plane/Surface to World hit point under cursor and align to View"),
EModifierKey::Control | EModifierKey::Shift, EKeys::T,
[this]() { PendingWorkPlaneUpdate = EPendingWorkPlaneUpdate::MoveToHitPositionViewAligned; });
}
//
// Change Tracking
//
void UDynamicMeshSculptTool::BeginChange(bool bIsVertexChange)
{
check(ActiveVertexChange == nullptr);
check(ActiveMeshChange == nullptr);
LongTransactions.Open(LOCTEXT("MeshSculptChange", "Brush Stroke"), GetToolManager()); // Open brush stroke transaction to prevent undo during the operation
if (bIsVertexChange)
{
ActiveVertexChange = new FMeshVertexChangeBuilder();
}
else
{
ActiveMeshChange = new FDynamicMeshChangeTracker(DynamicMeshComponent->GetMesh());
ActiveMeshChange->BeginChange();
}
}
void UDynamicMeshSculptTool::EndChange()
{
if (ActiveVertexChange != nullptr)
{
GetToolManager()->EmitObjectChange(DynamicMeshComponent, MoveTemp(ActiveVertexChange->Change), LOCTEXT("MeshSculptChange", "Brush Stroke"));
delete ActiveVertexChange;
ActiveVertexChange = nullptr;
}
if (ActiveMeshChange != nullptr)
{
FMeshChange* NewMeshChange = new FMeshChange();
NewMeshChange->DynamicMeshChange = ActiveMeshChange->EndChange();
//NewMeshChange->DynamicMeshChange->CheckValidity();
TUniquePtr<FMeshChange> NewChange(NewMeshChange);
GetToolManager()->EmitObjectChange(DynamicMeshComponent, MoveTemp(NewChange), LOCTEXT("MeshSculptChange", "Brush Stroke"));
delete ActiveMeshChange;
ActiveMeshChange = nullptr;
}
LongTransactions.Close(GetToolManager());
}
void UDynamicMeshSculptTool::CancelChange()
{
LongTransactions.Close(GetToolManager());
delete ActiveVertexChange;
ActiveVertexChange = nullptr;
delete ActiveMeshChange;
ActiveMeshChange = nullptr;
}
void UDynamicMeshSculptTool::SaveActiveROI()
{
if (ActiveMeshChange != nullptr)
{
// must save triangles containing vertex ROI or they will not be included in emitted mesh change (due to limitations of change tracker)
ActiveMeshChange->SaveVertexOneRingTriangles(VertexROI, true);
}
}
void UDynamicMeshSculptTool::UpdateMaterialMode(EMeshEditingMaterialModes MaterialMode)
{
if (MaterialMode == EMeshEditingMaterialModes::ExistingMaterial)
{
DynamicMeshComponent->ClearOverrideRenderMaterial();
DynamicMeshComponent->SetShadowsEnabled(UE::ToolTarget::GetTargetComponent(Target)->bCastDynamicShadow);
ActiveOverrideMaterial = nullptr;
}
else
{
if (MaterialMode == EMeshEditingMaterialModes::Custom)
{
if (ViewProperties->CustomMaterial.IsValid())
{
ActiveOverrideMaterial = UMaterialInstanceDynamic::Create(ViewProperties->CustomMaterial.Get(), this);
}
else
{
DynamicMeshComponent->ClearOverrideRenderMaterial();
ActiveOverrideMaterial = nullptr;
}
}
else if (MaterialMode == EMeshEditingMaterialModes::CustomImage)
{
ActiveOverrideMaterial = ToolSetupUtil::GetCustomImageBasedSculptMaterial(GetToolManager(), ViewProperties->Image);
if (ViewProperties->Image != nullptr)
{
ActiveOverrideMaterial->SetTextureParameterValue(TEXT("ImageTexture"), ViewProperties->Image);
}
}
else if (MaterialMode == EMeshEditingMaterialModes::Transparent)
{
ActiveOverrideMaterial = ToolSetupUtil::GetTransparentSculptMaterial(GetToolManager(),
ViewProperties->TransparentMaterialColor, ViewProperties->Opacity, ViewProperties->bTwoSided);
}
else
{
UMaterialInterface* SculptMaterial = nullptr;
switch (MaterialMode)
{
case EMeshEditingMaterialModes::Diffuse:
SculptMaterial = ToolSetupUtil::GetDefaultSculptMaterial(GetToolManager());
break;
case EMeshEditingMaterialModes::Grey:
SculptMaterial = ToolSetupUtil::GetImageBasedSculptMaterial(GetToolManager(), ToolSetupUtil::ImageMaterialType::DefaultBasic);
break;
case EMeshEditingMaterialModes::Soft:
SculptMaterial = ToolSetupUtil::GetImageBasedSculptMaterial(GetToolManager(), ToolSetupUtil::ImageMaterialType::DefaultSoft);
break;
case EMeshEditingMaterialModes::TangentNormal:
SculptMaterial = ToolSetupUtil::GetImageBasedSculptMaterial(GetToolManager(), ToolSetupUtil::ImageMaterialType::TangentNormalFromView);
break;
case EMeshEditingMaterialModes::VertexColor:
SculptMaterial = ToolSetupUtil::GetVertexColorMaterial(GetToolManager());
break;
}
if (SculptMaterial != nullptr)
{
ActiveOverrideMaterial = UMaterialInstanceDynamic::Create(SculptMaterial, this);
}
}
if (ActiveOverrideMaterial != nullptr)
{
DynamicMeshComponent->SetOverrideRenderMaterial(ActiveOverrideMaterial);
ActiveOverrideMaterial->SetScalarParameterValue(TEXT("FlatShading"), (ViewProperties->bFlatShading) ? 1.0f : 0.0f);
}
DynamicMeshComponent->SetShadowsEnabled(false);
}
}
void UDynamicMeshSculptTool::UpdateFlatShadingSetting(bool bNewValue)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetScalarParameterValue(TEXT("FlatShading"), (bNewValue) ? 1.0f : 0.0f);
}
}
void UDynamicMeshSculptTool::UpdateColorSetting(FLinearColor NewColor)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetVectorParameterValue(TEXT("Color"), NewColor);
}
}
void UDynamicMeshSculptTool::UpdateOpacitySetting(double Opacity)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetScalarParameterValue(TEXT("Opacity"), Opacity);
}
}
void UDynamicMeshSculptTool::UpdateTwoSidedSetting(bool bOn)
{
ActiveOverrideMaterial = ToolSetupUtil::GetTransparentSculptMaterial(GetToolManager(),
ViewProperties->TransparentMaterialColor, ViewProperties->Opacity, bOn);
if (ActiveOverrideMaterial)
{
DynamicMeshComponent->SetOverrideRenderMaterial(ActiveOverrideMaterial);
}
}
void UDynamicMeshSculptTool::UpdateCustomMaterial(TWeakObjectPtr<UMaterialInterface> NewMaterial)
{
if (ViewProperties->MaterialMode == EMeshEditingMaterialModes::Custom)
{
if (NewMaterial.IsValid())
{
ActiveOverrideMaterial = UMaterialInstanceDynamic::Create(NewMaterial.Get(), this);
DynamicMeshComponent->SetOverrideRenderMaterial(ActiveOverrideMaterial);
}
else
{
DynamicMeshComponent->ClearOverrideRenderMaterial();
ActiveOverrideMaterial = nullptr;
}
}
}
void UDynamicMeshSculptTool::UpdateImageSetting(UTexture2D* NewImage)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetTextureParameterValue(TEXT("ImageTexture"), NewImage);
}
}
void UDynamicMeshSculptTool::UpdateBrushType(EDynamicMeshSculptBrushType BrushType)
{
static const FText BaseMessage = LOCTEXT("OnStartSculptTool", "Hold Shift to Smooth, Ctrl to Invert (where applicable). [/] and S/D change Size (+Shift to small-step), W/E changes Strength.");
FTextBuilder Builder;
Builder.AppendLine(BaseMessage);
SetToolPropertySourceEnabled(GizmoProperties, false);
SetToolPropertySourceEnabled(SculptMaxBrushProperties, false);
if (BrushType == EDynamicMeshSculptBrushType::FixedPlane)
{
Builder.AppendLine(LOCTEXT("FixedPlaneTip", "Use T to reposition Work Plane at cursor, Shift+T to align to Normal, Ctrl+Shift+T to align to View"));
SetToolPropertySourceEnabled(GizmoProperties, true);
}
if (BrushType == EDynamicMeshSculptBrushType::SculptMax)
{
SetToolPropertySourceEnabled(SculptMaxBrushProperties, true);
}
GetToolManager()->DisplayMessage(Builder.ToText(), EToolMessageLevel::UserNotification);
}
void UDynamicMeshSculptTool::SetFixedSculptPlaneFromWorldPos(const FVector& Position, const FVector& Normal, EPendingWorkPlaneUpdate UpdateType)
{
if (UpdateType == EPendingWorkPlaneUpdate::MoveToHitPositionNormal)
{
UpdateFixedSculptPlanePosition(Position);
FFrame3d CurFrame(FVector::ZeroVector, GizmoProperties->Rotation);
CurFrame.AlignAxis(2, (FVector3d)Normal );
UpdateFixedSculptPlaneRotation( (FQuat)CurFrame.Rotation );
}
else if (UpdateType == EPendingWorkPlaneUpdate::MoveToHitPositionViewAligned)
{
UpdateFixedSculptPlanePosition(Position);
FFrame3d CurFrame(FVector::ZeroVector, GizmoProperties->Rotation);
CurFrame.AlignAxis(2, -(FVector3d)CameraState.Forward());
UpdateFixedSculptPlaneRotation((FQuat)CurFrame.Rotation);
}
else
{
UpdateFixedSculptPlanePosition(Position);
}
if (PlaneTransformGizmo != nullptr)
{
PlaneTransformGizmo->SetNewGizmoTransform(FTransform(GizmoProperties->Rotation, GizmoProperties->Position));
}
}
void UDynamicMeshSculptTool::PlaneTransformChanged(UTransformProxy* Proxy, FTransform Transform)
{
UpdateFixedSculptPlaneRotation(Transform.GetRotation());
UpdateFixedSculptPlanePosition(Transform.GetLocation());
}
void UDynamicMeshSculptTool::UpdateFixedSculptPlanePosition(const FVector& Position)
{
GizmoProperties->Position = Position;
GizmoPositionWatcher.SilentUpdate();
}
void UDynamicMeshSculptTool::UpdateFixedSculptPlaneRotation(const FQuat& Rotation)
{
GizmoProperties->Rotation = Rotation;
GizmoRotationWatcher.SilentUpdate();
}
void UDynamicMeshSculptTool::UpdateGizmoFromProperties()
{
if (PlaneTransformGizmo != nullptr)
{
PlaneTransformGizmo->SetNewGizmoTransform(FTransform(GizmoProperties->Rotation, GizmoProperties->Position));
}
}
void UDynamicMeshSculptTool::UpdateFixedPlaneGizmoVisibility(bool bVisible)
{
if (bVisible == false)
{
if (PlaneTransformGizmo != nullptr)
{
GetToolManager()->GetPairedGizmoManager()->DestroyGizmo(PlaneTransformGizmo);
PlaneTransformGizmo = nullptr;
}
}
else
{
if (PlaneTransformGizmo == nullptr)
{
PlaneTransformGizmo = UE::TransformGizmoUtil::CreateCustomTransformGizmo(GetToolManager(),
ETransformGizmoSubElements::StandardTranslateRotate, this);
PlaneTransformGizmo->bUseContextCoordinateSystem = false;
PlaneTransformGizmo->CurrentCoordinateSystem = EToolContextCoordinateSystem::Local;
PlaneTransformGizmo->SetActiveTarget(PlaneTransformProxy, GetToolManager());
PlaneTransformGizmo->ReinitializeGizmoTransform(FTransform(GizmoProperties->Rotation, GizmoProperties->Position));
}
}
}
void UDynamicMeshSculptTool::DiscardAttributes()
{
TSharedPtr<FDynamicMesh3, ESPMode::ThreadSafe> BeforeMesh = MakeShared<FDynamicMesh3, ESPMode::ThreadSafe>(*DynamicMeshComponent->GetMesh());
TSharedPtr<FDynamicMesh3, ESPMode::ThreadSafe> AfterMesh = MakeShared<FDynamicMesh3, ESPMode::ThreadSafe>(*BeforeMesh);
// Reset attributes and compute per-vertex normals
// (we need to clear rather than permanently discard the attributes because UDynamicMesh::EditMeshInternal would automatically add them back anyway)
AfterMesh->DiscardAttributes();
AfterMesh->EnableAttributes();
FMeshNormals::InitializeOverlayToPerVertexNormals(AfterMesh->Attributes()->PrimaryNormals(), false);
TUniquePtr<FMeshReplacementChange> ReplaceChange = MakeUnique<FMeshReplacementChange>(BeforeMesh, AfterMesh);
DynamicMeshComponent->ApplyChange(ReplaceChange.Get(), false);
// other internals are still valid?
GetToolManager()->EmitObjectChange(DynamicMeshComponent, MoveTemp(ReplaceChange), LOCTEXT("SculptDiscardAttribsChange", "Discard Attributes"));
// The tool's warning messages about seams are no longer relevant after discarding attributes, so clear warning messages
GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning);
}
#undef LOCTEXT_NAMESPACE