Saturday, January 27, 2018

How To Implement Active Ragdoll

You might have seen video game characters getting hit by a bullet or an explosion and turn into ragdoll while ending up in a pose very similar to animated pose where they can blend back smoothly to the animation! In this post I try to show you how you can implement an active ragdoll to drive the joint motors toward the animation pose to achieve this goal. I've already written another post which was targeting how to use these kind of systems, if you already have them implemented. You may want to check it out too:

Combing Ragdoll and Keyframe Animation to Achieve Dyanmic Poses

So this post focuses on implementing such systems and it comes with required source code. Source codes here are written for Unreal Engine 4. Before going forward you might know that UE4 already supports active ragdoll as an engine feature and you don't really need to reimplement it. This post just carries an academic approach to let the developers know how these kind of systems are implemented in general. To find out how UE4's physically based animation works you can watch this nice video tutorial:

How To Make An Active Ragdoll / Unreal Engine 4

Physically based animation is also known as active ragdoll or animation driven ragdoll and if you have used Havok animation before, it's called powered ragdoll in Havok animation. In this article I call it active ragdoll implying on a ragdoll simulated character trying to follow animation poses(s).


How Does An Active Ragdoll Work?

So imagine you have setup your character with joint constraints and motors and it can simulate as a ragdoll on collisions. Your goal is to make the ragdoll following an animation pose while making the body still reacting to external physical forces and avoid having a loose physical pose.

To solve this problem, let's consider a simple case for just one bone. While you setup your character for ragdoll simulation, a rigid body should be attached to the bone to represent its physical properties like volume, mass, friction etc. The movement of this physical body is controlled by a physical constraint and a motor attached to it. So the angular and linear velocity of the bone is controlled and limited by the constraints it's assigned to. Now imagine you just do a normal ragdoll simulation without following any animation. When you run the simulation the bone as a rigid body gains some angular and linear velocity and based on these velocities the rigid body moves in space!

Now you know that we have angular and linear velocities for bones then we can control these velocities to adjust the physical body to follow the animation easily. To move the ragdoll toward the animation, we can have access to the rotation of the rigid body attached to bone (it's last frame rotation). We can also get the rotation of the bone from animation pose and we have access to the frame's delta time! The difference between the rotation of the bone from animation and the rigid body's simulated rotation from last frame shows how much we should rotate our bone in this frame. We can get the rotation axis of this difference rotation (in quaternions) and scale its length by the angle of the quaternion representing this rotation divided by delta time. We do this length scale because angular velocity is defined by a vector which its direction shows the rotation direction and its length represents angular speed.

This angular velocity can rotate the bone from its current rotation to the pose provided by animation because it rotates the bone from its current rotation to the rotation coming from animation pose in current frame's delta time. Just note, the animation should have been updated at this point to be sure  we're receiving this frames's valid animated pose but physics should be updated afterwards to avoid having one frame delay. Because we get the animation pose and then set the physics velocity and then we want to run the simulation so these calculations should be done before physics loop and after animation update or after certain pose update of animation like an animation node.

Same calculations can be done for bone's linear velocity. We just need to use bones's translation instead of rotation in this case and of course we need to work with vectors instead of quaternions. Applying linear velocity is helping the character to follow the animated bones' positions as well which can be used for bones carrying translation keyframes like pelvis.

If we do this procedure for all the bones in the skeleton hierarchy, we can be sure all the rigid bodies will exactly follow the animation pose. The bones velocity can still be changed because of the external physical forces applied to them from physics world. So this is the point that the bone can still react to external forces while trying to follow the pose from animation. You can find all the codes I've written for this case here. I provided an animation node handling the subject. Note that the skeletal mesh should be physically simulated to see the effect:

Header here:


#pragma once

#include "Engine.h"
#include "AnimGraphNode_Base.h"
#include "Runtime/Engine/Classes/Animation/AnimNodeBase.h"
#include "ActiveRagdollAnimNode.generated.h"

USTRUCT(BlueprintType)
struct FAnimNode_ActiveRagdoll : public FAnimNode_Base
{
 GENERATED_USTRUCT_BODY()

 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Links)
 FComponentSpacePoseLink mBasePose;

 UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = Links, meta = (PinShownByDefault) )
 FRotator mInitialRelativeRotation;

 UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = Links, meta = (PinShownByDefault) )
 FVector mInitialRelativeTranslation;

 UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = Links, meta = (PinShownByDefault) )
 float mTranslationWeight;

 UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = Links, meta = (PinShownByDefault) )
 float mRotationWeight;

protected:
 float mDeltaTime;

public:
 /************/
 FAnimNode_ActiveRagdoll();

 /****************************************************************/
 void Initialize_AnyThread( const FAnimationInitializeContext& Context ) override;

 /****************************************************************/
 void Update_AnyThread( const FAnimationUpdateContext& Context ) override;
/****************************************************************/
void CacheBones_AnyThread( const FAnimationCacheBonesContext& Context ) override;

 /*********************************************************/
 void EvaluateComponentSpace_AnyThread( FComponentSpacePoseContext& Output ) override;
};


