// 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(); if (!RigidsSolver) { return nullptr; } return RigidsSolver->GetEvolution(); } template 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& ShapeData : ShapeArray) { BufferData.CollisionData.Add(ShapeData->GetCollisionData()); BufferData.QueryData.Add(ShapeData->GetQueryData()); BufferData.SimData.Add(ShapeData->GetSimData()); } if constexpr (std::is_base_of_v) { BufferData.bIsAnchored = Particle->IsAnchored(); } } } FClusterUnionPhysicsProxy::FClusterUnionPhysicsProxy(UObject* InOwner, const FClusterCreationParameters& InParameters, const FClusterUnionInitData& InInitData) : Base(InOwner) , ClusterParameters(InParameters) , InitData(InInitData) { InterpolationData = MakeUnique(); } 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& 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& 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& ShapeParticles) { RemoveParticlesFromClusterUnionGeometry(Particle_External.Get(), ShapeParticles, GeometryChildParticles_External); } void FClusterUnionPhysicsProxy::MergeGeometry_External(TArray&& ImplicitGeometries, const TArray& 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& 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(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 ParticlesToUpdate; TArray 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(); 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& 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(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(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(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(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(); 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 ObjectsCopy = AsUnion->GetObjects(); BufferData.Geometry = MakeImplicitObjectPtr(MoveTemp(ObjectsCopy)); } else { // Fallback to deep copy geometry hierarchy BufferData.Geometry = ClusterUnion->Geometry->DeepCopyGeometry(); } if (FImplicitObjectUnion* Union = BufferData.Geometry->AsA()) { 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(Manager, DataIdx, Particle_External->XR()); RemoteData.SyncRemote(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(); 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(); 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(); 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& Objects, const TArray& 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 Particles = Interface.GetAllRigidParticles(Objects, /* bIncludeNulls */ true); if (ensure(Particles.Num() == Objects.Num())) { FPBDRigidsEvolutionGBF& Evolution = *static_cast(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& Objects, bool bIsMain) { if (!Solver) { return; } Solver->EnqueueCommandImmediate( [this, Objects, bIsMain]() mutable { FReadPhysicsObjectInterface_Internal Interface = FPhysicsObjectInternalInterface::GetRead(); TArray Particles = Interface.GetAllRigidParticles(Objects); FPBDRigidsEvolutionGBF& Evolution = *static_cast(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; } } } } ); } }