Ragdoll with PhysX

I love ragdoll, it's a fun and effective way to animate inanimate creatures in games. From having worked with animation and Physx in our project group's games, the idea spurred to me - Why not combine the two?

A ragdolling character using PhysX with in-house game engine

First, a testing gym was setup to increase iteration speed by using an animation debugging tool, which was created by Goran Taha from my project group. With a few modifications to display joints and connections, plus to activate ragdoll, the tool was enough for this project. Next up is retrieving data from the animation to use.

Retrieving data from animation will look different in different engines. Our engine's animation framework originates from Ogldev which uses Assimp

 

To retrieve the joints’ transforms of the current frame, they’re looped through and transformed into world space. 

To retrieve the connections between the joints, I iterated through the linked nodes of the animation and stored the index of the current joint and parent joint for each connection. Also, the depth of the recursive function will function as radius later on.

With these two vectors of information I set out to create a PhysX Articulation, which will serve as the joint structure.

 

Preferably, when creating the physics components of an animation, it is tailored by a technical artist to ensure proper behaviour and a correct fit. I learned this by consulting with Simon Martinsson from my project group during the preconception of my specialization. In my case, it was implausible to execute on this. I settled for creating the physics strictly depending on the joints’ transforms and the connections between them. Which, of course, results in an anatomically less correct physics animation.

A PxArticulation consists of multiple links, PxArticulationLink. The links inherits from PxRigidBody and represents the body parts which are created in between joints. In order to create the links in between the joints, a position in between is located and a rotion is created by look at. The geometry of the link takes shape as a capsule, which in PhysX is defined to extend its height along its local x-axis. Hence, we "look at" with a forward vector of "right".

 

When these body parts are linked together they spit out an inbound joint, PxArticulationJoint, which needs the local transform of both its child and parent link to configurate.

 

CTransform CAnimationController::GetTransformOfBone(const Matrix4f& aMatrix, int aBoneIndex) const
{
    const uint boneIndex = aBoneIndex;

    const aiMatrix4x4& offset = myBoneStaticOffsets[boneIndex];
    const aiMatrix4x4& transformation = myBoneTransforms[boneIndex];
    aiMatrix4x4 offsetInv = offset;
    offsetInv.Inverse();

    aiMatrix4x4 matrix = (transformation * offsetInv);
    return matrix.GetCUMatrix() * aMatrix;
}

std::vector<CTransform> CAnimationController::GetTransformOfBones(const Matrix4f& aMatrix) const
{
    std::vector<CTransform> transforms;
    transforms.resize(myBoneMapping.size());
    for (auto& mapping : myBoneMapping)
    {
        const uint boneIndex = mapping.second;
        transforms[boneIndex] = GetTransformOfBone(aMatrix, boneIndex);
    }
    return transforms;
}

 

void CAnimationController::CalculateConnections(int aDepth, aiNode* aNode, aiNode* aParent)
{
    std::string name = GetName(aNode);
    bool valid = GetIsValidName(name);

    if (valid)
    {
        int jointIndex = GetBoneIndex(name);

        if (GetIsRootNode(aParent))
        {
            SBoneConnection::SBoneInfo joint(name, jointIndex, aDepth);
            SBoneConnection::SBoneInfo jointParent("", -1, aDepth - 1);
            myConnections.push_back(SBoneConnection(joint, jointParent));
        }
        else
        {
            std::string nameParent = GetName(aParent);
            int jointIndexParent = GetBoneIndex(nameParent);

            SBoneConnection::SBoneInfo joint(name, jointIndex, aDepth);
            SBoneConnection::SBoneInfo jointParent(nameParent, jointIndexParent, aDepth - 1);
            myConnections.push_back(SBoneConnection(joint, jointParent));
        }
    }

    for (int i = 0; i < aNode->mNumChildren; i++)
    {
        CalculateConnections(aDepth + 1, aNode->mChildren[i], valid ? aNode : aParent);
    }
}

 

void CTransform::LookAt(const Vector3f& aPosition, const Vector3f& aForward)
{
    const Vector3f forward = (aPosition - GetPosition()).GetNormalized();

    Quaternion q;
    float dot = forward.Dot(aForward);
    if (abs(dot + 1.f) < 0.000001f)
    {
        // vector a and b point exactly in the opposite direction, 
        // so it is a 180 degrees turn around the up-axis
        q = Quaternion(VECTOR3F_UP.x, VECTOR3F_UP.y, VECTOR3F_UP.z, CU::Math::Pif);
    }
    else if (abs(dot - 1.f) < 0.000001f)
    {
        // vector a and b point exactly in the same direction
        // so we return the identity quaternion
    }
    else
    {
        float rotAngle = acos(dot);
        Vector3f rotAxis = -forward.Cross(aForward).GetNormalized();
        q = Quaternion(rotAxis, rotAngle);
    }

    myMatrix.SetRotation(q.GetCURotationMatrix());
}

physx::PxTransform Physics::CRagdollInternal::GetPxTransform(const CTransform& aTransformParent, const CTransform& aTransformChild) const
{
    const Vector3f positionParent = aTransformParent.GetPosition();
    const Vector3f positionChild = aTransformChild.GetPosition();
    const Vector3f delta = (positionChild - positionParent);
    Vector3f inBetween = positionParent + (delta * .5f);

    CTransform transformInBetween;
    transformInBetween.SetPosition(inBetween);
    transformInBetween.LookAt(positionParent, VECTOR3F_RIGHT);
    return GetPxTransform(transformInBetween);
}

    

    physx::PxArticulationLink* link;
    link = myArticulation.createLink(parentLink, transformInBetweenJoints);
    physx::PxArticulationJoint* joint = link->getInboundJoint();
    joint->setParentPose(parentAttachment);
    joint->setChildPose(childAttachment);

