Advanced Rigid Body Topics

NVIDIA PhysX SDK

Advanced Rigid Body Topics

Continuous Collision Detection

When continuous collision detection (or CCD) is turned on, the affected rigid bodies will not go through other objects at high velocities (a problem also known as tunneling). To enable CCD, three things need to be happen:

  1. CCD needs to be turned on at scene level:

    PxPhysics* physx;
    ...
    PxSceneDesc desc;
    desc.flags |= PxSceneFlag::eENABLE_SWEPT_INTEGRATION;
    ...
    
  2. Pairwise CCD needs to be enabled in the pair filter:

    static PxFilterFlags filterShader(
            PxFilterObjectAttributes attributes0,
            PxFilterData filterData0,
            PxFilterObjectAttributes attributes1,
            PxFilterData filterData1,
            PxPairFlags& pairFlags,
            const void* constantBlock,
            PxU32 constantBlockSize)
    {
            pairFlags = PxPairFlag::eRESOLVE_CONTACTS;
            pairFlags |= PxPairFlag::eSWEPT_INTEGRATION_LINEAR;
            return PxFilterFlags();
    }
    
    ...
    
    desc.filterShader       = testCCDFilterShader;
    physx->createScene(desc);
    
  3. Swept bounds need to be enabled for each shape that requires CCD:

    PxShape* shape;
    ...
    shape->setFlag(PxShapeFlag::eUSE_SWEPT_BOUNDS, true);
    

Once enabled, CCD only activates at above a certain velocity treshold. This treshold can be controlled using two PxSceneDesc parameters, PxSceneDesc::sweptIntegrationLinearSpeedFactor and PxSceneDesc::sweptIntegrationAngularSpeedFactor.

For a pair of objects with CCD enabled, swept integration will still be skipped if for both objects the below formula evaluates to false:

bool isMovingFast = smallest < ( (linearVelocity.magnitude() * a + angularVelocity.magnitude() * b * largest ) * dt )

Where:

smallest = bounds.halfDimensions().smallestDimension()
largest = bounds.halfDimensions().largestDimension()

a = sweptIntegrationLinearSpeedFactor
b = sweptIntegrationAngularSpeedFactor

a and b default to 2.0f because an object must only move half its size to be considered fast, and this accounts for it.

Articulations

An articulation is a single actor comprising a set of links (each of which behaves like a rigid body) connected together with special joints. Every articulation has a tree-like structure - so there can be no loops or breaks.

Articulations are a somewhat experimental feature of the SDK, under active research. Currently their primary use is modeling physically actuated characters. They support higher mass ratios, more accurate drive models, have better dynamic stability and a more robust recovery from joint separation than standard PhysX joints. However, they are considerably more expensive - the CPU budget on XBox360 for the collision and dynamics of a 20-link articulation is in the region of 0.5 CPU-milliseconds.

Although articulations do not directly build on joints, they use very similar configuration mechanisms. We assume in this section that you are already familiar with using PhysX joints.

Creating an Articulation

To create an articulation, first create the articulation actor without links:

PxArticulation *articulation = physics.createArticulation();

Then add links one by one, each time specifying a parent link (NULL for the parent of the initial link), and the pose of the new link:

PxsArticulationLink* link = articulation->createLink(parent, linkPose);
link->createShape(linkGeometry, material);
PxRigidBodyExt::updateMassAndInertia(*link, 1.0f);

Articulation links have a restricted subset of the functionality of rigid bodies. They may not be kinematic, and they do not support damping, velocity clamping, or contact force thresholds. Sleep state and solver iteration counts are properties of the entire articulation rather than the individual links.

Each time a link is created beyond the first, a PxArticulationJoint is created between it and its parent. Specify the joint frames for each joint, in exactly the same way as for a PxJoint:

PxArticulationJoint *joint = link->getInboundJoint();
joint->setParentPose(parentAttachment);
joint->setChildPose(childAttachment);

Finally, add the articulation to the scene:

scene.addArticulation(articulation);

Articulation Joints

The only form of articulation joint currently supported is an anatomical joint, whose properties are similar to D6 joint configured for a typical rag doll. Specifically, the joint is a spherical joint, with angular drive, a twist limit around the child joint frame's x-axis, and an elliptical swing cone limit around the parent joint frame's x-axis. The configuration of these properties is very similar to a D6 or spherical joint, but the options provided are slightly different.

The swing limit is a hard elliptical cone limit which does not support spring or restitution from movement perpendicular to the limit surface. You can set the limit ellipse angle as follows:

joint->setSwingLimit(yAngle, zAngle);

fot the limit angles around y and z. Unlike the PxJoint cone limit the limit provides a tangential spring to limit movement of the axis along the limit surface. Once configured, enable the swing limit:

joint->setSwingLimitEnabled(true);

The twist limit allows configuration of upper and lower angles:

joint->setTwistLimit(lower, upper);

and again you must explicitly enable it:

joint->setTwistLimitEnabled(true);

