Skip to content

Fix EPA contact point for mesh triangles with long edges#2065

Open
AndreiPotapov wants to merge 1 commit into
jrouwe:masterfrom
AndreiPotapov:master
Open

Fix EPA contact point for mesh triangles with long edges#2065
AndreiPotapov wants to merge 1 commit into
jrouwe:masterfrom
AndreiPotapov:master

Conversation

@AndreiPotapov

Copy link
Copy Markdown

When a convex shape collides with a mesh triangle whose edge length is much larger than the penetration depth (e.g. ~84m edge vs ~5cm penetration, a ~1700:1 ratio), EPA converges to the wrong Minkowski polytope facet due to float32 precision loss. GetPenetrationDepthStepEPA returns true but point2 can be tens of meters outside the triangle, producing degenerate contact data.

The fix runs after GetPenetrationDepthStepEPA in CollideConvexVsTriangles::Collide. A valid point2 lies on the triangle and must be inside its axis-aligned bounding box — this is the fast-path check (six float compares, no cost in the normal case). If point2 is outside the bbox, we compute the actual closest point on the triangle to point1 and, if point2 is farther from it than the query shape's AABB diagonal, snap point2 there and update penetration_axis. The two-level structure ensures correctness: any epsilon-outside false positives from the bbox check are caught by the distance threshold and produce no correction.

This is 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.

Actual data that caused assert on bad contact point produced:
Level geometry mesh vertices: (-75.6, 15.0, 320.2), (-112.7, 15.0, 245.4), (-104.2, 15.0, 262.5) - longest edge 83.5m, flat floor tile
Vehicle position: approximately (-90, 15.05, 300) - 5cm above the triangle plane
Actual penetration depth: ~5cm, scale ratio ~1700:1
EPA reported point2 = (-70.21, 15.0, 330.99) - 84m outside the triangle
Resulting contact: point1 = (-88.4, 15.05, 298.7), point2 = (-54.6, 15.0, 332.5), distance 34m, penetration depth -33.85m

@CLAassistant

CLAassistant commented Jun 25, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@AndreiPotapov

Copy link
Copy Markdown
Author

I've checked another one crash dump and removed fast-path check by bbox. It was passed by some invalid contacts.
point1 / wpos (on human): (-142.95, 147.26, 174.96)
point2 / wposB (on triangle): (-142.95, 47.37, 174.99)
depth -99.84m
human AABB: ±(0.3, 0.8, 0.3)
Mesh:
v0 = ( 0.252284, -172.936920, -0.093847 )
v1 = ( 0.242992, 27.608593, -0.127188 )
v2 = ( 0.228018, 27.608591, -0.140447 )
It's a needle form, but also very long.

@jrouwe

jrouwe commented Jun 27, 2026

Copy link
Copy Markdown
Owner

So far I've always managed to avoid extra checks like this by investigating what goes wrong inside the EPA algorithm.

Could you please provide me with all the values of the CollideConvexVsTriangles members and all values of mShape1 when the issue occurs so that I can reproduce it myself?

Can you also let me know which version of Jolt / which platform / compiler / compiler flags you're using to compile Jolt?

@AndreiPotapov

AndreiPotapov commented Jun 27, 2026

Copy link
Copy Markdown
Author

mShape1: CapsuleShape, mRadius = 0.25, mHalfHeightOfCylinder = 0.5
CollideConvexVsTriangles:

mScale1           = (1, 1, 1)
mScale2           = (1, 1, 1)
mScaleSign2       = 1
mSubShapeID1      = 0xFFFFFFFF

mTransform1 (col-major):
  col0 = (-0.954718,  0,        0.297513, 0)
  col1 = ( 0,         1,        0,        0)
  col2 = (-0.297513,  0,       -0.954718, 0)
  col3 = (-142.7733, 19.7557, 174.7865,   1)

mTransform2To1 (col-major):
  col0 = (-0.662932,  9.375e-5,  0.748680, 0)
  col1 = (-4.638e-5,  1,        -1.663e-4, 0)
  col2 = (-0.748680, -1.450e-4, -0.662932, 0)
  col3 = (-1.1200,   35.6943,   -1.3582,   1)

mBoundsOf1.mMin   = (-0.3,  -0.8, -0.3)
mBoundsOf1.mMax   = ( 0.3,   0.8,  0.3)

mBoundsOf1InSpaceOf2.mMin = (-0.1525, -36.4947, -2.1573)
mBoundsOf1InSpaceOf2.mMax = ( 0.6946, -34.8945, -1.3101)

mCollideShapeSettings:
  mActiveEdgeMode          = CollideOnlyWithActive
  mCollectFacesMode        = NoFaces
  mBackFaceMode            = IgnoreBackFaces
  mCollisionTolerance      = 1e-4
  mPenetrationTolerance    = 1e-4
  mMaxSeparationDistance   = 0.05
  mActiveEdgeMovementDirection = (0, 0, 0)
penetration_axis = {{mValue = {0.236910969, 0, -0.1233688, -0.1233688}, mF32 = {0.236910969, 0, -0.1233688,
      -0.1233688}}}
