diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ebcd4b7..3f734a73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,9 @@ option(LIBIGL_CYCODEBASE "Build igl::cycodebase bindings" ON) FetchContent_Declare( libigl GIT_REPOSITORY https://github.com/libigl/libigl.git - GIT_TAG f68f01c23ac82351e2543ce8c529b6d4c89fdf16 + # libigl main, including igl::lazy_cage and core igl::progressive_hulls (moved + # to core in libigl PR #2558). + GIT_TAG c07f8586733378c163aae57f08c9da2ce5352fb9 ) FetchContent_MakeAvailable(libigl) diff --git a/src/LazyCageDistance.cpp b/src/LazyCageDistance.cpp new file mode 100644 index 00000000..2a749a95 --- /dev/null +++ b/src/LazyCageDistance.cpp @@ -0,0 +1,16 @@ +#include "default_types.h" +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void bind_LazyCageDistance(nb::module_ &m) +{ + nb::enum_(m, "LazyCageDistance") + .value("LAZY_CAGE_DISTANCE_SIGNED", igl::LAZY_CAGE_DISTANCE_SIGNED) + .value("LAZY_CAGE_DISTANCE_UNSIGNED", igl::LAZY_CAGE_DISTANCE_UNSIGNED) + .value("NUM_LAZY_CAGE_DISTANCE", igl::NUM_LAZY_CAGE_DISTANCE) + .export_values() + ; +} diff --git a/src/LazyCageGridMode.cpp b/src/LazyCageGridMode.cpp new file mode 100644 index 00000000..307eab72 --- /dev/null +++ b/src/LazyCageGridMode.cpp @@ -0,0 +1,16 @@ +#include "default_types.h" +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void bind_LazyCageGridMode(nb::module_ &m) +{ + nb::enum_(m, "LazyCageGridMode") + .value("LAZY_CAGE_GRID_DENSE", igl::LAZY_CAGE_GRID_DENSE) + .value("LAZY_CAGE_GRID_SPARSE", igl::LAZY_CAGE_GRID_SPARSE) + .value("NUM_LAZY_CAGE_GRID_MODE", igl::NUM_LAZY_CAGE_GRID_MODE) + .export_values() + ; +} diff --git a/src/LazyCageMetric.cpp b/src/LazyCageMetric.cpp new file mode 100644 index 00000000..708533e6 --- /dev/null +++ b/src/LazyCageMetric.cpp @@ -0,0 +1,17 @@ +#include "default_types.h" +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void bind_LazyCageMetric(nb::module_ &m) +{ + nb::enum_(m, "LazyCageMetric") + .value("LAZY_CAGE_METRIC_SIGMA", igl::LAZY_CAGE_METRIC_SIGMA) + .value("LAZY_CAGE_METRIC_VOLUME", igl::LAZY_CAGE_METRIC_VOLUME) + .value("LAZY_CAGE_METRIC_SURFACE_AREA", igl::LAZY_CAGE_METRIC_SURFACE_AREA) + .value("NUM_LAZY_CAGE_METRIC", igl::NUM_LAZY_CAGE_METRIC) + .export_values() + ; +} diff --git a/src/copyleft/progressive_hulls.cpp b/src/copyleft/progressive_hulls.cpp deleted file mode 100644 index fd96b877..00000000 --- a/src/copyleft/progressive_hulls.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "default_types.h" -#include -#include -#include -#include - -namespace nb = nanobind; -using namespace nb::literals; - -namespace pyigl -{ - // igl::decimate is a nightmare of poor templating. Suffer copies. - auto progressive_hulls( - const Eigen::MatrixXd &V, - const Eigen::MatrixXi &F, - const int max_m) - { - Eigen::MatrixXd U; - Eigen::MatrixXi G; - Eigen::VectorXi J; - igl::copyleft::progressive_hulls(V,F,max_m,U,G,J); - return std::make_tuple( - U.cast().eval(), // If Numeric==double will this incur a copy? - G.cast().eval(), - J.cast().eval()); - } -} -void bind_progressive_hulls(nb::module_ &m) -{ - m.def( - "progressive_hulls", - &pyigl::progressive_hulls, - "V"_a, "F"_a, "max_m"_a=0, - R"(Performs progressive hull simplification on a mesh, collapsing edges until a target number of faces is reached. - - @param[in] V #V by dim list of vertex positions - @param[in] F #F by 3 list of face indices into V - @param[in] max_m Target number of output faces - @param[out] U Output vertex positions - @param[out] G Output face indices into U - @param[out] J Indices into F indicating the birth face for each face in G)"); -} diff --git a/src/lazy_cage.cpp b/src/lazy_cage.cpp new file mode 100644 index 00000000..be826636 --- /dev/null +++ b/src/lazy_cage.cpp @@ -0,0 +1,123 @@ +#include "default_types.h" +#include +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + // Simple overload: {shortest edge, midpoint} decimation, the sigma metric, the + // dense grid, and the signed-distance field. grid_size <= 0 picks a heuristic. + auto lazy_cage( + const nb::DRef &V, + const nb::DRef &F, + const int num_faces, + const int grid_size) + { + const Eigen::MatrixXd Vd = V; + const Eigen::MatrixXi Fi = F.cast(); + Eigen::MatrixXd CV; + Eigen::MatrixXi CF; + double sigma; + const bool success = igl::lazy_cage(Vd, Fi, num_faces, grid_size, CV, CF, sigma); + return std::make_tuple( + success, Eigen::MatrixXN(CV), Eigen::MatrixXI(CF.cast()), sigma); + } + + // Full overload exposing every knob. + auto lazy_cage_full( + const nb::DRef &V, + const nb::DRef &F, + const int num_faces, + const int grid_size, + const double max_sigma, + const int num_iters, + const bool use_qslim, + const igl::LazyCageMetric metric, + const igl::LazyCageGridMode grid_mode, + const igl::LazyCageDistance distance) + { + const Eigen::MatrixXd Vd = V; + const Eigen::MatrixXi Fi = F.cast(); + Eigen::MatrixXd CV; + Eigen::MatrixXi CF; + double sigma; + const bool success = igl::lazy_cage( + Vd, Fi, num_faces, grid_size, max_sigma, num_iters, use_qslim, + metric, grid_mode, distance, CV, CF, sigma); + return std::make_tuple( + success, Eigen::MatrixXN(CV), Eigen::MatrixXI(CF.cast()), sigma); + } +} + +void bind_lazy_cage(nb::module_ &m) +{ + m.def( + "lazy_cage_default_grid_size", + &igl::lazy_cage_default_grid_size, + "num_faces"_a, +R"(Heuristic isosurfacing grid resolution (cells across the largest side) for a +lazy cage with num_faces faces. + +@param[in] num_faces desired number of cage faces +@return grid resolution)"); + + m.def( + "lazy_cage", + &pyigl::lazy_cage, + "V"_a, + "F"_a, + "num_faces"_a, + "grid_size"_a = 0, +R"(Compute a "lazy cage" enclosing a closed input mesh: offset the surface and +decimate it down to num_faces while it stays a valid cage (encloses the input, +free of self-intersections, and not intersecting the input surface). + +Uses the default settings ({shortest edge, midpoint} decimation, the sigma +metric, the dense grid, and the signed-distance field). + +@param[in] V #V by 3 list of input vertex positions (closed, manifold) +@param[in] F #F by 3 list of input triangle indices into V +@param[in] num_faces desired number of faces in the cage +@param[in] grid_size isosurfacing resolution (cells along the largest side); + <= 0 uses lazy_cage_default_grid_size(num_faces) +@param[out] success true if a valid cage with num_faces faces was found +@param[out] CV #CV by 3 list of cage vertex positions +@param[out] CF #CF by 3 list of cage triangle indices into CV +@param[out] sigma the offset distance used to build the returned cage)"); + + m.def( + "lazy_cage", + &pyigl::lazy_cage_full, + "V"_a, + "F"_a, + "num_faces"_a, + "grid_size"_a, + "max_sigma"_a, + "num_iters"_a = 12, + "use_qslim"_a = false, + "metric"_a = igl::LAZY_CAGE_METRIC_SIGMA, + "grid_mode"_a = igl::LAZY_CAGE_GRID_DENSE, + "distance"_a = igl::LAZY_CAGE_DISTANCE_SIGNED, +R"(Compute a "lazy cage" with full control over the offset-and-decimate search. + +@param[in] V #V by 3 list of input vertex positions (closed, manifold) +@param[in] F #F by 3 list of input triangle indices into V +@param[in] num_faces desired number of faces in the cage +@param[in] grid_size isosurfacing resolution; <= 0 uses the heuristic +@param[in] max_sigma upper bound on the searched offset distance +@param[in] num_iters number of bisection iterations on sigma +@param[in] use_qslim decimate with igl.qslim (quadric error) instead of + {shortest edge, midpoint} +@param[in] metric objective minimized when choosing sigma (LazyCageMetric) +@param[in] grid_mode dense vs. sparse isosurface extraction (LazyCageGridMode) +@param[in] distance signed vs. unsigned distance field (LazyCageDistance) +@param[out] success true if a valid cage with num_faces faces was found +@param[out] CV #CV by 3 list of cage vertex positions +@param[out] CF #CF by 3 list of cage triangle indices into CV +@param[out] sigma the offset distance used to build the returned cage)"); +} diff --git a/src/progressive_hulls.cpp b/src/progressive_hulls.cpp new file mode 100644 index 00000000..e6c45254 --- /dev/null +++ b/src/progressive_hulls.cpp @@ -0,0 +1,60 @@ +#include "default_types.h" +#include +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + auto progressive_hulls( + const nb::DRef &V, + const nb::DRef &F, + const size_t max_m, + const bool block_intersections) + { + // igl::progressive_hulls takes concrete Eigen::MatrixXd / int32 MatrixXi. + const Eigen::MatrixXd Vd = V; + const Eigen::MatrixXi Fi = F.cast(); + Eigen::MatrixXd U; + Eigen::MatrixXi G; + Eigen::VectorXi J; + const bool success = + igl::progressive_hulls(Vd, Fi, max_m, block_intersections, U, G, J); + return std::make_tuple( + success, + Eigen::MatrixXN(U), + Eigen::MatrixXI(G.cast()), + Eigen::VectorXI(J.cast())); + } +} + +void bind_progressive_hulls(nb::module_ &m) +{ + m.def( + "progressive_hulls", + &pyigl::progressive_hulls, + "V"_a, + "F"_a, + "max_m"_a, + "block_intersections"_a = false, +R"(Collapse edges until the desired number of faces is achieved, placing each new +vertex outside all previous meshes ("progressive hulls" from Sander et al., +"Silhouette clipping" 2000). The result is a nested, enclosing coarsening of the +input. + +Assumes (V,F) is a closed manifold mesh. + +@param[in] V #V by 3 list of vertex positions +@param[in] F #F by 3 list of triangle indices into V +@param[in] max_m desired number of output faces +@param[in] block_intersections whether to refuse collapses that would introduce + self-intersections +@param[out] success true if max_m was reached (otherwise #G > max_m) +@param[out] U #U by 3 list of output vertex positions +@param[out] G #G by 3 list of output triangle indices into U +@param[out] J #G list of indices into F of birth faces)"); +} diff --git a/tests/test_all.py b/tests/test_all.py index 868622de..7ba07659 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -426,6 +426,53 @@ def test_decimate(): dV,dF,J,I = igl.decimate(V,F) dV,dF,J,I = igl.qslim(V,F) +def test_progressive_hulls(): + # a closed manifold with headroom to coarsen + V,F = igl.upsample(*igl.icosahedron(), 2) # 162 V, 320 F + success, U, G, J = igl.progressive_hulls(V, F, max_m=40) + assert success + assert U.shape[1] == 3 + assert G.shape[1] == 3 + assert G.shape[0] == 40 + assert G.max() < U.shape[0] + assert J.shape[0] == G.shape[0] + assert J.max() < F.shape[0] + # a progressive hull encloses the input (loose bbox check) + assert (U.min(axis=0) <= V.min(axis=0) + 1e-9).all() + assert (U.max(axis=0) >= V.max(axis=0) - 1e-9).all() + # the intersection-blocking variant also runs + success2, U2, G2, J2 = igl.progressive_hulls(V, F, max_m=40, block_intersections=True) + assert G2.shape[1] == 3 + +def test_lazy_cage(): + V,F = igl.icosahedron() + # helper + enum smoke + assert igl.lazy_cage_default_grid_size(100) > 0 + assert igl.LazyCageMetric.LAZY_CAGE_METRIC_SIGMA.value == 0 + + # simple overload: default {shortest edge, midpoint} / sigma / dense / signed + success, CV, CF, sigma = igl.lazy_cage(V, F, num_faces=50, grid_size=32) + assert success + assert CV.shape[1] == 3 + assert CF.shape[1] == 3 + assert CF.shape[0] == 50 # exactly num_faces when successful + assert CF.max() < CV.shape[0] + assert sigma > 0.0 + # the cage encloses the input mesh (loose bbox check) + assert (CV.min(axis=0) <= V.min(axis=0) + 1e-9).all() + assert (CV.max(axis=0) >= V.max(axis=0) - 1e-9).all() + + # full overload: exercise the enum-driven knobs (unsigned distance here) + diag = np.linalg.norm(V.max(axis=0) - V.min(axis=0)) + success_f, CV_f, CF_f, sigma_f = igl.lazy_cage( + V, F, 50, 32, 0.5 * diag, + num_iters=12, use_qslim=False, + metric=igl.LazyCageMetric.LAZY_CAGE_METRIC_VOLUME, + grid_mode=igl.LazyCageGridMode.LAZY_CAGE_GRID_DENSE, + distance=igl.LazyCageDistance.LAZY_CAGE_DISTANCE_UNSIGNED) + assert CF_f.shape[1] == 3 + assert success_f + def test_parameterization(): V,Q,E = igl.quad_grid(3,3); V,F = igl.triangulated_grid(3,3);