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.
o3de/Gems/PhysX/Code/Tests/PrimitiveShapeFitterTests.cpp

663 lines
24 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 <MathConversion.h>
#include <AzTest/AzTest.h>
#include <AzCore/std/sort.h>
#include <AzCore/Component/ComponentApplication.h>
#include <Source/Pipeline/PrimitiveShapeFitter/AbstractShapeParameterization.h>
#include <Source/Pipeline/PrimitiveShapeFitter/PrimitiveShapeFitter.h>
namespace PhysX::Pipeline
{
static constexpr double FitterTolerance = 0.1;
extern const AbstractShapeParameterization::Ptr SimpleSphere;
extern const AbstractShapeParameterization::Ptr TransformedSphere;
extern const AbstractShapeParameterization::Ptr DegenerateSphere;
extern const AbstractShapeParameterization::Ptr SimpleBox;
extern const AbstractShapeParameterization::Ptr TransformedBox;
extern const AbstractShapeParameterization::Ptr DegenerateBox;
extern const AbstractShapeParameterization::Ptr SimpleCapsule;
extern const AbstractShapeParameterization::Ptr TransformedCapsule;
extern const AbstractShapeParameterization::Ptr DegenerateCapsule;
extern const AZStd::array<AZ::Transform, 5> TestTransforms;
extern const AZStd::array<AZ::Vector3, 482> SphereVertices;
extern const AZStd::array<AZ::Vector3, 14> BoxVertices;
extern const AZStd::array<AZ::Vector3, 514> CapsuleVertices;
extern const AZStd::array<AZ::Vector3, 8> MinimalVertices;
static void ExpectNear(const AZ::Vector3& actual, const AZ::Vector3& expected, const double tolerance = 1.0e-6)
{
EXPECT_NEAR(actual(0), expected(0), tolerance);
EXPECT_NEAR(actual(1), expected(1), tolerance);
EXPECT_NEAR(actual(2), expected(2), tolerance);
}
static void ExpectParallel(const AZ::Vector3& actual, const AZ::Vector3& expected, const double tolerance = 1.0e-6)
{
if (expected.Dot(actual) > 0.0)
{
ExpectNear(actual, expected, tolerance);
}
else
{
ExpectNear(actual, -expected, tolerance);
}
}
static void ExpectRightHandedOrthonormalBasis(
const AZ::Vector3& xAxis,
const AZ::Vector3& yAxis,
const AZ::Vector3& zAxis
)
{
EXPECT_NEAR(xAxis.GetLengthSq(), 1.0, 1.0e-6);
EXPECT_NEAR(yAxis.GetLengthSq(), 1.0, 1.0e-6);
EXPECT_NEAR(zAxis.GetLengthSq(), 1.0, 1.0e-6);
EXPECT_NEAR(xAxis.Dot(yAxis), 0.0, 1.0e-6);
ExpectNear(zAxis, xAxis.Cross(yAxis), 1.0e-6);
}
static AZStd::vector<Vec3> AZVerticesToLYVertices(const AZStd::vector<AZ::Vector3>& vertices)
{
AZStd::vector<Vec3> converted(vertices.size(), Vec3(ZERO));
AZStd::transform(
vertices.begin(),
vertices.end(),
converted.begin(),
[] (const AZ::Vector3& vertex) {
return AZVec3ToLYVec3(vertex);
}
);
return converted;
}
template<size_t N>
static AZStd::vector<AZ::Vector3> TransformVertices(
const AZStd::array<AZ::Vector3, N>& vertices,
const AZ::Transform& transform
)
{
AZStd::vector<AZ::Vector3> transformed(N, AZ::Vector3::CreateZero());
AZStd::transform(
vertices.begin(),
vertices.end(),
transformed.begin(),
[&transform] (const AZ::Vector3& vertex) {
return transform.TransformVector(vertex) + transform.GetTranslation();
}
);
return transformed;
}
class ArgumentPackingTestFixture
: public ::testing::TestWithParam<AbstractShapeParameterization*>
{
};
TEST_P(ArgumentPackingTestFixture, ArgumentPackingTest)
{
AbstractShapeParameterization* shape = GetParam();
AZStd::vector<double> argsBefore = shape->PackArguments();
shape->UnpackArguments(argsBefore);
AZStd::vector<double> argsAfter = shape->PackArguments();
EXPECT_THAT(argsBefore, ::testing::SizeIs(shape->GetDegreesOfFreedom()));
EXPECT_THAT(argsAfter, ::testing::SizeIs(shape->GetDegreesOfFreedom()));
for (AZ::u32 i = 0; i < shape->GetDegreesOfFreedom(); ++i)
{
EXPECT_NEAR(argsBefore[i], argsAfter[i], 1e-6);
}
}
INSTANTIATE_TEST_CASE_P(
All,
ArgumentPackingTestFixture,
::testing::Values(
// Spheres
SimpleSphere.get(),
TransformedSphere.get(),
DegenerateSphere.get(),
// Boxes
SimpleBox.get(),
TransformedBox.get(),
DegenerateBox.get(),
// Capsules
SimpleCapsule.get(),
TransformedCapsule.get(),
DegenerateCapsule.get()
)
);
struct VolumeTestData
{
const AbstractShapeParameterization* m_shape;
const double m_expectedVolume;
};
class VolumeTestFixture
: public ::testing::TestWithParam<VolumeTestData>
{
};
TEST_P(VolumeTestFixture, VolumeTest)
{
const VolumeTestData& testData = GetParam();
EXPECT_NEAR(testData.m_shape->GetVolume(), testData.m_expectedVolume, 1e-6);
}
INSTANTIATE_TEST_CASE_P(
All,
VolumeTestFixture,
::testing::Values(
// Spheres
VolumeTestData{ SimpleSphere.get(), 4.188790205 },
VolumeTestData{ TransformedSphere.get(), 113.0973355 },
VolumeTestData{ DegenerateSphere.get(), 0.0 },
// Boxes
VolumeTestData{ SimpleBox.get(), 8.0 },
VolumeTestData{ TransformedBox.get(), 120.0 },
VolumeTestData{ DegenerateBox.get(), 8.0e-6 },
// Capsules
VolumeTestData{ SimpleCapsule.get(), 16.75516082 },
VolumeTestData{ TransformedCapsule.get(), 16.75516082 },
VolumeTestData{ DegenerateCapsule.get(), 6.283183213e-12 }
)
);
const AZStd::array<Vector, 5> squaredDistanceTestPoints{
Vector{{ 0.0, 0.0, 0.0 }},
Vector{{ 1.0, 2.0, 3.0 }},
Vector{{ -0.5, 0.0, 0.0 }},
Vector{{ 0.0, 1.8, 4.0 }},
Vector{{ -10.0, -10.0, -10.0 }}
};
struct SquaredDistanceTestData
{
const AbstractShapeParameterization* m_shape;
const AZStd::array<double, 5> m_expectedSquaredDistances;
};
class SquaredDistanceTestFixture
: public ::testing::TestWithParam<SquaredDistanceTestData>
{
};
TEST_P(SquaredDistanceTestFixture, SquaredDistanceTest)
{
const SquaredDistanceTestData& testData = GetParam();
// The following checks for an error in the test, so use a hard assert.
ASSERT_EQ(squaredDistanceTestPoints.size(), testData.m_expectedSquaredDistances.size());
for (AZ::u32 i = 0; i < squaredDistanceTestPoints.size(); ++i)
{
EXPECT_NEAR(
testData.m_shape->SquaredDistanceToShape(squaredDistanceTestPoints[i]),
testData.m_expectedSquaredDistances[i],
1.0e-6
);
}
}
INSTANTIATE_TEST_CASE_P(
All,
SquaredDistanceTestFixture,
::testing::Values(
// Spheres
SquaredDistanceTestData{
SimpleSphere.get(),
{{
1.0,
7.516685226,
0.25,
11.46731512,
266.3589838
}}
},
SquaredDistanceTestData{
TransformedSphere.get(),
{{
0.5500556794,
9.0,
0.8192509723,
2.470285886,
318.0040001
}}
},
SquaredDistanceTestData{
DegenerateSphere.get(),
{{
0.0,
14.0,
0.25,
19.24,
300.0
}}
},
// Boxes
SquaredDistanceTestData{
SimpleBox.get(),
{{
1.0,
5.0,
0.25,
9.64,
243.0
}}
},
SquaredDistanceTestData{
TransformedBox.get(),
{{
1.0,
1.0,
1.0,
0.64,
259.2590209
}}
},
SquaredDistanceTestData{
DegenerateBox.get(),
{{
1.0e-6,
9.999994,
1.0e-6,
16.639992,
261.99998
}}
},
// Capsules
SquaredDistanceTestData{
SimpleCapsule.get(),
{{
1.0,
6.788897449,
1.0,
11.46731512,
232.5038464
}}
},
SquaredDistanceTestData{
TransformedCapsule.get(),
{{
7.222962325,
1.0,
8.409713211,
0.3397693547,
385.6204406
}}
},
SquaredDistanceTestData{
DegenerateCapsule.get(),
{{
1.0e-6,
12.99999279,
1.0e-6,
19.23999123,
248.9999824
}}
}
)
);
TEST(GetShapeConfigurationTestFixture, SimpleSphereTest)
{
const MeshAssetData::ShapeConfigurationPair pair = SimpleSphere->GetShapeConfigurationPair();
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Sphere);
EXPECT_NEAR(static_cast<const Physics::SphereShapeConfiguration&>(*shapePtr).m_radius, 1.0, 1.0e-6);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
ExpectNear(configPtr->m_transform->GetTranslation(), AZ::Vector3::CreateZero());
}
TEST(GetShapeConfigurationTestFixture, TransformedSphereTest)
{
const MeshAssetData::ShapeConfigurationPair pair = TransformedSphere->GetShapeConfigurationPair();
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Sphere);
EXPECT_NEAR(static_cast<const Physics::SphereShapeConfiguration&>(*shapePtr).m_radius, 3.0, 1.0e-6);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
ExpectNear(configPtr->m_transform->GetTranslation(), AZ::Vector3(1.0, 2.0, 3.0));
}
TEST(GetShapeConfigurationTestFixture, SimpleBoxTest)
{
const MeshAssetData::ShapeConfigurationPair pair = SimpleBox->GetShapeConfigurationPair();
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Box);
ExpectNear(
static_cast<const Physics::BoxShapeConfiguration&>(*shapePtr).m_dimensions,
AZ::Vector3(2.0, 2.0, 2.0)
);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
ExpectNear(configPtr->m_transform->GetTranslation(), AZ::Vector3::CreateZero());
ExpectNear(configPtr->m_transform->GetBasisX(), AZ::Vector3(1.0, 0.0, 0.0));
ExpectNear(configPtr->m_transform->GetBasisY(), AZ::Vector3(0.0, 1.0, 0.0));
ExpectNear(configPtr->m_transform->GetBasisZ(), AZ::Vector3(0.0, 0.0, 1.0));
}
TEST(GetShapeConfigurationTestFixture, TransformedBoxTest)
{
const MeshAssetData::ShapeConfigurationPair pair = TransformedBox->GetShapeConfigurationPair();
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Box);
ExpectNear(
static_cast<const Physics::BoxShapeConfiguration&>(*shapePtr).m_dimensions,
AZ::Vector3(6.0, 2.0, 10.0)
);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
ExpectNear(configPtr->m_transform->GetTranslation(), AZ::Vector3(1.0, 2.0, 3.0));
ExpectNear(configPtr->m_transform->GetBasisX(), AZ::Vector3(0.8660254038, 0.0, -0.5));
ExpectNear(configPtr->m_transform->GetBasisY(), AZ::Vector3(0.0, 1.0, 0.0));
ExpectNear(configPtr->m_transform->GetBasisZ(), AZ::Vector3(0.5, 0.0, 0.8660254038));
}
TEST(GetShapeConfigurationTestFixture, SimpleCapsuleTest)
{
const MeshAssetData::ShapeConfigurationPair pair = SimpleCapsule->GetShapeConfigurationPair();
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Capsule);
EXPECT_NEAR(static_cast<const Physics::CapsuleShapeConfiguration&>(*shapePtr).m_height, 6.0, 1.0e-6);
EXPECT_NEAR(static_cast<const Physics::CapsuleShapeConfiguration&>(*shapePtr).m_radius, 1.0, 1.0e-6);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
ExpectNear(configPtr->m_transform->GetTranslation(), AZ::Vector3::CreateZero());
// For capsules, the z-axis is the primary axis.
ExpectNear(configPtr->m_transform->GetBasisZ(), AZ::Vector3(1.0, 0.0, 0.0));
}
TEST(GetShapeConfigurationTestFixture, TransformedCapsuleTest)
{
const MeshAssetData::ShapeConfigurationPair pair = TransformedCapsule->GetShapeConfigurationPair();
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Capsule);
EXPECT_NEAR(static_cast<const Physics::CapsuleShapeConfiguration&>(*shapePtr).m_height, 6.0, 1.0e-6);
EXPECT_NEAR(static_cast<const Physics::CapsuleShapeConfiguration&>(*shapePtr).m_radius, 1.0, 1.0e-6);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
ExpectNear(configPtr->m_transform->GetTranslation(), AZ::Vector3(1.0, 2.0, 3.0));
// For capsules, the z-axis is the primary axis.
ExpectNear(configPtr->m_transform->GetBasisZ(), AZ::Vector3(0.8660254038, 0.0, -0.5));
}
class GetDegenerateShapeConfigurationTestFixture
: public ::testing::TestWithParam<AbstractShapeParameterization*>
{
};
TEST_P(GetDegenerateShapeConfigurationTestFixture, GetDegenerateShapeConfigurationTest)
{
const MeshAssetData::ShapeConfigurationPair pair = GetParam()->GetShapeConfigurationPair();
EXPECT_THAT(pair.second, ::testing::IsNull());
}
INSTANTIATE_TEST_CASE_P(
All,
GetDegenerateShapeConfigurationTestFixture,
::testing::Values(
DegenerateSphere.get(),
DegenerateBox.get(),
DegenerateCapsule.get()
)
);
class FitPrimitiveShapeTestFixture
: public ::testing::TestWithParam<AZ::Transform>
{
};
TEST_P(FitPrimitiveShapeTestFixture, SphereTest)
{
const AZ::Transform& transform = GetParam();
MeshAssetData::ShapeConfigurationPair pair = FitPrimitiveShape(
"sphere",
AZVerticesToLYVertices(TransformVertices(SphereVertices, transform)),
0.0,
PrimitiveShapeTarget::Sphere
);
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Sphere);
EXPECT_NEAR(static_cast<const Physics::SphereShapeConfiguration&>(*shapePtr).m_radius, 1.0, FitterTolerance);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
ExpectNear(configPtr->m_transform->GetTranslation(), transform.GetTranslation(), FitterTolerance);
ExpectRightHandedOrthonormalBasis(
configPtr->m_transform->GetBasisX(),
configPtr->m_transform->GetBasisY(),
configPtr->m_transform->GetBasisZ()
);
}
TEST_P(FitPrimitiveShapeTestFixture, BoxTest)
{
const AZ::Transform& transform = GetParam();
MeshAssetData::ShapeConfigurationPair pair = FitPrimitiveShape(
"box",
AZVerticesToLYVertices(TransformVertices(BoxVertices, transform)),
0.0,
PrimitiveShapeTarget::Box
);
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Box);
ExpectNear(
static_cast<const Physics::BoxShapeConfiguration&>(*shapePtr).m_dimensions,
AZ::Vector3(10.0, 6.0, 2.0),
FitterTolerance
);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
const AZ::Vector3 xAxis = configPtr->m_transform->GetBasisX();
const AZ::Vector3 yAxis = configPtr->m_transform->GetBasisY();
const AZ::Vector3 zAxis = configPtr->m_transform->GetBasisZ();
ExpectNear(configPtr->m_transform->GetTranslation(), transform.GetTranslation(), FitterTolerance);
ExpectParallel(xAxis, transform.GetBasisX(), FitterTolerance);
ExpectParallel(yAxis, transform.GetBasisY(), FitterTolerance);
ExpectParallel(zAxis, transform.GetBasisZ(), FitterTolerance);
ExpectRightHandedOrthonormalBasis(xAxis, yAxis, zAxis);
}
TEST_P(FitPrimitiveShapeTestFixture, CapsuleTest)
{
const AZ::Transform& transform = GetParam();
MeshAssetData::ShapeConfigurationPair pair = FitPrimitiveShape(
"capsule",
AZVerticesToLYVertices(TransformVertices(CapsuleVertices, transform)),
0.0,
PrimitiveShapeTarget::Capsule
);
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Capsule);
EXPECT_NEAR(static_cast<const Physics::CapsuleShapeConfiguration&>(*shapePtr).m_height, 4.0, FitterTolerance);
EXPECT_NEAR(static_cast<const Physics::CapsuleShapeConfiguration&>(*shapePtr).m_radius, 1.0, FitterTolerance);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
const AZ::Vector3 xAxis = configPtr->m_transform->GetBasisX();
const AZ::Vector3 yAxis = configPtr->m_transform->GetBasisY();
const AZ::Vector3 zAxis = configPtr->m_transform->GetBasisZ();
ExpectNear(configPtr->m_transform->GetTranslation(), transform.GetTranslation(), FitterTolerance);
ExpectParallel(zAxis, transform.GetBasisX(), FitterTolerance);
ExpectRightHandedOrthonormalBasis(xAxis, yAxis, zAxis);
}
TEST_P(FitPrimitiveShapeTestFixture, VolumeMinimizationTest)
{
// This test verifies that the volume minimization coefficient works as expected. The vertices used here form a
// 2x2x2 cube centered at the origin. We let the fitter decide which primitive fits best, which should always be
// a cube that wraps the cube snugly. Note that this test can fail for certain very specific initializations
// which are at a local maximum with regard to the orientation parameters, so the derivatives for those parameters
// are zero and there is never any progress in updating them. This can happen for example with a 45 degree
// rotation about one of the axes.
const AZ::Transform& expectedTransform = GetParam();
AZStd::vector<AZ::Vector3> expectedVertices = TransformVertices(MinimalVertices, expectedTransform);
MeshAssetData::ShapeConfigurationPair pair = FitPrimitiveShape(
"minimal",
AZVerticesToLYVertices(expectedVertices),
5.0e-4,
PrimitiveShapeTarget::BestFit
);
const AZStd::shared_ptr<AssetColliderConfiguration> configPtr = pair.first;
const AZStd::shared_ptr<Physics::ShapeConfiguration> shapePtr = pair.second;
// Validate shape.
ASSERT_THAT(shapePtr, ::testing::NotNull());
ASSERT_TRUE(shapePtr->GetShapeType() == Physics::ShapeType::Box);
ExpectNear(
static_cast<const Physics::BoxShapeConfiguration&>(*shapePtr).m_dimensions,
AZ::Vector3(2.0, 2.0, 2.0),
FitterTolerance
);
// Validate transform.
ASSERT_THAT(configPtr, ::testing::NotNull());
ASSERT_TRUE(configPtr->m_transform.has_value());
const AZ::Transform& actualTransform = *(configPtr->m_transform);
const AZ::Vector3 xAxis = actualTransform.GetBasisX();
const AZ::Vector3 yAxis = actualTransform.GetBasisY();
const AZ::Vector3 zAxis = actualTransform.GetBasisZ();
ExpectNear(actualTransform.GetTranslation(), expectedTransform.GetTranslation(), FitterTolerance);
ExpectRightHandedOrthonormalBasis(xAxis, yAxis, zAxis);
// The basis vectors of the returned transform could be reflections and/or rotations of the basis vectors of the
// expected transform. Because of this, we instead check that the returned transform moves the eight vertices of
// the cube close to the expected vertices.
AZStd::vector<AZ::Vector3> actualVertices = TransformVertices(MinimalVertices, actualTransform);
// Sanity check.
ASSERT_EQ(expectedVertices.size(), actualVertices.size());
// Sort both sets of vertices so that we can compare them element by element. We sort them by x-coordinate
// first, then y-coordinate and finally z-cooridnate.
auto comparator = [] (const AZ::Vector3& lhs, const AZ::Vector3& rhs) {
return lhs(0) < rhs(0) || (lhs(0) == rhs(0) && lhs(1) < rhs(1) || (lhs(1) == rhs(1) && lhs(2) < rhs(2)));
};
AZStd::sort(expectedVertices.begin(), expectedVertices.end(), comparator);
AZStd::sort(actualVertices.begin(), actualVertices.end(), comparator);
for (AZ::u32 i = 0; i < actualVertices.size(); ++i)
{
ExpectNear(expectedVertices[i], actualVertices[i], FitterTolerance);
}
}
INSTANTIATE_TEST_CASE_P(
All,
FitPrimitiveShapeTestFixture,
::testing::ValuesIn(TestTransforms)
);
} // namespace PhysX::Pipeline