-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
distance_to_point_callback_test.cc
724 lines (635 loc) · 28.9 KB
/
distance_to_point_callback_test.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
#include "drake/geometry/proximity/distance_to_point_callback.h"
#include <fcl/fcl.h>
#include <gtest/gtest.h>
#include "drake/common/test_utilities/eigen_matrix_compare.h"
#include "drake/common/test_utilities/expect_throws_message.h"
#include "drake/geometry/proximity/proximity_utilities.h"
#include "drake/geometry/utilities.h"
#include "drake/math/autodiff_gradient.h"
#include "drake/math/rigid_transform.h"
#include "drake/math/rotation_matrix.h"
namespace drake {
namespace geometry {
namespace internal {
namespace point_distance {
namespace {
// TODO(SeanCurtis-TRI): Run through proximity_engine_test and pull the tests
// from there that better belong here.
using Eigen::Vector3d;
using math::RigidTransform;
using math::RigidTransformd;
using math::RotationMatrix;
using std::make_shared;
// Performs a point-to-shape signed-distance query and tests the result. This
// particularly focuses on the AutoDiff-valued version of these methods. It
// does a limited smoke-test on the results.
//
// It computes ∂distance / ∂p_WQ and compares it with the reported grad_W value
// with the assumption that they should be the same.
//
// The caller provides the nearest point p_GN_G (in the geometry frame G) and
// the offset from the nearest point to the query point Q (p_NQ_G) also in the
// geometry frame G. The query point is inferred from these two values. It
// performs the query and examines the results. It generally assumes
// non-negative signed distance. If the point is inside (i.e., negative signed
// distance), then the additional boolean `is_inside` should be set to true.
// To test robustness, the frame G can have an arbitrary pose in the world
// frame (defined by X_WG). Finally, an absolute tolerance is provided to
// define the scope of correctness.
//
// Note: this is *not* the complete test; we should also confirm that the
// derivatives of grad_W and the witness points are likewise correct.
// TODO(hongkai.dai): Extend these tests to test the AutoDiff derivatives of
// the reported gradient and witness points w.r.t. arbitrary basis as well.
template <typename Shape>
class PointShapeAutoDiffSignedDistanceTester {
public:
DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN(PointShapeAutoDiffSignedDistanceTester)
// Constructs a tester for a given shape G, pose in world X_WG, and tolerance.
// The shape must be non-null and must persist beyond the life of the tester
// as a reference to the shape will be stored.
PointShapeAutoDiffSignedDistanceTester(const Shape* shape,
const RigidTransformd& X_WG,
double tolerance)
: shape_(*shape), X_WG_(X_WG), tolerance_(tolerance) {}
// Perform the test with the particular N and Q.
::testing::AssertionResult Test(const Vector3d& p_GN_G,
const Vector3d& p_NQ_G,
bool is_inside = false,
bool is_grad_W_unique = true) {
const double sign = is_inside ? -1 : 1;
const double expected_distance = sign * p_NQ_G.norm();
const Vector3d p_GQ = p_GN_G + p_NQ_G;
const Vector3d p_WQ = X_WG_ * p_GQ;
::testing::AssertionResult failure = ::testing::AssertionFailure();
bool error = false;
// We take the gradient of the signed distance query w.r.t p_WQ.
Vector3<AutoDiffXd> p_WQ_ad = math::InitializeAutoDiff(p_WQ);
// The size of the variables we take gradient with (p_WQ) is 3.
const int grad_size = 3;
DistanceToPoint<AutoDiffXd> distance_to_point(
GeometryId::get_new_id(), X_WG_.cast<AutoDiffXd>(), p_WQ_ad);
SignedDistanceToPoint<AutoDiffXd> result = distance_to_point(shape_);
if (std::abs(result.distance.value() - expected_distance) > tolerance_) {
error = true;
failure << "The difference between expected distance and tested distance "
"is greater than the given tolerance_:\n"
<< " Expected distance: " << expected_distance << "\n"
<< " Tested distance: " << result.distance.value() << "\n"
<< " tolerance: " << tolerance_ << "\n"
<< " difference: "
<< (std::abs(result.distance.value() - expected_distance));
}
// The hand-computed `grad_W` value should match the autodiff-computed
// gradient.
if (result.distance.derivatives().size() != 3) {
if (error) failure << "\n";
error = true;
failure << "Test distance has no derivatives";
}
const Vector3d ddistance_dp_WQ = result.distance.derivatives();
const Vector3d grad_W_val = math::ExtractValue(result.grad_W);
if (grad_W_val.array().isNaN().any()) {
if (error) failure << "\n";
error = true;
failure << "Analytical gradient contains NaN: " << grad_W_val.transpose();
}
auto gradient_compare =
CompareMatrices(ddistance_dp_WQ, grad_W_val, tolerance_);
if (!gradient_compare) {
if (error) failure << "\n";
error = true;
failure << "grad_W and distance.derivatives() don't match:\n"
<< gradient_compare.message();
}
// Since grad_W is a unit vector, we know that grad_Wᵀ * grad_W = 1.
const AutoDiffXd grad_W_squared_norm = result.grad_W.dot(result.grad_W);
EXPECT_NEAR(grad_W_squared_norm.value(), 1, tolerance_);
// The gradient of grad_W_squared_norm should be 0.
if (grad_W_squared_norm.derivatives().size() > 0) {
auto grad_W_unit_length_derivative_compare =
CompareMatrices(grad_W_squared_norm.derivatives(),
Eigen::VectorXd::Zero(grad_size), tolerance_);
if (!grad_W_unit_length_derivative_compare) {
if (error) failure << "\n";
error = true;
failure << "grad_W_squared_norm.derivatives() isn't right:\n"
<< grad_W_unit_length_derivative_compare.message();
}
}
// We have the invariance p_WQ = p_WN + distance * grad_W.
const Vector3<AutoDiffXd> p_WN_ad = X_WG_.cast<AutoDiffXd>() * result.p_GN;
const Vector3<AutoDiffXd> p_WQ_ad_expected =
p_WN_ad + result.distance * result.grad_W;
auto p_WQ_val_compare = CompareMatrices(
math::ExtractValue(p_WQ_ad),
math::ExtractValue(p_WQ_ad_expected), tolerance_);
if (!p_WQ_val_compare) {
if (error) failure << "\n";
error = true;
failure << "p_WQ does not equal to p_WN + distance * grad_W:\n"
<< p_WQ_val_compare.message();
}
// We'll only test the derivatives of the gradient if we expect it to be
// unique.
if (is_grad_W_unique) {
auto p_WQ_derivative_compare = CompareMatrices(
math::ExtractGradient(p_WQ_ad),
math::ExtractGradient(p_WQ_ad_expected),
tolerance_);
if (!p_WQ_derivative_compare) {
if (error) failure << "\n";
error = true;
failure << "Gradient of p_WQ does not equal to gradient of (p_WN + "
"distance * grad_W):\n"
<< p_WQ_derivative_compare.message();
}
}
if (!error) return ::testing::AssertionSuccess();
return failure;
}
private:
const Shape& shape_;
const RigidTransformd X_WG_;
const double tolerance_{std::numeric_limits<double>::epsilon()};
};
// Simple smoke test for signed distance to Box. It does the following:
// Perform test of three different points w.r.t. a box: outside, on
// surface, and inside.
// Do it with AutoDiff relative to the query point's position.
// Confirm the *values* of the reported quantity.
// Confirm that the derivative of distance (extracted from AutoDiff) matches
// the derivative computed by hand (grad_W).
GTEST_TEST(DistanceToPoint, Box) {
const double kEps = 4 * std::numeric_limits<double>::epsilon();
// Provide some arbitrary pose of the box in the world.
const RotationMatrix<double> R_WG(
AngleAxis<double>(M_PI / 5, Vector3d{1, 2, 3}.normalized()));
const Vector3d p_WG{0.5, 1.25, -2};
const RigidTransformd X_WG(R_WG, p_WG);
const fcl::Boxd box(1.0, 2.0, 3.0);
PointShapeAutoDiffSignedDistanceTester<fcl::Boxd> tester(&box, X_WG, kEps);
// Case: Nearest point is a vertex.
{
for (double x : {-1, 1}) {
for (double y : {-1, 1}) {
for (double z : {-1, 1}) {
const Vector3d p_NQ_G{x, y, z};
const Vector3d p_GN_G = p_NQ_G.cwiseProduct(box.side / 2);
// The query point lies outside the box.
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
// The query point lies on the vertex of the box.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d::Zero(),
false /* not inside the box */,
false /* gradient ill defined */));
}
}
}
// A query point *inside* the box would not be nearest the vertex.
}
// Case: Nearest point lies on an edge.
for (int coord1 = 0; coord1 < 3; ++coord1) {
// p_NQ_G(coord1) = 0
int coord2 = (coord1 + 1) % 3;
int coord3 = (coord1 + 2) % 3;
for (double coord2_val : {-1, 1}) {
for (double coord3_val : {-1, 1}) {
Vector3d p_NQ_G;
p_NQ_G(coord1) = 0;
p_NQ_G(coord2) = coord2_val;
p_NQ_G(coord3) = coord3_val;
const Vector3d p_GN_G = p_NQ_G.cwiseProduct(box.side / 2);
// The query point lies outside the box.
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
// The query point lies on the edge of the box.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d::Zero(),
false /* not inside the box */,
false /* gradient ill defined */));
// A query point *inside* the box would not be nearest the edge.
}
}
}
// Case point lies *outside* the box, nearest a face.
{
for (double sign : {-1, 1}) {
for (int axis : {0, 1, 2}) {
Vector3d vhat_NG = Vector3d::Zero();
vhat_NG(axis) = sign;
Vector3d p_NQ_G = 1.5 * vhat_NG;
const Vector3d p_GN_G = vhat_NG.cwiseProduct(box.side / 2);
// The query point lies outside the box.
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
// The query point lies on the face of the box.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d::Zero()));
// The query point lies inside the box.
EXPECT_TRUE(tester.Test(p_GN_G, -0.1 * vhat_NG, true /* is inside */));
}
}
}
}
// Simple smoke test for signed distance to Capsule. It does the following:
// Perform test of three different points w.r.t. a capsule: outside, on
// surface, and inside.
// Do it with AutoDiff relative to the query point's position.
// Confirm the *values* of the reported quantity.
// Confirm that the derivative of distance (extracted from AutoDiff) matches
// the derivative computed by hand (grad_W).
GTEST_TEST(DistanceToPoint, Capsule) {
const double kEps = 6 * std::numeric_limits<double>::epsilon();
// Provide some arbitrary pose of the capsule G in the world.
const RotationMatrix<double> R_WG(
AngleAxis<double>(M_PI / 5, Vector3d{1, 2, 3}.normalized()));
const Vector3d p_WG{0.5, 1.25, -2};
const RigidTransform<double> X_WG(R_WG, p_WG);
const fcl::Capsuled capsule(0.7, 1.3);
PointShapeAutoDiffSignedDistanceTester<fcl::Capsuled> tester(&capsule, X_WG,
kEps);
// We want to test the 3 sections for when the point Q is nearest to:
// 1. The top end cap of the capsule.
// 2. The spine of the capsule.
// 3. The bottom end cap of the capsule.
// In each section, we pick a direction away from the capsule that *isn't*
// aligned with the frame basis and prepare the witness point N accordingly.
const Vector3d vhat_NQ_Gs[3] = {
Vector3d{2, -3, 6}.normalized(), // Upwards and away.
Vector3d{2, -3, 0}.normalized(), // Perpendicularly outwards.
Vector3d{2, -3, -6}.normalized() // Downwards and away.
};
const Vector3d p_GN_Gs[3] = {
capsule.radius * vhat_NQ_Gs[0] + Vector3d{0, 0, capsule.lz / 2},
capsule.radius * vhat_NQ_Gs[1] + Vector3d{0, 0, capsule.lz / 4},
capsule.radius * vhat_NQ_Gs[2] + Vector3d{0, 0, -capsule.lz / 2}};
for (int i = 0; i < 3; ++i) {
const Vector3d& vhat_NQ_G = vhat_NQ_Gs[i];
const Vector3d& p_GN_G = p_GN_Gs[i];
// Case: point lies *outside* the capsule.
{
const Vector3d p_NQ_G = 1.5 * vhat_NQ_G;
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
}
// Case: point lies *on* the capsule.
{
const Vector3d p_NQ_G = Vector3d::Zero();
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
}
// Case: point lies *in* the capsule.
{
const Vector3d p_NQ_G = -(0.5 * capsule.radius) * vhat_NQ_G;
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G, true /* is inside */));
}
// Case: point lies on the capsule's spine.
{
const Vector3d p_NQ_G = -capsule.radius * vhat_NQ_G;
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G, true /* is inside */,
false /* gradient ill defined */));
}
}
}
// Test the evaluation of signed distance to Ellipsoid. This explicitly spells
// out a double-valued test because AutoDiffXd is not yet supported. We hand
// construct a few points (and corresponding normals) on the surface of the
// ellipsoid and construct query points relative to those surface points.
// The surface points should report as the witness point, the normal is the
// gradient, etc.
//
// For each sample, we try a different distance and confirm all the fields of
// the returned type.
GTEST_TEST(DistanceToPoint, Ellipsoid) {
// Provide some arbitrary pose of the ellipsoid G in the world.
const RotationMatrix<double> R_WG(
AngleAxis<double>(M_PI / 5, Vector3d{1, 2, 3}.normalized()));
const Vector3d p_WG{0.5, 1.25, -2};
const RigidTransform<double> X_WG(R_WG, p_WG);
// Note: the more eccentric the ellipsoid becomes (with higher curvature), the
// more error there will be near the regions with high curvature. This is
// because we're using GJK/EPA to compute distance. So, to avoid highly
// variable error across the point samples, we'll keep the ellipsoid "close"
// to a sphere. If we stretch it out, we need either a) a larger tolerance
// value for the vectors or b) a per-sample tolerance values.
const fcl::Ellipsoidd ellipsoid(1.5, 0.75, 1.25);
// TODO(SeanCurtis-TRI): When point-to-ellipsoid supports AutoDiffXd, modify
// this to exercise PointShapeAutoDiffSignedDistanceTester.
auto get_sample = [&ellipsoid](double theta, double phi) {
// Compute a point on the surface of the ellipsoid and the normal at
// that point. For the point, we use the parametric equation of the
// ellipsoid (on two periodic parameters):
// E = [a⋅cos(θ)⋅sin(ϕ), b⋅sin(θ)⋅sin(ϕ), c⋅cos(ϕ)].
// For the normal, we normalize the gradient of
// f = x² / a² + y² / b² + z² / c²
// ∇f = <2x / a², 2y / b², 2z / c²>
// n = ∇f / |∇f|
const double a = ellipsoid.radii.x();
const double b = ellipsoid.radii.y();
const double c = ellipsoid.radii.z();
const double x = a * std::cos(theta) * std::sin(phi);
const double y = b * std::sin(theta) * std::sin(phi);
const double z = c * std::cos(phi);
// Because we're normalizing the gradient, we simply omit the redundant
// scale factor of 2.
return std::make_pair(
Vector3d{x, y, z},
Vector3d{x / a / a, y / b / b, z / c / c}.normalized());
};
struct EllipseCoord {
double theta{};
double phi{};
};
std::vector<EllipseCoord> coords{
EllipseCoord{0, 0}, // Bottom pole.
EllipseCoord{7 * M_PI / 5, M_PI / 6}, // Lower half.
EllipseCoord{3 * M_PI / 7, 4 * M_PI / 5} // Upper half.
};
constexpr double kDistTolerance = 5e-5;
constexpr double kVectorTolerance = 5e-4;
const GeometryId id = GeometryId::get_new_id();
for (const auto& coord : coords) {
const auto& [p_GN, n_G] = get_sample(coord.theta, coord.phi);
for (const double distance : {-0.125, 0.0, 0.2}) {
const Vector3d p_GQ = p_GN + n_G * distance;
const Vector3d p_WQ = X_WG * p_GQ;
DistanceToPoint<double> distance_to_point(id, X_WG, p_WQ);
const SignedDistanceToPoint<double> result = distance_to_point(ellipsoid);
SCOPED_TRACE(fmt::format(
"theta = {}, phi = {}, distance = {}\n p_GQ: {} {} {}", coord.theta,
coord.phi, distance, p_GQ.x(), p_GQ.y(), p_GQ.z()));
EXPECT_EQ(result.id_G, id);
EXPECT_NEAR(result.distance, distance, kDistTolerance);
EXPECT_TRUE(CompareMatrices(result.p_GN, p_GN, kVectorTolerance));
EXPECT_TRUE(CompareMatrices(result.grad_W, X_WG.rotation() * n_G,
kVectorTolerance));
}
}
// Special case where the query point lies on the medial axis of
// the ellipsoid. We don't know what FCL will produce as the nearest point.
// However, we can confirm:
// - distance from query to nearest point.
// - gradient is perpendicular to a known circle.
// - id is correct.
// To facilitate this, we'll pick the query point (0, 0, 0). Then the nearest
// point must be a distance equal to the shortest radii (0.75), it must lie
// either on the +y or -y axis (the axis with the shortest radii) and have
// a normal that points in one of those two directions.
const double min_radius = ellipsoid.radii.y();
const Vector3d min_axis_G(0, 1, 0);
const Vector3d p_WQ = X_WG * Vector3d::Zero();
DistanceToPoint<double> distance_to_point(id, X_WG, p_WQ);
const SignedDistanceToPoint<double> result = distance_to_point(ellipsoid);
EXPECT_EQ(result.id_G, id);
EXPECT_NEAR(result.distance, -min_radius, kDistTolerance);
EXPECT_TRUE(CompareMatrices(result.p_GN.cwiseAbs(), min_axis_G * min_radius,
kVectorTolerance));
EXPECT_TRUE(
CompareMatrices((X_WG.rotation().inverse() * result.grad_W).cwiseAbs(),
min_axis_G, kVectorTolerance));
}
// Simple smoke test for signed distance to Halfspace. It does the following:
// Perform test of three different points w.r.t. a halfspace: outside, on
// surface, and inside.
// Do it with AutoDiff relative to the query point's position.
// Confirm the *values* of the reported quantity.
// Confirm that the derivative of distance (extracted from AutoDiff) matches
// the derivative computed by hand (grad_W).
GTEST_TEST(DistanceToPoint, Halfspace) {
const double kEps = 4 * std::numeric_limits<double>::epsilon();
// Provide some arbitrary pose of the halfspace in the world.
const RotationMatrix<double> R_WG(
AngleAxis<double>(M_PI / 5, Vector3d{1, 2, 3}.normalized()));
const Vector3d p_WG{0.5, 1.25, -2};
const RigidTransformd X_WG(R_WG, p_WG);
const fcl::Halfspaced hs(Vector3d::UnitZ(), 0);
PointShapeAutoDiffSignedDistanceTester<fcl::Halfspaced> tester(&hs, X_WG,
kEps);
// An arbitrary direction away from the origin that *isn't* aligned with the
// frame basis.
const Vector3d phat_NQ_G = Vector3d::UnitZ();
const Vector3d p_NQ_G = 1.5 * phat_NQ_G;
const Vector3d p_GN_G = Vector3d(0.25, 0.5, 0);
// Case: point lies *outside* the halfspace.
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
// Case: point lies *on* the halfspace.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d::Zero()));
// Case: point lies *in* the halfspace.
EXPECT_TRUE(tester.Test(p_GN_G, -0.1 * phat_NQ_G, true /* is inside */));
}
// Simple smoke test for signed distance to Sphere. It does the following:
// Perform test of three different points w.r.t. a sphere: outside, on
// surface, and inside.
// Do it with AutoDiff relative to the query point's position.
// Confirm the *values* of the reported quantity.
// Confirm that the derivative of distance (extracted from AutoDiff) matches
// the derivative computed by hand (grad_W).
GTEST_TEST(DistanceToPoint, Sphere) {
const double kEps = 4 * std::numeric_limits<double>::epsilon();
// Provide some arbitrary pose of the sphere in the world.
const RotationMatrix<double> R_WG(
AngleAxis<double>(M_PI / 5, Vector3d{1, 2, 3}.normalized()));
const Vector3d p_WG{0.5, 1.25, -2};
const RigidTransform<double> X_WG(R_WG, p_WG);
const fcl::Sphered sphere(0.7);
PointShapeAutoDiffSignedDistanceTester<fcl::Sphered> tester(&sphere, X_WG,
kEps);
// An arbitrary direction away from the origin that *isn't* aligned with the
// frame basis.
const Vector3d vhat_NQ = Vector3d{2, -3, 6}.normalized();
const Vector3d p_NQ_G = 1.5 * vhat_NQ;
const Vector3d p_GN_G = sphere.radius * vhat_NQ;
// Case: point lies *outside* the sphere.
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
// Case: point lies *on* the sphere.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d::Zero()));
// Case: point lies *in* the sphere.
EXPECT_TRUE(tester.Test(p_GN_G, -0.1 * p_NQ_G, true /* is inside */));
// Case: point lies at origin of the sphere.
EXPECT_TRUE(tester.Test(p_GN_G, -sphere.radius * vhat_NQ,
true /* is inside */,
false /* gradient ill defined */));
}
// TODO(SeanCurtis-TRI): Point-to-cylinder with AutoDiff has been "disabled".
// However, this has been done at the callback level and these tests are
// structured to exercise DistanceToPoint directly. This is a short-term
// issue and will be addressed with a reformulation of the point-cylinder
// calculation. When fixed, these tests will become meaningful, so we're
// leaving it in to prevent losing the effort.
#if 0
// Simple smoke test for signed distance to Cylinder. It does the following:
// Perform test of three different points w.r.t. a cylinder: outside, on
// surface, and inside.
// Do it with AutoDiff relative to the query point's position.
// Confirm the *values* of the reported quantity.
// Confirm that the derivative of distance (extracted from AutoDiff) matches
// the derivative computed by hand (grad_W).
GTEST_TEST(DistanceToPoint, Cylinder) {
const double kEps = 4 * std::numeric_limits<double>::epsilon();
// Provide some arbitrary pose of the cylinder in the world.
const RotationMatrix<double> R_WG(
AngleAxis<double>(M_PI / 5, Vector3d{1, 2, 3}.normalized()));
const Vector3d p_WG{0.5, 1.25, -2};
const RigidTransform<double> X_WG(R_WG, p_WG);
const fcl::Cylinderd cylinder(0.75, 2.5);
PointShapeAutoDiffSignedDistanceTester<fcl::Cylinderd> tester(&cylinder, X_WG,
kEps);
// Case: Nearest point is on the cap.
{
// Test top and bottom caps.
for (double sign : {-1, 1}) {
// The nearest point N is the cap of the cylinder -- slightly perturbed to
// be away from the cap center.
const Vector3d p_GN_G{cylinder.radius * 0.25, cylinder.radius * 0.25,
sign * cylinder.lz / 2};
// The query point lies outside the cylinder
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d{0, 0, sign * 0.75}));
// The query point lies on the cap of the cylinder.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d{0, 0, 0}));
// The query point lies just inside the cap of the cylinder.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d{0, 0, -0.1 * sign},
true /* is inside */));
// The query point lies outside, but *on* the central axis.
EXPECT_TRUE(
tester.Test(Vector3d{0, 0, cylinder.lz / 2}, Vector3d{0, 0, 1.5}));
EXPECT_TRUE(
tester.Test(Vector3d{0, 0, cylinder.lz / 2}, Vector3d{0, 0, 0}));
EXPECT_TRUE(tester.Test(Vector3d{0, 0, cylinder.lz / 2},
Vector3d{0, 0, -0.1}, true /* is inside */));
}
}
// Case: Nearest point is on circular rim.
{
// The dimensions of the boundary of the cylinder. Used to find the
// "support" point on the cylinder rim in an arbitrary direction.
const Vector3d dim{cylinder.radius, cylinder.radius, cylinder.lz / 2};
// Test top and bottom rims.
for (double sign : {-1, 1}) {
// Put the nearest point on the rim at some arbitrary, non-trivial angle.
const double theta = M_PI / 7;
const Vector3d p_NQ_G{std::cos(theta), std::sin(theta), sign};
const Vector3d p_GN_G = p_NQ_G.cwiseProduct(dim);
// The query point lies outside the cylinder.
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
// The query point lies on the cap of the cylinder.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d::Zero()));
// A query point *inside* the cylinder would not be nearest the rim.
}
}
// Case: Nearest point is on the barrel.
{
// The dimensions of the cylinder -- note it's shorter to make sure that the
// query points are well within the region of the barrel.
const Vector3d dim{cylinder.radius, cylinder.radius, cylinder.lz / 4};
// Test upper- and lower-halves of the barrel.
for (double sign : {-1, 1}) {
// Put the nearest point on the rim at some arbitrary, non-trivial angle.
const double theta = M_PI / 7;
const double cos_theta = std::cos(theta);
const double sin_theta = std::sin(theta);
const Vector3d p_NQ_G{cos_theta, sin_theta, 0};
const Vector3d p_GN_G =
dim.cwiseProduct(Vector3d{cos_theta, sin_theta, sign});
// The query point lies outside the cylinder.
EXPECT_TRUE(tester.Test(p_GN_G, p_NQ_G));
// The query point lies on the barrel of the cylinder.
EXPECT_TRUE(tester.Test(p_GN_G, Vector3d::Zero()));
// The query point lies inside the cylinder.
EXPECT_TRUE(tester.Test(p_GN_G, -0.1 * p_NQ_G, true /* is inside */));
}
}
}
#endif
// Helper functions to indicate expectation on whether I get a distance result
// based on scalar type and fcl shape.
template <typename T, typename S>
int ExpectedResult() {
if constexpr (std::is_same_v<T, symbolic::Expression>) {
return 0;
}
return 1;
}
template <>
int ExpectedResult<AutoDiffXd, fcl::Cylinderd>() {
return 0;
}
template <>
int ExpectedResult<AutoDiffXd, fcl::Ellipsoidd>() {
return 0;
}
template <typename T>
void TestScalarShapeSupport() {
// Configure the basic query.
Vector3<T> p_WQ{10, 10, 10};
RigidTransform<T> X_WQ{Translation3<T>{p_WQ}};
auto point_geometry = make_shared<fcl::Sphered>(0);
const GeometryId point_id = GeometryId::get_new_id();
fcl::CollisionObjectd query_point(point_geometry);
query_point.setTranslation(convert_to_double(p_WQ));
EncodedData encoding(point_id, true);
encoding.write_to(&query_point);
std::vector<SignedDistanceToPoint<T>> distances;
double threshold = std::numeric_limits<double>::max();
const GeometryId other_id = GeometryId::get_new_id();
std::unordered_map<GeometryId, RigidTransform<T>> X_WGs{
{point_id, X_WQ}, {other_id, RigidTransform<T>::Identity()}};
CallbackData<T> data{&query_point, threshold, p_WQ, &X_WGs, &distances};
// The Drake-supported geometries (minus Mesh which isn't supported by
// ProximityEngine yet).
auto run_callback = [&query_point, &threshold, &distances, &data, other_id](
auto geometry_shared_ptr) {
// Note: the `threshold` value gets reset by invoking Callback(). So, we
// need to reset it each time.
threshold = std::numeric_limits<double>::max();
distances.clear();
fcl::CollisionObjectd object(geometry_shared_ptr);
EncodedData object_encoding(other_id, true);
object_encoding.write_to(&object);
Callback<T>(&query_point, &object, &data, threshold);
};
// Box
{
run_callback(make_shared<fcl::Boxd>(1.0, 2.0, 3.5));
EXPECT_EQ(distances.size(), (ExpectedResult<T, fcl::Boxd>()));
}
// Capsule
{
run_callback(make_shared<fcl::Capsuled>(1.0, 2.0));
EXPECT_EQ(distances.size(), (ExpectedResult<T, fcl::Capsuled>()));
}
// Cylinder
{
run_callback(make_shared<fcl::Cylinderd>(1.0, 2.0));
EXPECT_EQ(distances.size(), (ExpectedResult<T, fcl::Cylinderd>()));
}
// Ellipsoid
{ run_callback(make_shared<fcl::Ellipsoidd>(1.5, 0.7, 3));
EXPECT_EQ(distances.size(), (ExpectedResult<T, fcl::Ellipsoidd>()));
}
// HalfSpace
{
run_callback(make_shared<fcl::Halfspaced>(Vector3d::UnitZ(), 0));
EXPECT_EQ(distances.size(), (ExpectedResult<T, fcl::Halfspaced>()));
}
// Sphere
{
run_callback(make_shared<fcl::Sphered>(1.0));
EXPECT_EQ(distances.size(), (ExpectedResult<T, fcl::Sphered>()));
}
// Convex
// TODO(SeanCurtis-TRI): Add convex that is *not* supported; create a small
// utility test to generate a tetrahedron.
}
// This test simply confirms which scalar-shape combinations produce answers
// and which don't. That culling takes place at the Callback level so that is
// what is exercised here. Lack of support is signaled by no distance data
// being returned.
GTEST_TEST(DistanceToPoint, ScalarShapeSupportDouble) {
TestScalarShapeSupport<double>();
}
// The autodiff version of DistanceToPoint.ScalarShapeSupportDouble.
GTEST_TEST(DistanceToPoint, ScalarShapeSupportAutoDiff) {
TestScalarShapeSupport<AutoDiffXd>();
}
GTEST_TEST(DistanceToPoint, ScalarShapeSupportExpression) {
TestScalarShapeSupport<symbolic::Expression>();
}
} // namespace
} // namespace point_distance
} // namespace internal
} // namespace geometry
} // namespace drake