Here I encountered a problem. I mistook the parameters to take a transform in world space when they expect a local transform for the link. This resulted in an exploding set of objects which could be observed with PhysX Visual Debugger.

Worst yet is that I perceived the problem to be a symptom of PhysX dynamics solver, which separates overlapping dynamic objects, when it was not. In fact, the dynamics solver has no problem with creating overlapping links I found later on. Through some back and forth the PhysX scene was reconfigured to send debug information about joints to the visual debugger. With the debugging tool I could step through frames of physics simulation and observe that the joints were very far apart, which led me to the resolution of inputting correct transforms to the joint.

The objects are pulled apart rapidly due to incorrect joint configuration

With the joint configured, a shape needs to be applied to the link. We pass in the link to apply the PxShape to, and the desired height and radius of our capsule shape. Here the depth calculated previously is made use of to vary the radius, which is the thickness och the capsule. A PxMaterial i used to define the material properties of the shape and finally the mass is set. 

Here is how our animation data is used to create the link, set the joint and apply the shape as a whole.

With our articulation set up, gathering data for the animation to follow comes next. Unfortunately, the joints we configured has no interface for retreiving its transform.

    

    if (aRootFlag)
    {
        physx::PxRigidActorExt::createExclusiveShape(aBody, physx::PxSphereGeometry(startSize), aMaterial);
    }
    else
    {
        float radius = std::max(startSize - std::pow(aDepth, 1.1f), 0.5f);
        physx::PxRigidActorExt::createExclusiveShape(aBody, physx::PxCapsuleGeometry(radius, aHalfHeight), aMaterial);
    }
    physx::PxRigidBodyExt::updateMassAndInertia(aBody, 1.f);

    

    for (const auto& connection : someConnections)
    {
        unsigned int jointIndex = connection.myBone.myIndex;
        int jointParentIndex = connection.myParent.myIndex;
        const CTransform& jointTransform = someTransforms[jointIndex];

        bool isRoot = GetIsRoot(jointParentIndex);
        physx::PxArticulationLink* link; // link is the rigid body between two joints
        float halfHeight = 0.f;
        if (isRoot)
        {
            link = myArticulation.createLink(nullptr, GetPxTransform(jointTransform));
        }
        else
        {
            CTransform jointParentTransform = someTransforms[jointParentIndex];
            link = myArticulation.createLink(myLinks[jointParentIndex],
                GetPxTransform(someTransforms[jointParentIndex], jointTransform));
            halfHeight = (jointTransform.GetPosition() - jointParentTransform.GetPosition()).Length() / 2.f;

            SetInboundJoint(*static_cast<physx::PxArticulationJoint*>(link->getInboundJoint()),
                GetLocalPxTransformWithOffset(jointTransform, halfHeight),
                GetLocalPxTransformWithOffset(jointParentTransform, -myHalfHeights[key]));
        }

        SetShape(*link, jointIndex, connection.myBone.myDepth, halfHeight, aMaterial, isRoot);

        myLinks[jointIndex] = link;
    }

By storing the links mapped to corresponding joint indices, the links' transforms can be accessed and used to calculate the tranforms of the joints. However, this calculation is insufficient. By storing the height offset of the link the correct position is calculated. But the rotation of the joint has great impact on the animation, and I found no easy way to derive the rotation from the link.

 

std::vector<Matrix4f> Physics::CRagdollInternal::GetGlobalTransforms() const
{
    std::vector<Matrix4f> matrices;
    matrices.resize(myLinks.size());
    for (size_t i = 0; i < myLinks.size(); i++)
    {
        physx::PxArticulationLink& link = *myLinks.find(i)->second;
        CTransform transform(GetTransform(link.getGlobalPose()));
        const Vector3f position = transform.GetPosition();
        const Vector3f offset = transform.GetMatrix().GetRightVector() * -myHalfHeights.find(i)->second;
        transform.SetPosition(position + offset);
        matrices[i] = transform.GetMatrix();
    }
    return matrices;
}

By this point the deadline of the project had almost been reached and I settled for the result.

Finally, all that's left is to set the animation's joints' transforms.

There are great many things I would like to continue on this project. Of course, beginning with getting the correct transform for each joint. Another great improvement in behaviour would be to set swing and twist limits for the joints, that way the ragdoll would only move in ways the creator allows. 

 

But way beyond the scope of the project, I'd want to not only move the animtion through physics simulation, but also update the physics on frames of the animation.

 

By combining beautiful hand-tailored animations and variance based on physical simulation, I believe great feedback and fidelity will follow.

 

In conclusion, using PhysX can be finicky and moving joints is difficult. Due to unfortunate circumstances, this project had to be done teleworking, and the project seemed to expand everytime I learned something new. However, through the frustration of discovering uncharted territory I had great fun.

        

        const std::vector<Matrix4f> globalMatrices = myRagdoll->GetGlobalTransforms();
        for (int i = 0; i < globalMatrices.size(); i++)
        {
            myBoneRagdollTransforms[i] = myBoneStaticOffsets[i].GetCUMatrix() * globalMatrices[i];
        }