jrouwe / JoltPhysics

A multi core friendly rigid body physics and collision detection library. Written in C++. Suitable for games and VR applications. Used by Horizon Forbidden West.
MIT License
6.55k stars 425 forks source link

Spheres collide with internal edges despite mitigations #1155

Closed mihe closed 3 months ago

mihe commented 3 months ago

As talked about in godot-jolt/godot-jolt#587.

The actual relevant circumstances/properties surrounding this one isn't super clear to me, so I'll just lay out the scenario as shown in the issue linked above.

When a SphereShape dynamic body is moving across a static MeshShape body, seemingly with a significant angular velocity, the dynamic body will sometimes "catch" on the internal/inactive edges of the static body and jump up as a result.

This seems to happen regardless of whether Jolt's active edge detection is enabled, or whether the more recent mEnhancedInternalEdgeRemoval is enabled on the dynamic body.

The workaround suggested in the issue linked above was to enable EMotionQuality::LinearCast on the dynamic body, lower the mLinearCastThreshold setting to something like 25% and set both mPenetrationSlop and mSpeculativeContactDistance to 0. While this does resolve the issue of catching on the internal/inactive edges, this however produces another artifact, namely that the dynamic body will randomly get stuck on the static body, almost as if linearly constrained, while its angular movement remains unconstrained, causing it to spin in-place.

I've created a repro for both scenarios in the Samples projects, by hijacking SimpleTest:

Header File

```cpp #pragma once #include #include class SimpleTest : public Test { public: JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, SimpleTest) virtual void Initialize() override; virtual void PrePhysicsUpdate(const PreUpdateParams &inParams) override; private: BodyID mLevelBall; BodyID mSlopeBall; }; ```

Source File

```cpp #include #include #include #include #include #include #include "SimpleTest.h" JPH_IMPLEMENT_RTTI_VIRTUAL(SimpleTest) { JPH_ADD_BASE_CLASS(SimpleTest, Test) } void SimpleTest::Initialize() { MeshShapeSettings plane_mesh({ { Float3(-10, 0, -10), Float3(-10, 0, 10), Float3(10, 0, 10) }, { Float3(-10, 0, -10), Float3(10, 0, 10), Float3(10, 0, -10) }, }); plane_mesh.SetEmbedded(); BodyCreationSettings level_plane(&plane_mesh, RVec3(-10, 0, 0), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING); level_plane.mFriction = 1; mBodyInterface->CreateAndAddBody(level_plane, EActivation::DontActivate); BodyCreationSettings level_ball(new SphereShape(0.5f), RVec3(-10, 1, -9), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING); // level_ball.mMotionQuality = EMotionQuality::LinearCast; level_ball.mEnhancedInternalEdgeRemoval = true; level_ball.mFriction = 1; level_ball.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia; level_ball.mMassPropertiesOverride.mMass = 1; mLevelBall = mBodyInterface->CreateAndAddBody(level_ball, EActivation::Activate); BodyCreationSettings slope_plane(&plane_mesh, RVec3(10, 0, 0), Quat::sRotation(Vec3::sAxisX(), DegreesToRadians(45)), EMotionType::Static, Layers::NON_MOVING); slope_plane.mFriction = 1; mBodyInterface->CreateAndAddBody(slope_plane, EActivation::DontActivate); BodyCreationSettings slope_ball(new SphereShape(0.5f), RVec3(10, 8, -6), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING); // slope_ball.mMotionQuality = EMotionQuality::LinearCast; slope_ball.mEnhancedInternalEdgeRemoval = true; slope_ball.mFriction = 1; slope_ball.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia; slope_ball.mMassPropertiesOverride.mMass = 1; mSlopeBall = mBodyInterface->CreateAndAddBody(slope_ball, EActivation::Activate); PhysicsSettings physics_settings = mPhysicsSystem->GetPhysicsSettings(); // physics_settings.mPenetrationSlop = 0.0f; // physics_settings.mSpeculativeContactDistance = 0.0f; // physics_settings.mLinearCastThreshold = 0.25f; mPhysicsSystem->SetPhysicsSettings(physics_settings); } void SimpleTest::PrePhysicsUpdate(const PreUpdateParams &inParams) { mBodyInterface->AddTorque(mLevelBall, Vec3(JPH_PI * 4, 0, 0)); } ```

Here's what that repro looks like when just using the enhanced internal edge removal (and friction set to 1 on all bodies):

https://github.com/jrouwe/JoltPhysics/assets/4884246/f7245562-0c78-4404-a59c-ef7067568007

Here's what the repro looks like when uncommenting all the commented lines, meaning enabling CCD and changing PhysicsSettings to what was mentioned above:

https://github.com/jrouwe/JoltPhysics/assets/4884246/ddd37b4b-11b3-41a1-b69f-06ed1960160b

leftbones commented 3 months ago

I’m willing to help troubleshoot this in any way I can!

jrouwe commented 3 months ago

Thanks, I'll take a look!

jrouwe commented 3 months ago

I've created a fix for the repro case.

Note that if I uncomment the commented lines, the issue of the ball getting stuck on the slope still happens. This is because in this mode it will only simulate until the first hit (aka time stealing). If:

physics_settings.mPenetrationSlop = 0.0f;

Then it means that it will collide immediately, so will make no progress. Allowing a little slop (10x less than the default):

physics_settings.mPenetrationSlop = 0.002f;

Makes the ball roll down the slope without issues.

mihe commented 3 months ago

I take it (based on #1162) that I should disable the active edge detection for the shape queries in Godot Jolt as well then, when using InternalEdgeRemovingCollector?

jrouwe commented 3 months ago

Yes, I think so. Although we might want to verify first that this actually fixes the original reported issue (which I find hard to judge when I compile godot-jolt with this new code and run the BallStuckBug_2.zip repro).

mihe commented 3 months ago

I can confirm that both BallStuckBug.zip and BallStuckBug_2.zip are fixed.

Before #1162 TestSceneBall.tscn in BallStuckBug.zip shows a significant jump when crossing over the internal edge, with or without mEnhancedInternalEdgeRemoval, but now it only shows it when mEnhancedInternalEdgeRemoval is disabled.

BallStuckBug_2.zip had all the settings applied that we'd talked about, so you'd need to revert those first in order for TestSceneBall.tscn to do the jump on the internal edge. With those reverted it's the same story as with BallStuckBug.zip.