736 lines
27 KiB
C++
736 lines
27 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Operations/ExtrudeBoundaryEdges.h"
|
|
|
|
#include "BoxTypes.h"
|
|
#include "DynamicMesh/DynamicMesh3.h"
|
|
#include "DynamicMeshEditor.h"
|
|
#include "Math/NumericLimits.h"
|
|
#include "Util/ProgressCancel.h"
|
|
#include "Parameterization/DynamicMeshUVEditor.h"
|
|
#include "VectorUtil.h"
|
|
|
|
namespace ExtrudeBoundaryEdgesLocals
|
|
{
|
|
using namespace UE::Geometry;
|
|
|
|
// Gives some vector orthogonal to NormalizedVectorIn
|
|
FVector3d GetOrthogonalVector(const FVector3d& NormalizedVectorIn)
|
|
{
|
|
FVector3d Result = NormalizedVectorIn.Cross(FVector3d::UnitY());
|
|
if (Result.IsZero())
|
|
{
|
|
// Must have been colinear with Y, so Z will work
|
|
return FVector3d::UnitZ();
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
// Given a non degenerate normal and edge, find direction in which we would want to extrude the edge to stay
|
|
// in the plane of the face and move edge orthogonally outward.
|
|
FVector3d GetOutwardVector(const FVector3d& NormalVector, const FVector3d& EdgeVector)
|
|
{
|
|
// If we're looking at the triangle with edge vector pointing up and triangle to the left (and
|
|
// Z towards us), then X should point right. In unreal's left handed coordinate system that
|
|
// actually means this:
|
|
return NormalVector.Cross(EdgeVector);
|
|
}
|
|
|
|
FExtrudeBoundaryEdges::FExtrudeFrame GetExtrudeFrameAtVertex(const FVector3d SourcePosition,
|
|
const FVector3d& IncomingTriNormal, const FVector3d& IncomingEdgeVector,
|
|
const FVector3d& OutgoingTriNormal, const FVector3d& OutgoingEdgeVector,
|
|
double ScalingLimit)
|
|
{
|
|
// TODO: We try to do some handling of degenerate normals below, but it is generally not good
|
|
// enough, and we are very likely to get nonsense extrude frames when we have degenerate normals.
|
|
// One option is to propagate normal information from neighbors, or propagate frame information
|
|
// from adjacent edges where a good one could be picked. These options are a fair amount of
|
|
// work for something that may not have much benefit, because the user probably still won't have
|
|
// much control and at best we would be hiding the effects of flawed geometry... Still, might
|
|
// be something to come back to.
|
|
|
|
if (OutgoingEdgeVector.IsZero() && IncomingEdgeVector.IsZero())
|
|
{
|
|
// Not much we can do here.
|
|
return FExtrudeBoundaryEdges::FExtrudeFrame(FFrame3d(SourcePosition));
|
|
}
|
|
|
|
|
|
auto CreateFrameFromEdgeAndNormal = [&SourcePosition](const FVector3d& NormalVector, const FVector3d& EdgeVector)
|
|
{
|
|
FVector3d FrameZ = NormalVector;
|
|
FVector3d FrameX = GetOutwardVector(NormalVector, EdgeVector);
|
|
// With the way we calculate the outward vector, this will actually just always end up -EdgeVector,
|
|
// but we'll just do the proper thing.
|
|
FVector3d FrameY = FrameZ.Cross(FrameX);
|
|
|
|
return FExtrudeBoundaryEdges::FExtrudeFrame(
|
|
FFrame3d(SourcePosition, FrameX, FrameY, FrameZ));
|
|
};
|
|
|
|
// Handle cases where we only have data on one neighbor.
|
|
if (IncomingEdgeVector.IsZero())
|
|
{
|
|
ensure(!OutgoingEdgeVector.IsZero());
|
|
return CreateFrameFromEdgeAndNormal(
|
|
OutgoingTriNormal.IsZero() ? GetOrthogonalVector(OutgoingEdgeVector) : OutgoingTriNormal,
|
|
OutgoingEdgeVector);
|
|
}
|
|
if (OutgoingEdgeVector.IsZero())
|
|
{
|
|
ensure(!IncomingEdgeVector.IsZero());
|
|
return CreateFrameFromEdgeAndNormal(
|
|
IncomingTriNormal.IsZero() ? GetOrthogonalVector(IncomingTriNormal) : IncomingTriNormal,
|
|
IncomingEdgeVector);
|
|
}
|
|
|
|
// If we got to here, we had neighbors on both sides. We'd like to use the normals in setting up the extrude
|
|
// frame, but one or both of them could be zeroes if the triangles were degenerate. Not totally clear what
|
|
// the best solution is, but for now we either find an orthogonal direction if they are both degenerate, or
|
|
// else we pick a plane for the degenerate edge such that it lies in the same plane as the desired X for the
|
|
// other face.
|
|
FVector3d IncomingNormalToUse = IncomingTriNormal;
|
|
FVector3d OutgoingNormalToUse = OutgoingTriNormal;
|
|
if (IncomingNormalToUse.IsZero())
|
|
{
|
|
if (OutgoingTriNormal.IsZero())
|
|
{
|
|
// Both faces degrenerate. X direction is orthogonal to both. Again, remember left handedness.
|
|
FVector3d FrameX = IncomingEdgeVector.Cross(OutgoingEdgeVector);
|
|
FrameX.Normalize();
|
|
if (FrameX.IsZero())
|
|
{
|
|
FrameX = GetOrthogonalVector(IncomingEdgeVector);
|
|
}
|
|
|
|
IncomingNormalToUse = IncomingEdgeVector.Cross(FrameX);
|
|
IncomingNormalToUse.Normalize();
|
|
OutgoingNormalToUse = OutgoingEdgeVector.Cross(FrameX);
|
|
OutgoingNormalToUse.Normalize();
|
|
}
|
|
else
|
|
{
|
|
FVector3d FrameX = GetOutwardVector(OutgoingTriNormal, OutgoingEdgeVector);
|
|
|
|
IncomingNormalToUse = IncomingEdgeVector.Cross(FrameX);
|
|
if (!IncomingNormalToUse.Normalize())
|
|
{
|
|
// If we were colinear with the X we picked, let's say that we're coplanar with the
|
|
// other face.
|
|
IncomingNormalToUse = OutgoingNormalToUse;
|
|
}
|
|
}
|
|
}
|
|
else if (OutgoingTriNormal.IsZero())
|
|
{
|
|
FVector3d FrameX = GetOutwardVector(IncomingTriNormal, IncomingEdgeVector);
|
|
|
|
OutgoingNormalToUse = OutgoingEdgeVector.Cross(FrameX);
|
|
if (!OutgoingNormalToUse.Normalize())
|
|
{
|
|
OutgoingNormalToUse = IncomingNormalToUse;
|
|
}
|
|
}
|
|
|
|
// At this point we have non degenerate edge vectors and normals.
|
|
// There are a few ways we can pick our frame. One option we've tried in the past is picking
|
|
// the X to be in the plane of both neighboring triangles, which works for following the "crease"
|
|
// of some edges nicely, but falls apart frequently if both edges lie in the plane of at least
|
|
// one of the triangles, because you frequently end up extruding along one of the edges. (E.g. imagine
|
|
// a classic pentagonal prism "house" shape and remove one of the roof faces, then try extruding that
|
|
// boundary. This method will nicely extend the adjacent faces in their planes. But if you delete only
|
|
// half of the roof face and try to extrude the boundary, you'll get very weird results as some vertices
|
|
// try to move along boundary edges instead, due to the flat portion of the roof constraining them.)
|
|
// A significantly more reliable option is to make X an average of the two outward directions. This
|
|
// tends to be a lot more predictable and less likely to generate garbage geometry. This is what we do.
|
|
|
|
FVector3d OutwardVector1 = GetOutwardVector(IncomingNormalToUse, IncomingEdgeVector);
|
|
FVector3d OutwardVector2 = GetOutwardVector(OutgoingNormalToUse, OutgoingEdgeVector);
|
|
FVector3d FrameX = OutwardVector1 + OutwardVector2;
|
|
FrameX.Normalize();
|
|
if (FrameX.IsZero())
|
|
{
|
|
// It seems like getting this situation would imply some very weird twisting topology
|
|
// where the next triangle manages to flip which side of the edge it is on, or Z fights
|
|
// the original triangle while moving backwards. There isn't a perfect thing to do here,
|
|
// so just use one of the edges and ignore the other.
|
|
return CreateFrameFromEdgeAndNormal(
|
|
IncomingTriNormal.IsZero() ? GetOrthogonalVector(IncomingTriNormal) : IncomingTriNormal,
|
|
IncomingEdgeVector);
|
|
}
|
|
FVector3d FrameZ = IncomingNormalToUse + OutgoingNormalToUse;
|
|
if (FrameZ.IsZero())
|
|
{
|
|
// This means that the normals flipped, usually by having the triangles fold back onto each
|
|
// other. We can pick a Z pointing in the plane of the edge by using the edge vectors here.
|
|
FrameZ = IncomingEdgeVector - OutgoingEdgeVector;
|
|
}
|
|
// Not sure whether X and Z are necessarily orthogonal at this point with our approaches... Seems
|
|
// like it might be the case, but let's be safe and not assume it.
|
|
FVector3d FrameY = FrameZ.Cross(FrameX);
|
|
FrameY.Normalize();
|
|
FrameZ = FrameX.Cross(FrameY);
|
|
// X and Y normalized and orthogonal, so Z shouldn't need normalization.
|
|
|
|
FExtrudeBoundaryEdges::FExtrudeFrame ExtrudeFrame(FFrame3d(
|
|
SourcePosition, FrameX, FrameY, FrameZ));
|
|
|
|
if (ScalingLimit > 1)
|
|
{
|
|
// This means that we're going to be adjusting our extrude directions in an attempt to keep extruded
|
|
// edges parallel. E.g. imagine extruding two edges at a 90 degree corner: the extrude directions
|
|
// at the flat sides are directly outward, whereas the outward direction at the corner is at an angle.
|
|
// If we extrude the same distance along each direction, the resulting corner is no longer 90 degrees
|
|
// because the vertex would need to move further along the diagonal.
|
|
// The problem is made more complicated by the fact that we are not necessarily extruding in the plane
|
|
// of the edges, and that the extrude frames at each vertex might be arbitrarily rotated depending on
|
|
// the adjoining faces. So, having some in-frame adjustment that keeps edges parallel without knowledge
|
|
// of adjacent frames is impossible.
|
|
// Instead we'll try to do our best for some common use cases. In particular, imagine extruding some
|
|
// edges on the boundary of a tesselated open-top box partially outward. We will adjust them such
|
|
// that the projection onto the plane of the original boundary is parallel, and we will do it by
|
|
// scaling only in the appropriate direction in the plane of the boundary. This will handle "tube"
|
|
// extrusions and simple planar mesh extrusion, and for planar boundaries, the user can scale down
|
|
// the result along the orthogonal axis to get parallel output even if incident faces made for
|
|
// inconsistent frame rotations.
|
|
|
|
// Direction to scale is average of the two incoming edge vectors
|
|
FVector3d DirectionToScale = IncomingEdgeVector - OutgoingEdgeVector;
|
|
DirectionToScale.Normalize();
|
|
|
|
// If direction to scale is zero, that means we're along a flat section, where we don't need
|
|
// to adjust.
|
|
if (!DirectionToScale.IsZero())
|
|
{
|
|
|
|
// We compare against an outward vector in the plane of the two vectors (rather than in the plane of
|
|
// adjoining triangles, which we used when finding the extrude frame)
|
|
FVector3d NormalToEdges = IncomingEdgeVector.Cross(OutgoingEdgeVector); // Unnormalized
|
|
FVector3d OutwardVectorInEdgePlane = IncomingEdgeVector.Cross(NormalToEdges);
|
|
OutwardVectorInEdgePlane.Normalize();
|
|
|
|
// Figure out how far to scale, with an upper limit
|
|
double CosTheta = DirectionToScale.Dot(OutwardVectorInEdgePlane);
|
|
if (CosTheta == 0)
|
|
{
|
|
// Presumably this comes about from the edges doubling back onto themselves, but we'll double
|
|
// check that we're not dealing with a straight edge to be safe.
|
|
if (IncomingEdgeVector.Dot(OutgoingEdgeVector) < 0)
|
|
{
|
|
ExtrudeFrame.InFrameScaleDirection = ExtrudeFrame.Frame.ToFrameVector(DirectionToScale);
|
|
ExtrudeFrame.Scaling = ScalingLimit;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ExtrudeFrame.InFrameScaleDirection = ExtrudeFrame.Frame.ToFrameVector(DirectionToScale);
|
|
ExtrudeFrame.Scaling = FMath::Min(1 / FMath::Abs(CosTheta), ScalingLimit);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ExtrudeFrame;
|
|
}
|
|
|
|
int32 FindSharpestAngleMatchEdge(const FDynamicMesh3& Mesh, int32 EidToMatch, int32 Vid,
|
|
const FVector3d& VertexNormal, const TArray<int32> CandidateEids)
|
|
{
|
|
// The approach here is pretty much the same as in FMeshBoundaryLoops::FindLeftTurnEdge, except that
|
|
// we're actually looking for the right turn edge (better if you're concerned with following a hole/boundary
|
|
// instead of the smallest island), we only look at candidate eids, and we can look from outgoing edge
|
|
// instead.
|
|
|
|
FIndex2i InputEdgeVids = Mesh.GetOrientedBoundaryEdgeV(EidToMatch);
|
|
bool bInputIsIncomingEdge = InputEdgeVids.B == Vid;
|
|
|
|
if (!ensure(bInputIsIncomingEdge || InputEdgeVids.A == Vid))
|
|
{
|
|
return IndexConstants::InvalidID;
|
|
}
|
|
|
|
// We're going to be measuring the angle from incoming to outgoing edge, and we want it to be the most
|
|
// clockwise. Normally that would mean most negative in the plane, but with left handed coordinates
|
|
// it is most positive...
|
|
double BestAngle = TNumericLimits<double>::Lowest();
|
|
int32 BestEid = IndexConstants::InvalidID;
|
|
|
|
FVector3d InputEdgeVector = Mesh.GetVertex(InputEdgeVids.B) - Mesh.GetVertex(InputEdgeVids.A);
|
|
|
|
for (int32 Eid : CandidateEids)
|
|
{
|
|
if (Eid == EidToMatch)
|
|
{
|
|
continue;
|
|
}
|
|
FIndex2i Vids = Mesh.GetOrientedBoundaryEdgeV(Eid);
|
|
bool bIsIncomingEdge = Vids.B == Vid;
|
|
if (!ensure(bIsIncomingEdge || Vids.A == Vid))
|
|
{
|
|
continue;
|
|
}
|
|
if (bInputIsIncomingEdge == bIsIncomingEdge)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FVector3d EdgeVector = Mesh.GetVertex(Vids.B) - Mesh.GetVertex(Vids.A);
|
|
double Angle = TNumericLimits<double>::Min();
|
|
if (bInputIsIncomingEdge)
|
|
{
|
|
Angle = VectorUtil::PlaneAngleSignedD(InputEdgeVector, EdgeVector, VertexNormal);
|
|
}
|
|
else
|
|
{
|
|
Angle = VectorUtil::PlaneAngleSignedD(EdgeVector, InputEdgeVector, VertexNormal);
|
|
}
|
|
|
|
if (Angle > BestAngle)
|
|
{
|
|
BestEid = Eid;
|
|
BestAngle = Angle;
|
|
}
|
|
}
|
|
|
|
return BestEid;
|
|
}
|
|
}
|
|
|
|
namespace UE::Geometry
|
|
{
|
|
FExtrudeBoundaryEdges::FExtrudeBoundaryEdges(FDynamicMesh3* MeshIn)
|
|
: Mesh(MeshIn)
|
|
{
|
|
}
|
|
|
|
bool FExtrudeBoundaryEdges::Apply(FProgressCancel* Progress)
|
|
{
|
|
using namespace ExtrudeBoundaryEdgesLocals;
|
|
|
|
// Complications in exturding edges:
|
|
// 1. We can't just always split bowties in our input, because we might be using the operation to fix
|
|
// a bowtie. For instance imagine two circle holes tangent at a bowtie. We might be selecting just
|
|
// one of the holes and extruding inward. If that bowtie were broken, we would get an undesirable
|
|
// split in the result, instead of a complete quad ring.
|
|
// 2. We DO need to do something about bowties if we're extruding more than two incident boundary edges
|
|
// (imagine extruding the boundaries of both holes in the example above). Keeping the vert alone in
|
|
// the destination would attempt to create a non-manifold edge on it.
|
|
// 3. Degenerate edges are a problem when determining per-vertex extrude frames, because they don't
|
|
// have proper edge/normal vectors, and as such could end up going to different places than their
|
|
// neighbors.
|
|
|
|
// We take the following approach for 1 and 2:
|
|
// Every extruded edge gets a quad with verts a, b, c, d, where a -> b is one of the original edges,
|
|
// oriented in triangle direction, and c -> d is the "extruded edge". When creating c or d, we'll
|
|
// enforce the constraint that each copy can be used as "c" in no more than one quad, and "d" in no
|
|
// more than one quad. In other words, we'll create a new copy if the source vid is the destination
|
|
// or the source for more than one edge in the original edges to extrude.
|
|
// This means that we will actually end up splitting verts in the destination even if they were not
|
|
// a bowtie vert if the original vert had flipped neighbor triangles. That seems like a reasonable
|
|
// thing to do in a situation where the user really shouldn't have any expectation.
|
|
//
|
|
// For 3, we currently decide to not do anything, instead allowing for whatever garbage results
|
|
// might be created when a user tries to extrude degenerate edges (though the results won't be garbage
|
|
// when extruding in a single direction). In an ideal world, we would implement some grouping of
|
|
// vertices that are connected by degenerate edges, propage the vector information across them, and
|
|
// make sure that vertices in the group get the same extrude frame. While possible, that seems not
|
|
// worth the pain and potential pitfalls, especially if you consider pathological cases of some web
|
|
// of degenerate boundary edges rather than a single chain, where there are multiple extrude frames
|
|
// that each might want to use. The saner approach is to expect the user to collapse degenerate edges
|
|
// in the input first.
|
|
|
|
if (!ensure(Mesh && OffsetPositionFunc))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (InputEids.IsEmpty())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (Progress && Progress->Cancelled()) { return false; }
|
|
|
|
TArray<FNewVertSourceData> NewVertData;
|
|
TArray<int32> NewVertVids; // eventually 1:1 with NewVertData
|
|
TMap<int32, FIndex2i> EidToIndicesIntoNewVerts;
|
|
|
|
// Pair up the edges across vertices so that we know when to split verts
|
|
bool bSuccess = FExtrudeBoundaryEdges::GetInputEdgePairings(
|
|
*Mesh, InputEids, bAssignAnyBoundaryNeighborToUnmatched,
|
|
NewVertData, EidToIndicesIntoNewVerts);
|
|
if (!ensure(bSuccess)) { return false; }
|
|
|
|
if (NewVertData.IsEmpty())
|
|
{
|
|
return true;
|
|
}
|
|
if (Progress && Progress->Cancelled()) { return false; }
|
|
|
|
// Create all the new verts
|
|
for (const FNewVertSourceData& VertSourceData : NewVertData)
|
|
{
|
|
int32 CreatedVid = Mesh->AppendVertex(*Mesh, VertSourceData.SourceVid);
|
|
NewVertVids.Add(CreatedVid);
|
|
}
|
|
if (Progress && Progress->Cancelled()) { return false; }
|
|
|
|
// Calculate extrude frames, if used
|
|
TArray<FExtrudeFrame> NewVertExtrudeFrames; // if used, 1:1 with NewVertData
|
|
if (bUsePerVertexExtrudeFrames)
|
|
{
|
|
for (const FNewVertSourceData& VertSourceData : NewVertData)
|
|
{
|
|
FExtrudeFrame& ExtrudeFrame = NewVertExtrudeFrames.Emplace_GetRef();
|
|
GetExtrudeFrame(*Mesh, VertSourceData.SourceVid,
|
|
VertSourceData.SourceEidPair.A, VertSourceData.SourceEidPair.B,
|
|
ExtrudeFrame, ScalingAdjustmentLimit);
|
|
}
|
|
if (Progress && Progress->Cancelled()) { return false; }
|
|
}
|
|
|
|
// Perform the offset for all of the created Vids
|
|
for (int32 i = 0; i < NewVertVids.Num(); ++i)
|
|
{
|
|
int32 NewVid = NewVertVids[i];
|
|
int32 SourceVid = NewVertData[i].SourceVid;
|
|
|
|
Mesh->SetVertex(NewVid, OffsetPositionFunc(
|
|
Mesh->GetVertex(NewVid),
|
|
bUsePerVertexExtrudeFrames ? NewVertExtrudeFrames[i] : FExtrudeFrame(),
|
|
SourceVid));
|
|
}
|
|
if (Progress && Progress->Cancelled()) { return false; }
|
|
|
|
// TODO: We could pretty easily add a version of stitching code to FDynamicMeshEditor that stitches
|
|
// arbitrary vert sequences rather than loops, and then maybe we could follow FOffsetMeshRegion and
|
|
// FInsetMeshRegion's examples to process the resulting strips. However it would require code that
|
|
// assembles our input eids into sequences first. That's not too hard once we have the pairings above,
|
|
// but it isn't clear whether the extra effort will or will not be worthwhile. For now we do the easy
|
|
// thing and just prep all the quads and stitch them, then just box project UV's onto the whole thing.
|
|
// The UV generation would be the main thing we'd want to improve, probably.
|
|
|
|
int32 GroupID = 0;
|
|
if (GroupsToSetPerEid.IsSet() && !ensure(GroupsToSetPerEid->Num() == InputEids.Num()))
|
|
{
|
|
GroupsToSetPerEid.Reset();
|
|
}
|
|
if (Mesh->HasTriangleGroups() && !GroupsToSetPerEid.IsSet())
|
|
{
|
|
GroupID = Mesh->AllocateTriangleGroup();
|
|
}
|
|
|
|
// Do the stitching
|
|
for (int32 i = 0; i < InputEids.Num(); ++i)
|
|
{
|
|
int32 Eid = InputEids[i];
|
|
GroupID = GroupsToSetPerEid.IsSet() ? (*GroupsToSetPerEid)[i] : GroupID;
|
|
|
|
if (!Mesh->IsBoundaryEdge(Eid))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const FIndex2i* NewVertIndices = EidToIndicesIntoNewVerts.Find(Eid);
|
|
if (!ensure(NewVertIndices
|
|
&& NewVertIndices->A >= 0 && NewVertIndices->A < NewVertVids.Num()
|
|
&& NewVertIndices->B >= 0 && NewVertIndices->B < NewVertVids.Num()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FIndex2i Vids = Mesh->GetOrientedBoundaryEdgeV(Eid);
|
|
int32 VidA = Vids.A;
|
|
int32 VidB = Vids.B;
|
|
int32 VidC = NewVertVids[NewVertIndices->A];
|
|
int32 VidD = NewVertVids[NewVertIndices->B];
|
|
|
|
FIndex3i Tri1(VidB, VidA, VidD);
|
|
NewTids.Add(Mesh->AppendTriangle(Tri1, GroupID));
|
|
FIndex3i Tri2(VidA, VidC, VidD);
|
|
NewTids.Add(Mesh->AppendTriangle(Tri2, GroupID));
|
|
|
|
NewExtrudedEids.Add(Mesh->FindEdgeFromTri(VidC, VidD, NewTids.Last()));
|
|
}
|
|
|
|
// Give UVs to the new triangles
|
|
if (Mesh->HasAttributes())
|
|
{
|
|
if (Progress && Progress->Cancelled()) { return false; }
|
|
|
|
FAxisAlignedBox3d Box;
|
|
for (int32 Tid : NewTids)
|
|
{
|
|
FVector3d Vert1, Vert2, Vert3;
|
|
Mesh->GetTriVertices(Tid, Vert1, Vert2, Vert3);
|
|
Box.Contain(Vert1);
|
|
Box.Contain(Vert2);
|
|
Box.Contain(Vert3);
|
|
}
|
|
|
|
FDynamicMeshUVEditor UVEd(Mesh, Mesh->Attributes()->PrimaryUV());
|
|
FFrame3d BoxFrame(Box.Center());
|
|
FVector3d BoxDimensions = Box.Extents() * 2;
|
|
UVEd.SetTriangleUVsFromBoxProjection(NewTids, [](FVector3d Position) {return Position; }, BoxFrame, BoxDimensions);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// See comment in Apply()
|
|
/**
|
|
* @param NewVertDataOut One entry for each vertex that should be created, with information on its relevant neighbor edges
|
|
* and the source vertex.
|
|
* @param EidToIndicesIntoNewVertsOut Mapping from Eid to two indices into NewVertDataOut that indicate the two new vertices
|
|
* that would be stitched into the quad containing this edge.
|
|
*/
|
|
bool FExtrudeBoundaryEdges::GetInputEdgePairings(
|
|
const FDynamicMesh3& Mesh, TArray<int32>& InputEids, bool bAssignAnyBoundaryNeighborToUnmatched,
|
|
TArray<FNewVertSourceData>& NewVertDataOut, TMap<int32, FIndex2i>& EidToIndicesIntoNewVertsOut)
|
|
{
|
|
using namespace ExtrudeBoundaryEdgesLocals;
|
|
|
|
NewVertDataOut.Reset();
|
|
EidToIndicesIntoNewVertsOut.Reset();
|
|
|
|
// Helper that updates both NewVertDataOut and EidToIndicesIntoNewVertsOut
|
|
auto AddEdgePairing = [&NewVertDataOut, &EidToIndicesIntoNewVertsOut](int32 SourceVid, int32 IncomingEid, int32 OutgoingEid)
|
|
{
|
|
int32 NewVertIndex = NewVertDataOut.Emplace();
|
|
NewVertDataOut[NewVertIndex].SourceVid = SourceVid;
|
|
NewVertDataOut[NewVertIndex].SourceEidPair = FIndex2i(IncomingEid, OutgoingEid);
|
|
|
|
auto UpdateMapping = [NewVertIndex, &EidToIndicesIntoNewVertsOut](int32 Eid, bool bUpdatingOutgoingEdge)
|
|
{
|
|
if (Eid == IndexConstants::InvalidID)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FIndex2i* ExistingEntry = EidToIndicesIntoNewVertsOut.Find(Eid);
|
|
if (ExistingEntry)
|
|
{
|
|
// If we're updating the outgoing edge, then we are its first vertex
|
|
(*ExistingEntry)[bUpdatingOutgoingEdge ? 0 : 1] = NewVertIndex;
|
|
}
|
|
else
|
|
{
|
|
FIndex2i Entry;
|
|
Entry[bUpdatingOutgoingEdge ? 0 : 1] = NewVertIndex;
|
|
EidToIndicesIntoNewVertsOut.Add(Eid, Entry);
|
|
}
|
|
};
|
|
|
|
UpdateMapping(IncomingEid, false);
|
|
UpdateMapping(OutgoingEid, true);
|
|
};
|
|
|
|
// We will be building up a list of associations of verts to two neighbor edges. If we run into
|
|
// a situation where there are more than two candidates, we will put the entry into AmbiguousVids
|
|
// and (later) remove it from this map.
|
|
TMap<int32, FIndex2i> UnambiguousVidToEdgePairings;
|
|
|
|
TArray<int32> AmbiguousVids;
|
|
|
|
for (int32 Eid : InputEids)
|
|
{
|
|
if (!Mesh.IsBoundaryEdge(Eid))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FIndex2i Vids = Mesh.GetOrientedBoundaryEdgeV(Eid);
|
|
for (int SubIdx = 0; SubIdx < 2; ++SubIdx)
|
|
{
|
|
int32 Vid = Vids[SubIdx];
|
|
FIndex2i* EdgePairing = UnambiguousVidToEdgePairings.Find(Vid);
|
|
if (!EdgePairing)
|
|
{
|
|
EdgePairing = &UnambiguousVidToEdgePairings.Add(Vid, FIndex2i::Invalid());
|
|
}
|
|
if ((*EdgePairing)[1-SubIdx] != IndexConstants::InvalidID)
|
|
{
|
|
// Found multiple incoming or outgoing edges. We'll handle this later.
|
|
AmbiguousVids.Add(Vid);
|
|
}
|
|
else
|
|
{
|
|
(*EdgePairing)[1-SubIdx] = Eid;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Crude (not angle-weighed) vertex normal calculation used for projection below, same as
|
|
// FMeshBoundaryLoops::GetVertexNormal.
|
|
auto CalculateVertexNormal = [&Mesh](int32 Vid)
|
|
{
|
|
FVector3d VertexNormal = FVector3d::Zero();
|
|
for (int32 Tid : Mesh.VtxTrianglesItr(Vid))
|
|
{
|
|
VertexNormal += Mesh.GetTriNormal(Tid);
|
|
}
|
|
VertexNormal.Normalize();
|
|
return VertexNormal;
|
|
};
|
|
|
|
TSet<int32> SelectedEids(InputEids);
|
|
|
|
// Sort out the ambiguous verts first, and remove those entries from UnambiguousVidToEdgePairings
|
|
for (int32 AmbiguousVid : AmbiguousVids)
|
|
{
|
|
UnambiguousVidToEdgePairings.Remove(AmbiguousVid);
|
|
|
|
TArray<int32> IncomingEids;
|
|
TArray<int32> OutgoingEids;
|
|
for (int32 Eid : Mesh.VtxEdgesItr(AmbiguousVid))
|
|
{
|
|
if (!Mesh.IsBoundaryEdge(Eid) || !SelectedEids.Contains(Eid))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FIndex2i Vids = Mesh.GetOrientedBoundaryEdgeV(Eid);
|
|
if (Vids.A == AmbiguousVid)
|
|
{
|
|
OutgoingEids.Add(Eid);
|
|
}
|
|
else
|
|
{
|
|
ensure(Vids.B == AmbiguousVid);
|
|
IncomingEids.Add(Eid);
|
|
}
|
|
}
|
|
|
|
FVector3d VertexNormal = CalculateVertexNormal(AmbiguousVid);
|
|
|
|
for (int32 IncomingEid : IncomingEids)
|
|
{
|
|
int32 OutgoingEid = FindSharpestAngleMatchEdge(Mesh, IncomingEid, AmbiguousVid, VertexNormal, OutgoingEids);
|
|
if (OutgoingEid != IndexConstants::InvalidID)
|
|
{
|
|
// One candidate used up
|
|
OutgoingEids.Remove(OutgoingEid);
|
|
}
|
|
AddEdgePairing(AmbiguousVid, IncomingEid, OutgoingEid);
|
|
}
|
|
// If we still have some outgoing edges, we know they are unmatched now.
|
|
for (int32 OutgoingEid : OutgoingEids)
|
|
{
|
|
AddEdgePairing(AmbiguousVid, IndexConstants::InvalidID, OutgoingEid);
|
|
}
|
|
}// done processing ambiguous verts
|
|
|
|
for (const TPair<int32, FIndex2i>& KeyValue : UnambiguousVidToEdgePairings)
|
|
{
|
|
AddEdgePairing(KeyValue.Key, KeyValue.Value.A, KeyValue.Value.B);
|
|
}
|
|
|
|
if (bAssignAnyBoundaryNeighborToUnmatched)
|
|
{
|
|
for (FNewVertSourceData& VertData : NewVertDataOut)
|
|
{
|
|
if (!VertData.SourceEidPair.Contains(IndexConstants::InvalidID))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FVector3d VertexNormal = CalculateVertexNormal(VertData.SourceVid);
|
|
TArray<int32> NeighborhoodEids;
|
|
Mesh.GetAllVtxBoundaryEdges(VertData.SourceVid, NeighborhoodEids);
|
|
|
|
if (VertData.SourceEidPair.A == IndexConstants::InvalidID
|
|
&& ensure(VertData.SourceEidPair.B != IndexConstants::InvalidID))
|
|
{
|
|
VertData.SourceEidPair.A = FindSharpestAngleMatchEdge(Mesh,
|
|
VertData.SourceEidPair.B, VertData.SourceVid, VertexNormal, NeighborhoodEids);
|
|
}
|
|
else if (VertData.SourceEidPair.B == IndexConstants::InvalidID
|
|
&& ensure(VertData.SourceEidPair.A != IndexConstants::InvalidID))
|
|
{
|
|
VertData.SourceEidPair.B = FindSharpestAngleMatchEdge(Mesh,
|
|
VertData.SourceEidPair.A, VertData.SourceVid, VertexNormal, NeighborhoodEids);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FExtrudeBoundaryEdges::GetExtrudeFrame(const FDynamicMesh3& Mesh, int32 Vid,
|
|
int32 IncomingEid, int32 OutgoingEid, FExtrudeFrame& ExtrudeFrameOut,
|
|
double ScalingLimit)
|
|
{
|
|
using namespace ExtrudeBoundaryEdgesLocals;
|
|
|
|
auto GetEdgeVectorAndNormal = [&Mesh, Vid](int32 Eid, bool bIsIncoming, FVector3d& EdgeVectorOut, FVector3d& NormalVectorOut) -> bool
|
|
{
|
|
if (Eid == IndexConstants::InvalidID)
|
|
{
|
|
EdgeVectorOut = FVector3d::Zero();
|
|
NormalVectorOut = FVector3d::Zero();
|
|
return true;
|
|
}
|
|
|
|
if (!ensure(Mesh.IsBoundaryEdge(Eid)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FIndex2i Vids = Mesh.GetOrientedBoundaryEdgeV(Eid);
|
|
if (bIsIncoming && !ensure(Vids.B == Vid))
|
|
{
|
|
return false;
|
|
}
|
|
else if (!bIsIncoming && !ensure(Vids.A == Vid))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int32 Tid = Mesh.GetEdgeT(Eid).A;
|
|
NormalVectorOut = Mesh.GetTriNormal(Tid);
|
|
|
|
EdgeVectorOut = Mesh.GetVertex(Vids.B) - Mesh.GetVertex(Vids.A);
|
|
EdgeVectorOut.Normalize();
|
|
|
|
return true;
|
|
};
|
|
|
|
FVector3d EdgeVector1, EdgeVector2, Normal1, Normal2;
|
|
|
|
if (!GetEdgeVectorAndNormal(IncomingEid, true, EdgeVector1, Normal1)
|
|
|| !GetEdgeVectorAndNormal(OutgoingEid, false, EdgeVector2, Normal2))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ExtrudeFrameOut = GetExtrudeFrameAtVertex(
|
|
Mesh.GetVertex(Vid),
|
|
Normal1, EdgeVector1,
|
|
Normal2, EdgeVector2, ScalingLimit);
|
|
|
|
return true;
|
|
}
|
|
|
|
FVector3d FExtrudeBoundaryEdges::FExtrudeFrame::FromFramePoint(FVector3d FramePoint) const
|
|
{
|
|
if (InFrameScaleDirection.IsSet() && Scaling != 1)
|
|
{
|
|
FVector3d Offset = FramePoint.Dot(InFrameScaleDirection.GetValue()) * (Scaling - 1) * InFrameScaleDirection.GetValue();
|
|
return Frame.FromFramePoint(FramePoint + Offset);
|
|
}
|
|
return Frame.FromFramePoint(FramePoint);
|
|
}
|
|
|
|
FVector3d UE::Geometry::FExtrudeBoundaryEdges::FExtrudeFrame::ToFramePoint(FVector3d WorldPoint) const
|
|
{
|
|
FVector3d FramePoint = Frame.ToFramePoint(WorldPoint);
|
|
if (InFrameScaleDirection.IsSet() && Scaling != 1
|
|
// Can't undo zero scaling, but that shouldn't come up as a value
|
|
&& ensure(Scaling != 0))
|
|
{
|
|
double DotProduct = FramePoint.Dot(InFrameScaleDirection.GetValue());
|
|
return FramePoint - (1 - (1 / Scaling)) * DotProduct * InFrameScaleDirection.GetValue();
|
|
}
|
|
return FramePoint;
|
|
}
|
|
|
|
}//end UE::Geometry
|