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.
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.
#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;
}
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
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