You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
310 lines
13 KiB
C++
310 lines
13 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include <Editor/PolygonPrismMeshUtils.h>
|
|
#include <AzCore/std/containers/unordered_map.h>
|
|
#include <AzCore/Math/Vector3.h>
|
|
|
|
namespace PolygonPrismMeshUtils
|
|
{
|
|
AZ::Vector3 CalculateAngles(p2t::Triangle& triangle)
|
|
{
|
|
// Calculate the internal angles of the triangle from the edge lengths using the law of cosines
|
|
const float e0 = static_cast<float>((*triangle.GetPoint(1) - *triangle.GetPoint(0)).Length());
|
|
const float e1 = static_cast<float>((*triangle.GetPoint(2) - *triangle.GetPoint(1)).Length());
|
|
const float e2 = static_cast<float>((*triangle.GetPoint(0) - *triangle.GetPoint(2)).Length());
|
|
|
|
const float angle0 = acosf(AZ::GetClamp((e0 * e0 + e2 * e2 - e1 * e1) / (2.0f * e0 * e2), -1.0f, 1.0f));
|
|
const float angle1 = acosf(AZ::GetClamp((e0 * e0 + e1 * e1 - e2 * e2) / (2.0f * e0 * e1), -1.0f, 1.0f));
|
|
|
|
return AZ::Vector3(angle0, angle1, AZ::Constants::Pi - angle0 - angle1);
|
|
}
|
|
|
|
bool InternalEdgeCompare::operator()(const InternalEdge& left, const InternalEdge& right) const
|
|
{
|
|
return left.m_minAngle > right.m_minAngle;
|
|
}
|
|
|
|
void Mesh2D::CreateFromPoly2Tri(const std::vector<p2t::Triangle*>& triangles)
|
|
{
|
|
const int numTriangles = static_cast<int>(triangles.size());
|
|
m_faces.resize(numTriangles);
|
|
m_halfEdges.resize(3 * numTriangles);
|
|
|
|
AZStd::unordered_map<p2t::Triangle*, int> triangleIndexMap;
|
|
|
|
for (int faceIndex = 0; faceIndex < numTriangles; faceIndex++)
|
|
{
|
|
p2t::Triangle* triangle = triangles[faceIndex];
|
|
triangleIndexMap.emplace(AZStd::make_pair(triangle, faceIndex));
|
|
|
|
// The index of the first half-edge in this face
|
|
const int firstHalfEdgeIndex = 3 * faceIndex;
|
|
|
|
// Populate the face data
|
|
m_faces[faceIndex].m_edge = &m_halfEdges[firstHalfEdgeIndex];
|
|
m_faces[faceIndex].m_numEdges = 3;
|
|
|
|
// Populate the half-edge data, apart from the twin pointers which will require a second pass
|
|
const AZ::Vector3 angles = CalculateAngles(*triangle);
|
|
for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++)
|
|
{
|
|
const int nextIndex = (edgeIndex + 1) % 3;
|
|
const int prevIndex = (edgeIndex + 2) % 3;
|
|
|
|
HalfEdge* halfEdge = &m_halfEdges[firstHalfEdgeIndex + edgeIndex];
|
|
halfEdge->m_face = &m_faces[faceIndex];
|
|
const p2t::Point* point = triangle->GetPoint(edgeIndex);
|
|
halfEdge->m_origin = AZ::Vector2(static_cast<float>(point->x), static_cast<float>(point->y));
|
|
halfEdge->m_prev = &m_halfEdges[firstHalfEdgeIndex + prevIndex];
|
|
halfEdge->m_next = &m_halfEdges[firstHalfEdgeIndex + nextIndex];
|
|
halfEdge->m_prevAngle = angles.GetElement(edgeIndex);
|
|
halfEdge->m_nextAngle = angles.GetElement(nextIndex);
|
|
}
|
|
}
|
|
|
|
// Figure out twin half-edges, and populate the queue of internal edges to consider for removal
|
|
for (int faceIndex = 0; faceIndex < numTriangles; faceIndex++)
|
|
{
|
|
p2t::Triangle* triangle = triangles[faceIndex];
|
|
for (int edgeIndex = 0; edgeIndex < 3; edgeIndex++)
|
|
{
|
|
HalfEdge* halfEdge = &m_halfEdges[3 * faceIndex + edgeIndex];
|
|
if (halfEdge->m_visited)
|
|
{
|
|
// We have already visited this half-edge when considering its twin, so there is nothing to do
|
|
continue;
|
|
}
|
|
|
|
p2t::Triangle* twinFace = triangle->NeighborCCW(*triangle->GetPoint(edgeIndex));
|
|
if (twinFace == nullptr)
|
|
{
|
|
// This half-edge doesn't have a twin, so it is an external edge and there is nothing to do
|
|
continue;
|
|
}
|
|
|
|
auto triangleIndexPair = triangleIndexMap.find(twinFace);
|
|
if (triangleIndexPair == triangleIndexMap.end())
|
|
{
|
|
// Poly2tri can have triangles outside of the polygon, so it is possible for them not to be
|
|
// found in the face map, but in this case we should do nothing
|
|
continue;
|
|
}
|
|
|
|
const int twinFaceIndex = triangleIndexPair->second;
|
|
if (twinFaceIndex >= numTriangles)
|
|
{
|
|
AZ_Error("PolygonPrismMeshUtils", false, "Invalid triangle index.");
|
|
continue;
|
|
}
|
|
|
|
const int nextIndex = (edgeIndex + 1) % 3;
|
|
const int twinEdgeIndex = twinFace->Index(triangle->GetPoint(nextIndex));
|
|
|
|
HalfEdge* twinHalfEdge = &m_halfEdges[3 * twinFaceIndex + twinEdgeIndex];
|
|
halfEdge->m_twin = twinHalfEdge;
|
|
halfEdge->m_visited = true;
|
|
twinHalfEdge->m_twin = halfEdge;
|
|
twinHalfEdge->m_visited = true;
|
|
|
|
InternalEdge internalEdge;
|
|
internalEdge.m_edges[0] = halfEdge;
|
|
internalEdge.m_edges[1] = twinHalfEdge;
|
|
internalEdge.m_minAngle = AZStd::GetMin(
|
|
AZStd::GetMin(halfEdge->m_prevAngle, halfEdge->m_nextAngle),
|
|
AZStd::GetMin(twinHalfEdge->m_prevAngle, twinHalfEdge->m_nextAngle));
|
|
m_edgeQueue.push(internalEdge);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Mesh2D::CreateFromSimpleConvexPolygon(const AZStd::vector<AZ::Vector2>& vertices)
|
|
{
|
|
const int numVertices = static_cast<int>(vertices.size());
|
|
m_faces.resize(1);
|
|
m_halfEdges.resize(numVertices);
|
|
|
|
m_faces[0].m_edge = &m_halfEdges[0];
|
|
m_faces[0].m_numEdges = numVertices;
|
|
|
|
for (int edgeIndex = 0; edgeIndex < numVertices; edgeIndex++)
|
|
{
|
|
const int nextIndex = (edgeIndex + 1) % numVertices;
|
|
const int prevIndex = (edgeIndex + numVertices - 1) % numVertices;
|
|
|
|
m_halfEdges[edgeIndex].m_face = &m_faces[0];
|
|
m_halfEdges[edgeIndex].m_origin = vertices[edgeIndex];
|
|
m_halfEdges[edgeIndex].m_prev = &m_halfEdges[prevIndex];
|
|
m_halfEdges[edgeIndex].m_next = &m_halfEdges[nextIndex];
|
|
}
|
|
}
|
|
|
|
void Mesh2D::RemoveInternalEdge(const InternalEdge& internalEdge)
|
|
{
|
|
HalfEdge* halfEdge0 = internalEdge.m_edges[0];
|
|
HalfEdge* halfEdge1 = internalEdge.m_edges[1];
|
|
Face* faceToKeep = halfEdge0->m_face;
|
|
Face* faceToRemove = halfEdge1->m_face;
|
|
const float newAngle0 = halfEdge0->m_prevAngle + halfEdge1->m_nextAngle;
|
|
const float newAngle1 = halfEdge0->m_nextAngle + halfEdge1->m_prevAngle;
|
|
const int newNumEdges = halfEdge0->m_face->m_numEdges + halfEdge1->m_face->m_numEdges - 2;
|
|
|
|
faceToKeep->m_numEdges = newNumEdges;
|
|
faceToRemove->m_removed = true;
|
|
|
|
// Make sure the face doesn't point to the half-edge that will be removed.
|
|
if (faceToKeep->m_edge == internalEdge.m_edges[0])
|
|
{
|
|
faceToKeep->m_edge = internalEdge.m_edges[0]->m_next;
|
|
}
|
|
|
|
// Make all the half-edges in the face that will be removed point to the kept face instead.
|
|
HalfEdge* currentEdge = internalEdge.m_edges[1];
|
|
for (int edgeIndex = 0; edgeIndex < faceToRemove->m_numEdges; edgeIndex++)
|
|
{
|
|
currentEdge->m_face = faceToKeep;
|
|
currentEdge = currentEdge->m_next;
|
|
}
|
|
|
|
// Update the previous and next half-edge pointers and angles for the 4 half-edges adjacent
|
|
// to the edge that will be removed.
|
|
halfEdge0->m_prev->m_dirty = true;
|
|
halfEdge0->m_next->m_dirty = true;
|
|
halfEdge1->m_prev->m_dirty = true;
|
|
halfEdge1->m_next->m_dirty = true;
|
|
|
|
halfEdge0->m_prev->m_next = halfEdge1->m_next;
|
|
halfEdge0->m_next->m_prev = halfEdge1->m_prev;
|
|
halfEdge1->m_prev->m_next = halfEdge0->m_next;
|
|
halfEdge1->m_next->m_prev = halfEdge0->m_prev;
|
|
|
|
halfEdge0->m_prev->m_nextAngle = newAngle0;
|
|
halfEdge0->m_next->m_prevAngle = newAngle1;
|
|
halfEdge1->m_prev->m_nextAngle = newAngle1;
|
|
halfEdge1->m_next->m_prevAngle = newAngle0;
|
|
}
|
|
|
|
size_t Mesh2D::ConvexMerge()
|
|
{
|
|
size_t numFacesRemoved = 0;
|
|
|
|
while (!m_edgeQueue.empty())
|
|
{
|
|
const InternalEdge& internalEdge = m_edgeQueue.top();
|
|
|
|
if (internalEdge.m_edges[0] != nullptr && internalEdge.m_edges[1] != nullptr)
|
|
{
|
|
// If either of the half-edges are marked dirty due to previously removing adjacent edges,
|
|
// update the edge and place back into the queue.
|
|
if (internalEdge.m_edges[0]->m_dirty || internalEdge.m_edges[1]->m_dirty)
|
|
{
|
|
InternalEdge updatedInternalEdge(internalEdge);
|
|
updatedInternalEdge.m_minAngle = AZStd::GetMin(
|
|
AZStd::GetMin(internalEdge.m_edges[0]->m_prevAngle, internalEdge.m_edges[0]->m_nextAngle),
|
|
AZStd::GetMin(internalEdge.m_edges[1]->m_prevAngle, internalEdge.m_edges[1]->m_nextAngle));
|
|
internalEdge.m_edges[0]->m_dirty = false;
|
|
internalEdge.m_edges[1]->m_dirty = false;
|
|
m_edgeQueue.pop();
|
|
m_edgeQueue.push(updatedInternalEdge);
|
|
}
|
|
else
|
|
{
|
|
// There are two conditions that need to be satisfied in order to allow removing this edge.
|
|
// First, the merged face should remain convex, i.e. the two new internal angles which would be
|
|
// created must both be less than 180 degrees.
|
|
// Secondly, the merged polygon must meet the PhysX vertex limit for convex meshes.
|
|
HalfEdge* halfEdge0 = internalEdge.m_edges[0];
|
|
HalfEdge* halfEdge1 = internalEdge.m_edges[1];
|
|
const float newAngle0 = halfEdge0->m_prevAngle + halfEdge1->m_nextAngle;
|
|
const float newAngle1 = halfEdge0->m_nextAngle + halfEdge1->m_prevAngle;
|
|
const int newNumEdges = halfEdge0->m_face->m_numEdges + halfEdge1->m_face->m_numEdges - 2;
|
|
|
|
// Allow angles very slightly larger than 180 degrees to avoid unnecessary splitting due to
|
|
// precision issues.
|
|
const float epsilon = 1e-5f;
|
|
const float maxAngle = AZ::Constants::Pi + epsilon;
|
|
|
|
if (newAngle0 < maxAngle &&
|
|
newAngle1 < maxAngle &&
|
|
newNumEdges <= MaxPolygonPrismEdges)
|
|
{
|
|
RemoveInternalEdge(internalEdge);
|
|
numFacesRemoved++;
|
|
}
|
|
|
|
m_edgeQueue.pop();
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
AZ_Error("PhysX Shape Collider Component", false, "Invalid half-edge.");
|
|
m_edgeQueue.pop();
|
|
}
|
|
}
|
|
|
|
return numFacesRemoved;
|
|
}
|
|
|
|
const AZStd::vector<Face>& Mesh2D::GetFaces() const
|
|
{
|
|
return m_faces;
|
|
}
|
|
|
|
const AZStd::priority_queue<InternalEdge, AZStd::vector<InternalEdge>, InternalEdgeCompare>& Mesh2D::GetInternalEdges() const
|
|
{
|
|
return m_edgeQueue;
|
|
}
|
|
|
|
const AZStd::vector<AZ::Vector3>& Mesh2D::GetDebugDrawPoints(float height, const AZ::Vector3& nonUniformScale) const
|
|
{
|
|
if (m_debugDrawDirty)
|
|
{
|
|
m_debugDrawPoints.clear();
|
|
|
|
for (const auto& face : m_faces)
|
|
{
|
|
if (!face.m_removed)
|
|
{
|
|
int numEdges = face.m_numEdges;
|
|
PolygonPrismMeshUtils::HalfEdge* currentEdge = face.m_edge;
|
|
for (int edgeIndex = 0; edgeIndex < numEdges; edgeIndex++)
|
|
{
|
|
PolygonPrismMeshUtils::HalfEdge* nextEdge = currentEdge->m_next;
|
|
const auto v1 = nonUniformScale * AZ::Vector3(currentEdge->m_origin.GetX(), currentEdge->m_origin.GetY(), 0.0f);
|
|
const auto v2 = nonUniformScale * AZ::Vector3(currentEdge->m_origin.GetX(), currentEdge->m_origin.GetY(), height);
|
|
const auto v3 = nonUniformScale * AZ::Vector3(nextEdge->m_origin.GetX(), nextEdge->m_origin.GetY(), 0.0f);
|
|
const auto v4 = nonUniformScale * AZ::Vector3(nextEdge->m_origin.GetX(), nextEdge->m_origin.GetY(), height);
|
|
|
|
m_debugDrawPoints.insert(m_debugDrawPoints.end(), { v1, v2, v1, v3, v2, v4 });
|
|
|
|
currentEdge = currentEdge->m_next;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_debugDrawDirty = false;
|
|
}
|
|
|
|
return m_debugDrawPoints;
|
|
}
|
|
|
|
void Mesh2D::SetDebugDrawDirty()
|
|
{
|
|
m_debugDrawDirty = true;
|
|
}
|
|
|
|
void Mesh2D::Clear()
|
|
{
|
|
m_halfEdges.clear();
|
|
m_faces.clear();
|
|
m_edgeQueue = AZStd::priority_queue<InternalEdge, AZStd::vector<InternalEdge>, InternalEdgeCompare>();
|
|
m_debugDrawDirty = true;
|
|
}
|
|
} // namespace PolygonPrismMeshUtils
|