3b9762142a
Using triangle mesh with a kinematic rigid body is allowed, but the options "Compute COM", "Compute Mass" and "Compute Inertia" are not supported by PhysX and an error in logged that default values for COM, Mass and Inertia will be used. Now this situation is captured and an explanatory warning is used instead. - Improved RigidBody::UpdateMassProperties function to apply the same logic in the treatment of shapes for all three parameters: COM, Mass and Inertia. - Improved UpdateMassProperties function by using references for the override parameters instead of pointers. - Improved function that computes the Center of Mass UpdateCenterOfMass (renamed from UpdateComputedCenterOfMass), to include the same shapes that the compute mass and inertia functions in physx updateMassAndInertia, which is to include all shapes if includeAllShapesInMassCalculation is true, else include only the shapes with eSIMULATION_SHAPE flag. - Removed unused private function RigidBody::ComputeInertia. - Added unit test to check when the warnings are fired correctly when COM, Mass or Inertia are asked to be computed on a rigid body with triangle mesh shapes. - Improved MassComputeFixture tests by not only using Box shape, but also sphere and capture, plus improved the PossibleMassComputeFlags parameters to include all possible variations of the MassComputeFlags flags. Fixes #3322 Fixes #3979 Signed-off-by: moraaar <moraaar@amazon.com>
1614 lines
73 KiB
C++
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
|
|
|