// 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 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(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(); 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(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(); 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 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(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 UnProjectedStampPoints; double Spacing = FMath::Clamp(BrushProperties->Spacing * BrushProperties->BrushSize.GetWorldRadius() * 2 , 0.1, 100000); TArray 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(); 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& 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 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* 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 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* 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* 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* FoundProps = BrushOpPropSets.Find(Identifier); if (FoundProps != nullptr) { SetToolPropertySourceEnabled(*FoundProps, bBrushOpPropsVisible); PrimaryVisiblePropSet = *FoundProps; PrimaryBrushOp->PropertySet = PrimaryVisiblePropSet; } } void UMeshSculptToolBase::SetActiveSecondaryBrushType(int32 Identifier) { TUniquePtr* Factory = SecondaryBrushOpFactories.Find(Identifier); if (Factory == nullptr) { ensure(false); return; } if (SecondaryVisiblePropSet != nullptr) { SetToolPropertySourceEnabled(SecondaryVisiblePropSet, false); SecondaryVisiblePropSet = nullptr; } SecondaryBrushOp = (*Factory)->Build(); TSharedPtr SecondaryFalloff = MakeShared();; SecondaryFalloff->FalloffFunc = UE::SculptFalloffs::MakeStandardSmoothFalloff(); SecondaryBrushOp->Falloff = SecondaryFalloff; TObjectPtr* FoundProps = SecondaryBrushOpPropSets.Find(Identifier); if (FoundProps != nullptr) { SetToolPropertySourceEnabled(*FoundProps, bBrushOpPropsVisible); SecondaryVisiblePropSet = *FoundProps; SecondaryBrushOp->PropertySet = SecondaryVisiblePropSet; } } TUniquePtr& 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(); 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& 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& 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::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::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 UpdateStampPosition, TFunction ExecuteStampOperation) { ProcessPerTickStamps(UpdateStampPosition, [](int StampCount) {}, ExecuteStampOperation, [] {}); } void UMeshSculptToolBase::ProcessPerTickStamps(TFunction UpdateStampPosition, TFunction PreExecuteStampsOperation, TFunction ExecuteStampOperation, TFunction 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 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 CachedActivePressure(ActivePressure, ActivePressure); TArray UnProjectedStampPoints; if (StrokePath.Num() > 1) { TArray 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& 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& StampTriangles, bool bIgnoreDepth, bool bViewAligned, bool bInvDistFalloff) { check(false); return FFrame3d(); } FFrame3d UMeshSculptToolBase::ComputeStampRegionPlane(const FFrame3d& StampFrame, const TArray& 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& 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)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 WorldSizeRange = BrushProperties->BrushSize.WorldSizeRange; return FMath::Lerp(WorldSizeRange.Min, BrushProperties->BrushSize.GetWorldRadius(), GetActivePressure()); } } return CurrentBrushRadius; } double UMeshSculptToolBase::GetActiveBrushStrength() { TUniquePtr& 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& BrushOp = GetActiveBrushOp(); if (BrushOp->PropertySet.IsValid()) { return FMathd::Clamp(BrushOp->PropertySet->GetStrength(), 0.0, 1.0); } return 1.0; } double UMeshSculptToolBase::GetCurrentBrushDepth() { TUniquePtr& 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 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()); BrushIndicator = GetToolManager()->GetPairedGizmoManager()->CreateGizmo(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(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(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