Thickness and gaps
What it does
Measures, for every facet of a closed surface (issue #62, roadmap #58):
- the thickness: how thick the body is behind the facet;
- the gap: how far the next wall is in front of the facet, which is the clearance between two parts of a body or between two bodies merged into one surface. It is
hugewhen nothing is in front.
On real geometry, immersed-boundary methods fail where a wall or a gap is thinner than the cell. Such a wall gives the wrong sign, cells with no fluid, and image points that land across the wall. The ambiguous foot points of a query find that afterwards, point by point. Thickness and gap find it before the grid exists, for the whole surface.
On the wing the strip under h = 1/32 runs along the trailing edge and widens towards the tip, where the chord is shorter. The leading edge is in it too: its radius of curvature (0.016 of the chord for NACA 0012) bounds the inscribed ball there. As h halves, the strip shrinks to the trailing edge, whose thickness tends to zero, so it is never resolved.
Ray method
The ray method is the shape diameter function of Shapira, Shamir and Cohen-Or (2008), measured rather than used for segmentation.
- Rays. From the facet centroid,
raysrays (default 31) inside a cone of full aperturecone_angle(default 60°), aroundfor the thickness and for the gap. The directions are fixed: ray 1 is the axis, and ray has the polar cosine (equal-area steps up to the rim) and the azimuth golden angles. No random numbers, so the same facet gives the same value on every run. - The source facet is excluded, not stepped over:
intersect_ray_first(exclude=f)skips it, so no origin offset enters the lengths. - Thickness rays count when their first hit leaves the solid (
). A miss, or a hit through a facet facing the wrong way, does not count. - Gap rays count when their first hit enters the solid (
), and when they miss, with length . - Value. The lower median of the counted lengths. A facet with fewer than half of its rays counted gets
hugeand is counted ininvalid: an open or misoriented surface is reported, never silently averaged. A gap whose median is a miss ishugebut valid: nothing is in front. - Vertex fields (
vertex_thickness,vertex_gap) are area-weighted averages of the finite values of the incident facets. - Parallel. OpenMP over the facets. Each facet is computed on its own, so the result does not depend on the thread count.
Bias of the cone
A cone measures oblique lengths, and the median keeps that bias. On a plate and a sphere it is known in closed form:
| Body | Ray thickness | Default (60°, 31 rays) | Shapira's 120° |
|---|---|---|---|
| plate of thickness | |||
| sphere of radius | |||
any, rays=1 | the axial chord |
The default cone is narrower than Shapira's 120°, which serves segmentation. For a measurement it would overestimate a plate by a third. A cone still matters: one axial ray can graze an edge, while the median of 31 rays does not depend on any single one.
segment_sdf keeps its own shape diameter (120°, an origin offset, no orientation test), unchanged: it is a segmentation feature, not a measurement.
Medial method
The medial method is the shrinking ball of Ma, Bae and Choi (2012). The thickness at a facet is the diameter
- Start.
, with the bounding-box diagonal and the first hit of the axial ray. This is an upper bound: the ball's diameter along the axis cannot pass the first wall it meets. So the medial value never exceeds the axial chord. - Shrink. The ball centred at
(or for the gap). If the closest surface point to is inside it, the ball becomes the one through and , tangent at : , with the unit direction from to the centre ( ball_radiusinfossil_thickness). The radius strictly decreases. - Stop. When
is on the sphere ( ). Or when the new ball would see and under a separation angle below separation_angle(default 30°, the feature-edge threshold). This is Ma et al.'s denoising test. Below that angleis a tessellation neighbour on the same smooth sheet, or a bump of a scanned mesh, not an opposite wall, and the ball keeps its radius. - Batched. Each iteration is one
query_manyover the facets not yet converged, OpenMP-parallel and deterministic. A facet still moving after 64 iterations ishugeand counted ininvalid. A gap ball still athas nothing in front: huge.
Ray and medial compared
| Body | Ray (default cone) | Ray, rays=1 | Medial |
|---|---|---|---|
| plate of thickness | |||
| faceted sphere, radius 1, 1280 facets | |||
| NACA 0012, | |||
| two boxes 0.2 apart, gap |
On a faceted sphere the medial value converges as O(h), from below. The ball is tangent at the facet centroid, which is offset from the circumcentre, so its centre misses the sphere's centre by O(h), and the polyhedron's facets stop it there.
On a blobby body the two methods measure different things. The ray gives the chord along the normal, and the medial the local feature size, which curvature bounds well below the chord. src/tests/validation/fossil_val_thickness.f90 on the Stanford bunny (69 451 facets, not watertight), with the ratio ray / medial over the facets where both are finite:
separation_angle | thickness ratio, quantiles 5 / 50 / 95 % | facets with a medial gap | medial time (-O2) |
|---|---|---|---|
| 30° (default) | 1.10 / 2.46 / 8.81 | 50 109 | 10.3 s |
| 60° | 1.08 / 1.65 / 5.72 | 32 652 | 8.0 s |
| 90° | 1.02 / 1.36 / 4.50 | 18 025 | 5.9 s |
The ray method takes 11.3 s there (31 rays per side). Its 401 invalid facets are where the bunny's holes let the rays escape. A larger separation angle keeps only contacts with walls facing away from
Under-resolution report
thin_regions answers "where is this body thinner than my cell?" before any grid exists. A facet is thin when its local size, min(thickness, gap), is below factor * h. The thin facets are grouped into regions connected through shared edges (union-find over the facet connectivity), sorted by decreasing area, ties broken by the lowest facet id. The arrays are inputs, so a sweep over h, or one call per AMR level, does not recompute the thickness, which is the expensive part.
The fin is 0.05 thick, built with boolean as the union of two boxes. For h = 0.1 it is one region. For h = 0.04 its sides drop out, since their local size is exactly 0.05, but its cap and end faces stay. A ball tangent to the cap away from its centre line meets the side walls first, so the local feature size there is below the fin thickness. The edges of a thin wall are its thinnest part.
AMR criterion
call surface%build_thin(thickness=thickness, gap=gap) ! store min(thickness, gap) in the BVH
call surface%tag_block(x, y, z, band, tags, thin_factor=1._R8P, thin_band=1, thin_tags=thin_tags)
call surface%box_thin_many(lo, hi, factor, touched) ! any boxes; box_thin_many_device too- Criterion. A box is thin-touched when it meets a facet whose local size is below
factortimes the box's largest side. The box-triangle separating-axis test runs on the box inflated bybox_tolerance, so it never misses a contact. Each AMR block uses its own h, and refinement stops where the cells are fine enough. - Pruning.
build_thinstores the local size of every SAH BVH payload triangle and its minimum over every node.bvh_box_thin_oacprunes a subtree whose minimum is above the threshold, as well as one whose box misses the cell. - Lifetime.
analyzedrops the stored sizes, because the facet ids change.movekeeps them, because a rigid motion changes neither the thickness nor the tree topology. - Device.
box_thin_many_deviceruns the same routine in an OpenACC loop.enter_devicemaps the two arrays when they are built. The bench linethin device vs serial mismatchesoffossil_test_distance_benchmust be 0. - Tags.
tag_block(thin_factor=, thin_band=, thin_tags=)gives the Chebyshev cell distance to the nearest thin-touched cell, capped atthin_band + 1, throughcell_bands, as the feature tags do.
2-D curves
curve%thickness is the twin for curves. It measures from the midpoint of every segment along its outward normal. The ray method casts a fan in the curve's plane: ray 1 on the axis, then pairs at huge.
curve%thin_regions(thickness, gap, h, factor, region, region_length)groups the thin segments into runs along each loop (a closed loop wraps around), sorted by decreasing length.curve%build_thin,box_thin_many(_device)andtag_block(..., thin_factor, thin_band, thin_tags)give the AMR criterion. The box-segment test is exact (box_segment_overlap_2d), andcurve_box_thin_oacis shared by the CPU and the device. The bench linethin device vs serial mismatchesoffossil_test_curve_benchmust be 0.- On a NACA 0012 section, the thin regions shrink towards the trailing edge as h decreases, and the trailing edge stays thin at every h: its thickness tends to zero.
Output for inspection
call surface%save_thickness_into_file(file_name, thickness, gap, region, status)The file is legacy ASCII VTK POLYDATA, which ParaView opens directly: the vertex pool as POINTS, every facet as a triangle in POLYGONS, and the cell scalars given. thickness and gap are written as doubles, -1 where undefined or where nothing is in front, and region as an int.
From the command line:
fossilizer -i part.stl --thickness part-thickness.vtk [--thickness_method ray|medial] [--h 0.01 --thin_factor 1]thickness and gaps, medial method
facets 48 invalid 0
thinnest wall 3.333333333333333E-01
narrowest gap none (nothing in front of any facet)
under 1.00000E+00: facets 48 regions 1
largest region area 6.000000000000000E+00
written to part-thickness.vtk(the output for cube-01.stl with --h 1). src/tests/fossil_test_thickness_cli.f90 checks the summary against the library on the same file, and reads the VTK file back: every thickness bit for bit, every gap -1, every region.
API
call surface%thickness(thickness, gap, method, cone_angle, rays, vertex_thickness, vertex_gap, invalid, status, &
separation_angle)
! thickness(nf), gap(nf) per facet, huge where undefined or nothing is in front
! method THICKNESS_RAY (default) or THICKNESS_MEDIAL
! cone_angle, rays ray method: full aperture [degrees] (default 60), rays per facet (default 31)
! separation_angle medial method: denoising angle [degrees] (default 30)
! vertex_thickness, vertex_gap per vertex of the vertex pool
! invalid facets whose rays gave no valueEvery argument is optional, and at least one output is required. The constants THICKNESS_RAY, THICKNESS_MEDIAL, THICKNESS_DEFAULT_CONE, THICKNESS_DEFAULT_RAYS and THICKNESS_DEFAULT_SEPARATION come from use fossil.
Validation
src/tests/fossil_test_thickness_ray.f90:
| Case | Result |
|---|---|
| a plate 10 × 10 × 0.1, facets of the large faces | rays=1, huge everywhere |
| a faceted unit sphere (1280 facets) | |
| two unit boxes 0.2 apart | gap huge on the others |
| a box without one face | the facets whose rays escape are invalid, thickness huge there |
exclude | equal, facet and intersect_ray_all |
| 1 and 4 OpenMP threads | bit-identical |
src/tests/fossil_test_thickness_medial.f90:
| Case | Result |
|---|---|
| the plate | |
| the faceted sphere | |
| an extruded NACA 0012 (100 points per side), mid-span facets with | the closed form |
| two unit boxes 0.2 apart | gap huge on the others |
| 1 and 4 OpenMP threads | bit-identical |
src/tests/fossil_test_thin_regions.f90, on the plate with the fin (medial thickness):
| Case | Result |
|---|---|
thin_regions, h = 0.1 | every fin side in region 1, nothing below the plate top, region 1 of area ≥ the two fin sides |
| h sweep | the thin area never grows as h decreases, and is empty below min(local size) |
before build_thin | STATUS_INVALID_INPUT |
box_thin_many, 16³ cells, factor 2 | equal to a brute force over every facet with the same predicate; thin_tags equal to cell_bands of it |
box_thin_many_device | equal to the CPU |
after move / after analyze | kept, and equal to the brute force on the moved facets / dropped |
src/tests/fossil_test_curve_thickness.f90:
| Case | Result |
|---|---|
| NACA 0012, 100 points per side, segments with | medial against |
| an annulus, radii 1 (256 sides) and 0.5 (128 sides) | medial thickness 0.5 to 1.9e-5; hole gap 1 to 3.0e-4; outer gap huge; ray thickness against the exact median chord |
| h decreasing on the section | thin length never grows; the two trailing-edge segments thin at every h |
| tagging, 32 × 32 cells, factor 2 | refused before build_thin; equal to a brute force over every segment; thin_tags equal to cell_bands of it; device equal |
Limits
- An estimate. Ray thickness carries the cone bias above. The medial value is exact on flat walls and converges as O(h) on curved ones.
- Separation angle. It decides what counts as an opposite wall. See the bunny sweep above.
- Creases. The medial value is the local feature size, and the medial axis reaches every convex crease sharper than the separation angle. A facet next to such a crease, or a sliver, gets a small value even on a thick body. With the default, tagging and
thin_regionsmark sharp creases as well as thin walls. - Closed, outward-oriented input. Rays escape from open surfaces and hit misoriented facets from the wrong side. Both are counted in
invalid.
References
- J. Ma, S. W. Bae, S. Choi, 3D medial axis point approximation using nearest neighbors and the normal field, The Visual Computer 28 (2012). The shrinking ball and its separation-angle denoising.
- L. Shapira, A. Shamir, D. Cohen-Or, Consistent mesh partitioning and skeletonisation using the shape diameter function, The Visual Computer 24 (2008).
See also
- Ray queries: the first-hit traversal the rays use.
- Immersed-boundary queries: ambiguous foot points, the per-query symptom of a thin wall.