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/Code/Framework/Tests/Physics/PhysicsGenericInterfaceTest...

920 lines
40 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzFramework/Physics/Shape.h>
#include <AzFramework/Physics/RigidBodyBus.h>
#include <AzFramework/Physics/World.h>
#include <PhysX/ColliderComponentBus.h>
namespace Physics
{
static auto GetEntityInRayCastHitCallBack = [](AZ::EntityId entityId)
{
return [entityId](const RayCastHit& hit)
{
return hit.m_body->GetEntityId() == entityId;
};
};
TEST_F(GenericPhysicsInterfaceTest, World_CreateNewWorld_ReturnsNewWorld)
{
EXPECT_TRUE(CreateTestWorld() != nullptr);
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_CastAgainstNothing_ReturnsNoHits)
{
RayCastRequest request;
request.m_start = AZ::Vector3(-100.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(1.0f, 0.0f, 0.0f);
request.m_distance = 200.0f;
RayCastHit hit;
WorldRequestBus::BroadcastResult(hit, &WorldRequests::RayCast, request);
EXPECT_FALSE(hit);
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_CastAgainstSphere_ReturnsHits)
{
auto sphereEntity = AddSphereEntity(AZ::Vector3(0.0f), 10.0f);
RayCastRequest request;
request.m_start = AZ::Vector3(-100.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(1.0f, 0.0f, 0.0f);
request.m_distance = 200.0f;
RayCastHit hit;
WorldRequestBus::BroadcastResult(hit, &WorldRequests::RayCast, request);
EXPECT_TRUE(hit);
bool hitsIncludeSphereEntity = (hit.m_body->GetEntityId() == sphereEntity->GetId());
EXPECT_TRUE(hitsIncludeSphereEntity);
delete sphereEntity;
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_CastAgainstSphere_ReturnsCorrectShapeAndMaterial)
{
auto sphereEntity = AZStd::shared_ptr<AZ::Entity>(AddSphereEntity(AZ::Vector3(0.0f), 10.0f));
RayCastRequest request;
request.m_start = AZ::Vector3(-100.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(1.0f, 0.0f, 0.0f);
request.m_distance = 200.0f;
RayCastHit result;
WorldRequestBus::BroadcastResult(result, &WorldRequests::RayCast, request);
ASSERT_TRUE(result);
RigidBody* rigidBody;
Physics::RigidBodyRequestBus::EventResult(rigidBody, sphereEntity->GetId(), &RigidBodyRequestBus::Events::GetRigidBody);
ASSERT_NE(rigidBody->GetShape(0), nullptr);
ASSERT_NE(result.m_material, nullptr);
ASSERT_EQ(result.m_shape, rigidBody->GetShape(0).get());
ASSERT_EQ(result.m_material, rigidBody->GetShape(0).get()->GetMaterial().get());
const AZStd::string& typeName = result.m_material->GetSurfaceTypeName();
ASSERT_EQ(typeName, AZStd::string("Default"));
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_CastAgainstStaticObject_ReturnsHits)
{
auto boxEntity = AZStd::shared_ptr<AZ::Entity>(AddStaticBoxEntity(AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3(10.0f, 10.0f, 10.0f)));
RayCastRequest request;
request.m_start = AZ::Vector3(-100.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(1.0f, 0.0f, 0.0f);
request.m_distance = 200.0f;
RayCastHit result;
WorldRequestBus::BroadcastResult(result, &WorldRequests::RayCast, request);
EXPECT_TRUE(result);
bool hitsIncludeEntity = (result.m_body->GetEntityId() == boxEntity->GetId());
EXPECT_TRUE(hitsIncludeEntity);
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_CastAgainstFilteredSpheres_ReturnsHits)
{
auto entity1 = AddSphereEntity(AZ::Vector3(0.0f, 0.0f, 10.0f), 10.0f, CollisionLayer(0));
auto entity2 = AddCapsuleEntity(AZ::Vector3(0.0f, 0.0f, 20.0f), 10.0f, 2.0f, CollisionLayer(1));
auto entity3 = AddStaticBoxEntity(AZ::Vector3(0.0f, 0.0f, 30.0f), AZ::Vector3(20.0f, 20.0f, 20.0f), CollisionLayer(2));
CollisionGroup group = CollisionGroup::All;
group.SetLayer(CollisionLayer(0), true);
group.SetLayer(CollisionLayer(1), false);
group.SetLayer(CollisionLayer(2), true);
RayCastRequest request;
request.m_start = AZ::Vector3(0.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(0.0f, 0.0f, 1.0f);
request.m_distance = 200.0f;
request.m_collisionGroup = group;
AZStd::vector<RayCastHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::RayCastMultiple, request);
ASSERT_TRUE(hits.size() == 2);
EXPECT_TRUE(hits[1].m_body->GetEntityId() == entity1->GetId());
EXPECT_TRUE(hits[0].m_body->GetEntityId() == entity3->GetId());
delete entity1;
delete entity2;
delete entity3;
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_AgainstStaticOnly_ReturnsStaticBox)
{
auto dynamicSphere = AddSphereEntity(AZ::Vector3(0.0f, 0.0f, 10.0f), 10.0f);
auto staticBox = AddStaticBoxEntity(AZ::Vector3(0.0f, 0.0f, 30.0f), AZ::Vector3(20.0f, 20.0f, 20.0f));
RayCastRequest request;
request.m_start = AZ::Vector3(0.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(0.0f, 0.0f, 1.0f);
request.m_queryType = Physics::QueryType::Static;
AZStd::vector<RayCastHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::RayCastMultiple, request);
ASSERT_EQ(hits.size(), 1);
ASSERT_EQ(hits[0].m_body->GetEntityId(), staticBox->GetId());
delete dynamicSphere;
delete staticBox;
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_AgainstDynamicOnly_ReturnsDynamicSphere)
{
auto dynamicSphere = AddSphereEntity(AZ::Vector3(0.0f, 0.0f, 10.0f), 10.0f);
auto staticBox = AddStaticBoxEntity(AZ::Vector3(0.0f, 0.0f, 30.0f), AZ::Vector3(20.0f, 20.0f, 20.0f));
RayCastRequest request;
request.m_start = AZ::Vector3(0.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(0.0f, 0.0f, 1.0f);
request.m_queryType = Physics::QueryType::Dynamic;
AZStd::vector<RayCastHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::RayCastMultiple, request);
ASSERT_EQ(hits.size(), 1);
ASSERT_EQ(hits[0].m_body->GetEntityId(), dynamicSphere->GetId());
delete dynamicSphere;
delete staticBox;
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_AgainstStaticAndDynamic_ReturnsBothObjects)
{
auto dynamicSphere = AddSphereEntity(AZ::Vector3(0.0f, 0.0f, 10.0f), 10.0f);
auto staticBox = AddStaticBoxEntity(AZ::Vector3(0.0f, 0.0f, 30.0f), AZ::Vector3(20.0f, 20.0f, 20.0f));
RayCastRequest request;
request.m_start = AZ::Vector3(0.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(0.0f, 0.0f, 1.0f);
request.m_queryType = Physics::QueryType::StaticAndDynamic;
AZStd::vector<RayCastHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::RayCastMultiple, request);
ASSERT_EQ(hits.size(), 2);
ASSERT_EQ(hits[0].m_body->GetEntityId(), staticBox->GetId());
ASSERT_EQ(hits[1].m_body->GetEntityId(), dynamicSphere->GetId());
delete dynamicSphere;
delete staticBox;
}
TEST_F(GenericPhysicsInterfaceTest, RayCast_AgainstMultipleTouchAndBlockHits_ReturnsClosestBlockAndTouches)
{
auto dynamicSphere = AddSphereEntity(AZ::Vector3(20.0f, 0.0f, 0.0f), 10.0f);
auto staticBox = AddStaticBoxEntity(AZ::Vector3(40.0f, 0.0f, 0.0f), AZ::Vector3(5.0f, 5.0f, 5.0f));
auto blockingSphere = AddSphereEntity(AZ::Vector3(60.0f, 0.0f, 0.0f), 5.0f);
auto blockingBox = AddStaticBoxEntity(AZ::Vector3(80.0f, 0.0f, 0.0f), AZ::Vector3(5.0f, 5.0f, 5.0f));
auto farSphere = AddSphereEntity(AZ::Vector3(120.0f, 0.0f, 0.0f), 10.0f);
RayCastRequest request;
request.m_start = AZ::Vector3(0.0f, 0.0f, 0.0f);
request.m_direction = AZ::Vector3(1.0f, 0.0f, 0.0f);
request.m_queryType = Physics::QueryType::StaticAndDynamic;
request.m_filterCallback = [&](const Physics::WorldBody* body, [[maybe_unused]] const Physics::Shape* shape)
{
if (body->GetEntityId() == blockingBox->GetId() || body->GetEntityId() == blockingSphere->GetId())
{
return QueryHitType::Block;
}
return QueryHitType::Touch;
};
AZStd::vector<RayCastHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::RayCastMultiple, request);
ASSERT_EQ(hits.size(), 3);
ASSERT_EQ(1, AZStd::count_if(hits.begin(), hits.end(), GetEntityInRayCastHitCallBack(dynamicSphere->GetId())));
ASSERT_EQ(1, AZStd::count_if(hits.begin(), hits.end(), GetEntityInRayCastHitCallBack(staticBox->GetId())));
ASSERT_EQ(1, AZStd::count_if(hits.begin(), hits.end(), GetEntityInRayCastHitCallBack(blockingSphere->GetId())));
delete dynamicSphere;
delete staticBox;
delete blockingSphere;
delete blockingBox;
delete farSphere;
}
TEST_F(GenericPhysicsInterfaceTest, ShapeCast_CastAgainstNothing_ReturnsNoHits)
{
Physics::RayCastHit hit;
WorldRequestBus::BroadcastResult(hit, &WorldRequests::SphereCast,
1.0f,
AZ::Transform::CreateTranslation(AZ::Vector3(-20.0f, 0.0f, 0.0f)),
AZ::Vector3(1.0f, 0.0f, 0.0f),
20.0f, Physics::QueryType::StaticAndDynamic,
Physics::CollisionGroup::All,
nullptr
);
EXPECT_FALSE(hit);
}
TEST_F(GenericPhysicsInterfaceTest, ShapeCast_CastAgainstSphere_ReturnsHits)
{
auto sphereEntity = AddSphereEntity(AZ::Vector3(0.0f), 10.0f);
Physics::RayCastHit hit;
WorldRequestBus::BroadcastResult(hit, &WorldRequests::SphereCast,
1.0f,
AZ::Transform::CreateTranslation(AZ::Vector3(-20.0f, 0.0f, 0.0f)),
AZ::Vector3(1.0f, 0.0f, 0.0f),
20.0f, Physics::QueryType::StaticAndDynamic,
Physics::CollisionGroup::All,
nullptr
);
EXPECT_TRUE(hit);
EXPECT_EQ(hit.m_body->GetEntityId(), sphereEntity->GetId());
// clear up scene
delete sphereEntity;
}
TEST_F(GenericPhysicsInterfaceTest, ShapeCast_SphereCastAgainstStaticObject_ReturnsHits)
{
auto boxEntity = AZStd::shared_ptr<AZ::Entity>(AddStaticBoxEntity(AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3(1.0f, 1.0f, 1.0f)));
Physics::RayCastHit hit;
WorldRequestBus::BroadcastResult(hit, &WorldRequests::SphereCast,
1.5f,
AZ::Transform::CreateTranslation(AZ::Vector3(-20.0f, 0.0f, 0.0f)),
AZ::Vector3(1.0f, 0.0f, 0.0f),
20.0f, Physics::QueryType::StaticAndDynamic,
Physics::CollisionGroup::All,
nullptr
);
EXPECT_TRUE(hit);
EXPECT_EQ(hit.m_body->GetEntityId(), boxEntity->GetId());
}
TEST_F(GenericPhysicsInterfaceTest, ShapeCast_SphereCastAgainstFilteredObjects_ReturnsHits)
{
auto entity1 = AddSphereEntity(AZ::Vector3(0.0f, 0.0f, 10.0f), 10.0f, CollisionLayer(0));
auto entity2 = AddCapsuleEntity(AZ::Vector3(0.0f, 0.0f, 20.0f), 10.0f, 2.0f, CollisionLayer(1));
auto entity3 = AddStaticBoxEntity(AZ::Vector3(0.0f, 0.0f, 30.0f), AZ::Vector3(20.0f, 20.0f, 20.0f), CollisionLayer(2));
CollisionGroup group = CollisionGroup::All;
group.SetLayer(CollisionLayer(0), true);
group.SetLayer(CollisionLayer(1), false);
group.SetLayer(CollisionLayer(2), true);
AZStd::vector<Physics::RayCastHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::SphereCastMultiple,
1.5f,
AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 0.0f, 0.0f)),
AZ::Vector3(0.0f, 0.0f, 1.0f),
200.0f, Physics::QueryType::StaticAndDynamic,
group,
nullptr
);
ASSERT_TRUE(hits.size() == 2);
EXPECT_TRUE(hits[1].m_body->GetEntityId() == entity1->GetId());
EXPECT_TRUE(hits[0].m_body->GetEntityId() == entity3->GetId());
delete entity1;
delete entity2;
delete entity3;
}
TEST_F(GenericPhysicsInterfaceTest, ShapeCast_AgainstMultipleTouchAndBlockHits_ReturnsClosestBlockAndTouches)
{
auto dynamicSphere = AddSphereEntity(AZ::Vector3(20.0f, 0.0f, 0.0f), 10.0f);
auto staticBox = AddStaticBoxEntity(AZ::Vector3(40.0f, 0.0f, 0.0f), AZ::Vector3(5.0f, 5.0f, 5.0f));
auto blockingSphere = AddSphereEntity(AZ::Vector3(60.0f, 0.0f, 0.0f), 5.0f);
auto blockingBox = AddStaticBoxEntity(AZ::Vector3(80.0f, 0.0f, 0.0f), AZ::Vector3(5.0f, 5.0f, 5.0f));
auto farSphere = AddSphereEntity(AZ::Vector3(120.0f, 0.0f, 0.0f), 10.0f);
AZStd::vector<Physics::RayCastHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::SphereCastMultiple,
1.5f,
AZ::Transform::CreateTranslation(AZ::Vector3(0.0f, 0.0f, 0.0f)),
AZ::Vector3(1.0f, 0.0f, 0.0f),
200.0f, Physics::QueryType::StaticAndDynamic,
Physics::CollisionGroup::All,
[&](const Physics::WorldBody* body, [[maybe_unused]] const Physics::Shape* shape)
{
if (body->GetEntityId() == blockingBox->GetId() || body->GetEntityId() == blockingSphere->GetId())
{
return QueryHitType::Block;
}
return QueryHitType::Touch;
}
);
ASSERT_EQ(hits.size(), 3);
ASSERT_EQ(1, AZStd::count_if(hits.begin(), hits.end(), GetEntityInRayCastHitCallBack(dynamicSphere->GetId())));
ASSERT_EQ(1, AZStd::count_if(hits.begin(), hits.end(), GetEntityInRayCastHitCallBack(staticBox->GetId())));
ASSERT_EQ(1, AZStd::count_if(hits.begin(), hits.end(), GetEntityInRayCastHitCallBack(blockingSphere->GetId())));
delete dynamicSphere;
delete staticBox;
delete blockingSphere;
delete blockingBox;
delete farSphere;
}
TEST_F(GenericPhysicsInterfaceTest, Overlap_OverlapMultipleObjects_ReturnsHits)
{
AZStd::shared_ptr<AZ::Entity> sphereEntity(AddSphereEntity(AZ::Vector3(10.0f, 0.0f, 0.0f), 3.0f));
AZStd::shared_ptr<AZ::Entity> boxEntity(AddBoxEntity(AZ::Vector3(7.0f, 4.0f, 0.0f), AZ::Vector3(1.0f)));
AZStd::shared_ptr<AZ::Entity> capsuleEntity(AddCapsuleEntity(AZ::Vector3(15.0f, 0.0f, 0.0f), 3.0f, 1.0f));
BoxShapeConfiguration overlapShape;
overlapShape.m_dimensions = AZ::Vector3(3.0f);
OverlapRequest request;
request.m_pose = AZ::Transform::CreateTranslation(AZ::Vector3(13.0f, 0.0f, 0.0f));
request.m_shapeConfiguration = &overlapShape;
AZStd::vector<OverlapHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::Overlap, request);
EXPECT_EQ(hits.size(), 2);
// boxEntity shouldn't be included in the result
EXPECT_FALSE(AZStd::any_of(hits.begin(), hits.end(),
[idToFind = boxEntity->GetId()](const OverlapHit& hit) { return hit.m_body->GetEntityId() == idToFind; }));
}
TEST_F(GenericPhysicsInterfaceTest, Overlap_OverlapMultipleObjectsUseFriendlyFunctions_ReturnsHits)
{
AZStd::shared_ptr<AZ::Entity> sphereEntity(AddSphereEntity(AZ::Vector3(10.0f, 0.0f, 0.0f), 3.0f));
AZStd::shared_ptr<AZ::Entity> boxEntity(AddBoxEntity(AZ::Vector3(7.0f, 4.0f, 0.0f), AZ::Vector3(1.0f)));
AZStd::shared_ptr<AZ::Entity> capsuleEntity(AddCapsuleEntity(AZ::Vector3(15.0f, 0.0f, 0.0f), 3.0f, 1.0f));
AZStd::shared_ptr<World> defaultWorld;
DefaultWorldBus::BroadcastResult(defaultWorld, &DefaultWorldRequests::GetDefaultWorld);
{
AZStd::vector<OverlapHit> hits = defaultWorld->OverlapBox(AZ::Vector3(3.0f), AZ::Transform::CreateTranslation(AZ::Vector3(13.0f, 0.0f, 0.0f)));
EXPECT_EQ(hits.size(), 2);
// boxEntity shouldn't be included in the result
EXPECT_FALSE(AZStd::any_of(hits.begin(), hits.end(),
[idToFind = boxEntity->GetId()](const OverlapHit& hit) { return hit.m_body->GetEntityId() == idToFind; }));
}
{
AZStd::vector<OverlapHit> hits = defaultWorld->OverlapSphere(3.0f, AZ::Transform::CreateTranslation(AZ::Vector3(13.0f, 0.0f, 0.0f)));
EXPECT_EQ(hits.size(), 2);
// boxEntity shouldn't be included in the result
EXPECT_FALSE(AZStd::any_of(hits.begin(), hits.end(),
[idToFind = boxEntity->GetId()](const OverlapHit& hit) { return hit.m_body->GetEntityId() == idToFind; }));
}
}
TEST_F(GenericPhysicsInterfaceTest, Overlap_OverlapMultipleObjectsUseFriendlyFunctionsCustomFiltering_ReturnsHits)
{
AZStd::shared_ptr<AZ::Entity> sphereEntity(AddSphereEntity(AZ::Vector3(10.0f, 0.0f, 0.0f), 3.0f));
AZStd::shared_ptr<AZ::Entity> boxEntity(AddBoxEntity(AZ::Vector3(7.0f, 4.0f, 0.0f), AZ::Vector3(1.0f)));
AZStd::shared_ptr<AZ::Entity> capsuleEntity(AddCapsuleEntity(AZ::Vector3(15.0f, 0.0f, 0.0f), 3.0f, 1.0f));
AZStd::shared_ptr<World> defaultWorld;
DefaultWorldBus::BroadcastResult(defaultWorld, &DefaultWorldRequests::GetDefaultWorld);
// Here we do an overlap test that covers all objects in the scene
// However we provide a custom filtering function that filters out a specific entity
{
AZ::EntityId entityIdToFilterOut = capsuleEntity->GetId();
AZStd::vector<Physics::OverlapHit> hits = defaultWorld->OverlapCapsule(100.0f, 30.0f, AZ::Transform::CreateTranslation(AZ::Vector3(13.0f, 0.0f, 0.0f)),
[entityIdToFilterOut](const Physics::WorldBody* body, [[maybe_unused]] const Physics::Shape* shape)
{
return body->GetEntityId() != entityIdToFilterOut;
});
EXPECT_EQ(hits.size(), 2);
EXPECT_FALSE(AZStd::any_of(hits.begin(), hits.end(),
[entityIdToFilterOut](const OverlapHit& hit) { return hit.m_body->GetEntityId() == entityIdToFilterOut; }));
}
}
TEST_F(GenericPhysicsInterfaceTest, Overlap_OverlapMultipleObjects_ReturnsFilteredHits)
{
AZStd::shared_ptr<AZ::Entity> sphereEntity(AddSphereEntity(AZ::Vector3(10.0f, 0.0f, 0.0f), 3.0f, CollisionLayer(0)));
AZStd::shared_ptr<AZ::Entity> boxEntity(AddStaticBoxEntity(AZ::Vector3(12.0f, 0.0f, 0.0f), AZ::Vector3(1.0f), CollisionLayer(1)));
AZStd::shared_ptr<AZ::Entity> capsuleEntity(AddCapsuleEntity(AZ::Vector3(14.0f, 0.0f, 0.0f), 3.0f, 1.0f, CollisionLayer(2)));
BoxShapeConfiguration overlapShape;
overlapShape.m_dimensions = AZ::Vector3(1.0f);
OverlapRequest request;
request.m_pose = AZ::Transform::CreateTranslation(AZ::Vector3(13.0f, 0.0f, 0.0f));
request.m_shapeConfiguration = &overlapShape;
request.m_collisionGroup = CollisionGroup::All;
request.m_collisionGroup.SetLayer(CollisionLayer(0), false); // Filter out the sphere
request.m_collisionGroup.SetLayer(CollisionLayer(1), true);
request.m_collisionGroup.SetLayer(CollisionLayer(2), true);
AZStd::vector<OverlapHit> hits;
WorldRequestBus::BroadcastResult(hits, &WorldRequests::Overlap, request);
EXPECT_EQ(hits.size(), 2);
EXPECT_FALSE(AZStd::any_of(hits.begin(), hits.end(),
[sphereEntity](const OverlapHit& hit)
{
// Make sure the sphere was not included
return hit.m_body->GetEntityId() == sphereEntity->GetId();
}));
}
TEST_F(GenericPhysicsInterfaceTest, Gravity_DynamicBody_BodyFalls)
{
auto world = CreateTestWorld();
auto rigidBody = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 0.0f, 100.0f));
UpdateWorld(world.get(), 1.0f / 60.f, 60);
// expect velocity to be -gt and distance fallen to be 1/2gt^2, but allow quite a lot of tolerance
// due to potential differences in back end integration schemes etc.
EXPECT_NEAR(rigidBody->GetLinearVelocity().GetZ(), -10.0f, 0.5f);
EXPECT_NEAR(rigidBody->GetTransform().GetTranslation().GetZ(), 95.0f, 0.5f);
EXPECT_NEAR(rigidBody->GetCenterOfMassWorld().GetZ(), 95.0f, 0.5f);
EXPECT_NEAR(rigidBody->GetPosition().GetZ(), 95.0f, 0.5f);
}
TEST_F(GenericPhysicsInterfaceTest, World_SplitSimulation_BodyFallsTheSameInBothWorlds)
{
auto worldA = CreateTestWorld();
auto worldB = CreateTestWorld();
AZ::Vector3 initialPosition(0.0f, 0.0f, 100.0f);
auto rigidBodyA = AddUnitBoxToWorld(worldA.get(), initialPosition);
auto rigidBodyB = AddUnitBoxToWorld(worldB.get(), initialPosition);
Physics::WorldConfiguration worldConfiguration;
float deltaTime = worldConfiguration.m_fixedTimeStep;
AZ::u32 numSteps = 60;
UpdateWorld(worldA.get(), deltaTime, numSteps);
UpdateWorldSplitSim(worldB.get(), deltaTime, numSteps);
// expect velocity to be -gt and distance fallen to be 1/2gt^2, but allow quite a lot of tolerance
// due to potential differences in back end integration schemes etc.
EXPECT_NEAR(rigidBodyA->GetLinearVelocity().GetZ(), -10.0f, 0.5f);
EXPECT_NEAR(rigidBodyA->GetTransform().GetTranslation().GetZ(), 95.0f, 0.5f);
EXPECT_NEAR(rigidBodyA->GetCenterOfMassWorld().GetZ(), 95.0f, 0.5f);
EXPECT_NEAR(rigidBodyA->GetPosition().GetZ(), 95.0f, 0.5f);
// Verify simulation results are the same
EXPECT_TRUE(rigidBodyA->GetLinearVelocity().IsClose(rigidBodyB->GetLinearVelocity()));
EXPECT_TRUE(rigidBodyA->GetTransform().GetTranslation().IsClose(rigidBodyB->GetTransform().GetTranslation()));
EXPECT_TRUE(rigidBodyA->GetCenterOfMassWorld().IsClose(rigidBodyB->GetCenterOfMassWorld()));
EXPECT_TRUE(rigidBodyA->GetPosition().IsClose(rigidBodyB->GetPosition()));
}
TEST_F(GenericPhysicsInterfaceTest, IncreaseMass_StaggeredTowerOfBoxes_TowerOverbalances)
{
auto world = CreateTestWorld();
// make a tower of boxes which is staggered but should still balance if all the blocks are the same mass
auto boxA = AddStaticUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 0.0f, 0.5f));
auto boxB = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.3f, 0.0f, 1.5f));
auto boxC = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.6f, 0.0f, 2.5f));
// check that the tower balances
UpdateWorld(world.get(), 1.0f / 60.0f, 60);
EXPECT_NEAR(2.5f, boxC->GetPosition().GetZ(), 0.01f);
// increasing the mass of the top block in the tower should overbalance it
boxC->SetMass(5.0f);
EXPECT_NEAR(1.0f, boxB->GetMass(), 0.01f);
EXPECT_NEAR(1.0f, boxB->GetInverseMass(), 0.01f);
EXPECT_NEAR(5.0f, boxC->GetMass(), 0.01f);
EXPECT_NEAR(0.2f, boxC->GetInverseMass(), 0.01f);
boxB->ForceAwake();
boxC->ForceAwake();
UpdateWorld(world.get(), 1.0f / 60.0f, 300);
EXPECT_GT(0.0f, static_cast<float>(boxC->GetPosition().GetZ()));
}
TEST_F(GenericPhysicsInterfaceTest, GetCenterOfMass_FallingBody_CenterOfMassCorrectDuringFall)
{
auto world = CreateTestWorld();
auto boxStatic = AddStaticUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 0.0f, 0.0f));
auto boxDynamic = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 0.0f, 2.0f));
auto tolerance = 1e-3f;
EXPECT_TRUE(boxDynamic->GetCenterOfMassWorld().IsClose(AZ::Vector3(0.0f, 0.0f, 2.0f), tolerance));
EXPECT_TRUE(boxDynamic->GetCenterOfMassLocal().IsClose(AZ::Vector3(0.0f, 0.0f, 0.0f), tolerance));
UpdateWorld(world.get(), 1.0f / 60.0f, 300);
EXPECT_NEAR(static_cast<float>(boxDynamic->GetCenterOfMassWorld().GetZ()), 1.0f, 1e-3f);
EXPECT_TRUE(boxDynamic->GetCenterOfMassLocal().IsClose(AZ::Vector3(0.0f, 0.0f, 0.0f), tolerance));
}
TEST_F(GenericPhysicsInterfaceTest, SetLinearVelocity_DynamicBox_AffectsTrajectory)
{
auto world = CreateTestWorld();
auto boxA = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, -5.0f, 10.0f));
auto boxB = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 5.0f, 10.0f));
boxA->SetLinearVelocity(AZ::Vector3(10.0f, 0.0f, 0.0f));
for (int i = 1; i < 10; i++)
{
float xPreviousA = boxA->GetPosition().GetX();
float xPreviousB = boxB->GetPosition().GetX();
UpdateWorld(world.get(), 1.0f / 60.0f, 10);
EXPECT_GT(static_cast<float>(boxA->GetPosition().GetX()), xPreviousA);
EXPECT_NEAR(boxB->GetPosition().GetX(), xPreviousB, 1e-3f);
}
}
TEST_F(GenericPhysicsInterfaceTest, ApplyLinearImpulse_DynamicBox_AffectsTrajectory)
{
auto world = CreateTestWorld();
auto boxA = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 0.0f, 100.0f));
auto boxB = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 10.0f, 100.0f));
boxA->ApplyLinearImpulse(AZ::Vector3(10.0f, 0.0f, 0.0f));
for (int i = 1; i < 10; i++)
{
float xPreviousA = boxA->GetPosition().GetX();
float xPreviousB = boxB->GetPosition().GetX();
UpdateWorld(world.get(), 1.0f / 60.0f, 10);
EXPECT_GT(static_cast<float>(boxA->GetPosition().GetX()), xPreviousA);
EXPECT_NEAR(boxB->GetPosition().GetX(), xPreviousB, 1e-3f);
}
}
// allow a more generous tolerance on tests involving objects in contact, since the way physics engines normally
// handle multiple contacts between objects can lead to slight imbalances in contact forces
static constexpr float ContactTestTolerance = 0.01f;
TEST_F(GenericPhysicsInterfaceTest, GetAngularVelocity_DynamicCapsuleOnSlope_GainsAngularVelocity)
{
auto world = CreateTestWorld();
AZ::Transform slopeTransform = AZ::Transform::CreateRotationY(0.1f);
auto slope = AddStaticFloorToWorld(world.get(), slopeTransform);
auto capsule = AddCapsuleToWorld(world.get(), slopeTransform.TransformPoint(AZ::Vector3::CreateAxisZ()));
// the capsule should roll down the slope, picking up angular velocity parallel to the Y axis
float angularVelocityMagnitude = capsule->GetAngularVelocity().GetLength();
UpdateWorld(world.get(), 1.0f / 60.0f, 60);
angularVelocityMagnitude = capsule->GetAngularVelocity().GetLength();
for (int i = 0; i < 60; i++)
{
world->Update(1.0f / 60.0f);
auto angularVelocity = capsule->GetAngularVelocity();
EXPECT_TRUE(angularVelocity.IsPerpendicular(AZ::Vector3::CreateAxisX(), ContactTestTolerance));
EXPECT_TRUE(angularVelocity.IsPerpendicular(AZ::Vector3::CreateAxisZ(), ContactTestTolerance));
EXPECT_TRUE(capsule->GetAngularVelocity().GetLength() > angularVelocityMagnitude);
angularVelocityMagnitude = angularVelocity.GetLength();
}
}
TEST_F(GenericPhysicsInterfaceTest, SetAngularVelocity_DynamicCapsule_StartsRolling)
{
auto world = CreateTestWorld();
auto floor = AddStaticFloorToWorld(world.get());
auto capsule = AddCapsuleToWorld(world.get(), AZ::Vector3::CreateAxisZ());
// capsule should remain stationary
for (int i = 0; i < 60; i++)
{
world->Update(1.0f / 60.0f);
EXPECT_TRUE(capsule->GetPosition().IsClose(AZ::Vector3::CreateAxisZ(), ContactTestTolerance));
EXPECT_TRUE(capsule->GetLinearVelocity().IsClose(AZ::Vector3::CreateZero(), ContactTestTolerance));
EXPECT_TRUE(capsule->GetAngularVelocity().IsClose(AZ::Vector3::CreateZero(), ContactTestTolerance));
}
// apply an angular velocity and it should start rolling
auto angularVelocity = AZ::Vector3::CreateAxisY(10.0f);
capsule->SetAngularVelocity(angularVelocity);
EXPECT_TRUE(capsule->GetAngularVelocity().IsClose(angularVelocity));
for (int i = 0; i < 60; i++)
{
float xPrevious = capsule->GetPosition().GetX();
world->Update(1.0f / 60.0f);
EXPECT_TRUE(capsule->GetPosition().GetX() > xPrevious);
}
}
TEST_F(GenericPhysicsInterfaceTest, GetLinearVelocityAtWorldPoint_FallingRotatingCapsule_EdgeVelocitiesCorrect)
{
auto world = CreateTestWorld();
// create dynamic capsule and start it falling and rotating
auto capsule = AddCapsuleToWorld(world.get(), AZ::Vector3::CreateAxisZ());
float angularVelocityMagnitude = 1.0f;
capsule->SetAngularVelocity(AZ::Vector3::CreateAxisY(angularVelocityMagnitude));
capsule->SetAngularDamping(0.0f);
UpdateWorld(world.get(), 1.0f / 60.0f, 60);
// check the velocities at some points on the rim of the capsule are as expected
for (int i = 0; i < 60; i++)
{
world->Update(1.0f / 60.0f);
auto position = capsule->GetPosition();
float fallingSpeed = capsule->GetLinearVelocity().GetZ();
float radius = 0.5f;
AZ::Vector3 z = AZ::Vector3::CreateAxisZ(radius);
AZ::Vector3 x = AZ::Vector3::CreateAxisX(radius);
auto v1 = capsule->GetLinearVelocityAtWorldPoint(position - z);
auto v2 = capsule->GetLinearVelocityAtWorldPoint(position - x);
auto v3 = capsule->GetLinearVelocityAtWorldPoint(position + x);
EXPECT_TRUE(v1.IsClose(AZ::Vector3(-radius * angularVelocityMagnitude, 0.0f, fallingSpeed)));
EXPECT_TRUE(v2.IsClose(AZ::Vector3(0.0f, 0.0f, fallingSpeed + radius * angularVelocityMagnitude)));
EXPECT_TRUE(v3.IsClose(AZ::Vector3(0.0f, 0.0f, fallingSpeed - radius * angularVelocityMagnitude)));
}
}
TEST_F(GenericPhysicsInterfaceTest, GetPosition_RollingCapsule_OrientationCorrect)
{
auto world = CreateTestWorld();
auto floor = AddStaticFloorToWorld(world.get());
// create dynamic capsule and start it rolling
auto capsule = AddCapsuleToWorld(world.get(), AZ::Vector3::CreateAxisZ());
capsule->SetLinearVelocity(AZ::Vector3::CreateAxisX(5.0f));
capsule->SetAngularVelocity(AZ::Vector3::CreateAxisY(10.0f));
UpdateWorld(world.get(), 1.0f / 60.0f, 60);
// check the capsule orientation evolves as expected
for (int i = 0; i < 60; i++)
{
auto orientationPrevious = capsule->GetOrientation();
float xPrevious = capsule->GetPosition().GetX();
world->Update(1.0f / 60.0f);
float angle = 2.0f * (capsule->GetPosition().GetX() - xPrevious);
EXPECT_TRUE(capsule->GetOrientation().IsClose(orientationPrevious * AZ::Quaternion::CreateRotationY(angle)));
}
}
TEST_F(GenericPhysicsInterfaceTest, OffCenterImpulse_DynamicCapsule_StartsRotating)
{
auto world = CreateTestWorld();
auto floor = AddStaticFloorToWorld(world.get());
AZ::Vector3 posA(0.0f, -5.0f, 1.0f);
AZ::Vector3 posB(0.0f, 0.0f, 1.0f);
AZ::Vector3 posC(0.0f, 5.0f, 1.0f);
auto capsuleA = AddCapsuleToWorld(world.get(), posA);
auto capsuleB = AddCapsuleToWorld(world.get(), posB);
auto capsuleC = AddCapsuleToWorld(world.get(), posC);
// all the capsules should be stationary initially
for (int i = 0; i < 10; i++)
{
world->Update(1.0f / 60.0f);
EXPECT_TRUE(capsuleA->GetPosition().IsClose(posA));
EXPECT_TRUE(capsuleA->GetAngularVelocity().IsClose(AZ::Vector3::CreateZero(), ContactTestTolerance));
EXPECT_TRUE(capsuleB->GetPosition().IsClose(posB));
EXPECT_TRUE(capsuleB->GetAngularVelocity().IsClose(AZ::Vector3::CreateZero(), ContactTestTolerance));
EXPECT_TRUE(capsuleC->GetPosition().IsClose(posC));
EXPECT_TRUE(capsuleC->GetAngularVelocity().IsClose(AZ::Vector3::CreateZero(), ContactTestTolerance));
}
// apply off-center impulses to capsule A and C, and an impulse through the center of B
AZ::Vector3 impulse(0.0f, 0.0f, 10.0f);
capsuleA->ApplyLinearImpulseAtWorldPoint(impulse, posA + AZ::Vector3::CreateAxisX(0.5f));
capsuleB->ApplyLinearImpulseAtWorldPoint(impulse, posB);
capsuleC->ApplyLinearImpulseAtWorldPoint(impulse, posC + AZ::Vector3::CreateAxisX(-0.5f));
// A and C should be rotating in opposite directions, B should still have 0 angular velocity
for (int i = 0; i < 30; i++)
{
world->Update(1.0f / 60.0f);
EXPECT_TRUE(capsuleA->GetAngularVelocity().GetY() < 0.0f);
EXPECT_TRUE(capsuleB->GetAngularVelocity().IsClose(AZ::Vector3::CreateZero(), ContactTestTolerance));
EXPECT_TRUE(capsuleC->GetAngularVelocity().GetY() > 0.0f);
}
}
TEST_F(GenericPhysicsInterfaceTest, ApplyAngularImpulse_DynamicSphere_StartsRotating)
{
auto world = CreateTestWorld();
auto floor = AddStaticFloorToWorld(world.get());
AZStd::shared_ptr<RigidBody> spheres[3];
for (int i = 0; i < 3; i++)
{
spheres[i] = AddSphereToWorld(world.get(), AZ::Vector3(0.0f, -5.0f + 5.0f * i, 1.0f));
}
// all the spheres should start stationary
UpdateWorld(world.get(), 1.0f / 60.0f, 10);
for (int i = 0; i < 3; i++)
{
EXPECT_TRUE(spheres[i]->GetAngularVelocity().IsClose(AZ::Vector3::CreateZero()));
}
// apply angular impulses and they should gain angular velocity parallel to the impulse direction
AZ::Vector3 impulses[3] = { AZ::Vector3(2.0f, 4.0f, 0.0f), AZ::Vector3(-3.0f, 1.0f, 0.0f),
AZ::Vector3(-2.0f, 3.0f, 0.0f) };
for (int i = 0; i < 3; i++)
{
spheres[i]->ApplyAngularImpulse(impulses[i]);
}
UpdateWorld(world.get(), 1.0f / 60.0f, 10);
for (int i = 0; i < 3; i++)
{
auto angVel = spheres[i]->GetAngularVelocity();
EXPECT_TRUE(angVel.GetProjected(impulses[i]).IsClose(angVel, 0.1f));
}
}
TEST_F(GenericPhysicsInterfaceTest, StartAsleep_FallingBox_DoesNotFall)
{
auto world = CreateTestWorld();
// Box should start asleep
RigidBodyConfiguration config;
config.m_startAsleep = true;
// Create rigid body
AZStd::shared_ptr<RigidBody> box;
SystemRequestBus::BroadcastResult(box, &SystemRequests::CreateRigidBody, config);
world->AddBody(*box);
UpdateWorld(world.get(), 1.0f / 60.0f, 100);
// Check the box is still at 0 and hasn't dropped
EXPECT_NEAR(0.0f, box->GetPosition().GetZ(), 0.01f);
}
TEST_F(GenericPhysicsInterfaceTest, ForceAsleep_FallingBox_BecomesStationary)
{
auto world = CreateTestWorld();
auto floor = AddStaticFloorToWorld(world.get());
auto box = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 0.0f, 10.0f));
UpdateWorld(world.get(), 1.0f / 60.0f, 60);
EXPECT_TRUE(box->IsAwake());
auto pos = box->GetPosition();
box->ForceAsleep();
EXPECT_FALSE(box->IsAwake());
UpdateWorld(world.get(), 1.0f / 60.0f, 30);
EXPECT_FALSE(box->IsAwake());
// the box should be asleep so it shouldn't have moved
EXPECT_TRUE(box->GetPosition().IsClose(pos));
}
TEST_F(GenericPhysicsInterfaceTest, ForceAwake_SleepingBox_SleepStateCorrect)
{
auto world = CreateTestWorld();
auto floor = AddStaticFloorToWorld(world.get());
auto box = AddUnitBoxToWorld(world.get(), AZ::Vector3(0.0f, 0.0f, 1.0f));
UpdateWorld(world.get(), 1.0f / 60.0f, 60);
EXPECT_FALSE(box->IsAwake());
box->ForceAwake();
EXPECT_TRUE(box->IsAwake());
UpdateWorld(world.get(), 1.0f / 60.0f, 60);
// the box should have gone back to sleep
EXPECT_FALSE(box->IsAwake());
}
TEST_F(GenericPhysicsInterfaceTest, GetAabb_Box_ValidExtents)
{
auto world = CreateTestWorld();
AZ::Vector3 posBox(0.0f, 0.0f, 0.0f);
auto box = AddUnitBoxToWorld(world.get(), posBox);
EXPECT_TRUE(box->GetAabb().GetMin().IsClose(posBox - 0.5f * AZ::Vector3::CreateOne()));
EXPECT_TRUE(box->GetAabb().GetMax().IsClose(posBox + 0.5f * AZ::Vector3::CreateOne()));
// rotate the box and check the bounding box is still correct
AZ::Quaternion quat = AZ::Quaternion::CreateRotationZ(0.25f * AZ::Constants::Pi);
box->SetTransform(AZ::Transform::CreateFromQuaternionAndTranslation(quat, posBox));
AZ::Vector3 boxExtent(sqrtf(0.5f), sqrtf(0.5f), 0.5f);
EXPECT_TRUE(box->GetAabb().GetMin().IsClose(posBox - boxExtent));
EXPECT_TRUE(box->GetAabb().GetMax().IsClose(posBox + boxExtent));
}
TEST_F(GenericPhysicsInterfaceTest, GetAabb_Sphere_ValidExtents)
{
auto world = CreateTestWorld();
AZ::Vector3 posSphere(0.0f, 0.0f, 0.0f);
auto sphere = AddSphereToWorld(world.get(), posSphere);
EXPECT_TRUE(sphere->GetAabb().GetMin().IsClose(posSphere - 0.5f * AZ::Vector3::CreateOne()));
EXPECT_TRUE(sphere->GetAabb().GetMax().IsClose(posSphere + 0.5f * AZ::Vector3::CreateOne()));
// rotate the sphere and check the bounding box is still correct
AZ::Quaternion quat = AZ::Quaternion::CreateRotationZ(0.25f * AZ::Constants::Pi);
sphere->SetTransform(AZ::Transform::CreateFromQuaternionAndTranslation(quat, posSphere));
EXPECT_TRUE(sphere->GetAabb().GetMin().IsClose(posSphere - 0.5f * AZ::Vector3::CreateOne()));
EXPECT_TRUE(sphere->GetAabb().GetMax().IsClose(posSphere + 0.5f * AZ::Vector3::CreateOne()));
}
TEST_F(GenericPhysicsInterfaceTest, GetAabb_Capsule_ValidExtents)
{
auto world = CreateTestWorld();
AZ::Vector3 posCapsule(0.0f, 0.0f, 0.0f);
auto capsule = AddCapsuleToWorld(world.get(), posCapsule);
EXPECT_TRUE(capsule->GetAabb().GetMin().IsClose(posCapsule - AZ::Vector3(0.5f, 1.0f, 0.5f)));
EXPECT_TRUE(capsule->GetAabb().GetMax().IsClose(posCapsule + AZ::Vector3(0.5f, 1.0f, 0.5f)));
// rotate the bodies and check the bounding boxes are still correct
AZ::Quaternion quat = AZ::Quaternion::CreateRotationZ(0.25f * AZ::Constants::Pi);
capsule->SetTransform(AZ::Transform::CreateFromQuaternionAndTranslation(quat, posCapsule));
AZ::Vector3 capsuleExtent(0.5f + sqrt(0.125f), 0.5f + sqrt(0.125f), 0.5f);
EXPECT_TRUE(capsule->GetAabb().GetMin().IsClose(posCapsule - capsuleExtent));
EXPECT_TRUE(capsule->GetAabb().GetMax().IsClose(posCapsule + capsuleExtent));
}
TEST_F(GenericPhysicsInterfaceTest, Materials_BoxesSharingDefaultMaterial_JumpingSameHeight)
{
auto world = CreateTestWorld();
auto boxA = AddStaticFloorToWorld(world.get());
auto boxB = AddUnitBoxToWorld(world.get(), AZ::Vector3(1.0f, 0.0f, 10.0f));
auto boxC = AddUnitBoxToWorld(world.get(), AZ::Vector3(-1.0f, 0.0f, 10.0f));
auto material = boxC->GetShape(0)->GetMaterial();
material->SetRestitution(1.0f);
UpdateWorld(world.get(), 1.0f / 60.0f, 150);
// boxB and boxC should have the same material (default)
// so they should both bounce high
EXPECT_NEAR(boxB->GetPosition().GetZ(), boxC->GetPosition().GetZ(), 0.5f);
}
TEST_F(GenericPhysicsInterfaceTest, World_GetNativePtrByWorldName_ReturnsNativePtr)
{
void* validNativePtr = nullptr;
WorldRequestBus::EventResult(validNativePtr, Physics::DefaultPhysicsWorldId, &WorldRequests::GetNativePointer);
EXPECT_TRUE(validNativePtr != nullptr);
void* invalidNativePtr = nullptr;
WorldRequestBus::EventResult(invalidNativePtr, AZ_CRC("Bad World Name"), &WorldRequests::GetNativePointer);
EXPECT_TRUE(invalidNativePtr == nullptr);
}
TEST_F(GenericPhysicsInterfaceTest, Collider_ColliderTag_IsSetFromConfiguration)
{
const AZStd::string colliderTagName = "ColliderTestTag";
Physics::ColliderConfiguration colliderConfig;
colliderConfig.m_tag = colliderTagName;
Physics::SphereShapeConfiguration shapeConfig;
AZStd::shared_ptr<Physics::Shape> shape;
SystemRequestBus::BroadcastResult(shape, &SystemRequests::CreateShape, colliderConfig, shapeConfig);
EXPECT_EQ(shape->GetTag(), AZ::Crc32(colliderTagName));
}
} // namespace Physics