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/Gems/NvCloth/Code/Tests/NvClothTest.cpp

264 lines
10 KiB
C++

/*
* 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 <AzTest/AzTest.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Math/Transform.h>
#include <AzCore/Component/TickBus.h>
#include <NvCloth/IClothSystem.h>
#include <NvCloth/ICloth.h>
#include <NvCloth/IClothConfigurator.h>
#include <NvCloth/IFabricCooker.h>
#include <TriangleInputHelper.h>
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<NvCloth::SimParticleFormat>& 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<NvCloth::SimParticleFormat>& updatedParticles);
AZ::Transform m_clothTransform = AZ::Transform::CreateIdentity();
AZStd::vector<AZ::Vector4> 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<NvCloth::SimParticleFormat>& 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<NvCloth::SimParticleFormat>& 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<NvCloth::FabricCookedData> cookedData = AZ::Interface<NvCloth::IFabricCooker>::Get()->CookFabric(planeXY.m_vertices, planeXY.m_indices);
if (!cookedData)
{
return false;
}
// Create cloth instance
m_cloth = AZ::Interface<NvCloth::IClothSystem>::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<NvCloth::IClothSystem>::Get()->AddCloth(m_cloth);
return true;
}
void NvClothTestFixture::DestroyCloth()
{
if (m_cloth)
{
AZ::Interface<NvCloth::IClothSystem>::Get()->RemoveCloth(m_cloth);
AZ::Interface<NvCloth::IClothSystem>::Get()->DestroyCloth(m_cloth);
}
}
void NvClothTestFixture::OnPreSimulation([[maybe_unused]] NvCloth::ClothId clothId, float deltaTime)
{
m_cloth->GetClothConfigurator()->SetTransform(m_clothTransform);
constexpr float velocity = 1.0f;
for (auto& sphere : m_sphereColliders)
{
sphere.SetY(sphere.GetY() + velocity * deltaTime);
}
auto clothInverseTransform = m_clothTransform.GetInverse();
auto sphereColliders = m_sphereColliders;
for (auto& sphere : sphereColliders)
{
sphere.Set(clothInverseTransform.TransformPoint(sphere.GetAsVector3()), sphere.GetW());
}
m_cloth->GetClothConfigurator()->SetSphereColliders(sphereColliders);
}
void NvClothTestFixture::OnPostSimulation(
NvCloth::ClothId clothId, float deltaTime,
const AZStd::vector<NvCloth::SimParticleFormat>& 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<float>::max();
float m_beforeLargestY = -std::numeric_limits<float>::max();
float m_afterSmallestZ = std::numeric_limits<float>::max();
float m_afterLargestY = -std::numeric_limits<float>::max();
};
static ParticleBounds GetBeforeAndAfterParticleBounds(const AZStd::vector<NvCloth::SimParticleFormat>& particlesBefore,
const AZStd::vector<NvCloth::SimParticleFormat>& 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<NvCloth::SimParticleFormat> 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<NvCloth::SimParticleFormat> 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