As usual with joint limits, it is good practice to use a sufficient limit contactDistance value that the solver will start to enforce the limit before the limit threshold is exceeded.

Articulation joints are not breakable, and it is not possible to retrieve the constraint force applied at the joint.

Driving an Articulation

Articulations are driven through joint acceleration springs. You can set a position target, a velocity target, and spring and damping parameters that control how strongly the joint drives towards the target. You can also set compliance values, indicating how strongly a joint resists acceleration. A compliance near zero indicates very strong resistance, and a compliance of 1 indicates no resistance.

Articulations are driven in two phases. First the joint spring forces are applied (we use the term internal forces for these) and then any external forces such as gravity and contact forces. You may supply different compliance values for at each joint for each phase.

Note that with joint acceleration springs, the required strength of the spring is estimated using just the mass of the two bodies connected by the joint. By contrast, articulation drive springs account for the masses of all the bodies in the articulation, and any stiffness from actuation at other joints. This estimation is an iterative process, controlled using the externalDriveIterations and internalDriveIterations properties of the PxArticulation class.

Articulation Projection

When any of the joints in an articulation separate beyond a specified threshold, the articulation is projected back together automatically. Projection is an iterative process, and the PxArticulation attributes separationThreshold and projectionIterations control when projection occurs and trade cost for robustness.

Substepping

You may want the simulation frequency of physx to be higher than the frame rate of your application, to allow for higher fidelity simulation or better stability. The simplest way to do this is just to call simulate() and fetchResults() multiple times:

for(PxU32 i=0; i<substepCount; i++)
{
        ... pre-simulation work (update controllers, etc) ...
        scene->simulate(substepSize);
        scene->fetchResults(true);
        ... post simulation work (process physics events, etc) ...
}

The code in Samples/SampleBase/SampleStepper.cpp in the sample framework demonstrates a different approach using completion tasks.

Using Completion Tasks

If you submit a completion task to the scene in the simulate() call, the simulation will decrement its reference count when simulation completes, which (assuming there are no outstanding references) will cause the task to run. The completion task first calls fetchResults and performs any per-substep work:

mScene->fetchResults(true);
mSample->onSubstep(mSubStepSize);

Since a task may not submit itself as a completion to simulate(), the completion tasks are double buffered. To start another simulation step, the completion task registers the other task with the task manager (which also sets the reference count to 1), calls simulate() again if necessary:

StepperTask &s = ownerTask == &mCompletion0 ? mCompletion1 : mCompletion0;
s.setContinuation(*mScene->getTaskManager(), NULL);
mScene->simulate(mSubStepSize, &s);

Finally, it releases the reference which prevents the new completion task from executing:

s.removeReference();

Synchronizing with Other Threads

An important consideration for substepping is that simulate() and fetchResults() are classed as write calls on the scene, and it is therefore illegal to read from or write to a scene while those functions are running. PhysX does not lock its scene graph, but it will report an error in the checked build if it detects that multiple threads make concurrent calls to the same scene, unless they are all read calls.

To synchronize with the rendering thread the sample stepper holds an extra reference to the first completion task until the renderDone() method is invoked, so that the renderer can safely read the scene in parallel with simulation. On completion of all substeps, the stepper signals a synchronization object which may be checked with a wait() method.

Custom Constraints

Constraint is a more general term for joints. Constraints use shaders for the same reason as contact filtering: There is a requirement to inject performance sensitive custom code into the SDK. While joints were native objects of the PhysX 2.x API, PhysX 3.0 only supports a fully customizeable constraint object in the core API, and all 2.x joint types are implemented using this mechanism as extensions. Let us take a short look at how this works. Once the reader understands, he will be in a good position to create his own joint types. You should read the chapter on joints before you try to understand their workings, however.

When you call PxJointCreate(), the extensions library first fills out a PxConstraintDesc object, which is a bunch of parameters for constraint creation. Here is the code for a spherical joint:

PxConstraintDesc nxDesc;
nxDesc.actor[0]                         = desc.actor[0];
nxDesc.actor[1]                         = desc.actor[1];
nxDesc.flags                            = desc.constraintFlags;
nxDesc.linearBreakImpulse       = desc.breakForce;
nxDesc.angularBreakImpulse      = desc.breakTorque;

nxDesc.solverPrep                       = SphericalJointSolverPrep;
nxDesc.project                          = SphericalJointProject;
nxDesc.visualize                        = SphericalJointVisualize;

nxDesc.dataSize                         = sizeof(SphericalJointData);
nxDesc.connector                        = joint->getConnector();

The first few settings are self explanatory ... like the actors to connect, when the joint should break, and so on. The next three are three callback functions -- user defined shaders. (See the section on filter shaders to find out what shaders are, and the rules that apply to them.) They contain the code that mathematically defines the behavior of the joint. Every time the joint needs to be solved, the simulation will call these functions.

Finally, the 'connector' is a class of additional user defined joint specific functionality that are not called from the solver directly, and are not shaders.

Lastly, the filled out descriptor is used to create the constraint object:

PxConstraint* constraint = physics.createConstraint(nxDesc);