// 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& Vertices) { TArray Planes; TArray> FaceVertices; TArray 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 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 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 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& 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 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 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 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 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 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 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 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 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 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 Planes; TArray> FaceIndices; TArray 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 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 Planes; TArray> FaceVertices; TArray 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 VertexIndexCount; TArray 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 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 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 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 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 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> 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 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 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 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 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> TetFaces = { { 1, 2, 3 }, // Base { 0, 2, 1 }, // Side0 { 0, 3, 2 }, // Side1 { 0, 1, 3 }, // Side2 { 0, 1, 4 }, // Extra degenerate face }; TArray> TetPlanes = { // Values don't matter for this test TPlaneConcrete(FVec3(0), FVec3(0,0,1)), TPlaneConcrete(FVec3(0), FVec3(0,0,1)), TPlaneConcrete(FVec3(0), FVec3(0,0,1)), TPlaneConcrete(FVec3(0), FVec3(0,0,1)), TPlaneConcrete(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); } }