point1 = {{mValue = {-142.950653, 147.255692, 174.962708, 1}, mF32 = {-142.950653, 147.255692, 174.962708, 1}}}
point2 = {{mValue = {-142.949219, 47.3650627, 174.988419, 1}, mF32 = {-142.949219, 47.3650627, 174.988419, 1}}}
pen_depth = {mGJK = {<JPH::NonCopyable> = {<No data fields>}, mY = {{{mValue = {-0.228017807, -28.1085911,
            0.140446782, -1}, mF32 = {-0.228017807, -28.1085911, 0.140446782, -1}}}, {{mValue = {-0.252283931,
            173.43692, 0.093847394, -1}, mF32 = {-0.252283931, 173.43692, 0.093847394, -1}}}, {{mValue = {
            -0.252283931, 172.43692, 0.093847394, -1}, mF32 = {-0.252283931, 172.43692, 0.093847394, -1}}}, {{
          mValue = {-0.228017807, -28.1085911, 0.140446782, -1}, mF32 = {-0.228017807, -28.1085911, 0.140446782,
            -1}}}}, mP = {{{mValue = {0, -0.5, 0, 0}, mF32 = {0, -0.5, 0, 0}}}, {{mValue = {0, 0.5, 0, 0}, mF32 = {0,
            0.5, 0, 0}}}, {{mValue = {0, -0.5, 0, 0}, mF32 = {0, -0.5, 0, 0}}}, {{mValue = {0, -0.5, 0, 0}, mF32 = {0,
            -0.5, 0, 0}}}}, mQ = {{{mValue = {0.228017807, 27.6085911, -0.140446782, 1}, mF32 = {0.228017807,
            27.6085911, -0.140446782, 1}}}, {{mValue = {0.252283931, -172.93692, -0.093847394, 1}, mF32 = {
            0.252283931, -172.93692, -0.093847394, 1}}}, {{mValue = {0.252283931, -172.93692, -0.093847394, 1},
          mF32 = {0.252283931, -172.93692, -0.093847394, 1}}}, {{mValue = {0.228017807, 27.6085911, -0.140446782, 1},
          mF32 = {0.228017807, 27.6085911, -0.140446782, 1}}}}, mNumPoints = 3}, mGJKTolerance = 9.99999975e-05}

Problem mesh in local space of mShape1:

  v0 = ( 0.252284, -172.9369, -0.093847)
  v1 = ( 0.242992,   27.6086, -0.127188)
  v2 = ( 0.228018,   27.6086, -0.140447)

Happened on linux, compiled by clang +-21, with -march=haswell or something near, but FMA was explicitly disabled for Jolt. So you can use -mavx. Nothing wrong with EPA algo. There an classic problem of mixing very small float32 with a big float32 in calculations. First one loosing most of it's preciosion.
We using version v5.5.0-80-g7b9fbc356 updated at ~12 April.

@AndreiPotapov

Copy link
Copy Markdown
Author

Added test, you should be able to reproduce it with CROSS_PLATFORM_DETERMINISTIC enabled. Fast math on, fma off. GJK also can produce bad contact instead of Status::Indeterminate, so I added workaround.

@AndreiPotapov AndreiPotapov force-pushed the master branch 3 times, most recently from c23bfd2 to a1918f9 Compare June 30, 2026 10:34
@AndreiPotapov

Copy link
Copy Markdown
Author

Added also point2 check after GJK

@jrouwe

jrouwe commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Thanks, I will need some time to look at this properly.

@AndreiPotapov

Copy link
Copy Markdown
Author

I got third crash. All of them was caused by a long needle-form triangle, formally Jolt doesn't consider them as degenerate. In all crash dumps GJK returned Indeterminate, but I can't get same result in my tests with the same data. So, there is definitely some dependency on the compiler and its options, but the main problem lies in the limitations of float32. Our compilation options are quite conservative for games, things that usually breaks precision was disabled:
-ffast-math -ffinite-math-only -mno-recip -ffp-contract=off -mno-fma -fno-unsafe-math-optimizations -march=haswell

Triangles data from 3 crashes:

Crash Vertices (shape-1 local space) Edges (m) Area (m^2) Thickness Aspect Normal Cross-product test: LengthSq(n) vs 1e-12 Surface
#1 v0 (10.6453, -19.1050, -0.4494)
v1 (-30.0585, 53.8154, -0.4567)
v2 (-20.7404, 37.1188, -0.4550)
83.5 / 19.1 / 64.4 0.069 1.6 mm 50711:1 (+0.020, +0.011, +0.9997) 0.0189 horizontal floor tile
#2 v0 (0.2523, -172.9369, -0.0938)
v1 (0.2430, 27.6086, -0.1272)
v2 (0.2280, 27.6086, -0.1404)
200.5 / 0.02 / 200.5 2.006 20 mm 10027:1 (-0.663, 0.000, +0.749) 16.09 zeppelin cable
#3 v0 (-0.1016, -172.1183, 0.2623)
v1 (-0.0873, -172.1183, 0.2483)
v2 (0.0022, 28.4272, 0.2536)
0.02 / 200.5 / 200.5 2.006 20 mm 10027:1 (+0.699, -0.000, +0.715) 16.09 zeppelin cable

Our level designers will try to avoid such strange triangles in collisions, but I think the technical problem of how to validate or fix it remains open. It was hard to investigate the root reason why our phys objects sometimes acquire cosmic speeds.

@AndreiPotapov

Copy link
Copy Markdown
Author

Added one more validation, if both points are not fixable after EPA, ignore contact in that case.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants