4406 lines
136 KiB
C++
4406 lines
136 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 "RewindData.h"
|
|
#include "GeometryCollection/GeometryCollectionTestFramework.h"
|
|
#include "Chaos/SimCallbackObject.h"
|
|
#include "Chaos/Framework/ChaosResultsManager.h"
|
|
|
|
#ifndef REWIND_DESYNC
|
|
#define REWIND_DESYNC 0
|
|
#endif
|
|
|
|
namespace ChaosTest {
|
|
|
|
using namespace Chaos;
|
|
using namespace GeometryCollectionTest;
|
|
|
|
template <typename TSolver>
|
|
void TickSolverHelper(TSolver* Solver, FReal Dt = 1.0)
|
|
{
|
|
Solver->AdvanceAndDispatch_External(Dt);
|
|
Solver->UpdateGameThreadStructures();
|
|
}
|
|
|
|
auto* CreateSolverHelper(int32 StepMode, int32 RewindHistorySize, int32 Optimization, FReal& OutSimDt)
|
|
{
|
|
constexpr FReal FixedDt = 1;
|
|
constexpr FReal DtSizes[] = { FixedDt, FixedDt, FixedDt * 0.25, FixedDt * 4 }; //test fixed dt, sub-stepping, step collapsing
|
|
|
|
// Make a solver
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
auto* Solver = Module->CreateSolver(nullptr,/*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
/** int32 Optimization starts at 0 and iterate up to 2
|
|
* 0 = No optimizations
|
|
* 1 = CollisionResimCache
|
|
* 2 = Optimized RewindData */
|
|
|
|
Solver->EnableRewindCapture(RewindHistorySize, Optimization == 1); // Use CollisionResimCache optimization as optimization 1
|
|
if (Chaos::FRewindData* RewindData = Solver->GetRewindData())
|
|
{
|
|
RewindData->SetRewindDataOptimization(Optimization == 2); // Use optimized RewindData in as optimization 2
|
|
}
|
|
Solver->SetThreadingMode_External(EThreadingModeTemp::SingleThread);
|
|
OutSimDt = DtSizes[StepMode];
|
|
if (StepMode > 0)
|
|
{
|
|
Solver->EnableAsyncMode(DtSizes[StepMode]);
|
|
}
|
|
|
|
return Solver;
|
|
}
|
|
|
|
struct TRewindHelper
|
|
{
|
|
template <typename TLambda>
|
|
static void TestEmpty(const TLambda& Lambda, int32 RewindHistorySize = 200)
|
|
{
|
|
for (int Optimization = 0; Optimization < 3; ++Optimization)
|
|
{
|
|
for (int DtMode = 0; DtMode < 4; ++DtMode)
|
|
{
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
FReal SimDt;
|
|
auto* Solver = CreateSolverHelper(DtMode, RewindHistorySize, Optimization, SimDt);
|
|
Solver->SetMaxDeltaTime_External(SimDt); //make sure it can step even for huge steps
|
|
|
|
Lambda(Solver, SimDt, Optimization);
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename TLambda>
|
|
static void TestDynamicSphere(const TLambda& Lambda, int32 RewindHistorySize = 200)
|
|
{
|
|
TestEmpty([&Lambda, RewindHistorySize](auto* Solver, FReal SimDt, int32 Optimization)
|
|
{
|
|
Chaos::FImplicitObjectPtr Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
|
|
// Make particles
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
|
|
Particle.SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
|
|
Lambda(Solver, SimDt, Optimization, Proxy, Sphere.GetReference());
|
|
|
|
}, RewindHistorySize);
|
|
}
|
|
};
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_MovingGeomChange)
|
|
{
|
|
TRewindHelper::TestEmpty([](auto* Solver, FReal SimDt, int32 Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(0), FVec3(1)));
|
|
auto Box2 = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(2), FVec3(3)));
|
|
|
|
// Make particles
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
|
|
Particle.SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
const int32 LastGameStep = 20;
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
//property that changes every step
|
|
Particle.SetX(FVec3(0, 0, 100 - Step));
|
|
|
|
//property that changes once half way through
|
|
if (Step == 3)
|
|
{
|
|
Particle.SetGeometry(Box);
|
|
}
|
|
|
|
if (Step == 5)
|
|
{
|
|
Particle.SetGeometry(Box2);
|
|
}
|
|
|
|
if (Step == 7)
|
|
{
|
|
Particle.SetGeometry(Box);
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
//ended up at z = 100 - LastGameStep
|
|
EXPECT_EQ(Particle.X()[2], 100 - LastGameStep);
|
|
|
|
//ended up with box geometry
|
|
EXPECT_EQ(Box.GetReference(), Particle.GetGeometry());
|
|
|
|
const FRewindData* RewindData = Solver->GetRewindData();
|
|
|
|
//check state at every step except latest
|
|
const int32 LastSimStep = LastGameStep / SimDt;
|
|
for (int SimStep = 0; SimStep < LastSimStep - 1; ++SimStep)
|
|
{
|
|
const FReal TimeStart = SimStep * SimDt;
|
|
const FReal TimeEnd = (SimStep + 1) * SimDt;
|
|
const FReal LastInputTime = SimDt <= 1 ? TimeStart : TimeEnd - 1; //latest gt time associated with this interval
|
|
|
|
const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), SimStep);
|
|
EXPECT_EQ(ParticleState.GetX()[2], 100 - FMath::FloorToInt(LastInputTime)); //We teleported on GT so no interpolation
|
|
|
|
if (LastInputTime < 3)
|
|
{
|
|
//was sphere
|
|
EXPECT_EQ(ParticleState.GetGeometry(), Sphere.GetReference());
|
|
}
|
|
else if (LastInputTime < 5 || LastInputTime >= 7)
|
|
{
|
|
//then became box
|
|
EXPECT_EQ(ParticleState.GetGeometry(), Box.GetReference());
|
|
}
|
|
else
|
|
{
|
|
//second box
|
|
EXPECT_EQ(ParticleState.GetGeometry(), Box2.GetReference());
|
|
}
|
|
}
|
|
|
|
Solver->UnregisterObject(Proxy);
|
|
});
|
|
}
|
|
|
|
struct FRewindCallbackTestHelper : public IRewindCallback
|
|
{
|
|
FRewindCallbackTestHelper(const int32 InStepToRewindOn, const int32 InRewindToStep = 0)
|
|
: StepToRewindOn(InStepToRewindOn)
|
|
, RewindToStep(InRewindToStep)
|
|
{
|
|
}
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 PhysStep) override
|
|
{
|
|
return bRewound ? INDEX_NONE : TriggerRewindFunc(PhysStep);
|
|
}
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
ProcessInputsFunc(PhysicsStep, bRewound);
|
|
}
|
|
|
|
virtual void PreResimStep_Internal(int32 Step, bool bFirst) override
|
|
{
|
|
bRewound = true;
|
|
}
|
|
|
|
virtual void PostResimStep_Internal(int32 Step) override
|
|
{
|
|
PostFunc(Step);
|
|
bRewound = false;
|
|
}
|
|
|
|
int32 StepToRewindOn;
|
|
int32 RewindToStep;
|
|
bool bRewound = false;
|
|
TFunction<void(int32, bool)> ProcessInputsFunc = [](int32, bool) {};
|
|
TFunction<void(int32)> PostFunc = [](int32) {};
|
|
TFunction<int32(int32)> TriggerRewindFunc = [this](int32 PhysicsStep) -> int32
|
|
{
|
|
return PhysicsStep == StepToRewindOn ? RewindToStep : INDEX_NONE;
|
|
};
|
|
};
|
|
|
|
template <typename TSolver>
|
|
FRewindCallbackTestHelper* RegisterCallbackHelper(TSolver* Solver, const int32 NumStepsBeforeRewind = 0, const int32 RewindTo = 0)
|
|
{
|
|
auto Callback = MakeUnique<FRewindCallbackTestHelper>(NumStepsBeforeRewind - 1, RewindTo);
|
|
FRewindCallbackTestHelper* Result = Callback.Get();
|
|
Solver->SetRewindCallback(MoveTemp(Callback));
|
|
return Result;
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_AddImpulseFromGT)
|
|
{
|
|
//We expect anything that came from GT to automatically be reapplied during rewind
|
|
//This is for things that come outside of the net prediction system, like a teleport
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
const int32 LastGameStep = 20;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt));
|
|
Helper->PostFunc = [Proxy, SimDt](int32 Step)
|
|
{
|
|
const int32 GameStep = SimDt <= 1 ? Step * SimDt : (Step + 1) * SimDt;
|
|
if (GameStep < 5)
|
|
{
|
|
EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->V()[2], 0);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->V()[2], 100);
|
|
}
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
if (Step == 5)
|
|
{
|
|
Particle.SetLinearImpulse(FVec3(0, 0, 100), /*bIsVelocity=*/false);
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_DeleteFromGT)
|
|
{
|
|
//GT writes of a deleted particle should be ignored during a resim (not crash)
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
const int32 LastGameStep = 20;
|
|
RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt));
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
if (Step == 5)
|
|
{
|
|
Particle.SetLinearImpulse(FVec3(0, 0, 100), /*bIsVelocity=*/false);
|
|
}
|
|
|
|
if(Step == 15)
|
|
{
|
|
Solver->UnregisterObject(Proxy);
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimBeforeCreation)
|
|
{
|
|
//GT creates object half way through sim - we want to make sure resim properly ignores this
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
const int32 LastGameStep = 20;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt));
|
|
Helper->PostFunc = [Proxy, SimDt](int32 Step)
|
|
{
|
|
//simple movement without hitting object until we hit floor, should not ever hit sphere
|
|
const FReal NoHitZ = 100 - (Step + 1) * SimDt * 10;
|
|
if (NoHitZ > 40)
|
|
{
|
|
EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->X()[2], NoHitZ);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_GE(Proxy->GetPhysicsThreadAPI()->X()[2], 35); //floor should stop us (gave 5 units of error for solver)
|
|
}
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
ChaosTest::SetParticleSimDataToCollide({ Proxy->GetParticle_LowLevel() });
|
|
|
|
|
|
Particle.SetX(FVec3(0, 0, 100));
|
|
Particle.SetV(FVec3(0, 0, -10));
|
|
|
|
auto SphereGeom = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
FSingleParticlePhysicsProxy* SecondSphere = nullptr;
|
|
|
|
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, 0), FVec3(100, 100, 30)));
|
|
FSingleParticlePhysicsProxy* Floor = nullptr;
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
if(Step == 5)
|
|
{
|
|
// Make blocking floor
|
|
Floor = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
|
|
auto& FloorParticle = Floor->GetGameThreadAPI();
|
|
|
|
FloorParticle.SetGeometry(FloorGeom);
|
|
Solver->RegisterObject(Floor);
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ Floor->GetParticle_LowLevel() });
|
|
}
|
|
if (Step == 15)
|
|
{
|
|
// Make particles
|
|
SecondSphere = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& SecondSphereParticle = SecondSphere->GetGameThreadAPI();
|
|
|
|
SecondSphereParticle.SetGravityEnabled(false);
|
|
SecondSphereParticle.SetX(FVec3(0, 0, 80)); //if resim doesn't disable, this will be in the Sphere's way even though it didn't exist yet
|
|
SecondSphereParticle.SetGeometry(SphereGeom);
|
|
Solver->RegisterObject(SecondSphere);
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ SecondSphere->GetParticle_LowLevel() });
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
Solver->UnregisterObject(SecondSphere);
|
|
Solver->UnregisterObject(Floor);
|
|
});
|
|
}
|
|
|
|
// @todo(chaos): Rewind does not support SimData changes yet
|
|
// (note this test used to pass, but there was a bug in UpdateShapesArrayFromGeometry that would leave SimData
|
|
// as all-zero if you don't have a Union at the root. That bug is fixed which exposes this test failure)
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_ResimShapeFilter)
|
|
{
|
|
//GT modifies filter after object passes through, want to make sure resim restores this state correctly
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
const int32 LastGameStep = 20;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt));
|
|
Helper->PostFunc = [Proxy, SimDt](int32 Step)
|
|
{
|
|
//simple movement without hitting object
|
|
const FReal NoHitZ = 100 - (Step + 1) * SimDt * 10;
|
|
EXPECT_EQ(Proxy->GetPhysicsThreadAPI()->X()[2], NoHitZ);
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
ChaosTest::SetParticleSimDataToCollide({ Proxy->GetParticle_LowLevel() });
|
|
|
|
|
|
Particle.SetX(FVec3(0, 0, 100));
|
|
Particle.SetV(FVec3(0, 0, -10));
|
|
|
|
auto SphereGeom = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
FSingleParticlePhysicsProxy* SecondSphere = nullptr;
|
|
|
|
// Make particles
|
|
SecondSphere = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& SecondSphereParticle = SecondSphere->GetGameThreadAPI();
|
|
|
|
SecondSphereParticle.SetGravityEnabled(false);
|
|
SecondSphereParticle.SetX(FVec3(0, 0, 10)); //if resim doesn't reset collision, this will be in the Sphere's way
|
|
SecondSphereParticle.SetGeometry(SphereGeom);
|
|
Solver->RegisterObject(SecondSphere);
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
if (Step == 15)
|
|
{
|
|
//enable collision after particle passed
|
|
ChaosTest::SetParticleSimDataToCollide({ SecondSphere->GetParticle_LowLevel() });
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
Solver->UnregisterObject(SecondSphere);
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimSleepChange)
|
|
{
|
|
//change object state on physics thread, and make sure the state change is properly recorded in rewind data
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver);
|
|
Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData()](int32 PhysicsStep, bool bResim)
|
|
{
|
|
for (int32 Step = 0; Step < PhysicsStep; ++Step)
|
|
{
|
|
const EObjectStateType OldState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step).ObjectState();
|
|
if (Step < 4) //we set to sleep on step 3, which means we won't see it as input state until 4
|
|
{
|
|
EXPECT_EQ(OldState, EObjectStateType::Dynamic);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(OldState, EObjectStateType::Sleeping);
|
|
}
|
|
}
|
|
|
|
if (PhysicsStep == 3)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping);
|
|
}
|
|
};
|
|
|
|
const int32 LastGameStep = 32;
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ModifyVelocityFromSimCallback)
|
|
{
|
|
//modify velocity from sim callback and from solver
|
|
//rewind data should give us the velocity _before_ the sim callback (i.e. the solver is not stomping with the wrong PreV)
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver);
|
|
Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData(), SimDt](int32 PhysicsStep, bool bResim)
|
|
{
|
|
if (SimDt * PhysicsStep == 4)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 0));
|
|
}
|
|
|
|
for (int32 Step = 0; Step < PhysicsStep; ++Step)
|
|
{
|
|
const FReal OldV = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step).GetV()[2];
|
|
const FReal Time = Step * SimDt;
|
|
if (Time == 2) //velocity was reset by gt
|
|
{
|
|
EXPECT_NEAR(OldV, 0, 1e-2);
|
|
}
|
|
else if (Time < 2) //simple acceleration
|
|
{
|
|
EXPECT_NEAR(OldV, -Time, 1e-2);
|
|
}
|
|
else if (Time <= 4) //we set velocity at time = 4, so rewind data will not see it until frame 5
|
|
{
|
|
if (SimDt > 2) //if SimDt is this large, the reset of zero at time 2 is swallowed by first step so we don't really know about it
|
|
{
|
|
EXPECT_NEAR(OldV, -Time, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
//otherwise we see integration but from time 2 instead of time 0
|
|
EXPECT_NEAR(OldV, -(Time - 2), 1e-2);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//everyone sees reset after time 4 so integration is from that time
|
|
EXPECT_NEAR(OldV, -(Time - 4), 1e-2);
|
|
}
|
|
}
|
|
};
|
|
|
|
const int32 LastGameStep = 32;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(true);
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
|
|
Particle.SetV(FVec3(0, 0, 0));
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
if (Step == 2)
|
|
{
|
|
Particle.SetV(FVec3(0, 0, 0)); //reset velocity
|
|
}
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_SpawnEarlierCorrection)
|
|
{
|
|
// Test resim when object spawned earlier as part of correction
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
FSingleParticlePhysicsProxy* Floor = nullptr;
|
|
bool bHasResimmed = false;
|
|
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver);
|
|
Helper->TriggerRewindFunc = [&Floor, &bHasResimmed](int32 PhysicsStep) -> int32
|
|
{
|
|
if(!bHasResimmed && Floor)
|
|
{
|
|
bHasResimmed = true;
|
|
return 0;
|
|
}
|
|
return INDEX_NONE;
|
|
};
|
|
|
|
Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData(), SimDt, &Floor, &bHasResimmed](int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
const FReal Time = PhysicsStep * SimDt;
|
|
|
|
if (bIsResimming)
|
|
{
|
|
if (PhysicsStep == 1)
|
|
{
|
|
RewindData->SpawnProxyIfNeeded(*Floor);
|
|
}
|
|
}
|
|
|
|
|
|
if (!bHasResimmed || Time < 4.5)
|
|
{
|
|
//simply movement without hitting floor because it's spawned too late
|
|
EXPECT_NEAR(Proxy->GetPhysicsThreadAPI()->X()[2], 14.5 - Time, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
//floor spawned earlier so we hit it
|
|
EXPECT_GE(Proxy->GetPhysicsThreadAPI()->X()[2], 10 - 1e-2);
|
|
}
|
|
};
|
|
|
|
const int32 LastGameStep = 32;
|
|
|
|
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal,3>(FVec3(-100, -100, -1), FVec3(100, 100, 0)));
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, -1));
|
|
Particle.SetX(FVec3(0, 0, 14.5));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ Proxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
|
|
//spawn floor way late to ensure no collision on first run
|
|
if (Step == 12)
|
|
{
|
|
Floor = FSingleParticlePhysicsProxy::Create(Chaos::FGeometryParticle::CreateParticle());
|
|
auto& FloorParticle = Floor->GetGameThreadAPI();
|
|
FloorParticle.SetGeometry(FloorGeom);
|
|
Solver->RegisterObject(Floor);
|
|
ChaosTest::SetParticleSimDataToCollide({ Floor->GetParticle_LowLevel() });
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_SpawnEarlierCorrection2)
|
|
{
|
|
// Test resim when object spawned earlier as part of correction
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
FSingleParticlePhysicsProxy* SpawnedProxy = nullptr;
|
|
FSingleParticlePhysicsProxy* SpawnedProxyNoCorrection = nullptr;
|
|
|
|
bool bHasResimmed = false;
|
|
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver);
|
|
Helper->TriggerRewindFunc = [&SpawnedProxy, &SpawnedProxyNoCorrection, &bHasResimmed](int32 PhysicsStep) -> int32
|
|
{
|
|
|
|
if(!bHasResimmed && SpawnedProxy)
|
|
{
|
|
bHasResimmed = true;
|
|
return 0;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
};
|
|
|
|
Helper->ProcessInputsFunc = [RewindData = Solver->GetRewindData(), SimDt, &SpawnedProxy, &SpawnedProxyNoCorrection, &bHasResimmed](int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
const FReal Time = PhysicsStep * SimDt;
|
|
|
|
if (bIsResimming && SpawnedProxy)
|
|
{
|
|
if (PhysicsStep == 10)
|
|
{
|
|
RewindData->SpawnProxyIfNeeded(*SpawnedProxy);
|
|
SpawnedProxy->GetPhysicsThreadAPI()->SetX(FVec3(500, 0, 100.0));
|
|
|
|
}
|
|
}
|
|
|
|
if (SpawnedProxy && SpawnedProxyNoCorrection)
|
|
{
|
|
auto PT0 = SpawnedProxy->GetPhysicsThreadAPI();
|
|
auto PT1 = SpawnedProxyNoCorrection->GetPhysicsThreadAPI();
|
|
|
|
// After we've applied the correction and simmed past frame 10, we expect the first proxy to be "ahead" of the second one that didn't get corrected
|
|
if (bHasResimmed && PhysicsStep > 10)
|
|
{
|
|
EXPECT_LT(PT0->X()[2], PT1->X()[2]);
|
|
}
|
|
}
|
|
};
|
|
|
|
const int32 LastGameStep = 32;
|
|
|
|
auto SphereGeom = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, -1));
|
|
Particle.SetX(FVec3(0, 0, 14.5));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ Proxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
|
|
//spawn floor way late to ensure no collision on first run
|
|
if (Step == 12)
|
|
{
|
|
|
|
// Make particles. E.g:
|
|
// we just found out from the server these were spawned and these are the latest positions replicated from the server.
|
|
// but actually these positions are the server positions from frame 10. Lets see if we can correct this.
|
|
{
|
|
SpawnedProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& GT = SpawnedProxy->GetGameThreadAPI();
|
|
|
|
GT.SetV(FVec3(0, 0, 0.0));
|
|
GT.SetX(FVec3(500, 0, 100.0));
|
|
|
|
GT.SetGeometry(SphereGeom);
|
|
Solver->RegisterObject(SpawnedProxy);
|
|
ChaosTest::SetParticleSimDataToCollide({ SpawnedProxy->GetParticle_LowLevel() });
|
|
}
|
|
|
|
{
|
|
SpawnedProxyNoCorrection = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& GT = SpawnedProxyNoCorrection->GetGameThreadAPI();
|
|
|
|
GT.SetV(FVec3(0, 0, 0.0));
|
|
GT.SetX(FVec3(100, 0, 100.0));
|
|
|
|
GT.SetGeometry(SphereGeom);
|
|
Solver->RegisterObject(SpawnedProxyNoCorrection);
|
|
ChaosTest::SetParticleSimDataToCollide({ SpawnedProxyNoCorrection->GetParticle_LowLevel() });
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_MovingToNotMovingInterpolation)
|
|
{
|
|
//
|
|
//This tests that even though it's only dirty for one frame, we still interpolate it over many
|
|
//Makes sure rewind data is still passed back to GT even though particle is asleep before and after it moves (i.e. not dirty during rewind step)
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
//only care about resim when async results are used
|
|
if (Solver->IsUsingAsyncResults() == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FReal ResimTime = 20;
|
|
const FReal SleepTime = 12;
|
|
|
|
bool bHasResimmed = false;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver);
|
|
Helper->TriggerRewindFunc = [&bHasResimmed, ResimTime, SimDt](int32 PhysicsStep) -> int32
|
|
{
|
|
const FReal Time = PhysicsStep * SimDt;
|
|
if (!bHasResimmed && Time == ResimTime)
|
|
{
|
|
bHasResimmed = true;
|
|
return 2;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
};
|
|
|
|
Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData(), SimDt, ResimTime, SleepTime, &bHasResimmed](int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
const FReal Time = PhysicsStep * SimDt;
|
|
|
|
if (bHasResimmed)
|
|
{
|
|
|
|
|
|
if (Proxy->GetPhysicsThreadAPI()->V()[2] == 1)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 0));
|
|
}
|
|
|
|
|
|
}
|
|
else
|
|
{
|
|
if (Time >= SleepTime && Proxy->GetPhysicsThreadAPI()->ObjectState() == EObjectStateType::Dynamic)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping);
|
|
}
|
|
}
|
|
};
|
|
|
|
const int32 LastGameStep = 64;
|
|
|
|
auto FloorGeom = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, -1), FVec3(100, 100, 0)));
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 0));
|
|
Particle.SetX(FVec3(0, 0, 0));
|
|
|
|
FReal Time = 0;
|
|
const FReal StartMovingTime = 4;
|
|
const FReal StartMovingTimeDiscrete = FMath::FloorToInt(StartMovingTime / SimDt) * SimDt;
|
|
const FReal GTDt = 1;
|
|
const FReal InterpStartTime = ResimTime + SimDt;
|
|
const FReal SleepLocation = SleepTime - StartMovingTimeDiscrete;
|
|
const FReal CorrectedLocation = SimDt <= 1 ? 0 : 4;
|
|
FReal PrevZDuringInterp = SleepLocation; //shouldn't be further then this because already interpolating back to 0
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
|
|
if (Time == StartMovingTime)
|
|
{
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
|
|
Time += GTDt;
|
|
const FReal InterpolatedTime = Time - SimDt * Solver->GetAsyncInterpolationMultiplier();
|
|
|
|
|
|
if (InterpolatedTime <= InterpStartTime)
|
|
{
|
|
if(InterpolatedTime < StartMovingTimeDiscrete)
|
|
{
|
|
//No movement yet
|
|
EXPECT_NEAR(Particle.X()[2], 0, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
//simple movement with constant velocity until goes to sleep
|
|
if(InterpolatedTime >= SleepTime)
|
|
{
|
|
EXPECT_NEAR(Particle.X()[2], SleepLocation, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_NEAR(Particle.X()[2], InterpolatedTime - StartMovingTimeDiscrete, 1e-2);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//leash mode
|
|
EXPECT_GE(Particle.X()[2], CorrectedLocation);
|
|
EXPECT_LE(Particle.X()[2], PrevZDuringInterp);
|
|
PrevZDuringInterp = Particle.X()[2];
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimInSync)
|
|
{
|
|
//apply forces in the same way until resim step 4 which should trigger a desync
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FSingleParticlePhysicsProxy* Proxy;
|
|
FRewindData* RewindData;
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
bool bIsResimming = RewindData->IsResim();
|
|
if(PhysicsStep == 2)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->AddForce(FVec3(1, 0, 0));
|
|
}
|
|
|
|
if(bIsResimming && PhysicsStep == 4)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->AddForce(FVec3(1, 0, 0)); //cause a desync
|
|
}
|
|
|
|
if(bIsResimming)
|
|
{
|
|
if(PhysicsStep <= 4)
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::HardDesync);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync);
|
|
}
|
|
}
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override
|
|
{
|
|
if (LastCompletedStep == ResimEndFrame)
|
|
{
|
|
return ResimStartFrame;
|
|
}
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
int32 ResimStartFrame = 1;
|
|
int32 ResimEndFrame = 10;
|
|
};
|
|
|
|
const int32 LastGameStep = 32;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
auto UniqueRewindCallback = MakeUnique<FRewindCallback>();
|
|
auto RewindCallback = UniqueRewindCallback.Get();
|
|
RewindCallback->Proxy = Proxy;
|
|
RewindCallback->RewindData = Solver->GetRewindData();
|
|
|
|
Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback));
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
if(Step == 3)
|
|
{
|
|
Particle.AddForce(FVec3(0, 0, -10));
|
|
}
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimInSync2)
|
|
{
|
|
//different velocity during resim step 5 which should cause a desync
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FSingleParticlePhysicsProxy* Proxy;
|
|
FRewindData* RewindData;
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
bool bIsResimming = RewindData->IsResim();
|
|
if (PhysicsStep == 2)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0,0,2));
|
|
}
|
|
|
|
if (bIsResimming)
|
|
{
|
|
if(PhysicsStep == 3)
|
|
{
|
|
//only setting velocity during resim, but exact same value so should still be in sync
|
|
Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 2));
|
|
}
|
|
|
|
if(PhysicsStep == 5)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 5));
|
|
}
|
|
}
|
|
|
|
if (bIsResimming)
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), PhysicsStep <= 5 ? ESyncState::InSync : ESyncState::HardDesync);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync);
|
|
}
|
|
}
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override
|
|
{
|
|
if (LastCompletedStep == ResimEndFrame)
|
|
{
|
|
return ResimStartFrame;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
int32 ResimStartFrame = 1;
|
|
int32 ResimEndFrame = 10;
|
|
};
|
|
|
|
const int32 LastGameStep = 32;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
auto UniqueRewindCallback = MakeUnique<FRewindCallback>();
|
|
auto RewindCallback = UniqueRewindCallback.Get();
|
|
RewindCallback->Proxy = Proxy;
|
|
RewindCallback->RewindData = Solver->GetRewindData();
|
|
|
|
Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback));
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimInSync3)
|
|
{
|
|
//different position during resim step 5 which should cause a desync
|
|
//want a completely clean property to make sure we properly update resim buffer when property is dirtied only on second run
|
|
TRewindHelper::TestEmpty([](auto* Solver, FReal SimDt, int32 Optimization)
|
|
{
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FSingleParticlePhysicsProxy* Proxy;
|
|
FRewindData* RewindData;
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
bool bIsResimming = RewindData->IsResim();
|
|
if (bIsResimming)
|
|
{
|
|
if (PhysicsStep == 5)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetX(FVec3(0, 0, 5));
|
|
}
|
|
}
|
|
|
|
if (bIsResimming)
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), PhysicsStep <= 5 ? ESyncState::InSync : ESyncState::HardDesync);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync);
|
|
}
|
|
}
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override
|
|
{
|
|
if (LastCompletedStep == ResimEndFrame)
|
|
{
|
|
return ResimStartFrame;
|
|
}
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
int32 ResimStartFrame = 1;
|
|
int32 ResimEndFrame = 10;
|
|
};
|
|
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
Proxy->GetGameThreadAPI().SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
|
|
const int32 LastGameStep = 32;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
auto UniqueRewindCallback = MakeUnique<FRewindCallback>();
|
|
auto RewindCallback = UniqueRewindCallback.Get();
|
|
RewindCallback->Proxy = Proxy;
|
|
RewindCallback->RewindData = Solver->GetRewindData();
|
|
|
|
Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback));
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimInSync4)
|
|
{
|
|
//different position during resim step 5 which should cause a desync
|
|
//want a completely clean property to make sure we properly update resim buffer when property is dirtied only on second run
|
|
//add a dirty property every frame to make sure we are getting an entry in the original sim, but with a clean property
|
|
//unrelated property must be dirtied on GT because simcallback dirty just copies all properties at the moment
|
|
TRewindHelper::TestEmpty([](auto* Solver, FReal SimDt, int32 Optimization)
|
|
{
|
|
//this test requires dirty writes from GT
|
|
//we want those to line up with PT tick 1 to 1
|
|
if (SimDt != 1) { return; }
|
|
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FSingleParticlePhysicsProxy* Proxy;
|
|
FRewindData* RewindData;
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
bool bIsResimming = true;
|
|
if (PhysicsStep > HighestStep)
|
|
{
|
|
bIsResimming = false;
|
|
HighestStep = PhysicsStep;
|
|
}
|
|
ensure(bIsResimming == RewindData->IsResim()); // this would catch if IsResim is lieing to us
|
|
|
|
if (bIsResimming)
|
|
{
|
|
if (PhysicsStep == 5)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetX(FVec3(0, 0, 5));
|
|
}
|
|
}
|
|
|
|
if(bIsResimming)
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), PhysicsStep <= 5 ? ESyncState::InSync : ESyncState::HardDesync);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Proxy->GetHandle_LowLevel()->SyncState(), ESyncState::InSync);
|
|
}
|
|
}
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override
|
|
{
|
|
if (LastCompletedStep == ResimEndFrame)
|
|
{
|
|
return ResimStartFrame;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
int32 ResimStartFrame = 1;
|
|
int32 ResimEndFrame = 10;
|
|
|
|
int32 HighestStep = INDEX_NONE;
|
|
|
|
};
|
|
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
Proxy->GetGameThreadAPI().SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
|
|
|
|
const int32 LastGameStep = 32;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
auto UniqueRewindCallback = MakeUnique<FRewindCallback>();
|
|
auto RewindCallback = UniqueRewindCallback.Get();
|
|
RewindCallback->Proxy = Proxy;
|
|
RewindCallback->RewindData = Solver->GetRewindData();
|
|
|
|
Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback));
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
Proxy->GetGameThreadAPI().SetV(FVec3(0, 0, Step)); //dirty an unrelated property to still get an entry in the history buffer
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimSleepChangeRewind)
|
|
{
|
|
// Test puting object to sleep during Resim
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
int32 ExpectedSleepFrame = INDEX_NONE;
|
|
const int32 ResimStartFrame = 1;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 11, ResimStartFrame);
|
|
Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData()](const int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
if (bIsResimming)
|
|
{
|
|
if (PhysicsStep >= ResimStartFrame + 2)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping, false, false);
|
|
ExpectedSleepFrame = ResimStartFrame + 3;
|
|
}
|
|
else
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Dynamic, false, false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
for (int32 Step = ResimStartFrame; Step < PhysicsStep; ++Step)
|
|
{
|
|
const EObjectStateType OldState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step).ObjectState();
|
|
|
|
if (ExpectedSleepFrame != INDEX_NONE && Step >= ExpectedSleepFrame)
|
|
{
|
|
EXPECT_EQ(OldState, EObjectStateType::Sleeping);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(OldState, EObjectStateType::Dynamic);
|
|
}
|
|
}
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
|
|
for (int Step = 0; Step <= 32; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimSleepChangeRewind2)
|
|
{
|
|
// This does two ObjectState corrections
|
|
// -Object starts out asleep
|
|
// -On step 10 we force a rewind to frame 4 and wake it up during the resim (apply "correction")
|
|
// -This on its own works fine
|
|
// -On step 12 we force another rewind to frame 6 and put it to sleep during the resim
|
|
// -This seems to have no effect: the object stays awake
|
|
//
|
|
//
|
|
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FSingleParticlePhysicsProxy* Proxy;
|
|
FRewindData* RewindData;
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
if (bIsResimming)
|
|
{
|
|
if (ForceAwakeFrame != INDEX_NONE && PhysicsStep == ForceAwakeFrame)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Dynamic);
|
|
}
|
|
|
|
if (ForceSleepFrame != INDEX_NONE && PhysicsStep == ForceSleepFrame)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
for (int32 Step = ResimStartFrame; Step < PhysicsStep; ++Step)
|
|
{
|
|
const EObjectStateType OldState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step).ObjectState();
|
|
if (TestAwakeFrame == INDEX_NONE || Step < TestAwakeFrame)
|
|
{
|
|
// If we haven't applied the first awake correction yet, or this is a step prior to that correction, we should be asleep
|
|
EXPECT_EQ(OldState, EObjectStateType::Sleeping);
|
|
}
|
|
else if (TestSleepFrame != INDEX_NONE && Step >= TestSleepFrame)
|
|
{
|
|
// If we *have* applied the second sleep correction and this is a step on or after that correction, we should be asleep
|
|
EXPECT_EQ(OldState, EObjectStateType::Sleeping);
|
|
}
|
|
else
|
|
{
|
|
// Everything else falls in the middle, we should be awake
|
|
EXPECT_EQ(OldState, EObjectStateType::Dynamic);
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override
|
|
{
|
|
if (bIsResimming)
|
|
{
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
if (LastCompletedStep == 10)
|
|
{
|
|
ForceAwakeFrame = 4;
|
|
bIsResimming = true;
|
|
ResimEndFrame = LastCompletedStep;
|
|
TestAwakeFrame = ForceAwakeFrame+1;
|
|
ResimStartFrame = ForceAwakeFrame;
|
|
return ResimStartFrame;
|
|
}
|
|
|
|
if (LastCompletedStep == 12)
|
|
{
|
|
ForceSleepFrame = 6;
|
|
bIsResimming = true;
|
|
ResimEndFrame = LastCompletedStep;
|
|
TestSleepFrame = ForceSleepFrame+1;
|
|
ResimStartFrame = ForceSleepFrame;
|
|
return ResimStartFrame;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
void PostResimStep_Internal(int32 PhysicsStep) override
|
|
{
|
|
if (ResimEndFrame == PhysicsStep)
|
|
{
|
|
ForceAwakeFrame = INDEX_NONE;
|
|
ForceSleepFrame = INDEX_NONE;
|
|
bIsResimming = false;
|
|
}
|
|
}
|
|
|
|
int32 ForceAwakeFrame = INDEX_NONE;
|
|
int32 ForceSleepFrame = INDEX_NONE;
|
|
|
|
int32 TestAwakeFrame = INDEX_NONE;
|
|
int32 TestSleepFrame = INDEX_NONE;
|
|
int32 ResimStartFrame = 0;
|
|
|
|
bool bIsResimming = false;
|
|
int32 ResimEndFrame = INDEX_NONE;
|
|
};
|
|
|
|
const int32 LastGameStep = 32;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
auto UniqueRewindCallback = MakeUnique<FRewindCallback>();
|
|
auto RewindCallback = UniqueRewindCallback.Get();
|
|
RewindCallback->Proxy = Proxy;
|
|
RewindCallback->RewindData = Solver->GetRewindData();
|
|
|
|
Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback));
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
Particle.SetObjectState(EObjectStateType::Sleeping);
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_RewindBeforeSleep)
|
|
{
|
|
// Rewind to before an object was put to sleep and see that the Active view is valid
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
int32 ExpectedSleepFrame = INDEX_NONE;
|
|
const int32 ResimStartFrame = 0;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 11, ResimStartFrame);
|
|
Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData(), Solver](const int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
if(PhysicsStep == 5)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 0));
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Sleeping);
|
|
}
|
|
|
|
//before sleep the active view contains the particle, after we put it to sleep the active view is empty
|
|
EXPECT_EQ(Solver->GetParticles().GetActiveParticlesView().Num(), PhysicsStep < 5 ? 1 : 0);
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
|
|
for (int Step = 0; Step <= 32; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_RewindBeforeAwake)
|
|
{
|
|
// Rewind to before an object was made awake and confirm that the active view is valid
|
|
// This test rewinds to step 3 because it ensures there's no PushData which may fixup the view. Need to make sure it's fixed regardless of push data
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
int32 ExpectedSleepFrame = INDEX_NONE;
|
|
const int32 ResimStartFrame = 3;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 13, ResimStartFrame);
|
|
Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData(), Solver](const int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
if (PhysicsStep == 12)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetV(FVec3(0, 0, 1));
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Dynamic);
|
|
}
|
|
|
|
//before wake up the active view was empty, after we wake it up the view contains the particle
|
|
EXPECT_EQ(Solver->GetParticles().GetActiveParticlesView().Num(), PhysicsStep < 12 ? 0 : 1);
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetObjectState(EObjectStateType::Sleeping);
|
|
|
|
for (int Step = 0; Step <= 32; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_RewindBeforeMadeKinematic)
|
|
{
|
|
// Rewind before a dynamic was made kinematic and check the view is properly handling the rewind
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
int32 ExpectedSleepFrame = INDEX_NONE;
|
|
const int32 ResimStartFrame = 3;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 13, ResimStartFrame);
|
|
Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData(), Solver](const int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
if (PhysicsStep == 12)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Kinematic);
|
|
}
|
|
|
|
if(PhysicsStep < 12)
|
|
{
|
|
EXPECT_EQ(Solver->GetParticles().GetNonDisabledDynamicView().Num(), 1);
|
|
EXPECT_EQ(Solver->GetParticles().GetActiveKinematicParticlesView().Num(), 0);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Solver->GetParticles().GetNonDisabledDynamicView().Num(), 0);
|
|
EXPECT_EQ(Solver->GetParticles().GetActiveKinematicParticlesView().Num(), 1);
|
|
}
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
|
|
for (int Step = 0; Step <= 32; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_RewindBeforeMadeDynamic)
|
|
{
|
|
// Rewind before a kinematic was made dynamic and check the view is properly handling the rewind
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
int32 ExpectedSleepFrame = INDEX_NONE;
|
|
const int32 ResimStartFrame = 3;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, 13, ResimStartFrame);
|
|
Helper->ProcessInputsFunc = [&ExpectedSleepFrame, ResimStartFrame, Proxy, RewindData = Solver->GetRewindData(), Solver](const int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
if (PhysicsStep == 12)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetObjectState(EObjectStateType::Dynamic);
|
|
}
|
|
|
|
if (PhysicsStep < 12)
|
|
{
|
|
EXPECT_EQ(Solver->GetParticles().GetNonDisabledDynamicView().Num(), 0);
|
|
EXPECT_EQ(Solver->GetParticles().GetActiveKinematicParticlesView().Num(), 1);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(Solver->GetParticles().GetNonDisabledDynamicView().Num(), 1);
|
|
EXPECT_EQ(Solver->GetParticles().GetActiveKinematicParticlesView().Num(), 0);
|
|
}
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
Proxy->GetGameThreadAPI().SetObjectState(EObjectStateType::Kinematic);
|
|
|
|
for (int Step = 0; Step <= 32; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_RecordForcesInSimCallback)
|
|
{
|
|
//Makes sure that we record the forces applied during sim callback
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver);
|
|
Helper->ProcessInputsFunc = [Proxy, RewindData = Solver->GetRewindData(), Optimization](const int32 PhysicsStep, bool bIsResimming)
|
|
{
|
|
if (PhysicsStep == 3)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetAcceleration(FVec3(0, 0, 10) + Proxy->GetPhysicsThreadAPI()->Acceleration(), 0);
|
|
Proxy->GetPhysicsThreadAPI()->SetAngularAcceleration(FVec3(0, 0, 10) + Proxy->GetPhysicsThreadAPI()->AngularAcceleration());
|
|
}
|
|
|
|
for (int32 Step = 0; Step < PhysicsStep; ++Step)
|
|
{
|
|
if (Optimization != 2) // Optimization 2 does not cache data in PrePushData so no need to check that phase
|
|
{
|
|
//Before push no force or torque at all
|
|
FGeometryParticleState PrePushState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step, FFrameAndPhase::EParticleHistoryPhase::PrePushData);
|
|
EXPECT_EQ(PrePushState.Acceleration()[2], 0);
|
|
EXPECT_EQ(PrePushState.AngularAcceleration()[2], 0);
|
|
}
|
|
|
|
{
|
|
//After push (but before callback) only see GT force, and no torque
|
|
FGeometryParticleState PostPushState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step, FFrameAndPhase::EParticleHistoryPhase::PostPushData);
|
|
EXPECT_EQ(PostPushState.Acceleration()[2], 1); //GT always sets force of 1
|
|
EXPECT_EQ(PostPushState.AngularAcceleration()[2], 0); //GT never sets torque
|
|
}
|
|
|
|
if (Optimization != 2) // Optimization 2 does not cache data in PostCallbacks so no need to check that phase
|
|
{
|
|
FGeometryParticleState State = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step, FFrameAndPhase::EParticleHistoryPhase::PostCallbacks);
|
|
if (Step == 3)
|
|
{
|
|
EXPECT_EQ(State.Acceleration()[2], 11); //1 from GT + 10 from callback
|
|
EXPECT_EQ(State.AngularAcceleration()[2], 10); //10 from callback (nothing from GT)
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(State.Acceleration()[2], 1); //GT always sets force of 1
|
|
EXPECT_EQ(State.AngularAcceleration()[2], 0); //GT never sets torque
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetGravityEnabled(false);
|
|
|
|
for (int Step = 0; Step <= 32; ++Step)
|
|
{
|
|
Particle.AddForce(FVec3(0, 0, 1));
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_AddForce)
|
|
{
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
const int32 LastGameStep = 20;
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
//sim-writable property that changes every step
|
|
Particle.AddForce(FVec3(0, 0, Step + 1));
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
const FRewindData* RewindData = Solver->GetRewindData();
|
|
|
|
//check state at every step except latest
|
|
const int32 LastSimStep = LastGameStep / SimDt;
|
|
for (int Step = 0; Step < LastSimStep - 1; ++Step)
|
|
{
|
|
const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step);
|
|
FReal ExpectedForce = Step + 1;
|
|
if (SimDt < 1)
|
|
{
|
|
//each sub-step gets a constant force applied
|
|
ExpectedForce = FMath::FloorToFloat(Step * SimDt) + 1;
|
|
}
|
|
else if (SimDt > 1)
|
|
{
|
|
//each step gets an average of the forces applied ((step+1) + (step+2) + (step+3) + (step+4))/4 = step + (1+2+3+4)/4 = step + 2.5
|
|
//where step is game step: so really it's step * 4
|
|
ExpectedForce = Step * 4 + 2.5;
|
|
}
|
|
EXPECT_EQ(ParticleState.Acceleration()[2], ExpectedForce);
|
|
}
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_IntermittentForce)
|
|
{
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
const int32 LastGameStep = 20;
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
//sim-writable property that changes infrequently and not at beginning
|
|
if (Step == 3)
|
|
{
|
|
Particle.AddForce(FVec3(0, 0, Step));
|
|
}
|
|
|
|
if (Step == 5)
|
|
{
|
|
Particle.AddForce(FVec3(0, 0, Step));
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
const FRewindData* RewindData = Solver->GetRewindData();
|
|
|
|
//check state at every step except latest
|
|
const int32 LastSimStep = LastGameStep / SimDt;
|
|
for (int Step = 0; Step < LastSimStep - 1; ++Step)
|
|
{
|
|
const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step);
|
|
|
|
if (SimDt <= 1)
|
|
{
|
|
const float SimTime = Step * SimDt;
|
|
if (SimTime >= 3 && SimTime < 4)
|
|
{
|
|
EXPECT_EQ(ParticleState.Acceleration()[2], 3);
|
|
}
|
|
else if (SimTime >= 5 && SimTime < 6)
|
|
{
|
|
EXPECT_EQ(ParticleState.Acceleration()[2], 5);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(ParticleState.Acceleration()[2], 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//we get an average
|
|
if (Step == 0)
|
|
{
|
|
EXPECT_EQ(ParticleState.Acceleration()[2], 3 / 4.f);
|
|
}
|
|
else if (Step == 1)
|
|
{
|
|
EXPECT_EQ(ParticleState.Acceleration()[2], 5 / 4.f);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(ParticleState.Acceleration()[2], 0);
|
|
}
|
|
}
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_IntermittentGeomChange)
|
|
{
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(0), FVec3(1)));
|
|
auto Box2 = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(2), FVec3(3)));
|
|
|
|
const int32 LastGameStep = 20;
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
//property that changes once half way through
|
|
if (Step == 3)
|
|
{
|
|
Particle.SetGeometry(Box);
|
|
}
|
|
|
|
if (Step == 5)
|
|
{
|
|
Particle.SetGeometry(Box2);
|
|
}
|
|
|
|
if (Step == 7)
|
|
{
|
|
Particle.SetGeometry(Box);
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
const FRewindData* RewindData = Solver->GetRewindData();
|
|
|
|
//check state at every step except latest
|
|
const int32 LastSimStep = LastGameStep / SimDt;
|
|
for (int Step = 0; Step < LastSimStep - 1; ++Step)
|
|
{
|
|
const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step);
|
|
if (SimDt <= 1)
|
|
{
|
|
const float SimTime = Step * SimDt;
|
|
if (SimTime < 3)
|
|
{
|
|
//was sphere
|
|
EXPECT_EQ(ParticleState.GetGeometry(), Sphere);
|
|
}
|
|
else if (SimTime < 5 || SimTime >= 7)
|
|
{
|
|
//then became box
|
|
EXPECT_EQ(ParticleState.GetGeometry(), Box.GetReference());
|
|
}
|
|
else
|
|
{
|
|
//second box
|
|
EXPECT_EQ(ParticleState.GetGeometry(), Box2.GetReference());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//changes happen within interval so stays box entire time
|
|
EXPECT_EQ(ParticleState.GetGeometry(), Box.GetReference());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_FallingObjectWithTeleport)
|
|
{
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
Particle.SetGravityEnabled(true);
|
|
Particle.SetX(FVec3(0, 0, 100));
|
|
|
|
const int32 LastGameStep = 20;
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
//teleport from GT
|
|
if (Step == 5)
|
|
{
|
|
Particle.SetX(FVec3(0, 0, 10));
|
|
Particle.SetV(FVec3(0, 0, 0));
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
const FRewindData* RewindData = Solver->GetRewindData();
|
|
|
|
//check state at every step except latest
|
|
const int32 LastSimStep = LastGameStep / SimDt;
|
|
FReal ExpectedVZ = 0;
|
|
FReal ExpectedXZ = 100;
|
|
|
|
for (int Step = 0; Step < LastSimStep - 1; ++Step)
|
|
{
|
|
const auto ParticleState = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step);
|
|
|
|
const FReal SimStart = SimDt * Step;
|
|
const FReal SimEnd = SimDt * (Step + 1);
|
|
if (SimStart <= 5 && SimEnd > 5)
|
|
{
|
|
ExpectedVZ = 0;
|
|
ExpectedXZ = 10;
|
|
}
|
|
|
|
EXPECT_NEAR(ParticleState.GetX()[2], ExpectedXZ, 1e-4);
|
|
EXPECT_NEAR(ParticleState.GetV()[2], ExpectedVZ, 1e-4);
|
|
|
|
ExpectedVZ -= SimDt;
|
|
ExpectedXZ += ExpectedVZ * SimDt;
|
|
}
|
|
});
|
|
}
|
|
|
|
struct FSimCallbackHelperInput : FSimCallbackInput
|
|
{
|
|
void Reset() {}
|
|
int InCounter;
|
|
};
|
|
|
|
struct FSimCallbackHelperOutput : FSimCallbackOutput
|
|
{
|
|
void Reset() {}
|
|
int OutCounter;
|
|
};
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_SimCallbackInputsOutputs)
|
|
{
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
struct FSimCallbackHelper : TSimCallbackObject<FSimCallbackHelperInput, FSimCallbackHelperOutput>
|
|
{
|
|
int32 TriggerCount = 0;
|
|
virtual void OnPreSimulate_Internal() override
|
|
{
|
|
GetProducerOutputData_Internal().OutCounter = TriggerCount++;
|
|
}
|
|
};
|
|
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FRewindCallback(int32 InNumPhysicsSteps, FReal InSimDt)
|
|
: NumPhysicsSteps(InNumPhysicsSteps)
|
|
, SimDt(InSimDt)
|
|
{
|
|
|
|
}
|
|
|
|
TArray<int32> InCounters;
|
|
int32 NumPhysicsSteps;
|
|
bool bResim = false;
|
|
FReal SimDt;
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override
|
|
{
|
|
if (LastCompletedStep + 1 == NumPhysicsSteps && NumPhysicsSteps != INDEX_NONE)
|
|
{
|
|
NumPhysicsSteps = INDEX_NONE; //don't resim again after this
|
|
return 0;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
virtual void ProcessInputs_External(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
EXPECT_EQ(bResim, false); //should never be triggered during resim, since resim happens on internal thread
|
|
EXPECT_EQ(SimCallbackInputs.Num(), 1);
|
|
FSimCallbackHelperInput* Input = static_cast<FSimCallbackHelperInput*>(SimCallbackInputs[0].Input);
|
|
if(SimDt > 1)
|
|
{
|
|
//several external ticks before we finally get the final input
|
|
EXPECT_EQ(FMath::TruncToInt((PhysicsStep+1) * SimDt - 1), Input->InCounter);
|
|
}
|
|
else
|
|
{
|
|
//potentially the same input over multiple sub-steps
|
|
EXPECT_EQ(FMath::TruncToInt(PhysicsStep * SimDt), Input->InCounter);
|
|
}
|
|
}
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
EXPECT_EQ(SimCallbackInputs.Num(), 1);
|
|
FSimCallbackHelperInput* Input = static_cast<FSimCallbackHelperInput*>(SimCallbackInputs[0].Input);
|
|
if(bResim)
|
|
{
|
|
EXPECT_EQ(InCounters[PhysicsStep], Input->InCounter);
|
|
}
|
|
else
|
|
{
|
|
InCounters.Add(Input->InCounter);
|
|
}
|
|
}
|
|
|
|
virtual void PreResimStep_Internal(int32 PhysicsStep, bool bFirst) override
|
|
{
|
|
bResim = true;
|
|
}
|
|
|
|
virtual void PostResimStep_Internal(int32 PhysicsStep) override
|
|
{
|
|
bResim = false;
|
|
}
|
|
};
|
|
|
|
const int32 LastGameStep = 20;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
Solver->SetRewindCallback(MakeUnique<FRewindCallback>(NumPhysSteps, SimDt));
|
|
FSimCallbackHelper* SimCallback = Solver->template CreateAndRegisterSimCallbackObject_External<FSimCallbackHelper>();
|
|
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
Particle.SetGravityEnabled(true);
|
|
Particle.SetX(FVec3(0, 0, 100));
|
|
|
|
for (int Step = 0; Step < LastGameStep; ++Step)
|
|
{
|
|
SimCallback->GetProducerInputData_External()->InCounter = Step;
|
|
TickSolverHelper(Solver);
|
|
}
|
|
}
|
|
|
|
//during async we can't consume all outputs right away, so we won't get the resim results right away
|
|
//with sync we should be able to get all the results right away and this acts as a good test
|
|
if(!Solver->IsUsingAsyncResults())
|
|
{
|
|
//we get all the original outputs plus the rewind outputs, they counter just keeps going up, but the InternalTime should reflect the rewind
|
|
int32 Count = 0;
|
|
FReal CurTime = 0.f;
|
|
while (TSimCallbackOutputHandle<FSimCallbackHelperOutput> Output = SimCallback->PopOutputData_External())
|
|
{
|
|
EXPECT_FLOAT_EQ(Output->InternalTime, CurTime);
|
|
EXPECT_EQ(Count, Output->OutCounter);
|
|
++Count;
|
|
|
|
if(Count == NumPhysSteps)
|
|
{
|
|
CurTime = 0; //reset time for resim
|
|
}
|
|
else
|
|
{
|
|
CurTime += SimDt;
|
|
}
|
|
}
|
|
|
|
EXPECT_EQ(Count, NumPhysSteps * 2); //should have two results for each physics step since we rewound
|
|
}
|
|
|
|
|
|
});
|
|
}
|
|
|
|
struct FSimCallbackHelperInput2 : FSimCallbackInput
|
|
{
|
|
void Reset() { StepToCounter.Reset(); }
|
|
TMap<int32, int32> StepToCounter;
|
|
};
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_SimCallbackProcessExternalInputs)
|
|
{
|
|
//If inputs are not set until external callback, make sure they are associated with the right frame
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
struct FSimCallbackHelper : TSimCallbackObject<FSimCallbackHelperInput2>
|
|
{
|
|
FPBDRigidsSolver* Solver = nullptr;
|
|
virtual void OnPreSimulate_Internal() override
|
|
{
|
|
EXPECT_EQ(GetConsumerInput_Internal()->StepToCounter[Solver->GetCurrentFrame()], Solver->GetCurrentFrame());
|
|
}
|
|
};
|
|
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FSimCallbackHelper* SimCallback;
|
|
FRewindCallback(FSimCallbackHelper* Callback) : SimCallback(Callback){}
|
|
virtual void InjectInputs_External(int32 PhysicsStep, int32 NumSteps) override
|
|
{
|
|
for(int32 Idx = 0; Idx < NumSteps; ++Idx)
|
|
{
|
|
const int32 Step = Idx + PhysicsStep;
|
|
SimCallback->GetProducerInputData_External()->StepToCounter.Add(Step, Step);
|
|
}
|
|
}
|
|
};
|
|
|
|
FSimCallbackHelper* SimCallback = Solver->template CreateAndRegisterSimCallbackObject_External<FSimCallbackHelper>();
|
|
SimCallback->Solver = Solver;
|
|
Solver->SetRewindCallback(MakeUnique<FRewindCallback>(SimCallback));
|
|
|
|
for (int Step = 0; Step < 32; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_SimCallbackInputsCorrection)
|
|
{
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
struct FSimCallbackHelper : TSimCallbackObject<FSimCallbackHelperInput, FSimCallbackHelperOutput>
|
|
{
|
|
int32 TriggerCount = 0;
|
|
virtual void OnPreSimulate_Internal() override
|
|
{
|
|
GetProducerOutputData_Internal().OutCounter = GetConsumerInput_Internal()->InCounter;
|
|
}
|
|
};
|
|
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FRewindCallback(int32 InNumPhysicsSteps)
|
|
: NumPhysicsSteps(InNumPhysicsSteps)
|
|
{
|
|
|
|
}
|
|
|
|
TArray<int32> InCounters;
|
|
int32 NumPhysicsSteps;
|
|
bool bResim = false;
|
|
const int32 Correction = -10;
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override
|
|
{
|
|
if (LastCompletedStep + 1 == NumPhysicsSteps && NumPhysicsSteps != INDEX_NONE)
|
|
{
|
|
NumPhysicsSteps = INDEX_NONE; //don't resim again after this
|
|
return 0;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
EXPECT_EQ(SimCallbackInputs.Num(), 1);
|
|
FSimCallbackHelperInput* Input = static_cast<FSimCallbackHelperInput*>(SimCallbackInputs[0].Input);
|
|
if (bResim)
|
|
{
|
|
Input->InCounter = InCounters[PhysicsStep] + Correction;
|
|
EXPECT_EQ(InCounters[PhysicsStep] + Correction, Input->InCounter);
|
|
}
|
|
else
|
|
{
|
|
InCounters.Add(Input->InCounter);
|
|
}
|
|
}
|
|
|
|
virtual void PreResimStep_Internal(int32 PhysicsStep, bool bFirst) override
|
|
{
|
|
bResim = true;
|
|
}
|
|
|
|
virtual void PostResimStep_Internal(int32 PhysicsStep) override
|
|
{
|
|
bResim = false;
|
|
}
|
|
};
|
|
|
|
const int32 LastGameStep = 20;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
auto UniqueRewindCallback = MakeUnique<FRewindCallback>(NumPhysSteps);
|
|
auto RewindCallback = UniqueRewindCallback.Get();
|
|
|
|
Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback));
|
|
FSimCallbackHelper* SimCallback = Solver->template CreateAndRegisterSimCallbackObject_External<FSimCallbackHelper>();
|
|
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
Particle.SetGravityEnabled(true);
|
|
Particle.SetX(FVec3(0, 0, 100));
|
|
|
|
for (int Step = 0; Step < LastGameStep; ++Step)
|
|
{
|
|
SimCallback->GetProducerInputData_External()->InCounter = Step;
|
|
TickSolverHelper(Solver);
|
|
}
|
|
}
|
|
|
|
//during async we can't consume all outputs right away, so we won't get the resim results right away
|
|
//with sync we should be able to get all the results right away and this acts as a good test
|
|
if (!Solver->IsUsingAsyncResults())
|
|
{
|
|
//we get all the original outputs plus the rewind outputs, the correction happens at NumPhysSteps and then the outputs should be the resim + correction
|
|
int32 Count = 0;
|
|
FReal CurTime = 0.f;
|
|
int32 Correction = 0;
|
|
while (TSimCallbackOutputHandle<FSimCallbackHelperOutput> Output = SimCallback->PopOutputData_External())
|
|
{
|
|
EXPECT_FLOAT_EQ(Output->InternalTime, CurTime);
|
|
const int32 ExpectedResult = FMath::FloorToInt(Output->InternalTime * SimDt) + Correction;
|
|
EXPECT_EQ(ExpectedResult, Output->OutCounter);
|
|
++Count;
|
|
|
|
if (Count == NumPhysSteps)
|
|
{
|
|
CurTime = 0; //reset time for resim
|
|
Correction = RewindCallback->Correction;
|
|
}
|
|
else
|
|
{
|
|
CurTime += SimDt;
|
|
}
|
|
}
|
|
|
|
EXPECT_EQ(Count, NumPhysSteps * 2); //should have two results for each physics step since we rewound
|
|
}
|
|
|
|
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_SimCallbackCorrectionInterpolation)
|
|
{
|
|
// NOTE: Disabled because the logic that is being tested is no longer used.
|
|
|
|
/*want to interpolate between original sim and correction
|
|
edge cases to test:
|
|
- gt write should stomp interpolation
|
|
- make sure sleep state works
|
|
- not moving in original, but moving in resim
|
|
- moving in original, but not moving in resim
|
|
- moving along one axis and corrected on another
|
|
- resim while interpolation is still happening
|
|
*/
|
|
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
//only care about resim when async results are used
|
|
if (Solver->IsUsingAsyncResults() == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
struct FRewindCallback : public IRewindCallback
|
|
{
|
|
FRewindCallback(int32 InNumPhysicsSteps)
|
|
: NumPhysicsSteps(InNumPhysicsSteps)
|
|
{
|
|
|
|
}
|
|
|
|
int32 NumPhysicsSteps;
|
|
bool bResim = false;
|
|
bool bPendingCorrection = true;
|
|
const FReal ZCorrection = 100;
|
|
FSingleParticlePhysicsProxy* Proxy;
|
|
|
|
virtual int32 TriggerRewindIfNeeded_Internal(int32 LastCompletedStep) override
|
|
{
|
|
if (LastCompletedStep + 1 == NumPhysicsSteps && NumPhysicsSteps != INDEX_NONE)
|
|
{
|
|
NumPhysicsSteps = INDEX_NONE; //don't resim again after this
|
|
return 0;
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
virtual void ProcessInputs_Internal(int32 PhysicsStep, const TArray<FSimCallbackInputAndObject>& SimCallbackInputs) override
|
|
{
|
|
if (bResim && bPendingCorrection)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetX(FVec3(0, 0, ZCorrection));
|
|
bPendingCorrection = false;
|
|
}
|
|
}
|
|
|
|
virtual void PreResimStep_Internal(int32 PhysicsStep, bool bFirst) override
|
|
{
|
|
bResim = true;
|
|
}
|
|
|
|
virtual void PostResimStep_Internal(int32 PhysicsStep) override
|
|
{
|
|
bResim = false;
|
|
}
|
|
};
|
|
|
|
const int32 LastGameStep = 20;
|
|
const int32 NumPhysSteps = FMath::TruncToInt(LastGameStep / SimDt);
|
|
|
|
auto UniqueRewindCallback = MakeUnique<FRewindCallback>(NumPhysSteps);
|
|
auto RewindCallback = UniqueRewindCallback.Get();
|
|
RewindCallback->Proxy = Proxy;
|
|
const FReal GTDt = 1;
|
|
FReal Time = 0;
|
|
FReal ZStart = 0;
|
|
const FReal ZVel = -1;
|
|
FReal LastCorrectionStep = 0;
|
|
|
|
Solver->SetRewindCallback(MoveTemp(UniqueRewindCallback));
|
|
Solver->SetAsyncInterpolationMultiplier(4.0f);
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetX(FVec3(0, 0, 0));
|
|
Particle.SetV(FVec3(0, 0, -1));
|
|
|
|
for (int Step = 0; Step < LastGameStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
|
|
Time += GTDt;
|
|
const FReal InterpolatedTime = Time - SimDt * Solver->GetAsyncInterpolationMultiplier();
|
|
if (InterpolatedTime < 0)
|
|
{
|
|
//not enough time to interpolate so just take initial value
|
|
EXPECT_NEAR(Particle.X()[2], ZStart, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
//interpolated
|
|
EXPECT_NEAR(Particle.X()[2], ZStart + ZVel * InterpolatedTime, 1e-2);
|
|
}
|
|
}
|
|
|
|
//resim happened
|
|
Solver->SetAsyncInterpolationMultiplier(2.0f);
|
|
for (int Step = LastGameStep; Step < 2*LastGameStep; ++Step)
|
|
{
|
|
const FReal PrevZ = Particle.X()[2];
|
|
TickSolverHelper(Solver);
|
|
|
|
Time += GTDt;
|
|
const FReal InterpolatedTime = Time - SimDt * Solver->GetAsyncInterpolationMultiplier();
|
|
|
|
if(InterpolatedTime > 20) //resim happened
|
|
{
|
|
ZStart = 100; //corrected to 100 start point
|
|
}
|
|
|
|
//expected interpolation from pt to gt
|
|
const int32 NextSimStep = FMath::CeilToInt(InterpolatedTime / SimDt);
|
|
const FReal NextSimStepTime = NextSimStep * SimDt;
|
|
const FReal ExpectedValue = ZStart + ZVel * InterpolatedTime;
|
|
// const FReal TargetValue = ZStart + ZVel * NextSimStepTime;
|
|
|
|
FProxyInterpolationBase* InterpolationData = Proxy->GetInterpolationData();
|
|
if (InterpolationData && InterpolationData->IsErrorSmoothing())
|
|
{
|
|
if (!InterpolationData->IsErrorVelocitySmoothing())
|
|
{
|
|
const FReal CorrectionStep = Particle.X()[2] - PrevZ;
|
|
if (LastCorrectionStep != 0)
|
|
{
|
|
// Make sure we have a linear correction
|
|
EXPECT_NEAR(LastCorrectionStep, CorrectionStep, 1e-2);
|
|
}
|
|
|
|
LastCorrectionStep = CorrectionStep;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!!LastCorrectionStep)
|
|
{
|
|
// Correction is now done, check if the object arrived at the current position by the correction or if it was snapped into place via not doing correction anymore
|
|
EXPECT_NEAR(PrevZ, Particle.X()[2] - LastCorrectionStep, 1e-2);
|
|
LastCorrectionStep = 0;
|
|
}
|
|
|
|
//no resim interpolation, just simple value interpolation
|
|
EXPECT_NEAR(Particle.X()[2], ExpectedValue, 1e-2);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimFallingObjectWithTeleport)
|
|
{
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
const int32 LastGameStep = 20;
|
|
FReal ExpectedVZ = 0;
|
|
FReal ExpectedXZ = 100;
|
|
int32 FirstStepResim = INT_MAX;
|
|
int32 NumResimSteps = 0;
|
|
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, FMath::TruncToInt(LastGameStep / SimDt) );
|
|
Helper->ProcessInputsFunc = [Proxy, SimDt, &ExpectedVZ, &ExpectedXZ, &FirstStepResim, &NumResimSteps](const int32 Step, const bool bResim)
|
|
{
|
|
if(bResim)
|
|
{
|
|
FirstStepResim = FMath::Min(Step, FirstStepResim);
|
|
NumResimSteps++;
|
|
auto& Particle = *Proxy->GetPhysicsThreadAPI();
|
|
const float SimStart = SimDt * Step;
|
|
const float SimEnd = SimDt * (Step + 1);
|
|
if (SimStart <= 5 && SimEnd > 5)
|
|
{
|
|
ExpectedVZ = 0;
|
|
ExpectedXZ = 10;
|
|
Particle.SetX(FVec3(0, 0, 10));
|
|
Particle.SetV(FVec3(0, 0, 0));
|
|
}
|
|
|
|
EXPECT_NEAR(Particle.X()[2], ExpectedXZ, 1e-4);
|
|
EXPECT_NEAR(Particle.V()[2], ExpectedVZ, 1e-4);
|
|
}
|
|
|
|
};
|
|
|
|
Helper->PostFunc = [Proxy, SimDt, &ExpectedVZ, &ExpectedXZ](const int32 Step)
|
|
{
|
|
auto& Particle = *Proxy->GetPhysicsThreadAPI();
|
|
ExpectedVZ -= SimDt;
|
|
ExpectedXZ += ExpectedVZ * SimDt;
|
|
|
|
EXPECT_NEAR(Particle.X()[2], ExpectedXZ, 1e-4);
|
|
EXPECT_NEAR(Particle.V()[2], ExpectedVZ, 1e-4);
|
|
};
|
|
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
Particle.SetGravityEnabled(true);
|
|
Particle.SetX(FVec3(0, 0, 100));
|
|
|
|
for (int Step = 0; Step < LastGameStep; ++Step)
|
|
{
|
|
//teleport from GT
|
|
if (Step == 5)
|
|
{
|
|
Particle.SetX(FVec3(0, 0, 10));
|
|
Particle.SetV(FVec3(0, 0, 0));
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
}
|
|
|
|
EXPECT_EQ(FirstStepResim, 0);
|
|
EXPECT_EQ(NumResimSteps, LastGameStep / SimDt);
|
|
|
|
//no desync so should be empty
|
|
#if REWIND_DESYNC
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 0);
|
|
#endif
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimFallingObjectWithTeleportAsFollower)
|
|
{
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
const int32 LastGameStep = 20;
|
|
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
Particle.SetGravityEnabled(true);
|
|
Particle.SetX(FVec3(0, 0, 100));
|
|
Particle.SetResimType(EResimType::ResimAsFollower);
|
|
|
|
for (int Step = 0; Step <= LastGameStep; ++Step)
|
|
{
|
|
//teleport from GT
|
|
if (Step == 5)
|
|
{
|
|
Particle.SetX(FVec3(0, 0, 10));
|
|
Particle.SetV(FVec3(0, 0, 0));
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
}
|
|
|
|
FPhysicsThreadContextScope Scope(true);
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//RewindData->RewindToFrame(0);
|
|
Solver->DisableAsyncMode(); //during resim we sim directly at fixed dt
|
|
|
|
auto& Particle = *Proxy->GetPhysicsThreadAPI();
|
|
|
|
const int32 LastSimStep = LastGameStep / SimDt;
|
|
FReal ExpectedVZ = 0;
|
|
FReal ExpectedXZ = 100;
|
|
|
|
for (int Step = 0; Step < LastSimStep - 1; ++Step)
|
|
{
|
|
const float SimStart = SimDt * Step;
|
|
const float SimEnd = SimDt * (Step + 1);
|
|
if (SimStart <= 5 && SimEnd > 5)
|
|
{
|
|
ExpectedVZ = 0;
|
|
ExpectedXZ = 10;
|
|
}
|
|
else
|
|
{
|
|
#if REWIND_DESYNC
|
|
//we'll see the teleport automatically because ResimAsFollower
|
|
//but it's done by solver so before tick teleport is not known
|
|
EXPECT_NEAR(Particle.X()[2], ExpectedXZ, 1e-4);
|
|
EXPECT_NEAR(Particle.GetV()[2], ExpectedVZ, 1e-4);
|
|
#endif
|
|
}
|
|
|
|
TickSolverHelper(Solver, SimDt);
|
|
|
|
ExpectedVZ -= SimDt;
|
|
ExpectedXZ += ExpectedVZ * SimDt;
|
|
|
|
#if REWIND_DESYNC
|
|
EXPECT_NEAR(Particle.X()[2], ExpectedXZ, 1e-4);
|
|
EXPECT_NEAR(Particle.GetV()[2], ExpectedVZ, 1e-4);
|
|
#endif
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//no desync so should be empty
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 0);
|
|
#endif
|
|
});
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_NumDirty)
|
|
{
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
//note: this 5 is just a suggestion, there could be more frames saved than that
|
|
Solver->EnableRewindCapture(5, !!Optimization);
|
|
|
|
// Make particles
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
|
|
Particle.SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
Particle.SetGravityEnabled(true);
|
|
|
|
for (int Step = 0; Step < 10; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
|
|
const FRewindData* RewindData = Solver->GetRewindData();
|
|
EXPECT_EQ(RewindData->GetNumDirtyParticles(), 1);
|
|
}
|
|
|
|
//stop movement
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0));
|
|
|
|
// Wait for sleep (active particles get added to the dirty list)
|
|
// NOTE: Sleep requires 20 frames of inactivity by default, plus the time for smoothed velocity to damp to zero
|
|
// (see FPBDIslandManager::SleepInactive)
|
|
for(int Step = 0; Step < 500; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
{
|
|
//enough frames with no changes so no longer dirty
|
|
const FRewindData* RewindData = Solver->GetRewindData();
|
|
EXPECT_EQ(RewindData->GetNumDirtyParticles(), 0);
|
|
}
|
|
|
|
{
|
|
//single change so back to being dirty
|
|
Particle.SetGravityEnabled(true);
|
|
TickSolverHelper(Solver);
|
|
|
|
const FRewindData* RewindData = Solver->GetRewindData();
|
|
EXPECT_EQ(RewindData->GetNumDirtyParticles(), 1);
|
|
}
|
|
|
|
// Throw out the proxy
|
|
Solver->UnregisterObject(Proxy);
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_Resim)
|
|
{
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
|
|
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(5, !!Optimization);
|
|
|
|
// Make particles
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
|
|
const int32 LastStep = 12;
|
|
TArray<FVec3> X;
|
|
|
|
const int RewindStep = 7;
|
|
FRewindCallbackTestHelper* Helper = RegisterCallbackHelper(Solver, LastStep, RewindStep);
|
|
Helper->ProcessInputsFunc = [Proxy, KinematicProxy](const int32 Step, const bool bResim)
|
|
{
|
|
if (bResim)
|
|
{
|
|
if(Step == 7)
|
|
{
|
|
Proxy->GetPhysicsThreadAPI()->SetX(FVec3(0, 0, 100));
|
|
KinematicProxy->GetPhysicsThreadAPI()->SetX(FVec3(2));
|
|
}
|
|
else if (Step == 8)
|
|
{
|
|
KinematicProxy->GetPhysicsThreadAPI()->SetX(FVec3(50));
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
|
|
Particle.SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
Particle.SetGravityEnabled(true);
|
|
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Kinematic.SetGeometry(Sphere);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
Kinematic.SetX(FVec3(2, 2, 2));
|
|
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
X.Add(Particle.X());
|
|
|
|
if (Step == 8)
|
|
{
|
|
Kinematic.SetX(FVec3(50, 50, 50));
|
|
}
|
|
|
|
if (Step == 10)
|
|
{
|
|
Kinematic.SetX(FVec3(60, 60, 60));
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
|
|
|
|
X[Step] = Particle.X();
|
|
TickSolverHelper(Solver);
|
|
|
|
auto PTParticle = Proxy->GetHandle_LowLevel()->CastToRigidParticle(); //using handle directly because outside sim callback scope and we have ensures for that
|
|
auto PTKinematic = KinematicProxy->GetHandle_LowLevel()->CastToKinematicParticle();
|
|
|
|
//see that particle has desynced
|
|
if (Step < LastStep)
|
|
{
|
|
|
|
//If we're still in the past make sure future has been marked as desync
|
|
FGeometryParticleState State(*Proxy->GetHandle_LowLevel());
|
|
EXPECT_EQ(EFutureQueryResult::Desync, RewindData->GetFutureStateAtFrame(State, Step));
|
|
|
|
EXPECT_EQ(PTParticle->SyncState(), ESyncState::HardDesync);
|
|
|
|
FGeometryParticleState KinState(*KinematicProxy->GetHandle_LowLevel());
|
|
const EFutureQueryResult KinFutureStatus = RewindData->GetFutureStateAtFrame(KinState, Step);
|
|
if (Step < 10)
|
|
{
|
|
EXPECT_EQ(KinFutureStatus, EFutureQueryResult::Ok);
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(KinFutureStatus, EFutureQueryResult::Desync);
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::HardDesync);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Last resim frame ran so everything is marked as in sync
|
|
EXPECT_EQ(PTParticle->SyncState(), ESyncState::InSync);
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync);
|
|
}
|
|
}
|
|
|
|
//expect both particles to be hard desynced
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 2);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[1].MostDesynced, ESyncState::HardDesync);
|
|
#endif
|
|
|
|
#if 0 //can't read from resim data in public API
|
|
EXPECT_EQ(Kinematic.X()[2], 50); //Rewound kinematic and only did one update, so use that first update
|
|
|
|
//Make sure we recorded the new data
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
const FGeometryParticleState State = RewindData->GetPastStateAtFrame(*Proxy->GetHandle_LowLevel(), Step);
|
|
EXPECT_EQ(State.X()[2], X[Step][2]);
|
|
|
|
const FGeometryParticleState KinState = RewindData->GetPastStateAtFrame(*KinematicProxy->GetHandle_LowLevel(), Step);
|
|
if (Step < 8)
|
|
{
|
|
EXPECT_EQ(KinState.X()[2], 2);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_EQ(KinState.X()[2], 50); //in resim we didn't do second move, so recorded data must be updated
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
// Throw out the proxy
|
|
Solver->UnregisterObject(Proxy);
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimDesyncAfterMissingTeleport)
|
|
{
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(7, !!Optimization);
|
|
|
|
// Make particles
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
const int LastStep = 11;
|
|
TArray<FVec3> X;
|
|
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
|
|
Particle.SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
Particle.SetGravityEnabled(true);
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
if (Step == 7)
|
|
{
|
|
Particle.SetX(FVec3(0, 0, 5));
|
|
}
|
|
|
|
if (Step == 9)
|
|
{
|
|
Particle.SetX(FVec3(0, 0, 1));
|
|
}
|
|
X.Add(Particle.X());
|
|
TickSolverHelper(Solver);
|
|
}
|
|
X.Add(Particle.X());
|
|
|
|
}
|
|
|
|
const int RewindStep = 5;
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
auto& Particle = *Proxy->GetPhysicsThreadAPI();
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
#if REWIND_DESYNC
|
|
|
|
FGeometryParticleState FutureState(*Proxy->GetHandle_LowLevel());
|
|
EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step + 1), Step < 10 ? EFutureQueryResult::Ok : EFutureQueryResult::Desync);
|
|
if (Step < 10)
|
|
{
|
|
EXPECT_EQ(X[Step + 1][2], FutureState.X()[2]);
|
|
}
|
|
#endif
|
|
|
|
if (Step == 7)
|
|
{
|
|
Particle.SetX(FVec3(0, 0, 5));
|
|
}
|
|
|
|
//skip step 9 SetX to trigger a desync
|
|
|
|
TickSolverHelper(Solver);
|
|
|
|
#if REWIND_DESYNC
|
|
//can't compare future with end of frame because we overwrite the result
|
|
if (Step != 6 && Step != 8 && Step < 9)
|
|
{
|
|
EXPECT_EQ(Particle.X()[2], FutureState.X()[2]);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//expected desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 1);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[0].Particle, Proxy->GetHandle_LowLevel());
|
|
#endif
|
|
|
|
// Throw out the proxy
|
|
Solver->UnregisterObject(Proxy);
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimDesyncAfterChangingMass)
|
|
{
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(7, !!Optimization);
|
|
|
|
FReal CurMass = 1.0;
|
|
int32 LastStep = 11;
|
|
|
|
// Make particles
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
{
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
|
|
Particle.SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
Particle.SetGravityEnabled(true);
|
|
|
|
Particle.SetM(CurMass);
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
if (Step == 7)
|
|
{
|
|
Particle.SetM(2);
|
|
}
|
|
|
|
if (Step == 9)
|
|
{
|
|
Particle.SetM(3);
|
|
}
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
}
|
|
|
|
const int RewindStep = 5;
|
|
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
auto& Particle = *Proxy->GetPhysicsThreadAPI();
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
#if REWIND_DESYNC
|
|
FGeometryParticleState FutureState(*Proxy->GetHandle_LowLevel());
|
|
EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step), Step < 10 ? EFutureQueryResult::Ok : EFutureQueryResult::Desync);
|
|
if (Step < 7)
|
|
{
|
|
EXPECT_EQ(1, FutureState.M());
|
|
}
|
|
#endif
|
|
|
|
if (Step == 7)
|
|
{
|
|
Particle.SetM(2);
|
|
}
|
|
|
|
//skip step 9 SetM to trigger a desync
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//expected desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 1);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[0].Particle, Proxy->GetHandle_LowLevel());
|
|
#endif
|
|
|
|
// Throw out the proxy
|
|
Solver->UnregisterObject(Proxy);
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_DesyncFromPT)
|
|
{
|
|
#if REWIND_DESYNC
|
|
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
//We want to detect when sim results change
|
|
//Detecting output of position and velocity is expensive and hard to track
|
|
//Instead we need to rely on fast forward mechanism, this is still in progress
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, -100), FVec3(100, 100, 0)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(7, !!Optimization);
|
|
|
|
// Make particles
|
|
|
|
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
const int32 LastStep = 11;
|
|
|
|
{
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Sphere);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
|
|
Dynamic.SetX(FVec3(0, 0, 17));
|
|
Dynamic.SetGravityEnabled(false);
|
|
Dynamic.SetV(FVec3(0, 0, -1));
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
|
|
Kinematic.SetX(FVec3(0, 0, 0));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() });
|
|
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 10);
|
|
EXPECT_LE(Dynamic.X()[2], 11);
|
|
}
|
|
|
|
const int RewindStep = 5;
|
|
auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI();
|
|
auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI();
|
|
FPhysicsThreadContextScope Scope(true);
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
Kinematic.SetX(FVec3(0, 0, -1));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
//at the end of frame 6 a desync occurs because velocity is no longer clamped (kinematic moved)
|
|
//because of this desync will happen for any step after 6
|
|
if (Step <= 6)
|
|
{
|
|
FGeometryParticleState FutureState(*DynamicProxy->GetHandle_LowLevel());
|
|
EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step), EFutureQueryResult::Ok);
|
|
}
|
|
else if (Step >= 8)
|
|
{
|
|
//collision would have happened at frame 7, so anything after will desync. We skip a few frames because solver is fuzzy at that point
|
|
//that is we can choose to solve velocity in a few ways. Main thing we want to know is that a desync eventually happened
|
|
FGeometryParticleState FutureState(*DynamicProxy->GetHandle_LowLevel());
|
|
EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step), EFutureQueryResult::Desync);
|
|
}
|
|
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
|
|
//both kinematic and simulated are desynced
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 2);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[1].MostDesynced, ESyncState::HardDesync);
|
|
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 9);
|
|
EXPECT_LE(Dynamic.X()[2], 10);
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_ResimDesyncFromChangeForce)
|
|
{
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(7, !!Optimization);
|
|
|
|
// Make particles
|
|
auto Proxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
int32 LastStep = 11;
|
|
|
|
{
|
|
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
|
|
Particle.SetGeometry(Sphere);
|
|
Solver->RegisterObject(Proxy);
|
|
Particle.SetGravityEnabled(false);
|
|
Particle.SetV(FVec3(0, 0, 10));
|
|
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
if (Step == 7)
|
|
{
|
|
Particle.AddForce(FVec3(0, 1, 0));
|
|
}
|
|
|
|
if (Step == 9)
|
|
{
|
|
Particle.AddForce(FVec3(100, 0, 0));
|
|
}
|
|
TickSolverHelper(Solver);
|
|
}
|
|
}
|
|
const int RewindStep = 5;
|
|
auto& Particle = *Proxy->GetPhysicsThreadAPI();
|
|
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
{
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
#if REWIND_DESYNC
|
|
FGeometryParticleState FutureState(*Proxy->GetHandle_LowLevel());
|
|
EXPECT_EQ(RewindData->GetFutureStateAtFrame(FutureState, Step), Step < 10 ? EFutureQueryResult::Ok : EFutureQueryResult::Desync);
|
|
#endif
|
|
|
|
if (Step == 7)
|
|
{
|
|
Particle.AddForce(FVec3(0, 1, 0));
|
|
}
|
|
|
|
//skip step 9 SetF to trigger a desync
|
|
|
|
TickSolverHelper(Solver);
|
|
}
|
|
EXPECT_EQ(Particle.V()[0], 0);
|
|
|
|
#if REWIND_DESYNC
|
|
//desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 1);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
#endif
|
|
}
|
|
|
|
//rewind to exactly step 7 to make sure force is not already applied for us
|
|
{
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(7));
|
|
EXPECT_EQ(Particle.Acceleration()[1], 0);
|
|
}
|
|
|
|
// Throw out the proxy
|
|
Solver->UnregisterObject(Proxy);
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimAsFollower)
|
|
{
|
|
#if REWIND_DESYNC
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, -100), FVec3(100, 100, 0)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(7, !!Optimization);
|
|
|
|
// Make particles
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
const int32 LastStep = 11;
|
|
TArray<FVec3> Xs;
|
|
|
|
{
|
|
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Sphere);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
|
|
Dynamic.SetX(FVec3(0, 0, 17));
|
|
Dynamic.SetGravityEnabled(false);
|
|
Dynamic.SetV(FVec3(0, 0, -1));
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
Dynamic.SetResimType(EResimType::ResimAsFollower);
|
|
|
|
Kinematic.SetX(FVec3(0, 0, 0));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(Dynamic.X());
|
|
}
|
|
|
|
|
|
EXPECT_GE(Dynamic.X()[2], 10);
|
|
EXPECT_LE(Dynamic.X()[2], 11);
|
|
}
|
|
|
|
const int RewindStep = 5;
|
|
|
|
FPhysicsThreadContextScope Scope(true);
|
|
auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI();
|
|
auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI();
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
//make avoid collision
|
|
Kinematic.SetX(FVec3(0, 0, 100000));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
//Resim but dynamic will take old path since it's marked as ResimAsFollower
|
|
TickSolverHelper(Solver);
|
|
|
|
EXPECT_VECTOR_FLOAT_EQ(Dynamic.X(), Xs[Step]);
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
// follower - so dynamic in sync, kinematic desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 1);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[0].Particle, KinematicProxy->GetHandle_LowLevel());
|
|
#endif
|
|
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 10);
|
|
EXPECT_LE(Dynamic.X()[2], 11);
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_FullResimFallSeeCollisionCorrection)
|
|
{
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, -100), FVec3(100, 100, 0)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(100, !!Optimization);
|
|
|
|
// Make particles
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
const int32 LastStep = 11;
|
|
|
|
TArray<FVec3> Xs;
|
|
{
|
|
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Sphere);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
|
|
Dynamic.SetX(FVec3(0, 0, 17));
|
|
Dynamic.SetGravityEnabled(false);
|
|
Dynamic.SetV(FVec3(0, 0, -1));
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
|
|
Kinematic.SetX(FVec3(0, 0, -1000));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(Dynamic.X());
|
|
}
|
|
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 5);
|
|
EXPECT_LE(Dynamic.X()[2], 6);
|
|
}
|
|
|
|
const int RewindStep = 0;
|
|
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI();
|
|
auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI();
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
//force collision
|
|
Kinematic.SetX(FVec3(0, 0, 0));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
//Resim sees collision since it's ResimAsFull
|
|
TickSolverHelper(Solver);
|
|
#if REWIND_DESYNC
|
|
EXPECT_GE(Dynamic.X()[2], 10);
|
|
#endif
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 10);
|
|
EXPECT_LE(Dynamic.X()[2], 11);
|
|
#endif
|
|
|
|
#if REWIND_DESYNC
|
|
//both desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 2);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[1].MostDesynced, ESyncState::HardDesync);
|
|
#endif
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_ResimAsFollowerFallIgnoreCollision)
|
|
{
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, -100), FVec3(100, 100, 0)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(100, !!Optimization);
|
|
|
|
// Make particles
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
const int32 LastStep = 11;
|
|
TArray<FVec3> Xs;
|
|
|
|
{
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Sphere);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
|
|
Dynamic.SetX(FVec3(0, 0, 17));
|
|
Dynamic.SetGravityEnabled(false);
|
|
Dynamic.SetV(FVec3(0, 0, -1));
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
Dynamic.SetResimType(EResimType::ResimAsFollower);
|
|
|
|
Kinematic.SetX(FVec3(0, 0, -1000));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(Dynamic.X());
|
|
}
|
|
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 5);
|
|
EXPECT_LE(Dynamic.X()[2], 6);
|
|
}
|
|
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI();
|
|
auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI();
|
|
const int RewindStep = 0;
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
//force collision
|
|
Kinematic.SetX(FVec3(0, 0, 0));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
//Resim ignores collision since it's ResimAsFollower
|
|
TickSolverHelper(Solver);
|
|
|
|
EXPECT_VECTOR_FLOAT_EQ(Dynamic.X(), Xs[Step]);
|
|
}
|
|
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 5);
|
|
EXPECT_LE(Dynamic.X()[2], 6);
|
|
|
|
#if REWIND_DESYNC
|
|
//dynamic follower so only kinematic desyncs
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 1);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[0].Particle, KinematicProxy->GetHandle_LowLevel());
|
|
#endif
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimAsFollowerWithForces)
|
|
{
|
|
#if REWIND_DESYNC
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-10, -10, -10), FVec3(10, 10, 10)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(7, !!Optimization);
|
|
|
|
// Make particles
|
|
auto FullSimProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto FollowerSimProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
const int32 LastStep = 11;
|
|
TArray<FVec3> Xs;
|
|
|
|
{
|
|
auto& FullSim = FullSimProxy->GetGameThreadAPI();
|
|
auto& FollowerSim = FollowerSimProxy->GetGameThreadAPI();
|
|
|
|
FullSim.SetGeometry(Box);
|
|
FullSim.SetGravityEnabled(false);
|
|
Solver->RegisterObject(FullSimProxy);
|
|
|
|
FollowerSim.SetGeometry(Box);
|
|
FollowerSim.SetGravityEnabled(false);
|
|
Solver->RegisterObject(FollowerSimProxy);
|
|
|
|
FullSim.SetX(FVec3(0, 0, 20));
|
|
FullSim.SetObjectState(EObjectStateType::Dynamic);
|
|
FullSim.SetM(1);
|
|
FullSim.SetInvM(1);
|
|
|
|
FollowerSim.SetX(FVec3(0, 0, 0));
|
|
FollowerSim.SetResimType(EResimType::ResimAsFollower);
|
|
FollowerSim.SetM(1);
|
|
FollowerSim.SetInvM(1);
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ FullSimProxy->GetParticle_LowLevel(),FollowerSimProxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
FollowerSim.SetLinearImpulse(FVec3(0, 0, 0.5));
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(FullSim.X());
|
|
}
|
|
}
|
|
|
|
FPhysicsThreadContextScope Scope(true);
|
|
const int RewindStep = 5;
|
|
auto& FullSim = *FullSimProxy->GetPhysicsThreadAPI();
|
|
auto& FollowerSim = *FollowerSimProxy->GetPhysicsThreadAPI();
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
//resim - follower sim should have its impulses automatically added thus moving FullSim in the exact same way
|
|
TickSolverHelper(Solver);
|
|
|
|
EXPECT_VECTOR_FLOAT_EQ(FullSim.X(), Xs[Step]);
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//follower so no desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 0);
|
|
#endif
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimAsFollowerWokenUp)
|
|
{
|
|
#if REWIND_DESYNC
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-10, -10, -10), FVec3(10, 10, 10)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(7, !!Optimization);
|
|
|
|
// Make particles
|
|
auto ImpulsedObjProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto HitObjProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
|
|
const int32 ApplyImpulseStep = 8;
|
|
const int32 LastStep = 11;
|
|
|
|
TArray<FVec3> Xs;
|
|
{
|
|
auto& ImpulsedObj = ImpulsedObjProxy->GetGameThreadAPI();
|
|
auto& HitObj = HitObjProxy->GetGameThreadAPI();
|
|
|
|
ImpulsedObj.SetGeometry(Box);
|
|
ImpulsedObj.SetGravityEnabled(false);
|
|
Solver->RegisterObject(ImpulsedObjProxy);
|
|
|
|
HitObj.SetGeometry(Box);
|
|
HitObj.SetGravityEnabled(false);
|
|
Solver->RegisterObject(HitObjProxy);
|
|
|
|
ImpulsedObj.SetX(FVec3(0, 0, 20));
|
|
ImpulsedObj.SetM(1);
|
|
ImpulsedObj.SetInvM(1);
|
|
ImpulsedObj.SetResimType(EResimType::ResimAsFollower);
|
|
ImpulsedObj.SetObjectState(EObjectStateType::Sleeping);
|
|
|
|
HitObj.SetX(FVec3(0, 0, 0));
|
|
HitObj.SetM(1);
|
|
HitObj.SetInvM(1);
|
|
HitObj.SetResimType(EResimType::ResimAsFollower);
|
|
HitObj.SetObjectState(EObjectStateType::Sleeping);
|
|
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ ImpulsedObjProxy->GetParticle_LowLevel(),HitObjProxy->GetParticle_LowLevel() });
|
|
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
if (ApplyImpulseStep == Step)
|
|
{
|
|
ImpulsedObj.SetLinearImpulse(FVec3(0, 0, -10));
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(HitObj.X());
|
|
}
|
|
}
|
|
|
|
auto& ImpulsedObj = *ImpulsedObjProxy->GetPhysicsThreadAPI();
|
|
auto& HitObj = *HitObjProxy->GetPhysicsThreadAPI();
|
|
|
|
FPhysicsThreadContextScope Scope(true);
|
|
const int RewindStep = 5;
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
|
|
EXPECT_VECTOR_FLOAT_EQ(HitObj.X(), Xs[Step]);
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//follower so no desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 0);
|
|
#endif
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_ResimAsFollowerWokenUpNoHistory)
|
|
{
|
|
#if REWIND_DESYNC
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-10, -10, -10), FVec3(10, 10, 10)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(7, !!Optimization);
|
|
|
|
// Make particles
|
|
auto ImpulsedObjProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto HitObjProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
|
|
const int32 ApplyImpulseStep = 97;
|
|
const int32 LastStep = 100;
|
|
|
|
TArray<FVec3> Xs;
|
|
{
|
|
auto& ImpulsedObj = ImpulsedObjProxy->GetGameThreadAPI();
|
|
auto& HitObj = HitObjProxy->GetGameThreadAPI();
|
|
|
|
ImpulsedObj.SetGeometry(Box);
|
|
ImpulsedObj.SetGravityEnabled(false);
|
|
Solver->RegisterObject(ImpulsedObjProxy);
|
|
|
|
HitObj.SetGeometry(Box);
|
|
HitObj.SetGravityEnabled(false);
|
|
Solver->RegisterObject(HitObjProxy);
|
|
|
|
ImpulsedObj.SetX(FVec3(0, 0, 20));
|
|
ImpulsedObj.SetM(1);
|
|
ImpulsedObj.SetInvM(1);
|
|
ImpulsedObj.SetObjectState(EObjectStateType::Sleeping);
|
|
|
|
HitObj.SetX(FVec3(0, 0, 0));
|
|
HitObj.SetM(1);
|
|
HitObj.SetInvM(1);
|
|
HitObj.SetResimType(EResimType::ResimAsFollower);
|
|
HitObj.SetObjectState(EObjectStateType::Sleeping);
|
|
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ ImpulsedObjProxy->GetParticle_LowLevel(),HitObjProxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(HitObj.X()); //not a full re-sim so we should end up with exact same result
|
|
}
|
|
}
|
|
|
|
const int RewindStep = 95;
|
|
FPhysicsThreadContextScope Scope(true);
|
|
auto& ImpulsedObj = *ImpulsedObjProxy->GetPhysicsThreadAPI();
|
|
auto& HitObj = *HitObjProxy->GetPhysicsThreadAPI();
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
//during resim apply correction impulse
|
|
if (ApplyImpulseStep == Step)
|
|
{
|
|
ImpulsedObj.SetLinearImpulse(FVec3(0, 0, -10));
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
|
|
//even though there's now a different collision in the sim, the final result of follower is the same as before
|
|
EXPECT_VECTOR_FLOAT_EQ(HitObj.X(), Xs[Step]);
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//only desync non-follower
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 1);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[0].Particle, ImpulsedObjProxy->GetHandle_LowLevel());
|
|
#endif
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_DesyncSimOutOfCollision)
|
|
{
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, -100), FVec3(100, 100, 0)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(100, !!Optimization);
|
|
|
|
// Make particles
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
|
|
const int32 LastStep = 11;
|
|
|
|
TArray<FVec3> Xs;
|
|
|
|
{
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Sphere);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
|
|
Dynamic.SetX(FVec3(0, 0, 17));
|
|
Dynamic.SetGravityEnabled(true);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
|
|
Kinematic.SetX(FVec3(0, 0, 0));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(Dynamic.X());
|
|
}
|
|
|
|
EXPECT_GE(Dynamic.X()[2], 10);
|
|
}
|
|
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI();
|
|
auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI();
|
|
|
|
const int RewindStep = 8;
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
//remove from collision, should wakeup entire island and force a soft desync
|
|
Kinematic.SetX(FVec3(0, 0, -10000));
|
|
|
|
auto PTDynamic = DynamicProxy->GetHandle_LowLevel()->CastToRigidParticle(); //using handle directly because outside sim callback scope and we have ensures for that
|
|
auto PTKinematic = KinematicProxy->GetHandle_LowLevel()->CastToKinematicParticle();
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
//physics sim desync will not be known until the next frame because we can only compare inputs (teleport overwrites result of end of frame for example)
|
|
if (Step > RewindStep + 1)
|
|
{
|
|
#if REWIND_DESYNC
|
|
EXPECT_EQ(PTDynamic->SyncState(), ESyncState::HardDesync);
|
|
#endif
|
|
}
|
|
|
|
TickSolverHelper(Solver);
|
|
EXPECT_LE(Dynamic.X()[2], 10 + KINDA_SMALL_NUMBER);
|
|
|
|
#if REWIND_DESYNC
|
|
//kinematic desync will be known at end of frame because the simulation doesn't write results (so we know right away it's a desync)
|
|
if (Step < LastStep)
|
|
{
|
|
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::HardDesync);
|
|
}
|
|
else
|
|
{
|
|
//everything in sync after last step
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync);
|
|
EXPECT_EQ(PTDynamic->SyncState(), ESyncState::InSync);
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//both desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 2);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, ESyncState::HardDesync);
|
|
EXPECT_EQ(DesyncedParticles[1].MostDesynced, ESyncState::HardDesync);
|
|
#endif
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_SoftDesyncFromSameIsland)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, -100), FVec3(100, 100, 0)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(100,true); //soft desync only exists when resim optimization is on
|
|
|
|
// Make particles
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
|
|
const int32 LastStep = 11;
|
|
|
|
TArray<FVec3> Xs;
|
|
{
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Sphere);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
|
|
Dynamic.SetX(FVec3(0, 0, 37));
|
|
Dynamic.SetGravityEnabled(true);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
|
|
Kinematic.SetX(FVec3(0, 0, 0));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() });
|
|
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(Dynamic.X());
|
|
}
|
|
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 10);
|
|
EXPECT_LE(Dynamic.X()[2], 12);
|
|
}
|
|
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI();
|
|
auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI();
|
|
|
|
const int RewindStep = 0;
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
//mark kinematic as desynced (this should give us identical results which will trigger all particles in island to be soft desync)
|
|
|
|
auto PTDynamic = DynamicProxy->GetHandle_LowLevel()->CastToRigidParticle(); //using handle directly because outside sim callback scope and we have ensures for that
|
|
auto PTKinematic = KinematicProxy->GetHandle_LowLevel()->CastToKinematicParticle();
|
|
PTKinematic->SetSyncState(ESyncState::HardDesync);
|
|
bool bEverSoft = false;
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
|
|
#if REWIND_DESYNC
|
|
//kinematic desync will be known at end of frame because the simulation doesn't write results (so we know right away it's a desync)
|
|
if (Step < LastStep)
|
|
{
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::HardDesync);
|
|
|
|
//islands merge and split depending on internal solve
|
|
//but we should see dynamic being soft desync at least once when islands merge
|
|
if (PTDynamic->SyncState() == ESyncState::SoftDesync)
|
|
{
|
|
bEverSoft = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//everything in sync after last step
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync);
|
|
EXPECT_EQ(PTDynamic->SyncState(), ESyncState::InSync);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//kinematic hard desync, dynamic only soft desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 2);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, DesyncedParticles[0].Particle == KinematicProxy->GetHandle_LowLevel() ? ESyncState::HardDesync : ESyncState::SoftDesync);
|
|
EXPECT_EQ(DesyncedParticles[1].MostDesynced, DesyncedParticles[1].Particle == KinematicProxy->GetHandle_LowLevel() ? ESyncState::HardDesync : ESyncState::SoftDesync);
|
|
|
|
EXPECT_TRUE(bEverSoft);
|
|
|
|
// We may end up a bit away from the surface (dt * V), due to solving for 0 velocity and not 0 position error
|
|
EXPECT_GE(Dynamic.X()[2], 10);
|
|
EXPECT_LE(Dynamic.X()[2], 12);
|
|
#endif
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_SoftDesyncFromSameIslandThenBackToInSync)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-100, -100, -10), FVec3(100, 100, 0)));
|
|
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
|
|
Solver->EnableRewindCapture(100,true); //soft desync only exists when resim optimization is on
|
|
|
|
// Make particles
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
|
|
const int32 LastStep = 15;
|
|
|
|
TArray<FVec3> Xs;
|
|
|
|
{
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Sphere);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
|
|
Dynamic.SetX(FVec3(1000, 0, 37));
|
|
Dynamic.SetGravityEnabled(true);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
|
|
Kinematic.SetX(FVec3(0, 0, 0));
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),KinematicProxy->GetParticle_LowLevel() });
|
|
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
Xs.Add(Dynamic.X());
|
|
}
|
|
}
|
|
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
auto& Dynamic = *DynamicProxy->GetPhysicsThreadAPI();
|
|
auto& Kinematic = *KinematicProxy->GetPhysicsThreadAPI();
|
|
|
|
const int RewindStep = 0;
|
|
|
|
FRewindData* RewindData = Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
//move kinematic very close but do not alter dynamic
|
|
//should be soft desync while in island and then get back to in sync
|
|
|
|
auto PTDynamic = DynamicProxy->GetHandle_LowLevel()->CastToRigidParticle(); //using handle directly because outside sim callback scope and we have ensures for that
|
|
auto PTKinematic = KinematicProxy->GetHandle_LowLevel()->CastToKinematicParticle();
|
|
Kinematic.SetX(FVec3(1000 - 110, 0, 0));
|
|
|
|
bool bEverSoft = false;
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
|
|
#if REWIND_DESYNC
|
|
//kinematic desync will be known at end of frame because the simulation doesn't write results (so we know right away it's a desync)
|
|
if (Step < LastStep)
|
|
{
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::HardDesync);
|
|
|
|
//islands merge and split depending on internal solve
|
|
//but we should see dynamic being soft desync at least once when islands merge
|
|
if (PTDynamic->SyncState() == ESyncState::SoftDesync)
|
|
{
|
|
bEverSoft = true;
|
|
}
|
|
|
|
//by end should be in sync because islands should definitely be split at this point
|
|
if (Step == LastStep - 1)
|
|
{
|
|
EXPECT_EQ(PTDynamic->SyncState(), ESyncState::InSync);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
//everything in sync after last step
|
|
EXPECT_EQ(PTKinematic->SyncState(), ESyncState::InSync);
|
|
EXPECT_EQ(PTDynamic->SyncState(), ESyncState::InSync);
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
#if REWIND_DESYNC
|
|
//kinematic hard desync, dynamic only soft desync
|
|
const TArray<FDesyncedParticleInfo> DesyncedParticles = RewindData->ComputeDesyncInfo();
|
|
EXPECT_EQ(DesyncedParticles.Num(), 2);
|
|
EXPECT_EQ(DesyncedParticles[0].MostDesynced, DesyncedParticles[0].Particle == KinematicProxy->GetHandle_LowLevel() ? ESyncState::HardDesync : ESyncState::SoftDesync);
|
|
EXPECT_EQ(DesyncedParticles[1].MostDesynced, DesyncedParticles[1].Particle == KinematicProxy->GetHandle_LowLevel() ? ESyncState::HardDesync : ESyncState::SoftDesync);
|
|
|
|
//no collision so just kept falling
|
|
EXPECT_LT(Dynamic.X()[2], 10);
|
|
#endif
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DISABLED_RewindTest_SoftDesyncFromSameIslandThenBackToInSync_GeometryCollection_SingleFallingUnderGravity)
|
|
{
|
|
//TODO: disabled because at the moment GC particles are always marked as dirty - this messes up with transient dirty during rewind
|
|
//Should probably rethink why GC has its own dirty view
|
|
for (int Optimization = 0; Optimization < 2; ++Optimization)
|
|
{
|
|
FGeometryCollectionWrapper* Collection = TNewSimulationObject<GeometryType::GeometryCollectionWithSingleRigid>::Init()->As<FGeometryCollectionWrapper>();
|
|
|
|
FFramework UnitTest;
|
|
UnitTest.Solver->EnableRewindCapture(100, !!Optimization);
|
|
UnitTest.AddSimulationObject(Collection);
|
|
UnitTest.Initialize();
|
|
|
|
TArray<FReal> Xs;
|
|
const int32 LastStep = 10;
|
|
for (int Step = 0; Step <= LastStep; ++Step)
|
|
{
|
|
UnitTest.Advance();
|
|
Xs.Add(Collection->DynamicCollection->GetTransform(0).GetTranslation()[2]);
|
|
}
|
|
|
|
const int32 RewindStep = 3;
|
|
|
|
#if PHYSICS_THREAD_CONTEXT
|
|
FPhysicsThreadContextScope Scope(true);
|
|
#endif
|
|
FRewindData* RewindData = UnitTest.Solver->GetRewindData();
|
|
//EXPECT_TRUE(RewindData->RewindToFrame(RewindStep));
|
|
|
|
//GC doesn't marshal data from GT to PT so at the moment all we get is the GT data immediately after rewind, but it doesn't make it over to PT or collection
|
|
//Not sure if I can even access GT particle so can't verify that, but saw it in debugger at least
|
|
|
|
for (int Step = RewindStep; Step <= LastStep; ++Step)
|
|
{
|
|
UnitTest.Advance();
|
|
|
|
//TODO: turn this on when we find a way to marshal data from GT to PT
|
|
//EXPECT_EQ(Collection->DynamicCollection->Transform[0].GetTranslation()[2],Xs[Step]);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Helps compare multiple runs for determinism
|
|
//Also helps comparing runs across different compilers and delta times
|
|
class FSimComparisonHelper
|
|
{
|
|
public:
|
|
|
|
void SaveFrame(const TParticleView<TPBDRigidParticles<FReal, 3>>& NonDisabledDyanmic)
|
|
{
|
|
FEntry Frame;
|
|
Frame.X.Reserve(NonDisabledDyanmic.Num());
|
|
Frame.R.Reserve(NonDisabledDyanmic.Num());
|
|
|
|
for (const auto& Dynamic : NonDisabledDyanmic)
|
|
{
|
|
Frame.X.Add(Dynamic.GetX());
|
|
Frame.R.Add(Dynamic.GetR());
|
|
}
|
|
History.Add(MoveTemp(Frame));
|
|
}
|
|
|
|
static void ComputeMaxErrors(const FSimComparisonHelper& A, const FSimComparisonHelper& B, FReal& OutMaxLinearError,
|
|
FReal& OutMaxAngularError, int32 HistoryMultiple = 1, const TArray<int32>* BMapping = nullptr)
|
|
{
|
|
ensure(B.History.Num() == (A.History.Num() * HistoryMultiple));
|
|
|
|
FReal MaxLinearError2 = 0;
|
|
FReal MaxAngularError2 = 0;
|
|
|
|
for (int32 Idx = 0; Idx < A.History.Num(); ++Idx)
|
|
{
|
|
const int32 OtherIdx = Idx * HistoryMultiple + (HistoryMultiple - 1);
|
|
const FEntry& Entry = A.History[Idx];
|
|
const FEntry& OtherEntry = B.History[OtherIdx];
|
|
|
|
FReal MaxLinearError, MaxAngularError;
|
|
FEntry::CompareEntry(Entry, OtherEntry, MaxLinearError, MaxAngularError, BMapping);
|
|
|
|
MaxLinearError2 = FMath::Max(MaxLinearError2, MaxLinearError * MaxLinearError);
|
|
MaxAngularError2 = FMath::Max(MaxAngularError2, MaxAngularError * MaxAngularError);
|
|
}
|
|
|
|
OutMaxLinearError = FMath::Sqrt(MaxLinearError2);
|
|
OutMaxAngularError = FMath::Sqrt(MaxAngularError2);
|
|
}
|
|
|
|
private:
|
|
struct FEntry
|
|
{
|
|
TArray<FVec3> X;
|
|
TArray<FRotation3> R;
|
|
|
|
static void CompareEntry(const FEntry& A, const FEntry& B, FReal& OutMaxLinearError, FReal& OutMaxAngularError, const TArray<int32>* BMapping = nullptr)
|
|
{
|
|
FReal MaxLinearError2 = 0;
|
|
FReal MaxAngularError2 = 0;
|
|
|
|
auto BMappingHelper = [BMapping](const int32 Idx)
|
|
{
|
|
return BMapping ? (*BMapping)[Idx] : Idx;
|
|
};
|
|
|
|
check(A.X.Num() == A.R.Num());
|
|
check(A.X.Num() == B.X.Num());
|
|
for (int32 Idx = 0; Idx < A.X.Num(); ++Idx)
|
|
{
|
|
const FReal LinearError2 = (A.X[Idx] - B.X[BMappingHelper(Idx)]).SizeSquared();
|
|
MaxLinearError2 = FMath::Max(LinearError2, MaxLinearError2);
|
|
|
|
//if exactly the same we want 0 for testing purposes, inverse does not get that so just skip it
|
|
if (B.R[BMappingHelper(Idx)] != A.R[Idx])
|
|
{
|
|
//For angular error we look at the rotation needed to go from B to A
|
|
const FRotation3 Delta = B.R[BMappingHelper(Idx)] * A.R[Idx].Inverse();
|
|
|
|
FVec3 Axis;
|
|
FReal Angle;
|
|
Delta.ToAxisAndAngleSafe(Axis, Angle, FVec3(0, 0, 1));
|
|
const FReal Angle2 = Angle * Angle;
|
|
MaxAngularError2 = FMath::Max(Angle2, MaxAngularError2);
|
|
}
|
|
}
|
|
|
|
OutMaxLinearError = FMath::Sqrt(MaxLinearError2);
|
|
OutMaxAngularError = FMath::Sqrt(MaxAngularError2);
|
|
}
|
|
};
|
|
|
|
TArray<FEntry> History;
|
|
};
|
|
|
|
template <typename InitLambda>
|
|
void RunHelper(FSimComparisonHelper& SimComparison, int32 NumSteps, FReal Dt, const InitLambda& InitFunc, const TArray<int32>* Mapping = nullptr)
|
|
{
|
|
FChaosSolversModule* Module = FChaosSolversModule::GetModule();
|
|
|
|
// Make a solver
|
|
auto* Solver = Module->CreateSolver(nullptr, /*AsyncDt=*/-1);
|
|
InitSolverSettings(Solver);
|
|
Solver->SetIsDeterministic(true);
|
|
|
|
TArray<FPhysicsActorHandle> Storage = InitFunc(Solver, Mapping);
|
|
|
|
for (int32 Step = 0; Step < NumSteps; ++Step)
|
|
{
|
|
TickSolverHelper(Solver, Dt);
|
|
SimComparison.SaveFrame(Solver->GetParticles().GetNonDisabledDynamicView());
|
|
}
|
|
|
|
Module->DestroySolver(Solver);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DeterministicSim_SimpleFallingBox)
|
|
{
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-10, -10, -10), FVec3(10, 10, 10)));
|
|
|
|
const auto InitLambda = [&Box](auto& Solver, auto)
|
|
{
|
|
TArray<FPhysicsActorHandle> Storage;
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Box);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
|
|
Storage.Add(DynamicProxy);
|
|
return Storage;
|
|
};
|
|
|
|
FSimComparisonHelper FirstRun;
|
|
RunHelper(FirstRun, 100, 1 / 30.f, InitLambda);
|
|
|
|
FSimComparisonHelper SecondRun;
|
|
RunHelper(SecondRun, 100, 1 / 30.f, InitLambda);
|
|
|
|
FReal MaxLinearError, MaxAngularError;
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError);
|
|
EXPECT_EQ(MaxLinearError, 0);
|
|
EXPECT_EQ(MaxAngularError, 0);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DeterministicSim_ThresholdTest)
|
|
{
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-10, -10, -10), FVec3(10, 10, 10)));
|
|
|
|
FVec3 StartPos(0);
|
|
FRotation3 StartRotation = FRotation3::FromIdentity();
|
|
|
|
const auto InitLambda = [&Box, &StartPos, &StartRotation](auto& Solver, auto)
|
|
{
|
|
TArray<FPhysicsActorHandle> Storage;
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Box);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -1), 0);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
Dynamic.SetX(StartPos);
|
|
Dynamic.SetR(StartRotation);
|
|
|
|
Storage.Add(DynamicProxy);
|
|
return Storage;
|
|
};
|
|
|
|
FSimComparisonHelper FirstRun;
|
|
RunHelper(FirstRun, 10, 1 / 30.f, InitLambda);
|
|
|
|
//move X within threshold
|
|
StartPos = FVec3(0, 0, 1);
|
|
|
|
FSimComparisonHelper SecondRun;
|
|
RunHelper(SecondRun, 10, 1 / 30.f, InitLambda);
|
|
|
|
FReal MaxLinearError, MaxAngularError;
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError);
|
|
EXPECT_EQ(MaxAngularError, 0);
|
|
EXPECT_LT(MaxLinearError, 1.01);
|
|
EXPECT_GT(MaxLinearError, 0.99);
|
|
|
|
//move R within threshold
|
|
StartPos = FVec3(0, 0, 0);
|
|
StartRotation = FRotation3::FromAxisAngle(FVec3(1, 1, 0).GetSafeNormal(), 1);
|
|
|
|
FSimComparisonHelper ThirdRun;
|
|
RunHelper(ThirdRun, 10, 1 / 30.f, InitLambda);
|
|
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, ThirdRun, MaxLinearError, MaxAngularError);
|
|
EXPECT_EQ(MaxLinearError, 0);
|
|
EXPECT_LT(MaxAngularError, 1.01);
|
|
EXPECT_GT(MaxAngularError, 0.99);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DeterministicSim_DoubleTick)
|
|
{
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-10, -10, -10), FVec3(10, 10, 10)));
|
|
|
|
const auto InitLambda = [&Box](auto& Solver, auto)
|
|
{
|
|
TArray<FPhysicsActorHandle> Storage;
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Box);
|
|
Dynamic.SetGravityEnabled(false);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
Dynamic.SetV(FVec3(1, 0, 0));
|
|
|
|
Storage.Add(DynamicProxy);
|
|
return Storage;
|
|
};
|
|
|
|
FSimComparisonHelper FirstRun;
|
|
RunHelper(FirstRun, 100, 1 / 30.f, InitLambda);
|
|
|
|
//tick twice as often
|
|
|
|
FSimComparisonHelper SecondRun;
|
|
RunHelper(SecondRun, 200, 1 / 60.f, InitLambda);
|
|
|
|
FReal MaxLinearError, MaxAngularError;
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 2);
|
|
EXPECT_NEAR(MaxLinearError, 0, 1e-4);
|
|
EXPECT_NEAR(MaxAngularError, 0, 1e-4);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DeterministicSim_DoubleTickGravity)
|
|
{
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-10, -10, -10), FVec3(10, 10, 10)));
|
|
const FReal Gravity = -980;
|
|
|
|
const auto InitLambda = [&Box, Gravity](auto& Solver, auto)
|
|
{
|
|
TArray<FPhysicsActorHandle> Storage;
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Box);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, Gravity), 0);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
|
|
Storage.Add(DynamicProxy);
|
|
return Storage;
|
|
};
|
|
|
|
const int32 NumSteps = 7;
|
|
FSimComparisonHelper FirstRun;
|
|
RunHelper(FirstRun, NumSteps, 1 / 30.f, InitLambda);
|
|
|
|
//tick twice as often
|
|
|
|
FSimComparisonHelper SecondRun;
|
|
RunHelper(SecondRun, NumSteps * 2, 1 / 60.f, InitLambda);
|
|
|
|
//expected integration gravity error
|
|
const auto EulerIntegrationHelper = [Gravity](int32 Steps, FReal Dt)
|
|
{
|
|
FReal Z = 0;
|
|
FReal V = 0;
|
|
for (int32 Step = 0; Step < Steps; ++Step)
|
|
{
|
|
V += Gravity * Dt;
|
|
Z += V * Dt;
|
|
}
|
|
|
|
return Z;
|
|
};
|
|
|
|
const FReal ExpectedZ30 = EulerIntegrationHelper(NumSteps, 1 / 30.f);
|
|
const FReal ExpectedZ60 = EulerIntegrationHelper(NumSteps * 2, 1 / 60.f);
|
|
EXPECT_LT(ExpectedZ30, ExpectedZ60); //30 gains speed faster (we use the end velocity to integrate so the bigger dt, the more added energy)
|
|
const FReal ExpectedError = ExpectedZ60 - ExpectedZ30;
|
|
|
|
FReal MaxLinearError, MaxAngularError;
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 2);
|
|
EXPECT_LT(MaxLinearError, ExpectedError + 1e-4);
|
|
EXPECT_EQ(MaxAngularError, 0);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DeterministicSim_DoubleTickCollide)
|
|
{
|
|
auto Sphere = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 50));
|
|
|
|
const auto InitLambda = [&Sphere](auto& Solver, auto)
|
|
{
|
|
TArray<FPhysicsActorHandle> Storage;
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(Sphere);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
Dynamic.SetGravityEnabled(false);
|
|
Dynamic.SetV(FVec3(0, 0, -25));
|
|
|
|
|
|
auto DynamicProxy2 = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Dynamic2 = DynamicProxy2->GetGameThreadAPI();
|
|
|
|
Dynamic2.SetGeometry(Sphere);
|
|
Solver->RegisterObject(DynamicProxy2);
|
|
Dynamic2.SetX(FVec3(0, 0, -100 - 25 / 60.f - 0.1)); //make it so it overlaps for 30fps but not 60
|
|
Dynamic2.SetGravityEnabled(false);
|
|
|
|
ChaosTest::SetParticleSimDataToCollide({ DynamicProxy->GetParticle_LowLevel(),DynamicProxy2->GetParticle_LowLevel() });
|
|
|
|
Storage.Add(DynamicProxy);
|
|
Storage.Add(DynamicProxy2);
|
|
|
|
return Storage;
|
|
};
|
|
|
|
const int32 NumSteps = 7;
|
|
FSimComparisonHelper FirstRun;
|
|
RunHelper(FirstRun, NumSteps, 1 / 30.f, InitLambda);
|
|
|
|
//tick twice as often
|
|
|
|
FSimComparisonHelper SecondRun;
|
|
RunHelper(SecondRun, NumSteps * 2, 1 / 60.f, InitLambda);
|
|
|
|
FReal MaxLinearError, MaxAngularError;
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 2);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DeterministicSim_DoubleTickStackCollide)
|
|
{
|
|
auto SmallBox = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-50, -50, -50), FVec3(50, 50, 50)));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-1000, -1000, -1000), FVec3(1000, 1000, 0)));
|
|
|
|
const auto InitLambda = [&SmallBox, &Box](auto& Solver, auto)
|
|
{
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -980), 0);
|
|
TArray<FPhysicsActorHandle> Storage;
|
|
for (int Idx = 0; Idx < 5; ++Idx)
|
|
{
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(SmallBox);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Dynamic.SetX(FVec3(0, 20 * Idx, 100 * Idx)); //slightly offset
|
|
|
|
Storage.Add(DynamicProxy);
|
|
}
|
|
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
Kinematic.SetX(FVec3(0, 0, -50));
|
|
|
|
Storage.Add(KinematicProxy);
|
|
|
|
for (int i = 0; i < Storage.Num(); ++i)
|
|
{
|
|
for (int j = i + 1; j < Storage.Num(); ++j)
|
|
{
|
|
ChaosTest::SetParticleSimDataToCollide({ Storage[i]->GetParticle_LowLevel(),Storage[j]->GetParticle_LowLevel() });
|
|
}
|
|
}
|
|
|
|
return Storage;
|
|
};
|
|
|
|
const int32 NumSteps = 20;
|
|
FSimComparisonHelper FirstRun;
|
|
RunHelper(FirstRun, NumSteps, 1 / 30.f, InitLambda);
|
|
|
|
//tick twice as often
|
|
|
|
FSimComparisonHelper SecondRun;
|
|
RunHelper(SecondRun, NumSteps, 1 / 30.f, InitLambda);
|
|
|
|
//make sure deterministic
|
|
FReal MaxLinearError, MaxAngularError;
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 1);
|
|
EXPECT_EQ(MaxLinearError, 0);
|
|
EXPECT_EQ(MaxAngularError, 0);
|
|
|
|
//try with 60fps
|
|
FSimComparisonHelper ThirdRun;
|
|
RunHelper(ThirdRun, NumSteps * 2, 1 / 60.f, InitLambda);
|
|
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, ThirdRun, MaxLinearError, MaxAngularError, 2);
|
|
}
|
|
|
|
GTEST_TEST(AllTraits, DeterministicSim_DifferentCreationOrder)
|
|
{
|
|
auto SmallBox = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-50, -50, -50), FVec3(50, 50, 50)));
|
|
auto Box = Chaos::FImplicitObjectPtr(new TBox<FReal, 3>(FVec3(-1000, -1000, -1000), FVec3(1000, 1000, 0)));
|
|
|
|
const int32 NumParticles = 50;
|
|
const auto InitLambda = [&SmallBox, &Box, NumParticles](auto& Solver, const TArray<int32>* Mapping)
|
|
{
|
|
auto MappingHelper = [Mapping](const int32 Idx) { return Mapping ? (*Mapping)[Idx] : Idx; };
|
|
Solver->GetEvolution()->GetGravityForces().SetAcceleration(FVec3(0, 0, -980), 0);
|
|
TArray<FPhysicsActorHandle> Storage;
|
|
for (int Idx = 0; Idx < NumParticles; ++Idx)
|
|
{
|
|
auto DynamicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Dynamic = DynamicProxy->GetGameThreadAPI();
|
|
|
|
Dynamic.SetGeometry(SmallBox);
|
|
Solver->RegisterObject(DynamicProxy);
|
|
Dynamic.SetObjectState(EObjectStateType::Dynamic);
|
|
Dynamic.SetGravityEnabled(true);
|
|
Dynamic.SetParticleID(FParticleID{ MappingHelper(Idx), INDEX_NONE });
|
|
Dynamic.SetX(FVec3(0, 5 * MappingHelper(Idx), 100 * MappingHelper(Idx) + 50)); //slightly offset
|
|
Dynamic.SetI(FVec3(1000, 1000, 1000));
|
|
Dynamic.SetInvI(FVec3(1/1000.0, 1/1000.0, 1/1000.0));
|
|
|
|
Storage.Add(DynamicProxy);
|
|
}
|
|
|
|
auto KinematicProxy = FSingleParticlePhysicsProxy::Create(Chaos::FKinematicGeometryParticle::CreateParticle());
|
|
auto& Kinematic = KinematicProxy->GetGameThreadAPI();
|
|
|
|
Kinematic.SetGeometry(Box);
|
|
Solver->RegisterObject(KinematicProxy);
|
|
Kinematic.SetX(FVec3(0, 0, 0));
|
|
|
|
Storage.Add(KinematicProxy);
|
|
|
|
for (int i = 0; i < Storage.Num(); ++i)
|
|
{
|
|
for (int j = i + 1; j < Storage.Num(); ++j)
|
|
{
|
|
ChaosTest::SetParticleSimDataToCollide({ Storage[i]->GetParticle_LowLevel(),Storage[j]->GetParticle_LowLevel() });
|
|
}
|
|
}
|
|
|
|
return Storage;
|
|
};
|
|
|
|
const int32 NumSteps = 20;
|
|
FSimComparisonHelper FirstRun;
|
|
|
|
RunHelper(FirstRun, NumSteps, 1 / 30.f, InitLambda);
|
|
|
|
//tick twice as often
|
|
|
|
TArray<int32> Mapping;
|
|
for (int32 Idx = 0; Idx < NumParticles; ++Idx)
|
|
{
|
|
Mapping.Add(NumParticles - Idx - 1);
|
|
//Mapping.Add(Idx);
|
|
}
|
|
|
|
FSimComparisonHelper SecondRun;
|
|
RunHelper(SecondRun, NumSteps, 1 / 30.f, InitLambda, &Mapping);
|
|
|
|
//make sure deterministic
|
|
FReal MaxLinearError, MaxAngularError;
|
|
FSimComparisonHelper::ComputeMaxErrors(FirstRun, SecondRun, MaxLinearError, MaxAngularError, 1, &Mapping);
|
|
EXPECT_EQ(MaxLinearError, 0);
|
|
EXPECT_EQ(MaxAngularError, 0);
|
|
}
|
|
|
|
|
|
GTEST_TEST(AllTraits, RewindTest_InterpolatedTwoChannels)
|
|
{
|
|
Chaos::AsyncInterpolationMultiplier = 3.0f;
|
|
int32 PrevNumActiveChannels = Chaos::DefaultNumActiveChannels;
|
|
Chaos::DefaultNumActiveChannels = 2;
|
|
//Have two moving particles, one in each channel to see that there's a delay in time on second channel
|
|
TRewindHelper::TestDynamicSphere([](auto* Solver, FReal SimDt, int32 Optimization, auto Proxy, auto Sphere)
|
|
{
|
|
if (!Solver->IsUsingAsyncResults()) { return; }
|
|
auto& Particle = Proxy->GetGameThreadAPI();
|
|
Particle.SetV(FVec3(0, 0, 1));
|
|
Particle.SetGravityEnabled(false);
|
|
|
|
auto Proxy2 = FSingleParticlePhysicsProxy::Create(Chaos::FPBDRigidParticle::CreateParticle());
|
|
auto& Particle2 = Proxy2->GetGameThreadAPI();
|
|
|
|
auto Sphere2 = Chaos::FImplicitObjectPtr(new Chaos::FSphere(FVec3(0), 10));
|
|
Particle2.SetGeometry(Sphere2);
|
|
Particle2.SetV(FVec3(0, 0, 1));
|
|
Particle2.SetGravityEnabled(false);
|
|
if (FProxyInterpolationBase* InterpolationData = Proxy2->GetInterpolationData())
|
|
{
|
|
InterpolationData->SetInterpChannel_External(1);
|
|
}
|
|
Solver->RegisterObject(Proxy2);
|
|
|
|
FReal Time = 0;
|
|
const FReal GtDt = 1;
|
|
for (int Step = 0; Step < 32; ++Step)
|
|
{
|
|
TickSolverHelper(Solver);
|
|
|
|
Time += GtDt;
|
|
const FReal InterpolatedTime0 = Time - SimDt * Solver->GetAsyncInterpolationMultiplier();
|
|
const FReal InterpolatedTime1 = InterpolatedTime0 - Chaos::SecondChannelDelay;
|
|
|
|
if (InterpolatedTime0 < 0)
|
|
{
|
|
//No movement yet
|
|
EXPECT_NEAR(Particle.X()[2], 0, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_NEAR(Particle.X()[2], InterpolatedTime0, 1e-2);
|
|
}
|
|
|
|
if (InterpolatedTime1 < 0)
|
|
{
|
|
//No movement yet
|
|
EXPECT_NEAR(Particle2.X()[2], 0, 1e-2);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_NEAR(Particle2.X()[2], InterpolatedTime1, 1e-2);
|
|
}
|
|
}
|
|
});
|
|
|
|
Chaos::DefaultNumActiveChannels = PrevNumActiveChannels;
|
|
}
|
|
|
|
|
|
}
|