NVIDIAGameWorks / PhysX

NVIDIA PhysX SDK
Other
3.17k stars 802 forks source link

Chained fixed PxD6 joint is very weak #308

Closed pauzel closed 3 years ago

pauzel commented 4 years ago

I have been observing some undesired behaviour with a chain of two non-kinematic rigid bodies connected to a stationary kinematic rigid body over two PxD6 joints like this:

base (kinematic) -> fixed PxD6Joint -> movable1 -> fixed PxD6Joint -> movable2

While the first joint seems reasonably rigid, the second joint is not. This can be seen in the following gifs, where I applied the same vertical impulse once to movable1 and once to movable2:

Vertical impulse on movable1 (middle cube): y_impulse_movable1

Vertical impulse on movable2 (right cube): y_impulse_movable2

As can be seen, the joint betwen movable1 and movable2 exhibits lots of flexibility and the resulting oscillation takes a long time to settle down.

I observed the same behaviour if I set all the DoFs of the PxD6Joints to free and used very strong drive stiffnesses instead for rotation and translation (>1e9).

My question would be if this is expected, and if yes, is there some logical explanation for this behaviour? Am I doing something wrong in my code? Below you can find my test snippet I compiled with PhysX 4.1.

Thank you!

#include <ctype.h>

#include "PxPhysicsAPI.h"

#include "../snippetcommon/SnippetPrint.h"
#include "../snippetcommon/SnippetPVD.h"
#include "../snippetutils/SnippetUtils.h"

using namespace physx;

PxDefaultAllocator      gAllocator;
PxDefaultErrorCallback  gErrorCallback;

PxFoundation* gFoundation = NULL;
PxPhysics* gPhysics = NULL;

PxDefaultCpuDispatcher* gDispatcher = NULL;
PxScene* gScene = NULL;

PxMaterial* gMaterial = NULL;

PxPvd* gPvd = NULL;

PxD6Joint* Joint1 = nullptr;
PxRigidDynamic* Movable1 = nullptr;

PxD6Joint* Joint2 = nullptr;
PxRigidDynamic* Movable2 = nullptr;

bool ImpulseTriggered1 = false;
bool ImpulseTriggered2 = false;

void initPhysics(bool interactive)
{
    PX_UNUSED(interactive);

#ifdef PX_FOUNDATION_VERSION
    gFoundation = PxCreateFoundation(PX_FOUNDATION_VERSION, gAllocator, gErrorCallback);
#else
    gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);
#endif

    gPvd = PxCreatePvd(*gFoundation);
    PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
    gPvd->connect(*transport, PxPvdInstrumentationFlag::eALL);

    gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(), true, gPvd);

    PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
    sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
    gDispatcher = PxDefaultCpuDispatcherCreate(2);
    sceneDesc.cpuDispatcher = gDispatcher;
    sceneDesc.filterShader = PxDefaultSimulationFilterShader;
    gScene = gPhysics->createScene(sceneDesc);

    PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
    if (pvdClient)
    {
        pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
        pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
        pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
    }
    gMaterial = gPhysics->createMaterial(0.5f, 0.5f, 0.6f);

    PxRigidStatic* groundPlane = PxCreatePlane(*gPhysics, PxPlane(0, 1, 0, 0), *gMaterial);
    gScene->addActor(*groundPlane);

    PxRigidDynamic* base = PxCreateDynamic(*gPhysics, PxTransform(PxVec3(0, 0.5, 0)), PxBoxGeometry(0.5f, 0.5f, 0.5f), *gMaterial, 1.0f);
    base->setMass(1);
    base->setRigidBodyFlag(PxRigidBodyFlag::eKINEMATIC, true);
    gScene->addActor(*base);

    Movable1 = PxCreateDynamic(*gPhysics, PxTransform(PxVec3(0, 2.5, 2.0)), PxBoxGeometry(0.5f, 0.5f, 0.5f), *gMaterial, 1.0f);
    Movable1->setMass(1);
    gScene->addActor(*Movable1);

    PxTransform jT1 = PxTransform(Movable1->getGlobalPose().p);
    Joint1 = PxD6JointCreate(
        *gPhysics,
        base,
        base->getGlobalPose().getInverse().transform(jT1),
        Movable1,
        Movable1->getGlobalPose().getInverse().transform(jT1)
    );

    Movable2 = PxCreateDynamic(*gPhysics, PxTransform(PxVec3(0, 4.5, 4.0)), PxBoxGeometry(0.5f, 0.5f, 0.5f), *gMaterial, 1.0f);
    Movable2->setMass(1);
    gScene->addActor(*Movable2);

    PxTransform jT2 = PxTransform(Movable2->getGlobalPose().p);
    Joint2 = PxD6JointCreate(
        *gPhysics,
        Movable1,
        Movable1->getGlobalPose().getInverse().transform(jT2),
        Movable2,
        Movable2->getGlobalPose().getInverse().transform(jT2)
    );
}

void stepPhysics(bool interactive)
{
    PX_UNUSED(interactive);
    gScene->simulate(1.0f / 60.0f);
    gScene->fetchResults(true);

    if (Joint1 != nullptr && Movable1 != nullptr)
    {
        PxSceneWriteLock scopedLock(*gScene);

        if (ImpulseTriggered1)
        {
            Movable1->setLinearVelocity(PxVec3(0, 10, 0));
            ImpulseTriggered1 = false;
        }

        if (ImpulseTriggered2)
        {
            Movable2->setLinearVelocity(PxVec3(0, 10, 0));
            ImpulseTriggered2 = false;
        }
    }
}

