Convex hull straight from the descriptor
Updated 2026-10-30 04:58; study: ~/code/data/meshes/fcp_3010_hull (sections/hull/README.txt).
Package v5.31 (commit db3e6ceb; the working tree only raises MAX_ATOMS to 12288; defaults: parts="tracked-fit", interp="poly"), the 12 objects
of the collision section, encoded at 128 layers (1024 or 6144 atoms) and at 256 layers (6144 atoms). The hull is built from the
descriptor without a decode grid, a mesh or qhull: the solid rings are evaluated from the Fourier and Chebyshev
coefficients (holes are inside the solid and never touch the hull), each part's end rings are held to the part's
outer edges, and per level (a layer centre or a part edge) one numba kernel takes the 2D hull of the ring samples and
its support point in M directions. In each direction a hull vertex must lie on the upper concave envelope of
(height, support) over the levels; neighbouring directions' envelopes are joined by height and the end levels capped,
which gives a closed triangle surface in O(levels × M). Reference: scipy's qhull on the source mesh's vertices,
and the route through the decoded mesh (fcp.decode, then qhull). CPU: Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz, one thread for both
(median of 21 runs; the decode route of 7). Distances in % of the source mesh's diagonal, from 20k points on the
source mesh's hull to the descriptor's hull; volume relative to the source mesh's hull.
| layers | atoms | directions M | FCP hull, ms (median) | qhull on the mesh, ms (median) | decode + qhull, ms (median) | FCP faster than qhull | speed-up, median (min–max) | volume / true hull, median (min) | gap, mean % diag | gap, max % diag | non-convex dent, max % diag | hulls with no over-shared edge |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 128 | 1024 | 64 | 3.8 | 4.0 | 116 | 6 / 12 | 0.98 (0.45–1.89) | 0.991 (0.961) | 0.162 | 1.31 | 0.59 | 7 / 12 |
| 128 | 6144 | 64 | 4.0 | 4.0 | 107 | 7 / 12 | 1.06 (0.43–2.36) | 0.990 (0.962) | 0.128 | 1.21 | 0.71 | 8 / 12 |
| 128 | 6144 | 128 | 5.8 | 4.0 | 107 | 1 / 12 | 0.60 (0.26–1.05) | 0.993 (0.965) | 0.111 | 1.14 | 0.35 | 7 / 12 |
| 256 | 6144 | 128 | 7.9 | 4.0 | 407 | 0 / 12 | 0.44 (0.15–0.89) | 0.995 (0.965) | 0.104 | 1.14 | 0.24 | 5 / 12 |
Time: at 128 layers and M = 64 the descriptor's hull takes 2.3–13.9 ms, against 2.1–16.3 ms for qhull on these meshes (2.3k–38k vertices): about even, faster on 7 of 12 (mug, bowl, bowl serving, shoe, jogger, spring, knot), at most 2.36×. The gain is in not having the mesh: the decode route is 69–195 ms, 13–66× slower. qhull's cost grows with the hull it outputs, and the descriptor's hull has more vertices than the mesh's (the rings are dense where the mesh is coarse), so a large source mesh is where the descriptor wins outright (a 222k-vertex dress outside this set: 3.5 ms against 39 ms); the knot and spring, with many parts per level, are the slowest descriptor hulls. M = 128 and 256 layers cost 1.5× and 2.0× the time (medians). Accuracy: the volume is 0.962–0.998 of the true hull at 128 layers, the mean gap 0.05–0.29 % of the diagonal, the largest 1.21 % (mug). Nearly all of it is the encoding, not the hull: the decoded mesh's hull (qhull) is as far from the true hull (mean 0.02–0.25 %, max up to 1.15 %), and the descriptor's hull is within a mean 0.02–0.07 % of it (max 0.18–0.72 %, the spring and knot at the top: 64 directions are too few there; M = 128 lowers their max gap, knot 0.58 → 0.15 %, spring 0.74 → 0.33 % (decoded mesh's hull: 0.09, 0.24 %)). A flat top or bottom also moves: a part ends at a layer edge, so a cap sits up to half a layer from the true one (the mug's top: 0.19 % of the diagonal). 256 layers lower the gap only where the layers were the limit (horse, mug ace, spring); elsewhere it is the rings' own error. Defects: the surface is convex only up to the M directions (dents up to 0.71 % of the diagonal at M = 64, on the spring); and where one pair of support points lies on the envelopes of two non-neighbouring directions, the bands on both sides use that edge: no open edge on any of the 48 hulls, but 0–11 edges shared by four triangles (a fold, not a hole; the volume is unaffected). A final qhull over the descriptor hull's own vertices would remove both (1–6 ms on 500–2400 such vertices in the first trials), giving up most of the gain. Prototype, not in the package.
| object (128 layers, 6144 atoms, M = 64) | mesh vertices | atoms held | FCP hull, ms | qhull, ms | decode + qhull, ms | FCP hull vertices | true hull vertices | volume / true | gap, mean % | gap, max % | decoded mesh's hull: gap, mean % | its gap, max % | FCP hull to decoded hull, mean % |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| mug | 4227 | 5515 | 3.9 | 5.0 | 189 | 1819 | 792 | 0.992 | 0.137 | 1.21 | 0.128 | 1.15 | 0.040 |
| mug ace | 5612 | 6144 | 4.3 | 2.8 | 81 | 983 | 610 | 0.998 | 0.054 | 0.34 | 0.035 | 0.33 | 0.025 |
| bowl | 7017 | 2562 | 2.3 | 5.5 | 149 | 2945 | 1151 | 0.962 | 0.285 | 0.96 | 0.253 | 0.95 | 0.034 |
| bowl serving | 2975 | 2945 | 2.9 | 4.0 | 195 | 3188 | 1356 | 0.977 | 0.157 | 0.72 | 0.132 | 0.71 | 0.037 |
| bowl dog | 2308 | 2246 | 3.1 | 2.9 | 103 | 2181 | 953 | 0.992 | 0.177 | 0.87 | 0.169 | 0.84 | 0.045 |
| flower pot | 3884 | 4542 | 3.5 | 3.2 | 69 | 1855 | 881 | 0.992 | 0.135 | 0.94 | 0.114 | 0.87 | 0.046 |
| shoe | 12133 | 6144 | 4.1 | 4.1 | 130 | 1133 | 601 | 0.981 | 0.140 | 0.60 | 0.123 | 0.54 | 0.032 |
| jogger | 10041 | 6144 | 3.6 | 4.0 | 108 | 1055 | 683 | 0.984 | 0.128 | 0.70 | 0.103 | 0.70 | 0.031 |
| dino | 9845 | 6144 | 4.9 | 2.1 | 98 | 348 | 213 | 0.986 | 0.103 | 0.52 | 0.116 | 0.65 | 0.045 |
| horse | 6922 | 6144 | 4.9 | 2.2 | 103 | 560 | 350 | 0.988 | 0.082 | 0.55 | 0.058 | 0.58 | 0.040 |
| spring | 38400 | 6144 | 13.9 | 16.3 | 176 | 2765 | 3638 | 0.997 | 0.080 | 0.74 | 0.023 | 0.24 | 0.066 |
| knot | 14400 | 6144 | 7.7 | 8.6 | 105 | 4658 | 2313 | 0.993 | 0.063 | 0.58 | 0.020 | 0.09 | 0.046 |