/* * 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 #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 { namespace { const AZ::Uuid EditorActorComponentTypeId = "{A863EE1B-8CFD-4EDD-BA0D-1CEC2879AD44}"; const AZ::Uuid ActorComponentTypeId = "{BDC97E7F-A054-448B-A26F-EA2B5D78E377}"; } //! 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(EditorActorComponentTypeId); // 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(EditorActorComponentTypeId); // 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(EditorActorComponentTypeId); 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(ActorComponentTypeId) != nullptr); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_OnActivationNoMeshCreated_ReturnsMeshNodeListWithNoAssetMessage) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); editorEntity->CreateComponent(EditorActorComponentTypeId); editorEntity->Activate(); const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); EXPECT_EQ(meshNodeList.size(), 1); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageNoAsset); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_OnMeshCreatedWithEmptyActor_ReturnsMeshNodeListWithNoClothMessage) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); editorEntity->CreateComponent(EditorActorComponentTypeId); editorEntity->Activate(); { auto actor = AZStd::make_unique("actor_test"); actor->FinishSetup(); auto actorAsset = CreateAssetFromActor(AZStd::move(actor)); EMotionFX::Integration::ActorComponentRequestBus::Event(editorEntity->GetId(), &EMotionFX::Integration::ActorComponentRequestBus::Handler::SetActorAsset, actorAsset.GetId()); } const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); EXPECT_EQ(meshNodeList.size(), 1); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageNoClothNodes); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_OnMeshCreatedWithActorWithoutClothMesh_ReturnsMeshNodeListWithNoClothMessage) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); editorEntity->CreateComponent(EditorActorComponentTypeId); editorEntity->Activate(); { auto actor = AZStd::make_unique("actor_test"); auto jointRootIndex = actor->AddJoint(JointRootName); actor->SetMesh(LodLevel, jointRootIndex, CreateEMotionFXMesh(jointRootIndex, MeshVertices, MeshIndices, {}, MeshUVs)); actor->FinishSetup(); auto actorAsset = CreateAssetFromActor(AZStd::move(actor)); EMotionFX::Integration::ActorComponentRequestBus::Event(editorEntity->GetId(), &EMotionFX::Integration::ActorComponentRequestBus::Handler::SetActorAsset, actorAsset.GetId()); } const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); EXPECT_EQ(meshNodeList.size(), 1); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageNoClothNodes); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_OnMeshCreatedWithActorWithClothMesh_ReturnsValidMeshNodeList) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); editorEntity->CreateComponent(EditorActorComponentTypeId); 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(meshNodeIndex, MeshVertices, MeshIndices, {}, MeshUVs, MeshClothData)); actor->FinishSetup(); auto actorAsset = CreateAssetFromActor(AZStd::move(actor)); EMotionFX::Integration::ActorComponentRequestBus::Event(editorEntity->GetId(), &EMotionFX::Integration::ActorComponentRequestBus::Handler::SetActorAsset, actorAsset.GetId()); } const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); EXPECT_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(); editorEntity->CreateComponent(EditorActorComponentTypeId); 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(meshNodeIndex, MeshVertices, MeshIndices, {}, MeshUVs, meshClothDataNoBackstop)); actor->FinishSetup(); auto actorAsset = CreateAssetFromActor(AZStd::move(actor)); EMotionFX::Integration::ActorComponentRequestBus::Event(editorEntity->GetId(), &EMotionFX::Integration::ActorComponentRequestBus::Handler::SetActorAsset, actorAsset.GetId()); } const auto& meshNodesWithBackstopData = editorClothComponent->GetMeshNodesWithBackstopData(); EXPECT_TRUE(meshNodesWithBackstopData.empty()); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_OnMeshCreatedWithActorWithBackstop_ReturnsValidMeshNodesWithBackstopData) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); editorEntity->CreateComponent(EditorActorComponentTypeId); 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(meshNodeIndex, MeshVertices, MeshIndices, {}, MeshUVs, MeshClothData)); actor->FinishSetup(); auto actorAsset = CreateAssetFromActor(AZStd::move(actor)); EMotionFX::Integration::ActorComponentRequestBus::Event(editorEntity->GetId(), &EMotionFX::Integration::ActorComponentRequestBus::Handler::SetActorAsset, actorAsset.GetId()); } const auto& meshNodesWithBackstopData = editorClothComponent->GetMeshNodesWithBackstopData(); EXPECT_EQ(meshNodesWithBackstopData.size(), 1); EXPECT_TRUE(meshNodesWithBackstopData.find(MeshNodeName) != meshNodesWithBackstopData.end()); } TEST_F(NvClothEditorClothComponent, EditorClothComponent_OnMeshDestroyed_ReturnsMeshNodeListWithNoAssetMessage) { auto editorEntity = CreateInactiveEditorEntity("ClothComponentEditorEntity"); auto* editorClothComponent = editorEntity->CreateComponent(); editorEntity->CreateComponent(EditorActorComponentTypeId); 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(meshNodeIndex, MeshVertices, MeshIndices, {}, MeshUVs, MeshClothData)); actor->FinishSetup(); auto actorAsset = CreateAssetFromActor(AZStd::move(actor)); EMotionFX::Integration::ActorComponentRequestBus::Event(editorEntity->GetId(), &EMotionFX::Integration::ActorComponentRequestBus::Handler::SetActorAsset, actorAsset.GetId()); } editorClothComponent->OnMeshDestroyed(); const NvCloth::MeshNodeList& meshNodeList = editorClothComponent->GetMeshNodeList(); const auto& meshNodesWithBackstopData = editorClothComponent->GetMeshNodesWithBackstopData(); EXPECT_EQ(meshNodeList.size(), 1); EXPECT_TRUE(meshNodeList[0] == NvCloth::Internal::StatusMessageNoAsset); EXPECT_TRUE(meshNodesWithBackstopData.empty()); } } // namespace UnitTest