2242 lines
76 KiB
C++
2242 lines
76 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "HeadlessChaosTestUtility.h"
|
|
#include "Chaos/ParticleHandle.h"
|
|
#include "Chaos/ErrorReporter.h"
|
|
#include "PhysicsProxy/SingleParticlePhysicsProxy.h"
|
|
#include "PhysicsProxy/GeometryCollectionPhysicsProxy.h"
|
|
#include "Chaos/Utilities.h"
|
|
#include "PBDRigidsSolver.h"
|
|
#include "ChaosSolversModule.h"
|
|
|
|
#include "Modules/ModuleManager.h"
|
|
#include "Chaos/ChaosEngineInterface.h"
|
|
#include "Chaos/ChaosScene.h"
|
|
#include "SQAccelerator.h"
|
|
#include "CollisionQueryFilterCallbackCore.h"
|
|
#include "BodyInstanceCore.h"
|
|
|
|
namespace Chaos
|
|
{
|
|
extern CHAOS_API float AsyncInterpolationMultiplier;
|
|
}
|
|
|
|
namespace ChaosTest {
|
|
|
|
using namespace Chaos;
|
|
using namespace ChaosInterface;
|
|
|
|
// Returns true on raycast if we hit payload bounds.
|
|
struct FSimpleRaycastVisitor: ISpatialVisitor<FAccelerationStructureHandle>
|
|
{
|
|
using FPayload = FAccelerationStructureHandle;
|
|
FVec3 Start;
|
|
bool bHit;
|
|
bool bQueryGameThread; // Query game thread or physics thread data?
|
|
|
|
bool bUseQueryFilter;
|
|
FCollisionFilterData FilterData;
|
|
|
|
FSimpleRaycastVisitor(const FVec3& InStart, bool bInQueryGameThread)
|
|
: Start(InStart)
|
|
, bHit(false)
|
|
, bQueryGameThread(bInQueryGameThread)
|
|
{
|
|
}
|
|
|
|
FSimpleRaycastVisitor(const FVec3& InStart, FCollisionFilterData& InFilterData, bool bInQueryGameThread)
|
|
: Start(InStart)
|
|
, bHit(false)
|
|
, bQueryGameThread(bInQueryGameThread)
|
|
, bUseQueryFilter(true)
|
|
, FilterData(InFilterData)
|
|
|
|
{
|
|
}
|
|
|
|
virtual const void* GetQueryData() const override
|
|
{
|
|
if (bUseQueryFilter)
|
|
{
|
|
return &FilterData;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
enum class SQType
|
|
{
|
|
Raycast,
|
|
Sweep,
|
|
Overlap
|
|
};
|
|
|
|
bool VisitRaycast(const TSpatialVisitorData<FPayload>& Data, FQueryFastData& CurData)
|
|
{
|
|
FReal OutTime = 0;
|
|
FVec3 OutPos;
|
|
FVec3 OutNorm;
|
|
int32 FaceIdx;
|
|
|
|
if (Data.Bounds.Raycast(Start, CurData.Dir, CurData.CurrentLength, 0, OutTime, OutPos, OutNorm, FaceIdx))
|
|
{
|
|
if (bQueryGameThread)
|
|
{
|
|
FTransform ParticleTransform(Data.Payload.GetExternalGeometryParticle_ExternalThread()->R(), Data.Payload.GetExternalGeometryParticle_ExternalThread()->GetX());
|
|
const FVec3 DirLocal = ParticleTransform.InverseTransformVectorNoScale(CurData.Dir);
|
|
const FVec3 StartLocal = ParticleTransform.InverseTransformPositionNoScale(Start);
|
|
bHit = Data.Payload.GetExternalGeometryParticle_ExternalThread()->GetGeometry()->Raycast(StartLocal, DirLocal, CurData.CurrentLength, 0, OutTime, OutPos, OutNorm, FaceIdx);
|
|
}
|
|
else
|
|
{
|
|
FTransform ParticleTransform(Data.Payload.GetGeometryParticleHandle_PhysicsThread()->GetR(), Data.Payload.GetGeometryParticleHandle_PhysicsThread()->GetX());
|
|
const FVec3 DirLocal = ParticleTransform.InverseTransformVectorNoScale(CurData.Dir);
|
|
const FVec3 StartLocal = ParticleTransform.InverseTransformPositionNoScale(Start);
|
|
bHit = Data.Payload.GetGeometryParticleHandle_PhysicsThread()->GetGeometry()->Raycast(StartLocal, DirLocal, CurData.CurrentLength, 0, OutTime, OutPos, OutNorm, FaceIdx);
|
|
}
|
|
|
|
if (bHit)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VisitSweep(const TSpatialVisitorData<FPayload>& Data, FQueryFastData& CurData)
|
|
{
|
|
check(false);
|
|
return false;
|
|
}
|
|
|
|
bool VisitOverlap(const TSpatialVisitorData<FPayload>& Data)
|
|
{
|
|
check(false);
|
|
return false;
|
|
}
|
|
|
|
virtual bool Overlap(const TSpatialVisitorData<FPayload>& Instance) override
|
|
{
|
|
check(false);
|
|
return false;
|
|
}
|
|
|
|
virtual bool Raycast(const TSpatialVisitorData<FPayload>& Instance, FQueryFastData& CurData) override
|
|
{
|
|
return VisitRaycast(Instance, CurData);
|
|
}
|
|
|
|
virtual bool Sweep(const TSpatialVisitorData<FPayload>& Instance, FQueryFastData& CurData) override
|
|
{
|
|
check(false);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
FSQHitBuffer<ChaosInterface::FOverlapHit> InSphereHelper(const FChaosScene& Scene, const FTransform& InTM, const FReal Radius)
|
|
{
|
|
FChaosSQAccelerator SQAccelerator(*Scene.GetSpacialAcceleration());
|
|
FSQHitBuffer<ChaosInterface::FOverlapHit> HitBuffer;
|
|
FOverlapAllQueryCallback QueryCallback;
|
|
SQAccelerator.Overlap(Chaos::FSphere(FVec3(0), Radius), InTM, HitBuffer, FChaosQueryFilterData(), QueryCallback, FQueryDebugParams());
|
|
return HitBuffer;
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, CreateAndReleaseActor)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Proxy->GetGameThreadAPI().SetGeometry(Sphere);
|
|
}
|
|
|
|
FChaosEngineInterface::ReleaseActor(Proxy, &Scene);
|
|
EXPECT_EQ(Proxy, nullptr);
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, CreateMoveAndReleaseInScene)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
//make sure acceleration structure has new actor right away
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 1);
|
|
}
|
|
|
|
//make sure acceleration structure sees moved actor right away
|
|
const FTransform MovedTM(FQuat::Identity, FVec3(100, 0, 0));
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, MovedTM);
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 0);
|
|
|
|
const auto HitBuffer2 = InSphereHelper(Scene, MovedTM, 3);
|
|
EXPECT_EQ(HitBuffer2.GetNumHits(), 1);
|
|
}
|
|
|
|
//move actor back and acceleration structure sees it right away
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform::Identity);
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 1);
|
|
}
|
|
|
|
FChaosEngineInterface::ReleaseActor(Proxy, &Scene);
|
|
EXPECT_EQ(Proxy, nullptr);
|
|
|
|
//make sure acceleration structure no longer has actor
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 0);
|
|
}
|
|
|
|
}
|
|
|
|
template <typename TSolver>
|
|
void AdvanceSolverNoPushHelper(TSolver* Solver, FReal Dt)
|
|
{
|
|
Solver->AdvanceSolverBy(Dt);
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, AccelerationStructureHasSyncTimestamp)
|
|
{
|
|
//make sure acceleration structure has appropriate sync time
|
|
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); //timestamp of 0 because we flush when scene is created
|
|
|
|
FReal TotalDt = 0;
|
|
for (int Step = 1; Step < 10; ++Step)
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav, 1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration(); //make sure we get a new tree every step
|
|
Scene.EndFrame();
|
|
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), Step - 1);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, AccelerationStructureHasSyncTimestamp_MultiFrameDelay)
|
|
{
|
|
//make sure acceleration structure has appropriate sync time when PT falls behind GT
|
|
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->SetStealAdvanceTasks_ForTesting(true); // prevents execution on StartFrame so we can execute task manually.
|
|
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); //timestamp of 0 because we flush when scene is created
|
|
|
|
FVec3 Grav(0, 0, -1);
|
|
Scene.SetUpForFrame(&Grav, 1, 0, 99999, 99999, 10, false);
|
|
|
|
// Game thread enqueues second solver task before first completes (we did not execute advance task)
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
Scene.StartFrame();
|
|
|
|
// Execute first enqueued advance task
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
|
|
Scene.EndFrame();
|
|
|
|
// Still timestamp 0, as we have only processed first PT step..
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0);
|
|
|
|
Scene.StartFrame();
|
|
|
|
// PT catches up during this frame
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
Scene.EndFrame();
|
|
|
|
// New structure should be at 2, 3 steps have been processed, PT/GT are in sync.
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 2);
|
|
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, AccelerationStructureHasSyncTimestamp_MultiFrameDelay2)
|
|
{
|
|
//make sure acceleration structure has appropriate sync time when PT falls behind GT
|
|
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->SetStealAdvanceTasks_ForTesting(true); // prevents execution on StartFrame so we can execute task manually.
|
|
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0); //timestamp of 0 because we flush when scene is created
|
|
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav, 1,0,99999,99999,10,false);
|
|
|
|
// PT not finished yet (we didn't execute solver task), should still be 0.
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0);
|
|
|
|
// PT not finished yet (we didn't execute solver task), should still be 0.
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0);
|
|
|
|
// First PT task finished this frame, we are two behind, still at 0 as structure is from first GT input (timestamp 0).
|
|
Scene.StartFrame();
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
Scene.EndFrame();
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0);
|
|
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
// Remaining two PT tasks finish, we are caught up, GT is still time 0 as EndFrame has not updated our structure.
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 0);
|
|
|
|
// Popping acceleration structures from physics thread will give us timestamp of 2. (3 total GT inputs processed)
|
|
Scene.CopySolverAccelerationStructure();
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 2);
|
|
|
|
// PT task this frame finishes before EndFrame, putting us at 3, in sync with GT.
|
|
Scene.StartFrame();
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
Scene.EndFrame();
|
|
EXPECT_EQ(Scene.GetSpacialAcceleration()->GetSyncTimestamp(), 3);
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, PullFromPhysicsState_MultiFrameDelay)
|
|
{
|
|
// This test is designed to verify pulldata is being timestamped correctly, and that we will not write to a deleted GT Proxy
|
|
// in this case.
|
|
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->SetStealAdvanceTasks_ForTesting(true); // prevents execution on StartFrame so we can execute task manually.
|
|
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav, 1,0,99999,99999,10,false);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
Params.bSimulatePhysics = true;
|
|
Params.bEnableGravity = true;
|
|
Params.bStartAwake = true;
|
|
|
|
|
|
// Create two Proxys, one to remove for test, the other to ensure we have > 0 proxies to hit the pull physics data path.
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
FPhysicsActorHandle Proxy2 = nullptr;
|
|
FChaosEngineInterface::CreateActor(Params, Proxy2);
|
|
auto& Particle2 = Proxy2->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy2, nullptr);
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle2.SetGeometry(Sphere);
|
|
}
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy, Proxy2 };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
// verify external timestamps are as expected.
|
|
auto& MarshallingManager = Scene.GetSolver()->GetMarshallingManager();
|
|
EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 0);
|
|
|
|
// Execute a frame such that Proxys should be initialized in physics thread and game thread.
|
|
Scene.StartFrame();
|
|
EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 1);
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.EndFrame();
|
|
|
|
// run GT frame, no PT task executed.
|
|
Scene.StartFrame();
|
|
EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 2);
|
|
Scene.EndFrame();
|
|
|
|
// enqueue another frame.
|
|
Scene.StartFrame();
|
|
EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 3);
|
|
|
|
// Remove Proxy, is stamped with external time 3. PT needs to run 3 frames before this will be removed,
|
|
// as we are two PT tasks behind, and this has not been enqueued yet.
|
|
auto StaleProxy = Proxy;
|
|
FChaosEngineInterface::ReleaseActor(Proxy, &Scene);
|
|
EXPECT_EQ(Proxy, nullptr);
|
|
EXPECT_EQ(StaleProxy->GetSyncTimestamp()->bDeleted, true);
|
|
|
|
// Run PT task for internal timestamp 1.
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
|
|
// Proxy should not get touched in Pull, as timestamp from removal should be greater than pulldata timestamp.
|
|
// (if it was touched we'd crash as it is now deleted).
|
|
Scene.EndFrame();
|
|
|
|
|
|
Scene.StartFrame();
|
|
EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 4);
|
|
EXPECT_EQ(StaleProxy->GetSyncTimestamp()->bDeleted, true);
|
|
|
|
// run pt task for internal timestamp 3. Proxy still not removed on PT.
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
EXPECT_EQ(Scene.GetSolver()->GetEvolution()->GetParticles().GetAllParticlesView().Num(), 2); // none have been removed on pt, still 2 Proxys.
|
|
|
|
// Proxy should not get touched in pull, as timestamp from removal is less than pulldata timestamp (3 < 4)
|
|
// If this crashes in pull, that means this test has regressed. (Pulldata timestamp is likely wrong).
|
|
Scene.EndFrame();
|
|
|
|
|
|
Scene.StartFrame();
|
|
EXPECT_EQ(MarshallingManager.GetExternalTimestamp_External(), 5);
|
|
EXPECT_EQ(StaleProxy->GetSyncTimestamp()->bDeleted, true);
|
|
EXPECT_EQ(Scene.GetSolver()->GetEvolution()->GetParticles().GetAllParticlesView().Num(), 2); // Proxys not yet removed on pt, still 2.
|
|
|
|
|
|
// This is PT task that should remove Proxy (internal timestamp 4, matching stamp on removed Proxy's dirty data).
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
EXPECT_EQ(Scene.GetSolver()->GetEvolution()->GetParticles().GetAllParticlesView().Num(), 1); // one Proxy removed on pt, one remaining.
|
|
|
|
// This PT task catches up to gamethread.
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
Scene.EndFrame();
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, UpdatingAccelerationStructurePrePreFilterOnShapeFilterChange)
|
|
{
|
|
const float PhysicsTimestep = 1; // 1 second
|
|
FChaosScene Scene(nullptr, PhysicsTimestep);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
float DeltaSeconds = PhysicsTimestep;
|
|
FVec3 Grav(0, 0, -1);
|
|
Scene.SetUpForFrame(&Grav, DeltaSeconds, 0, 9999, 9999, 9999, false);
|
|
|
|
// Raycast params, aimed to hit our particle at (0,0,0)
|
|
const FVector Start(0, 0, -5);
|
|
const FVector Dir(0, 0, 1);
|
|
const float Length = 50;
|
|
|
|
// Init kinematic particle, sphere radius 3
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
Params.bSimulatePhysics = false;
|
|
Params.bEnableGravity = true;
|
|
Params.bStartAwake = true;
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
// Execute a whole frame such that particle is initialized on physics thread
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
|
|
// Make query filter that will allow query against particle that blocks/touches all channels.
|
|
// Filter will fail against particle that has no query allowed (default query filter).
|
|
FCollisionFilterData QueryFilter;
|
|
QueryFilter.Word0 = 1; // Setting to non-zero to set query type that will filter
|
|
|
|
// This is setting a somewhat arbritrary trace channels. It's very hard to make sense of these bitfields at this level of API.
|
|
// Below particle uses a filter that touches/blocks anything, so these bits are enough to make filter pass.
|
|
QueryFilter.Word3 = 7 << 21;
|
|
|
|
|
|
// Get collision data off shape
|
|
for (const TUniquePtr<Chaos::FPerShapeData>& Shape : Particle.ShapesArray())
|
|
{
|
|
const FCollisionData& CollisionData = Shape->GetCollisionData();
|
|
EXPECT_EQ(CollisionData.QueryData.Word0, 0); // ensure query filter is defaulted to 0 (no query allowed at all)
|
|
|
|
// Verify query is filtered out with default collision data on shape
|
|
bool bFiltered = PrePreQueryFilterImp(QueryFilter, CollisionData.QueryData);
|
|
EXPECT_EQ(bFiltered, true);
|
|
}
|
|
|
|
// Query against particle on game thread, should fail to hit due to particle filter being defaulted, no touch/block set.
|
|
{
|
|
bool bQueryGameThread = true;
|
|
FSimpleRaycastVisitor Visitor(Start, QueryFilter, bQueryGameThread);
|
|
Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor);
|
|
EXPECT_EQ(Visitor.bHit, false);
|
|
}
|
|
|
|
// Change filter data on game thread to contain touch/block on all channels.
|
|
FCollisionFilterData NewParticleQueryFilter;
|
|
NewParticleQueryFilter.Word1 = TNumericLimits<int32>::Max();
|
|
NewParticleQueryFilter.Word2 = TNumericLimits<int32>::Max();
|
|
for (const TUniquePtr<Chaos::FPerShapeData>& Shape : Particle.ShapesArray())
|
|
{
|
|
const FCollisionData& CollisionData = Shape->GetCollisionData();
|
|
|
|
// Update filter
|
|
FCollisionData NewCollisionData = CollisionData;
|
|
NewCollisionData.QueryData = NewParticleQueryFilter;
|
|
Shape->SetCollisionData(NewCollisionData);
|
|
|
|
// Filter with new data, ensuring we pass and are not filtered out.
|
|
bool bFiltered = PrePreQueryFilterImp(QueryFilter, NewCollisionData.QueryData);
|
|
EXPECT_EQ(bFiltered, false);
|
|
}
|
|
|
|
// Update particle in GT accel structure so cached PrePreFilter updates
|
|
Scene.UpdateActorInAccelerationStructure(Proxy);
|
|
|
|
// Query against particle on game thread, should hit with new filter.
|
|
{
|
|
bool bQueryGameThread = true;
|
|
FSimpleRaycastVisitor Visitor(Start, QueryFilter, bQueryGameThread);
|
|
Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor);
|
|
EXPECT_EQ(Visitor.bHit, true);
|
|
}
|
|
|
|
// Tick to push to physics thread
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
// Query particle on physics thread, expected to hit with new filter.
|
|
// If this fails it means we did not update cached filter data in acceleration structure entry.
|
|
{
|
|
bool bQueryGameThread = false;
|
|
FSimpleRaycastVisitor Visitor(Start, QueryFilter, bQueryGameThread);
|
|
Scene.GetSolver()->GetEvolution()->GetSpatialAcceleration()->Raycast(Start, Dir, Length, Visitor);
|
|
EXPECT_EQ(Visitor.bHit, true);
|
|
}
|
|
}
|
|
|
|
// Disabled until we move fix with kineamtic bounds update on PushToPhysicsState into this branch. Might also need to remove bounds computation in ApplyKinematicTarget.
|
|
GTEST_TEST(EngineInterface, DISABLED_KinematicTargetsPassingGTWrongAccelBoundsBeforeHittingTarget)
|
|
{
|
|
// This test is designed to catch an edge case with kinematic targets (or other things interpolated over multiple physics steps), and acceleration structure bounds.
|
|
// Timestep is setup such that 1 GT frame = 10 physics steps, we have to make sure that if a non-final step gives an acceleration structure to game thread, in which
|
|
// kinematic has not reached target yet, that the bounds in structure representing interpolated position do not make it to game thread, otherwise game thread has
|
|
// position at target, but bounds that don't match.
|
|
|
|
const float PhysicsTimestep = 1; // 1 second
|
|
|
|
// Setup solver so we can manually execute each physics step.
|
|
FChaosScene Scene(nullptr, PhysicsTimestep);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->SetStealAdvanceTasks_ForTesting(true);
|
|
|
|
// In this test we have a 10s Dt, split into 10 physics steps of 1s.
|
|
const int32 PhysicsStepsInFrame = 10;
|
|
float DeltaSeconds = PhysicsTimestep * PhysicsStepsInFrame;
|
|
FVec3 Grav(0, 0, -1);
|
|
|
|
Scene.SetUpForFrame(&Grav, DeltaSeconds, 0, 9999, 9999, 9999, false);
|
|
|
|
// Raycast params, aimed to hit our kinematic target (10,0,0)
|
|
const FVector Start(10, 0, -5);
|
|
const FVector Dir(0, 0,1);
|
|
const float Length = 50;
|
|
|
|
// Init kinematic particle, sphere radius 3
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
Params.bSimulatePhysics = false;
|
|
Params.bEnableGravity = true;
|
|
Params.bStartAwake = true;
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
// Execute a whole frame such that particle is initialized on physics thread
|
|
Scene.StartFrame();
|
|
for (int32 PhysicsTicks = 0; PhysicsTicks < PhysicsStepsInFrame; ++PhysicsTicks)
|
|
{
|
|
// Tick each physics step generated from game thread input
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
}
|
|
Scene.EndFrame();
|
|
|
|
// Set kinematic target to (10,0,0) on game thread
|
|
FTransform Target(FVector(10, 0, 0));
|
|
FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, Target);
|
|
|
|
// Confirm particle is at target on game thread with raycast.
|
|
{
|
|
FSimpleRaycastVisitor Visitor(Start, true);
|
|
Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor);
|
|
EXPECT_EQ(Visitor.bHit, true);
|
|
}
|
|
|
|
|
|
|
|
// Tick game thread again, this enqueues 10 physics steps, kinematic will interpolate
|
|
// to target on physics thread over duration of these 10 steps.
|
|
Scene.StartFrame();
|
|
|
|
|
|
|
|
for (int32 PhysicsTick = 0; PhysicsTick < PhysicsStepsInFrame; ++PhysicsTick)
|
|
{
|
|
|
|
Scene.GetSolver()->PopAndExecuteStolenAdvanceTask_ForTesting();
|
|
|
|
if (PhysicsTick == 2)
|
|
{
|
|
// On this arbritrary tick, copy acceleration structure to game thread,
|
|
// at this point we have sim'd only some of the physics steps for this frame.
|
|
// kinematic target is still interpolating, has not reached target of (10,0,0) yet.
|
|
// When this was broken this would give game thread a structure with
|
|
// the bounds of interpolated position (which is wrong because game thread particle is at target!)
|
|
Scene.CopySolverAccelerationStructure();
|
|
|
|
// Verify the game thread particle can still be queried at target (verifying bounds and particle position are still correct)
|
|
{
|
|
FSimpleRaycastVisitor Visitor(Start, true);
|
|
Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor);
|
|
EXPECT_EQ(Visitor.bHit, true);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Finish frame
|
|
Scene.EndFrame();
|
|
|
|
// Verify can still query game thread particle at our target.
|
|
{
|
|
FSimpleRaycastVisitor Visitor(Start, true);
|
|
Scene.GetSpacialAcceleration()->Raycast(Start, Dir, Length, Visitor);
|
|
EXPECT_EQ(Visitor.bHit, true);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, CreateActorPostFlush)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
//tick solver but don't call EndFrame (want to flush and swap manually)
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
}
|
|
|
|
//make sure acceleration structure is built
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
|
|
//create actor after structure is finished, but before swap happens
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 1);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, MoveActorPostFlush)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
//create actor before structure is ticked
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
//tick solver so that Proxy is created, but don't call EndFrame (want to flush and swap manually)
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
}
|
|
|
|
//make sure acceleration structure is built
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
|
|
//move object to get hit (shows pending move is applied)
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(FRotation3::FromIdentity(), FVec3(100, 0, 0)));
|
|
|
|
Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply
|
|
{
|
|
TRigidTransform<FReal, 3> OverlapTM(FVec3(100, 0, 0), FRotation3::FromIdentity());
|
|
const auto HitBuffer = InSphereHelper(Scene, OverlapTM, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 1);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, RemoveActorPostFlush)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
//create actor before structure is ticked
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
//tick solver so that Proxy is created, but don't call EndFrame (want to flush and swap manually)
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
}
|
|
|
|
//make sure acceleration structure is built
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
|
|
//delete object to get no hit
|
|
FChaosEngineInterface::ReleaseActor(Proxy, &Scene);
|
|
|
|
Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 0);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, RemoveActorPostFlush0Dt)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
//create actor before structure is ticked
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
//tick solver so that Proxy is created, but don't call EndFrame (want to flush and swap manually)
|
|
{
|
|
//use 0 dt to make sure pending operations are not sensitive to 0 dt
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,0,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
}
|
|
|
|
//make sure acceleration structure is built
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
|
|
//delete object to get no hit
|
|
FChaosEngineInterface::ReleaseActor(Proxy, &Scene);
|
|
|
|
Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 0);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, CreateAndRemoveActorPostFlush)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
//tick solver, but don't call EndFrame (want to flush and swap manually)
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
}
|
|
|
|
//make sure acceleration structure is built
|
|
Scene.GetSolver()->GetEvolution()->FlushSpatialAcceleration();
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
//create actor after flush
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
//delete object right away to get no hit
|
|
FChaosEngineInterface::ReleaseActor(Proxy, &Scene);
|
|
|
|
Scene.CopySolverAccelerationStructure(); //trigger swap manually and see pending changes apply
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 0);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, CreateDelayed)
|
|
{
|
|
for (int Delay = 0; Delay < 4; ++Delay)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->GetMarshallingManager().SetTickDelay_External(Delay);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
//create actor after flush
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
for (int Repeat = 0; Repeat < Delay; ++Repeat)
|
|
{
|
|
//tick solver
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
}
|
|
|
|
//make sure sim hasn't seen it yet
|
|
{
|
|
FPBDRigidsEvolution* Evolution = Scene.GetSolver()->GetEvolution();
|
|
const auto& SOA = Evolution->GetParticles();
|
|
EXPECT_EQ(SOA.GetAllParticlesView().Num(), 0);
|
|
}
|
|
|
|
//make sure external thread knows about it
|
|
{
|
|
const auto HitBuffer = InSphereHelper(Scene, FTransform::Identity, 3);
|
|
EXPECT_EQ(HitBuffer.GetNumHits(), 1);
|
|
}
|
|
}
|
|
|
|
//tick solver one last time
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
}
|
|
|
|
//now sim knows about it
|
|
{
|
|
FPBDRigidsEvolution* Evolution = Scene.GetSolver()->GetEvolution();
|
|
const auto& SOA = Evolution->GetParticles();
|
|
EXPECT_EQ(SOA.GetAllParticlesView().Num(), 1);
|
|
}
|
|
|
|
Particle.SetX(FVec3(5, 0, 0));
|
|
|
|
for (int Repeat = 0; Repeat < Delay; ++Repeat)
|
|
{
|
|
//tick solver
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
}
|
|
|
|
//make sure sim hasn't seen new X yet
|
|
{
|
|
FPBDRigidsEvolution* Evolution = Scene.GetSolver()->GetEvolution();
|
|
const auto& SOA = Evolution->GetParticles();
|
|
const auto& InternalProxy = *SOA.GetAllParticlesView().Begin();
|
|
EXPECT_EQ(InternalProxy.GetX()[0], 0);
|
|
}
|
|
}
|
|
|
|
//tick solver one last time
|
|
{
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
}
|
|
|
|
//now sim knows about new X
|
|
{
|
|
FPBDRigidsEvolution* Evolution = Scene.GetSolver()->GetEvolution();
|
|
const auto& SOA = Evolution->GetParticles();
|
|
const auto& InternalProxy = *SOA.GetAllParticlesView().Begin();
|
|
EXPECT_EQ(InternalProxy.GetX()[0], 5);
|
|
}
|
|
|
|
//make sure commands are also deferred
|
|
|
|
int Count = 0;
|
|
int ExternalCount = 0;
|
|
TUniqueFunction<void()> Lambda = [&]()
|
|
{
|
|
++Count;
|
|
EXPECT_EQ(Count, 1); //only hit once on internal thread
|
|
EXPECT_EQ(ExternalCount, Delay); //internal hits with expected delay
|
|
};
|
|
|
|
Scene.GetSolver()->EnqueueCommandImmediate(Lambda);
|
|
|
|
for (int Repeat = 0; Repeat < Delay + 1; ++Repeat)
|
|
{
|
|
//tick solver
|
|
FVec3 Grav(0,0,-1);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,1,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
++ExternalCount;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, RemoveDelayed)
|
|
{
|
|
for (int Delay = 0; Delay < 4; ++Delay)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->GetMarshallingManager().SetTickDelay_External(Delay);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
Params.bSimulatePhysics = true; //simulate so that sync body is triggered
|
|
Params.bStartAwake = true;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
Particle.SetV(FVec3(0, 0, -1));
|
|
}
|
|
|
|
|
|
//make second simulating Proxy that we don't delete. Needed to trigger a sync
|
|
//this is because some data is cleaned up on GT immediately
|
|
FPhysicsActorHandle Proxy2 = nullptr;
|
|
FChaosEngineInterface::CreateActor(Params, Proxy2);
|
|
auto& Particle2 = Proxy2->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy2, nullptr);
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle2.SetGeometry(Sphere);
|
|
Particle2.SetV(FVec3(0, -1, 0));
|
|
}
|
|
|
|
//create actor
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy, Proxy2 };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
//tick until it's being synced from sim
|
|
for (int Repeat = 0; Repeat < Delay; ++Repeat)
|
|
{
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
}
|
|
}
|
|
|
|
//x starts at 0
|
|
EXPECT_NEAR(Particle.X()[2], 0, 1e-4);
|
|
EXPECT_NEAR(Particle2.X()[1], 0, 1e-4);
|
|
|
|
//tick solver and see new position synced from sim
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
EXPECT_NEAR(Particle.X()[2], -1, 1e-4);
|
|
EXPECT_NEAR(Particle2.X()[1], -1, 1e-4);
|
|
}
|
|
|
|
//tick solver and delete in between solver finishing and sync
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
|
|
//delete Proxy
|
|
FChaosEngineInterface::ReleaseActor(Proxy, &Scene);
|
|
|
|
Scene.EndFrame();
|
|
EXPECT_NEAR(Particle2.X()[1], -2, 1e-4); //other Proxy keeps moving
|
|
}
|
|
|
|
|
|
//tick again and don't crash
|
|
for (int Repeat = 0; Repeat < Delay + 1; ++Repeat)
|
|
{
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
EXPECT_NEAR(Particle2.X()[1], -3 - Repeat, 1e-4); //other Proxy keeps moving
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, MoveDelayed)
|
|
{
|
|
for (int Delay = 0; Delay < 4; ++Delay)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->GetMarshallingManager().SetTickDelay_External(Delay);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
Params.bSimulatePhysics = true; //simulated so that gt conflicts with sim thread
|
|
Params.bStartAwake = true;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
EXPECT_NE(Proxy, nullptr);
|
|
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
Particle.SetV(FVec3(0, 0, -1));
|
|
}
|
|
|
|
//create actor
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
//tick until it's being synced from sim
|
|
for (int Repeat = 0; Repeat < Delay; ++Repeat)
|
|
{
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
}
|
|
}
|
|
|
|
//x starts at 0
|
|
EXPECT_NEAR(Particle.X()[2], 0, 1e-4);
|
|
|
|
//tick solver and see new position synced from sim
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
EXPECT_NEAR(Particle.X()[2], -1, 1e-4);
|
|
}
|
|
|
|
//set new x position and make sure we see it right away even though there's delay
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(FQuat::Identity, FVec3(0, 0, 10)));
|
|
|
|
for (int Repeat = 0; Repeat < Delay; ++Repeat)
|
|
{
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
EXPECT_NEAR(Particle.X()[2], 10, 1e-4); //until we catch up, just use GT data
|
|
}
|
|
}
|
|
|
|
//tick solver one last time, should see sim results from the place we teleported to
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
EXPECT_NEAR(Particle.X()[2], 9, 1e-4);
|
|
}
|
|
|
|
//set x after sim but before EndFrame, make sure to see gt position since it was written after
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(FQuat::Identity, FVec3(0, 0, 100)));
|
|
Scene.EndFrame();
|
|
EXPECT_NEAR(Particle.X()[2], 100, 1e-4);
|
|
}
|
|
|
|
for (int Repeat = 0; Repeat < Delay; ++Repeat)
|
|
{
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
EXPECT_NEAR(Particle.X()[2], 100, 1e-4); //until we catch up, just use GT data
|
|
}
|
|
}
|
|
|
|
//tick solver one last time, should see sim results from the place we teleported to
|
|
{
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
EXPECT_NEAR(Particle.X()[2], 99, 1e-4);
|
|
}
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, SimRoundTrip)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
Particle.SetObjectState(EObjectStateType::Dynamic);
|
|
Particle.AddForce(FVec3(0, 0, 10) * Particle.M());
|
|
|
|
FVec3 Grav(0,0,0);
|
|
Scene.SetUpForFrame(&Grav,1,0,99999,99999,10,false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
//integration happened and we get results back
|
|
EXPECT_EQ(Particle.X(), FVec3(0, 0, 10));
|
|
EXPECT_EQ(Particle.V(), FVec3(0, 0, 10));
|
|
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, SimInterpolated)
|
|
{
|
|
//Need to test:
|
|
//position interpolation
|
|
//position interpolation from an inactive Proxy (i.e a step function)
|
|
//position interpolation from an active to an inactive Proxy (i.e a step function but reversed)
|
|
//interpolation to a deleted Proxy
|
|
//state change should be a step function (sleep state)
|
|
//wake events must be collapsed (sleep awake sleep becomes sleep)
|
|
//collision events must be collapsed
|
|
//forces are averaged
|
|
const FReal FixedDT = 1;
|
|
FChaosScene Scene(nullptr, FixedDT);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
FPhysicsActorHandle Proxy2 = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
Params.bSimulatePhysics = true;
|
|
FChaosEngineInterface::CreateActor(Params, Proxy2);
|
|
auto& Particle2 = Proxy2->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle2.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy, Proxy2 };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
Particle.SetObjectState(EObjectStateType::Dynamic);
|
|
const FReal ZVel = 10;
|
|
const FReal ZStart = 100;
|
|
const FVec3 ConstantForce(0, 0, 1 * Particle2.M());
|
|
Particle.SetV(FVec3(0, 0, ZVel));
|
|
Particle.SetX(FVec3(0, 0, ZStart));
|
|
const int32 NumGTSteps = 24;
|
|
const int32 NumPTSteps = 24 / 4;
|
|
|
|
struct FCallback : public TSimCallbackObject<FSimCallbackNoInput>
|
|
{
|
|
virtual void OnPreSimulate_Internal() override
|
|
{
|
|
EXPECT_EQ(GetConsumerInput_Internal(), nullptr); //no inputs passed in
|
|
//we expect the dt to be 1
|
|
EXPECT_EQ(GetDeltaTime_Internal(), 1);
|
|
EXPECT_EQ(GetSimTime_Internal(), Count);
|
|
Count++;
|
|
}
|
|
|
|
int32 Count = 0;
|
|
|
|
int32 NumPTSteps;
|
|
};
|
|
|
|
auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External<FCallback>();
|
|
Callback->NumPTSteps = NumPTSteps;
|
|
FReal Time = 0;
|
|
const FReal GTDt = FixedDT * 0.25f;
|
|
for (int32 Step = 0; Step < NumGTSteps; Step++)
|
|
{
|
|
//set force every external frame
|
|
Particle2.AddForce(ConstantForce);
|
|
FVec3 Grav(0, 0, 0);
|
|
Scene.SetUpForFrame(&Grav, GTDt, 0, 99999, 99999, 1, false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
Time += GTDt;
|
|
const FReal InterpolatedTime = Time - FixedDT * Chaos::AsyncInterpolationMultiplier;
|
|
const FReal ExpectedVFromForce = Time;
|
|
if (InterpolatedTime < 0)
|
|
{
|
|
//not enough time to interpolate so just take initial value
|
|
EXPECT_NEAR(Particle.X()[2], ZStart, 1e-2);
|
|
EXPECT_NEAR(Particle2.V()[2], 0, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
//interpolated
|
|
EXPECT_NEAR(Particle.X()[2], ZStart + ZVel * InterpolatedTime, 1e-2);
|
|
EXPECT_NEAR(Particle2.V()[2], InterpolatedTime, 1e-2);
|
|
}
|
|
}
|
|
|
|
EXPECT_EQ(Callback->Count, NumPTSteps);
|
|
const FReal LastInterpolatedTime = NumGTSteps * GTDt - FixedDT * Chaos::AsyncInterpolationMultiplier;
|
|
EXPECT_NEAR(Particle.X()[2], ZStart + ZVel * LastInterpolatedTime, 1e-2);
|
|
EXPECT_NEAR(Particle.V()[2], ZVel, 1e-2);
|
|
}
|
|
|
|
void ExpectVectorEqual(const FVec3& V0, const FVec3& V1)
|
|
{
|
|
EXPECT_EQ(V0.X, V1.X);
|
|
EXPECT_EQ(V0.Y, V1.Y);
|
|
EXPECT_EQ(V0.Z, V1.Z);
|
|
}
|
|
|
|
void TestKinematicTarget(const bool bInUpdateKinematicFromSimulation)
|
|
{
|
|
// Need to test:
|
|
// GT particle position is immediately updated after calling SetKinematicTarget_AssumesLocked
|
|
// GT particle positions and velocities are correctly updated
|
|
// PT particle positions and velocities are correctly updated
|
|
// Velocity becomes zero if no KinematicTarget is set in the current frame
|
|
// Particle positions and velocities are correct after SetKinematicTarget_AssumesLocked, SetKinematicTarget_AssumesLocked
|
|
// Velocity is zero if only SetGlobalPose_AssumesLocked is called (Teleport)
|
|
// Particle positions and velocities are correct after SetGlobalPose_AssumesLocked, SetKinematicTarget_AssumesLocked (Teleport)
|
|
// Particle positions and velocities are correct after SetKinematicTarget_AssumesLocked, SetGlobalPose_AssumesLocked (Teleport, KinematicTarget is cleared)
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
Particle.SetObjectState(EObjectStateType::Kinematic);
|
|
Particle.SetUpdateKinematicFromSimulation(bInUpdateKinematicFromSimulation);
|
|
|
|
struct FDummyInput : FSimCallbackInput
|
|
{
|
|
FSingleParticlePhysicsProxy* Proxy;
|
|
FVec3 CorrectX;
|
|
FVec3 CorrectV;
|
|
bool bKinematicWritebackEnabled;
|
|
void Reset() {}
|
|
};
|
|
|
|
struct FCallback : public TSimCallbackObject<FDummyInput>
|
|
{
|
|
virtual void OnPreSimulate_Internal() override
|
|
{
|
|
const FVec3 ExpectedX = GetConsumerInput_Internal()->CorrectX;
|
|
const FVec3 ExpectedV = GetConsumerInput_Internal()->CorrectV;
|
|
|
|
auto Handle = GetConsumerInput_Internal()->Proxy->GetPhysicsThreadAPI();
|
|
ExpectVectorEqual(Handle->X(), ExpectedX);
|
|
ExpectVectorEqual(Handle->V(), ExpectedV);
|
|
}
|
|
};
|
|
|
|
auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External<FCallback>();
|
|
|
|
Callback->GetProducerInputData_External()->Proxy = Proxy;
|
|
Callback->GetProducerInputData_External()->bKinematicWritebackEnabled = bInUpdateKinematicFromSimulation;
|
|
|
|
FVec3 Grav(0, 0, 0);
|
|
float Dt = 1;
|
|
|
|
auto AdvanceFrameAndRunTest = [&](const FVec3 &CorrectX, const FVec3 &CorrectV)
|
|
{
|
|
Scene.SetUpForFrame(&Grav, Dt, 0, 99999, 99999, 10, false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
// Test X and V on GT
|
|
// NOTE: GT velocity will not be updated if kinematic writeback from the physics thread is disabled
|
|
ExpectVectorEqual(Particle.X(), CorrectX);
|
|
if (Callback->GetProducerInputData_External()->bKinematicWritebackEnabled)
|
|
{
|
|
ExpectVectorEqual(Particle.V(), CorrectV);
|
|
}
|
|
// Test X and V on PT, this is going to be used in OnPreSimulate_Internal in next frame.
|
|
Callback->GetProducerInputData_External()->CorrectX = CorrectX;
|
|
Callback->GetProducerInputData_External()->CorrectV = CorrectV;
|
|
};
|
|
|
|
// Set initial transform
|
|
FVec3 CurrentX = FVec3(1, 2, 3);
|
|
FVec3 CurrentV = FVec3(0, 0, 0);
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(CurrentX));
|
|
|
|
Callback->GetProducerInputData_External()->CorrectX = CurrentX;
|
|
Callback->GetProducerInputData_External()->CorrectV = CurrentV;
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
|
|
// Test SetKinematicTarget_AssumesLocked
|
|
CurrentX = FVec3(2, 3, 4);
|
|
CurrentV = FVec3(1, 1, 1);
|
|
FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(CurrentX));
|
|
|
|
// Test if position is immediately updated on GT after SetKinematicTarget_AssumesLocked (if we aren't reading data back from PT)
|
|
if (!bInUpdateKinematicFromSimulation)
|
|
{
|
|
ExpectVectorEqual(Particle.X(), CurrentX);
|
|
}
|
|
|
|
// This will fail when bInUpdateKinematicFromSimulation is false becasuse GT and PT disagree on velocity
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
|
|
// Test if velocity becomes zero when no kinematic target is set
|
|
CurrentX = FVec3(2, 3, 4);
|
|
CurrentV = FVec3(0, 0, 0);
|
|
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
|
|
// Test if particle positions and velocities are correct after SetKinematicTarget_AssumesLocked, SetKinematicTarget_AssumesLocked
|
|
CurrentX = FVec3(0, 0, 0);
|
|
CurrentV = FVec3(-2, -3, -4);
|
|
FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(FVec3(1, 2, 3)));
|
|
FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(CurrentX));
|
|
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
|
|
// Test if velocity is zero if only SetGlobalPose_AssumesLocked is called (Teleport)
|
|
CurrentX = FVec3(0, 0, 0);
|
|
CurrentV = FVec3(0, 0, 0);
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(CurrentX));
|
|
|
|
Callback->GetProducerInputData_External()->CorrectX = CurrentX;
|
|
Callback->GetProducerInputData_External()->CorrectV = CurrentV;
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
|
|
// Test if particle positions and velocities are correct after SetGlobalPose_AssumesLocked, SetKinematicTarget_AssumesLocked
|
|
CurrentX = FVec3(-1, -2, -3);
|
|
CurrentV = FVec3(0, 0, 0);
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(CurrentX));
|
|
FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(CurrentX));
|
|
|
|
Callback->GetProducerInputData_External()->CorrectX = CurrentX;
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
|
|
// Test if particle state to sleeping change after setting a kinematic target it's position and velocity should remain the same
|
|
FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(FVec3(1, 2, 3)));
|
|
Particle.SetObjectState(EObjectStateType::Sleeping);
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
|
|
// Test if particle positions and velocities are correct after SetKinematicTarget_AssumesLocked, SetGlobalPose_AssumesLocked
|
|
CurrentX = FVec3(3, 2, 1);
|
|
CurrentV = FVec3(0, 0, 0);
|
|
FChaosEngineInterface::SetKinematicTarget_AssumesLocked(Proxy, FTransform(CurrentX));
|
|
FChaosEngineInterface::SetGlobalPose_AssumesLocked(Proxy, FTransform(CurrentX));
|
|
|
|
Callback->GetProducerInputData_External()->CorrectX = CurrentX;
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
|
|
// Test if the PT positions and velocities are right from previous frame
|
|
CurrentX = FVec3(3, 2, 1);
|
|
CurrentV = FVec3(0, 0, 0);
|
|
AdvanceFrameAndRunTest(CurrentX, CurrentV);
|
|
}
|
|
|
|
// Test SetKinematicTarget when writeback from PT is enabled
|
|
GTEST_TEST(EngineInterface, SetKinematicTargetWriteBackEnabled)
|
|
{
|
|
TestKinematicTarget(true);
|
|
}
|
|
|
|
// Test SetKinematicTarget when writeback from PT is disabled
|
|
GTEST_TEST(EngineInterface, SetKinematicTargetWriteBackDisabled)
|
|
{
|
|
TestKinematicTarget(false);
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, PerPropertySetOnGT)
|
|
{
|
|
//Need to test:
|
|
//setting transform, velocities, wake state, on external thread means we overwrite results until sim catches up
|
|
//deleted proxy does not incorrectly update after it's deleted on gt
|
|
const FReal FixedDT = 1;
|
|
FChaosScene Scene(nullptr, FixedDT);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->EnableAsyncMode(1); //tick 1 dt at a time
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
Particle.SetObjectState(EObjectStateType::Dynamic);
|
|
const FReal ZVel = 10;
|
|
const FReal ZStart = 100;
|
|
Particle.SetV(FVec3(0, 0, ZVel));
|
|
Particle.SetX(FVec3(0, 0, ZStart));
|
|
const int32 NumGTSteps = 100;
|
|
const FVec3 TeleportLocation(5, 5, ZStart);
|
|
|
|
FReal Time = 0;
|
|
const FReal GTDt = FixedDT * 0.5f;
|
|
const int32 ChangeVelStep = 20;
|
|
const FReal ChangeVelTime = ChangeVelStep * GTDt;
|
|
const FReal YVelAfterChange = 10;
|
|
const int32 TeleportStep = 10;
|
|
const FReal TeleportTime = TeleportStep * GTDt;
|
|
bool bHasTeleportedOnGT = false;
|
|
bool bVelHasChanged = false;
|
|
bool bWasPutToSleep = false;
|
|
bool bWasWoken = false;
|
|
const int32 SleepStep = 50;
|
|
const int32 WakeStep = 70;
|
|
const FReal PutToSleepTime = SleepStep * GTDt;
|
|
const FReal WokenTime = WakeStep * GTDt;
|
|
FReal SleepZPosition(0);
|
|
|
|
for (int32 Step = 0; Step < NumGTSteps; Step++)
|
|
{
|
|
if (Step == TeleportStep)
|
|
{
|
|
Particle.SetX(TeleportLocation);
|
|
bHasTeleportedOnGT = true;
|
|
}
|
|
|
|
if(Step == ChangeVelStep)
|
|
{
|
|
Particle.SetV(FVec3(0, YVelAfterChange, ZVel));
|
|
bVelHasChanged = true;
|
|
}
|
|
|
|
if(Step == SleepStep)
|
|
{
|
|
bWasPutToSleep = true;
|
|
Particle.SetObjectState(EObjectStateType::Sleeping);
|
|
SleepZPosition = Particle.X()[2]; //record position when gt wants to sleep
|
|
}
|
|
|
|
if(Step == WakeStep)
|
|
{
|
|
bWasWoken = true;
|
|
Particle.SetV(FVec3(0, YVelAfterChange, ZVel));
|
|
Particle.SetObjectState(EObjectStateType::Dynamic);
|
|
}
|
|
|
|
FVec3 Grav(0, 0, 0);
|
|
Scene.SetUpForFrame(&Grav, GTDt, 0, 99999, 99999, 10, false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
Time += GTDt;
|
|
const FReal InterpolatedTime = Time - FixedDT * Chaos::AsyncInterpolationMultiplier;
|
|
if (InterpolatedTime < 0)
|
|
{
|
|
//not enough time to interpolate so just take initial value
|
|
EXPECT_NEAR(Particle.X()[2], ZStart, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
//interpolated
|
|
if(bHasTeleportedOnGT)
|
|
{
|
|
EXPECT_NEAR(Particle.X()[0], TeleportLocation[0], 1e-2); //X never changes so as soon as gt teleports we should see it
|
|
|
|
//if we haven't caught up to teleport, we just use the value set on GT for z value
|
|
if(InterpolatedTime < TeleportTime)
|
|
{
|
|
EXPECT_NEAR(Particle.X()[2], TeleportLocation[2], 1e-3);
|
|
}
|
|
else
|
|
{
|
|
if(!bWasPutToSleep)
|
|
{
|
|
//caught up so expect normal movement to marshal back
|
|
EXPECT_NEAR(Particle.X()[2], TeleportLocation[2] + ZVel * (InterpolatedTime - TeleportTime), 1e-2);
|
|
}
|
|
else if(InterpolatedTime < WokenTime)
|
|
{
|
|
//currently asleep so position is held constant
|
|
EXPECT_NEAR(Particle.X()[2], SleepZPosition, 1e-2);
|
|
if(!bWasWoken)
|
|
{
|
|
EXPECT_NEAR(Particle.V()[2], 0, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_NEAR(Particle.V()[2], ZVel, 1e-2);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//woke back up so position is moving again
|
|
EXPECT_NEAR(Particle.X()[2], SleepZPosition + ZVel * (InterpolatedTime - WokenTime), 1e-2);
|
|
EXPECT_NEAR(Particle.V()[2], ZVel, 1e-2);
|
|
}
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
EXPECT_NEAR(Particle.X()[2], ZStart + ZVel * InterpolatedTime, 1e-2);
|
|
}
|
|
|
|
if(bVelHasChanged)
|
|
{
|
|
if(!bWasPutToSleep || bWasWoken)
|
|
{
|
|
EXPECT_EQ(Particle.V()[1], YVelAfterChange);
|
|
}
|
|
else
|
|
{
|
|
//asleep so velocity is 0
|
|
EXPECT_EQ(Particle.V()[1], 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Particle.V()[1], 0);
|
|
}
|
|
|
|
if(bWasPutToSleep && !bWasWoken)
|
|
{
|
|
EXPECT_EQ(Particle.ObjectState(), EObjectStateType::Sleeping);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Particle.ObjectState(), EObjectStateType::Dynamic);
|
|
}
|
|
}
|
|
}
|
|
|
|
const FReal LastInterpolatedTime = NumGTSteps * GTDt - FixedDT * Chaos::AsyncInterpolationMultiplier;
|
|
EXPECT_EQ(Particle.V()[2], ZVel);
|
|
EXPECT_EQ(Particle.V()[1], YVelAfterChange);
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, IterationSetOnGT)
|
|
{
|
|
//Need to test:
|
|
//Before set iteration on game thread the iterations are default (8, 2, 1)
|
|
//After set iteration on game thread the data goes through.
|
|
FPBDPositionConstraints SinglePositionConstraint;
|
|
const FReal FixedDT = 1;
|
|
FChaosScene Scene(nullptr, FixedDT);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
FRigidBodyHandle_External& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
Particle.SetObjectState(EObjectStateType::Dynamic);
|
|
Particle.SetGravityEnabled(true);
|
|
|
|
FReal Time = 0;
|
|
const FReal GTDt = FixedDT * 4;
|
|
const int32 BigIteration = 100;
|
|
|
|
FVec3 Grav(0, 0, -1);
|
|
Scene.SetUpForFrame(&Grav, GTDt, 0, 99999, 99999, 10, false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
Chaos::Private::FIterationSettings GroupIterations = Scene.GetSolver()->GetEvolution()->GetIslandGroupManager().GetIslandGroupIterations(0);
|
|
|
|
EXPECT_EQ(GroupIterations.GetNumPositionIterations(), 8);
|
|
EXPECT_EQ(GroupIterations.GetNumVelocityIterations(), 2);
|
|
EXPECT_EQ(GroupIterations.GetNumProjectionIterations(), 1);
|
|
|
|
auto FirstHandle = Scene.GetSolver()->GetEvolution()->GetParticleHandles().Handle(0)->CastToRigidParticle();
|
|
TArray<FPBDRigidParticleHandle*> Dynamics = { FirstHandle };
|
|
TArray<FVec3> Positions = { FVec3(1) };
|
|
FPBDPositionConstraints PositionConstraints(MoveTemp(Positions), MoveTemp(Dynamics), 1.f);
|
|
SinglePositionConstraint = PositionConstraints;
|
|
Scene.GetSolver()->GetEvolution()->AddConstraintContainer(SinglePositionConstraint);
|
|
|
|
Particle.SetPositionSolverIterations(BigIteration);
|
|
Particle.SetVelocitySolverIterations(BigIteration);
|
|
Particle.SetProjectionSolverIterations(BigIteration);
|
|
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
Chaos::Private::FIterationSettings GroupIterationsTemp = Scene.GetSolver()->GetEvolution()->GetIslandGroupManager().GetIslandGroupIterations(0);
|
|
EXPECT_EQ(GroupIterationsTemp.GetNumPositionIterations(), 100);
|
|
EXPECT_EQ(GroupIterationsTemp.GetNumVelocityIterations(), 100);
|
|
EXPECT_EQ(GroupIterationsTemp.GetNumProjectionIterations(), 100);
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, FlushCommand)
|
|
{
|
|
//Need to test:
|
|
//flushing commands works and sees state changes for both fixed dt and not
|
|
//sim callback is not called
|
|
|
|
bool bHitOnShutDown = false;
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
Scene.GetSolver()->EnableAsyncMode(1); //tick 1 dt at a time
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
Particle.SetX(FVec3(0, 0, 3));
|
|
|
|
Scene.GetSolver()->EnqueueCommandImmediate([Proxy]()
|
|
{
|
|
//sees change immediately
|
|
EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->X()[2], 3);
|
|
});
|
|
|
|
struct FCallback : public TSimCallbackObject<>
|
|
{
|
|
virtual void OnPreSimulate_Internal() override
|
|
{
|
|
EXPECT_FALSE(true); //this should never hit
|
|
}
|
|
};
|
|
|
|
auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External<FCallback>();
|
|
|
|
FVec3 Grav(0, 0, 0);
|
|
Scene.SetUpForFrame(&Grav, 0, 0, 99999, 99999, 10, false); //flush with dt 0
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
Scene.GetSolver()->EnqueueCommandImmediate([&bHitOnShutDown]()
|
|
{
|
|
//command enqueued and then solver shuts down, so flush must happen
|
|
bHitOnShutDown = true;
|
|
});
|
|
}
|
|
|
|
EXPECT_TRUE(bHitOnShutDown);
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, SimSubstep)
|
|
{
|
|
//Need to test:
|
|
//forces and torques are extrapolated (i.e. held constant for sub-steps)
|
|
//kinematic targets are interpolated over the sub-step
|
|
//identical inputs are given to sub-steps
|
|
|
|
const FReal FixedDT = 1;
|
|
FChaosScene Scene(nullptr, FixedDT);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
Particle.SetObjectState(EObjectStateType::Dynamic);
|
|
Particle.SetGravityEnabled(true);
|
|
|
|
struct FDummyInput : FSimCallbackInput
|
|
{
|
|
int32 ExternalFrame;
|
|
void Reset() {}
|
|
};
|
|
|
|
struct FCallback : public TSimCallbackObject<FDummyInput>
|
|
{
|
|
virtual void OnPreSimulate_Internal() override
|
|
{
|
|
EXPECT_EQ(GetConsumerInput_Internal()->ExternalFrame, ExpectedFrame);
|
|
EXPECT_NEAR(GetSimTime_Internal(), InternalSteps * GetDeltaTime_Internal(), 1e-2); //sim start is changing per sub-step
|
|
++InternalSteps;
|
|
}
|
|
|
|
int32 ExpectedFrame;
|
|
int32 InternalSteps = 0;
|
|
};
|
|
|
|
auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External<FCallback>();
|
|
|
|
FReal Time = 0;
|
|
const FReal GTDt = FixedDT * 4;
|
|
for (int32 Step = 0; Step < 10; Step++)
|
|
{
|
|
Callback->ExpectedFrame = Step;
|
|
Callback->GetProducerInputData_External()->ExternalFrame = Step; //make sure input matches for all sub-steps
|
|
|
|
//set force every external frame
|
|
Particle.AddForce(FVec3(0, 0, 1 * Particle.M())); //should counteract gravity
|
|
FVec3 Grav(0, 0, -1);
|
|
Scene.SetUpForFrame(&Grav, GTDt, 0, 99999, 99999, 10, false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
|
|
Time += GTDt;
|
|
|
|
//should have no movement because forces cancel out
|
|
EXPECT_NEAR(Particle.X()[2], 0, 1e-2);
|
|
EXPECT_NEAR(Particle.V()[2], 0, 1e-2);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, SimDestroyedProxy)
|
|
{
|
|
//Need to test:
|
|
//destroyed proxy still valid in callback, but Proxy is nulled out
|
|
//valid for multiple sub-steps
|
|
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
Scene.GetSolver()->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
const FReal FixedDT = 1;
|
|
Scene.GetSolver()->EnableAsyncMode(FixedDT); //tick 1 dt at a time
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Proxy = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Proxy);
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
{
|
|
auto Sphere = MakeImplicitObjectPtr<Chaos::FSphere>(FVec3(0), 3);
|
|
Particle.SetGeometry(Sphere);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Proxys = { Proxy };
|
|
Scene.AddActorsToScene_AssumesLocked(Proxys);
|
|
|
|
struct FDummyInput : FSimCallbackInput
|
|
{
|
|
FSingleParticlePhysicsProxy* Proxy;
|
|
void Reset() {}
|
|
};
|
|
|
|
struct FCallback : public TSimCallbackObject<FDummyInput>
|
|
{
|
|
virtual void OnPreSimulate_Internal() override
|
|
{
|
|
EXPECT_EQ(GetConsumerInput_Internal()->Proxy->GetHandle_LowLevel(), nullptr);
|
|
}
|
|
};
|
|
|
|
auto Callback = Scene.GetSolver()->CreateAndRegisterSimCallbackObject_External<FCallback>();
|
|
|
|
Callback->GetProducerInputData_External()->Proxy = Proxy;
|
|
Scene.GetSolver()->UnregisterObject(Proxy);
|
|
|
|
FVec3 Grav(0, 0, -1);
|
|
Scene.SetUpForFrame(&Grav, FixedDT * 3, 0, 99999, 99999, 10, false);
|
|
Scene.StartFrame();
|
|
Scene.EndFrame();
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, OverlapOffsetActor)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
Params.bSimulatePhysics = false;
|
|
Params.bStatic = true;
|
|
Params.InitialTM = FTransform::Identity;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle StaticCube = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, StaticCube);
|
|
ASSERT_NE(StaticCube, nullptr);
|
|
|
|
// Add geometry, placing a box at the origin
|
|
constexpr FReal BoxSize = static_cast<FReal>(50.0);
|
|
const FVec3 HalfBoxExtent{ BoxSize };
|
|
|
|
// We require a union here, although the second geometry isn't used we need the particle to
|
|
// have more than one shape in its shapes array otherwise the query acceleration will treat
|
|
// it as a special case and skip bounds checking during the overlap
|
|
TArray<Chaos::FImplicitObjectPtr> Geoms;
|
|
Geoms.Emplace(MakeImplicitObjectPtr<TBox<FReal, 3>>(-HalfBoxExtent, HalfBoxExtent));
|
|
Geoms.Emplace(MakeImplicitObjectPtr<TBox<FReal, 3>>(-HalfBoxExtent, HalfBoxExtent));
|
|
|
|
auto& Particle = StaticCube->GetGameThreadAPI();
|
|
{
|
|
Chaos::FImplicitObjectPtr GeomUnion = MakeImplicitObjectPtr<FImplicitObjectUnion>(MoveTemp(Geoms));
|
|
Particle.SetGeometry(GeomUnion);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Particles{ StaticCube };
|
|
Scene.AddActorsToScene_AssumesLocked(Particles);
|
|
|
|
FChaosSQAccelerator SQ{ *Scene.GetSpacialAcceleration() };
|
|
FSQHitBuffer<ChaosInterface::FOverlapHit> HitBuffer;
|
|
FOverlapAllQueryCallback QueryCallback;
|
|
|
|
// Here we query from a position under the box, but using a shape that has an offset. This tests
|
|
// a failure case that was previously present where the query system assumed that the QueryTM
|
|
// was inside the geometry being used to query.
|
|
const FTransform QueryTM{ FVec3{0.0f, 0.0f, -110.0f} };
|
|
constexpr FReal SphereRadius = static_cast<FReal>(50.0);
|
|
SQ.Overlap(Chaos::FSphere(FVec3(0.0f, 0.0f, 100.0f), SphereRadius), QueryTM, HitBuffer, FChaosQueryFilterData(), QueryCallback, FQueryDebugParams());
|
|
|
|
EXPECT_TRUE(HitBuffer.HasBlockingHit());
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, SweepOffsetActor)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
Params.bSimulatePhysics = false;
|
|
Params.bStatic = true;
|
|
Params.InitialTM = FTransform::Identity;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle StaticCube = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, StaticCube);
|
|
ASSERT_NE(StaticCube, nullptr);
|
|
|
|
// Add geometry, placing a box at the origin
|
|
constexpr FReal BoxSize = static_cast<FReal>(50.0);
|
|
const FVec3 HalfBoxExtent{ BoxSize };
|
|
|
|
// We require a union here, although the second geometry isn't used we need the particle to
|
|
// have more than one shape in its shapes array otherwise the query acceleration will treat
|
|
// it as a special case and skip bounds checking during the overlap
|
|
TArray<Chaos::FImplicitObjectPtr> Geoms;
|
|
Geoms.Emplace(MakeImplicitObjectPtr<TBox<FReal, 3>>(-HalfBoxExtent, HalfBoxExtent));
|
|
Geoms.Emplace(MakeImplicitObjectPtr<TBox<FReal, 3>>(-HalfBoxExtent, HalfBoxExtent));
|
|
|
|
auto& Particle = StaticCube->GetGameThreadAPI();
|
|
{
|
|
Chaos::FImplicitObjectPtr GeomUnion = MakeImplicitObjectPtr<FImplicitObjectUnion>(MoveTemp(Geoms));
|
|
Particle.SetGeometry(GeomUnion);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Particles{ StaticCube };
|
|
Scene.AddActorsToScene_AssumesLocked(Particles);
|
|
|
|
FChaosSQAccelerator SQ{ *Scene.GetSpacialAcceleration() };
|
|
FSQHitBuffer<ChaosInterface::FSweepHit> HitBuffer;
|
|
FBlockAllQueryCallback QueryCallback;
|
|
|
|
// Another box of same size that is offset from origin by 200.
|
|
FVec3 Offset(200.f,0,0);
|
|
TBox<FReal, 3> QueryBox(-HalfBoxExtent + Offset, HalfBoxExtent + Offset);
|
|
|
|
// Sweep positions offset box directly above box at origin, should hit box sweeping downward.
|
|
const FTransform QueryTM{ FVec3{-200.f, 0, 100.0f} };
|
|
const FVec3 Dir(0,0,-1);
|
|
const FReal Length = 200;
|
|
SQ.Sweep(QueryBox, QueryTM, Dir, Length, HitBuffer, EHitFlags::None, FQueryFilterData(), QueryCallback, FQueryDebugParams());
|
|
|
|
EXPECT_TRUE(HitBuffer.HasBlockingHit());
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, BodyWithTwoShapes_SweepWithInitialOverlap_MinimalTOIIsReturned)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
Params.bSimulatePhysics = false;
|
|
Params.bStatic = true;
|
|
Params.InitialTM = FTransform::Identity;
|
|
Params.Scene = &Scene;
|
|
|
|
FPhysicsActorHandle Actor0 = nullptr;
|
|
|
|
FChaosEngineInterface::CreateActor(Params, Actor0);
|
|
ASSERT_NE(Actor0, nullptr);
|
|
|
|
TArray<Chaos::FImplicitObjectPtr> Geoms;
|
|
Geoms.Emplace(MakeImplicitObjectPtr<TBox<FReal, 3>>(FVec3(-50, -50, -50), FVec3(0, 50, 50)));
|
|
Geoms.Emplace(MakeImplicitObjectPtr<TBox<FReal, 3>>(FVec3(0, -50, -50), FVec3(50, 50, 50)));
|
|
|
|
FRigidBodyHandle_External& Particle = Actor0->GetGameThreadAPI();
|
|
{
|
|
Chaos::FImplicitObjectPtr GeomUnion = MakeImplicitObjectPtr<FImplicitObjectUnion>(MoveTemp(Geoms));
|
|
Particle.SetGeometry(GeomUnion);
|
|
}
|
|
|
|
TArray<FPhysicsActorHandle> Particles{ Actor0 };
|
|
Scene.AddActorsToScene_AssumesLocked(Particles);
|
|
|
|
FChaosSQAccelerator SQ{ *Scene.GetSpacialAcceleration() };
|
|
FSQHitBuffer<ChaosInterface::FSweepHit> HitBuffer;
|
|
FBlockAllQueryCallback QueryCallback;
|
|
|
|
const FReal SphereRadius = 25;
|
|
const Chaos::FSphere QuerySphere(FVec3::ZeroVector, SphereRadius);
|
|
|
|
// Sweep down over both shapes with an initial overlap, however center over shape 0 more.
|
|
FTransform QueryTM{ FVec3{-5, 0, 50} };
|
|
const FVec3 Dir(0,0,-1);
|
|
const FReal Length = 200;
|
|
SQ.Sweep(QuerySphere, QueryTM, Dir, Length, HitBuffer, EHitFlags::MTD, FQueryFilterData(), QueryCallback, FQueryDebugParams());
|
|
|
|
EXPECT_TRUE(HitBuffer.HasBlockingHit());
|
|
const FSweepHit* BlockingHit = HitBuffer.GetBlock();
|
|
EXPECT_EQ(BlockingHit->Shape, Particle.ShapesArray()[0].Get());
|
|
EXPECT_EQ(BlockingHit->Distance, -25);
|
|
EXPECT_VECTOR_NEAR(BlockingHit->WorldPosition, FVector(-5, 0, 50), KINDA_SMALL_NUMBER);
|
|
EXPECT_VECTOR_NEAR(BlockingHit->WorldNormal, FVector(0, 0, 1), KINDA_SMALL_NUMBER);
|
|
|
|
// Now do a second sweep down, this time over shape 1 more.
|
|
QueryTM = FTransform{ FVec3{5, 0, 50} };
|
|
SQ.Sweep(QuerySphere, QueryTM, Dir, Length, HitBuffer, EHitFlags::MTD, FQueryFilterData(), QueryCallback, FQueryDebugParams());
|
|
|
|
EXPECT_TRUE(HitBuffer.HasBlockingHit());
|
|
BlockingHit = HitBuffer.GetBlock();
|
|
EXPECT_EQ(BlockingHit->Shape, Particle.ShapesArray()[1].Get());
|
|
EXPECT_EQ(BlockingHit->Distance, -25);
|
|
EXPECT_VECTOR_NEAR(BlockingHit->WorldPosition, FVector(5, 0, 50), KINDA_SMALL_NUMBER);
|
|
EXPECT_VECTOR_NEAR(BlockingHit->WorldNormal, FVector(0, 0, 1), KINDA_SMALL_NUMBER);
|
|
}
|
|
|
|
GTEST_TEST(EngineInterface, TwoBodies_SweepWithInitialOverlap_MinimalTOIIsReturned)
|
|
{
|
|
FChaosScene Scene(nullptr, /*AsyncDt=*/-1);
|
|
|
|
FActorCreationParams Params;
|
|
Params.Scene = &Scene;
|
|
Params.bSimulatePhysics = false;
|
|
Params.bStatic = true;
|
|
Params.InitialTM = FTransform::Identity;
|
|
Params.Scene = &Scene;
|
|
|
|
auto CreateActor = [](const FActorCreationParams& Params, const FImplicitObjectPtr& Geometry) -> FPhysicsActorHandle
|
|
{
|
|
FPhysicsActorHandle ActorHandle = nullptr;
|
|
FChaosEngineInterface::CreateActor(Params, ActorHandle);
|
|
FRigidBodyHandle_External& Particle = ActorHandle->GetGameThreadAPI();
|
|
Particle.SetGeometry(Geometry);
|
|
return ActorHandle;
|
|
};
|
|
|
|
FImplicitObjectPtr Box0 = MakeImplicitObjectPtr<TBox<FReal, 3>>(FVec3(-50, -50, -50), FVec3(0, 50, 50));
|
|
FPhysicsActorHandle Actor0 = CreateActor(Params, Box0);
|
|
ASSERT_NE(Actor0, nullptr);
|
|
FSingleParticlePhysicsProxy* Actor0Proxy = Actor0->GetGameThreadAPI().GetProxy();
|
|
|
|
FImplicitObjectPtr Box1 = MakeImplicitObjectPtr<TBox<FReal, 3>>(FVec3(0, -50, -50), FVec3(50, 50, 50));
|
|
FPhysicsActorHandle Actor1 = CreateActor(Params, Box1);
|
|
ASSERT_NE(Actor1, nullptr);
|
|
FSingleParticlePhysicsProxy* Actor1Proxy = Actor1->GetGameThreadAPI().GetProxy();
|
|
|
|
TArray<FPhysicsActorHandle> Particles{ Actor0, Actor1 };
|
|
Scene.AddActorsToScene_AssumesLocked(Particles);
|
|
|
|
FChaosSQAccelerator SQ{ *Scene.GetSpacialAcceleration() };
|
|
FSQHitBuffer<ChaosInterface::FSweepHit> HitBuffer;
|
|
FBlockAllQueryCallback QueryCallback;
|
|
|
|
const FReal SphereRadius = 25;
|
|
const Chaos::FSphere QuerySphere(FVec3::ZeroVector, SphereRadius);
|
|
|
|
// Sweep down over both shapes with an initial overlap, however center over shape 0 more.
|
|
FTransform QueryTM{ FVec3{-5, 0, 50} };
|
|
const FVec3 Dir(0, 0, -1);
|
|
const FReal Length = 200;
|
|
SQ.Sweep(QuerySphere, QueryTM, Dir, Length, HitBuffer, EHitFlags::MTD, FQueryFilterData(), QueryCallback, FQueryDebugParams());
|
|
|
|
EXPECT_TRUE(HitBuffer.HasBlockingHit());
|
|
const FSweepHit* BlockingHit = HitBuffer.GetBlock();
|
|
EXPECT_EQ(BlockingHit->Actor->GetProxy(), Actor0Proxy);
|
|
EXPECT_EQ(BlockingHit->Distance, -25);
|
|
EXPECT_VECTOR_NEAR(BlockingHit->WorldPosition, FVector(-5, 0, 50), KINDA_SMALL_NUMBER);
|
|
EXPECT_VECTOR_NEAR(BlockingHit->WorldNormal, FVector(0, 0, 1), KINDA_SMALL_NUMBER);
|
|
|
|
// Now do a second sweep down, this time over shape 1 more.
|
|
QueryTM = FTransform{ FVec3{5, 0, 50} };
|
|
SQ.Sweep(QuerySphere, QueryTM, Dir, Length, HitBuffer, EHitFlags::MTD, FQueryFilterData(), QueryCallback, FQueryDebugParams());
|
|
|
|
EXPECT_TRUE(HitBuffer.HasBlockingHit());
|
|
BlockingHit = HitBuffer.GetBlock();
|
|
EXPECT_EQ(BlockingHit->Actor->GetProxy(), Actor1Proxy);
|
|
EXPECT_EQ(BlockingHit->Distance, -25);
|
|
EXPECT_VECTOR_NEAR(BlockingHit->WorldPosition, FVector(5, 0, 50), KINDA_SMALL_NUMBER);
|
|
EXPECT_VECTOR_NEAR(BlockingHit->WorldNormal, FVector(0, 0, 1), KINDA_SMALL_NUMBER);
|
|
}
|
|
|
|
// Disable a moving kinematic particle and switch it to dynamic while disabled. Verify that when
|
|
// disabled it is not in any active lists, and that when re-enabled it is not duplicated
|
|
//
|
|
// There was a (benign) bug where particles were not removed from the MovingKinematics list until the
|
|
// next call to ApplyKinematicTargets, which means they can be included in collision detection.
|
|
//
|
|
GTEST_TEST(EvolutionTests, TestDisableKinematicEnableDynamic)
|
|
{
|
|
const FReal Dt = FReal(1.0 / 60.0);
|
|
FParticleUniqueIndicesMultithreaded UniqueIndices;
|
|
FPBDRigidsSOAs Particles(UniqueIndices);
|
|
THandleArray<FChaosPhysicsMaterial> PhysicalMaterials;
|
|
FPBDRigidsEvolutionGBF Evolution(Particles, PhysicalMaterials);
|
|
|
|
Evolution.GetGravityForces().SetAcceleration(FVec3(0), 0);
|
|
|
|
// Create a moving kinematic particle
|
|
TArray<FPBDRigidParticleHandle*> ParticleHandles = Evolution.CreateDynamicParticles(1);
|
|
Evolution.EnableParticle(ParticleHandles[0]);
|
|
Evolution.SetParticleObjectState(ParticleHandles[0], EObjectStateType::Kinematic);
|
|
Evolution.SetParticleKinematicTarget(ParticleHandles[0], FKinematicTarget::MakePositionTarget(FVec3(10,0,0), FRotation3::FromIdentity()));
|
|
|
|
Evolution.AdvanceOneTimeStep(Dt);
|
|
|
|
EXPECT_FALSE(ParticleHandles[0]->IsDynamic());
|
|
EXPECT_TRUE(ParticleHandles[0]->IsKinematic());
|
|
|
|
// Check that it is in the moving kinematics list
|
|
{
|
|
const TParticleView<TPBDRigidParticles<FReal, 3>>& DynamicSleepingView = Particles.GetNonDisabledDynamicView();
|
|
const TParticleView<TPBDRigidParticles<FReal, 3>>& DynamicMovingKinematicView = Particles.GetActiveDynamicMovingKinematicParticlesView();
|
|
EXPECT_EQ(DynamicSleepingView.Num(), 0);
|
|
EXPECT_EQ(DynamicMovingKinematicView.Num(), 1);
|
|
}
|
|
|
|
// Disable the kinematic
|
|
Evolution.DisableParticle(ParticleHandles[0]);
|
|
|
|
// It should not be in any active views now
|
|
{
|
|
const TParticleView<TPBDRigidParticles<FReal, 3>>& DynamicSleepingView = Particles.GetNonDisabledDynamicView();
|
|
const TParticleView<TPBDRigidParticles<FReal, 3>>& DynamicMovingKinematicView = Particles.GetActiveDynamicMovingKinematicParticlesView();
|
|
EXPECT_EQ(DynamicSleepingView.Num(), 0);
|
|
EXPECT_EQ(DynamicMovingKinematicView.Num(), 0);
|
|
}
|
|
|
|
// Make the particle dynamic and enable it
|
|
Evolution.SetParticleObjectState(ParticleHandles[0], EObjectStateType::Dynamic);
|
|
Evolution.EnableParticle(ParticleHandles[0]);
|
|
|
|
// Check that it is in the active views, but not duplicated in either
|
|
{
|
|
const TParticleView<TPBDRigidParticles<FReal, 3>>& DynamicSleepingView = Particles.GetNonDisabledDynamicView();
|
|
const TParticleView<TPBDRigidParticles<FReal, 3>>& DynamicMovingKinematicView = Particles.GetActiveDynamicMovingKinematicParticlesView();
|
|
EXPECT_EQ(DynamicSleepingView.Num(), 1);
|
|
EXPECT_EQ(DynamicMovingKinematicView.Num(), 1);
|
|
}
|
|
}
|
|
|
|
// Check that we cannot set a kinematic target on a dynamic particle.
|
|
//
|
|
// This would cause the dynamic particle to be added to the MovingKinematics
|
|
// list which means it would appear twice in the GetActiveDynamicMovingKinematicParticlesView
|
|
// which can result in a race condition in collision detection as a particle pair will be
|
|
// considered twice and possibly on different threads.
|
|
GTEST_TEST(EvolutionTests, TestKinematicTargetOnDynamic)
|
|
{
|
|
const FReal Dt = FReal(1.0 / 60.0);
|
|
FParticleUniqueIndicesMultithreaded UniqueIndices;
|
|
FPBDRigidsSOAs Particles(UniqueIndices);
|
|
THandleArray<FChaosPhysicsMaterial> PhysicalMaterials;
|
|
FPBDRigidsEvolutionGBF Evolution(Particles, PhysicalMaterials);
|
|
|
|
Evolution.GetGravityForces().SetAcceleration(FVec3(0), 0);
|
|
|
|
// Create a dynamic particle
|
|
TArray<FPBDRigidParticleHandle*> ParticleHandles = Evolution.CreateDynamicParticles(1);
|
|
Evolution.EnableParticle(ParticleHandles[0]);
|
|
|
|
// Set the kinematic target
|
|
Evolution.SetParticleKinematicTarget(ParticleHandles[0], FKinematicTarget::MakePositionTarget(FVec3(10, 0, 0), FRotation3::FromIdentity()));
|
|
|
|
// We should not have a kinematic target
|
|
EXPECT_FALSE(ParticleHandles[0]->KinematicTarget().IsSet());
|
|
|
|
Evolution.AdvanceOneTimeStep(Dt);
|
|
|
|
// Check that it is in the active views, but not duplicated in either
|
|
{
|
|
const TParticleView<TPBDRigidParticles<FReal, 3>>& DynamicSleepingView = Particles.GetNonDisabledDynamicView();
|
|
const TParticleView<TPBDRigidParticles<FReal, 3>>& DynamicMovingKinematicView = Particles.GetActiveDynamicMovingKinematicParticlesView();
|
|
EXPECT_EQ(DynamicSleepingView.Num(), 1);
|
|
EXPECT_EQ(DynamicMovingKinematicView.Num(), 1);
|
|
}
|
|
}
|
|
}
|