Skip to content

Commit d426697

Browse files
Add real world convex-convex regression test (#658)
The focus of this fix is how EPA computes the outward facing normal for a polytope face: faceNormalPointingOutward(). 1. This function's ability to *meaningfully* pick an outward normal depends on how easy it is to classify the side of a face on which polytope vertices lie. This classification works best when we have some real *volume* in the polytope. - When creating a polytope from a *triangle*, rather than picking the fourth vertex based on its distance from the *triangle*, we pick based on the distance to the triangle plane. - By picking distance to triangle, we may simply pick a vertex that is co-planar with the triangle, but far removed (which renders outward normal computation random). - update convert2SimplexToTetrahedron() to reflect this. 2. faceNormalPointingOutward() was unconditionally normalizing the face normal but return the unnormalized vector. - Most call sites then had to subsequently normalize it. - We'll simply return the unit normal and not do redundant work. - This is incidental to the bug, but obvious and worthwhile. 3. faceNormalPointingOutward() has a test where the tet is sufficiently flat where we compare max < |min|. - This is only meaningful if we can guarantee that max >= 0. - Rather than relying on finite precision, we change our initializaiton values. 4. Removal of a frivolous test where the candidate points for creating the tet were both on the triangle. I've removed the frivolous test and added documentation for why we don't treat this case as special.
1 parent 355919e commit d426697

2 files changed

Lines changed: 209 additions & 69 deletions

File tree

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

Lines changed: 86 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ static int convert2SimplexToTetrahedron(const void* obj1, const void* obj2,
817817
assert(isPolytopeEmpty(*polytope));
818818
assert(simplex->last == 2); // a 2-simplex.
819819
const ccd_support_t *a, *b, *c;
820-
ccd_support_t d, d2;
820+
ccd_support_t candidate_1, candidate_2;
821821
ccd_vec3_t ab, ac, dir;
822822
ccd_pt_vertex_t* v[4];
823823
ccd_pt_edge_t* e[6];
@@ -831,37 +831,32 @@ static int convert2SimplexToTetrahedron(const void* obj1, const void* obj2,
831831
// The 2-simplex is just a triangle containing the origin. We will expand this
832832
// triangle to a tetrahedron, by adding the support point along the normal
833833
// direction of the triangle.
834+
//
835+
// There are two possible support points to consider; one on each side of the
836+
// triangle. "Expanding" the polytope depends on its ability to define
837+
// outside. In *some* cases (e.g., when the origin lies on a polytope face),
838+
// that requires clearly recognizing which side of a polytope face the
839+
// polytope vertices lie. That works best when the tetrahedron has a larger
840+
// volume (i.e., the fourth vertex is as far away from the triangle's plane
841+
// as possible). So, that's the candidate we'll select.
842+
//
843+
// We don't need to know *true* distance. We just need to know which candidate
844+
// is farther. So, we can forego normalizing the cross product, knowing the
845+
// true distances of the candidate points are scaled by the same factor.
834846
ccdVec3Sub2(&ab, &b->v, &a->v);
835847
ccdVec3Sub2(&ac, &c->v, &a->v);
836848
ccdVec3Cross(&dir, &ab, &ac);
837-
__ccdSupport(obj1, obj2, &dir, ccd, &d);
838-
const ccd_real_t dist_squared =
839-
ccdVec3PointTriDist2NoWitness(&d.v, &a->v, &b->v, &c->v);
840849

841-
// and second one take in opposite direction
842-
ccdVec3Scale(&dir, -CCD_ONE);
843-
__ccdSupport(obj1, obj2, &dir, ccd, &d2);
844-
const ccd_real_t dist_squared_opposite =
845-
ccdVec3PointTriDist2NoWitness(&d2.v, &a->v, &b->v, &c->v);
850+
// The scaled distance of the first candidate.
851+
__ccdSupport(obj1, obj2, &dir, ccd, &candidate_1);
852+
const ccd_real_t scale_distance_1 = ccdVec3Dot(&dir, &candidate_1.v);
846853

847-
// check if face isn't already on edge of minkowski sum and thus we
848-
// have touching contact
849-
if (ccdIsZero(dist_squared) || ccdIsZero(dist_squared_opposite)) {
850-
v[0] = ccdPtAddVertex(polytope, a);
851-
v[1] = ccdPtAddVertex(polytope, b);
852-
v[2] = ccdPtAddVertex(polytope, c);
853-
e[0] = ccdPtAddEdge(polytope, v[0], v[1]);
854-
e[1] = ccdPtAddEdge(polytope, v[1], v[2]);
855-
e[2] = ccdPtAddEdge(polytope, v[2], v[0]);
856-
*nearest = (ccd_pt_el_t*)ccdPtAddFace(polytope, e[0], e[1], e[2]);
857-
if (*nearest == NULL) return -2;
854+
// The scaled distance of the second candidate,
855+
ccdVec3Scale(&dir, -CCD_ONE);
856+
__ccdSupport(obj1, obj2, &dir, ccd, &candidate_2);
857+
const ccd_real_t scale_distance_2 = ccdVec3Dot(&dir, &candidate_2.v);
858858

859-
return -1;
860-
}
861-
// Form a tetrahedron with abc as one face, pick either d or d2, based
862-
// on which one has larger distance to the face abc. We pick the larger
863-
// distance because it gives a tetrahedron with larger volume, so potentially
864-
// more "expanded" than the one with the smaller volume.
859+
// Form a tetrahedron with abc as one face and a fourth point `v`.
865860
auto FormTetrahedron = [polytope, a, b, c, &v,
866861
&e](const ccd_support_t& new_support) -> int {
867862
v[0] = ccdPtAddVertex(polytope, a);
@@ -892,10 +887,13 @@ static int convert2SimplexToTetrahedron(const void* obj1, const void* obj2,
892887
return 0;
893888
};
894889

895-
if (std::abs(dist_squared) > std::abs(dist_squared_opposite)) {
896-
return FormTetrahedron(d);
890+
// Note: it is highly unlikely that both of these distances are functionally
891+
// zero. That would require a Minkowski sum that is flat w.r.t. a particular
892+
// direction. That would require that both shapes be flat in that direction.
893+
if (std::abs(scale_distance_1) > std::abs(scale_distance_2)) {
894+
return FormTetrahedron(candidate_1);
897895
} else {
898-
return FormTetrahedron(d2);
896+
return FormTetrahedron(candidate_2);
899897
}
900898
}
901899

@@ -1047,13 +1045,12 @@ static bool triangle_area_is_zero(const ccd_vec3_t& a, const ccd_vec3_t& b,
10471045
/**
10481046
* Computes the normal vector of a triangular face on a polytope, and the normal
10491047
* vector points outward from the polytope. Notice we assume that the origin
1050-
* lives within the polytope, and the normal vector may not have unit length.
1048+
* lives within the polytope.
10511049
* @param[in] polytope The polytope on which the face lives. We assume that the
10521050
* origin also lives inside the polytope.
10531051
* @param[in] face The face for which the normal vector is computed.
1054-
* @retval dir The vector normal to the face, and points outward from the
1055-
* polytope. `dir` is unnormalized, that it does not necessarily have a unit
1056-
* length.
1052+
* @retval dir The unit vector normal to the face, and points outward from the
1053+
* polytope.
10571054
* @throws UnexpectedConfigurationException if built in debug mode _and_ the
10581055
* triangle has zero area (either by being too small, or being co-linear).
10591056
*/
@@ -1080,13 +1077,12 @@ static ccd_vec3_t faceNormalPointingOutward(const ccd_pt_t* polytope,
10801077
&(face->edge[0]->vertex[0]->v.v));
10811078
ccdVec3Sub2(&e2, &(face->edge[1]->vertex[1]->v.v),
10821079
&(face->edge[1]->vertex[0]->v.v));
1083-
ccd_vec3_t dir;
10841080
// TODO(hongkai.dai): we ignore the degeneracy here, namely we assume e1 and
10851081
// e2 are not colinear. We should check if e1 and e2 are colinear, and handle
10861082
// this corner case.
1087-
ccdVec3Cross(&dir, &e1, &e2);
1088-
const ccd_real_t dir_norm = std::sqrt(ccdVec3Len2(&dir));
1089-
ccd_vec3_t unit_dir = dir;
1083+
ccd_vec3_t unit_dir;
1084+
ccdVec3Cross(&unit_dir, &e1, &e2);
1085+
const ccd_real_t dir_norm = std::sqrt(ccdVec3Len2(&unit_dir));
10901086
ccdVec3Scale(&unit_dir, 1.0 / dir_norm);
10911087
// The winding of the triangle is *not* guaranteed. The normal `n = e₁ × e₂`
10921088
// may point inside or outside. We rely on the fact that the origin lies
@@ -1107,59 +1103,80 @@ static ccd_vec3_t faceNormalPointingOutward(const ccd_pt_t* polytope,
11071103
// seems large, the fall through case of comparing the maximum distance will
11081104
// always guarantee correctness.
11091105
const ccd_real_t dist_tol = 0.01;
1110-
// origin_distance_to_plane computes the signed distance from the origin to
1111-
// the plane nᵀ * (x - v) = 0 coinciding with the triangle, where v is a
1106+
1107+
// Note: all distances are *signed* distances.
1108+
1109+
// Origin_distance_to_plane computes the signed distance from the origin to
1110+
// the plane nᵀ * (x - v) = 0 coinciding with the triangle, where v is a
11121111
// point on the triangle.
11131112
const ccd_real_t origin_distance_to_plane =
11141113
ccdVec3Dot(&unit_dir, &(face->edge[0]->vertex[0]->v.v));
11151114
if (origin_distance_to_plane < -dist_tol) {
1116-
// Origin is more than `dist_tol` away from the plane, but the negative
1117-
// value implies that the normal vector is pointing in the wrong direction;
1118-
// flip it.
1119-
ccdVec3Scale(&dir, ccd_real_t(-1));
1115+
// Origin is more than `dist_tol` away from the plane, so it serves as a
1116+
// reliable witness. The negative value implies that the normal vector is
1117+
// pointing in the wrong direction; flip it.
1118+
ccdVec3Scale(&unit_dir, -CCD_ONE);
11201119
} else if (-dist_tol <= origin_distance_to_plane &&
11211120
origin_distance_to_plane <= dist_tol) {
11221121
// The origin is close to the plane of the face. Pick another vertex to test
11231122
// the normal direction.
1124-
ccd_real_t max_distance_to_plane = -CCD_REAL_MAX;
1125-
ccd_real_t min_distance_to_plane = CCD_REAL_MAX;
1123+
1124+
// For the final test below, we need max_distance_to_plane to be
1125+
// non-negative. Mathematically, the measured max_distance_to_plane is
1126+
// strictly non-negative; we evaluate the distance of *all* polytope
1127+
// vertices and know the distance to the face vertices is, by construction,
1128+
// zero.
1129+
//
1130+
// Similarly, if everything were perfectly co-planar the *largest* minimum
1131+
// value we would get would be the face vertices' zero distance.
1132+
//
1133+
// Rather than relying on numerical accuracy to yield an exact zero
1134+
// (generally a bad bet), we can initialize the max/min values to zero,
1135+
// representing the limits on those values imposed by the face vertices.
1136+
ccd_real_t max_distance_to_plane = 0;
1137+
ccd_real_t min_distance_to_plane = 0;
11261138
ccd_pt_vertex_t* v;
11271139
// If the magnitude of the distance_to_plane is larger than dist_tol,
11281140
// then it means one of the vertices is at least `dist_tol` away from the
11291141
// plane coinciding with the face.
11301142
ccdListForEachEntry(&polytope->vertices, v, ccd_pt_vertex_t, list) {
1131-
// distance_to_plane is the signed distance from the
1132-
// vertex v->v.v to the face, i.e., distance_to_plane = nᵀ *
1133-
// (v->v.v - face_point). Note that origin_distance_to_plane = nᵀ *
1134-
// face_point.
1143+
// TODO(SeanCurtis-TRI): We might consider testing the vertex v against
1144+
// the known face vertices and skip it if v is one of those three.
1145+
1146+
// distance_to_plane is the signed distance from the vertex v->v.v to the
1147+
// face, i.e., distance_to_plane = nᵀ * (v->v.v - face_point). Note that
1148+
// origin_distance_to_plane = nᵀ * face_point.
11351149
const ccd_real_t distance_to_plane =
11361150
ccdVec3Dot(&unit_dir, &(v->v.v)) - origin_distance_to_plane;
11371151
if (distance_to_plane > dist_tol) {
11381152
// The vertex is at least dist_tol away from the face plane, on the same
1139-
// direction of `dir`. So we flip dir to point it outward from the
1140-
// polytope.
1141-
ccdVec3Scale(&dir, ccd_real_t(-1));
1142-
return dir;
1153+
// direction of `unit_dir`. So we flip unit_dir to point it outward from
1154+
// the polytope.
1155+
ccdVec3Scale(&unit_dir, -CCD_ONE);
1156+
return unit_dir;
11431157
} else if (distance_to_plane < -dist_tol) {
11441158
// The vertex is at least `dist_tol` away from the face plane, on the
1145-
// opposite direction of `dir`. So `dir` points outward already.
1146-
return dir;
1159+
// opposite direction of `unit_dir`. So `unit_dir` points outward
1160+
// already.
1161+
return unit_dir;
11471162
} else {
11481163
max_distance_to_plane =
11491164
std::max(max_distance_to_plane, distance_to_plane);
11501165
min_distance_to_plane =
11511166
std::min(min_distance_to_plane, distance_to_plane);
11521167
}
11531168
}
1169+
11541170
// If max_distance_to_plane > |min_distance_to_plane|, it means that the
1155-
// vertices that are on the positive side of the plane, has a larger maximal
1156-
// distance than the vertices on the negative side of the plane. Thus we
1157-
// regard that `dir` points into the polytope. Hence we flip `dir`.
1171+
// vertices that are on the positive side of the plane, have a larger
1172+
// maximal distance than the vertices on the negative side of the plane.
1173+
// Thus we regard that `unit_dir` points into the polytope. Hence we flip
1174+
// `unit_dir`.
11581175
if (max_distance_to_plane > std::abs(min_distance_to_plane)) {
1159-
ccdVec3Scale(&dir, ccd_real_t(-1));
1176+
ccdVec3Scale(&unit_dir, -CCD_ONE);
11601177
}
11611178
}
1162-
return dir;
1179+
return unit_dir;
11631180
}
11641181

11651182
// Return true if the point `pt` is on the outward side of the half-plane, on
@@ -1169,11 +1186,11 @@ static ccd_vec3_t faceNormalPointingOutward(const ccd_pt_t* polytope,
11691186
// @param pt A point.
11701187
static bool isOutsidePolytopeFace(const ccd_pt_t* polytope,
11711188
const ccd_pt_face_t* f, const ccd_vec3_t* pt) {
1172-
ccd_vec3_t n = faceNormalPointingOutward(polytope, f);
1189+
ccd_vec3_t n_hat = faceNormalPointingOutward(polytope, f);
11731190
// r_VP is the vector from a vertex V on the face `f`, to the point P `pt`.
11741191
ccd_vec3_t r_VP;
11751192
ccdVec3Sub2(&r_VP, pt, &(f->edge[0]->vertex[0]->v.v));
1176-
return ccdVec3Dot(&n, &r_VP) > 0;
1193+
return ccdVec3Dot(&n_hat, &r_VP) > 0;
11771194
}
11781195

11791196
#ifndef NDEBUG
@@ -1572,7 +1589,7 @@ static ccd_vec3_t supportEPADirection(const ccd_pt_t* polytope,
15721589
point on a face, and the support direction is the normal of that face,
15731590
pointing outward from the polytope.
15741591
*/
1575-
ccd_vec3_t dir;
1592+
ccd_vec3_t unit_dir;
15761593
if (ccdIsZero(nearest_feature->dist)) {
15771594
// nearest point is the origin.
15781595
switch (nearest_feature->type) {
@@ -1590,23 +1607,23 @@ static ccd_vec3_t supportEPADirection(const ccd_pt_t* polytope,
15901607
// arbitrarily choose faces[0] normal.
15911608
const ccd_pt_edge_t* edge =
15921609
reinterpret_cast<const ccd_pt_edge_t*>(nearest_feature);
1593-
dir = faceNormalPointingOutward(polytope, edge->faces[0]);
1610+
unit_dir = faceNormalPointingOutward(polytope, edge->faces[0]);
15941611
break;
15951612
}
15961613
case CCD_PT_FACE: {
15971614
// If origin is an interior point of a face, then choose the normal of
15981615
// that face as the sample direction.
15991616
const ccd_pt_face_t* face =
16001617
reinterpret_cast<const ccd_pt_face_t*>(nearest_feature);
1601-
dir = faceNormalPointingOutward(polytope, face);
1618+
unit_dir = faceNormalPointingOutward(polytope, face);
16021619
break;
16031620
}
16041621
}
16051622
} else {
1606-
ccdVec3Copy(&dir, &(nearest_feature->witness));
1623+
ccdVec3Copy(&unit_dir, &(nearest_feature->witness));
1624+
ccdVec3Normalize(&unit_dir);
16071625
}
1608-
ccdVec3Normalize(&dir);
1609-
return dir;
1626+
return unit_dir;
16101627
}
16111628

16121629
/** Finds next support point (and stores it in out argument).
@@ -1773,9 +1790,9 @@ static void validateNearestFeatureOfPolytopeBeingEdge(ccd_pt_t* polytope) {
17731790
kEps * std::max(static_cast<ccd_real_t>(1.0), v0_dist);
17741791

17751792
for (int i = 0; i < 2; ++i) {
1793+
// faceNormalPointingOutward() already normalizes the normal.
17761794
face_normals[i] =
17771795
faceNormalPointingOutward(polytope, nearest_edge->faces[i]);
1778-
ccdVec3Normalize(&face_normals[i]);
17791796
// If the origin o is on the "inner" side of the face, then
17801797
// n̂ ⋅ (o - vₑ) ≤ 0 or, with simplification, -n̂ ⋅ vₑ ≤ 0 (since n̂ ⋅ o = 0).
17811798
origin_to_face_distance[i] =

0 commit comments

Comments
 (0)