/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using ::testing::NiceMock; using ::testing::Return; namespace PhysXEditorTests { AZStd::vector GetSamples() { AZStd::vector samples{ { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 0 }, { 2.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 1 }, { 1.5f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 2 }, { 1.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 0 }, { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 1 }, { 1.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 2 }, { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 0 }, { 0.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 1 }, { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 2 } }; return samples; } AZStd::vector GetMaterialList() { AZStd::vector materials{ {Physics::MaterialId::FromUUID("{EC976D51-2C26-4C1E-BBF2-75BAAAFA162C}")}, {Physics::MaterialId::FromUUID("{B9836F51-A235-4781-95E3-A6302BEE9EFF}")}, {Physics::MaterialId::FromUUID("{7E060707-BB03-47EB-B046-4503C7145B6E}")} }; return materials; } EntityPtr SetupHeightfieldComponent() { // create an editor entity with a shape collider component and a box shape component EntityPtr editorEntity = CreateInactiveEditorEntity("HeightfieldColliderComponentEditorEntity"); editorEntity->CreateComponent(); editorEntity->CreateComponent(LmbrCentral::EditorAxisAlignedBoxShapeComponentTypeId); editorEntity->CreateComponent(); AZ::ComponentApplicationBus::Broadcast( &AZ::ComponentApplicationRequests::RegisterComponentDescriptor, UnitTest::MockPhysXHeightfieldProviderComponent::CreateDescriptor()); return editorEntity; } void CleanupHeightfieldComponent() { AZ::ComponentApplicationBus::Broadcast( &AZ::ComponentApplicationRequests::UnregisterComponentDescriptor, UnitTest::MockPhysXHeightfieldProviderComponent::CreateDescriptor()); } void SetupMockMethods(NiceMock& mockShapeRequests) { ON_CALL(mockShapeRequests, GetHeightfieldTransform).WillByDefault(Return(AZ::Transform::CreateTranslation({ 1, 2, 0 }))); ON_CALL(mockShapeRequests, GetHeightfieldGridSpacing).WillByDefault(Return(AZ::Vector2(1, 1))); ON_CALL(mockShapeRequests, GetHeightsAndMaterials).WillByDefault(Return(GetSamples())); ON_CALL(mockShapeRequests, GetHeightfieldGridSize) .WillByDefault( [](int32_t& numColumns, int32_t& numRows) { numColumns = 3; numRows = 3; }); ON_CALL(mockShapeRequests, GetHeightfieldHeightBounds) .WillByDefault( [](float& x, float& y) { x = -3.0f; y = 3.0f; }); ON_CALL(mockShapeRequests, GetMaterialList).WillByDefault(Return(GetMaterialList())); } EntityPtr TestCreateActiveGameEntityFromEditorEntity(AZ::Entity* editorEntity) { EntityPtr gameEntity = AZStd::make_unique(); AzToolsFramework::ToolsApplicationRequestBus::Broadcast( &AzToolsFramework::ToolsApplicationRequests::PreExportEntity, *editorEntity, *gameEntity); gameEntity->Init(); return gameEntity; } class PhysXEditorHeightfieldFixture : public PhysXEditorFixture { public: void SetUp() override { PhysXEditorFixture::SetUp(); PopulateDefaultMaterialLibrary(); m_editorEntity = SetupHeightfieldComponent(); m_editorMockShapeRequests = AZStd::make_unique>(m_editorEntity->GetId()); SetupMockMethods(*m_editorMockShapeRequests.get()); m_editorEntity->Activate(); m_gameEntity = TestCreateActiveGameEntityFromEditorEntity(m_editorEntity.get()); m_gameMockShapeRequests = AZStd::make_unique>(m_gameEntity->GetId()); SetupMockMethods(*m_gameMockShapeRequests.get()); m_gameEntity->Activate(); } void TearDown() override { CleanupHeightfieldComponent(); m_editorEntity = nullptr; m_gameEntity = nullptr; m_editorMockShapeRequests = nullptr; m_gameMockShapeRequests = nullptr; PhysXEditorFixture::TearDown(); } void PopulateDefaultMaterialLibrary() { AZ::Data::AssetId assetId = AZ::Data::AssetId(AZ::Uuid::Create()); // Create an asset out of our Script Event Physics::MaterialLibraryAsset* matLibAsset = aznew Physics::MaterialLibraryAsset; { const AZStd::vector matIds = GetMaterialList(); for (const Physics::MaterialId& matId : matIds) { Physics::MaterialFromAssetConfiguration matConfig; matConfig.m_id = matId; matConfig.m_configuration.m_surfaceType = matId.GetUuid().ToString(); matLibAsset->AddMaterialData(matConfig); } } // Note: There is no interface to simply update material library asset. It has to go via updating the entire configuration which causes assets reloading. // It makes sense as a safety mechanism in the Editor but makes it harder to write tests. // Hence have to work around it via const_cast here to be able to simply set the generated asset into configuration. AzPhysics::SystemConfiguration* sysConfig = const_cast(AZ::Interface::Get()->GetConfiguration()); AZ::Data::Asset assetData(assetId, matLibAsset, AZ::Data::AssetLoadBehavior::Default); sysConfig->m_materialLibraryAsset = assetData; } Physics::Material* GetMaterialFromRaycast(float x, float y) { AzPhysics::RayCastRequest request; request.m_start = AZ::Vector3(x, y, 5.0f); request.m_direction = AZ::Vector3(0.0f, 0.0f, -1.0f); request.m_distance = 10.0f; //query the scene auto* sceneInterface = AZ::Interface::Get(); AzPhysics::SceneQueryHits result = sceneInterface->QueryScene(m_defaultSceneHandle, &request); EXPECT_EQ(result.m_hits.size(), 1); if (result) { return result.m_hits[0].m_material; } return nullptr; }; EntityPtr m_editorEntity; EntityPtr m_gameEntity; AZStd::unique_ptr> m_editorMockShapeRequests; AZStd::unique_ptr> m_gameMockShapeRequests; }; TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentDependenciesSatisfiedEntityIsValid) { EntityPtr entity = CreateInactiveEditorEntity("HeightfieldColliderComponentEditorEntity"); entity->CreateComponent(); entity->CreateComponent(LmbrCentral::EditorAxisAlignedBoxShapeComponentTypeId); entity->CreateComponent()->CreateDescriptor(); // the entity should be in a valid state because the shape component and // the Terrain Physics Collider Component requirement is satisfied. AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); EXPECT_TRUE(sortOutcome.IsSuccess()); } TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentDependenciesMissingEntityIsInvalid) { EntityPtr entity = CreateInactiveEditorEntity("HeightfieldColliderComponentEditorEntity"); entity->CreateComponent(); // the entity should not be in a valid state because the heightfield collider component requires // a shape component and the Terrain Physics Collider Component AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); EXPECT_FALSE(sortOutcome.IsSuccess()); EXPECT_TRUE(sortOutcome.GetError().m_code == AZ::Entity::DependencySortResult::MissingRequiredService); } TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentMultipleHeightfieldColliderComponentsEntityIsInvalid) { EntityPtr entity = CreateInactiveEditorEntity("HeightfieldColliderComponentEditorEntity"); entity->CreateComponent(); entity->CreateComponent(LmbrCentral::EditorAxisAlignedBoxShapeComponentTypeId); // adding a second heightfield collider component should make the entity invalid entity->CreateComponent(); AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); EXPECT_FALSE(sortOutcome.IsSuccess()); EXPECT_TRUE(sortOutcome.GetError().m_code == AZ::Entity::DependencySortResult::HasIncompatibleServices); } TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentHeightfieldColliderWithCorrectComponentsCorrectRuntimeComponents) { EntityPtr editorEntity = SetupHeightfieldComponent(); NiceMock mockShapeRequests(editorEntity->GetId()); SetupMockMethods(mockShapeRequests); editorEntity->Activate(); EntityPtr gameEntity = TestCreateActiveGameEntityFromEditorEntity(editorEntity.get()); NiceMock mockShapeRequests2(gameEntity->GetId()); SetupMockMethods(mockShapeRequests2); gameEntity->Activate(); // check that the runtime entity has the expected components EXPECT_TRUE(gameEntity->FindComponent() != nullptr); EXPECT_TRUE(gameEntity->FindComponent() != nullptr); EXPECT_TRUE(gameEntity->FindComponent(LmbrCentral::AxisAlignedBoxShapeComponentTypeId) != nullptr); CleanupHeightfieldComponent(); } TEST_F(PhysXEditorHeightfieldFixture, EditorHeightfieldColliderComponentHeightfieldColliderWithAABoxCorrectRuntimeGeometry) { AZ::EntityId gameEntityId = m_gameEntity->GetId(); AzPhysics::SimulatedBody* staticBody = nullptr; AzPhysics::SimulatedBodyComponentRequestsBus::EventResult( staticBody, gameEntityId, &AzPhysics::SimulatedBodyComponentRequests::GetSimulatedBody); const auto* pxRigidStatic = static_cast(staticBody->GetNativePointer()); PHYSX_SCENE_READ_LOCK(pxRigidStatic->getScene()); // there should be a single shape on the rigid body and it should be a heightfield EXPECT_EQ(pxRigidStatic->getNbShapes(), 1); physx::PxShape* shape = nullptr; pxRigidStatic->getShapes(&shape, 1, 0); EXPECT_EQ(shape->getGeometryType(), physx::PxGeometryType::eHEIGHTFIELD); physx::PxHeightFieldGeometry heightfieldGeometry; shape->getHeightFieldGeometry(heightfieldGeometry); physx::PxHeightField* heightfield = heightfieldGeometry.heightField; int32_t numRows{ 0 }; int32_t numColumns{ 0 }; Physics::HeightfieldProviderRequestsBus::Event( gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); EXPECT_EQ(numColumns, heightfield->getNbColumns()); EXPECT_EQ(numRows, heightfield->getNbRows()); for (int sampleRow = 0; sampleRow < numRows; ++sampleRow) { for (int sampleColumn = 0; sampleColumn < numColumns; ++sampleColumn) { float minHeightBounds{ 0.0f }; float maxHeightBounds{ 0.0f }; Physics::HeightfieldProviderRequestsBus::Event( gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, maxHeightBounds); AZStd::vector samples; Physics::HeightfieldProviderRequestsBus::EventResult( samples, gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f }; const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits::max() / halfBounds; physx::PxHeightFieldSample samplePhysX = heightfield->getSample(sampleRow, sampleColumn); Physics::HeightMaterialPoint samplePhysics = samples[sampleRow * numColumns + sampleColumn]; EXPECT_EQ(samplePhysX.height, azlossy_cast(samplePhysics.m_height * scaleFactor)); } } } TEST_F(PhysXEditorHeightfieldFixture, EditorHeightfieldColliderComponentHeightfieldColliderCorrectMaterials) { AZ::EntityId gameEntityId = m_gameEntity->GetId(); int32_t numRows{ 0 }; int32_t numColumns{ 0 }; Physics::HeightfieldProviderRequestsBus::Event( gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); EXPECT_EQ(numRows, 3); EXPECT_EQ(numColumns, 3); AZStd::vector samples; Physics::HeightfieldProviderRequestsBus::EventResult( samples, gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); AzPhysics::SimulatedBody* staticBody = nullptr; AzPhysics::SimulatedBodyComponentRequestsBus::EventResult( staticBody, gameEntityId, &AzPhysics::SimulatedBodyComponentRequests::GetSimulatedBody); const auto* pxRigidStatic = static_cast(staticBody->GetNativePointer()); PHYSX_SCENE_READ_LOCK(pxRigidStatic->getScene()); physx::PxShape* shape = nullptr; pxRigidStatic->getShapes(&shape, 1, 0); physx::PxHeightFieldGeometry heightfieldGeometry; shape->getHeightFieldGeometry(heightfieldGeometry); physx::PxHeightField* heightfield = heightfieldGeometry.heightField; AZStd::vector physicsSurfaceTypes; for (Physics::MaterialId materialId : GetMaterialList()) { physicsSurfaceTypes.emplace_back(materialId.GetUuid().ToString()); } // PhysX Heightfield cooking doesn't map 1-1 sample material indices to triangle material indices // Hence hardcoding the expected material indices in the test const AZStd::array physicsMaterialsValidationDataIndex = {0, 2, 1, 1}; for (int sampleRow = 0; sampleRow < numRows; ++sampleRow) { for (int sampleColumn = 0; sampleColumn < numColumns; ++sampleColumn) { physx::PxHeightFieldSample samplePhysX = heightfield->getSample(sampleRow, sampleColumn); auto [materialIndex0, materialIndex1] = PhysX::Utils::GetPhysXMaterialIndicesFromHeightfieldSamples(samples, sampleRow, sampleColumn, numRows, numColumns); EXPECT_EQ(samplePhysX.materialIndex0, materialIndex0); EXPECT_EQ(samplePhysX.materialIndex1, materialIndex1); if (sampleRow != numRows - 1 && sampleColumn != numColumns - 1) { const float x_offset = -0.25f; const float y_offset = 0.75f; const float secondRayOffset = 0.5f; float rayX = x_offset + sampleColumn; float rayY = y_offset + sampleRow; Physics::Material* mat1 = GetMaterialFromRaycast(rayX, rayY); EXPECT_NE(mat1, nullptr); Physics::Material* mat2 = GetMaterialFromRaycast(rayX + secondRayOffset, rayY + secondRayOffset); EXPECT_NE(mat2, nullptr); if (mat1) { AZStd::string expectedMaterialName = physicsSurfaceTypes[physicsMaterialsValidationDataIndex[sampleRow * 2 + sampleColumn]]; EXPECT_EQ(mat1->GetSurfaceTypeName(), expectedMaterialName); } } } } } } // namespace PhysXEditorTests