UCLASS(BlueprintType)
class ANIMBASEDRAGDOLL_API UAnimGraphNode_ActiveRagdoll : public UAnimGraphNode_Base
{
 GENERATED_BODY()
 
public:
 UPROPERTY(EditAnywhere, Category = Links)
 FAnimNode_ActiveRagdoll mAnimPose;

 virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
 virtual FLinearColor GetNodeTitleColor() const override;
 virtual FString GetNodeCategory() const override;
 virtual void CreateOutputPins() override;
};



CPP here:


#include "AnimBasedRagdoll.h"
#include "ActiveRagdollAnimNode.h"
#include "AnimationGraphSchema.h"
#include "AnimInstanceProxy.h"
#include "PhysicsEngine/PhysicsAsset.h"
#include "Components/SkeletalMeshComponent.h"

#define ANIM_MATH_PI 3.141592724f

//*******FAnimPoseNode implmentations*******************
FAnimNode_ActiveRagdoll::FAnimNode_ActiveRagdoll()
{
 mInitialRelativeRotation = FRotator( 0.f, 0.f, 0.f );
 mInitialRelativeTranslation = FVector::ZeroVector;
}


/*********************************************************/
void FAnimNode_ActiveRagdoll::Initialize_AnyThread( const FAnimationInitializeContext& Context )
{
 mBasePose.Initialize( Context );
};

/****************************************************************/
void FAnimNode_ActiveRagdoll::Update_AnyThread( const FAnimationUpdateContext& Context )
{
 mBasePose.Update( Context );
 mDeltaTime = Context.GetDeltaTime();
}
/****************************************************************/
void FAnimNode_ActiveRagdoll::CacheBones_AnyThread( const FAnimationCacheBonesContext& Context )
{
 mBasePose.CacheBones( Context );
}

/****************************************************************/
void FAnimNode_ActiveRagdoll::EvaluateComponentSpace_AnyThread( FComponentSpacePoseContext& Output )
{
 mBasePose.EvaluateComponentSpace( Output );
 
 const UAnimInstance* const lAnimInstance = Cast< UAnimInstance >( Output.AnimInstanceProxy->GetAnimInstanceObject() );
 USkeletalMeshComponent* lSkel = lAnimInstance->GetOwningComponent();

 if( lSkel && lSkel->IsSimulatingPhysics() )
 {
  FQuat lBoneQuaternion;
  FQuat lOwnerRotation;
  
  FVector lBoneTranslation;
  FVector lOwnerTranslation;

  const AActor* const lCharOwner = lAnimInstance->GetOwningActor();
  const UINT32 lPhysicsBoneCount = lSkel->GetPhysicsAsset()->SkeletalBodySetups.Num();
  
  if( lCharOwner )
  {
   lOwnerRotation = lCharOwner->GetActorRotation().Quaternion();
   lOwnerTranslation = lCharOwner->GetActorLocation();
  }
  else
  {
   lOwnerRotation = FQuat::Identity;
   lOwnerTranslation = FVector::ZeroVector;
  }

  for( uint8 i = 0; i < lPhysicsBoneCount; i++ )
  {
   const FName lBoneName = lSkel->GetPhysicsAsset()->SkeletalBodySetups[ i ]->BoneName;
   const FCompactPoseBoneIndex lInd( lSkel->GetBoneIndex( lBoneName ) );
   
   lBoneQuaternion = Output.Pose.GetComponentSpaceTransform( lInd ).GetRotation();
   lBoneTranslation = Output.Pose.GetComponentSpaceTransform( lInd ).GetLocation();

   //Setting up Linear Velocity

   // Updating bone positions in world space
   if( mTranslationWeight > 0.f )
   {
    const FVector lTargetPositionInWorld = lOwnerTranslation + lOwnerRotation *(
     (mInitialRelativeTranslation + mInitialRelativeRotation.Quaternion() * lBoneTranslation));

    FVector finalVelocity = (lTargetPositionInWorld - lSkel->GetBoneLocation( lBoneName )) / mDeltaTime;

    if( mTranslationWeight < 1.0f )
    {
     finalVelocity = FMath::Lerp( FVector::ZeroVector, finalVelocity, mTranslationWeight );
    }

    lSkel->SetPhysicsLinearVelocity( finalVelocity, false, lBoneName );
   }


   //Setting up angular velocity
   if( mRotationWeight > 0.f )
   {
    const FQuat lTargetRotationInWorld = lOwnerRotation * mInitialRelativeRotation.Quaternion() * lBoneQuaternion;
    const FQuat lRotDiff = lTargetRotationInWorld * lSkel->GetBoneQuaternion( lBoneName ).Inverse();
    const float lAngDiff = 2.0f * FMath::Acos( lRotDiff.W );
    FVector lAngVelocity;

    //Checking for shortest arc.
    if( lAngDiff < ANIM_MATH_PI )
    {
     lAngVelocity = lRotDiff.GetRotationAxis().GetSafeNormal() * lAngDiff / mDeltaTime;
    }
    else
    {
     lAngVelocity = -lRotDiff.GetRotationAxis().GetSafeNormal() * (2.0f * ANIM_MATH_PI - lAngDiff) / mDeltaTime;
    }

    if( mRotationWeight < 1.0f )
    {
     const FVector currentAngularVelNormalized = lSkel->GetPhysicsAngularVelocityInRadians( lBoneName ).GetSafeNormal();
     FQuat lInterpolatedRot = FQuat::FindBetweenVectors( currentAngularVelNormalized, lAngVelocity.GetSafeNormal() );
     lInterpolatedRot = FQuat::Slerp( FQuat::Identity, lInterpolatedRot, mRotationWeight );
     const float lRotRad = FMath::Lerp( 0.f, lAngVelocity.Size(), mRotationWeight );

     lAngVelocity = lAngVelocity.GetSafeNormal() * lRotRad;
    }

    lSkel->SetPhysicsAngularVelocityInRadians( lAngVelocity, false, lBoneName );
   }
  }
 }
}

