/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include namespace NvCloth { namespace Internal { extern const char* const StatusMessageSelectNode; extern const char* const StatusMessageNoAsset; extern const char* const StatusMessageNoClothNodes; } } namespace UnitTest { //! Sets up a mock global environment to //! change between server and client. class NvClothEditorClothComponent : public ::testing::Test { public: const AZStd::string JointRootName = "root_node"; const AZStd::string MeshNodeName = "cloth_mesh_node"; const AZStd::vector MeshVertices = {{ AZ::Vector3(-1.0f, 0.0f, 0.0f), AZ::Vector3(1.0f, 0.0f, 0.0f), AZ::Vector3(0.0f, 1.0f, 0.0f) }}; const AZStd::vector MeshIndices = {{ 0, 1, 2 }}; const AZStd::vector MeshUVs = {{ AZ::Vector2(0.0f, 0.0f), AZ::Vector2(1.0f, 0.0f), AZ::Vector2(0.5f, 1.0f) }}; // [inverse mass, motion constrain radius, backstop offset, backstop radius] const AZStd::vector MeshClothData = {{ AZ::Color(0.75f, 0.6f, 0.5f, 0.1f), AZ::Color(1.0f, 0.16f, 0.1f, 1.0f), AZ::Color(0.25f, 1.0f, 0.9f, 0.5f) }}; const AZ::u32 LodLevel = 0; static void SetUpTestCase(); static void TearDownTestCase(); protected: AZStd::unique_ptr CreateInactiveEditorEntity(const char* entityName); AZStd::unique_ptr CreateActiveGameEntityFromEditorEntity(AZ::Entity* editorEntity); private: static AZStd::unique_ptr s_mockGEnv; static SSystemGlobalEnvironment* s_previousGEnv; }; AZStd::unique_ptr NvClothEditorClothComponent::s_mockGEnv; SSystemGlobalEnvironment* NvClothEditorClothComponent::s_previousGEnv = nullptr; void NvClothEditorClothComponent::SetUpTestCase() { // override global environment s_previousGEnv = gEnv; s_mockGEnv = AZStd::make_unique(); gEnv = s_mockGEnv.get(); #if !defined(CONSOLE) // Set environment to not be a server by default. gEnv->SetIsDedicated(false); #endif } void NvClothEditorClothComponent::TearDownTestCase() { // restore global environment gEnv = s_previousGEnv; s_mockGEnv.reset(); s_previousGEnv = nullptr; } AZStd::unique_ptr NvClothEditorClothComponent::CreateInactiveEditorEntity(const char* entityName) { AZ::Entity* entity = nullptr; UnitTest::CreateDefaultEditorEntity(entityName, &entity); entity->Deactivate(); return AZStd::unique_ptr(entity); } AZStd::unique_ptr NvClothEditorClothComponent::CreateActiveGameEntityFromEditorEntity(AZ::Entity* editorEntity) { auto gameEntity = AZStd::make_unique(); AzToolsFramework::ToolsApplicationRequestBus::Broadcast( &AzToolsFramework::ToolsApplicationRequests::PreExportEntity, *editorEntity, *gameEntity); gameEntity->Init(); gameEntity->Activate(); return gameEntity; } TEST_F(NvClothEditorClothComponent, EditorClothComponent_DependencyMissing_EntityIsInvalid) { auto entity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); entity->CreateComponent(); // the entity should not be in a valid state because the cloth component requires a mesh or an actor component AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); EXPECT_FALSE(sortOutcome.IsSuccess()); EXPECT_TRUE(sortOutcome.GetError().m_code == AZ::Entity::DependencySortResult::MissingRequiredService); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_ActorDependencySatisfied_EntityIsValid) { auto entity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); entity->CreateComponent(); entity->CreateComponent(); // the entity should be in a valid state because the cloth component requirement is satisfied AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); EXPECT_TRUE(sortOutcome.IsSuccess()); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_MultipleClothComponents_EntityIsValid) { auto entity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); entity->CreateComponent(); entity->CreateComponent(); // the cloth component should be compatible with multiple cloth components entity->CreateComponent(); entity->CreateComponent(); // the entity should be in a valid state because the cloth component requirement is satisfied AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); EXPECT_TRUE(sortOutcome.IsSuccess()); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_ClothWithActor_CorrectRuntimeComponents) { // create an editor entity with a cloth component and an actor component auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); editorEntity->CreateComponent(); editorEntity->CreateComponent(); editorEntity->Activate(); auto gameEntity = CreateActiveGameEntityFromEditorEntity(editorEntity.get()); // check that the runtime entity has the expected components EXPECT_TRUE(gameEntity->FindComponent() != nullptr); EXPECT_TRUE(gameEntity->FindComponent() != nullptr); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_OnActivationNoMeshCreated_ReturnsMeshNodeListWithNoAssetMessage) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); editorEntity->CreateComponent(); editorEntity->Activate(); const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); ASSERT_EQ(meshNodeList.size(), 1); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageNoAsset); } // [TODO LYN-1891] // Revisit when Cloth Component Mesh works with Actors adapted to Atom models. // Editor Cloth component now uses the new AZ::Render::MeshComponentNotificationBus::OnModelReady // notification and this test does not setup a model yet. TEST_F(NvClothEditorClothComponent, DISABLED_EditorClothComponent_OnMeshCreatedWithEmptyActor_ReturnsMeshNodeListWithNoClothMessage) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); auto* editorActorComponent = editorEntity->CreateComponent(); editorEntity->Activate(); { auto actor = AZStd::make_unique("actor_test"); actor->FinishSetup(); editorActorComponent->SetActorAsset(CreateAssetFromActor(AZStd::move(actor))); } const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); ASSERT_EQ(meshNodeList.size(), 1); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageNoClothNodes); } // [TODO LYN-1891] // Revisit when Cloth Component Mesh works with Actors adapted to Atom models. // Editor Cloth component now uses the new AZ::Render::MeshComponentNotificationBus::OnModelReady // notification and this test does not setup a model yet. TEST_F(NvClothEditorClothComponent, DISABLED_EditorClothComponent_OnMeshCreatedWithActorWithoutClothMesh_ReturnsMeshNodeListWithNoClothMessage) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); auto* editorActorComponent = editorEntity->CreateComponent(); editorEntity->Activate(); { auto actor = AZStd::make_unique("actor_test"); auto jointRootIndex = actor->AddJoint(JointRootName); actor->SetMesh(LodLevel, jointRootIndex, CreateEMotionFXMesh(MeshVertices, MeshIndices, {}, MeshUVs)); actor->FinishSetup(); editorActorComponent->SetActorAsset(CreateAssetFromActor(AZStd::move(actor))); } const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); ASSERT_EQ(meshNodeList.size(), 1); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageNoClothNodes); } // [TODO LYN-1891] // Revisit when Cloth Component Mesh works with Actors adapted to Atom models. // Editor Cloth component now uses the new AZ::Render::MeshComponentNotificationBus::OnModelReady // notification and this test does not setup a model yet. TEST_F(NvClothEditorClothComponent, DISABLED_EditorClothComponent_OnMeshCreatedWithActorWithClothMesh_ReturnsValidMeshNodeList) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); auto* editorActorComponent = editorEntity->CreateComponent(); editorEntity->Activate(); { auto actor = AZStd::make_unique("actor_test"); actor->AddJoint(JointRootName); auto meshNodeIndex = actor->AddJoint(MeshNodeName, AZ::Transform::CreateIdentity(), JointRootName); actor->SetMesh(LodLevel, meshNodeIndex, CreateEMotionFXMesh(MeshVertices, MeshIndices, {}, MeshUVs/*, MeshClothData*/)); actor->FinishSetup(); editorActorComponent->SetActorAsset(CreateAssetFromActor(AZStd::move(actor))); } const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); ASSERT_EQ(meshNodeList.size(), 2); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageSelectNode); EXPECT_TRUE(meshNodeList[1] == MeshNodeName); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_OnMeshCreatedWithActorWithNoBackstop_ReturnsEmptyMeshNodesWithBackstopData) { // [inverse mass, motion constrain radius, backstop offset, backstop radius] const AZStd::vector meshClothDataNoBackstop = {{ AZ::Color(0.75f, 1.0f, 0.5f, 0.0f), AZ::Color(1.0f, 1.0f, 0.5f, 0.0f), AZ::Color(0.25f, 1.0f, 0.5f, 0.0f) }}; auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); auto* editorActorComponent = editorEntity->CreateComponent(); editorEntity->Activate(); { auto actor = AZStd::make_unique("actor_test"); actor->AddJoint(JointRootName); auto meshNodeIndex = actor->AddJoint(MeshNodeName, AZ::Transform::CreateIdentity(), JointRootName); actor->SetMesh(LodLevel, meshNodeIndex, CreateEMotionFXMesh(MeshVertices, MeshIndices, {}, MeshUVs/*, meshClothDataNoBackstop*/)); actor->FinishSetup(); editorActorComponent->SetActorAsset(CreateAssetFromActor(AZStd::move(actor))); } const auto& meshNodesWithBackstopData = editorClothComponent->GetMeshNodesWithBackstopData(); EXPECT_TRUE(meshNodesWithBackstopData.empty()); } // [TODO LYN-1891] // Revisit when Cloth Component Mesh works with Actors adapted to Atom models. // Editor Cloth component now uses the new AZ::Render::MeshComponentNotificationBus::OnModelReady // notification and this test does not setup a model yet. TEST_F(NvClothEditorClothComponent, DISABLED_EditorClothComponent_OnMeshCreatedWithActorWithBackstop_ReturnsValidMeshNodesWithBackstopData) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); auto* editorActorComponent = editorEntity->CreateComponent(); editorEntity->Activate(); { auto actor = AZStd::make_unique("actor_test"); actor->AddJoint(JointRootName); auto meshNodeIndex = actor->AddJoint(MeshNodeName, AZ::Transform::CreateIdentity(), JointRootName); actor->SetMesh(LodLevel, meshNodeIndex, CreateEMotionFXMesh(MeshVertices, MeshIndices, {}, MeshUVs/*, MeshClothData*/)); actor->FinishSetup(); editorActorComponent->SetActorAsset(CreateAssetFromActor(AZStd::move(actor))); } const auto& meshNodesWithBackstopData = editorClothComponent->GetMeshNodesWithBackstopData(); EXPECT_EQ(meshNodesWithBackstopData.size(), 1); EXPECT_TRUE(meshNodesWithBackstopData.find(MeshNodeName) != meshNodesWithBackstopData.end()); } // [TODO LYN-1891] // Revisit when Cloth Component Mesh works with Actors adapted to Atom models. // Editor Cloth component now uses the new AZ::Render::MeshComponentNotificationBus::OnModelReady // notification and this test does not setup a model yet. TEST_F(NvClothEditorClothComponent, DISABLED_EditorClothComponent_OnModelPreDestroy_ReturnsMeshNodeListWithNoAssetMessage) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); auto* editorActorComponent = editorEntity->CreateComponent(); editorEntity->Activate(); { auto actor = AZStd::make_unique("actor_test"); actor->AddJoint(JointRootName); auto meshNodeIndex = actor->AddJoint(MeshNodeName, AZ::Transform::CreateIdentity(), JointRootName); actor->SetMesh(LodLevel, meshNodeIndex, CreateEMotionFXMesh(MeshVertices, MeshIndices, {}, MeshUVs/*, MeshClothData*/)); actor->FinishSetup(); editorActorComponent->SetActorAsset(CreateAssetFromActor(AZStd::move(actor))); } editorClothComponent->OnModelPreDestroy(); const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); const auto& meshNodesWithBackstopData = editorClothComponent->GetMeshNodesWithBackstopData(); ASSERT_EQ(meshNodeList.size(), 1); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageNoAsset); EXPECT_TRUE(meshNodesWithBackstopData.empty()); } } // namespace UnitTest