// Copyright Epic Games, Inc. All Rights Reserved. #include "BoneControllers/AnimNode_RigidBody.h" #include "AnimationRuntime.h" #include "Animation/AnimInstanceProxy.h" #include "HAL/Event.h" #include "HAL/LowLevelMemTracker.h" #include "PhysicsEngine/BodySetup.h" #include "PhysicsEngine/PhysicsAsset.h" #include "PhysicsEngine/PhysicsConstraintTemplate.h" #include "GameFramework/PawnMovementComponent.h" #include "Physics/PhysicsInterfaceCore.h" #include "Physics/ImmediatePhysics/ImmediatePhysicsActorHandle.h" #include "Physics/ImmediatePhysics/ImmediatePhysicsSimulation.h" #include "Physics/ImmediatePhysics/ImmediatePhysicsStats.h" #include "PhysicsField/PhysicsFieldComponent.h" #include "PhysicsEngine/PhysicsSettings.h" #include "Logging/MessageLog.h" #include "Logging/LogMacros.h" LLM_DEFINE_TAG(Animation_RigidBody); //PRAGMA_DISABLE_OPTIMIZATION ///////////////////////////////////////////////////// // FAnimNode_RigidBody #define LOCTEXT_NAMESPACE "ImmediatePhysics" DEFINE_STAT(STAT_RigidBodyNodeInitTime); CSV_DECLARE_CATEGORY_MODULE_EXTERN(ENGINE_API, Animation); #if UE_BUILD_SHIPPING || UE_BUILD_TEST DECLARE_LOG_CATEGORY_EXTERN(LogRBAN, Log, Warning); #else DECLARE_LOG_CATEGORY_EXTERN(LogRBAN, Log, All); #endif DEFINE_LOG_CATEGORY(LogRBAN); TAutoConsoleVariable CVarEnableRigidBodyNode(TEXT("p.RigidBodyNode"), 1, TEXT("Enables/disables the whole rigid body node system. When disabled, avoids all allocations and runtime costs. Can be used to disable RB Nodes on low-end platforms."), ECVF_Default); TAutoConsoleVariable CVarEnableRigidBodyNodeSimulation(TEXT("p.RigidBodyNode.EnableSimulation"), 1, TEXT("Runtime Enable/Disable RB Node Simulation for debugging and testing (node is initialized and bodies and constraints are created, even when disabled.)"), ECVF_Default); TAutoConsoleVariable CVarRigidBodyLODThreshold(TEXT("p.RigidBodyLODThreshold"), -1, TEXT("Max LOD that rigid body node is allowed to run on. Provides a global threshold that overrides per-node the LODThreshold property. -1 means no override."), ECVF_Scalability); int32 RBAN_MaxSubSteps = 4; bool bRBAN_EnableTimeBasedReset = true; bool bRBAN_EnableComponentAcceleration = true; int32 RBAN_WorldObjectExpiry = 4; FAutoConsoleVariableRef CVarRigidBodyNodeMaxSteps(TEXT("p.RigidBodyNode.MaxSubSteps"), RBAN_MaxSubSteps, TEXT("Set the maximum number of simulation steps in the update loop"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeEnableTimeBasedReset(TEXT("p.RigidBodyNode.EnableTimeBasedReset"), bRBAN_EnableTimeBasedReset, TEXT("If true, Rigid Body nodes are reset when they have not been updated for a while (default true)"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeEnableComponentAcceleration(TEXT("p.RigidBodyNode.EnableComponentAcceleration"), bRBAN_EnableComponentAcceleration, TEXT("Enable/Disable the simple acceleration transfer system for component- or bone-space simulation"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeWorldObjectExpiry(TEXT("p.RigidBodyNode.WorldObjectExpiry"), RBAN_WorldObjectExpiry, TEXT("World objects are removed from the simulation if not detected after this many tests"), ECVF_Default); // FSimSpaceSettings forced overrides for testing bool bRBAN_SimSpace_EnableOverride = false; FSimSpaceSettings RBAN_SimSpaceOverride; FAutoConsoleVariableRef CVarRigidBodyNodeSpaceOverride(TEXT("p.RigidBodyNode.Space.Override"), bRBAN_SimSpace_EnableOverride, TEXT("Force-enable the advanced simulation space movement forces"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceMasterAlpha(TEXT("p.RigidBodyNode.Space.MasterAlpha"), RBAN_SimSpaceOverride.MasterAlpha, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceVelScaleZ(TEXT("p.RigidBodyNode.Space.VelocityScaleZ"), RBAN_SimSpaceOverride.VelocityScaleZ, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceMaxCompLinVel(TEXT("p.RigidBodyNode.Space.MaxLinearVelocity"), RBAN_SimSpaceOverride.MaxLinearVelocity, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceMaxCompAngVel(TEXT("p.RigidBodyNode.Space.MaxAngularVelocity"), RBAN_SimSpaceOverride.MaxAngularVelocity, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceMaxCompLinAcc(TEXT("p.RigidBodyNode.Space.MaxLinearAcceleration"), RBAN_SimSpaceOverride.MaxLinearAcceleration, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceMaxCompAngAcc(TEXT("p.RigidBodyNode.Space.MaxAngularAcceleration"), RBAN_SimSpaceOverride.MaxAngularAcceleration, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); // LWC_TODO: Double support for console variables #if UE_LARGE_WORLD_COORDINATES_DISABLED FAutoConsoleVariableRef CVarRigidBodyNodeSpaceExternalLinearDragX(TEXT("p.RigidBodyNode.Space.ExternalLinearDrag.X"), RBAN_SimSpaceOverride.ExternalLinearDragV.X, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceExternalLinearDragY(TEXT("p.RigidBodyNode.Space.ExternalLinearDrag.Y"), RBAN_SimSpaceOverride.ExternalLinearDragV.Y, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceExternalLinearDragZ(TEXT("p.RigidBodyNode.Space.ExternalLinearDrag.Z"), RBAN_SimSpaceOverride.ExternalLinearDragV.Z, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceExternalLinearVelocityX(TEXT("p.RigidBodyNode.Space.ExternalLinearVelocity.X"), RBAN_SimSpaceOverride.ExternalLinearVelocity.X, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceExternalLinearVelocityY(TEXT("p.RigidBodyNode.Space.ExternalLinearVelocity.Y"), RBAN_SimSpaceOverride.ExternalLinearVelocity.Y, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); FAutoConsoleVariableRef CVarRigidBodyNodeSpaceExternalLinearVelocityZ(TEXT("p.RigidBodyNode.Space.ExternalLinearVelocity.Z"), RBAN_SimSpaceOverride.ExternalLinearVelocity.Z, TEXT("RBAN SimSpaceSettings overrides"), ECVF_Default); #endif bool bRBAN_UseDeferredTask = false; FAutoConsoleVariableRef CVarRigidBodyNodeDeferredSimulation(TEXT("p.RigidBodyNode.UseDeferredTask"), bRBAN_UseDeferredTask, TEXT("Whether to defer the simulation results by one frame so that they can run in a task"), ECVF_Default); // Array of priorities that can be indexed into with CVars, since task priorities cannot be set from scalability .ini static FAutoConsoleTaskPriority GRigidBodyNodeTaskPriorities[] = { FAutoConsoleTaskPriority( TEXT("p.RigidBodyNode.TaskPriorities.Maximum"), TEXT("Maximum priority for rigid body node simulation"), ENamedThreads::HighThreadPriority, ENamedThreads::HighTaskPriority, ENamedThreads::HighTaskPriority // High task priority when forced to normal thread priority ), FAutoConsoleTaskPriority( TEXT("p.RigidBodyNode.TaskPriorities.High"), TEXT("High priority for rigid body node simulation"), ENamedThreads::HighThreadPriority, ENamedThreads::NormalTaskPriority, ENamedThreads::NormalTaskPriority // Normal (lowest) task priority if forced to normal thread priority ), FAutoConsoleTaskPriority( TEXT("p.RigidBodyNode.TaskPriorities.Normal"), TEXT("Normal priority for rigid body node simulation"), ENamedThreads::NormalThreadPriority, ENamedThreads::NormalTaskPriority, ENamedThreads::NormalTaskPriority // Normal (lowest) task priority if forced to normal thread priority ), FAutoConsoleTaskPriority( TEXT("p.RigidBodyNode.TaskPriorities.Low"), TEXT("Low priority for rigid body node simulation"), ENamedThreads::BackgroundThreadPriority, ENamedThreads::HighTaskPriority, ENamedThreads::NormalTaskPriority // Normal (lowest) task priority if forced to normal thread priority ), FAutoConsoleTaskPriority( TEXT("p.RigidBodyNode.TaskPriorities.Minimum"), TEXT("Minimum priority for rigid body node simulation"), ENamedThreads::BackgroundThreadPriority, ENamedThreads::NormalTaskPriority, ENamedThreads::NormalTaskPriority // Normal (lowest) task priority if forced to normal thread priority ), }; static int32 GRigidBodyNodeSimulationTaskPriority = 0; FAutoConsoleVariableRef CVarRigidBodyNodeSimulationTaskPriority( TEXT("p.RigidBodyNode.TaskPriority.Simulation"), GRigidBodyNodeSimulationTaskPriority, TEXT("Task priority index for running the rigid body node simulation task"), ECVF_Default ); FSimSpaceSettings::FSimSpaceSettings() : MasterAlpha(0) , VelocityScaleZ(1) , MaxLinearVelocity(10000) , MaxAngularVelocity(10000) , MaxLinearAcceleration(10000) , MaxAngularAcceleration(10000) , ExternalLinearDrag_DEPRECATED(0) , ExternalLinearDragV(FVector::ZeroVector) , ExternalLinearVelocity(FVector::ZeroVector) , ExternalAngularVelocity(FVector::ZeroVector) { } void FSimSpaceSettings::PostSerialize(const FArchive& Ar) { if (Ar.IsLoading()) { if (ExternalLinearDrag_DEPRECATED != 0.0f) { ExternalLinearDragV = FVector(ExternalLinearDrag_DEPRECATED, ExternalLinearDrag_DEPRECATED, ExternalLinearDrag_DEPRECATED); } } } FAnimNode_RigidBody::FAnimNode_RigidBody() : OverridePhysicsAsset(nullptr) , PreviousCompWorldSpaceTM() , CurrentTransform() , PreviousTransform() , UsePhysicsAsset(nullptr) , OverrideWorldGravity(0.0f) , ExternalForce(0.0f) , ComponentLinearAccScale(0.0f) , ComponentLinearVelScale(0.0f) , ComponentAppliedLinearAccClamp(10000.0f) , SimSpaceSettings() , CachedBoundsScale(1.2f) , BaseBoneRef() , OverlapChannel(ECC_WorldStatic) , SimulationSpace(ESimulationSpace::ComponentSpace) , bForceDisableCollisionBetweenConstraintBodies(false) , ResetSimulatedTeleportType(ETeleportType::None) , bEnableWorldGeometry(false) , bOverrideWorldGravity(false) , bTransferBoneVelocities(false) , bFreezeIncomingPoseOnStart(false) , bClampLinearTranslationLimitToRefPose(false) , WorldSpaceMinimumScale(0.01f) , EvaluationResetTime(0.01f) , bEnabled(false) , bSimulationStarted(false) , bCheckForBodyTransformInit(false) #if WITH_EDITORONLY_DATA , bComponentSpaceSimulation_DEPRECATED(true) #endif , WorldTimeSeconds(0.0f) , LastEvalTimeSeconds(0.0f) , AccumulatedDeltaTime(0.0f) , AnimPhysicsMinDeltaTime(0.0f) , bSimulateAnimPhysicsAfterReset(false) , SkelMeshCompWeakPtr() , PhysicsSimulation(nullptr) , SolverIterations() , SimulationTaskState() , OutputBoneData() , Bodies() , SkeletonBoneIndexToBodyIndex() , BodyAnimData() , Constraints() , PendingRadialForces() , PerSolverField() , ComponentsInSim() , ComponentsInSimTick(0) , WorldSpaceGravity(0.0f) , TotalMass(0.0f) , CachedBounds(FVector::ZeroVector, 0.0f) , QueryParams(NAME_None, FCollisionQueryParams::GetUnknownStatId()) , PhysScene(nullptr) , UnsafeWorld(nullptr) , UnsafeOwner(nullptr) , CapturedBoneVelocityBoneContainer() , CapturedBoneVelocityPose() , CapturedFrozenPose() , CapturedFrozenCurves() , PreviousComponentLinearVelocity(0.0f) , SimSpacePreviousComponentToWorld() , SimSpacePreviousBoneToComponent() , SimSpacePreviousComponentLinearVelocity(0.0f) , SimSpacePreviousComponentAngularVelocity(0.0f) , SimSpacePreviousBoneLinearVelocity(0.0f) , SimSpacePreviousBoneAngularVelocity(0.0f) { } FAnimNode_RigidBody::~FAnimNode_RigidBody() { DestroyPhysicsSimulation(); } void FAnimNode_RigidBody::GatherDebugData(FNodeDebugData& DebugData) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(GatherDebugData) FString DebugLine = DebugData.GetNodeName(this); DebugLine += "("; AddDebugNodeData(DebugLine); DebugLine += ")"; DebugData.AddDebugItem(DebugLine); const bool bUsingFrozenPose = bFreezeIncomingPoseOnStart && bSimulationStarted && (CapturedFrozenPose.GetPose().GetNumBones() > 0); if (!bUsingFrozenPose) { ComponentPose.GatherDebugData(DebugData); } } FTransform SpaceToWorldTransform(ESimulationSpace Space, const FTransform& ComponentToWorld, const FTransform& BaseBoneTM) { switch (Space) { case ESimulationSpace::ComponentSpace: return ComponentToWorld; case ESimulationSpace::WorldSpace: return FTransform::Identity; case ESimulationSpace::BaseBoneSpace: return BaseBoneTM * ComponentToWorld; default: return FTransform::Identity; } } FVector WorldVectorToSpaceNoScale(ESimulationSpace Space, const FVector& WorldDir, const FTransform& ComponentToWorld, const FTransform& BaseBoneTM) { switch(Space) { case ESimulationSpace::ComponentSpace: return ComponentToWorld.InverseTransformVectorNoScale(WorldDir); case ESimulationSpace::WorldSpace: return WorldDir; case ESimulationSpace::BaseBoneSpace: return BaseBoneTM.InverseTransformVectorNoScale(ComponentToWorld.InverseTransformVectorNoScale(WorldDir)); default: return FVector::ZeroVector; } } FVector WorldPositionToSpace(ESimulationSpace Space, const FVector& WorldPoint, const FTransform& ComponentToWorld, const FTransform& BaseBoneTM) { switch (Space) { case ESimulationSpace::ComponentSpace: return ComponentToWorld.InverseTransformPosition(WorldPoint); case ESimulationSpace::WorldSpace: return WorldPoint; case ESimulationSpace::BaseBoneSpace: return BaseBoneTM.InverseTransformPosition(ComponentToWorld.InverseTransformPosition(WorldPoint)); default: return FVector::ZeroVector; } } FORCEINLINE_DEBUGGABLE FTransform ConvertCSTransformToSimSpace(ESimulationSpace SimulationSpace, const FTransform& InCSTransform, const FTransform& ComponentToWorld, const FTransform& BaseBoneTM) { switch (SimulationSpace) { case ESimulationSpace::ComponentSpace: return InCSTransform; case ESimulationSpace::WorldSpace: return InCSTransform * ComponentToWorld; case ESimulationSpace::BaseBoneSpace: return InCSTransform.GetRelativeTransform(BaseBoneTM); break; default: ensureMsgf(false, TEXT("Unsupported Simulation Space")); return InCSTransform; } } void FAnimNode_RigidBody::UpdateComponentPose_AnyThread(const FAnimationUpdateContext& Context) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(UpdateComponentPose_AnyThread) // Only freeze update graph after initial update, as we want to get that pose through. if (bFreezeIncomingPoseOnStart && bSimulationStarted && ResetSimulatedTeleportType == ETeleportType::None) { // If we have a Frozen Pose captured, // then we don't need to update the rest of the graph. if (CapturedFrozenPose.GetPose().GetNumBones() > 0) { } else { // Create a new context with zero deltatime to freeze time in rest of the graph. // This will be used to capture a frozen pose. FAnimationUpdateContext FrozenContext = Context.FractionalWeightAndTime(1.f, 0.f); Super::UpdateComponentPose_AnyThread(FrozenContext); } } else { Super::UpdateComponentPose_AnyThread(Context); } } void FAnimNode_RigidBody::EvaluateComponentPose_AnyThread(FComponentSpacePoseContext& Output) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(EvaluateComponentPose_AnyThread) if (bFreezeIncomingPoseOnStart && bSimulationStarted) { // If we have a Frozen Pose captured, use it. // Only after our intialize setup. As we need new pose for that. if (ResetSimulatedTeleportType == ETeleportType::None && (CapturedFrozenPose.GetPose().GetNumBones() > 0)) { Output.Pose.CopyPose(CapturedFrozenPose); Output.Curve.CopyFrom(CapturedFrozenCurves); } // Otherwise eval graph to capture it. else { Super::EvaluateComponentPose_AnyThread(Output); CapturedFrozenPose.CopyPose(Output.Pose); CapturedFrozenCurves.CopyFrom(Output.Curve); } } else { Super::EvaluateComponentPose_AnyThread(Output); } // Capture incoming pose if 'bTransferBoneVelocities' is set. // That is, until simulation starts. if (bTransferBoneVelocities && !bSimulationStarted) { CapturedBoneVelocityPose.CopyPose(Output.Pose); CapturedBoneVelocityPose.CopyAndAssignBoneContainer(CapturedBoneVelocityBoneContainer); } } void FAnimNode_RigidBody::InitializeNewBodyTransformsDuringSimulation(FComponentSpacePoseContext& Output, const FTransform& ComponentTransform, const FTransform& BaseBoneTM) { for (const FOutputBoneData& OutputData : OutputBoneData) { const int32 BodyIndex = OutputData.BodyIndex; FBodyAnimData& BodyData = BodyAnimData[BodyIndex]; if (!BodyData.bBodyTransformInitialized) { BodyData.bBodyTransformInitialized = true; // If we have a parent body, we need to grab relative transforms to it. if (OutputData.ParentBodyIndex != INDEX_NONE) { ensure(BodyAnimData[OutputData.ParentBodyIndex].bBodyTransformInitialized); FTransform BodyRelativeTransform = FTransform::Identity; for (const FCompactPoseBoneIndex& CompactBoneIndex : OutputData.BoneIndicesToParentBody) { const FTransform& LocalSpaceTM = Output.Pose.GetLocalSpaceTransform(CompactBoneIndex); BodyRelativeTransform = BodyRelativeTransform * LocalSpaceTM; } const FTransform WSBodyTM = BodyRelativeTransform * Bodies[OutputData.ParentBodyIndex]->GetWorldTransform(); Bodies[BodyIndex]->InitWorldTransform(WSBodyTM); BodyAnimData[BodyIndex].RefPoseLength = BodyRelativeTransform.GetLocation().Size(); } // If we don't have a parent body, then we can just grab the incoming pose in component space. else { const FTransform& ComponentSpaceTM = Output.Pose.GetComponentSpaceTransform(OutputData.CompactPoseBoneIndex); const FTransform BodyTM = ConvertCSTransformToSimSpace(SimulationSpace, ComponentSpaceTM, ComponentTransform, BaseBoneTM); Bodies[BodyIndex]->InitWorldTransform(BodyTM); } } } } void FAnimNode_RigidBody::InitSimulationSpace( const FTransform& ComponentToWorld, const FTransform& BoneToComponent) { SimSpacePreviousComponentToWorld = ComponentToWorld; SimSpacePreviousBoneToComponent = BoneToComponent; SimSpacePreviousComponentLinearVelocity = FVector::ZeroVector; SimSpacePreviousComponentAngularVelocity = FVector::ZeroVector; SimSpacePreviousBoneLinearVelocity = FVector::ZeroVector; SimSpacePreviousBoneAngularVelocity = FVector::ZeroVector; } void FAnimNode_RigidBody::CalculateSimulationSpace( ESimulationSpace Space, const FTransform& ComponentToWorld, const FTransform& BoneToComponent, const float Dt, const FSimSpaceSettings& Settings, FTransform& SpaceTransform, FVector& SpaceLinearVel, FVector& SpaceAngularVel, FVector& SpaceLinearAcc, FVector& SpaceAngularAcc) { // World-space transform of the simulation space SpaceTransform = SpaceToWorldTransform(Space, ComponentToWorld, BoneToComponent); SpaceLinearVel = FVector::ZeroVector; SpaceAngularVel = FVector::ZeroVector; SpaceLinearAcc = FVector::ZeroVector; SpaceAngularAcc = FVector::ZeroVector; // If the system is disabled, nothing else to do if ((Settings.MasterAlpha == 0.0f) || (Dt < SMALL_NUMBER)) { return; } if (Space == ESimulationSpace::WorldSpace) { SpaceLinearVel = Settings.ExternalLinearVelocity; SpaceAngularVel = Settings.ExternalAngularVelocity; return; } // World-space component velocity and acceleration FVector CompLinVel = Chaos::FVec3::CalculateVelocity(SimSpacePreviousComponentToWorld.GetTranslation(), ComponentToWorld.GetTranslation(), Dt); FVector CompAngVel = Chaos::FRotation3::CalculateAngularVelocity(SimSpacePreviousComponentToWorld.GetRotation(), ComponentToWorld.GetRotation(), Dt); FVector CompLinAcc = (CompLinVel - SimSpacePreviousComponentLinearVelocity) / Dt; FVector CompAngAcc = (CompAngVel - SimSpacePreviousComponentAngularVelocity) / Dt; SimSpacePreviousComponentToWorld = ComponentToWorld; SimSpacePreviousComponentLinearVelocity = CompLinVel; SimSpacePreviousComponentAngularVelocity = CompAngVel; if (Space == ESimulationSpace::ComponentSpace) { CompLinVel.Z *= Settings.VelocityScaleZ; CompLinAcc.Z *= Settings.VelocityScaleZ; SpaceLinearVel = CompLinVel.GetClampedToMaxSize(Settings.MaxLinearVelocity) + Settings.ExternalLinearVelocity; SpaceAngularVel = CompAngVel.GetClampedToMaxSize(Settings.MaxAngularVelocity) + Settings.ExternalAngularVelocity; SpaceLinearAcc = CompLinAcc.GetClampedToMaxSize(Settings.MaxLinearAcceleration); SpaceAngularAcc = CompAngAcc.GetClampedToMaxSize(Settings.MaxAngularAcceleration); return; } if (Space == ESimulationSpace::BaseBoneSpace) { // World-space component-relative bone velocity and acceleration FVector BoneLinVel = Chaos::FVec3::CalculateVelocity(SimSpacePreviousBoneToComponent.GetTranslation(), BoneToComponent.GetTranslation(), Dt); FVector BoneAngVel = Chaos::FRotation3::CalculateAngularVelocity(SimSpacePreviousBoneToComponent.GetRotation(), BoneToComponent.GetRotation(), Dt); BoneLinVel = ComponentToWorld.TransformVector(BoneLinVel); BoneAngVel = ComponentToWorld.TransformVector(BoneAngVel); FVector BoneLinAcc = (BoneLinVel - SimSpacePreviousBoneLinearVelocity) / Dt; FVector BoneAngAcc = (BoneAngVel - SimSpacePreviousBoneAngularVelocity) / Dt; SimSpacePreviousBoneToComponent = BoneToComponent; SimSpacePreviousBoneLinearVelocity = BoneLinVel; SimSpacePreviousBoneAngularVelocity = BoneAngVel; // World-space bone velocity and acceleration FVector NetAngVel = CompAngVel + BoneAngVel; FVector NetAngAcc = CompAngAcc + BoneAngAcc; // If we limit the angular velocity, we also need to limit the component of linear velocity that comes from (angvel x offset) float AngVelScale = 1.0f; float NetAngVelLenSq = NetAngVel.SizeSquared(); if (NetAngVelLenSq > FMath::Square(Settings.MaxAngularVelocity)) { AngVelScale = Settings.MaxAngularVelocity * FMath::InvSqrt(NetAngVelLenSq); } // Add the linear velocity and acceleration that comes from rotation of the space about the component // NOTE: Component angular velocity constribution is scaled FVector SpaceCompOffset = ComponentToWorld.TransformVector(BoneToComponent.GetTranslation()); FVector NetLinVel = CompLinVel + BoneLinVel + FVector::CrossProduct(AngVelScale * CompAngVel, SpaceCompOffset); FVector NetLinAcc = CompLinAcc + BoneLinAcc + FVector::CrossProduct(AngVelScale * CompAngAcc, SpaceCompOffset); NetLinVel.Z *= Settings.VelocityScaleZ; NetLinAcc.Z *= Settings.VelocityScaleZ; SpaceLinearVel = NetLinVel.GetClampedToMaxSize(Settings.MaxLinearVelocity) + Settings.ExternalLinearVelocity; SpaceAngularVel = NetAngVel.GetClampedToMaxSize(Settings.MaxAngularVelocity) + Settings.ExternalAngularVelocity; SpaceLinearAcc = NetLinAcc.GetClampedToMaxSize(Settings.MaxLinearAcceleration); SpaceAngularAcc = NetAngAcc.GetClampedToMaxSize(Settings.MaxAngularAcceleration); return; } } DECLARE_CYCLE_STAT(TEXT("RigidBody_Eval"), STAT_RigidBody_Eval, STATGROUP_Anim); DECLARE_CYCLE_STAT(TEXT("RigidBodyNode_Simulation"), STAT_RigidBodyNode_Simulation, STATGROUP_Anim); DECLARE_CYCLE_STAT(TEXT("RigidBodyNode_SimulationWait"), STAT_RigidBodyNode_SimulationWait, STATGROUP_Anim); DECLARE_CYCLE_STAT(TEXT("FAnimNode_RigidBody::EvaluateSkeletalControl_AnyThread"), STAT_ImmediateEvaluateSkeletalControl, STATGROUP_ImmediatePhysics); class FRigidBodyNodeSimulationTask { public: FRigidBodyNodeSimulationTask(FAnimNode_RigidBody* InRigidBodyNode, const float InDeltaSeconds, const FVector& InSimSpaceGravity) : RigidBodyNode(InRigidBodyNode) , DeltaSeconds(InDeltaSeconds) , SimSpaceGravity(InSimSpaceGravity) { } static FORCEINLINE TStatId GetStatId() { RETURN_QUICK_DECLARE_CYCLE_STAT(FRigidBodyNodeSimulationTask, STATGROUP_TaskGraphTasks); } static FORCEINLINE ENamedThreads::Type GetDesiredThread() { const int32 PriorityIndex = FMath::Clamp(GRigidBodyNodeSimulationTaskPriority, 0, UE_ARRAY_COUNT(GRigidBodyNodeTaskPriorities) - 1); return GRigidBodyNodeTaskPriorities[PriorityIndex].Get(); } static FORCEINLINE ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; } void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& CompletionGraphEvent) { RigidBodyNode->RunPhysicsSimulation(DeltaSeconds, SimSpaceGravity); } private: FAnimNode_RigidBody* const RigidBodyNode; const float DeltaSeconds; const FVector SimSpaceGravity; }; void FAnimNode_RigidBody::RunPhysicsSimulation(float DeltaSeconds, const FVector& SimSpaceGravity) { SCOPE_CYCLE_COUNTER(STAT_RigidBodyNode_Simulation); CSV_SCOPED_TIMING_STAT(Animation, RigidBodyNodeSimulation); FScopeCycleCounterUObject AdditionalScope(UsePhysicsAsset, GET_STATID(STAT_RigidBodyNode_Simulation)); const int32 MaxSteps = RBAN_MaxSubSteps; const float MaxDeltaSeconds = 1.f / 30.f; #if !WITH_CHAOS const int32 NumSteps = FMath::Clamp(FMath::CeilToInt(DeltaSeconds / MaxDeltaSeconds), 1, MaxSteps); const float StepDeltaTime = DeltaSeconds / float(NumSteps); for (int32 Step = 1; Step <= NumSteps; Step++) { // We call the _AssumesLocked version here without a lock as the simulation is local to this node and we know // we're not going to alter anything while this is running. PhysicsSimulation->Simulate_AssumesLocked(StepDeltaTime, SimSpaceGravity); } #else PhysicsSimulation->Simulate_AssumesLocked(DeltaSeconds, MaxDeltaSeconds, MaxSteps, SimSpaceGravity); #endif } void FAnimNode_RigidBody::FlushDeferredSimulationTask() { FGraphEventRef& CompletionEvent = SimulationTaskState.SimulationCompletionEvent; if (CompletionEvent && !CompletionEvent->IsComplete()) { SCOPE_CYCLE_COUNTER(STAT_RigidBodyNode_SimulationWait); CSV_SCOPED_TIMING_STAT(Animation, RigidBodyNodeSimulationWait); FTaskGraphInterface::Get().WaitUntilTasksComplete({CompletionEvent}); } } void FAnimNode_RigidBody::DestroyPhysicsSimulation() { FlushDeferredSimulationTask(); delete PhysicsSimulation; PhysicsSimulation = nullptr; } void FAnimNode_RigidBody::EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output, TArray& OutBoneTransforms) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(EvaluateSkeletalControl_AnyThread) SCOPE_CYCLE_COUNTER(STAT_RigidBody_Eval); CSV_SCOPED_TIMING_STAT(Animation, RigidBodyEval); SCOPE_CYCLE_COUNTER(STAT_ImmediateEvaluateSkeletalControl); //SCOPED_NAMED_EVENT_TEXT("FAnimNode_RigidBody::EvaluateSkeletalControl_AnyThread", FColor::Magenta); if (CVarEnableRigidBodyNodeSimulation.GetValueOnAnyThread() == 0) { return; } const float DeltaSeconds = AccumulatedDeltaTime; AccumulatedDeltaTime = 0.f; if (bEnabled && PhysicsSimulation) { FlushDeferredSimulationTask(); const FBoneContainer& BoneContainer = Output.Pose.GetPose().GetBoneContainer(); const FTransform CompWorldSpaceTM = Output.AnimInstanceProxy->GetComponentTransform(); bool bFirstEvalSinceReset = !Output.AnimInstanceProxy->GetEvaluationCounter().HasEverBeenUpdated(); // First-frame initialization if (bFirstEvalSinceReset) { PreviousCompWorldSpaceTM = CompWorldSpaceTM; ResetSimulatedTeleportType = ETeleportType::ResetPhysics; } // See if we need to reset physics because too much time passed since our last update (e.g., because we we off-screen for a while), // in which case the current sim state may be too far from the current anim pose. This is mostly a problem with world-space // simulation, whereas bone- and component-space sims can be fairly robust against missing updates. // Don't do this on first frame or if time-based reset is disabled. if ((EvaluationResetTime > 0.0f) && !bFirstEvalSinceReset) { // NOTE: under normal conditions, when this anim node is being serviced at the usual rate (which may not be every frame // if URO is enabled), we expect that WorldTimeSeconds == (LastEvalTimeSeconds + DeltaSeconds). DeltaSeconds is the // accumulated time since the last update, including frames dropped by URO, but not frames dropped because of // being off-screen or LOD changes. if (WorldTimeSeconds - (LastEvalTimeSeconds + DeltaSeconds) > EvaluationResetTime) { UE_LOG(LogRBAN, Verbose, TEXT("%s Time-Based Reset"), *Output.AnimInstanceProxy->GetAnimInstanceName()); ResetSimulatedTeleportType = ETeleportType::ResetPhysics; } } // Update the evaluation time to the current time LastEvalTimeSeconds = WorldTimeSeconds; // Disable simulation below minimum scale in world space mode. World space sim doesn't play nice with scale anyway - we do not scale joint offets or collision shapes. if ((SimulationSpace == ESimulationSpace::WorldSpace) && (CompWorldSpaceTM.GetScale3D().SizeSquared() < WorldSpaceMinimumScale * WorldSpaceMinimumScale)) { return; } const FTransform BaseBoneTM = Output.Pose.GetComponentSpaceTransform(BaseBoneRef.GetCompactPoseIndex(BoneContainer)); // Initialize potential new bodies because of LOD change. if (ResetSimulatedTeleportType == ETeleportType::None && bCheckForBodyTransformInit) { bCheckForBodyTransformInit = false; InitializeNewBodyTransformsDuringSimulation(Output, CompWorldSpaceTM, BaseBoneTM); } // If time advances, update simulation // Reset if necessary bool bDynamicsReset = (ResetSimulatedTeleportType != ETeleportType::None); if (bDynamicsReset) { // Capture bone velocities if we have captured a bone velocity pose. if (bTransferBoneVelocities && (CapturedBoneVelocityPose.GetPose().GetNumBones() > 0)) { for (const FOutputBoneData& OutputData : OutputBoneData) { const int32 BodyIndex = OutputData.BodyIndex; FBodyAnimData& BodyData = BodyAnimData[BodyIndex]; if (BodyData.bIsSimulated) { const FCompactPoseBoneIndex NextCompactPoseBoneIndex = OutputData.CompactPoseBoneIndex; // Convert CompactPoseBoneIndex to SkeletonBoneIndex... const FSkeletonPoseBoneIndex PoseSkeletonBoneIndex = BoneContainer.GetSkeletonPoseIndexFromCompactPoseIndex(NextCompactPoseBoneIndex); // ... So we can convert to the captured pose CompactPoseBoneIndex. // In case there was a LOD change, and poses are not compatible anymore. const FCompactPoseBoneIndex PrevCompactPoseBoneIndex = CapturedBoneVelocityBoneContainer.GetCompactPoseIndexFromSkeletonPoseIndex(PoseSkeletonBoneIndex); if (PrevCompactPoseBoneIndex.IsValid()) { const FTransform PrevCSTM = CapturedBoneVelocityPose.GetComponentSpaceTransform(PrevCompactPoseBoneIndex); const FTransform NextCSTM = Output.Pose.GetComponentSpaceTransform(NextCompactPoseBoneIndex); const FTransform PrevSSTM = ConvertCSTransformToSimSpace(SimulationSpace, PrevCSTM, CompWorldSpaceTM, BaseBoneTM); const FTransform NextSSTM = ConvertCSTransformToSimSpace(SimulationSpace, NextCSTM, CompWorldSpaceTM, BaseBoneTM); if(DeltaSeconds > 0.0f) { // Linear Velocity BodyData.TransferedBoneLinearVelocity = ((NextSSTM.GetLocation() - PrevSSTM.GetLocation()) / DeltaSeconds); // Angular Velocity const FQuat DeltaRotation = (NextSSTM.GetRotation().Inverse() * PrevSSTM.GetRotation()); const float RotationAngle = DeltaRotation.GetAngle() / DeltaSeconds; BodyData.TransferedBoneAngularVelocity = (FQuat(DeltaRotation.GetRotationAxis(), RotationAngle)); } else { BodyData.TransferedBoneLinearVelocity = (FVector::ZeroVector); BodyData.TransferedBoneAngularVelocity = (FQuat::Identity); } } } } } switch(ResetSimulatedTeleportType) { case ETeleportType::TeleportPhysics: { UE_LOG(LogRBAN, Verbose, TEXT("%s TeleportPhysics (Scale: %f %f %f)"), *Output.AnimInstanceProxy->GetAnimInstanceName(), CompWorldSpaceTM.GetScale3D().X, CompWorldSpaceTM.GetScale3D().Y, CompWorldSpaceTM.GetScale3D().Z); // Teleport bodies. for (const FOutputBoneData& OutputData : OutputBoneData) { const int32 BodyIndex = OutputData.BodyIndex; BodyAnimData[BodyIndex].bBodyTransformInitialized = true; FTransform BodyTM = Bodies[BodyIndex]->GetWorldTransform(); FTransform ComponentSpaceTM; switch(SimulationSpace) { case ESimulationSpace::ComponentSpace: ComponentSpaceTM = BodyTM; break; case ESimulationSpace::WorldSpace: ComponentSpaceTM = BodyTM.GetRelativeTransform(PreviousCompWorldSpaceTM); break; case ESimulationSpace::BaseBoneSpace: ComponentSpaceTM = BodyTM * BaseBoneTM; break; default: ensureMsgf(false, TEXT("Unsupported Simulation Space")); ComponentSpaceTM = BodyTM; } BodyTM = ConvertCSTransformToSimSpace(SimulationSpace, ComponentSpaceTM, CompWorldSpaceTM, BaseBoneTM); Bodies[BodyIndex]->SetWorldTransform(BodyTM); if (OutputData.ParentBodyIndex != INDEX_NONE) { BodyAnimData[BodyIndex].RefPoseLength = BodyTM.GetRelativeTransform(Bodies[OutputData.ParentBodyIndex]->GetWorldTransform()).GetLocation().Size(); } } } break; case ETeleportType::ResetPhysics: { UE_LOG(LogRBAN, Verbose, TEXT("%s ResetPhysics (Scale: %f %f %f)"), *Output.AnimInstanceProxy->GetAnimInstanceName(), CompWorldSpaceTM.GetScale3D().X, CompWorldSpaceTM.GetScale3D().Y, CompWorldSpaceTM.GetScale3D().Z); InitSimulationSpace(CompWorldSpaceTM, BaseBoneTM); // Completely reset bodies. for (const FOutputBoneData& OutputData : OutputBoneData) { const int32 BodyIndex = OutputData.BodyIndex; BodyAnimData[BodyIndex].bBodyTransformInitialized = true; const FTransform& ComponentSpaceTM = Output.Pose.GetComponentSpaceTransform(OutputData.CompactPoseBoneIndex); const FTransform BodyTM = ConvertCSTransformToSimSpace(SimulationSpace, ComponentSpaceTM, CompWorldSpaceTM, BaseBoneTM); Bodies[BodyIndex]->InitWorldTransform(BodyTM); if (OutputData.ParentBodyIndex != INDEX_NONE) { BodyAnimData[BodyIndex].RefPoseLength = BodyTM.GetRelativeTransform(Bodies[OutputData.ParentBodyIndex]->GetWorldTransform()).GetLocation().Size(); } } } break; } // Always reset after a teleport PreviousCompWorldSpaceTM = CompWorldSpaceTM; ResetSimulatedTeleportType = ETeleportType::None; PreviousComponentLinearVelocity = FVector::ZeroVector; } // Run simulation at a minimum of 30 FPS to prevent system from exploding. // DeltaTime can be higher due to URO, so take multiple iterations in that case. const bool bUseDeferredSimulationTask = bRBAN_UseDeferredTask; FVector SimSpaceGravity(0.f); // Only need to tick physics if we didn't reset and we have some time to simulate const bool bNeedsSimulationTick = ((bSimulateAnimPhysicsAfterReset || !bDynamicsReset) && DeltaSeconds > AnimPhysicsMinDeltaTime); if (bNeedsSimulationTick) { // Transfer bone velocities previously captured. if (bTransferBoneVelocities && (CapturedBoneVelocityPose.GetPose().GetNumBones() > 0)) { for (const FOutputBoneData& OutputData : OutputBoneData) { const int32 BodyIndex = OutputData.BodyIndex; const FBodyAnimData& BodyData = BodyAnimData[BodyIndex]; if (BodyData.bIsSimulated) { ImmediatePhysics::FActorHandle* Body = Bodies[BodyIndex]; Body->SetLinearVelocity(BodyData.TransferedBoneLinearVelocity); const FQuat AngularVelocity = BodyData.TransferedBoneAngularVelocity; Body->SetAngularVelocity(AngularVelocity.GetRotationAxis() * AngularVelocity.GetAngle()); } } // Free up our captured pose after it's been used. CapturedBoneVelocityPose.Empty(); } else if ((SimulationSpace != ESimulationSpace::WorldSpace) && bRBAN_EnableComponentAcceleration) { if (!ComponentLinearVelScale.IsNearlyZero() || !ComponentLinearAccScale.IsNearlyZero()) { // Calc linear velocity const FVector ComponentDeltaLocation = CurrentTransform.GetTranslation() - PreviousTransform.GetTranslation(); const FVector ComponentLinearVelocity = ComponentDeltaLocation / DeltaSeconds; // Apply acceleration that opposed velocity (basically 'drag') FVector ApplyLinearAcc = WorldVectorToSpaceNoScale(SimulationSpace, -ComponentLinearVelocity, CompWorldSpaceTM, BaseBoneTM) * ComponentLinearVelScale; // Calc linear acceleration const FVector ComponentLinearAcceleration = (ComponentLinearVelocity - PreviousComponentLinearVelocity) / DeltaSeconds; PreviousComponentLinearVelocity = ComponentLinearVelocity; // Apply opposite acceleration to bodies ApplyLinearAcc += WorldVectorToSpaceNoScale(SimulationSpace, -ComponentLinearAcceleration, CompWorldSpaceTM, BaseBoneTM) * ComponentLinearAccScale; // Iterate over bodies for (const FOutputBoneData& OutputData : OutputBoneData) { const int32 BodyIndex = OutputData.BodyIndex; const FBodyAnimData& BodyData = BodyAnimData[BodyIndex]; if (BodyData.bIsSimulated) { ImmediatePhysics::FActorHandle* Body = Bodies[BodyIndex]; // Apply const float BodyInvMass = Body->GetInverseMass(); if (BodyInvMass > 0.f) { // Final desired acceleration to apply to body FVector FinalBodyLinearAcc = ApplyLinearAcc; // Clamp if desired if (!ComponentAppliedLinearAccClamp.IsNearlyZero()) { FinalBodyLinearAcc = FinalBodyLinearAcc.BoundToBox(-ComponentAppliedLinearAccClamp, ComponentAppliedLinearAccClamp); } // Apply to body Body->AddForce(FinalBodyLinearAcc / BodyInvMass); } } } } } // @todo(ccaulfield): We should be interpolating kinematic targets for each sub-step below for (const FOutputBoneData& OutputData : OutputBoneData) { const int32 BodyIndex = OutputData.BodyIndex; if (!BodyAnimData[BodyIndex].bIsSimulated) { const FTransform& ComponentSpaceTM = Output.Pose.GetComponentSpaceTransform(OutputData.CompactPoseBoneIndex); const FTransform BodyTM = ConvertCSTransformToSimSpace(SimulationSpace, ComponentSpaceTM, CompWorldSpaceTM, BaseBoneTM); Bodies[BodyIndex]->SetKinematicTarget(BodyTM); } } UpdateWorldForces(CompWorldSpaceTM, BaseBoneTM, DeltaSeconds); SimSpaceGravity = WorldVectorToSpaceNoScale(SimulationSpace, WorldSpaceGravity, CompWorldSpaceTM, BaseBoneTM); #if WITH_CHAOS FSimSpaceSettings* UseSimSpaceSettings = &SimSpaceSettings; if (bRBAN_SimSpace_EnableOverride) { UseSimSpaceSettings = &RBAN_SimSpaceOverride; } FTransform SimulationTransform; FVector SimulationLinearVelocity; FVector SimulationAngularVelocity; FVector SimulationLinearAcceleration; FVector SimulationAngularAcceleration; CalculateSimulationSpace( SimulationSpace, CompWorldSpaceTM, BaseBoneTM, DeltaSeconds, *UseSimSpaceSettings, SimulationTransform, SimulationLinearVelocity, SimulationAngularVelocity, SimulationLinearAcceleration, SimulationAngularAcceleration); UpdateWorldObjects(SimulationTransform); PhysicsSimulation->UpdateSimulationSpace( SimulationTransform, SimulationLinearVelocity, SimulationAngularVelocity, SimulationLinearAcceleration, SimulationAngularAcceleration); PhysicsSimulation->SetSimulationSpaceSettings( UseSimSpaceSettings->MasterAlpha, UseSimSpaceSettings->ExternalLinearDragV); PhysicsSimulation->SetSolverIterations( SolverIterations.FixedTimeStep, SolverIterations.SolverIterations, SolverIterations.JointIterations, SolverIterations.CollisionIterations, SolverIterations.SolverPushOutIterations, SolverIterations.JointPushOutIterations, SolverIterations.CollisionPushOutIterations); #endif if (!bUseDeferredSimulationTask) { RunPhysicsSimulation(DeltaSeconds, SimSpaceGravity); } } //write back to animation system const FTransform& SimulationWorldSpaceTM = bUseDeferredSimulationTask ? PreviousCompWorldSpaceTM : CompWorldSpaceTM; for (const FOutputBoneData& OutputData : OutputBoneData) { const int32 BodyIndex = OutputData.BodyIndex; if (BodyAnimData[BodyIndex].bIsSimulated) { FTransform BodyTM = Bodies[BodyIndex]->GetWorldTransform(); // if we clamp translation, we only do this when all linear translation are locked // // @todo(ccaulfield): this shouldn't be required with Chaos - projection should be handling it... if (bClampLinearTranslationLimitToRefPose &&BodyAnimData[BodyIndex].LinearXMotion == ELinearConstraintMotion::LCM_Locked && BodyAnimData[BodyIndex].LinearYMotion == ELinearConstraintMotion::LCM_Locked && BodyAnimData[BodyIndex].LinearZMotion == ELinearConstraintMotion::LCM_Locked) { // grab local space of length from ref pose // we have linear limit value - see if that works // calculate current local space from parent // find parent transform const int32 ParentBodyIndex = OutputData.ParentBodyIndex; FTransform ParentTransform = FTransform::Identity; if (ParentBodyIndex != INDEX_NONE) { ParentTransform = Bodies[ParentBodyIndex]->GetWorldTransform(); } // get local transform FTransform LocalTransform = BodyTM.GetRelativeTransform(ParentTransform); const float CurrentLength = LocalTransform.GetTranslation().Size(); // this is inconsistent with constraint. The actual linear limit is set by constraint if (!FMath::IsNearlyEqual(CurrentLength, BodyAnimData[BodyIndex].RefPoseLength, KINDA_SMALL_NUMBER)) { float RefPoseLength = BodyAnimData[BodyIndex].RefPoseLength; if (CurrentLength > RefPoseLength) { float Scale = (CurrentLength > KINDA_SMALL_NUMBER) ? RefPoseLength / CurrentLength : 0.f; // we don't use 1.f here because 1.f can create pops based on float issue. // so we only activate clamping when less than 90% if (Scale < 0.9f) { LocalTransform.ScaleTranslation(Scale); BodyTM = LocalTransform * ParentTransform; Bodies[BodyIndex]->SetWorldTransform(BodyTM); } } } } FTransform ComponentSpaceTM; switch(SimulationSpace) { case ESimulationSpace::ComponentSpace: ComponentSpaceTM = BodyTM; break; case ESimulationSpace::WorldSpace: ComponentSpaceTM = BodyTM.GetRelativeTransform(SimulationWorldSpaceTM); break; case ESimulationSpace::BaseBoneSpace: ComponentSpaceTM = BodyTM * BaseBoneTM; break; default: ensureMsgf(false, TEXT("Unsupported Simulation Space")); ComponentSpaceTM = BodyTM; } OutBoneTransforms.Add(FBoneTransform(OutputData.CompactPoseBoneIndex, ComponentSpaceTM)); } } // Deferred task must be started after we read actor poses to avoid a race if (bNeedsSimulationTick && bUseDeferredSimulationTask) { // FlushDeferredSimulationTask() should have already ensured task is done. ensure(!SimulationTaskState.SimulationCompletionEvent || SimulationTaskState.SimulationCompletionEvent->IsComplete()); SimulationTaskState.SimulationCompletionEvent = TGraphTask::CreateTask().ConstructAndDispatchWhenReady(this, DeltaSeconds, SimSpaceGravity); } PreviousCompWorldSpaceTM = CompWorldSpaceTM; } } void ComputeBodyInsertionOrder(TArray& InsertionOrder, const USkeletalMeshComponent& SKC) { //We want to ensure simulated bodies are sorted by LOD so that the first simulated bodies are at the highest LOD. //Since LOD2 is a subset of LOD1 which is a subset of LOD0 we can change the number of simulated bodies without any reordering //For this to work we must first insert all simulated bodies in the right order. We then insert all the kinematic bodies in the right order InsertionOrder.Reset(); if (SKC.SkeletalMesh == nullptr) { return; } const int32 NumLODs = SKC.GetNumLODs(); if(NumLODs > 0) { TArray RequiredBones0; TArray ComponentSpaceTMs0; SKC.ComputeRequiredBones(RequiredBones0, ComponentSpaceTMs0, 0, /*bIgnorePhysicsAsset=*/ true); TArray InSortedOrder; InSortedOrder.AddZeroed(SKC.SkeletalMesh->GetRefSkeleton().GetNum()); auto MergeIndices = [&InsertionOrder, &InSortedOrder](const TArray& RequiredBones) -> void { for (FBoneIndexType BoneIdx : RequiredBones) { if (!InSortedOrder[BoneIdx]) { InsertionOrder.Add(BoneIdx); } InSortedOrder[BoneIdx] = true; } }; for(int32 LodIdx = NumLODs - 1; LodIdx > 0; --LodIdx) { TArray RequiredBones; TArray ComponentSpaceTMs; SKC.ComputeRequiredBones(RequiredBones, ComponentSpaceTMs, LodIdx, /*bIgnorePhysicsAsset=*/ true); MergeIndices(RequiredBones); } MergeIndices(RequiredBones0); } } void FAnimNode_RigidBody::InitPhysics(const UAnimInstance* InAnimInstance) { LLM_SCOPE_BYNAME(TEXT("Animation/RigidBody")); SCOPE_CYCLE_COUNTER(STAT_RigidBodyNodeInitTime); DestroyPhysicsSimulation(); const USkeletalMeshComponent* SkeletalMeshComp = InAnimInstance->GetSkelMeshComponent(); const USkeletalMesh* SkeletalMeshAsset = SkeletalMeshComp->SkeletalMesh; USkeleton* SkeletonAsset = InAnimInstance->CurrentSkeleton; if(!SkeletalMeshAsset || !SkeletonAsset) { // Without both the skeleton and the mesh we can't create a new simulation. // The previous simulation has just been cleaned up above so we can return early here and not instantiate a new one return; } const FReferenceSkeleton& SkelMeshRefSkel = SkeletalMeshAsset->GetRefSkeleton(); UsePhysicsAsset = OverridePhysicsAsset ? ToRawPtr(OverridePhysicsAsset) : InAnimInstance->GetSkelMeshComponent()->GetPhysicsAsset(); ensure(SkeletonAsset == SkeletalMeshAsset->GetSkeleton()); const int32 SkelMeshLinkupIndex = SkeletonAsset->GetMeshLinkupIndex(SkeletalMeshAsset); ensure(SkelMeshLinkupIndex != INDEX_NONE); const FSkeletonToMeshLinkup& SkeletonToMeshLinkupTable = SkeletonAsset->LinkupCache[SkelMeshLinkupIndex]; const TArray& MeshToSkeletonBoneIndex = SkeletonToMeshLinkupTable.MeshToSkeletonTable; const int32 NumSkeletonBones = SkeletonAsset->GetReferenceSkeleton().GetNum(); SkeletonBoneIndexToBodyIndex.Reset(NumSkeletonBones); SkeletonBoneIndexToBodyIndex.Init(INDEX_NONE, NumSkeletonBones); PreviousTransform = SkeletalMeshComp->GetComponentToWorld(); ComponentsInSim.Reset(); ComponentsInSimTick = 0; if (UPhysicsSettings* Settings = UPhysicsSettings::Get()) { AnimPhysicsMinDeltaTime = Settings->AnimPhysicsMinDeltaTime; bSimulateAnimPhysicsAfterReset = Settings->bSimulateAnimPhysicsAfterReset; } else { AnimPhysicsMinDeltaTime = 0.f; bSimulateAnimPhysicsAfterReset = false; } bEnabled = UsePhysicsAsset && SkeletalMeshComp->GetAllowRigidBodyAnimNode() && CVarEnableRigidBodyNode.GetValueOnAnyThread() != 0; if(bEnabled) { PhysicsSimulation = new ImmediatePhysics::FSimulation(); const int32 NumBodies = UsePhysicsAsset->SkeletalBodySetups.Num(); Bodies.Empty(NumBodies); BodyAnimData.Reset(NumBodies); BodyAnimData.AddDefaulted(NumBodies); TotalMass = 0.f; // Instantiate a FBodyInstance/FConstraintInstance set that will be cloned into the Immediate Physics sim. // NOTE: We do not have a skeleton at the moment, so we have to use the ref pose TArray HighLevelBodyInstances; TArray HighLevelConstraintInstances; // Chaos relies on the initial pose to set up constraint positions bool bCreateBodiesInRefPose = (WITH_CHAOS != 0); SkeletalMeshComp->InstantiatePhysicsAssetRefPose( *UsePhysicsAsset, SimulationSpace == ESimulationSpace::WorldSpace ? SkeletalMeshComp->GetComponentToWorld().GetScale3D() : FVector(1.f), HighLevelBodyInstances, HighLevelConstraintInstances, nullptr, nullptr, INDEX_NONE, FPhysicsAggregateHandle(), bCreateBodiesInRefPose); TMap NamesToHandles; TArray IgnoreCollisionActors; TArray InsertionOrder; ComputeBodyInsertionOrder(InsertionOrder, *SkeletalMeshComp); // NOTE: NumBonesLOD0 may be less than NumBonesTotal, and it may be middle bones that are missing from LOD0. // In this case, LOD0 bone indices may be >= NumBonesLOD0, but always < NumBonesTotal. Arrays indexed by // bone index must be size NumBonesTotal. const int32 NumBonesLOD0 = InsertionOrder.Num(); const int32 NumBonesTotal = SkelMeshRefSkel.GetNum(); // If our skeleton is not the one that was used to build the PhysicsAsset, some bodies may be missing, or rearranged. // We need to map the original indices to the new bodies for use by the CollisionDisableTable. // NOTE: This array is indexed by the original BodyInstance body index (BodyInstance->InstanceBodyIndex) TArray BodyIndexToActorHandle; BodyIndexToActorHandle.AddZeroed(HighLevelBodyInstances.Num()); TArray BodiesSorted; BodiesSorted.AddZeroed(NumBonesTotal); for (FBodyInstance* BI : HighLevelBodyInstances) { if(BI->IsValidBodyInstance()) { BodiesSorted[BI->InstanceBoneIndex] = BI; } } // Create the immediate physics bodies for (FBoneIndexType InsertBone : InsertionOrder) { if (FBodyInstance* BodyInstance = BodiesSorted[InsertBone]) { UBodySetup* BodySetup = UsePhysicsAsset->SkeletalBodySetups[BodyInstance->InstanceBodyIndex]; bool bSimulated = (BodySetup->PhysicsType == EPhysicsType::PhysType_Simulated); ImmediatePhysics::EActorType ActorType = bSimulated ? ImmediatePhysics::EActorType::DynamicActor : ImmediatePhysics::EActorType::KinematicActor; ImmediatePhysics::FActorHandle* NewBodyHandle = PhysicsSimulation->CreateActor(ActorType, BodyInstance, BodyInstance->GetUnrealWorldTransform()); if (NewBodyHandle) { if (bSimulated) { const float InvMass = NewBodyHandle->GetInverseMass(); TotalMass += InvMass > 0.f ? 1.f / InvMass : 0.f; } const int32 BodyIndex = Bodies.Add(NewBodyHandle); const int32 SkeletonBoneIndex = MeshToSkeletonBoneIndex[InsertBone]; SkeletonBoneIndexToBodyIndex[SkeletonBoneIndex] = BodyIndex; BodyAnimData[BodyIndex].bIsSimulated = bSimulated; NamesToHandles.Add(BodySetup->BoneName, NewBodyHandle); BodyIndexToActorHandle[BodyInstance->InstanceBodyIndex] = NewBodyHandle; if (BodySetup->CollisionReponse == EBodyCollisionResponse::BodyCollision_Disabled) { IgnoreCollisionActors.Add(NewBodyHandle); } #if WITH_CHAOS NewBodyHandle->SetName(BodySetup->BoneName); #endif } } } //Insert joints so that they coincide body order. That is, if we stop simulating all bodies past some index, we can simply ignore joints past a corresponding index without any re-order //For this to work we consider the most last inserted bone in each joint TArray InsertionOrderPerBone; InsertionOrderPerBone.AddUninitialized(NumBonesTotal); for(int32 Position = 0; Position < NumBonesLOD0; ++Position) { InsertionOrderPerBone[InsertionOrder[Position]] = Position; } HighLevelConstraintInstances.Sort([&InsertionOrderPerBone, &SkelMeshRefSkel](const FConstraintInstance& LHS, const FConstraintInstance& RHS) { if(LHS.IsValidConstraintInstance() && RHS.IsValidConstraintInstance()) { const int32 BoneIdxLHS1 = SkelMeshRefSkel.FindBoneIndex(LHS.ConstraintBone1); const int32 BoneIdxLHS2 = SkelMeshRefSkel.FindBoneIndex(LHS.ConstraintBone2); const int32 BoneIdxRHS1 = SkelMeshRefSkel.FindBoneIndex(RHS.ConstraintBone1); const int32 BoneIdxRHS2 = SkelMeshRefSkel.FindBoneIndex(RHS.ConstraintBone2); const int32 MaxPositionLHS = FMath::Max(InsertionOrderPerBone[BoneIdxLHS1], InsertionOrderPerBone[BoneIdxLHS2]); const int32 MaxPositionRHS = FMath::Max(InsertionOrderPerBone[BoneIdxRHS1], InsertionOrderPerBone[BoneIdxRHS2]); return MaxPositionLHS < MaxPositionRHS; } return false; }); TArray IgnorePairs; if(NamesToHandles.Num() > 0) { //constraints for(int32 ConstraintIdx = 0; ConstraintIdx < HighLevelConstraintInstances.Num(); ++ConstraintIdx) { FConstraintInstance* CI = HighLevelConstraintInstances[ConstraintIdx]; ImmediatePhysics::FActorHandle* Body1Handle = NamesToHandles.FindRef(CI->ConstraintBone1); ImmediatePhysics::FActorHandle* Body2Handle = NamesToHandles.FindRef(CI->ConstraintBone2); if(Body1Handle && Body2Handle) { if (Body1Handle->IsSimulated() || Body2Handle->IsSimulated()) { PhysicsSimulation->CreateJoint(CI, Body1Handle, Body2Handle); if (bForceDisableCollisionBetweenConstraintBodies) { int32 BodyIndex1 = UsePhysicsAsset->FindBodyIndex(CI->ConstraintBone1); int32 BodyIndex2 = UsePhysicsAsset->FindBodyIndex(CI->ConstraintBone2); if (BodyIndex1 != INDEX_NONE && BodyIndex2 != INDEX_NONE) { UsePhysicsAsset->DisableCollision(BodyIndex1, BodyIndex2); } } int32 BodyIndex; if (Bodies.Find(Body1Handle, BodyIndex)) { BodyAnimData[BodyIndex].LinearXMotion = CI->GetLinearXMotion(); BodyAnimData[BodyIndex].LinearYMotion = CI->GetLinearYMotion(); BodyAnimData[BodyIndex].LinearZMotion = CI->GetLinearZMotion(); BodyAnimData[BodyIndex].LinearLimit = CI->GetLinearLimit(); //set limit to ref pose FTransform Body1Transform = Body1Handle->GetWorldTransform(); FTransform Body2Transform = Body2Handle->GetWorldTransform(); BodyAnimData[BodyIndex].RefPoseLength = Body1Transform.GetRelativeTransform(Body2Transform).GetLocation().Size(); } if (CI->IsCollisionDisabled()) { ImmediatePhysics::FSimulation::FIgnorePair Pair; Pair.A = Body1Handle; Pair.B = Body2Handle; IgnorePairs.Add(Pair); } } } } ResetSimulatedTeleportType = ETeleportType::ResetPhysics; } // Terminate all the constraint instances for (FConstraintInstance* CI : HighLevelConstraintInstances) { CI->TermConstraint(); delete CI; } // Terminate all of the instances, cannot be done during insert or we may break constraint chains for(FBodyInstance* Instance : HighLevelBodyInstances) { if(Instance->IsValidBodyInstance()) { Instance->TermBody(true); } delete Instance; } HighLevelConstraintInstances.Empty(); HighLevelBodyInstances.Empty(); BodiesSorted.Empty(); const TMap& DisableTable = UsePhysicsAsset->CollisionDisableTable; for(auto ConstItr = DisableTable.CreateConstIterator(); ConstItr; ++ConstItr) { int32 IndexA = ConstItr.Key().Indices[0]; int32 IndexB = ConstItr.Key().Indices[1]; if ((IndexA < BodyIndexToActorHandle.Num()) && (IndexB < BodyIndexToActorHandle.Num())) { if ((BodyIndexToActorHandle[IndexA] != nullptr) && (BodyIndexToActorHandle[IndexB] != nullptr)) { ImmediatePhysics::FSimulation::FIgnorePair Pair; Pair.A = BodyIndexToActorHandle[IndexA]; Pair.B = BodyIndexToActorHandle[IndexB]; IgnorePairs.Add(Pair); } } } PhysicsSimulation->SetIgnoreCollisionPairTable(IgnorePairs); PhysicsSimulation->SetIgnoreCollisionActors(IgnoreCollisionActors); #if WITH_CHAOS SolverIterations = UsePhysicsAsset->SolverIterations; PhysicsSimulation->SetSolverIterations( SolverIterations.FixedTimeStep, SolverIterations.SolverIterations, SolverIterations.JointIterations, SolverIterations.CollisionIterations, SolverIterations.SolverPushOutIterations, SolverIterations.JointPushOutIterations, SolverIterations.CollisionPushOutIterations ); #endif } } DECLARE_CYCLE_STAT(TEXT("FAnimNode_RigidBody::UpdateWorldGeometry"), STAT_ImmediateUpdateWorldGeometry, STATGROUP_ImmediatePhysics); void FAnimNode_RigidBody::UpdateWorldGeometry(const UWorld& World, const USkeletalMeshComponent& SKC) { LLM_SCOPE_BYNAME(TEXT("Animation/RigidBody")); SCOPE_CYCLE_COUNTER(STAT_ImmediateUpdateWorldGeometry); QueryParams = FCollisionQueryParams(SCENE_QUERY_STAT(RagdollNodeFindGeometry), /*bTraceComplex=*/false); #if WITH_EDITOR if(!World.IsGameWorld()) { QueryParams.MobilityType = EQueryMobilityType::Any; //If we're in some preview world trace against everything because things like the preview floor are not static QueryParams.AddIgnoredComponent(&SKC); } else #endif { QueryParams.MobilityType = EQueryMobilityType::Static; //We only want static actors } // Check for deleted world objects and flag for removal (later in anim task) ExpireWorldObjects(); // If we have moved outside of the bounds we checked for world objects we need to gather new world objects FSphere Bounds = SKC.CalcBounds(SKC.GetComponentToWorld()).GetSphere(); if (!Bounds.IsInside(CachedBounds)) { // Since the cached bounds are no longer valid, update them. CachedBounds = Bounds; CachedBounds.W *= CachedBoundsScale; // Cache the PhysScene and World for use in UpdateWorldForces and CollectWorldObjects // When these are non-null it is an indicator that we need to update the collected world objects list PhysScene = World.GetPhysicsScene(); UnsafeWorld = &World; UnsafeOwner = SKC.GetOwner(); // A timer to track objects we haven't detected in a while ++ComponentsInSimTick; } } DECLARE_CYCLE_STAT(TEXT("FAnimNode_RigidBody::UpdateWorldForces"), STAT_ImmediateUpdateWorldForces, STATGROUP_ImmediatePhysics); void FAnimNode_RigidBody::UpdateWorldForces(const FTransform& ComponentToWorld, const FTransform& BaseBoneTM, const float DeltaSeconds) { LLM_SCOPE_BYNAME(TEXT("Animation/RigidBody")); SCOPE_CYCLE_COUNTER(STAT_ImmediateUpdateWorldForces); if(TotalMass > 0.f) { for (const USkeletalMeshComponent::FPendingRadialForces& PendingRadialForce : PendingRadialForces) { const FVector RadialForceOrigin = WorldPositionToSpace(SimulationSpace, PendingRadialForce.Origin, ComponentToWorld, BaseBoneTM); for(ImmediatePhysics::FActorHandle* Body : Bodies) { const float InvMass = Body->GetInverseMass(); if(InvMass > 0.f) { const float StrengthPerBody = PendingRadialForce.bIgnoreMass ? PendingRadialForce.Strength : PendingRadialForce.Strength / (TotalMass * InvMass); ImmediatePhysics::EForceType ForceType; if (PendingRadialForce.Type == USkeletalMeshComponent::FPendingRadialForces::AddImpulse) { ForceType = PendingRadialForce.bIgnoreMass ? ImmediatePhysics::EForceType::AddVelocity : ImmediatePhysics::EForceType::AddImpulse; } else { ForceType = PendingRadialForce.bIgnoreMass ? ImmediatePhysics::EForceType::AddAcceleration : ImmediatePhysics::EForceType::AddForce; } Body->AddRadialForce(RadialForceOrigin, StrengthPerBody, PendingRadialForce.Radius, PendingRadialForce.Falloff, ForceType); } } } if(!ExternalForce.IsNearlyZero()) { const FVector ExternalForceInSimSpace = WorldVectorToSpaceNoScale(SimulationSpace, ExternalForce, ComponentToWorld, BaseBoneTM); for (ImmediatePhysics::FActorHandle* Body : Bodies) { const float InvMass = Body->GetInverseMass(); if (InvMass > 0.f) { Body->AddForce(ExternalForceInSimSpace); } } } if(DeltaSeconds != 0.0) { if(!PerSolverField.IsEmpty()) { TArray& SamplePositions = PerSolverField.GetSamplePositions(); TArray& SampleIndices = PerSolverField.GetSampleIndices(); SamplePositions.SetNum(Bodies.Num(),false); SampleIndices.SetNum(Bodies.Num(), false); int32 Index = 0; for (ImmediatePhysics::FActorHandle* Body : Bodies) { SamplePositions[Index] = (Body->GetWorldTransform() * SpaceToWorldTransform(SimulationSpace, ComponentToWorld, BaseBoneTM)).GetLocation(); SampleIndices[Index] = FFieldContextIndex(Index, Index); ++Index; } PerSolverField.ComputeFieldRigidImpulse(WorldTimeSeconds); const TArray& LinearVelocities = PerSolverField.GetOutputResults(EFieldCommandOutputType::LinearVelocity); const TArray& LinearForces = PerSolverField.GetOutputResults(EFieldCommandOutputType::LinearForce); const TArray& AngularVelocities = PerSolverField.GetOutputResults(EFieldCommandOutputType::AngularVelocity); const TArray& AngularTorques = PerSolverField.GetOutputResults(EFieldCommandOutputType::AngularTorque); if (LinearVelocities.Num() == Bodies.Num()) { Index = 0; for (ImmediatePhysics::FActorHandle* Body : Bodies) { const FVector ExternalForceInSimSpace = WorldVectorToSpaceNoScale(SimulationSpace, LinearVelocities[Index++], ComponentToWorld, BaseBoneTM) * Body->GetMass() / DeltaSeconds; Body->AddForce(ExternalForceInSimSpace); } } if (LinearForces.Num() == Bodies.Num()) { Index = 0; for (ImmediatePhysics::FActorHandle* Body : Bodies) { const FVector ExternalForceInSimSpace = WorldVectorToSpaceNoScale(SimulationSpace, LinearForces[Index++], ComponentToWorld, BaseBoneTM); Body->AddForce(ExternalForceInSimSpace); } } if (AngularVelocities.Num() == Bodies.Num()) { Index = 0; for (ImmediatePhysics::FActorHandle* Body : Bodies) { const FVector ExternalTorqueInSimSpace = WorldVectorToSpaceNoScale(SimulationSpace, AngularVelocities[Index++], ComponentToWorld, BaseBoneTM) * Body->GetInertia() / DeltaSeconds; Body->AddTorque(ExternalTorqueInSimSpace); } } if (AngularTorques.Num() == Bodies.Num()) { Index = 0; for (ImmediatePhysics::FActorHandle* Body : Bodies) { const FVector ExternalTorqueInSimSpace = WorldVectorToSpaceNoScale(SimulationSpace, AngularTorques[Index++], ComponentToWorld, BaseBoneTM); Body->AddTorque(ExternalTorqueInSimSpace); } } } } } } bool FAnimNode_RigidBody::NeedsDynamicReset() const { return true; } void FAnimNode_RigidBody::ResetDynamics(ETeleportType InTeleportType) { // This will be picked up next evaluate and reset our simulation. // Teleport type can only go higher - i.e. if we have requested a reset, then a teleport will still reset fully ResetSimulatedTeleportType = ((InTeleportType > ResetSimulatedTeleportType) ? InTeleportType : ResetSimulatedTeleportType); } DECLARE_CYCLE_STAT(TEXT("RigidBody_PreUpdate"), STAT_RigidBody_PreUpdate, STATGROUP_Anim); void FAnimNode_RigidBody::PreUpdate(const UAnimInstance* InAnimInstance) { // Don't update geometry if RBN is disabled if(!bEnabled) { return; } SCOPE_CYCLE_COUNTER(STAT_RigidBody_PreUpdate); USkeletalMeshComponent* SKC = InAnimInstance->GetSkelMeshComponent(); APawn* PawnOwner = InAnimInstance->TryGetPawnOwner(); UPawnMovementComponent* MovementComp = PawnOwner ? PawnOwner->GetMovementComponent() : nullptr; #if WITH_EDITOR && !WITH_CHAOS if (bEnableWorldGeometry && SimulationSpace != ESimulationSpace::WorldSpace) { FMessageLog("PIE").Warning(FText::Format(LOCTEXT("WorldCollisionComponentSpace", "Trying to use world collision without world space simulation for ''{0}''. This is not supported, please change SimulationSpace to WorldSpace"), FText::FromString(GetPathNameSafe(SKC)))); } #endif UWorld* World = InAnimInstance->GetWorld(); if (World) { WorldSpaceGravity = bOverrideWorldGravity ? OverrideWorldGravity : (MovementComp ? FVector(0.f, 0.f, MovementComp->GetGravityZ()) : FVector(0.f, 0.f, World->GetGravityZ())); if(SKC) { // Store game time for use in parallel evaluation. This may be the totol time (inc pauses) or the time the game has been unpaused. WorldTimeSeconds = SKC->PrimaryComponentTick.bTickEvenWhenPaused ? World->UnpausedTimeSeconds : World->TimeSeconds; if (PhysicsSimulation && bEnableWorldGeometry) { UpdateWorldGeometry(*World, *SKC); } PendingRadialForces = SKC->GetPendingRadialForces(); PreviousTransform = CurrentTransform; CurrentTransform = SKC->GetComponentToWorld(); if (World->PhysicsField) { const FBox BoundingBox = SKC->CalcBounds(SKC->GetComponentTransform()).GetBox(); World->PhysicsField->FillTransientCommands(false, BoundingBox, WorldTimeSeconds, PerSolverField.GetTransientCommands()); World->PhysicsField->FillPersistentCommands(false, BoundingBox, WorldTimeSeconds, PerSolverField.GetPersistentCommands()); } } } } int32 FAnimNode_RigidBody::GetLODThreshold() const { if(CVarRigidBodyLODThreshold.GetValueOnAnyThread() != -1) { if(LODThreshold != -1) { return FMath::Min(LODThreshold, CVarRigidBodyLODThreshold.GetValueOnAnyThread()); } else { return CVarRigidBodyLODThreshold.GetValueOnAnyThread(); } } else { return LODThreshold; } } DECLARE_CYCLE_STAT(TEXT("RigidBody_Update"), STAT_RigidBody_Update, STATGROUP_Anim); void FAnimNode_RigidBody::UpdateInternal(const FAnimationUpdateContext& Context) { LLM_SCOPE_BYNAME(TEXT("Animation/RigidBody")); DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(UpdateInternal) // Avoid this work if RBN is disabled, as the results would be discarded if(!bEnabled) { return; } SCOPE_CYCLE_COUNTER(STAT_RigidBody_Update); // Must flush the simulation since we may be making changes to the scene FlushDeferredSimulationTask(); // Accumulate deltatime elapsed during update. To be used during evaluation. AccumulatedDeltaTime += Context.AnimInstanceProxy->GetDeltaSeconds(); if (UnsafeWorld != nullptr) { // Node is valid to evaluate. Simulation is starting. bSimulationStarted = true; } // Remove expired objects from the sim PurgeExpiredWorldObjects(); // Find nearby world objects to add to the sim (gated on UnsafeWorld - see UpdateWorldGeometry) CollectWorldObjects(); // These get set again if our bounds change. Subsequent calls to CollectWorldObjects will early-out until then UnsafeWorld = nullptr; UnsafeOwner = nullptr; PhysScene = nullptr; } void FAnimNode_RigidBody::CollectWorldObjects() { if ((UnsafeWorld != nullptr) && (PhysScene != nullptr)) { // @todo(ccaulfield): should this use CachedBounds? TArray Overlaps; UnsafeWorld->OverlapMultiByChannel(Overlaps, CachedBounds.Center, FQuat::Identity, OverlapChannel, FCollisionShape::MakeSphere(CachedBounds.W), QueryParams, FCollisionResponseParams(ECR_Overlap)); // @todo(ccaulfield): is there an engine-independent way to do this? #if WITH_PHYSX && PHYSICS_INTERFACE_PHYSX SCOPED_SCENE_READ_LOCK(PhysScene ? PhysScene->GetPxScene() : nullptr); //TODO: expose this part to the anim node #endif for (const FOverlapResult& Overlap : Overlaps) { if (UPrimitiveComponent* OverlapComp = Overlap.GetComponent()) { FWorldObject* WorldObject = ComponentsInSim.Find(OverlapComp); if (WorldObject != nullptr) { // Existing object - reset its age WorldObject->LastSeenTick = ComponentsInSimTick; } else { #if WITH_PHYSX && PHYSICS_INTERFACE_PHYSX ComponentsInSim.Add(OverlapComp); // Not sure why this happens, adding check to fix crash in CheckRBN engine test. if (OverlapComp->BodyInstance.BodySetup != nullptr) { PhysicsSimulation->CreateActor(ImmediatePhysics::EActorType::StaticActor, &OverlapComp->BodyInstance, OverlapComp->BodyInstance.GetUnrealWorldTransform()); } #elif WITH_CHAOS // New object - add it to the sim const bool bIsSelf = (UnsafeOwner == OverlapComp->GetOwner()); if (!bIsSelf) { // Create a kinematic actor. Not using Static as world-static objects may move in the simulation's frame of reference ImmediatePhysics::FActorHandle* ActorHandle = PhysicsSimulation->CreateActor(ImmediatePhysics::EActorType::KinematicActor, &OverlapComp->BodyInstance, OverlapComp->GetComponentTransform()); PhysicsSimulation->AddToCollidingPairs(ActorHandle); ComponentsInSim.Add(OverlapComp, FWorldObject(ActorHandle, ComponentsInSimTick)); } #endif } } } } } // Flag invalid objects for purging void FAnimNode_RigidBody::ExpireWorldObjects() { #if WITH_CHAOS // Invalidate deleted and expired world objects TArray PrunedEntries; for (auto& WorldEntry : ComponentsInSim) { const UPrimitiveComponent* WorldComp = WorldEntry.Key; FWorldObject& WorldObject = WorldEntry.Value; // Do we need to expire this object? const int32 ExpireTickCount = RBAN_WorldObjectExpiry; bool bIsInvalid = ((ComponentsInSimTick - WorldObject.LastSeenTick) > ExpireTickCount) // Haven't seen this object for a while || !IsValid(WorldComp) || (WorldComp->GetBodyInstance() == nullptr) || (!WorldComp->GetBodyInstance()->IsValidBodyInstance()); // Remove from sim if necessary if (bIsInvalid) { WorldObject.bExpired = true; } } #endif } void FAnimNode_RigidBody::PurgeExpiredWorldObjects() { #if WITH_CHAOS // Destroy expired simulated objects TArray PurgedEntries; for (auto& WorldEntry : ComponentsInSim) { FWorldObject& WorldObject = WorldEntry.Value; if (WorldObject.bExpired) { PhysicsSimulation->DestroyActor(WorldObject.ActorHandle); WorldObject.ActorHandle = nullptr; PurgedEntries.Add(WorldEntry.Key); } } // Remove purged map entries for (const UPrimitiveComponent* PurgedEntry : PurgedEntries) { ComponentsInSim.Remove(PurgedEntry); } #endif } // Update the transforms of the world objects we added to the sim. This is required // if we have a component- or bone-space simulation as even world-static objects // will be moving in the simulation's frame of reference. void FAnimNode_RigidBody::UpdateWorldObjects(const FTransform& SpaceTransform) { LLM_SCOPE_BYNAME(TEXT("Animation/RigidBody")); #if WITH_CHAOS if (SimulationSpace != ESimulationSpace::WorldSpace) { for (const auto& WorldEntry : ComponentsInSim) { const UPrimitiveComponent* OverlapComp = WorldEntry.Key; if (OverlapComp != nullptr) { ImmediatePhysics::FActorHandle* ActorHandle = WorldEntry.Value.ActorHandle; // Calculate the sim-space transform of this object const FTransform CompWorldTransform = OverlapComp->BodyInstance.GetUnrealWorldTransform(); FTransform CompSpaceTransform; CompSpaceTransform.SetTranslation(SpaceTransform.InverseTransformPosition(CompWorldTransform.GetLocation())); CompSpaceTransform.SetRotation(SpaceTransform.InverseTransformRotation(CompWorldTransform.GetRotation())); CompSpaceTransform.SetScale3D(FVector::OneVector); // TODO - sort out scale for world objects in local sim // Update the sim's copy of the world object ActorHandle->SetWorldTransform(CompSpaceTransform); } } } #endif } void FAnimNode_RigidBody::InitializeBoneReferences(const FBoneContainer& RequiredBones) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(InitializeBoneReferences) /** We only need to update simulated bones and children of simulated bones*/ const int32 NumBodies = Bodies.Num(); const TArray& RequiredBoneIndices = RequiredBones.GetBoneIndicesArray(); const int32 NumRequiredBoneIndices = RequiredBoneIndices.Num(); const FReferenceSkeleton& RefSkeleton = RequiredBones.GetReferenceSkeleton(); OutputBoneData.Empty(NumBodies); int32 NumSimulatedBodies = 0; // if no name is entered, use root if (BaseBoneRef.BoneName == NAME_None) { BaseBoneRef.BoneName = RefSkeleton.GetBoneName(0); } if (BaseBoneRef.BoneName != NAME_None) { BaseBoneRef.Initialize(RequiredBones); } bool bHasInvalidBoneReference = false; for (int32 Index = 0; Index < NumRequiredBoneIndices; ++Index) { const FCompactPoseBoneIndex CompactPoseBoneIndex(Index); const FBoneIndexType SkeletonBoneIndex = RequiredBones.GetSkeletonIndex(CompactPoseBoneIndex); const FBoneIndexType IndexToBodyNum = SkeletonBoneIndexToBodyIndex.Num(); // If we have a missing bone in our skeleton, we don't want to have an out of bounds access. if (SkeletonBoneIndex >= IndexToBodyNum) { bHasInvalidBoneReference = true; break; } const int32 BodyIndex = SkeletonBoneIndexToBodyIndex[SkeletonBoneIndex]; if (BodyIndex != INDEX_NONE) { //If we have a body we need to save it for later FOutputBoneData* OutputData = new (OutputBoneData) FOutputBoneData(); OutputData->BodyIndex = BodyIndex; OutputData->CompactPoseBoneIndex = CompactPoseBoneIndex; if (BodyAnimData[BodyIndex].bIsSimulated) { ++NumSimulatedBodies; } OutputData->BoneIndicesToParentBody.Add(CompactPoseBoneIndex); // Walk up parent chain until we find parent body. OutputData->ParentBodyIndex = INDEX_NONE; FCompactPoseBoneIndex CompactParentIndex = RequiredBones.GetParentBoneIndex(CompactPoseBoneIndex); while (CompactParentIndex != INDEX_NONE) { const FBoneIndexType SkeletonParentBoneIndex = RequiredBones.GetSkeletonIndex(CompactParentIndex); // Must check our parent as well for a missing bone. if (SkeletonParentBoneIndex >= IndexToBodyNum) { bHasInvalidBoneReference = true; break; } OutputData->ParentBodyIndex = SkeletonBoneIndexToBodyIndex[SkeletonParentBoneIndex]; if (OutputData->ParentBodyIndex != INDEX_NONE) { break; } OutputData->BoneIndicesToParentBody.Add(CompactParentIndex); CompactParentIndex = RequiredBones.GetParentBoneIndex(CompactParentIndex); } if (bHasInvalidBoneReference) { break; } } } if (bHasInvalidBoneReference) { // If a bone was missing, let us know which asset it happened on, and clear our bone container to make the bad asset visible. ensureMsgf(false, TEXT("FAnimNode_RigidBody::InitializeBoneReferences: The Skeleton %s, is missing bones that SkeletalMesh %s needs. Skeleton might need to be resaved."), *GetNameSafe(RequiredBones.GetSkeletonAsset()), *GetNameSafe(RequiredBones.GetSkeletalMeshAsset())); OutputBoneData.Empty(); } else { // New bodies potentially introduced with new LOD // We'll have to initialize their transform. bCheckForBodyTransformInit = true; if (PhysicsSimulation) { PhysicsSimulation->SetNumActiveBodies(NumSimulatedBodies); } // We're switching to a new LOD, this invalidates our captured poses. CapturedFrozenPose.Empty(); CapturedFrozenCurves.Empty(); } } void FAnimNode_RigidBody::AddImpulseAtLocation(FVector Impulse, FVector Location, FName BoneName) { #if WITH_CHAOS // Find the body. This is currently only used in the editor and will need optimizing if used in game for (int32 BodyIndex = 0; BodyIndex < Bodies.Num(); ++BodyIndex) { ImmediatePhysics::FActorHandle* Body = Bodies[BodyIndex]; if (Body->GetName() == BoneName) { Body->AddImpulseAtLocation(Impulse, Location); } } #endif } void FAnimNode_RigidBody::OnInitializeAnimInstance(const FAnimInstanceProxy* InProxy, const UAnimInstance* InAnimInstance) { InitPhysics(InAnimInstance); } #if WITH_EDITORONLY_DATA void FAnimNode_RigidBody::PostSerialize(const FArchive& Ar) { if(bComponentSpaceSimulation_DEPRECATED == false) { //If this is not the default value it means we have old content where we were simulating in world space SimulationSpace = ESimulationSpace::WorldSpace; bComponentSpaceSimulation_DEPRECATED = true; } } #endif bool FAnimNode_RigidBody::IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones) { return BaseBoneRef.IsValidToEvaluate(RequiredBones); } #undef LOCTEXT_NAMESPACE