Files
2025-05-18 13:04:45 +08:00

180 lines
8.0 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "DynamicMesh/DynamicMesh3.h"
#include "DynamicMesh/DynamicMeshAABBTree3.h"
#include "Quaternion.h"
#include "Spatial/PointHashGrid3.h"
#define UE_API DYNAMICMESH_API
namespace UE
{
namespace Geometry
{
/**
* FMeshPlanarSymmetry detects pairwise symmetry relationships between vertices in a mesh, given a symmetry frame.
* Once those relationships are known, symmetry can be re-enforced after mesh edits.
*
* Vertices within a tolerance band of the symmetry plane do not have a mirror and are considered "on plane"
* and will be snapped to the symmetry plane when enforcing symmetry after edits.
*
* The Symmetry Plane is specified by a Frame3d where the XY axes define the plane and the Z axis is the plane normal.
* So references to "Positive" side below are relative to that Z axis/plane
*/
class FMeshPlanarSymmetry
{
public:
// If true, symmetry-finding can return false if it detects a symmetry where all points are on the symmetry plane, rather than attempt to fit a symmetry plane in that case.
bool bCanIgnoreDegenerateSymmetries = true;
/**
* Given a Mesh, an AABBTree, and a Symmetry Plane/Frame, detect any pairs of vertices with
* planar/mirror-symmetry relationships, as well as "on plane" vertices
* @return false if any non-on-plane vertex fails to find a match.
*/
UE_API bool Initialize(FDynamicMesh3* Mesh, FDynamicMeshAABBTree3* Spatial, FFrame3d SymmetryFrameIn);
/**
* Given a Mesh and its bounding box, and a Symmetry Plane/Frame, detect any pairs of vertices with
* planar/mirror-symmetry relationships, as well as "on plane" vertices
* @return false if any non-on-plane vertex fails to find a match.
*/
UE_API bool Initialize(FDynamicMesh3* Mesh, const FAxisAlignedBox3d& Bounds, FFrame3d SymmetryFrameIn);
/**
* Given a Mesh and its bounding box, find a Symmetry Plane/Frame and detect any pairs of vertices with
* planar/mirror-symmetry relationships, as well as "on plane" vertices
* @param SymmetryFrameOut Returns the discovered symmetry frame by reference, if one was found.
* @param PreferredNormals Optionally try to find a symmetry frame aligned to any normals passed in to this array. Tries the normals in order, so the first normal that fits (if any) will be used.
* @return false if any non-on-plane vertex fails to find a match.
*/
UE_API bool FindPlaneAndInitialize(FDynamicMesh3* Mesh, const FAxisAlignedBox3d& Bounds, FFrame3d& SymmetryFrameOut, TArrayView<const FVector3d> PreferredNormals = TArrayView<const FVector3d>());
/**
* @return the input Point mirrored across the Symmetry plane
*/
UE_API FVector3d GetMirroredPosition(const FVector3d& Position) const;
/**
* @return the input Vector/Axis mirrored across the Symmetry plane
*/
UE_API FVector3d GetMirroredAxis(const FVector3d& Axis) const;
/**
* @return the input Quaternion mirrored across the Symmetry plane
*/
UE_API FQuaterniond GetMirroredOrientation(const FQuaterniond& Orientation) const;
/**
* @return the input Frame mirrored to the "positive" side (unchanged if it is already on the positive side)
*/
UE_API FFrame3d GetPositiveSideFrame(FFrame3d FromFrame) const;
/**
* Update all the symmetry vertex positions based on their Positive-side pair vertex.
* Also snap all on-plane vertices to the symmetry plane
*/
UE_API void FullSymmetryUpdate();
/** @return true if the TestMesh vertices match the current tracked vertex symmetry correspondence (i.e., the TestMesh vertex IDs match the known correspondence, and have the expected mirror symmetry) */
UE_API bool ValidateSymmetry(const FDynamicMesh3& TestMesh) const;
/** @return true if the TestMesh vertices in VertexROI match the current tracked vertex symmetry correspondence (i.e., the vertex IDs match the known correspondence, and have the expected mirror symmetry) */
UE_API bool ValidateSymmetry(const FDynamicMesh3& TestMesh, TConstArrayView<int32> VertexROI) const;
//
// The set of functions below are intended to be used together in situations like 3D sculpting
// where we want to apply symmetry constraints within a "brush region of interest (ROI)"
// See UMeshVertexSculptTool for example usage
//
/**
* Computes list of vertices that are mirror-constrained to vertices in VertexROI.
* If bForceSameSizeWithGaps is true, MirrorVertexROIOut will be the same size as VertexROI, and -1 is stored
* for vertices in VertexROI that are source vertices or on the symmetry plane. Otherwise those vertices are skipped.
*/
UE_API void GetMirrorVertexROI(const TArray<int>& VertexROI, TArray<int>& MirrorVertexROIOut, bool bForceSameSizeWithGaps) const;
/**
* For any vertices in VertexIndices that are on-plane, take the position in VertexPositionsInOut, apply the
* on-plane constraint, and return the constrained position in VertexPositionsInOut (ie VertexPositionsInOut is updated to new positions)
*/
UE_API void ApplySymmetryPlaneConstraints(const TArray<int>& VertexIndices, TArray<FVector3d>& VertexPositionsInOut) const;
/**
* Given the pairing (SourceVertexROI, SourceVertexPositions), compute the symmetry-constrained vertex positions for
* MirrorVertexROI and store in MirrorVertexPositionsOut. This function assumes that the MirrorVertexROI was computed
* by calling GetMirrorVertexROI(SourceVertexROI, MirrorVertexROI, bForceSameSizeWithGaps=true), ie all the arrays
* must be the same length
*/
UE_API void ComputeSymmetryConstrainedPositions(
const TArray<int>& SourceVertexROI,
const TArray<int>& MirrorVertexROI,
const TArray<FVector3d>& SourceVertexPositions,
TArray<FVector3d>& MirrorVertexPositionsOut) const;
protected:
FDynamicMesh3* TargetMesh = nullptr;
FFrame3d SymmetryFrame;
FVector3d CachedSymmetryAxis;
struct FSymmetryVertex
{
// initial distance to symmetry plane
double PlaneSignedDistance = 0.0;
// true if this is a positive-side vertex
bool bIsSourceVertex = true;
// true if this is an on-plane vertex (todo: possibly combine with bIsSourceVertex as an enum)
bool bOnPlane = false;
// VertexID of paired vertex
int32 PairedVertex = -1;
};
// list of vertices, size == TargetMesh.MaxVertexID()
TArray<FSymmetryVertex> Vertices;
UE_API void UpdateSourceVertex(int32 VertexID);
UE_API void UpdatePlaneVertex(int32 VertexID);
private:
// Helper for computing + assigning symmetry matches
bool AssignMatches(const FDynamicMesh3* Mesh, const TPointHashGrid3d<int32>& VertexHash, const TArray<FVector3d>& InvariantFeatures, FFrame3d SymmetryFrameIn);
void ComputeMeshInfo(const FDynamicMesh3* Mesh, const FAxisAlignedBox3d& Bounds, TArray<FVector3d>& InvariantFeaturesOut, FVector3d& MeshCentroidOut);
bool Validate(const FDynamicMesh3* Mesh);
//
// Tolerances used for matching / hashing in symmetry-finding
//
// Point must be within this distance from the symmetry plane to be considered "on" the plane.
// Note: Vertices can still be matched below this tolerance; this is just the distance at which a lack of match is considered a lack of symmetry
// Note: Each Tolerance Factor will be multiplied by "ErrorScale", so that it can appropriately scale up for larger inputs. SetErrorScale() must be called before using these tolerances.
constexpr static double OnPlaneToleranceFactor = (double)FMathf::ZeroTolerance * .5;
constexpr static double MatchVertexToleranceFactor = OnPlaneToleranceFactor * 2;
// Note performance of vertex hashing is much better when VertexHashCellSize is large enough that most hash lookups only need to look at a single cell
constexpr static double VertexHashCellSizeFactor = MatchVertexToleranceFactor * 10;
// Note for features we specify a direct tolerance instead of a factor, as features are already in their own re-scaled space
constexpr static double MatchFeaturesTolerance = UE_DOUBLE_KINDA_SMALL_NUMBER;
// Internal error scale; for large meshes, we need larger tolerances because floating point values will be less accurate
double ErrorScale = 1;
void SetErrorScale(const FAxisAlignedBox3d& Bounds);
};
} // end namespace UE::Geometry
} // end namespace UE
#undef UE_API