Skip to content

PxPrismaticJoint breaks depending on actor order at initialization #482

Description

@ashur80

I'm experiencing a strange issue with the PxPrismaticJoint, which in my case connects an articulation link to the ground. I'm trying to model a simple slider-crank mechanism, but depending on the order in which actors are added to the joint, the simulation either works as expected or the prismatic joint breaks entirely.

Edit: The problem I'm adressing with this is the following: I have two articulation links (or some other suitable actor pair) with known world transforms T0 and T1. I want to connect them with a PxPrismaticJoint at a known world transform TJ.
The local frames should be:
L0 = T0.transformInv(TJ)
L1 = T1.transformInv(TJ)
This does not work for some reason. The behavior also appears to depend on actor order in PxPrismaticJointCreate.

Library and Version

PhysX 5.6.1

Operating System

Windows 11

Steps to Trigger Behavior

  1. Create the articulation for the slider-crank mechanism (as shown in the snippet)
  2. Connect the slider link to the ground via a PxPrismaticJoint with actor0 = ground, actor1 = slider link, and their respective local transforms -> simulation works as expected
  3. Reverse the order (actor0 = slider link, actor1 = ground) -> joint breaks immediately

Increasing the number of solver iterations prevents the joint from breaking, but the "reversed order" case still exhibits significantly higher positional error.

The direct and reversed actor orders can be toggled via the reversedOrder boolean variable. The number of solver iterations can be increased by setting iterate32 to true.

Expected Behavior

The actor order has no impact upon the prismatic joint

Actual Behavior

PxPrismaticJoint does not work with the reversed actor order

Code Snippet to Reproduce Behavior

#include "../snippetcommon/SnippetPrint.h"
#include "../snippetcommon/SnippetPVD.h"
#include "../snippetutils/SnippetUtils.h"
#ifdef RENDER_SNIPPET
#include "../snippetrender/SnippetRender.h"
#endif


using namespace physx;
static PxDefaultAllocator      gAllocator;
static PxDefaultErrorCallback  gErrorCallback;

static PxFoundation*           gFoundation = NULL;
static PxPhysics*              gPhysics = NULL;
static PxDefaultCpuDispatcher* gDispatcher = NULL;
static PxScene*                gScene = NULL;
static PxMaterial*             gMaterial = NULL;
static PxPvd*                  gPvd = NULL;

static PxU32		gSceneIndex = 0;
static const bool reversedOrder = true;
static const bool iterate32 = false;


struct moveScene
{
  virtual void createScene() = 0;
  virtual void stepScene(PxReal step) = 0;
};

static moveScene* gMoveScene = nullptr;

struct collidingSliderCrank : moveScene
{
  void createScene() override
  {
    // --- Geometry parameters -------------------------------------------------
    // Crank rotates around X-axis, mechanism extends along Y-axis
    const PxReal crankLength = 1.0f;        // Crank radius [m]
    const PxReal couplerLength = 3.0f;      // Connecting rod length [m]
    const PxReal crankAngle = 0.0f;         // Initial crank angle (0 = along +Y)
    const PxReal density = 500.f;           // Material density [kg/m^3]

    // Calculate initial positions (forward kinematics only, no closed loop)
    // Crank pin position at given angle (rotation around X-axis)
    const PxReal crankPinY = crankLength * PxCos(crankAngle);
    const PxReal crankPinZ = crankLength * PxSin(crankAngle);
    const PxReal crankPinX = 0.0f;

    // Coupler angle: connects crank pin to slider position
    const PxReal couplerAngle = 0.0f;
    const PxReal sliderPosY = crankPinY + couplerLength;

    auto setupLink = [&](PxArticulationLink* link, PxReal hx, PxReal hy, PxReal hz)
    {
      PxShape* shape = gPhysics->createShape(
        PxBoxGeometry(hx, hy, hz), *gMaterial, true);
      link->attachShape(*shape);
      shape->release();
      PxRigidBodyExt::updateMassAndInertia(*link, density);
    };

    // --- Create articulation (open chain: ground -> crank -> coupler -> slider)
    mArticulation = gPhysics->createArticulationReducedCoordinate();
    mArticulation->setArticulationFlag(PxArticulationFlag::eFIX_BASE, true);

    if(iterate32) mArticulation->setSolverIterationCounts(32, 16);

    // --- Base link (fixed pivot at origin) ---------------------------------
    PxArticulationLink* baseLink = mArticulation->createLink(
      nullptr, PxTransform(PxVec3(0.f, 0.f, 0.f)));
    setupLink(baseLink, 0.2f, 0.2f, 0.2f);

    // --- Crank link (rotates around X-axis) --------------------------------
    PxArticulationLink* crankLink = mArticulation->createLink(
      baseLink, PxTransform(PxIdentity));
    setupLink(crankLink, 0.05f, crankLength * 0.5f, 0.05f);

    PxArticulationJointReducedCoordinate* crankJoint = crankLink->getInboundJoint();
    crankJoint->setJointType(PxArticulationJointType::eREVOLUTE_UNWRAPPED);

    // Parent frame: at pivot point (origin in base local space)
    // No rotation needed - X is the default rotation axis for REVOLUTE joint
    crankJoint->setParentPose(PxTransform(PxIdentity));

    // Child frame: at near end of crank (-Y in crank local space)
    crankJoint->setChildPose(PxTransform(PxVec3(shift, -crankLength * 0.5f, 0.f)));

    // Free rotation around X-axis (eTWIST is the rotation around joint X axis)
    crankJoint->setMotion(PxArticulationAxis::eTWIST, PxArticulationMotion::eFREE);
    crankJoint->setJointPosition(PxArticulationAxis::eTWIST, crankAngle);

    // --- Coupler link (connecting rod) --------------------------------------
    PxArticulationLink* couplerLink = mArticulation->createLink(
      crankLink, PxTransform(PxIdentity));
    setupLink(couplerLink, 0.04f, couplerLength * 0.5f, 0.04f);

    PxArticulationJointReducedCoordinate* couplerJoint = couplerLink->getInboundJoint();
    couplerJoint->setJointType(PxArticulationJointType::eREVOLUTE);

    // Parent frame: at crank pin (far end of crank, +Y in crank local space)
    couplerJoint->setParentPose(PxTransform(PxVec3(shift, crankLength * 0.5f, 0.f)));

    // Child frame: at near end of coupler (-Y in coupler local space)
    couplerJoint->setChildPose(PxTransform(PxVec3(-shift, -couplerLength * 0.5f, 0.f)));

    // Free rotation around X-axis
    couplerJoint->setMotion(PxArticulationAxis::eTWIST, PxArticulationMotion::eFREE);
    couplerJoint->setJointPosition(PxArticulationAxis::eTWIST, couplerAngle);

    // --- Slider link (piston) -----------------------------------------------
    gSliderLink = mArticulation->createLink(
      couplerLink, PxTransform(PxIdentity));
    setupLink(gSliderLink, 0.1f, 0.15f, 0.1f);

    PxArticulationJointReducedCoordinate* sliderJoint = gSliderLink->getInboundJoint();
    sliderJoint->setJointType(PxArticulationJointType::eREVOLUTE);

    // Parent frame: at far end of coupler (+Y in coupler local space)
    sliderJoint->setParentPose(PxTransform(PxVec3(-shift, couplerLength * 0.5f, 0.f)));

    // Child frame: identity - joint Y axis aligns with world Y axis
    sliderJoint->setChildPose(PxTransform(PxIdentity));

    // Allow motion only along Y axis (X and Z locked)

    sliderJoint->setMotion(PxArticulationAxis::eTWIST, PxArticulationMotion::eFREE);

    sliderJoint->setJointPosition(PxArticulationAxis::eY, sliderPosY);

    // --- Add drive to crank joint (rotate around X-axis) --------------------
    PxArticulationDrive drive;
    drive.stiffness = 1e7f;        // High stiffness for position control
    drive.damping = 1e4f;          // Adequate damping
    drive.maxForce = 1e6f;
    drive.driveType = PxArticulationDriveType::eACCELERATION;  // Use FORCE for position control
    crankJoint->setDriveParams(PxArticulationAxis::eTWIST, drive);
    crankJoint->setDriveVelocity(PxArticulationAxis::eTWIST, 0.f);
    mCrankJoint = crankJoint;
    // Disable self-collisions between links in the articulation
    mArticulation->setArticulationFlag(PxArticulationFlag::eDISABLE_SELF_COLLISION, true);
    // --- Add articulation to scene -----------------------------------------
    gScene->addArticulation(*mArticulation);

    // Create prismatic joint between slider link and ground
    // Rotate the joint so its X-axis aligns with global Y-axis
    PxQuat jointRotation(PxHalfPi, PxVec3(0.f, 0.f, 1.f)); // Rotate 90 degrees around Z
    PxTransform sliderPose = PxTransform(PxVec3(0.f, 4.f, 0.f));
    PxTransform jointWorldPose(sliderPose.p, jointRotation);
    PxTransform groundPose = PxTransform(PxIdentity);
    const PxTransform localPoseGround = groundPose.transformInv(jointWorldPose);
    const PxTransform localPoseSlider = sliderPose.transformInv(jointWorldPose);
    if (reversedOrder)
    {
      gSliderGroundJoint = PxPrismaticJointCreate(
        *gPhysics,
        gSliderLink,                                       // Child: slider link
        localPoseSlider,
        nullptr,                                      // Parent: ground
        localPoseGround
        // Parent frame rotated
      );          // Child frame also rotated
    }
    else
    {
      gSliderGroundJoint = PxPrismaticJointCreate(
        *gPhysics,
        nullptr,                                      // Parent: ground
        localPoseGround,
        gSliderLink,                                       // Child: slider link
        localPoseSlider
        // Parent frame rotated
      );          // Child frame also rotated
    }
    PX_UNUSED(gSliderGroundJoint);
  }

  void stepScene(PxReal step) override
  {
    if (!mCrankJoint) return;

    mTime += step;

    PxReal targetPosition = PxPi * PxSin(mTime);  // Range: -pi to pi

    mCrankJoint->setDriveTarget(PxArticulationAxis::eTWIST, targetPosition);
    const PxReal sliderZ = gSliderLink->getGlobalPose().p.z;
    if (PxAbs(sliderZ) > PxAbs(mSliderZAbsMax))
      mSliderZAbsMax = sliderZ;
    printf("SliderZ: %f, SliderZAbsMax: %f, Broken: %d\n", sliderZ, mSliderZAbsMax, 
      gSliderGroundJoint != nullptr ? gSliderGroundJoint->getConstraintFlags().isSet(PxConstraintFlag::eBROKEN) : false);
  }
  const PxReal shift = 0.0f;
  PxArticulationReducedCoordinate*        mArticulation = nullptr;
  PxArticulationJointReducedCoordinate*   mCrankJoint = nullptr;
  PxReal                                  mTargetVelocity = 10.0f;
  PxReal mTime = 0;
  PxArticulationLink* gSliderLink = nullptr;
  PxReal mSliderZAbsMax = 0;
  PxPrismaticJoint* gSliderGroundJoint = nullptr;
};

class TestScene
{
  PX_NOCOPY(TestScene)
public:


  static void								 createScene();
  static void                createScene0();
};

void TestScene::createScene0()
{
  gMoveScene = new collidingSliderCrank();
}


