Files
UnrealEngineUWP/Engine/Source/Programs/HeadlessChaos/Private/HeadlessChaosTestConvex.cpp
chris caulfield ffdbb6fdd6 Chaos - Resubmit 31279946 with additional fix
- fix AABB support vertex index and match Box indices to AABB indices
- fix Box GetOpposingNormal to match new indexing scheme
- unit test to verify box support vertices for all cardinal and diagonal directions
- unit test to verify that AABB and Box vertex indices match

There was a mismatch between the vertex indices on TBox versus TAABB, but also the TAABB's vertex index calculation in the support functions was incorrect. Luckily this didnt matter, but some upcoming changes now require that the Box support functions return the correct vertex index, and that requires the above two fixes.

#rb vincent.robert

[CL 31451332 by chris caulfield in ue5-main branch]
2024-02-13 17:50:33 -05:00

759 lines
28 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "HeadlessChaosTestEPA.h"
#include "HeadlessChaos.h"
#include "HeadlessChaosTestUtility.h"
#include "Chaos/Core.h"
#include "Chaos/GJK.h"
#include "Chaos/Convex.h"
#include "Chaos/ImplicitObjectScaled.h"
#include "Chaos/Particles.h"
#include "../Resource/TestGeometry2.h"
#include "Logging/LogScopedVerbosityOverride.h"
namespace ChaosTest
{
using namespace Chaos;
// Check that convex creation with face merging is working correctly.
// The initial creation generates a set of triangles, and the merge step should
// leave the hull with only one face per normal.
void TestConvexBuilderConvexBoxFaceMerge(const TArray<FConvex::FVec3Type>& Vertices)
{
TArray<FConvex::FPlaneType> Planes;
TArray<TArray<int32>> FaceVertices;
TArray<Chaos::FConvex::FVec3Type> SurfaceParticles;
FConvex::FAABB3Type LocalBounds;
FConvexBuilder::Build(Vertices, Planes, FaceVertices, SurfaceParticles, LocalBounds);
FConvexBuilder::MergeFaces(Planes, FaceVertices, SurfaceParticles, 1.0f);
// Check that we have the right number of faces and particles
EXPECT_EQ(SurfaceParticles.Num(), 8);
EXPECT_EQ(Planes.Num(), 6);
EXPECT_EQ(FaceVertices.Num(), 6);
// Make sure the verts are correct and agree on the normal
for (int32 FaceIndex = 0; FaceIndex < FaceVertices.Num(); ++FaceIndex)
{
EXPECT_EQ(FaceVertices[FaceIndex].Num(), 4);
for (int32 VertexIndex0 = 0; VertexIndex0 < FaceVertices[FaceIndex].Num(); ++VertexIndex0)
{
int32 VertexIndex1 = Chaos::Utilities::WrapIndex(VertexIndex0 + 1, 0, FaceVertices[FaceIndex].Num());
int32 VertexIndex2 = Chaos::Utilities::WrapIndex(VertexIndex0 + 2, 0, FaceVertices[FaceIndex].Num());
const FVec3 Vertex0 = SurfaceParticles[FaceVertices[FaceIndex][VertexIndex0]];
const FVec3 Vertex1 = SurfaceParticles[FaceVertices[FaceIndex][VertexIndex1]];
const FVec3 Vertex2 = SurfaceParticles[FaceVertices[FaceIndex][VertexIndex2]];
// All vertices should lie in a plane at the same distance
const FReal Dist0 = FVec3::DotProduct(Vertex0, Planes[FaceIndex].Normal());
const FReal Dist1 = FVec3::DotProduct(Vertex1, Planes[FaceIndex].Normal());
const FReal Dist2 = FVec3::DotProduct(Vertex2, Planes[FaceIndex].Normal());
EXPECT_NEAR(Dist0, 50.0f, 1.e-3f);
EXPECT_NEAR(Dist1, 50.0f, 1.e-3f);
EXPECT_NEAR(Dist2, 50.0f, 1.e-3f);
// All sequential edge pairs should agree on winding
const FReal Winding = FVec3::DotProduct(FVec3::CrossProduct(Vertex1 - Vertex0, Vertex2 - Vertex1), Planes[FaceIndex].Normal());
EXPECT_GT(Winding, 0.0f);
}
}
}
// Check that face merging works for a convex box
GTEST_TEST(ConvexStructureTests, TestConvexBoxFaceMerge)
{
const TArray<FConvex::FVec3Type> Vertices =
{
{-50, -50, -50},
{-50, -50, 50},
{-50, 50, -50},
{-50, 50, 50},
{50, -50, -50},
{50, -50, 50},
{50, 50, -50},
{50, 50, 50},
};
TestConvexBuilderConvexBoxFaceMerge(Vertices);
}
// Check that the convex structure data is consistent (works for TBox and TConvex)
template<typename T_GEOM> void TestConvexStructureDataImpl(const T_GEOM& Convex)
{
// Note: This tolerance matches the one passed to FConvexBuilder::MergeFaces in the FConvex constructor, but it should be dependent on size
//const FReal Tolerance = 1.e-4f * Convex.BoundingBox().OriginRadius();
const FReal Tolerance = 1.0f;
// Check all per-plane data
for (int32 PlaneIndex = 0; PlaneIndex < Convex.NumPlanes(); ++PlaneIndex)
{
// All vertices should be on the plane
for (int32 PlaneVertexIndex = 0; PlaneVertexIndex < Convex.NumPlaneVertices(PlaneIndex); ++PlaneVertexIndex)
{
const auto Plane = Convex.GetPlane(PlaneIndex);
const int32 VertexIndex = Convex.GetPlaneVertex(PlaneIndex, PlaneVertexIndex);
const FVec3 Vertex = Convex.GetVertex(VertexIndex);
const FReal VertexDistance = FVec3::DotProduct(Plane.Normal(), Vertex - Plane.X());
EXPECT_NEAR(VertexDistance, 0.0f, Tolerance);
}
}
// Check all per-vertex data
for (int32 VertexIndex = 0; VertexIndex < Convex.NumVertices(); ++VertexIndex)
{
// Get all the planes for the vertex
TArray<int32> PlaneIndices;
PlaneIndices.SetNum(128);
int32 NumPlanes = Convex.FindVertexPlanes(VertexIndex, PlaneIndices.GetData(), PlaneIndices.Num());
PlaneIndices.SetNum(NumPlanes);
for (int32 PlaneIndex : PlaneIndices)
{
const auto Plane = Convex.GetPlane(PlaneIndex);
const FVec3 Vertex = Convex.GetVertex(VertexIndex);
const FReal VertexDistance = FVec3::DotProduct(Plane.Normal(), Vertex - Plane.X());
EXPECT_NEAR(VertexDistance, 0.0f, Tolerance);
}
}
}
// Check that the convex structure data is consistent
void TestConvexStructureData(const TArray<FConvex::FVec3Type>& Vertices)
{
FConvex Convex(Vertices, 0.0f);
TestConvexStructureDataImpl(Convex);
}
// Check that the convex structure data is consistent for a simple convex box
GTEST_TEST(ConvexStructureTests, TestConvexStructureData)
{
const TArray<FConvex::FVec3Type> Vertices =
{
{-50, -50, -50},
{-50, -50, 50},
{-50, 50, -50},
{-50, 50, 50},
{50, -50, -50},
{50, -50, 50},
{50, 50, -50},
{50, 50, 50},
};
TestConvexStructureData(Vertices);
}
// Check that the convex structure data is consistent for a complex convex shape
GTEST_TEST(ConvexStructureTests, TestConvexStructureData2)
{
const TArray<FConvex::FVec3Type> Vertices =
{
{0, 0, 12.0f},
{-0.707f, -0.707f, 10.0f},
{0, -1, 10.0f},
{0.707f, -0.707f, 10.0f},
{1, 0, 10.0f},
{0.707f, 0.707f, 10.0f},
{0.0f, 1.0f, 10.0f},
{-0.707f, 0.707f, 10.0f},
{-1.0f, 0.0f, 10.0f},
{-0.707f, -0.707f, 0.0f},
{0, -1, 0.0f},
{0.707f, -0.707f, 0.0f},
{1, 0, 0.0f},
{0.707f, 0.707f, 0.0f},
{0.0f, 1.0f, 0.0f},
{-0.707f, 0.707f, 0.0f},
{-1.0f, 0.0f, 0.0f},
{0, 0, -2.0f},
};
TestConvexStructureData(Vertices);
}
// Check that the convex structure data is consistent for a standard box
GTEST_TEST(ConvexStructureTests, TestBoxStructureData)
{
FImplicitBox3 Box(FVec3(-50, -50, -50), FVec3(50, 50, 50), 0.0f);
TestConvexStructureDataImpl(Box);
// Make sure all planes are at the correct distance
for (int32 PlaneIndex = 0; PlaneIndex < Box.NumPlanes(); ++PlaneIndex)
{
// All vertices should be on the plane
const TPlaneConcrete<FReal, 3> Plane = Box.GetPlane(PlaneIndex);
EXPECT_NEAR(FVec3::DotProduct(Plane.X(), Plane.Normal()), 50.0f, KINDA_SMALL_NUMBER);
}
}
// Check the reverse mapping planes->vertices->planes is intact
template<typename T_STRUCTUREDATA>
void TestConvexStructureDataMapping(const T_STRUCTUREDATA& StructureData)
{
// For each plane, get the list of vertices that make its edges.
// Then check that the list of planes used by that vertex contains the original plane
for (int32 PlaneIndex = 0; PlaneIndex < StructureData.NumPlanes(); ++PlaneIndex)
{
for (int32 PlaneVertexIndex = 0; PlaneVertexIndex < StructureData.NumPlaneVertices(PlaneIndex); ++PlaneVertexIndex)
{
const int32 VertexIndex = StructureData.GetPlaneVertex(PlaneIndex, PlaneVertexIndex);
// Check that the plane's vertex has the plane in its list
TArray<int32> PlaneIndices;
PlaneIndices.SetNum(128);
const int32 NumPlanes = StructureData.FindVertexPlanes(VertexIndex, PlaneIndices.GetData(), PlaneIndices.Num());
PlaneIndices.SetNum(NumPlanes);
const bool bFoundPlane = PlaneIndices.Contains(PlaneIndex);
EXPECT_TRUE(bFoundPlane);
}
}
}
// Check that the structure data is good for convex shapes that have faces merged during construction
// This test uses the small index size in StructureData.
GTEST_TEST(ConvexStructureTests, TestSmallIndexStructureData)
{
FMath::RandInit(53799058);
const FReal Radius = 1000.0f;
const int32 NumVertices = TestGeometry2::RawVertexArray.Num() / 3;
TArray<FConvex::FVec3Type> Particles;
Particles.SetNum(NumVertices);
for (int32 ParticleIndex = 0; ParticleIndex < NumVertices; ++ParticleIndex)
{
Particles[ParticleIndex] = FConvex::FVec3Type(
TestGeometry2::RawVertexArray[3 * ParticleIndex + 0],
TestGeometry2::RawVertexArray[3 * ParticleIndex + 1],
TestGeometry2::RawVertexArray[3 * ParticleIndex + 2]
);
}
FConvex Convex(Particles, 0.0f);
const FConvexStructureData::FConvexStructureDataMedium& StructureData = Convex.GetStructureData().DataM();
TestConvexStructureDataMapping(StructureData);
TestConvexStructureDataImpl(Convex);
}
// Check that the structure data is good for convex shapes that have faces merged during construction
// This test uses the large index size in StructureData.
// This test is disabled - the convex building is too slow for this many verts
GTEST_TEST(ConvexStructureTests, DISABLED_TestLargeIndexStructureData2)
{
FMath::RandInit(53799058);
const FReal Radius = 10000.0f;
const int32 NumVertices = 50000;
// Make a convex with points on a sphere.
TArray<FConvex::FVec3Type> Vertices;
Vertices.SetNum(NumVertices);
for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex)
{
const FConvex::FRealType Theta = FMath::RandRange(-PI, PI);
const FConvex::FRealType Phi = FMath::RandRange(-0.5f * PI, 0.5f * PI);
Vertices[VertexIndex] = Radius * FConvex::FVec3Type(FMath::Cos(Theta), FMath::Sin(Theta), FMath::Sin(Phi));
}
FConvex Convex(Vertices, 0.0f);
EXPECT_GT(Convex.NumVertices(), 800);
EXPECT_GT(Convex.NumPlanes(), 500);
const FConvexStructureData::FConvexStructureDataLarge& StructureData = Convex.GetStructureData().DataL();
TestConvexStructureDataMapping(StructureData);
TestConvexStructureDataImpl(Convex);
}
// Check that extremely small generated triangle don't trigger the normal check
GTEST_TEST(ConvexStructureTests, TestConvexFaceNormalCheck)
{
// Create a long mesh with a extremely small end (YZ plane)
// so that it generate extremely sized triangle that will produce extremely small (unormalized) normals
const float SmallNumber = 0.001f;
const FConvex::FVec3Type Range{ 100.0f, SmallNumber, SmallNumber };
const TArray<FConvex::FVec3Type> Vertices =
{
{0, 0, 0},
{Range.X, 0, 0},
{Range.X, Range.Y, 0},
{Range.X, Range.Y, Range.Z},
{Range.X + SmallNumber, Range.Y * 0.5f, Range.Z * 0.5f},
};
TestConvexStructureData(Vertices);
}
GTEST_TEST(ConvexStructureTests, TestConvexFailsSafelyOnPlanarObject)
{
using namespace Chaos;
// This list of vertices is a plane with many duplicated vertices and previously was causing
// a check to fire inside the convex builder as we classified the object incorrectly and didn't
// safely handle a failure due to a planar object. This test verifies that the builder can
// safely fail to build a convex from a plane.
const TArray<FConvex::FVec3Type> Vertices =
{
{-15.1425571, 16.9698563, 0.502334476},
{-15.1425571, 16.9698563, 0.502334476},
{-15.1425571, 16.9698563, 0.502334476},
{-16.9772491, -15.1373663, -0.398189038},
{-15.1425571, 16.9698563, 0.502334476},
{16.9772491, 15.1373663, 0.398189038},
{16.9772491, 15.1373663, 0.398189038},
{16.9772491, 15.1373663, 0.398189038},
{-15.1425571, 16.9698563, 0.502334476},
{-16.9772491, -15.1373663, -0.398189038},
{-16.9772491, -15.1373663, -0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{-16.9772491, -15.1373663, -0.398189038},
{-16.9772491, -15.1373663, -0.398189038},
{16.9772491, 15.1373663, 0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{-15.1425571, 16.9698563, 0.502334476},
{16.9772491, 15.1373663, 0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{15.1425571, -16.9698563, -0.502334476},
{16.9772491, 15.1373663, 0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{-16.9772491, -15.1373663, -0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{-15.1425571, 16.9698563, 0.502334476},
{-15.1425571, 16.9698563, 0.502334476},
{-15.1425571, 16.9698563, 0.502334476},
{-16.9772491, -15.1373663, -0.398189038},
{-15.1425571, 16.9698563, 0.502334476},
{16.9772491, 15.1373663, 0.398189038},
{16.9772491, 15.1373663, 0.398189038},
{16.9772491, 15.1373663, 0.398189038},
{-15.1425571, 16.9698563, 0.502334476},
{-16.9772491, -15.1373663, -0.398189038},
{-16.9772491, -15.1373663, -0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{-16.9772491, -15.1373663, -0.398189038},
{-16.9772491, -15.1373663, -0.398189038},
{16.9772491, 15.1373663, 0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{-15.1425571, 16.9698563, 0.502334476},
{16.9772491, 15.1373663, 0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{15.1425571, -16.9698563, -0.502334476},
{16.9772491, 15.1373663, 0.398189038},
{15.1425571, -16.9698563, -0.502334476},
{-16.9772491, -15.1373663, -0.398189038},
{15.1425571, -16.9698563, -0.502334476}
};
TArray<FConvex::FPlaneType> Planes;
TArray<TArray<int32>> FaceIndices;
TArray<FConvex::FVec3Type> FinalVertices;
FConvex::FAABB3Type LocalBounds;
{
// Temporarily set LogChaos to error, we're expecting this to fire warnings and don't want that to fail a CIS run.
LOG_SCOPE_VERBOSITY_OVERRIDE(LogChaos, ELogVerbosity::Error);
FConvexBuilder::Build(Vertices, Planes, FaceIndices, FinalVertices, LocalBounds);
}
// Check that we've failed to build a 3D convex hull and safely returned
EXPECT_EQ(Planes.Num(), 0);
}
GTEST_TEST(ConvexStructureTests, TestConvexHalfEdgeStructureData_Box)
{
const TArray<FConvex::FVec3Type> InputVertices =
{
FVec3(-50, -50, -50),
FVec3(-50, -50, 50),
FVec3(-50, 50, -50),
FVec3(-50, 50, 50),
FVec3(50, -50, -50),
FVec3(50, -50, 50),
FVec3(50, 50, -50),
FVec3(50, 50, 50),
};
TArray<FConvex::FPlaneType> Planes;
TArray<TArray<int32>> FaceVertices;
TArray<FConvex::FVec3Type> Vertices;
FConvex::FAABB3Type LocalBounds;
FConvexBuilder::Build(InputVertices, Planes, FaceVertices, Vertices, LocalBounds);
FConvexBuilder::MergeFaces(Planes, FaceVertices, Vertices, 1.0f);
FConvex Convex(Vertices, 0.0f);
const FConvexStructureData::FConvexStructureDataSmall& StructureData = Convex.GetStructureData().DataS();
EXPECT_EQ(StructureData.NumPlanes(), 6);
EXPECT_EQ(StructureData.NumHalfEdges(), 24);
EXPECT_EQ(StructureData.NumVertices(), 8);
// Count how many times each vertex and edge is referenced
TArray<int32> VertexIndexCount;
TArray<int32> EdgeIndexCount;
VertexIndexCount.SetNumZeroed(StructureData.NumVertices());
EdgeIndexCount.SetNumZeroed(StructureData.NumHalfEdges());
for (int32 PlaneIndex = 0; PlaneIndex < StructureData.NumPlanes(); ++PlaneIndex)
{
EXPECT_EQ(StructureData.NumPlaneHalfEdges(PlaneIndex), 4);
for (int32 PlaneEdgeIndex = 0; PlaneEdgeIndex < StructureData.NumPlaneHalfEdges(PlaneIndex); ++PlaneEdgeIndex)
{
const int32 EdgeIndex = StructureData.GetPlaneHalfEdge(PlaneIndex, PlaneEdgeIndex);
const int32 VertexIndex = StructureData.GetHalfEdgeVertex(EdgeIndex);
EdgeIndexCount[EdgeIndex]++;
VertexIndexCount[VertexIndex]++;
}
}
// Every vertex is used by 3 half-edges (and planes)
for (int32 VertexCount : VertexIndexCount)
{
EXPECT_EQ(VertexCount, 3);
}
// Each half edge is used by a single plane
for (int32 EdgeCount : EdgeIndexCount)
{
EXPECT_EQ(EdgeCount, 1);
}
// Vertex Plane iterator generates 3 planes and all the edges have the same primary vertex
for (int32 VertexIndex = 0; VertexIndex < StructureData.NumVertices(); ++VertexIndex)
{
int32 PlaneCount = 0;
TArray<int32> VertexPlanes;
VertexPlanes.SetNum(128);
const int32 NumPlanes = StructureData.FindVertexPlanes(VertexIndex, VertexPlanes.GetData(), VertexPlanes.Num());
VertexPlanes.SetNum(NumPlanes);
for (int32 PlaneIndex : VertexPlanes)
{
EXPECT_NE(PlaneIndex, INDEX_NONE);
++PlaneCount;
}
// Everty vertex belongs to 3 planes
EXPECT_EQ(PlaneCount, 3);
// Every vertex's first edge should have that vertex as its root vertex
const int32 VertexHalfEdgeIndex = StructureData.GetVertexFirstHalfEdge(VertexIndex);
EXPECT_EQ(VertexIndex, StructureData.GetHalfEdgeVertex(VertexHalfEdgeIndex));
}
}
template<typename ConvexType>
void TestConvexPlaneVertices(const ConvexType& Convex)
{
const FReal NormalTolerance = UE_SMALL_NUMBER;
const FReal PositionTolerance = UE_KINDA_SMALL_NUMBER;
for (int32 PlaneIndex = 0; PlaneIndex < Convex.NumPlanes(); ++PlaneIndex)
{
const FVec3 PlaneN = Convex.GetPlane(PlaneIndex).Normal();
const FVec3 PlaneX = Convex.GetPlane(PlaneIndex).X();
const int NumPlaneVertices = Convex.NumPlaneVertices(PlaneIndex);
for (int32 PlaneVertexIndex0 = 0; PlaneVertexIndex0 < NumPlaneVertices; ++PlaneVertexIndex0)
{
const int32 VertexIndex0 = Convex.GetPlaneVertex(PlaneIndex, PlaneVertexIndex0);
// All vertices are actually on the plane
const FVec3 Vertex0 = Convex.GetVertex(VertexIndex0);
EXPECT_NEAR(FVec3::DotProduct(Vertex0, PlaneN), FVec3::DotProduct(PlaneX, PlaneN), PositionTolerance) << "PlaneIndex=" << PlaneIndex << " PlaneVertexIndex0=" << PlaneVertexIndex0;
// Winding is correct
int PlaneVertexIndex1 = (PlaneVertexIndex0 < NumPlaneVertices - 1) ? PlaneVertexIndex0 + 1 : 0;
int PlaneVertexIndex2 = (PlaneVertexIndex0 < NumPlaneVertices - 2) ? PlaneVertexIndex0 + 2 : PlaneVertexIndex0 - NumPlaneVertices + 2;
const int32 VertexIndex1 = Convex.GetPlaneVertex(PlaneIndex, PlaneVertexIndex1);
const int32 VertexIndex2 = Convex.GetPlaneVertex(PlaneIndex, PlaneVertexIndex2);
const FVec3 Vertex1 = Convex.GetVertex(VertexIndex1);
const FVec3 Vertex2 = Convex.GetVertex(VertexIndex2);
const FReal WindingMag = FVec3::DotProduct(FVec3::CrossProduct(Vertex1 - Vertex0, Vertex2 - Vertex1), PlaneN);
const FReal Winding = FMath::Sign(WindingMag);
const int32 ExpectedWinding = Convex.GetWindingOrder();
EXPECT_EQ(Winding, ExpectedWinding) << "PlaneIndex=" << PlaneIndex << " PlaneVertexIndex0=" << PlaneVertexIndex0;
}
}
}
template<typename ConvexType>
void TestConvexEdges(const ConvexType& Convex)
{
// Check the edges
for (int32 EdgeIndex = 0; EdgeIndex < Convex.NumEdges(); ++EdgeIndex)
{
const int PlaneIndex0 = Convex.GetEdgePlane(EdgeIndex, 0);
const int PlaneIndex1 = Convex.GetEdgePlane(EdgeIndex, 1);
const int32 VertexIndex0 = Convex.GetEdgeVertex(EdgeIndex, 0);
const int32 VertexIndex1 = Convex.GetEdgeVertex(EdgeIndex, 0);
// Plane0 contains the two vertices
bool bFoundVertex0 = false;
bool bFoundVertex1 = false;
for (int32 PlaneVertexIndex0 = 0; PlaneVertexIndex0 < Convex.NumPlaneVertices(PlaneIndex0); ++PlaneVertexIndex0)
{
const int32 ThisVertexIndex = Convex.GetPlaneVertex(PlaneIndex0, PlaneVertexIndex0);
if (ThisVertexIndex == VertexIndex0)
{
bFoundVertex0 = true;
}
if (ThisVertexIndex == VertexIndex1)
{
bFoundVertex1 = true;
}
}
EXPECT_TRUE(bFoundVertex0) << "EdgeIndex=" << EdgeIndex << "PlaneIndex=" << PlaneIndex0 << " VertexIndex=" << VertexIndex0;
EXPECT_TRUE(bFoundVertex1) << "EdgeIndex=" << EdgeIndex << "PlaneIndex=" << PlaneIndex0 << " VertexIndex=" << VertexIndex1;
// Plane1 contains the two vertices
bFoundVertex0 = false;
bFoundVertex1 = false;
for (int32 PlaneVertexIndex1 = 0; PlaneVertexIndex1 < Convex.NumPlaneVertices(PlaneIndex1); ++PlaneVertexIndex1)
{
const int32 ThisVertexIndex = Convex.GetPlaneVertex(PlaneIndex1, PlaneVertexIndex1);
if (ThisVertexIndex == VertexIndex0)
{
bFoundVertex0 = true;
}
if (ThisVertexIndex == VertexIndex1)
{
bFoundVertex1 = true;
}
}
EXPECT_TRUE(bFoundVertex0) << "EdgeIndex=" << EdgeIndex << "PlaneIndex=" << PlaneIndex1 << " VertexIndex=" << VertexIndex0;
EXPECT_TRUE(bFoundVertex1) << "EdgeIndex=" << EdgeIndex << "PlaneIndex=" << PlaneIndex1 << " VertexIndex=" << VertexIndex1;
}
}
// Verify that the box Plane Edge and Vertex APIs return the elements exactly as they are defined in Box.cpp
GTEST_TEST(ConvexStructureTests, TestBoxStructureDataDetails)
{
// These arrays are copied from Box.cpp - any changes there should trigger a failure here
// so we can be sure the change was expected.
const TArray<FVec3> PlaneNormals =
{
FVec3(-1, 0, 0), // -X
FVec3(0, -1, 0), // -Y
FVec3(0, 0, -1), // -Z
FVec3(1, 0, 0), // X
FVec3(0, 1, 0), // Y
FVec3(0, 0, 1), // Z
};
const TArray<FVec3> UnitVertices =
{
FVec3(-1, -1, -1), // 0
FVec3(1, -1, -1), // 1
FVec3(-1, 1, -1), // 2
FVec3(1, 1, -1), // 3
FVec3(-1, -1, 1), // 4
FVec3(1, -1, 1), // 5
FVec3(-1, 1, 1), // 6
FVec3(1, 1, 1), // 7
};
TArray<TArray<int32>> PlaneVertices
{
{ 0, 4, 6, 2 }, // -X,
{ 0, 1, 5, 4 }, // -Y
{ 0, 2, 3, 1 }, // -Z
{ 1, 3, 7, 5 }, // X
{ 2, 6, 7, 3 }, // Y
{ 4, 5, 7, 6 }, // Z
};
const FReal NormalTolerance = UE_SMALL_NUMBER;
const FReal PositionTolerance = UE_KINDA_SMALL_NUMBER;
const FVec3 Center = FVec3(0, 0, 0);
const FVec3 HalfExtent = FVec3(100, 200, 300);
const FReal Margin = FReal(0);
const FImplicitBox3 Box = FImplicitBox3(Center - HalfExtent, Center + HalfExtent, Margin);
EXPECT_EQ(Box.NumPlanes(), 6);
EXPECT_EQ(Box.NumEdges(), 12);
EXPECT_EQ(Box.NumVertices(), 8);
// Check that the vertices are in the expected order
for (int32 VertexIndex = 0; VertexIndex < UnitVertices.Num(); ++VertexIndex)
{
const FVec3 Vertex = Box.GetVertex(VertexIndex);
const FVec3 ExpectedVertex = UnitVertices[VertexIndex] * HalfExtent;
EXPECT_NEAR(Vertex.X, ExpectedVertex.X, PositionTolerance);
EXPECT_NEAR(Vertex.Y, ExpectedVertex.Y, PositionTolerance);
EXPECT_NEAR(Vertex.Z, ExpectedVertex.Z, PositionTolerance);
}
// Check that the planes have the correct normal and position
for (int32 PlaneIndex = 0; PlaneIndex < PlaneNormals.Num(); ++PlaneIndex)
{
TPlaneConcrete<FReal> Plane = Box.GetPlane(PlaneIndex);
// Normals are in the expected direction
EXPECT_NEAR(Plane.Normal().X, PlaneNormals[PlaneIndex].X, NormalTolerance) << "PlaneIndex=" << PlaneIndex;
EXPECT_NEAR(Plane.Normal().Y, PlaneNormals[PlaneIndex].Y, NormalTolerance) << "PlaneIndex=" << PlaneIndex;
EXPECT_NEAR(Plane.Normal().Z, PlaneNormals[PlaneIndex].Z, NormalTolerance) << "PlaneIndex=" << PlaneIndex;
// Positions are in the correct plane
const FReal PlaneDistance = FVec3::DotProduct(Plane.Normal(), Plane.X());
const FReal ExpectedPlaneDistance = FVec3::DotProduct(PlaneNormals[PlaneIndex], PlaneNormals[PlaneIndex] * HalfExtent);
EXPECT_NEAR(PlaneDistance, ExpectedPlaneDistance, PositionTolerance);
}
// Check that the planes have the correct vertices
for (int32 PlaneIndex = 0; PlaneIndex < PlaneNormals.Num(); ++PlaneIndex)
{
const int NumPlaneVertices = Box.NumPlaneVertices(PlaneIndex);
EXPECT_EQ(NumPlaneVertices, PlaneVertices[PlaneIndex].Num()) << "PlaneIndex=" << PlaneIndex; // Always 4
for (int32 PlaneVertexIndex0 = 0; PlaneVertexIndex0 < NumPlaneVertices; ++PlaneVertexIndex0)
{
const int32 VertexIndex0 = Box.GetPlaneVertex(PlaneIndex, PlaneVertexIndex0);
EXPECT_EQ(VertexIndex0, PlaneVertices[PlaneIndex][PlaneVertexIndex0]) << "PlaneIndex=" << PlaneIndex << " PlaneVertexIndex0=" << PlaneVertexIndex0;
}
}
// Check the plane vertices are in the plane and have the correct winding order
TestConvexPlaneVertices(Box);
// Check that the edges report planes that actually share vertices
TestConvexEdges(Box);
}
// Check that a Box implemented as a FImplicitConvex3 meets the same specs as ImplicitBox3
GTEST_TEST(ConvexStructureTests, TestConvexBoxStructureDataDetails)
{
const FVec3f Center = FVec3(0, 0, 0);
const FVec3f HalfExtent = FVec3(100, 200, 300);
const FRealSingle Margin = FReal(0);
const TArray<FVec3f> Vertices =
{
Center + HalfExtent * FVec3f(-1, -1, -1), // 0
Center + HalfExtent * FVec3f( 1, -1, -1), // 1
Center + HalfExtent * FVec3f(-1, 1, -1), // 2
Center + HalfExtent * FVec3f( 1, 1, -1), // 3
Center + HalfExtent * FVec3f(-1, -1, 1), // 4
Center + HalfExtent * FVec3f( 1, -1, 1), // 5
Center + HalfExtent * FVec3f(-1, 1, 1), // 6
Center + HalfExtent * FVec3f( 1, 1, 1), // 7
};
FImplicitConvex3 Convex = FImplicitConvex3(Vertices, Margin);
// Check the plane vertices are in the plane and have the correct winding order
TestConvexPlaneVertices(Convex);
// Check that the edges report planes that actually share vertices
TestConvexEdges(Convex);
}
// The set of vertices generated from a unit box when creating a GeometryCollection from the default box in the editor.
// The default cube is tesselated. It has 26 vertices which include the 8 corners, plus mid-points along each edge and in the middle of each face.
//
// This was causing the convex builder to produce a denegerate triangle (3 points in a row) leading to a zero normal and a crash in the solver.
//
// The fix was to modify TConvexHull3 to produce convex faces rather than triangles (one of which could be nearly degenerate),
// and a post process on its results to eliminate colinear edges (within some tolerance)
//
GTEST_TEST(ConvexBuilderTests, TestDefaultStaticMeshBox)
{
FConvexBuilder::EBuildMethod BuildMethod = FConvexBuilder::EBuildMethod::Default;
const FReal Margin = 9.9999997473787516e-05;
TArray<FVec3f> BoxVerts =
{
{-50.0000000f, 50.0000000f, -50.0000000f},
{50.0000000f, 50.0000000f, -50.0000000f},
{50.0000000f, -50.0000000f, -50.0000000f},
{50.0000000f, -50.0000000f, 50.0000000f},
{50.0000000f, 50.0000000f, 50.0000000f},
{-50.0000000f, -50.0000000f, 50.0000000f},
{-50.0000000f, 50.0000000f, 50.0000000f},
{-50.0000000f, -50.0000000f, -50.0000000f},
{0.00000000f, 50.0000000f, -50.0000000f},
{50.0000000f, 0.00000000f, -50.0000000f},
{0.00000000f, 50.0000000f, 50.0000000f},
{-50.0000000f, -50.0000000f, 3.06161689e-15f},
{-50.0000000f, 50.0000000f, -3.06161689e-15f},
{-50.0000000f, 0.00000000f, -50.0000000f},
{50.0000000f, -50.0000000f, 3.06161689e-15f},
{0.00000000f, -50.0000000f, -50.0000000f},
{50.0000000f, 1.22464676e-14f, 50.0000000f},
{0.00000000f, -50.0000000f, 50.0000000f},
{-50.0000000f, 1.22464676e-14f, 50.0000000f},
{50.0000000f, 50.0000000f, -3.06161689e-15f},
{0.00000000f, 50.0000000f, -3.06161689e-15f},
{0.00000000f, 0.00000000f, -50.0000000f},
{0.00000000f, 1.22464676e-14f, 50.0000000f},
{0.00000000f, -50.0000000f, 3.06161689e-15f},
{50.0000000f, 6.12323379e-15f, -1.87469967e-31f},
{-50.0000000f, 6.12323379e-15f, -1.87469967e-31f},
};
FImplicitConvex3 Convex(BoxVerts, Margin, BuildMethod);
// The convex should be a box
EXPECT_EQ(Convex.NumVertices(), 8);
EXPECT_EQ(Convex.NumEdges(), 12);
EXPECT_EQ(Convex.NumPlanes(), 6);
// All planes normals should be...normalized
const FReal NormalTolerance = 1.e-4;
for (int32 PlaneIndex = 0; PlaneIndex < Convex.NumPlanes(); ++PlaneIndex)
{
const FVec3 PlaneN = Convex.GetPlane(PlaneIndex).Normal();
EXPECT_NEAR(PlaneN.Size(), FReal(1), NormalTolerance);
}
}
// Create a tet with an extra degenerate triangle in it. Verify that MergeColinearEdges handles this case
// and does not leave an invalid 2-vertex face behind.
// NOTE: We should not be able to create a FImplicitConvex3 that calls MergeColinearEdges in this condition
// but better safe than sorry.
GTEST_TEST(ConvexBuilderTests, TestColinearEdgeInTriangle)
{
// A right angled tet with an extra degenerate triangular face in there
TArray<FVec3f> TetVerts =
{
{0.0000000f, 0.0000000f, 50.0000000f}, // Top
{0.0000000f, 0.0000000f, 0.0000000f}, // Base0
{50.0000000f, 0.0000000f, 0.0000000f}, // Base1
{0.0000000f, 50.0000000f, 0.0000000f}, // Base2
{-1.e-15f, -1.e-14f, 25.0000000f}, // Extra vert along the vertical edge
};
TArray<TArray<int32>> TetFaces =
{
{ 1, 2, 3 }, // Base
{ 0, 2, 1 }, // Side0
{ 0, 3, 2 }, // Side1
{ 0, 1, 3 }, // Side2
{ 0, 1, 4 }, // Extra degenerate face
};
TArray<TPlaneConcrete<FRealSingle>> TetPlanes =
{
// Values don't matter for this test
TPlaneConcrete<FRealSingle>(FVec3(0), FVec3(0,0,1)),
TPlaneConcrete<FRealSingle>(FVec3(0), FVec3(0,0,1)),
TPlaneConcrete<FRealSingle>(FVec3(0), FVec3(0,0,1)),
TPlaneConcrete<FRealSingle>(FVec3(0), FVec3(0,0,1)),
TPlaneConcrete<FRealSingle>(FVec3(0), FVec3(0,0,1)),
};
const FRealSingle AngleTolerance = 1.e-6f;
FConvexBuilder::MergeColinearEdges(TetPlanes, TetFaces, TetVerts, AngleTolerance);
// The invalid face and its vertex should have been stripped
EXPECT_EQ(TetVerts.Num(), 4);
EXPECT_EQ(TetFaces.Num(), 4);
EXPECT_EQ(TetPlanes.Num(), 4);
}
}