Mesh generalized winding numbers via The Antipodal Method (SIGGRAPH 2026) (#2540)

* initial libigl-friendly implementation of the Antipodal Method for generalized winding numbers.

Only contains the triangle-mesh version and Embree intersector support

* removed sse intrinsics code from the intersector

* split into separate files as per style guide

* moved paper/project references to the entry header
This commit is contained in:
Philip Trettner
2026-05-06 14:32:32 -04:00
committed by GitHub
parent 83807ad36f
commit a5e8177ec7
7 changed files with 632 additions and 0 deletions
+259
View File
@@ -0,0 +1,259 @@
// This file is part of libigl, a simple c++ geometry processing library.
//
// Copyright (C) 2026 Philip Trettner <trettner@shapedcode.com>, Cedric Martens <cedric.martens@umontreal.ca>
//
// This Source Code Form is subject to the terms of the Mozilla Public License
// v. 2.0. If a copy of the MPL was not distributed with this file, You can
// obtain one at http://mozilla.org/MPL/2.0/.
#ifndef IGL_WINDINGNUMBERANTIPODALSCENE_H
#define IGL_WINDINGNUMBERANTIPODALSCENE_H
#include "PI.h"
#include "parallel_for.h"
#include <Eigen/Core>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <unordered_map>
#include <vector>
namespace igl
{
/// Precomputed scene for the Antipodal Method generalized winding number
///
/// The scene stores only the weighted open-boundary edges of the input
/// triangle mesh and a fixed antipodal reference direction `x0`. Closed
/// (manifold) meshes have an empty boundary and the fractional term is
/// exactly zero.
///
/// Querying the winding number additionally requires an `Intersector` that
/// returns the signed ray-mesh crossing count along `(p, x0)` over the
/// original mesh; the scene itself is intersector-agnostic. See
/// `igl::embree::EmbreeIntersector::signedIntersectionsRay` for an
/// optimized concrete implementation.
///
/// ### Intersector concept
/// A type `I` satisfies the concept when it exposes:
/// - `using OriginType = ...;` (3D row vector type)
/// - `using DirectionType = ...;` (3D row vector type)
/// - `int signedIntersectionsRay(
/// OriginType origin, DirectionType direction,
/// /* defaulted tnear, tfar, mask */) const;`
///
/// `winding_number` casts query point/direction to the intersector's types
/// at the call site, so a `double`-precision scene against a float-only
/// intersector works without an adaptor.
template <typename Scalar>
class WindingNumberAntipodalScene
{
public:
using Point = Eigen::Matrix<Scalar, 1, 3>;
using Direction = Eigen::Matrix<Scalar, 1, 3>;
private:
struct WeightedSeg
{
Point a;
Point b;
Scalar w;
};
public:
/// Build a scene from a 3D triangle mesh.
///
/// @param[in] V #V by 3 list of vertex positions
/// @param[in] F #F by 3 list of triangle indices
/// @param[in] x0 unit reference direction (defaults to a fixed non-axis-aligned vector)
template <typename DerivedV, typename DerivedF>
WindingNumberAntipodalScene(
const Eigen::MatrixBase<DerivedV> & V,
const Eigen::MatrixBase<DerivedF> & F,
const Direction & x0 = default_x0())
: m_x0(x0), m_face_count(static_cast<size_t>(F.rows()))
{
assert(V.cols() == 3 && "WindingNumberAntipodalScene: only 3D vertex positions are supported");
assert(F.cols() == 3 && "WindingNumberAntipodalScene: only triangle meshes are supported");
build_boundary_segments(V, F, m_boundary);
}
/// Single-point query: full generalized winding number at `p`
/// (fractional + signed integer crossings).
template <typename Intersector, typename Derivedp>
Scalar winding_number(
const Intersector & intersector,
const Eigen::MatrixBase<Derivedp> & p) const
{
const Point pp(static_cast<Scalar>(p(0)),
static_cast<Scalar>(p(1)),
static_cast<Scalar>(p(2)));
const Direction x1 = -m_x0;
Scalar area = Scalar(0);
for (const auto & ws : m_boundary)
{
const Point v0 = ws.a - pp;
const Point v1 = ws.b - pp;
area += ws.w * half_solid_angle_unorm(x1, v0, v1);
}
const Scalar frac = area / (Scalar(2) * Scalar(igl::PI));
using IO = typename Intersector::OriginType;
using ID = typename Intersector::DirectionType;
const IO io(static_cast<typename IO::Scalar>(pp(0)),
static_cast<typename IO::Scalar>(pp(1)),
static_cast<typename IO::Scalar>(pp(2)));
const ID id(static_cast<typename ID::Scalar>(m_x0(0)),
static_cast<typename ID::Scalar>(m_x0(1)),
static_cast<typename ID::Scalar>(m_x0(2)));
const int c = intersector.signedIntersectionsRay(io, id);
return frac + Scalar(c);
}
/// Batch query, parallelized via `igl::parallel_for`.
///
/// @param[in] intersector Concept-compatible intersector built over the
/// same mesh used to construct the scene. Must
/// be safe to query concurrently.
/// @param[in] O #O by 3 list of query points
/// @param[out] W #O by 1 list of winding numbers
template <typename Intersector, typename DerivedO, typename DerivedW>
void winding_number(
const Intersector & intersector,
const Eigen::MatrixBase<DerivedO> & O,
Eigen::PlainObjectBase<DerivedW> & W) const
{
W.resize(O.rows(), 1);
// Adaptive parallel-for threshold.
//
// The libigl thread pool has a roughly fixed ~1 ms TOTAL overhead per
// parallel_for invocation (not per iteration). So we only spawn the
// pool when the WHOLE batch is expected to take ≥ 1 ms.
//
// Per-query work heuristic:
// t_q ≈ 50 * B + 100 * sqrt(F) ns
// with B = boundary segment count, F = triangle count. Pool wins once
// t_q * O > 10^6 ns ⇒ O > 10^6 / t_q
// which is exactly parallel_for's `min_parallel` semantics.
//
// (This is a rough heuristic and should be revisited once parallel_for becomes lower-overhead)
const double t_q_ns =
50.0 * static_cast<double>(m_boundary.size()) +
100.0 * std::sqrt(static_cast<double>(m_face_count));
const size_t min_parallel = static_cast<size_t>(
std::ceil(1.0e6 / std::max(t_q_ns, 1.0)));
igl::parallel_for(O.rows(), [&](const int o)
{
W(o) = winding_number(intersector, O.row(o));
}, min_parallel);
}
/// Reference direction `x0` used to evaluate this scene.
const Direction & x0() const { return m_x0; }
/// Number of weighted boundary edge segments.
size_t num_boundary_segments() const { return m_boundary.size(); }
/// Number of triangles in the original mesh.
size_t num_faces() const { return m_face_count; }
/// Default reference direction: normalize(1, sqrt(2), sqrt(3)). A fixed
/// non-axis-aligned unit vector; any unit vector works per the paper;
/// this choice avoids accidental alignment with axis-aligned geometry.
static Direction default_x0()
{
Direction d(Scalar(1),
Scalar(std::sqrt(2.0)),
Scalar(std::sqrt(3.0)));
return d / d.norm();
}
/// Half the signed solid angle subtended by the spherical triangle
/// (x1, v0, v1) at the origin, via the unnormalized
/// Van Oosterom-Strackee formula. `x1` is expected to be a unit vector
/// (the antipodal "south pole" `-x0`); `v0`, `v1` need not be normalized.
/// Callers accumulate per-edge contributions and divide by 2π.
static Scalar half_solid_angle_unorm(
const Direction & x1, const Point & v0, const Point & v1)
{
const Scalar l0 = v0.norm();
const Scalar l1 = v1.norm();
const Scalar num = x1.dot(v0.cross(v1));
const Scalar denom = l0 * l1
+ l1 * x1.dot(v0)
+ l0 * x1.dot(v1)
+ v0.dot(v1);
return std::atan2(num, denom);
}
/// Extract the open boundary as oriented, weighted edge segments.
///
/// Each undirected edge {min, max} accumulates +1 for every triangle
/// that traverses it as min→max and -1 for every traversal max→min.
/// Interior edges of an oriented manifold cancel to zero and are
/// dropped. Surviving edges are emitted with positive weight; the
/// segment direction is flipped when the net count is negative so the
/// weight is always > 0. Non-manifold (≥3 incident triangles per edge)
/// is handled the same way. The surviving net count becomes the weight.
template <typename DerivedV, typename DerivedF>
static void build_boundary_segments(
const Eigen::MatrixBase<DerivedV> & V,
const Eigen::MatrixBase<DerivedF> & F,
std::vector<WeightedSeg> & out)
{
out.clear();
std::unordered_map<std::uint64_t, int> counts;
counts.reserve(static_cast<size_t>(F.rows()) * 3);
const auto pack = [](int lo, int hi) -> std::uint64_t
{
return (static_cast<std::uint64_t>(static_cast<std::uint32_t>(lo)) << 32)
| static_cast<std::uint64_t>(static_cast<std::uint32_t>(hi));
};
for (Eigen::Index t = 0; t < F.rows(); ++t)
{
const int tri[3] = {
static_cast<int>(F(t, 0)),
static_cast<int>(F(t, 1)),
static_cast<int>(F(t, 2))
};
for (int e = 0; e < 3; ++e)
{
const int u = tri[e];
const int v = tri[(e + 1) % 3];
const int lo = u < v ? u : v;
const int hi = u < v ? v : u;
const std::uint64_t k = pack(lo, hi);
counts[k] += (u < v) ? +1 : -1;
}
}
out.reserve(counts.size());
for (const auto & kv : counts)
{
const int sum = kv.second;
if (sum == 0) continue;
const std::uint32_t lo = static_cast<std::uint32_t>(kv.first >> 32);
const std::uint32_t hi = static_cast<std::uint32_t>(kv.first & 0xFFFFFFFFu);
const Point a(static_cast<Scalar>(V(lo, 0)),
static_cast<Scalar>(V(lo, 1)),
static_cast<Scalar>(V(lo, 2)));
const Point b(static_cast<Scalar>(V(hi, 0)),
static_cast<Scalar>(V(hi, 1)),
static_cast<Scalar>(V(hi, 2)));
WeightedSeg ws;
if (sum > 0) { ws.a = a; ws.b = b; ws.w = static_cast<Scalar>(sum); }
else { ws.a = b; ws.b = a; ws.w = static_cast<Scalar>(-sum); }
out.push_back(ws);
}
}
std::vector<WeightedSeg> m_boundary;
Direction m_x0;
size_t m_face_count = 0;
};
}
#endif
+55
View File
@@ -386,3 +386,58 @@ igl::embree::EmbreeIntersector
ray.hit.instID[0] = RTC_INVALID_GEOMETRY_ID;
ray.hit.primID = RTC_INVALID_GEOMETRY_ID;
}
IGL_INLINE int
igl::embree::EmbreeIntersector
::signedIntersectionsRay(
const OriginType & origin,
const DirectionType & direction,
float tnear,
float tfar,
int mask) const
{
struct query_context
{
RTCRayQueryContext base; // MUST be first for the reinterpret_cast in the filter
int sum;
};
query_context q;
rtcInitRayQueryContext(&q.base);
q.sum = 0;
RTCRay ray{};
ray.org_x = origin[0];
ray.org_y = origin[1];
ray.org_z = origin[2];
ray.dir_x = direction[0];
ray.dir_y = direction[1];
ray.dir_z = direction[2];
ray.tnear = tnear;
ray.tfar = tfar;
ray.mask = static_cast<unsigned int>(mask);
ray.flags = 0;
RTCOccludedArguments rargs;
rtcInitOccludedArguments(&rargs);
rargs.flags = (RTCRayQueryFlags)(
RTC_RAY_QUERY_FLAG_COHERENT | RTC_RAY_QUERY_FLAG_INVOKE_ARGUMENT_FILTER);
rargs.feature_mask = RTC_FEATURE_FLAG_ALL;
rargs.context = &q.base;
rargs.filter = +[](RTCFilterFunctionNArguments const* fargs)
{
assert(fargs->N == 1 && fargs->valid[0]);
auto const& fray = reinterpret_cast<RTCRay&>(*fargs->ray);
auto const& fhit = reinterpret_cast<RTCHit&>(*fargs->hit);
auto const d = fray.dir_x * fhit.Ng_x
+ fray.dir_y * fhit.Ng_y
+ fray.dir_z * fhit.Ng_z;
reinterpret_cast<query_context*>(fargs->context)->sum += d > 0.0f ? +1 : -1;
// Reject the hit so traversal continues and we visit every crossing.
fargs->valid[0] = 0;
};
rargs.occluded = nullptr;
rtcOccluded1(scene, &ray, &rargs);
return q.sum;
}
+19
View File
@@ -37,6 +37,8 @@ namespace igl
public:
typedef Eigen::Matrix<float,Eigen::Dynamic,3> PointMatrixType;
typedef Eigen::Matrix<int,Eigen::Dynamic,3> FaceMatrixType;
typedef Eigen::RowVector3f OriginType;
typedef Eigen::RowVector3f DirectionType;
public:
EmbreeIntersector();
private:
@@ -150,6 +152,23 @@ namespace igl
Hit<float> &hit,
int mask = 0xFFFFFFFF) const;
/// Signed ray-mesh crossing count along `(origin, direction)`. Visits
/// every hit (occluded ray + argument filter rejecting each) and
/// accumulates `sign(direction · Ng)` per hit.
///
/// @param[in] origin ray origin
/// @param[in] direction ray direction (need not be normalized)
/// @param[in] tnear start of ray segment
/// @param[in] tfar end of ray segment
/// @param[in] mask a 32 bit mask to identify active geometries
/// @return signed crossing count
int signedIntersectionsRay(
const OriginType & origin,
const DirectionType& direction,
float tnear = 0.0f,
float tfar = std::numeric_limits<float>::infinity(),
int mask = 0xFFFFFFFF) const;
private:
struct Vertex {float x,y,z,a;};
@@ -0,0 +1,29 @@
// This file is part of libigl, a simple c++ geometry processing library.
//
// Copyright (C) 2026 Philip Trettner <trettner@shapedcode.com>, Cedric Martens <cedric.martens@umontreal.ca>
//
// This Source Code Form is subject to the terms of the Mozilla Public License
// v. 2.0. If a copy of the MPL was not distributed with this file, You can
// obtain one at http://mozilla.org/MPL/2.0/.
//
// Embree-target satellite that emits explicit template instantiations of
// `igl::winding_number_antipodal` against `igl::embree::EmbreeIntersector`.
// igl_core does not link embree, so these instantiations cannot live in
// include/igl/winding_number_antipodal.cpp. In header-only mode this file
// is empty after preprocessing.
#include "../winding_number_antipodal.h"
#ifdef IGL_STATIC_LIBRARY
// Pull in the function body so we can instantiate it here. In static-lib
// mode the header does not transitively include the body.
#include "../winding_number_antipodal.cpp"
#include "EmbreeIntersector.h"
// Mirror of the (V, F, O, W) instantiations in include/igl/winding_number.cpp,
// adapted to the antipodal API (3D triangle meshes only, with EmbreeIntersector
// as the intersector).
template void igl::winding_number_antipodal<igl::embree::EmbreeIntersector, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, 1, 0, -1, 1> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, igl::embree::EmbreeIntersector const&, Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, 1, 0, -1, 1> >&);
template void igl::winding_number_antipodal<igl::embree::EmbreeIntersector, Eigen::Matrix<double, -1, 3, 0, -1, 3>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, 3, 0, -1, 3>, Eigen::Matrix<double, -1, 1, 0, -1, 1> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, 3, 0, -1, 3> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, igl::embree::EmbreeIntersector const&, Eigen::MatrixBase<Eigen::Matrix<double, -1, 3, 0, -1, 3> > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, 1, 0, -1, 1> >&);
template void igl::winding_number_antipodal<igl::embree::EmbreeIntersector, Eigen::Matrix<float, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<float, -1, -1, 0, -1, -1>, Eigen::Matrix<float, -1, 1, 0, -1, 1> >(Eigen::MatrixBase<Eigen::Matrix<float, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, igl::embree::EmbreeIntersector const&, Eigen::MatrixBase<Eigen::Matrix<float, -1, -1, 0, -1, -1> > const&, Eigen::PlainObjectBase<Eigen::Matrix<float, -1, 1, 0, -1, 1> >&);
template void igl::winding_number_antipodal<igl::embree::EmbreeIntersector, Eigen::Matrix<float, -1, 3, 0, -1, 3>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<float, -1, 3, 0, -1, 3>, Eigen::Matrix<float, -1, 1, 0, -1, 1> >(Eigen::MatrixBase<Eigen::Matrix<float, -1, 3, 0, -1, 3> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, igl::embree::EmbreeIntersector const&, Eigen::MatrixBase<Eigen::Matrix<float, -1, 3, 0, -1, 3> > const&, Eigen::PlainObjectBase<Eigen::Matrix<float, -1, 1, 0, -1, 1> >&);
#endif
+33
View File
@@ -0,0 +1,33 @@
// This file is part of libigl, a simple c++ geometry processing library.
//
// Copyright (C) 2026 Philip Trettner <trettner@shapedcode.com>, Cedric Martens <cedric.martens@umontreal.ca>
//
// This Source Code Form is subject to the terms of the Mozilla Public License
// v. 2.0. If a copy of the MPL was not distributed with this file, You can
// obtain one at http://mozilla.org/MPL/2.0/.
#include "winding_number_antipodal.h"
#include "WindingNumberAntipodalScene.h"
template <
typename Intersector,
typename DerivedV,
typename DerivedF,
typename DerivedO,
typename DerivedW>
IGL_INLINE void igl::winding_number_antipodal(
const Eigen::MatrixBase<DerivedV> & V,
const Eigen::MatrixBase<DerivedF> & F,
const Intersector & intersector,
const Eigen::MatrixBase<DerivedO> & O,
Eigen::PlainObjectBase<DerivedW> & W)
{
using Scalar = typename DerivedV::Scalar;
igl::WindingNumberAntipodalScene<Scalar> scene(V, F);
scene.winding_number(intersector, O, W);
}
// No explicit template instantiations live in this file: the function is
// templated on `Intersector`, which is opaque to the core target. Concrete
// instantiations against `igl::embree::EmbreeIntersector` live in
// include/igl/embree/winding_number_antipodal.cpp (igl_embree target),
// because igl_core does not link embree.
+61
View File
@@ -0,0 +1,61 @@
// This file is part of libigl, a simple c++ geometry processing library.
//
// Copyright (C) 2026 Philip Trettner <trettner@shapedcode.com>, Cedric Martens <cedric.martens@umontreal.ca>
//
// This Source Code Form is subject to the terms of the Mozilla Public License
// v. 2.0. If a copy of the MPL was not distributed with this file, You can
// obtain one at http://mozilla.org/MPL/2.0/.
#ifndef IGL_WINDING_NUMBER_ANTIPODAL_H
#define IGL_WINDING_NUMBER_ANTIPODAL_H
#include "igl_inline.h"
#include <Eigen/Core>
namespace igl
{
/// Generalized winding number via the Antipodal Method
/// [Martens, Trettner, Bessmeltsev 2026, "The Antipodal Method: Fast,
/// Accurate, and Robust 3D Generalized Winding Numbers", SIGGRAPH 2026,
/// https://arxiv.org/abs/2605.01536].
///
/// Project: https://martenscedric.github.io/academic-page/publications/antipodal_wn.html
///
/// One-shot convenience: builds a `WindingNumberAntipodalScene<Scalar>`
/// (with `Scalar` deduced from `DerivedV`) and runs the batch query against
/// the supplied `Intersector`. For repeated queries on the same mesh,
/// construct the scene once and call its `winding_number(...)` member
/// directly.
///
/// Mirrors the API shape of `igl::winding_number(V, F, O, W)`.
///
/// @param[in] V #V by 3 list of vertex positions
/// @param[in] F #F by 3 list of triangle indices
/// @param[in] intersector Concept-compatible intersector built over (V, F).
/// `igl::embree::EmbreeIntersector` satisfies the
/// concept and works out of the box. Just call
/// its `init(V.cast<float>(), F.cast<int>())`.
/// @param[in] O #O by 3 list of query points
/// @param[out] W #O by 1 list of winding numbers
///
/// \see WindingNumberAntipodalScene
/// \see igl::embree::EmbreeIntersector::signedIntersectionsRay
template <
typename Intersector,
typename DerivedV,
typename DerivedF,
typename DerivedO,
typename DerivedW>
IGL_INLINE void winding_number_antipodal(
const Eigen::MatrixBase<DerivedV> & V,
const Eigen::MatrixBase<DerivedF> & F,
const Intersector & intersector,
const Eigen::MatrixBase<DerivedO> & O,
Eigen::PlainObjectBase<DerivedW> & W);
}
#ifndef IGL_STATIC_LIBRARY
# include "winding_number_antipodal.cpp"
#endif
#endif
@@ -0,0 +1,176 @@
#include <test_common.h>
#include <igl/winding_number_antipodal.h>
#include <igl/WindingNumberAntipodalScene.h>
#include <igl/embree/EmbreeIntersector.h>
#include <igl/winding_number.h>
#include <igl/read_triangle_mesh.h>
#include <Eigen/Core>
#include <cmath>
#include <random>
TEST_CASE("winding_number_antipodal: closed mesh is integer-valued", "[igl/embree]")
{
Eigen::MatrixXd V;
Eigen::MatrixXi F;
igl::read_triangle_mesh(test_common::data_path("decimated-knight.obj"), V, F);
// Build a grid of query points inside the AABB.
const Eigen::RowVector3d mn = V.colwise().minCoeff();
const Eigen::RowVector3d mx = V.colwise().maxCoeff();
const Eigen::RowVector3d c = 0.5 * (mn + mx);
Eigen::MatrixXd P(8, 3);
for (int i = 0; i < 8; ++i)
{
P(i,0) = c(0) + ((i & 1) ? 0.1 : -0.1) * (mx(0)-mn(0));
P(i,1) = c(1) + ((i & 2) ? 0.1 : -0.1) * (mx(1)-mn(1));
P(i,2) = c(2) + ((i & 4) ? 0.1 : -0.1) * (mx(2)-mn(2));
}
Eigen::VectorXd W_ref;
igl::winding_number(V, F, P, W_ref);
igl::embree::EmbreeIntersector e;
e.init(V.cast<float>(), F.cast<int>());
igl::WindingNumberAntipodalScene<double> scene(V, F);
Eigen::VectorXd W;
scene.winding_number(e, P, W);
REQUIRE(W.size() == W_ref.size());
for (int i = 0; i < W.size(); ++i)
{
REQUIRE(std::abs(W(i) - W_ref(i)) < 1e-3);
}
}
TEST_CASE("winding_number_antipodal: single triangle has fractional contribution", "[igl/embree]")
{
// Triangle in the z=0 plane, oriented CCW seen from +z.
Eigen::MatrixXd V(3, 3);
V << 0, 0, 0,
1, 0, 0,
0, 1, 0;
Eigen::MatrixXi F(1, 3);
F << 0, 1, 2;
igl::embree::EmbreeIntersector e;
e.init(V.cast<float>(), F.cast<int>());
igl::WindingNumberAntipodalScene<double> scene(V, F);
// Query points well above and below the triangle, near its centroid.
const Eigen::RowVector3d c(1.0/3.0, 1.0/3.0, 0.0);
Eigen::MatrixXd P(2, 3);
P.row(0) = c + Eigen::RowVector3d(0, 0, 0.5);
P.row(1) = c + Eigen::RowVector3d(0, 0, -0.5);
Eigen::VectorXd W;
scene.winding_number(e, P, W);
// For a single oriented triangle, the GWN is roughly ±(solid angle / 4π).
// For a point on the normal axis at height h above the centroid of an
// equilateral-ish triangle, |W| stays well under 0.5 but is nonzero, and
// the two sides have opposite signs.
REQUIRE(std::abs(W(0)) > 1e-3);
REQUIRE(std::abs(W(1)) > 1e-3);
REQUIRE(W(0) * W(1) < 0.0);
}
TEST_CASE("winding_number_antipodal: random non-manifold soup matches reference", "[igl/embree]")
{
// Fixed seed: deterministic across runs.
std::mt19937 rng(0xA1B2C3D4u);
std::uniform_real_distribution<double> coord(-1.0, 1.0);
std::uniform_int_distribution<int> vidx(0, 9);
// 10 random vertices.
Eigen::MatrixXd V(10, 3);
for (int i = 0; i < V.rows(); ++i)
for (int d = 0; d < 3; ++d)
V(i, d) = coord(rng);
// 30 random triangles (degenerate ones, repeated indices, are skipped
// and resampled so igl::winding_number's solid_angle stays well-defined).
Eigen::MatrixXi F(30, 3);
for (int t = 0; t < F.rows(); ++t)
{
int a, b, c;
do { a = vidx(rng); b = vidx(rng); c = vidx(rng); }
while (a == b || b == c || a == c);
F(t, 0) = a; F(t, 1) = b; F(t, 2) = c;
}
// 100 random query points in a slightly larger box.
Eigen::MatrixXd P(100, 3);
std::uniform_real_distribution<double> qcoord(-1.5, 1.5);
for (int i = 0; i < P.rows(); ++i)
for (int d = 0; d < 3; ++d)
P(i, d) = qcoord(rng);
Eigen::VectorXd W_ref;
igl::winding_number(V, F, P, W_ref);
igl::embree::EmbreeIntersector e;
e.init(V.cast<float>(), F.cast<int>());
igl::WindingNumberAntipodalScene<double> scene(V, F);
Eigen::VectorXd W;
scene.winding_number(e, P, W);
REQUIRE(W.size() == W_ref.size());
for (int i = 0; i < W.size(); ++i)
{
REQUIRE(std::abs(W(i) - W_ref(i)) < 1e-5);
}
}
TEST_CASE("winding_number_antipodal: free-function one-shot matches scene", "[igl/embree]")
{
Eigen::MatrixXd V;
Eigen::MatrixXi F;
igl::read_triangle_mesh(test_common::data_path("decimated-knight.obj"), V, F);
const Eigen::RowVector3d c = 0.5 * (V.colwise().minCoeff() + V.colwise().maxCoeff());
Eigen::MatrixXd P(4, 3);
P.row(0) = c;
P.row(1) = c + Eigen::RowVector3d(0.1, 0.0, 0.0);
P.row(2) = c + Eigen::RowVector3d(0.0, 0.1, 0.0);
P.row(3) = c + Eigen::RowVector3d(1e3, 0.0, 0.0); // far outside
igl::embree::EmbreeIntersector e;
e.init(V.cast<float>(), F.cast<int>());
Eigen::VectorXd W;
igl::winding_number_antipodal(V, F, e, P, W);
Eigen::VectorXd W_ref;
igl::winding_number(V, F, P, W_ref);
REQUIRE(W.size() == W_ref.size());
for (int i = 0; i < W.size(); ++i)
REQUIRE(std::abs(W(i) - W_ref(i)) < 1e-3);
}
TEST_CASE("winding_number_antipodal: float precision compiles", "[igl/embree]")
{
using V3f = Eigen::Matrix<float, Eigen::Dynamic, 3>;
using F3i = Eigen::Matrix<int, Eigen::Dynamic, 3>;
V3f V(3, 3);
V << 0, 0, 0,
1, 0, 0,
0, 1, 0;
F3i F(1, 3);
F << 0, 1, 2;
igl::embree::EmbreeIntersector e;
e.init(V, F);
igl::WindingNumberAntipodalScene<float> scene(V, F);
Eigen::Matrix<float, Eigen::Dynamic, 3> P(1, 3);
P << 1.0f/3.0f, 1.0f/3.0f, 0.5f;
Eigen::VectorXf W;
scene.winding_number(e, P, W);
REQUIRE(std::abs(W(0)) > 1e-3f);
}