// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "HeadlessChaos.h" #include "HeadlessChaosTestUtility.h" #include "Chaos/Island/IslandManager.h" #include "Chaos/ParticleHandle.h" #include "Chaos/PBDNullConstraints.h" #include "Chaos/PBDRigidsEvolutionGBF.h" #include "Chaos/Utilities.h" #include "Modules/ModuleManager.h" #include "HAL/ConsoleManager.h" namespace ChaosTest { using namespace Chaos; class GraphEvolutionTests : public ::testing::TestWithParam { }; class FGraphEvolutionTest { public: FGraphEvolutionTest(const int32 NumParticles, const bool bPartialSleeping) : UniqueIndices() , Particles(UniqueIndices) , PhysicalMaterials() , Evolution(Particles, PhysicalMaterials) , Constraints() , TickCount(0) , CVarPartialSleeping() { IslandManager = &Evolution.GetIslandManager(); // Bind the constraints to the evolution Evolution.AddConstraintContainer(Constraints); Evolution.GetGravityForces().SetAcceleration(FVec3(0), 0); // Create some particles and constraints connecting them in a chain: 0-1-2-3-...-N ParticleHandles = Evolution.CreateDynamicParticles(NumParticles); for (FGeometryParticleHandle* ParticleHandle : ParticleHandles) { Evolution.EnableParticle(ParticleHandle); } CVarPartialSleeping = IConsoleManager::Get().FindConsoleVariable(TEXT("p.Chaos.Solver.Sleep.PartialIslandSleep"), false); check(CVarPartialSleeping && CVarPartialSleeping->IsVariableBool()); CVarPartialSleeping->Set(bPartialSleeping); } // Connect all the particles in a chain void MakeChain() { for (int32 ParticleIndex = 0; ParticleIndex < ParticleHandles.Num() - 1; ++ParticleIndex) { ConstraintHandles.Add(Constraints.AddConstraint({ ParticleHandles[ParticleIndex], ParticleHandles[ParticleIndex + 1] })); } } // Treat particle0 like a kinematic floor with all the other particles sat on it void MakeFloor() { Evolution.SetParticleObjectState(ParticleHandles[0], EObjectStateType::Kinematic); for (int32 ParticleIndex = 0; ParticleIndex < ParticleHandles.Num() - 1; ++ParticleIndex) { ConstraintHandles.Add(Constraints.AddConstraint({ ParticleHandles[0], ParticleHandles[ParticleIndex + 1] })); } } void Advance() { Evolution.AdvanceOneTimeStep(FReal(1.0 / 60.0)); ++TickCount; } void AdvanceUntilSleeping() { const int32 MaxIterations = 50; const int32 MaxTickCount = TickCount + MaxIterations; bool bIsSleeping = false; while (!bIsSleeping && (TickCount < MaxTickCount)) { Advance(); bIsSleeping = true; for (FPBDRigidParticleHandle* ParticleHandle : ParticleHandles) { if (ParticleHandle->IsDynamic() && !ParticleHandle->IsSleeping()) { bIsSleeping = false; } } } EXPECT_TRUE(bIsSleeping); EXPECT_LT(TickCount, MaxTickCount); } FParticleUniqueIndicesMultithreaded UniqueIndices; FPBDRigidsSOAs Particles; THandleArray PhysicalMaterials; FPBDRigidsEvolutionGBF Evolution; FPBDNullConstraints Constraints; TArray ParticleHandles; TArray ConstraintHandles; Private::FPBDIslandManager* IslandManager; int32 TickCount; IConsoleVariable* CVarPartialSleeping; }; // Veryify that the Null Constraint mockup is working as intended. We can create the container and constraints, and they are correctly bound to the evolution TEST_P(GraphEvolutionTests, TestNullConstraint) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeChain(); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); } // Start with a kinematic connected to a dynamic. Verify that removing the // constraint removes both particles. // This version explicitly removes the constraint from the graph. // // (d=dynamic, s=sleeping, k=kinematic) // Ak - Bd // => {} // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicDynamic_Remove) { FGraphEvolutionTest Test(2, GetParam()); Test.MakeChain(); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.Advance(); // Should have 1 island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); // Disable the constraint and remove it from the graph Test.ConstraintHandles[0]->SetEnabled(false); Test.IslandManager->RemoveConstraint(Test.ConstraintHandles[0]); Test.Advance(); // Should have no islands and all particles should have been removed EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); EXPECT_FALSE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_FALSE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsInConstraintGraph()); } // Start with a kinematic connected to a dynamic. Verify that removing the // constraint removes both particles. // This version has the constraint removed by making all particles kinematic // (d=dynamic, s=sleeping, k=kinematic) // Ak - Bd // => Ak - Bk // => {} // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicDynamic_Remove2) { FGraphEvolutionTest Test(2, GetParam()); Test.MakeChain(); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.Advance(); // Should have 1 island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); // Make the other particle kinematic Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Kinematic); Test.Advance(); // Should have no islands and all particles should have been removed EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); EXPECT_FALSE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_FALSE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsInConstraintGraph()); } // Start with a kinematic connected to a sleeping dynamic. Verify that removing the // kinematic removes both particles from the graph (because we do not keep islands // unless there are constraints in them), but the dynamic particle is now awake. // (d=dynamic, s=sleeping, k=kinematic) // Ak - Bs // => {} (B is dynamic/awake but the graph is empty) // // This tests a bug where we were not waking a particle if we removed all other particles // from its island. We now defer island destruction to after sleep handling. // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicDynamic_RemoveKinematic) { FGraphEvolutionTest Test(2, GetParam()); Test.MakeChain(); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.AdvanceUntilSleeping(); // Should have 1 island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); // B should be asleep EXPECT_TRUE(Test.ParticleHandles[1]->IsSleeping()); // Remove A Test.Evolution.DisableParticle(Test.ParticleHandles[0]); // A and the constraint should have been removed from the graph EXPECT_FALSE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsInConstraintGraph()); // B will be removed from the graph because we do not track islands without // constraints, but not until the next tick. For now it will still be in // the graph and still asleep. EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsSleeping()); // Tick physics. This will update the graph, waking B's island and therefore B. // B's island will then be destroyed because it has no constraints. Test.Advance(); // Graph should be empty EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); EXPECT_FALSE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_FALSE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsInConstraintGraph()); // B should be awake EXPECT_FALSE(Test.ParticleHandles[1]->IsSleeping()); } // Start with an island containing 4 particle connected in a chain, then make the second one kinematic. // Check that the island splits. // (d=dynamic, s=sleeping, k=kinematic) // Ad - Bd - Cd - Dd // => Ad - Bk Bk - Cd - Dd // TEST_P(GraphEvolutionTests, TestConstraintGraph_ToKinematic) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeChain(); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); // Convert particle B to kinematic to split the islands: A-B, B-C-D Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Kinematic); Test.Advance(); // Should now have 2 islands EXPECT_EQ(Test.IslandManager->GetNumIslands(), 2); // A should be in its own island EXPECT_EQ(Test.IslandManager->GetParticleIsland(Test.ParticleHandles[0])->GetNumParticles(), 1); // C and D should be in same island EXPECT_EQ(Test.IslandManager->GetParticleIsland(Test.ParticleHandles[2]), Test.IslandManager->GetParticleIsland(Test.ParticleHandles[3])); // B should be in 2 islands EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 2); } // Start with an island containing 2 particle connected in a chain, then invalidate one of the particles. // Check that the particles and constraints are removed from the graph and then re-added at the next tick. // (d=dynamic, s=sleeping, k=kinematic) // Ad - Bd // => Ad Bd // => Ad - Bd // TEST_P(GraphEvolutionTests, TestConstraintGraph_Invalidate) { FGraphEvolutionTest Test(2, GetParam()); Test.MakeChain(); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); // Invalidate B Test.Evolution.InvalidateParticle(Test.ParticleHandles[1]); // Constraint was kicked from the graph, but particles remain until // explicitly removed or they have no constraint on next update EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsInConstraintGraph()); Test.Advance(); // Everything was added back to the graph EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); } // An isolated stationary particle with no gravity should go to sleep TEST_P(GraphEvolutionTests, TestConstraintGraph_ParticleSleep_Isolated) { FGraphEvolutionTest Test(1, GetParam()); Test.Evolution.GetGravityForces().SetAcceleration(FVec3(0), 0); // Make all the particles sleep Test.AdvanceUntilSleeping(); // Particle should be asleep and it should have taken 21 ticks (ChaosSolverCollisionDefaultSleepCounterThreshold) EXPECT_TRUE(Test.ParticleHandles[0]->IsSleeping()); EXPECT_EQ(Test.TickCount, 21); } // Wait for all particles to go to sleep naturally (i.e., as part of the tick and not by explicitly setting the state) // then check that the islands are preserved. TEST_P(GraphEvolutionTests, TestConstraintGraph_ParticleSleep_Natural) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeChain(); Test.Advance(); // All constraints in graph in 1 island that is awake EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Make all the particles sleep Test.AdvanceUntilSleeping(); // Island should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); } // Sleep all particles in the scene and ensure that the island manager puts the island to sleep // but retains all the constraints and particles in the island. // TEST_P(GraphEvolutionTests, TestConstraintGraph_ParticleSleep_Manual) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeChain(); Test.Advance(); // All constraints in graph in 1 island that is awake EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Make all the particles sleep for (FPBDRigidParticleHandle* ParticleHandle : Test.ParticleHandles) { Test.Evolution.SetParticleObjectState(ParticleHandle, EObjectStateType::Sleeping); } Test.Advance(); // Island should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); } // Start with an island containing 4 particle connected in a chain, then make the middle two kinematic. // This makes the B-C constraint kinematic which means it does not belong in any island and is kicked // out of the graph (the edge is deleted). // Check that the island manager handles kinematic-kinematic constraints // A-B-C-D // => A-B C-D // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicKinematic) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeChain(); Test.Advance(); // All constraints in graph in 1 island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); // Convert a particle B and C to kinematic to split the islands: A-B, C-D Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Kinematic); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[2], EObjectStateType::Kinematic); Test.Advance(); // Constraint[1] was kicked from the graph EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_FALSE(Test.ConstraintHandles[1]->IsInConstraintGraph()); // Not in graph EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); // Should now have 2 islands EXPECT_EQ(Test.IslandManager->GetNumIslands(), 2); } // Same as TestConstraintGraph_KinematicKinematic but islands are sleeping when the change is made. // // (d=dynamic, s=sleeping, k=kinematic) // As - Bs - Cs - Ds // => As - Bk Ck - Ds // => Ad - Bk Ck - Ds // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicKinematic_Sleeping) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeChain(); Test.Advance(); // All constraints in graph in 1 island that is awake EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); // Wait for the sleep state Test.AdvanceUntilSleeping(); // Island should be asleep but still contain all the particles and constraints EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); // Convert a particle B and C to kinematic to split the islands: A-B, C-D Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Kinematic); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[2], EObjectStateType::Kinematic); Test.Advance(); // The kinematic-kinematic constraint was kicked from graph EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_FALSE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); // Island will have split into two sleeping islands EXPECT_EQ(Test.IslandManager->GetNumIslands(), 2); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(Test.IslandManager->GetIsland(1)->IsSleeping()); // Wake a dynamic particle Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Dynamic); Test.Advance(); // Should now have 2 islands and only one awake particle EXPECT_EQ(Test.IslandManager->GetNumIslands(), 2); EXPECT_FALSE(Test.ParticleHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ParticleHandles[3]->IsSleeping()); } // 3 objects sat on the floor awake. Make the floor dynamic. // This tests what happens when a kinematic in multiple islands gets converted to a dynamic. // // (d=dynamic, s=sleeping, k=kinematic) // Bd Cd Dd Bd Cd Dd // \ | / => \ | / // Ak Ad // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicToDynamic) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeFloor(); Test.Advance(); // Each particle in its own island (kinematic will be in all 3) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // Convert A to dynamic which should merge all the islands into 1 Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Dynamic); Test.Advance(); // All particles in same island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); } // 3 objects sat on the floor asleep. Move the kinematic floor by setting its transform. // This tests what happens when a non-moving kinematic in multiple islands starts moving. // // (d=dynamic, s=sleeping, k=kinematic, km=kinematic, moving) // Bs Cs Ds Bd Cd Dd // \ | / => \ | / // Ak Akm // TEST_P(GraphEvolutionTests, TestConstraintGraph_MoveKinematicFloor) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeFloor(); Test.AdvanceUntilSleeping(); // Each particle in its own island (kinematic will be in all 3) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // Teleport a particle by setting its transform explicitly const bool bIsTeleport = true; Test.Evolution.SetParticleKinematicTarget(Test.ParticleHandles[0], FKinematicTarget::MakePositionTarget(FVec3(0, 3, 0), FRotation3())); Test.Advance(); // Each particle in its own island (kinematic will be in all 3) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // All particles awake EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); } // 3 objects sat on the floor asleep. Make the floor dynamic. // This tests what happens when a kinematic in multiple sleeping islands gets converted to a dynamic. // // (d=dynamic, s=sleeping, k=kinematic) // Bs Cs Ds Bd Cd Dd // \ | / => \ | / // Ak Ad // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicToDynamic_WithSleep1) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeFloor(); Test.AdvanceUntilSleeping(); // Each particle in its own island (kinematic will be in all 3) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // Convert A to dynamic which should merge all the islands into 1 Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Dynamic); Test.Advance(); // All particles in one awake island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // All particles awake EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); } // 3 Objects sat on the floor asleep. Make the floor dynamic and asleep. // This tests that adding a sleeping body to an island does not wake it. // This is required for streaming to work which adds bodies over multiple frames. // // (d=dynamic, s=sleeping, k=kinematic) // Bs Cs Ds Bs Cs Ds // \ | / => \ | / // Ak As // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicToDynamic_WithSleep2) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeFloor(); Test.AdvanceUntilSleeping(); // Each particle in its own island (kinematic will be in all 3) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // Convert A to dynamic sleeping which should merge all the islands into 1 but leave it asleep Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Sleeping); Test.Advance(); // All particles in one asleep island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // All particles asleep EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); } // 3 Objects sat on the floor, 2 asleep and 1 awake. Make the floor dynamic and asleep. // In this case we should get 1 awake island and all particles should wake. // // Island sleeping: (d=dynamic, s=sleeping, k=kinematic) // Bs Cs Dd Bd Cd Dd // \ | / => \ | / // Ak Ad // // Partial island sleeping: (d=dynamic, s=sleeping, k=kinematic) // Bs Cs Dd Bs Cs Dd // \ | / => \ | / // Ak As // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicToDynamic_WithSleep3) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeFloor(); Test.AdvanceUntilSleeping(); // Wake D Test.Evolution.SetParticleObjectState(Test.ParticleHandles[3], EObjectStateType::Dynamic); Test.Advance(); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); // Each particle in its own island (kinematic will be in all 3) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // Convert A to dynamic sleeping Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Sleeping); Test.Advance(); if (!Test.CVarPartialSleeping->GetBool()) // island sleeping { // All particles in one awake island (D was awake so it would wake the island) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // All particles awake EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); } else // partial island sleeping { // All particles are asleep except for D which remains awake EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // All particles except for D asleep EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); } } // 3 Objects sat on the floor, 2 asleep and 1 awake. Make the floor dynamic and asleep. // Same as TestConstraintGraph_KinematicToDynamic_WithSleep3 except we wake a different particle to // be sure we weren't just lucky above (when we make a kinematic into a dynamic we add it to one // of the islands it is in. This is testing that this is ok). // // Island sleeping (d=dynamic, s=sleeping, k=kinematic) // Bd Cs Ds Bd Cd Dd // \ | / => \ | / // Ak Ad // // Partial island sleeping (d=dynamic, s=sleeping, k=kinematic) // Bd Cs Ds Bd Cs Ds // \ | / => \ | / // Ak As // TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicToDynamic_WithSleep4) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeFloor(); Test.AdvanceUntilSleeping(); // Wake B Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Dynamic); Test.Advance(); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); // Each particle in its own island (kinematic will be in all 3) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 3); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // Convert A to dynamic sleeping Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Sleeping); Test.Advance(); if (!Test.CVarPartialSleeping->GetBool()) // island sleeping { // All particles in one awake island (B was awake so it would wake the island) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // All particles awake EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); } else // partial island sleeping { // All particles are asleep except for B which remains awake EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[0]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[1]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[2]).Num(), 1); EXPECT_EQ(Test.IslandManager->FindParticleIslands(Test.ParticleHandles[3]).Num(), 1); // All particles except for B asleep EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); } } // Start with an island containing 4 awake particles connected in a chain. Then sleep the island // by explicitly putting all particles to sleep // // (d=dynamic, s=sleeping, k=kinematic) // Ad - Bd - Cd - Dd => As - Bs - Cs - Ds // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepIsland) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeChain(); Test.Advance(); // All particles and costraints are awake EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); // Put all of the particles to sleep Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Sleeping); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Sleeping); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[2], EObjectStateType::Sleeping); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[3], EObjectStateType::Sleeping); Test.Advance(); // Island and all particles and constraints should now be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); if (Test.IslandManager->GetNumIslands() == 1) { EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); } EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); } // @todo(chaos): Implement this. TestConstraintGraph_SleepIsland is intended to reproduce a bug // exp[osed by collision constraints where collisions were being destroyed on particles that were // explicitly put to sleep. However that bug was a result of how collision constraints are // destroyed (i.e., when they are not updated this tick) and the NullConstraints don't have that same // behaviour. We need a unit testing constraint that can reproduce that behaviour. TEST_P(GraphEvolutionTests, DISABLED_TestConstraintGraph_SleepIsland_Collisions) { } // Add a constraint between a sleeping and a kinematic body and tick. // Nothing should change. // // (d=dynamic, s=sleeping, k=kinematic) // Ak - Bs => Ak - Bs // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepingKinematicConstraint) { FGraphEvolutionTest Test(2, GetParam()); // Make A kinematic, B sleeping Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Sleeping); // Add a constraint A-B Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.Advance(); // Everything asleep EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); } // Add a constraint between a sleeping and a kinematic body, one tick after the bodies were added. // // This differs from TestConstraintGraph_SleepingKinematicConstraint in that we tick the scene one // time before adding the constraint, which means the particles are already in separate islands. // Nothing should wake and the constraint should be flagged as sleeping. // // This behaviour is required for streaming to work since scene creation may // be amortized over multiple frames and constraints may be made betweens // sleeping particles in a later tick. // // (d=dynamic, s=sleeping, k=kinematic) // Ak Bs => Ak - Bs // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepingKinematicConstraint2) { FGraphEvolutionTest Test(2, GetParam()); // Make A kinematic, B sleeping Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Sleeping); Test.Advance(); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); // Add a constraint A-B Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.Advance(); // B still asleep EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); } // Add a constraint between two sleeping particles. // Nothing should wake and the constraint should be flagged as sleeping. // This behaviour is required for streaming to work since scene creation may // be amortized over multiple frames and constraints may be made betweens // sleeping particles in a later tick. // In this case, A and B start sleeping and get merged into a single // still-sleeping island when we add the constraint. // // (d=dynamic, s=sleeping, k=kinematic) // As Bs => As - Bs // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepingSleepingConstraint) { FGraphEvolutionTest Test(2, GetParam()); // Make A and B sleeping Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Sleeping); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Sleeping); Test.Advance(); // Particles without any constraints are not in the graph EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); // Add a constraint A-B Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.Advance(); // A and B still asleep but now in an island with the constraint EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); } // Similar to TestConstraintGraph_SleepingKinematicConstraint, but we are adding a constraint // between sleeping and kinematic particles that are already in an existing sleeping island // with multiple sleeping constraints. // // (d=dynamic, s=sleeping, k=kinematic) // Ak - Bs - Cs => Ak - Bs - Cs // ^--------^ // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepingKinematicConstraint_SameIsland) { FGraphEvolutionTest Test(3, GetParam()); // Chains the particles and make the first one kinematic Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.MakeChain(); // Wait for sleep Test.AdvanceUntilSleeping(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); // Add a constraint A - C Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[2] })); Test.Advance(); // All still asleep, including the new constraint EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); } // Similar to TestConstraintGraph_SleepingKinematicConstraint, but we are adding a constraint // between two sleeping particles in different island, but where each island already contains // sleeping constraints. // // (d=dynamic, s=sleeping, k=kinematic) // As - Bs Cs - Ds => As - Bs - Cs - Ds // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepingSleepingConstraint_MergeIslands) { FGraphEvolutionTest Test(4, GetParam()); // Add constraints A-B and C-D Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[3] })); // Wait for sleep Test.AdvanceUntilSleeping(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 2); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); // Add a constraint B - C Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[2] })); Test.Advance(); // All still asleep, including the new constraint EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); } // Add constraints beween objects on the tick where their island goes to sleep, and one either side just to be sure. // i.e., The SleepCounter does not get reset when we add a constraint between two particles. // // (d=dynamic, s=sleeping, k=kinematic) // Ad Bd => As - Bs // // @todo(chaos): this test would fail because we do not transfer isolated particle // sleep counts to the graph when we add a constraint to them. We could fix this // but probably not worth worrying about // TEST_P(GraphEvolutionTests, DISABLED_TestConstraintGraph_SleepingSleepingConstraint_Timing_Isolated) { // Count how many frames it takes the simulation to sleep FGraphEvolutionTest SleepTest(2, GetParam()); SleepTest.AdvanceUntilSleeping(); const int32 TicksToSleep = SleepTest.TickCount; // Create a new simulation up to the sleep tick +/- a tick // Verify that adding a constraint on that tick leaves the scene as expected for (int32 SleepRelativeTickCount = -1; SleepRelativeTickCount < 2; ++SleepRelativeTickCount) { FGraphEvolutionTest Test(2, GetParam()); for (int32 Frame = 0; Frame < TicksToSleep + SleepRelativeTickCount; ++Frame) { Test.Advance(); } const bool bExpectSleep = (SleepRelativeTickCount >= 0); // Should have no islands (because we have no constraints) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); EXPECT_EQ(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping(), bExpectSleep); EXPECT_EQ(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping(), bExpectSleep); // Add a constraint A-B and tick Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.Advance(); // Should now have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Constraint should also be asleep EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); } } // Add constraints beween objects on the tick where their island goes to sleep, and one either side just to be sure. // i.e., The SleepCounter does not get reset when we add a constraint between two particles. // // (d=dynamic, s=sleeping, k=kinematic) // Ad - Bd Cd - Dd => As - Bs - Cs - Ds // // NOTE: this one works where DISABLED_TestConstraintGraph_SleepingSleepingConstraint_Timing_Isolated // would fail because we retain SleepCounter when merging islands (but not when adding isloated // particles that have their own sleep counter) // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepingSleepingConstraint_Timing) { // Count how many frames it takes the simulation to sleep int32 TicksToSleep = 0; { FGraphEvolutionTest SleepTest(4, GetParam()); SleepTest.ConstraintHandles.Add(SleepTest.Constraints.AddConstraint({ SleepTest.ParticleHandles[0], SleepTest.ParticleHandles[1] })); SleepTest.ConstraintHandles.Add(SleepTest.Constraints.AddConstraint({ SleepTest.ParticleHandles[2], SleepTest.ParticleHandles[3] })); SleepTest.AdvanceUntilSleeping(); TicksToSleep = SleepTest.TickCount; } // Create a new simulation up to the sleep tick +/- a tick // Verify that adding a constraint on that tick leaves the scene as expected for (int32 SleepRelativeTickCount = -1; SleepRelativeTickCount < 2; ++SleepRelativeTickCount) { FGraphEvolutionTest Test(4, GetParam()); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[3] })); for (int32 Frame = 0; Frame < TicksToSleep + SleepRelativeTickCount; ++Frame) { Test.Advance(); } const bool bExpectSleep = (SleepRelativeTickCount >= 0); // Should have no islands (because we have no constraints) EXPECT_EQ(Test.IslandManager->GetNumIslands(), 2); EXPECT_EQ(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping(), bExpectSleep); EXPECT_EQ(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping(), bExpectSleep); EXPECT_EQ(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping(), bExpectSleep); EXPECT_EQ(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping(), bExpectSleep); // Add a constraint B-C and tick Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[2] })); Test.Advance(); // Should now have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); // Constraints should also be asleep EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); } } // Test an edge case bug that is probably easy to accidentally reintroduce. This would leave // a dangling pointer in the constraint graph due to a collision constraint being deleted while // in a sleeping island. // // The fix was to ensure that we build the Island particle and constraint lists for islands that // have just been put to sleep (we still don't bother for thoise that were already asleep) so // that we can visit all the particles and constraints to set the sleep state. // // 1: A dynamic particle is in its own awake island // - Tick // 2a: The particle is manually put to sleep // 2b: A constraint is added between the particle and a kinematic // - Tick // During the graph update on this tick, the particle's island is put to sleep in UpdateGraph // because all particles in it are asleep. However, the constraint was added this tick as well, // but when it was added the island was awake, so the constraint starts in the awake state. // // Verify that the constraint does actually get put to sleep at some point in the graph update. // // (d=dynamic, s=sleeping, k=kinematic) // Ak Bd => As - Bs // // NOTE: the transition to sleep is by a user call, not the automatic sleep-when-not-moving system // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepingSleepingConstraint_Timing2) { FGraphEvolutionTest Test(2, GetParam()); // Make A kinematic, B dynamic Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Dynamic); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); // Explicitly put B to sleep Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Sleeping); // Add a constraint A-B. B is asleep Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.Advance(); // Everything should be asleep // The bug was that the constraint was still flagged as awake, but in a sleeping island. EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); } // This is a very similar test to TestConstraintGraph_SleepingSleepingConstraint_Timing2 // in that is exposes the same bug where an island that is implicitly put to sleep because // all its particles were explicitly put to sleep did not put its constraints to sleep. // // (d=dynamic, s=sleeping, k=kinematic) // Ad - Bd => As - Bs // // NOTE: the transition to sleep is by a user call, not the automatic sleep-when-not-moving system // TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepingSleepingConstraint_Timing3) { FGraphEvolutionTest Test(2, GetParam()); // Make A, B dynamic Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Dynamic); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Dynamic); // Add a constraint A-B Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Explicitly put both particles (and therefore their island) to sleep Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Sleeping); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Sleeping); Test.Advance(); // Everything should be asleep // The bug was that the constraint was still flagged as awake, but in a sleeping island. EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); } // Test isolated particles are not present in the graph TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicRemoveFromGraph) { // Create a scene with 3 dynamic particles FGraphEvolutionTest Test(3, GetParam()); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 0); // Change a particle to kinematic Test.Evolution.SetParticleObjectState(Test.ParticleHandles[1], EObjectStateType::Kinematic); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 0); Test.Advance(); // State should not have changed with a second tick EXPECT_EQ(Test.IslandManager->GetNumIslands(), 0); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 0); } // Test the conditions for a kinematic particle waking an island // If a kinematic is being animated by velocity or by setting a target // position the island should wake but only if the target velocity is // non-zero or the target transform is different from the identity TEST_P(GraphEvolutionTests, TestConstraintGraph_KinematicWakeIslandConditions) { FGraphEvolutionTest Test(4, GetParam()); Test.MakeChain(); // Set the root of the chain to be kinematic and the rest to be sleeping Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); for (int32 ParticleIndex = 1; ParticleIndex < Test.ParticleHandles.Num(); ++ParticleIndex) { Test.Evolution.SetParticleObjectState(Test.ParticleHandles[ParticleIndex], EObjectStateType::Sleeping); } Test.Advance(); // Expect one sleeping island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_NE(Test.ConstraintHandles[0]->GetConstraintGraphEdge(), nullptr); EXPECT_NE(Test.ConstraintHandles[1]->GetConstraintGraphEdge(), nullptr); EXPECT_NE(Test.ConstraintHandles[2]->GetConstraintGraphEdge(), nullptr); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Set to velocity mode and animate FKinematicGeometryParticleHandle* KinematicParticle = Test.ParticleHandles[0]->CastToKinematicParticle(); ASSERT_NE(KinematicParticle, nullptr); KinematicParticle->KinematicTarget().SetVelocityMode(); Test.Advance(); // Expect one sleeping island as the velocity of the kinematic particle is still zero EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); KinematicParticle->SetV(FVec3(10.0f, 0.0f, 0.0f)); Test.Advance(); // Expect one awake island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Put all particles back to sleep and now set angular velocity KinematicParticle->SetV(FVec3(0.0f, 0.0f, 0.0f)); for (int32 ParticleIndex = 1; ParticleIndex < Test.ParticleHandles.Num(); ++ParticleIndex) { Test.Evolution.SetParticleObjectState(Test.ParticleHandles[ParticleIndex], EObjectStateType::Sleeping); } Test.Advance(); // Check we've put the island back to sleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Now set angular velocity. Island should wake KinematicParticle->SetW(FVec3(0.0f, 1.0f, 0.0f)); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Put all particles back to sleep KinematicParticle->SetW(FVec3(0.0f, 0.0f, 0.0f)); for (int32 ParticleIndex = 1; ParticleIndex < Test.ParticleHandles.Num(); ++ParticleIndex) { Test.Evolution.SetParticleObjectState(Test.ParticleHandles[ParticleIndex], EObjectStateType::Sleeping); } Test.Advance(); // Check we've put the island back to sleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Now set to position mode. Initially the island should stay sleeping as the target // transform is the identity FKinematicTarget KinematicTarget; KinematicTarget.SetTargetMode(FRigidTransform3::Identity); KinematicParticle->SetKinematicTarget(KinematicTarget); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Set a non-zero position target. Should cause the island to wake KinematicTarget.SetTargetMode(FVec3(10.0f, 0.0f, 0.0f), FRotation3::Identity); KinematicParticle->SetKinematicTarget(KinematicTarget); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Put all particles back to sleep KinematicTarget.SetTargetMode(FRigidTransform3::Identity); KinematicParticle->SetKinematicTarget(KinematicTarget); for (int32 ParticleIndex = 1; ParticleIndex < Test.ParticleHandles.Num(); ++ParticleIndex) { Test.Evolution.SetParticleObjectState(Test.ParticleHandles[ParticleIndex], EObjectStateType::Sleeping); } Test.Advance(); // Check we've put the island back to sleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); // Set a non-identity rotation target. Should cause the island to wake FRigidTransform3 TargetTransform(FVec3(0.0f, 0.0f, 0.0f), FQuat::MakeFromEuler(FVec3(1.0f, 0.0f, 2.0f))); KinematicTarget.SetTargetMode(TargetTransform); KinematicParticle->SetKinematicTarget(KinematicTarget); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); } // Test that island merging works if we remove the last constraint // in an island that was already queued for merge (PLAY-6440). // // (d=dynamic, s=sleeping, k=kinematic) // Ad - Bd Cd - Dd // => Ad - Bd - Cd Dd // TEST_P(GraphEvolutionTests, TestConstraintGraph_IslandMerge_EnableDisable) { FGraphEvolutionTest Test(4, GetParam()); // Create constraints in a chain but disable the middle constraint so we have two islands {A-B} and {C-D} Test.MakeChain(); Test.ConstraintHandles[1]->SetEnabled(false); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 2); EXPECT_FALSE(Test.ConstraintHandles[1]->IsInConstraintGraph()); // Enable the constraint B-C. // NOTE: In the implementation, the enable will add the constraint to one of the islands // and queue the two islands to be merged, but the actual merging happens in Advance(). Test.ConstraintHandles[1]->SetEnabled(true); // Disable the constraint C-D. // NOTE: This will leave the second island without any constraints, but it is // not destroyed immediately because it is queued for merging and, even though // it has no constraints, it still contains particle C which needs to be // copied to the new merged island. Particle D would have been removed because // it does not have any constraints. // Issue (PLAY-6440) was caused by the island being destroyed because it was // empty, but it was still queued to be merged. Test.ConstraintHandles[2]->SetEnabled(false); Test.Advance(); // We should now only have 1 island and D should not be in the graph EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_FALSE(Test.ConstraintHandles[2]->IsInConstraintGraph()); // Not in graph EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[2]->IsInConstraintGraph()); EXPECT_FALSE(Test.ParticleHandles[3]->IsInConstraintGraph()); // Not in graph } // Test the sparse array repeatable index assignment. See TSparseArray::SortFreeList() GTEST_TEST(SparseArrayTests, TestSortFreeList) { TSparseArray Values; // The first time we add objects, they should be in consecutive indices starting from 0 Values.Add(0); Values.Add(1); Values.Add(2); EXPECT_EQ(Values[0], 0); EXPECT_EQ(Values[1], 1); EXPECT_EQ(Values[2], 2); // Remove a couple items in the same order we added them Values.RemoveAt(1); Values.RemoveAt(2); // Add the items again, they will end up in reverse order // We don't rely on this behaviour but I'm testing here because if this changes // in the future then we may be able to remove our calls to SortFreeList in the graph. Values.Add(1); Values.Add(2); EXPECT_EQ(Values[0], 0); EXPECT_EQ(Values[1], 2); // Swapped EXPECT_EQ(Values[2], 1); // Swapped // Now do the same as above on a new array, but call SortFreeList before reusing it TSparseArray Values2; Values2.Add(0); Values2.Add(1); Values2.Add(2); Values2.RemoveAt(1); Values2.RemoveAt(2); // Rebuild the free list Values2.SortFreeList(); // We should now get the same order as the first time we added items Values2.Add(1); Values2.Add(2); EXPECT_EQ(Values2[0], 0); EXPECT_EQ(Values2[1], 1); EXPECT_EQ(Values2[2], 2); } // Test validating that a sleeping island wakes up when teleporting one of its particles. // Teleporting means explicitly updating the transform of the particle. TEST_P(GraphEvolutionTests, TestConstraintGraph_TeleportSleeping) { FGraphEvolutionTest Test(3, GetParam()); Test.MakeChain(); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.AdvanceUntilSleeping(); // Should have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[2]->IsInConstraintGraph()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); // Teleport a particle by setting its transform explicitly const bool bIsTeleport = true; const bool bWakeUp = true; Test.Evolution.SetParticleTransform(Test.ParticleHandles[2], FVec3(0, 3, 0), FRotation3(), bIsTeleport, bWakeUp); Test.Advance(); // Should wake up the entire island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[1]->IsSleeping()); } // Test validating that a sleeping island wakes up when adding an impulse, acceleration, etc. to one particle. // NOTE: This code emulates what happens if dynamics data is updated in @PushToPhysicsStateImp (SingleParticlePhysicsProxy.cpp). TEST_P(GraphEvolutionTests, TestConstraintGraph_SetVelocityOfSleeping) { FGraphEvolutionTest Test(3, GetParam()); Test.MakeChain(); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.AdvanceUntilSleeping(); // Should have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[2]->IsInConstraintGraph()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); // Explicitly set a non-zero velocity FParticleDynamics Dynamics; Dynamics.SetAcceleration(FVec3(0, 0, 0)); Dynamics.SetAngularAcceleration(FVec3(0, 0, 0)); Dynamics.SetLinearImpulseVelocity(FVec3(50, 50, 0)); Dynamics.SetAngularImpulseVelocity(FVec3(0, 0, 0)); // This emulates what happens in @PushToPhysicsStateImp (SingleParticlePhysicsProxy.cpp). Test.ParticleHandles[2]->SetDynamics(Dynamics); Test.Evolution.ResetVSmoothFromForces(*Test.ParticleHandles[2]); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[2], EObjectStateType::Dynamic); Test.Advance(); // Should wake up the entire island EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[1]->IsSleeping()); } // Test validating the wake-up propagation throughout a sleeping island if adding an awake particle to the top of the stack. // 1) For island sleeping, the entire island should wake up in a single tick. // (d=dynamic, s=sleeping, k=kinematic) // Ak - Bs - Cs - Ds // => Ak - Bd - Cd - Dd - Ed // 2) For partial island sleeping, the awake particle will be added to the island and only wake the particle closest to it. // (d=dynamic, s=sleeping, k=kinematic) // Ak - Bs - Cs - Ds // => Ak - Bs - Cs - Dd - Ed // TEST_P(GraphEvolutionTests, TestConstraintGraph_WakeUpPropagation_AddToStackTop) { FGraphEvolutionTest Test(4, GetParam()); if (Test.CVarPartialSleeping->GetBool()) { Test.MakeChain(); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.AdvanceUntilSleeping(); // Should have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[2]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[3]->IsInConstraintGraph()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); // Add another particle and connect it to the top of the chain Test.ParticleHandles.Add(Test.Evolution.CreateDynamicParticles(1)[0]); Test.Evolution.EnableParticle(Test.ParticleHandles.Last()); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles.Last(1), Test.ParticleHandles.Last() })); // NOTE: Set a non-zero velocity to make the particle move and trigger a wake-up event. FParticleDynamics Dynamics; Dynamics.SetAcceleration(FVec3(0, 0, 0)); Dynamics.SetAngularAcceleration(FVec3(0, 0, 0)); Dynamics.SetLinearImpulseVelocity(FVec3(50, 50, 0)); Dynamics.SetAngularImpulseVelocity(FVec3(0, 0, 0)); Test.ParticleHandles.Last()->SetDynamics(Dynamics); Test.Evolution.ResetVSmoothFromForces(*Test.ParticleHandles.Last()); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 5); EXPECT_EQ(Test.IslandManager->GetNumConstraints(), 4); // Will wake up the top particle of the original stack EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[3]->IsSleeping()); } } // Test validating the wake-up propagation throughout a sleeping island if adding an awake particle to the top of the stack. // Partial island sleeping only: the awake particle will be added to the island and only wake the particle closest to it. // (d=dynamic, s=sleeping, k=kinematic) // Ak - Bs - Cs - Ds // => Ak - Bs - Cd - Dd // | // Ed TEST_P(GraphEvolutionTests, TestConstraintGraph_WakeUpPropagation_AddToStackCenter) { FGraphEvolutionTest Test(4, GetParam()); if (Test.CVarPartialSleeping->GetBool()) { Test.MakeChain(); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.AdvanceUntilSleeping(); // Should have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(Test.ParticleHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[2]->IsInConstraintGraph()); EXPECT_TRUE(Test.ParticleHandles[3]->IsInConstraintGraph()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsInConstraintGraph()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); // Add another particle and connect it to the top of the chain Test.ParticleHandles.Add(Test.Evolution.CreateDynamicParticles(1)[0]); Test.Evolution.EnableParticle(Test.ParticleHandles.Last()); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles.Last(2), Test.ParticleHandles.Last() })); // NOTE: Set a non-zero velocity to make the particle move and trigger a wake-up event. FParticleDynamics Dynamics; Dynamics.SetAcceleration(FVec3(0, 0, 0)); Dynamics.SetAngularAcceleration(FVec3(0, 0, 0)); Dynamics.SetLinearImpulseVelocity(FVec3(50, 50, 0)); Dynamics.SetAngularImpulseVelocity(FVec3(0, 0, 0)); Test.ParticleHandles.Last()->SetDynamics(Dynamics); Test.Evolution.ResetVSmoothFromForces(*Test.ParticleHandles.Last()); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 5); EXPECT_EQ(Test.IslandManager->GetNumConstraints(), 4); // Will wake up the top particle of the original stack EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[3]->IsSleeping()); } } // Test validating the wake-up propagation for a triangular brick wall with lateral spacing. // We add another particle near the top of the wall (partial island sleeping only). // Gs // / \ // Es Fs // / \ / \ // Bs Cs Ds // \ | / // Ak // => // Hd Gd // \ / \ // Ed Fs // / \ / \ // Bs Cs Ds // \ | / // Ak TEST_P(GraphEvolutionTests, TestConstraintGraph_WakeUpPropagation_AddToWallWithSpacing) { FGraphEvolutionTest Test(7, GetParam()); if (Test.CVarPartialSleeping->GetBool()) { Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[2] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[3] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[4] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[4] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[3], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[4], Test.ParticleHandles[6] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[5], Test.ParticleHandles[6] })); Test.AdvanceUntilSleeping(); // Should have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[8]->IsSleeping()); // Add another particle near the top of the wall Test.ParticleHandles.Add(Test.Evolution.CreateDynamicParticles(1)[0]); Test.Evolution.EnableParticle(Test.ParticleHandles.Last()); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles.Last(3), Test.ParticleHandles.Last() })); // NOTE: Set a non-zero velocity to make the particle move and trigger a wake-up event. FParticleDynamics Dynamics; Dynamics.SetAcceleration(FVec3(0, 0, 0)); Dynamics.SetAngularAcceleration(FVec3(0, 0, 0)); Dynamics.SetLinearImpulseVelocity(FVec3(50, 50, 0)); Dynamics.SetAngularImpulseVelocity(FVec3(0, 0, 0)); Test.ParticleHandles.Last()->SetDynamics(Dynamics); Test.Evolution.ResetVSmoothFromForces(*Test.ParticleHandles.Last()); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 8); EXPECT_EQ(Test.IslandManager->GetNumConstraints(), 10); // Will wake up the two particles connected by the new constraint and the top of the wall EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[7])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[8]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[9]->IsSleeping()); } } // Test validating the wake-up propagation for a triangular brick wall without lateral spacing. // We add another particle near the top of the wall (partial island sleeping only). // Gs // / \ // Es - Fs // / \ / \ // Bs - Cs - Ds // \ | / // Ak // => // Hd Gd // \ / \ // Ed - Fs // / \ / \ // Bs - Cs - Ds // \ | / // Ak TEST_P(GraphEvolutionTests, TestConstraintGraph_WakeUpPropagation_AddToWallWithoutSpacing) { FGraphEvolutionTest Test(7, GetParam()); if (Test.CVarPartialSleeping->GetBool()) { Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[2] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[3] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[2] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[3] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[4] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[4] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[3], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[4], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[4], Test.ParticleHandles[6] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[5], Test.ParticleHandles[6] })); Test.AdvanceUntilSleeping(); // Should have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[8]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[9]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[10]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[11]->IsSleeping()); // Add another particle near the top of the wall Test.ParticleHandles.Add(Test.Evolution.CreateDynamicParticles(1)[0]); Test.Evolution.EnableParticle(Test.ParticleHandles.Last()); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles.Last(3), Test.ParticleHandles.Last() })); // NOTE: Set a non-zero velocity to make the particle move and trigger a wake-up event. FParticleDynamics Dynamics; Dynamics.SetAcceleration(FVec3(0, 0, 0)); Dynamics.SetAngularAcceleration(FVec3(0, 0, 0)); Dynamics.SetLinearImpulseVelocity(FVec3(50, 50, 0)); Dynamics.SetAngularImpulseVelocity(FVec3(0, 0, 0)); Test.ParticleHandles.Last()->SetDynamics(Dynamics); Test.Evolution.ResetVSmoothFromForces(*Test.ParticleHandles.Last()); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 8); EXPECT_EQ(Test.IslandManager->GetNumConstraints(), 13); // Will wake up the two particles connected by the new constraint and the top of the wall EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[7])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[8]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[9]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[10]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[11]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[12]->IsSleeping()); } } // Test validating the wake-up propagation for a rectangular block wall without lateral spacing. // We add another constraint to connect two particles diagonally. // Hs - Ks - Ls // | | | // Es - Fs - Gs // | | | // Bs - Cs - Ds // \ | / // Ak // => // Hs - Ks - Ls // | \ | | // Es - Fs - Gs // | | | // Bs - Cs - Ds // \ | / // Ak TEST_P(GraphEvolutionTests, TestConstraintGraph_WakeUpPropagation_AddToBlockWithoutSpacingSleeping) { FGraphEvolutionTest Test(10, GetParam()); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); // Constraints with the ground Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[2] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[3] })); // Horizontal constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[2] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[3] })); // Vertical constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[4] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[3], Test.ParticleHandles[6] })); // Horizontal constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[4], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[5], Test.ParticleHandles[6] })); // Vertical constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[4], Test.ParticleHandles[7] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[5], Test.ParticleHandles[8] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[6], Test.ParticleHandles[9] })); // Horizontal constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[7], Test.ParticleHandles[8] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[8], Test.ParticleHandles[9] })); Test.AdvanceUntilSleeping(); // Should have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[7])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[8])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[9])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[8]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[9]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[10]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[11]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[12]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[13]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[14]->IsSleeping()); // Add another constraint to diagonally connect to particles in the block Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[5], Test.ParticleHandles[7] })); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 10); EXPECT_EQ(Test.IslandManager->GetNumConstraints(), 16); EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[7])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[8])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[9])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[8]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[9]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[10]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[11]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[12]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[13]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[14]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[15]->IsSleeping()); } // Test validating the wake-up propagation for a rectangular block wall without lateral spacing. // We add another constraint to connect two particles diagonally, wake particle H and add an impulse to it. // Hs - Ks - Ls // | | | // Es - Fs - Gs // | | | // Bs - Cs - Ds // \ | / // Ak // 1) For full island sleeping, the entire island will wake up. // (d=dynamic, s=sleeping, k=kinematic) // Hd - Kd - Ld // | \ | | // Ed - Fd - Gd // | | | // Bd - Cd - Dd // \ | / // Ak // 2) For partial island sleeping, the state change in particle H will wake up its immediate neighbors and all particles at a higher level. // (d=dynamic, s=sleeping, k=kinematic) // Hd - Kd - Ls // | \ | | // Ed - Fd - Gs // | | | // Bs - Cs - Ds // \ | / // As TEST_P(GraphEvolutionTests, TestConstraintGraph_WakeUpPropagation_AddToBlockWithoutSpacingAwake) { FGraphEvolutionTest Test(10, GetParam()); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[0], EObjectStateType::Kinematic); // Constraints with the ground Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[1] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[2] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[0], Test.ParticleHandles[3] })); // Horizontal constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[2] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[3] })); // Vertical constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[1], Test.ParticleHandles[4] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[2], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[3], Test.ParticleHandles[6] })); // Horizontal constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[4], Test.ParticleHandles[5] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[5], Test.ParticleHandles[6] })); // Vertical constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[4], Test.ParticleHandles[7] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[5], Test.ParticleHandles[8] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[6], Test.ParticleHandles[9] })); // Horizontal constraints Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[7], Test.ParticleHandles[8] })); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[8], Test.ParticleHandles[9] })); Test.AdvanceUntilSleeping(); // Should have 1 island and it should be asleep EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_TRUE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[7])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[8])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[9])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[8]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[9]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[10]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[11]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[12]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[13]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[14]->IsSleeping()); // Add another constraint to diagonally connect to particles in the block Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles[5], Test.ParticleHandles[7] })); Test.Evolution.SetParticleObjectState(Test.ParticleHandles[7], EObjectStateType::Dynamic); // NOTE: Set a non-zero velocity to make the particle move and trigger a wake-up event. FParticleDynamics Dynamics; Dynamics.SetAcceleration(FVec3(0, 0, 0)); Dynamics.SetAngularAcceleration(FVec3(0, 0, 0)); Dynamics.SetLinearImpulseVelocity(FVec3(50, 50, 0)); Dynamics.SetAngularImpulseVelocity(FVec3(0, 0, 0)); Test.ParticleHandles[7]->SetDynamics(Dynamics); Test.Evolution.ResetVSmoothFromForces(*Test.ParticleHandles[7]); Test.Advance(); EXPECT_EQ(Test.IslandManager->GetNumParticles(), 10); EXPECT_EQ(Test.IslandManager->GetNumConstraints(), 16); if (!Test.CVarPartialSleeping->GetBool()) // island sleeping { EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[7])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[8])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[9])->Sleeping()); EXPECT_FALSE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[8]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[9]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[10]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[11]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[12]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[13]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[14]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[15]->IsSleeping()); } else // partial island sleeping { EXPECT_EQ(Test.IslandManager->GetNumIslands(), 1); EXPECT_FALSE(Test.IslandManager->GetIsland(0)->IsSleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[0])->IsKinematic()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[1])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[2])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[3])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[4])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[5])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[6])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[7])->Sleeping()); EXPECT_FALSE(FConstGenericParticleHandle(Test.ParticleHandles[8])->Sleeping()); EXPECT_TRUE(FConstGenericParticleHandle(Test.ParticleHandles[9])->Sleeping()); EXPECT_TRUE(Test.ConstraintHandles[0]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[1]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[2]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[3]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[4]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[5]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[6]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[7]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[8]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[9]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[10]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[11]->IsSleeping()); EXPECT_TRUE(Test.ConstraintHandles[12]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[13]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[14]->IsSleeping()); EXPECT_FALSE(Test.ConstraintHandles[15]->IsSleeping()); } } TEST_P(GraphEvolutionTests, TestConstraintGraph_SleepCounterReset) { // Partial island sleeping only if (GetParam()) { FGraphEvolutionTest Test(2, GetParam()); Test.MakeChain(); // Number of steps is a few less than the sleep counter threshold IConsoleVariable* CVarPartialSleeping = IConsoleManager::Get().FindConsoleVariable(TEXT("p.Chaos.Solver.Sleep.Defaults.SleepCounterThreshold"), false); check(CVarPartialSleeping && CVarPartialSleeping->IsVariableInt()); const int32 SleepCounterThreshold = CVarPartialSleeping->GetInt(); const int32 Steps = SleepCounterThreshold - 2; for (int32 I = 0; I < Steps; ++I) { Test.Advance(); } for (FPBDRigidParticleHandle* Particle : Test.ParticleHandles) { EXPECT_FALSE(Particle->IsSleeping()); const int32 SleepCounter = Particle->SleepCounter(); EXPECT_EQ(SleepCounter, Steps); } // Set a non-zero velocity to make the particle move. FParticleDynamics Dynamics; Dynamics.SetAcceleration(FVec3(0, 0, 0)); Dynamics.SetAngularAcceleration(FVec3(0, 0, 0)); Dynamics.SetLinearImpulseVelocity(FVec3(50, 50, 0)); Dynamics.SetAngularImpulseVelocity(FVec3(0, 0, 0)); for (FPBDRigidParticleHandle* Particle : Test.ParticleHandles) { Particle->SetDynamics(Dynamics); Test.Evolution.ResetVSmoothFromForces(*Particle); } // Step one more time, the sleep counter should be reset to 0 because the particles are moving Test.Advance(); for (FPBDRigidParticleHandle* Particle : Test.ParticleHandles) { EXPECT_FALSE(Particle->IsSleeping()); const int32 SleepCounter = Particle->SleepCounter(); EXPECT_EQ(SleepCounter, 0); } } } TEST_P(GraphEvolutionTests, TestConstraintGraph_PreventSleepDuringWakeEvent) { // Partial island sleeping only if (GetParam()) { FGraphEvolutionTest Test(2, GetParam()); Test.MakeChain(); // Number of steps is the sleep counter threshold IConsoleVariable* CVarPartialSleeping = IConsoleManager::Get().FindConsoleVariable(TEXT("p.Chaos.Solver.Sleep.Defaults.SleepCounterThreshold"), false); check(CVarPartialSleeping && CVarPartialSleeping->IsVariableInt()); const int32 SleepCounterThreshold = CVarPartialSleeping->GetInt(); const int32 Steps = SleepCounterThreshold; for (int32 I = 0; I < Steps; ++I) { Test.Advance(); } for (FPBDRigidParticleHandle* Particle : Test.ParticleHandles) { EXPECT_FALSE(Particle->IsSleeping()); const int32 SleepCounter = Particle->SleepCounter(); EXPECT_EQ(SleepCounter, Steps); } // Add another particle and connect it to the top of the chain Test.ParticleHandles.Add(Test.Evolution.CreateDynamicParticles(1)[0]); Test.Evolution.EnableParticle(Test.ParticleHandles.Last()); Test.ConstraintHandles.Add(Test.Constraints.AddConstraint({ Test.ParticleHandles.Last(1), Test.ParticleHandles.Last() })); // NOTE: Set a non-zero velocity to make the particle move and trigger a wake-up event. FParticleDynamics Dynamics; Dynamics.SetAcceleration(FVec3(0, 0, 0)); Dynamics.SetAngularAcceleration(FVec3(0, 0, 0)); Dynamics.SetLinearImpulseVelocity(FVec3(50, 50, 0)); Dynamics.SetAngularImpulseVelocity(FVec3(0, 0, 0)); Test.ParticleHandles.Last()->SetDynamics(Dynamics); Test.Evolution.ResetVSmoothFromForces(*Test.ParticleHandles.Last()); // Step one more time, the particles should not sleep since a wake-up event just happened. Test.Advance(); for (FPBDRigidParticleHandle* Particle : Test.ParticleHandles) { EXPECT_FALSE(Particle->IsSleeping()); } // Resting particles EXPECT_EQ(Test.ParticleHandles[0]->SleepCounter(), 1); EXPECT_EQ(Test.ParticleHandles[1]->SleepCounter(), 1); // Moving particle EXPECT_EQ(Test.ParticleHandles[2]->SleepCounter(), 0); } } INSTANTIATE_TEST_SUITE_P(, GraphEvolutionTests, testing::Bool()); }