Skip to content

Fast/robust point-in-mesh test: c_pointsinside(method=) - #6

Merged
jefferis merged 7 commits into
masterfrom
feature/fast-winding-libigl
Sep 20, 2026
Merged

jefferis merged 7 commits into
masterfrom
feature/fast-winding-libigl

Conversation

@jefferis

Copy link
Copy Markdown
Collaborator

Adds c_pointsinside(), a robust point-in-mesh classifier based on the generalised (solid-angle) winding number — normal-free, so it avoids the spurious "outside classified as inside" results normal-based tests give near thin protrusions.

API

c_pointsinside(points, vertices, faces,
               method = c("auto", "bvh", "bruteforce"),
               threads = NULL, accuracy = 2)

A single exported function with two back ends behind it:

  • "bruteforce" — self-contained O(P*F) winding number, parallelised over points with RcppThread. No setup cost.
  • "bvh" — libigl "Fast Winding Numbers for Soups and Clouds" (Barill et al. 2018); builds a bounding-volume hierarchy once, then each query is O(log F). libigl (MPL-2.0) vendored under src/vendor/igl, requires RcppEigen. The bundled Houdini HDK amalgamation is marked a system header (one-line #pragma) so its third-party warnings don't surface as install-time R CMD check WARNINGs (confirmed clean on win-builder r-devel + rhub).
  • "auto" (default) — picks bvh once a mesh is non-trivial (>=1000 faces with a real query workload), else brute force; a cuboid always takes the cheap path.

Auto threshold

Tuned from a benchmark rather than guessed. The pick is asymmetric-safe (a wrong bvh choice only costs the bounded octree build; a wrong brute-force on a large mesh costs seconds):

faces brute vs bvh
280 brute 5–6× faster
7,080 bvh 4× faster
50,880 bvh ~28× faster

Other

  • Uses the package threads = NULL policy (natcpp_threads()), not a hard-coded 4.
  • The C++ back ends stay internal .Call targets behind the R wrapper.

🤖 Generated with Claude Code

jefferis and others added 7 commits September 18, 2026 06:04
Robust generalised (solid-angle) winding number implemented in C++ and
parallelised over query points with RcppThread. Thresholding abs(w) > 0.5
classifies points as inside a closed triangle mesh.

Unlike a closest-point signed-distance test (e.g. Rvcg::vcgClostKD) it does
not depend on surface normals and has no ray-casting tie-breaking, so it does
not produce the spurious "outside point classified as inside" results that
normal-based tests give near thin protrusions or sharp features. Intended as
the accelerated back end for nat::pointsinside().

The exported c_pointsinside() returns a logical vector; the underlying
c_mesh_winding_number() (raw winding numbers) is kept internal, useful for
correctness checks. Default threads = 4 matches the rest of the package.
Analytic tetrahedron (interior w ~ +/-1, exterior ~ 0), orientation
independence, thread-count invariance and input validation, plus a real
visual-CA1 mesh (bundled in testdata) where the four points a normal-based
test wrongly called inside are confirmed outside and 2000 bbox-sampled points
match an independent oracle (CGAL Side_of_triangle_mesh) baked into the rds.

All parallel calls capped at threads = 2 to respect CRAN's check-farm limit;
suite passes with _R_CHECK_LIMIT_CORES_=TRUE.
Add the transitive include closure of igl::fast_winding_number (10 files, MPL-2.0)
under src/vendor/igl, plus LICENSE.MPL2 and a provenance README (libigl commit
7100764). No CGAL/Boost/GMP/MPFR/TBB is compiled; the only new dependency is
Eigen, added via RcppEigen in LinkingTo. libigl contributors credited as a
copyright holder in Authors@R.
c_fast_mesh_winding_number() builds a bounding-volume hierarchy once over the
mesh (Barill et al. 2018) and evaluates each query point in O(log F), scaling to
millions of points on large meshes. Query points are distributed with
RcppThread (natcpp's threads convention) rather than libigl's own std::thread
pool, so the core count stays controllable for CRAN's check-farm limit.

Each worker calls the underlying UT_SolidAngle::computeSolidAngle primitive
directly -- the same call libigl's batch overload makes internally. Do NOT loop
libigl's templated fast_winding_number(bvh, accuracy, p) wrapper per point
instead: that convenience overload is only explicitly instantiated for dynamic
float matrices and carries heavy per-call overhead, making it ~450x slower than
the primitive (1.82M points on the CA1 mesh: 89s vs 1.6s single-threaded). We
also avoid libigl's batch API, whose internal parallel_for reads a process-wide
thread-count singleton that cannot honour a per-call `threads` argument.

Kept internal (unexported) alongside the internal c_fast_pointsinside() R
wrapper: this accelerated path is intended as the back end for nat::pointsinside,
while the brute-force winding number remains as an O(P*F) correctness reference.
Fast path agrees with the brute-force winding number on the analytic
tetrahedron, calls the four known CA1 false positives outside, and matches the
independent CGAL oracle on the 2000-point bbox sample. Threads capped at 2.
FastWindingNumberForSoups.h is an upstream Side Effects HDK amalgamation
(vendored via libigl). Under GCC/Clang it emits -Wpedantic (anonymous
structs) and -Wclass-memaccess warnings that CRAN's win-builder r-devel
flags as significant, tripping an install-time "R CMD check" WARNING.

Add a single `#pragma GCC system_header` at the top so both compilers treat
its diagnostics as toolchain-header diagnostics and stay silent, without
altering any semantics and without non-portable -Wno-* flags in Makevars.
The file is otherwise byte-identical upstream.
Collapse the separate brute-force export and internal libigl wrapper into
one exported c_pointsinside() with method = c("auto", "bvh", "bruteforce").
"auto" builds the libigl BVH only for large meshes (>=1000 faces with a
non-trivial query workload) and uses the brute-force O(P*F) test otherwise,
so a simple mesh such as a cuboid always takes the cheaper path; the choice
is asymmetric-safe since a mistaken "bvh" only costs the bounded octree
build while a mistaken "bruteforce" on a large mesh costs seconds.

Adopt the package threads=NULL policy (natcpp_threads()) in place of the
hard-coded threads=4L, and drop the redundant internal c_fast_pointsinside()
wrapper. The two C++ back ends (c_mesh_winding_number,
c_fast_mesh_winding_number) remain internal .Call targets behind the wrapper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jefferis
jefferis merged commit 340b78e into master Sep 20, 2026
9 checks passed
@jefferis
jefferis deleted the feature/fast-winding-libigl branch September 20, 2026 08:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant