Skip to content

Commit efffd3a

Browse files
thowellcopybara-github
authored andcommitted
Avoid floating-point cancellation in capsule-capsule collision.
In `mjraw_CapsuleCapsule`, computing the segment-segment determinant via `ma*mc - mb*mb` suffers from floating-point cancellation for parallel capsule axes in both single and double precision (see google-deepmind/mujoco_warp#1758). In addition, an absolute `mjMINVAL` threshold depends on capsule length and is exceeded by single-precision rounding across independently rotated bodies, solving `x1` and `x2` independently in the non-parallel branch introduces uncorrelated numerator cancellation errors for near-parallel axes, and computing `mc*u - mb*v` suffers from `1 - cos^2(theta)` cancellation under axial offset. - Compute `det` via the squared norm of the cross product of `axis1` and `axis2` (`mju_dot3(cross, cross)`). - Use a scale-invariant relative parallel threshold `det > sin2*ma*mc` (`sin2 = 1e-8`). - Use the Binet-Cauchy identity `(axis1 x axis2)' * (axis2 x dif)` for the `x1` numerator and Ericson's sequential segment-segment solve (`x2` derived from clipped `x1`), replacing the independent solve and clipping cascade. PiperOrigin-RevId: 996394618 Change-Id: I3a6e1d477adcebc2054dbcf724ab5d1a387d83ac
1 parent bd83ea0 commit efffd3a

3 files changed

Lines changed: 78 additions & 20 deletions

File tree

‎doc/changelog.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Engine
1818
degrees of freedom. Other flexes keep all their contacts. Since version 3.6.0 the contacts of every flex were reduced,
1919
and in the midphase for all geoms of a body together, so a cloth dropped on a plane or on many static geoms in the
2020
world body was held by at most 50 contacts and fell through.
21+
- Fixed floating-point cancellation in capsule-capsule collision detection for parallel and near-parallel axes. Thanks
22+
to :github:user:`ayonga` for reporting the issue in :doc:`MJWarp <mjwarp/index>` issue
23+
`1758 <https://github.com/google-deepmind/mujoco_warp/issues/1758>`__.
2124

2225
General
2326
^^^^^^^

‎src/engine/engine_collision_primitive.c‎

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -430,33 +430,30 @@ int mjraw_CapsuleCapsule(mjPreContact* con, mjtNum margin,
430430
mjtNum axis2[3] = {mat2[2] * size2[1], mat2[5] * size2[1], mat2[8] * size2[1]};
431431
mjtNum dif[3] = {pos1[0] - pos2[0], pos1[1] - pos2[1], pos1[2] - pos2[2]};
432432

433-
// compute matrix coefficients and determinant
433+
// compute matrix coefficients
434434
mjtNum ma = mju_dot3(axis1, axis1);
435435
mjtNum mb = -mju_dot3(axis1, axis2);
436436
mjtNum mc = mju_dot3(axis2, axis2);
437437
mjtNum u = -mju_dot3(axis1, dif);
438438
mjtNum v = mju_dot3(axis2, dif);
439-
mjtNum det = ma*mc - mb*mb;
439+
440+
// determinant via cross product to avoid cancellation for parallel axes
441+
mjtNum cross[3];
442+
mji_cross(cross, axis1, axis2);
443+
mjtNum det = mju_dot3(cross, cross);
440444

441445
// general configuration (non-parallel axes)
442-
if (mju_abs(det) >= mjMINVAL) {
443-
// find projections, clip to segments
444-
mjtNum x1 = (mc*u - mb*v) / det;
445-
mjtNum x2 = (ma*v - mb*u) / det;
446-
447-
if (x1 > 1) {
448-
x1 = 1;
449-
x2 = (v - mb) / mc;
450-
} else if (x1 < -1) {
451-
x1 = -1;
452-
x2 = (v + mb) / mc;
453-
}
454-
if (x2 > 1) {
455-
x2 = 1;
456-
x1 = mju_clip((u - mb) / ma, -1, 1);
457-
} else if (x2 < -1) {
458-
x2 = -1;
459-
x1 = mju_clip((u + mb) / ma, -1, 1);
446+
const mjtNum sin2 = 1e-8; // sin^2(0.006 deg)
447+
if (det > sin2*ma*mc) {
448+
// find projections, clip to segments (Ericson, Real-Time Collision Detection, 5.1.8)
449+
// Binet-Cauchy identity: mc*u - mb*v = (axis1 x axis2)' * (axis2 x dif)
450+
mjtNum cross_dif[3];
451+
mji_cross(cross_dif, axis2, dif);
452+
mjtNum x1 = mju_clip(mju_dot3(cross, cross_dif) / det, -1, 1);
453+
mjtNum x2 = (v - mb*x1) / mc;
454+
if (x2 > 1 || x2 < -1) {
455+
x2 = mju_clip(x2, -1, 1);
456+
x1 = mju_clip((u - mb*x2) / ma, -1, 1);
460457
}
461458

462459
// find nearest points, do sphere-sphere test

‎test/engine/engine_collision_driver_test.cc‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,5 +714,63 @@ TEST_F(MjCollisionTest, Flex3DActiveLayersMidphaseDisabled) {
714714
}
715715
}
716716

717+
TEST_F(MjCollisionTest, ParallelCapsuleCapsule) {
718+
constexpr char xml[] = R"(
719+
<mujoco>
720+
<worldbody>
721+
<body euler="30 45 60">
722+
<geom name="a" type="capsule" size="0.01 2.0" pos="0 0.005 0"/>
723+
</body>
724+
<body euler="30 0 0">
725+
<body euler="0 45 60">
726+
<geom name="b" type="capsule" size="0.01 1.5" pos="0 -0.005 0.3"/>
727+
<geom name="c" type="capsule" size="0.01 1.5" pos="0 -0.095 0.3"/>
728+
<geom name="d" type="capsule" size="0.01 1.5" pos="0 -0.095 0.3" euler="0 0.1 0"/>
729+
<geom name="e" type="capsule" size="0.01 1.5" pos="0 -0.015 0.3" euler="0 0.01 0"/>
730+
</body>
731+
</body>
732+
</worldbody>
733+
<contact>
734+
<pair geom1="a" geom2="b"/>
735+
</contact>
736+
</mujoco>
737+
)";
738+
char error[1024];
739+
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
740+
ASSERT_THAT(m.get(), NotNull()) << error;
741+
MjDataPtr d = MakeData(m);
742+
ASSERT_THAT(d, NotNull());
743+
744+
mj_forward(m.get(), d.get());
745+
746+
// Penetrating rotated parallel capsules (a, b) should generate 2 contacts.
747+
ASSERT_EQ(d->ncon, 2);
748+
EXPECT_THAT(d->contact[0].dist, MjNear(-0.01, 1e-12, 1e-5));
749+
EXPECT_THAT(d->contact[1].dist, MjNear(-0.01, 1e-12, 1e-5));
750+
751+
// Separated rotated parallel capsules (a, c) have surface distance 0.08.
752+
int a = mj_name2id(m.get(), mjOBJ_GEOM, "a");
753+
int c = mj_name2id(m.get(), mjOBJ_GEOM, "c");
754+
EXPECT_THAT(mj_geomDistance(m.get(), d.get(), a, c, 0.2, nullptr),
755+
MjNear(0.08, 1e-12, 1e-5));
756+
757+
// Near-parallel (0.1 deg) capsules (a, d): surface distance 0.08, closest
758+
// point on d is at its center cross-section (distance 0.01 from d's xpos).
759+
int d_id = mj_name2id(m.get(), mjOBJ_GEOM, "d");
760+
mjtNum fromto[6];
761+
EXPECT_THAT(mj_geomDistance(m.get(), d.get(), a, d_id, 0.2, fromto),
762+
MjNear(0.08, 1e-12, 1e-5));
763+
EXPECT_THAT(mju_dist3(fromto + 3, d->geom_xpos + 3 * d_id),
764+
MjNear(0.01, 1e-12, 1e-2));
765+
766+
// Near-parallel (0.01 deg) capsules (a, e): surface distance 0.0, closest
767+
// point on e is at its center cross-section (distance 0.01 from e's xpos).
768+
int e_id = mj_name2id(m.get(), mjOBJ_GEOM, "e");
769+
EXPECT_THAT(mj_geomDistance(m.get(), d.get(), a, e_id, 0.2, fromto),
770+
MjNear(0.0, 1e-12, 1e-5));
771+
EXPECT_THAT(mju_dist3(fromto + 3, d->geom_xpos + 3 * e_id),
772+
MjNear(0.01, 1e-12, 1e-1));
773+
}
774+
717775
} // namespace
718776
} // namespace mujoco

0 commit comments

Comments
 (0)
⌂ 换网址