-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
obb_test.cc
1302 lines (1199 loc) · 57.3 KB
/
obb_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
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "drake/geometry/proximity/obb.h"
#include <fmt/format.h>
#include <gtest/gtest.h>
#include "drake/common/test_utilities/eigen_matrix_compare.h"
#include "drake/geometry/proximity/aabb.h"
#include "drake/geometry/proximity/make_box_mesh.h"
#include "drake/geometry/proximity/make_ellipsoid_mesh.h"
#include "drake/geometry/proximity/make_sphere_mesh.h"
#include "drake/geometry/proximity/triangle_surface_mesh.h"
#include "drake/geometry/proximity/volume_mesh.h"
#include "drake/geometry/shape_specification.h"
namespace drake {
namespace geometry {
namespace internal {
using Eigen::AngleAxisd;
using Eigen::Matrix3d;
using Eigen::Vector3d;
using math::RigidTransformd;
using math::RollPitchYawd;
using math::RotationMatrixd;
// Friend class for accessing Obb's private functionality.
class ObbTester : public ::testing::Test {
public:
static constexpr double kTolerance = Obb::kTolerance;
};
// Friend class for accessing ObbMaker's private functionality for testing.
template <typename MeshType>
class ObbMakerTester {
public:
ObbMakerTester(const MeshType& mesh_M, const std::set<int>& vertices)
: obb_maker_(mesh_M, vertices) {}
RotationMatrixd CalcOrientationByPca() const {
return obb_maker_.CalcOrientationByPca();
}
Obb CalcOrientedBox(const math::RotationMatrixd& R_MB) const {
return obb_maker_.CalcOrientedBox(R_MB);
}
// CalcVolumeGradient() is also tested as a part of OptimizeObbVolume().
Obb OptimizeObbVolume(const Obb& box) const {
return obb_maker_.OptimizeObbVolume(box);
}
private:
ObbMaker<MeshType> obb_maker_;
};
namespace {
// Verifies the order of eigenvalues from Eigen::SelfAdjointEigenSolver in
// all representative cases. We are verifying that an Eigen utility works as
// advertised (rather than testing some of our own code) because:
//
// 1. There is no standard choice of the order (increasing or decreasing) of
// eigenvalues among different linear-algebra libraries. Eigen library
// chooses the increasing order.
// 2. Our CalcOrientationByPca() promises to return principal components,
// which, by definition:
// https://en.wikipedia.org/wiki/Principal_component_analysis
// must start from the most important component, i.e., the largest
// dimension must be in Bx direction of the OBB's frame B, and the smallest
// dimension must be in Bz direction.
// 3. Our code relies on the consistent order of eigenvalues from Eigen, so
// we can fulfill the promise in 2. If an upgrade to Eigen library changes
// it, we want to know right away.
//
// Additionally, the concepts of ranks of covariance matrices and multiplicities
// of their eigenvalues could be perplexing to engineers (including this
// author), and they can affect our applications. For example, multiplicity
// can cause an arbitrary choice of basis that leaves us with a poor OBB.
// Therefore, this unit test also demonstrates examples of these special cases.
//
// This table shows classification of the cases regarding ranks and
// multiplicities of eigenvalues of covariance matrices:
// -------+--------------------------
// | multiplicity
// | 1 2 3
// -------+--------------------------
// rank 3 | case 1 case 5 case 4
// 2 | case 2 case 3
// 1 | case 6
// 0 | case 7
// -------+--------------------------
// multiplicity 1 = all 3 eigenvalues are distinct.
// multiplicity 2 = two repeated eigenvalues + 1 unique eigenvalue.
// multiplicity 3 = all 3 eigenvalues are the same.
//
// Examples:
// case 1: Vertices of a box of different sizes in each dimension.
// case 2: Vertices of a rectangle (width ≠ height).
// case 3: Vertices of an equilateral triangle or a square.
// case 4: Vertices of a regular octahedron (and possibly other Platonic
// solids: tetrahedron, cube, dodecahedron, icosahedron).
// case 5: Take a regular octahedron and stretch (or shrink) it in one diagonal
// direction.
// case 6: Collinear points. (Two zero eigenvalues + one positive eigenvalue.)
// case 7: One single point. (All three eigenvalues are zero.)
//
// However, we only test cases 1, 2, 3, 4, 5 because they are most relevant
// to us. In the future, if cases 6 and 7 become important, we can add them.
GTEST_TEST(ObbMakerTest, VerifyEigenvalues) {
// Empirical tolerance from solving eigenvalue problems, about 2e-14. You
// might have to change it if you change the examples.
const double kEps = 100.0 * std::numeric_limits<double>::epsilon();
// In all cases, points are measured and expressed in frame M,
// and C is the centroid (average) of the points.
// Case 1: (rank 3, multiplicity 1).
// Covariance matrix of 8 vertices of the box [-1,-1]x[-2,2]x[-3,3]
// has distinct eigenvalues in increasing order: 1, 2², 3².
{
Matrix3d covariance_M = Matrix3d::Zero();
for (const double x : {-1., -1.}) {
for (const double y : {-2., 2.}) {
for (const double z : {-3., 3.}) {
// The centroid C of the points is at Mo, so p_CV_M = p_MV = (x,y,z).
const Vector3d p_CV_M(x, y, z);
covariance_M += p_CV_M * p_CV_M.transpose();
}
}
}
covariance_M /= 8.;
Eigen::SelfAdjointEigenSolver<Matrix3d> es;
es.computeDirect(covariance_M);
EXPECT_NEAR(es.eigenvalues()[0], 1., kEps);
EXPECT_NEAR(es.eigenvalues()[1], 4., kEps);
EXPECT_NEAR(es.eigenvalues()[2], 9., kEps);
}
// Case 2: (rank 2, multiplicity 1).
// Covariance matrix of 4 vertices of the rectangle [-1,-1]x[-2,-2]x{0}
// has distinct eigenvalues: 0, 1, 2². The least value is 0 because the
// points are co-planar.
{
Matrix3d covariance_M = Matrix3d::Zero();
for (const double x : {-1., -1.}) {
for (const double y : {-2., 2.}) {
// The centroid C of the points is at Mo, so p_CV_M = p_MV = (x,y,0).
const Vector3d p_CV_M(x, y, 0);
covariance_M += p_CV_M * p_CV_M.transpose();
}
}
covariance_M /= 4.;
Eigen::SelfAdjointEigenSolver<Matrix3d> es;
es.computeDirect(covariance_M);
EXPECT_NEAR(es.eigenvalues()[0], 0., kEps);
EXPECT_NEAR(es.eigenvalues()[1], 1., kEps);
EXPECT_NEAR(es.eigenvalues()[2], 4., kEps);
}
// Case 3: (rank 2, multiplicity 2).
// Covariance matrix of 3 vertices of an equilateral triangle {UnitX(),
// UnitY(), UnitZ()} has eigenvalues 0, 1/3, 1/3. The least value is 0
// because the points are co-planar.
{
Matrix3d covariance_M = Matrix3d::Zero();
Vector3d p_MC(1. / 3., 1. / 3., 1. / 3.);
for (const auto& p_MV :
{Vector3d::UnitX(), Vector3d::UnitY(), Vector3d::UnitZ()}) {
const Vector3d p_CV_M = p_MV - p_MC;
covariance_M += p_CV_M * p_CV_M.transpose();
}
covariance_M /= 3.;
Eigen::SelfAdjointEigenSolver<Matrix3d> es;
es.computeDirect(covariance_M);
EXPECT_NEAR(es.eigenvalues()[0], 0., kEps);
EXPECT_NEAR(es.eigenvalues()[1], 1. / 3., kEps);
EXPECT_NEAR(es.eigenvalues()[2], 1. / 3., kEps);
}
// Case 4: (rank 3, multiplicity 3).
// Covariance matrix of 6 vertices of a "unit" regular octahedron has one
// eigenvalue repeated three times: 2, 2, 2.
{
Matrix3d covariance_M = Matrix3d::Zero();
// Use std::vector<> here to instruct compiler to convert every entry to
// Vector3d. Strictly speaking, Vector3d and -Vector3d have different
// types.
for (const Vector3d& p_MV : std::vector<Vector3d>{
Vector3d::UnitX(), Vector3d::UnitY(), Vector3d::UnitZ(),
-Vector3d::UnitX(), -Vector3d::UnitY(), -Vector3d::UnitZ()}) {
// The centroid C is at Mo, so we can use p_MV for p_CV_M.
covariance_M += p_MV * p_MV.transpose();
}
covariance_M / 6.;
Eigen::SelfAdjointEigenSolver<Matrix3d> es;
es.computeDirect(covariance_M);
EXPECT_NEAR(es.eigenvalues()[0], 2., kEps);
EXPECT_NEAR(es.eigenvalues()[1], 2., kEps);
EXPECT_NEAR(es.eigenvalues()[2], 2., kEps);
}
// Case 5: (rank 3, multiplicity 2).
// Stretching the previous example 3X in Mx direction, its covariance matrix
// has one eigenvalue repeated two times followed by the largest
// eigenvalue: 2, 2, 2²*2.
{
Matrix3d covariance_M = Matrix3d::Zero();
// Use std::vector<> here to instruct compiler to convert every entry to
// Vector3d. Strictly speaking, Vector3d and -Vector3d have different
// types.
for (const Vector3d& p_MV : std::vector<Vector3d>{
2. * Vector3d::UnitX(), Vector3d::UnitY(), Vector3d::UnitZ(),
-2. * Vector3d::UnitX(), -Vector3d::UnitY(), -Vector3d::UnitZ()}) {
// The centroid C is at Mo, so we can use p_MV for p_CV_M.
covariance_M += p_MV * p_MV.transpose();
}
covariance_M / 6.;
Eigen::SelfAdjointEigenSolver<Matrix3d> es;
es.computeDirect(covariance_M);
EXPECT_NEAR(es.eigenvalues()[0], 2., kEps);
EXPECT_NEAR(es.eigenvalues()[1], 2., kEps);
EXPECT_NEAR(es.eigenvalues()[2], 8., kEps);
}
}
// Test fixture class for testing PCA with a unique solution up to sign flips,
// so it is invariant under rigid transform modulo sign-flips. We use a
// rectangular box that is not uniform (width < depth < height).
class ObbMakerTestRectangularBox : public ::testing::Test {
public:
ObbMakerTestRectangularBox()
: ::testing::Test(),
// Resolution hint 10 guarantees that the box mesh will be the
// coarsest possible mesh with only 8 vertices and 12 triangles.
mesh_M_(MakeBoxSurfaceMesh<double>(Box(2, 4, 6), 10)) {}
void SetUp() override {
ASSERT_EQ(mesh_M_.num_vertices(), 8);
for (int i = 0; i < mesh_M_.num_vertices(); ++i) {
test_vertices_.insert(i);
}
}
protected:
TriangleSurfaceMesh<double> mesh_M_;
std::set<int> test_vertices_;
};
// Test the creation of obb orientation via PCA. We want to test the following:
// 1. The resulting basis is right-handed.
// 2. The basis is, in some sense, invariant to rigid transformations.
// 3. The function will return a predictable basis.
TEST_F(ObbMakerTestRectangularBox, CalcOrientationByPca) {
// B is the frame of PCA solution for the oriented bounding box. We will
// check it against the tested box's frame M.
const RotationMatrixd R_MB =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh_M_, test_vertices_)
.CalcOrientationByPca();
const Vector3d Mx_M = Vector3d::UnitX();
const Vector3d My_M = Vector3d::UnitY();
const Vector3d Mz_M = Vector3d::UnitZ();
const Vector3d& Bx_M = R_MB.col(0);
const Vector3d& By_M = R_MB.col(1);
const Vector3d& Bz_M = R_MB.col(2);
// Empirical tolerance for comparing unit vectors from characteristic
// equation AX = λX and rigid transform.
double kEps = 100. * std::numeric_limits<double>::epsilon();
// Simple analysis:
// 1. The box of size 2x4x6 has the half-width vector (1,2,3).
// 2. The covariance matrix of its six vertices has distinct
// eigenvalues 1, 2², 3².
// 3. PCA solution is unique up to sign flips.
// 4. The principal directions Bx, By, Bz are along Mz, My, Mx of the
// tested box because PCA directions are sorted in decreasing order of size
// (the box is longest in its Mz direction).
//
// Both principal direction D and -D are equivalent in PCA, so we allow two
// possibilities in each check below. (If V is an eigenvector of AX=λX, so
// is -V.)
//
// We use the box dimensions in Mx,My,Mz in increasing order, but
// the principal components are always in the order of decreasing variance.
EXPECT_TRUE(CompareMatrices(Bx_M, Mz_M, kEps) ||
CompareMatrices(Bx_M, -Mz_M, kEps));
EXPECT_TRUE(CompareMatrices(By_M, My_M, kEps) ||
CompareMatrices(By_M, -My_M, kEps));
EXPECT_TRUE(CompareMatrices(Bz_M, Mx_M, kEps) ||
CompareMatrices(Bz_M, -Mx_M, kEps));
// Confirm the basis is right-handed.
EXPECT_TRUE(R_MB.IsValid());
// Apply rigid transform X_FM, and test that the PCA solution is invariant
// under rigid transform (modulo sign flips) from frame M to frame F. We can
// do this because this example's PCA solution is unique up to sign flips.
const RotationMatrixd R_FM(
RollPitchYawd(2. * M_PI / 3., M_PI / 4., M_PI / 5.));
const RigidTransformd X_FM(R_FM, Vector3d(-0.2, 2, 1.5));
mesh_M_.TransformVertices(X_FM);
// Now we use alias mesh_F because `mesh_M_` has been transformed; its stored
// vertices are now measured and expressed in frame F.
const TriangleSurfaceMesh<double>& mesh_F = mesh_M_;
const RotationMatrixd R_FB =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh_F, test_vertices_)
.CalcOrientationByPca();
const Vector3d& Bx_F = R_FB.col(0);
const Vector3d& By_F = R_FB.col(1);
const Vector3d& Bz_F = R_FB.col(2);
// Check components of the new orientation R_FB. Allow sign flip for the
// same reason as before.
EXPECT_TRUE(CompareMatrices(Bx_F, R_FM * Bx_M, kEps) ||
CompareMatrices(Bx_F, -(R_FM * Bx_M), kEps));
EXPECT_TRUE(CompareMatrices(By_F, R_FM * By_M, kEps) ||
CompareMatrices(By_F, -(R_FM * By_M), kEps));
EXPECT_TRUE(CompareMatrices(Bz_F, R_FM * Bz_M, kEps) ||
CompareMatrices(Bz_F, -(R_FM * Bz_M), kEps));
// Confirm the basis is right-handed.
EXPECT_TRUE(R_FB.IsValid());
}
// The purpose of this test fixture is:
// 1. Ensures test coverage of ObbMaker::CalcOrientationByPca() and
// ObbMaker::CalcOrientedBox().
// 2. Tests an example with non-unique PCA solution with eigenvalue of
// multiplicity 2.
//
// We use an equilateral triangle whose vertices are at unit distance from the
// origin along each axis of frame M like this:
//
// Mz
// |
// v2
// |
// |
// +-----v1--My
// /
// /
// v0
// /
// Mx
//
class ObbMakerTestTriangle : public ::testing::Test {
public:
ObbMakerTestTriangle()
: ::testing::Test(),
mesh_M_({{0, 1, 2}},
{Vector3d::UnitX(), Vector3d::UnitY(), Vector3d::UnitZ()}),
test_vertices_{0, 1, 2} {}
protected:
TriangleSurfaceMesh<double> mesh_M_;
const std::set<int> test_vertices_;
};
TEST_F(ObbMakerTestTriangle, CalcOrientationByPca) {
// B is the frame of PCA solution for the oriented bounding box. We will check
// it against the mesh's frame M.
const RotationMatrixd R_MB =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh_M_, test_vertices_)
.CalcOrientationByPca();
const Vector3d Bx_M = R_MB.col(0);
const Vector3d By_M = R_MB.col(1);
const Vector3d Bz_M = R_MB.col(2);
// Simple analysis of this example:
// 1. Covariance matrix has eigenvalues 0, 1/3, 1/3.
// 2. The largest eigenvalue 1/3 with multiplicity 2 indicates that the first
// two principal components span the plane of the triangle without a
// preferred line of direction. PCA solution is arbitrary; it can be any
// two basis vectors of the plane of the triangle.
// 3. The smallest eigenvalue 0 means there is no variation left in the last
// principal component. It makes sense because there is no other points
// outside the plane of the triangle.
//
// We can only expect that Bx and By span the plane of triangle without
// being more specific. As a result, we only check that Bz is ±N, where N
// is the normal vector of the triangle.
const Vector3d N_M = Vector3d(1, 1, 1).normalized();
// Empirical tolerance for comparing unit vectors from characteristic
// equation AX = λX and rigid transform.
double kEps = 100. * std::numeric_limits<double>::epsilon();
EXPECT_TRUE(CompareMatrices(Bz_M, N_M, kEps) ||
CompareMatrices(Bz_M, -N_M, kEps));
// Apply rigid transform X_FM, and show that the particular PCA solution that
// we have is **not** invariant under rigid transform from frame M to frame F.
// This is because the PCA solution is not unique.
const RotationMatrixd R_FM(
RollPitchYawd(2. * M_PI / 3., M_PI / 4., M_PI / 5.));
const RigidTransformd X_FM(R_FM, Vector3d(-0.2, 2, 1.5));
mesh_M_.TransformVertices(X_FM);
// Now we use alias mesh_F because `mesh_M_` has been transformed; its stored
// vertices are now measured and expressed in frame F.
const TriangleSurfaceMesh<double>& mesh_F = mesh_M_;
const RotationMatrixd R_FB =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh_F, test_vertices_)
.CalcOrientationByPca();
const Vector3d& Bx_F = R_FB.col(0);
const Vector3d& By_F = R_FB.col(1);
const Vector3d& Bz_F = R_FB.col(2);
// Check that Bz_F is the transform of Bz_M. It's the only unique
// principal component (eigenvalue 0 without multiplicity).
EXPECT_TRUE(CompareMatrices(Bz_F, R_FM * Bz_M, kEps) ||
CompareMatrices(Bz_F, -(R_FM * Bz_M), kEps));
// The other two principal components are not unique. In our current
// implementation, we do not get the invariance under rigid transform. If
// we change PCA implementation, we might have to change these checks.
EXPECT_FALSE(CompareMatrices(Bx_F, R_FM * Bx_M, kEps) ||
CompareMatrices(Bx_F, -(R_FM * Bx_M), kEps));
EXPECT_FALSE(CompareMatrices(By_F, R_FM * By_M, kEps) ||
CompareMatrices(By_F, -(R_FM * By_M), kEps));
}
TEST_F(ObbMakerTestTriangle, CalcOrientedBox) {
// clang-format off
const RotationMatrixd R_MB =
RotationMatrixd::MakeFromOrthonormalColumns(
Vector3d(1., -0.5, -0.5).normalized(),
Vector3d(0., 1., -1.).normalized(),
Vector3d(1., 1., 1.).normalized());
// clang-format on
const Obb obb =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh_M_, test_vertices_)
.CalcOrientedBox(R_MB);
// Check the input R_MB passes to the output obb exactly.
EXPECT_TRUE(obb.pose().rotation().IsExactlyEqualTo(R_MB));
// Check the output center of the box. Note that the center(1/2,1/4,1/4) of
// the box is *not* the centroid(1/3,1/3,1/3) of the triangle.
const Vector3d expect_p_MB(0.5, 0.25, 0.25);
EXPECT_TRUE(CompareMatrices(obb.center(), expect_p_MB,
std::numeric_limits<double>::epsilon()));
// Check the output half-width vector. The extra tolerance term was
// introduced by padding (see Obb::PadBoundary()).
const Vector3d expect_half_width =
Vector3d(std::sqrt(6.) / 4, 1. / std::sqrt(2.), 0.) +
Vector3d::Constant(ObbTester::kTolerance);
EXPECT_TRUE(CompareMatrices(obb.half_width(), expect_half_width,
std::numeric_limits<double>::epsilon()));
}
// Returns true iff the oriented bounding box contains all the specified
// vertices in the mesh.
template <typename MeshType>
bool Contain(const Obb& obb_M, const MeshType& mesh_M,
const std::set<int>& vertices) {
const RigidTransformd& X_MB = obb_M.pose();
const RigidTransformd X_BM = X_MB.inverse();
for (int v : vertices) {
Vector3d p_MV = mesh_M.vertex(v);
Vector3d p_BV = X_BM * p_MV;
if ((p_BV.array() > obb_M.half_width().array()).any()) {
return false;
}
if ((p_BV.array() < -obb_M.half_width().array()).any()) {
return false;
}
}
return true;
}
GTEST_TEST(ObbMakerTest, TestOptimizeObbVolume) {
// Create an octahedron that is anisotropic (different lengths in different
// axes) via a coarse mesh of an ellipsoid. Then, transform vertices to
// a general pose for testing robustness.
// The ellipsoid's canonical frame is E, and the general frame is M.
const RigidTransformd X_ME(
RotationMatrixd(RollPitchYawd(2. * M_PI / 3., M_PI / 4., M_PI / 5.)),
Vector3d(-0.2, 2, 1.5));
// The first line calls it mesh_M even though it's actually mesh_E. The
// second line correctly makes it mesh_M.
TriangleSurfaceMesh<double> mesh_M =
MakeEllipsoidSurfaceMesh<double>(Ellipsoid(1., 2., 3.), 6);
mesh_M.TransformVertices(X_ME);
// Confirm that it is an octahedron.
ASSERT_EQ(8, mesh_M.num_triangles());
ASSERT_EQ(6, mesh_M.num_vertices());
const std::set<int> test_vertices{0, 1, 2, 3, 4, 5};
// Initial obb is aligned with the three axes of the ellipsoid.
const Obb initial_obb_M(X_ME, Vector3d(1., 2., 3.));
const double initial_volume = initial_obb_M.CalcVolume();
// The constructor Obb() already did padding for us. No need to pass
// tolerance for numerics.
ASSERT_TRUE(Contain(initial_obb_M, mesh_M, test_vertices));
const Obb optimized_obb_M =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh_M, test_vertices)
.OptimizeObbVolume(initial_obb_M);
EXPECT_TRUE(Contain(optimized_obb_M, mesh_M, test_vertices));
const double percent_improvement =
100. * (initial_volume - optimized_obb_M.CalcVolume()) / initial_volume;
// Empirically we saw about 22% improvement for this particular example.
// This check make sure the future code will not degrade this optimization.
EXPECT_GT(percent_improvement, 22.0);
// Now repeat the test, but with a mesh that is so small that there's no point
// in optimizing it. The resulting "optimized" Obb should be identical to the
// original Obb.
// The whole mesh is much smaller than 1mm -- that should be sufficient.
TriangleSurfaceMesh<double> mesh2_M =
MakeEllipsoidSurfaceMesh<double>(Ellipsoid(1e-5, 2e-5, 3e-5), 6);
mesh2_M.TransformVertices(X_ME);
const Obb initial_obb2_M(X_ME, Vector3d(1e-5, 2e-5, 3e-5));
const Obb optimized_obb2_M =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh2_M, test_vertices)
.OptimizeObbVolume(initial_obb2_M);
// The pose and half_width will be bit identical as evidence that no work was
// done when the volume would not change appreciably.
EXPECT_TRUE(CompareMatrices(initial_obb2_M.half_width(),
optimized_obb2_M.half_width()));
EXPECT_TRUE(CompareMatrices(initial_obb2_M.pose().GetAsMatrix34(),
optimized_obb2_M.pose().GetAsMatrix34()));
}
// TODO(DamrongGuoy): Update the tests when we improve the algorithm
// for computing oriented bounding boxes.
// Test fixture for another corner case to show surprising results
// when a point set has its covariance matrix with triple multiplicity.
// Not only the OBB orientation from PCA can be arbitrary in all three
// directions, but also the post-optimization OBBs can be very different too.
// We use the vertex set of a regular octahedron. There are two special
// behaviors to demonstrate using this test fixture:
// 1. CalcOrientationByPca() can give arbitrary orientations in all three
// directions under a rigid transform.
// 2. ObbMaker.Compute() can give very different volumes of oriented bounding
// boxes under rigid transform, i.e., our algorithm is not always optimal.
class ObbMakerTestOctahedron : public ::testing::Test {
public:
ObbMakerTestOctahedron()
: ::testing::Test(),
// Use a coarse sphere, i.e. an octahedron, as the underlying mesh.
mesh_M_(MakeSphereSurfaceMesh<double>(Sphere(1.5), 3)),
test_vertices_{0, 1, 2, 3, 4, 5} {}
void SetUp() override { ASSERT_EQ(mesh_M_.num_vertices(), 6); }
protected:
TriangleSurfaceMesh<double> mesh_M_;
const std::set<int> test_vertices_;
};
// Test PCA problem whose eigenvalue has triple multiplicity.
TEST_F(ObbMakerTestOctahedron, CalcOrientationByPca) {
// B is the frame of PCA solution for the oriented bounding box. We will check
// it against the mesh's frame M.
const RotationMatrixd R_MB =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh_M_, test_vertices_)
.CalcOrientationByPca();
const Vector3d Bx_M = R_MB.col(0);
const Vector3d By_M = R_MB.col(1);
const Vector3d Bz_M = R_MB.col(2);
// Analysis of this example:
// 1. Covariance matrix has eigenvalue 0.75 with multiplicity 3
// (det(A-λI) = (0.75-λ)³ = 0), so PCA solution is arbitrary.
// 2. Mathematically the solution can be any orthonormal basis of ℝ³. The
// octahedron has no preferred direction measured by sum of squared
// distance.
// 3. The result from CalcOrientationByPca() is deterministic but likely
// unstable under rigid transform.
//
// We will apply a rigid transform from frame M to frame F below and show that
// we will get inconsistent PCA solution.
const RotationMatrixd R_FM(
RollPitchYawd(2. * M_PI / 3., M_PI / 4., M_PI / 5.));
const RigidTransformd X_FM(R_FM, Vector3d(-0.2, 2, 1.5));
mesh_M_.TransformVertices(X_FM);
// Now we use alias mesh_F because `mesh_M_` has been transformed; its stored
// vertices are now measured and expressed in frame F.
const TriangleSurfaceMesh<double>& mesh_F = mesh_M_;
const RotationMatrixd R_FB =
ObbMakerTester<TriangleSurfaceMesh<double>>(mesh_F, test_vertices_)
.CalcOrientationByPca();
const Vector3d& Bx_F = R_FB.col(0);
const Vector3d& By_F = R_FB.col(1);
const Vector3d& Bz_F = R_FB.col(2);
// Empirical tolerance for comparing unit vectors from characteristic
// equation AX = λX and rigid transform.
double kEps = 100. * std::numeric_limits<double>::epsilon();
EXPECT_FALSE(CompareMatrices(Bx_F, R_FM * Bx_M, kEps) ||
CompareMatrices(Bx_F, -(R_FM * Bx_M), kEps));
EXPECT_FALSE(CompareMatrices(By_F, R_FM * By_M, kEps) ||
CompareMatrices(By_F, -(R_FM * By_M), kEps));
EXPECT_FALSE(CompareMatrices(Bz_F, R_FM * Bz_M, kEps) ||
CompareMatrices(Bz_F, -(R_FM * Bz_M), kEps));
}
// Test computing oriented bounding boxes of an octahedron in a standard pose
// and in a general pose, and show that the two cases have very different
// volumes, i.e., our algorithm is not always optimal.
//
// It is not easy to predict the pose and size of the expected bounding box,
// so we check the result indirectly in two steps:
// 1. Check that the box contains all the specified vertices.
// 2. Check that the box has volume around an empirically determined threshold.
//
// We also show that our algorithm is not always optimal. We use the same
// octahedron at two different poses and get two boxes with very different
// sizes.
TEST_F(ObbMakerTestOctahedron, ObbMakerCompute) {
const Obb obb_M = ObbMaker(mesh_M_, test_vertices_).Compute();
EXPECT_TRUE(Contain(obb_M, mesh_M_, test_vertices_));
// The threshold was set empirically . It is slightly smaller than the
// trivial upper bound 27.0, which is the volume of a cube bounding the
// sphere. It is possible to do better as shown in the next test below.
EXPECT_NEAR(obb_M.CalcVolume(), 26.997, 0.001);
// The covariance matrix of the test vertices has one single eigenvalue with
// multiplicity 3, so mathematically the PCA solution is arbitrary; any
// arbitrary orthonormal basis spanning ℝ³ is a valid solution.
// Computationally, the solution for this example is unstable. A rigid
// transform from frame M to frame F below can perturb the new solution
// significantly. As a result, the OBB's volume will shrink about 66%.
const RigidTransformd X_MF(
RotationMatrixd(RollPitchYawd(M_PI / 6., M_PI / 3., M_PI / 5.)),
Vector3d(-0.2, 2, 1.5));
mesh_M_.TransformVertices(X_MF);
// Now we use alias mesh_F because `mesh_M_` has been transformed; its stored
// vertices are now measured and expressed in frame F.
const TriangleSurfaceMesh<double>& mesh_F = mesh_M_;
const Obb obb_F = ObbMaker(mesh_F, test_vertices_).Compute();
EXPECT_TRUE(Contain(obb_F, mesh_F, test_vertices_));
// The threshold was set empirically. It is much smaller (about 9.7 m³)
// than the previous test before vertex transform (about 27 m³).
// We use about 10-percent tolerance (0.9 m³) because the numerical
// optimization varies among different computing platforms.
EXPECT_NEAR(obb_F.CalcVolume(), 9.741, 0.9);
// Empirical tolerance for comparing unit vectors from characteristic
// equation AX = λX and rigid transform.
double kEps = 100. * std::numeric_limits<double>::epsilon();
EXPECT_FALSE(CompareMatrices(obb_F.pose().GetAsMatrix4(),
(X_MF * obb_M.pose()).GetAsMatrix4(), kEps));
}
// TODO(DamrongGuoy): Update or remove this test if we improve the algorithm.
// Currently our oriented box can be worse than an axis-aligned box.
// Show that our OBB algorithm can do significantly worse than AABB.
// PCA created an initial orientation that made the OBB volume larger than
// the original AABB, and our simple optimization couldn't improve it.
//
// We use vertices of a truncated rectangular box. From the 8 vertices of
// a 6x4x2 box, we omit two vertices of a diagonal and use only 6 vertices for
// testing. We have checked that the truncated box has three distinct eigen
// values of its covariance matrix (about 0.600, 3.633, and 9.762), so PCA
// solution is unique in this case.
GTEST_TEST(ObbMakerTest, TestTruncatedBox) {
// Resolution hint 10 is larger than the largest box dimension 6, so we
// should get the coarsest mesh: 8 vertices, 12 triangles.
auto surface_mesh = MakeBoxSurfaceMesh<double>(Box(6, 4, 2), 10);
ASSERT_EQ(surface_mesh.num_vertices(), 8);
ASSERT_EQ(surface_mesh.num_triangles(), 12);
std::set<int> test_vertices;
for (int i = 0; i < 8; ++i) {
const Vector3d& p_MV = surface_mesh.vertex(i);
// Omit vertices on a diagonal.
if (CompareMatrices(p_MV, Vector3d(-3, -2, -1), 1e-5)) continue;
if (CompareMatrices(p_MV, Vector3d(3, 2, 1), 1e-5)) continue;
test_vertices.insert(i);
}
ASSERT_EQ(test_vertices.size(), 6);
const Obb obb = ObbMaker(surface_mesh, test_vertices).Compute();
EXPECT_TRUE(Contain(obb, surface_mesh, test_vertices));
// The final basis is the same as the PCA basis -- optimization couldn't
// improve it which implies we're at a local optimum.
const RotationMatrixd R_MB =
ObbMakerTester<TriangleSurfaceMesh<double>>(surface_mesh, test_vertices)
.CalcOrientationByPca();
EXPECT_TRUE(CompareMatrices(R_MB.matrix(), obb.pose().rotation().matrix()));
// The original box of size 6x4x2 has volume 48.0. Our oriented bounding
// box is far from optimal. Its volume is larger than the original box.
EXPECT_NEAR(obb.CalcVolume(), 82.281, 0.001);
// The original box has the half-width vector (3,2,1). Our OBB is almost 20%
// and 50% longer in width and depth.
EXPECT_TRUE(
CompareMatrices(obb.half_width(), Vector3d(3.552, 2.960, 0.977), 0.001));
}
// Smoke test that it works with VolumeMesh<double>.
GTEST_TEST(ObbMakerTest, TestVolumeMesh) {
// Use a very coarse mesh.
const VolumeMesh<double> volume_mesh = MakeEllipsoidVolumeMesh<double>(
Ellipsoid(1., 2., 3.), 6, TessellationStrategy::kSingleInteriorVertex);
std::set<int> test_vertices;
for (int i = 0; i < volume_mesh.num_vertices(); ++i) {
test_vertices.insert(i);
}
Obb obb = ObbMaker(volume_mesh, test_vertices).Compute();
EXPECT_TRUE(Contain(obb, volume_mesh, test_vertices));
// The threshold was set empirically. It is slightly smaller than the
// trivial upper bound 48.0, which is the volume of the box of size 2x4x6
// that fits the ellipsoid. We put a check that the future code will not
// create a bigger bounding box.
EXPECT_LT(obb.CalcVolume(), 41.317);
}
// Tests API of ObbMaker that it respects the input vertex indices.
GTEST_TEST(ObbMakerTestAPI, ObbMakerCompute) {
const TriangleSurfaceMesh<double> mesh(
// The triangles are not relevant to the test.
{{0, 1, 2}, {0, 3, 1}}, {Vector3d::Zero(), Vector3d::UnitX(),
2. * Vector3d::UnitY(), 3. * Vector3d::UnitZ()});
const std::set<int> test_vertices{0, 1};
const Obb obb = ObbMaker(mesh, test_vertices).Compute();
// The mesh has four vertices, but we use only two vertices for testing.
// Therefore, we expect the OBB to contain only the two testing vertices
// but not the remaining vertices of the mesh.
EXPECT_TRUE(Contain(obb, mesh, test_vertices));
EXPECT_NEAR(obb.half_width().x(), 0.5, ObbTester::kTolerance);
EXPECT_NEAR(obb.half_width().y(), 0., ObbTester::kTolerance);
EXPECT_NEAR(obb.half_width().z(), 0., ObbTester::kTolerance);
const std::set<int> remaining_vertices{2, 3};
EXPECT_FALSE(Contain(obb, mesh, remaining_vertices));
}
// Tests calculating the bounding box volume. Due to boundary padding, the
// volume is increased from 8abc to 8((a + ε)*(b + ε)*(c+ε)), i.e.:
// 8[abc + (ab + bc + ac)ε + (a + b + c)ε² + ε³].
TEST_F(ObbTester, TestVolume) {
Obb obb(RigidTransformd(Vector3d(-1, 2, 1)), Vector3d(2, 0.5, 2.7));
// In this case the dominating error term is 8(ab + bc + ac)ε, which caps
// out under kTolerance * 70.
const double volume = obb.CalcVolume();
EXPECT_NEAR(volume, 21.6, kTolerance * 70);
EXPECT_GT(volume, 21.6);
Obb zero_obb(RigidTransformd(RotationMatrixd(RollPitchYawd(
M_PI / 6., M_PI / 3., M_PI / 5.)),
Vector3d(3, -4, 1.3)),
Vector3d(0, 0, 0));
// Since a, b and c are 0, only the ε³ term is left and kTolerance³ is
// within kTolerance.
const double zero_volume = zero_obb.CalcVolume();
EXPECT_NEAR(zero_volume, 0, kTolerance);
EXPECT_GT(zero_volume, 0);
}
// Tests padding the boundary of the bounding box volume.
TEST_F(ObbTester, TestPadBoundary) {
Obb obb(RigidTransformd(Vector3d(-1, 0.5, 1)), Vector3d(1.2, 2.5, 0.3));
Vector3d padded = Vector3d(1.2, 2.5, 0.3).array() + kTolerance;
EXPECT_TRUE(CompareMatrices(obb.half_width(), padded));
// Large boxes should have a bigger padding based on either the maximum
// half width or position in the frame.
const double padding = 300 * std::numeric_limits<double>::epsilon();
ASSERT_GT(padding, kTolerance);
// Max is set from half_width.z.
obb = Obb(RigidTransformd(Vector3d(-1, 1.5, 1)), Vector3d(120, 250, 300));
padded = Vector3d(120, 250, 300).array() + padding;
// Expect the two Vector3d to be exactly equal down to the last bit.
EXPECT_TRUE(CompareMatrices(obb.half_width(), padded));
// Max is set from |center.x|.
obb = Obb(RigidTransformd(
RotationMatrixd(RollPitchYawd(M_PI / 6., M_PI / 3., M_PI / 5.)),
Vector3d(-300, 50, 100)),
Vector3d(1, 2, 0.5));
padded = Vector3d(1, 2, 0.5).array() + padding;
// Expect the two Vector3d to be exactly equal down to the last bit.
EXPECT_TRUE(CompareMatrices(obb.half_width(), padded));
}
// We want to compute X_AB such that B is posed relative to A as documented in
// TestObbOverlap. We can do so by generating the rotation component, R_AB, such
// that Bq has a minimum value along the chosen axis, and we can solve for
// the translation component, p_AoBo_A = p_AoAf_A + p_AfBq_A + p_BqBo_A.
RigidTransformd CalcCornerTransform(const Obb& a, const Obb& b, const int axis,
const bool expect_overlap) {
const int axis1 = (axis + 1) % 3;
const int axis2 = (axis + 2) % 3;
// Construct the rotation matrix, R_AB, that has meaningful (non-zero)
// values everywhere for the remaining 2 axes and no symmetry.
const RotationMatrixd R_AB =
RotationMatrixd(AngleAxisd(M_PI / 5, Vector3d::Unit(axis1))) *
RotationMatrixd(AngleAxisd(-M_PI / 5, Vector3d::Unit(axis2)));
// We define p_BqBo in Frame A from box b's minimum corner Q to its center.
const Vector3d p_BqBo_A = R_AB * b.half_width();
// Reality check that the minimum corner and the center are strictly
// increasing along the given axis because we chose the rotation R_AB to
// support this property.
DRAKE_DEMAND(p_BqBo_A[axis] > 0.);
// We construct Bq to be a small relative offset either side of Af along the
// given A[axis], depending on whether we expect the boxes to overlap.
Vector3d p_AfBq_A{0, 0, 0};
p_AfBq_A[axis] = expect_overlap ? -0.01 : 0.01;
// We construct Af by taking the maximum corner and offsetting it along the
// remaining 2 axes, e.g. by a quarter across. This ensures we thoroughly
// exercise all bits instead of simply using any midpoints or corners.
//
// A[axis1]
// ^
// --------|--------
// | | |
// | | Af
// | | |
// | Ao------->A[axis]
// | |
// | |
// | |
// -----------------
//
Vector3d p_AoAf_A = a.half_width();
p_AoAf_A[axis1] /= 2;
p_AoAf_A[axis2] /= 2;
const Vector3d p_AoBo_A = p_AoAf_A + p_AfBq_A + p_BqBo_A;
return RigidTransformd(R_AB, p_AoBo_A);
}
// We want to compute X_AB such that B is posed relative to A as documented
// in TestObbOverlap. We can do so by generating the rotation component, R_AB,
// such that Bq lies on the minimum edge along the chosen axis, and we can solve
// for the translation component, p_AoBo_A = p_AoAf_A + p_AfBq_A + p_BqBo_A.
RigidTransformd CalcEdgeTransform(const Obb& a, const Obb& b, const int a_axis,
const int b_axis, const bool expect_overlap) {
const int a_axis1 = (a_axis + 1) % 3;
const int a_axis2 = (a_axis + 2) % 3;
const int b_axis1 = (b_axis + 1) % 3;
const int b_axis2 = (b_axis + 2) % 3;
// Construct a rotation matrix that has meaningful (non-zero) values
// everywhere for the remaining 2 axes and no symmetry. Depending on the
// combination of axes, we need to rotate around different axes to ensure
// the edge remains as the minimum.
RotationMatrixd R_AB;
const double theta = M_PI / 5;
// For cases Ax × Bx, Ay × By, and Az × Bz.
if (a_axis == b_axis) {
R_AB = RotationMatrixd(AngleAxisd(theta, Vector3d::Unit(b_axis1))) *
RotationMatrixd(AngleAxisd(theta, Vector3d::Unit(b_axis2)));
// For cases Ax × By, Ay × Bz, and Az × Bx.
} else if (a_axis1 == b_axis) {
R_AB = RotationMatrixd(AngleAxisd(theta, Vector3d::Unit(b_axis1))) *
RotationMatrixd(AngleAxisd(-theta, Vector3d::Unit(b_axis2)));
// For cases Ax × Bz, Ay × Bx, and Az × By.
} else {
R_AB = RotationMatrixd(AngleAxisd(-theta, Vector3d::Unit(b_axis2))) *
RotationMatrixd(AngleAxisd(theta, Vector3d::Unit(b_axis1)));
}
// We define p_BqBo in Frame B taking a point on the minimum edge aligned
// with the given axis, offset it to be without symmetry, then convert it
// to Frame A by applying the rotation.
Vector3d p_BqBo_B = b.half_width();
p_BqBo_B[b_axis] -= b.half_width()[b_axis] / 2;
const Vector3d p_BqBo_A = R_AB * p_BqBo_B;
// Reality check that the point Bq and the center Bo are strictly
// increasing along the remaining 2 axes because we chose the rotation R_AB
// to support this property.
DRAKE_DEMAND(p_BqBo_A[a_axis1] > 0);
DRAKE_DEMAND(p_BqBo_A[a_axis2] > 0);
// We construct Bq to be a small relative offset either side of Af along the
// given axis, depending on whether we expect the boxes to overlap.
Vector3d p_AfBq_A{0, 0, 0};
const double offset = expect_overlap ? -0.01 : 0.01;
p_AfBq_A[a_axis1] = offset;
p_AfBq_A[a_axis2] = offset;
// We construct Af by taking the maximum corner and offsetting it along the
// given edge to thoroughly exercise all bits.
Vector3d p_AoAf_A = a.half_width();
p_AoAf_A[a_axis] -= a.half_width()[a_axis] / 2;
Vector3d p_AoBo_A = p_AoAf_A + p_AfBq_A + p_BqBo_A;
// Finally we combine the components to form the transform X_AB.
return RigidTransformd(R_AB, p_AoBo_A);
}
// Tests whether OBBs overlap. We use *this* test to completely test the
// BoxesOverlap function. Therefore, there are 15 cases to test, each covering
// a separating axis between the two bounding boxes. The first 3 cases use the
// axes of Frame A, the next 3 cases use the axes of Frame B, and the remaining
// 9 cases use the axes defined by the cross product of axes from Frame A and
// Frame B. We also test that it is robust for the case of parallel boxes.
GTEST_TEST(ObbTest, TestObbOverlap) {
// Frame strategy. For the canonical frame A of box `a` and the
// canonical frame B of box `b`, we want to control the pose X_AB between
// the two boxes. However, we need to give HasOverlap() the pose X_GH
// between the hierarchy frames G and H to which box `a` and box `b`
// belong. Our strategy is to control the tests through X_AB and then compose
// X_GH as:
// X_GH = X_GA * X_AB * X_BH.
// Frame A of box `a` is arbitrarily posed in the hierarchy frame G.
const RigidTransformd X_GA{
RotationMatrixd(RollPitchYawd(2. * M_PI / 3., M_PI_4, -M_PI / 3.)),
Vector3d(1, 2, 3)};
// Frame B of box `b` is arbitrarily posed in the hierarchy frame H.
const RigidTransformd X_HB{
RotationMatrixd(RollPitchYawd(M_PI_4, M_PI / 5., M_PI / 6.)),
Vector3d(2, 0.5, 4)};
const RigidTransformd X_BH = X_HB.inverse();
// One box is fully contained in the other and they are parallel. We make
// them parallel by setting X_AB to identity.
RigidTransformd X_AB = RigidTransformd::Identity();
Obb a(X_GA, Vector3d(1, 2, 1));
Obb b(X_HB, Vector3d(0.5, 1, 0.5));
EXPECT_TRUE(Obb::HasOverlap(a, b, X_GA * X_AB * X_BH /* X_GH */));
// To cover the cases of the axes of Frame A, we need to pose box B along
// each axis. For example, in the case of the Ax-axis, in a 2D view they would
// look like:
// Ay
// ^
// --------|-------- ⋰ ⋱ ↗By
// | | | ⋰ ⋱ ↗
// | | Af Bq ↗⋱
// | | | ⋱ Bo ⋱
// | Ao--------->Ax ⋱ ↘ ⋱
// | | ⋱ ↘⋰
// | | ⋱⋰ ↘Bx
// | |
// -----------------
//
// Hy
// ⇑
// Gy Gx ⇑
// ⇖ ⇗ ⇑
// ⇖ ⇗ ⇑
// ⇖ ⇗ ⇑
// Go Ho ⇒ ⇒ ⇒ ⇒ ⇒ Hx
//
//
// For this test, we define Point Bq as the minimum corner of the box B (i.e.,
// center - half width). We want to pose box B so Bq is the uniquely closest
// point to box A at a Point Af in the interior of +Ax face. The rest of
// the box B extends farther along the +Ax axis (as suggested in the above
// illustration). Point Bq will be a small epsilon away from the nearby face
// either outside (if expect_overlap is false) or inside (if true).
a = Obb(X_GA, Vector3d(2, 4, 3));
b = Obb(X_HB, Vector3d(3.5, 2, 1.5));
for (int axis = 0; axis < 3; ++axis) {
X_AB = CalcCornerTransform(a, b, axis, false /* expect_overlap */);
EXPECT_FALSE(Obb::HasOverlap(a, b, X_GA * X_AB * X_BH /* X_GH */));
X_AB = CalcCornerTransform(a, b, axis, true /* expect_overlap */);
EXPECT_TRUE(Obb::HasOverlap(a, b, X_GA * X_AB * X_BH /* X_GH */));
}
// To cover the local axes out of B, we can use the same method by swapping
// the order of the boxes and then using the inverse of the transform.
for (int axis = 0; axis < 3; ++axis) {
X_AB =
CalcCornerTransform(b, a, axis, false /* expect_overlap */).inverse();
EXPECT_FALSE(Obb::HasOverlap(a, b, X_GA * X_AB * X_BH /* X_GH */));
X_AB = CalcCornerTransform(b, a, axis, true /* expect_overlap */).inverse();
EXPECT_TRUE(Obb::HasOverlap(a, b, X_GA * X_AB * X_BH /* X_GH */));
}
// To cover the remaining 9 cases, we need to pose an edge from box B along
// an edge from box A. The axes that the edges are aligned with form the
// two inputs into the cross product for the separating axis. For example,
// in the following illustration, Af lies on the edge aligned with A's y-axis.
// Assuming that Bq lies on an edge aligned with B's x-axis, this would form
// the case testing the separating axis Ay × Bx.
// _________
// +z /________/\ .
// ^ \ \ \ .
// | ______________ Bq \ \ .
// | |\ Af \ Bo \ \ .
// | | \ _____________\ \ \ \ .
// +y | | | Ao | \_______\/ .
// \ | \ | | .
// \| \|______________| .
// -----------------------------------> +x
//
// For this test, we define point Bq on the minimum edge of the box in its
// own frame (i.e., center - half width + an offset along the edge). We want
// to pose box B so Bq is the uniquely closest point to A at a Point Af on the
// edge between the +x and +z face of box A. The rest of box B extends
// farther along the +Ax and +Az axis (as suggested in the above
// illustration). Point Bq will be a small epsilon away from the nearby
// edge either outside (if expect_overlap is false) or inside (if true).
for (int a_axis = 0; a_axis < 3; ++a_axis) {
for (int b_axis = 0; b_axis < 3; ++b_axis) {
X_AB =
CalcEdgeTransform(a, b, a_axis, b_axis, false /* expect_overlap */);
// Separate along a's y-axis and b's x-axis.
EXPECT_FALSE(Obb::HasOverlap(a, b, X_GA * X_AB * X_BH /* X_GH */));
X_AB = CalcEdgeTransform(a, b, a_axis, b_axis, true /* expect_overlap */);
// Separate along a's y-axis and b's x-axis.
EXPECT_TRUE(Obb::HasOverlap(a, b, X_GA * X_AB * X_BH /* X_GH */));
}