void cleanupPhysics(bool interactive)
{
    PX_UNUSED(interactive);
    gScene->release();
    gDispatcher->release();
    gPhysics->release();
    PxPvdTransport* transport = gPvd->getTransport();
    gPvd->release();
    transport->release();

    gFoundation->release();

    printf("SnippetConstraint done.\n");
}

void keyPress(unsigned char key, const PxTransform& camera)
{
    PX_UNUSED(camera);

    switch (toupper(key))
    {
    case '1':   ImpulseTriggered1 = true; break;
    case '2':   ImpulseTriggered2 = true; break;
    }
}

int snippetMain(int, const char* const*)
{
#ifdef RENDER_SNIPPET
    extern void renderLoop();
    renderLoop();
#else
    static const PxU32 frameCount = 100;
    initPhysics(false);
    for (PxU32 i = 0; i < frameCount; i++)
        stepPhysics(false);
    cleanupPhysics(false);
#endif

    return 0;
}
kstorey-nvidia commented 4 years ago

The default settings in PhysX are mostly aimed at high-performance simulations for games. It uses an iterative solver that, by default, runs just 4 iterations.

You have a few options available to you.

You could continue to use rigid bodies and joints and improve behaviour by a combination of (1) increasing the number of solver position iterations (2) switching to use the TGS solver

Simply increasing the number of position iterations should resolve your issue. However, if you increase the complexity of the jointed models you are simulating, you will still be able to create some configurations where solver error is still visible - this will be especially evident if you have large mass ratios between connected rigid bodies. The TGS solver converges faster than the default PGS solver, which means it can handle larger mass ratios more stably. However, there are still limits to what can be simulated with it.

An alternative would be to switch to use the reduced coordinate articulation system. This system provides an error-free simulation for these kinds of multi-body simulation problems that does not suffer from issues related to mass ratios.

pauzel commented 4 years ago

Thank you for your response and suggestions. I understand that the PhysX rigid body implementation is not optimized for maximum accuracy and I will try your suggested changes. I am a bit limited in my options unfortunately since I am using UE4, which does not expose articulations (yet).

Still, I found the behaviour a bit surprising since I was only expecting major accuracy issues for large mass ratios, whereas the masses I use in the snippet are all identical. I assume a kinematic rigid body effectively counts as an infinite mass, so that may be part of the explanation. But then it is unclear to me why the joint that is actually attached to two extremely different masses works really well and the joint that is attached to two identical masses does not.

Is there a way to intuitively predict such issues for certain joint/mass configurations before running into them? This would be helpful to avoid design decisions that later don't work out because of simulation issues...

Thank you for your help!

kstorey-nvidia commented 4 years ago

The simplest way to think of it is like this: with an iterative solver, if you just have 1 constraint, you can always solve that perfectly. Even with large mass ratios, it will solve perfectly because the iterative solver can calculate how to solve a single constraint absolutely perfectly.

Where it starts to struggle is when you have multiple constraints influencing a single body. In this case, the solver has to find a solution that satisfies not a single constraint, but multiple constraints. However, it only knows how to solve a single constraint perfectly, and it has to iterate to converge on a global solution that satisfies all of the constraints.

A single d6 joint acting as a fixed joint actually introduces 6 constraints into the system, so the current system you are solving has 12 constraints to satisfy.

PhysX pre-processes each D6 constraint to ensure that a single D6 joint converges to a perfect answer in a single iteration, which explains why you don't see any drift in a single-D6/two-body case. However, this pre-processing doesn't extend to larger systems and, as such, we rely on the iterative solver to converge. The default 4 solver iterations are not enough to satisfy this exact case with large forces being applied perfectly. If you don't apply large forces, it will reach a stable state with a small amount of residual error able to counteract the gravitational effect, but it won't solve completely error-free unless allowed to run more iterations.

The following might be counter-intuitive, but fixed joints are actually one of the most difficult kind of joint for a maximal coordinate solver to solve because it constrains all degrees of freedom. However, it is very easy to work around this with fixed joints because you can alternatively create a multi-shape rigid body and avoid creating a joint at all.

Also, an interesting observation - your rigid bodies may have uniform masses, but the joint between the 2nd and 3rd body is located at the COM of the 3rd body, which means that the effective mass ratio in this joint caused by the moments is not uniform (but also not super extreme so it's within the range that the solver is expected to handle OK given sufficient iterations).

pauzel commented 4 years ago

Thank you for the detailed explanation, I think I understand the problem a bit better now! A multi shaped rigid body is unfortunately not a solution for me, since I want to change/animate the constraint transform at runtime. I tried increasing the iteration count as you proposed, and the behaviour improved a lot with an iteration count of 50-100, though the joint still has some glitches that cause issues with my application.

I guess I will have a look into how difficult it is to add some sort of articulation support to a custom UE4 build, though I guess that would be quite a significant effort.

Thanks again for your help! Please feel free to close this ticket.

PierreTerdiman commented 3 years ago

"Please feel free to close this ticket."

So, I did :)