Files
UnrealEngine/Engine/Source/Programs/HeadlessChaos/Private/HeadlessChaosTestContactModification.cpp
2025-05-18 13:04:45 +08:00

1274 lines
57 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Chaos/ContactModification.h"
#include "Chaos/CCDModification.h"
#include "ChaosSolversModule.h"
#include "HeadlessChaosTestUtility.h"
#include "PBDRigidsSolver.h"
#include "PhysicsProxy/SingleParticlePhysicsProxy.h"
namespace ChaosTest
{
using namespace Chaos;
class FContactModificationTestCallback : public Chaos::TSimCallbackObject<
Chaos::FSimCallbackNoInput,
Chaos::FSimCallbackNoOutput,
Chaos::ESimCallbackOptions::Presimulate | Chaos::ESimCallbackOptions::ContactModification | Chaos::ESimCallbackOptions::CCDModification>
{
public:
TUniqueFunction<void(Chaos::FCollisionContactModifier&)> TestLambda;
TUniqueFunction<void(Chaos::FCCDModifierAccessor&)> TestCCDLambda = nullptr;
private:
virtual void OnPreSimulate_Internal() override {}
virtual void OnContactModification_Internal(Chaos::FCollisionContactModifier& Modifier) override;
virtual void OnCCDModification_Internal(Chaos::FCCDModifierAccessor& Accessor) override;
};
void FContactModificationTestCallback::OnContactModification_Internal(Chaos::FCollisionContactModifier& Modifier)
{
TestLambda(Modifier);
}
void FContactModificationTestCallback::OnCCDModification_Internal(Chaos::FCCDModifierAccessor& Accessor)
{
if (TestCCDLambda)
{
TestCCDLambda(Accessor);
}
}
GTEST_TEST(AllTraits, ContactModification_Disable)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// create a static floor and two boxes falling onto it.
// One box has contacts disabled and should fall through, one should collide.
// simulated cube with downward velocity,should collide with floor and not fall through.
FSingleParticlePhysicsProxy* CollidingCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& CollidingCubeParticle = CollidingCubeProxy->GetGameThreadAPI();
auto CollidingCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
CollidingCubeParticle.SetGeometry(CollidingCubeGeom);
Solver->RegisterObject(CollidingCubeProxy);
CollidingCubeParticle.SetGravityEnabled(false);
CollidingCubeParticle.SetV(FVec3(0,0,-100));
CollidingCubeParticle.SetX(FVec3(200,0,500));
SetCubeInertiaTensor(CollidingCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({CollidingCubeProxy->GetParticle_LowLevel()});
// Simulated cube with downawrd velocity, contact modification disables collision with floor, should fall through.
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(false);
ModifiedCubeParticle.SetV(FVec3(0, 0, -100));
ModifiedCubeParticle.SetX(FVec3(-200, 0, 500));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0,0,0));
ChaosTest::SetParticleSimDataToCollide({FloorProxy->GetParticle_LowLevel()});
// Save Unique indices of floor and modified cube to disable in contact mod.
TVec2<FUniqueIdx> UniqueIndices({ModifiedCubeParticle.UniqueIdx(), FloorParticle.UniqueIdx()});
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [UniqueIndices](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
FUniqueIdx Idx0 = Particles[0]->UniqueIdx();
FUniqueIdx Idx1 = Particles[1]->UniqueIdx();
// If unique indices match disable the pair.
if( (UniqueIndices[0] == Idx0 && UniqueIndices[1] == Idx1) ||
(UniqueIndices[0] == Idx1 && UniqueIndices[1] == Idx0))
{
PairModifier.Disable();
}
}
};
const float Dt = 1.0f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Modified cube should be below floor because we disabled collision.
EXPECT_LT(ModifiedCubeParticle.X().Z, FloorParticle.X().Z);
// Colliding cube should be above floor due to collision.
EXPECT_GT(CollidingCubeParticle.X().Z, FloorParticle.X().Z);
// Floor should be at origin.
EXPECT_EQ(FloorParticle.X().Z, 0);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(CollidingCubeProxy);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_Probe)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// create a static floor and two boxes falling onto it.
// Both boxes should turn all contacts to probes, one of them has CCD enabled
// and the other one doesn't.
auto CubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
// Fall through the floor, no ccd
FSingleParticlePhysicsProxy* CubeProxyA = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& CubeParticleA = CubeProxyA->GetGameThreadAPI();
CubeParticleA.SetGeometry(CubeGeom);
Solver->RegisterObject(CubeProxyA);
CubeParticleA.SetGravityEnabled(false);
CubeParticleA.SetCCDEnabled(false);
CubeParticleA.SetV(FVec3(0, 0, -100));
CubeParticleA.SetX(FVec3(200, 0, 500));
SetCubeInertiaTensor(CubeParticleA, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ CubeProxyA->GetParticle_LowLevel() });
// Fall through the floor, with ccd
FSingleParticlePhysicsProxy* CubeProxyB = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& CubeParticleB = CubeProxyB->GetGameThreadAPI();
CubeParticleB.SetGeometry(CubeGeom);
Solver->RegisterObject(CubeProxyB);
CubeParticleB.SetGravityEnabled(false);
CubeParticleB.SetCCDEnabled(true);
CubeParticleB.SetV(FVec3(0, 0, -1000));
CubeParticleB.SetX(FVec3(-200, 0, 500));
SetCubeInertiaTensor(CubeParticleB, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ CubeProxyB->GetParticle_LowLevel() });
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
PairModifier.ConvertToProbe();
}
};
Callback->TestCCDLambda = [CubeProxyB](Chaos::FCCDModifierAccessor& Accessor)
{
if (Chaos::FGeometryParticleHandle* ParticleHandle = CubeProxyB->GetHandle_LowLevel())
{
for (FCCDModifier& CCDModifier : Accessor.GetModifiers(ParticleHandle))
{
CCDModifier.ConvertToProbe();
}
}
};
const float Dt = 1.0f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// TODO: Check for hit callbacks?
// Both cubes should be below the floor
EXPECT_LT(CubeParticleA.X().Z, FloorParticle.X().Z);
EXPECT_LT(CubeParticleB.X().Z, FloorParticle.X().Z);
// Floor should be at origin.
EXPECT_EQ(FloorParticle.X().Z, 0);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(CubeProxyA);
Solver->UnregisterObject(CubeProxyB);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
// Disabling due to: UE-216793; Objects fall through the floor with ConvertToNonProbe modification.
GTEST_TEST(AllTraits, DISABLED_ContactModification_NonProbe)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// Create a static floor and two cubes falling onto it.
// Both cubes start off as probes and should apply a modification to turn all contacts to non-probes,
// CubeB has CCD enabled and CubeA does not.
auto CubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
// CubeA - Probe & No CCD
FSingleParticlePhysicsProxy* CubeProxyA = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& CubeParticleA = CubeProxyA->GetGameThreadAPI();
CubeParticleA.SetGeometry(CubeGeom);
Solver->RegisterObject(CubeProxyA);
CubeParticleA.SetGravityEnabled(false);
CubeParticleA.SetCCDEnabled(false);
CubeParticleA.SetV(FVec3(0, 0, -100));
CubeParticleA.SetX(FVec3(200, 0, 500));
CubeParticleA.ShapesArray()[0]->SetIsProbe(true);
SetCubeInertiaTensor(CubeParticleA, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ CubeProxyA->GetParticle_LowLevel() });
// CubeB - Probe & With CCD
FSingleParticlePhysicsProxy* CubeProxyB = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& CubeParticleB = CubeProxyB->GetGameThreadAPI();
CubeParticleB.SetGeometry(CubeGeom);
Solver->RegisterObject(CubeProxyB);
CubeParticleB.SetGravityEnabled(false);
CubeParticleB.SetCCDEnabled(true);
CubeParticleB.SetV(FVec3(0, 0, -1000));
CubeParticleB.SetX(FVec3(-200, 0, 500));
CubeParticleB.ShapesArray()[0]->SetIsProbe(true);
SetCubeInertiaTensor(CubeParticleB, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ CubeProxyB->GetParticle_LowLevel() });
// Static floor at origin, occupying Z = [-100,0], XY = [-500, 500]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
PairModifier.ConvertToNonProbe();
}
};
const float Dt = 1.0f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Both cubes should be above the floor
EXPECT_GT(CubeParticleA.X().Z, FloorParticle.X().Z);
EXPECT_GT(CubeParticleB.X().Z, FloorParticle.X().Z);
// Floor should still be at origin.
EXPECT_EQ(FloorParticle.X().Z, 0);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(CubeProxyA);
Solver->UnregisterObject(CubeProxyB);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifySeparation)
{
// The amount to pad the saparation by in the collision callback. Currently this must
// be less than the CullDistance specified in the settings (3.0)
// @todo(chaos): allow the user to pad bounds to support position modification
const FReal SeparationPadding = 2.0f;
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// create a static floor and two boxes falling onto it.
// One box has separation modified to float 5 units above floor.
// Other box is not modified and should rest on top of floor.
// simulated cube with downward velocity, should rest directly on floor.
FSingleParticlePhysicsProxy* RegularCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& RegularCubeParticle = RegularCubeProxy->GetGameThreadAPI();
auto RegularCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
RegularCubeParticle.SetGeometry(RegularCubeGeom);
Solver->RegisterObject(RegularCubeProxy);
RegularCubeParticle.SetGravityEnabled(true);
RegularCubeParticle.SetX(FVec3(200, 0, 110));
SetCubeInertiaTensor(RegularCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ RegularCubeProxy->GetParticle_LowLevel() });
// Simulated cube with downawrd velocity, contact modification subtracts SeparationPadding from separation, causing cube to rest above floor.
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(true);
ModifiedCubeParticle.SetX(FVec3(-200, 0, 110));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
// Save Unique indices of floor and modified cube to disable in contact mod.
TVec2<FUniqueIdx> UniqueIndices({ ModifiedCubeParticle.UniqueIdx(), FloorParticle.UniqueIdx() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [UniqueIndices, SeparationPadding](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
FUniqueIdx Idx0 = Particles[0]->UniqueIdx();
FUniqueIdx Idx1 = Particles[1]->UniqueIdx();
// If unique indices match disable the pair.
if ((UniqueIndices[0] == Idx0 && UniqueIndices[1] == Idx1) ||
(UniqueIndices[0] == Idx1 && UniqueIndices[1] == Idx0))
{
int32 NumContacts = PairModifier.GetNumContacts();
for (int32 PointIdx = 0; PointIdx < NumContacts; ++PointIdx)
{
PairModifier.ModifyTargetSeparation(SeparationPadding, PointIdx);
}
}
}
};
const float Dt = 0.1f;
const int32 Steps = 30;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
const float PositionTolerance = 1.e-2f;
// Modified cube should be resting SeparationPadding above floor, as we added that penetration through contact mod.
EXPECT_NEAR(ModifiedCubeParticle.X().Z, 100.f + SeparationPadding, PositionTolerance);
// Colliding cube should be resting on floor.
EXPECT_NEAR(RegularCubeParticle.X().Z, 100.f, PositionTolerance);
// Floor should be at origin.
EXPECT_EQ(FloorParticle.X().Z, 0);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(RegularCubeProxy);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyNormal)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// create a static floor and two boxes falling onto it.
// One box has normal modified to be parallel to floor, should fall through floor due to non-upward normal.
// Other box is not modified and should rest on top of floor.
// simulated cube with downward velocity, should rest directly on floor.
FSingleParticlePhysicsProxy* RegularCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& RegularCubeParticle = RegularCubeProxy->GetGameThreadAPI();
auto RegularCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
RegularCubeParticle.SetGeometry(RegularCubeGeom);
Solver->RegisterObject(RegularCubeProxy);
RegularCubeParticle.SetGravityEnabled(false);
RegularCubeParticle.SetV(FVec3(0, 0, -100));
RegularCubeParticle.SetX(FVec3(200, 0, 500));
SetCubeInertiaTensor(RegularCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ RegularCubeProxy->GetParticle_LowLevel() });
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(false);
ModifiedCubeParticle.SetV(FVec3(0, 0, -100));
ModifiedCubeParticle.SetX(FVec3(-200, 0, 500));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
// Save Unique indices of floor and modified cube to disable in contact mod.
TVec2<FUniqueIdx> UniqueIndices({ ModifiedCubeParticle.UniqueIdx(), FloorParticle.UniqueIdx() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [UniqueIndices](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
FUniqueIdx Idx0 = Particles[0]->UniqueIdx();
FUniqueIdx Idx1 = Particles[1]->UniqueIdx();
// If unique indices match disable the pair.
if ((UniqueIndices[0] == Idx0 && UniqueIndices[1] == Idx1) ||
(UniqueIndices[0] == Idx1 && UniqueIndices[1] == Idx0))
{
int32 NumContacts = PairModifier.GetNumContacts();
for (int32 PointIdx = 0; PointIdx < NumContacts; ++PointIdx)
{
FVec3 NewNormal(-1, 0, 0);
PairModifier.ModifyWorldNormal(NewNormal, PointIdx);
}
}
}
};
const float Dt = 1.0f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Normal was modified to be parallel to floor, should fall through and not collide.
EXPECT_LT(ModifiedCubeParticle.X().Z, 0.f);
EXPECT_LT(ModifiedCubeParticle.V().Z, 0.f);
// non-modified cube should be resting on floor.
EXPECT_NEAR(RegularCubeParticle.X().Z, 100.f, KINDA_SMALL_NUMBER);
EXPECT_NEAR(RegularCubeParticle.V().Z, 0.f, KINDA_SMALL_NUMBER);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(RegularCubeProxy);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyLocationWorldSpace)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// create a static floor and two boxes colliding on edge of floor each with center of mass over the edge.
// One cube should rotate off side, the other has contact point locations moved under center of mass,
// so cube does not rotate and fall, instead remains on floor.
// simulated cube falling onto floor with center of mass over hanging past edge, should fall under floor,
FSingleParticlePhysicsProxy* FallingCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& FallingCubeParticle = FallingCubeProxy->GetGameThreadAPI();
auto FallingCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
FallingCubeParticle.SetGeometry(FallingCubeGeom);
Solver->RegisterObject(FallingCubeProxy);
FallingCubeParticle.SetGravityEnabled(true);
FallingCubeParticle.SetX(FVec3(550, 0, 110));
SetCubeInertiaTensor(FallingCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ FallingCubeProxy->GetParticle_LowLevel() });
// cube with CoM hanging past edge of floor, contact mod moves contact under CoM so it will not tip off edge.
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(true);
ModifiedCubeParticle.SetX(FVec3(-550, 0, 110));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor at origin, X/Y spanning [-500, 500] and Z spanning [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
// Save Unique indices of floor and modified cube to disable in contact mod.
TVec2<FUniqueIdx> UniqueIndices({ ModifiedCubeParticle.UniqueIdx(), FloorParticle.UniqueIdx() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [UniqueIndices](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
FUniqueIdx Idx0 = Particles[0]->UniqueIdx();
FUniqueIdx Idx1 = Particles[1]->UniqueIdx();
// If unique indices match disable the pair.
if ((UniqueIndices[0] == Idx0 && UniqueIndices[1] == Idx1) ||
(UniqueIndices[0] == Idx1 && UniqueIndices[1] == Idx0))
{
int32 NumContacts = PairModifier.GetNumContacts();
for (int32 PointIdx = 0; PointIdx < NumContacts; ++PointIdx)
{
// Move contact locations below center of mass
FVec3 WorldPos0;
FVec3 WorldPos1;
PairModifier.GetWorldContactLocations(PointIdx, WorldPos0, WorldPos1);
int32 DynamicIdx = (UniqueIndices[0] == Idx0) ? 0 : 1;
const FVec3 CoM = FParticleUtilities::GetCoMWorldPosition(FConstGenericParticleHandle(Particles[DynamicIdx]));
// Move point0 under center of mass and move second point under CoM but keep the same distance between bodies
FVec3 PointUnderCoM0(CoM.X, CoM.Y, WorldPos0.Z);
FVec3 PointUnderCoM1(CoM.X, CoM.Y, WorldPos1.Z);
PairModifier.ModifyWorldContactLocations(PointUnderCoM0, PointUnderCoM1, PointIdx);
}
}
}
};
const float Dt = 0.1f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Modified contact points to be below CoM, cube should not tip off edge of floor, but rest on it instead.
EXPECT_NEAR(ModifiedCubeParticle.X().Z, 100.f, KINDA_SMALL_NUMBER);
// Expected to tip off edge as CoM hangs off of floor.
EXPECT_LT(FallingCubeParticle.X().Z, 0.f);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(FallingCubeProxy);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyRestitution_NoBounce)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// Collide cube with static floor with restitution modified to 0
// Simulated cube with downawrd velocity, should not bounce, and end up with zero velocity.
FSingleParticlePhysicsProxy* NoBounceCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& NoBounceCubeParticle = NoBounceCubeProxy->GetGameThreadAPI();
auto NoBounceCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
NoBounceCubeParticle.SetGeometry(NoBounceCubeGeom);
Solver->RegisterObject(NoBounceCubeProxy);
NoBounceCubeParticle.SetGravityEnabled(false);
NoBounceCubeParticle.SetX(FVec3(-200, 0, 200));
SetCubeInertiaTensor(NoBounceCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ NoBounceCubeProxy->GetParticle_LowLevel() });
NoBounceCubeParticle.SetV(FVec3(0, 0, -100));
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
// Save Unique indices of floor and modified cube to disable in contact mod.
TVec2<FUniqueIdx> NoBounceUniqueIndices({ NoBounceCubeParticle.UniqueIdx(), FloorParticle.UniqueIdx() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [NoBounceUniqueIndices](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
FUniqueIdx Idx0 = Particles[0]->UniqueIdx();
FUniqueIdx Idx1 = Particles[1]->UniqueIdx();
if ((NoBounceUniqueIndices[0] == Idx0 && NoBounceUniqueIndices[1] == Idx1) ||
(NoBounceUniqueIndices[0] == Idx1 && NoBounceUniqueIndices[1] == Idx0))
{
int32 NumContacts = PairModifier.GetNumContacts();
for (int32 PointIdx = 0; PointIdx < NumContacts; ++PointIdx)
{
// Make sure that values are not held between frames
// @todo(chaos): this test actually has zero restitution
//EXPECT_GT(PairModifier.GetRestitution(), 0);
// Remove restitution
PairModifier.ModifyRestitution(0);
}
}
}
};
// If we rely on good restitution, we need more velocity iterations
Solver->GetEvolution()->SetNumVelocityIterations(4);
const float Dt = 0.1f;
const int32 Steps = 30;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
EXPECT_NEAR(NoBounceCubeParticle.V().Z, 0.f, 0.1);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(NoBounceCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyRestitution_Bounce)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// Collide cubes with static floor with restitution modified to 1
// simulated cube with downward velocity, should bounce on floor and end up with upward velocity.
FSingleParticlePhysicsProxy* BounceCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& BounceCubeParticle = BounceCubeProxy->GetGameThreadAPI();
auto BounceCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
BounceCubeParticle.SetGeometry(BounceCubeGeom);
Solver->RegisterObject(BounceCubeProxy);
BounceCubeParticle.SetGravityEnabled(false);
BounceCubeParticle.SetX(FVec3(200, 0, 200));
SetCubeInertiaTensor(BounceCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ BounceCubeProxy->GetParticle_LowLevel() });
BounceCubeParticle.SetV(FVec3(0, 0, -100));
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
// Save Unique indices of floor and modified cube to disable in contact mod.
TVec2<FUniqueIdx> BounceUniqueIndices({ BounceCubeParticle.UniqueIdx(), FloorParticle.UniqueIdx() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [BounceUniqueIndices](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
FUniqueIdx Idx0 = Particles[0]->UniqueIdx();
FUniqueIdx Idx1 = Particles[1]->UniqueIdx();
if ((BounceUniqueIndices[0] == Idx0 && BounceUniqueIndices[1] == Idx1) ||
(BounceUniqueIndices[0] == Idx1 && BounceUniqueIndices[1] == Idx0))
{
int32 NumContacts = PairModifier.GetNumContacts();
for (int32 PointIdx = 0; PointIdx < NumContacts; ++PointIdx)
{
// set restitution
PairModifier.ModifyRestitution(1);
// Our object is slow enough restitution will not be applied with default threshold.
PairModifier.ModifyRestitutionThreshold(99);
}
}
}
};
// If we rely on good restitution, we need more velocity iterations
Solver->GetEvolution()->SetNumVelocityIterations(4);
const float Dt = 0.1f;
const int32 Steps = 30;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
EXPECT_NEAR(BounceCubeParticle.V().Z, 100.f, 0.1);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(BounceCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyFriction)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// two cubes fall on tilted floor, one is expecting to slide off, one has friction modified to keep it on floor.
FSingleParticlePhysicsProxy* SlidingCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& SlidingCubeParticle = SlidingCubeProxy->GetGameThreadAPI();
auto SlidingCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
SlidingCubeParticle.SetGeometry(SlidingCubeGeom);
Solver->RegisterObject(SlidingCubeProxy);
SlidingCubeParticle.SetGravityEnabled(true);
SlidingCubeParticle.SetX(FVec3(200, -200, 50));
SlidingCubeParticle.SetR(FQuat::MakeFromEuler(FVec3(0, 20, 0)));
SetCubeInertiaTensor(SlidingCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ SlidingCubeProxy->GetParticle_LowLevel() });
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(true);
ModifiedCubeParticle.SetX(FVec3(200, 200, 50));
ModifiedCubeParticle.SetR(FQuat::MakeFromEuler(FVec3(0, 20, 0)));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor rotated 30 degrees
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
FloorParticle.SetR(FQuat::MakeFromEuler(FVec3(0, 20, 0)));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
// Save Unique indices of floor and modified cube to disable in contact mod.
TVec2<FUniqueIdx> UniqueIndices({ ModifiedCubeParticle.UniqueIdx(), FloorParticle.UniqueIdx() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [UniqueIndices](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
FUniqueIdx Idx0 = Particles[0]->UniqueIdx();
FUniqueIdx Idx1 = Particles[1]->UniqueIdx();
// If unique indices match disable the pair.
if ((UniqueIndices[0] == Idx0 && UniqueIndices[1] == Idx1) ||
(UniqueIndices[0] == Idx1 && UniqueIndices[1] == Idx0))
{
// Make sure that values are not held between frames
EXPECT_LT(PairModifier.GetDynamicFriction(), 1);
EXPECT_LT(PairModifier.GetStaticFriction(), 1);
PairModifier.ModifyDynamicFriction(1);
PairModifier.ModifyStaticFriction(1);
}
}
};
const float Dt = 0.1f;
const int32 Steps = 50;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Verify modified cube with increased friction sticks to floor.
EXPECT_NEAR(ModifiedCubeParticle.V().Z, 0.f, KINDA_SMALL_NUMBER);
// This cube should have slid off floor.
EXPECT_LT(SlidingCubeParticle.V().Z, 0.f);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(SlidingCubeProxy);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyParticleVelocity)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// simulated cube with downward velocity, on contact modification set an upward velocity so it should move away from floor,.
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(false);
ModifiedCubeParticle.SetX(FVec3(200, 0, 500));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
ModifiedCubeParticle.SetV(FVec3(0, 0, -100));
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
FVec3 NewVelocity(100,0,100);
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [NewVelocity](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
int32 DynamicParticleIdx = (Particles[0]->ObjectState() == EObjectStateType::Dynamic) ? 0 : 1;
PairModifier.ModifyParticleVelocity(NewVelocity, DynamicParticleIdx);
}
};
const float Dt = 1.0f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
EXPECT_NEAR(ModifiedCubeParticle.V().X, NewVelocity.X, KINDA_SMALL_NUMBER);
EXPECT_NEAR(ModifiedCubeParticle.V().Y, NewVelocity.Y, KINDA_SMALL_NUMBER);
EXPECT_NEAR(ModifiedCubeParticle.V().Z, NewVelocity.Z, KINDA_SMALL_NUMBER);
// Make sure we didn't somehow go through floor, once close to floor should have been moving parallel to floor.
EXPECT_GT(ModifiedCubeParticle.X().Z, 0.f);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyParticleAngularVelocity)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// simulated cube falling on floor, on contact modification set angular velocity to make it spin
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(true);
ModifiedCubeParticle.SetX(FVec3(200, 0, 500));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
int32 DynamicParticleIdx = (Particles[0]->ObjectState() == EObjectStateType::Dynamic) ? 0 : 1;
PairModifier.ModifyParticleAngularVelocity(FVec3(0, 0, 1), DynamicParticleIdx);
}
};
const float Dt = 0.1f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Did the modification of angular velocity work?
EXPECT_NEAR(ModifiedCubeParticle.W().Z, 1.f, 0.1);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyParticlePositionAndVelocity)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// simulated cube with downward velocity, on contact modification teleport particle and clear velocity.
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(false);
ModifiedCubeParticle.SetX(FVec3(200, 0, 500));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
ModifiedCubeParticle.SetV(FVec3(0, 0, -100));
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
FVec3 TeleportPosition(1000,2000,3000);
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [TeleportPosition](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
int32 DynamicParticleIdx = (Particles[0]->ObjectState() == EObjectStateType::Dynamic) ? 0 : 1;
// Clear velocity
PairModifier.ModifyParticleVelocity(FVec3(0, 0, 0), DynamicParticleIdx);
// Maintain change in velocity, we do not want moving particle to change implicit velocity.
PairModifier.ModifyParticlePosition(TeleportPosition, /*bMaintainVelocity=*/true, DynamicParticleIdx);
}
};
const float Dt = 1.0f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Are we where we moved particle to?
EXPECT_NEAR(ModifiedCubeParticle.X().X, TeleportPosition.X, KINDA_SMALL_NUMBER);
EXPECT_NEAR(ModifiedCubeParticle.X().Y, TeleportPosition.Y, KINDA_SMALL_NUMBER);
EXPECT_NEAR(ModifiedCubeParticle.X().Z, TeleportPosition.Z, KINDA_SMALL_NUMBER);
// Make sure we did not get any velocity once clearing it in contact mod.
EXPECT_NEAR(ModifiedCubeParticle.V().SizeSquared(), 0.f, KINDA_SMALL_NUMBER);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_ModifyParticleRotationAndMaintainAngularVelocity)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// We use contact mod to rotate cube and maintain angular velocity of 0.
// simulated cube with downward velocity, on contact modification rotate particle.
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(true);
ModifiedCubeParticle.SetX(FVec3(200, 0, 500));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
FRotation3 ModificationRotation(FQuat::MakeFromEuler(FVec3(0, 0, 45)));
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [ModificationRotation](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
int32 DynamicParticleIdx = (Particles[0]->ObjectState() == EObjectStateType::Dynamic) ? 0 : 1;
PairModifier.ModifyDynamicFriction(0);
PairModifier.ModifyStaticFriction(0);
PairModifier.ModifyParticleRotation(ModificationRotation, /*bMaintainVelocity=*/true, DynamicParticleIdx);
}
};
const float Dt = .1f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Do we have rotation applied in contact mod?
EXPECT_NEAR(ModifiedCubeParticle.R().X, ModificationRotation.X, .001);
EXPECT_NEAR(ModifiedCubeParticle.R().Y, ModificationRotation.Y, .001);
EXPECT_NEAR(ModifiedCubeParticle.R().Z, ModificationRotation.Z, .001);
EXPECT_NEAR(ModifiedCubeParticle.R().W, ModificationRotation.W, .001);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
// Drop a dynamic cube onto a kinematic cube with an offset so the dynamic cube would start to
// rotate, except that we have set the inertia scale to zero so it shouldn't rotate. It should actually
// just stop when it hits, and never tip off.
GTEST_TEST(AllTraits, ContactModification_ModifyParticleInertiaZero)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// We use contact mod to rotate cube and maintain angular velocity of 0.
// simulated cube dropped from just above the floor
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(true);
ModifiedCubeParticle.SetX(FVec3(0, 0, 150));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(-90, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [](Chaos::FCollisionContactModifier& Modifier)
{
for (FContactPairModifier& PairModifier : Modifier)
{
TVec2<FGeometryParticleHandle*> Particles = PairModifier.GetParticlePair();
int32 DynamicParticleIdx = (Particles[0]->ObjectState() == EObjectStateType::Dynamic) ? 0 : 1;
// Make sure that the values were not held over from the last call
EXPECT_NEAR(PairModifier.GetInvInertiaScale(0), 1.0, UE_SMALL_NUMBER);
EXPECT_NEAR(PairModifier.GetInvInertiaScale(1), 1.0, UE_SMALL_NUMBER);
PairModifier.ModifyRestitution(0);
PairModifier.ModifyInvInertiaScale(0, DynamicParticleIdx);
}
};
const float Dt = .1f;
const int32 Steps = 20;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Do we have rotation applied in contact mod?
EXPECT_NEAR(ModifiedCubeParticle.R().X, 0.0, 0.001);
EXPECT_NEAR(ModifiedCubeParticle.R().Y, 0.0, 0.001);
EXPECT_NEAR(ModifiedCubeParticle.R().Z, 0.0, 0.001);
EXPECT_NEAR(ModifiedCubeParticle.R().W, 1.0, 0.001);
// Body should be sat on the floor even though it is hanging off the edge
EXPECT_NEAR(ModifiedCubeParticle.X().X, 0.0, 0.001);
EXPECT_NEAR(ModifiedCubeParticle.X().Y, 0.0, 0.001);
EXPECT_NEAR(ModifiedCubeParticle.X().Z, 100.0, 0.001);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
GTEST_TEST(AllTraits, ContactModification_SelectByParticle)
{
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
InitSolverSettings(Solver);
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
// Similar to the "Disable" test:
// - Create a static floor and two boxes falling onto it.
// - One box has contacts disabled and should fall through, one should collide.
// - Rather than loop over all contacts, get contacts for a particular particle proxy
// simulated cube with downward velocity,should collide with floor and not fall through.
FSingleParticlePhysicsProxy* CollidingCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& CollidingCubeParticle = CollidingCubeProxy->GetGameThreadAPI();
auto CollidingCubeGeom = TRefCountPtr<FImplicitObject>(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
CollidingCubeParticle.SetGeometry(CollidingCubeGeom);
Solver->RegisterObject(CollidingCubeProxy);
CollidingCubeParticle.SetGravityEnabled(false);
CollidingCubeParticle.SetV(FVec3(0, 0, -100));
CollidingCubeParticle.SetX(FVec3(200, 0, 500));
SetCubeInertiaTensor(CollidingCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ CollidingCubeProxy->GetParticle_LowLevel() });
// Simulated cube with downawrd velocity, contact modification disables collision with floor, should fall through.
FSingleParticlePhysicsProxy* ModifiedCubeProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
auto& ModifiedCubeParticle = ModifiedCubeProxy->GetGameThreadAPI();
auto ModifiedCubeGeom = TRefCountPtr<FImplicitObject>(new TBox<FReal, 3>(FVec3(-100), FVec3(100)));
ModifiedCubeParticle.SetGeometry(ModifiedCubeGeom);
Solver->RegisterObject(ModifiedCubeProxy);
ModifiedCubeParticle.SetGravityEnabled(false);
ModifiedCubeParticle.SetV(FVec3(0, 0, -100));
ModifiedCubeParticle.SetX(FVec3(-200, 0, 500));
SetCubeInertiaTensor(ModifiedCubeParticle, /*Dimension=*/200, /*Mass=*/1);
ChaosTest::SetParticleSimDataToCollide({ ModifiedCubeProxy->GetParticle_LowLevel() });
// static floor at origin, occupying Z = [-100,0]
FSingleParticlePhysicsProxy* FloorProxy = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
auto& FloorParticle = FloorProxy->GetGameThreadAPI();
auto FloorGeom = TRefCountPtr<FImplicitObject>(new TBox<FReal, 3>(FVec3(-500, -500, -100), FVec3(500, 500, 0)));
FloorParticle.SetGeometry(FloorGeom);
Solver->RegisterObject(FloorProxy);
FloorParticle.SetX(FVec3(0, 0, 0));
ChaosTest::SetParticleSimDataToCollide({ FloorProxy->GetParticle_LowLevel() });
// Save Unique indices of floor and modified cube to disable in contact mod.
TVec2<FUniqueIdx> UniqueIndices({ ModifiedCubeParticle.UniqueIdx(), FloorParticle.UniqueIdx() });
FContactModificationTestCallback* Callback = Solver->CreateAndRegisterSimCallbackObject_External<FContactModificationTestCallback>();
Callback->TestLambda = [UniqueIndices, ModifiedCubeProxy](Chaos::FCollisionContactModifier& Modifier)
{
Chaos::FGeometryParticleHandle* ModifiedCubeParticle = ModifiedCubeProxy->GetHandle_LowLevel();
for (FContactPairModifier& PairModifier : Modifier.GetContacts(ModifiedCubeParticle))
{
PairModifier.Disable();
}
};
const float Dt = 1.0f;
const int32 Steps = 10;
for (int Step = 0; Step < Steps; ++Step)
{
Solver->AdvanceAndDispatch_External(Dt);
Solver->UpdateGameThreadStructures();
}
// Modified cube should be below floor because we disabled collision.
EXPECT_LT(ModifiedCubeParticle.X().Z, FloorParticle.X().Z);
// Colliding cube should be above floor due to collision.
EXPECT_GT(CollidingCubeParticle.X().Z, FloorParticle.X().Z);
// Floor should be at origin.
EXPECT_EQ(FloorParticle.X().Z, 0);
Solver->UnregisterAndFreeSimCallbackObject_External(Callback);
Solver->UnregisterObject(CollidingCubeProxy);
Solver->UnregisterObject(ModifiedCubeProxy);
Solver->UnregisterObject(FloorProxy);
Module->DestroySolver(Solver);
}
}