759 lines
28 KiB
C++
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);
|
|
}
|
|
}
|