Skip to content

Commit c23bfd2

Browse files
committed
Fix EPA contact point for mesh triangles with long edges
When a convex shape collides with a mesh triangle whose edge length is much larger than the penetration depth (e.g. ~84 m edge vs ~5 cm penetration), EPA converges to the wrong Minkowski polytope facet due to float32 precision loss. GetPenetrationDepthStepEPA returns true but point2 can end up far from point1, producing degenerate contact data. If point2 is farther from point1 than the query shape bounding-box diagonal, snap it to the closest point on the triangle and update penetration_axis. On the same geometry GJK can also return EStatus::Colliding with a garbage point1; that is caught by checking point1 against a 2x-expanded mBoundsOf1 and redirecting to EPA. Reproducible with open-world game levels where large floor/wall tiles are used directly as physics mesh geometry. Subdividing such meshes is not always viable: the BVH codec has a hard size limit that is exceeded when large terrain meshes are split to a safe edge length. Regression test uses the exact triangle vertices from a production crash. Requires CROSS_PLATFORM_DETERMINISTIC to reproduce; with FMA enabled the GJK arithmetic avoids the bad path.
1 parent f26e382 commit c23bfd2

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

Jolt/Physics/Collision/CollideConvexVsTriangles.cpp

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <Jolt/Physics/Collision/NarrowPhaseStats.h>
1313
#include <Jolt/Geometry/EPAPenetrationDepth.h>
1414
#include <Jolt/Geometry/Plane.h>
15+
#include <Jolt/Geometry/ClosestPoint.h>
1516

1617
JPH_NAMESPACE_BEGIN
1718

