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

1998 lines
65 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Sculpting/MeshSculptToolBase.h"
#include "InteractiveToolManager.h"
#include "InteractiveGizmoManager.h"
#include "BaseGizmos/BrushStampIndicator.h"
#include "ToolSetupUtil.h"
#include "ToolSceneQueriesUtil.h"
#include "PreviewMesh.h"
#include "BaseGizmos/GizmoComponents.h"
#include "BaseGizmos/TransformGizmoUtil.h"
#include "Drawing/MeshDebugDrawing.h"
#include "Drawing/PreviewGeometryActor.h"
#include "Sculpting/StampFalloffs.h"
#include "Generators/SphereGenerator.h"
#include "ModelingToolTargetUtil.h"
#include "BaseBehaviors/TwoAxisPropertyEditBehavior.h"
#include "Drawing/PreviewGeometryActor.h"
#include "Polyline3.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(MeshSculptToolBase)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UMeshSculptToolBase"
namespace
{
const FString VertexSculptIndicatorGizmoType = TEXT("VertexSculptIndicatorGizmoType");
}
namespace MeshSculptBaseLocals
{
const FString& StrokePointSetID(TEXT("StrokePointSet"));
const FString& StrokeLineSetID(TEXT("StrokeLineSet"));
static FAutoConsoleVariable EnableMouseTrail(TEXT("modeling.Sculpting.EnableMouseTrail"), false, TEXT("Enable visualizing the path the mouse takes while sculpting."));
}
void FBrushToolRadius::InitializeWorldSizeRange(TInterval<float> Range, bool bValidateWorldRadius)
{
WorldSizeRange = Range;
if (WorldRadius < WorldSizeRange.Min)
{
WorldRadius = WorldSizeRange.Interpolate(0.2);
}
else if (WorldRadius > WorldSizeRange.Max)
{
WorldRadius = WorldSizeRange.Interpolate(0.8);
}
}
float FBrushToolRadius::GetWorldRadius() const
{
if (SizeType == EBrushToolSizeType::Adaptive)
{
return 0.5 * WorldSizeRange.Interpolate( FMath::Max(0, AdaptiveSize) );
}
else
{
return WorldRadius;
}
}
void FBrushToolRadius::IncreaseRadius(bool bSmallStep)
{
float StepSize = (bSmallStep) ? 0.005f : 0.025f;
if (SizeType == EBrushToolSizeType::Adaptive)
{
AdaptiveSize = FMath::Clamp(AdaptiveSize + StepSize, 0.0f, 1.0f);
}
else
{
float dt = StepSize * 0.5 * WorldSizeRange.Size();
WorldRadius = FMath::Clamp(WorldRadius + dt, WorldSizeRange.Min, WorldSizeRange.Max);
}
}
void FBrushToolRadius::DecreaseRadius(bool bSmallStep)
{
float StepSize = (bSmallStep) ? 0.005f : 0.025f;
if (SizeType == EBrushToolSizeType::Adaptive)
{
AdaptiveSize = FMath::Clamp(AdaptiveSize - StepSize, 0.0f, 1.0f);
}
else
{
float dt = StepSize * 0.5 * WorldSizeRange.Size();
WorldRadius = FMath::Clamp(WorldRadius - dt, WorldSizeRange.Min, WorldSizeRange.Max);
}
}
void UMeshSculptToolBase::SetWorld(UWorld* World)
{
this->TargetWorld = World;
}
void UMeshSculptToolBase::Setup()
{
UMeshSurfacePointTool::Setup();
BrushProperties = NewObject<USculptBrushProperties>(this);
if (SharesBrushPropertiesChanges())
{
BrushProperties->RestoreProperties(this);
}
// Note that brush properties includes BrushRadius, which, when not used as a constant,
// serves as an output property based on target size and brush size, and so it would need
// updating after the RestoreProperties() call. But deriving classes will call
// InitializeBrushSizeRange after this Setup() call to finish the brush setup, which will
// update the output property if necessary.
// work plane
GizmoProperties = NewObject<UWorkPlaneProperties>();
GizmoProperties->RestoreProperties(this);
// 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, &UMeshSculptToolBase::PlaneTransformChanged);
//GizmoProperties->WatchProperty(GizmoProperties->Position,
// [this](FVector NewPosition) { UpdateGizmoFromProperties(); });
//GizmoProperties->WatchProperty(GizmoProperties->Rotation,
// [this](FQuat NewRotation) { UpdateGizmoFromProperties(); });
GizmoPositionWatcher.Initialize(
[this]() { return GizmoProperties->Position; },
[this](FVector NewPosition) { UpdateGizmoFromProperties(); }, GizmoProperties->Position);
GizmoRotationWatcher.Initialize(
[this]() { return GizmoProperties->Rotation; },
[this](FQuat NewRotation) { UpdateGizmoFromProperties(); }, GizmoProperties->Rotation);
// display
ViewProperties = NewObject<UMeshEditingViewProperties>();
ViewProperties->RestoreProperties(this);
ViewProperties->WatchProperty(ViewProperties->bShowWireframe,
[this](bool bNewValue) { UpdateWireframeVisibility(bNewValue); });
ViewProperties->WatchProperty(ViewProperties->MaterialMode,
[this](EMeshEditingMaterialModes NewMode) { UpdateMaterialMode(NewMode); });
ViewProperties->WatchProperty(ViewProperties->CustomMaterial,
[this](TWeakObjectPtr<UMaterialInterface> NewMaterial) { UpdateCustomMaterial(NewMaterial); });
ViewProperties->WatchProperty(ViewProperties->bFlatShading,
[this](bool bNewValue) { UpdateFlatShadingSetting(bNewValue); });
ViewProperties->WatchProperty(ViewProperties->Color,
[this](FLinearColor NewColor) { UpdateColorSetting(NewColor); });
// This can actually use the same function since the parameter names for the material are the same
ViewProperties->WatchProperty(ViewProperties->TransparentMaterialColor,
[this](FLinearColor NewColor) { UpdateTransparentColorSetting(NewColor); });
ViewProperties->WatchProperty(ViewProperties->Opacity,
[this](double NewValue) { UpdateOpacitySetting(NewValue); });
ViewProperties->WatchProperty(ViewProperties->bTwoSided,
[this](bool bOn) { UpdateTwoSidedSetting(bOn); });
ViewProperties->WatchProperty(ViewProperties->Image,
[this](UTexture2D* NewImage) { UpdateImageSetting(NewImage); });
// 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(USculptBrushProperties::StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(USculptBrushProperties, 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 - LastBrushFrameWorld.Origin).Length() : 1.f);
};
BrushEditBehavior->HorizontalProperty.Name = LOCTEXT("BrushRadius", "Radius");
BrushEditBehavior->HorizontalProperty.bEnabled = true;
BrushEditBehavior->OnDragUpdated.AddWeakLambda(this, [this]()
{
CalculateBrushRadius();
NotifyOfPropertyChangeByTool(BrushProperties);
});
BrushEditBehavior->Initialize();
AddInputBehavior(BrushEditBehavior.Get());
}
}
void UMeshSculptToolBase::OnCompleteSetup()
{
RestoreAllBrushTypeProperties(this);
for (auto Pair : BrushOpPropSets)
{
SetToolPropertySourceEnabled(Pair.Value, false);
}
}
void UMeshSculptToolBase::Shutdown(EToolShutdownType ShutdownType)
{
if (StrokeGeometry)
{
StrokeGeometry->Disconnect();
StrokeGeometry = nullptr;
}
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;
}
UMeshSurfacePointTool::Shutdown(ShutdownType);
BrushIndicatorMesh->Disconnect();
BrushIndicatorMesh = nullptr;
GetToolManager()->GetPairedGizmoManager()->DestroyAllGizmosByOwner(this);
BrushIndicator = nullptr;
GetToolManager()->GetPairedGizmoManager()->DeregisterGizmoType(VertexSculptIndicatorGizmoType);
if (SharesBrushPropertiesChanges())
{
BrushProperties->SaveProperties(this);
}
if (GizmoProperties)
{
GizmoProperties->SaveProperties(this);
}
ViewProperties->SaveProperties(this);
SaveAllBrushTypeProperties(this);
// bake result
UBaseDynamicMeshComponent* DynamicMeshComponent = GetSculptMeshComponent();
if (DynamicMeshComponent != nullptr)
{
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
CommitResult(DynamicMeshComponent, false);
}
DynamicMeshComponent->UnregisterComponent();
DynamicMeshComponent->DestroyComponent();
DynamicMeshComponent = nullptr;
}
LongTransactions.CloseAll(GetToolManager());
}
void UMeshSculptToolBase::CommitResult(UBaseDynamicMeshComponent* Component, bool bModifiedTopology)
{
GetToolManager()->BeginUndoTransaction(LOCTEXT("SculptMeshToolTransactionName", "Sculpt Mesh"));
Component->ProcessMesh([&](const FDynamicMesh3& CurMesh)
{
UE::ToolTarget::CommitDynamicMeshUpdate(Target, CurMesh, bModifiedTopology);
});
GetToolManager()->EndUndoTransaction();
}
void UMeshSculptToolBase::OnTick(float DeltaTime)
{
GizmoPositionWatcher.CheckAndUpdate();
GizmoRotationWatcher.CheckAndUpdate();
ActivePressure = GetCurrentDevicePressure();
if (InStroke() == false)
{
SaveActiveStrokeModifiers();
}
else
{
AccumulateStrokeTime(DeltaTime);
}
// update cached falloff
CurrentBrushFalloff = 0.5;
if (GetActiveBrushOp()->PropertySet.IsValid())
{
CurrentBrushFalloff = FMathd::Clamp(GetActiveBrushOp()->PropertySet->GetFalloff(), 0.0, 1.0);
}
UpdateHoverStamp(GetBrushFrameWorld());
if (MeshSculptBaseLocals::EnableMouseTrail->GetBool() && StrokePath.Num() > 1)
{
TArray<FVector3d> UnProjectedStampPoints;
double Spacing = FMath::Clamp(BrushProperties->Spacing * BrushProperties->BrushSize.GetWorldRadius() * 2 , 0.1, 100000);
TArray<UE::Geometry::FPolyline3d> SpacingSections;
int32 SectionIndex = 0;
SpacingSections.Add(UE::Geometry::FPolyline3d());
SpacingSections[SectionIndex].AppendVertex(StrokePath[0].Position);
UnProjectedStampPoints.Add(StrokePath[0].Position);
for (int32 Index = 0; Index < StrokePath.Num() - 1; Index++)
{
FVector3d Span = StrokePath[Index + 1].Position - StrokePath[Index].Position;
FVector3d SpanDir = Span.GetSafeNormal();
double RemainingSpace = FMath::Max(0, Spacing - SpacingSections[SectionIndex].Length());
double OverflowSpace = Span.Length() - RemainingSpace;
if (OverflowSpace < 0)
{
SpacingSections[SectionIndex].AppendVertex(StrokePath[Index + 1].Position);
}
else
{
SpacingSections[SectionIndex].AppendVertex(StrokePath[Index].Position + (SpanDir * RemainingSpace));
while (OverflowSpace > Spacing)
{
FVector3d TerminalPosition = SpacingSections[SectionIndex].End();
SpacingSections.Add(UE::Geometry::FPolyline3d());
SectionIndex++;
UnProjectedStampPoints.Add(TerminalPosition);
SpacingSections[SectionIndex].AppendVertex(TerminalPosition);
SpacingSections[SectionIndex].AppendVertex(TerminalPosition + (SpanDir * Spacing));
OverflowSpace -= Spacing;
}
FVector3d TerminalPosition = SpacingSections[SectionIndex].End();
SpacingSections.Add(UE::Geometry::FPolyline3d());
SectionIndex++;
UnProjectedStampPoints.Add(TerminalPosition);
SpacingSections[SectionIndex].AppendVertex(TerminalPosition);
SpacingSections[SectionIndex].AppendVertex(TerminalPosition + (SpanDir * OverflowSpace));
}
}
if (!StrokeGeometry)
{
// Set up all the components we need to visualize things.
StrokeGeometry = NewObject<UPreviewGeometry>();
StrokeGeometry->CreateInWorld(TargetWorld, FTransform());
// These visualize the current spline edges that would be extracted
StrokeGeometry->AddPointSet(MeshSculptBaseLocals::StrokePointSetID);
StrokeGeometry->AddLineSet(MeshSculptBaseLocals::StrokeLineSetID);
}
ULineSetComponent* LineSet = StrokeGeometry->FindLineSet(MeshSculptBaseLocals::StrokeLineSetID);
if (LineSet)
{
LineSet->Clear();
for (int32 StrokeSectionIndex = 0; StrokeSectionIndex < SpacingSections.Num(); ++StrokeSectionIndex)
{
FColor SectionColor = (StrokeSectionIndex % 2) ? FColor::Red : FColor::Cyan;
for (int32 SegmentIndex = 0; SegmentIndex < SpacingSections[StrokeSectionIndex].SegmentCount(); ++SegmentIndex)
{
LineSet->AddLine(SpacingSections[StrokeSectionIndex].GetSegment(SegmentIndex).StartPoint(),
SpacingSections[StrokeSectionIndex].GetSegment(SegmentIndex).EndPoint(),
SectionColor, 3.0);
}
}
}
UPointSetComponent* PointSet = StrokeGeometry->FindPointSet(MeshSculptBaseLocals::StrokePointSetID);
if (PointSet)
{
PointSet->Clear();
for (int32 UnProjectedStampPointsIndex = 0; UnProjectedStampPointsIndex < UnProjectedStampPoints.Num(); ++UnProjectedStampPointsIndex)
{
FRay3d CameraToPathRay(CameraState.Position, (UnProjectedStampPoints[UnProjectedStampPointsIndex] - CameraState.Position).GetSafeNormal());
TUniquePtr<FMeshSculptBrushOp>& UseBrushOp = GetActiveBrushOp();
ESculptBrushOpTargetType TargetType = UseBrushOp->GetBrushTargetType();
FVector3d ProjectedPosition;
FVector3d ProjectedNormal;
int32 HitTriangle = IndexConstants::InvalidID;
switch (TargetType)
{
case ESculptBrushOpTargetType::SculptMesh:
ProjectWorldRayOnSculptMesh(CameraToPathRay, true, ProjectedPosition, ProjectedNormal, HitTriangle);
break;
case ESculptBrushOpTargetType::TargetMesh:
ProjectWorldRayOnTargetMesh(CameraToPathRay, true, ProjectedPosition, ProjectedNormal, HitTriangle);
break;
case ESculptBrushOpTargetType::ActivePlane:
ProjectWorldRayOnActivePlane(CameraToPathRay, ProjectedPosition, ProjectedNormal);
break;
}
FColor PointColor = FColor::Green;
PointSet->AddPoint(ProjectedPosition, PointColor, 6.0, 0.0);
}
}
}
else
{
if (StrokeGeometry)
{
ULineSetComponent* LineSet = StrokeGeometry->FindLineSet(MeshSculptBaseLocals::StrokeLineSetID);
if (LineSet)
{
LineSet->Clear();
}
UPointSetComponent* PointSet = StrokeGeometry->FindPointSet(MeshSculptBaseLocals::StrokePointSetID);
if (PointSet)
{
PointSet->Clear();
}
}
}
// always using HoverStamp here because it's position should always be up-to-date for the current apply-stamp...
BrushIndicator->Update((float)GetCurrentBrushRadius(),
HoverStamp.WorldFrame.ToFTransform(), 1.0f - (float)GetCurrentBrushFalloff());
UpdateWorkPlane();
}
void UMeshSculptToolBase::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();
if (BrushIndicatorMaterial)
{
double FixedDimScale = ToolSceneQueriesUtil::CalculateDimensionFromVisualAngleD(RenderCameraState, HoverStamp.WorldFrame.Origin, 1.5f);
BrushIndicatorMaterial->SetScalarParameterValue(TEXT("FalloffWidth"), FixedDimScale);
}
if (ShowWorkPlane() && bDrawWorkPlaneGridLines)
{
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 UMeshSculptToolBase::InitializeSculptMeshComponent(UBaseDynamicMeshComponent* Component, AActor* Actor)
{
ToolSetupUtil::ApplyRenderingConfigurationToPreview(Component, nullptr);
// disable shadows initially, as changing shadow settings invalidates the SceneProxy
Component->SetShadowsEnabled(false);
Component->SetupAttachment(Actor->GetRootComponent());
Component->RegisterComponent();
// initialize from LOD-0 MeshDescription
static FGetMeshParameters GetMeshParams;
GetMeshParams.bWantMeshTangents = true;
Component->SetMesh(UE::ToolTarget::GetDynamicMeshCopy(Target, GetMeshParams));
double MaxDimension = Component->GetMesh()->GetBounds(true).MaxDim();
// bake rotation and scaling into mesh because handling these inside sculpting 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());
Component->ApplyTransform(InitialTargetTransform, false);
CurTargetTransform = FTransform3d(Translation);
Component->SetWorldTransform((FTransform)CurTargetTransform);
// hide input Component
UE::ToolTarget::HideSourceObject(Target);
}
void UMeshSculptToolBase::RegisterBrushType(int32 Identifier, FText Name, TUniquePtr<FMeshSculptBrushOpFactory> Factory, UMeshSculptBrushOpProps* PropSet)
{
FBrushTypeInfo TypeInfo;
TypeInfo.Name = Name;
TypeInfo.Identifier = Identifier;
RegisteredPrimaryBrushTypes.Add(TypeInfo);
// ensure we are not adding duplicates of PropSets when brush registration occurs
if (TObjectPtr<UMeshSculptBrushOpProps>* OldPropSet = BrushOpPropSets.Find(Identifier))
{
RemoveToolPropertySource(*OldPropSet);
}
BrushOpPropSets.Add(Identifier, PropSet);
BrushOpFactories.Add(Identifier, MoveTemp(Factory));
if (!ToolPropertyObjects.Contains(PropSet))
{
AddToolPropertySource(PropSet);
}
SetToolPropertySourceEnabled(PropSet, false);
}
void UMeshSculptToolBase::RegisterSecondaryBrushType(int32 Identifier, FText Name, TUniquePtr<FMeshSculptBrushOpFactory> Factory, UMeshSculptBrushOpProps* PropSet)
{
FBrushTypeInfo TypeInfo;
TypeInfo.Name = Name;
TypeInfo.Identifier = Identifier;
RegisteredSecondaryBrushTypes.Add(TypeInfo);
// ensure we are not adding duplicates of PropSets when brush registration occurs
if (TObjectPtr<UMeshSculptBrushOpProps>* OldPropSet = SecondaryBrushOpPropSets.Find(Identifier))
{
RemoveToolPropertySource(*OldPropSet);
}
SecondaryBrushOpPropSets.Add(Identifier, PropSet);
SecondaryBrushOpFactories.Add(Identifier, MoveTemp(Factory));
if (!ToolPropertyObjects.Contains(PropSet))
{
AddToolPropertySource(PropSet);
}
SetToolPropertySourceEnabled(PropSet, false);
}
void UMeshSculptToolBase::SaveAllBrushTypeProperties(UInteractiveTool* SaveFromTool)
{
for (auto Pair : BrushOpPropSets)
{
Pair.Value->SaveProperties(SaveFromTool);
}
for (auto Pair : SecondaryBrushOpPropSets)
{
Pair.Value->SaveProperties(SaveFromTool);
}
}
void UMeshSculptToolBase::RestoreAllBrushTypeProperties(UInteractiveTool* RestoreToTool)
{
for (auto Pair : BrushOpPropSets)
{
Pair.Value->RestoreProperties(RestoreToTool);
}
for (auto Pair : SecondaryBrushOpPropSets)
{
Pair.Value->RestoreProperties(RestoreToTool);
}
}
void UMeshSculptToolBase::SetActivePrimaryBrushType(int32 Identifier)
{
TUniquePtr<FMeshSculptBrushOpFactory>* Factory = BrushOpFactories.Find(Identifier);
if (Factory == nullptr)
{
ensure(false);
return;
}
if (PrimaryVisiblePropSet != nullptr)
{
SetToolPropertySourceEnabled(PrimaryVisiblePropSet, false);
PrimaryVisiblePropSet = nullptr;
}
PrimaryBrushOp = (*Factory)->Build();
PrimaryBrushOp->Falloff = PrimaryFalloff;
TObjectPtr<UMeshSculptBrushOpProps>* FoundProps = BrushOpPropSets.Find(Identifier);
if (FoundProps != nullptr)
{
SetToolPropertySourceEnabled(*FoundProps, bBrushOpPropsVisible);
PrimaryVisiblePropSet = *FoundProps;
PrimaryBrushOp->PropertySet = PrimaryVisiblePropSet;
}
}
void UMeshSculptToolBase::SetActiveSecondaryBrushType(int32 Identifier)
{
TUniquePtr<FMeshSculptBrushOpFactory>* Factory = SecondaryBrushOpFactories.Find(Identifier);
if (Factory == nullptr)
{
ensure(false);
return;
}
if (SecondaryVisiblePropSet != nullptr)
{
SetToolPropertySourceEnabled(SecondaryVisiblePropSet, false);
SecondaryVisiblePropSet = nullptr;
}
SecondaryBrushOp = (*Factory)->Build();
TSharedPtr<FMeshSculptFallofFunc> SecondaryFalloff = MakeShared<FMeshSculptFallofFunc>();;
SecondaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeStandardSmoothFalloff();
SecondaryBrushOp->Falloff = SecondaryFalloff;
TObjectPtr<UMeshSculptBrushOpProps>* FoundProps = SecondaryBrushOpPropSets.Find(Identifier);
if (FoundProps != nullptr)
{
SetToolPropertySourceEnabled(*FoundProps, bBrushOpPropsVisible);
SecondaryVisiblePropSet = *FoundProps;
SecondaryBrushOp->PropertySet = SecondaryVisiblePropSet;
}
}
TUniquePtr<FMeshSculptBrushOp>& UMeshSculptToolBase::GetActiveBrushOp()
{
if (GetInSmoothingStroke())
{
return SecondaryBrushOp;
}
else
{
return PrimaryBrushOp;
}
}
void UMeshSculptToolBase::SetBrushOpPropsVisibility(bool bVisible)
{
bBrushOpPropsVisible = bVisible;
if (PrimaryVisiblePropSet)
{
SetToolPropertySourceEnabled(PrimaryVisiblePropSet, bVisible);
}
if (SecondaryVisiblePropSet)
{
SetToolPropertySourceEnabled(SecondaryVisiblePropSet, bVisible);
}
}
void UMeshSculptToolBase::RegisterStandardFalloffTypes()
{
RegisteredPrimaryFalloffTypes.Add( FFalloffTypeInfo{ LOCTEXT("Smooth", "Smooth"), TEXT("Smooth"), (int32)EMeshSculptFalloffType::Smooth});
RegisteredPrimaryFalloffTypes.Add( FFalloffTypeInfo{ LOCTEXT("Linear", "Linear"), TEXT("Linear"), (int32)EMeshSculptFalloffType::Linear } );
RegisteredPrimaryFalloffTypes.Add( FFalloffTypeInfo{ LOCTEXT("Inverse", "Inverse"), TEXT("Inverse"), (int32)EMeshSculptFalloffType::Inverse } );
RegisteredPrimaryFalloffTypes.Add( FFalloffTypeInfo{ LOCTEXT("Round", "Round"), TEXT("Round"), (int32)EMeshSculptFalloffType::Round } );
RegisteredPrimaryFalloffTypes.Add( FFalloffTypeInfo{ LOCTEXT("BoxSmooth", "BoxSmooth"), TEXT("BoxSmooth"), (int32)EMeshSculptFalloffType::BoxSmooth } );
RegisteredPrimaryFalloffTypes.Add( FFalloffTypeInfo{ LOCTEXT("BoxLinear", "BoxLinear"), TEXT("BoxLinear"), (int32)EMeshSculptFalloffType::BoxLinear } );
RegisteredPrimaryFalloffTypes.Add( FFalloffTypeInfo{ LOCTEXT("BoxInverse", "BoxInverse"), TEXT("BoxInverse"), (int32)EMeshSculptFalloffType::BoxInverse } );
RegisteredPrimaryFalloffTypes.Add( FFalloffTypeInfo{ LOCTEXT("BoxRound", "BoxRound"), TEXT("BoxRound"), (int32)EMeshSculptFalloffType::BoxRound } );
}
void UMeshSculptToolBase::SetPrimaryFalloffType(EMeshSculptFalloffType FalloffType)
{
PrimaryFalloff = MakeShared<FMeshSculptFallofFunc>();
switch (FalloffType)
{
default:
case EMeshSculptFalloffType::Smooth:
PrimaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeStandardSmoothFalloff();
break;
case EMeshSculptFalloffType::Linear:
PrimaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeLinearFalloff();
break;
case EMeshSculptFalloffType::Inverse:
PrimaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeInverseFalloff();
break;
case EMeshSculptFalloffType::Round:
PrimaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeRoundFalloff();
break;
case EMeshSculptFalloffType::BoxSmooth:
PrimaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeSmoothBoxFalloff();
break;
case EMeshSculptFalloffType::BoxLinear:
PrimaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeLinearBoxFalloff();
break;
case EMeshSculptFalloffType::BoxInverse:
PrimaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeInverseBoxFalloff();
break;
case EMeshSculptFalloffType::BoxRound:
PrimaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeRoundBoxFalloff();
break;
}
}
bool UMeshSculptToolBase::HitTest(const FRay& Ray, FHitResult& OutHit)
{
FRay3d LocalRay = GetLocalRay(Ray);
int HitTID = FindHitSculptMeshTriangle(LocalRay);
if (HitTID != IndexConstants::InvalidID)
{
FTriangle3d Triangle;
FDynamicMesh3* Mesh = GetSculptMesh();
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 UMeshSculptToolBase::ClearStrokePath()
{
StrokePath.Empty();
}
void UMeshSculptToolBase::AppendStrokePath(const FVector3d& NextPoint)
{
if(StrokePath.Num() == 0 || !FMath::IsNearlyZero((StrokePath.Last().Position - NextPoint).SquaredLength()))
{
StrokePath.Add({ .Timestamp = ActiveStrokeTime, .Position = NextPoint, .Pressure = GetActivePressure() });
}
}
void UMeshSculptToolBase::OnBeginDrag(const FRay& WorldRay)
{
SaveActiveStrokeModifiers();
FHitResult OutHit;
if (HitTest(WorldRay, OutHit))
{
bInStroke = true;
ResetStrokeTime();
UpdateBrushTargetPlaneFromHit(WorldRay, OutHit);
// initialize first stamp
PendingStampRay = WorldRay;
bIsStampPending = true;
// set falloff
PrimaryBrushOp->Falloff = PrimaryFalloff;
OnBeginStroke(WorldRay);
}
}
void UMeshSculptToolBase::OnUpdateDrag(const FRay& WorldRay)
{
if (InStroke())
{
PendingStampRay = WorldRay;
TUniquePtr<FMeshSculptBrushOp>& UseBrushOp = GetActiveBrushOp();
ESculptBrushOpTargetType TargetType = UseBrushOp->GetBrushTargetType();
FVector3d ProjectedPosition;
FVector3d ProjectedNormal;
int32 HitTriangle = IndexConstants::InvalidID;
switch (TargetType)
{
case ESculptBrushOpTargetType::SculptMesh:
ProjectWorldRayOnSculptMesh(WorldRay, true, ProjectedPosition, ProjectedNormal, HitTriangle);
break;
case ESculptBrushOpTargetType::TargetMesh:
ProjectWorldRayOnTargetMesh(WorldRay, true, ProjectedPosition, ProjectedNormal, HitTriangle);
break;
case ESculptBrushOpTargetType::ActivePlane:
ProjectWorldRayOnActivePlane(WorldRay, ProjectedPosition, ProjectedNormal);
break;
}
AppendStrokePath(ProjectedPosition);
}
}
void UMeshSculptToolBase::OnEndDrag(const FRay& Ray)
{
bInStroke = false;
// cancel any outstanding stamps! otherwise change record could become invalid
bIsStampPending = false;
OnEndStroke();
}
void UMeshSculptToolBase::OnCancelDrag()
{
bInStroke = false;
// cancel any outstanding stamps
bIsStampPending = false;
OnCancelStroke();
}
FRay3d UMeshSculptToolBase::GetLocalRay(const FRay& WorldRay) const
{
FRay3d LocalRay(CurTargetTransform.InverseTransformPosition((FVector3d)WorldRay.Origin),
CurTargetTransform.InverseTransformVector((FVector3d)WorldRay.Direction));
UE::Geometry::Normalize(LocalRay.Direction);
return LocalRay;
}
bool UMeshSculptToolBase::GetBrushSizePressureSensitivityEnabled() const
{
if (ToolPropertyObjects.Contains(BrushProperties) && BrushProperties->BrushSize.bToolSupportsPressureSensitivity)
{
return BrushProperties->BrushSize.bEnablePressureSensitivity;
}
return false;
}
bool UMeshSculptToolBase::GetBrushStrengthPressureEnabled()
{
TUniquePtr<FMeshSculptBrushOp>& BrushOp = GetActiveBrushOp();
if (BrushOp->PropertySet.IsValid())
{
return BrushOp->PropertySet->GetStrengthPressureEnabled();
}
return false;
}
void UMeshSculptToolBase::UpdateBrushFrameWorld(const FVector3d& NewPosition, const FVector3d& NewNormal)
{
FFrame3d PrevBrushFrameWorld = LastBrushFrameWorld;
bool bTriedFrameRepair = false;
retry_frame_update:
FFrame3d NewFrame = LastBrushFrameWorld;
NewFrame.Origin = NewPosition;
NewFrame.AlignAxis(2, NewNormal);
FVector3d CameraUp = (FVector3d)CameraState.Up();
if (FMathd::Abs(CameraUp.Dot(NewNormal)) < 0.98)
{
NewFrame.ConstrainedAlignAxis(1, CameraUp, NewFrame.Z());
}
if ( (NewFrame.Rotation.Length() - 1.0) > 0.1 ) // try to recover from normalization failure
{
LastBrushFrameWorld = FFrame3d(LastBrushFrameWorld.Origin);
if (bTriedFrameRepair == false)
{
bTriedFrameRepair = true;
goto retry_frame_update;
}
}
if (InStroke() && BrushProperties->Lazyness > 0)
{
double t = FMathd::Lerp(1.0, 0.1, (double)BrushProperties->Lazyness);
LastBrushFrameWorld.Origin = UE::Geometry::Lerp(LastBrushFrameWorld.Origin, NewFrame.Origin, t);
LastBrushFrameWorld.Rotation = FQuaterniond(LastBrushFrameWorld.Rotation, NewFrame.Rotation, t);
}
else
{
LastBrushFrameWorld = NewFrame;
}
ActiveStrokePathArcLen += Distance(LastBrushFrameWorld.Origin, PrevBrushFrameWorld.Origin);
LastBrushFrameLocal = LastBrushFrameWorld;
LastBrushFrameLocal.Transform(CurTargetTransform.InverseUnsafe()); // Note: Unsafe inverse used because we cannot handle scales on a frame regardless.
// TODO: in the case of a non-uniform scale, consider whether we should do additional work to align the Z axis?
}
void UMeshSculptToolBase::AlignBrushToView()
{
UpdateBrushFrameWorld(GetBrushFrameWorld().Origin, -(FVector3d)CameraState.Forward());
}
void UMeshSculptToolBase::UpdateBrushTargetPlaneFromHit(const FRay& WorldRayIn, const FHitResult& Hit)
{
FRay3d WorldRay(WorldRayIn);
FVector3d WorldPosWithBrushDepth = WorldRay.PointAt(Hit.Distance) + GetCurrentBrushDepth() * GetCurrentBrushRadius() * WorldRay.Direction;
ActiveBrushTargetPlaneWorld = FFrame3d(WorldPosWithBrushDepth, -WorldRay.Direction);
}
bool UMeshSculptToolBase::ProjectWorldRayOnActivePlane(const FRay& WorldRayIn, FVector3d& ProjectedPosition, FVector3d& ProjectedNormal) const
{
FRay3d WorldRay(WorldRayIn);
ActiveBrushTargetPlaneWorld.RayPlaneIntersection(WorldRay.Origin, WorldRay.Direction, 2, ProjectedPosition);
ProjectedNormal = ActiveBrushTargetPlaneWorld.Z();
return true;
}
bool UMeshSculptToolBase::ProjectWorldRayOnTargetMesh(const FRay& WorldRayIn, bool bFallbackToViewPlane, FVector3d& ProjectedPosition, FVector3d& ProjectedNormal, int32& HitTriangle) const
{
FRay3d WorldRay(WorldRayIn);
FRay3d LocalRay = GetLocalRay(WorldRayIn);
int32 HitTID = FindHitTargetMeshTriangleConst(LocalRay);
if (HitTID != IndexConstants::InvalidID)
{
const FDynamicMesh3* BaseMesh = GetBaseMesh();
FIntrRay3Triangle3d Query = TMeshQueries<FDynamicMesh3>::TriangleIntersection(*BaseMesh, HitTID, LocalRay);
ProjectedNormal = CurTargetTransform.TransformNormal(BaseMesh->GetTriNormal(HitTID));
ProjectedPosition = CurTargetTransform.TransformPosition(LocalRay.PointAt(Query.RayParameter));
HitTriangle = HitTID;
return true;
}
if (bFallbackToViewPlane)
{
FFrame3d BrushPlane(GetBrushFrameWorld().Origin, (FVector3d)CameraState.Forward());
BrushPlane.RayPlaneIntersection(WorldRay.Origin, WorldRay.Direction, 2, ProjectedPosition);
ProjectedNormal = ActiveBrushTargetPlaneWorld.Z();
return true;
}
return false;
}
bool UMeshSculptToolBase::ProjectWorldRayOnSculptMesh(const FRay& WorldRayIn, bool bFallbackToViewPlane, FVector3d& ProjectedPosition, FVector3d& ProjectedNormal, int32& HitTriangle) const
{
HitTriangle = IndexConstants::InvalidID;
FRay3d WorldRay(WorldRayIn);
FRay3d LocalRay = GetLocalRay(WorldRayIn);
int32 HitTID = FindHitSculptMeshTriangleConst(LocalRay);
if (HitTID != IndexConstants::InvalidID)
{
const FDynamicMesh3* SculptMesh = GetSculptMesh();
FIntrRay3Triangle3d Query = TMeshQueries<FDynamicMesh3>::TriangleIntersection(*SculptMesh, HitTID, LocalRay);
ProjectedNormal = CurTargetTransform.TransformNormal(SculptMesh->GetTriNormal(HitTID));
ProjectedPosition = CurTargetTransform.TransformPosition(LocalRay.PointAt(Query.RayParameter));
HitTriangle = HitTID;
return true;
}
if (bFallbackToViewPlane)
{
FFrame3d BrushPlane(GetBrushFrameWorld().Origin, (FVector3d)CameraState.Forward());
BrushPlane.RayPlaneIntersection(WorldRay.Origin, WorldRay.Direction, 2, ProjectedPosition);
ProjectedNormal = ActiveBrushTargetPlaneWorld.Z();
return true;
}
return false;
}
bool UMeshSculptToolBase::UpdateBrushPositionOnActivePlane(const FRay& WorldRayIn)
{
LastBrushTriangleID = IndexConstants::InvalidID;
FVector3d NewHitPosWorld;
FVector3d NewHitNormalWorld;
bool bHit = ProjectWorldRayOnActivePlane(WorldRayIn, NewHitPosWorld, NewHitNormalWorld);
UpdateBrushFrameWorld(NewHitPosWorld, ActiveBrushTargetPlaneWorld.Z());
return bHit;
}
bool UMeshSculptToolBase::UpdateBrushPositionOnTargetMesh(const FRay& WorldRayIn, bool bFallbackToViewPlane)
{
LastBrushTriangleID = IndexConstants::InvalidID;
FRay3d LocalRay = GetLocalRay(WorldRayIn);
FVector3d NewHitPosWorld;
FVector3d NewHitNormalWorld;
int32 HitTID;
bool bHit = ProjectWorldRayOnTargetMesh(WorldRayIn, bFallbackToViewPlane, NewHitPosWorld, NewHitNormalWorld, HitTID);
UpdateBrushFrameWorld(NewHitPosWorld, NewHitNormalWorld);
UpdateHitTargetMeshTriangle(HitTID, LocalRay);
if (HitTID != IndexConstants::InvalidID)
{
LastBrushTriangleID = HitTID;
}
return bHit;
}
bool UMeshSculptToolBase::UpdateBrushPositionOnSculptMesh(const FRay& WorldRayIn, bool bFallbackToViewPlane)
{
LastBrushTriangleID = IndexConstants::InvalidID;
FRay3d LocalRay = GetLocalRay(WorldRayIn);
FVector3d NewHitPosWorld;
FVector3d NewHitNormalWorld;
int32 HitTID;
bool bHit = ProjectWorldRayOnSculptMesh(WorldRayIn, bFallbackToViewPlane, NewHitPosWorld, NewHitNormalWorld, HitTID);
UpdateBrushFrameWorld(NewHitPosWorld, NewHitNormalWorld);
UpdateHitSculptMeshTriangle(HitTID, LocalRay);
if (HitTID != IndexConstants::InvalidID)
{
LastBrushTriangleID = HitTID;
}
return bHit;
}
void UMeshSculptToolBase::SaveActiveStrokeModifiers()
{
bSmoothing = GetShiftToggle();
bInvert = GetCtrlToggle();
}
void UMeshSculptToolBase::UpdateHoverStamp(const FFrame3d& StampFrameWorld)
{
HoverStamp.WorldFrame = StampFrameWorld;
HoverStamp.LocalFrame = HoverStamp.WorldFrame;
HoverStamp.LocalFrame.Transform(CurTargetTransform.InverseUnsafe());
}
float UMeshSculptToolBase::GetStampTemporalFlowRate() const
{
return BrushProperties->FlowRate;
}
void UMeshSculptToolBase::ProcessPerTickStamps(TFunction<bool(const FRay& StampRay)> UpdateStampPosition, TFunction<void(int StampIndex, const FRay& StampRay)> ExecuteStampOperation)
{
ProcessPerTickStamps(UpdateStampPosition, [](int StampCount) {}, ExecuteStampOperation, [] {});
}
void UMeshSculptToolBase::ProcessPerTickStamps(TFunction<bool(const FRay& StampRay)> UpdateStampPosition, TFunction<void(int StampCount)> PreExecuteStampsOperation, TFunction<void(int StampIndex, const FRay& StampRay)> ExecuteStampOperation, TFunction<void()> PostExecuteStampsOperation)
{
if (InStroke())
{
TRACE_CPUPROFILER_EVENT_SCOPE(BaseSculptTool_Tick_ProcessPerTickStamps);
FDynamicMesh3* Mesh = GetSculptMesh();
// We save the active pressure here, so we can then override and fake it during the stamp application to generate interpolated pressures along the stroke.
TGuardValue<double> CachedActivePressure(ActivePressure, ActivePressure);
// update brush position
if (UpdateStampPosition(GetPendingStampRayWorld()) == false)
{
return;
}
UpdateStampPendingState();
if (IsStampPending() == false)
{
return;
}
if (!PendingStampRays.IsEmpty())
{
PreExecuteStampsOperation(PendingStampRays.Num());
for (int Index = 0; Index < PendingStampRays.Num(); ++Index)
{
const FStampRayData& StampRay = PendingStampRays[Index];
ActivePressure = StampRay.Pressure;
UpdateStampPosition(StampRay.StampRay);
ExecuteStampOperation(Index, StampRay.StampRay);
StampsDuringStroke++;
}
PostExecuteStampsOperation();
}
}
}
void UMeshSculptToolBase::UpdateStampPendingState()
{
if (InStroke() == false) return;
PendingStampRays.Empty();
// Fail out early with a stamp if we don't support the currently requested stroke type
if (!GetActiveBrushOp()->SupportsStrokeType(BrushProperties->StrokeType))
{
PendingStampRays.Add({ .StampRay = PendingStampRay, .Pressure = GetActivePressure() });
bIsStampPending = true;
return;
}
switch (BrushProperties->StrokeType)
{
case EMeshSculptStrokeType::Airbrush:
{
bool bFlowStampPending = false;
float UseStampFlowRate = GetStampTemporalFlowRate();
if (UseStampFlowRate >= 1.0)
{
bFlowStampPending = true;
}
else if (UseStampFlowRate == 0.0)
{
bFlowStampPending = (LastFlowTimeStamp++ == 0);
}
else
{
double dt = (1.0 - UseStampFlowRate);
int FlowTimestamp = (int)(ActiveStrokeTime / dt);
if (FlowTimestamp > LastFlowTimeStamp)
{
LastFlowTimeStamp = FlowTimestamp;
bFlowStampPending = true;
}
}
PendingStampRays.Add({ .StampRay = PendingStampRay, .Pressure = GetActivePressure() });
bIsStampPending = bFlowStampPending;
return;
}
break;
case EMeshSculptStrokeType::Spacing:
{
// Save our active pressure since we'll be manipulating it during the next block
TGuardValue<double> CachedActivePressure(ActivePressure, ActivePressure);
TArray<FTimedStrokePoint> UnProjectedStampPoints;
if (StrokePath.Num() > 1)
{
TArray<FTimedStrokePoint> UnconsumedPathPoints;
int32 SectionIndex = 0;
double SpacingSectionConsumedLength = 0.0;
UnProjectedStampPoints.Add(StrokePath[0]);
UnconsumedPathPoints.Add(StrokePath[0]);
for (int32 Index = 0; Index < StrokePath.Num() - 1; Index++)
{
// Compute average spacing over stroke section - needed for dynamic spacing as the pressure changes over the path.
double Spacing = 1.0;
{
ActivePressure = StrokePath[Index].Pressure;
double SpacingAtBeginning = FMath::Clamp(BrushProperties->Spacing * GetActiveBrushRadius() * 2, 0.1, 100000);
ActivePressure = StrokePath[Index + 1].Pressure;
double SpacingAtEnding = FMath::Clamp(BrushProperties->Spacing * GetActiveBrushRadius() * 2, 0.1, 100000);
Spacing = (SpacingAtBeginning + SpacingAtEnding) * 0.5;
}
FVector3d Span = StrokePath[Index + 1].Position - StrokePath[Index].Position; // Vector representing our stroke segment
FVector3d SpanDir = Span.GetSafeNormal(); // The direction of our stroke segment
double RemainingSpace = FMath::Max(0, Spacing - SpacingSectionConsumedLength); // How much space do we have left before a stamp happens
double OverflowSpace = Span.Length() - RemainingSpace; // If positive, our stroke section meets or exceeds the remaining space for a stamp to happen
if (OverflowSpace < 0)
{
SpacingSectionConsumedLength += Span.Length();
UnconsumedPathPoints.Add(StrokePath[Index + 1]);
}
else
{
double SpanAlpha = FMath::IsNearlyZero(Span.Length()) ? 0.0 : RemainingSpace / Span.Length(); // How far along, within the current stroke segment, are we?
double OverflowAlphaIncrement = FMath::IsNearlyZero(Span.Length()) ? 0.0 : Spacing / Span.Length(); // For our current stroke segment, what is the percentage of it's length in terms of a spacing interval?
FVector3d TerminalPosition = FMath::LerpStable(StrokePath[Index].Position, StrokePath[Index + 1].Position, SpanAlpha);
SpacingSectionConsumedLength += RemainingSpace;
UnProjectedStampPoints.Add({ .Position = TerminalPosition, .Pressure = FMath::LerpStable(StrokePath[Index].Pressure, StrokePath[Index + 1].Pressure, SpanAlpha) });
while (OverflowSpace > Spacing) // Handle cases where we have so much line we inject multiple points...
{
SpanAlpha += OverflowAlphaIncrement;
TerminalPosition = FMath::LerpStable(StrokePath[Index].Position, StrokePath[Index + 1].Position, SpanAlpha);
UnProjectedStampPoints.Add({ .Position = TerminalPosition, .Pressure = FMath::LerpStable(StrokePath[Index].Pressure, StrokePath[Index + 1].Pressure, SpanAlpha) });
OverflowSpace -= Spacing;
}
ensure(SpanAlpha <= 1.0);
SpacingSectionConsumedLength = OverflowSpace;
UnconsumedPathPoints.Empty();
UnconsumedPathPoints.Add(
{
.Timestamp = FMath::LerpStable(StrokePath[Index].Timestamp, StrokePath[Index + 1].Timestamp, SpanAlpha),
.Position = TerminalPosition,
.Pressure = FMath::LerpStable(StrokePath[Index].Pressure, StrokePath[Index + 1].Pressure, SpanAlpha)
});
UnconsumedPathPoints.Add({ StrokePath[Index + 1] });
}
}
StrokePath = UnconsumedPathPoints;
}
// We want to ensure that when starting the stroke, we stamp right at the beginning of the path
int StartIndex = 1;
if (StampsDuringStroke == 0)
{
StartIndex = 0;
}
for (int32 UnProjectedStampPointsIndex = StartIndex; UnProjectedStampPointsIndex < UnProjectedStampPoints.Num(); ++UnProjectedStampPointsIndex)
{
FRay3d CameraToPathRay(CameraState.Position, (UnProjectedStampPoints[UnProjectedStampPointsIndex].Position - CameraState.Position).GetSafeNormal());
TUniquePtr<FMeshSculptBrushOp>& UseBrushOp = GetActiveBrushOp();
ESculptBrushOpTargetType TargetType = UseBrushOp->GetBrushTargetType();
FVector3d ProjectedPosition = FVector3d::ZeroVector;
FVector3d ProjectedNormal = FVector3d::ZeroVector;
int32 HitTriangle = IndexConstants::InvalidID;
switch (TargetType)
{
case ESculptBrushOpTargetType::SculptMesh:
ProjectWorldRayOnSculptMesh(CameraToPathRay, true, ProjectedPosition, ProjectedNormal, HitTriangle);
break;
case ESculptBrushOpTargetType::TargetMesh:
ProjectWorldRayOnTargetMesh(CameraToPathRay, true, ProjectedPosition, ProjectedNormal, HitTriangle);
break;
case ESculptBrushOpTargetType::ActivePlane:
ProjectWorldRayOnActivePlane(CameraToPathRay, ProjectedPosition, ProjectedNormal);
break;
}
PendingStampRays.Add({ .StampRay = FRay3d(CameraState.Position, (ProjectedPosition - CameraState.Position).GetSafeNormal()), .Pressure = UnProjectedStampPoints[UnProjectedStampPointsIndex].Pressure });
}
if (!PendingStampRays.IsEmpty())
{
bIsStampPending = true;
}
return;
}
break;
case EMeshSculptStrokeType::Dots:
{
PendingStampRays.Add({ .StampRay = PendingStampRay, .Pressure = GetActivePressure() });
bIsStampPending = true;
return;
}
break;
default:
break;
}
}
void UMeshSculptToolBase::ResetStrokeTime()
{
ActiveStrokeTime = 0.0;
LastFlowTimeStamp = 0;
ActiveStrokePathArcLen = 0;
LastSpacingTimestamp = 0;
StampsDuringStroke = 0;
ClearStrokePath();
}
void UMeshSculptToolBase::AccumulateStrokeTime(float DeltaTime)
{
ActiveStrokeTime += DeltaTime;
}
FFrame3d UMeshSculptToolBase::ComputeStampRegionPlane(const FFrame3d& StampFrame, const TSet<int32>& StampTriangles, bool bIgnoreDepth, bool bViewAligned, bool bInvDistFalloff)
{
check(false);
return FFrame3d();
}
FFrame3d UMeshSculptToolBase::ComputeStampRegionPlane(const FFrame3d& StampFrame, const TArray<int32>& StampTriangles, bool bIgnoreDepth, bool bViewAligned, bool bInvDistFalloff)
{
const FDynamicMesh3* Mesh = GetSculptMesh();
double FalloffRadius = GetCurrentBrushRadius();
if (bInvDistFalloff)
{
FalloffRadius *= 0.5;
}
FVector3d StampNormal = StampFrame.Z();
FVector3d AverageNormal(0, 0, 0);
FVector3d AveragePos(0, 0, 0);
double WeightSum = 0;
for (int TriID : StampTriangles)
{
FVector3d Normal, Centroid; double Area;
Mesh->GetTriInfo(TriID, Normal, Area, Centroid);
if (Normal.Dot(StampNormal) < -0.2) // ignore back-facing (heuristic to avoid "other side")
{
continue;
}
double Distance = UE::Geometry::Distance(StampFrame.Origin, Centroid);
double NormalizedDistance = (Distance / FalloffRadius) + 0.0001;
double Weight = Area;
if (bInvDistFalloff)
{
double RampT = FMathd::Clamp(1.0 - NormalizedDistance, 0.0, 1.0);
Weight *= FMathd::Clamp(RampT * RampT * RampT, 0.0, 1.0);
}
else
{
if (NormalizedDistance > 0.5)
{
double d = FMathd::Clamp((NormalizedDistance - 0.5) / (1.0 - 0.5), 0.0, 1.0);
double t = (1.0 - d * d);
Weight *= (t * t * t);
}
}
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 -= GetCurrentBrushDepth() * GetCurrentBrushRadius() * Result.Z();
}
return Result;
}
void UMeshSculptToolBase::UpdateStrokeReferencePlaneForROI(const FFrame3d& StampFrame, const TArray<int32>& TriangleROI, bool bViewAligned)
{
StrokePlane = ComputeStampRegionPlane(GetBrushFrameLocal(), TriangleROI, false, bViewAligned);
}
void UMeshSculptToolBase::UpdateStrokeReferencePlaneFromWorkPlane()
{
StrokePlane = FFrame3d(
CurTargetTransform.InverseTransformPosition((FVector3d)GizmoProperties->Position),
CurTargetTransform.GetRotation().Inverse() * (FQuaterniond)GizmoProperties->Rotation);
}
void UMeshSculptToolBase::InitializeBrushSizeRange(const FAxisAlignedBox3d& TargetBounds)
{
double MaxDimension = TargetBounds.MaxDim();
BrushRelativeSizeRange = FInterval1d(MaxDimension * 0.01, MaxDimension);
BrushProperties->BrushSize.InitializeWorldSizeRange(
TInterval<float>((float)BrushRelativeSizeRange.Min, (float)BrushRelativeSizeRange.Max));
CalculateBrushRadius();
}
void UMeshSculptToolBase::CalculateBrushRadius()
{
CurrentBrushRadius = BrushProperties->BrushSize.GetWorldRadius();
}
double UMeshSculptToolBase::GetActiveBrushRadius()
{
if (GetBrushSizePressureSensitivityEnabled())
{
if (BrushProperties->BrushSize.SizeType == EBrushToolSizeType::Adaptive)
{
return 0.5 * BrushProperties->BrushSize.WorldSizeRange.Interpolate(BrushProperties->BrushSize.AdaptiveSize * GetActivePressure());
}
if (BrushProperties->BrushSize.SizeType == EBrushToolSizeType::World)
{
const TInterval<float> WorldSizeRange = BrushProperties->BrushSize.WorldSizeRange;
return FMath::Lerp(WorldSizeRange.Min, BrushProperties->BrushSize.GetWorldRadius(), GetActivePressure());
}
}
return CurrentBrushRadius;
}
double UMeshSculptToolBase::GetActiveBrushStrength()
{
TUniquePtr<FMeshSculptBrushOp>& BrushOp = GetActiveBrushOp();
if (BrushOp->PropertySet.IsValid())
{
double Strength = GetCurrentBrushStrength();
if (GetBrushStrengthPressureEnabled())
{
Strength *= GetActivePressure();
}
return FMathd::Clamp(Strength, 0.0, 1.0);
}
return 1.0;
}
double UMeshSculptToolBase::GetCurrentBrushStrength()
{
TUniquePtr<FMeshSculptBrushOp>& BrushOp = GetActiveBrushOp();
if (BrushOp->PropertySet.IsValid())
{
return FMathd::Clamp(BrushOp->PropertySet->GetStrength(), 0.0, 1.0);
}
return 1.0;
}
double UMeshSculptToolBase::GetCurrentBrushDepth()
{
TUniquePtr<FMeshSculptBrushOp>& BrushOp = GetActiveBrushOp();
if (BrushOp->PropertySet.IsValid())
{
return FMathd::Clamp(BrushOp->PropertySet->GetDepth(), -1.0, 1.0);
}
return 0.0;
}
void UMeshSculptToolBase::IncreaseBrushRadiusAction()
{
BrushProperties->BrushSize.IncreaseRadius(false);
NotifyOfPropertyChangeByTool(BrushProperties);
CalculateBrushRadius();
}
void UMeshSculptToolBase::DecreaseBrushRadiusAction()
{
BrushProperties->BrushSize.DecreaseRadius(false);
NotifyOfPropertyChangeByTool(BrushProperties);
CalculateBrushRadius();
}
void UMeshSculptToolBase::IncreaseBrushRadiusSmallStepAction()
{
BrushProperties->BrushSize.IncreaseRadius(true);
NotifyOfPropertyChangeByTool(BrushProperties);
CalculateBrushRadius();
}
void UMeshSculptToolBase::DecreaseBrushRadiusSmallStepAction()
{
BrushProperties->BrushSize.DecreaseRadius(true);
NotifyOfPropertyChangeByTool(BrushProperties);
CalculateBrushRadius();
}
FBox UMeshSculptToolBase::GetWorldSpaceFocusBox()
{
if (LastBrushTriangleID == INDEX_NONE)
{
return Super::GetWorldSpaceFocusBox();
}
FVector Center = LastBrushFrameWorld.Origin;
double Size = GetCurrentBrushRadius();
return FBox(Center - FVector(Size), Center + FVector(Size));
}
void UMeshSculptToolBase::SetViewPropertiesEnabled(bool bNewValue)
{
if (ViewProperties)
{
SetToolPropertySourceEnabled(ViewProperties, bNewValue);
}
}
void UMeshSculptToolBase::UpdateWireframeVisibility(bool bNewValue)
{
GetSculptMeshComponent()->SetEnableWireframeRenderPass(bNewValue);
}
void UMeshSculptToolBase::UpdateFlatShadingSetting(bool bNewValue)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetScalarParameterValue(TEXT("FlatShading"), (bNewValue) ? 1.0f : 0.0f);
}
}
void UMeshSculptToolBase::UpdateColorSetting(FLinearColor NewColor)
{
if (ViewProperties->MaterialMode != EMeshEditingMaterialModes::Transparent)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetVectorParameterValue(TEXT("Color"), NewColor);
}
}
}
void UMeshSculptToolBase::UpdateTransparentColorSetting(FLinearColor NewColor)
{
// only want to update the active material if it is the transparent one...
if (ViewProperties->MaterialMode == EMeshEditingMaterialModes::Transparent)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetVectorParameterValue(TEXT("Color"), NewColor);
}
}
}
void UMeshSculptToolBase::UpdateImageSetting(UTexture2D* NewImage)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetTextureParameterValue(TEXT("ImageTexture"), NewImage);
}
}
void UMeshSculptToolBase::UpdateOpacitySetting(double Opacity)
{
if (ActiveOverrideMaterial != nullptr)
{
ActiveOverrideMaterial->SetScalarParameterValue(TEXT("Opacity"), Opacity);
}
}
void UMeshSculptToolBase::UpdateTwoSidedSetting(bool bOn)
{
if (ViewProperties->MaterialMode == EMeshEditingMaterialModes::Transparent)
{
ActiveOverrideMaterial = ToolSetupUtil::GetTransparentSculptMaterial(GetToolManager(),
ViewProperties->TransparentMaterialColor, ViewProperties->Opacity, bOn);
if (ActiveOverrideMaterial)
{
GetSculptMeshComponent()->SetOverrideRenderMaterial(ActiveOverrideMaterial);
}
}
}
void UMeshSculptToolBase::UpdateCustomMaterial(TWeakObjectPtr<UMaterialInterface> NewMaterial)
{
if (ViewProperties->MaterialMode == EMeshEditingMaterialModes::Custom)
{
if (NewMaterial.IsValid())
{
ActiveOverrideMaterial = UMaterialInstanceDynamic::Create(NewMaterial.Get(), this);
GetSculptMeshComponent()->SetOverrideRenderMaterial(ActiveOverrideMaterial);
}
else
{
GetSculptMeshComponent()->ClearOverrideRenderMaterial();
ActiveOverrideMaterial = nullptr;
}
}
}
void UMeshSculptToolBase::UpdateMaterialMode(EMeshEditingMaterialModes MaterialMode)
{
if (MaterialMode == EMeshEditingMaterialModes::ExistingMaterial)
{
GetSculptMeshComponent()->ClearOverrideRenderMaterial();
if (UPrimitiveComponent* TargetComponent = UE::ToolTarget::GetTargetComponent(Target))
{
GetSculptMeshComponent()->SetShadowsEnabled(TargetComponent->bCastDynamicShadow);
}
ActiveOverrideMaterial = nullptr;
}
else
{
if (MaterialMode == EMeshEditingMaterialModes::Custom)
{
if (ViewProperties->CustomMaterial.IsValid())
{
ActiveOverrideMaterial = UMaterialInstanceDynamic::Create(ViewProperties->CustomMaterial.Get(), this);
}
else
{
GetSculptMeshComponent()->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::VertexColor)
{
ActiveOverrideMaterial = ToolSetupUtil::GetVertexColorMaterial(GetToolManager());
}
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;
}
if (SculptMaterial != nullptr )
{
ActiveOverrideMaterial = UMaterialInstanceDynamic::Create(SculptMaterial, this);
}
}
if (ActiveOverrideMaterial != nullptr)
{
GetSculptMeshComponent()->SetOverrideRenderMaterial(ActiveOverrideMaterial);
ActiveOverrideMaterial->SetScalarParameterValue(TEXT("FlatShading"), (ViewProperties->bFlatShading) ? 1.0f : 0.0f);
}
GetSculptMeshComponent()->SetShadowsEnabled(false);
}
}
void UMeshSculptToolBase::InitializeIndicator()
{
// register and spawn brush indicator gizmo
GetToolManager()->GetPairedGizmoManager()->RegisterGizmoType(VertexSculptIndicatorGizmoType, NewObject<UBrushStampIndicatorBuilder>());
BrushIndicator = GetToolManager()->GetPairedGizmoManager()->CreateGizmo<UBrushStampIndicator>(VertexSculptIndicatorGizmoType, FString(), this);
BrushIndicatorMesh = MakeBrushIndicatorMesh(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);
bIsVolumetricIndicator = true;
}
bool UMeshSculptToolBase::GetIsVolumetricIndicator()
{
return bIsVolumetricIndicator;
}
void UMeshSculptToolBase::ConfigureIndicator(bool bVolumetric)
{
if (bIsVolumetricIndicator == bVolumetric) return;
bIsVolumetricIndicator = bVolumetric;
BrushIndicatorMesh->SetVisible(GetIndicatorVisibility() && bIsVolumetricIndicator);
if (bVolumetric)
{
BrushIndicator->bDrawRadiusCircle = false;
}
else
{
BrushIndicator->bDrawRadiusCircle = true;
}
}
void UMeshSculptToolBase::SetIndicatorVisibility(bool bVisible)
{
if (GetIndicatorVisibility() != bVisible)
{
BrushIndicator->bVisible = bVisible;
BrushIndicatorMesh->SetVisible(bVisible && bIsVolumetricIndicator);
}
}
bool UMeshSculptToolBase::GetIndicatorVisibility() const
{
return BrushIndicator->bVisible;
}
UPreviewMesh* UMeshSculptToolBase::MakeBrushIndicatorMesh(UObject* Parent, UWorld* World)
{
UPreviewMesh* SphereMesh = NewObject<UPreviewMesh>(Parent);
SphereMesh->CreateInWorld(World, FTransform::Identity);
FSphereGenerator SphereGen;
SphereGen.NumPhi = SphereGen.NumTheta = 32;
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 UMeshSculptToolBase::UpdateWorkPlane()
{
bool bGizmoVisible = ShowWorkPlane() && (GizmoProperties->bShowGizmo);
UpdateFixedPlaneGizmoVisibility(bGizmoVisible);
GizmoProperties->bPropertySetEnabled = ShowWorkPlane();
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 = GetLocalRay(CursorWorldRay);
int32 SculptMeshTid = FindHitSculptMeshTriangle(LocalRay);
bool bObjectHit = (SculptMeshTid != IndexConstants::InvalidID);
if (bWorldHit &&
(bObjectHit == false || (CursorWorldRay.GetParameter(Result.ImpactPoint) < CursorWorldRay.GetParameter((FVector)HoverStamp.WorldFrame.Origin))))
{
SetFixedSculptPlaneFromWorldPos(Result.ImpactPoint, Result.ImpactNormal, PendingWorkPlaneUpdate);
}
else
{
FVector3d Normal = HoverStamp.WorldFrame.Z();
// We're updating from a sculpt mesh hit. We can expect HoverStamp to be at the proper location,
// but the alignment might differ depending on how the cient is brushing, so get the triangle
// normal if we need it.
if (bObjectHit && PendingWorkPlaneUpdate == EPendingWorkPlaneUpdate::MoveToHitPositionNormal)
{
FDynamicMesh3* Mesh = GetSculptMesh();
if (ensure(Mesh))
{
Normal = Mesh->GetTriNormal(SculptMeshTid);
}
}
SetFixedSculptPlaneFromWorldPos((FVector)HoverStamp.WorldFrame.Origin, Normal, PendingWorkPlaneUpdate);
}
PendingWorkPlaneUpdate = EPendingWorkPlaneUpdate::NoUpdatePending;
}
}
void UMeshSculptToolBase::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 UMeshSculptToolBase::PlaneTransformChanged(UTransformProxy* Proxy, FTransform Transform)
{
UpdateFixedSculptPlaneRotation(Transform.GetRotation());
UpdateFixedSculptPlanePosition(Transform.GetLocation());
}
void UMeshSculptToolBase::UpdateFixedSculptPlanePosition(const FVector& Position)
{
GizmoProperties->Position = Position;
GizmoPositionWatcher.SilentUpdate();
}
void UMeshSculptToolBase::UpdateFixedSculptPlaneRotation(const FQuat& Rotation)
{
GizmoProperties->Rotation = Rotation;
GizmoRotationWatcher.SilentUpdate();
}
void UMeshSculptToolBase::UpdateGizmoFromProperties()
{
if (PlaneTransformGizmo != nullptr)
{
PlaneTransformGizmo->SetNewGizmoTransform(FTransform(GizmoProperties->Rotation, GizmoProperties->Position));
}
}
void UMeshSculptToolBase::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 UMeshSculptToolBase::RegisterActions(FInteractiveToolActionSet& ActionSet)
{
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 1,
TEXT("NextBrushMode"),
LOCTEXT("SculptNextBrushMode", "Next Brush Type"),
LOCTEXT("SculptNextBrushModeTooltip", "Cycle to next Brush Type"),
EModifierKey::None, EKeys::A,
[this]() { NextBrushModeAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 2,
TEXT("PreviousBrushMode"),
LOCTEXT("SculptPreviousBrushMode", "Previous Brush Type"),
LOCTEXT("SculptPreviousBrushModeTooltip", "Cycle to previous Brush Type"),
EModifierKey::None, EKeys::Q,
[this]() { PreviousBrushModeAction(); });
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::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::IncreaseBrushSize,
TEXT("SculptIncreaseRadius"),
LOCTEXT("SculptIncreaseRadius", "Increase Radius"),
LOCTEXT("SculptIncreaseRadiusTooltip", "Increase Brush Radius"),
EModifierKey::None, EKeys::RightBracket,
[this]() { IncreaseBrushRadiusAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::DecreaseBrushSize,
TEXT("SculptDecreaseRadius"),
LOCTEXT("SculptDecreaseRadius", "Decrease Radius"),
LOCTEXT("SculptDecreaseRadiusTooltip", "Decrease Brush Radius"),
EModifierKey::None, EKeys::LeftBracket,
[this]() { DecreaseBrushRadiusAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 54,
TEXT("SculptIncreaseRadiusSmallStep"),
LOCTEXT("SculptIncreaseRadius", "Increase Radius"),
LOCTEXT("SculptIncreaseRadiusTooltip", "Increase Brush Radius"),
EModifierKey::Shift, EKeys::RightBracket,
[this]() { IncreaseBrushRadiusSmallStepAction(); });
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 55,
TEXT("SculptDecreaseRadiusSmallStemp"),
LOCTEXT("SculptDecreaseRadius", "Decrease Radius"),
LOCTEXT("SculptDecreaseRadiusTooltip", "Decrease Brush Radius"),
EModifierKey::Shift, EKeys::LeftBracket,
[this]() { DecreaseBrushRadiusSmallStepAction(); });
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; });
}
#undef LOCTEXT_NAMESPACE