/*******AnimGraphNode Implmentations. Codes Below Are Just Used for Unreal Editor. No Run-Time Code*******************


/******************Title Color!****************************/
FLinearColor UAnimGraphNode_ActiveRagdoll::GetNodeTitleColor() const
{
 return FLinearColor(0, 12.0f, 12.0f, 1.0f);
}

/***************Node Category***********************/
FString UAnimGraphNode_ActiveRagdoll::GetNodeCategory() const
{
 return FString("Active Ragdoll");
}

/*******************************Node Title************************************/
FText UAnimGraphNode_ActiveRagdoll::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
 return FText::FromString("Move Physical Bones to Animation Pose");
}


/*******************************Exposing Output Pins************************************/
void UAnimGraphNode_ActiveRagdoll::CreateOutputPins()
{
 const UAnimationGraphSchema* Schema = GetDefault();
 CreatePin( EGPD_Output, Schema->PC_Struct, TEXT( "" ), FComponentSpacePoseLink::StaticStruct(), /*bIsArray=*/ false, /*bIsReference=*/ false, TEXT( "Pose" ) );
}


As you can see there are weight values for both angular and linear velocity. If these weights are set to 1.0, the physics will follow the animation pose 100%. So I suggest to change these two weight values based on gameplay events. For example if the character gets hit by an explosion the weights can be set to zero so we can have pure ragdoll simulation and after a while based on the average linear speed of the skeletal mesh (or it's pelvis velocity), you can increase the value until it gets to 1.0 and afterwards you can blend back to animation and turn off simulation. Simulation should be turned on when it's needed because it can be an expensive procedure.

And here are some GIFs out of the results:




GIF above shows the character with 100% angular velocity adjustment applied and zero percent of linear velocity. As you can see the character follows joint rotations while responding to external forces.





GIF above shows the character with 50% angular velocity adjustment  applied and zero percent of linear velocity. As you can see the character follows joint rotations but not completely!




GIF above shows the character with 10% angular velocity applied and zero percent of linear velocity. As you can see the character has a loose pose but still carries some percentage of animation pose.




GIF above shows the character with 100% angular and linear velocity applied. As it's shown, character saves its animated form completely with  both bones positions and rotations but the responses to external forces look like a glitch!


GIF above shows the character with 30% angular and linear velocity applied. As It's shown in the GIF, character saves its animated form partially but the response to external forces is smoother now!

Results above shows these weights should be controlled externally based on the gameplay events to let the character have a natural physical pose based on the context of gameplay.


Alternative Approach Using Spring Dampers

There is also an alternative approach to make active ragdoll happening. So instead of directly setting the angular and linear velocity of the bones we can apply a desired torque or force to make them following the animation pose. This approach uses a spring-damper equation to move the bones toward the animation pose. Here, the animation pose is considered as equilibrium point for the system. First we see how far the bone is from its equilibrium point then we apply a torque which can move the bone toward the point (Spring). Then there is resistant angular velocity which is applied in the opposite direction of the bone to smooth out the movement (damper) and avoid oscillation as much as possible.

The torque applied to each bone is calculated with this equation:

Joint_Torque = k * ( joint_rotation_from_animation * Inverse( current_joint_rotation ) ).RotaionAxis - m *  joint_angular_velocity; //rotations are in quaternions and in world space.


As you can see there are two gain values involved in this equation and you need to set them manually. These gains are used to scale the effect of spring and dampness for the movement. These gains should be different from joint to joint based on their mass and their momentum! The problem with this approach is that we need to do a lot of trial and error to set these gain values for each bone to avoid crazy oscillation but it also has pros as well. It can make smoother external physical reactions because we're not force setting the velocities directly and just trying to apply torques and forces toward the target so the bones can carry their current pure momentum.

However the famous paper below suggests an inertia scaled method for joints where all the bone gains are scaled automatically based on their parent bone's angular momentum. With this, specifying the gains can be done with less trial and error but still needs consideration.

ZORDAN, V. B., AND HODGINS, J. K. 2002. Motion capturedrivensimulations that hit and react. In ACM SIGGRAPH / EurographicsSymposium on Computer Animation, 89–96. 

No comments:

Post a Comment