void TestScene::createScene()
{

  PX_RELEASE(gScene);
  PX_DELETE(gMoveScene);
  // -- Scene descriptor -----------------------------------------------------
  PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
  sceneDesc.gravity = PxVec3(0.0f, 0.0f, -9.81f);
  sceneDesc.filterShader = PxDefaultSimulationFilterShader;

  gDispatcher = PxDefaultCpuDispatcherCreate(2);
  sceneDesc.cpuDispatcher = gDispatcher;
  sceneDesc.solverType = PxSolverType::ePGS;

  gScene = gPhysics->createScene(sceneDesc);
  PX_ASSERT(gScene);

  // -- Debug visualization --------------------------------------------------
    //  The built-in snippet renderer draws ONLY what PxScene emits via the
    //  PxVisualizationParameter system.  Everything is off by default.
    //  Scale must be > 0.0f to enable the system at all; 1.0f = world-scale.
  gScene->setVisualizationParameter(PxVisualizationParameter::eSCALE, 1.0f);

  // Actors / shapes
  gScene->setVisualizationParameter(PxVisualizationParameter::eACTOR_AXES, 0.5f);
  gScene->setVisualizationParameter(PxVisualizationParameter::eCOLLISION_SHAPES, 1.0f);
  gScene->setVisualizationParameter(PxVisualizationParameter::eCOLLISION_AABBS, 0.5f); // optional, useful for debugging

  // Joints / constraints
  gScene->setVisualizationParameter(PxVisualizationParameter::eJOINT_LOCAL_FRAMES, 1.0f);
  gScene->setVisualizationParameter(PxVisualizationParameter::eJOINT_LIMITS, 1.0f);

  // Bodies
  gScene->setVisualizationParameter(PxVisualizationParameter::eBODY_AXES, 0.5f);
  gScene->setVisualizationParameter(PxVisualizationParameter::eBODY_LIN_VELOCITY, 1.0f);

  // -- PVD scene client (transmit constraints so joints appear in debugger) --
  PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
  if (pvdClient)
  {
    pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
    pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
    pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
  }

  // -- Default material -----------------------------------------------------
  gMaterial = gPhysics->createMaterial(0.5f, 0.5f, 0.1f);

  const PxU32 index = gSceneIndex;
  createScene0();
  gMoveScene->createScene();
}



void initPhysics(bool /*interactive*/)
{
  // -- Foundation -----------------------------------------------------------
  gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);
  PX_ASSERT(gFoundation);

  // -- PVD (optional, connect PhysX Visual Debugger) ------------------------
  gPvd = PxCreatePvd(*gFoundation);
  PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
  gPvd->connect(*transport, PxPvdInstrumentationFlag::eALL);

  // -- Physics --------------------------------------------------------------
  gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation,
    PxTolerancesScale(), /*trackAllocations=*/true, gPvd);
  PX_ASSERT(gPhysics);

  PxInitExtensions(*gPhysics, gPvd);

  // -- Scene content --------------------------------------------------------
  TestScene::createScene();
}

void stepPhysics(bool /*interactive*/)
{
  if (!gScene)
    return;
  const PxReal dt = 1.0f / 50.0f;
  gMoveScene->stepScene(dt);
  gScene->simulate(dt);
  gScene->fetchResults(true);
}

void cleanupPhysics(bool /*interactive*/)
{
  // Release order matters:
  //   scene -> dispatcher -> extensions -> physics -> cuda -> pvd -> foundation
  PX_DELETE(gMoveScene);
  PX_RELEASE(gScene);
  PX_RELEASE(gDispatcher);
  PxCloseExtensions();
  PX_RELEASE(gPhysics);

  if (gPvd)
  {
    PxPvdTransport* transport = gPvd->getTransport();
    gPvd->release();
    gPvd = NULL;
    PX_RELEASE(transport);
  }

  PX_RELEASE(gFoundation);

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

void keyPress(unsigned char key, const PxTransform& /*camera*/)
{
  if (gScene)
  {
    if (key >= 1 && key <= 7)
    {
      gSceneIndex = key - 1;
      PX_RELEASE(gScene);
      TestScene::createScene();
    }

    if (key == 'r' || key == 'R')
    {
      PX_RELEASE(gScene);
      TestScene::createScene();
    }
  }
}

void renderText()
{
#ifdef RENDER_SNIPPET
  Snippets::print("Press F1 to F7 to select a scene.");
#endif
}

int snippetMain(int, const char*const*)
{
  printf("Test snippet. Use these keys:\n");
  printf(" R        - reset scene\n");
  printf(" F1 to F7 - select scene\n");
  printf("\n");
#ifdef RENDER_SNIPPET
  extern void renderLoop();
  renderLoop();
#else
  static const PxU32 frameCount = 600;    // 10 s at 60 Hz
  initPhysics(false);
  for (PxU32 i = 0; i < frameCount; i++)
    stepPhysics(false);
  cleanupPhysics(false);
#endif
  return 0;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions