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.42k stars 414 forks source link

Epsilon values in asserts causing issues in Debug builds #1037

Closed jankrassnigg closed 4 months ago

jankrassnigg commented 5 months ago

I typically use Debug builds with Jolt asserts enabled, and I often encounter situations where Jolt asserts that something isn't perfectly normalized and such.

So far I have been able to fix those issues by ensuring that values really are normalized before sending them to Jolt, which is a bit of a performance waste, because they already are normalized, just not with the precision that Jolt expects. So I've already considered just disabling Jolt asserts in Debug builds.

Anyway, recently I've run into a problem with a character controller moving over a landscape and colliding with triangle meshes, which I'm not able to fix this way.

In GetPenetrationDepthStepEPA() there is an assert JPH_ASSERT(support_points.mY[0].IsNearZero(1.0e-8f)); that easily fires for this character (not for others, but I guess it's just because the character uses a wider capsule shape than the others).

image

I've updated to latest Jolt version as of April 5th and can easily reproduce this still. Any ideas? It would be nice if Jolt would either use larger epsilon values in Debug builds, or different types of asserts, that I could disable or where I could configure how sensitive it should be. Because the precision of a lot of computations also depends on the compiler and the compiler settings (e.g. I use my own CMake files).

For reference, here is a video where it happens. There isn't anything to see, though, it crashes at the last second of the video, and the assert message box isn't captured by the recording software, but it shows the type of geometry involved.

https://1drv.ms/v/s!Ajrhg3sdAbZvmIZ3z-EkzppIMhScPg?e=bNv8q7

jrouwe commented 5 months ago

The assert handler passes the filename and line number, so in principle you could use that to ignore certain asserts (albeit with extra work if the line numbers change).

I'd be interested in seeing why this particular assert triggers. The value is nearly 8 mm from the origin which is quite a lot. Would it be possible to isolate an example when this assert triggers? What I usually do is to go a few levels up in the call stack and copy the values from the debugger in a unit test like here:

https://github.com/jrouwe/JoltPhysics/blob/4deaf12b3c12d5890d0cc0d55234fef54d63436b/UnitTests/Physics/CollideShapeTests.cpp#L280-L432

jankrassnigg commented 5 months ago

That's going to be a bit of work, I'll have to see when I can find the time for that.

jrouwe commented 5 months ago

If you paste the debugger values here then I can also create the example. I don't know your callstack so it's hard to tell you exactly which values I need, but it probably comes from CollideConvexVsTriangles::Collide in which case I need the values passed to the constructor of that class (should be a bit higher up in the callstack) + all the parameters to the Collide function and I'm going to need all the internal members of inShape1 passed to the constructor. The classes should all be opened up fully so that I can copy paste the float values with their significant digits.

jankrassnigg commented 4 months ago

Finally had some time to investigate.

So in my character code I call NarrowPhaseQuery::CollideShape() directly to detect contacts with the surroundings. See here.

Arguments to NarrowPhaseQuery::CollideShape() are:

inShape is a CapsuleShape with half height 0.00999999978 and radius 0.300000012. inShapeScale is just all 1, inBaseOffset is all 0.

inCenterOfMassTransform is [Row 0] 1.00000000, 0.00000000, 0.00000000, 7.33719635 [Row 1] 0.00000000, -1.19209290e-07, -1.00000012, 16.2943859
[Row 2] 0.00000000, 1.00000012, -1.19209290e-07, 0.0854607671 [Row 3] 0.00000000, 0.00000000, 0.00000000, 1.00000000

inCenterOfMassTransform was built from a quaternion {x=0.707106829 y=0.00000000 z=0.00000000 w=0.707106829} and position { x=7.33719635, y=16.2943859, z=0.0854607671 }.

The callstack looks like this:

Jolt.dll!JPH::EPAPenetrationDepth::GetPenetrationDepthStepEPA<JPH::AddConvexRadius<JPH::ConvexShape::Support>,JPH::TriangleConvexSupport>(const JPH::AddConvexRadius<JPH::ConvexShape::Support> & inAIncludingConvexRadius, const JPH::TriangleConvexSupport & inBIncludingConvexRadius, float inTolerance, JPH::Vec3 & outV, JPH::Vec3 & outPointA, JPH::Vec3 & outPointB) Line 157    C++
Jolt.dll!JPH::CollideConvexVsTriangles::Collide(const JPH::Vec3 inV0, const JPH::Vec3 inV1, const JPH::Vec3 inV2, unsigned char inActiveEdges, const JPH::SubShapeID & inSubShapeID2) Line 99   C++
Jolt.dll!JPH::MeshShape::sCollideConvexVsMesh(const JPH::Shape * inShape1, const JPH::Shape * inShape2, const JPH::Vec3 inScale1, const JPH::Vec3 inScale2, const JPH::Mat44 & inCenterOfMassTransform1, const JPH::Mat44 & inCenterOfMassTransform2, const JPH::SubShapeIDCreator & inSubShapeIDCreator1, const JPH::SubShapeIDCreator & inSubShapeIDCreator2, const JPH::CollideShapeSettings & inCollideShapeSettings, JPH::CollisionCollector<JPH::CollideShapeResult,JPH::CollisionCollectorTraitsCollideShape> & ioCollector, const JPH::ShapeFilter & inShapeFilter) Line 1116   C++
Jolt.dll!JPH::CollisionDispatch::sCollideShapeVsShape(const JPH::Shape * inShape1, const JPH::Shape * inShape2, const JPH::Vec3 inScale1, const JPH::Vec3 inScale2, const JPH::Mat44 & inCenterOfMassTransform1, const JPH::Mat44 & inCenterOfMassTransform2, const JPH::SubShapeIDCreator & inSubShapeIDCreator1, const JPH::SubShapeIDCreator & inSubShapeIDCreator2, const JPH::CollideShapeSettings & inCollideShapeSettings, JPH::CollisionCollector<JPH::CollideShapeResult,JPH::CollisionCollectorTraitsCollideShape> & ioCollector, const JPH::ShapeFilter & inShapeFilter) Line 40 C++
ezJoltPlugin.dll!ezJoltCustomShapeInfo::sCollideShapeVsUser1(const JPH::Shape * inShape1, const JPH::Shape * inShape2, const JPH::Vec3 inScale1, const JPH::Vec3 inScale2, const JPH::Mat44 & inCenterOfMassTransform1, const JPH::Mat44 & inCenterOfMassTransform2, const JPH::SubShapeIDCreator & inSubShapeIDCreator1, const JPH::SubShapeIDCreator & inSubShapeIDCreator2, const JPH::CollideShapeSettings & inCollideShapeSettings, JPH::CollisionCollector<JPH::CollideShapeResult,JPH::CollisionCollectorTraitsCollideShape> & ioCollector, const JPH::ShapeFilter & inShapeFilter) Line 154 C++
Jolt.dll!JPH::CollisionDispatch::sCollideShapeVsShape(const JPH::Shape * inShape1, const JPH::Shape * inShape2, const JPH::Vec3 inScale1, const JPH::Vec3 inScale2, const JPH::Mat44 & inCenterOfMassTransform1, const JPH::Mat44 & inCenterOfMassTransform2, const JPH::SubShapeIDCreator & inSubShapeIDCreator1, const JPH::SubShapeIDCreator & inSubShapeIDCreator2, const JPH::CollideShapeSettings & inCollideShapeSettings, JPH::CollisionCollector<JPH::CollideShapeResult,JPH::CollisionCollectorTraitsCollideShape> & ioCollector, const JPH::ShapeFilter & inShapeFilter) Line 40 C++
Jolt.dll!JPH::TransformedShape::CollideShape(const JPH::Shape * inShape, const JPH::Vec3 inShapeScale, const JPH::Mat44 & inCenterOfMassTransform, const JPH::CollideShapeSettings & inCollideShapeSettings, const JPH::Vec3 inBaseOffset, JPH::CollisionCollector<JPH::CollideShapeResult,JPH::CollisionCollectorTraitsCollideShape> & ioCollector, const JPH::ShapeFilter & inShapeFilter) Line 95    C++
Jolt.dll!`JPH::NarrowPhaseQuery::CollideShape'::`2'::MyCollector::AddHit(const JPH::BodyID & inResult) Line 258 C++
Jolt.dll!JPH::QuadTree::CollideAABox(const JPH::AABox & inBox, JPH::CollisionCollector<JPH::BodyID,JPH::CollisionCollectorTraitsCollideShape> & ioCollector, const JPH::ObjectLayerFilter & inObjectLayerFilter, const std::vector<JPH::QuadTree::Tracking,JPH::STLAllocator<JPH::QuadTree::Tracking>> & inTracking) Line 1149  C++
Jolt.dll!JPH::BroadPhaseQuadTree::CollideAABox(const JPH::AABox & inBox, JPH::CollisionCollector<JPH::BodyID,JPH::CollisionCollectorTraitsCollideShape> & ioCollector, const JPH::BroadPhaseLayerFilter & inBroadPhaseLayerFilter, const JPH::ObjectLayerFilter & inObjectLayerFilter) Line 440 C++
Jolt.dll!JPH::NarrowPhaseQuery::CollideShape(const JPH::Shape * inShape, const JPH::Vec3 inShapeScale, const JPH::Mat44 & inCenterOfMassTransform, const JPH::CollideShapeSettings & inCollideShapeSettings, const JPH::Vec3 inBaseOffset, JPH::CollisionCollector<JPH::CollideShapeResult,JPH::CollisionCollectorTraitsCollideShape> & ioCollector, const JPH::BroadPhaseLayerFilter & inBroadPhaseLayerFilter, const JPH::ObjectLayerFilter & inObjectLayerFilter, const JPH::BodyFilter & inBodyFilter, const JPH::ShapeFilter & inShapeFilter) Line 281 C++
>   ezJoltPlugin.dll!ezJoltCharacterControllerComponent::CollectContacts(ezDynamicArray<ezJoltCharacterControllerComponent::ContactPoint,ezDefaultAllocatorWrapper> & out_Contacts, const JPH::Shape * pShape, const ezVec3Template<float> & vQueryPosition, const ezQuatTemplate<float> & qQueryRotation, float fCollisionTolerance) Line 314  C++
ezJoltPlugin.dll!ezJoltDefaultCharacterComponent::CheckFeet() Line 463  C++
ezJoltPlugin.dll!ezJoltDefaultCharacterComponent::UpdateCharacter() Line 587    C++
ezJoltPlugin.dll!ezJoltCharacterControllerComponent::Update(ezTime deltaTime) Line 402  C++
ezJoltPlugin.dll!ezJoltWorldModule::FetchResults(const ezWorldModule::UpdateContext & context) Line 672 C++

Now when we reach CollideConvexVsTriangles::Collide() the arguments are as follows:

inV0 is 7.02168655, 15.9783554, 0.0718481541, L^2=304.617096 inV1 is 7.64038849, 15.9783554, 0.0336148739, L^2=313.684509 inV2 is 7.33101273, 16.2876892, 0.0787103176, L^2=319.038757 inActiveEdges is 0 inSubShapeID2 is {mValue=4293976543 }

One step up in the callstack Visitor is initialized with these values: shape1 is the CapsuleShape with {mRadius=0.300000012 mHalfHeightOfCylinder=0.00999999978 } inScale is 1.00000000, 1.00000000, 1.00000000, L^2=3.00000000 inScale2 is 1.00000000, 1.00000000, 1.00000000, L^2=3.00000000 inCenterOfMassTransform1 is 1.00000000, 0.00000000, 0.00000000, 7.33719635 | 0.00000000, -1.19209290e-07, -1.00000012, 16.2943859 | 0.00000000, 1.00000012, -1.19209290e-07, 0.0854607671 inCenterOfMassTransform2 is 1.00000000, 0.00000000, 0.00000000, 0.00000000 | 0.00000000, 1.00000000, 0.00000000, 0.00000000 | 0.00000000, 0.00000000, 1.00000000, 0.00000000

If you are wondering that in the callstack there is a custum shape type ezJoltCustomShapeInfo::sCollideShapeVsUser1() which is just passing things through.

Finally in GetPenetrationDepthStepEPA() where it asserts, support_points.mY.size() is 1 and the first array item is 0.00618362427, -0.00324955024, -0.00669670105, L^2=9.36425931e-05.

mGJK is this:

-       mGJK    {mY=0x00000037e59efa60 {0.00618362427, -0.00324955024, -0.00669670105, L^2=9.36425931e-05, 0.00618362427, -0.00324955024, -0.00669670105, L^2=9.36425931e-05, ...} ...} JPH::GJKClosestPoint
        JPH::NonCopyable    {...}   JPH::NonCopyable
-       mY  0x00000037e59efa60 {0.00618362427, -0.00324955024, -0.00669670105, L^2=9.36425931e-05, 0.00618362427, -0.00324955024, -0.00669670105, L^2=9.36425931e-05, ...}  JPH::Vec3[4]
+       [0] 0.00618362427, -0.00324955024, -0.00669670105, L^2=9.36425931e-05   JPH::Vec3
+       [1] 0.00618362427, -0.00324955024, -0.00669670105, L^2=9.36425931e-05   JPH::Vec3
+       [2] -107374176., -107374176., -107374176., L^2=3.45876422e+16   JPH::Vec3
+       [3] -107374176., -107374176., -107374176., L^2=3.45876422e+16   JPH::Vec3
-       mP  0x00000037e59efaa0 {0.00000000, -0.00999999978, 0.00000000, L^2=9.99999975e-05, 0.00000000, -0.00999999978, 0.00000000, L^2=9.99999975e-05, ...}    JPH::Vec3[4]
+       [0] 0.00000000, -0.00999999978, 0.00000000, L^2=9.99999975e-05  JPH::Vec3
+       [1] 0.00000000, -0.00999999978, 0.00000000, L^2=9.99999975e-05  JPH::Vec3
+       [2] -107374176., -107374176., -107374176., L^2=3.45876422e+16   JPH::Vec3
+       [3] -107374176., -107374176., -107374176., L^2=3.45876422e+16   JPH::Vec3
-       mQ  0x00000037e59efae0 {-0.00618362427, -0.00675044954, 0.00669670105, L^2=0.000128651576, -0.00618362427, -0.00675044954, 0.00669670105, L^2=0.000128651576, ...}  JPH::Vec3[4]
+       [0] -0.00618362427, -0.00675044954, 0.00669670105, L^2=0.000128651576   JPH::Vec3
+       [1] -0.00618362427, -0.00675044954, 0.00669670105, L^2=0.000128651576   JPH::Vec3
+       [2] -107374176., -107374176., -107374176., L^2=3.45876422e+16   JPH::Vec3
+       [3] -107374176., -107374176., -107374176., L^2=3.45876422e+16   JPH::Vec3
        mNumPoints  1   int

As far as I can tell, there is no badly normalized rotation that I pass in (I only have one quaternion that rotates the shape by 90 degree and it looks fine to me). The triangle also seems to not be degenerate (it shouldn't be, it is basically a heightmap, so just a grid of quads, don't see how those can deform too badly). It does seem to only happen with this capsule, I have used capsules with a different radius before and never encountered the issue with them.

This is in a Debug build, and I don't use the Jolt CMake file, so maybe some code generation option is different.

I hope that's all the data you need. Thanks for the help!

jrouwe commented 4 months ago

I hijacked CapsuleVsBoxTest.cpp:

#include <TestFramework.h>

#include <Tests/ConvexCollision/CapsuleVsBoxTest.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/CollideShape.h>
#include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
#include <Jolt/Physics/Collision/CollideConvexVsTriangles.h>
#include <Jolt/Renderer/DebugRenderer.h>

JPH_IMPLEMENT_RTTI_VIRTUAL(CapsuleVsBoxTest)
{
    JPH_ADD_BASE_CLASS(CapsuleVsBoxTest, Test)
}

void CapsuleVsBoxTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
{
    Ref<CapsuleShape> shape = new CapsuleShape(0.00999999978f, 0.300000012f);

    Mat44 com1 = Mat44(Vec4(1.00000000f, 0.00000000f, 0.00000000f, 7.33719635f), Vec4(0.00000000f, -1.19209290e-07f, -1.00000012f, 16.2943859f), Vec4(0.00000000f, 1.00000012f, -1.19209290e-07f, 0.0854607671f), Vec4(0, 0, 0, 1)).Transposed();
    Mat44 com2 = Mat44(Vec4(1.00000000f, 0.00000000f, 0.00000000f, 0.00000000f), Vec4(0.00000000f, 1.00000000f, 0.00000000f, 0.00000000f), Vec4(0.00000000f, 0.00000000f, 1.00000000f, 0.00000000f), Vec4(0, 0, 0, 1)).Transposed();

    CollideShapeSettings settings; // <--- What are these?
    AllHitCollisionCollector<CollideShapeCollector> collector;
    CollideConvexVsTriangles collider(shape, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), com1, com2, SubShapeID(), settings, collector);

    Vec3 v0(7.02168655f, 15.9783554f, 0.0718481541f);
    Vec3 v1(7.64038849f, 15.9783554f, 0.0336148739f);
    Vec3 v2(7.33101273f, 16.2876892f, 0.0787103176f);

    collider.Collide(v0, v1, v2, 0, SubShapeID());

    shape->Draw(DebugRenderer::sInstance, RMat44(com1), Vec3::sReplicate(1.0f), Color::sGreen, false, true);
    DebugRenderer::sInstance->DrawWireTriangle(RVec3(com2 * v0), RVec3(com2 * v1), RVec3(com2 * v2), Color::sGreen);
    DebugRenderer::sInstance->DrawMarker(RVec3(collector.mHits[0].mContactPointOn1), Color::sRed, 0.1f);
    DebugRenderer::sInstance->DrawMarker(RVec3(collector.mHits[0].mContactPointOn2), Color::sRed, 0.1f);
}