@@ -83,7 +84,22 @@ void CollideConvexVsTriangles::Collide(Vec3Arg inV0, Vec3Arg inV1, Vec3Arg inV2,
8384
// Check result of collision detection
8485
if (status == EPAPenetrationDepth::EStatus::NotColliding)
8586
return;
86-
else if (status == EPAPenetrationDepth::EStatus::Indeterminate)
87+
88+
// When a mesh triangle has edges much longer than the query shape, GJK can compute
89+
// wrong barycentric weights on the simplex (float32 cancellation in cross products of
90+
// large vectors), returning Colliding with point1 far outside the shape's bounding box.
91+
// Use a generous 2x tolerance so floating-point rounding at the boundary of large
92+
// separation-distance shapes does not trigger a false positive.
93+
// Treat it as Indeterminate so EPA re-derives the contact from scratch.
94+
if (status == EPAPenetrationDepth::EStatus::Colliding)
95+
{
96+
Vec3 bounds_size = mBoundsOf1.mMax - mBoundsOf1.mMin;
97+
AABox expanded_bounds(mBoundsOf1.mMin - bounds_size, mBoundsOf1.mMax + bounds_size);
98+
if (!expanded_bounds.Contains(point1))
99+
status = EPAPenetrationDepth::EStatus::Indeterminate;
100+
}
101+
102+
if (status == EPAPenetrationDepth::EStatus::Indeterminate)
87103
{
88104
// Need to run expensive EPA algorithm
89105

@@ -103,6 +119,18 @@ void CollideConvexVsTriangles::Collide(Vec3Arg inV0, Vec3Arg inV1, Vec3Arg inV2,
103119
// Perform EPA step
104120
if (!pen_depth.GetPenetrationDepthStepEPA(shape1_add_max_separation_distance, triangle, mCollideShapeSettings.mPenetrationTolerance, penetration_axis, point1, point2))
105121
return;
122+
123+
// For triangles with edges much longer than the query shape, EPA can converge
124+
// to the wrong facet and report point2 far from point1 (even snapped onto a far
125+
// vertex of the triangle). A valid contact keeps point2 within the shape's size
126+
// of point1; otherwise snap point2 to the closest point on the triangle.
127+
Vec3 extent = mBoundsOf1.mMax - mBoundsOf1.mMin;
128+
if ((point2 - point1).LengthSq() > extent.LengthSq())
129+
{
130+
uint32 set;
131+
point2 = ClosestPoint::GetClosestPointOnTriangle(v0 - point1, v1 - point1, v2 - point1, set) + point1;
132+
penetration_axis = point2 - point1;
133+
}
106134
}
107135

108136
// Check if the penetration is bigger than the early out fraction

UnitTests/Physics/ConvexVsTrianglesTest.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "UnitTestFramework.h"
66
#include "PhysicsTestContext.h"
77
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
8+
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
89
#include <Jolt/Physics/Collision/Shape/TriangleShape.h>
910
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
1011
#include <Jolt/Physics/Collision/CollideShape.h>
@@ -358,4 +359,58 @@ TEST_SUITE("ConvexVsTrianglesTest")
358359
{
359360
sTestConvexVsTriangles<CollideSphereVsTriangles>();
360361
}
362+
363+
// Regression test for EPA producing a degenerate contact when the mesh triangle has
364+
// a very long edge relative to the query shape. In production a ~200 m needle triangle
365+
// against a 0.25 m capsule caused EPA to converge to the wrong Minkowski polytope facet
366+
// (float32 precision loss in cross products of 200 m vectors) and report point2 ~100 m
367+
// from point1, failing a downstream sanity assert.
368+
TEST_CASE("TestCapsuleVsNeedleTriangle")
369+
{
370+
// CapsuleShape from the crash dump: halfHeight=0.5, radius=0.25.
371+
Ref<CapsuleShape> capsule = new CapsuleShape(0.5f, 0.25f);
372+
373+
Mat44 transform1 = Mat44::sIdentity();
374+
Mat44 transform2 = Mat44::sIdentity();
375+
376+
Vec3 inV0(0.252283931f, -172.936920f, -0.093847394f);
377+
Vec3 inV1(0.242991686f, 27.608593f, -0.127187848f);
378+
Vec3 inV2(0.228017807f, 27.608591f, -0.140446782f);
379+
380+
CollideShapeSettings settings;
381+
settings.mBackFaceMode = EBackFaceMode::IgnoreBackFaces;
382+
settings.mActiveEdgeMode = EActiveEdgeMode::CollideOnlyWithActive;
383+
settings.mCollisionTolerance = 1.0e-4f;
384+
settings.mPenetrationTolerance = 1.0e-4f;
385+
settings.mMaxSeparationDistance = 0.05f;
386+
387+
AllHitCollisionCollector<CollideShapeCollector> collector;
388+
CollideConvexVsTriangles collider(capsule, Vec3::sOne(), Vec3::sOne(),
389+
transform1, transform2,
390+
SubShapeID(), settings, collector);
391+
collider.Collide(inV0, inV1, inV2, 0b011, SubShapeID());
392+
393+
CHECK(collector.mHits.size() == 1);
394+
if (collector.mHits.empty()) return;
395+
const CollideShapeResult &hit = collector.mHits[0];
396+
397+
// Without the fix, EPA snaps point2 to a far triangle vertex ~100 m from point1,
398+
// giving mPenetrationDepth ~ -100 m. With the fix the contact must lie within
399+
// the capsule's own size (radius 0.25 + halfHeight 0.5 = 0.75 m; 2x as margin).
400+
float capsule_max_extent = 2.0f * (0.5f + 0.25f); // 1.5 m
401+
CHECK(hit.mPenetrationDepth > -capsule_max_extent);
402+
CHECK(hit.mPenetrationDepth < capsule_max_extent);
403+
404+
// point2 must be close to point1, not 100 m away.
405+
float dist = (hit.mContactPointOn2 - hit.mContactPointOn1).Length();
406+
CHECK(dist < capsule_max_extent);
407+
408+
// The penetration axis must point from the capsule towards the triangle, i.e.
409+
// against the triangle's (v1-v0)x(v2-v0) normal (the capsule center sits on the
410+
// positive-normal side of the triangle plane). Guards the contact normal that
411+
// feeds collision response from coming out backwards.
412+
Vec3 triangle_normal = (inV1 - inV0).Cross(inV2 - inV0);
413+
CHECK(hit.mPenetrationAxis.Dot(triangle_normal) < 0.0f);
414+
}
415+
361416
}

0 commit comments

Comments
 (0)