Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 16 additions & 0 deletions src/LazyCageDistance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include "default_types.h"
#include <igl/lazy_cage.h>
#include <nanobind/nanobind.h>

namespace nb = nanobind;
using namespace nb::literals;

void bind_LazyCageDistance(nb::module_ &m)
{
nb::enum_<igl::LazyCageDistance>(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()
;
}
16 changes: 16 additions & 0 deletions src/LazyCageGridMode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include "default_types.h"
#include <igl/lazy_cage.h>
#include <nanobind/nanobind.h>

namespace nb = nanobind;
using namespace nb::literals;

void bind_LazyCageGridMode(nb::module_ &m)
{
nb::enum_<igl::LazyCageGridMode>(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()
;
}
17 changes: 17 additions & 0 deletions src/LazyCageMetric.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include "default_types.h"
#include <igl/lazy_cage.h>
#include <nanobind/nanobind.h>

namespace nb = nanobind;
using namespace nb::literals;

void bind_LazyCageMetric(nb::module_ &m)
{
nb::enum_<igl::LazyCageMetric>(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()
;
}
42 changes: 0 additions & 42 deletions src/copyleft/progressive_hulls.cpp

This file was deleted.

123 changes: 123 additions & 0 deletions src/lazy_cage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#include "default_types.h"
#include <igl/lazy_cage.h>
#include <nanobind/nanobind.h>
#include <nanobind/ndarray.h>
#include <nanobind/eigen/dense.h>
#include <nanobind/stl/tuple.h>

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<const Eigen::MatrixXN> &V,
const nb::DRef<const Eigen::MatrixXI> &F,
const int num_faces,
const int grid_size)
{
const Eigen::MatrixXd Vd = V;
const Eigen::MatrixXi Fi = F.cast<int>();
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<Integer>()), sigma);
}

// Full overload exposing every knob.
auto lazy_cage_full(
const nb::DRef<const Eigen::MatrixXN> &V,
const nb::DRef<const Eigen::MatrixXI> &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<int>();
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<Integer>()), 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)");
}
60 changes: 60 additions & 0 deletions src/progressive_hulls.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include "default_types.h"
#include <igl/progressive_hulls.h>
#include <nanobind/nanobind.h>
#include <nanobind/ndarray.h>
#include <nanobind/eigen/dense.h>
#include <nanobind/stl/tuple.h>

namespace nb = nanobind;
using namespace nb::literals;

namespace pyigl
{
auto progressive_hulls(
const nb::DRef<const Eigen::MatrixXN> &V,
const nb::DRef<const Eigen::MatrixXI> &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<int>();
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<Integer>()),
Eigen::VectorXI(J.cast<Integer>()));
}
}

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)");
}
47 changes: 47 additions & 0 deletions tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading