-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
make_cylinder_mesh.h
689 lines (607 loc) · 28.8 KB
/
make_cylinder_mesh.h
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
#pragma once
#include <algorithm>
#include <limits>
#include <unordered_map>
#include <utility>
#include <vector>
#include "drake/common/drake_copyable.h"
#include "drake/common/eigen_types.h"
#include "drake/common/sorted_pair.h"
#include "drake/geometry/proximity/triangle_surface_mesh.h"
#include "drake/geometry/proximity/volume_mesh.h"
#include "drake/geometry/proximity/volume_to_surface_mesh.h"
#include "drake/geometry/shape_specification.h"
namespace drake {
namespace geometry {
namespace internal {
/* Generates a tetrahedral volume mesh approximating a given cylinder by medial
axis (MA) subdivision. The cylinder is subdivided into blocks using its MA,
and then the blocks are subdivided into tetrahedra. Using this mesh with
MeshFieldLinear, we can represent the distance-to-boundary function φ of the
cylinder (which can be scaled by a constant elastic modulus to create a
hydroelastic pressure field) accurately and economically. However, this mesh
may not be suitable for other kinds of pressure fields, e.g., a squared
distance field.
The cylinder's MA is the set of all points having more than one closest point
on the cylinder's boundary. (See https://en.wikipedia.org/wiki/Medial_axis for
more about the medial axis.)
This table show how this function classifies cylinders into three kinds.
|long cylinder | cylinder.length()/2 > cylinder.radius() |
|medium cylinder | cylinder.length()/2 = cylinder.radius() |
|short cylinder | cylinder.length()/2 < cylinder.radius() |
The following picture file shows examples of a mesh of a long cylinder,
a mesh of a medium cylinder, and a mesh of a short cylinder. The files are
distributed with the source code.
|geometry/proximity/images/cylinder_mesh_medial_axis_pressure.png |
| Meshes of a long cylinder, a medium cylinder, and a short cylinder|
The following three pictures show X-Z cross sections of the three kinds of
cylinders together with their MAs. Each cylinder has its Z-axis as the axis
of rotation.
1. The long cylinder has cylinder.length()/2 > cylinder.radius(). Its MA
includes the two cone surfaces tapering from the bottom and the top
circular caps of the cylinder. Each cone has its apex at one of the two
medial vertices M₀ and M₁. The MA also includes the line segment
connecting M₀ and M₁. The following picture shows MA's cross section
consisting of four line segments (drawn with arrows) from four corners of
the cylinder's cross section (drawn as a rectangle with "+", "--", "|")
to the medial vertices M₀ and M₁ (drawn with "o"). It also shows the
medial line segment (drawn with "║") connecting M₀ and M₁.
Z (axis of rotation)
↑
|
+-----------+
| ↘ ↙ |
| ↘ ↙ |
| M₁ o |
| ║ |
| ║ | ----→ X
| ║ |
| M₀ o |
| ↗ ↖ |
| ↗ ↖ |
+-----------+
2. The medium cylinder has cylinder.length()/2 = cylinder.radius(). Its MA
consists of two cone surfaces tapering from the bottom and the top
circular caps of the cylinder. The two cones share their common apex at the
medial vertex M₀. The following picture shows MA's cross section
consisting of four line segments (drawn with arrows) from four corners of
the cylinder's cross section (drawn as a square with "+", "--", "|") to
the medial vertex M₀ (drawn with "o").
Z (axis of rotation)
↑
|
+-----------+
| ↘ ↙ |
| ↘ ↙ |
| M₀ o | ----→ X
| ↗ ↖ |
| ↗ ↖ |
+-----------+
3. The short cylinder has cylinder.length()/2 < cylinder.radius(). Its MA
includes surfaces of the two circular frusta (truncated cones) tapering
from the bottom and the top circular caps of the cylinder. The two frusta
share the medial circular disk D₀. The following picture shows MA's cross
section consisting of four line segments (drawn with arrows) from four
corners of the cylinder's cross section (drawn as a rectangle with "+",
"--", "|") to the two endpoints of a line segment (drawn with "~"), which
is a diameter of the disk D₀.
Z (axis of rotation)
↑
|
+-------------+
| ↘ D₀ ↙ |
| ~~~~~~~ | ----→ X
| ↗ ↖ |
+-------------+
The output mesh will have these properties:
1. The generated vertices are unique. There are no repeating vertices in
the list of vertex coordinates.
2. The generated tetrahedra are _conforming_. Two tetrahedra intersect in
their shared face, or shared edge, or shared vertex, or not at all.
There is no partial overlapping of two tetrahedra.
3. Let n be ⌈2πr/h⌉, where r = cylinder.radius(), and h is `resolution_hint`,
i.e., n is the number of vertices per one circular rim of the cylinder.
Depending on the shape of the cylinder, the mesh has 5n, 4n, or 9n
tetrahedra and 2n+4, 2n+3, or 3n+3 vertices, respectively.
3.1 The mesh of a long cylinder has 5n tetrahedra with 2n+4 vertices.
3.2 The mesh of a medium cylinder has 4n tetrahedra with 2n+3 vertices.
3.3 The mesh of a short cylinder has 9n tetrahedra with 3n+3 vertices.
@param[in] cylinder The cylinder shape specification.
@param[in] resolution_hint The target length of each edge of the mesh on
each of the bottom and the top circular rims
of the cylinder. The smaller value of
resolution_hint will make the larger number of
tetrahedra.
@pre resolution_hint is positive.
@retval volume_mesh
@tparam_nonsymbolic_scalar
@note A long cylinder or a short cylinder that is almost a medium cylinder
(i.e., the difference between cylinder.length()/2 and cylinder.radius() is a
very small non-zero number) may have tetrahedral elements with very short
edges near its medial axis. However, an internal tolerance prevents such
short edges from being arbitrarily small.
*/
template <typename T>
VolumeMesh<T> MakeCylinderVolumeMeshWithMa(const Cylinder& cylinder,
double resolution_hint);
// Helper methods for MakeCylinderVolumeMesh().
#ifndef DRAKE_DOXYGEN_CXX
// TODO(DamrongGuoy): Consider removing the classification of vertex types.
// We could probably remove ProjectOntoCylinderSide() and use only
// ProjectMidpointToMiddleCylinder() for the midpoints of all edges on the
// side, on the cap, or in the interior volume.
// Determines the boundary type of a vertex of the cylinder mesh.
// Vertices on the circular boundary of the cap are considered
// CylinderVertexType::kSide vertices so that their children inherit the
// CylinderVertexType::kSide type and are projected on the side.
// Also so children of them and a pure side vertex (one that is not on the
// cap) also inherit the CylinderVertexType::kSide type and are projected to
// the side. The order of the enumerators is important because a child of two
// vertices with different types will inherit the first one in the enumerator
// list.
enum class CylinderVertexType { kInternal, kCap, kSide };
// TODO(DamrongGuoy): Consider removing ProjectOntoCylinderSide().
// Projects the point p to the side of the cylinder in the XY direction.
// The point p is projected along the line perpendicular to the center line
// of the cylinder (z-axis).
// @pre This function is supposed to be used only on the outer-most shell, so
// we demand that `p` is at least half the `radius` from the center line
// of the cylinder.
template <typename T>
Vector3<T> ProjectOntoCylinderSide(const Vector3<T>& p, const double radius) {
Vector2<T> p_xy = Vector2<T>(p[0], p[1]);
T norm = p_xy.norm();
DRAKE_DEMAND(norm >= T(radius / 2.0));
p_xy /= norm;
Vector2<T> cylinder_size_xy = radius * p_xy;
return Vector3<T>(cylinder_size_xy.x(), cylinder_size_xy.y(), p.z());
}
// Projects the midpoint between two vertices `p` and `q` to the cylinder
// halfway between the cylinder passing through `p` and the cylinder passing
// through `q`. If `p` and `q` are on the same cylinder, the projection is on
// the common cylinder. The vertices could be a cap vertex or an internal
// vertex. The midpoint is projected along the line perpendicular to the
// center axis of the cylinder (z-axis). The projection does not change the
// z-coordinate of the midpoint.
template <typename T>
Vector3<T> ProjectMidpointToMiddleCylinder(const Vector3<T>& p,
const Vector3<T>& q) {
Vector3<T> midpoint = (p + q) / 2.0;
Vector2<T> v_xy = Vector2<T>(midpoint[0], midpoint[1]);
// The midpoint is on or near the center axis of the cylinder. No projection.
T kEps = std::numeric_limits<double>::epsilon();
if (v_xy.squaredNorm() <= kEps * kEps) {
return midpoint;
}
Vector2<T> p_xy = Vector2<T>(p[0], p[1]);
Vector2<T> q_xy = Vector2<T>(q[0], q[1]);
auto desired_length = (p_xy.norm() + q_xy.norm()) / 2.0;
v_xy.normalize();
Vector2<T> middle_circle_xy = desired_length * v_xy;
return Vector3<T>(middle_circle_xy.x(), middle_circle_xy.y(), midpoint.z());
}
// Projects the midpoint between two vertices based on the boundary type of the
// vertex.
template <typename T>
Vector3<T> ProjectMidPoint(const Vector3<T>& x, const Vector3<T>& y,
CylinderVertexType v_type, const double radius) {
// Project boundary vertices onto the surface of the cylinder.
const Vector3<T> v = (x + y) / 2.0;
// Internal vertices can be projected in the same manner as cap vertices
// for better quality
switch (v_type) {
case CylinderVertexType::kSide:
return ProjectOntoCylinderSide(v, radius);
case CylinderVertexType::kCap:
case CylinderVertexType::kInternal:
return ProjectMidpointToMiddleCylinder(x, y);
default:
return v;
}
}
// TODO(DamrongGuoy): Remove `split_vertex_type_ptr`. Replace two functions
// ProjectMidpointToMiddleCylinder() and ProjectMidPoint() with one function
// that is called from CreateNewVertex() without relying on vertex type.
// Bootstrapping for creating a new vertex in the mesh
// Determines the boundary type, chooses the type of projection (if any),
// adds the vertex to the mesh data structures, and hashes the new
// vertex in the parents -> child map
template <typename T>
int CreateNewVertex(int vertex_a_index, int vertex_b_index,
std::vector<Vector3<T>>* split_mesh_vertices_ptr,
std::vector<CylinderVertexType>* split_vertex_type_ptr,
std::unordered_map<SortedPair<int>, int>* vertex_map_ptr,
const double radius) {
DRAKE_DEMAND(split_mesh_vertices_ptr != nullptr);
DRAKE_DEMAND(split_vertex_type_ptr != nullptr);
DRAKE_DEMAND(vertex_map_ptr != nullptr);
std::vector<Vector3<T>>& split_mesh_vertices = *split_mesh_vertices_ptr;
std::vector<CylinderVertexType>& split_vertex_type = *split_vertex_type_ptr;
std::unordered_map<SortedPair<int>, int>& vertex_map = *vertex_map_ptr;
const CylinderVertexType p_vertex_type = std::min(
split_vertex_type[vertex_a_index], split_vertex_type[vertex_b_index]);
const Vector3<T>& A = split_mesh_vertices[vertex_a_index];
const Vector3<T>& B = split_mesh_vertices[vertex_b_index];
const Vector3<T> p = ProjectMidPoint(A, B, p_vertex_type, radius);
const SortedPair<int> p_parents =
MakeSortedPair(vertex_a_index, vertex_b_index);
const int new_vertex_index = static_cast<int>(split_mesh_vertices.size());
vertex_map[p_parents] = new_vertex_index;
split_mesh_vertices.emplace_back(p);
split_vertex_type.push_back(p_vertex_type);
return new_vertex_index;
}
// Refines a tetrahedron into 8 tetrahedra.
// @param[in] tet
// The tetrahedron to refine.
// @param[in,out] split_mesh_vertices_ptr
// Original vertices plus new vertices on return.
// @param[out] split_mesh_tetrahedra_ptr
// Original tetrahedra plus new tetrahedra on return.
// @param[in,out] split_vertex_type_ptr
// Types of original vertices plus types of new vertices on return.
// @param[in,out] vertex_map_ptr
// Parents to child map. The mapping for new vertices are added on return.
// @param[in] radius
// Radius of the cylinder.
template <typename T>
void RefineCylinderTetrahdron(
const VolumeElement& tet,
std::vector<Vector3<T>>* split_mesh_vertices_ptr,
std::vector<VolumeElement>* split_mesh_tetrahedra_ptr,
std::vector<CylinderVertexType>* split_vertex_type_ptr,
std::unordered_map<SortedPair<int>, int>* vertex_map_ptr,
const double radius) {
DRAKE_DEMAND(split_mesh_vertices_ptr != nullptr);
DRAKE_DEMAND(split_mesh_tetrahedra_ptr != nullptr);
DRAKE_DEMAND(split_vertex_type_ptr != nullptr);
DRAKE_DEMAND(vertex_map_ptr != nullptr);
std::vector<Vector3<T>>& split_mesh_vertices = *split_mesh_vertices_ptr;
std::vector<CylinderVertexType>& split_vertex_type = *split_vertex_type_ptr;
std::vector<VolumeElement>& split_mesh_tetrahedra =
*split_mesh_tetrahedra_ptr;
std::unordered_map<SortedPair<int>, int>& vertex_map = *vertex_map_ptr;
// Index a corresponds to vertex A, index b to vertex B, etc.
const int a = tet.vertex(0);
const int b = tet.vertex(1);
const int c = tet.vertex(2);
const int d = tet.vertex(3);
// 6 new vertices are created, each as the midpoint of every pair of
// vertices in a tet. For example, vertex e is created as the midpoint of
// vertices a and b. Once one tet generates this vertex, we do not want
// another tet that shares the common vertices a and b to duplicate the
// vertex e. Therefore, we store the index of e in split_mesh_vertices
// in the unordered_map "vertex_map". The key for a vertex in this map
// is a std::pair of the indices of its parents. The hash for this map is
// commutative, that is the key (a,b) hashes to the same index as the key
// (b,a) and returns true when checking for equivalence of the keys.
auto get_child_vertex = [&vertex_map, &split_mesh_vertices,
&split_vertex_type, &radius](int p, int q) {
SortedPair<int> parents{p, q};
auto iter = vertex_map.find(parents);
if (iter != vertex_map.end()) return iter->second;
return CreateNewVertex(parents.first(), parents.second(),
&split_mesh_vertices, &split_vertex_type,
&vertex_map, radius);
};
// The index of each of the child vertices
const int e = get_child_vertex(a, b);
const int f = get_child_vertex(a, c);
const int g = get_child_vertex(a, d);
const int h = get_child_vertex(b, c);
const int i = get_child_vertex(b, d);
const int j = get_child_vertex(c, d);
// The four tetrahedra at the corners.
split_mesh_tetrahedra.emplace_back(a, e, f, g);
split_mesh_tetrahedra.emplace_back(b, h, e, i);
split_mesh_tetrahedra.emplace_back(f, h, c, j);
split_mesh_tetrahedra.emplace_back(j, g, i, d);
// TODO(joemasterjohn): Split along EJ, FI and GH and choose
// the partition with best quality factor as described in [Everett, 1997],
// Currently we arbitrarily choose to partition along the GH edge.
split_mesh_tetrahedra.emplace_back(g, h, i, e);
split_mesh_tetrahedra.emplace_back(g, f, h, e);
split_mesh_tetrahedra.emplace_back(g, i, h, j);
split_mesh_tetrahedra.emplace_back(g, h, f, j);
}
// Splits a mesh by calling RefineCylinderTetrahdron() on each
// tetrahedron of `mesh`. `vertex_type` is a vector describing the
// CylinderVertexType of each vertex in `mesh`
//
// When splitting an edge, the midpoint of the edge is projected along the line
// orthogonal to the z-axis. For an edge on the side surface of the cylinder,
// the newly created vertex is placed on the side surface. For other kinds
// of edges, the projection places the newly created vertex at the distance
// from the z-axis equal to the average of the two distances from the z-axis
// of the two original vertices of the split edge. If the two original
// vertices have the same distance from the z-axis, the newly created vertex
// will have the same distance from the z-axis as the original vertices. As
// a result, the mesh vertices are placed on the 2ⁿ concentric cylindrical
// surfaces inside the cylinder, where n is the `refinement_level`.
//
template <typename T>
std::pair<VolumeMesh<T>, std::vector<CylinderVertexType>> RefineCylinderMesh(
const VolumeMesh<T>& mesh,
const std::vector<CylinderVertexType>& vertex_type, const double radius) {
// Copy the vertex, and boundary information into the vectors for the
// new subdivided mesh
std::vector<Vector3<T>> split_mesh_vertices = mesh.vertices();
std::vector<CylinderVertexType> split_vertex_type = vertex_type;
// Original tets are all subdivied, so split_mesh_tetrahedra will only
// contain new tets.
std::vector<VolumeElement> split_mesh_tetrahedra;
// A map from two parent vertices in the original mesh to the new vertex in
// the refined mesh.
std::unordered_map<SortedPair<int>, int> vertex_map(6 * mesh.num_elements());
for (const auto& t : mesh.tetrahedra()) {
RefineCylinderTetrahdron<T>(t, &split_mesh_vertices, &split_mesh_tetrahedra,
&split_vertex_type, &vertex_map, radius);
}
return std::make_pair(VolumeMesh<T>(std::move(split_mesh_tetrahedra),
std::move(split_mesh_vertices)),
split_vertex_type);
}
// Creates the initial mesh for refinement_level = 0.
// The initial mesh is a rectangular prism with
// x,y,z dimensions: sqrt(2)*radius, sqrt(2)*radius, height,
// so that the diagonal on the xy plane has length 2 * radius.
template <typename T>
std::pair<VolumeMesh<T>, std::vector<CylinderVertexType>>
MakeCylinderMeshLevel0(const double& height, const double& radius) {
std::vector<VolumeElement> tetrahedra;
std::vector<Vector3<T>> vertices;
// Initial subdivisions along the length of the cylinder are made based on
// the aspect ratio so that, for a long cylinder, initial tetrahedra are
// somewhat regular in shape. However, for a short cylinder (like a disk),
// there are 2 subdivisions, so we have at least one interior vertex and
// avoid forcing the extent field to be zero everywhere.
int subdivisions =
static_cast<int>(std::max(2.0, std::floor(height / radius)));
const double top_z = height / 2.0;
const double bot_z = -top_z;
// Initial configuration of a set of vertices for a
// level 0 cylinder with 2 subdivisions
//
//
// +Z -X
// | /
// | v2
// | /
// |/
// -Y---v3------v4+------v1---+Y
// /|
// / |
// v0 |
// / |
// +X | -X
// | /
// | v7
// | /
// |/
// -Y---v8------v9+------v6---+Y
// /|
// / |
// v5 |
// / |
// +X | -X
// | /
// | v12
// | /
// |/
// -Y---v13----v14+------v11---+Y
// /|
// / |
// v10 |
// / |
// +X |
// -Z
// Groups of 5 vertices are on a slice of the rectangular prism
// with a plane perpendicular to the Z axis at a given height "z".
//
// "subdivisions" is how many slices perpendicular to the Z axis we
// make.
//
// Every 5th vertex is exactly at (0, 0, z) for a given height "z" where:
// bot_z <= z <= top_z.
auto add_slice_vertices = [&vertices, &radius](double z) {
vertices.emplace_back(radius, 0.0, z);
vertices.emplace_back(0.0, radius, z);
vertices.emplace_back(-radius, 0.0, z);
vertices.emplace_back(0.0, -radius, z);
vertices.emplace_back(0.0, 0.0, z);
};
const double slab_height = height / subdivisions;
for (int i = 0; i < subdivisions; i++) {
add_slice_vertices(top_z - i * slab_height);
}
add_slice_vertices(bot_z);
// Each j-th iteration adds 12 tetrahedra of a rectangular prism of one
// subdivision.
// Each i-th iteration add 3 tetrahedra of a triangular prism, four of which
// makes a rectangular prism.
for (int j = 0; j < subdivisions; j++) {
for (int i = 0; i < 4; i++) {
const int a = 5 * j + i;
const int b = 5 * j + ((i + 1) % 4);
const int c = 5 * j + 4;
const int d = 5 * (j + 1) + i;
const int e = 5 * (j + 1) + ((i + 1) % 4);
const int f = 5 * (j + 1) + 4;
tetrahedra.emplace_back(a, c, b, f);
tetrahedra.emplace_back(a, b, e, f);
tetrahedra.emplace_back(a, e, d, f);
}
}
// Most vertices start on the side
// Two are cap vertices
// There are subdivisions - 1 internal vertices
std::vector<CylinderVertexType> vertex_type(5 * (subdivisions + 1),
CylinderVertexType::kSide);
vertex_type[4] = CylinderVertexType::kCap;
vertex_type[5 * subdivisions + 4] = CylinderVertexType::kCap;
for (int i = 1; i < subdivisions; i++) {
vertex_type[5 * i + 4] = CylinderVertexType::kInternal;
}
return std::make_pair(
VolumeMesh<T>(std::move(tetrahedra), std::move(vertices)), vertex_type);
}
#endif
// Generates a tetrahedral volume mesh of a cylinder whose bounding box is
// [-radius, radius]x[-radius,radius]x[-length/2, length/2], i.e., the center
// line of the cylinder is the z-axis, and the center of the cylinder is at
// the origin.
//
// The level of tessellation is guided by the `resolution_hint` parameter.
// Smaller values create higher-resolution meshes with smaller tetrahedra.
// The resolution hint is interpreted as an edge length, and the cylinder is
// subdivided to guarantee that edge lengths along the boundary circle of
// the top and bottom caps will be less than or equal to that edge length.
//
// The resolution of the final mesh will change discontinuously. Small
// changes to `resolution_hint` will likely produce the same mesh.
// However, cutting `resolution_hint` in half will likely increase the
// number of tetrahedra.
//
// Ultimately, successively smaller values of `resolution_hint` will no
// longer change the output mesh. This algorithm will not produce more than
// 100 million tetrahedra.
//
// This method implements a variant of the generator described in
// [Everett, 1997]. The algorithm has diverged a bit from the one in the paper,
// but the main ideas used from the paper are:
//
// - the pattern of subdividing edges to create vertices
// - the projection of surface vertices
// - the consideration of the combinatorics of how to associate new
// subdivision tetrahedra to the newly created vertices.
//
// It is based on a recursive refinement of an initial
// (refinement_level = 0) coarse mesh representation of a cylinder with
// given height and radius. The initial mesh discretizes a rectangular
// prism, subdividing to make roughly regular tetrahedra.
// At each refinement level, each tetrahedron is split into eight new
// tetrahedra by splitting each edge in half.
//
// As edges get split, cylindrical shells are created. Each shell layer
// conforms to a cylindrical offset surface of the outer cylindrical surface.
//
// @param[in] cylinder
// Specification of the parameterized cylinder the output mesh should
// approximate.
// @param[in] resolution_hint
// The positive characteristic edge length for the mesh. The coarsest
// possible mesh (a rectangular prism) is guaranteed for any value of
// `resolution_hint` greater than √2 times the radius of the cylinder.
// @tparam T
// The Eigen-compatible scalar for representing the mesh vertex positions.
// @pre resolution_hint is positive.
//
// [Everett, 1997] Everett, M.E., 1997. A three-dimensional spherical mesh
// generator. Geophysical Journal International, 130(1), pp.193-200.
template <typename T>
VolumeMesh<T> MakeCylinderVolumeMesh(const Cylinder& cylinder,
double resolution_hint) {
const double length = cylinder.length();
const double radius = cylinder.radius();
std::pair<VolumeMesh<T>, std::vector<CylinderVertexType>> pair =
MakeCylinderMeshLevel0<T>(length, radius);
VolumeMesh<T>& mesh = pair.first;
std::vector<CylinderVertexType>& vertex_type = pair.second;
/*
Calculate the refinement level `L` to satisfy the resolution hint, which
bounds the length `e` of mesh edges on the boundary circle of the top
and bottom caps.
The volume mesh is formed by successively refining a rectangular
prism. At the boundary circle of the top and bottom caps, we go from 4
edges, to 8 edges, to 16 edges, etc., doubling at each level of
refinement. These edges are simply chords across fixed-length arcs of the
circle. Based on that we can easily compute the length `e` of the chord
and bound it by resolution_hint.
We can calculate `e` from the radius r of the cylinder and the central
angle θ supported by the chord in this picture:
x x x x x
x | \ x
x | \ x
x | \ x
x | \ x
x radius r | e \ x
x | \ x
x | \ x
x | θ \
x +---------------x
x radius r x
x x
x x
x x
x x
x x
x x
x x
x x x x x
The chord length e = 2⋅r⋅sin(θ/2). Solving for θ we get: θ = 2⋅sin⁻¹(e/2⋅r).
We can relate θ with the refinement level L.
θ = 2π / 4⋅2ᴸ
= π / 2⋅2ᴸ
= π / 2ᴸ⁺¹
Substituting for θ, we get:
π / 2ᴸ⁺¹ = 2⋅sin⁻¹(e/2⋅r)
2ᴸ⁺¹ = π / 2⋅sin⁻¹(e/2⋅r)
L + 1 = log₂(π / 2⋅sin⁻¹(e/2⋅r))
L = log₂(π / 2⋅sin⁻¹(e/2⋅r)) - 1
L = ⌈log₂(π / sin⁻¹(e/2⋅r))⌉ - 2
*/
DRAKE_DEMAND(resolution_hint > 0.0);
// Make sure the arcsin doesn't blow up.
const double e = std::min(resolution_hint, 2.0 * radius);
const int L = std::max(
0, static_cast<int>(
std::ceil(std::log2(M_PI / std::asin(e / (2.0 * radius)))) - 2));
// TODO(DamrongGuoy): Reconsider the limit of 100 million tetrahedra.
// Should it be smaller like 1 million? It will depend on how the system
// perform in practice.
// Limit refinement_level to 100 million tetrahedra. Each refinement level
// increases the number of tetrahedra by a factor of 8. Let N₀ be the
// number of initial tetrahedra. The number of tetrahedra N with refinement
// level L would be:
// N = N₀ * 8ᴸ
// L = log₈(N/N₀)
// We can limit N to 100 million by:
// L ≤ log₈(1e8/N₀) = log₂(1e8/N₀)/3
const int refinement_level = std::min(
L,
static_cast<int>(std::floor(std::log2(1.e8 / mesh.num_elements()) / 3.)));
// If the original mesh contains N tetrahedra, the resulting mesh with
// refinement_level = L will contain N·8ᴸ tetrahedra.
for (int level = 1; level <= refinement_level; ++level) {
auto split_pair = RefineCylinderMesh<T>(mesh, vertex_type, radius);
mesh = split_pair.first;
vertex_type = split_pair.second;
DRAKE_DEMAND(mesh.vertices().size() == vertex_type.size());
}
return mesh;
}
// Creates a surface mesh for the given `cylinder`; the level of
// tessellation is guided by the `resolution_hint` parameter in the same way
// as MakeCylinderVolumeMeshWithMa().
//
// @param[in] cylinder
// Specification of the parameterized cylinder the output surface mesh
// should approximate.
// @param[in] resolution_hint
// The positive characteristic edge length for the mesh. The coarsest
// possible mesh (a triangular prism) is guaranteed for any value of
// `resolution_hint` at least 3 times the radius of the cylinder.
// @returns The triangulated surface mesh for the given cylinder.
// @pre resolution_hint is positive.
// @tparam T
// The Eigen-compatible scalar for representing the mesh vertex positions.
template <typename T>
TriangleSurfaceMesh<T> MakeCylinderSurfaceMesh(const Cylinder& cylinder,
double resolution_hint) {
DRAKE_DEMAND(resolution_hint > 0.0);
return ConvertVolumeToSurfaceMesh<T>(
MakeCylinderVolumeMeshWithMa<T>(cylinder, resolution_hint));
}
} // namespace internal
} // namespace geometry
} // namespace drake