/* * 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 #include #include namespace UnitTest { //! Sets up a cloth and colliders for each test case. class NvClothTestFixture : public ::testing::Test { protected: // ::testing::Test overrides ... void SetUp() override; void TearDown() override; //! Sends tick events to make cloth simulation happen. //! Returns the position of cloth particles at tickBefore, continues ticking till tickAfter. void TickClothSimulation(const AZ::u32 tickBefore, const AZ::u32 tickAfter, AZStd::vector& particlesBefore); NvCloth::ICloth* m_cloth = nullptr; NvCloth::ICloth::PreSimulationEvent::Handler m_preSimulationEventHandler; NvCloth::ICloth::PostSimulationEvent::Handler m_postSimulationEventHandler; bool m_postSimulationEventInvoked = false; private: bool CreateCloth(); void DestroyCloth(); // ICloth notifications void OnPreSimulation(NvCloth::ClothId clothId, float deltaTime); void OnPostSimulation(NvCloth::ClothId clothId, float deltaTime, const AZStd::vector& updatedParticles); AZ::Transform m_clothTransform = AZ::Transform::CreateIdentity(); AZStd::vector m_sphereColliders; }; void NvClothTestFixture::SetUp() { m_preSimulationEventHandler = NvCloth::ICloth::PreSimulationEvent::Handler( [this](NvCloth::ClothId clothId, float deltaTime) { this->OnPreSimulation(clothId, deltaTime); }); m_postSimulationEventHandler = NvCloth::ICloth::PostSimulationEvent::Handler( [this](NvCloth::ClothId clothId, float deltaTime, const AZStd::vector& updatedParticles) { this->OnPostSimulation(clothId, deltaTime, updatedParticles); }); bool clothCreated = CreateCloth(); ASSERT_TRUE(clothCreated); } void NvClothTestFixture::TearDown() { DestroyCloth(); } void NvClothTestFixture::TickClothSimulation(const AZ::u32 tickBefore, const AZ::u32 tickAfter, AZStd::vector& particlesBefore) { const float timeOneFrameSeconds = 0.016f; //approx 60 fps for (AZ::u32 tickCount = 0; tickCount < tickAfter; ++tickCount) { AZ::TickBus::Broadcast(&AZ::TickEvents::OnTick, timeOneFrameSeconds, AZ::ScriptTimePoint(AZStd::chrono::system_clock::now())); if (tickCount == tickBefore) { particlesBefore = m_cloth->GetParticles(); } } } bool NvClothTestFixture::CreateCloth() { const float width = 2.0f; const float height = 2.0f; const AZ::u32 segmentsX = 10; const AZ::u32 segmentsY = 10; const TriangleInput planeXY = CreatePlane(width, height, segmentsX, segmentsY); // Cook Fabric AZStd::optional cookedData = AZ::Interface::Get()->CookFabric(planeXY.m_vertices, planeXY.m_indices); if (!cookedData) { return false; } // Create cloth instance m_cloth = AZ::Interface::Get()->CreateCloth(planeXY.m_vertices, *cookedData); if (!m_cloth) { return false; } m_sphereColliders.emplace_back(512.0f, 512.0f, 35.0f, 1.0f); m_clothTransform.SetTranslation(AZ::Vector3(512.0f, 519.0f, 35.0f)); m_cloth->GetClothConfigurator()->SetTransform(m_clothTransform); m_cloth->GetClothConfigurator()->ClearInertia(); // Add cloth to default solver to be simulated AZ::Interface::Get()->AddCloth(m_cloth); return true; } void NvClothTestFixture::DestroyCloth() { if (m_cloth) { AZ::Interface::Get()->RemoveCloth(m_cloth); AZ::Interface::Get()->DestroyCloth(m_cloth); } } void NvClothTestFixture::OnPreSimulation([[maybe_unused]] NvCloth::ClothId clothId, float deltaTime) { m_cloth->GetClothConfigurator()->SetTransform(m_clothTransform); static float time = 0.0f; static float velocity = 1.0f; time += deltaTime; for (auto& sphere : m_sphereColliders) { sphere.SetY(sphere.GetY() + velocity * deltaTime); } auto clothInverseTransform = m_clothTransform.GetInverseFast(); auto sphereColliders = m_sphereColliders; for (auto& sphere : sphereColliders) { sphere.Set(clothInverseTransform * sphere.GetAsVector3(), sphere.GetW()); } m_cloth->GetClothConfigurator()->SetSphereColliders(sphereColliders); } void NvClothTestFixture::OnPostSimulation( NvCloth::ClothId clothId, float deltaTime, const AZStd::vector& updatedParticles) { AZ_UNUSED(clothId); AZ_UNUSED(deltaTime); AZ_UNUSED(updatedParticles); m_postSimulationEventInvoked = true; } //! Smallest Z and largest Y coordinates for a list of particles before, and a list of particles after simulation for some time. struct ParticleBounds { float m_beforeSmallestZ = std::numeric_limits::max(); float m_beforeLargestY = -std::numeric_limits::max(); float m_afterSmallestZ = std::numeric_limits::max(); float m_afterLargestY = -std::numeric_limits::max(); }; static ParticleBounds GetBeforeAndAfterParticleBounds(const AZStd::vector& particlesBefore, const AZStd::vector& particlesAfter) { assert(particlesBefore.size() == particlesAfter.size()); ParticleBounds beforeAndAfterParticleBounds; for (size_t particleIndex = 0; particleIndex < particlesBefore.size(); ++particleIndex) { if (particlesBefore[particleIndex].GetZ() < beforeAndAfterParticleBounds.m_beforeSmallestZ) { beforeAndAfterParticleBounds.m_beforeSmallestZ = particlesBefore[particleIndex].GetZ(); } if (particlesBefore[particleIndex].GetY() > beforeAndAfterParticleBounds.m_beforeLargestY) { beforeAndAfterParticleBounds.m_beforeLargestY = particlesBefore[particleIndex].GetY(); } if (particlesAfter[particleIndex].GetZ() < beforeAndAfterParticleBounds.m_afterSmallestZ) { beforeAndAfterParticleBounds.m_afterSmallestZ = particlesAfter[particleIndex].GetZ(); } if (particlesAfter[particleIndex].GetY() > beforeAndAfterParticleBounds.m_afterLargestY) { beforeAndAfterParticleBounds.m_afterLargestY = particlesAfter[particleIndex].GetY(); } } return beforeAndAfterParticleBounds; } //! Tests that basic cloth simulation works. TEST_F(NvClothTestFixture, Cloth_NoCollision_FallWithGravity) { const AZ::u32 tickBefore = 150; const AZ::u32 tickAfter = 300; AZStd::vector particlesBefore; TickClothSimulation(tickBefore, tickAfter, particlesBefore); ParticleBounds particleBounds = GetBeforeAndAfterParticleBounds(particlesBefore, m_cloth->GetParticles()); // Cloth was extended horizontally in the y-direction earlier. // If cloth fell with gravity, its y-extent should be smaller later, // and its z-extent should go lower to a smaller Z value later. ASSERT_TRUE((particleBounds.m_afterLargestY < particleBounds.m_beforeLargestY) && (particleBounds.m_afterSmallestZ < particleBounds.m_beforeSmallestZ)); } //! Tests that collision works and pre/post simulation events work. TEST_F(NvClothTestFixture, Cloth_Collision_CollidedWithPrePostSimEvents) { m_cloth->ConnectPreSimulationEventHandler(m_preSimulationEventHandler); // The pre-simulation callback moves the sphere collider towards the cloth every tick. m_cloth->ConnectPostSimulationEventHandler(m_postSimulationEventHandler); const AZ::u32 tickBefore = 150; const AZ::u32 tickAfter = 320; AZStd::vector particlesBefore; TickClothSimulation(tickBefore, tickAfter, particlesBefore); ParticleBounds particleBounds = GetBeforeAndAfterParticleBounds(particlesBefore, m_cloth->GetParticles()); // Cloth starts extended horizontally (along Y-axis). Simulation makes it swing down with gravity (as tested with the other unit test). // Then the sphere collider collides with the cloth and pushes it back up. So it is again extended in the Y-direction and // at about the same vertical height (Z-coord) as before. const float threshold = 0.25f; EXPECT_TRUE(AZ::IsClose(particleBounds.m_beforeSmallestZ , -0.97f, threshold)); EXPECT_TRUE(AZ::IsClose(particleBounds.m_beforeLargestY, 0.76f, threshold)); EXPECT_TRUE(AZ::IsClose(particleBounds.m_afterSmallestZ, -1.1f, threshold)); EXPECT_TRUE(AZ::IsClose(particleBounds.m_afterLargestY, 0.72f, threshold)); ASSERT_TRUE((fabsf(particleBounds.m_afterLargestY - particleBounds.m_beforeLargestY) < threshold) && (fabsf(particleBounds.m_afterSmallestZ - particleBounds.m_beforeSmallestZ) < threshold)); // Check that post simulation event was invoked. ASSERT_TRUE(m_postSimulationEventInvoked); m_preSimulationEventHandler.Disconnect(); m_postSimulationEventHandler.Disconnect(); } } // namespace UnitTest