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/PhysXSpecificTest.cpp

1614 lines
73 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 "PhysXTestFixtures.h"
#include "PhysXTestUtil.h"
#include <AzTest/AzTest.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/UnitTest/UnitTest.h>
#include <AZTestShared/Math/MathTestHelpers.h>
#include <AZTestShared/Utils/Utils.h>
#include <AzFramework/Physics/SystemBus.h>
#include <AzFramework/Physics/Collision/CollisionGroups.h>
#include <AzFramework/Physics/Collision/CollisionLayers.h>
#include <AzFramework/Physics/PhysicsSystem.h>
#include <AzFramework/Physics/Collision/CollisionEvents.h>
#include <AzFramework/Physics/Common/PhysicsSimulatedBody.h>
#include <AzFramework/Physics/Common/PhysicsTypes.h>
#include <AzFramework/Physics/Configuration/RigidBodyConfiguration.h>
#include <AzFramework/Physics/Configuration/StaticRigidBodyConfiguration.h>
#include <RigidBodyStatic.h>
#include <SphereColliderComponent.h>
#include <Utils.h>
#include <PhysX/MathConversion.h>
#include <PhysX/PhysXLocks.h>
#include <PhysX/SystemComponentBus.h>
#include <Tests/PhysXTestCommon.h>
namespace PhysX
{
class PhysXSpecificTest
: public PhysXDefaultWorldTest
, public UnitTest::TraceBusRedirector
{
protected:
float tolerance = 1e-3f;
};
namespace PhysXTests
{
typedef EntityPtr(* EntityFactoryFunc)(AzPhysics::SceneHandle, const AZ::Vector3&, const char*);
}
class PhysXEntityFactoryParamTest
: public PhysXSpecificTest
, public ::testing::WithParamInterface<PhysXTests::EntityFactoryFunc>
{
};
void SetCollisionLayerName(AZ::u8 index, const AZStd::string& name)
{
AZ::Interface<Physics::CollisionRequests>::Get()->SetCollisionLayerName(index, name);
}
void CreateCollisionGroup(const AzPhysics::CollisionGroup& group, const AZStd::string& name)
{
AZ::Interface<Physics::CollisionRequests>::Get()->CreateCollisionGroup(name, group);
}
void SanityCheckValidFrustumParams(const AZStd::vector<AZ::Vector3>& points, float validHeight, float validBottomRadius, float validTopRadius, AZ::u8 validSubdivisions)
{
double rad = 0;
const double step = AZ::Constants::TwoPi / aznumeric_cast<double>(validSubdivisions);
const float halfHeight = validHeight * 0.5f;
for (auto i = 0; i < points.size() / 2; i++)
{
// Canonical way to plot points on the circumference a cicle
// If any attempt to refactor/optimize the implemented algorithm fails, this test will fail
const float x = aznumeric_cast<float>(std::cos(rad));
const float y = aznumeric_cast<float>(std::sin(rad));
// Top face point is offset half the height along the positive z axis
{
const AZ::Vector3& p = points[i * 2];
EXPECT_FLOAT_EQ(p.GetX(), x * validTopRadius);
EXPECT_FLOAT_EQ(p.GetY(), y * validTopRadius);
EXPECT_FLOAT_EQ(p.GetZ(), +halfHeight);
}
// Bottom face point is offset half the height along the negative z axis
{
const AZ::Vector3& p = points[i * 2 + 1];
EXPECT_FLOAT_EQ(p.GetX(), x * validBottomRadius);
EXPECT_FLOAT_EQ(p.GetY(), y * validBottomRadius);
EXPECT_FLOAT_EQ(p.GetZ(), -halfHeight);
}
rad += step;
}
}
// Helper functions for calculating the volume
float GetShapeVolume(const Physics::BoxShapeConfiguration& box)
{
return box.m_dimensions.GetX() * box.m_dimensions.GetY() * box.m_dimensions.GetZ() *
box.m_scale.GetX() * box.m_scale.GetY() * box.m_scale.GetZ();
}
float GetShapeVolume(const Physics::SphereShapeConfiguration& sphere)
{
return 4.0f * AZ::Constants::Pi * sphere.m_radius * sphere.m_radius * sphere.m_radius / 3.0f;
}
TEST_F(PhysXSpecificTest, VectorConversion_ConvertToPxVec3_ConvertedVectorsCorrect)
{
AZ::Vector3 lyA(3.0f, -4.0f, 12.0f);
AZ::Vector3 lyB(-8.0f, 1.0f, -4.0f);
physx::PxVec3 pxA = PxMathConvert(lyA);
physx::PxVec3 pxB = PxMathConvert(lyB);
EXPECT_NEAR(pxA.magnitudeSquared(), 169.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxB.magnitudeSquared(), 81.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxA.dot(pxB), -76.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxA.cross(pxB).x, 4.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxA.cross(pxB).y, -84.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxA.cross(pxB).z, -29.0f, PhysXSpecificTest::tolerance);
}
TEST_F(PhysXSpecificTest, VectorConversion_ConvertToLyVec3_ConvertedVectorsCorrect)
{
physx::PxVec3 pxA(3.0f, -4.0f, 12.0f);
physx::PxVec3 pxB(-8.0f, 1.0f, -4.0f);
AZ::Vector3 lyA = PxMathConvert(pxA);
AZ::Vector3 lyB = PxMathConvert(pxB);
EXPECT_NEAR(lyA.GetLengthSq(), 169.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyB.GetLengthSq(), 81.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyA.Dot(lyB), -76.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyA.Cross(lyB).GetX(), 4.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyA.Cross(lyB).GetY(), -84.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyA.Cross(lyB).GetZ(), -29.0f, PhysXSpecificTest::tolerance);
}
TEST_F(PhysXSpecificTest, ExtendedVectorConversion_ConvertToPxExtendedVec3_ConvertedVectorsCorrect)
{
AZ::Vector3 lyA(3.0f, -4.0f, 12.0f);
AZ::Vector3 lyB(-8.0f, 1.0f, -4.0f);
physx::PxExtendedVec3 pxA = PxMathConvertExtended(lyA);
physx::PxExtendedVec3 pxB = PxMathConvertExtended(lyB);
EXPECT_NEAR(pxA.magnitudeSquared(), 169.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxB.magnitudeSquared(), 81.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxA.cross(pxB).x, 4.0, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxA.cross(pxB).y, -84.0, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxA.cross(pxB).z, -29.0, PhysXSpecificTest::tolerance);
}
TEST_F(PhysXSpecificTest, ExtendedVectorConversion_ConvertToLyVec3_ConvertedVectorsCorrect)
{
physx::PxExtendedVec3 pxA(3.0, -4.0, 12.0);
physx::PxExtendedVec3 pxB(-8.0, 1.0, -4.0);
AZ::Vector3 lyA = PxMathConvertExtended(pxA);
AZ::Vector3 lyB = PxMathConvertExtended(pxB);
EXPECT_NEAR(lyA.GetLengthSq(), 169.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyB.GetLengthSq(), 81.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyA.Dot(lyB), -76.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyA.Cross(lyB).GetX(), 4.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyA.Cross(lyB).GetY(), -84.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyA.Cross(lyB).GetZ(), -29.0f, PhysXSpecificTest::tolerance);
}
TEST_F(PhysXSpecificTest, QuaternionConversion_ConvertToPxQuat_ConvertedQuatsCorrect)
{
AZ::Quaternion lyQ = AZ::Quaternion(9.0f, -8.0f, -4.0f, 8.0f) / 15.0f;
physx::PxQuat pxQ = PxMathConvert(lyQ);
physx::PxVec3 pxV = pxQ.rotate(physx::PxVec3(-8.0f, 1.0f, -4.0f));
EXPECT_NEAR(pxQ.magnitudeSquared(), 1.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxQ.getImaginaryPart().magnitudeSquared(), 161.0f / 225.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxQ.w, 8.0f / 15.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxV.magnitudeSquared(), 81.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxV.x, 8.0f / 9.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxV.y, 403.0f / 45.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxV.z, 4.0f / 45.0f, PhysXSpecificTest::tolerance);
}
TEST_F(PhysXSpecificTest, QuaternionConversion_ConvertToLyQuat_ConvertedQuatsCorrect)
{
physx::PxQuat pxQ = physx::PxQuat(9.0f, -8.0f, -4.0f, 8.0f) * (1.0f / 15.0f);
AZ::Quaternion lyQ = PxMathConvert(pxQ);
AZ::Vector3 lyV = lyQ.TransformVector(AZ::Vector3(-8.0f, 1.0f, -4.0f));
EXPECT_NEAR(lyQ.GetLengthSq(), 1.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyQ.GetImaginary().GetLengthSq(), 161.0f / 225.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyQ.GetW(), 8.0f / 15.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyV.GetLengthSq(), 81.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyV.GetX(), 8.0f / 9.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyV.GetY(), 403.0f / 45.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyV.GetZ(), 4.0f / 45.0f, PhysXSpecificTest::tolerance);
}
TEST_F(PhysXSpecificTest, TransformConversion_ConvertToPxTransform_ConvertedTransformsCorrect)
{
// create an AZ::Transform and convert it to a pxTransform
const AZ::Vector3 eulerAngles(40.0f, 25.0f, 37.0f);
AZ::Transform lyTm;
lyTm.SetFromEulerDegrees(eulerAngles);
physx::PxTransform pxTm = PxMathConvert(lyTm);
// transform a vector with each transform
const float x = 0.8f;
const float y = -1.4f;
const float z = 0.3f;
AZ::Vector3 lyVec3 = lyTm.TransformPoint(AZ::Vector3(x, y, z));
physx::PxVec3 pxVec3 = pxTm.transform(physx::PxVec3(x, y, z));
// check the results are close for both transforms
EXPECT_NEAR(pxVec3.x, lyVec3.GetX(), PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxVec3.y, lyVec3.GetY(), PhysXSpecificTest::tolerance);
EXPECT_NEAR(pxVec3.z, lyVec3.GetZ(), PhysXSpecificTest::tolerance);
}
TEST_F(PhysXSpecificTest, TransformConversion_ConvertToLyTransform_ConvertedTransformsCorrect)
{
physx::PxTransform pxTm(physx::PxVec3(2.0f, 10.0f, 9.0f), physx::PxQuat(6.0f, -8.0f, -5.0f, 10.0f) * (1.0f / 15.0f));
AZ::Transform lyTm = PxMathConvert(pxTm);
AZ::Vector3 lyV = lyTm.TransformPoint(AZ::Vector3(4.0f, -12.0f, 3.0f));
EXPECT_NEAR(lyV.GetX(), -14.0f / 45.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyV.GetY(), 22.0f / 45.0f, PhysXSpecificTest::tolerance);
EXPECT_NEAR(lyV.GetZ(), 4.0f / 9.0f, PhysXSpecificTest::tolerance);
}
TEST_F(PhysXSpecificTest, RigidBody_GetNativeShape_ReturnsCorrectShape)
{
AZ::Vector3 halfExtents(1.0f, 2.0f, 3.0f);
Physics::BoxShapeConfiguration shapeConfig(halfExtents * 2.0f);
Physics::ColliderConfiguration colliderConfig;
colliderConfig.m_rotation = AZ::Quaternion::CreateRotationX(AZ::Constants::HalfPi);
AZStd::shared_ptr<Physics::Shape> shape = AZ::Interface<Physics::System>::Get()->CreateShape(colliderConfig, shapeConfig);
AzPhysics::RigidBodyConfiguration rigidBodyConfiguration;
rigidBodyConfiguration.m_colliderAndShapeData = shape;
AzPhysics::RigidBody* rigidBody = nullptr;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfiguration);
rigidBody = azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
ASSERT_TRUE(rigidBody != nullptr);
auto nativeShape = rigidBody->GetShape(0);
ASSERT_TRUE(nativeShape != nullptr);
{
auto* actor = static_cast<physx::PxRigidDynamic*>(rigidBody->GetNativePointer());
PHYSX_SCENE_READ_LOCK(actor->getScene());
auto pxShape = AZStd::rtti_pointer_cast<PhysX::Shape>(shape);
ASSERT_TRUE(pxShape->GetPxShape()->getGeometryType() == physx::PxGeometryType::eBOX);
physx::PxBoxGeometry boxGeometry;
pxShape->GetPxShape()->getBoxGeometry(boxGeometry);
EXPECT_NEAR(boxGeometry.halfExtents.x, halfExtents.GetX(), PhysXSpecificTest::tolerance);
EXPECT_NEAR(boxGeometry.halfExtents.y, halfExtents.GetY(), PhysXSpecificTest::tolerance);
EXPECT_NEAR(boxGeometry.halfExtents.z, halfExtents.GetZ(), PhysXSpecificTest::tolerance);
}
}
auto entityFactories = { TestUtils::AddUnitTestObject<BoxColliderComponent>, TestUtils::AddUnitTestBoxComponentsMix };
INSTANTIATE_TEST_CASE_P(DifferentBoxes, PhysXEntityFactoryParamTest, ::testing::ValuesIn(entityFactories));
TEST_F(PhysXSpecificTest, RigidBody_GetNativeType_ReturnsPhysXRigidBodyType)
{
AzPhysics::RigidBodyConfiguration rigidBodyConfiguration;
AzPhysics::RigidBody* rigidBody = nullptr;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfiguration);
rigidBody = azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
EXPECT_EQ(rigidBody->GetNativeType(), AZ::Crc32("PhysXRigidBody"));
}
TEST_F(PhysXSpecificTest, RigidBody_GetNativePointer_ReturnsValidPointer)
{
AzPhysics::RigidBodyConfiguration rigidBodyConfiguration;
AzPhysics::RigidBody* rigidBody = nullptr;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfiguration);
rigidBody = azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
physx::PxBase* nativePointer = static_cast<physx::PxBase*>(rigidBody->GetNativePointer());
EXPECT_TRUE(strcmp(nativePointer->getConcreteTypeName(), "PxRigidDynamic") == 0);
}
TEST_F(PhysXSpecificTest, TriggerArea_RigidBodyEnteringAndLeavingTrigger_EnterLeaveCallbackCalled)
{
// set up a trigger box
auto triggerBox = TestUtils::CreateTriggerAtPosition<BoxColliderComponent>(AZ::Vector3(0.0f, 0.0f, 12.0f));
auto* triggerBody = azdynamic_cast<PhysX::StaticRigidBody*>(triggerBox->FindComponent<PhysX::StaticRigidBodyComponent>()->GetSimulatedBody());
auto triggerShape = triggerBody->GetShape(0);
TestTriggerAreaNotificationListener testTriggerAreaNotificationListener(triggerBox->GetId());
// Create a test box above the trigger so when it falls down it'd enter and leave the trigger box
auto testBox = TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 16.0f), "TestBox");
auto testBoxBody = testBox->FindComponent<RigidBodyComponent>()->GetRigidBody();
auto testBoxShape = testBoxBody->GetShape(0);
// run the simulation for a while
TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 500);
const auto& enteredEvents = testTriggerAreaNotificationListener.GetEnteredEvents();
const auto& exitedEvents = testTriggerAreaNotificationListener.GetExitedEvents();
ASSERT_EQ(enteredEvents.size(), 1);
ASSERT_EQ(exitedEvents.size(), 1);
EXPECT_EQ(enteredEvents[0].m_triggerBody, triggerBody);
EXPECT_EQ(enteredEvents[0].m_triggerShape, triggerShape.get());
EXPECT_EQ(enteredEvents[0].m_otherBody, testBoxBody);
EXPECT_EQ(enteredEvents[0].m_otherShape, testBoxShape.get());
EXPECT_EQ(exitedEvents[0].m_triggerBody, triggerBody);
EXPECT_EQ(exitedEvents[0].m_triggerShape, triggerShape.get());
EXPECT_EQ(exitedEvents[0].m_otherBody, testBoxBody);
EXPECT_EQ(exitedEvents[0].m_otherShape, testBoxShape.get());
}
TEST_F(PhysXSpecificTest, TriggerArea_RigidBodiesEnteringAndLeavingTriggers_EnterLeaveCallbackCalled)
{
// set up triggers
AZStd::vector<EntityPtr> triggers =
{
TestUtils::CreateTriggerAtPosition<BoxColliderComponent>(AZ::Vector3(0.0f, 0.0f, 12.0f)),
TestUtils::CreateTriggerAtPosition<SphereColliderComponent>(AZ::Vector3(0.0f, 0.0f, 8.0f))
};
// set up dynamic objs
AZStd::vector<EntityPtr> testBoxes =
{
TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 16.0f), "TestBox"),
TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 18.0f), "TestBox2")
};
// set up listeners on triggers
TestTriggerAreaNotificationListener testTriggerBoxNotificationListener(triggers[0]->GetId());
TestTriggerAreaNotificationListener testTriggerSphereNotificationListener(triggers[1]->GetId());
// run the simulation for a while
TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 500);
for (const auto& triggerListener : {&testTriggerBoxNotificationListener, &testTriggerSphereNotificationListener})
{
const auto& enteredEvents = triggerListener->GetEnteredEvents();
ASSERT_EQ(2, enteredEvents.size());
EXPECT_EQ(enteredEvents[0].m_otherBody, testBoxes[0]->FindComponent<RigidBodyComponent>()->GetRigidBody());
EXPECT_EQ(enteredEvents[0].m_otherShape, testBoxes[0]->FindComponent<RigidBodyComponent>()->GetRigidBody()->GetShape(0).get());
EXPECT_EQ(enteredEvents[1].m_otherBody, testBoxes[1]->FindComponent<RigidBodyComponent>()->GetRigidBody());
EXPECT_EQ(enteredEvents[1].m_otherShape, testBoxes[1]->FindComponent<RigidBodyComponent>()->GetRigidBody()->GetShape(0).get());
const auto& exitedEvents = triggerListener->GetExitedEvents();
ASSERT_EQ(2, enteredEvents.size());
EXPECT_EQ(exitedEvents[0].m_otherBody, testBoxes[0]->FindComponent<RigidBodyComponent>()->GetRigidBody());
EXPECT_EQ(exitedEvents[0].m_otherShape, testBoxes[0]->FindComponent<RigidBodyComponent>()->GetRigidBody()->GetShape(0).get());
EXPECT_EQ(exitedEvents[1].m_otherBody, testBoxes[1]->FindComponent<RigidBodyComponent>()->GetRigidBody());
EXPECT_EQ(exitedEvents[1].m_otherShape, testBoxes[1]->FindComponent<RigidBodyComponent>()->GetRigidBody()->GetShape(0).get());
}
}
TEST_F(PhysXSpecificTest, RigidBody_CollisionCallback_SimpleCallbackOfTwoSpheres)
{
auto obj01 = TestUtils::AddUnitTestObject<SphereColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 10.0f), "TestSphere01");
auto obj02 = TestUtils::AddUnitTestObject<SphereColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 0.0f), "TestSphere01");
auto body01 = obj01->FindComponent<RigidBodyComponent>()->GetRigidBody();
auto body02 = obj02->FindComponent<RigidBodyComponent>()->GetRigidBody();
auto shape01 = body01->GetShape(0).get();
auto shape02 = body02->GetShape(0).get();
CollisionCallbacksListener listener01(obj01->GetId());
CollisionCallbacksListener listener02(obj02->GetId());
Physics::RigidBodyRequestBus::Event(obj02->GetId(), &Physics::RigidBodyRequestBus::Events::ApplyLinearImpulse, AZ::Vector3(0.0f, 0.0f, 50.0f));
// run the simulation for a while
TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 500);
// We expect to have two (CollisionBegin and CollisionEnd) events for both objects
ASSERT_EQ(listener01.m_beginCollisions.size(), 1);
ASSERT_EQ(listener01.m_endCollisions.size(), 1);
ASSERT_EQ(listener02.m_beginCollisions.size(), 1);
ASSERT_EQ(listener02.m_endCollisions.size(), 1);
// First collision recorded is CollisionBegin event
auto collisionBegin01 = listener01.m_beginCollisions[0];
EXPECT_EQ(collisionBegin01.m_body2->GetEntityId(), obj02->GetId());
EXPECT_EQ(collisionBegin01.m_body2, body02);
EXPECT_EQ(collisionBegin01.m_shape2, shape02);
// Checkes one of the collision point details
ASSERT_EQ(collisionBegin01.m_contacts.size(), 1);
EXPECT_NEAR(collisionBegin01.m_contacts[0].m_impulse.GetZ(), -37.12f, 0.01f);
float dotNormal = collisionBegin01.m_contacts[0].m_normal.Dot(AZ::Vector3(0.0f, 0.0f, -1.0f));
EXPECT_NEAR(dotNormal, 1.0f, 0.01f);
EXPECT_NEAR(collisionBegin01.m_contacts[0].m_separation, -0.12, 0.01f);
// Second collision recorded is CollisionExit event
auto collisionEnd01 = listener01.m_endCollisions[0];
EXPECT_EQ(collisionEnd01.m_body2->GetEntityId(), obj02->GetId());
EXPECT_EQ(collisionEnd01.m_body2, body02);
EXPECT_EQ(collisionEnd01.m_shape2, shape02);
// Some checks for the second sphere
auto collisionBegin02 = listener02.m_beginCollisions[0];
EXPECT_EQ(collisionBegin02.m_body2->GetEntityId(), obj01->GetId());
EXPECT_EQ(collisionBegin02.m_body2, body01);
EXPECT_EQ(collisionBegin02.m_shape2, shape01);
auto collisionEnd02 = listener02.m_endCollisions[0];
EXPECT_EQ(collisionEnd02.m_body2->GetEntityId(), obj01->GetId());
EXPECT_EQ(collisionEnd02.m_body2, body01);
EXPECT_EQ(collisionEnd02.m_shape2, shape01);
}
TEST_F(PhysXSpecificTest, RigidBody_CollisionCallback_SimpleCallbackSphereFallingOnStaticBox)
{
auto obj01 = TestUtils::AddUnitTestObject<SphereColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 10.0f), "TestSphere01");
auto obj02 = TestUtils::AddStaticUnitTestObject<BoxColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 0.0f), "TestBox01");
auto body01 = obj01->FindComponent<RigidBodyComponent>()->GetRigidBody();
auto* body02 = azdynamic_cast<PhysX::StaticRigidBody*>(obj02->FindComponent<PhysX::StaticRigidBodyComponent>()->GetSimulatedBody());
auto shape01 = body01->GetShape(0).get();
auto shape02 = body02->GetShape(0).get();
CollisionCallbacksListener listener01(obj01->GetId());
CollisionCallbacksListener listener02(obj02->GetId());
// run the simulation for a while
TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 500);
// Ball should bounce at least 2 times, generating CollisionBegin and CollisionEnd events
ASSERT_GE(listener01.m_beginCollisions.size(), 2);
ASSERT_GE(listener01.m_endCollisions.size(), 2);
ASSERT_GE(listener02.m_beginCollisions.size(), 2);
ASSERT_GE(listener02.m_endCollisions.size(), 2);
EXPECT_EQ(listener01.m_beginCollisions[0].m_body2->GetEntityId(), obj02->GetId());
EXPECT_EQ(listener01.m_beginCollisions[0].m_body2, body02);
EXPECT_EQ(listener01.m_beginCollisions[0].m_shape2, shape02);
EXPECT_EQ(listener02.m_beginCollisions[0].m_body2->GetEntityId(), obj01->GetId());
EXPECT_EQ(listener02.m_beginCollisions[0].m_body2, body01);
EXPECT_EQ(listener02.m_beginCollisions[0].m_shape2, shape01);
}
TEST_F(PhysXSpecificTest, CollisionFiltering_CollisionLayers_CombineLayersIntoGroup)
{
// Start with empty group
AzPhysics::CollisionGroup group = AzPhysics::CollisionGroup::None;
AzPhysics::CollisionLayer layer1(1);
AzPhysics::CollisionLayer layer2(2);
// Check nothing is set
EXPECT_FALSE(group.IsSet(layer1));
EXPECT_FALSE(group.IsSet(layer2));
// Combine layers into group
group = layer1 | layer2;
// Check they are set
EXPECT_TRUE(group.IsSet(layer1));
EXPECT_TRUE(group.IsSet(layer2));
}
TEST_F(PhysXSpecificTest, CollisionFiltering_CollisionLayers_ConstructLayerByName)
{
// Set layer names
SetCollisionLayerName(1, "Layer1");
SetCollisionLayerName(2, "Layer2");
SetCollisionLayerName(3, "Layer3");
// Lookup layers by name
AzPhysics::CollisionLayer layer1("Layer1");
AzPhysics::CollisionLayer layer2("Layer2");
AzPhysics::CollisionLayer layer3("Layer3");
// Check they match what was set before
EXPECT_EQ(1, layer1.GetIndex());
EXPECT_EQ(2, layer2.GetIndex());
EXPECT_EQ(3, layer3.GetIndex());
}
TEST_F(PhysXSpecificTest, CollisionFiltering_CollisionGroups_AppendLayerToGroup)
{
// Start with empty group
AzPhysics::CollisionGroup group = AzPhysics::CollisionGroup::None;
AzPhysics::CollisionLayer layer1(1);
EXPECT_FALSE(group.IsSet(layer1));
// Append layer to group
group = group | layer1;
// Check its set
EXPECT_TRUE(group.IsSet(layer1));
}
TEST_F(PhysXSpecificTest, CollisionFiltering_CollisionGroups_ConstructGroupByName)
{
// Create a collision group preset from layers
CreateCollisionGroup(AzPhysics::CollisionLayer(5) | AzPhysics::CollisionLayer(13), "TestGroup");
// Lookup the group by name
AzPhysics::CollisionGroup group("TestGroup");
// Check it looks correct
EXPECT_TRUE(group.IsSet(AzPhysics::CollisionLayer(5)));
EXPECT_TRUE(group.IsSet(AzPhysics::CollisionLayer(13)));
}
TEST_F(PhysXSpecificTest, RigidBody_CenterOfMassOffsetComputed)
{
AZ::Vector3 halfExtents(1.0f, 2.0f, 3.0f);
auto shapeConfig = AZStd::make_shared<Physics::BoxShapeConfiguration>(halfExtents * 2.0f);
auto colliderConfig = AZStd::make_shared<Physics::ColliderConfiguration>();
colliderConfig->m_rotation = AZ::Quaternion::CreateRotationX(AZ::Constants::HalfPi);
AzPhysics::RigidBodyConfiguration rigidBodyConfiguration;
rigidBodyConfiguration.m_computeCenterOfMass = true;
rigidBodyConfiguration.m_computeInertiaTensor = true;
rigidBodyConfiguration.m_colliderAndShapeData = AzPhysics::ShapeColliderPair(colliderConfig, shapeConfig);
AzPhysics::RigidBody* rigidBody = nullptr;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfiguration);
rigidBody = azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
ASSERT_TRUE(rigidBody != nullptr);
auto com = rigidBody->GetCenterOfMassLocal();
EXPECT_TRUE(com.IsClose(AZ::Vector3::CreateZero(), PhysXSpecificTest::tolerance));
}
TEST_F(PhysXSpecificTest, RigidBody_CenterOfMassOffsetSpecified)
{
AZ::Vector3 halfExtents(1.0f, 2.0f, 3.0f);
auto shapeConfig = AZStd::make_shared<Physics::BoxShapeConfiguration>(halfExtents * 2.0f);
auto colliderConfig = AZStd::make_shared<Physics::ColliderConfiguration>();
colliderConfig->m_rotation = AZ::Quaternion::CreateRotationX(AZ::Constants::HalfPi);
AzPhysics::RigidBodyConfiguration rigidBodyConfiguration;
rigidBodyConfiguration.m_computeCenterOfMass = false;
rigidBodyConfiguration.m_centerOfMassOffset = AZ::Vector3::CreateOne();
rigidBodyConfiguration.m_computeInertiaTensor = true;
rigidBodyConfiguration.m_colliderAndShapeData = AzPhysics::ShapeColliderPair(colliderConfig, shapeConfig);
AzPhysics::RigidBody* rigidBody = nullptr;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfiguration);
rigidBody = azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
ASSERT_TRUE(rigidBody != nullptr);
auto com = rigidBody->GetCenterOfMassLocal();
EXPECT_TRUE(com.IsClose(AZ::Vector3::CreateOne(), PhysXSpecificTest::tolerance));
}
TEST_F(PhysXSpecificTest, TriggerArea_BodyDestroyedInsideTrigger_OnTriggerExitEventRaised)
{
// set up a trigger box
auto triggerBox = TestUtils::CreateTriggerAtPosition<BoxColliderComponent>(AZ::Vector3(0.0f, 0.0f, 0.0f));
auto* triggerBody = azdynamic_cast<PhysX::StaticRigidBody*>(triggerBox->FindComponent<PhysX::StaticRigidBodyComponent>()->GetSimulatedBody());
// Create a test box above the trigger so when it falls down it'd enter and leave the trigger box
auto testBox = TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 1.5f), "TestBox");
auto testBoxBody = testBox->FindComponent<RigidBodyComponent>()->GetRigidBody();
// Listen for trigger events on the box
TestTriggerAreaNotificationListener testTriggerAreaNotificationListener(triggerBox->GetId());
// run the simulation for a while
const auto& enteredEvents = testTriggerAreaNotificationListener.GetEnteredEvents();
const auto& exitedEvents = testTriggerAreaNotificationListener.GetExitedEvents();
for (int timeStep = 0; timeStep < 100; timeStep++)
{
m_defaultScene->StartSimulation(AzPhysics::SystemConfiguration::DefaultFixedTimestep);
m_defaultScene->FinishSimulation();
// Body entered the trigger area, kill it!!!
if (enteredEvents.size() > 0 && testBox != nullptr)
{
testBox.reset();
}
}
ASSERT_EQ(testBox, nullptr);
ASSERT_EQ(enteredEvents.size(), 1);
ASSERT_EQ(exitedEvents.size(), 1);
EXPECT_EQ(enteredEvents[0].m_triggerBody, triggerBody);
EXPECT_EQ(enteredEvents[0].m_otherBody, testBoxBody);
EXPECT_EQ(exitedEvents[0].m_triggerBody, triggerBody);
EXPECT_EQ(exitedEvents[0].m_otherBody, testBoxBody);
}
TEST_F(PhysXSpecificTest, TriggerArea_StaticBodyDestroyedInsideDynamicTrigger_OnTriggerExitEventRaised)
{
// Set up a static non trigger box
auto staticBox = TestUtils::AddStaticUnitTestObject<BoxColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 0.0f));
auto* staticBody = azdynamic_cast<PhysX::StaticRigidBody*>(staticBox->FindComponent<PhysX::StaticRigidBodyComponent>()->GetSimulatedBody());
// Create a test trigger box above the static box so when it falls down it'd enter and leave the trigger box
auto dynamicTrigger = TestUtils::CreateDynamicTriggerAtPosition<BoxColliderComponent>(AZ::Vector3(0.0f, 0.0f, 5.0f));
auto dynamicBody = dynamicTrigger->FindComponent<RigidBodyComponent>()->GetRigidBody();
// Listen for trigger events on the box
TestTriggerAreaNotificationListener testTriggerAreaNotificationListener(dynamicTrigger->GetId());
// run the simulation for a while
const auto& enteredEvents = testTriggerAreaNotificationListener.GetEnteredEvents();
const auto& exitedEvents = testTriggerAreaNotificationListener.GetExitedEvents();
for (int timeStep = 0; timeStep < 100; timeStep++)
{
m_defaultScene->StartSimulation(AzPhysics::SystemConfiguration::DefaultFixedTimestep);
m_defaultScene->FinishSimulation();
// Body entered the trigger area, kill it!!!
if (enteredEvents.size() > 0 && staticBox != nullptr)
{
staticBox.reset();
}
}
ASSERT_EQ(staticBox, nullptr);
ASSERT_EQ(enteredEvents.size(), 1);
ASSERT_EQ(exitedEvents.size(), 1);
EXPECT_EQ(enteredEvents[0].m_triggerBody, dynamicBody);
EXPECT_EQ(enteredEvents[0].m_otherBody, staticBody);
EXPECT_EQ(exitedEvents[0].m_triggerBody, dynamicBody);
EXPECT_EQ(exitedEvents[0].m_otherBody, staticBody);
}
TEST_F(PhysXSpecificTest, TriggerArea_BodyDestroyedOnTriggerEnter_DoesNotCrash)
{
// Given a rigid body falling into a trigger.
auto triggerBox = TestUtils::CreateTriggerAtPosition<BoxColliderComponent>(AZ::Vector3(0.0f, 0.0f, 0.0f));
auto testBox = TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 1.2f), "TestBox");
// When the rigid body is deleted inside on trigger enter event.
TestTriggerAreaNotificationListener testTriggerAreaNotificationListener(triggerBox->GetId());
testTriggerAreaNotificationListener.m_onTriggerEnter = [&]([[maybe_unused]] const AzPhysics::TriggerEvent& triggerEvent)
{
testBox.reset();
};
// Update the world. This should not crash.
TestUtils::UpdateScene(m_defaultScene, 1.0f / 30.0f, 30);
/// Then the program does not crash (If you made it this far the test passed).
ASSERT_TRUE(true);
}
TEST_F(PhysXSpecificTest, TriggerArea_BodyDestroyedOnTriggerExit_DoesNotCrash)
{
// Given a rigid body falling into a trigger.
auto triggerBox = TestUtils::CreateTriggerAtPosition<BoxColliderComponent>(AZ::Vector3(0.0f, 0.0f, 0.0f));
auto testBox = TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 1.2f), "TestBox");
// When the rigid body is deleted inside on trigger enter event.
TestTriggerAreaNotificationListener testTriggerAreaNotificationListener(triggerBox->GetId());
testTriggerAreaNotificationListener.m_onTriggerExit = [&]([[maybe_unused]] const AzPhysics::TriggerEvent& triggerEvent)
{
testBox.reset();
};
// Update the world. This should not crash.
TestUtils::UpdateScene(m_defaultScene, 1.0f / 30.0f, 30);
/// Then the program does not crash (If you made it this far the test passed).
ASSERT_TRUE(true);
}
TEST_F(PhysXSpecificTest, CollisionEvents_BodyDestroyedOnCollisionBegin_DoesNotCrash)
{
// Given a rigid body falling onto a static box.
auto staticBox = TestUtils::AddStaticUnitTestObject<BoxColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 0.0f), "StaticTestBox");
auto testBox = TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 1.2f), "TestBox");
// When the rigid body is deleted inside on collision begin event.
CollisionCallbacksListener collisionListener(testBox->GetId());
collisionListener.m_onCollisionBegin = [&]([[maybe_unused]] const AzPhysics::CollisionEvent& collisionEvent)
{
testBox.reset();
};
// Update the world. This should not crash.
TestUtils::UpdateScene(m_defaultScene, 1.0f / 30.0f, 30);
/// Then the program does not crash (If you made it this far the test passed).
ASSERT_TRUE(true);
}
TEST_F(PhysXSpecificTest, CollisionEvents_BodyDestroyedOnCollisionPersist_DoesNotCrash)
{
// Given a rigid body falling onto a static box.
auto staticBox = TestUtils::AddStaticUnitTestObject<BoxColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 0.0f), "StaticTestBox");
auto testBox = TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 1.2f), "TestBox");
// When the rigid body is deleted inside on collision begin event.
CollisionCallbacksListener collisionListener(testBox->GetId());
collisionListener.m_onCollisionPersist = [&]([[maybe_unused]] const AzPhysics::CollisionEvent& collisionEvent)
{
testBox.reset();
};
// Update the world. This should not crash.
TestUtils::UpdateScene(m_defaultScene, 1.0f / 30.0f, 30);
/// Then the program does not crash (If you made it this far the test passed).
ASSERT_TRUE(true);
}
TEST_F(PhysXSpecificTest, CollisionEvents_BodyDestroyedOnCollisionEnd_DoesNotCrash)
{
// Given a rigid body falling onto a static box.
auto staticBox = TestUtils::AddStaticUnitTestObject<BoxColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 0.0f), "StaticTestBox");
auto testBox = TestUtils::AddUnitTestObject(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 1.2f), "TestBox");
// When the rigid body is deleted inside on collision begin event.
CollisionCallbacksListener collisionListener(testBox->GetId());
collisionListener.m_onCollisionEnd = [&]([[maybe_unused]] const AzPhysics::CollisionEvent& collisionEvent)
{
testBox.reset();
};
// Update the world. This should not crash.
TestUtils::UpdateScene(m_defaultScene, 1.0f / 30.0f, 30);
/// Then the program does not crash (If you made it this far the test passed).
ASSERT_TRUE(true);
}
TEST_F(PhysXSpecificTest, RigidBody_ConvexRigidBodyCreatedFromCookedMesh_CachedMeshObjectCreated)
{
// Create rigid body
AzPhysics::RigidBodyConfiguration rigidBodyConfiguration;
AzPhysics::RigidBody* rigidBody = nullptr;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfiguration);
rigidBody = azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
ASSERT_TRUE(rigidBody != nullptr);
// Generate input data
const PointList testPoints = TestUtils::GeneratePyramidPoints(1.0f);
AZStd::vector<AZ::u8> cookedData;
bool cookingResult = false;
Physics::SystemRequestBus::BroadcastResult(cookingResult, &Physics::SystemRequests::CookConvexMeshToMemory,
testPoints.data(), static_cast<AZ::u32>(testPoints.size()), cookedData);
EXPECT_TRUE(cookingResult);
// Setup shape & collider configurations
Physics::CookedMeshShapeConfiguration shapeConfig;
shapeConfig.SetCookedMeshData(cookedData.data(), cookedData.size(),
Physics::CookedMeshShapeConfiguration::MeshType::Convex);
Physics::ColliderConfiguration colliderConfig;
// Create the first shape
AZStd::shared_ptr<Physics::Shape> firstShape = AZ::Interface<Physics::System>::Get()->CreateShape(colliderConfig, shapeConfig);
ASSERT_TRUE(firstShape != nullptr);
rigidBody->AddShape(firstShape);
// Validate the cached mesh is there
EXPECT_NE(shapeConfig.GetCachedNativeMesh(), nullptr);
// Make some changes in the configuration for the second shape
colliderConfig.m_position.SetX(1.0f);
shapeConfig.m_scale = AZ::Vector3(2.0f, 2.0f, 2.0f);
// Create the second shape
AZStd::shared_ptr<Physics::Shape> secondShape = AZ::Interface<Physics::System>::Get()->CreateShape(colliderConfig, shapeConfig);
ASSERT_TRUE(secondShape != nullptr);
rigidBody->AddShape(secondShape);
AZ::Vector3 initialPosition = rigidBody->GetPosition();
// Tick the world
TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 20);
// Verify the actor has moved
EXPECT_NE(rigidBody->GetPosition(), initialPosition);
}
TEST_F(PhysXSpecificTest, RigidBody_TriangleMeshRigidBodyCreatedFromCookedMesh_CachedMeshObjectCreated)
{
// Generate input data
VertexIndexData cubeMeshData = TestUtils::GenerateCubeMeshData(3.0f);
AZStd::vector<AZ::u8> cookedData;
bool cookingResult = false;
Physics::SystemRequestBus::BroadcastResult(cookingResult, &Physics::SystemRequests::CookTriangleMeshToMemory,
cubeMeshData.first.data(), static_cast<AZ::u32>(cubeMeshData.first.size()),
cubeMeshData.second.data(), static_cast<AZ::u32>(cubeMeshData.second.size()),
cookedData);
EXPECT_TRUE(cookingResult);
// Setup shape & collider configurations
Physics::CookedMeshShapeConfiguration shapeConfig;
shapeConfig.SetCookedMeshData(cookedData.data(), cookedData.size(),
Physics::CookedMeshShapeConfiguration::MeshType::TriangleMesh);
Physics::ColliderConfiguration colliderConfig;
// Create the first shape
AZStd::shared_ptr<Physics::Shape> firstShape = AZ::Interface<Physics::System>::Get()->CreateShape(colliderConfig, shapeConfig);
AZ_Assert(firstShape != nullptr, "Failed to create a shape from cooked data");
// Create static rigid body
AzPhysics::StaticRigidBodyConfiguration staticBodyConfiguration;
staticBodyConfiguration.m_colliderAndShapeData = firstShape;
AzPhysics::StaticRigidBody* rigidBody = nullptr;
AzPhysics::SimulatedBodyHandle rigidBodyHandle = AzPhysics::InvalidSimulatedBodyHandle;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
rigidBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &staticBodyConfiguration);
rigidBody = azdynamic_cast<AzPhysics::StaticRigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, rigidBodyHandle));
}
// Validate the cached mesh is there
EXPECT_NE(shapeConfig.GetCachedNativeMesh(), nullptr);
// Make some changes in the configuration for the second shape
colliderConfig.m_position.SetX(4.0f);
shapeConfig.m_scale = AZ::Vector3(2.0f, 2.0f, 2.0f);
// Create the second shape
AZStd::shared_ptr<Physics::Shape> secondShape = AZ::Interface<Physics::System>::Get()->CreateShape(colliderConfig, shapeConfig);
AZ_Assert(secondShape != nullptr, "Failed to create a shape from cooked data");
rigidBody->AddShape(secondShape);
// Drop a sphere
auto sphereActor = TestUtils::AddUnitTestObject<SphereColliderComponent>(m_testSceneHandle, AZ::Vector3(0.0f, 0.0f, 8.0f), "TestSphere01");
AzPhysics::RigidBody* sphereRigidBody = sphereActor->FindComponent<RigidBodyComponent>()->GetRigidBody();
// Tick the world
TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 120);
// Verify the sphere is lying on top of the mesh
AZ::Vector3 spherePosition = sphereRigidBody->GetPosition();
EXPECT_NEAR(spherePosition.GetZ(), 6.5f, 0.01f);
// Clean up
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
sceneInterface->RemoveSimulatedBody(m_testSceneHandle, rigidBodyHandle);
}
rigidBody = nullptr;
}
TEST_F(PhysXSpecificTest, Shape_ConstructorDestructor_PxShapeReferenceCounterIsCorrect)
{
// Create physx::PxShape object
AzPhysics::CollisionGroup assignedCollisionGroup = AzPhysics::CollisionGroup::None;
physx::PxShape* shape = Utils::CreatePxShapeFromConfig(
Physics::ColliderConfiguration(), Physics::BoxShapeConfiguration(), assignedCollisionGroup);
// physx::PxShape object ref count is expected to be 1 after creation
EXPECT_EQ(shape->getReferenceCount(), 1);
// Create PhysX::Shape wrapper object and verify physx::PxShape ref count is increased to 2
AZStd::unique_ptr<Shape> shapeWrapper = AZStd::make_unique<Shape>(shape);
EXPECT_EQ(shape->getReferenceCount(), 2);
// Destroy PhysX::Shape wrapper object and verify physx::PxShape ref count is back to 1
shapeWrapper = nullptr;
EXPECT_EQ(shape->getReferenceCount(), 1);
// Clean up
shape->release();
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_CreateWithInvalidHeight_ReturnsEmpty)
{
// Given a frustum with an invalid height
float invalidHeight = 0.0f;
float validBottomRadius = 1.0f;
float validTopRadius = 1.0f;
AZ::u8 validSubdivisions = Utils::MinFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(invalidHeight, validBottomRadius, validTopRadius, validSubdivisions);
// The frustum creation will be unsuccessful
EXPECT_FALSE(points.has_value());
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_CreateWithInvalidBottomRadius_ReturnsEmpty)
{
// Given a frustum with an invalid bottom radius
float validHeight = 1.0f;
float invalidBottomRadius = -1.0f;
float validTopRadius = 1.0f;
AZ::u8 validSubdivisions = Utils::MinFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, invalidBottomRadius, validTopRadius, validSubdivisions);
// Expect the frustum creation to be unsuccessful
EXPECT_FALSE(points.has_value());
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_CreateFromInvalidTopRadius_ReturnsEmpty)
{
// Given a frustum with an invalid top radius
float validHeight = 1.0f;
float validBottomRadius = 1.0f;
float invalidTopRadius = -1.0f;
AZ::u8 validSubdivisions = Utils::MinFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, invalidTopRadius, validSubdivisions);
// Expect the frustum creation to be unsuccessful
EXPECT_FALSE(points.has_value());
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_CreateFromInvalidBottomAndTopRadius_ReturnsEmpty)
{
// Given a frustum with an invalid bottom and top radius
float validHeight = 1.0f;
float invalidBottomRadius = 0.0f;
float invalidTopRadius = 0.0f;
AZ::u8 validSubdivisions = Utils::MinFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, invalidBottomRadius, invalidTopRadius, validSubdivisions);
// Expect the frustum creation to be unsuccessful
EXPECT_FALSE(points.has_value());
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_CreateFromInvalidMinSubdivisions_ReturnsEmpty)
{
// Given a frustum with an invalid minimum subdivisions
float validHeight = 1.0f;
float validBottomRadius = 1.0f;
float validTopRadius = 1.0f;
AZ::u8 invalidMinSubdivisions = Utils::MinFrustumSubdivisions - 1;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, validTopRadius, invalidMinSubdivisions);
// Expect the frustum creation to be unsuccessful
EXPECT_FALSE(points.has_value());
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_CreateFromInvalidMaxSubdivisions_ReturnsEmpty)
{
// Given a frustum with an invalid maximum subdivisions
float validHeight = 1.0f;
float validBottomRadius = 1.0f;
float validTopRadius = 1.0f;
AZ::u8 invalidMaxSubdivisions = Utils::MaxFrustumSubdivisions + 1;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, validTopRadius, invalidMaxSubdivisions);
// Expect the frustum creation to be unsuccessful
EXPECT_FALSE(points.has_value());
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_Create3SidedFrustum_ReturnsPoints)
{
// Given a valid unit frustum with MinSubdivisions subdivisions
float validHeight = 1.0f;
float validBottomRadius = 1.0f;
float validTopRadius = 1.0f;
AZ::u8 validSubdivisions = Utils::MinFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, validTopRadius, validSubdivisions);
// Expect the frustum creation to be successful
EXPECT_TRUE(points.has_value());
// Expect each generated point to be equal to the canonical frustum plotting algorithm
SanityCheckValidFrustumParams(points.value(), validHeight, validBottomRadius, validTopRadius, validSubdivisions);
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_Create3SidedBottomCone_ReturnsPoints)
{
// Given a valid unit frustum with MinSubdivisions subdivisions
float validHeight = 1.0f;
float validBottomRadius = 0.0f;
float validTopRadius = 1.0f;
AZ::u8 validSubdivisions = Utils::MinFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, validTopRadius, validSubdivisions);
// Expect the frustum creation to be successful
EXPECT_TRUE(points.has_value());
// Expect each generated point to be equal to the canonical frustum plotting algorithm
SanityCheckValidFrustumParams(points.value(), validHeight, validBottomRadius, validTopRadius, validSubdivisions);
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_Create3SidedTopCone_ReturnsPoints)
{
// Given a valid unit frustum with MinSubdivisions subdivisions
float validHeight = 1.0f;
float validBottomRadius = 1.0f;
float validTopRadius = 0.0f;
AZ::u8 validSubdivisions = Utils::MinFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, validTopRadius, validSubdivisions);
// Expect the frustum creation to be successful
EXPECT_TRUE(points.has_value());
// Expect each generated point to be equal to the canonical frustum plotting algorithm
SanityCheckValidFrustumParams(points.value(), validHeight, validBottomRadius, validTopRadius, validSubdivisions);
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_Create125SidedFrustum_ReturnsPoints)
{
// Given a valid unit frustum with MaxSubdivisions subdivisions
float validHeight = 1.0f;
float validBottomRadius = 1.0f;
float validTopRadius = 1.0f;
AZ::u8 validSubdivisions = Utils::MaxFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, validTopRadius, validSubdivisions);
// Expect the frustum creation to be successful
EXPECT_TRUE(points.has_value());
// Expect each generated point to be equal to the canonical frustum plotting algorithm
SanityCheckValidFrustumParams(points.value(), validHeight, validBottomRadius, validTopRadius, validSubdivisions);
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_Create125SidedBottomCone_ReturnsPoints)
{
// Given a valid unit frustum with MaxSubdivisions subdivisions
float validHeight = 1.0f;
float validBottomRadius = 0.0f;
float validTopRadius = 1.0f;
AZ::u8 validSubdivisions = Utils::MaxFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, validTopRadius, validSubdivisions);
// Expect the frustum creation to be successful
EXPECT_TRUE(points.has_value());
// Expect each generated point to be equal to the canonical frustum plotting algorithm
SanityCheckValidFrustumParams(points.value(), validHeight, validBottomRadius, validTopRadius, validSubdivisions);
}
TEST_F(PhysXSpecificTest, FrustumCreatePoints_Create125SidedTopCone_ReturnsPoints)
{
// Given a valid unit frustum with MaxSubdivisions subdivisions
float validHeight = 1.0f;
float validBottomRadius = 1.0f;
float validTopRadius = 0.0f;
AZ::u8 validSubdivisions = Utils::MaxFrustumSubdivisions;
// Attempt to create a frustum point list from the given parameters
auto points = Utils::CreatePointsAtFrustumExtents(validHeight, validBottomRadius, validTopRadius, validSubdivisions);
// Expect the frustum creation to be successful
EXPECT_TRUE(points.has_value());
// Expect each generated point to be equal to the canonical frustum plotting algorithm
SanityCheckValidFrustumParams(points.value(), validHeight, validBottomRadius, validTopRadius, validSubdivisions);
}
TEST_F(PhysXSpecificTest, RigidBody_RigidBodyWithAxisLockFlagsCreated_InternalPhysXFlagsSetAccordingly)
{
// Helper function wrapping creation logic
auto CreateRigidBody = [this](bool linearX, bool linearY, bool linearZ, bool angularX, bool angularY, bool angularZ) -> AzPhysics::RigidBody*
{
AzPhysics::RigidBodyConfiguration rigidBodyConfig;
rigidBodyConfig.m_lockLinearX = linearX;
rigidBodyConfig.m_lockLinearY = linearY;
rigidBodyConfig.m_lockLinearZ = linearZ;
rigidBodyConfig.m_lockAngularX = angularX;
rigidBodyConfig.m_lockAngularY = angularY;
rigidBodyConfig.m_lockAngularZ = angularZ;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfig);
return azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
return nullptr;
};
auto RemoveRigidBody = [](AzPhysics::RigidBody*& rigidBody)
{
auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (rigidBody && sceneInterface)
{
sceneInterface->RemoveSimulatedBody(rigidBody->m_sceneOwner, rigidBody->m_bodyHandle);
}
rigidBody = nullptr;
};
auto TestLockFlags = [&CreateRigidBody, &RemoveRigidBody](bool linearX, bool linearY, bool linearZ,
bool angularX, bool angularY, bool angularZ,
physx::PxRigidDynamicLockFlags expectedFlags)
{
auto* rigidBody = CreateRigidBody(linearX, linearY, linearZ, angularX, angularY, angularZ);
ASSERT_TRUE(rigidBody != nullptr);
physx::PxRigidDynamic* pxRigidBody = static_cast<physx::PxRigidDynamic*>(rigidBody->GetNativePointer());
// These values need to be cast to integral types to prevent a compilation error on somme platforms.
EXPECT_EQ(static_cast<AZ::u32>(pxRigidBody->getRigidDynamicLockFlags()), static_cast<AZ::u32>((expectedFlags)));
RemoveRigidBody(rigidBody);
};
TestLockFlags(false, false, false, false, false, false, physx::PxRigidDynamicLockFlags(0));
TestLockFlags(true, false, false, false, false, false, physx::PxRigidDynamicLockFlags(physx::PxRigidDynamicLockFlag::eLOCK_LINEAR_X));
TestLockFlags(false, false, false, false, true, false, physx::PxRigidDynamicLockFlags(physx::PxRigidDynamicLockFlag::eLOCK_ANGULAR_Y));
TestLockFlags(false, true, false, false, false, true,
physx::PxRigidDynamicLockFlags(physx::PxRigidDynamicLockFlag::eLOCK_LINEAR_Y | physx::PxRigidDynamicLockFlag::eLOCK_ANGULAR_Z));
}
TEST_F(PhysXSpecificTest, RigidBody_RigidBodyWithSimulatedFlagsHitsPlane_OnlySimulatedShapeCollidesWithPlane)
{
// Helper function wrapping creation logic
auto CreateBoxRigidBody = [this](const AZ::Vector3& position, bool simulatedFlag, bool triggerFlag) -> AzPhysics::RigidBody*
{
auto colliderConfig = AZStd::make_shared<Physics::ColliderConfiguration>();
colliderConfig->m_isSimulated = simulatedFlag;
colliderConfig->m_isTrigger = triggerFlag;
AzPhysics::RigidBodyConfiguration rigidBodyConfig;
rigidBodyConfig.m_entityId = AZ::EntityId(0); // Set entity ID to avoid warnings in OnTriggerEnter
rigidBodyConfig.m_position = position;
rigidBodyConfig.m_colliderAndShapeData = AzPhysics::ShapeColliderPair(
colliderConfig, AZStd::make_shared<Physics::BoxShapeConfiguration>());
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfig);
return azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
return nullptr;
};
// Create a box with m_isSimulated = false
AzPhysics::RigidBody* rigidBodyNonSim =
CreateBoxRigidBody(AZ::Vector3(-5.0f, 0.0f, 5.0f), false, false);
AzPhysics::RigidBody* rigidBodySolid =
CreateBoxRigidBody(AZ::Vector3(5.0f, 0.0f, 5.0f), true, false);
AzPhysics::RigidBody* rigidBodyTrigger =
CreateBoxRigidBody(AZ::Vector3(0.0f, 0.0f, 5.0f), true, true);
// Create ground at origin
auto ground = TestUtils::CreateStaticBoxEntity(m_testSceneHandle, AZ::Vector3::CreateZero(), AZ::Vector3(20.0f, 20.0f, 0.5f));
TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 60);
// Solid rigid body is above the ground
EXPECT_GT(rigidBodySolid->GetPosition().GetZ(), 0.5f);
// Non sim rigid body fell through the ground
EXPECT_LT(rigidBodyNonSim->GetPosition().GetZ(), 0.5f);
// Trigger rigid body fell through the ground
EXPECT_LT(rigidBodyTrigger->GetPosition().GetZ(), 0.5f);
}
// Fixture for testing combinations of densities on multiple shapes
class MultiShapesDensityTestFixture
: public ::testing::TestWithParam<AZStd::pair<float, float>>
{
public:
void SetUp() override
{
if (auto* physicsSystem = AZ::Interface<AzPhysics::SystemInterface>::Get())
{
AzPhysics::SceneConfiguration sceneConfiguration = physicsSystem->GetDefaultSceneConfiguration();
sceneConfiguration.m_sceneName = AzPhysics::DefaultPhysicsSceneName;
m_testSceneHandle = physicsSystem->AddScene(sceneConfiguration);
}
}
void TearDown() override
{
//Clean up the Test scene
if (auto* physicsSystem = AZ::Interface<AzPhysics::SystemInterface>::Get())
{
physicsSystem->RemoveScene(m_testSceneHandle);
}
m_testSceneHandle = AzPhysics::InvalidSceneHandle;
}
AzPhysics::SceneHandle m_testSceneHandle = AzPhysics::InvalidSceneHandle;
};
TEST_P(MultiShapesDensityTestFixture, RigidBody_CreateShapesWithDifferentDensity_ResultingMassMatchesExpected)
{
Physics::System* physics = AZ::Interface<Physics::System>::Get();
AzPhysics::RigidBodyConfiguration rigidBodyConfig;
AzPhysics::RigidBody* rigidBody = nullptr;
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &rigidBodyConfig);
rigidBody = azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
// Create materials for each density
Physics::MaterialConfiguration materialProperties;
materialProperties.m_density = AZStd::get<0>(GetParam());
AZStd::shared_ptr<Physics::Material> boxMaterial = physics->CreateMaterial(materialProperties);
materialProperties.m_density = AZStd::get<1>(GetParam());
AZStd::shared_ptr<Physics::Material> sphereMaterial = physics->CreateMaterial(materialProperties);
// Create the shapes with their corresponding materials
Physics::ColliderConfiguration colliderConfig;
colliderConfig.m_position = AZ::Vector3(1.0f, 0.0f, 0.0f);
Physics::BoxShapeConfiguration boxShapeConfig;
AZStd::shared_ptr<Physics::Shape> boxShape =
physics->CreateShape(colliderConfig, boxShapeConfig);
boxShape->SetMaterial(boxMaterial);
rigidBody->AddShape(boxShape);
colliderConfig.m_position = AZ::Vector3(-1.0f, 0.0f, 0.0f);
Physics::SphereShapeConfiguration sphereShapeConfig;
AZStd::shared_ptr<Physics::Shape> sphereShape =
physics->CreateShape(colliderConfig, sphereShapeConfig);
sphereShape->SetMaterial(sphereMaterial);
rigidBody->AddShape(sphereShape);
// Do mass properties calculation
rigidBody->UpdateMassProperties();
// Verify the calculated mass matches the expected
const float mass = rigidBody->GetMass();
const float expectedMass = boxMaterial->GetDensity() * GetShapeVolume(boxShapeConfig) +
sphereMaterial->GetDensity() * GetShapeVolume(sphereShapeConfig);
EXPECT_TRUE(AZ::IsClose(expectedMass, mass, 0.001f));
}
// Valid material density values: [0.01f, 1e5f]
INSTANTIATE_TEST_CASE_P(PhysX, MultiShapesDensityTestFixture,
::testing::Values(
AZStd::make_pair(0.01f, 0.01f),
AZStd::make_pair(1e5f, 1e5f),
AZStd::make_pair(0.01f, 1e5f),
AZStd::make_pair(2364.0f, 10.0f)
));
// Fixture for testing extreme density values
class DensityBoundariesTestFixture
: public ::testing::TestWithParam<float>
{
};
TEST_P(DensityBoundariesTestFixture, Material_ExtremeDensityValues_ResultingDensityClampedToValidRange)
{
Physics::System* physics = AZ::Interface<Physics::System>::Get();
Physics::MaterialConfiguration materialProperties;
materialProperties.m_density = GetParam();
AZStd::shared_ptr<Physics::Material> material = physics->CreateMaterial(materialProperties);
// Resulting density should be in the valid range
float resultingDensity = material->GetDensity();
EXPECT_TRUE(resultingDensity >= Physics::MaterialConfiguration::MinDensityLimit
&& resultingDensity <= Physics::MaterialConfiguration::MaxDensityLimit);
}
// Valid material density values: [0.01f, 1e5f]
INSTANTIATE_TEST_CASE_P(PhysX, DensityBoundariesTestFixture,
::testing::Values(
std::numeric_limits<float>::min(),
std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max(),
0.0f,
1.0f,
1e9f,
0.01f,
1e5f
));
enum class SimulatedShapesMode
{
NONE,
MIXED,
ALL
};
class MassComputeFixture
: public ::testing::TestWithParam<::testing::tuple<Physics::ShapeType, SimulatedShapesMode, AzPhysics::MassComputeFlags, bool, bool>>
{
public:
void SetUp() override final
{
if (auto* physicsSystem = AZ::Interface<AzPhysics::SystemInterface>::Get())
{
AzPhysics::SceneConfiguration sceneConfiguration = physicsSystem->GetDefaultSceneConfiguration();
sceneConfiguration.m_sceneName = AzPhysics::DefaultPhysicsSceneName;
m_testSceneHandle = physicsSystem->AddScene(sceneConfiguration);
}
AzPhysics::MassComputeFlags massComputeFlags = GetMassComputeFlags();
m_rigidBodyConfig.SetMassComputeFlags(massComputeFlags);
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
AzPhysics::SimulatedBodyHandle simBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &m_rigidBodyConfig);
m_rigidBody = azdynamic_cast<AzPhysics::RigidBody*>(sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, simBodyHandle));
}
ASSERT_TRUE(m_rigidBody != nullptr);
}
void TearDown() override final
{
//Clean up the Test scene
if (auto* physicsSystem = AZ::Interface<AzPhysics::SystemInterface>::Get())
{
physicsSystem->RemoveScene(m_testSceneHandle);
}
m_testSceneHandle = AzPhysics::InvalidSceneHandle;
m_rigidBodyConfig = AzPhysics::RigidBodyConfiguration();
m_rigidBody = nullptr;
}
Physics::ShapeType GetShapeType() const
{
return ::testing::get<0>(GetParam());
}
SimulatedShapesMode GetShapesMode() const
{
return ::testing::get<1>(GetParam());
}
AzPhysics::MassComputeFlags GetMassComputeFlags() const
{
const AzPhysics::MassComputeFlags massComputeFlags = ::testing::get<2>(GetParam());
if (IncludeAllShapes())
{
return massComputeFlags | AzPhysics::MassComputeFlags::INCLUDE_ALL_SHAPES;
}
else
{
return massComputeFlags;
}
}
bool IncludeAllShapes() const
{
return ::testing::get<3>(GetParam());
}
bool IsMultiShapeTest() const
{
return ::testing::get<4>(GetParam());
}
bool IsMassExpectedToChange() const
{
return m_rigidBodyConfig.m_computeMass &&
(GetShapesMode() != SimulatedShapesMode::NONE || m_rigidBodyConfig.m_includeAllShapesInMassCalculation);
}
bool IsComExpectedToChange() const
{
return m_rigidBodyConfig.m_computeCenterOfMass &&
(GetShapesMode() != SimulatedShapesMode::NONE || m_rigidBodyConfig.m_includeAllShapesInMassCalculation);
}
bool IsInertiaExpectedToChange() const
{
return m_rigidBodyConfig.m_computeInertiaTensor &&
(GetShapesMode() != SimulatedShapesMode::NONE || m_rigidBodyConfig.m_includeAllShapesInMassCalculation);
}
AZStd::shared_ptr<Physics::Shape> CreateShape(const Physics::ColliderConfiguration& colliderConfiguration, Physics::ShapeType shapeType)
{
AZStd::shared_ptr<Physics::Shape> shape;
Physics::System* physics = AZ::Interface<Physics::System>::Get();
switch (shapeType)
{
case Physics::ShapeType::Sphere:
shape = physics->CreateShape(colliderConfiguration, Physics::SphereShapeConfiguration());
break;
case Physics::ShapeType::Box:
shape = physics->CreateShape(colliderConfiguration, Physics::BoxShapeConfiguration());
break;
case Physics::ShapeType::Capsule:
shape = physics->CreateShape(colliderConfiguration, Physics::CapsuleShapeConfiguration());
break;
}
return shape;
};
AzPhysics::RigidBodyConfiguration m_rigidBodyConfig;
AzPhysics::RigidBody* m_rigidBody = nullptr;
AzPhysics::SceneHandle m_testSceneHandle = AzPhysics::InvalidSceneHandle;
};
TEST_P(MassComputeFixture, RigidBody_ComputeMassFlagsCombinationsTwoShapes_MassPropertiesCalculatedAccordingly)
{
const Physics::ShapeType shapeType = GetShapeType();
const SimulatedShapesMode shapeMode = GetShapesMode();
const AzPhysics::MassComputeFlags massComputeFlags = GetMassComputeFlags();
const bool multiShapeTest = IsMultiShapeTest();
// Save initial values
const AZ::Vector3 comBefore = m_rigidBody->GetCenterOfMassWorld();
const AZ::Matrix3x3 inertiaBefore = m_rigidBody->GetInverseInertiaWorld();
const float massBefore = m_rigidBody->GetMass();
// Shape will be simulated for ALL and MIXED shape modes
Physics::ColliderConfiguration colliderConfig;
colliderConfig.m_isSimulated =
(shapeMode == SimulatedShapesMode::ALL || shapeMode == SimulatedShapesMode::MIXED);
colliderConfig.m_position = AZ::Vector3(1.0f, 0.0f, 0.0f);
AZStd::shared_ptr<Physics::Shape> shape = CreateShape(colliderConfig, shapeType);
m_rigidBody->AddShape(shape);
if (multiShapeTest)
{
// Sphere shape will be simulated only for the ALL shape mode
Physics::ColliderConfiguration sphereColliderConfig;
sphereColliderConfig.m_isSimulated = (shapeMode == SimulatedShapesMode::ALL);
sphereColliderConfig.m_position = AZ::Vector3(-2.0f, 0.0f, 0.0f);
AZStd::shared_ptr<Physics::Shape> sphereShape = CreateShape(sphereColliderConfig, Physics::ShapeType::Sphere);
m_rigidBody->AddShape(sphereShape);
}
// Verify swapping materials results in changes in the mass.
m_rigidBody->UpdateMassProperties(massComputeFlags, m_rigidBodyConfig.m_centerOfMassOffset,
m_rigidBodyConfig.m_inertiaTensor, m_rigidBodyConfig.m_mass);
const float massAfter = m_rigidBody->GetMass();
const AZ::Vector3 comAfter = m_rigidBody->GetCenterOfMassWorld();
const AZ::Matrix3x3 inertiaAfter = m_rigidBody->GetInverseInertiaWorld();
using ::testing::Not;
using ::testing::FloatNear;
using ::UnitTest::IsClose;
if (IsMassExpectedToChange())
{
EXPECT_THAT(massBefore, Not(FloatNear(massAfter, FLT_EPSILON)));
}
else
{
EXPECT_THAT(massBefore, FloatNear(massAfter, FLT_EPSILON));
}
if (IsComExpectedToChange())
{
EXPECT_THAT(comBefore, Not(IsClose(comAfter)));
}
else
{
EXPECT_THAT(comBefore, IsClose(comAfter));
}
if (IsInertiaExpectedToChange())
{
EXPECT_THAT(inertiaBefore, Not(IsClose(inertiaAfter)));
}
else
{
EXPECT_THAT(inertiaBefore, IsClose(inertiaAfter));
}
}
static const AzPhysics::MassComputeFlags PossibleMassComputeFlags[] =
{
// No compute
AzPhysics::MassComputeFlags::NONE,
// Compute Mass only
AzPhysics::MassComputeFlags::COMPUTE_MASS,
// Compute Inertia only
AzPhysics::MassComputeFlags::COMPUTE_INERTIA,
// Compute COM only
AzPhysics::MassComputeFlags::COMPUTE_COM,
// Compute combinations of 2
AzPhysics::MassComputeFlags::COMPUTE_MASS | AzPhysics::MassComputeFlags::COMPUTE_COM,
AzPhysics::MassComputeFlags::COMPUTE_MASS | AzPhysics::MassComputeFlags::COMPUTE_INERTIA,
AzPhysics::MassComputeFlags::COMPUTE_COM | AzPhysics::MassComputeFlags::COMPUTE_INERTIA,
// Compute all
AzPhysics::MassComputeFlags::DEFAULT, // COMPUTE_COM | COMPUTE_INERTIA | COMPUTE_MASS
};
INSTANTIATE_TEST_CASE_P(PhysX, MassComputeFixture, ::testing::Combine(
::testing::ValuesIn({ Physics::ShapeType::Sphere, Physics::ShapeType::Box, Physics::ShapeType::Capsule }), // Values for GetShapeType()
::testing::ValuesIn({ SimulatedShapesMode::NONE, SimulatedShapesMode::MIXED, SimulatedShapesMode::ALL }), // Values for GetShapesMode()
::testing::ValuesIn(PossibleMassComputeFlags), // Values for GetMassComputeFlags()
::testing::Bool(), // Values for IncludeAllShapes()
::testing::Bool())); // Values for IsMultiShapeTest()
class MassPropertiesWithTriangleMesh
: public ::testing::TestWithParam<AzPhysics::MassComputeFlags>
{
public:
void SetUp() override
{
if (auto* physicsSystem = AZ::Interface<AzPhysics::SystemInterface>::Get())
{
AzPhysics::SceneConfiguration sceneConfiguration = physicsSystem->GetDefaultSceneConfiguration();
sceneConfiguration.m_sceneName = AzPhysics::DefaultPhysicsSceneName;
m_testSceneHandle = physicsSystem->AddScene(sceneConfiguration);
}
}
void TearDown() override
{
// Clean up the Test scene
if (auto* physicsSystem = AZ::Interface<AzPhysics::SystemInterface>::Get())
{
physicsSystem->RemoveScene(m_testSceneHandle);
}
m_testSceneHandle = AzPhysics::InvalidSceneHandle;
}
AzPhysics::MassComputeFlags GetMassComputeFlags() const
{
return GetParam();
}
AzPhysics::SceneHandle m_testSceneHandle = AzPhysics::InvalidSceneHandle;
};
TEST_P(MassPropertiesWithTriangleMesh, KinematicRigidBody_ComputeMassProperties_TriggersWarnings)
{
const AzPhysics::MassComputeFlags flags = GetMassComputeFlags();
const bool doesComputeCenterOfMass = AzPhysics::MassComputeFlags::COMPUTE_COM == (flags & AzPhysics::MassComputeFlags::COMPUTE_COM);
const bool doesComputeMass = AzPhysics::MassComputeFlags::COMPUTE_MASS == (flags & AzPhysics::MassComputeFlags::COMPUTE_MASS);
const bool doesComputeInertia = AzPhysics::MassComputeFlags::COMPUTE_INERTIA == (flags & AzPhysics::MassComputeFlags::COMPUTE_INERTIA);
UnitTest::ErrorHandler computeCenterOfMassWarningHandler(
"cannot compute COM");
UnitTest::ErrorHandler computeMassWarningHandler(
"cannot compute Mass");
UnitTest::ErrorHandler computeIneriaWarningHandler(
"cannot compute Inertia");
AzPhysics::SimulatedBodyHandle rigidBodyhandle = TestUtils::AddKinematicTriangleMeshCubeToScene(m_testSceneHandle, 3.0f, flags);
EXPECT_TRUE(rigidBodyhandle != AzPhysics::InvalidSimulatedBodyHandle);
EXPECT_EQ(computeCenterOfMassWarningHandler.GetExpectedWarningCount(), doesComputeCenterOfMass ? 1 : 0);
EXPECT_EQ(computeMassWarningHandler.GetExpectedWarningCount(), doesComputeMass ? 1 : 0);
EXPECT_EQ(computeIneriaWarningHandler.GetExpectedWarningCount(), doesComputeInertia ? 1 : 0);
if (auto* sceneInterface = AZ::Interface<AzPhysics::SceneInterface>::Get())
{
sceneInterface->RemoveSimulatedBody(m_testSceneHandle, rigidBodyhandle);
}
}
INSTANTIATE_TEST_CASE_P(PhysX, MassPropertiesWithTriangleMesh,
::testing::ValuesIn(PossibleMassComputeFlags)); // Values for GetMassComputeFlags()
} // namespace PhysX