NVIDIAGameWorks / PhysX

NVIDIA PhysX SDK
Other
3.15k stars 796 forks source link

Unexpected behavior from the simulation for a high elapsed time #79

Closed totoze closed 5 years ago

totoze commented 5 years ago

Hello. I have noticed an unexpected behavior from the physics simulation when the delta time (or elapsed time) value of my application is higher than normal. (about 0.5 to 2.0). The simulation seems to make all non kinematic move up (kinematic done by setting mass and inertia to zero , according to the docs :

"When provided with a 0 mass or inertia value, PhysX interprets this to mean infinite mass or inertia around that principal axis. This can be used to create bodies that resist all linear motion or that resist all or some angular motion. Examples of the effects that could be achieved using this approach are: Bodies that behave as if they were kinematic." )

Here's the code used to init physX

mFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator,gReporter);

f (!mFoundation)
            Debug::Critical("PhysX: PxCreateFoundation failed!");
        bool recordMemoryAllocations = true;
        mPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *mFoundation,
            PxTolerancesScale(), recordMemoryAllocations);
        if (!mPhysics)
            Debug::Critical("PhysX: PxCreatePhysics failed!");
        PxSceneDesc sceneDesc(mPhysics->getTolerancesScale());
        sceneDesc.gravity = PxVec3(0.0f, 0.0f, 0.0f);
        mDispatcher = PxDefaultCpuDispatcherCreate(std::thread::hardware_concurrency());
        sceneDesc.cpuDispatcher = mDispatcher;
        sceneDesc.filterShader = PxDefaultSimulationFilterShader;
        mScene = mPhysics->createScene(sceneDesc);
        if (!mScene)
            Debug::Critical("PhysX: createScene failed!");
        mMaterial = mPhysics->createMaterial(0.5f, 0.5f, 0.5f);
        Debug::Info("PhysX init");

Every frame I do mScene->fetchResults(true);before update and mScene->simulate(deltaTime);after the update. Since im sure it's hard to visualize the issue I have a 30 sec GIF of the bug in action. On the gif you can see the balls which are dynamicbodys with a sphere shape attached bounce up when delta time is high (in this case delta time is high since the window resize event blocked the main thread)

ezgif Note the actors don't disappear at the end of the GIF they just went very high up and will fall from the sky back to the ground (which is a dynamic body with a box shape attached and a mass set to zero). It also seems like the amount in which the actors go up increases when the value of delta time is higher. Here's the code running every frame


   void PreUpdate(float deltaTime)override {    //Set Physics simulation changes to the scene
                mScene->fetchResults(true);
        for (auto entity : Events::scene->entities)
            for (auto component : entity->components)
                if (component->GetName() == "RigidBody")
                    entity->transform = ((RigidBody*)component)->GetTransform();
    }
    void PostUpdate(float deltaTime)override {  //Set Scene changes To Physics simulation
        for (auto entity : Events::scene->entities)
            for (auto component : entity->components)
                if (component->GetName() == "RigidBody")
                    ((RigidBody*)component)->SetTransform(entity->transform);
                mScene->simulate(deltaTime);
                if(deltaTime > 0.2f)Debug::Log("delta time {0}" , deltaTime);
    }

And the ground is sated up like this:

//state is a pointer to a physx::PxRigidDynamic
        state->setMass(0.f);
    state->setMassSpaceInertiaTensor(PxVec3(0.f));
    state->setActorFlag(PxActorFlag::eDISABLE_GRAVITY,true);

physX is also sending three massages only at the start of the simulation :

[2019-03-07 23:16:15.657] [error] PhysX: PxScene::fetchResults: fetchResults() called illegally! It must be called after advance() or simulate() 
[2019-03-07 23:16:15.659] [error] PhysX: PxScene::collide/simulate: The elapsed time must be positive! 
[2019-03-07 23:16:15.754] [error] PhysX: PxScene::fetchResults: fetchResults() called illegally! It must be called after advance() or simulate() 

I assume it's something that has to do with the first frame only and isn't really related to current issue. What am I doing wrong here ?

kstorey-nvidia commented 5 years ago

Hi

I think the issue might be the following:

(1) When you resize the screen, there is a sudden perf spike and the elapsed time passed to PhysX is large (2) Due to this large elapsed time, the effect of gravity on the spheres is large (large input velocity, leading to a bounce or, if a collision is missed, the spheres will pass through the ground (3) If the collision(s) were missed and the frame rate returns to normal, there is a large impulse applied (proportional to penetration * 1/dt) to correct the error, leading to the spheres exploding into the sky.

A time-step of 2s will lead to a stationary sphere's velocity becoming -19.6m/s (-9.8*2) and it instantaneously travelling 39.2m below its previous position due to how Euler integration works. If it was positioned just above the ground and the frame rate returned to 1/60, it would need to correct 39.2m of error in 1/60th of a second, leading to a new velocity of 2352m/s, which is apparently 6.86x the speed of sound!

As a general rule, these kind of bad situations can happen if you simulate physics using variable, unbounded time-steps. It is recommended that you either simulate using fixed time-steps (ideally) or, alternatively, semi-variable time-steps, e.g. there is some variability but you do not allow the time-step to become large. What constituted "too large" depends on your particular use-case but most game engines won't simulate physics with time-steps larger than 0.03333s by default.

If you simulate with fixed time-steps, you may need to simulate multiple times for each rendered frame if your frame rate is low. If you run variable time-steps, you would just need to simulate once per frame. However, in both cases, you should clamp the total amount of time that is simulated to ensure that (1) performance does not suffer if you are simulating multiple fixed time-steps and (2) that behaviour does not become unacceptable if you are simulating variable time-steps. This would have the effect of your simulation running slower than real-time, but this would be preferable to bad behavior or a large number of physics simulation calls limiting performance.

With regards to kinematic bodies - you can emulate a kinematic by adjusting the mass/inertia and disabling gravity on bodies, but the more efficient way to simulate kinematics is to set the PxRigidBodyFlag::eKINEMATIC flag on the rigid body. If you do this, PhysX can skip some work inside the solver with those bodies. In addition, if the bodies will never move, you should use PxRigidStatic rather than a kinematic PxRigidDynamic as this will reduce memory usage and improve performance compared to kinematic bodies.

Hope this helps

totoze commented 5 years ago

Thanks you for your detailed answer. regarding the option of simulating with fixed time steps something I saw game engines like unity(using physX) do and seems like the more correct solution for the problem. Would I still be able to take advantage of multiple threads and run the simulations (some frames will have zero simulations and others a few simulations) asynchronously?.

totoze commented 5 years ago

the issue is solved. I implemented a fixed update event firing 60 times per second. I stopped using physX asynchronously ,but actually got a performance increase since I have to make less calls to physX when my application is running on more than 60 fps. I implemented the fixed update event like this:

//very simplified example code  
float fixedTime = 1.f/60.f;
float deltaCache = 0;
void DoFrame() {
        auto start = Time::now();
        PollEvents();   
        renderer.Clear();   
        Events::OnUpdate();
        deltaCache += deltaTime;
        for (int i = 0; i < floor(deltaCache / fixedTime); i++)
        {
            Events::OnFixedUpdate();
            deltaCache -= fixedTime;
        };
        Render();
        SwapBuffers(Screen::window);
        auto end = Time::now(); 
        deltaTime = ((fsec)(end - start)).count();
}