Skip to content

Commit 41f4e00

Browse files
Address EPA failure from issue 493
- Introduce two regression tests (the two posted in the issue). - Correct a few confusing whitepsace formatting issues. - Modify the validateNearestFeatureOfPolytopeBeingEdge() function in two ways - Distance from origin to face must now be greater than epsilon (instead of zero). Justificaiton for this change provided. - Test to determine if the edge *truly* is the nearest feature has been simplified and documentation updated. This led to the removal of the is_point_on_line_segment() function.
1 parent b8ef685 commit 41f4e00

File tree

3 files changed

+116
-49
lines changed

3 files changed

+116
-49
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
[#472](https://github.com/flexible-collision-library/fcl/pull/472)
3434
* Documentation for OcTree no longer mistakenly excluded from doxygen:
3535
[#472](https://github.com/flexible-collision-library/fcl/pull/472)
36+
* Another failure mode in the GJK/EPA signed distance query patched:
37+
[#494](https://github.com/flexible-collision-library/fcl/pull/494)
3638

3739
* Build/Test/Misc
3840

include/fcl/narrowphase/detail/convexity_based_algorithm/gjk_libccd-inl.h

+41-46
Original file line numberDiff line numberDiff line change
@@ -966,28 +966,6 @@ static bool triangle_area_is_zero(const ccd_vec3_t& a, const ccd_vec3_t& b,
966966
return false;
967967
}
968968

969-
/**
970-
* Determines if the point P is on the line segment AB.
971-
* If A, B, P are coincident, report true.
972-
*/
973-
static bool is_point_on_line_segment(const ccd_vec3_t& p, const ccd_vec3_t& a,
974-
const ccd_vec3_t& b) {
975-
if (are_coincident(a, b)) {
976-
return are_coincident(a, p);
977-
}
978-
// A and B are not coincident, if the triangle ABP has non-zero area, then P
979-
// is not on the line adjoining AB, and hence not on the line segment AB.
980-
if (!triangle_area_is_zero(a, b, p)) {
981-
return false;
982-
}
983-
// P is on the line adjoinging AB. If P is on the line segment AB, then
984-
// PA.dot(PB) <= 0.
985-
ccd_vec3_t PA, PB;
986-
ccdVec3Sub2(&PA, &p, &a);
987-
ccdVec3Sub2(&PB, &p, &b);
988-
return ccdVec3Dot(&PA, &PB) <= 0;
989-
}
990-
991969
/**
992970
* Computes the normal vector of a triangular face on a polytope, and the normal
993971
* vector points outward from the polytope. Notice we assume that the origin
@@ -1691,6 +1669,14 @@ static int __ccdGJK(const void *obj1, const void *obj2,
16911669
*/
16921670
static void validateNearestFeatureOfPolytopeBeingEdge(ccd_pt_t* polytope) {
16931671
assert(polytope->nearest_type == CCD_PT_EDGE);
1672+
1673+
// We define epsilon to include an additional bit of noise. The goal is to
1674+
// pick the smallest epsilon possible. This factor of two proved necessary
1675+
// due to unit test behavior on the mac. In the future, as we collect
1676+
// more evidence, it may be necessary to increase to more bits. But the need
1677+
// should always be demonstrable and not purely theoretical.
1678+
constexpr ccd_real_t kEps = 2 * constants<ccd_real_t>::eps();
1679+
16941680
// Only verify the feature if the nearest feature is an edge.
16951681

16961682
const ccd_pt_edge_t* const nearest_edge =
@@ -1701,6 +1687,14 @@ static void validateNearestFeatureOfPolytopeBeingEdge(ccd_pt_t* polytope) {
17011687
// normalized.
17021688
std::array<ccd_vec3_t, 2> face_normals;
17031689
std::array<double, 2> origin_to_face_distance;
1690+
1691+
// We define the plane equation using vertex[0]. If vertex[0] is far away
1692+
// from the origin, it can magnify rounding error. We scale epsilon to account
1693+
// for this possibility.
1694+
const ccd_real_t v0_dist =
1695+
std::sqrt(ccdVec3Len2(&nearest_edge->vertex[0]->v.v));
1696+
const ccd_real_t plane_threshold = kEps * std::max(1.0, v0_dist);
1697+
17041698
for (int i = 0; i < 2; ++i) {
17051699
face_normals[i] =
17061700
faceNormalPointingOutward(polytope, nearest_edge->faces[i]);
@@ -1709,27 +1703,29 @@ static void validateNearestFeatureOfPolytopeBeingEdge(ccd_pt_t* polytope) {
17091703
// n̂ ⋅ (o - vₑ) ≤ 0 or, with simplification, -n̂ ⋅ vₑ ≤ 0 (since n̂ ⋅ o = 0).
17101704
origin_to_face_distance[i] =
17111705
-ccdVec3Dot(&face_normals[i], &nearest_edge->vertex[0]->v.v);
1712-
if (origin_to_face_distance[i] > 0) {
1706+
// If the origin lies *on* the edge, then it also lies on the two adjacent
1707+
// faces. Rather than failing on strictly *positive* signed distance, we
1708+
// introduce an epsilon threshold. This usage of epsilon is to account for a
1709+
// discrepancy in the signed distance computation. How GJK (and partially
1710+
// EPA) compute the signed distance from origin to face may *not* be exactly
1711+
// the same as done in this test (i.e. for a given set of vertices, the
1712+
// plane equation can be defined various ways. Those ways are
1713+
// *mathematically* equivalent but numerically will differ due to rounding).
1714+
// We account for those differences by allowing a very small positive signed
1715+
// distance to be considered zero. We assume that the GJK/EPA code
1716+
// ultimately classifies inside/outside around *zero* and *not* epsilon.
1717+
if (origin_to_face_distance[i] > plane_threshold) {
17131718
FCL_THROW_FAILED_AT_THIS_CONFIGURATION(
17141719
"The origin is outside of the polytope. This should already have "
17151720
"been identified as separating.");
17161721
}
17171722
}
1718-
// We compute the projection of the origin onto the plane as
1719-
// -face_normals[i] * origin_to_face_distance[i]
1720-
// If the projection to both faces are on the edge, the the edge is the
1721-
// closest feature.
1722-
bool is_edge_closest_feature = true;
1723-
for (int i = 0; i < 2; ++i) {
1724-
ccd_vec3_t origin_projection_to_plane = face_normals[i];
1725-
ccdVec3Scale(&(origin_projection_to_plane), -origin_to_face_distance[i]);
1726-
if (!is_point_on_line_segment(origin_projection_to_plane,
1727-
nearest_edge->vertex[0]->v.v,
1728-
nearest_edge->vertex[1]->v.v)) {
1729-
is_edge_closest_feature = false;
1730-
break;
1731-
}
1732-
}
1723+
1724+
// We know the reported squared distance to the edge. If that distance is
1725+
// functionally zero, then the edge must *truly* be the nearest feature.
1726+
// If it isn't, then it must be one of the adjacent faces.
1727+
const bool is_edge_closest_feature = nearest_edge->dist < kEps * kEps;
1728+
17331729
if (!is_edge_closest_feature) {
17341730
// We assume that libccd is not crazily wrong. Although the closest
17351731
// feature is not the edge, it is near that edge. Hence we select the
@@ -1779,7 +1775,7 @@ static int __ccdEPA(const void *obj1, const void *obj2,
17791775
return -2;
17801776
}
17811777

1782-
while (1){
1778+
while (1) {
17831779
// get triangle nearest to origin
17841780
*nearest = ccdPtNearest(polytope);
17851781
if (polytope->nearest_type == CCD_PT_EDGE) {
@@ -1791,14 +1787,13 @@ static int __ccdEPA(const void *obj1, const void *obj2,
17911787
*nearest = ccdPtNearest(polytope);
17921788
}
17931789

1794-
// get next support point
1795-
if (nextSupport(polytope, obj1, obj2, ccd, *nearest, &supp) != 0) {
1796-
break;
1797-
}
1790+
// get next support point
1791+
if (nextSupport(polytope, obj1, obj2, ccd, *nearest, &supp) != 0) {
1792+
break;
1793+
}
17981794

1799-
// expand nearest triangle using new point - supp
1800-
if (expandPolytope(polytope, *nearest, &supp) != 0)
1801-
return -2;
1795+
// expand nearest triangle using new point - supp
1796+
if (expandPolytope(polytope, *nearest, &supp) != 0) return -2;
18021797
}
18031798

18041799
return 0;

test/test_fcl_signed_distance.cpp

+73-3
Original file line numberDiff line numberDiff line change
@@ -325,12 +325,22 @@ void test_distance_box_box_helper(const Vector3<S>& box1_size,
325325
// p_B1P1 is the position of the witness point P1 on box 1, measured
326326
// and expressed in the box 1 frame B1.
327327
const Vector3<S> p_B1P1 = X_WB1.inverse() * result.nearest_points[0];
328-
const double tol = 10 * std::numeric_limits<S>::epsilon();
329-
EXPECT_TRUE((p_B1P1.array().abs() <= (box1_size / 2).array() + tol).all());
328+
constexpr double tol = 10 * constants<S>::eps();
329+
const double tol_1 = tol * std::max(S(1), (box1_size / 2).maxCoeff());
330+
EXPECT_TRUE(
331+
(p_B1P1.array().abs() <= (box1_size / 2).array() + tol_1).all())
332+
<< "\n p_B1P1: " << p_B1P1.transpose()
333+
<< "\n box1_size / 2: " << (box1_size / 2).transpose()
334+
<< "\n tol: " << tol_1;
330335
// p_B2P2 is the position of the witness point P2 on box 2, measured
331336
// and expressed in the box 2 frame B2.
337+
const double tol_2 = tol * std::max(S(1), (box2_size / 2).maxCoeff());
332338
const Vector3<S> p_B2P2 = X_WB2.inverse() * result.nearest_points[1];
333-
EXPECT_TRUE((p_B2P2.array().abs() <= (box2_size / 2).array() + tol).all());
339+
EXPECT_TRUE(
340+
(p_B2P2.array().abs() <= (box2_size / 2).array() + tol_2).all())
341+
<< "\n p_B2P2: " << p_B2P2.transpose()
342+
<< "\n box2_size / 2: " << (box2_size / 2).transpose()
343+
<< "\n tol: " << tol_2;
334344

335345
// An expected distance has been provided; let's test that the value is as
336346
// expected.
@@ -345,6 +355,7 @@ void test_distance_box_box_helper(const Vector3<S>& box1_size,
345355
// reported in https://github.com/flexible-collision-library/fcl/issues/388
346356
template <typename S>
347357
void test_distance_box_box_regression1() {
358+
SCOPED_TRACE("test_distance_box_box_regression1");
348359
const Vector3<S> box1_size(0.03, 0.12, 0.1);
349360
Transform3<S> X_WB1 = Transform3<S>::Identity();
350361
X_WB1.matrix() << -3.0627937852578681533e-08, -0.99999999999999888978,
@@ -370,6 +381,7 @@ void test_distance_box_box_regression1() {
370381
// reported in https://github.com/flexible-collision-library/fcl/issues/395
371382
template <typename S>
372383
void test_distance_box_box_regression2() {
384+
SCOPED_TRACE("test_distance_box_box_regression2");
373385
const Vector3<S> box1_size(0.46, 0.48, 0.01);
374386
Transform3<S> X_WB1 = Transform3<S>::Identity();
375387
X_WB1.matrix() << 1,0,0, -0.72099999999999997424,
@@ -393,6 +405,7 @@ void test_distance_box_box_regression2() {
393405
// reported in https://github.com/flexible-collision-library/fcl/issues/415
394406
template <typename S>
395407
void test_distance_box_box_regression3() {
408+
SCOPED_TRACE("test_distance_box_box_regression3");
396409
const Vector3<S> box1_size(0.49, 0.05, 0.21);
397410
Transform3<S> X_WB1 = Transform3<S>::Identity();
398411
// clang-format off
@@ -416,6 +429,7 @@ void test_distance_box_box_regression3() {
416429
// reported in https://github.com/flexible-collision-library/fcl/issues/398
417430
template <typename S>
418431
void test_distance_box_box_regression4() {
432+
SCOPED_TRACE("test_distance_box_box_regression4");
419433
const Vector3<S> box1_size(0.614, 3, 0.37);
420434
Transform3<S> X_WB1 = Transform3<S>::Identity();
421435
X_WB1.translation() << -0.675, 0, 0.9115;
@@ -429,6 +443,7 @@ void test_distance_box_box_regression4() {
429443
// reported in https://github.com/flexible-collision-library/fcl/issues/428
430444
template <typename S>
431445
void test_distance_box_box_regression5() {
446+
SCOPED_TRACE("test_distance_box_box_regression5");
432447
const Vector3<S> box1_size(0.2, 0.33, 0.1);
433448
Transform3<S> X_WB1 = Transform3<S>::Identity();
434449
X_WB1.translation() << -0.071000000000000035305, -0.77200000000000001954, 0.79999999999999993339;
@@ -440,6 +455,7 @@ void test_distance_box_box_regression5() {
440455

441456
template <typename S>
442457
void test_distance_box_box_regression6() {
458+
SCOPED_TRACE("test_distance_box_box_regression6");
443459
const Vector3<S> box1_size(0.31650000000000000355, 0.22759999999999999676, 0.1768000000000000127);
444460
Transform3<S> X_WB1 = Transform3<S>::Identity();
445461
// clang-format off
@@ -460,11 +476,64 @@ void test_distance_box_box_regression6() {
460476
test_distance_box_box_helper(box1_size, X_WB1, box2_size, X_WB2, &expected_distance);
461477
}
462478

479+
// Issue #493 outlines a number of scenarios that caused signed distance
480+
// failure. They consisted of two identical, stacked boxes. The boxes are
481+
// slightly tilted. The boxes were essentially touching but were separated by
482+
// infinitesimally small distances. The issue outlines three different examples.
483+
// Rather than reproducing each of them verbatim, this test attempts to
484+
// generalize those cases by testing the stacked scenario across various box
485+
// sizes and separation amounts (ranging from slightly penetrating to slightly
486+
// separated). These should essentially cover the variations described in the
487+
// issue.
488+
template <typename S>
489+
void test_distance_box_box_regression_tilted_kissing_contact() {
490+
SCOPED_TRACE("test_distance_box_box_regression_tilted_kissing_contact");
491+
// The boxes are posed relative to each other in a common frame F (such that
492+
// it is easy to reason about their separation). The stack is rotated around
493+
// box A's origin and translated into the world frame.
494+
Matrix3<S> R_WF;
495+
R_WF <<
496+
0.94096063217417758029, 0.29296840037289501035, 0.16959541586174811667,
497+
-0.23569836841299879326, 0.92661523595848427348, -0.29296840037289506586,
498+
-0.2429801799032638987, 0.23569836841299884878, 0.94096063217417758029;
499+
500+
for (const S dim : {S(0.01), S(0.25), S(0.5), S(10), S(1000)}) {
501+
const Vector3<S> box_size(dim, dim, dim);
502+
503+
const Vector3<S> p_WA(0, 0, 5 * dim);
504+
Transform3<S> X_WA;
505+
X_WA.linear() = R_WF;
506+
X_WA.translation() = p_WA;
507+
Transform3<S> X_WB;
508+
X_WB.linear() = R_WF;
509+
510+
// Both boxes always have the same orientation and the *stack* is always
511+
// located at p_WA. Only the translational component of X_WB changes with
512+
// varying separation distance.
513+
514+
// By design, the distances are all near epsilon. We'll scale them up for
515+
// larger boxes to make sure the distance doesn't simply disappear in
516+
// the rounding noise.
517+
for (const S distance : {S(-1e-15), S(-2.5e-16), S(-1e-16), S(0), S(1e-16),
518+
S(2.5e-16), S(1e-15)}) {
519+
const S scaled_distance = distance * std::max(S(1), dim);
520+
const Vector3<S> p_AB_F = Vector3<S>(0, dim + scaled_distance, 0);
521+
522+
X_WB.translation() = p_WA + R_WF * p_AB_F;
523+
SCOPED_TRACE("dim: " + std::to_string(dim) +
524+
", distance: " + std::to_string(distance));
525+
test_distance_box_box_helper(box_size, X_WA, box_size, X_WB,
526+
&scaled_distance);
527+
}
528+
}
529+
}
530+
463531
// This is a *specific* case that has cropped up in the wild that reaches the
464532
// unexpected `validateNearestFeatureOfPolytopeBeingEdge` error. This error was
465533
// reported in https://github.com/flexible-collision-library/fcl/issues/408
466534
template <typename S>
467535
void test_distance_sphere_box_regression1() {
536+
SCOPED_TRACE("test_distance_sphere_box_regression1");
468537
using CollisionGeometryPtr_t = std::shared_ptr<fcl::CollisionGeometry<S>>;
469538
const S sphere_radius = 0.06;
470539
CollisionGeometryPtr_t sphere_geo(new fcl::Sphere<S>(sphere_radius));
@@ -533,6 +602,7 @@ GTEST_TEST(FCL_SIGNED_DISTANCE, RealWorldRegression) {
533602
test_distance_box_box_regression4<double>();
534603
test_distance_box_box_regression5<double>();
535604
test_distance_box_box_regression6<double>();
605+
test_distance_box_box_regression_tilted_kissing_contact<double>();
536606
test_distance_sphere_box_regression1<double>();
537607
}
538608

0 commit comments

Comments
 (0)