NVIDIAGameWorks / PhysX

NVIDIA PhysX SDK

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Shape insertion order affects contact target velocity

alektron opened this issue · comments

When trying to simulate a conveyor belt by using the contact modification callback and PxContactsSet::setTargetVelocity, under very specific circumstances it is possible that a dynamic actor that sits on top of the 'conveyor' moves in the opposite direction,
specified in setTargetVelocity.

A standalone code snippet is added below. I tried to make it as comprehensible as possible, yet it is still rather large.
The example sets up contact modification for a kinematic actor (our 'conveyor') with a constant target velocity in the positive x direction. We then perform the following steps before the simulation runs:

  1. A simple dynamic actor A (consisting only of a single shape) and a conveyor C (in this order) get added to the scene.
  2. Run the simulation. A will begin moving in the positive x direction, as expected.
  3. After a short time, we remove A from the scene.
  4. Add a new dynamic actor B to the scene. This time however, the actor will consist of more than just one shape.
    It now moves into the negative x direction.

Some details are important to reliably reproduce the bug:

  1. The first dynamic actor must only consist of a SINGLE shape while the second dynamic actor must consist of MORE THAN ONE shape.
  2. The first dynamic actor must be added to the scene BEFORE the conveyor.
  3. The first dynamic actor must be removed from the scene while it is still moving on the conveyor.
    If it drops off the conveyor before being removed, the bug will not happen.

PhysX build settings: vc16win64.xml | build static lib | use dynamic WINCRT lib | Precise math: false
PhysX 4.1
Commit: c3d5537
Visual Studio 2019 v142
C++ 14

The code snippet below has no dependencies other than PhysX.
PVD must be running.

#include "PxPhysicsAPI.h"

using namespace physx;

class PhysicsCallbacks : public PxContactModifyCallback
{
  virtual void onContactModify(PxContactModifyPair* const pairs, PxU32 count)
  {
    for (PxU32 i = 0; i < count; i++) {
      PxContactModifyPair& pair = pairs[i];
      for (PxU32 j = 0; j < pair.contacts.size(); j++) {
        //We set a constant target velocity for every contact that requests contact modification
        pair.contacts.setTargetVelocity(j, PxVec3(0.5f, 0, 0));
      }
    }
  }
};

PxFilterFlags SimulationFilterShader(
  PxFilterObjectAttributes attributes0, PxFilterData filterData0,
  PxFilterObjectAttributes attributes1, PxFilterData filterData1,
  PxPairFlags& pairFlags, const void*, PxU32)
{
  pairFlags |= PxPairFlag::eCONTACT_DEFAULT;

  //we store 'true' in word0 to indicate that this shape has a surface velocity
  if (filterData0.word0 || filterData1.word0)
    pairFlags |= PxPairFlag::eMODIFY_CONTACTS;

  return PxFilterFlag::eDEFAULT;
}

struct Shape
{
  float HalfX = 0;
  float HalfY = 0;
  float HalfZ = 0;

  float PosX = 0;
  float PosY = 0;
  float PosZ = 0;
};

PxRigidDynamic* CreateActor(PxPhysics* ph, const PxMaterial* material, float yOffset, Shape* shapes, size_t numShapes, bool kinematic, bool surfaceVel)
{
  PxRigidDynamic* dyn = ph->createRigidDynamic(PxTransform(PxVec3(0, yOffset, 0)));
  dyn->setRigidBodyFlag(PxRigidBodyFlag::eKINEMATIC, kinematic);

  for (size_t i = 0; i < numShapes; i++) {
    auto& shape = shapes[i];

    auto pxShape = ph->createShape(PxBoxGeometry(shape.HalfX, shape.HalfY, shape.HalfZ), *material, true);
    pxShape->setLocalPose(PxTransform(PxVec3(shape.PosX, shape.PosY, shape.PosZ)));
    PxFilterData filter{};
    filter.word0 = surfaceVel;
    pxShape->setSimulationFilterData(filter);
    dyn->attachShape(*pxShape);
    pxShape->release();
  }

  PxRigidBodyExt::setMassAndUpdateInertia(*dyn, 5.0f);
  return dyn;
}

int main()
{
  //Initialize the SDK. Mostly default values, nothing fancy
  PxDefaultErrorCallback errCb;
  PxDefaultAllocator defaultAlloc;
  auto foundation = PxCreateFoundation(PX_PHYSICS_VERSION, defaultAlloc, errCb);

  auto pvd = PxCreatePvd(*foundation);
  auto transport = PxDefaultPvdSocketTransportCreate("127.0.0.1", 5425, 1500);
  pvd->connect(*transport, PxPvdInstrumentationFlag::eALL);

  auto physics = PxCreateBasePhysics(PX_PHYSICS_VERSION, *foundation, PxTolerancesScale(), false, pvd);
  auto pxMaterial = physics->createMaterial(0.4f, 0.4f, 0.8f);

  //Create a scene. Same as before, mostly default values.
  //Except for our FilterShader and contact modification callback
  PxSceneDesc sceneDesc = PxSceneDesc(physics->getTolerancesScale());
  sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
  sceneDesc.cpuDispatcher = PxDefaultCpuDispatcherCreate(0);
  sceneDesc.filterShader = SimulationFilterShader;
  sceneDesc.contactModifyCallback = new PhysicsCallbacks();

  auto scene = physics->createScene(sceneDesc);

  //Create some simple shapes to be used by CreateActor
  Shape conveyorShapes  [] = { { /*HalfSize*/ 2.5f, 0.5f, 0.5f, /*Pos*/ 0.0f, 0, 0 } };
  Shape simpleDynShapes [] = { { /*HalfSize*/ 0.5f, 0.5f, 0.5f, /*Pos*/ 0.0f, 0, 0 } };
  Shape complexDynShapes[] = 
  { 
    { /*HalfSize*/ 0.5f, 0.2f, 0.5f, /*Pos*/ 0,  0.3f, 0 },
    { /*HalfSize*/ 0.5f, 0.2f, 0.5f, /*Pos*/ 0, -0.3f, 0 },
  };

  auto conveyor   = CreateActor(physics, pxMaterial, 0.0f, conveyorShapes  , 1, true, true);
  auto simpleDyn  = CreateActor(physics, pxMaterial, 1.0f, simpleDynShapes , 1, false, false);
  auto complexDyn = CreateActor(physics, pxMaterial, 1.0f, complexDynShapes, 2, false, false);

  //Several conditions (X) must be true to reproduce the bug.
  //(1) A dynamic actor (simpleDyn) that only consists of a single shape (!) gets added to the scene BEFORE the conveyor.
  const bool SHOW_BUGGY_BEHAVIOUR = true;
  if (SHOW_BUGGY_BEHAVIOUR) {
    //The bug only happens if the dynamic actor gets added to the scene BEFORE the conveyor.
    //(Creation order aka the order of calls to CreateActor above does not seem to affect it).
    scene->addActor(*simpleDyn);
    scene->addActor(*conveyor);
  }
  else {
    //If the conveyor gets added before the dynamic, everything works as expected
    scene->addActor(*conveyor);
    scene->addActor(*simpleDyn);

    //In addition, if we replace simpleDyn with complexDyn, the bug also does not reproduce
    //scene->addActor(*conveyor);
    //scene->addActor(*complexDyn);
  }

  size_t frameCount = 0;
  while (true) {
    scene->simulate(0.033f);
    scene->fetchResults(true);
    
    //(2) The simple dynamic actor (single shape only) gets removed.
    //IMPORTANT: This has to happen before the actor falls off the conveyor!
    if (frameCount == 20) {
      simpleDyn->release();
    }

    //(3) After the simple dynamic actor got removed, a new actor that consists of MORE THAN ONE shape
    //gets added to the scene.
    //Despite a target velocity of +0.5 on the x axis, the actor will now move in the negative x direction
    if (frameCount == 21) {
      scene->addActor(*complexDyn);
    }

    if (frameCount > 60) {
      break;
    }
    frameCount++;
  }

  return 0;
}

My knowledge of physics engines is VERY limited.
However I have been trying for a while to debug the PhysX SDK to find the issue. I obviously could not fix the issue but there are some things I have noticed. I will add them here, in case it might help.

  1. When solving the contacts for actor B, solveContactBlock gets chosen from gVTableSolveBlock but ONLY in the reproduction case. When e.g. using a multi shape actor for actor A AND B, other solver functions get used.
  2. solveContactBlock then calls solveContact where the 'flipping' of the velocity sign seems to take place.
    Specifically when linVel1 gets calculated. deltaF is a positive value, which (at least intuitively) makes sense. However the result of the calculation
linVel1 = V3NegScaleSub(delLinVel1, deltaF, linVel1);

causes linVel1 to become negative which seems to produce the negative velocity (?)

Hi @alektron ,
Thanks for filing this issue and providing a reproduction snippet, and sorry for the very long response time.

I tried to run your snippet with the newest version of PhysX (https://github.com/NVIDIA-Omniverse/PhysX) and could not reproduce it anymore. A (potentially) related ticket was filed there (NVIDIA-Omniverse/PhysX#153), however undoing that fix still does not cause the behavior you describe. Could you please double-check whether the problem still occurs for you?

Note that for your snippet to run with PhysX 5, all I had to change was to replace PxCreateBasePhysics with PxCreatePhysics.

While I did not try to reproduce the issue with PhysX 5, I can give some insight in what seemed to cause the issue and what fixed it for us.
The main culprit seemed to be the order in which the solver (and therefore the contact modification callback) was receiving the shapes in the shapes pair.

With the shape pair being:

shape[0] = dynamic actor
shape[1] = conveyor

everything works as expected.
But with the shape pair being:

shape[0] = conveyor
shape[1] = dynamic actor

the dynamic actor was going in the opposite direction.
The insertion order of the shapes/actors seemed to influence the order in the shape pair and in some scenarios lead to the second case.
Our 'fix' was to just double check which of the shapes is the one that represents the 'conveyor' and flip the target velocity if necessary.

After some debugging sessions I did have some more insight into what exactly was causing that behavior but unfortunately I can not recall most of it. The only hint I have is this comment I left in our code. It may not even be correct but maybe it will be helpful to someone:

//The physx solver will evaluate velocities relative as seen from the shape at index 1 (pair.shape[1]).
//Since the target velocity is set on a per-contact basis (and not on a per-shape basis) and there is no guarantee in what order
//the solver will get the shapes, we must account for that and flip the target velocity if the shape with surface velocity is shape 0.

To the best of my knowledge there are no guarantees about the order of pairs that come into the onContactModify callback. So you indeed need to check which body is the conveyor and apply the target velocity accordingly.

I just always assumed that the target velocity must be in global space.
As far as I know the documentation doesn't explicitly mention it so it seemed to be the most obvious assumption.

Maybe this is something that could/should be added to the docs.
In my case global space was working just fine for quite a while (and especially during testing) until the bug happened seemingly at random in production.
It was quite an effort to reproduce locally and track down. This could prevent some headaches for other people in the future.

I will add this to the documentation. Thanks again for raising awareness and tracking down the problem you were seeing :)