// Copyright Epic Games, Inc. All Rights Reserved. #include "HeadlessChaosTestCollisions.h" #include "HeadlessChaos.h" #include "HeadlessChaosCollisionConstraints.h" #include "Chaos/Box.h" #include "Chaos/CollisionResolution.h" #include "Chaos/CollisionResolutionTypes.h" #include "Chaos/Collision/PBDCollisionContainerSolver.h" #include "Chaos/Convex.h" #include "Chaos/Sphere.h" #include "Chaos/GJK.h" #include "Chaos/Pair.h" #include "Chaos/PhysicalMaterials.h" #include "Chaos/PBDCollisionConstraints.h" #include "Chaos/PBDRigidParticles.h" #include "Chaos/PBDRigidsEvolution.h" #include "Chaos/Utilities.h" #include "Modules/ModuleManager.h" namespace ChaosTest { using namespace Chaos; // Two boxes that use a margin around a core AABB. // Test that collision detection treats the margin as part of the shape. void TestBoxBoxCollisionMargin( const FReal Margin0, const FReal Margin1, const FVec3& Size, const FVec3& Delta, const FReal ExpectedPhi, const FVec3& ExpectedNormal) { TArrayCollectionArray Collided; TUniquePtr PhysicsMaterial = MakeUnique(); PhysicsMaterial->Friction = 0; PhysicsMaterial->Restitution = 0; TArrayCollectionArray> PhysicsMaterials; TArrayCollectionArray> PerParticlePhysicsMaterials; FParticleUniqueIndicesMultithreaded UniqueIndices; FPBDRigidsSOAs Particles(UniqueIndices); Particles.GetParticleHandles().AddArray(&Collided); Particles.GetParticleHandles().AddArray(&PhysicsMaterials); Particles.GetParticleHandles().AddArray(&PerParticlePhysicsMaterials); auto Box0 = AppendDynamicParticleBoxMargin(Particles, Size, Margin0); Box0->SetX(FVec3(0, 0, 0)); Box0->SetR(FRotation3(FQuat::Identity)); Box0->SetV(FVec3(0)); Box0->SetPreV(Box0->GetV()); Box0->SetP(Box0->GetX()); Box0->SetQ(Box0->GetR()); Box0->AuxilaryValue(PhysicsMaterials) = MakeSerializable(PhysicsMaterial); auto Box1 = AppendDynamicParticleBoxMargin(Particles, Size, Margin1); Box1->SetX(Delta); Box1->SetR(FRotation3(FQuat::Identity)); Box1->SetV(FVec3(0)); Box1->SetPreV(Box1->GetV()); Box1->SetP(Box1->GetX()); Box1->SetQ(Box1->GetR()); Box1->AuxilaryValue(PhysicsMaterials) = MakeSerializable(PhysicsMaterial); const FImplicitBox3* BoxImplicit0 = Box0->GetGeometry()->template GetObject(); const FImplicitBox3* BoxImplicit1 = Box1->GetGeometry()->template GetObject(); const FReal Tolerance = 2.0f * KINDA_SMALL_NUMBER; // Boxes should have a margin, limited by the minimum box dimension float ExpectedMargin0 = BoxImplicit0->ClampedMargin(Margin0); float ExpectedMargin1 = BoxImplicit0->ClampedMargin(Margin1); EXPECT_NEAR(BoxImplicit0->GetMarginf(), ExpectedMargin0, Tolerance); EXPECT_NEAR(BoxImplicit1->GetMarginf(), ExpectedMargin1, Tolerance); // Box Bounds should include margin const FAABB3 BoxBounds0 = BoxImplicit0->BoundingBox(); const FAABB3 BoxBounds1 = BoxImplicit1->BoundingBox(); EXPECT_NEAR(BoxBounds0.Extents().X, Size.X, Tolerance); EXPECT_NEAR(BoxBounds0.Extents().Y, Size.Y, Tolerance); EXPECT_NEAR(BoxBounds0.Extents().Z, Size.Z, Tolerance); EXPECT_NEAR(BoxBounds1.Extents().X, Size.X, Tolerance); EXPECT_NEAR(BoxBounds1.Extents().Y, Size.Y, Tolerance); EXPECT_NEAR(BoxBounds1.Extents().Z, Size.Z, Tolerance); Private::FCollisionConstraintAllocator CollisionAllocator; CollisionAllocator.SetMaxContexts(1); FPBDCollisionConstraintPtr Constraint = CollisionAllocator.GetContextAllocator(0)->CreateConstraint( Box0, Box0->GetGeometry(), Box0->ShapesArray()[0].Get(), nullptr, FRigidTransform3(), Box1, Box1->GetGeometry(), Box1->ShapesArray()[0].Get(), nullptr, FRigidTransform3(), FLT_MAX, true, EContactShapesType::BoxBox); FRigidTransform3 ShapeWorldTransform0 = Constraint->GetShapeRelativeTransform0() * Collisions::GetTransform(Constraint->GetParticle0()); FRigidTransform3 ShapeWorldTransform1 = Constraint->GetShapeRelativeTransform1() * Collisions::GetTransform(Constraint->GetParticle1()); Constraint->SetShapeWorldTransforms(ShapeWorldTransform0, ShapeWorldTransform1); // Detect collisions Constraint->ResetPhi(Constraint->GetCullDistance()); Collisions::UpdateConstraint(*Constraint, ShapeWorldTransform0, ShapeWorldTransform1, 1 / 30.0f); EXPECT_NEAR(Constraint->GetPhi(), ExpectedPhi, Tolerance); EXPECT_NEAR(Constraint->CalculateWorldContactNormal().X, ExpectedNormal.X, Tolerance); EXPECT_NEAR(Constraint->CalculateWorldContactNormal().Y, ExpectedNormal.Y, Tolerance); EXPECT_NEAR(Constraint->CalculateWorldContactNormal().Z, ExpectedNormal.Z, Tolerance); } TEST(CollisionTests, TestBoxBoxCollisionMargin) { // Zero-phi tests TestBoxBoxCollisionMargin(0, 0, FVec3(20, 100, 50), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(1, 1, FVec3(20, 100, 50), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(5, 10, FVec3(20, 100, 50), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(10, 5, FVec3(20, 100, 50), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); // Positive-phi test TestBoxBoxCollisionMargin(0, 0, FVec3(20, 100, 50), FVec3(0, -110, 0), 10.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(1, 1, FVec3(20, 100, 50), FVec3(0, -110, 0), 10.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(5, 10, FVec3(20, 100, 50), FVec3(0, -110, 0), 10.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(10, 5, FVec3(20, 100, 50), FVec3(0, -110, 0), 10.0f, FVec3(0, 1, 0)); // Negative-phi test TestBoxBoxCollisionMargin(0, 0, FVec3(20, 100, 50), FVec3(0, -90, 0), -10.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(1, 1, FVec3(20, 100, 50), FVec3(0, -90, 0), -10.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(5, 10, FVec3(20, 100, 50), FVec3(0, -90, 0), -10.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(10, 5, FVec3(20, 100, 50), FVec3(0, -90, 0), -10.0f, FVec3(0, 1, 0)); // If the specified margin is too large the margin will get reduced and it should all still work TestBoxBoxCollisionMargin(15, 15, FVec3(20, 100, 100), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); TestBoxBoxCollisionMargin(15, 15, FVec3(20, 100, 100), FVec3(20, 0, 0), 0.0f, FVec3(-1, 0, 0)); } // Two boxes that use a margin around a core AABB. // Test that collision detection treats the margin as part of the shape. void TestConvexConvexCollisionMargin( const FReal Margin0, const FReal Margin1, const FVec3& Size, const FVec3& Delta, const FReal ExpectedPhi, const FVec3& ExpectedNormal) { TArrayCollectionArray Collided; TUniquePtr PhysicsMaterial = MakeUnique(); PhysicsMaterial->Friction = 0; PhysicsMaterial->Restitution = 0; TArrayCollectionArray> PhysicsMaterials; TArrayCollectionArray> PerParticlePhysicsMaterials; FParticleUniqueIndicesMultithreaded UniqueIndices; FPBDRigidsSOAs Particles(UniqueIndices); Particles.GetParticleHandles().AddArray(&Collided); Particles.GetParticleHandles().AddArray(&PhysicsMaterials); Particles.GetParticleHandles().AddArray(&PerParticlePhysicsMaterials); auto Box0 = AppendDynamicParticleConvexBoxMargin(Particles, 0.5f * Size, Margin0); Box0->SetX(FVec3(0, 0, 0)); Box0->SetR(FRotation3(FQuat::Identity)); Box0->SetV(FVec3(0)); Box0->SetPreV(Box0->GetV()); Box0->SetP(Box0->GetX()); Box0->SetQ(Box0->GetR()); Box0->AuxilaryValue(PhysicsMaterials) = MakeSerializable(PhysicsMaterial); auto Box1 = AppendDynamicParticleConvexBoxMargin(Particles, 0.5f * Size, Margin1); Box1->SetX(Delta); Box1->SetR(FRotation3(FQuat::Identity)); Box1->SetV(FVec3(0)); Box1->SetPreV(Box1->GetV()); Box1->SetP(Box1->GetX()); Box1->SetQ(Box1->GetR()); Box1->AuxilaryValue(PhysicsMaterials) = MakeSerializable(PhysicsMaterial); const FImplicitConvex3* ConvexImplicit0 = Box0->GetGeometry()->template GetObject(); const FImplicitConvex3* ConvexImplicit1 = Box1->GetGeometry()->template GetObject(); const FReal Tolerance = 2.0f * KINDA_SMALL_NUMBER; // Should have a margin EXPECT_NEAR(ConvexImplicit0->GetMargin(), Margin0, Tolerance); EXPECT_NEAR(ConvexImplicit1->GetMargin(), Margin1, Tolerance); // Bounds should include margin const FAABB3 BoxBounds0 = ConvexImplicit0->BoundingBox(); const FAABB3 BoxBounds1 = ConvexImplicit1->BoundingBox(); EXPECT_NEAR(BoxBounds0.Extents().X, Size.X, Tolerance); EXPECT_NEAR(BoxBounds0.Extents().Y, Size.Y, Tolerance); EXPECT_NEAR(BoxBounds0.Extents().Z, Size.Z, Tolerance); EXPECT_NEAR(BoxBounds1.Extents().X, Size.X, Tolerance); EXPECT_NEAR(BoxBounds1.Extents().Y, Size.Y, Tolerance); EXPECT_NEAR(BoxBounds1.Extents().Z, Size.Z, Tolerance); Private::FCollisionConstraintAllocator CollisionAllocator; CollisionAllocator.SetMaxContexts(1); FPBDCollisionConstraintPtr Constraint = CollisionAllocator.GetContextAllocator(0)->CreateConstraint( Box0, Box0->GetGeometry(), Box0->ShapesArray()[0].Get(), nullptr, FRigidTransform3(), Box1, Box1->GetGeometry(), Box1->ShapesArray()[0].Get(), nullptr, FRigidTransform3(), FLT_MAX, true, EContactShapesType::GenericConvexConvex); FRigidTransform3 ShapeWorldTransform0 = Constraint->GetShapeRelativeTransform0() * Collisions::GetTransform(Constraint->GetParticle0()); FRigidTransform3 ShapeWorldTransform1 = Constraint->GetShapeRelativeTransform1() * Collisions::GetTransform(Constraint->GetParticle1()); Constraint->SetShapeWorldTransforms(ShapeWorldTransform0, ShapeWorldTransform1); // Detect collisions Collisions::UpdateConstraint(*Constraint, ShapeWorldTransform0, ShapeWorldTransform1, 1 / 30.0f); EXPECT_NEAR(Constraint->GetPhi(), ExpectedPhi, Tolerance); EXPECT_NEAR(Constraint->CalculateWorldContactNormal().X, ExpectedNormal.X, Tolerance); EXPECT_NEAR(Constraint->CalculateWorldContactNormal().Y, ExpectedNormal.Y, Tolerance); EXPECT_NEAR(Constraint->CalculateWorldContactNormal().Z, ExpectedNormal.Z, Tolerance); } TEST(CollisionTests, TestConvexConvexCollisionMargin) { // Zero-phi tests TestConvexConvexCollisionMargin(0, 0, FVec3(20, 100, 50), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(1, 1, FVec3(20, 100, 50), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(5, 10, FVec3(20, 100, 50), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(10, 5, FVec3(20, 100, 50), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); // Positive-phi test TestConvexConvexCollisionMargin(0, 0, FVec3(20, 100, 50), FVec3(0, -110, 0), 10.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(1, 1, FVec3(20, 100, 50), FVec3(0, -110, 0), 10.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(5, 10, FVec3(20, 100, 50), FVec3(0, -110, 0), 10.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(10, 5, FVec3(20, 100, 50), FVec3(0, -110, 0), 10.0f, FVec3(0, 1, 0)); // Negative-phi test TestConvexConvexCollisionMargin(0, 0, FVec3(20, 100, 50), FVec3(0, -90, 0), -10.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(1, 1, FVec3(20, 100, 50), FVec3(0, -90, 0), -10.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(5, 10, FVec3(20, 100, 50), FVec3(0, -90, 0), -10.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(10, 5, FVec3(20, 100, 50), FVec3(0, -90, 0), -10.0f, FVec3(0, 1, 0)); } TEST(CollisionTests, DISABLED_TestConvexConvexCollisionMarginTooLarge) { // If the margin is too large, the margin should be limited // @todo(chaos): fix this for convex - we do not have margin limits implemented TestConvexConvexCollisionMargin(15, 15, FVec3(20, 100, 100), FVec3(0, -100, 0), 0.0f, FVec3(0, 1, 0)); TestConvexConvexCollisionMargin(15, 15, FVec3(20, 100, 100), FVec3(20, 0, 0), 0.0f, FVec3(-1, 0, 0)); } // Check that the margin does not impact the box raycast functions void TestBoxRayCastsMargin( const FReal Margin0, const FVec3& Size, const FVec3& StartPos, const FVec3& Dir, const FReal Length, const bool bExpectedHit, const FReal ExpectedTime, const FVec3& ExpectedPosition, const FVec3& ExpectedNormal) { TArrayCollectionArray Collided; TUniquePtr PhysicsMaterial = MakeUnique(); PhysicsMaterial->Friction = 0; PhysicsMaterial->Restitution = 0; TArrayCollectionArray> PhysicsMaterials; TArrayCollectionArray> PerParticlePhysicsMaterials; FParticleUniqueIndicesMultithreaded UniqueIndices; FPBDRigidsSOAs Particles(UniqueIndices); Particles.GetParticleHandles().AddArray(&Collided); Particles.GetParticleHandles().AddArray(&PhysicsMaterials); Particles.GetParticleHandles().AddArray(&PerParticlePhysicsMaterials); auto Box0 = AppendDynamicParticleBoxMargin(Particles, Size, Margin0); Box0->SetX(FVec3(0, 0, 0)); Box0->SetR(FRotation3(FQuat::Identity)); Box0->SetV(FVec3(0)); Box0->SetPreV(Box0->GetV()); Box0->SetP(Box0->GetX()); Box0->SetQ(Box0->GetR()); Box0->AuxilaryValue(PhysicsMaterials) = MakeSerializable(PhysicsMaterial); const FImplicitBox3* BoxImplicit0 = Box0->GetGeometry()->template GetObject(); const FReal Tolerance = KINDA_SMALL_NUMBER; { FReal Time; FVec3 Position, Normal; int32 FaceIndex; bool bHit = BoxImplicit0->Raycast(StartPos, Dir, Length, 0.0f, Time, Position, Normal, FaceIndex); EXPECT_EQ(bHit, bExpectedHit); if (bHit) { EXPECT_NEAR(Time, ExpectedTime, Tolerance); EXPECT_NEAR(Position.X, ExpectedPosition.X, Tolerance); EXPECT_NEAR(Position.Y, ExpectedPosition.Y, Tolerance); EXPECT_NEAR(Position.Z, ExpectedPosition.Z, Tolerance); EXPECT_NEAR(Normal.X, ExpectedNormal.X, Tolerance); EXPECT_NEAR(Normal.Y, ExpectedNormal.Y, Tolerance); EXPECT_NEAR(Normal.Z, ExpectedNormal.Z, Tolerance); } } { FReal Time; FVec3 Position; bool bParallel[3]; FVec3 InvDir; for (int Axis = 0; Axis < 3; ++Axis) { bParallel[Axis] = FMath::IsNearlyZero(Dir[Axis], (FReal)1.e-8); InvDir[Axis] = bParallel[Axis] ? 0 : 1 / Dir[Axis]; } bool bHit = BoxImplicit0->RaycastFast(BoxImplicit0->Min(), BoxImplicit0->Max(), StartPos, Dir, InvDir, bParallel, Length, 1.0f / Length, Time, Position); EXPECT_EQ(bHit, bExpectedHit); if (bHit) { EXPECT_NEAR(Time, ExpectedTime, Tolerance); EXPECT_NEAR(Position.X, ExpectedPosition.X, Tolerance); EXPECT_NEAR(Position.Y, ExpectedPosition.Y, Tolerance); EXPECT_NEAR(Position.Z, ExpectedPosition.Z, Tolerance); } } } TEST(CollisionTests, TestBoxRayCastsMargin) { TestBoxRayCastsMargin(0, FVec3(100, 100, 100), FVec3(-200, 0, 0), FVec3(1, 0, 0), 500.0f, true, 150.0f, FVec3(-50, 0, 0), FVec3(-1, 0, 0)); // No Margin TestBoxRayCastsMargin(1, FVec3(100, 100, 100), FVec3(-200, 0, 0), FVec3(1, 0, 0), 500.0f, true, 150.0f, FVec3(-50, 0, 0), FVec3(-1, 0, 0)); // Small Margin TestBoxRayCastsMargin(50, FVec3(100, 100, 100), FVec3(-200, 0, 0), FVec3(1, 0, 0), 500.0f, true, 150.0f, FVec3(-50, 0, 0), FVec3(-1, 0, 0)); // Max margin TestBoxRayCastsMargin(70, FVec3(100, 100, 100), FVec3(-200, 0, 0), FVec3(1, 0, 0), 500.0f, true, 150.0f, FVec3(-50, 0, 0), FVec3(-1, 0, 0)); // Too much margin } TEST(ShapeInstanceTests, TestSingleMaterial) { { FImplicitObjectPtr CollidingCubeGeom = MakeImplicitObjectPtr>(-FVec3(100.0), FVec3(100.0)); TUniquePtr ShapeInstance1 = FShapeInstance::Make(0, CollidingCubeGeom); EXPECT_EQ(ShapeInstance1->NumMaterials(), 0); FMaterialHandle MaterialHandle = FPhysicalMaterialManager::Get().Create(); FMaterialData MaterialData; MaterialData.Materials.Add(MaterialHandle); ShapeInstance1->SetMaterialData(MaterialData); EXPECT_EQ(ShapeInstance1->NumMaterials(), 1); EXPECT_TRUE(ShapeInstance1->GetMaterial(0).InnerHandle.IsValid()); } { THandleArray SimMaterials; FChaosMaterialHandle MaterialInnerHandle = SimMaterials.Create(); FMaterialHandle MaterialHandle = FPhysicalMaterialManager::Get().Create(); MaterialHandle.InnerHandle = MaterialInnerHandle; FImplicitObjectPtr CollidingCubeGeom = MakeImplicitObjectPtr>(-FVec3(100.0), FVec3(100.0)); TUniquePtr ShapeInstance1 = FShapeInstance::Make(0, CollidingCubeGeom); EXPECT_EQ(ShapeInstance1->NumMaterialsInternal(&SimMaterials), 0); FMaterialData MaterialData; MaterialData.Materials.Add(MaterialHandle); ShapeInstance1->SetMaterialData(MaterialData); EXPECT_EQ(ShapeInstance1->NumMaterialsInternal(&SimMaterials), 1); EXPECT_TRUE(ShapeInstance1->GetMaterialInternal(0, &SimMaterials).InnerHandle.IsValid()); } } }