// Copyright Epic Games, Inc. All Rights Reserved. #include "HeadlessChaosTestCloth.h" #include "HeadlessChaos.h" #include "HeadlessChaosTestUtility.h" #include "Chaos/PBDSoftsEvolutionFwd.h" #include "Chaos/PBDEvolution.h" #include "Chaos/GeometryParticles.h" #include "Chaos/PBDAxialSpringConstraints.h" #include "Chaos/PBDSpringConstraints.h" #include "Chaos/TriangleMesh.h" #include "Chaos/Utilities.h" #include "GeometryCollection/ManagedArrayCollection.h" #include "Modules/ModuleManager.h" namespace ChaosTest { using namespace Chaos; DEFINE_LOG_CATEGORY_STATIC(LogChaosTestCloth, Verbose, All); TUniquePtr InitPBDEvolution( const int32 NumIterations=1, const Softs::FSolverReal CollisionThickness=KINDA_SMALL_NUMBER, const Softs::FSolverReal SelfCollisionThickness=KINDA_SMALL_NUMBER, const Softs::FSolverReal Friction=0.0, const Softs::FSolverReal Damping=0.04) { Chaos::Softs::FSolverParticles Particles; Chaos::Softs::FSolverCollisionParticles RigidParticles; TUniquePtr Evolution( new Softs::FPBDEvolution( MoveTemp(Particles), MoveTemp(RigidParticles), {}, NumIterations, CollisionThickness, SelfCollisionThickness, Friction, Damping)); return Evolution; } template void InitSingleParticle( TEvolutionPtr& Evolution, const Softs::FSolverVec3& Position = Softs::FSolverVec3(0), const Softs::FSolverVec3& Velocity = Softs::FSolverVec3(0), const Softs::FSolverReal Mass = 1.0) { auto& Particles = Evolution->Particles(); const uint32 Idx = Particles.Size(); Particles.AddParticles(1); Particles.SetX(Idx, Position); Particles.V(Idx) = Velocity; Particles.M(Idx) = Mass; Particles.InvM(Idx) = 1.0 / Mass; } template void InitTriMesh_EquilateralTri( FTriangleMesh& TriMesh, TEvolutionPtr& Evolution, const Softs::FSolverVec3& XOffset=Softs::FSolverVec3(0)) { auto& Particles = Evolution->Particles(); const uint32 InitialNumParticles = Particles.Size(); FTriangleMesh::InitEquilateralTriangleYZ(TriMesh, Particles); // Initialize particles. Use 1/3 area of connected triangles for particle mass. for (uint32 i = InitialNumParticles; i < Particles.Size(); i++) { Particles.SetX(i, Particles.GetX(i) + XOffset); Particles.V(i) = Chaos::Softs::FSolverVec3(0); Particles.M(i) = 0; } for (const Chaos::TVec3& Tri : TriMesh.GetElements()) { const Softs::FSolverReal TriArea = 0.5 * Chaos::Softs::FSolverVec3::CrossProduct( Particles.GetX(Tri[1]) - Particles.GetX(Tri[0]), Particles.GetX(Tri[2]) - Particles.GetX(Tri[0])).Size(); for (int32 i = 0; i < 3; i++) { Particles.M(Tri[i]) += TriArea / 3; } } for (uint32 i = InitialNumParticles; i < Particles.Size(); i++) { check(Particles.M(i) > SMALL_NUMBER); Particles.InvM(i) = 1.0 / Particles.M(i); } } template void AddEdgeLengthConstraint( TEvolutionPtr& Evolution, const TArray>& Topology, const Softs::FSolverReal Stiffness) { check(Stiffness >= 0. && Stiffness <= 1.); // TODO: Use Add AddConstraintRuleRange //Evolution->AddPBDConstraintFunction( // [SpringConstraints = Chaos::FPBDSpringConstraints( // Evolution->Particles(), Topology, Stiffness)]( // TPBDParticles& InParticles, const float Dt) // { // SpringConstraints.Apply(InParticles, Dt); // }); } template void AddAxialConstraint( TEvolutionPtr& Evolution, TArray>&& Topology, const Softs::FSolverReal Stiffness) { check(Stiffness >= 0. && Stiffness <= 1.); // TODO: Use Add AddConstraintRuleRange //Evolution->AddPBDConstraintFunction( // [SpringConstraints = Chaos::FPBDAxialSpringConstraints( // Evolution->Particles(), MoveTemp(Topology), Stiffness)]( // TPBDParticles& InParticles, const float Dt) // { // SpringConstraints.Apply(InParticles, Dt); // }); } template void AdvanceTime( TEvolutionPtr& Evolution, const uint32 NumFrames=1, const uint32 NumTimeStepsPerFrame=1, const uint32 FPS=24) { check(NumTimeStepsPerFrame > 0); Evolution->SetIterations(NumTimeStepsPerFrame); check(FPS > 0); const Softs::FSolverReal Dt = 1.0 / FPS; for (uint32 i = 0; i < NumFrames; i++) { Evolution->AdvanceOneTimeStep(Dt); } } template TArray CopyPoints(const TParticleContainer& Particles) { TArray Points; Points.SetNum(Particles.Size()); for (uint32 i = 0; i < Particles.Size(); i++) Points[i] = Particles.GetX(i); return Points; } template void Reset(TParticleContainer& Particles, const TArray& Points) { for (uint32 i = 0; i < Particles.Size(); i++) { Particles.SetX(i, Points[i]); Particles.V(i) = Chaos::Softs::FSolverVec3(0); } } TArray GetDifference(const TArray& A, const TArray& B) { TArray C; C.SetNum(A.Num()); for (int32 i = 0; i < A.Num(); i++) C[i] = A[i] - B[i]; return C; } TArray GetMagnitude(const TArray& V) { TArray M; M.SetNum(V.Num()); for (int32 i = 0; i < V.Num(); i++) M[i] = V[i].Size(); return M; } template bool AllSame(const TArray& V, int32& Idx, const T Tolerance=KINDA_SMALL_NUMBER) { if (!V.Num()) return true; const T& V0 = V[0]; for (int32 i = 1; i < V.Num(); i++) { if (!FMath::IsNearlyEqual(V0, V[i], Tolerance)) { Idx = i; return false; } } return true; } template bool RunDropTest( TUniquePtr& Evolution, const Softs::FSolverReal GravMag, const TV& GravDir, const TArray& InitialPoints, const int32 SubFrameSteps, const Softs::FSolverReal DistTolerance, const char* TestID) { const Softs::FSolverReal PreTime = Evolution->GetTime(); AdvanceTime(Evolution, 24, SubFrameSteps); // 1 second const Softs::FSolverReal PostTime = Evolution->GetTime(); EXPECT_NEAR(PostTime-PreTime, 1.0, KINDA_SMALL_NUMBER) << TestID << "Evolution advanced time by " << (PostTime - PreTime) << " seconds, expected 1.0 seconds."; const TArray PostPoints = CopyPoints(Evolution->Particles()); const TArray Diff = GetDifference(PostPoints, InitialPoints); const TArray ScalarDiff = GetMagnitude(Diff); // All points did the same thing int32 Idx = 0; EXPECT_TRUE(AllSame(ScalarDiff, Idx, (Softs::FSolverReal)0.1)) << TestID << "Points fell different distances - Index 0: " << ScalarDiff[0] << " != Index " << Idx << ": " << ScalarDiff[Idx] << " +/- 0.1."; // Fell the right amount EXPECT_NEAR(ScalarDiff[0], (Softs::FSolverReal)0.5 * GravMag, DistTolerance) << TestID << "Points fell by " << ScalarDiff[0] << ", expected " << ((Softs::FSolverReal)0.5 * GravMag) << " +/- " << DistTolerance << "."; // Fell the right direction const Softs::FSolverReal DirDot = Chaos::Softs::FSolverVec3::DotProduct(GravDir, Diff[0].GetSafeNormal()); EXPECT_NEAR(DirDot, (Softs::FSolverReal)1.0, (Softs::FSolverReal)KINDA_SMALL_NUMBER) << TestID << "Points fell in different directions."; return true; } void DeformableGravity() { const Softs::FSolverReal DistTol = 0.0002; // // Initialize solver and gravity // TUniquePtr Evolution = InitPBDEvolution(); const Softs::FSolverVec3 GravDir(0, 0, -1); const Softs::FSolverReal GravMag = 980.665; // // Drop a single particle // InitSingleParticle(Evolution); TArray InitialPoints = CopyPoints(Evolution->Particles()); RunDropTest(Evolution, GravMag, GravDir, InitialPoints, 1, DistTol, "Single point falling under gravity, iters: 1 - "); Reset(Evolution->Particles(), InitialPoints); RunDropTest(Evolution, GravMag, GravDir, InitialPoints, 100, DistTol, "Single point falling under gravity, iters: 100 - "); Reset(Evolution->Particles(), InitialPoints); // // Add a triangle mesh // Chaos::FTriangleMesh TriMesh; InitTriMesh_EquilateralTri(TriMesh, Evolution); InitialPoints = CopyPoints(Evolution->Particles()); // // Points falling under gravity // RunDropTest(Evolution, GravMag, GravDir, InitialPoints, 1, DistTol, "Points falling under gravity, iters: 1 - "); Reset(Evolution->Particles(), InitialPoints); RunDropTest(Evolution, GravMag, GravDir, InitialPoints, 100, DistTol, "Points falling under gravity, iters: 100 - "); Reset(Evolution->Particles(), InitialPoints); // // Points falling under gravity with edge length constraint // AddEdgeLengthConstraint(Evolution, TriMesh.GetSurfaceElements(), 1.0); RunDropTest(Evolution, GravMag, GravDir, InitialPoints, 1, DistTol, "Points falling under gravity & edge cnstr, iters: 1 - "); Reset(Evolution->Particles(), InitialPoints); RunDropTest(Evolution, GravMag, GravDir, InitialPoints, 100, DistTol, "Points falling under gravity & edge cnstr, iters: 100 - "); Reset(Evolution->Particles(), InitialPoints); } void EdgeConstraints() { TUniquePtr Evolution = InitPBDEvolution(); FTriangleMesh TriMesh; auto& Particles = Evolution->Particles(); Particles.AddParticles(2145); TArray> Triangles; Triangles.SetNum(2048); // 32 n, 32 m // 6 + 4*(n-1) + (m - 1)(3 + 2*(n-1)) = 2*n*m for (int32 TriIndex = 0; TriIndex < 2048; TriIndex++) { int32 i = ((float)rand()) / ((float)RAND_MAX) * 2144; int32 j = ((float)rand()) / ((float)RAND_MAX) * 2144; int32 k = ((float)rand()) / ((float)RAND_MAX) * 2144; Triangles[TriIndex] = TVec3(i, j, k); } AddEdgeLengthConstraint(Evolution, Triangles, 1.0); AddAxialConstraint(Evolution, MoveTemp(Triangles), 1.0); } void ClothCollection() { FManagedArrayCollection ManagedArrayCollection; const FName DataGroup("Data"); TManagedArray Value; ManagedArrayCollection.AddExternalAttribute("Value", DataGroup, Value); const FName ViewGroup("View"); FManagedArrayCollection::FConstructionParameters DataDependency(DataGroup); TManagedArray ValueStart; TManagedArray ValueEnd; TManagedArray Id; ManagedArrayCollection.AddExternalAttribute("ValueStart", ViewGroup, ValueStart, DataDependency); ManagedArrayCollection.AddExternalAttribute("ValueEnd", ViewGroup, ValueEnd, DataDependency); ManagedArrayCollection.AddExternalAttribute("Id", ViewGroup, Id); auto InsertView = [&ManagedArrayCollection, &Value, &ValueStart, &ValueEnd, &Id, &DataGroup, &ViewGroup](int32 ViewPosition, int32 NumValues) { check(ViewPosition <= ManagedArrayCollection.NumElements(ViewGroup)); EXPECT_TRUE(ManagedArrayCollection.InsertElements(1, ViewPosition, ViewGroup) == ViewPosition); static int32 ViewId = 0; Id[ViewPosition] = ViewId++; if (NumValues) { int32 PrevValuePosition = INDEX_NONE; for (int32 Index = ViewPosition - 1; Index >= 0; --Index) { if (ValueEnd[Index] != INDEX_NONE) { PrevValuePosition = ValueEnd[Index]; break; } } const int32 ValuePosition = PrevValuePosition + 1; EXPECT_TRUE(ManagedArrayCollection.InsertElements(NumValues, ValuePosition, DataGroup) == ValuePosition); ValueStart[ViewPosition] = ValuePosition; ValueEnd[ViewPosition] = ValuePosition + NumValues - 1; for (int32 ValueIndex = ValueStart[ViewPosition]; ValueIndex <= ValueEnd[ViewPosition]; ++ValueIndex) { Value[ValueIndex] = Id[ViewPosition]; } } else { ValueStart[ViewPosition] = INDEX_NONE; ValueEnd[ViewPosition] = INDEX_NONE; } }; auto RemoveViews = [&ManagedArrayCollection, &ValueStart, &ValueEnd, &DataGroup, &ViewGroup](int32 ViewPosition, int32 NumViews) { const int32 ViewStart = ViewPosition; const int32 ViewEnd = ViewPosition + NumViews - 1; for (int32 ViewIndex = ViewStart; ViewIndex <= ViewEnd; ++ViewIndex) { // Remove data const int32 Position = ValueStart[ViewIndex]; if (Position != INDEX_NONE) { const int32 NumValues = ValueEnd[ViewIndex] - ValueStart[ViewIndex] + 1; ManagedArrayCollection.RemoveElements(DataGroup, NumValues, Position); } } // Remove views ManagedArrayCollection.RemoveElements(ViewGroup, NumViews, ViewPosition); }; auto SetViewSize = [&ManagedArrayCollection, &Value, &ValueStart, &ValueEnd, &Id, &DataGroup, &ViewGroup](int32 ViewIndex, int32 InNumValues) { check(InNumValues >= 0); int32& Start = ValueStart[ViewIndex]; int32& End = ValueEnd[ViewIndex]; check(Start != INDEX_NONE || End == INDEX_NONE); const int32 NumValues = (Start == INDEX_NONE) ? 0 : End - Start + 1; if (const int32 Delta = InNumValues - NumValues) { if (Delta > 0) { // Find a previous valid index range to insert after when the range is empty auto ComputeEnd = [&ValueEnd](int32 ViewIndex)->int32 { for (int32 Index = ViewIndex; Index >= 0; --Index) { if (ValueEnd[Index] != INDEX_NONE) { return ValueEnd[Index]; } } return INDEX_NONE; }; // Grow the array const int32 Position = ComputeEnd(ViewIndex) + 1; ManagedArrayCollection.InsertElements(Delta, Position, DataGroup); // Update Start/End if (!NumValues) { Start = Position; } End = Start + InNumValues - 1; // Fill the test values with the id check for (int32 ValueIndex = Start; ValueIndex <= End; ++ValueIndex) { Value[ValueIndex] = Id[ViewIndex]; } } else { // Shrink the array const int32 Position = Start + InNumValues; ManagedArrayCollection.RemoveElements(DataGroup, -Delta, Position); // Update Start/End if (InNumValues) { End = Position - 1; } else { End = Start = INDEX_NONE; } } } const int32 Size = ValueEnd[ViewIndex] - ValueStart[ViewIndex] + 1; check( (InNumValues == 0 && ValueStart[ViewIndex] == INDEX_NONE && ValueEnd[ViewIndex] == INDEX_NONE) || (InNumValues == Size && ValueStart[ViewIndex] != INDEX_NONE && ValueEnd[ViewIndex] != INDEX_NONE)); }; auto HasKeptIntegrity = [&Value , &ValueStart, &ValueEnd, &Id]()->bool { EXPECT_TRUE(ValueEnd.Num() == ValueStart.Num() && Id.Num() == ValueStart.Num()); for (int32 ViewIndex = 0; ViewIndex < ValueStart.Num(); ++ViewIndex) { if (ValueStart[ViewIndex] == INDEX_NONE || ValueEnd[ViewIndex] == INDEX_NONE) { if (ValueStart[ViewIndex] != ValueEnd[ViewIndex]) { return false; } continue; } for (int32 ValueIndex = ValueStart[ViewIndex]; ValueIndex <= ValueEnd[ViewIndex]; ++ValueIndex) { if (Value[ValueIndex] != Id[ViewIndex]) { return false; } } } return true; }; auto HasKeptOrder = [&Value, &ValueStart, &ValueEnd, &Id]()->bool { int32 PrevValueEnd = -1; for (int32 ViewIndex = 0; ViewIndex < ValueStart.Num(); ++ViewIndex) { if (ValueStart[ViewIndex] == INDEX_NONE) { check(ValueEnd[ViewIndex] == INDEX_NONE); continue; } if (ValueStart[ViewIndex] != PrevValueEnd + 1) { return false; } PrevValueEnd = ValueEnd[ViewIndex]; } return PrevValueEnd == Value.Num() - 1; }; // Insert 5 views from the start of the array for (int32 Index = 0; Index < 5; ++Index) { InsertView(0, (Index + 1) * 100); } // Insert 2 more views for testing consecutive empty views for (int32 Index = 0; Index < 2; ++Index) { InsertView(0, 0); } EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Add 1 view in the middle of the array InsertView(ManagedArrayCollection.NumElements(ViewGroup) / 2, 500); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Add 1 view at the end of the array InsertView(ManagedArrayCollection.NumElements(ViewGroup), 1000); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Remove 1 view from the start of the array RemoveViews(0, 1); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Remove 1 view from the end of the array RemoveViews(ManagedArrayCollection.NumElements(ViewGroup) - 1, 1); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Remove 2 views from the middle RemoveViews(ManagedArrayCollection.NumElements(ViewGroup) / 2, 2); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Remove all remaining 5 views RemoveViews(0, 5); EXPECT_TRUE(ManagedArrayCollection.NumElements(ViewGroup) == 0); EXPECT_TRUE(ManagedArrayCollection.NumElements(DataGroup) == 0); // Insert 5 empty views for (int32 Index = 0; Index < 5; ++Index) { InsertView(0, 0); } EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Resize view 1 SetViewSize(1, 11); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Resize view 2 SetViewSize(2, 22); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Resize view 3 SetViewSize(3, 33); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Remove view 2 RemoveViews(2, 1); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Resize view 1 to 0 SetViewSize(1, 0); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Resize view 3 to 33 SetViewSize(3, 33); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Resize view 3 to 0 SetViewSize(3, 0); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); // Resize view 0 SetViewSize(0, 1); EXPECT_TRUE(HasKeptIntegrity()); EXPECT_TRUE(HasKeptOrder()); } } // namespace ChaosTest