Files
UnrealEngine/Engine/Source/Runtime/Experimental/Chaos/Private/PhysicsProxy/ClusterUnionPhysicsProxy.cpp
2025-05-18 13:04:45 +08:00

971 lines
36 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PhysicsProxy/ClusterUnionPhysicsProxy.h"
#include "Chaos/ClusterUnionManager.h"
#include "Chaos/ParticleHandle.h"
#include "Chaos/PhysicsObjectInternal.h"
#include "Chaos/PhysicsObjectInternalInterface.h"
#include "Chaos/PullPhysicsDataImp.h"
#include "Math/UnrealMathUtility.h"
#include "PBDRigidsSolver.h"
#include "Chaos/DebugDrawQueue.h"
namespace Chaos
{
static int32 GShallowCopyClusterUnionGeometryOnUpdate = 1;
FAutoConsoleVariableRef CVar_ShallowCopyClusterUnionGeometryOnUpdate(TEXT("p.ShallowCopyOnClusterUnionUpdate"), GShallowCopyClusterUnionGeometryOnUpdate, TEXT("If 1, shallow copy the root union geometry of a cluster union when its geometry updates, otherwise deep copy the geometry hierarchy"));
namespace CVars
{
extern bool bChaosEnableOverrideSingleParticleTypeToCluster;
}
namespace
{
FPBDRigidsEvolutionGBF* GetEvolution(FClusterUnionPhysicsProxy* Proxy)
{
if (!Proxy)
{
return nullptr;
}
FPBDRigidsSolver* RigidsSolver = Proxy->GetSolver<FPBDRigidsSolver>();
if (!RigidsSolver)
{
return nullptr;
}
return RigidsSolver->GetEvolution();
}
template<typename TParticle>
void BufferPhysicsResultsImp(FClusterUnionPhysicsProxy* Proxy, TParticle* Particle, FDirtyClusterUnionData& BufferData)
{
if (!Particle || !Proxy)
{
return;
}
BufferData.SetProxy(*Proxy);
BufferData.X = Particle->GetX();
BufferData.R = Particle->GetR();
BufferData.V = Particle->GetV();
BufferData.W = Particle->GetW();
BufferData.Mass = FRealSingle(Particle->M());
BufferData.Inertia = FVec3f(Particle->I());
BufferData.ObjectState = Particle->ObjectState();
BufferData.Geometry = nullptr;
const FShapesArray& ShapeArray = Particle->ShapesArray();
BufferData.CollisionData.Empty(ShapeArray.Num());
BufferData.QueryData.Empty(ShapeArray.Num());
BufferData.SimData.Empty(ShapeArray.Num());
for (const TUniquePtr<Chaos::FPerShapeData>& ShapeData : ShapeArray)
{
BufferData.CollisionData.Add(ShapeData->GetCollisionData());
BufferData.QueryData.Add(ShapeData->GetQueryData());
BufferData.SimData.Add(ShapeData->GetSimData());
}
if constexpr (std::is_base_of_v<FClusterUnionPhysicsProxy::FInternalParticle, TParticle>)
{
BufferData.bIsAnchored = Particle->IsAnchored();
}
}
}
FClusterUnionPhysicsProxy::FClusterUnionPhysicsProxy(UObject* InOwner, const FClusterCreationParameters& InParameters, const FClusterUnionInitData& InInitData)
: Base(InOwner)
, ClusterParameters(InParameters)
, InitData(InInitData)
{
InterpolationData = MakeUnique<FProxyInterpolationBase>();
}
void FClusterUnionPhysicsProxy::Initialize_External()
{
// Create game thread particle as well as the physics object.
Particle_External = FExternalParticle::CreateParticle();
check(Particle_External != nullptr);
#if CHAOS_DEBUG_NAME
Particle_External->SetDebugName(InitData.DebugName);
#endif
Particle_External->SetProxy(this);
Particle_External->SetUserData(InitData.UserData);
Particle_External->SetX(InitData.InitialTransform.GetTranslation());
Particle_External->SetR(InitData.InitialTransform.GetRotation());
Particle_External->SetCCDEnabled(InitData.bCCDEnabled);
Particle_External->SetMACDEnabled(InitData.bMACDEnabled);
// NO DIRTY FLAGS ALLOWED. We must strictly manage the dirty flags on the particle.
// Setting the particle's XR on the particle will set the XR dirty flag but that isn't
// used for the cluster union (there is no functionality in Chaos to let the particle
// be easily managed by a proxy that isn't the single particle physics proxy). And if the
// XR dirty flag is set, we'll try to access buffers that don't exist for cluster union proxies.
Particle_External->ClearDirtyFlags();
PhysicsObject = FPhysicsObjectFactory::CreatePhysicsObject(this);
}
void FClusterUnionPhysicsProxy::Initialize_Internal(FPBDRigidsSolver* RigidsSolver, FPBDRigidsSolver::FParticlesType& Particles)
{
if (!ensure(RigidsSolver) || !ensure(Particle_External != nullptr))
{
return;
}
bIsInitializedOnPhysicsThread = true;
bEnableStrainOnCollision_Internal = ClusterParameters.bEnableStrainOnCollision;
FPBDRigidsEvolutionGBF* Evolution = RigidsSolver->GetEvolution();
if (!ensure(Evolution))
{
return;
}
FUniqueIdx UniqueIndex = Particle_External->UniqueIdx();
FClusterUnionManager& ClusterUnionManager = Evolution->GetRigidClustering().GetClusterUnionManager();
FClusterUnionCreationParameters ClusterUnionParameters;
ClusterUnionParameters.UniqueIndex = &UniqueIndex;
ClusterUnionParameters.ActorId = InitData.ActorId;
ClusterUnionParameters.ComponentId = InitData.ComponentId;
ClusterUnionParameters.GravityGroupOverride = InitData.GravityGroupOverride;
ClusterUnionIndex = ClusterUnionManager.CreateNewClusterUnion(ClusterParameters, ClusterUnionParameters);
if (FClusterUnion* ClusterUnion = ClusterUnionManager.FindClusterUnion(ClusterUnionIndex); ensure(ClusterUnion != nullptr))
{
Particle_Internal = ClusterUnion->InternalCluster;
Particle_Internal->SetPhysicsProxy(this);
Particle_Internal->GTGeometryParticle() = Particle_External.Get();
Particle_Internal->SetUnbreakable(InitData.bUnbreakable);
Particle_Internal->SetX(InitData.InitialTransform.GetTranslation());
Particle_Internal->SetR(InitData.InitialTransform.GetRotation());
Particle_Internal->SetVf(Chaos::FVec3f(0.f));
Particle_Internal->SetWf(Chaos::FVec3f(0.f));
Particle_Internal->SetP(Particle_Internal->GetX());
Particle_Internal->SetQf(Particle_Internal->GetRf());
Particle_Internal->SetCenterOfMass(FVector3f::ZeroVector);
Particle_Internal->SetRotationOfMass(FQuat::Identity);
Particle_Internal->SetCCDEnabled(InitData.bCCDEnabled);
Particle_Internal->SetMACDEnabled(InitData.bMACDEnabled);
#if CHAOS_DEBUG_NAME
if (Particle_External.IsValid())
{
Particle_Internal->SetDebugName(Particle_External->DebugName());
}
#endif
// For explicit cluster unions (i.e. cluster unions created via a cluster union component rather than the cluster group index), the cluster union itself is responsible for initializing
// the particle's XR from the component's transform. Therefore, we never want the PT to try and initialize the XR of the cluster union - the only changes to XR from the PT should
// be a result of physical simulation.
ClusterUnion->bNeedsXRInitialization = false;
ClusterUnion->bCheckConnectivity = InitData.bCheckConnectivity;
ClusterUnion->bGenerateConnectivityEdges = InitData.bGenerateConnectivityEdges;
}
}
bool FClusterUnionPhysicsProxy::HasChildren_Internal() const
{
if (!Particle_Internal)
{
return false;
}
return Particle_Internal->ClusterIds().NumChildren > 0;
}
void FClusterUnionPhysicsProxy::AddPhysicsObjects_External(const TArray<FPhysicsObjectHandle>& Objects)
{
if (!Solver)
{
return;
}
Solver->EnqueueCommandImmediate(
[this, Objects=Objects]() mutable
{
FReadPhysicsObjectInterface_Internal Interface = FPhysicsObjectInternalInterface::GetRead();
if (FPBDRigidsEvolutionGBF* Evolution = GetEvolution(this))
{
Evolution->GetRigidClustering().GetClusterUnionManager().AddPendingClusterIndexOperation(ClusterUnionIndex, EClusterUnionOperation::Add, Interface.GetAllRigidParticles(Objects));
}
}
);
}
void FClusterUnionPhysicsProxy::RemovePhysicsObjects_External(const TSet<FPhysicsObjectHandle>& Objects)
{
if (!Solver || Objects.IsEmpty())
{
return;
}
Solver->EnqueueCommandImmediate(
[this, Objects = Objects]() mutable
{
FReadPhysicsObjectInterface_Internal Interface = FPhysicsObjectInternalInterface::GetRead();
if (FPBDRigidsEvolutionGBF* Evolution = GetEvolution(this))
{
Evolution->GetRigidClustering().GetClusterUnionManager().AddPendingClusterIndexOperation(ClusterUnionIndex, EClusterUnionOperation::Remove, Interface.GetAllRigidParticles(Objects.Array()));
}
}
);
}
void FClusterUnionPhysicsProxy::SetIsAnchored_External(bool bIsAnchored)
{
if (!Solver || !ensure(Particle_External))
{
return;
}
Solver->EnqueueCommandImmediate(
[this, bIsAnchored]() mutable
{
if (Particle_Internal)
{
if (FPBDRigidsEvolutionGBF* Evolution = GetEvolution(this))
{
const bool bWasAnchored = Particle_Internal->IsAnchored();
Particle_Internal->SetIsAnchored(bIsAnchored);
// We also need to make sure the cluster union won't try to override this value that we set.
Chaos::FClusterUnionManager& Manager = Evolution->GetRigidClustering().GetClusterUnionManager();
if (Chaos::FClusterUnion* ClusterUnion = Manager.FindClusterUnion(ClusterUnionIndex))
{
ClusterUnion->bAnchorLock = true;
}
// Let the cluster union manager apply the proper properties on the particle.
if (bWasAnchored != bIsAnchored)
{
Manager.RequestDeferredClusterPropertiesUpdate(ClusterUnionIndex, Chaos::EUpdateClusterUnionPropertiesFlags::UpdateKinematicProperties);
}
}
}
}
);
}
EObjectStateType FClusterUnionPhysicsProxy::GetObjectState_External() const
{
if (!ensure(Particle_External))
{
return EObjectStateType::Uninitialized;
}
return Particle_External->ObjectState();
}
void FClusterUnionPhysicsProxy::SetObjectState_External(EObjectStateType State)
{
if (!Solver || !ensure(Particle_External))
{
return;
}
Solver->EnqueueCommandImmediate(
[this, State]() mutable
{
if (Particle_Internal)
{
if (Particle_Internal->ObjectState() != State)
{
if (FPBDRigidsEvolutionGBF* Evolution = GetEvolution(this))
{
Evolution->SetParticleObjectState(Particle_Internal, State);
}
}
}
}
);
}
void FClusterUnionPhysicsProxy::Wake_External()
{
if (!Solver || !ensure(Particle_External))
{
return;
}
Solver->EnqueueCommandImmediate(
[this]() mutable
{
if (Particle_Internal)
{
if (FPBDRigidsEvolutionGBF* Evolution = GetEvolution(this))
{
Evolution->WakeParticle(Particle_Internal);
}
}
}
);
}
void FClusterUnionPhysicsProxy::SetMass_External(Chaos::FReal InMass)
{
if (!Solver || !ensure(Particle_External))
{
return;
}
Chaos::FRealSingle Mass = Chaos::FRealSingle(InMass);
Chaos::FRealSingle InvMass = 0;
Chaos::FVec3f Inertia = FVec3f(0);
Chaos::FVec3f InvInertia = FVec3f(0);
if (Mass > 0)
{
InvMass = 1.0f / Mass;
Chaos::FRealSingle OldMass = Chaos::FRealSingle(Particle_External->M());
if (OldMass > 0)
{
const Chaos::FRealSingle MassScale = Mass / OldMass;
Inertia = Particle_External->I() * MassScale;
InvInertia = Particle_External->InvI() / MassScale;
}
}
if (Particle_External)
{
Particle_External->SetM(Mass);
Particle_External->SetInvM(InvMass);
Particle_External->SetI(Inertia);
Particle_External->SetInvI(InvInertia);
}
Solver->EnqueueCommandImmediate(
[this, Mass, InvMass, Inertia, InvInertia]()
{
if (Particle_Internal)
{
Particle_Internal->SetM(Mass);
Particle_Internal->SetInvM(InvMass);
Particle_Internal->SetI(Inertia);
Particle_Internal->SetInvI(InvInertia);
}
}
);
}
void FClusterUnionPhysicsProxy::RemoveShapes_External(const TArray<FPBDRigidParticle*>& ShapeParticles)
{
RemoveParticlesFromClusterUnionGeometry(Particle_External.Get(), ShapeParticles, GeometryChildParticles_External);
}
void FClusterUnionPhysicsProxy::MergeGeometry_External(TArray<Chaos::FImplicitObjectPtr>&& ImplicitGeometries, const TArray<FPBDRigidParticle*>& ShapeParticles)
{
if (!ensure(Particle_External) || !ensure(ImplicitGeometries.Num() == ShapeParticles.Num()))
{
return;
}
// In the cases where this is necessary, the SQ should have a valid state - it's just the geometry itself that isn't valid.
ModifyAdditionOfChildrenToClusterUnionGeometry(
Particle_External.Get(),
ShapeParticles,
InitData.ActorId,
InitData.ComponentId,
[this, ImplicitGeometries=MoveTemp(ImplicitGeometries)]() mutable
{
Particle_External->MergeGeometry(MoveTemp(ImplicitGeometries));
}
);
GeometryChildParticles_External.Append(ShapeParticles);
}
void FClusterUnionPhysicsProxy::SetGeometry_External(const Chaos::FImplicitObjectPtr& Geometry, const TArray<FPBDRigidParticle*>& ShapeParticles)
{
if (!ensure(Particle_External))
{
return;
}
// In the cases where this is necessary, the SQ should have a valid state - it's just the geometry itself that isn't valid.
ModifyAdditionOfChildrenToClusterUnionGeometry(
Particle_External.Get(),
ShapeParticles,
InitData.ActorId,
InitData.ComponentId,
[this, &Geometry]()
{
Particle_External->SetGeometry(Geometry);
}
);
GeometryChildParticles_External = ShapeParticles;
}
DECLARE_CYCLE_STAT(TEXT("FClusterUnionPhysicsProxy::PushToPhysicsState"), STAT_ClusterUnionPhysicsProxyPushToPhysicsState, STATGROUP_Chaos);
void FClusterUnionPhysicsProxy::PushToPhysicsState(const FDirtyPropertiesManager& Manager, int32 DataIdx, const FDirtyProxy& Dirty)
{
SCOPE_CYCLE_COUNTER(STAT_ClusterUnionPhysicsProxyPushToPhysicsState);
if (!ensure(Solver) || !ensure(Particle_Internal))
{
return;
}
const FDirtyChaosProperties& ParticleData = Dirty.PropertyData;
FPBDRigidsSolver& RigidsSolver = *static_cast<FPBDRigidsSolver*>(Solver);
if (!ensure(RigidsSolver.GetEvolution()))
{
return;
}
FPBDRigidsEvolutionGBF& Evolution = *RigidsSolver.GetEvolution();
if (const FParticlePositionRotation* NewXR = ParticleData.FindClusterXR(Manager, DataIdx))
{
// If we change the cluster union's location and the child particles are not soft locked with a given child to parent,
// our goal is to maintain the particle's location rather than its child to parent.
FTransform NewTransform{ NewXR->R(), NewXR->X() };
TArray<FPBDRigidParticleHandle*> ParticlesToUpdate;
TArray<FTransform> NewChildToParent;
FClusterUnionManager& ClusterUnionManager = Evolution.GetRigidClustering().GetClusterUnionManager();
if (FClusterUnion* Union = ClusterUnionManager.FindClusterUnion(ClusterUnionIndex); Union && !Union->bNeedsXRInitialization)
{
for (FPBDRigidParticleHandle* Particle : Union->ChildParticles)
{
if (FPBDRigidClusteredParticleHandle* ClusterParticle = Particle->CastToClustered())
{
if (!ClusterParticle->IsChildToParentLocked())
{
ParticlesToUpdate.Add(Particle);
const FRigidTransform3 ChildWorldTM(Particle->GetX(), Particle->GetR());
NewChildToParent.Add(ChildWorldTM.GetRelativeTransform(NewTransform));
}
}
}
}
Evolution.SetParticleTransform(Particle_Internal, NewXR->GetX(), NewXR->R(), true);
if (!ParticlesToUpdate.IsEmpty() && !NewChildToParent.IsEmpty())
{
// Do not lock this child to parent as it's not coming from the server. It's just to smooth out the XR replication on the client
// while we still have not yet received the ChildToParent yet.
ClusterUnionManager.UpdateClusterUnionParticlesChildToParent(ClusterUnionIndex, ParticlesToUpdate, NewChildToParent, false);
}
// Particle needs to actually be marked dirty! Less so to pull state from the PT back to the GT for the cluster union
// but this is primarily for proxies within the cluster union. They (i.e. GCs) need to see this change to the particle's transform.
Evolution.GetParticles().MarkTransientDirtyParticle(Particle_Internal);
const FRigidTransform3 WorldTransform{ Particle_Internal->GetX(), Particle_Internal->GetR() };
Particle_Internal->UpdateWorldSpaceState(WorldTransform, FVec3(0));
Evolution.DirtyParticle(*Particle_Internal);
}
if (const FParticleVelocities* NewVelocities = ParticleData.FindClusterVelocities(Manager, DataIdx))
{
Evolution.SetParticleVelocities(Particle_Internal, NewVelocities->V(), NewVelocities->W());
}
}
DECLARE_CYCLE_STAT(TEXT("FClusterUnionPhysicsProxy::PullFromPhysicsState"), STAT_ClusterUnionPhysicsProxyPullFromPhysicsState, STATGROUP_Chaos);
bool FClusterUnionPhysicsProxy::PullFromPhysicsState(const FDirtyClusterUnionData& PullData, int32 SolverSyncTimestamp, const FDirtyClusterUnionData* NextPullData, const FRealSingle* Alpha, const FDirtyRigidParticleReplicationErrorData* Error, const Chaos::FReal AsyncFixedTimeStep)
{
SCOPE_CYCLE_COUNTER(STAT_ClusterUnionPhysicsProxyPullFromPhysicsState);
if (!ensure(Particle_External))
{
return false;
}
const FDirtyClusterUnionData& CurrentPullData = NextPullData ? *NextPullData : PullData;
const FClusterUnionProxyTimestamp* ProxyTimestamp = CurrentPullData.GetTimestamp();
if (!ProxyTimestamp)
{
return false;
}
FProxyInterpolationBase* InterpData = GetInterpolationData();
if (Error)
{
InterpData = GetOrCreateErrorInterpolationData<FProxyInterpolationError>();
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(Particle_External->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 bool bHasInterpolationData = InterpData != nullptr;
SyncedData_External.bIsAnchored = CurrentPullData.bIsAnchored;
SyncedData_External.bDidSyncGeometry = false;
SyncedData_External.ChildParticles.Empty(CurrentPullData.ChildParticles.Num());
// I question the need to do this individual copy one by one...maybe we should've used the same type for the synced data.
for (const FDirtyClusterUnionParticleData& InData : CurrentPullData.ChildParticles)
{
FClusterUnionChildData ConvertedData;
ConvertedData.ParticleIdx = InData.ParticleIdx;
ConvertedData.ChildToParent = InData.ChildToParent;
ConvertedData.Proxy = InData.Proxy;
ConvertedData.CachedOwner = InData.CachedOwner;
ConvertedData.BoneId = InData.BoneId;
SyncedData_External.ChildParticles.Add(ConvertedData);
}
if (CurrentPullData.Geometry)
{
Particle_External->SetGeometry(CurrentPullData.Geometry);
SyncedData_External.bDidSyncGeometry = true;
}
FReal M = FReal(CurrentPullData.Mass);
FReal InvM = (M > 0.0) ? (1.0 / M) : 0.0;
Particle_External->SetM(M, /*bInvalidate=*/false);
Particle_External->SetInvM(InvM, /*bInvalidate=*/false);
FVec3 I = FVec3(CurrentPullData.Inertia);
FVec3 InvI = (!I.IsZero()) ? FVec3(1) / I : FVec3(0);
Particle_External->SetI(I, /*bInvalidate=*/false);
Particle_External->SetInvI(InvI, /*bInvalidate=*/false);
Particle_External->SetObjectState(CurrentPullData.ObjectState, true, /*bInvalidate=*/false);
const FShapesArray& ShapeArray = Particle_External->ShapesArray();
for (int32 ShapeIndex = 0; ShapeIndex < ShapeArray.Num(); ++ShapeIndex)
{
if (const TUniquePtr<Chaos::FPerShapeData>& ShapeData = ShapeArray[ShapeIndex])
{
if (CurrentPullData.CollisionData.IsValidIndex(ShapeIndex))
{
ShapeData->SetCollisionData(CurrentPullData.CollisionData[ShapeIndex]);
}
if (CurrentPullData.QueryData.IsValidIndex(ShapeIndex))
{
ShapeData->SetQueryData(CurrentPullData.QueryData[ShapeIndex]);
}
if (CurrentPullData.SimData.IsValidIndex(ShapeIndex))
{
ShapeData->SetSimData(CurrentPullData.SimData[ShapeIndex]);
}
}
}
if (NextPullData)
{
// This is the same as in the SingleParticlePhysicsProxy.
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 (bHasInterpolationData)
{
InterpData->UpdateError(SolverSyncTimestamp, AsyncFixedTimeStep);
}
const bool bIsReplicationErrorSmoothing = bHasInterpolationData ? InterpData->IsErrorSmoothing() : false;
bool DirectionalDecayPerformed = false;
if (const FVec3* Prev = LerpHelper(PullData.X, ProxyTimestamp->OverWriteX))
{
if (bHasInterpolationData)
{
DirectionalDecayPerformed = InterpData->DirectionalDecay(NextPullData->X - *Prev, RenderInterpolationCVars::RenderInterpErrorDirectionalDecayMultiplier);
}
const FVec3 NewX = bIsReplicationErrorSmoothing ?
FMath::Lerp(*Prev, NextPullData->X, *Alpha) + InterpData->GetErrorX(*Alpha) :
FMath::Lerp(*Prev, NextPullData->X, *Alpha);
Particle_External->SetX(NewX, false);
}
if (const FQuat* Prev = LerpHelper(PullData.R, ProxyTimestamp->OverWriteR))
{
const FQuat NewR = bIsReplicationErrorSmoothing ?
FMath::Lerp(*Prev, NextPullData->R, *Alpha) * InterpData->GetErrorR(*Alpha) :
FMath::Lerp(*Prev, NextPullData->R, *Alpha); // Add rotational error offset in local space
Particle_External->SetR(NewR, false);
}
if (const FVec3* Prev = LerpHelper(PullData.V, ProxyTimestamp->OverWriteV))
{
const FVec3 NewV = FMath::Lerp(*Prev, NextPullData->V, *Alpha);
Particle_External->SetV(NewV, false);
}
if (const FVec3* Prev = LerpHelper(PullData.W, ProxyTimestamp->OverWriteW))
{
const FVec3 NewW = FMath::Lerp(*Prev, NextPullData->W, *Alpha);
Particle_External->SetW(NewW, false);
}
#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 + Particle_External->GetX(), FVector(2, 1, 1), Particle_External->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);
}
Chaos::FDebugDrawQueue::GetInstance().DrawDebugDirectionalArrow(ZOffset + (Particle_External->GetX() - InterpData->GetErrorX(*Alpha)), ZOffset + Particle_External->GetX(), 1, FColor::Blue, false, 5.0f, 0, 0.5f);
}
}
#endif // CHAOS_DEBUG_DRAW
}
else
{
if (SolverSyncTimestamp >= ProxyTimestamp->OverWriteX.Timestamp)
{
Particle_External->SetX(PullData.X, false);
}
if (SolverSyncTimestamp >= ProxyTimestamp->OverWriteR.Timestamp)
{
Particle_External->SetR(PullData.R, false);
}
if (SolverSyncTimestamp >= ProxyTimestamp->OverWriteV.Timestamp)
{
Particle_External->SetV(PullData.V, false);
}
if (SolverSyncTimestamp >= ProxyTimestamp->OverWriteW.Timestamp)
{
Particle_External->SetW(PullData.W, false);
}
}
// Note: Is their a better way to achieve this?
// Don't want to iterate over all children if we don't need to so using flag
if (CVars::bChaosEnableOverrideSingleParticleTypeToCluster)
{
for (const FDirtyClusterUnionParticleData& InData : CurrentPullData.ChildParticles)
{
// SingleParticleProxies will not render in the correct location if they are still part on an intact cluster
// so update the transform of these 'kinematic' child particles
if (InData.Proxy && InData.Proxy->GetType() == EPhysicsProxyType::SingleParticleProxy)
{
if (Chaos::FSingleParticlePhysicsProxy* SingleParticleProxy = static_cast<Chaos::FSingleParticlePhysicsProxy*>(InData.Proxy))
{
Chaos::FRigidBodyHandle_External& Body_External = SingleParticleProxy->GetGameThreadAPI();
FTransform ClusterTransform(Particle_External->R(), Particle_External->X());
FTransform ChildWorldTransform = InData.ChildToParent * ClusterTransform;
Body_External.SetX(ChildWorldTransform.GetLocation());
Body_External.SetR(ChildWorldTransform.GetRotation());
}
}
}
}
Particle_External->UpdateShapeBounds();
return true;
}
DECLARE_CYCLE_STAT(TEXT("FClusterUnionPhysicsProxy::BufferPhysicsResults_Internal"), STAT_ClusterUnionPhysicsProxyBufferPhysicsResultsInternal, STATGROUP_Chaos);
void FClusterUnionPhysicsProxy::BufferPhysicsResults_Internal(FDirtyClusterUnionData& BufferData)
{
SCOPE_CYCLE_COUNTER(STAT_ClusterUnionPhysicsProxyBufferPhysicsResultsInternal);
BufferPhysicsResultsImp(this, Particle_Internal, BufferData);
FPBDRigidsEvolutionGBF& Evolution = *static_cast<FPBDRigidsSolver*>(Solver)->GetEvolution();
FClusterUnionManager& ClusterUnionManager = Evolution.GetRigidClustering().GetClusterUnionManager();
if (FClusterUnion* ClusterUnion = ClusterUnionManager.FindClusterUnion(ClusterUnionIndex))
{
BufferData.ChildParticles.Empty(ClusterUnion->ChildParticles.Num());
for (FPBDRigidParticleHandle* Particle : ClusterUnion->ChildParticles)
{
if (!ensure(Particle))
{
continue;
}
if (IPhysicsProxyBase* Proxy = Particle->PhysicsProxy())
{
FDirtyClusterUnionParticleData Data;
Data.ParticleIdx = Particle->UniqueIdx();
if (FPBDRigidClusteredParticleHandle* ClusteredParticle = Particle->CastToClustered())
{
Data.ChildToParent = ClusteredParticle->ChildToParent();
}
else
{
Data.ChildToParent = FRigidTransform3::Identity;
}
Data.Proxy = Proxy;
Data.CachedOwner = reinterpret_cast<void*>(Proxy->GetOwner());
if (Proxy->GetType() == EPhysicsProxyType::GeometryCollectionType)
{
// A bit hacky, but the only way we can communicate what particle we care about back to the cluster union on the GT.
FGeometryCollectionPhysicsProxy* GCProxy = static_cast<FGeometryCollectionPhysicsProxy*>(Proxy);
Data.BoneId = GCProxy->GetTransformGroupIndexFromHandle(Particle);
}
else if (Proxy->GetType() == EPhysicsProxyType::SingleParticleProxy)
{
// There's no way to reconstitute the BoneID! BoneID is used to match the particle on the client, but this makes no sense when dealing with SingleParticle
Data.BoneId = BufferData.ChildParticles.Num(); // This could be wrong, but seems to work for now!
}
BufferData.ChildParticles.Add(Data);
}
}
if (ClusterUnion->bGeometryModified && !ClusterUnion->ChildParticles.IsEmpty())
{
if(FImplicitObjectUnion* AsUnion = ClusterUnion->Geometry->AsA<FImplicitObjectUnion>();
GShallowCopyClusterUnionGeometryOnUpdate && AsUnion)
{
// Shallow copy the root union for the GT update
// This ensures the GT has a snapshot of the union as it is now for this results data. The PT is free to
// continue modifying its geometry without potentially disrupting GT reads. Internally we don't want
// to duplicate all the geometry - we essentially just need the list of objects in the root union to
// point to the correct internal geometries, but have a separate list on GT and PT.
TArray<FImplicitObjectPtr> ObjectsCopy = AsUnion->GetObjects();
BufferData.Geometry = MakeImplicitObjectPtr<FImplicitObjectUnion>(MoveTemp(ObjectsCopy));
}
else
{
// Fallback to deep copy geometry hierarchy
BufferData.Geometry = ClusterUnion->Geometry->DeepCopyGeometry();
}
if (FImplicitObjectUnion* Union = BufferData.Geometry->AsA<FImplicitObjectUnion>())
{
Union->SetLockedRecursive(true);
}
ClusterUnion->bGeometryModified = false;
}
}
}
DECLARE_CYCLE_STAT(TEXT("FClusterUnionPhysicsProxy::BufferPhysicsResults_External"), STAT_ClusterUnionPhysicsProxyBufferPhysicsResultsExternal, STATGROUP_Chaos);
void FClusterUnionPhysicsProxy::BufferPhysicsResults_External(FDirtyClusterUnionData& BufferData)
{
SCOPE_CYCLE_COUNTER(STAT_ClusterUnionPhysicsProxyBufferPhysicsResultsExternal);
BufferPhysicsResultsImp(this, Particle_External.Get(), BufferData);
BufferData.bIsAnchored = SyncedData_External.bIsAnchored;
BufferData.ChildParticles.Empty(SyncedData_External.ChildParticles.Num());
for (const FClusterUnionChildData& Data : SyncedData_External.ChildParticles)
{
FDirtyClusterUnionParticleData ConvertedData;
ConvertedData.ParticleIdx = Data.ParticleIdx;
ConvertedData.ChildToParent = Data.ChildToParent;
ConvertedData.Proxy = Data.Proxy;
ConvertedData.CachedOwner = Data.CachedOwner;
ConvertedData.BoneId = Data.BoneId;
BufferData.ChildParticles.Add(ConvertedData);
}
}
DECLARE_CYCLE_STAT(TEXT("FClusterUnionPhysicsProxy::SyncRemoteData"), STAT_ClusterUnionPhysicsProxySyncRemoteData, STATGROUP_Chaos);
void FClusterUnionPhysicsProxy::SyncRemoteData(FDirtyPropertiesManager& Manager, int32 DataIdx, FDirtyChaosProperties& RemoteData) const
{
SCOPE_CYCLE_COUNTER(STAT_ClusterUnionPhysicsProxySyncRemoteData);
if (!ensure(Particle_External))
{
return;
}
// This is similar to TGeometryParticle::SyncRemoteData except it puts it into the cluster properties.
RemoteData.SetParticleBufferType(Particle_External->Type);
// We need to modify the dirty flags to remove the non cluster properties to be 100% safe.
FDirtyChaosPropertyFlags DirtyFlags = Particle_External->DirtyFlags();
// We need to suppress V501 in PVS Studio since it's a false positive warning in this case. This is actually necessary for the codegen here.
//-V:CHAOS_PROPERTY:501
#define CHAOS_PROPERTY(PropName, Type, ProxyType) if constexpr (ProxyType != EPhysicsProxyType::ClusterUnionProxy) { DirtyFlags.MarkClean(EChaosPropertyFlags::PropName); }
#include "Chaos/ParticleProperties.inl" //-V:CHAOS_PROPERTY:501
#undef CHAOS_PROPERTY
RemoteData.SetFlags(DirtyFlags);
// SyncRemote will check the dirty flags and will skip the change in value if the dirty flag is not actually set.
RemoteData.SyncRemote<FParticlePositionRotation, EChaosProperty::ClusterXR>(Manager, DataIdx, Particle_External->XR());
RemoteData.SyncRemote<FParticleVelocities, EChaosProperty::ClusterVelocities>(Manager, DataIdx, Particle_External->Velocities());
}
void FClusterUnionPhysicsProxy::ClearAccumulatedData()
{
if (!ensure(Particle_External))
{
return;
}
Particle_External->ClearDirtyFlags();
}
void FClusterUnionPhysicsProxy::SetXR_External(const FVector& X, const FQuat& R)
{
if (!Solver || !ensure(Particle_External))
{
return;
}
Particle_External->SetX(X, false);
Particle_External->SetR(R, false);
Particle_External->ForceDirty(EChaosPropertyFlags::ClusterXR);
FClusterUnionProxyTimestamp& SyncTS = GetSyncTimestampAs<FClusterUnionProxyTimestamp>();
SyncTS.OverWriteX.Set(GetSolverSyncTimestamp_External(), X);
SyncTS.OverWriteR.Set(GetSolverSyncTimestamp_External(), R);
Solver->AddDirtyProxy(this);
}
void FClusterUnionPhysicsProxy::SetLinearVelocity_External(const FVector& V)
{
if (!Solver || !ensure(Particle_External))
{
return;
}
Particle_External->SetV(V, false);
Particle_External->ForceDirty(EChaosPropertyFlags::ClusterVelocities);
FClusterUnionProxyTimestamp& SyncTS = GetSyncTimestampAs<FClusterUnionProxyTimestamp>();
SyncTS.OverWriteV.Set(GetSolverSyncTimestamp_External(), V);
Solver->AddDirtyProxy(this);
}
void FClusterUnionPhysicsProxy::SetAngularVelocity_External(const FVector& W)
{
if (!ensure(Particle_External))
{
return;
}
Particle_External->SetW(W, false);
Particle_External->ForceDirty(EChaosPropertyFlags::ClusterVelocities);
FClusterUnionProxyTimestamp& SyncTS = GetSyncTimestampAs<FClusterUnionProxyTimestamp>();
SyncTS.OverWriteW.Set(GetSolverSyncTimestamp_External(), W);
Solver->AddDirtyProxy(this);
}
void FClusterUnionPhysicsProxy::SetChildToParent_External(FPhysicsObjectHandle Child, const FTransform& RelativeTransform, bool bLock)
{
if (ensure(Child != nullptr))
{
BulkSetChildToParent_External({ Child }, { RelativeTransform }, bLock);
}
}
void FClusterUnionPhysicsProxy::BulkSetChildToParent_External(const TArray<FPhysicsObjectHandle>& Objects, const TArray<FTransform>& Transforms, bool bLock)
{
if (!Solver || !ensure(Particle_External) || !ensure(Objects.Num() == Transforms.Num()))
{
return;
}
Solver->EnqueueCommandImmediate(
[this, Objects, Transforms, bLock]() mutable
{
FReadPhysicsObjectInterface_Internal Interface = FPhysicsObjectInternalInterface::GetRead();
TArray<FPBDRigidParticleHandle*> Particles = Interface.GetAllRigidParticles(Objects, /* bIncludeNulls */ true);
if (ensure(Particles.Num() == Objects.Num()))
{
FPBDRigidsEvolutionGBF& Evolution = *static_cast<FPBDRigidsSolver*>(Solver)->GetEvolution();
FClusterUnionManager& ClusterUnionManager = Evolution.GetRigidClustering().GetClusterUnionManager();
ClusterUnionManager.UpdateClusterUnionParticlesChildToParent(ClusterUnionIndex, Particles, Transforms, bLock);
}
}
);
}
void FClusterUnionPhysicsProxy::SetEnableStrainOnCollision_External(bool bEnable)
{
if (Solver && ensure(Particle_External))
{
Solver->EnqueueCommandImmediate(
[this, bEnable]() mutable
{
bEnableStrainOnCollision_Internal = bEnable;
}
);
}
}
void FClusterUnionPhysicsProxy::ChangeMainParticleStatus_External(const TArray<FPhysicsObjectHandle>& Objects, bool bIsMain)
{
if (!Solver)
{
return;
}
Solver->EnqueueCommandImmediate(
[this, Objects, bIsMain]() mutable
{
FReadPhysicsObjectInterface_Internal Interface = FPhysicsObjectInternalInterface::GetRead();
TArray<FPBDRigidParticleHandle*> Particles = Interface.GetAllRigidParticles(Objects);
FPBDRigidsEvolutionGBF& Evolution = *static_cast<FPBDRigidsSolver*>(Solver)->GetEvolution();
FClusterUnionManager& ClusterUnionManager = Evolution.GetRigidClustering().GetClusterUnionManager();
if (FClusterUnion* Union = ClusterUnionManager.FindClusterUnion(ClusterUnionIndex))
{
for (FPBDRigidParticleHandle* Particle : Particles)
{
if (FClusterUnionParticleProperties* Props = Union->ChildProperties.Find(Particle))
{
Props->bIsAuxiliaryParticle = !bIsMain;
}
}
}
}
);
}
}