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