test_sphere_cylinder.cpp
Go to the documentation of this file.
1 /*
2  * Software License Agreement (BSD License)
3  *
4  * Copyright (c) 2018. Toyota Research Institute
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * * Redistributions of source code must retain the above copyright
12  * notice, this list of conditions and the following disclaimer.
13  * * Redistributions in binary form must reproduce the above
14  * copyright notice, this list of conditions and the following
15  * disclaimer in the documentation and/or other materials provided
16  * with the distribution.
17  * * Neither the name of CNRS-LAAS and AIST nor the names of its
18  * contributors may be used to endorse or promote products derived
19  * from this software without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25  * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32  * POSSIBILITY OF SUCH DAMAGE.
33  */
34 
37 // Tests the custom sphere-cylinder tests: distance and collision.
38 
40 
41 #include <sstream>
42 #include <string>
43 
44 #include <gtest/gtest.h>
45 
46 #include "eigen_matrix_compare.h"
49 
50 namespace fcl {
51 namespace detail {
52 namespace {
53 
54 // In the worst case (with arbitrary frame orientations) it seems like I'm
55 // losing about 4 bits of precision in the solution (compared to performing
56 // the equivalent query without any rotations). This encodes that bit loss to
57 // an epsilon value appropriate to the scalar type.
58 //
59 // TODO(SeanCurtis-TRI): These eps values are *not* optimal. They are the result
60 // of a *number* of issues.
61 // 1. Generally, for float the scalar must be at least 20 * ε. The arbitrary
62 // rotation *really* beats up on the precision.
63 // 2. CI uses Eigen 3.2.0. The threshold must be 22 * ε for the tests to pass.
64 // This is true, even for doubles. Later versions (e.g., 3.2.92, aka
65 // 3.3-beta1) can pass with a tolerance of 16 * ε.
66 // 3. Mac CI requires another bump in the multiplier for floats. So, floats here
67 // are 24.
68 // Upgrade the Eigen version so that this tolerance can be reduced.
69 template <typename S>
70 struct Eps {
71  using Real = typename constants<S>::Real;
72  static Real value() { return 22 * constants<S>::eps(); }
73 };
74 
75 template <>
76 struct Eps<float> {
77  using Real = typename constants<float>::Real;
78  static Real value() { return 24 * constants<float>::eps(); }
79 };
80 
81 // Utility function for evaluating points inside cylinders. Tests various
82 // configurations of points and cylinders.
83 template <typename S> void NearestPointInCylinder() {
84  // Picking sizes that are *not* powers of two and *not* uniform in size.
85  const S r = 0.6;
86  const S h = 1.8;
87  Vector3<S> p_CN;
88  Vector3<S> p_CQ;
89 
90  // Case: query point at origin.
91  p_CQ << 0, 0, 0;
92  bool N_is_not_S = nearestPointInCylinder(h, r, p_CQ, &p_CN);
93  EXPECT_FALSE(N_is_not_S) << "point at origin";
95  << "point at origin";
96 
97  // Per cylinder-half tests (i.e., above and below the z = 0 plane).
98  for (S z_sign : {-1, 1}) {
99  for (const auto& dir : {Vector3<S>(1, 0, 0),
100  Vector3<S>(0, 1, 0),
101  Vector3<S>(1, 1, 0).normalized(),
102  Vector3<S>(-1, 2, 0).normalized(),
103  Vector3<S>(1, -2, 0).normalized(),
104  Vector3<S>(-2, -3, 0).normalized()}) {
105  const Vector3<S> z_offset_internal{0, 0, h * S(0.5) * z_sign};
106  const Vector3<S> z_offset_external{0, 0, h * S(1.5) * z_sign};
107  const Vector3<S> radial_offset_internal = dir * (r * S(0.5));
108  const Vector3<S> radial_offset_external = dir * (r * S(1.5));
109 
110  using std::to_string;
111 
112  std::stringstream ss;
113  ss << "dir: " << dir.transpose() << ", z: " << z_sign;
114  const std::string parameters = ss.str();
115  // Case: point inside (no clamped values).
116  p_CQ = radial_offset_internal + z_offset_internal;
117  N_is_not_S = nearestPointInCylinder(h, r, p_CQ, &p_CN);
118  EXPECT_FALSE(N_is_not_S) << "Sphere at origin - " << parameters;
119  EXPECT_TRUE(
121  << "Sphere at origin - " << parameters;
122 
123  // Case: clamped only by the barrel.
124  p_CQ = radial_offset_external + z_offset_internal;
125  N_is_not_S = nearestPointInCylinder(h, r, p_CQ, &p_CN);
126  EXPECT_TRUE(N_is_not_S)
127  << "Clamped by barrel - " << parameters;
128  const Vector3<S> point_on_barrel = z_offset_internal + dir * r;
129  EXPECT_NEAR(point_on_barrel(0), p_CN(0), Eps<S>::value())
130  << "Clamped by barrel - " << parameters;
131  EXPECT_NEAR(point_on_barrel(1), p_CN(1), Eps<S>::value())
132  << "Clamped by barrel - " << parameters;
133  EXPECT_EQ(p_CQ(2), p_CN(2))
134  << "Clamped by barrel - " << parameters;
135 
136  // Case: clamped only by the end face.
137  p_CQ = radial_offset_internal + z_offset_external;
138  N_is_not_S = nearestPointInCylinder(h, r, p_CQ, &p_CN);
139  EXPECT_TRUE(N_is_not_S) << "Clamped by end face - " << parameters;
140  EXPECT_EQ(p_CQ(0), p_CN(0)) << "Clamped by end face - " << parameters;
141  EXPECT_EQ(p_CQ(1), p_CN(1)) << "Clamped by end face - " << parameters;
142  EXPECT_EQ(0.5 * h * z_sign, p_CN(2)) << "Clamped by end face - "
143  << parameters;
144 
145  // Case: clamped by both end face and barrel.
146  p_CQ = radial_offset_external + z_offset_external;
147  N_is_not_S = nearestPointInCylinder(h, r, p_CQ, &p_CN);
148  EXPECT_TRUE(N_is_not_S) << "Fully clamped - " << parameters;
149  EXPECT_NEAR(point_on_barrel(0), p_CN(0), Eps<S>::value())
150  << "Fully clamped - " << parameters;
151  EXPECT_NEAR(point_on_barrel(1), p_CN(1), Eps<S>::value())
152  << "Fully clamped - " << parameters;
153  EXPECT_EQ(0.5 * h * z_sign, p_CN(2)) << "Fully clamped - "
154  << parameters;
155  }
156  }
157 }
158 
159 // Defines the test configuration for a single test. It includes the geometry
160 // and the pose of the sphere in the cylinder's frame C. It also includes the
161 // expected answers in that same frame. It does not include those quantities
162 // that vary from test invocation to invocation (e.g., the pose of the cylinder
163 // in the world frame or the *orientation* of the sphere).
164 //
165 // Collision and distance are complementary queries -- two objects in collision
166 // have no defined distance because they are *not* separated and vice versa.
167 // These configurations allow for the test of the complementarity property.
168 template <typename S>
169 struct TestConfiguration {
170  TestConfiguration(const std::string& name_in, const S& r_c_in,
171  const S& h_c_in, const S& r_s_in,
172  const Vector3<S> &p_CSo_in, bool colliding)
173  : name(name_in),
174  cylinder_half_len(h_c_in / 2),
175  r_c(r_c_in),
176  r_s(r_s_in),
177  p_CSo(p_CSo_in),
178  expected_colliding(colliding) {}
179 
180  // Descriptive name of the test configuration.
181  std::string name;
182  // Half the length of the cylinder along the z-axis.
184  // Radius of the cylinder.
185  S r_c;
186  // Radius of the sphere.
187  S r_s;
188  // Position of the sphere's center in the cylinder frame.
189  Vector3<S> p_CSo;
190 
191  // Indicates if this test configuration is expected to be in collision.
192  bool expected_colliding{false};
193 
194  // Collision values; only valid if expected_colliding is true.
196  Vector3<S> expected_normal;
197  Vector3<S> expected_pos;
198 
199  // Distance values; only valid if expected_colliding is false.
201  // The points on sphere and cylinder, respectively, closest to the other shape
202  // measured and expressed in the cylinder frame C. Only defined if separated.
203  Vector3<S> expected_p_CSc;
204  Vector3<S> expected_p_CCs;
205 };
206 
207 // Utility for creating a copy of the input configurations and appending more
208 // labels to the configuration name -- aids in debugging.
209 template <typename S>
210 std::vector<TestConfiguration<S>> AppendLabel(
211  const std::vector<TestConfiguration<S>>& configurations,
212  const std::string& label) {
213  std::vector<TestConfiguration<S>> configs;
214  for (const auto& config : configurations) {
215  configs.push_back(config);
216  configs.back().name += " - " + label;
217  }
218  return configs;
219 }
220 
221 // Returns a collection of configurations where sphere and cylinder are similar
222 // in size.
223 template <typename S>
224 std::vector<TestConfiguration<S>> GetUniformConfigurations() {
225  std::vector<TestConfiguration<S>> configurations;
226 
227  // Common configuration values
228  // Cylinder and sphere dimensions.
229  const S h_c = 0.6;
230  const S half_h_c = h_c / 2;
231  const S r_c = 1.2;
232  const S r_s = 0.7;
233  const bool collides = true;
234 
235  // Collection of directions parallel to the z = 0 plane. Used for sampling
236  // queries in various directions around the barrel. Note: for the tests to be
237  // correct, these normals must all have unit length and a zero z-component.
238  std::vector<Vector3<S>> barrel_directions{
239  Vector3<S>{1, 0, 0},
240  Vector3<S>{0, 1, 0},
241  Vector3<S>(1, S(0.5), 0).normalized(),
242  Vector3<S>(-1, S(0.5), 0).normalized(),
243  Vector3<S>(-1, -S(0.5), 0).normalized(),
244  Vector3<S>(1, -S(0.5), 0).normalized()};
245 
246  {
247  // Case: Completely separated. Nearest point on the +z face.
248  const Vector3<S> p_CS{r_c * S(0.25), r_c * S(0.25),
249  half_h_c + r_s * S(1.1)};
250  configurations.emplace_back(
251  "Separated; nearest face +z", r_c, h_c, r_s, p_CS, !collides);
252 
253  TestConfiguration<S>& config = configurations.back();
254  // Not colliding --> no collision values.
255  config.expected_distance = p_CS(2) - half_h_c - r_s;
256  config.expected_p_CCs = Vector3<S>{p_CS(0), p_CS(1), half_h_c};
257  config.expected_p_CSc = Vector3<S>{p_CS(0), p_CS(1), p_CS(2) - r_s};
258  }
259 
260  {
261  // Case: Sphere completely separated with center in barrel Voronoi region.
262  const S target_distance = 0.1;
263  const Vector3<S> n_SC = Vector3<S>{-1, -1, 0}.normalized();
264  const Vector3<S> p_CS = Vector3<S>{0, 0, half_h_c * S(0.5)} -
265  n_SC * (r_s + r_c + target_distance);
266  configurations.emplace_back(
267  "Separated; near barrel", r_c, h_c, r_s, p_CS, !collides);
268 
269  TestConfiguration<S>& config = configurations.back();
270  // Not colliding --> no collision values.
271  config.expected_distance = target_distance;
272  config.expected_p_CCs = -n_SC * r_c + Vector3<S>{0, 0, p_CS(2)};
273  config.expected_p_CSc = p_CS + n_SC * r_s;
274  }
275 
276  {
277  // Case: Sphere completely separated with center in *edge* Voronoi region.
278  const S target_distance = .1;
279  const Vector3<S> n_SC = Vector3<S>{-1, -1, -1}.normalized();
280  const Vector3<S> p_CCs = Vector3<S>{0, 0, half_h_c} +
281  Vector3<S>{-n_SC(0), -n_SC(1), 0}.normalized() * r_c;
282  const Vector3<S> p_CS = p_CCs - n_SC * (r_s + target_distance);
283  configurations.emplace_back(
284  "Separated; near barrel edge", r_c, h_c, r_s, p_CS, !collides);
285 
286  TestConfiguration<S>& config = configurations.back();
287  // Not colliding --> no collision values.
288  config.expected_distance = target_distance;
289  config.expected_p_CCs = p_CCs;
290  config.expected_p_CSc = p_CS + n_SC * r_s;
291  }
292 
293  using std::min;
294  const S target_depth = min(r_c, r_s) * S(0.25);
295 
296  // Case contact - sphere center outside cylinder.
297  // Sub-cases intersection *only* through cap face; one test for each face.
298  for (S sign : {S(-1), S(1)}) {
299  const Vector3<S> n_SC = Vector3<S>::UnitZ() * -sign;
300  const Vector3<S> p_CCs = Vector3<S>{r_c * S(0.25), r_c * S(0.25),
301  half_h_c * sign};
302  const Vector3<S> p_CS = p_CCs - n_SC * (r_s - target_depth);
303  configurations.emplace_back(
304  "Colliding external sphere center; cap face in " +
305  (sign < 0 ? std::string("-z") : std::string("+z")),
306  r_c, h_c, r_s, p_CS, collides);
307 
308  TestConfiguration<S>& config = configurations.back();
309  config.expected_depth = target_depth;
310  config.expected_normal = n_SC;
311  config.expected_pos = p_CCs + n_SC * (target_depth / 2);
312  // Colliding; no distance values required.
313  }
314 
315  // Sub-cases intersection *only* through barrel. Sampled in multiple
316  // directions.
317  for (const Vector3<S>& n_CS : barrel_directions) {
318  const Vector3<S> p_CCs = Vector3<S>{0, 0, half_h_c * S(.1)} +
319  n_CS * r_c;
320  const Vector3<S> p_CS = p_CCs + n_CS * (r_s - target_depth);
321  std::stringstream ss;
322  ss << "Colliding external sphere center; barrel from sphere center in"
323  << n_CS.transpose() << " direction";
324  configurations.emplace_back(ss.str(), r_c, h_c, r_s, p_CS, collides);
325 
326  TestConfiguration<S>& config = configurations.back();
327  config.expected_depth = target_depth;
328  config.expected_normal = -n_CS;
329  config.expected_pos = p_CCs - n_CS * (target_depth / 2);
330  // Colliding; no distance values required.
331  }
332 
333  // Sub-cases intersection through edge.
334  for (S sign : {S(-1), S(1)}) {
335  // Projection of vector from cylinder center to sphere center on the z=0
336  // plane (and then normalized).
337  for (const Vector3<S>& n_CS_xy : barrel_directions) {
338  const Vector3<S> p_CCs = Vector3<S>{0, 0, sign * half_h_c} +
339  n_CS_xy * r_c;
340  const Vector3<S> n_CS = p_CCs.normalized();
341  const Vector3<S> p_CS = p_CCs + n_CS * (r_s - target_depth);
342  std::stringstream ss;
343  ss << "Colliding external sphere center; edge from sphere center in"
344  << n_CS.transpose() << " direction";
345  configurations.emplace_back(ss.str(), r_c, h_c, r_s, p_CS, collides);
346 
347  TestConfiguration<S>& config = configurations.back();
348  config.expected_depth = target_depth;
349  config.expected_normal = -n_CS;
350  config.expected_pos = p_CCs - n_CS * (target_depth / 2);
351  // Colliding; no distance values required.
352  }
353  }
354 
355  // Case contact - sphere center *inside* cylinder.
356 
357  // Sub-cases: sphere is unambiguously closest to end face. One test for each
358  // end face.
359  for (S sign : {S(-1), S(1)}) {
360  // Distance from sphere center S to face F.
361  const S d_SF = 0.1;
362  const Vector3<S> n_SC = Vector3<S>::UnitZ() * -sign;
363  const Vector3<S> p_CCs = Vector3<S>{r_c * S(0.25), r_c * S(0.25),
364  half_h_c * sign};
365  const Vector3<S> p_CS = p_CCs + n_SC * d_SF;
366  configurations.emplace_back(
367  "Colliding internal sphere center; cap face in " +
368  (sign < 0 ? std::string("-z") : std::string("+z")),
369  r_c, h_c, r_s, p_CS, collides);
370 
371  TestConfiguration<S>& config = configurations.back();
372  config.expected_depth = d_SF + r_s;
373  config.expected_normal = n_SC;
374  config.expected_pos = p_CCs + n_SC * (config.expected_depth / 2);
375  // Colliding; no distance values required.
376  }
377 
378  // Sub-cases: sphere is unambiguously closest to barrel; sampling multiple
379  // directions.
380  for (const Vector3<S>& n_CS : barrel_directions) {
381  // Distance from sphere center S to point B on barrel.
382  const S d_SB = 0.1;
383  const Vector3<S> p_CCs = Vector3<S>{0, 0, half_h_c * S(.1)} +
384  n_CS * r_c;
385  const Vector3<S> p_CS = p_CCs - n_CS * d_SB;
386  std::stringstream ss;
387  ss << "Colliding internal sphere center; barrel from sphere located in "
388  << n_CS.transpose() << " direction";
389  configurations.emplace_back(ss.str(), r_c, h_c, r_s, p_CS, collides);
390 
391  TestConfiguration<S>& config = configurations.back();
392  config.expected_depth = r_s + d_SB;
393  config.expected_normal = -n_CS;
394  config.expected_pos = p_CCs - n_CS * (config.expected_depth / 2);
395  // Colliding; no distance values required.
396  }
397 
398  // Case contact - sphere center is *near* error-dominated regions
399 
400  // Sub-case: Sphere center is within epsilon *outside* of end face.
401  // Numerically, this is processed as if the center were inside the cylinder.
402  // For face contact, there's no difference. This test subsumes the test where
403  // the center lies *on* the surface of the cylinder.
404  for (S sign : {S(-1), S(1)}) {
405  // Distance from sphere center S to face F.
406  const S d_SF = Eps<S>::value() / 2;
407  const Vector3<S> n_SC = Vector3<S>::UnitZ() * -sign;
408  const Vector3<S> p_CCs = Vector3<S>{r_c * S(0.25), r_c * S(0.25),
409  half_h_c * sign};
410  const Vector3<S> p_CS = p_CCs - n_SC * d_SF;
411  configurations.emplace_back(
412  "Colliding sphere center outside by ε; cap face in " +
413  (sign < 0 ? std::string("-z") : std::string("+z")),
414  r_c, h_c, r_s, p_CS, collides);
415 
416  TestConfiguration<S>& config = configurations.back();
417  config.expected_depth = r_s;
418  config.expected_normal = n_SC;
419  config.expected_pos = p_CCs + n_SC * (config.expected_depth / 2);
420  // Colliding; no distance values required.
421  }
422 
423  // Sub-case: Sphere center is within epsilon *outside* of barrel.
424  // Numerically, this is processed as if the center were inside the cylinder.
425  // For barrel contact, there's no difference. This test subsumes the test
426  // where the center lies *on* the surface of the cylinder.
427  for (const Vector3<S>& n_CS : barrel_directions) {
428  // Distance from sphere center S to point B on barrel.
429  const S d_SB = Eps<S>::value() / 2;
430  const Vector3<S> p_CCs = Vector3<S>{0, 0, half_h_c * S(.1)} +
431  n_CS * r_c;
432  const Vector3<S> p_CS = p_CCs - n_CS * d_SB;
433  std::stringstream ss;
434  ss << "Colliding sphere center outside by ε; barrel from sphere located in "
435  << n_CS.transpose() << " direction";
436  configurations.emplace_back(ss.str(), r_c, h_c, r_s, p_CS, collides);
437 
438  TestConfiguration<S>& config = configurations.back();
439  config.expected_depth = r_s;
440  config.expected_normal = -n_CS;
441  config.expected_pos = p_CCs - n_CS * (config.expected_depth / 2);
442  // Colliding; no distance values required.
443  }
444 
445  // Sub-case: Sphere center is within epsilon *outside* of edge.
446  // Numerically, this is processed as if the center were inside the cylinder.
447  // If the center is in the Voronoi region of the edge, the reported normal
448  // will be either the face or the barrel -- whichever is closer. In this
449  // configuration, it is the face normal. This test subsumes the test where
450  // the center lies *on* the surface of the cylinder.
451  for (S sign : {S(-1), S(1)}) {
452  // Projection of vector from cylinder center to sphere center on the z=0
453  // plane (and then normalized).
454  const S d_SC = Eps<S>::value() / 2;
455  for (const Vector3<S>& n_CS_xy : barrel_directions) {
456  const Vector3<S> p_CCs = Vector3<S>{0, 0, sign * half_h_c} +
457  n_CS_xy * r_c;
458  const Vector3<S> n_CS = p_CCs.normalized();
459  const Vector3<S> p_CS = p_CCs + n_CS * d_SC;
460  std::stringstream ss;
461  ss << "Colliding sphere center outside by ε; edge from sphere center in"
462  << n_CS.transpose() << " direction";
463  configurations.emplace_back(ss.str(), r_c, h_c, r_s, p_CS, collides);
464 
465  TestConfiguration<S>& config = configurations.back();
466  config.expected_depth = r_s;
467  // NOTE: Epsilon *outside* is considered inside so the normal direction
468  // will be either face or barrel -- and, in this case, it's face.
469  config.expected_normal = -sign * Vector3<S>::UnitZ();
470  config.expected_pos = p_CCs + config.expected_normal * (r_s / 2);
471  // Colliding; no distance values required.
472  }
473  }
474 
475  {
476  // Sub-case: Sphere center is on origin - face is closer. It should prefer
477  // the +z face.
478  const Vector3<S> p_CS = Vector3<S>::Zero();
479  // Guarantee that the barrel is farther than the face.
480  const S big_radius = h_c * 2;
481  configurations.emplace_back(
482  "Collision with sphere at origin; face nearest", big_radius, h_c, r_s,
483  p_CS, collides);
484 
485  TestConfiguration<S>& config = configurations.back();
486  config.expected_depth = r_s + h_c / 2;
487  config.expected_normal = -Vector3<S>::UnitZ();
488  config.expected_pos = Vector3<S>{0, 0, h_c / 2 - config.expected_depth / 2};
489  // Colliding; no distance values required.
490  }
491 
492  {
493  // Sub-case: Sphere center is on origin - barrel is closer.
494  const Vector3<S> p_CS = Vector3<S>::Zero();
495  // Guarantee that the barrel is closer than the face.
496  const S big_height = r_c * 4;
497  configurations.emplace_back(
498  "Collision with sphere at origin; barrel nearest", r_c, big_height, r_s,
499  p_CS, collides);
500 
501  TestConfiguration<S>& config = configurations.back();
502  config.expected_depth = r_s + r_c;
503  config.expected_normal = -Vector3<S>::UnitX();
504  config.expected_pos = Vector3<S>{r_c - config.expected_depth / 2, 0, 0};
505  // Colliding; no distance values required.
506  }
507 
508  return configurations;
509 }
510 
511 // Returns a collection of configurations where sphere and cylinder are scaled
512 // very differently.
513 template <typename S>
514 std::vector<TestConfiguration<S>> GetNonUniformConfigurations() {
515  std::vector<TestConfiguration<S>> configurations;
516 
517  // Case: Large, flat cylinder and tiny sphere.
518  {
519  const S r_c = 9;
520  const S h_c = 0.1;
521  const S r_s = 0.025;
522  const bool collides = true;
523  const S target_depth = r_s / 2;
524 
525  // Sub-case: Colliding -- contact with +z face.
526  {
527  // Colliding sub-case.
528  const Vector3<S> p_CCs = Vector3<S>(1, 2, 0).normalized() * (r_c - r_s) +
529  Vector3<S>::UnitZ() * (h_c / 2);
530  const Vector3<S> p_CS{p_CCs + Vector3<S>::UnitZ() * (r_s - target_depth)};
531  configurations.emplace_back(
532  "Collision large disk, small sphere - contact at face",
533  r_c, h_c, r_s, p_CS, collides);
534 
535  TestConfiguration<S>& config = configurations.back();
536  config.expected_depth = target_depth;
537  config.expected_normal = -Vector3<S>::UnitZ();
538  config.expected_pos = p_CCs - Vector3<S>::UnitZ() * (target_depth / 2);
539  // Colliding; no distance values required.
540  }
541 
542  // Sub-case: Separated -- nearest feature +z face.
543  {
544  // Separated sub-case.
545  const Vector3<S> p_CCs = Vector3<S>(1, 2, 0).normalized() * (r_c - r_s) +
546  Vector3<S>::UnitZ() * (h_c / 2);
547  const Vector3<S> p_CS{p_CCs +
548  Vector3<S>::UnitZ() * (r_s + target_depth)};
549  configurations.emplace_back(
550  "Separation large disk, small sphere - nearest +z face",
551  r_c, h_c, r_s, p_CS, !collides);
552 
553  TestConfiguration<S>& config = configurations.back();
554  // Not colliding --> no collision values.
555  config.expected_distance = target_depth;
556  config.expected_p_CCs = p_CCs;
557  config.expected_p_CSc = p_CS - Vector3<S>::UnitZ() * r_s;
558  }
559 
560  // Sub-case: Colliding -- contact with barrel.
561  const Vector3<S> n_CS = Vector3<S>(1, 2, 0).normalized();
562  const Vector3<S> p_CCs = n_CS * r_c + Vector3<S>::UnitZ() * (r_s * 0.1);
563  {
564  // Colliding sub-case.
565  const Vector3<S> p_CS{p_CCs + n_CS * (r_s - target_depth)};
566  configurations.emplace_back(
567  "Collision large disk, small sphere - contact at barrel",
568  r_c, h_c, r_s, p_CS, collides);
569 
570  TestConfiguration<S>& config = configurations.back();
571  config.expected_depth = target_depth;
572  config.expected_normal = -n_CS;
573  config.expected_pos = p_CCs - n_CS * (target_depth / 2);
574  // Colliding; no distance values required.
575  }
576 
577  // Sub-case: Separated -- nearest feature is barrel.
578  {
579  // Separated sub-case.
580  const Vector3<S> p_CS{p_CCs + n_CS * (r_s + target_depth)};
581  configurations.emplace_back(
582  "Separation large disk, small sphere - nearest barrel",
583  r_c, h_c, r_s, p_CS, !collides);
584 
585  TestConfiguration<S>& config = configurations.back();
586  // Not colliding --> no collision values.
587  config.expected_distance = target_depth;
588  config.expected_p_CCs = p_CCs;
589  config.expected_p_CSc = p_CS - n_CS * r_s;
590  }
591  }
592 
593  // Case: Large sphere and *tiny* cylinder.
594  {
595  const S r_c = 0.025;
596  const S h_c = 0.1;
597  const S r_s = 9;
598  const bool collides = true;
599  const S target_depth = r_c / 2;
600 
601  // Sub-case -- nearest feature is +z face.
602  {
603  const Vector3<S> p_CCs =
604  Vector3<S>(1, 2, 0).normalized() * (r_c * S(0.5)) +
605  Vector3<S>::UnitZ() * (h_c / 2);
606 
607  // Sub-case: Colliding.
608  {
609  const Vector3<S>
610  p_CS{p_CCs + Vector3<S>::UnitZ() * (r_s - target_depth)};
611  configurations.emplace_back(
612  "Collision large sphere, small disk - contact at face",
613  r_c, h_c, r_s, p_CS, collides);
614 
615  TestConfiguration<S>& config = configurations.back();
616  config.expected_depth = target_depth;
617  config.expected_normal = -Vector3<S>::UnitZ();
618  config.expected_pos = p_CCs - Vector3<S>::UnitZ() * (target_depth / 2);
619  // Colliding; no distance values required.
620  }
621 
622  // Subsub-case: Separated
623  {
624  const Vector3<S> p_CS{p_CCs +
625  Vector3<S>::UnitZ() * (r_s + target_depth)};
626  configurations.emplace_back(
627  "Separation large sphere, small disk - nearest +z face",
628  r_c, h_c, r_s, p_CS, !collides);
629 
630  TestConfiguration<S>& config = configurations.back();
631  // Not colliding --> no collision values.
632  config.expected_distance = target_depth;
633  config.expected_p_CCs = p_CCs;
634  config.expected_p_CSc = p_CS - Vector3<S>::UnitZ() * r_s;
635  }
636  }
637 
638  // Sub-case: Nearest feature is barrel
639  {
640  const Vector3<S> n_CS = Vector3<S>(1, 2, 0).normalized();
641  const Vector3<S> p_CCs = n_CS * r_c + Vector3<S>::UnitZ() * (h_c * 0.1);
642 
643  // Subsub-case: Colliding.
644  {
645  const Vector3<S> p_CS{p_CCs + n_CS * (r_s - target_depth)};
646  configurations.emplace_back(
647  "Collision large sphere, small disk - contact at barrel",
648  r_c, h_c, r_s, p_CS, collides);
649 
650  TestConfiguration<S>& config = configurations.back();
651  config.expected_depth = target_depth;
652  config.expected_normal = -n_CS;
653  config.expected_pos = p_CCs - n_CS * (target_depth / 2);
654  // Colliding; no distance values required.
655  }
656 
657  // Subsub-case: Separated .
658  {
659  const Vector3<S> p_CS{p_CCs + n_CS * (r_s + target_depth)};
660  configurations.emplace_back(
661  "Separation large sphere, small disk - nearest barrel",
662  r_c, h_c, r_s, p_CS, !collides);
663 
664  TestConfiguration<S>& config = configurations.back();
665  // Not colliding --> no collision values.
666  config.expected_distance = target_depth;
667  config.expected_p_CCs = p_CCs;
668  config.expected_p_CSc = p_CS - n_CS * r_s;
669  }
670  }
671  }
672 
673  return configurations;
674 }
675 
676 template <typename S>
677 using EvalFunc =
678 std::function<void(const TestConfiguration<S>&, const Transform3<S>&,
679  const Matrix3<S>&, S)>;
680 
681 // This evaluates an instance of a test configuration and confirms the results
682 // match the expected data. The test configuration is defined in the cylinder's
683 // frame with an unrotated sphere. The parameters provide the test parameters:
684 // the sphere orientation and the cylinder's pose in the world frame.
685 //
686 // Evaluates the collision query twice. Once as the boolean "is colliding" test
687 // and once with the collision characterized with depth, normal, and position.
688 template <typename S>
689 void EvalCollisionForTestConfiguration(const TestConfiguration<S>& config,
690  const Transform3<S>& X_WC,
691  const Matrix3<S>& R_CS,
692  S eps) {
693  // Set up the experiment from input parameters and test configuration.
694  Cylinder<S> cylinder(config.r_c, config.cylinder_half_len * S(2));
695  Sphere<S> sphere{config.r_s};
696  Transform3<S> X_CS = Transform3<S>::Identity();
697  X_CS.translation() = config.p_CSo;
698  X_CS.linear() = R_CS;
699  Transform3<S> X_WS = X_WC * X_CS;
700 
701  bool colliding = sphereCylinderIntersect<S>(sphere, X_WS, cylinder, X_WC,
702  nullptr);
703  EXPECT_EQ(config.expected_colliding, colliding) << config.name;
704 
705  std::vector<ContactPoint<S>> contacts;
706  colliding = sphereCylinderIntersect<S>(sphere, X_WS, cylinder, X_WC, &contacts);
707  EXPECT_EQ(colliding, config.expected_colliding) << config.name;
708  if (config.expected_colliding) {
709  EXPECT_EQ(1u, contacts.size()) << config.name;
710  const ContactPoint<S> &contact = contacts[0];
711  EXPECT_NEAR(config.expected_depth, contact.penetration_depth, eps)
712  << config.name;
713  EXPECT_TRUE(CompareMatrices(contact.normal,
714  X_WC.linear() * config.expected_normal, eps,
716  << config.name;
717  EXPECT_TRUE(CompareMatrices(contact.pos, X_WC * config.expected_pos, eps,
719  << config.name;
720  } else {
721  EXPECT_EQ(contacts.size(), 0u) << config.name;
722  }
723 }
724 
725 // This evaluates an instance of a test configuration and confirms the results
726 // match the expected data. The test configuration is defined in the cylinder's
727 // frame with an unrotated sphere. The parameters provide the test
728 // configuration.
729 //
730 // Evaluates the distance query twice. Once as the boolean "is separated" test
731 // and once with the separation characterized with distance and surface points.
732 template <typename S>
733 void EvalDistanceForTestConfiguration(const TestConfiguration<S>& config,
734  const Transform3<S>& X_WC,
735  const Matrix3<S>& R_CS,
736  S eps) {
737  // Set up the experiment from input parameters and test configuration.
738  Cylinder<S> cylinder(config.r_c, config.cylinder_half_len * S(2));
739  Sphere<S> sphere{config.r_s};
740  Transform3<S> X_CS = Transform3<S>::Identity();
741  X_CS.translation() = config.p_CSo;
742  X_CS.linear() = R_CS;
743  Transform3<S> X_WS = X_WC * X_CS;
744 
745  bool separated = sphereCylinderDistance<S>(sphere, X_WS, cylinder, X_WC,
746  nullptr, nullptr, nullptr);
747  EXPECT_NE(separated, config.expected_colliding) << config.name;
748 
749  // Initializing this to -2, to confirm that a non-colliding scenario sets
750  // distance to -1.
751  S distance{-2};
752  Vector3<S> p_WSc{0, 0, 0};
753  Vector3<S> p_WCs{0, 0, 0};
754 
755  separated = sphereCylinderDistance<S>(sphere, X_WS, cylinder, X_WC, &distance,
756  &p_WSc, &p_WCs);
757  EXPECT_NE(separated, config.expected_colliding) << config.name;
758  if (!config.expected_colliding) {
759  EXPECT_NEAR(distance, config.expected_distance, eps)
760  << config.name;
762  X_WC * config.expected_p_CSc, eps,
764  << config.name;
766  X_WC * config.expected_p_CCs, eps,
768  << config.name;
769  } else {
770  EXPECT_EQ(distance, S(-1)) << config.name;
771  EXPECT_TRUE(CompareMatrices(p_WSc, Vector3<S>::Zero(), 0,
773  EXPECT_TRUE(CompareMatrices(p_WCs, Vector3<S>::Zero(), 0,
775  }
776 }
777 
778 // This test defines the transforms for performing the single collision test.
779 template <typename S>
780 void QueryWithVaryingWorldFrames(
781  const std::vector<TestConfiguration<S>>& configurations,
782  EvalFunc<S> query_eval, const Matrix3<S>& R_CS = Matrix3<S>::Identity()) {
783  // Evaluate all the configurations with the given cylinder pose in frame F.
784  auto evaluate_all = [&R_CS, query_eval](
785  const std::vector<TestConfiguration<S>>& configs,
786  const Transform3<S>& X_FC) {
787  for (const auto config : configs) {
788  query_eval(config, X_FC, R_CS, Eps<S>::value());
789  }
790  };
791 
792  // Frame F is coincident and aligned with the cylinder frame.
793  Transform3<S> X_FC = Transform3<S>::Identity();
794  evaluate_all(AppendLabel(configurations, "X_FC = I"), X_FC);
795 
796  // Simple arbitrary translation away from the origin.
797  X_FC.translation() << 1.3, 2.7, 6.5;
798  evaluate_all(AppendLabel(configurations, "X_FC is translation"), X_FC);
799 
800  std::string axis_name[] = {"x", "y", "z"};
801  // 90 degree rotation around each axis.
802  for (int axis = 0; axis < 3; ++axis) {
803  std::string label = "X_FC is 90-degree rotation around " + axis_name[axis];
804  AngleAxis<S> angle_axis{constants<S>::pi() / 2, Vector3<S>::Unit(axis)};
805  X_FC.linear() << angle_axis.matrix();
806  evaluate_all(AppendLabel(configurations, label), X_FC);
807  }
808 
809  // Arbitrary orientation.
810  {
811  AngleAxis<S> angle_axis{constants<S>::pi() / 3,
812  Vector3<S>{1, 2, 3}.normalized()};
813  X_FC.linear() << angle_axis.matrix();
814  evaluate_all(AppendLabel(configurations, "X_FC is arbitrary rotation"),
815  X_FC);
816  }
817 
818  // Near axis aligned.
819  {
820  AngleAxis<S> angle_axis{constants<S>::eps_12(), Vector3<S>::UnitX()};
821  X_FC.linear() << angle_axis.matrix();
822  evaluate_all(AppendLabel(configurations, "X_FC is near identity"),
823  X_FC);
824  }
825 }
826 
827 // Runs all test configurations across multiple poses in the world frame --
828 // changing the orientation of the sphere -- should have no affect on the
829 // results.
830 template <typename S>
831 void QueryWithOrientedSphere(
832  const std::vector<TestConfiguration<S>>& configurations,
833  EvalFunc<S> query_eval) {
834 
835  std::string axis_name[] = {"x", "y", "z"};
836 
837  // 90 degree rotation around each axis.
838  for (int axis = 0; axis < 3; ++axis) {
839  AngleAxis<S> angle_axis{constants<S>::pi() / 2, Vector3<S>::Unit(axis)};
840  std::string label = "sphere rotate 90-degrees around " + axis_name[axis];
841  QueryWithVaryingWorldFrames<S>(AppendLabel(configurations, label),
842  query_eval, angle_axis.matrix());
843  }
844 
845  // Arbitrary orientation.
846  {
847  AngleAxis<S> angle_axis{constants<S>::pi() / 3,
848  Vector3<S>{1, 2, 3}.normalized()};
849  std::string label = "sphere rotated arbitrarily";
850  QueryWithVaryingWorldFrames<S>(AppendLabel(configurations, label),
851  query_eval, angle_axis.matrix());
852  }
853 
854  // Near axis aligned.
855  {
856  AngleAxis<S> angle_axis{constants<S>::eps_12(), Vector3<S>::UnitX()};
857  std::string label = "sphere rotated near axes";
858  QueryWithVaryingWorldFrames<S>(AppendLabel(configurations, label),
859  query_eval, angle_axis.matrix());
860  }
861 }
862 
863 //======================================================================
864 
865 // Tests the helper function that finds the closest point in the cylinder.
866 GTEST_TEST(SphereCylinderPrimitiveTest, NearestPointInCylinder) {
867  NearestPointInCylinder<float>();
868  NearestPointInCylinder<double>();
869 }
870 
871 // Evaluates collision on all test configurations across multiple poses in the
872 // world frame - but the sphere rotation is always the identity.
873 GTEST_TEST(SphereCylinderPrimitiveTest, CollisionAcrossVaryingWorldFrames) {
874  QueryWithVaryingWorldFrames<float>(GetUniformConfigurations<float>(),
875  EvalCollisionForTestConfiguration<float>);
876  QueryWithVaryingWorldFrames<double>(
877  GetUniformConfigurations<double>(),
878  EvalCollisionForTestConfiguration<double>);
879 }
880 
881 // Evaluates collision on all test configurations across multiple poses in the
882 // world frame - the sphere is rotated arbitrarily.
883 GTEST_TEST(SphereCylinderPrimitiveTest, CollisionWithSphereRotations) {
884  QueryWithOrientedSphere<float>(GetUniformConfigurations<float>(),
885  EvalCollisionForTestConfiguration<float>);
886  QueryWithOrientedSphere<double>(GetUniformConfigurations<double>(),
887  EvalCollisionForTestConfiguration<double>);
888 }
889 
890 // Evaluates collision on a small set of configurations where the cylinder and
891 // sphere are of radically different scales - evaluation across multiple poses
892 // in the world frame.
893 GTEST_TEST(SphereCylinderPrimitiveTest, CollisionIncompatibleScales) {
894  QueryWithVaryingWorldFrames<float>(GetNonUniformConfigurations<float>(),
895  EvalCollisionForTestConfiguration<float>);
896  QueryWithVaryingWorldFrames<double>(
897  GetNonUniformConfigurations<double>(),
898  EvalCollisionForTestConfiguration<double>);
899 }
900 
901 // Evaluates distance on all test configurations across multiple poses in the
902 // world frame - but the sphere rotation is always the identity.
903 GTEST_TEST(SphereCylinderPrimitiveTest, DistanceAcrossVaryingWorldFrames) {
904  QueryWithVaryingWorldFrames<float>(GetUniformConfigurations<float>(),
905  EvalDistanceForTestConfiguration<float>);
906  QueryWithVaryingWorldFrames<double>(GetUniformConfigurations<double>(),
907  EvalDistanceForTestConfiguration<double>);
908 }
909 
910 // Evaluates distance on all test configurations across multiple poses in the
911 // world frame - the sphere is rotated arbitrarily.
912 GTEST_TEST(SphereCylinderPrimitiveTest, DistanceWithSphereRotations) {
913  QueryWithOrientedSphere<float>(GetUniformConfigurations<float>(),
914  EvalDistanceForTestConfiguration<float>);
915  QueryWithOrientedSphere<double>(GetUniformConfigurations<double>(),
916  EvalDistanceForTestConfiguration<double>);
917 }
918 
919 // Evaluates distance on a small set of configurations where the cylinder and
920 // sphere are of radically different scales - evaluation across multiple poses
921 // in the world frame.
922 GTEST_TEST(SphereCylinderPrimitiveTest, DistanceIncompatibleScales) {
923  QueryWithVaryingWorldFrames<float>(GetNonUniformConfigurations<float>(),
924  EvalDistanceForTestConfiguration<float>);
925  QueryWithVaryingWorldFrames<double>(GetNonUniformConfigurations<double>(),
926  EvalDistanceForTestConfiguration<double>);
927 }
928 
929 } // namespace
930 } // namespace detail
931 } // namespace fcl
932 
933 //==============================================================================
934 int main(int argc, char *argv[]) {
935  ::testing::InitGoogleTest(&argc, argv);
936  return RUN_ALL_TESTS();
937 }
cylinder_half_len
S cylinder_half_len
Definition: test_sphere_cylinder.cpp:183
eigen_matrix_compare.h
sphere.h
main
int main(int argc, char *argv[])
Definition: test_sphere_cylinder.cpp:934
fcl::CompareMatrices
::testing::AssertionResult CompareMatrices(const Eigen::MatrixBase< DerivedA > &m1, const Eigen::MatrixBase< DerivedB > &m2, double tolerance=0.0, MatrixCompareType compare_type=MatrixCompareType::absolute)
Definition: eigen_matrix_compare.h:63
r_s
S r_s
Definition: test_sphere_cylinder.cpp:187
fcl::constants::eps
static constexpr Real eps()
Returns ε for the precision of the underlying scalar type.
Definition: constants.h:151
expected_depth
S expected_depth
Definition: test_sphere_cylinder.cpp:195
label
string label
Definition: test_half_space_convex.cpp:143
EXPECT_TRUE
#define EXPECT_TRUE(args)
expected_colliding
bool expected_colliding
Definition: test_sphere_cylinder.cpp:192
expected_distance
S expected_distance
Definition: test_sphere_cylinder.cpp:200
sphere_cylinder-inl.h
fcl::detail::nearestPointInCylinder
bool nearestPointInCylinder(const S &height, const S &radius, const Vector3< S > &p_CQ, Vector3< S > *p_CN_ptr)
Definition: sphere_cylinder-inl.h:79
fcl::constants::Real
detail::ScalarTrait< S >::type Real
Definition: constants.h:131
EXPECT_NEAR
#define EXPECT_NEAR(a, b, prec)
expected_pos
Vector3< S > expected_pos
Definition: test_sphere_cylinder.cpp:197
fcl::MatrixCompareType::absolute
@ absolute
r
S r
Definition: test_sphere_box.cpp:171
fcl::detail::distance
template void distance(DistanceTraversalNodeBase< double > *node, BVHFrontList *front_list, int qsize)
p_CSo
Vector3< S > p_CSo
Definition: test_sphere_cylinder.cpp:189
fcl::detail::GTEST_TEST
GTEST_TEST(FCL_GJK_EPA, faceNormalPointingOutward)
Definition: test_gjk_libccd-inl_epa.cpp:205
fcl::constants::pi
static constexpr S pi()
The mathematical constant pi.
Definition: constants.h:134
expected_p_CCs
Vector3< S > expected_p_CCs
Definition: test_sphere_cylinder.cpp:204
min
static T min(T x, T y)
Definition: svm.cpp:49
fcl::constants::eps_12
static Real eps_12()
Returns ε^(1/2) for the precision of the underlying scalar type.
Definition: constants.h:176
expected_p_CSc
Vector3< S > expected_p_CSc
Definition: test_sphere_cylinder.cpp:203
r_c
S r_c
Definition: test_sphere_cylinder.cpp:185
expected_normal
Vector3< S > expected_normal
Definition: test_sphere_cylinder.cpp:196
fcl
Main namespace.
Definition: broadphase_bruteforce-inl.h:45
EXPECT_EQ
#define EXPECT_EQ(a, b)
EXPECT_FALSE
#define EXPECT_FALSE(args)
cylinder.h
name
std::string name
Definition: test_sphere_cylinder.cpp:181


fcl
Author(s):
autogenerated on Tue Dec 5 2023 03:40:49