// Copyright Epic Games, Inc. All Rights Reserved. #include "PhysicsProxy/SingleParticlePhysicsProxy.h" #include "ChaosStats.h" #include "Chaos/Box.h" #include "Chaos/Capsule.h" #include "Chaos/Sphere.h" #include "Chaos/ErrorReporter.h" #include "Chaos/Serializable.h" #include "Chaos/PerParticleGravity.h" #include "Chaos/ParticleHandle.h" #include "Chaos/PhysicsObjectInternal.h" #include "Chaos/Framework/MultiBufferResource.h" #include "PhysicsSolver.h" #include "Chaos/ChaosMarshallingManager.h" #include "Chaos/PullPhysicsDataImp.h" #include "RewindData.h" #include "Chaos/DebugDrawQueue.h" namespace Chaos { // This allows forcing the game thread/actor to get or not get transform updates from the simulation result for // kinematics - noting that they may already have been set in the SetKinematicTransform function. // Velocity and such will still be copied in any case. The default value of -1 uses the UpdateKinematicFromSimulation // flag on the BodyInstance. CHAOS_API int32 SyncKinematicOnGameThread = -1; FAutoConsoleVariableRef CVar_SyncKinematicOnGameThread(TEXT("P.Chaos.SyncKinematicOnGameThread"), SyncKinematicOnGameThread, TEXT( "If set to 1, kinematic bodies will always send their transforms back to the game thread, following the " "simulation step/results. If 0, then they will never do so, and kinematics will be updated immediately " "their kinematic target is set. Any other value (e.g. the default -1) means that the behavior is " "determined on a per-object basis with the UpdateKinematicFromSimulation flag in BodyInstance.")); static bool bPullPhysicsStateForceUpdateBounds = false; static FAutoConsoleVariableRef CVar_PullPhysicsStateForceUpdateBounds( TEXT("P.Chaos.PullPhysicsStateForceUpdateBounds"), bPullPhysicsStateForceUpdateBounds, TEXT("If true, updates rigid particle bounds regardless of if X or R changed. The optimal setting is false.") ); FSingleParticlePhysicsProxy::FSingleParticlePhysicsProxy(TUniquePtr&& InParticle, FParticleHandle* InHandle, UObject* InOwner) : IPhysicsProxyBase(EPhysicsProxyType::SingleParticleProxy, InOwner, MakeShared()) , Particle(MoveTemp(InParticle)) , Handle(InHandle) { Particle->SetProxy(this); Reference = FPhysicsObjectFactory::CreatePhysicsObject(this); InterpolationData = MakeUnique(); } FSingleParticlePhysicsProxy::~FSingleParticlePhysicsProxy() { } CHAOS_API int32 ForceNoCollisionIntoSQ = 0; FAutoConsoleVariableRef CVarForceNoCollisionIntoSQ(TEXT("p.ForceNoCollisionIntoSQ"), ForceNoCollisionIntoSQ, TEXT("When enabled, all particles end up in sq structure, even ones with no collision")); template void PushToPhysicsStateImp(const Chaos::FDirtyPropertiesManager& Manager, Chaos::FGeometryParticleHandle* Handle, int32 DataIdx, const Chaos::FDirtyProxy& Dirty, Chaos::FShapeDirtyData* ShapesData, Chaos::FPBDRigidsSolver& Solver, bool bResimInitialized, Chaos::FReal ExternalDt) { using namespace Chaos; constexpr bool bHasKinematicData = ParticleType != EParticleType::Static; constexpr bool bHasDynamicData = ParticleType == EParticleType::Rigid; auto KinematicHandle = bHasKinematicData ? static_cast(Handle) : nullptr; auto RigidHandle = bHasDynamicData ? static_cast(Handle) : nullptr; const FDirtyChaosProperties& ParticleData = Dirty.PropertyData; FPBDRigidsEvolutionGBF& Evolution = *Solver.GetEvolution(); const FParticleNonFrequentData* NewNonFrequentData = ParticleData.FindNonFrequentData(Manager, DataIdx); #if UE_WITH_REMOTE_OBJECT_HANDLE if (FSerializedDataBufferPtr PendingStateData = Dirty.Proxy && !Dirty.Proxy->IsInitialized() ? Solver.GetSerializer().PopPendingInternalSerializedStateForProxy(Dirty.Proxy) : nullptr) { // Currently we don't serialize collision geometry and shape data, so we need to apply it first // This might change in the future if (NewNonFrequentData) { Handle->SetNonFrequentData(*NewNonFrequentData); } Solver.GetSerializer().ApplySerializedStateToParticle(Handle, *PendingStateData); } else #endif { if (bResimInitialized) //todo: assumes particles are always initialized as enabled. This is not true in future versions of code, so check PushData { Evolution.EnableParticle(Handle); } // move the copied game thread data into the handle { auto NewXR = ParticleData.FindXR(Manager, DataIdx); auto NewDynamicMiscData = ParticleData.FindDynamicMisc(Manager, DataIdx); if(NewXR) { // @todo(chaos): we need to know if this is a teleport or not and pass that on. See UE-165746 // For now we just set bIsTeleport to true since that's the no-impact option for SetParticleTransform // (there would be issues if we report a non-teleport move for an initial-position a long way from the origin) const bool bIsTeleport = true; bool bWakeUp = true; if (NewDynamicMiscData && NewDynamicMiscData->ObjectState() == EObjectStateType::Sleeping) { bWakeUp = false; } Evolution.SetParticleTransform(Handle, NewXR->X(), NewXR->R(), bIsTeleport, bWakeUp); } if(NewNonFrequentData) { // Geometry may have changed, we need to remove the particle and its collisions from the graph Evolution.InvalidateParticle(Handle); Handle->SetNonFrequentData(*NewNonFrequentData); } auto NewVelocities = bHasKinematicData ? ParticleData.FindVelocities(Manager, DataIdx) : nullptr; if(NewVelocities) { Evolution.SetParticleVelocities(KinematicHandle, NewVelocities->V(), NewVelocities->W()); } auto NewKinematicTargetGT = bHasKinematicData ? ParticleData.FindKinematicTarget(Manager, DataIdx) : nullptr; if (NewKinematicTargetGT) { Evolution.SetParticleKinematicTarget(KinematicHandle, *NewKinematicTargetGT); } if(NewXR || NewNonFrequentData || NewVelocities || NewKinematicTargetGT) { // Update world-space cached state like the bounds // @todo(chaos): do we need to do this here? It should be done in Integrate and ApplyKinematicTarget so only really Statics need this... const bool bHasKinematicTarget = (NewKinematicTargetGT != nullptr) && (NewKinematicTargetGT->GetMode() == EKinematicTargetMode::Position); const FRigidTransform3 WorldTransform = !bHasKinematicTarget ? FRigidTransform3(Handle->GetX(), Handle->GetR()) : NewKinematicTargetGT->GetTransform(); Handle->UpdateWorldSpaceState(WorldTransform, FVec3(0)); Evolution.DirtyParticle(*Handle); } if(bHasDynamicData) { if(auto NewData = ParticleData.FindMassProps(Manager,DataIdx)) { RigidHandle->SetMassProps(*NewData); } if(auto NewData = ParticleData.FindDynamics(Manager, DataIdx)) { RigidHandle->SetDynamics(*NewData); Evolution.ResetVSmoothFromForces(*RigidHandle); } if(NewDynamicMiscData ) { Solver.SetParticleDynamicMisc(RigidHandle, *NewDynamicMiscData); } } } } //shape properties bool bUpdateCollisionData = false; bool bHasCollision = false; bool bHasMaterial = false; for(int32 ShapeDataIdx : Dirty.ShapeDataIndices) { const FShapeDirtyData& ShapeData = ShapesData[ShapeDataIdx]; const int32 ShapeIdx = ShapeData.GetShapeIdx(); if(auto NewData = ShapeData.FindCollisionData(Manager, ShapeDataIdx)) { bUpdateCollisionData = true; Handle->ShapesArray()[ShapeIdx]->SetCollisionData(*NewData); const FCollisionData& CollisionData = Handle->ShapesArray()[ShapeIdx]->GetCollisionData(); bHasCollision |= CollisionData.HasCollisionData(); } if(auto NewData = ShapeData.FindMaterials(Manager, ShapeDataIdx)) { Handle->ShapesArray()[ShapeIdx]->SetMaterialData(*NewData); bHasMaterial = true; } } // If the material, geometry, shape data, or sleep properties changed, we need to notify any systems that cache material data if (bHasMaterial || bUpdateCollisionData || NewNonFrequentData || bHasDynamicData) { Evolution.ParticleMaterialChanged(Handle); } if(bUpdateCollisionData && !ForceNoCollisionIntoSQ) { //Some shapes were not dirty and may have collision - so have to iterate them all. TODO: find a better way to handle this case if(!bHasCollision && Dirty.ShapeDataIndices.Num() != Handle->ShapesArray().Num()) { for (const TUniquePtr& Shape : Handle->ShapesArray()) { const FCollisionData& CollisionData = Shape->GetCollisionData(); bHasCollision |= CollisionData.HasCollisionData(); if (bHasCollision) { break; } } } Handle->SetHasCollision(bHasCollision); if(bHasCollision) { // destroy collision constraints so that mid-phase is recreated with newly added shapes if any Evolution.DestroyTransientConstraints(Handle); //make sure it's in acceleration structure Evolution.DirtyParticle(*Handle); } else { Evolution.RemoveParticleFromAccelerationStructure(*Handle); } } } // // TGeometryParticle template specialization // void FSingleParticlePhysicsProxy::PushToPhysicsState(const Chaos::FDirtyPropertiesManager& Manager, int32 DataIdx, const Chaos::FDirtyProxy& Dirty, Chaos::FShapeDirtyData* ShapesData, Chaos::FReal ExternalDt) { using namespace Chaos; FPBDRigidsSolver& RigidsSolver = *static_cast(Solver); const int32 CurFrame = RigidsSolver.GetCurrentFrame(); const FRewindData* RewindData = RigidsSolver.GetRewindData(); const bool bResimInitialized = RewindData && RewindData->IsResim() && CurFrame == InitializedOnStep; switch(Dirty.PropertyData.GetParticleBufferType()) { case EParticleType::Static: PushToPhysicsStateImp(Manager, Handle, DataIdx, Dirty, ShapesData, RigidsSolver, bResimInitialized, ExternalDt); break; case EParticleType::Kinematic: PushToPhysicsStateImp(Manager, Handle, DataIdx, Dirty, ShapesData, RigidsSolver, bResimInitialized, ExternalDt); break; case EParticleType::Rigid: PushToPhysicsStateImp(Manager, Handle, DataIdx, Dirty, ShapesData, RigidsSolver, bResimInitialized, ExternalDt); break; default: check(false); //unexpected path } } void FSingleParticlePhysicsProxy::ClearAccumulatedData() { if(auto Rigid = Particle->CastToRigidParticle()) { Rigid->ClearForces(false); Rigid->ClearTorques(false); Rigid->SetLinearImpulseVelocity(Chaos::FVec3(0), false); Rigid->SetAngularImpulseVelocity(Chaos::FVec3(0), false); } Particle->ClearDirtyFlags(); } template void BufferPhysicsResultsImp(Chaos::FDirtyRigidParticleData& PullData, T* Particle) { PullData.X = Particle->GetX(); PullData.R = Particle->GetR(); PullData.V = Particle->GetV(); PullData.W = Particle->GetW(); PullData.ObjectState = Particle->ObjectState(); } void FSingleParticlePhysicsProxy::BufferPhysicsResults(Chaos::FDirtyRigidParticleData& PullData) { using namespace Chaos; // Move simulation results into the double buffer. FPBDRigidParticleHandle* RigidHandle = Handle ? Handle->CastToRigidParticle() : nullptr; //TODO: can handle be null? if(RigidHandle) { PullData.SetProxy(*this); BufferPhysicsResultsImp(PullData, RigidHandle); } } void FSingleParticlePhysicsProxy::BufferPhysicsResults_External(Chaos::FDirtyRigidParticleData& PullData) { if(auto Rigid = Particle->CastToRigidParticle()) { PullData.SetProxy(*this); BufferPhysicsResultsImp(PullData, Rigid); } } bool ShouldUpdateTransformFromSimulation(const Chaos::FPBDRigidParticle& Rigid) { if (Rigid.ObjectState() == Chaos::EObjectStateType::Kinematic) { switch (Chaos::SyncKinematicOnGameThread) { case 0: return false; case 1: return true; default: return Rigid.UpdateKinematicFromSimulation(); } } return true; } bool FSingleParticlePhysicsProxy::PullFromPhysicsState(const Chaos::FDirtyRigidParticleData& PullData,int32 SolverSyncTimestamp, const Chaos::FDirtyRigidParticleData* NextPullData, const Chaos::FRealSingle* Alpha, const FDirtyRigidParticleReplicationErrorData* Error, const Chaos::FReal AsyncFixedTimeStep) { using namespace Chaos; // Move buffered data into the TPBDRigidParticle without triggering invalidation of the physics state. Chaos::FPBDRigidParticle* Rigid = Particle ? Particle->CastToRigidParticle() : nullptr; if (Rigid) { bool bNeedUpdateShapeBounds = bPullPhysicsStateForceUpdateBounds; // Note that kinematics should either be updated here (following simulation), or when the // kinematic target is set in FChaosEngineInterface::SetKinematicTarget_AssumesLocked If the // logic in one place is changed, it should be checked in the other place too. bool bUpdatePositionFromSimulation = ShouldUpdateTransformFromSimulation(*Rigid); const FSingleParticleProxyTimestamp* ProxyTimestamp = PullData.GetTimestamp(); FProxyInterpolationBase* InterpData = GetInterpolationData(); if (Error) { if (RenderInterpolationCVars::bRenderInterpErrorVelocityCorrection) { InterpData = GetOrCreateErrorInterpolationData(); } else { InterpData = GetOrCreateErrorInterpolationData(); } check(InterpData); const FReal ErrorDistanceSqr = Error->ErrorX.SizeSquared(); FReal ErrorCorrectionDuration = RenderInterpolationCVars::RenderInterpErrorCorrectionDuration; FReal MaxErrorCorrection = RenderInterpolationCVars::RenderInterpMaximumErrorCorrectionBeforeSnapping; FReal MaxErrorDesyncTime = RenderInterpolationCVars::RenderInterpMaximumErrorCorrectionDesyncTimeBeforeSnapping; if (FErrorInterpolationSettings * ErrorSettings = InterpData->GetErrorInterpolationSettings()) { ErrorCorrectionDuration = ErrorSettings->ErrorCorrectionDuration; MaxErrorCorrection = ErrorSettings->MaximumErrorCorrectionBeforeSnapping; MaxErrorDesyncTime = ErrorSettings->MaximumErrorCorrectionDesyncTimeBeforeSnapping; } MaxErrorCorrection = MaxErrorCorrection * MaxErrorCorrection; // Square the value auto VelocitySnapLimitHelper = [ErrorDistanceSqr, MaxErrorDesyncTime](const FVec3& Velocity) -> const bool { if (MaxErrorDesyncTime <= 0.0) { return false; } return ErrorDistanceSqr < (Velocity * MaxErrorDesyncTime).SizeSquared(); }; // If error is within interpolation limit, set the number of physics frames to interpolate over, else leave at 0 frames to instantly correct the error. int32 RenderInterpErrorCorrectionDurationTicks = 0; if (ErrorDistanceSqr < MaxErrorCorrection || VelocitySnapLimitHelper(Rigid->V()) || VelocitySnapLimitHelper(PullData.V) || (NextPullData && VelocitySnapLimitHelper(NextPullData->V))) { RenderInterpErrorCorrectionDurationTicks = FMath::FloorToInt32(ErrorCorrectionDuration / AsyncFixedTimeStep); // Convert duration from seconds to simulation ticks } InterpData->AccumlateErrorXR(Error->ErrorX, Error->ErrorR, SolverSyncTimestamp, RenderInterpErrorCorrectionDurationTicks); const int32 RenderInterpErrorVelocitySmoothingDurationTicks = FMath::FloorToInt32(RenderInterpolationCVars::RenderInterpErrorVelocitySmoothingDuration / AsyncFixedTimeStep); // Convert duration from seconds to simulation ticks InterpData->SetVelocitySmoothing(Rigid->V(), Rigid->X(), RenderInterpErrorVelocitySmoothingDurationTicks); } const bool bHasInterpolationData = InterpData != nullptr; if (NextPullData) { auto LerpHelper = [SolverSyncTimestamp](const auto& Prev, const auto& OverwriteProperty) -> const auto* { //if overwrite is in the future, do nothing //if overwrite is on this step, we want to interpolate from overwrite to the result of the frame that consumed the overwrite //if overwrite is in the past, just do normal interpolation //this is nested because otherwise compiler can't figure out the type of nullptr with an auto return type return OverwriteProperty.Timestamp <= SolverSyncTimestamp ? (OverwriteProperty.Timestamp < SolverSyncTimestamp ? &Prev : &OverwriteProperty.Value) : nullptr; }; if (bUpdatePositionFromSimulation) { if (bHasInterpolationData) { InterpData->UpdateError(SolverSyncTimestamp, AsyncFixedTimeStep); } const bool bIsReplicationErrorSmoothing = bHasInterpolationData ? InterpData->IsErrorSmoothing() : false; bool DirectionalDecayPerformed = false; if (const FVec3* Prev = LerpHelper(PullData.X, ProxyTimestamp->OverWriteX)) { FVec3 Target = FMath::Lerp(*Prev, NextPullData->X, *Alpha); if (bIsReplicationErrorSmoothing) { DirectionalDecayPerformed = InterpData->DirectionalDecay(NextPullData->X - *Prev, RenderInterpolationCVars::RenderInterpErrorDirectionalDecayMultiplier); Target += InterpData->GetErrorX(*Alpha); if (InterpData->IsErrorVelocitySmoothing()) { #if CHAOS_DEBUG_DRAW if (RenderInterpolationCVars::bRenderInterpDebugDraw) { const FVector ZOffset = FVector(0,0, RenderInterpolationCVars::RenderInterpDebugDrawZOffset); Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(ZOffset + (Target - InterpData->GetErrorX(*Alpha)), ZOffset + Target, 1, FColor::Blue, false, 5.0f, 0, 0.5f); Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(ZOffset + InterpData->GetErrorVelocitySmoothingX(*Alpha), ZOffset + Target, 1, FColor::Blue, false, 5.0f, 0, 0.5f); Chaos::FDebugDrawQueue::GetInstance().DrawDebugBox(ZOffset + Target, FVector(2, 1, 1), Rigid->R(), FColor::Cyan, false, 5.f, 0, 0.25f); } #endif // CHAOS_DEBUG_DRAW const int32 RenderInterpErrorVelocitySmoothingDurationTicks = FMath::FloorToInt32(RenderInterpolationCVars::RenderInterpErrorVelocitySmoothingDuration / AsyncFixedTimeStep); // Convert duration from seconds to simulation ticks Target = FMath::Lerp(Target, InterpData->GetErrorVelocitySmoothingX(*Alpha), InterpData->GetErrorVelocitySmoothingAlpha(RenderInterpErrorVelocitySmoothingDurationTicks)); } } Rigid->SetX(Target, false); bNeedUpdateShapeBounds = true; } if (const FQuat* Prev = LerpHelper(PullData.R, ProxyTimestamp->OverWriteR)) { FQuat Target = FMath::Lerp(*Prev, NextPullData->R, *Alpha); if (bIsReplicationErrorSmoothing) { Target = Target * InterpData->GetErrorR(*Alpha); // Add rotational error offset in local space } Rigid->SetR(Target, false); bNeedUpdateShapeBounds = true; } #if CHAOS_DEBUG_DRAW if (RenderInterpolationCVars::bRenderInterpDebugDraw) { const FVector ZOffset = FVector(0, 0, RenderInterpolationCVars::RenderInterpDebugDrawZOffset); Chaos::FDebugDrawQueue::GetInstance().DrawDebugBox(ZOffset + NextPullData->X, FVector(2, 1, 1), NextPullData->R, FColor::Yellow, false, 5.f, 0, 0.5f); Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(ZOffset + PullData.X, ZOffset + NextPullData->X, 0.5f, FColor::Yellow, false, 5.0f, 0, 0.5f); Chaos::FDebugDrawQueue::GetInstance().DrawDebugBox(ZOffset + Rigid->X(), FVector(2, 1, 1), Rigid->R(), DirectionalDecayPerformed ? FColor::Cyan : FColor::Green, false, 5.f, 0, 0.5f); if (bIsReplicationErrorSmoothing) { if (Error) { Chaos::FDebugDrawQueue::GetInstance().DrawDebugBox(ZOffset + PullData.X, FVector(4, 2, 2), PullData.R, FColor::Red, false, 5.f, 0, 0.5f); Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(ZOffset + (PullData.X + Error->ErrorX), ZOffset + PullData.X, 1, FColor::Red, false, 5.0f, 0, 0.5f); } if (InterpData->IsErrorVelocitySmoothing()) { Chaos::FDebugDrawQueue::GetInstance().DrawDebugBox(ZOffset + InterpData->GetErrorVelocitySmoothingX(*Alpha), FVector(2, 2, 2), Rigid->R(), FColor::Purple, false, 5.f, 0, 0.5f); } else { Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(ZOffset + (Rigid->X() - InterpData->GetErrorX(*Alpha)), ZOffset + Rigid->X(), 1, FColor::Blue, false, 5.0f, 0, 0.5f); } } } #endif // CHAOS_DEBUG_DRAW } if (const FVec3* Prev = LerpHelper(PullData.V, ProxyTimestamp->OverWriteV)) { FVec3 Target = FMath::Lerp(*Prev, NextPullData->V, *Alpha); Rigid->SetV(Target, false); } if (const FVec3* Prev = LerpHelper(PullData.W, ProxyTimestamp->OverWriteW)) { FVec3 Target = FMath::Lerp(*Prev, NextPullData->W, *Alpha); Rigid->SetW(Target, false); } //we are interpolating from PullData to Next, but the timestamp is associated with Next //since we are interpolating it means we must have not seen Next yet, so the timestamp has to be strictly less than if (ProxyTimestamp->ObjectStateTimestamp < SolverSyncTimestamp) { Rigid->SetObjectState(PullData.ObjectState, true, /*bInvalidate=*/false); } else if(ProxyTimestamp->ObjectStateTimestamp == SolverSyncTimestamp && *Alpha == 1.f) { //if timestamp is the same as next, AND alpha is exactly 1, we are exactly at Next's time //so we can use its sleep state Rigid->SetObjectState(NextPullData->ObjectState, true, /*bInvalidate=*/false); } } else { if (bUpdatePositionFromSimulation) { //no interpolation, just ignore if overwrite comes after if (SolverSyncTimestamp >= ProxyTimestamp->OverWriteX.Timestamp) { Rigid->SetX(PullData.X, false); bNeedUpdateShapeBounds = true; } if (SolverSyncTimestamp >= ProxyTimestamp->OverWriteR.Timestamp) { Rigid->SetR(PullData.R, false); bNeedUpdateShapeBounds = true; } } if(SolverSyncTimestamp >= ProxyTimestamp->OverWriteV.Timestamp) { Rigid->SetV(PullData.V, false); } if(SolverSyncTimestamp >= ProxyTimestamp->OverWriteW.Timestamp) { Rigid->SetW(PullData.W, false); } if (SolverSyncTimestamp >= ProxyTimestamp->ObjectStateTimestamp) { Rigid->SetObjectState(PullData.ObjectState, true, /*bInvalidate=*/false); } } if (bNeedUpdateShapeBounds) { Rigid->UpdateShapeBounds(); } } return true; } bool FSingleParticlePhysicsProxy::IsDirty() { return Particle->IsDirty(); } Chaos::EWakeEventEntry FSingleParticlePhysicsProxy::GetWakeEvent() const { //question: should this API exist on proxy? auto Rigid = Particle->CastToRigidParticle(); return Rigid ? Rigid->GetWakeEvent() : Chaos::EWakeEventEntry::None; } void FSingleParticlePhysicsProxy::ClearEvents() { //question: should this API exist on proxy? if(auto Rigid = Particle->CastToRigidParticle()) { Rigid->ClearEvents(); } } }