/* * 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 #include #include #include #include #include #include namespace UnitTest { class CylinderShapeTest : public AllocatorsFixture { AZStd::unique_ptr m_serializeContext; AZStd::unique_ptr m_transformComponentDescriptor; AZStd::unique_ptr m_cylinderShapeComponentDescriptor; public: void SetUp() override { AllocatorsFixture::SetUp(); m_serializeContext = AZStd::make_unique(); m_transformComponentDescriptor = AZStd::unique_ptr(AzFramework::TransformComponent::CreateDescriptor()); m_transformComponentDescriptor->Reflect(&(*m_serializeContext)); m_cylinderShapeComponentDescriptor = AZStd::unique_ptr(LmbrCentral::CylinderShapeComponent::CreateDescriptor()); m_cylinderShapeComponentDescriptor->Reflect(&(*m_serializeContext)); } void TearDown() override { m_transformComponentDescriptor.reset(); m_cylinderShapeComponentDescriptor.reset(); m_serializeContext.reset(); AllocatorsFixture::TearDown(); } }; using CylinderParams = std::tuple; using BoundingBoxResult = std::tuple; using BoundingBoxParams = std::tuple; using IsPointInsideParams = std::tuple; using RayParams = std::tuple; using RayIntersectResult = std::tuple; using RayIntersectParams = std::tuple; using DistanceResultParams = std::tuple; using DistanceFromPointParams = std::tuple; class CylinderShapeRayIntersectTest : public CylinderShapeTest , public testing::WithParamInterface { public: static const std::vector ShouldPass; static const std::vector ShouldFail; }; class CylinderShapeAABBTest : public CylinderShapeTest , public testing::WithParamInterface { public: static const std::vector ShouldPass; }; class CylinderShapeTransformAndLocalBoundsTest : public CylinderShapeTest , public testing::WithParamInterface { public: static const std::vector ShouldPass; }; class CylinderShapeIsPointInsideTest : public CylinderShapeTest , public testing::WithParamInterface { public: static const std::vector ShouldPass; static const std::vector ShouldFail; }; class CylinderShapeDistanceFromPointTest : public CylinderShapeTest , public testing::WithParamInterface { public: static const std::vector ShouldPass; }; const std::vector CylinderShapeRayIntersectTest::ShouldPass = { // Test case 0 { // Ray: src, dir { AZ::Vector3(0.0f, 5.0f, 5.0f), AZ::Vector3(0.0f, -1.0f, 0.0f) }, // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation( AZ::Vector3(0.0f, 0.0f, 5.0f)), 0.5f, 5.0f }, // Result: hit, distance, epsilon { true, 4.5f, 1e-4f } }, // Test case 1 { // Ray: src, dir { AZ::Vector3(-10.0f, -20.0f, 0.0f), AZ::Vector3(0.0f, 1.0f, 0.0f) }, // Cylinder: transform, radius, height { AZ::Transform::CreateFromQuaternionAndTranslation( AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisX(), AZ::Constants::HalfPi), AZ::Vector3(-10.0f, -10.0f, 0.0f)), 1.0f, 5.0f }, // Result: hit, distance, epsilon { true, 7.5f, 1e-2f } }, // Test case 2 {// Ray: src, dir { AZ::Vector3(-10.0f, -10.0f, -10.0f), AZ::Vector3(0.0f, 0.0f, 1.0f) }, // Cylinder: transform, radius, height { AZ::Transform::CreateFromQuaternionAndTranslation( AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisX(), AZ::Constants::HalfPi), AZ::Vector3(-10.0f, -10.0f, 0.0f)), 1.0f, 5.0f }, // Result: hit, distance, epsilon { true, 9.0f, 1e-2f } }, // Test case 3 { // Ray: src, dir { AZ::Vector3(-9.0f, -14.0f, -1.0f), AZ::Vector3(-1.0f, 0.0f, 0.0f) }, // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(-14.0f, -14.0f, -1.0f)) * AZ::Transform::CreateRotationY(AZ::Constants::HalfPi) * AZ::Transform::CreateRotationZ(AZ::Constants::HalfPi) * AZ::Transform::CreateUniformScale(4.0f), 1.0f, 1.25f }, // Result: hit, distance, epsilon { true, 2.5f, 1e-2f } }, // Test case 4 { // Ray: src, dir { AZ::Vector3(0.0f, 5.0f, 5.0f), AZ::Vector3(0.0f, -1.0f, 0.0f) }, // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation( AZ::Vector3(0.0f, 0.0f, 5.0f)), 0.0f, 5.0f }, // Result: hit, distance, epsilon { true, 0.0f, 1e-4f } }, // Test case 5 { // Ray: src, dir { AZ::Vector3(0.0f, 5.0f, 5.0f), AZ::Vector3(0.0f, -1.0f, 0.0f) }, // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation( AZ::Vector3(0.0f, 0.0f, 5.0f)), 0.0f, 5.0f }, // Result: hit, distance, epsilon { true, 0.0f, 1e-4f } }, // Test case 6 { // Ray: src, dir { AZ::Vector3(0.0f, 5.0f, 5.0f), AZ::Vector3(0.0f, -1.0f, 0.0f) }, // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation( AZ::Vector3(0.0f, 0.0f, 5.0f)), 0.0f, 0.0f }, // Result: hit, distance, epsilon { true, 0.0f, 1e-4f } } }; const std::vector CylinderShapeRayIntersectTest::ShouldFail = { // Test case 0 { // Ray: src, dir { AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3(1.0f, 0.0f, 0.0f) }, // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation( AZ::Vector3(0.0f, -10.0f, 0.0f)), 5.0f, 1.0f }, // Result: hit, distance, epsilon { false, 0.0f, 0.0f } } }; const std::vector CylinderShapeAABBTest::ShouldPass = { // Test case 0 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation( AZ::Vector3(0.0f, -10.0f, 0.0f)), 5.0f, 1.0f }, // AABB: min, max { AZ::Vector3(-5.0f, -15.0f, -0.5f), AZ::Vector3(5.0f, -5.0f, 0.5f) } }, // Test case 1 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(-10.0f, -10.0f, 0.0f)) * AZ::Transform::CreateRotationX(AZ::Constants::HalfPi) * AZ::Transform::CreateRotationY(AZ::Constants::QuarterPi), 1.0f, 5.0f }, // AABB: min, max { AZ::Vector3(-12.4748f, -12.4748f, -1.0f), AZ::Vector3(-7.52512f, -7.52512f, 1.0f) } }, // Test case 2 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(-10.0f, -10.0f, 10.0f)) * AZ::Transform::CreateUniformScale(3.5f), 1.0f, 5.0f }, // AABB: min, max { AZ::Vector3(-13.5f, -13.5f, 1.25f), AZ::Vector3(-6.5f, -6.5f, 18.75f) } }, // Test case 3 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 0.0f, 0.0f)), 0.0f, 1.0f }, // AABB: min, max { AZ::Vector3(0.0f, 0.0f, -0.5f), AZ::Vector3(0.0f, 0.0f,-0.5f) } }, // Test case 4 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 0.0f, 0.0f)), 1.0f, 0.0f }, // AABB: min, max { AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3(0.0f, 0.0f, 0.0f) } }, // Test case 5 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 0.0f, 0.0f)), 0.0f, 0.0f }, // AABB: min, max { AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3(0.0f, 0.0f, 0.0f) } } }; const std::vector CylinderShapeTransformAndLocalBoundsTest::ShouldPass = { // Test case 0 { // Cylinder: transform, radius, height { AZ::Transform::CreateIdentity(), 5.0f, 1.0f }, // Local bounds: min, max { AZ::Vector3(-5.0f, -5.0f, -0.5f), AZ::Vector3(5.0f, 5.0f, 0.5f) } }, // Test case 1 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(-10.0f, -10.0f, 10.0f)) * AZ::Transform::CreateUniformScale(3.5f), 5.0f, 5.0f }, // Local bounds: min, max { AZ::Vector3(-5.0f, -5.0f, -2.5f), AZ::Vector3(5.0f, 5.0f, 2.5f) } }, // Test case 2 { // Cylinder: transform, radius, height { AZ::Transform::CreateIdentity(), 0.0f, 5.0f }, // Local bounds: min, max { AZ::Vector3(0.0f, 0.0f, -2.5f), AZ::Vector3(0.0f, 0.0f, 2.5f) } }, // Test case 3 { // Cylinder: transform, radius, height { AZ::Transform::CreateIdentity(), 5.0f, 0.0f }, // Local bounds: min, max { AZ::Vector3(-5.0f, -5.0f, -0.0f), AZ::Vector3(5.0f, 5.0f, 0.0f) } }, // Test case 4 { // Cylinder: transform, radius, height { AZ::Transform::CreateIdentity(), 0.0f, 0.0f }, // Local bounds: min, max { AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3(0.0f, 0.0f, 0.0f) } }, }; const std::vector CylinderShapeIsPointInsideTest::ShouldPass = { // Test case 0 { // Cylinder: transform, radius, height {AZ::Transform::CreateTranslation(AZ::Vector3(27.0f, 28.0f, 38.0f)) * AZ::Transform::CreateUniformScale(2.5f), 0.5f, 2.0f}, // Point AZ::Vector3(27.0f, 28.5f, 40.0f), // Result true }, // Test case 1 { // Cylinder: transform, radius, height {AZ::Transform::CreateTranslation(AZ::Vector3(27.0f, 28.0f, 38.0f)) * AZ::Transform::CreateRotationX(AZ::Constants::HalfPi) * AZ::Transform::CreateRotationY(AZ::Constants::QuarterPi) * AZ::Transform::CreateUniformScale(0.5f), 0.5f, 2.0f}, // Point AZ::Vector3(27.0f, 28.155f, 37.82f), // Result true } }; const std::vector CylinderShapeIsPointInsideTest::ShouldFail = { // Test case 0 { // Cylinder: transform, radius, height {AZ::Transform::CreateTranslation(AZ::Vector3(0, 0.0f, 0.0f)), 0.0f, 1.0f}, // Point AZ::Vector3(0.0f, 0.0f, 0.0f), // Result false }, // Test case 1 { // Cylinder: transform, radius, height {AZ::Transform::CreateTranslation(AZ::Vector3(0, 0.0f, 0.0f)), 1.0f, 0.0f}, // Point AZ::Vector3(0.0f, 0.0f, 0.0f), // Result false }, // Test case 2 { // Cylinder: transform, radius, height {AZ::Transform::CreateTranslation(AZ::Vector3(0, 0.0f, 0.0f)), 0.0f, 0.0f}, // Point AZ::Vector3(0.0f, 0.0f, 0.0f), // Result false } }; const std::vector CylinderShapeDistanceFromPointTest::ShouldPass = { // Test case 0 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(27.0f, 28.0f, 38.0f)) * AZ::Transform::CreateRotationX(AZ::Constants::HalfPi) * AZ::Transform::CreateRotationY(AZ::Constants::QuarterPi) * AZ::Transform::CreateUniformScale(2.0f), 0.5f, 4.0f }, // Point AZ::Vector3(27.0f, 28.0f, 41.0f), // Result: distance, epsilon { 2.0f, 1e-2f } }, // Test case 1 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(27.0f, 28.0f, 38.0f)) * AZ::Transform::CreateRotationX(AZ::Constants::HalfPi) * AZ::Transform::CreateRotationY(AZ::Constants::QuarterPi) * AZ::Transform::CreateUniformScale(2.0f), 0.5f, 4.0f }, // Point AZ::Vector3(22.757f, 32.243f, 38.0f), // Result: distance, epsilon { 2.0f, 1e-2f } }, // Test case 2 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 0.0f, 0.0f)), 0.0f, 1.0f }, // Point AZ::Vector3(0.0f, 5.0f, 0.0f), // Result: distance, epsilon { 5.0f, 1e-1f } }, // Test case 3 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 0.0f, 0.0f)), 1.0f, 0.0f }, // Point AZ::Vector3(0.0f, 5.0f, 0.0f), // Result: distance, epsilon { 5.0f, 1e-2f } }, // Test case 4 { // Cylinder: transform, radius, height { AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 0.0f, 0.0f)), 0.0f, 0.0f }, // Point AZ::Vector3(0.0f, 5.0f, 0.0f), // Result: distance, epsilon { 5.0f, 1e-2f } } }; void CreateCylinder(const AZ::Transform& transform, float radius, float height, AZ::Entity& entity) { entity.CreateComponent(); entity.CreateComponent(); entity.Init(); entity.Activate(); AZ::TransformBus::Event(entity.GetId(), &AZ::TransformBus::Events::SetWorldTM, transform); LmbrCentral::CylinderShapeComponentRequestsBus::Event(entity.GetId(), &LmbrCentral::CylinderShapeComponentRequestsBus::Events::SetHeight, height); LmbrCentral::CylinderShapeComponentRequestsBus::Event(entity.GetId(), &LmbrCentral::CylinderShapeComponentRequestsBus::Events::SetRadius, radius); } void CreateDefaultCylinder(const AZ::Transform& transform, AZ::Entity& entity) { CreateCylinder(transform, 10.0f, 10.0f, entity); } bool RandomPointsAreInCylinder(const AZ::RandomDistributionType distributionType) { //Apply a simple transform to put the Cylinder off the origin AZ::Transform transform = AZ::Transform::CreateIdentity(); transform.SetRotation(AZ::Quaternion::CreateRotationY(AZ::Constants::HalfPi)); transform.SetTranslation(AZ::Vector3(5.0f, 5.0f, 5.0f)); AZ::Entity entity; CreateDefaultCylinder(transform, entity); const AZ::u32 testPoints = 10000; AZ::Vector3 testPoint; bool testPointInVolume = false; //Test a bunch of random points generated with the Normal random distribution type //They should all end up in the volume for (AZ::u32 i = 0; i < testPoints; ++i) { LmbrCentral::ShapeComponentRequestsBus::EventResult(testPoint, entity.GetId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GenerateRandomPointInside, distributionType); LmbrCentral::ShapeComponentRequestsBus::EventResult(testPointInVolume, entity.GetId(), &LmbrCentral::ShapeComponentRequestsBus::Events::IsPointInside, testPoint); if (!testPointInVolume) { return false; } } return true; } TEST_F(CylinderShapeTest, NormalDistributionRandomPointsAreInVolume) { EXPECT_TRUE(RandomPointsAreInCylinder(AZ::RandomDistributionType::Normal)); } TEST_F(CylinderShapeTest, UniformRealDistributionRandomPointsAreInVolume) { EXPECT_TRUE(RandomPointsAreInCylinder(AZ::RandomDistributionType::UniformReal)); } TEST_P(CylinderShapeRayIntersectTest, GetRayIntersectCylinder) { const auto& [ray, cylinder, result] = GetParam(); const auto& [src, dir] = ray; const auto& [transform, radius, height] = cylinder; const auto& [expectedHit, expectedDistance, epsilon] = result; AZ::Entity entity; CreateCylinder(transform, radius, height, entity); AZ::Transform::CreateFromQuaternionAndTranslation( AZ::Quaternion::CreateIdentity(), AZ::Vector3(0.0f, 0.0f, 5.0f)); bool rayHit = false; float distance; LmbrCentral::ShapeComponentRequestsBus::EventResult( rayHit, entity.GetId(), &LmbrCentral::ShapeComponentRequests::IntersectRay, src, dir, distance); EXPECT_EQ(rayHit, expectedHit); if (expectedHit) { EXPECT_NEAR(distance, expectedDistance, epsilon); } } INSTANTIATE_TEST_CASE_P(ValidIntersections, CylinderShapeRayIntersectTest, ::testing::ValuesIn(CylinderShapeRayIntersectTest::ShouldPass) ); INSTANTIATE_TEST_CASE_P(InvalidIntersections, CylinderShapeRayIntersectTest, ::testing::ValuesIn(CylinderShapeRayIntersectTest::ShouldFail) ); TEST_P(CylinderShapeAABBTest, GetAabb) { const auto& [cylinder, AABB] = GetParam(); const auto& [transform, radius, height] = cylinder; const auto& [minExtent, maxExtent] = AABB; AZ::Entity entity; CreateCylinder(transform, radius, height, entity); AZ::Aabb aabb; LmbrCentral::ShapeComponentRequestsBus::EventResult( aabb, entity.GetId(), &LmbrCentral::ShapeComponentRequests::GetEncompassingAabb); EXPECT_TRUE(aabb.GetMin().IsClose(minExtent)); EXPECT_TRUE(aabb.GetMax().IsClose(maxExtent)); } INSTANTIATE_TEST_CASE_P(AABB, CylinderShapeAABBTest, ::testing::ValuesIn(CylinderShapeAABBTest::ShouldPass) ); TEST_P(CylinderShapeTransformAndLocalBoundsTest, GetTransformAndLocalBounds) { const auto& [cylinder, boundingBox] = GetParam(); const auto& [transform, radius, height] = cylinder; const auto& [minExtent, maxExtent] = boundingBox; AZ::Entity entity; CreateCylinder(transform, radius, height, entity); AZ::Transform transformOut; AZ::Aabb aabb; LmbrCentral::ShapeComponentRequestsBus::Event(entity.GetId(), &LmbrCentral::ShapeComponentRequests::GetTransformAndLocalBounds, transformOut, aabb); EXPECT_TRUE(transformOut.IsClose(transform)); EXPECT_TRUE(aabb.GetMin().IsClose(minExtent)); EXPECT_TRUE(aabb.GetMax().IsClose(maxExtent)); } INSTANTIATE_TEST_CASE_P(TransformAndLocalBounds, CylinderShapeTransformAndLocalBoundsTest, ::testing::ValuesIn(CylinderShapeTransformAndLocalBoundsTest::ShouldPass) ); // point inside scaled TEST_P(CylinderShapeIsPointInsideTest, IsPointInside) { const auto& [cylinder, point, expectedInside] = GetParam(); const auto& [transform, radius, height] = cylinder; AZ::Entity entity; CreateCylinder(transform, radius, height, entity); bool inside; LmbrCentral::ShapeComponentRequestsBus::EventResult( inside, entity.GetId(), &LmbrCentral::ShapeComponentRequests::IsPointInside, point); EXPECT_EQ(inside, expectedInside); } INSTANTIATE_TEST_CASE_P(ValidIsPointInside, CylinderShapeIsPointInsideTest, ::testing::ValuesIn(CylinderShapeIsPointInsideTest::ShouldPass) ); INSTANTIATE_TEST_CASE_P(InvalidIsPointInside, CylinderShapeIsPointInsideTest, ::testing::ValuesIn(CylinderShapeIsPointInsideTest::ShouldFail) ); // distance scaled - along length TEST_P(CylinderShapeDistanceFromPointTest, DistanceFromPoint) { const auto& [cylinder, point, result] = GetParam(); const auto& [transform, radius, height] = cylinder; const auto& [expectedDistance, epsilon] = result; AZ::Entity entity; CreateCylinder(transform, radius, height, entity); float distance; LmbrCentral::ShapeComponentRequestsBus::EventResult( distance, entity.GetId(), &LmbrCentral::ShapeComponentRequests::DistanceFromPoint, point); EXPECT_NEAR(distance, expectedDistance, epsilon); } INSTANTIATE_TEST_CASE_P(ValidIsDistanceFromPoint, CylinderShapeDistanceFromPointTest, ::testing::ValuesIn(CylinderShapeDistanceFromPointTest::ShouldPass) ); }