1705 lines
55 KiB
C++
1705 lines
55 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "DeformMeshPolygonsTool.h"
|
|
|
|
#include "Curves/RichCurve.h"
|
|
#include "DynamicMesh/MeshNormals.h"
|
|
#include "Engine/World.h"
|
|
#include "InteractiveToolManager.h"
|
|
#include "ModelingTaskTypes.h"
|
|
#include "ModelingToolTargetUtil.h"
|
|
#include "Solvers/ConstrainedMeshDeformer.h"
|
|
#include "ToolBuilderUtil.h"
|
|
#include "ToolSceneQueriesUtil.h"
|
|
#include "ToolSetupUtil.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(DeformMeshPolygonsTool)
|
|
|
|
using namespace UE::Geometry;
|
|
|
|
#define LOCTEXT_NAMESPACE "UDeformMeshPolygonsTool"
|
|
|
|
class FDeformTask;
|
|
|
|
|
|
//Stores per-vertex data needed by the laplacian deformer object
|
|
//TODO: May be a candidate for a subclass of the FGroupTopologyLaplacianDeformer
|
|
struct FDeformerVertexConstraintData
|
|
{
|
|
FDeformerVertexConstraintData& operator=(const FDeformerVertexConstraintData& other)
|
|
{
|
|
Position = other.Position;
|
|
Weight = other.Weight;
|
|
bPostFix = other.bPostFix;
|
|
return *this;
|
|
}
|
|
|
|
FVector3d Position;
|
|
double Weight{0.0};
|
|
bool bPostFix{false};
|
|
};
|
|
|
|
/**
|
|
* FDeformTask is an object which wraps an asynchronous task to be run multiple times on a separate thread.
|
|
* The Laplacian deformation process requires the use of potentially large sparse matrices and sparse multiplication.
|
|
*
|
|
* Expected usage:
|
|
*
|
|
*
|
|
* // define constraints. Need Constraints[VertID] to hold the constraints for the corresponding vertex.
|
|
* TArray<FDeformerVertexConstraintData> Constraints;
|
|
* ....
|
|
*
|
|
* // populate with the VertexIDs of the vertices that are in the region you wish to deform.
|
|
* TArray<int32> SrcVertIDs; //Basically a mini-index buffer.
|
|
* ...
|
|
*
|
|
* // Create or reuse a laplacian deformation task.
|
|
* FDeformTask* DeformTask = New FDeformTask(WeightScheme);
|
|
*
|
|
* // the deformer will have to build a new mesh that represents the regions in SrcVertIDs;
|
|
* // but set this to false on subsequent calls to UpdateDeformer if the SrcVertIDs array hasn't changed.
|
|
* bool bRequiresRegion = true;
|
|
|
|
* DefTask->UpdateDeformer(WeightScheme, Mesh, Constraints, SrcVertIDs, bRequiresRegion);
|
|
*
|
|
* DeformTask->DoWork(); or DeformTask->StartBackgroundTask(); //which calls DoWork on background thread.
|
|
*
|
|
* // wheh DeformTask->IsDone == true; you can copy the results back to the mesh
|
|
* DeformTask->ExportResults(Mesh);
|
|
*
|
|
* Note: if only the positions in the Constraints change (e.g. handle positions) then subsequent calls
|
|
* to UpdateDeformer() and DoWork() will be much faster as the matrix system will not be rebuilt or re-factored
|
|
*/
|
|
class FConstrainedMeshDeformerTask : public FNonAbandonableTask
|
|
{
|
|
friend class FAsyncTask<FDeformTask>;
|
|
|
|
public:
|
|
enum
|
|
{
|
|
INACTIVE_SUBSET_ID = -1
|
|
};
|
|
|
|
FConstrainedMeshDeformerTask(const ELaplacianWeightScheme SelectedWeightScheme)
|
|
: LaplacianWeightScheme(SelectedWeightScheme)
|
|
{
|
|
}
|
|
|
|
virtual ~FConstrainedMeshDeformerTask(){};
|
|
|
|
//NO idea what this is meant to do. Performance analysis maybe? Scheduling data?
|
|
FORCEINLINE TStatId GetStatId() const
|
|
{
|
|
RETURN_QUICK_DECLARE_CYCLE_STAT(FConstrainedMeshDeformerTask, STATGROUP_ThreadPoolAsyncTasks);
|
|
}
|
|
|
|
|
|
/** Called by the main thread in the tool, this copies the Constraint buffer right before the task begins on another thread.
|
|
* Ensures the FConstrainedMeshDeformer is using correct mesh subset and the selected settings, then updates on change in properties, i.e. weight scheme */
|
|
void UpdateDeformer(const ELaplacianWeightScheme SelectedWeightScheme, const FDynamicMesh3& Mesh,
|
|
const TArray<FDeformerVertexConstraintData>& ConstraintArray,
|
|
const TArray<int32>& SrcIDBufferSubset, bool bNewTransaction, const FRichCurve* Curve);
|
|
|
|
/** Required by the FAsyncTaskExecutor */
|
|
void SetAbortSource(bool* bAbort)
|
|
{
|
|
bAbortSource = bAbort;
|
|
};
|
|
|
|
/** Called by the FAsyncTask<FDeformTask> object for background computation. */
|
|
void DoWork();
|
|
|
|
|
|
/** Updates the positions in the target mesh for regions that correspond to the subset mesh */
|
|
void ExportResults(FDynamicMesh3& TargetMesh) const;
|
|
|
|
private:
|
|
/** Creates the mesh (i.e. SubsetMesh) that corresponds to the region of the SrcMesh defined by the partial index buffer SrcIDBufferSubset */
|
|
void InitializeSubsetMesh(const FDynamicMesh3& SrcMesh, const TArray<int32>& SrcIDBufferSubset);
|
|
|
|
/** Attenuates the weights of the constraints using the selected curve */
|
|
void ApplyAttenuation();
|
|
|
|
/** Denotes the weight scheme being used by the running background task. Changes when selected property changes in editor. */
|
|
ELaplacianWeightScheme LaplacianWeightScheme;
|
|
|
|
/** positions for each vertex in the subset mesh - for use in the deformer */
|
|
TArray<FVector3d> SubsetPositionBuffer;
|
|
|
|
/** constraint data for each vertex in subset mesh - for use by the deformer*/
|
|
TArray<FDeformerVertexConstraintData> SubsetConstraintBuffer;
|
|
|
|
FRichCurve WeightAttenuationCurve;
|
|
|
|
/** True only for the first update, and then false for the duration of the Input transaction
|
|
* It's passed in and copied in UpdateDeformer() */
|
|
bool bIsNewTransaction = true;
|
|
|
|
/** When true, the constraint weights will be attenuated based on distance using the provided curve object*/
|
|
bool bAttenuateWeights = false;
|
|
|
|
/** The abort bool used by the Task Deleter */
|
|
bool* bAbortSource = nullptr;
|
|
|
|
/** Used to initialize the array mapping, updated during the UpdateDeformer() function */
|
|
int SrcMeshMaxVertexID = 0;
|
|
|
|
/** A subset of the original mesh */
|
|
FDynamicMesh3 SubsetMesh;
|
|
|
|
/** Maps Subset Mesh VertexID to Src Mesh VertexID */
|
|
TArray<int32> SubsetVertexIDToSrcVertexIDMap;
|
|
|
|
/** Laplacian deformer object gets rebuilt each new transaction */
|
|
TUniquePtr<UE::Solvers::IConstrainedMeshSolver> ConstrainedDeformer;
|
|
};
|
|
|
|
class FGroupTopologyLaplacianDeformer : public FGroupTopologyDeformer
|
|
{
|
|
public:
|
|
FGroupTopologyLaplacianDeformer() = default;
|
|
|
|
virtual ~FGroupTopologyLaplacianDeformer();
|
|
|
|
/** Used to begin a procedural addition of modified vertices */
|
|
inline void ResetModifiedVertices()
|
|
{
|
|
ModifiedVertices.Empty();
|
|
};
|
|
|
|
/** Change tracking */
|
|
template<typename ValidSetAppendContainerType>
|
|
void RecordModifiedVertices(const ValidSetAppendContainerType& Container)
|
|
{
|
|
ModifiedVertices.Empty();
|
|
ModifiedVertices.Append(Container);
|
|
}
|
|
|
|
/** Used to iteratively add to the active change set (TSet<>)*/
|
|
inline void RecordModifiedVertex(int32 VertexID)
|
|
{
|
|
ModifiedVertices.Add(VertexID);
|
|
};
|
|
|
|
|
|
void SetActiveHandleFaces(const TArray<int>& FaceGroupIDs) override;
|
|
void SetActiveHandleEdges(const TArray<int>& TopologyEdgeIDs) override;
|
|
void SetActiveHandleCorners(const TArray<int>& TopologyCornerIDs) override;
|
|
|
|
|
|
/** Allocates shared storage for use in task synchronization */
|
|
void InitBackgroundWorker(const ELaplacianWeightScheme WeightScheme);
|
|
|
|
|
|
/** Coordinates the background tasks. Returns false if the worker was already running */
|
|
bool UpdateAndLaunchdWorker(const ELaplacianWeightScheme WeightScheme, const FRichCurve* Curve = nullptr);
|
|
|
|
/** Capture data about background task state.*/
|
|
bool IsTaskInFlight() const;
|
|
|
|
|
|
/** Sets the SrcMeshConstraintBuffer to have a size of MaxVertexID, and initializes with the current mesh positions, but weight zero*/
|
|
void InitializeConstraintBuffer();
|
|
|
|
/** Given an array of Group IDs, update the selection and record vertices */
|
|
void UpdateSelection(const FDynamicMesh3* TargetMesh, const TArray<int>& Groups, bool bLocalizeDeformation);
|
|
|
|
/** Updates the mesh preview and/or solvers upon user input, provided a deformation strategy */
|
|
void UpdateSolution(FDynamicMesh3* TargetMesh,
|
|
const TFunction<FVector3d(FDynamicMesh3* Mesh, int)>& HandleVertexDeformFunc) override;
|
|
|
|
/** Updates the vertex positions of the mesh with the result from the last deformation solve. */
|
|
void ExportDeformedPositions(FDynamicMesh3* TargetMesh);
|
|
|
|
/** Returns true if the asynchronous task has finished. */
|
|
inline bool IsDone()
|
|
{
|
|
return AsyncMeshDeformTask == nullptr || AsyncMeshDeformTask->IsDone();
|
|
};
|
|
|
|
/** Triggers abort on task and passes off ownership to deleter object */
|
|
inline void Shutdown();
|
|
|
|
const TArray<FROIFace>& GetROIFaces() const
|
|
{
|
|
return ROIFaces;
|
|
}
|
|
|
|
/** Stores the position of the vertex constraints and corresponding weights for the entire mesh. This is used as a form of scratch space.*/
|
|
TArray<FDeformerVertexConstraintData> SrcMeshConstraintBuffer;
|
|
|
|
/** Array of vertex indices organized in groups of three - basically an index buffer - that defines the subset of the mesh that the deformation task will work on.*/
|
|
TArray<int32> SubsetIDBuffer;
|
|
|
|
/** Need to update the task with the current submesh */
|
|
bool bTaskSubmeshIsDirty = true;
|
|
|
|
/** Asynchronous task object. This object deals with expensive matrix functionality that computes the deformation of a local mesh. */
|
|
FAsyncTaskExecuterWithAbort<FConstrainedMeshDeformerTask>* AsyncMeshDeformTask = nullptr;
|
|
|
|
|
|
/** The weight which will be applied to the constraints corresponding to the handle vertices. */
|
|
double HandleWeights = 1.0;
|
|
|
|
/** This is set to true whenever the user interacts with the tool under laplacian deformation mode.
|
|
* It is set to false immediately before beginning a background task and cannot be set to false again until the work is done. */
|
|
bool bDeformerNeedsToRun = false;
|
|
|
|
|
|
/** When true, tells the solver to attempt to postfix the actual position of the handles to the constrained position */
|
|
bool bPostfixHandles = false;
|
|
|
|
//This is set to false only after
|
|
// 1) the asynchronous deformation task is complete
|
|
// 2) the main thread has seen it complete, and
|
|
// 3) the main thread updates the vertex positions of the mesh one last time
|
|
bool bVertexPositionsNeedSync = false;
|
|
|
|
bool bLocalize = true;
|
|
};
|
|
|
|
|
|
//////////////////////////////
|
|
// DEBUG_SETTINGS
|
|
|
|
//Draw white triangles defining the selection subset
|
|
//#define DEBUG_ROI_TRIANGLES
|
|
|
|
//Draw pink circles around the handles
|
|
//#define DEBUG_ROI_HANDLES
|
|
|
|
//Draw points on the ROI vertices, White => Weight == 0, Black => Weight == 1
|
|
//#define DEBUG_ROI_WEIGHTS
|
|
|
|
//////////////////////////////
|
|
|
|
|
|
/*
|
|
* ToolBuilder
|
|
*/
|
|
UMeshSurfacePointTool* UDeformMeshPolygonsToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
|
|
{
|
|
UDeformMeshPolygonsTool* DeformTool = NewObject<UDeformMeshPolygonsTool>(SceneState.ToolManager);
|
|
DeformTool->SetWorld(SceneState.World);
|
|
return DeformTool;
|
|
}
|
|
|
|
/*
|
|
* Tool
|
|
*/
|
|
UDeformMeshPolygonsTransformProperties::UDeformMeshPolygonsTransformProperties()
|
|
{
|
|
DeformationStrategy = EGroupTopologyDeformationStrategy::Laplacian;
|
|
TransformMode = EQuickTransformerMode::AxisTranslation;
|
|
bSelectVertices = true;
|
|
bSelectFaces = true;
|
|
bSelectEdges = true;
|
|
bShowWireframe = false;
|
|
}
|
|
|
|
/*
|
|
* Asynchronous Task
|
|
*/
|
|
|
|
|
|
void FConstrainedMeshDeformerTask::UpdateDeformer(const ELaplacianWeightScheme SelectedWeightScheme,
|
|
const FDynamicMesh3& SrcMesh,
|
|
const TArray<FDeformerVertexConstraintData>& ConstraintArray,
|
|
const TArray<int32>& SrcIDBufferSubset, bool bNewTransaction,
|
|
const FRichCurve* Curve)
|
|
{
|
|
bIsNewTransaction = bNewTransaction;
|
|
SrcMeshMaxVertexID = SrcMesh.MaxVertexID();
|
|
|
|
LaplacianWeightScheme = SelectedWeightScheme;
|
|
|
|
bAttenuateWeights = (Curve != nullptr);
|
|
if (bAttenuateWeights)
|
|
{
|
|
WeightAttenuationCurve = *Curve;
|
|
}
|
|
|
|
// Set-up the subset mesh.
|
|
if (bIsNewTransaction)
|
|
{
|
|
//Copy the part of the mesh we want to deform into the SubsetMesh and create map from Src Mesh to the SubsetMesh.
|
|
InitializeSubsetMesh(SrcMesh, SrcIDBufferSubset);
|
|
}
|
|
|
|
// only want the subset of constraints that correspond to our subset mesh.
|
|
{
|
|
const int32 NumSubsetVerts = SubsetVertexIDToSrcVertexIDMap.Num();
|
|
SubsetConstraintBuffer.Empty(NumSubsetVerts);
|
|
SubsetConstraintBuffer.AddUninitialized(NumSubsetVerts);
|
|
|
|
for (int32 SubVertexID = 0; SubVertexID < SubsetVertexIDToSrcVertexIDMap.Num(); ++SubVertexID)
|
|
{
|
|
int32 SrcVtxID = SubsetVertexIDToSrcVertexIDMap[SubVertexID];
|
|
SubsetConstraintBuffer[SubVertexID] = ConstraintArray[SrcVtxID];
|
|
}
|
|
}
|
|
|
|
check(bIsNewTransaction || ConstrainedDeformer.IsValid());
|
|
}
|
|
|
|
|
|
void FConstrainedMeshDeformerTask::DoWork()
|
|
{
|
|
//TODO: (simple optimization) -
|
|
// Instead of SrcVertexIDtoSubsetVertexIDMap, use SubsetVertexIDToSetVertexIDMap - then we can use the VertexIndicesItr()
|
|
// on the SubsetMesh to minimize the quantity of vertex indices we need to iterate at every following step.
|
|
|
|
if (*bAbortSource == true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (bIsNewTransaction) //Will only be true once per input transaction (click+drag)
|
|
{
|
|
// Create a new deformation solver.
|
|
ConstrainedDeformer = UE::MeshDeformation::ConstructConstrainedMeshDeformer(LaplacianWeightScheme, SubsetMesh);
|
|
|
|
if (bAttenuateWeights)
|
|
{
|
|
ApplyAttenuation();
|
|
}
|
|
|
|
//Update our deformer's constraints before deforming using the copy of the constraint buffer
|
|
for (int32 SubsetVertexID = 0; SubsetVertexID < SubsetConstraintBuffer.Num(); ++SubsetVertexID)
|
|
{
|
|
FDeformerVertexConstraintData& CData = SubsetConstraintBuffer[SubsetVertexID];
|
|
ConstrainedDeformer->AddConstraint(SubsetVertexID, CData.Weight, CData.Position, CData.bPostFix);
|
|
}
|
|
|
|
bIsNewTransaction = false;
|
|
}
|
|
else
|
|
{
|
|
//This else block is run every consecutive frame after the start of the input transaction because UpdateConstraintPosition() is very cheap (no factorizing or rebuilding)
|
|
//Update only the positions of the constraints, as the weights cannot change mid-transaction
|
|
|
|
for (int32 SubsetVertexID = 0; SubsetVertexID < SubsetConstraintBuffer.Num(); ++SubsetVertexID)
|
|
{
|
|
FDeformerVertexConstraintData& CData = SubsetConstraintBuffer[SubsetVertexID];
|
|
ConstrainedDeformer->UpdateConstraintPosition(SubsetVertexID, CData.Position, CData.bPostFix);
|
|
}
|
|
}
|
|
|
|
if (*bAbortSource == true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Run the deformation process
|
|
|
|
const bool bSuccessfulSolve = ConstrainedDeformer->Deform(SubsetPositionBuffer);
|
|
|
|
|
|
if (bSuccessfulSolve)
|
|
{
|
|
if (*bAbortSource == true) //-V547
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//UE_LOG(LogTemp, Warning, TEXT("Laplacian deformation failed"));
|
|
}
|
|
}
|
|
|
|
inline void FConstrainedMeshDeformerTask::InitializeSubsetMesh(const FDynamicMesh3& SrcMesh,
|
|
const TArray<int32>& SrcIDBufferSubset)
|
|
{
|
|
//These can be re-used until the user stops dragging
|
|
SubsetMesh.Clear();
|
|
SubsetPositionBuffer.Reset();
|
|
|
|
//Initialize every element to -1, helps us keep track of vertices we've already added while iterating the triangles
|
|
TArray<int32> SrcVertexIDToSubsetVertexIDMap;
|
|
SrcVertexIDToSubsetVertexIDMap.Init(INACTIVE_SUBSET_ID, SrcMeshMaxVertexID);
|
|
|
|
|
|
//Iterate the triangle array to append vertices, and then triangles to the temporary subset mesh all at once
|
|
for (int32 i = 0; i < SrcIDBufferSubset.Num(); i += 3)
|
|
{
|
|
// Build the triangle
|
|
FIndex3i Triangle;
|
|
for (int32 v = 0; v < 3; ++v)
|
|
{
|
|
//It's the SrcVertexID because every element in the SrcIDBufferSubset is the Vertex ID of a vertex in the original mesh.
|
|
const int32 SrcVertexID = SrcIDBufferSubset[i + v];
|
|
int32& SubsetID = SrcVertexIDToSubsetVertexIDMap[SrcVertexID];
|
|
|
|
if (SubsetID == INACTIVE_SUBSET_ID) // we haven't already visited this vertex
|
|
{
|
|
const FVector3d Vertex = SrcMesh.GetVertex(SrcVertexID);
|
|
SubsetID = SubsetMesh.AppendVertex(Vertex);
|
|
}
|
|
|
|
Triangle[v] = SubsetID;
|
|
}
|
|
SubsetMesh.AppendTriangle(Triangle);
|
|
}
|
|
|
|
// create a mapping back to the original vertex IDs from the subset mesh
|
|
int32 MaxSubMeshVertexID = SubsetMesh.MaxVertexID(); // Really MaxID + 1
|
|
SubsetVertexIDToSrcVertexIDMap.Reset(MaxSubMeshVertexID);
|
|
SubsetVertexIDToSrcVertexIDMap.AddUninitialized(MaxSubMeshVertexID);
|
|
|
|
for (int32 SrcID = 0; SrcID < SrcVertexIDToSubsetVertexIDMap.Num(); ++SrcID)
|
|
{
|
|
const int32 SubsetVertexID = SrcVertexIDToSubsetVertexIDMap[SrcID];
|
|
if (SubsetVertexID != INACTIVE_SUBSET_ID)
|
|
{
|
|
SubsetVertexIDToSrcVertexIDMap[SubsetVertexID] = SrcID;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FConstrainedMeshDeformerTask::ExportResults(FDynamicMesh3& TargetMesh) const
|
|
{
|
|
//Update the position buffer result
|
|
for (int32 SubsetVertexID = 0; SubsetVertexID < SubsetVertexIDToSrcVertexIDMap.Num(); ++SubsetVertexID)
|
|
{
|
|
const int32 SrcVertexID = SubsetVertexIDToSrcVertexIDMap[SubsetVertexID];
|
|
const FVector3d Position = SubsetPositionBuffer[SubsetVertexID];
|
|
|
|
TargetMesh.SetVertex(SrcVertexID, Position);
|
|
}
|
|
|
|
FMeshNormals::QuickRecomputeOverlayNormals(TargetMesh);
|
|
}
|
|
|
|
void FConstrainedMeshDeformerTask::ApplyAttenuation()
|
|
{
|
|
TSet<int> Handles;
|
|
|
|
auto InPlaceMinMaxElements = [](FVector3d& Min, FVector3d& Max, const FVector3d Test) {
|
|
for (uint8 i = 0; i < 3; ++i)
|
|
{
|
|
Min[i] = Test[i] < Min[i] ? Test[i] : Min[i];
|
|
Max[i] = Test[i] > Max[i] ? Test[i] : Max[i];
|
|
}
|
|
};
|
|
|
|
//Experimental approach: Just going to try grabbing the bounding box of the entire mesh, then the bounding box of the handles as a point cloud.
|
|
// We need a T value to pass to the Weights curve, so let's try finding the distance of each vertex V from line segment formed by the min/max handles
|
|
// Divide the distance from the handles to vertex V by the length of the mesh's bounding box extent,
|
|
// and that will provide a **ROUGH** approximation of the time value for our curve.
|
|
//
|
|
// Distance( LineSegment(MaxHandle,MinHandle) , V )
|
|
// where T(V) is time value at V T(V) = -----------------------------------------------------
|
|
// and V is the position Length(MeshMin - MeshMax)
|
|
// of each vertex
|
|
|
|
FVector3d Min{std::numeric_limits<double>::max(), std::numeric_limits<double>::max(),
|
|
std::numeric_limits<double>::max()};
|
|
FVector3d Max{std::numeric_limits<double>::min(), std::numeric_limits<double>::min(),
|
|
std::numeric_limits<double>::min()};
|
|
FVector3d MinHandles = Min;
|
|
FVector3d MaxHandles = Max;
|
|
double LeastWeight = std::numeric_limits<double>::max();
|
|
|
|
|
|
for (int32 SubVertexID = 0; SubVertexID < SubsetConstraintBuffer.Num(); ++SubVertexID)
|
|
{
|
|
FDeformerVertexConstraintData& CData = SubsetConstraintBuffer[SubVertexID];
|
|
// Update bounding box
|
|
InPlaceMinMaxElements(Min, Max, CData.Position);
|
|
|
|
if (CData.Weight > 0.0)
|
|
{
|
|
LeastWeight = CData.Weight < LeastWeight ? CData.Weight : LeastWeight;
|
|
|
|
// update bounding box
|
|
InPlaceMinMaxElements(MinHandles, MaxHandles, CData.Position);
|
|
Handles.Add(SubVertexID);
|
|
}
|
|
}
|
|
|
|
double ExtentLength = Distance(Min, Max);
|
|
|
|
// Is this why the system has memory?
|
|
for (int32 SubVertexID = 0; SubVertexID < SubsetConstraintBuffer.Num(); ++SubVertexID)
|
|
{
|
|
if (!Handles.Contains(SubVertexID))
|
|
{
|
|
FDeformerVertexConstraintData& CData = SubsetConstraintBuffer[SubVertexID];
|
|
FVector3d OtherPoint = (FVector3d)FMath::ClosestPointOnSegment((FVector)CData.Position, (FVector)MinHandles, (FVector)MaxHandles);
|
|
double T = Distance(CData.Position, OtherPoint) / ExtentLength;
|
|
CData.Weight = WeightAttenuationCurve.Eval(T) * LeastWeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* FGroupTopologyLaplacianDeformer methods
|
|
*/
|
|
|
|
|
|
void FGroupTopologyLaplacianDeformer::InitBackgroundWorker(const ELaplacianWeightScheme WeightScheme)
|
|
{
|
|
//Initialize asynchronous deformation objects
|
|
if (AsyncMeshDeformTask == nullptr)
|
|
{
|
|
AsyncMeshDeformTask = new FAsyncTaskExecuterWithAbort<FConstrainedMeshDeformerTask>(WeightScheme);
|
|
}
|
|
}
|
|
|
|
void FGroupTopologyLaplacianDeformer::InitializeConstraintBuffer()
|
|
{
|
|
//MaxVertexID is used because the array is potentially sparse.
|
|
int MaxVertexID = Mesh->MaxVertexID();
|
|
|
|
SrcMeshConstraintBuffer.SetNum(MaxVertexID);
|
|
|
|
for (int32 VertexID : Mesh->VertexIndicesItr())
|
|
{
|
|
FDeformerVertexConstraintData& CD = SrcMeshConstraintBuffer[VertexID];
|
|
CD.Position = Mesh->GetVertex(VertexID);
|
|
CD.Weight = 0.0;
|
|
CD.bPostFix = false;
|
|
}
|
|
}
|
|
|
|
bool FGroupTopologyLaplacianDeformer::IsTaskInFlight() const
|
|
{
|
|
return (AsyncMeshDeformTask != nullptr && !AsyncMeshDeformTask->IsDone());
|
|
}
|
|
|
|
bool FGroupTopologyLaplacianDeformer::UpdateAndLaunchdWorker(const ELaplacianWeightScheme SelectedWeightScheme,
|
|
const FRichCurve* Curve)
|
|
{
|
|
/* Deformer needs to run if we've modified the constraints since the last time it finished. */
|
|
if (AsyncMeshDeformTask == nullptr)
|
|
{
|
|
InitBackgroundWorker(SelectedWeightScheme);
|
|
}
|
|
|
|
if (bDeformerNeedsToRun && AsyncMeshDeformTask->IsDone())
|
|
{
|
|
bool bRebuildSubsetMesh = bTaskSubmeshIsDirty;
|
|
|
|
FConstrainedMeshDeformerTask& Task = AsyncMeshDeformTask->GetTask();
|
|
|
|
// Update the deformer's buffers and weight scheme
|
|
// this creates the subset mesh if needed.
|
|
Task.UpdateDeformer(SelectedWeightScheme, *Mesh, SrcMeshConstraintBuffer, SubsetIDBuffer, bRebuildSubsetMesh,
|
|
Curve);
|
|
|
|
// task now has valid submesh
|
|
|
|
bTaskSubmeshIsDirty = false;
|
|
|
|
//Launch second thread
|
|
AsyncMeshDeformTask->StartBackgroundTask();
|
|
|
|
bDeformerNeedsToRun = false; // This was set to true above in UpdateSolution()
|
|
bVertexPositionsNeedSync = true; // The task will generate new vertex positions.
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FGroupTopologyLaplacianDeformer::SetActiveHandleFaces(const TArray<int>& FaceGroupIDs)
|
|
{
|
|
Reset();
|
|
|
|
check(FaceGroupIDs.Num() == 1); // multi-face not supported yet
|
|
int GroupID = FaceGroupIDs[0];
|
|
|
|
// find set of vertices in handle
|
|
Topology->CollectGroupVertices(GroupID, HandleVertices);
|
|
Topology->CollectGroupBoundaryVertices(GroupID, HandleBoundaryVertices);
|
|
ModifiedVertices = HandleVertices;
|
|
|
|
|
|
// list of adj groups. may contain duplicates.
|
|
TArray<int> AdjGroups;
|
|
for (int BoundaryVert : HandleBoundaryVertices)
|
|
{
|
|
Topology->FindVertexNbrGroups(BoundaryVert, AdjGroups);
|
|
}
|
|
|
|
// Local neighborhood - Adjacent groups plus self
|
|
TArray<int> NeighborhoodGroups;
|
|
|
|
// Collect the rest of the 1-ring groups that are adjacent to the selected one.
|
|
NeighborhoodGroups.Add(GroupID);
|
|
for (int AdjGroup : AdjGroups)
|
|
{
|
|
NeighborhoodGroups.AddUnique(AdjGroup); // remove duplicates by add unique
|
|
}
|
|
|
|
CalculateROI(FaceGroupIDs, NeighborhoodGroups);
|
|
|
|
UpdateSelection(Mesh, NeighborhoodGroups, bLocalize);
|
|
|
|
// Save the positions of the selected region.
|
|
SaveInitialPositions();
|
|
}
|
|
|
|
|
|
void FGroupTopologyLaplacianDeformer::SetActiveHandleEdges(const TArray<int>& TopologyEdgeIDs)
|
|
{
|
|
Reset();
|
|
|
|
for (int EdgeID : TopologyEdgeIDs)
|
|
{
|
|
const TArray<int>& EdgeVerts = Topology->GetGroupEdgeVertices(EdgeID);
|
|
for (int VertID : EdgeVerts)
|
|
{
|
|
HandleVertices.Add(VertID);
|
|
}
|
|
}
|
|
HandleBoundaryVertices = HandleVertices;
|
|
ModifiedVertices = HandleVertices;
|
|
|
|
TArray<int> HandleGroups;
|
|
TArray<int> NbrGroups;
|
|
Topology->FindEdgeNbrGroups(TopologyEdgeIDs, NbrGroups);
|
|
|
|
CalculateROI(HandleGroups, NbrGroups);
|
|
|
|
UpdateSelection(Mesh, NbrGroups, bLocalize);
|
|
|
|
// Save the positions of the selected region.
|
|
SaveInitialPositions();
|
|
}
|
|
|
|
void FGroupTopologyLaplacianDeformer::SetActiveHandleCorners(const TArray<int>& CornerIDs)
|
|
{
|
|
Reset();
|
|
|
|
for (int CornerID : CornerIDs)
|
|
{
|
|
int VertID = Topology->GetCornerVertexID(CornerID);
|
|
if (VertID >= 0)
|
|
{
|
|
HandleVertices.Add(VertID);
|
|
}
|
|
}
|
|
HandleBoundaryVertices = HandleVertices;
|
|
ModifiedVertices = HandleVertices;
|
|
|
|
TArray<int> HandleGroups;
|
|
TArray<int> NbrGroups;
|
|
|
|
Topology->FindCornerNbrGroups(CornerIDs, NbrGroups);
|
|
|
|
|
|
CalculateROI(HandleGroups, NbrGroups);
|
|
|
|
UpdateSelection(Mesh, NbrGroups, bLocalize);
|
|
|
|
// Save the positions of the selected region.
|
|
SaveInitialPositions();
|
|
}
|
|
|
|
|
|
void FGroupTopologyLaplacianDeformer::UpdateSelection(const FDynamicMesh3* TargetMesh, const TArray<int>& Groups,
|
|
bool bLocalizeDeformation)
|
|
{
|
|
// Build an index buffer (SubsetIdBuffer) and a vertexId buffer (ModifidedVertices) for the region we want to change
|
|
|
|
if (bLocalizeDeformation)
|
|
{
|
|
//For each group ID, retrieve the array of all TriangleIDs associated with that GroupID and append that array to the end of the TriSet to remove duplicates
|
|
TSet<int> TriSet;
|
|
for (const int32& GroupID : Groups)
|
|
{
|
|
TriSet.Append(Topology->GetGroupFaces(GroupID));
|
|
} //Now we have every triangle ID involved in the transaction
|
|
|
|
//Since we are flattening the Face to a set of 3 indices, we do 3 * number of triangles though it is too many.
|
|
SubsetIDBuffer.Reset(3 * TriSet.Num());
|
|
//Add each triangle's A,B, and C indices to the subset triangle array.
|
|
for (const int& Tri : TriSet)
|
|
{
|
|
FIndex3i Triple = TargetMesh->GetTriangle(Tri);
|
|
SubsetIDBuffer.Add(Triple.A);
|
|
SubsetIDBuffer.Add(Triple.B);
|
|
SubsetIDBuffer.Add(Triple.C);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// the entire mesh.
|
|
const int32 NumTris = TargetMesh->TriangleCount();
|
|
SubsetIDBuffer.Reset(3 * NumTris);
|
|
for (int TriId : TargetMesh->TriangleIndicesItr())
|
|
{
|
|
FIndex3i Triple = TargetMesh->GetTriangle(TriId);
|
|
SubsetIDBuffer.Add(Triple.A);
|
|
SubsetIDBuffer.Add(Triple.B);
|
|
SubsetIDBuffer.Add(Triple.C);
|
|
}
|
|
}
|
|
|
|
// Add the vertices to the set (eliminates duplicates.) Todo: don't use a set.
|
|
ResetModifiedVertices();
|
|
for (int32 VertexID : SubsetIDBuffer)
|
|
{
|
|
RecordModifiedVertex(VertexID);
|
|
}
|
|
}
|
|
|
|
// This actually updates constraints that correspond to the handle vertices.
|
|
void FGroupTopologyLaplacianDeformer::UpdateSolution(
|
|
FDynamicMesh3* TargetMesh, const TFunction<FVector3d(FDynamicMesh3* Mesh, int)>& HandleVertexDeformFunc)
|
|
{
|
|
// copy the current positions.
|
|
FVertexPositionCache CurrentPositions;
|
|
for (int VertexID : InitialPositions.Vertices)
|
|
{
|
|
CurrentPositions.AddVertex(TargetMesh, VertexID);
|
|
}
|
|
|
|
// Set the target mesh to the initial positions.
|
|
// Note: this only updates the vertices in the selected region.
|
|
InitialPositions.SetPositions(TargetMesh);
|
|
|
|
//Reset the constraints
|
|
for (int32 VertexID : ModifiedVertices)
|
|
{
|
|
//Get the vertex's data from the constraint buffer
|
|
FDeformerVertexConstraintData& CData = SrcMeshConstraintBuffer[VertexID];
|
|
|
|
CData.Position = TargetMesh->GetVertex(VertexID);
|
|
CData.Weight = 0.0; //A weight of zero is used to allow this point to move freely when moving the handles
|
|
CData.bPostFix = false;
|
|
}
|
|
|
|
//Actually deform the handles and add a constraint.
|
|
for (int VertexID : HandleVertices)
|
|
{
|
|
const FVector3d DeformPos = HandleVertexDeformFunc(TargetMesh, VertexID);
|
|
|
|
//Get the vertex's data from the constraint buffer
|
|
FDeformerVertexConstraintData& CData = SrcMeshConstraintBuffer[VertexID];
|
|
|
|
//Set the new vertex data
|
|
CData.Position = DeformPos;
|
|
CData.Weight = HandleWeights;
|
|
CData.bPostFix = bPostfixHandles;
|
|
}
|
|
|
|
// Restore Current Positions. This is done because the target mesh is being used to define the highlight region.
|
|
// if we don't reset the positions the highlight mesh will appear to reset momentarily until the first laplacian solver result is available
|
|
CurrentPositions.SetPositions(TargetMesh);
|
|
|
|
bDeformerNeedsToRun = true;
|
|
}
|
|
|
|
void FGroupTopologyLaplacianDeformer::ExportDeformedPositions(FDynamicMesh3* TargetMesh)
|
|
{
|
|
bool bIsWorking = IsTaskInFlight();
|
|
if (AsyncMeshDeformTask != nullptr && !bIsWorking)
|
|
{
|
|
const FConstrainedMeshDeformerTask& Task = AsyncMeshDeformTask->GetTask();
|
|
Task.ExportResults(*TargetMesh);
|
|
}
|
|
}
|
|
|
|
inline FGroupTopologyLaplacianDeformer::~FGroupTopologyLaplacianDeformer()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
inline void FGroupTopologyLaplacianDeformer::Shutdown()
|
|
{
|
|
if (AsyncMeshDeformTask != nullptr)
|
|
{
|
|
if (AsyncMeshDeformTask->IsDone())
|
|
{
|
|
delete AsyncMeshDeformTask;
|
|
}
|
|
else
|
|
{
|
|
AsyncMeshDeformTask->CancelAndDelete();
|
|
}
|
|
|
|
AsyncMeshDeformTask = nullptr;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Tool methods
|
|
*/
|
|
|
|
UDeformMeshPolygonsTool::UDeformMeshPolygonsTool()
|
|
{
|
|
UInteractiveTool::SetToolDisplayName(LOCTEXT("DeformPolygroupsToolName", "PolyGroup Deform"));
|
|
}
|
|
|
|
void UDeformMeshPolygonsTool::Setup()
|
|
{
|
|
UMeshSurfacePointTool::Setup();
|
|
|
|
LaplacianDeformer = MakePimpl<FGroupTopologyLaplacianDeformer>();
|
|
|
|
// create dynamic mesh component to use for live preview
|
|
check(TargetWorld.IsValid());
|
|
FActorSpawnParameters SpawnInfo;
|
|
PreviewMeshActor = TargetWorld->SpawnActor<AInternalToolFrameworkActor>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo);
|
|
|
|
DynamicMeshComponent = NewObject<UDynamicMeshComponent>(PreviewMeshActor);
|
|
DynamicMeshComponent->SetupAttachment(PreviewMeshActor->GetRootComponent());
|
|
DynamicMeshComponent->RegisterComponent();
|
|
WorldTransform = UE::ToolTarget::GetLocalToWorldTransform(Target);
|
|
DynamicMeshComponent->SetWorldTransform((FTransform)WorldTransform);
|
|
ToolSetupUtil::ApplyRenderingConfigurationToPreview(DynamicMeshComponent, Target);
|
|
|
|
// set materials
|
|
FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Target);
|
|
for (int k = 0; k < MaterialSet.Materials.Num(); ++k)
|
|
{
|
|
DynamicMeshComponent->SetMaterial(k, MaterialSet.Materials[k]);
|
|
}
|
|
|
|
// dynamic mesh configuration settings
|
|
DynamicMeshComponent->SetTangentsType(EDynamicMeshComponentTangentsMode::AutoCalculated);
|
|
DynamicMeshComponent->SetMesh(UE::ToolTarget::GetDynamicMeshCopy(Target));
|
|
OnDynamicMeshComponentChangedHandle =
|
|
DynamicMeshComponent->OnMeshChanged.Add(FSimpleMulticastDelegate::FDelegate::CreateUObject(
|
|
this, &UDeformMeshPolygonsTool::OnDynamicMeshComponentChanged));
|
|
|
|
|
|
// add properties
|
|
TransformProps = NewObject<UDeformMeshPolygonsTransformProperties>(this);
|
|
AddToolPropertySource(TransformProps);
|
|
|
|
// initialize AABBTree
|
|
MeshSpatial.SetMesh(DynamicMeshComponent->GetMesh());
|
|
PrecomputeTopology();
|
|
|
|
//initialize topology selector
|
|
TopoSelector.Initialize(DynamicMeshComponent->GetMesh(), &Topology);
|
|
TopoSelector.SetSpatialSource([this]() { return &GetSpatial(); });
|
|
TopoSelector.PointsWithinToleranceTest = [this](const FVector3d& Position1, const FVector3d& Position2, double TolScale) {
|
|
return ToolSceneQueriesUtil::PointSnapQuery(CameraState, WorldTransform.TransformPosition(Position1), WorldTransform.TransformPosition(Position2),
|
|
ToolSceneQueriesUtil::GetDefaultVisualAngleSnapThreshD() * TolScale);
|
|
};
|
|
|
|
// hide input StaticMeshComponent
|
|
UE::ToolTarget::HideSourceObject(Target);
|
|
|
|
// init state flags flags
|
|
bInDrag = false;
|
|
|
|
// initialize snap solver
|
|
QuickAxisTranslater.Initialize();
|
|
QuickAxisRotator.Initialize();
|
|
|
|
// set up visualizers
|
|
PolyEdgesRenderer.LineColor = FLinearColor::Red;
|
|
PolyEdgesRenderer.LineThickness = 2.0;
|
|
HilightRenderer.LineColor = FLinearColor::Green;
|
|
HilightRenderer.LineThickness = 4.0f;
|
|
|
|
// Allocates buffers, sets up the asynchronous task
|
|
// Copies the source mesh positions.
|
|
const ELaplacianWeightScheme LaplacianWeightScheme =
|
|
ConvertToLaplacianWeightScheme(TransformProps->SelectedWeightScheme);
|
|
LaplacianDeformer->InitBackgroundWorker(LaplacianWeightScheme);
|
|
|
|
|
|
/**
|
|
// How to add a curve for the weights.
|
|
//Add a default curve for falloff
|
|
FKeyHandle Keys[5];
|
|
Keys[0] = TransformProps->DefaultFalloffCurve.UpdateOrAddKey(0.f, 1.f);
|
|
Keys[1] = TransformProps->DefaultFalloffCurve.UpdateOrAddKey(0.25f, 0.25f);
|
|
Keys[2] = TransformProps->DefaultFalloffCurve.UpdateOrAddKey(0.3333333f, 0.25f);
|
|
Keys[3] = TransformProps->DefaultFalloffCurve.UpdateOrAddKey(0.6666667f, 1.25f);
|
|
Keys[4] = TransformProps->DefaultFalloffCurve.UpdateOrAddKey(1.f, 1.4f);
|
|
for (uint8 i = 0; i < 5; ++i)
|
|
{
|
|
TransformProps->DefaultFalloffCurve.SetKeyInterpMode(Keys[i], ERichCurveInterpMode::RCIM_Cubic);
|
|
}
|
|
TransformProps->WeightAttenuationCurve.EditorCurveData = TransformProps->DefaultFalloffCurve;
|
|
*/
|
|
|
|
if (Topology.Groups.Num() < 2)
|
|
{
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("NoGroupsWarning",
|
|
"This object has only a single PolyGroup. Use the GrpGen, GrpPnt or TriSel (Create PolyGroup) tools to modify PolyGroups."),
|
|
EToolMessageLevel::UserWarning);
|
|
}
|
|
|
|
GetToolManager()->DisplayMessage(
|
|
LOCTEXT("DeformMeshPolygonsToolDescription", "Deform the mesh by directly manipulating (i.e. click-and-drag) the PolyGroup edges, faces, and vertices."),
|
|
EToolMessageLevel::UserNotification);
|
|
}
|
|
|
|
void UDeformMeshPolygonsTool::Shutdown(EToolShutdownType ShutdownType)
|
|
{
|
|
LongTransactions.CloseAll(GetToolManager());
|
|
|
|
//Tell the background thread to cancel the rest of its jobs before we close;
|
|
LaplacianDeformer.Reset();
|
|
|
|
if (DynamicMeshComponent != nullptr)
|
|
{
|
|
DynamicMeshComponent->OnMeshChanged.Remove(OnDynamicMeshComponentChangedHandle);
|
|
|
|
UE::ToolTarget::ShowSourceObject(Target);
|
|
|
|
if (ShutdownType == EToolShutdownType::Accept)
|
|
{
|
|
// this block bakes the modified DynamicMeshComponent back into the StaticMeshComponent inside an undo transaction
|
|
GetToolManager()->BeginUndoTransaction(LOCTEXT("DeformMeshPolygonsToolTransactionName", "PolyGroup Deform"));
|
|
DynamicMeshComponent->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
|
|
{
|
|
FConversionToMeshDescriptionOptions ConversionOptions;
|
|
ConversionOptions.bSetPolyGroups =
|
|
false; // don't save polygroups, as we may change these temporarily in this tool just to get a different edit effect
|
|
UE::ToolTarget::CommitDynamicMeshUpdate(Target, ReadMesh, false, ConversionOptions);
|
|
});
|
|
GetToolManager()->EndUndoTransaction();
|
|
}
|
|
|
|
DynamicMeshComponent->UnregisterComponent();
|
|
DynamicMeshComponent->DestroyComponent();
|
|
DynamicMeshComponent = nullptr;
|
|
}
|
|
|
|
if (PreviewMeshActor != nullptr)
|
|
{
|
|
PreviewMeshActor->Destroy();
|
|
PreviewMeshActor = nullptr;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::NextTransformTypeAction()
|
|
{
|
|
if (bInDrag == false)
|
|
{
|
|
if (TransformProps->TransformMode == EQuickTransformerMode::AxisRotation)
|
|
{
|
|
TransformProps->TransformMode = EQuickTransformerMode::AxisTranslation;
|
|
}
|
|
else
|
|
{
|
|
TransformProps->TransformMode = EQuickTransformerMode::AxisRotation;
|
|
}
|
|
UpdateQuickTransformer();
|
|
}
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::RegisterActions(FInteractiveToolActionSet& ActionSet)
|
|
{
|
|
ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 2,
|
|
TEXT("DeformNextTransformType"), LOCTEXT("DeformNextTransformType", "Next Transform Type"),
|
|
LOCTEXT("DeformNextTransformTypeTooltip", "Cycle to next transform type"),
|
|
EModifierKey::None, EKeys::Q, [this]() { NextTransformTypeAction(); });
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::OnDynamicMeshComponentChanged()
|
|
{
|
|
bSpatialDirty = true;
|
|
TopoSelector.Invalidate(true, false);
|
|
|
|
|
|
//Makes sure the constraint buffer and position buffers reflect Undo/Redo changes
|
|
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
|
|
|
|
|
|
//Apply Undo/redo
|
|
for (int VertexID : Mesh->VertexIndicesItr())
|
|
{
|
|
const FVector3d Position = Mesh->GetVertex(VertexID);
|
|
LaplacianDeformer->SrcMeshConstraintBuffer[VertexID].Position = Position;
|
|
}
|
|
|
|
// a deform task could still be in flight.
|
|
if (LaplacianDeformer->AsyncMeshDeformTask != nullptr)
|
|
{
|
|
LaplacianDeformer->AsyncMeshDeformTask->CancelAndDelete();
|
|
LaplacianDeformer->AsyncMeshDeformTask = nullptr;
|
|
LaplacianDeformer->bTaskSubmeshIsDirty = true;
|
|
}
|
|
}
|
|
|
|
FDynamicMeshAABBTree3& UDeformMeshPolygonsTool::GetSpatial()
|
|
{
|
|
if (bSpatialDirty)
|
|
{
|
|
MeshSpatial.Build();
|
|
bSpatialDirty = false;
|
|
}
|
|
return MeshSpatial;
|
|
}
|
|
|
|
|
|
bool UDeformMeshPolygonsTool::HitTest(const FRay& WorldRay, FHitResult& OutHit)
|
|
{
|
|
FRay3d LocalRay(WorldTransform.InverseTransformPosition((FVector3d)WorldRay.Origin),
|
|
WorldTransform.InverseTransformVector((FVector3d)WorldRay.Direction));
|
|
UE::Geometry::Normalize(LocalRay.Direction);
|
|
|
|
FGroupTopologySelection Selection;
|
|
FVector3d LocalPosition, LocalNormal;
|
|
FGroupTopologySelector::FSelectionSettings TopoSelectorSettings = GetTopoSelectorSettings();
|
|
if (TopoSelector.FindSelectedElement(TopoSelectorSettings, LocalRay, Selection, LocalPosition, LocalNormal) == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Selection.SelectedCornerIDs.Num() > 0)
|
|
{
|
|
OutHit.FaceIndex = Selection.GetASelectedCornerID();
|
|
OutHit.Distance = LocalRay.GetParameter(LocalPosition);
|
|
OutHit.ImpactPoint = (FVector)WorldTransform.TransformPosition(LocalRay.PointAt(OutHit.Distance));
|
|
}
|
|
else if (Selection.SelectedEdgeIDs.Num() > 0)
|
|
{
|
|
OutHit.FaceIndex = Selection.GetASelectedEdgeID();
|
|
OutHit.Distance = LocalRay.GetParameter(LocalPosition);
|
|
OutHit.ImpactPoint = (FVector)WorldTransform.TransformPosition(LocalRay.PointAt(OutHit.Distance));
|
|
}
|
|
else
|
|
{
|
|
int HitTID = GetSpatial().FindNearestHitTriangle(LocalRay);
|
|
if (HitTID != IndexConstants::InvalidID)
|
|
{
|
|
FTriangle3d Triangle;
|
|
GetSpatial().GetMesh()->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)WorldTransform.TransformVectorNoScale(GetSpatial().GetMesh()->GetTriNormal(HitTID));
|
|
OutHit.ImpactPoint = (FVector)WorldTransform.TransformPosition(LocalRay.PointAt(Query.RayParameter));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::OnBeginDrag(const FRay& WorldRay)
|
|
{
|
|
FRay3d LocalRay(WorldTransform.InverseTransformPosition((FVector3d)WorldRay.Origin),
|
|
WorldTransform.InverseTransformVector((FVector3d)WorldRay.Direction));
|
|
UE::Geometry::Normalize(LocalRay.Direction);
|
|
|
|
HilightSelection.Clear();
|
|
|
|
FGroupTopologySelection Selection;
|
|
FVector3d LocalPosition, LocalNormal;
|
|
FGroupTopologySelector::FSelectionSettings TopoSelectorSettings = GetTopoSelectorSettings();
|
|
bool bHit = TopoSelector.FindSelectedElement(TopoSelectorSettings, LocalRay, Selection, LocalPosition, LocalNormal);
|
|
|
|
if (bHit == false)
|
|
{
|
|
bInDrag = false;
|
|
return;
|
|
}
|
|
|
|
HilightSelection = Selection;
|
|
|
|
FVector3d WorldHitPos = WorldTransform.TransformPosition(LocalPosition);
|
|
FVector3d WorldHitNormal = WorldTransform.TransformVector(LocalNormal);
|
|
|
|
bInDrag = true;
|
|
StartHitPosWorld = (FVector)WorldHitPos;
|
|
LastHitPosWorld = StartHitPosWorld;
|
|
StartHitNormalWorld = (FVector)WorldHitNormal;
|
|
|
|
QuickAxisRotator.ClearAxisLock();
|
|
UpdateActiveSurfaceFrame(HilightSelection);
|
|
UpdateQuickTransformer();
|
|
|
|
LastBrushPosLocal = (FVector)WorldTransform.InverseTransformPosition((FVector3d)LastHitPosWorld);
|
|
StartBrushPosLocal = LastBrushPosLocal;
|
|
|
|
// Record the requested deformation strategy - NB: will be forced to linear if there aren't any free points to solve.
|
|
|
|
DeformationStrategy = TransformProps->DeformationStrategy;
|
|
|
|
// Capture the part of the mesh that will deform
|
|
|
|
if (DeformationStrategy == EGroupTopologyDeformationStrategy::Laplacian)
|
|
{
|
|
LaplacianDeformer->bLocalize = true; // TransformProps->bLocalizeDeformation;
|
|
|
|
//Determine which of the following (corners, edges or faces) has been selected by counting the associated feature's IDs
|
|
if (Selection.SelectedCornerIDs.Num() > 0)
|
|
{
|
|
//Add all the the Corner's adjacent poly-groups (NbrGroups) to the ongoing array of groups.
|
|
LaplacianDeformer->SetActiveHandleCorners(Selection.SelectedCornerIDs.Array());
|
|
}
|
|
else if (Selection.SelectedEdgeIDs.Num() > 0)
|
|
{
|
|
//Add all the the edge's adjacent poly-groups (NbrGroups) to the ongoing array of groups.
|
|
LaplacianDeformer->SetActiveHandleEdges(Selection.SelectedEdgeIDs.Array());
|
|
}
|
|
else if (Selection.SelectedGroupIDs.Num() > 0)
|
|
{
|
|
LaplacianDeformer->SetActiveHandleFaces(Selection.SelectedGroupIDs.Array());
|
|
}
|
|
|
|
|
|
// If there are actually no interior points, then we can't actually use the laplacian deformer. Need to fall back to the linear.
|
|
bool bHasInteriorVerts = false;
|
|
const auto& ROIFaces = LaplacianDeformer->GetROIFaces();
|
|
for (const auto& Face : ROIFaces)
|
|
{
|
|
bHasInteriorVerts = bHasInteriorVerts || (Face.InteriorVerts.Num() != 0);
|
|
}
|
|
|
|
if (!bHasInteriorVerts)
|
|
{
|
|
// Change to the linear strategy for this case.
|
|
|
|
DeformationStrategy = EGroupTopologyDeformationStrategy::Linear;
|
|
}
|
|
else
|
|
{
|
|
// finalize the laplacian deformer : the task will need a new mesh that corresponds to the selected region.
|
|
|
|
LaplacianDeformer->bTaskSubmeshIsDirty = true;
|
|
}
|
|
}
|
|
|
|
if (DeformationStrategy == EGroupTopologyDeformationStrategy::Linear)
|
|
{
|
|
//Determine which of the following (corners, edges or faces) has been selected by counting the associated feature's IDs
|
|
if (Selection.SelectedCornerIDs.Num() > 0)
|
|
{
|
|
//Add all the the Corner's adjacent poly-groups (NbrGroups) to the ongoing array of groups.
|
|
LinearDeformer.SetActiveHandleCorners(Selection.SelectedCornerIDs.Array());
|
|
}
|
|
else if (Selection.SelectedEdgeIDs.Num() > 0)
|
|
{
|
|
//Add all the the edge's adjacent poly-groups (NbrGroups) to the ongoing array of groups.
|
|
LinearDeformer.SetActiveHandleEdges(Selection.SelectedEdgeIDs.Array());
|
|
}
|
|
else if (Selection.SelectedGroupIDs.Num() > 0)
|
|
{
|
|
LinearDeformer.SetActiveHandleFaces(Selection.SelectedGroupIDs.Array());
|
|
}
|
|
}
|
|
|
|
BeginChange();
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::UpdateActiveSurfaceFrame(FGroupTopologySelection& Selection)
|
|
{
|
|
// update surface frame
|
|
ActiveSurfaceFrame.Origin = (FVector3d)StartHitPosWorld;
|
|
if (HilightSelection.SelectedCornerIDs.Num() == 1)
|
|
{
|
|
// just keeping existing axes...we don't have enough info to do something smarter
|
|
}
|
|
else
|
|
{
|
|
ActiveSurfaceFrame.AlignAxis(2, (FVector3d)StartHitNormalWorld);
|
|
if (HilightSelection.SelectedEdgeIDs.Num() == 1)
|
|
{
|
|
FVector3d Tangent;
|
|
if (Topology.GetGroupEdgeTangent(HilightSelection.GetASelectedEdgeID(), Tangent))
|
|
{
|
|
Tangent = WorldTransform.TransformVector(Tangent);
|
|
ActiveSurfaceFrame.ConstrainedAlignAxis(0, Tangent, ActiveSurfaceFrame.Z());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
FQuickTransformer* UDeformMeshPolygonsTool::GetActiveQuickTransformer()
|
|
{
|
|
if (TransformProps->TransformMode == EQuickTransformerMode::AxisRotation)
|
|
{
|
|
return &QuickAxisRotator;
|
|
}
|
|
else
|
|
{
|
|
return &QuickAxisTranslater;
|
|
}
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::UpdateQuickTransformer()
|
|
{
|
|
bool bUseLocalAxes =
|
|
(GetToolManager()->GetContextQueriesAPI()->GetCurrentCoordinateSystem() == EToolContextCoordinateSystem::Local);
|
|
if (bUseLocalAxes)
|
|
{
|
|
GetActiveQuickTransformer()->SetActiveWorldFrame(ActiveSurfaceFrame);
|
|
}
|
|
else
|
|
{
|
|
GetActiveQuickTransformer()->SetActiveFrameFromWorldAxes((FVector3d)StartHitPosWorld);
|
|
}
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::UpdateChangeFromROI(bool bFinal)
|
|
{
|
|
if (ActiveVertexChange == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
const bool bIsLaplacian = (DeformationStrategy == EGroupTopologyDeformationStrategy::Laplacian);
|
|
|
|
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
|
|
const TSet<int>& ModifiedVertices =
|
|
(bIsLaplacian) ? LaplacianDeformer->GetModifiedVertices() : LinearDeformer.GetModifiedVertices();
|
|
ActiveVertexChange->SaveVertices(Mesh, ModifiedVertices, !bFinal);
|
|
const TSet<int>& ModifiedNormals =
|
|
(bIsLaplacian) ? LaplacianDeformer->GetModifiedOverlayNormals() : LinearDeformer.GetModifiedOverlayNormals();
|
|
ActiveVertexChange->SaveOverlayNormals(Mesh, ModifiedNormals, !bFinal);
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::OnUpdateDrag(const FRay& Ray)
|
|
{
|
|
if (bInDrag)
|
|
{
|
|
bUpdatePending = true;
|
|
UpdateRay = Ray;
|
|
}
|
|
}
|
|
|
|
void UDeformMeshPolygonsTool::OnEndDrag(const FRay& Ray)
|
|
{
|
|
bInDrag = false;
|
|
bUpdatePending = false;
|
|
|
|
// update spatial
|
|
bSpatialDirty = true;
|
|
|
|
HilightSelection.Clear();
|
|
TopoSelector.Invalidate(true, false);
|
|
QuickAxisRotator.Reset();
|
|
QuickAxisTranslater.Reset();
|
|
|
|
//If it's linear, it's computed real time with no delay. This may need to be restructured for clarity by using the background task for this as well.
|
|
if (DeformationStrategy == EGroupTopologyDeformationStrategy::Linear)
|
|
{
|
|
// close change record
|
|
EndChange();
|
|
}
|
|
}
|
|
|
|
void UDeformMeshPolygonsTool::OnCancelDrag()
|
|
{
|
|
bInDrag = false;
|
|
bUpdatePending = false;
|
|
|
|
HilightSelection.Clear();
|
|
TopoSelector.Invalidate(true, false);
|
|
QuickAxisRotator.Reset();
|
|
QuickAxisTranslater.Reset();
|
|
|
|
if (ActiveVertexChange)
|
|
{
|
|
LongTransactions.Close(GetToolManager());
|
|
delete ActiveVertexChange;
|
|
ActiveVertexChange = nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
bool UDeformMeshPolygonsTool::OnUpdateHover(const FInputDeviceRay& DevicePos)
|
|
{
|
|
//if (!bNeedEmitEndChange)
|
|
if (ActiveVertexChange == nullptr)
|
|
{
|
|
FRay3d LocalRay(WorldTransform.InverseTransformPosition((FVector3d)DevicePos.WorldRay.Origin),
|
|
WorldTransform.InverseTransformVector((FVector3d)DevicePos.WorldRay.Direction));
|
|
UE::Geometry::Normalize(LocalRay.Direction);
|
|
|
|
HilightSelection.Clear();
|
|
FVector3d LocalPosition, LocalNormal;
|
|
FGroupTopologySelector::FSelectionSettings TopoSelectorSettings = GetTopoSelectorSettings();
|
|
bool bHit = TopoSelector.FindSelectedElement(TopoSelectorSettings, LocalRay, HilightSelection, LocalPosition, LocalNormal);
|
|
|
|
if (bHit)
|
|
{
|
|
StartHitPosWorld = (FVector)WorldTransform.TransformPosition(LocalPosition);
|
|
StartHitNormalWorld = (FVector)WorldTransform.TransformVector(LocalNormal);
|
|
|
|
UpdateActiveSurfaceFrame(HilightSelection);
|
|
UpdateQuickTransformer();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::ComputeUpdate()
|
|
{
|
|
if (bUpdatePending == true)
|
|
{
|
|
// Linear Deformer : Update the solution
|
|
// Laplacain Deformer : Update the constraints (positions and weights) - the region was identified in onBeginDrag
|
|
|
|
|
|
if (TransformProps->TransformMode == EQuickTransformerMode::AxisRotation)
|
|
{
|
|
ComputeUpdate_Rotate();
|
|
}
|
|
else
|
|
{
|
|
ComputeUpdate_Translate();
|
|
}
|
|
}
|
|
|
|
if (DeformationStrategy == EGroupTopologyDeformationStrategy::Laplacian)
|
|
{
|
|
bool bIsWorking = LaplacianDeformer->IsTaskInFlight();
|
|
|
|
if (!bIsWorking)
|
|
{
|
|
// Sync update if we have new results.
|
|
if (LaplacianDeformer->bVertexPositionsNeedSync)
|
|
{
|
|
//Update the mesh with the provided solutions.
|
|
LaplacianDeformer->ExportDeformedPositions(DynamicMeshComponent->GetMesh());
|
|
|
|
LaplacianDeformer->bVertexPositionsNeedSync = false;
|
|
|
|
//Re-sync mesh, and flag the spatial data struct & topology for re-evaluation
|
|
DynamicMeshComponent->FastNotifyPositionsUpdated(true, false, false);
|
|
|
|
GetToolManager()->PostInvalidation();
|
|
bSpatialDirty = true;
|
|
TopoSelector.Invalidate(true, false);
|
|
}
|
|
|
|
// emit end change if we are done with the drag
|
|
if (!LaplacianDeformer->bDeformerNeedsToRun && !bInDrag)
|
|
{
|
|
EndChange();
|
|
}
|
|
|
|
// Not working but we have more work for it to do..
|
|
|
|
if (LaplacianDeformer->bDeformerNeedsToRun)
|
|
{
|
|
FRichCurve* Curve = NULL;
|
|
|
|
/**
|
|
// How to add a deformation curve
|
|
const bool bApplyAttenuationCurve = TransformProps->bApplyAttenuationCurve
|
|
if (bApplyAttenuationCurve)
|
|
{
|
|
Curve = TransformProps->WeightAttenuationCurve.GetRichCurve();
|
|
}
|
|
*/
|
|
const ELaplacianWeightScheme LaplacianWeightScheme =
|
|
ConvertToLaplacianWeightScheme(TransformProps->SelectedWeightScheme);
|
|
LaplacianDeformer->UpdateAndLaunchdWorker(LaplacianWeightScheme, Curve);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::ComputeUpdate_Rotate()
|
|
{
|
|
const bool bIsLaplacian = (DeformationStrategy == EGroupTopologyDeformationStrategy::Laplacian);
|
|
FGroupTopologyDeformer& SelectedDeformer = (bIsLaplacian) ? *LaplacianDeformer : LinearDeformer;
|
|
|
|
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
|
|
FVector NewHitPosWorld;
|
|
|
|
FVector3d SnappedPoint;
|
|
if (QuickAxisRotator.UpdateSnap(FRay3d(UpdateRay), SnappedPoint))
|
|
{
|
|
NewHitPosWorld = (FVector)SnappedPoint;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
// check if we are on back-facing part of rotation in which case we ignore...
|
|
FVector3d SphereCenter = QuickAxisRotator.GetActiveWorldFrame().Origin;
|
|
if (QuickAxisRotator.HaveActiveSnapRotation() && QuickAxisRotator.GetHaveLockedToAxis() == false)
|
|
{
|
|
FVector3d ToSnapPointVec = (SnappedPoint - SphereCenter);
|
|
FVector3d ToEyeVec = (SnappedPoint - (FVector3d)CameraState.Position);
|
|
if (ToSnapPointVec.Dot(ToEyeVec) > 0)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
// if we haven't snapped to a rotation we can exit
|
|
if (QuickAxisRotator.HaveActiveSnapRotation() == false)
|
|
{
|
|
QuickAxisRotator.ClearAxisLock();
|
|
|
|
SelectedDeformer.ClearSolution(Mesh);
|
|
|
|
//TODO: This is unseemly here, need to potentially defer this so that it's handled the same way as laplacian. Placeholder for now.
|
|
if (DeformationStrategy == EGroupTopologyDeformationStrategy::Linear)
|
|
{
|
|
DynamicMeshComponent->FastNotifyPositionsUpdated(true);
|
|
GetToolManager()->PostInvalidation();
|
|
}
|
|
bUpdatePending = false;
|
|
return;
|
|
}
|
|
|
|
// ok we have an axis...
|
|
if (QuickAxisRotator.GetHaveLockedToAxis() == false)
|
|
{
|
|
QuickAxisRotator.SetAxisLock();
|
|
RotationStartPointWorld = SnappedPoint;
|
|
RotationStartFrame = QuickAxisRotator.GetActiveRotationFrame();
|
|
}
|
|
|
|
FVector2d RotateStartVec = RotationStartFrame.ToPlaneUV(RotationStartPointWorld, 2);
|
|
UE::Geometry::Normalize(RotateStartVec);
|
|
FVector2d RotateToVec = RotationStartFrame.ToPlaneUV((FVector3d)NewHitPosWorld, 2);
|
|
UE::Geometry::Normalize(RotateToVec);
|
|
double AngleRad = UE::Geometry::SignedAngleR(RotateStartVec, RotateToVec);
|
|
FQuaterniond Rotation(WorldTransform.InverseTransformVectorNoScale(RotationStartFrame.Z()), AngleRad, false);
|
|
FVector3d LocalOrigin = WorldTransform.InverseTransformPosition(RotationStartFrame.Origin);
|
|
|
|
// Linear Deformer: Update Mesh the rotation,
|
|
// Laplacian Deformer: Update handles constraints with the rotation and set bDeformerNeedsToRun = true;.
|
|
SelectedDeformer.UpdateSolution(Mesh, [this, LocalOrigin, Rotation](FDynamicMesh3* TargetMesh, int VertIdx) {
|
|
FVector3d V = TargetMesh->GetVertex(VertIdx);
|
|
V -= LocalOrigin;
|
|
V = Rotation * V;
|
|
V += LocalOrigin;
|
|
return V;
|
|
});
|
|
|
|
//TODO: This is unseemly here, need to potentially defer this so that it's handled the same way as laplacian. Placeholder for now.
|
|
if (!bIsLaplacian)
|
|
{
|
|
DynamicMeshComponent->FastNotifyPositionsUpdated(true);
|
|
GetToolManager()->PostInvalidation();
|
|
}
|
|
bUpdatePending = false;
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::ComputeUpdate_Translate()
|
|
{
|
|
const bool bIsLaplacian = (DeformationStrategy == EGroupTopologyDeformationStrategy::Laplacian);
|
|
FGroupTopologyDeformer& SelectedDeformer = (bIsLaplacian) ? *LaplacianDeformer : LinearDeformer;
|
|
|
|
TFunction<FVector3d(const FVector3d&)> PointConstraintFunc = nullptr;
|
|
if (GetToolManager()->GetContextQueriesAPI()->GetCurrentCoordinateSystem() == EToolContextCoordinateSystem::World)
|
|
{
|
|
// We currently don't support grid snapping in local mode
|
|
PointConstraintFunc = [&](const FVector3d& Pos) {
|
|
FVector3d GridSnapPos;
|
|
return ToolSceneQueriesUtil::FindWorldGridSnapPoint(this, Pos, GridSnapPos) ? GridSnapPos : Pos;
|
|
};
|
|
}
|
|
|
|
FVector NewHitPosWorld;
|
|
FVector3d SnappedPoint;
|
|
if (QuickAxisTranslater.UpdateSnap(FRay3d(UpdateRay), SnappedPoint, PointConstraintFunc))
|
|
{
|
|
NewHitPosWorld = (FVector)SnappedPoint;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
FVector3d NewBrushPosLocal = WorldTransform.InverseTransformPosition(NewHitPosWorld);
|
|
FVector3d NewMoveDelta = NewBrushPosLocal - (FVector3d)StartBrushPosLocal;
|
|
|
|
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
|
|
if (LastMoveDelta.SquaredLength() > 0.)
|
|
{
|
|
if (NewMoveDelta.SquaredLength() > 0.)
|
|
{
|
|
// Linear Deformer: Update Mesh with the translation,
|
|
// Laplacian Deformer: Update handles constraints and set bDeformerNeedsToRun = true;.
|
|
|
|
SelectedDeformer.UpdateSolution(Mesh, [this, NewMoveDelta](FDynamicMesh3* TargetMesh, int VertIdx) {
|
|
return TargetMesh->GetVertex(VertIdx) + NewMoveDelta;
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// Reset mesh to initial positions.
|
|
|
|
SelectedDeformer.ClearSolution(Mesh);
|
|
}
|
|
//TODO: This is unseemly here, need to potentially defer this so that it's handled the same way as laplacian. Placeholder for now.
|
|
if (!bIsLaplacian)
|
|
{
|
|
DynamicMeshComponent->FastNotifyPositionsUpdated(true);
|
|
GetToolManager()->PostInvalidation();
|
|
}
|
|
}
|
|
|
|
LastMoveDelta = NewMoveDelta;
|
|
LastBrushPosLocal = NewBrushPosLocal;
|
|
|
|
bUpdatePending = false;
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::OnTick(float DeltaTime)
|
|
{
|
|
LaplacianDeformer->HandleWeights = TransformProps->HandleWeight;
|
|
LaplacianDeformer->bPostfixHandles = TransformProps->bPostFixHandles;
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::PrecomputeTopology()
|
|
{
|
|
FDynamicMesh3* Mesh = DynamicMeshComponent->GetMesh();
|
|
Topology = FGroupTopology(Mesh, true);
|
|
|
|
LinearDeformer.Initialize(Mesh, &Topology);
|
|
LaplacianDeformer->Initialize(Mesh, &Topology);
|
|
|
|
// Make the Constraint Buffer, zero weights, but current pos
|
|
LaplacianDeformer->InitializeConstraintBuffer();
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::Render(IToolsContextRenderAPI* RenderAPI)
|
|
{
|
|
ComputeUpdate();
|
|
|
|
GetToolManager()->GetContextQueriesAPI()->GetCurrentViewState(CameraState);
|
|
GetActiveQuickTransformer()->UpdateCameraState(CameraState);
|
|
|
|
DynamicMeshComponent->bExplicitShowWireframe = TransformProps->bShowWireframe;
|
|
FDynamicMesh3* TargetMesh = DynamicMeshComponent->GetMesh();
|
|
|
|
PolyEdgesRenderer.BeginFrame(RenderAPI, CameraState);
|
|
PolyEdgesRenderer.SetTransform((FTransform)WorldTransform);
|
|
|
|
|
|
for (FGroupTopology::FGroupEdge& Edge : Topology.Edges)
|
|
{
|
|
FVector3d A, B;
|
|
for (int eid : Edge.Span.Edges)
|
|
{
|
|
TargetMesh->GetEdgeV(eid, A, B);
|
|
PolyEdgesRenderer.DrawLine(A, B);
|
|
}
|
|
}
|
|
|
|
PolyEdgesRenderer.EndFrame();
|
|
|
|
|
|
HilightRenderer.BeginFrame(RenderAPI, CameraState);
|
|
HilightRenderer.SetTransform((FTransform)WorldTransform);
|
|
|
|
#ifdef DEBUG_ROI_WEIGHTS
|
|
FDynamicMesh3* MeshPtr = DynamicMeshComponent->GetMesh();
|
|
for (int32 VertexID : DynamicMeshComponent->GetMesh()->VertexIndicesItr())
|
|
{
|
|
float Color = 1.f - SrcMeshConstraintBuffer[VertexID].Weight;
|
|
HilightRenderer.DrawPoint(MeshPtr->GetVertex(VertexID), FLinearColor(Color, Color, Color, 1.f), 8, true);
|
|
}
|
|
|
|
#endif
|
|
|
|
#ifdef DEBUG_ROI_HANDLES
|
|
const FLinearColor FOOF{1.f, 0.f, 1.f, 1.f};
|
|
for (int VertIdx : HandleVertices)
|
|
{
|
|
HilightRenderer.DrawViewFacingCircle(TargetMesh->GetVertex(VertIdx), 0.8f, 8, FOOF, 3, false);
|
|
}
|
|
#endif
|
|
|
|
#ifdef DEBUG_ROI_TRIANGLES
|
|
const FLinearColor Whiteish{0.67f, 0.67f, 0.67f, 1.f};
|
|
for (int32 i = 0; i < SubsetIDBuffer.Num(); i += 3)
|
|
{
|
|
FVector3d A = TargetMesh->GetVertex(SubsetIDBuffer[i]);
|
|
FVector3d B = TargetMesh->GetVertex(SubsetIDBuffer[i + 1]);
|
|
FVector3d C = TargetMesh->GetVertex(SubsetIDBuffer[i + 2]);
|
|
HilightRenderer.DrawLine(A, B, Whiteish, 2.7f, true);
|
|
HilightRenderer.DrawLine(B, C, Whiteish, 2.7f, true);
|
|
HilightRenderer.DrawLine(C, A, Whiteish, 2.7f, true);
|
|
}
|
|
#endif
|
|
|
|
TopoSelector.VisualAngleSnapThreshold = this->VisualAngleSnapThreshold;
|
|
TopoSelector.DrawSelection(HilightSelection, &HilightRenderer, &CameraState);
|
|
HilightRenderer.EndFrame();
|
|
|
|
|
|
if (bInDrag)
|
|
{
|
|
GetActiveQuickTransformer()->Render(RenderAPI);
|
|
}
|
|
else
|
|
{
|
|
GetActiveQuickTransformer()->PreviewRender(RenderAPI);
|
|
}
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::OnPropertyModified(UObject* PropertySet, FProperty* Property) {}
|
|
|
|
FGroupTopologySelector::FSelectionSettings UDeformMeshPolygonsTool::GetTopoSelectorSettings()
|
|
{
|
|
FGroupTopologySelector::FSelectionSettings TopoSelectorSettings;
|
|
TopoSelectorSettings.bEnableFaceHits = TransformProps->bSelectFaces;
|
|
TopoSelectorSettings.bEnableEdgeHits = TransformProps->bSelectEdges;
|
|
TopoSelectorSettings.bEnableCornerHits = TransformProps->bSelectVertices;
|
|
|
|
return TopoSelectorSettings;
|
|
}
|
|
|
|
//
|
|
// Change Tracking
|
|
//
|
|
|
|
|
|
void UDeformMeshPolygonsTool::BeginChange()
|
|
{
|
|
const bool bIsLaplacian = (DeformationStrategy == EGroupTopologyDeformationStrategy::Laplacian);
|
|
if (!bIsLaplacian || LaplacianDeformer->IsDone())
|
|
{
|
|
if (ActiveVertexChange == nullptr)
|
|
{
|
|
ActiveVertexChange = new FMeshVertexChangeBuilder(EMeshVertexChangeComponents::VertexPositions |
|
|
EMeshVertexChangeComponents::OverlayNormals);
|
|
UpdateChangeFromROI(false);
|
|
LongTransactions.Open(LOCTEXT("PolyMeshDeformationChange", "PolyMesh Edit"), GetToolManager());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void UDeformMeshPolygonsTool::EndChange()
|
|
{
|
|
if (ActiveVertexChange != nullptr)
|
|
{
|
|
UpdateChangeFromROI(true);
|
|
GetToolManager()->EmitObjectChange(DynamicMeshComponent, MoveTemp(ActiveVertexChange->Change),
|
|
LOCTEXT("PolyMeshDeformationChange", "PolyMesh Edit"));
|
|
LongTransactions.Close(GetToolManager());
|
|
}
|
|
|
|
delete ActiveVertexChange;
|
|
ActiveVertexChange = nullptr;
|
|
}
|
|
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
|
|