I tried in Debug mode under MSVC 2022 17.9.6 and in Clang 17.0.3 (both under Windows) with the following defines:

JPH_DOUBLE_PRECISION;JPH_DEBUG_RENDERER;JPH_PROFILE_ENABLED;JPH_USE_AVX2;JPH_USE_AVX;JPH_USE_SSE4_1;JPH_USE_SSE4_2;JPH_USE_LZCNT;JPH_USE_TZCNT;JPH_USE_F16C;JPH_USE_FMADD

I don't get any asserts (b.t.w. JPH_DOUBLE_PRECISION didn't matter) and it does produce a collision:

image

but it doesn't even go through the EPA path (GJK already finds a valid contact between the 2 shapes, so it earlies out) and if I modify the code so that the early out doesn't exist then it finds support_points.mY.size() == 2 and also doesn't assert.

The only values that I didn't have are those from the CollideShapeSettings object. Perhaps you can provide these? (although I don't think it will make much difference).

Another thing I'd like you to try is if you copy paste the contents of PrePhysicsUpdate in your own codebase, will it then trigger the assert? (just to make sure that I got all the values right and to see if it could indeed be compiler config specific)

jankrassnigg commented 4 months ago

The CollideShapeSettings are indeed the key to making it crash!

I pasted your code into mine and it worked just fine. I then copied the settings from the failing code path, which are like this:

JPH::CollideShapeSettings settings;
settings.mCollisionTolerance = 0.00999999978f;
settings.mBackFaceMode = JPH::EBackFaceMode::CollideWithBackFaces;

Now it crashes reproducably. I hardcoded the collision tolerance to 0.01f, I assume that's the problem. Though frankly it's a value that I don't understand well, anyway.

jrouwe commented 4 months ago

Ah, that makes perfect sense now. You're telling the system that you're ok with reporting a collision if objects are actually separated by 0.01 (this is quite a high number). In this case we should have mY[0].Length() < 0.01 instead of 1.0e-4. I'll see if I can patch this up (unfortunately a different tolerance is passed to that function)

jrouwe commented 4 months ago

Should be fixed now!

jankrassnigg commented 4 months ago

Awesome! Thank you so much!