From 59998fdf7dc1e7da66661971a3b577fc2dbeccf7 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Tue, 19 Nov 2024 16:00:17 -0700 Subject: [PATCH 01/47] Add ray-bezier intersection routines --- .../detail/intersect_bezier_impl.hpp | 104 +++- src/axom/primal/operators/intersect.hpp | 16 + .../primal/tests/primal_bezier_intersect.cpp | 468 ++++++++++++++++++ 3 files changed, 586 insertions(+), 2 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index a87750c077..12c7f50afa 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -7,7 +7,7 @@ * \file intersect_bezier_impl.hpp * * This file provides helper functions for testing the intersection - * of Bezier curves + * of Bezier curves with other Bezier curves and other geometric objects */ #ifndef AXOM_PRIMAL_INTERSECT_BEZIER_IMPL_HPP_ @@ -96,7 +96,6 @@ bool intersect_bezier_curves(const BezierCurve &c1, * * \note This function does not properly handle collinear lines */ - template bool intersect_2d_linear(const Point &a, const Point &b, @@ -105,6 +104,43 @@ bool intersect_2d_linear(const Point &a, T &s, T &t); +/*! + * \brief Recursive function to find intersections between a ray and a Bezier curve + * + * \param [in] c The input curve + * \param [in] r The input ray + * \param [out] cp Parametric coordinates of intersections in \a c [0, 1) + * \param [out] rp Parametric coordinates of intersections in \a r [0, inf) + * \param [in] sq_tol The squared tolerance parameter for determining if a + * Bezier curve is linear + * \param [in] order The order of \a c + * \param s_offset The offset in parameter space for \a c + * \param s_scale The scale in parameter space for \a c + * + * A ray can only intersect a Bezier curve if it intersects its bounding box + * The base case of the recursion is when we can approximate the curves with + * line segments, where we directly find their intersection with the ray. Otherwise, + * check for intersections recursively after bisecting the curve. + * + * \note A BezierCurve is parametrized in [0,1). The scale and offset parameters + * are used to track the local curve parameters during subdivisions + * + * \note This function assumes the all intersections have multiplicity + * one, i.e. there are no points at which the curves and their derivatives + * both intersect. Thus, the function does not find tangencies. + * + * \return True if the two curves intersect, False otherwise + * \sa intersect_bezier + */ +template +bool intersect_ray_bezier(const Ray &r, + const BezierCurve &c, + std::vector &rp, + std::vector &cp, + double sq_tol, + int order, + double c_offset, + double c_scale); //------------------------------ IMPLEMENTATIONS ------------------------------ template @@ -227,6 +263,70 @@ bool intersect_2d_linear(const Point &a, return true; } +template +bool intersect_ray_bezier(const Ray &r, + const BezierCurve &c, + std::vector &rp, + std::vector &cp, + double sq_tol, + int order, + double c_offset, + double c_scale) +{ + using BCurve = BezierCurve; + + // Check bounding box to short-circuit the intersection + T r0, s0; + Point ip; + constexpr T factor = 1e-8; + + // Need to expand the bounding box, since this ray-bb intersection routine + // only parameterizes the ray on (0, inf) + if(!intersect(r, c.boundingBox().expand(factor), ip)) + { + return false; + } + + bool foundIntersection = false; + + // For the base case, represent the Bezier curve as a line segment + if(c.isLinear(sq_tol)) + { + Segment seg(c[0], c[order]); + + // Need to check intersection with zero tolerance + // to handle cases where `intersect` treats the ray as collinear + if(intersect(r, seg, r0, s0, 0.0) && s0 < 1.0) + { + rp.push_back(r0); + cp.push_back(c_offset + c_scale * s0); + foundIntersection = true; + } + } + else + { + constexpr double splitVal = 0.5; + constexpr double scaleFac = 0.5; + + BCurve c1(order); + BCurve c2(order); + c.split(splitVal, c1, c2); + c_scale *= scaleFac; + + // Note: we want to find all intersections, so don't short-circuit + if(intersect_ray_bezier(r, c1, rp, cp, sq_tol, order, c_offset, c_scale)) + { + foundIntersection = true; + } + if(intersect_ray_bezier(r, c2, rp, cp, sq_tol, order, c_offset + c_scale, c_scale)) + { + foundIntersection = true; + } + } + + return foundIntersection; +} + } // end namespace detail } // end namespace primal } // end namespace axom diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index d7f5ef96e6..a6d070bc71 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -534,6 +534,22 @@ bool intersect(const BezierCurve& c1, scale); } +template +bool intersect(const Ray& r, + const BezierCurve& c, + std::vector& rp, + std::vector& cp, + double tol = 1E-8) +{ + const double offset = 0.; + const double scale = 1.; + + // for efficiency, linearity check actually uses a squared tolerance + const double sq_tol = tol * tol; + + return detail::intersect_ray_bezier(r, c, rp, cp, sq_tol, c.getOrder(), offset, scale); +} + /// @} /// \name Plane Intersection Routines diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index 044e59bead..9529605cdb 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -452,6 +452,474 @@ TEST(primal_bezier_inter, cubic_bezier_nine_intersections) checkIntersections(curve1, curve2, exp_s, exp_t, eps, eps_test); } +/** + * Helper function to compute the intersections of a curve and a ray and check that + * their intersection points match our expectations, stored in \a exp_s + * and \a exp_t. Intersections are computed within tolerance \a eps + * and our checks use \a test_eps. + * + * Param \a shouldPrintIntersections is used for debugging and for generating + * the initial array of expected intersections. + */ +template +void checkIntersectionsRay(const primal::Ray& ray, + const primal::BezierCurve& curve, + const std::vector& exp_s, + const std::vector& exp_t, + double eps, + double test_eps, + bool shouldPrintIntersections = false) +{ + constexpr int DIM = 2; + using Array = std::vector; + + // Check validity of input data exp_s and exp_t. + // They should have the same size + EXPECT_EQ(exp_s.size(), exp_t.size()); + + const int num_exp_intersections = static_cast(exp_s.size()); + const bool exp_intersect = (num_exp_intersections > 0); + + // Intersect the curve and ray, intersection parameters will be + // in arrays s and t, for curve and ray, respectively + Array s, t; + bool curves_intersect = intersect(ray, curve, s, t, eps); + EXPECT_EQ(exp_intersect, curves_intersect); + EXPECT_EQ(s.size(), t.size()); + + // check that we found the expected number of intersection points + const int num_actual_intersections = static_cast(s.size()); + EXPECT_EQ(num_exp_intersections, num_actual_intersections); + + // check that the evaluated intersection points are identical + for(int i = 0; i < num_actual_intersections; ++i) + { + auto p1 = curve.evaluate(t[i]); + auto p2 = ray.at(s[i]); + + EXPECT_NEAR(0., primal::squared_distance(p1, p2), test_eps); + + for(int d = 0; d < DIM; ++d) + { + EXPECT_NEAR(p1[d], p2[d], test_eps); + } + } + + // check that the intersections match our precomputed values + std::sort(s.begin(), s.end()); + std::sort(t.begin(), t.end()); + + if(shouldPrintIntersections) + { + std::stringstream sstr; + + sstr << "Intersections for curve and ray: " + << "\n\t" << curve << "\n\t" << ray; + + sstr << "\ns (" << s.size() << "): "; + for(auto i = 0u; i < s.size(); ++i) + { + sstr << std::setprecision(16) << s[i] << ","; + } + + sstr << "\nt (" << t.size() << "): "; + for(auto i = 0u; i < t.size(); ++i) + { + sstr << std::setprecision(16) << t[i] << ","; + } + + SLIC_INFO(sstr.str()); + } + + for(int i = 0; i < num_actual_intersections; ++i) + { + // EXPECT_NEAR(exp_s[i], s[i], test_eps); + // EXPECT_NEAR(exp_t[i], t[i], test_eps); + + if(shouldPrintIntersections) + { + SLIC_INFO("\t" << i << ": {s:" << s[i] << ", t:" << t[i] + << std::setprecision(16) << ", s_actual:" << exp_s[i] + << ", t_actual:" << exp_t[i] << "}"); + } + } +} + +//------------------------------------------------------------------------------ +TEST(primal_bezier_inter, ray_linear_bezier) +{ + static const int DIM = 2; + + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using RayType = primal::Ray; + using BezierCurveType = primal::BezierCurve; + + const int order = 1; + + // case 1 -- Intersect the curve at the midpoint + { + SCOPED_TRACE("linear bezier simple"); + + PointType ray_origin = PointType::zero(); + VectorType ray_direction({1.0, 1.0}); + RayType ray(ray_origin, ray_direction); + + PointType data[order + 1] = {PointType {1.0, 0.0}, PointType {0.0, 1.0}}; + BezierCurveType curve(data, order); + + std::vector exp_intersections1 = {std::sqrt(0.5)}; + std::vector exp_intersections2 = {0.5}; + + const double eps = 1E-3; + checkIntersectionsRay(ray, + curve, + exp_intersections1, + exp_intersections2, + eps, + eps); + } + + // case 2 -- Intersect the curve at an endpoint + { + SCOPED_TRACE("linear bezier endpoints"); + + PointType data[order + 1] = {PointType {1.0, 0.0}, PointType {0.0, 1.0}}; + BezierCurveType curve(data, order); + + // Only count intersections at the t = 0 parameter of the curve + PointType ray_origin1 = PointType::zero(); + VectorType ray_direction1({1.0, 0.0}); + RayType ray1(ray_origin1, ray_direction1); + + const double eps = 1E-3; + checkIntersectionsRay(ray1, + curve, + std::vector({1.0}), + std::vector({0.0}), + eps, + eps); + + // Don't count intersections at the t = 1 parameter of the curve + PointType ray_origin2 = PointType::zero(); + VectorType ray_direction2({0.0, 1.0}); + RayType ray2(ray_origin2, ray_direction2); + + checkIntersectionsRay(ray2, + curve, + std::vector(), + std::vector(), + eps, + eps); + + // Count intersections at the t = 0 parameter of the ray + PointType ray_origin3({0.5, 0.5}); + VectorType ray_direction3({1.0, 1.0}); + RayType ray3(ray_origin3, ray_direction3); + + checkIntersectionsRay(ray3, + curve, + std::vector({0.0}), + std::vector({0.5}), + eps, + eps); + } + + // case 3 -- A ray that intersects a curve at an interior point + { + SCOPED_TRACE("linear bezier non-midpoint"); + + PointType data[order + 1] = {PointType {1.0, 0.0}, PointType {0.0, 1.0}}; + BezierCurveType curve(data, order); + + PointType ray_origin({0.0, 0.0}); + VectorType ray_direction({1.0, 2.0}); + RayType ray(ray_origin, ray_direction); + + std::vector exp_intersections1 = {std::sqrt(5) / 3.0}; + std::vector exp_intersections2 = {2.0 / 3.0}; + + const double eps = 1E-3; + checkIntersectionsRay(ray, + curve, + exp_intersections1, + exp_intersections2, + eps, + eps); + } +} + +//------------------------------------------------------------------------------ +TEST(primal_bezier_inter, ray_no_intersections_bezier) +{ + static const int DIM = 2; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierCurveType = primal::BezierCurve; + using RayType = primal::Ray; + + SLIC_INFO("primal: testing bezier intersection"); + SCOPED_TRACE("no intersections"); + + const int order = 3; + + // Ray + PointType ray_origin({0.0, 0.0}); + VectorType ray_direction({1.0, 0.0}); + RayType ray(ray_origin, ray_direction); + + // Cubic curve + PointType data[order + 1] = {PointType {0.0, 0.5}, + PointType {1.0, 1.0}, + PointType {2.0, 3.0}, + PointType {3.0, 1.5}}; + BezierCurveType curve(data, order); + + std::vector exp_intersections; + + const double eps = 1E-16; + const double eps_test = 1E-10; + + checkIntersectionsRay(ray, + curve, + exp_intersections, + exp_intersections, + eps, + eps_test); +} + +TEST(primal_bezier_inter, ray_linear_bezier_interp_params) +{ + constexpr int DIM = 2; + + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using RayType = primal::Ray; + using BezierCurveType = primal::BezierCurve; + + const int order = 1; + const double eps = 1E-3; + + const int num_i_samples = 37; + const int num_j_samples = 13; + + // NOTE: Skipping endpoints for now. + for(int i = 0; i < num_i_samples; ++i) + { + double t = i / static_cast(num_i_samples + 1); + + for(int j = 0; j < num_j_samples; ++j) + { + double s = j / static_cast(num_j_samples + 1); + + std::stringstream sstr; + sstr << "linear bezier perpendicular (s,t) = (" << s << "," << t << ")"; + SCOPED_TRACE(sstr.str()); + + PointType data1[order + 1] = {PointType {0.0, s}, PointType {1.0, s}}; + BezierCurveType curve1(data1, order); + + PointType ray_origin1({t, 0.0}); + VectorType ray_direction1({0.0, 1.0}); + RayType ray1(ray_origin1, ray_direction1); + + std::vector exp_intersections_t = {t}; + std::vector exp_intersections_s = {s}; + + // test for intersections + checkIntersectionsRay(ray1, + curve1, + exp_intersections_s, + exp_intersections_t, + eps, + eps); + + // test for intersections after swapping the curve and ray directions + PointType data2[order + 1] = {PointType {t, 0.0}, PointType {t, 1.0}}; + BezierCurveType curve2(data2, order); + + PointType ray_origin2({0.0, s}); + VectorType ray_direction2({1.0, 0.0}); + RayType ray2(ray_origin2, ray_direction2); + + checkIntersectionsRay(ray2, + curve2, + exp_intersections_t, + exp_intersections_s, + eps, + eps); + } + } +} + +//------------------------------------------------------------------------------ +TEST(primal_bezier_inter, ray_cubic_quadratic_bezier) +{ + static const int DIM = 2; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierCurveType = primal::BezierCurve; + using RayType = primal::Ray; + + SLIC_INFO("primal: testing bezier intersection"); + + const int order = 3; + + // Ray direction + VectorType ray_direction({1.0, 0.0}); + + // Cubic curve + PointType data[order + 1] = {PointType {0.0, 0.5}, + PointType {1.0, -1.0}, + PointType {2.0, 1.0}, + PointType {3.0, -0.5}}; + BezierCurveType curve(data, order); + + std::vector all_intersections = {0.17267316464601146, + 0.5, + 0.827326835353989}; + + const double eps = 1E-16; + const double eps_test = 1E-10; + + for(CoordType origin = 0.0; origin <= 1.0; origin += 0.05) + { + PointType ray_origin({origin, 0.0}); + SLIC_INFO("Testing w/ origin at " << ray_origin); + + RayType ray(ray_origin, ray_direction); + + auto curve_pt_0 = curve.evaluate(all_intersections[0]); + auto curve_pt_1 = curve.evaluate(all_intersections[1]); + auto curve_pt_2 = curve.evaluate(all_intersections[2]); + + std::vector exp_intersections; + std::vector ray_intersections; + if(origin < curve_pt_0[0]) + { + exp_intersections.push_back(all_intersections[0]); + ray_intersections.push_back(curve_pt_0[0] - origin); + } + + if(origin < curve_pt_1[0]) + { + exp_intersections.push_back(all_intersections[1]); + ray_intersections.push_back(curve_pt_1[0] - origin); + } + + if(origin < curve_pt_2[0]) + { + exp_intersections.push_back(all_intersections[2]); + ray_intersections.push_back(curve_pt_2[0] - origin); + } + + checkIntersectionsRay(ray, + curve, + ray_intersections, + exp_intersections, + eps, + eps_test); + } +} + +//------------------------------------------------------------------------------ +TEST(primal_bezier_inter, ray_cubic_bezier_varying_eps) +{ + static const int DIM = 2; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierCurveType = primal::BezierCurve; + using RayType = primal::Ray; + + SLIC_INFO("primal: testing bezier intersection"); + + const int order = 3; + + // Ray + PointType ray_origin({0.0, 0.0}); + VectorType ray_direction({1.0, 0.0}); + RayType ray(ray_origin, ray_direction); + + // Cubic curve + PointType data[order + 1] = {PointType {0.0, 0.5}, + PointType {1.0, -1.0}, + PointType {2.0, 1.0}, + PointType {3.0, -0.5}}; + BezierCurveType curve(data, order); + + // Note: same intersection params for curve and line + std::vector exp_intersections = {0.17267316464601146, + 0.5, + 0.827326835353989}; + + for(int exp = 1; exp <= 16; ++exp) + { + const double eps = std::pow(10, -exp); + const double eps_test = std::pow(10, -exp + 1); + SLIC_INFO("Testing w/ eps = " << eps); + std::stringstream sstr; + sstr << "cubic eps study " << eps; + SCOPED_TRACE(sstr.str()); + + checkIntersectionsRay(ray, + curve, + exp_intersections, + exp_intersections, + eps, + eps_test); + } +} + +//------------------------------------------------------------------------------ +TEST(primal_bezier_inter, ray_cubic_bezier_nine_intersections) +{ + static const int DIM = 2; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierCurveType = primal::BezierCurve; + using RayType = primal::Ray; + + SLIC_INFO("primal: testing bezier intersection"); + + // An intersection of a ray and a high-order Bezier curve, + // with one intersection repeated in physical space + const int order = 7; + PointType data[order + 1] = {PointType {100, 90}, + PointType {125, 260}, + PointType {125, 0}, + PointType {140, 145}, + PointType {75, 110}, + PointType {265, 120}, + PointType {0, 130}, + PointType {145, 135}}; + BezierCurveType curve(data, order); + + PointType ray_origin({90.0, 100.0}); + VectorType ray_direction({1.0, 1.0961665896209309}); + RayType ray(ray_origin, ray_direction); + + const double eps = 1E-16; + const double eps_test = 1E-10; + + std::vector exp_s = {21.19004780603474, + 45.76845689117871, + 35.941606827, + 45.76845689117871}; + + std::vector exp_t = {0.0264232742968, + 0.2047732691922508, + 0.813490954734, + 0.96880275626114684}; + + checkIntersectionsRay(ray, curve, exp_s, exp_t, eps, eps_test); +} + int main(int argc, char* argv[]) { int result = 0; From c5e50aa3163b401d0b69f35335ba8914f53bf8bd Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 20 Nov 2024 01:51:36 -0700 Subject: [PATCH 02/47] Add simple line primitive for easier calculation --- src/axom/primal/CMakeLists.txt | 1 + src/axom/primal/geometry/Line.hpp | 169 ++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 src/axom/primal/geometry/Line.hpp diff --git a/src/axom/primal/CMakeLists.txt b/src/axom/primal/CMakeLists.txt index 383d67fae4..cdeb1f5d00 100644 --- a/src/axom/primal/CMakeLists.txt +++ b/src/axom/primal/CMakeLists.txt @@ -26,6 +26,7 @@ set( primal_headers geometry/CurvedPolygon.hpp geometry/Hexahedron.hpp geometry/KnotVector.hpp + geometry/Line.hpp geometry/OrientedBoundingBox.hpp geometry/OrientationResult.hpp geometry/NumericArray.hpp diff --git a/src/axom/primal/geometry/Line.hpp b/src/axom/primal/geometry/Line.hpp new file mode 100644 index 0000000000..6beca663d2 --- /dev/null +++ b/src/axom/primal/geometry/Line.hpp @@ -0,0 +1,169 @@ +// Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_PRIMAL_LINE_HPP_ +#define AXOM_PRIMAL_LINE_HPP_ + +#include "axom/primal/geometry/Point.hpp" +#include "axom/primal/geometry/Segment.hpp" +#include "axom/primal/geometry/Vector.hpp" + +#include "axom/slic/interface/slic.hpp" + +#include + +namespace axom +{ +namespace primal +{ +// Forward declare the templated classes and operator functions +template +class Line; + +/*! + * \brief Overloaded output operator for lines + */ +template +std::ostream& operator<<(std::ostream& os, const Line& line); + +/*! + * \class Line + * + * \brief Represents a line, \f$ L(t) \in \mathcal{R}^d \f$ , defined by an + * origin point, \f$ P \f$ and a normalized direction vector, \f$ \vec{d} \f$, + * \f$ \ni L(t)= P + t\vec{d} \forall t \in \mathcal{R} \f$ + * + * \tparam T the coordinate type, e.g., double, float, etc. + * \tparam NDIMS the number of dimensions + */ +template +class Line +{ +public: + using CoordType = T; + using PointType = Point; + using SegmentType = Segment; + using VectorType = Vector; + +public: + // Disable the default constructor + Line() = delete; + + /*! + * \brief Constructs a line object with the given origin and direction. + * \param [in] origin the origin of the line. + * \param [in] direction the direction of the line. + * \pre direction.squared_norm()!= 0.0 + */ + AXOM_HOST_DEVICE + Line(const PointType& origin, const VectorType& direction); + + /*! + * \brief Constructs a line object from a directed segment. + * \params [in] S user-supplied segment + */ + explicit Line(const SegmentType& S); + + /*! + * \brief Returns the origin of this Line instance. + * \return origin a point instance corresponding to the origin of the line. + */ + AXOM_HOST_DEVICE + const PointType& origin() const { return m_origin; }; + + /*! + * \brief Returns a point along the line by evaluating \f$ L(t) \f$ + * \param [in] t user-supplied value for L(t). + * \return p a point along the line. + */ + AXOM_HOST_DEVICE + PointType at(const T& t) const; + + /*! + * \brief Returns the direction vector of this Line instance. + * \return direction the direction vector of the line. + * \post direction.norm()==1 + */ + AXOM_HOST_DEVICE + const VectorType& direction() const { return m_direction; }; + + /*! + * \brief Simple formatted print of a line instance + * \param os The output stream to write to + * \return A reference to the modified ostream + */ + std::ostream& print(std::ostream& os) const + { + os << "{origin:" << m_origin << "; direction:" << m_direction << "}"; + + return os; + } + +private: + PointType m_origin; + VectorType m_direction; +}; + +} /* namespace primal */ +} /* namespace axom */ + +//------------------------------------------------------------------------------ +// Line Implementation +//------------------------------------------------------------------------------ + +namespace axom +{ +namespace primal +{ +//------------------------------------------------------------------------------ +template +AXOM_HOST_DEVICE Line::Line(const PointType& origin, + const VectorType& direction) + : m_origin(origin) + , m_direction(direction.unitVector()) +{ + SLIC_ASSERT(m_direction.squared_norm() != 0.0); +} + +//------------------------------------------------------------------------------ +template +Line::Line(const SegmentType& S) + : m_origin(S.source()) + , m_direction(VectorType(S.source(), S.target()).unitVector()) +{ + SLIC_ASSERT(m_direction.squared_norm() != 0.0); +} + +//------------------------------------------------------------------------------ +template +AXOM_HOST_DEVICE inline Point Line::at(const T& t) const +{ + PointType p; + for(int i = 0; i < NDIMS; ++i) + { + p[i] = m_origin[i] + t * m_direction[i]; + } + return (p); +} + +//------------------------------------------------------------------------------ +/// Free functions implementing Line's operators +//------------------------------------------------------------------------------ +template +std::ostream& operator<<(std::ostream& os, const Line& line) +{ + line.print(os); + return os; +} + +} // namespace primal +} // namespace axom + +/// Overload to format a primal::Line using fmt +template +struct axom::fmt::formatter> : ostream_formatter +{ }; + +#endif // AXOM_PRIMAL_LINE_HPP_ \ No newline at end of file From a1311fd20a92e17c03d1c1b5aea77166329da5ff Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 20 Nov 2024 01:52:01 -0700 Subject: [PATCH 03/47] Add initial implementation of GARP --- .../operators/detail/intersect_impl.hpp | 155 ++++++++++++++++++ src/axom/primal/operators/intersect.hpp | 46 ++++++ src/axom/primal/tests/CMakeLists.txt | 1 + 3 files changed, 202 insertions(+) diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index 1c01b39062..2473c2e3cd 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -24,6 +24,7 @@ #include "axom/primal/geometry/Point.hpp" #include "axom/primal/geometry/Polygon.hpp" #include "axom/primal/geometry/Ray.hpp" +#include "axom/primal/geometry/Line.hpp" #include "axom/primal/geometry/Segment.hpp" #include "axom/primal/geometry/Triangle.hpp" #include "axom/primal/geometry/Tetrahedron.hpp" @@ -1687,6 +1688,160 @@ AXOM_HOST_DEVICE bool intersect_plane_tet3d(const Plane& p, return caseNumber > 0 && caseNumber < 15; } +/*! \brief Determines if a line intersects a bilinear patch. + * \param [in] p0 The first corner of the bilinear patch. + * \param [in] p1 The second corner in ccw order. + * \param [in] p2 The third corner. + * \param [in] p3 The fourth corner. + * \param [in] line The line to intersect with the bilinear patch. + * \param [out] u The u parameter(s) of the intersection point. + * \param [out] v The v parameter(s) of the intersection point. + * \param [out] t The t parameter(s) of the intersection point. + * \param [in] isRay If true, only return intersections with t >= 0. + * + * Implements GARP algorithm from Chapter 8 of Ray Tracing Gems (2019) + * + * \return true iff the line intersects the bilinear patch, otherwise false. + */ +AXOM_HOST_DEVICE +inline bool intersect_line_bilinear_patch(const Line& line, + const Point3& p0, + const Point3& p1, + const Point3& p2, + const Point3& p3, + axom::Array& u, + axom::Array& v, + axom::Array& t, + bool isRay = false) +{ + Vector3 q00(p0), q10(p1), q11(p2), q01(p3); + + Vector3 e10 = q10 - q00; + Vector3 e11 = q11 - q10; + Vector3 e00 = q01 - q00; + + Vector3 qn = Vector3::cross_product(e10, q01 - q11); + + q00.array() -= line.origin().array(); + q10.array() -= line.origin().array(); + + double a = Vector3::scalar_triple_product(q00, line.direction(), e00); + double c = Vector3::dot_product(qn, line.direction()); + double b = Vector3::scalar_triple_product(q10, line.direction(), e11) - a - c; + + double det = b * b - 4 * a * c; + if(det < 0) + { + return false; + } + + det = std::sqrt(det); + double u1, u2; + if(c == 0) + { + u1 = -a / b; + u2 = -1; + } + else + { + u1 = 0.5 * (-b - std::copysign(det, b)); + u2 = a / u1; + u1 /= c; + } + + if(0.0 <= u1 && u1 <= 1.0) + { + Vector3 pa = (1 - u1) * q00 + u1 * q10; + Vector3 pb = (1 - u1) * e00 + u1 * e11; // actually stores pb - pa + Vector3 n = Vector3::cross_product(line.direction(), pb); + det = Vector3::dot_product(n, n); + + if( det != 0 ) + { + n = Vector3::cross_product(n, pa); + double t1 = Vector3::dot_product(n, pb); + double v1 = Vector3::dot_product(n, line.direction()); + if(0 <= v1 && v1 <= det) + { + if(t1 >= 0 || !isRay) + { + t.push_back(t1 / det); + u.push_back(u1); + v.push_back(v1 / det); + } + } + } + else // Ray is parallel to the line segment pa + v * (pb - pa) + { + // Determine if the line is colinear to the segment + double cross = Vector3::cross_product( pa, line.direction() ).norm(); + if( cross == 0 ) + { + // Parameters of intersection are non-unique, + // so take the smallest magnitude t parameter as the intersection + double t1 = Vector3::dot_product( pa, line.direction() ); + double t2 = Vector3::dot_product( pa + pb, line.direction() ); + if( t1 * t2 < 0 ) + { + // Means the origin is inside the segment + t.push_back(0.0); + u.push_back(u1); + v.push_back(t1 / (t1 - t2)); + return true; + } + else if( t1 >= 0 ) + { + // The origin is outside the segment, but the ray intersects + t.push_back(t1); + u.push_back(u1); + v.push_back(0.0); + return true; + } + else if( !isRay ) + { + // The origin is outside the segment and the ray doesn't intersect + auto isSmaller = std::abs(t1) < std::abs(t2); + t.push_back(isSmaller ? t1 : t2); + u.push_back(u1); + v.push_back(isSmaller ? 0.0 : 1.0); + return true; + } + } + } + } + + if(0.0 <= u2 && u2 <= 1.0) + { + Vector3 pa = (1 - u2) * q00 + u2 * q10; + Vector3 pb = (1 - u2) * e00 + u2 * e11; // actually stores pb - pa + Vector3 n = Vector3::cross_product(line.direction(), pb); + det = Vector3::dot_product(n, n); + + if( det != 0 ) + { + n = Vector3::cross_product(n, pa); + double t2 = Vector3::dot_product(n, pb) / det; + double v2 = Vector3::dot_product(n, line.direction()); + if(0 <= v2 && v2 <= det && t2 >= 0) + { + if( t2 >= 0 || !isRay) + { + t.push_back(t2); + u.push_back(u2); + v.push_back(v2 / det); + } + } + } + else // Ray is parallel to the line segment pa + v * (pb - pa) + { + // If the line is colinear to the segment, it + // will have been handled in the u1 case, as u1 == u2 + } + } + + return !t.empty(); +} + } // end namespace detail } // end namespace primal } // end namespace axom diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index a6d070bc71..dccebf0293 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -23,11 +23,13 @@ #include "axom/primal/geometry/Point.hpp" #include "axom/primal/geometry/Polygon.hpp" #include "axom/primal/geometry/Ray.hpp" +#include "axom/primal/geometry/Line.hpp" #include "axom/primal/geometry/Segment.hpp" #include "axom/primal/geometry/Sphere.hpp" #include "axom/primal/geometry/Tetrahedron.hpp" #include "axom/primal/geometry/Triangle.hpp" #include "axom/primal/geometry/BezierCurve.hpp" +#include "axom/primal/geometry/BezierPatch.hpp" #include "axom/primal/operators/detail/intersect_impl.hpp" #include "axom/primal/operators/detail/intersect_ray_impl.hpp" @@ -630,6 +632,50 @@ AXOM_HOST_DEVICE bool intersect(const Plane& p, /// @} +/*! \brief Determines if a ray intersects a Bezier patch. + * \param [in] patch The Bezier patch to intersect with the ray. + * \param [in] ray The ray to intersect with the patch. + * \param [out] u The u parameter(s) of intersection point(s). + * \param [out] v The v parameter(s) of intersection point(s). + * \param [out] t The t parameter(s) of intersection point(s). + * \param [in] EPS The tolerance for intersection. + * + * For bilinear patches, implements GARP algorithm from Chapter 8 of Ray Tracing Gems (2019) + * For higher order patches, intersections are found through recursive subdivison + * until the subpatch is approximated by a bilinear patch. + * + * \return true iff the ray intersects the patch, otherwise false. + */ +template +AXOM_HOST_DEVICE bool intersect(const Ray& ray, + const BezierPatch& patch, + axom::Array& u, + axom::Array& v, + axom::Array& t) +{ + const int order_u = patch.getOrder_u(); + const int order_v = patch.getOrder_v(); + + if(order_u < 1 || order_v < 1) + { + // Patch has no surface area, ergo no intersections + return false; + } + else if(order_u == 1 && order_v == 1) + { + primal::Line line(ray.origin(), ray.direction()); + return detail::intersect_line_bilinear_patch(line, + patch(0, 0), + patch(order_u, 0), + patch(order_u, order_v), + patch(0, order_v), + u, + v, + t, + true); + } +} + } // namespace primal } // namespace axom diff --git a/src/axom/primal/tests/CMakeLists.txt b/src/axom/primal/tests/CMakeLists.txt index 00b3fe5bdc..2ef44828c4 100644 --- a/src/axom/primal/tests/CMakeLists.txt +++ b/src/axom/primal/tests/CMakeLists.txt @@ -36,6 +36,7 @@ set( primal_tests primal_sphere.cpp primal_split.cpp primal_squared_distance.cpp + primal_surface_intersect.cpp primal_tetrahedron.cpp primal_octahedron.cpp primal_triangle.cpp From 99ea55afa95b0e389e600ecf405ffdbd06285b0c Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 20 Nov 2024 01:52:10 -0700 Subject: [PATCH 04/47] Add new tests + fix old ones --- .../primal/tests/primal_bezier_intersect.cpp | 4 +- .../primal/tests/primal_surface_intersect.cpp | 275 ++++++++++++++++++ 2 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/axom/primal/tests/primal_surface_intersect.cpp diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index 9529605cdb..9dfd58d394 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -533,8 +533,8 @@ void checkIntersectionsRay(const primal::Ray& ray, for(int i = 0; i < num_actual_intersections; ++i) { - // EXPECT_NEAR(exp_s[i], s[i], test_eps); - // EXPECT_NEAR(exp_t[i], t[i], test_eps); + EXPECT_NEAR(exp_s[i], s[i], test_eps); + EXPECT_NEAR(exp_t[i], t[i], test_eps); if(shouldPrintIntersections) { diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp new file mode 100644 index 0000000000..50df231413 --- /dev/null +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -0,0 +1,275 @@ +// Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/*! + * \file primal_surface_intersect.cpp + * \brief This file tests surface (bilinear/patch/NURBS) intersection routines + */ + +#include "gtest/gtest.h" + +#include "axom/config.hpp" +#include "axom/slic.hpp" + +#include "axom/primal/geometry/BezierPatch.hpp" +#include "axom/primal/operators/intersect.hpp" + +#include +#include +#include +#include + +namespace primal = axom::primal; + +/** + * Helper function to compute the intersections of a Bezier patch and a ray + * and check that their intersection points match our expectations. + * Patch parameters are stored in \a exp_u, \a exp_v and \a exp_t. + * Intersections are computed within tolerance \a eps and our checks use \a test_eps. + * + * Param \a shouldPrintIntersections is used for debugging and for generating + * the initial array of expected intersections. + */ +template +void checkIntersections(const primal::Ray& ray, + const primal::BezierPatch& patch, + const axom::Array& exp_u, + const axom::Array& exp_v, + const axom::Array& exp_t, + double eps, + double test_eps, + bool shouldPrintIntersections = false) +{ + constexpr int DIM = 3; + using Array = axom::Array; + + // Check validity of input data. + // They should have the same size + EXPECT_EQ(exp_u.size(), exp_v.size()); + EXPECT_EQ(exp_u.size(), exp_t.size()); + + const int num_exp_intersections = static_cast(exp_u.size()); + const bool exp_intersect = (num_exp_intersections > 0); + + // Intersect the ray and the patch, intersection parameters will be + // in arrays (u, v) and t, for the patch and ray, respectively + Array u, v, t; + bool ray_intersects = intersect(ray, patch, u, v, t); + EXPECT_EQ(exp_intersect, ray_intersects); + EXPECT_EQ(u.size(), v.size()); + EXPECT_EQ(u.size(), t.size()); + + // check that we found the expected number of intersection points + const int num_actual_intersections = static_cast(u.size()); + EXPECT_EQ(num_exp_intersections, num_actual_intersections); + + // check that the evaluated intersection points are identical + for(int i = 0; i < num_actual_intersections; ++i) + { + auto p1 = ray.at(t[i]); + auto p2 = patch.evaluate(u[i], v[i]); + + EXPECT_NEAR(0., primal::squared_distance(p1, p2), test_eps); + + for(int d = 0; d < DIM; ++d) + { + EXPECT_NEAR(p1[d], p2[d], test_eps); + } + } + + if(shouldPrintIntersections) + { + std::stringstream sstr; + + sstr << "Intersections for ray and patch: " + << "\n\t" << ray << "\n\t" << patch; + + sstr << "\ns (" << u.size() << "): "; + for(auto i = 0u; i < u.size(); ++i) + { + sstr << std::setprecision(16) << "(" << u[i] << "," << v[i] << "),"; + } + + sstr << "\nt (" << t.size() << "): "; + for(auto i = 0u; i < t.size(); ++i) + { + sstr << std::setprecision(16) << t[i] << ","; + } + + SLIC_INFO(sstr.str()); + } + + for(int i = 0; i < num_actual_intersections; ++i) + { + EXPECT_NEAR(exp_u[i], u[i], test_eps); + EXPECT_NEAR(exp_v[i], v[i], test_eps); + EXPECT_NEAR(exp_t[i], t[i], test_eps); + + if(shouldPrintIntersections) + { + SLIC_INFO("\t" << i << ": {u:" << u[i] << ", v:" << v[i] + << std::setprecision(16) << ", t:" << t[i] + << ", u_actual:" << exp_u[i] << ", v_actual:" << exp_v[i] + << ", t_actual:" << exp_t[i] << "}"); + } + } +} + +//------------------------------------------------------------------------------ +TEST(primal_surface_inter, bilinear_intersect) +{ + static const int DIM = 3; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierPatchType = primal::BezierPatch; + using RayType = primal::Ray; + + const double eps = 1E-16; + const double eps_test = 1E-10; + + SLIC_INFO("primal: testing bilinear patch intersection"); + + // Set control points + BezierPatchType bilinear_patch(1, 1); + bilinear_patch(0, 0) = PointType({-1.0, 1.0, 1.0}); + bilinear_patch(1, 0) = PointType({-1.0, -1.0, 2.0}); + bilinear_patch(1, 1) = PointType({1.0, -1.0, 1.0}); + bilinear_patch(0, 1) = PointType({1.0, 1.0, 2.0}); + + // Ray with single intersection + PointType ray_origin({0.0, 0.0, 1.75}); + VectorType ray_direction({1.0, 1.0, 0.0}); + RayType ray(ray_origin, ray_direction); + + checkIntersections(ray, + bilinear_patch, + {0.146446609407}, + {0.853553390593}, + {1.0}, + eps, + eps_test); + + // Ray with no intersections + ray_direction = VectorType({1.0, -1.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + // Ray with no intersections, but in a way that is difficult for + // the standard GARP implementation + ray_direction = VectorType({1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + // Ray with two intersections + ray_origin = PointType({-1.0, -1.0, 1.75}); + ray_direction = VectorType({1.0, 1.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, + bilinear_patch, + {0.853553390593, 0.146446609407}, + {0.146446609407, 0.853553390593}, + {0.414213562373, 2.41421356237}, + eps, + eps_test); + + // Ray with infinitely many intersections + ray_origin = PointType({-2.0, 0.0, 1.5}); + ray_direction = VectorType({1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {0.5}, {0.0}, {1.0}, eps, eps_test); + + // Ray with no intersections on line with infinitely many intersections + ray_origin = PointType({2.0, 0.0, 1.5}); + ray_direction = VectorType({1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + // Ray with infinitely many intersections, but the origin is on the patch + ray_origin = PointType({0.4, 0.0, 1.5}); + ray_direction = VectorType({1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {0.5}, {0.7}, {0.0}, eps, eps_test); +} + +//------------------------------------------------------------------------------ +TEST(primal_surface_inter, flat_bilinear_intersect) +{ + static const int DIM = 3; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierPatchType = primal::BezierPatch; + using RayType = primal::Ray; + + const double eps = 1E-16; + const double eps_test = 1E-10; + + SLIC_INFO("primal: testing bilinear patch intersection"); + + // Set control points + BezierPatchType bilinear_patch(1, 1); + bilinear_patch(0, 0) = PointType({-2.0, 1.0, 1.0}); + bilinear_patch(1, 0) = PointType({-1.0, -1.0, 1.0}); + bilinear_patch(1, 1) = PointType({1.0, -1.0, 1.0}); + bilinear_patch(0, 1) = PointType({2.0, 1.0, 1.0}); + + // Ray with single intersection + PointType ray_origin({0.0, 0.0, 2.0}); + VectorType ray_direction({1, 0.5, -1.0}); + RayType ray(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {0.25}, {11. / 14.}, {1.5}, eps, eps_test); + + // Ray with a single intersection that is coplanar with an isocurve + ray_direction = VectorType({1.0, 0.0, -1.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {0.5}, {5. / 6.}, {sqrt(2)}, eps, eps_test); + + // Ray with no intersections + ray_direction = VectorType({1.0, -1.0, -0.5}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + // Ray with no intersections that is parallel to the patch + ray_direction = VectorType({1.0, 1.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + // Ray with no intersections that is parallel to the patch along an isocurve + ray_direction = VectorType({1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + // Ray with no intersections on a line with infinitely many intersections + ray_origin = PointType({-2.0, 0.5, 1}); + ray_direction = VectorType({-1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + // Ray with infinitely many intersections + ray_origin = PointType({-2.0, 0.0, 1.0}); + ray_direction = VectorType({1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + + // checkIntersections(ray, bilinear_patch, {0.25}, {0.0}, {0.25}, eps, eps_test); +} + +int main(int argc, char* argv[]) +{ + int result = 0; + + ::testing::InitGoogleTest(&argc, argv); + axom::slic::SimpleLogger logger; // create & initialize test logger, + + result = RUN_ALL_TESTS(); + + return result; +} From 38c2c1ba7aabc6b07a4e0a6e265f30aae8877518 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Thu, 21 Nov 2024 11:51:20 -0700 Subject: [PATCH 05/47] Add some comments and weirder tests --- .../operators/detail/intersect_impl.hpp | 49 ++++++++++++------- .../primal/tests/primal_surface_intersect.cpp | 37 ++++++++++++++ 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index 2473c2e3cd..af52fe6ec2 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -1725,6 +1725,8 @@ inline bool intersect_line_bilinear_patch(const Line& line, q00.array() -= line.origin().array(); q10.array() -= line.origin().array(); + // Solve a quadratic to find the parameters u0 of the B(u0, v) isocurves + // that are closest to the line double a = Vector3::scalar_triple_product(q00, line.direction(), e00); double c = Vector3::dot_product(qn, line.direction()); double b = Vector3::scalar_triple_product(q10, line.direction(), e11) - a - c; @@ -1739,8 +1741,17 @@ inline bool intersect_line_bilinear_patch(const Line& line, double u1, u2; if(c == 0) { - u1 = -a / b; - u2 = -1; + // If c == 0, there can only be one intersection + if(b != 0) + { + u1 = -a / b; + u2 = -1; + } + else // If b == 0 too, then the line is either coplanar with all v isocurves + { // or coplanar with the entire patch + + + } } else { @@ -1752,11 +1763,11 @@ inline bool intersect_line_bilinear_patch(const Line& line, if(0.0 <= u1 && u1 <= 1.0) { Vector3 pa = (1 - u1) * q00 + u1 * q10; - Vector3 pb = (1 - u1) * e00 + u1 * e11; // actually stores pb - pa + Vector3 pb = (1 - u1) * e00 + u1 * e11; // actually stores pb - pa Vector3 n = Vector3::cross_product(line.direction(), pb); det = Vector3::dot_product(n, n); - if( det != 0 ) + if(det != 0) { n = Vector3::cross_product(n, pa); double t1 = Vector3::dot_product(n, pb); @@ -1771,17 +1782,17 @@ inline bool intersect_line_bilinear_patch(const Line& line, } } } - else // Ray is parallel to the line segment pa + v * (pb - pa) + else // Ray is parallel to the line segment pa + v * (pb - pa) { // Determine if the line is colinear to the segment - double cross = Vector3::cross_product( pa, line.direction() ).norm(); - if( cross == 0 ) + double cross = Vector3::cross_product(pa, line.direction()).norm(); + if(cross == 0) { // Parameters of intersection are non-unique, // so take the smallest magnitude t parameter as the intersection - double t1 = Vector3::dot_product( pa, line.direction() ); - double t2 = Vector3::dot_product( pa + pb, line.direction() ); - if( t1 * t2 < 0 ) + double t1 = Vector3::dot_product(pa, line.direction()); + double t2 = Vector3::dot_product(pa + pb, line.direction()); + if(t1 * t2 < 0) { // Means the origin is inside the segment t.push_back(0.0); @@ -1789,15 +1800,15 @@ inline bool intersect_line_bilinear_patch(const Line& line, v.push_back(t1 / (t1 - t2)); return true; } - else if( t1 >= 0 ) + else if(t1 >= 0) { // The origin is outside the segment, but the ray intersects t.push_back(t1); u.push_back(u1); - v.push_back(0.0); + v.push_back(0.0); return true; } - else if( !isRay ) + else if(!isRay) { // The origin is outside the segment and the ray doesn't intersect auto isSmaller = std::abs(t1) < std::abs(t2); @@ -1807,24 +1818,24 @@ inline bool intersect_line_bilinear_patch(const Line& line, return true; } } - } + } } if(0.0 <= u2 && u2 <= 1.0) { Vector3 pa = (1 - u2) * q00 + u2 * q10; - Vector3 pb = (1 - u2) * e00 + u2 * e11; // actually stores pb - pa + Vector3 pb = (1 - u2) * e00 + u2 * e11; // actually stores pb - pa Vector3 n = Vector3::cross_product(line.direction(), pb); det = Vector3::dot_product(n, n); - if( det != 0 ) + if(det != 0) { n = Vector3::cross_product(n, pa); double t2 = Vector3::dot_product(n, pb) / det; double v2 = Vector3::dot_product(n, line.direction()); if(0 <= v2 && v2 <= det && t2 >= 0) { - if( t2 >= 0 || !isRay) + if(t2 >= 0 || !isRay) { t.push_back(t2); u.push_back(u2); @@ -1832,9 +1843,9 @@ inline bool intersect_line_bilinear_patch(const Line& line, } } } - else // Ray is parallel to the line segment pa + v * (pb - pa) + else // Ray is parallel to the line segment pa + v * (pb - pa) { - // If the line is colinear to the segment, it + // If the line is colinear to the segment, it // will have been handled in the u1 case, as u1 == u2 } } diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp index 50df231413..53f6334b74 100644 --- a/src/axom/primal/tests/primal_surface_intersect.cpp +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -198,6 +198,43 @@ TEST(primal_surface_inter, bilinear_intersect) checkIntersections(ray, bilinear_patch, {0.5}, {0.7}, {0.0}, eps, eps_test); } +//------------------------------------------------------------------------------ +TEST(primal_surface_inter, difficult_garp_case) +{ + static const int DIM = 3; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierPatchType = primal::BezierPatch; + using RayType = primal::Ray; + + const double eps = 1E-16; + const double eps_test = 1E-10; + + SLIC_INFO("primal: testing bilinear patch intersection"); + + // Set control points + BezierPatchType bilinear_patch(1, 1); + bilinear_patch(0, 0) = PointType({-2.0, 1.0, 2.0}); + bilinear_patch(1, 0) = PointType({-1.0, -1.0, 1.0}); + bilinear_patch(1, 1) = PointType({1.0, -1.0, 1.0}); + bilinear_patch(0, 1) = PointType({2.0, 1.0, 1.0}); + + VectorType ray_direction({-1.0, -2.0, 0.0}); + // The first step of the GARP algorithm is to solve a quadratic equation a + bt + ct^2 = 0, + // and this configuration of patch + ray direction is such that c = 0 + + // Ray with single intersection + PointType ray_origin({0.0, 1.0, 1.25}); + RayType ray(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {2./3.}, {0.25}, {sqrt(20. / 9.)}, eps, eps_test); + + // Ray with no intersections + ray_origin = PointType({0.0, 1.0, 1.75}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); +} + //------------------------------------------------------------------------------ TEST(primal_surface_inter, flat_bilinear_intersect) { From b40a8b67caa623ae36a7501647633c84ac8b898e Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Fri, 22 Nov 2024 14:04:46 -0700 Subject: [PATCH 06/47] Ineffective fix of issue --- .../primal/operators/detail/intersect_impl.hpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index af52fe6ec2..ef78d7b40f 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -1739,6 +1739,7 @@ inline bool intersect_line_bilinear_patch(const Line& line, det = std::sqrt(det); double u1, u2; + if(c == 0) { // If c == 0, there can only be one intersection @@ -1749,8 +1750,19 @@ inline bool intersect_line_bilinear_patch(const Line& line, } else // If b == 0 too, then the line is either coplanar with all v isocurves { // or coplanar with the entire patch - + if( Vector3::dot_product(q00, qn) != 0 ) + { + return false; // Line does not intersect with patch + } + else + { + // This case is exceptionally rare for well-posed models, so we + // tackle it relatively inefficiently + // Project the 3D shapes onto the plane of the patch + + + } } } else From 2bcf272c1c6d9f597126f2dc45a77782721d6dad Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sat, 23 Nov 2024 01:06:02 -0700 Subject: [PATCH 07/47] Change algorithm to remove directional dependence --- .../operators/detail/intersect_impl.hpp | 247 ++++++++++++------ .../primal/tests/primal_surface_intersect.cpp | 21 +- 2 files changed, 191 insertions(+), 77 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index ef78d7b40f..3bfbe9317f 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -1731,69 +1731,198 @@ inline bool intersect_line_bilinear_patch(const Line& line, double c = Vector3::dot_product(qn, line.direction()); double b = Vector3::scalar_triple_product(q10, line.direction(), e11) - a - c; - double det = b * b - 4 * a * c; - if(det < 0) + // Decide what to do based on the coefficients + if(b != 0.0 || c != 0.0) { - return false; - } - - det = std::sqrt(det); - double u1, u2; + double u1, u2; - if(c == 0) - { - // If c == 0, there can only be one intersection - if(b != 0) + // Decide what to do based on the discriminant + double det = b * b - 4 * a * c; + if(det < 0) // No solutions + { + return false; + } + else if(c == 0) // Quadratic is a line { u1 = -a / b; u2 = -1; } - else // If b == 0 too, then the line is either coplanar with all v isocurves - { // or coplanar with the entire patch - if( Vector3::dot_product(q00, qn) != 0 ) + else if(det == 0) // One repeated solution + { + u1 = -b / (2 * c); + u2 = -1; + } + else + { + u1 = 0.5 * (-b - std::copysign(std::sqrt(det), b)); + u2 = a / u1; + u1 /= c; + } + + // Find the point on the isocurve that is closest to the ray + for(auto u0 : {u1, u2}) + { + if(u0 < 0 || u0 > 1) continue; + + Vector3 pa = (1 - u0) * q00 + u0 * q10; + Vector3 pb = (1 - u0) * e00 + u0 * e11; // actually stores pb - pa + Vector3 n = Vector3::cross_product(line.direction(), pb); + det = Vector3::dot_product(n, n); + + if(det != 0) { - return false; // Line does not intersect with patch - } - else + n = Vector3::cross_product(n, pa); + double t0 = Vector3::dot_product(n, pb) / det; + double v0 = Vector3::dot_product(n, line.direction()) / det; + if(0.0 <= v0 && v0 <= 1.0) + { + if(t0 >= 0 || !isRay) + { + t.push_back(t0); + u.push_back(u0); + v.push_back(v0); + } + } + } + else // Ray is parallel to the line segment pa + v * (pb - pa) { - // This case is exceptionally rare for well-posed models, so we - // tackle it relatively inefficiently - - // Project the 3D shapes onto the plane of the patch - - + // Determine if the line is colinear to the segment + double cross = Vector3::cross_product(pa, line.direction()).norm(); + if(cross == 0) + { + // Parameters of intersection are non-unique, + // so take the center of the segment the intersection + // (this avoids any inclusion issues at the boundary) + double t1 = Vector3::dot_product(pa, line.direction()); + double t2 = Vector3::dot_product(pa + pb, line.direction()); + if(t1 * t2 < 0) + { + // Means the origin is inside the segment + t.push_back(0.5 * ((isRay ? 0.0 : t1) + t2)); + u.push_back(u0); + v.push_back(isRay ? (t1 - 0.5 * t2) / (t1 - t2) : 0.5); + return true; + } + else if(t1 >= 0 || !isRay) + { + // The origin is outside the segment, but the ray intersects + // (the line always intersects in this case) + t.push_back(0.5 * (t1 + t2)); + u.push_back(u0); + v.push_back(0.5); + return true; + } + } } } } else { - u1 = 0.5 * (-b - std::copysign(det, b)); - u2 = a / u1; - u1 /= c; - } + // Switch to finding B(u, v0) isocurves instead - if(0.0 <= u1 && u1 <= 1.0) - { - Vector3 pa = (1 - u1) * q00 + u1 * q10; - Vector3 pb = (1 - u1) * e00 + u1 * e11; // actually stores pb - pa - Vector3 n = Vector3::cross_product(line.direction(), pb); - det = Vector3::dot_product(n, n); + // Recalculate the quadratic coefficients + Vector3 e01 = q11 - q01; + Vector3 qm = Vector3::cross_product(e00, -e11); + q01.array() -= line.origin().array(); + + a = Vector3::scalar_triple_product(q00, line.direction(), e10); + c = Vector3::dot_product(qm, line.direction()); + b = Vector3::scalar_triple_product(q01, line.direction(), e01) - a - c; + + // If these are also all zero, weep bitterly + if(b == 0 && c == 0) + { + std::cout << "WEEPING BITTERLY" << std::endl; + return false; + } - if(det != 0) + double v1, v2; + + // Decide what to do based on the discriminant + double det = b * b - 4 * a * c; + if(det < 0) // No solutions + { + return false; + } + else if(c == 0) // Quadratic is a line + { + v1 = -a / b; + v2 = -1; + } + else if(det == 0) // One repeated solution { - n = Vector3::cross_product(n, pa); - double t1 = Vector3::dot_product(n, pb); - double v1 = Vector3::dot_product(n, line.direction()); - if(0 <= v1 && v1 <= det) + v1 = -b / (2 * c); + v2 = -1; + } + else + { + v1 = 0.5 * (-b - std::copysign(std::sqrt(det), b)); + v2 = a / v1; + v1 /= c; + } + + // Find the point on the isocurve that is closest to the ray + for(auto v0 : {v1, v2}) + { + if(v0 < 0 || v0 > 1) continue; + + Vector3 pa = (1 - v0) * q00 + v0 * q01; + Vector3 pb = (1 - v0) * e10 + v0 * e01; // actually stores pb - pa + Vector3 n = Vector3::cross_product(line.direction(), pb); + det = Vector3::dot_product(n, n); + + if(det != 0) { - if(t1 >= 0 || !isRay) + n = Vector3::cross_product(n, pa); + double t0 = Vector3::dot_product(n, pb) / det; + double u0 = Vector3::dot_product(n, line.direction()) / det; + if(0.0 <= u0 && u0 <= 1.0) { - t.push_back(t1 / det); - u.push_back(u1); - v.push_back(v1 / det); + if(t0 >= 0 || !isRay) + { + t.push_back(t0); + u.push_back(u0); + v.push_back(v0); + } + } + } + else // Ray is parallel to the line segment pa + u * (pb - pa) + { + // Determine if the line is colinear to the segment + double cross = Vector3::cross_product(pa, line.direction()).norm(); + if(cross == 0) + { + // Parameters of intersection are non-unique, + // so take the center of the segment the intersection + // (this avoids any inclusion issues at the boundary) + double t1 = Vector3::dot_product(pa, line.direction()); + double t2 = Vector3::dot_product(pa + pb, line.direction()); + if(t1 * t2 < 0) + { + // Means the origin is inside the segment + t.push_back(0.5 * ((isRay ? 0.0 : t1) + t2)); + u.push_back(isRay ? (t1 - 0.5 * t2) / (t1 - t2) : 0.5); + v.push_back(v0); + return true; + } + else if(t1 >= 0 || !isRay) + { + // The origin is outside the segment, but the ray intersects + // (the line always intersects in this case) + t.push_back(0.5 * (t1 + t2)); + u.push_back(0.5); + v.push_back(v0); + return true; + } } } } + } + + return !t.empty(); +} + +/* else // Ray is parallel to the line segment pa + v * (pb - pa) { // Determine if the line is colinear to the segment @@ -1831,39 +1960,7 @@ inline bool intersect_line_bilinear_patch(const Line& line, } } } - } - - if(0.0 <= u2 && u2 <= 1.0) - { - Vector3 pa = (1 - u2) * q00 + u2 * q10; - Vector3 pb = (1 - u2) * e00 + u2 * e11; // actually stores pb - pa - Vector3 n = Vector3::cross_product(line.direction(), pb); - det = Vector3::dot_product(n, n); - - if(det != 0) - { - n = Vector3::cross_product(n, pa); - double t2 = Vector3::dot_product(n, pb) / det; - double v2 = Vector3::dot_product(n, line.direction()); - if(0 <= v2 && v2 <= det && t2 >= 0) - { - if(t2 >= 0 || !isRay) - { - t.push_back(t2); - u.push_back(u2); - v.push_back(v2 / det); - } - } - } - else // Ray is parallel to the line segment pa + v * (pb - pa) - { - // If the line is colinear to the segment, it - // will have been handled in the u1 case, as u1 == u2 - } - } - - return !t.empty(); -} +*/ } // end namespace detail } // end namespace primal diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp index 53f6334b74..36bfdcd122 100644 --- a/src/axom/primal/tests/primal_surface_intersect.cpp +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -181,7 +181,7 @@ TEST(primal_surface_inter, bilinear_intersect) ray_direction = VectorType({1.0, 0.0, 0.0}); ray = RayType(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {0.5}, {0.0}, {1.0}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {0.5}, {0.5}, {2.0}, eps, eps_test); // Ray with no intersections on line with infinitely many intersections ray_origin = PointType({2.0, 0.0, 1.5}); @@ -195,7 +195,7 @@ TEST(primal_surface_inter, bilinear_intersect) ray_direction = VectorType({1.0, 0.0, 0.0}); ray = RayType(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {0.5}, {0.7}, {0.0}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {0.5}, {0.85}, {0.3}, eps, eps_test); } //------------------------------------------------------------------------------ @@ -233,6 +233,23 @@ TEST(primal_surface_inter, difficult_garp_case) ray_origin = PointType({0.0, 1.0, 1.75}); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + bilinear_patch(0, 0) = PointType({-1.0, 1.0, 1.0}); + bilinear_patch(1, 0) = PointType({-1.0, -1.0, 2.0}); + bilinear_patch(1, 1) = PointType({1.0, -1.0, 1.0}); + bilinear_patch(0, 1) = PointType({1.0, 1.0, 2.0}); + + // Double roots in the quadratic + + ray_origin = PointType({2.0, 0.0, 1.5}); + ray_direction = VectorType({-1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {0.5}, {0.5}, {2.0}, eps, eps_test); + + ray_origin = PointType({0.0, 2.0, 1.5}); + ray_direction = VectorType({0.0, -1.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {0.5}, {0.5}, {2.0}, eps, eps_test); } //------------------------------------------------------------------------------ From 45a73b42a1583a2632b51b9a07f9429d1079d70c Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Tue, 3 Dec 2024 10:55:07 -0700 Subject: [PATCH 08/47] Add skeleton for ray-patch intersection --- src/axom/primal/CMakeLists.txt | 1 + .../operators/detail/intersect_impl.hpp | 90 +++---- .../operators/detail/intersect_patch_impl.hpp | 228 ++++++++++++++++++ src/axom/primal/operators/intersect.hpp | 35 ++- 4 files changed, 291 insertions(+), 63 deletions(-) create mode 100644 src/axom/primal/operators/detail/intersect_patch_impl.hpp diff --git a/src/axom/primal/CMakeLists.txt b/src/axom/primal/CMakeLists.txt index cdeb1f5d00..0dbcb2c52f 100644 --- a/src/axom/primal/CMakeLists.txt +++ b/src/axom/primal/CMakeLists.txt @@ -66,6 +66,7 @@ set( primal_headers operators/detail/fuzzy_comparators.hpp operators/detail/intersect_bezier_impl.hpp operators/detail/intersect_bounding_box_impl.hpp + operators/detail/intersect_patch_impl.hpp operators/detail/intersect_impl.hpp operators/detail/intersect_ray_impl.hpp operators/detail/winding_number_impl.hpp diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index 3bfbe9317f..f12de37ec0 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -1701,6 +1701,10 @@ AXOM_HOST_DEVICE bool intersect_plane_tet3d(const Plane& p, * * Implements GARP algorithm from Chapter 8 of Ray Tracing Gems (2019) * + * \note A bilinear patch is parameterized in [0, 1) x [0, 1) + * + * \warning Always returns false if the line is coplanar to a planar polygon + * * \return true iff the line intersects the bilinear patch, otherwise false. */ AXOM_HOST_DEVICE @@ -1709,9 +1713,10 @@ inline bool intersect_line_bilinear_patch(const Line& line, const Point3& p1, const Point3& p2, const Point3& p3, + axom::Array& t, axom::Array& u, axom::Array& v, - axom::Array& t, + double EPS = 1e-8, bool isRay = false) { Vector3 q00(p0), q10(p1), q11(p2), q01(p3); @@ -1732,7 +1737,8 @@ inline bool intersect_line_bilinear_patch(const Line& line, double b = Vector3::scalar_triple_product(q10, line.direction(), e11) - a - c; // Decide what to do based on the coefficients - if(b != 0.0 || c != 0.0) + if(!axom::utilities::isNearlyEqual(b, 0.0, EPS) || + !axom::utilities::isNearlyEqual(c, 0.0, EPS)) { double u1, u2; @@ -1742,12 +1748,12 @@ inline bool intersect_line_bilinear_patch(const Line& line, { return false; } - else if(c == 0) // Quadratic is a line + else if(axom::utilities::isNearlyEqual(c, 0.0, EPS)) // Quadratic is a line { u1 = -a / b; u2 = -1; } - else if(det == 0) // One repeated solution + else if(axom::utilities::isNearlyEqual(det, 0.0, EPS)) // One repeated solution { u1 = -b / (2 * c); u2 = -1; @@ -1762,21 +1768,21 @@ inline bool intersect_line_bilinear_patch(const Line& line, // Find the point on the isocurve that is closest to the ray for(auto u0 : {u1, u2}) { - if(u0 < 0 || u0 > 1) continue; + if(u0 < 0.0 || u0 >= 1.0) continue; Vector3 pa = (1 - u0) * q00 + u0 * q10; Vector3 pb = (1 - u0) * e00 + u0 * e11; // actually stores pb - pa Vector3 n = Vector3::cross_product(line.direction(), pb); det = Vector3::dot_product(n, n); - if(det != 0) + if(!axom::utilities::isNearlyEqual(det, 0.0, EPS)) { n = Vector3::cross_product(n, pa); double t0 = Vector3::dot_product(n, pb) / det; double v0 = Vector3::dot_product(n, line.direction()) / det; - if(0.0 <= v0 && v0 <= 1.0) + if(0.0 <= v0 && v0 < 1.0) { - if(t0 >= 0 || !isRay) + if(t0 >= 0.0 || !isRay) { t.push_back(t0); u.push_back(u0); @@ -1788,7 +1794,7 @@ inline bool intersect_line_bilinear_patch(const Line& line, { // Determine if the line is colinear to the segment double cross = Vector3::cross_product(pa, line.direction()).norm(); - if(cross == 0) + if(axom::utilities::isNearlyEqual(cross, 0.0, EPS)) { // Parameters of intersection are non-unique, // so take the center of the segment the intersection @@ -1829,10 +1835,12 @@ inline bool intersect_line_bilinear_patch(const Line& line, c = Vector3::dot_product(qm, line.direction()); b = Vector3::scalar_triple_product(q01, line.direction(), e01) - a - c; - // If these are also all zero, weep bitterly - if(b == 0 && c == 0) + // If these are also all zero, then the ray is coplanar to a polygonal patch + if(axom::utilities::isNearlyEqual(b, 0.0, EPS) && + axom::utilities::isNearlyEqual(c, 0.0, EPS)) { - std::cout << "WEEPING BITTERLY" << std::endl; + // This case indicates the ray is tangent to the surface, + // which we don't count as an intersection return false; } @@ -1844,12 +1852,12 @@ inline bool intersect_line_bilinear_patch(const Line& line, { return false; } - else if(c == 0) // Quadratic is a line + else if(axom::utilities::isNearlyEqual(c, 0.0, EPS)) // Quadratic is a line { v1 = -a / b; v2 = -1; } - else if(det == 0) // One repeated solution + else if(axom::utilities::isNearlyEqual(det, 0.0, EPS)) // One repeated solution { v1 = -b / (2 * c); v2 = -1; @@ -1864,21 +1872,21 @@ inline bool intersect_line_bilinear_patch(const Line& line, // Find the point on the isocurve that is closest to the ray for(auto v0 : {v1, v2}) { - if(v0 < 0 || v0 > 1) continue; + if(v0 < 0.0 || v0 > 1.0) continue; - Vector3 pa = (1 - v0) * q00 + v0 * q01; - Vector3 pb = (1 - v0) * e10 + v0 * e01; // actually stores pb - pa + Vector3 pa = (1.0 - v0) * q00 + v0 * q01; + Vector3 pb = (1.0 - v0) * e10 + v0 * e01; // actually stores pb - pa Vector3 n = Vector3::cross_product(line.direction(), pb); det = Vector3::dot_product(n, n); - if(det != 0) + if(!axom::utilities::isNearlyEqual(det, 0.0, EPS)) { n = Vector3::cross_product(n, pa); double t0 = Vector3::dot_product(n, pb) / det; double u0 = Vector3::dot_product(n, line.direction()) / det; if(0.0 <= u0 && u0 <= 1.0) { - if(t0 >= 0 || !isRay) + if(t0 >= 0.0 || !isRay) { t.push_back(t0); u.push_back(u0); @@ -1890,7 +1898,7 @@ inline bool intersect_line_bilinear_patch(const Line& line, { // Determine if the line is colinear to the segment double cross = Vector3::cross_product(pa, line.direction()).norm(); - if(cross == 0) + if(axom::utilities::isNearlyEqual(cross, 0.0, EPS)) { // Parameters of intersection are non-unique, // so take the center of the segment the intersection @@ -1905,7 +1913,7 @@ inline bool intersect_line_bilinear_patch(const Line& line, v.push_back(v0); return true; } - else if(t1 >= 0 || !isRay) + else if(t1 >= 0.0 || !isRay) { // The origin is outside the segment, but the ray intersects // (the line always intersects in this case) @@ -1922,46 +1930,6 @@ inline bool intersect_line_bilinear_patch(const Line& line, return !t.empty(); } -/* - else // Ray is parallel to the line segment pa + v * (pb - pa) - { - // Determine if the line is colinear to the segment - double cross = Vector3::cross_product(pa, line.direction()).norm(); - if(cross == 0) - { - // Parameters of intersection are non-unique, - // so take the smallest magnitude t parameter as the intersection - double t1 = Vector3::dot_product(pa, line.direction()); - double t2 = Vector3::dot_product(pa + pb, line.direction()); - if(t1 * t2 < 0) - { - // Means the origin is inside the segment - t.push_back(0.0); - u.push_back(u1); - v.push_back(t1 / (t1 - t2)); - return true; - } - else if(t1 >= 0) - { - // The origin is outside the segment, but the ray intersects - t.push_back(t1); - u.push_back(u1); - v.push_back(0.0); - return true; - } - else if(!isRay) - { - // The origin is outside the segment and the ray doesn't intersect - auto isSmaller = std::abs(t1) < std::abs(t2); - t.push_back(isSmaller ? t1 : t2); - u.push_back(u1); - v.push_back(isSmaller ? 0.0 : 1.0); - return true; - } - } - } -*/ - } // end namespace detail } // end namespace primal } // end namespace axom diff --git a/src/axom/primal/operators/detail/intersect_patch_impl.hpp b/src/axom/primal/operators/detail/intersect_patch_impl.hpp new file mode 100644 index 0000000000..61d736ace6 --- /dev/null +++ b/src/axom/primal/operators/detail/intersect_patch_impl.hpp @@ -0,0 +1,228 @@ +// Copyright (c) 2017-2023, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/*! + * \file intersect_patch_impl.hpp + * + * This file provides helper functions for testing the intersection + * of rays and Bezier patches + */ + +#ifndef AXOM_PRIMAL_INTERSECT_PATCH_IMPL_HPP_ +#define AXOM_PRIMAL_INTERSECT_PATCH_IMPL_HPP_ + +#include "axom/primal/geometry/Point.hpp" +#include "axom/primal/geometry/Polygon.hpp" +#include "axom/primal/geometry/BoundingBox.hpp" +#include "axom/primal/geometry/BezierPatch.hpp" + +#include "axom/primal/operators/intersect.hpp" +#include "axom/primal/operators/in_polygon.hpp" +#include "axom/primal/operators/detail/intersect_impl.hpp" +#include "axom/primal/operators/detail/intersect_ray_impl.hpp" + +#include + +namespace axom +{ +namespace primal +{ +namespace detail +{ +//---------------------------- FUNCTION DECLARATIONS --------------------------- + +template +bool intersect_line_patch(const Line &line, + const BezierPatch &patch, + axom::Array &tp, + axom::Array &up, + axom::Array &vp, + double sq_tol, + double buffer, + int order_u, + int order_v, + double u_offset, + double u_scale, + double v_offset, + double v_scale, + double EPS, + bool isRay); + +//------------------------------ IMPLEMENTATIONS ------------------------------ + +template +bool intersect_line_patch(const Line &line, + const BezierPatch &patch, + axom::Array &tp, + axom::Array &up, + axom::Array &vp, + double sq_tol, + double buffer, + int order_u, + int order_v, + double u_offset, + double u_scale, + double v_offset, + double v_scale, + double EPS, + bool isRay) +{ + using BPatch = BezierPatch; + + // Check bounding box to short-circuit the intersection + // Need to expand the box a bit so that intersections near subdivision boundaries + // are accurately recorded + Point ip; + if(true)//!intersect(line, patch.boundingBox().scale(1.5), ip)) + { + return false; + } + + bool foundIntersection = false; + if(true) //patch.isBilinear(sq_tol)) + { + // Store candidate intersection points + axom::Array tc, uc, vc; + + foundIntersection = + detail::intersect_line_bilinear_patch(line, + patch(0, 0), + patch(order_u, 0), + patch(order_u, order_v), + patch(0, order_v), + tc, + uc, + vc, + EPS, + isRay); + + if(!foundIntersection) + { + return false; + } + + // This tolerance is in parameter space, so is independent of the patch + // constexpr double EPS = 1e-5; + + for(int i = 0; i < tc.size(); ++i) + { + const T t0 = tc[i]; + const T u0 = uc[i]; + const T v0 = vc[i]; + + if((u0 >= (u_offset == 0 ? -buffer / u_scale : 0) && + u0 <= 1.0 + (u_offset + u_scale == 1.0 ? buffer / v_scale : 0)) && + (v0 >= (v_offset == 0 ? -buffer / v_scale : 0) && + v0 <= 1.0 + (v_offset + v_scale == 1.0 ? buffer / u_scale : 0))) + { + // Extra check to avoid adding the same point twice if it's on the boundary of a subpatch + if(!(u_offset != 0.0 && axom::utilities::isNearlyEqual(u0, 0.0, EPS)) && + !(v_offset != 0.0 && axom::utilities::isNearlyEqual(v0, 0.0, EPS))) + { + if(t >= 0 || !isRay) + { + up.push_back(u_offset + u0 * u_scale); + vp.push_back(v_offset + v0 * v_scale); + tp.push_back(t0); + } + } + } + } + } + else + { + constexpr double splitVal = 0.5; + constexpr double scaleFac = 0.5; + + BPatch p1(order_u, order_v), p2(order_u, order_v), p3(order_u, order_v), + p4(order_u, order_v); + + patch.split(splitVal, splitVal, p1, p2, p3, p4); + u_scale *= scaleFac; + v_scale *= scaleFac; + + // Note: we want to find all intersections, so don't short-circuit + if(intersect_line_patch(line, + p1, + tp, + up, + vp, + sq_tol, + buffer, + order_u, + order_v, + u_offset, + u_scale, + v_offset, + v_scale, + EPS, + isRay)) + { + foundIntersection = true; + } + if(intersect_line_patch(line, + p2, + tp, + up, + vp, + sq_tol, + buffer, + order_u, + order_v, + u_offset + u_scale, + u_scale, + v_offset, + v_scale, + EPS, + isRay)) + { + foundIntersection = true; + } + if(intersect_line_patch(line, + p3, + tp, + up, + vp, + sq_tol, + buffer, + order_u, + order_v, + u_offset, + u_scale, + v_offset + v_scale, + v_scale, + EPS, + isRay)) + { + foundIntersection = true; + } + if(intersect_line_patch(line, + p4, + tp, + up, + vp, + sq_tol, + buffer, + order_u, + order_v, + u_offset + u_scale, + u_scale, + v_offset + v_scale, + v_scale, + EPS, + isRay)) + { + foundIntersection = true; + } + } + + return foundIntersection; +} + +} // end namespace detail +} // end namespace primal +} // end namespace axom + +#endif // AXOM_PRIMAL_INTERSECT_PATCH_IMPL_HPP_ diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index dccebf0293..2bbb50a81a 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -35,6 +35,7 @@ #include "axom/primal/operators/detail/intersect_ray_impl.hpp" #include "axom/primal/operators/detail/intersect_bounding_box_impl.hpp" #include "axom/primal/operators/detail/intersect_bezier_impl.hpp" +#include "axom/primal/operators/detail/intersect_patch_impl.hpp" namespace axom { @@ -643,19 +644,25 @@ AXOM_HOST_DEVICE bool intersect(const Plane& p, * For bilinear patches, implements GARP algorithm from Chapter 8 of Ray Tracing Gems (2019) * For higher order patches, intersections are found through recursive subdivison * until the subpatch is approximated by a bilinear patch. + * Assumes that the ray is not tangent to the patch * * \return true iff the ray intersects the patch, otherwise false. */ template AXOM_HOST_DEVICE bool intersect(const Ray& ray, const BezierPatch& patch, + axom::Array& t, axom::Array& u, axom::Array& v, - axom::Array& t) + double tol = 1e-8, + double EPS = 1e-8) { const int order_u = patch.getOrder_u(); const int order_v = patch.getOrder_v(); + // for efficiency, linearity check actually uses a squared tolerance + const double sq_tol = tol * tol; + if(order_u < 1 || order_v < 1) { // Patch has no surface area, ergo no intersections @@ -669,11 +676,35 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, patch(order_u, 0), patch(order_u, order_v), patch(0, order_v), + t, u, v, - t, + EPS, true); } + else + { + primal::Line line(ray.origin(), ray.direction()); + + double u_offset = 0., v_offset = 0.; + double u_scale = 1., v_scale = 1.; + + return detail::intersect_line_patch(line, + patch, + t, + u, + v, + sq_tol, + 0.0, + order_u, + order_v, + u_offset, + u_scale, + v_offset, + v_scale, + EPS, + true); + } } } // namespace primal From cab1cc80dab1d1877993c89542016c11130a5008 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Tue, 3 Dec 2024 10:56:40 -0700 Subject: [PATCH 09/47] Add line-bb routine --- .../operators/detail/intersect_ray_impl.hpp | 33 +++++++++++++++++++ src/axom/primal/operators/intersect.hpp | 24 ++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/axom/primal/operators/detail/intersect_ray_impl.hpp b/src/axom/primal/operators/detail/intersect_ray_impl.hpp index 25c744a976..8340bf5689 100644 --- a/src/axom/primal/operators/detail/intersect_ray_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_ray_impl.hpp @@ -323,6 +323,39 @@ AXOM_HOST_DEVICE inline bool intersect_ray( return intersects; } +template +AXOM_HOST_DEVICE inline bool intersect_line( + const primal::Line& L, + const primal::BoundingBox& bb, + primal::Point& ip, + T EPS = numerics::floating_point_limits::epsilon()) +{ + AXOM_STATIC_ASSERT(std::is_floating_point::value); + + T tmin = -axom::numerics::floating_point_limits::max(); + T tmax = axom::numerics::floating_point_limits::max(); + + bool intersects = true; + for(int d = 0; d < DIM; ++d) + { + intersects = intersects && + intersect_ray_bbox_test(L.origin()[d], + L.direction()[d], + bb.getMin()[d], + bb.getMax()[d], + tmin, + tmax, + EPS); + } + + if(intersects) + { + ip = L.at(tmin); + } + + return intersects; +} + } // namespace detail } // namespace primal } // namespace axom diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index 2bbb50a81a..4415e78c94 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -326,6 +326,30 @@ AXOM_HOST_DEVICE bool intersect(const Ray& R, return detail::intersect_ray(R, bb, ip); } +/*! + * \brief Computes the intersection of the given line, L, with the Box, bb. + * + * \param [in] L the specified line (two-sided ray) + * \param [in] bb the user-supplied axis-aligned bounding box + * + * \param [out] ip the intersection point where L intersects bb. + * + * \return status true iff bb intersects with R, otherwise, false. + * + * \see primal::Line + * \see primal::Segment + * \see primal::BoundingBox + * + * \note Computes Ray Box intersection using the slab method from pg 180 of + * Real Time Collision Detection by Christer Ericson. + */ +template +AXOM_HOST_DEVICE bool intersect(const Line& L, + const BoundingBox& bb, + Point& ip) +{ + return detail::intersect_line(L, bb, ip); +} /// @} /// \name Segment-BoundingBox Intersection Routines From 77c3c55443a79e46167f586b6e602c9731e5a7b7 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Tue, 3 Dec 2024 11:45:58 -0700 Subject: [PATCH 10/47] Add approximately-bilinear method --- src/axom/primal/geometry/BezierPatch.hpp | 80 ++++++++++++++++++++---- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/src/axom/primal/geometry/BezierPatch.hpp b/src/axom/primal/geometry/BezierPatch.hpp index 54fae15c09..d6a75ecaeb 100644 --- a/src/axom/primal/geometry/BezierPatch.hpp +++ b/src/axom/primal/geometry/BezierPatch.hpp @@ -1867,10 +1867,11 @@ class BezierPatch * This function checks if all control points of the BezierPatch * are approximately on the plane defined by its four corners * - * \param [in] tol Threshold for sum of squared distances + * \param [in] sq_tol Threshold for sum of squared distances + * \param [in] EPS Threshold for nearness to zero * \return True if c1 is near-planar */ - bool isPlanar(double tol = 1E-8) const + bool isPlanar(double sq_tol = 1E-8, double EPS = 1e-8) const { const int ord_u = getOrder_u(); const int ord_v = getOrder_v(); @@ -1895,7 +1896,7 @@ class BezierPatch if(!axom::utilities::isNearlyEqual( VectorType::scalar_triple_product(v1, v2, v3), 0.0, - tol)) + EPS)) { return false; } @@ -1915,9 +1916,9 @@ class BezierPatch double sqDist = 0.0; // Check all control points for simplicity - for(int p = 0; p <= ord_u && sqDist <= tol; ++p) + for(int p = 0; p <= ord_u && sqDist <= sq_tol; ++p) { - for(int q = ((p == 0) ? 1 : 0); q <= ord_v && sqDist <= tol; ++q) + for(int q = ((p == 0) ? 1 : 0); q <= ord_v && sqDist <= sq_tol; ++q) { const double signedDist = plane_normal.dot(m_controlPoints(p, q) - m_controlPoints(0, 0)); @@ -1925,19 +1926,20 @@ class BezierPatch } } - return (sqDist <= tol); + return (sqDist <= sq_tol); } /*! * \brief Predicate to check if the patch can be approximated by a polygon * * This function checks if a BezierPatch lies in a plane - * and that the edged are linear up to tolerance `tol` + * and that the edged are linear up to tolerance `sq_tol` * * \param [in] tol Threshold for sum of squared distances + * \param [in] EPS Threshold for nearness to zero * \return True if c1 is near-planar-polygonal */ - bool isPolygonal(double tol = 1E-8) const + bool isPolygonal(double sq_tol = 1E-8, double EPS = 1e-8) const { const int ord_u = getOrder_u(); const int ord_v = getOrder_v(); @@ -1956,25 +1958,25 @@ class BezierPatch } // Check if the patch is planar - if(!isPlanar(tol)) + if(!isPlanar(sq_tol, EPS)) { return false; } // Check if each bounding curve is linear - if(!isocurve_u(0).isLinear(tol)) + if(!isocurve_u(0).isLinear(sq_tol)) { return false; } - if(!isocurve_v(0).isLinear(tol)) + if(!isocurve_v(0).isLinear(sq_tol)) { return false; } - if(!isocurve_u(1).isLinear(tol)) + if(!isocurve_u(1).isLinear(sq_tol)) { return false; } - if(!isocurve_v(1).isLinear(tol)) + if(!isocurve_v(1).isLinear(sq_tol)) { return false; } @@ -1982,6 +1984,58 @@ class BezierPatch return true; } + /*! + * \brief Predicate to check if the Bezier patch is approximately bilinear + * + * This function checks if the non-corner control points of the patch form a grid + * with respect to the corner control points, up to a tolerance `sq_tol` + * + * \param [in] sq_tol Threshold for absolute squared distances + * \return True if patch is bilinear + */ + bool isBilinear(double tol = 1e-8) const + { + const int ord_u = getOrder_u(); + const int ord_v = getOrder_v(); + + if(ord_u <= 1 && ord_v <= 1) + { + return true; + } + + // Anonymous function to evaluate the bilinear patch defined by the corners + auto bilinear_patch = [&](T u, T v) -> PointType { + return lerp( + lerp(m_controlPoints(0, 0), m_controlPoints(0, ord_v), v), + lerp(m_controlPoints(ord_u, 0), m_controlPoints(ord_u, ord_v), v), + u); + }; + + for(int u = 0; u <= ord_u; ++u) + { + for(int v = 0; v <= ord_v; ++v) + { + // Don't need to check the corners + if((u == 0 && v == 0) || (u == 0 && v == ord_v) || + (u == ord_u && v == 0) || (u == ord_u && v == ord_v)) + { + continue; + } + + // Evaluate where the control point would be if the patch *was* bilinear + PointType bilinear_point = bilinear_patch(u / static_cast(ord_u), + v / static_cast(ord_v)); + + if(squared_distance(m_controlPoints(u, v), bilinear_point) > sq_tol) + { + return false; + } + } + } + + return true; + } + /*! * \brief Simple formatted print of a Bezier Patch instance * From 11542e1afc9639c6a4fcd2c932a6603a5caf6544 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Tue, 3 Dec 2024 16:17:11 -0700 Subject: [PATCH 11/47] Fix bilinear patch tolerances --- src/axom/primal/geometry/BezierPatch.hpp | 25 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/axom/primal/geometry/BezierPatch.hpp b/src/axom/primal/geometry/BezierPatch.hpp index d6a75ecaeb..b4c3f946c5 100644 --- a/src/axom/primal/geometry/BezierPatch.hpp +++ b/src/axom/primal/geometry/BezierPatch.hpp @@ -1993,7 +1993,7 @@ class BezierPatch * \param [in] sq_tol Threshold for absolute squared distances * \return True if patch is bilinear */ - bool isBilinear(double tol = 1e-8) const + bool isBilinear(double sq_tol = 1e-8) const { const int ord_u = getOrder_u(); const int ord_v = getOrder_v(); @@ -2005,10 +2005,19 @@ class BezierPatch // Anonymous function to evaluate the bilinear patch defined by the corners auto bilinear_patch = [&](T u, T v) -> PointType { - return lerp( - lerp(m_controlPoints(0, 0), m_controlPoints(0, ord_v), v), - lerp(m_controlPoints(ord_u, 0), m_controlPoints(ord_u, ord_v), v), - u); + PointType val; + for(int N = 0; N < NDIMS; ++N) + { + val[N] = axom::utilities::lerp( + axom::utilities::lerp(m_controlPoints(0, 0)[N], + m_controlPoints(0, ord_v)[N], + v), + axom::utilities::lerp(m_controlPoints(ord_u, 0)[N], + m_controlPoints(ord_u, ord_v)[N], + v), + u); + } + return val; }; for(int u = 0; u <= ord_u; ++u) @@ -2017,14 +2026,14 @@ class BezierPatch { // Don't need to check the corners if((u == 0 && v == 0) || (u == 0 && v == ord_v) || - (u == ord_u && v == 0) || (u == ord_u && v == ord_v)) + (u == ord_u && v == 0) || (u == ord_u && v == ord_v)) { continue; } // Evaluate where the control point would be if the patch *was* bilinear - PointType bilinear_point = bilinear_patch(u / static_cast(ord_u), - v / static_cast(ord_v)); + PointType bilinear_point = + bilinear_patch(u / static_cast(ord_u), v / static_cast(ord_v)); if(squared_distance(m_controlPoints(u, v), bilinear_point) > sq_tol) { From d65be3f789e71f2c2f7dce928786b0ced207dc0c Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Tue, 3 Dec 2024 16:17:38 -0700 Subject: [PATCH 12/47] Implements (error prone) subdivison method --- .../operators/detail/intersect_impl.hpp | 81 ++++++++++-------- .../operators/detail/intersect_patch_impl.hpp | 39 ++++----- src/axom/primal/operators/intersect.hpp | 82 ++++++++++++------- 3 files changed, 118 insertions(+), 84 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index f12de37ec0..45cdde04e5 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -1732,57 +1732,64 @@ inline bool intersect_line_bilinear_patch(const Line& line, // Solve a quadratic to find the parameters u0 of the B(u0, v) isocurves // that are closest to the line - double a = Vector3::scalar_triple_product(q00, line.direction(), e00); - double c = Vector3::dot_product(qn, line.direction()); - double b = Vector3::scalar_triple_product(q10, line.direction(), e11) - a - c; + double au = Vector3::scalar_triple_product(q00, line.direction(), e00); + double cu = Vector3::dot_product(qn, line.direction()); + double bu = + Vector3::scalar_triple_product(q10, line.direction(), e11) - au - cu; + + // Rescale the coefficients to avoid (some) numerical issues + double su = std::max(std::fabs(au), std::max(fabs(bu), fabs(cu))); + au /= su; + bu /= su; + cu /= su; // Decide what to do based on the coefficients - if(!axom::utilities::isNearlyEqual(b, 0.0, EPS) || - !axom::utilities::isNearlyEqual(c, 0.0, EPS)) + if(!axom::utilities::isNearlyEqual(su, 0.0, primal::PRIMAL_TINY)) { double u1, u2; // Decide what to do based on the discriminant - double det = b * b - 4 * a * c; + double det = bu * bu - 4 * au * cu; if(det < 0) // No solutions { return false; } - else if(axom::utilities::isNearlyEqual(c, 0.0, EPS)) // Quadratic is a line + else if(axom::utilities::isNearlyEqual(cu, 0.0, EPS)) // Quadratic is a line { - u1 = -a / b; + u1 = -au / bu; u2 = -1; } else if(axom::utilities::isNearlyEqual(det, 0.0, EPS)) // One repeated solution { - u1 = -b / (2 * c); + u1 = -bu / (2 * cu); u2 = -1; } else { - u1 = 0.5 * (-b - std::copysign(std::sqrt(det), b)); - u2 = a / u1; - u1 /= c; + u1 = 0.5 * (-bu - std::copysign(std::sqrt(det), bu)); + u2 = au / u1; + u1 /= cu; } // Find the point on the isocurve that is closest to the ray for(auto u0 : {u1, u2}) { - if(u0 < 0.0 || u0 >= 1.0) continue; + if(u0 < -EPS || u0 >= 1.0 + EPS) continue; Vector3 pa = (1 - u0) * q00 + u0 * q10; Vector3 pb = (1 - u0) * e00 + u0 * e11; // actually stores pb - pa Vector3 n = Vector3::cross_product(line.direction(), pb); det = Vector3::dot_product(n, n); - if(!axom::utilities::isNearlyEqual(det, 0.0, EPS)) + // Need a separate tolerance for this for the case of small patches + if(!axom::utilities::isNearlyEqual(det, 0.0, primal::PRIMAL_TINY)) { n = Vector3::cross_product(n, pa); double t0 = Vector3::dot_product(n, pb) / det; double v0 = Vector3::dot_product(n, line.direction()) / det; - if(0.0 <= v0 && v0 < 1.0) + if(-EPS <= v0 && v0 <= 1.0 + EPS) { - if(t0 >= 0.0 || !isRay) + if(t0 >= -EPS || !isRay) { t.push_back(t0); u.push_back(u0); @@ -1825,19 +1832,23 @@ inline bool intersect_line_bilinear_patch(const Line& line, else { // Switch to finding B(u, v0) isocurves instead - - // Recalculate the quadratic coefficients Vector3 e01 = q11 - q01; Vector3 qm = Vector3::cross_product(e00, -e11); q01.array() -= line.origin().array(); - a = Vector3::scalar_triple_product(q00, line.direction(), e10); - c = Vector3::dot_product(qm, line.direction()); - b = Vector3::scalar_triple_product(q01, line.direction(), e01) - a - c; + // Find the analogous coefficients for B(u, v0) isocurves + double av = Vector3::scalar_triple_product(q00, line.direction(), e10); + double cv = Vector3::dot_product(qm, line.direction()); + double bv = + Vector3::scalar_triple_product(q01, line.direction(), e01) - av - cv; + + double sv = std::max(std::fabs(av), std::max(fabs(bv), fabs(cv))); + av /= sv; + bv /= sv; + cv /= sv; // If these are also all zero, then the ray is coplanar to a polygonal patch - if(axom::utilities::isNearlyEqual(b, 0.0, EPS) && - axom::utilities::isNearlyEqual(c, 0.0, EPS)) + if(axom::utilities::isNearlyEqual(sv, 0.0, primal::PRIMAL_TINY)) { // This case indicates the ray is tangent to the surface, // which we don't count as an intersection @@ -1847,46 +1858,48 @@ inline bool intersect_line_bilinear_patch(const Line& line, double v1, v2; // Decide what to do based on the discriminant - double det = b * b - 4 * a * c; + double det = bv * bv - 4 * av * cv; if(det < 0) // No solutions { return false; } - else if(axom::utilities::isNearlyEqual(c, 0.0, EPS)) // Quadratic is a line + else if(axom::utilities::isNearlyEqual(cv, 0.0, EPS)) // Quadratic is a line { - v1 = -a / b; + v1 = -av / bv; v2 = -1; } else if(axom::utilities::isNearlyEqual(det, 0.0, EPS)) // One repeated solution { - v1 = -b / (2 * c); + v1 = -bv / (2 * cv); v2 = -1; } else { - v1 = 0.5 * (-b - std::copysign(std::sqrt(det), b)); - v2 = a / v1; - v1 /= c; + v1 = 0.5 * (-bv - std::copysign(std::sqrt(det), bv)); + v2 = av / v1; + v1 /= cv; } // Find the point on the isocurve that is closest to the ray for(auto v0 : {v1, v2}) { - if(v0 < 0.0 || v0 > 1.0) continue; + if(v0 < -EPS || v0 >= 1.0 + EPS) continue; Vector3 pa = (1.0 - v0) * q00 + v0 * q01; Vector3 pb = (1.0 - v0) * e10 + v0 * e01; // actually stores pb - pa Vector3 n = Vector3::cross_product(line.direction(), pb); det = Vector3::dot_product(n, n); - if(!axom::utilities::isNearlyEqual(det, 0.0, EPS)) + // Need a separate tolerance for this for the case of small patches + if(!axom::utilities::isNearlyEqual(det, 0.0, primal::PRIMAL_TINY)) { n = Vector3::cross_product(n, pa); double t0 = Vector3::dot_product(n, pb) / det; double u0 = Vector3::dot_product(n, line.direction()) / det; - if(0.0 <= u0 && u0 <= 1.0) + + if(-EPS <= u0 && u0 <= 1.0 + EPS) { - if(t0 >= 0.0 || !isRay) + if(t0 >= -EPS || !isRay) { t.push_back(t0); u.push_back(u0); diff --git a/src/axom/primal/operators/detail/intersect_patch_impl.hpp b/src/axom/primal/operators/detail/intersect_patch_impl.hpp index 61d736ace6..229bcac54a 100644 --- a/src/axom/primal/operators/detail/intersect_patch_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_patch_impl.hpp @@ -39,14 +39,13 @@ bool intersect_line_patch(const Line &line, axom::Array &tp, axom::Array &up, axom::Array &vp, - double sq_tol, - double buffer, int order_u, int order_v, double u_offset, double u_scale, double v_offset, double v_scale, + double sq_tol, double EPS, bool isRay); @@ -58,14 +57,13 @@ bool intersect_line_patch(const Line &line, axom::Array &tp, axom::Array &up, axom::Array &vp, - double sq_tol, - double buffer, int order_u, int order_v, double u_offset, double u_scale, double v_offset, double v_scale, + double sq_tol, double EPS, bool isRay) { @@ -75,13 +73,13 @@ bool intersect_line_patch(const Line &line, // Need to expand the box a bit so that intersections near subdivision boundaries // are accurately recorded Point ip; - if(true)//!intersect(line, patch.boundingBox().scale(1.5), ip)) + if(!intersect(line, patch.boundingBox().scale(1.5), ip)) { return false; } bool foundIntersection = false; - if(true) //patch.isBilinear(sq_tol)) + if(patch.isBilinear(sq_tol)) { // Store candidate intersection points axom::Array tc, uc, vc; @@ -103,29 +101,30 @@ bool intersect_line_patch(const Line &line, return false; } - // This tolerance is in parameter space, so is independent of the patch - // constexpr double EPS = 1e-5; - + foundIntersection = false; for(int i = 0; i < tc.size(); ++i) { const T t0 = tc[i]; const T u0 = uc[i]; const T v0 = vc[i]; - if((u0 >= (u_offset == 0 ? -buffer / u_scale : 0) && - u0 <= 1.0 + (u_offset + u_scale == 1.0 ? buffer / v_scale : 0)) && - (v0 >= (v_offset == 0 ? -buffer / v_scale : 0) && - v0 <= 1.0 + (v_offset + v_scale == 1.0 ? buffer / u_scale : 0))) + // Use EPS to record points near the boundary of the patch + if((u0 >= (u_offset == 0 ? -EPS / u_scale : 0) && + u0 <= 1.0 + (u_offset + u_scale == 1.0 ? EPS / v_scale : 0)) && + (v0 >= (v_offset == 0 ? -EPS / v_scale : 0) && + v0 <= 1.0 + (v_offset + v_scale == 1.0 ? EPS / u_scale : 0))) { // Extra check to avoid adding the same point twice if it's on the boundary of a subpatch if(!(u_offset != 0.0 && axom::utilities::isNearlyEqual(u0, 0.0, EPS)) && !(v_offset != 0.0 && axom::utilities::isNearlyEqual(v0, 0.0, EPS))) { - if(t >= 0 || !isRay) + if(t0 >= 0 || !isRay) { up.push_back(u_offset + u0 * u_scale); vp.push_back(v_offset + v0 * v_scale); tp.push_back(t0); + + foundIntersection = true; } } } @@ -149,14 +148,13 @@ bool intersect_line_patch(const Line &line, tp, up, vp, - sq_tol, - buffer, order_u, order_v, u_offset, u_scale, v_offset, v_scale, + sq_tol, EPS, isRay)) { @@ -167,14 +165,13 @@ bool intersect_line_patch(const Line &line, tp, up, vp, - sq_tol, - buffer, order_u, order_v, u_offset + u_scale, u_scale, v_offset, v_scale, + sq_tol, EPS, isRay)) { @@ -185,14 +182,13 @@ bool intersect_line_patch(const Line &line, tp, up, vp, - sq_tol, - buffer, order_u, order_v, u_offset, u_scale, v_offset + v_scale, v_scale, + sq_tol, EPS, isRay)) { @@ -203,14 +199,13 @@ bool intersect_line_patch(const Line &line, tp, up, vp, - sq_tol, - buffer, order_u, order_v, u_offset + u_scale, u_scale, v_offset + v_scale, v_scale, + sq_tol, EPS, isRay)) { diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index 4415e78c94..dc367c3559 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -663,8 +663,10 @@ AXOM_HOST_DEVICE bool intersect(const Plane& p, * \param [out] u The u parameter(s) of intersection point(s). * \param [out] v The v parameter(s) of intersection point(s). * \param [out] t The t parameter(s) of intersection point(s). - * \param [in] EPS The tolerance for intersection. - * + * \param [in] tol The tolerance for intersection (for physical distances). + * \param [in] EPS The tolerance for intersection (for parameter distances). + * \param [in] isHalfOpen True if the patch is parameterized in [0,1)^2. + * * For bilinear patches, implements GARP algorithm from Chapter 8 of Ray Tracing Gems (2019) * For higher order patches, intersections are found through recursive subdivison * until the subpatch is approximated by a bilinear patch. @@ -679,7 +681,8 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, axom::Array& u, axom::Array& v, double tol = 1e-8, - double EPS = 1e-8) + double EPS = 1e-8, + bool isHalfOpen = false) { const int order_u = patch.getOrder_u(); const int order_v = patch.getOrder_v(); @@ -687,6 +690,10 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, // for efficiency, linearity check actually uses a squared tolerance const double sq_tol = tol * tol; + // Store the candidate intersections + axom::Array tc, uc, vc; + bool foundIntersection = false; + if(order_u < 1 || order_v < 1) { // Patch has no surface area, ergo no intersections @@ -695,16 +702,17 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, else if(order_u == 1 && order_v == 1) { primal::Line line(ray.origin(), ray.direction()); - return detail::intersect_line_bilinear_patch(line, - patch(0, 0), - patch(order_u, 0), - patch(order_u, order_v), - patch(0, order_v), - t, - u, - v, - EPS, - true); + foundIntersection = + detail::intersect_line_bilinear_patch(line, + patch(0, 0), + patch(order_u, 0), + patch(order_u, order_v), + patch(0, order_v), + tc, + uc, + vc, + EPS, + true); } else { @@ -713,22 +721,40 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, double u_offset = 0., v_offset = 0.; double u_scale = 1., v_scale = 1.; - return detail::intersect_line_patch(line, - patch, - t, - u, - v, - sq_tol, - 0.0, - order_u, - order_v, - u_offset, - u_scale, - v_offset, - v_scale, - EPS, - true); + foundIntersection = detail::intersect_line_patch(line, + patch, + tc, + uc, + vc, + order_u, + order_v, + u_offset, + u_scale, + v_offset, + v_scale, + sq_tol, + EPS, + true); + } + + { + t = tc; + u = uc; + v = vc; + + return foundIntersection; } + // else + // { + // for(int i = 0; i < tc.size(); ++i) + // { + // const T t0 = tc[i]; + // const T u0 = uc[i]; + // const T v0 = vc[i]; + + // if(u0 >) + // } + // } } } // namespace primal From 031cb4879e72c92b1c05f9eb44ed3d001315a73a Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 4 Dec 2024 09:48:39 -0700 Subject: [PATCH 13/47] Start working on a different method for dupes --- .../operators/detail/intersect_patch_impl.hpp | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_patch_impl.hpp b/src/axom/primal/operators/detail/intersect_patch_impl.hpp index 229bcac54a..9991f7ba7c 100644 --- a/src/axom/primal/operators/detail/intersect_patch_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_patch_impl.hpp @@ -108,24 +108,17 @@ bool intersect_line_patch(const Line &line, const T u0 = uc[i]; const T v0 = vc[i]; - // Use EPS to record points near the boundary of the patch - if((u0 >= (u_offset == 0 ? -EPS / u_scale : 0) && - u0 <= 1.0 + (u_offset + u_scale == 1.0 ? EPS / v_scale : 0)) && - (v0 >= (v_offset == 0 ? -EPS / v_scale : 0) && - v0 <= 1.0 + (v_offset + v_scale == 1.0 ? EPS / u_scale : 0))) + // Use EPS to record points near the boundary of the bilinear approximation + if(u0 >= -EPS / u_scale && u0 <= 1.0 + EPS / u_scale && + v0 >= -EPS / v_scale && v0 <= 1.0 + EPS / v_scale) { - // Extra check to avoid adding the same point twice if it's on the boundary of a subpatch - if(!(u_offset != 0.0 && axom::utilities::isNearlyEqual(u0, 0.0, EPS)) && - !(v_offset != 0.0 && axom::utilities::isNearlyEqual(v0, 0.0, EPS))) + if(t0 >= -EPS || !isRay) { - if(t0 >= 0 || !isRay) - { - up.push_back(u_offset + u0 * u_scale); - vp.push_back(v_offset + v0 * v_scale); - tp.push_back(t0); - - foundIntersection = true; - } + up.push_back(u_offset + u0 * u_scale); + vp.push_back(v_offset + v0 * v_scale); + tp.push_back(t0); + + foundIntersection = true; } } } From 198e9b04ec3ecfeb8afedef1f0a6cfd78cb34934 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 4 Dec 2024 09:49:01 -0700 Subject: [PATCH 14/47] Add some more tests for flat patches --- .../primal/tests/primal_surface_intersect.cpp | 322 +++++++++++++++++- 1 file changed, 306 insertions(+), 16 deletions(-) diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp index 36bfdcd122..c040731f00 100644 --- a/src/axom/primal/tests/primal_surface_intersect.cpp +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -16,6 +16,8 @@ #include "axom/primal/geometry/BezierPatch.hpp" #include "axom/primal/operators/intersect.hpp" +#include "axom/core/numerics/matvecops.hpp" + #include #include #include @@ -23,7 +25,7 @@ namespace primal = axom::primal; -/** +/* * Helper function to compute the intersections of a Bezier patch and a ray * and check that their intersection points match our expectations. * Patch parameters are stored in \a exp_u, \a exp_v and \a exp_t. @@ -35,9 +37,9 @@ namespace primal = axom::primal; template void checkIntersections(const primal::Ray& ray, const primal::BezierPatch& patch, + const axom::Array& exp_t, const axom::Array& exp_u, const axom::Array& exp_v, - const axom::Array& exp_t, double eps, double test_eps, bool shouldPrintIntersections = false) @@ -56,7 +58,7 @@ void checkIntersections(const primal::Ray& ray, // Intersect the ray and the patch, intersection parameters will be // in arrays (u, v) and t, for the patch and ray, respectively Array u, v, t; - bool ray_intersects = intersect(ray, patch, u, v, t); + bool ray_intersects = intersect(ray, patch, t, u, v); EXPECT_EQ(exp_intersect, ray_intersects); EXPECT_EQ(u.size(), v.size()); EXPECT_EQ(u.size(), t.size()); @@ -93,7 +95,7 @@ void checkIntersections(const primal::Ray& ray, } sstr << "\nt (" << t.size() << "): "; - for(auto i = 0u; i < t.size(); ++i) + for(auto i = 0; i < t.size(); ++i) { sstr << std::setprecision(16) << t[i] << ","; } @@ -146,9 +148,9 @@ TEST(primal_surface_inter, bilinear_intersect) checkIntersections(ray, bilinear_patch, + {1.0}, {0.146446609407}, {0.853553390593}, - {1.0}, eps, eps_test); @@ -170,9 +172,9 @@ TEST(primal_surface_inter, bilinear_intersect) checkIntersections(ray, bilinear_patch, + {0.414213562373, 2.41421356237}, {0.853553390593, 0.146446609407}, {0.146446609407, 0.853553390593}, - {0.414213562373, 2.41421356237}, eps, eps_test); @@ -181,7 +183,7 @@ TEST(primal_surface_inter, bilinear_intersect) ray_direction = VectorType({1.0, 0.0, 0.0}); ray = RayType(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {0.5}, {0.5}, {2.0}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {2.0}, {0.5}, {0.5}, eps, eps_test); // Ray with no intersections on line with infinitely many intersections ray_origin = PointType({2.0, 0.0, 1.5}); @@ -195,7 +197,84 @@ TEST(primal_surface_inter, bilinear_intersect) ray_direction = VectorType({1.0, 0.0, 0.0}); ray = RayType(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {0.5}, {0.85}, {0.3}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {0.3}, {0.5}, {0.85}, eps, eps_test); +} + +//------------------------------------------------------------------------------ +TEST(primal_surface_inter, bilinear_boundary_condition) +{ + static const int DIM = 3; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierPatchType = primal::BezierPatch; + using RayType = primal::Ray; + + const double eps = 1E-16; + const double eps_test = 1E-10; + + SLIC_INFO("primal: testing bilinear patch intersection"); + + // Set control points + BezierPatchType bilinear_patch(1, 1); + bilinear_patch(0, 0) = PointType({-1.0, 1.0, 1.0}); + bilinear_patch(1, 0) = PointType({-1.0, -1.0, 2.0}); + bilinear_patch(1, 1) = PointType({1.0, -1.0, 1.0}); + bilinear_patch(0, 1) = PointType({1.0, 1.0, 2.0}); + + // Don't count intersections on the u=1 or v=1 isocurves + PointType ray_origin({0.0, 0.0, 3.0}); + VectorType ray_direction; + RayType ray(ray_origin, ray_direction); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.0, 1.0)); + ray = RayType(ray_origin, ray_direction); + // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.5, 1.0)); + ray = RayType(ray_origin, ray_direction); + // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.5)); + ray = RayType(ray_origin, ray_direction); + // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.0)); + ray = RayType(ray_origin, ray_direction); + // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.5)); + ray = RayType(ray_origin, ray_direction); + // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 1.0)); + ray = RayType(ray_origin, ray_direction); + // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.0, 0.0)); + ray = RayType(ray_origin, ray_direction); + // Incorrect due to floating point arithmetic + // checkIntersections(ray, bilinear_patch, {sqrt(6.0)}, {0.0}, {0.0}, eps, eps_test); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.5, 0.0)); + ray = RayType(ray_origin, ray_direction); + // checkIntersections(ray, + // bilinear_patch, + // {sqrt(13.0) / 2.0}, + // {0.5}, + // {0.0}, + // eps, + // eps_test); + + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.0, 0.5)); + ray = RayType(ray_origin, ray_direction); + // checkIntersections(ray, + // bilinear_patch, + // {sqrt(13.0) / 2.0}, + // {0.0}, + // {0.5}, + // eps, + // eps_test); } //------------------------------------------------------------------------------ @@ -223,11 +302,17 @@ TEST(primal_surface_inter, difficult_garp_case) VectorType ray_direction({-1.0, -2.0, 0.0}); // The first step of the GARP algorithm is to solve a quadratic equation a + bt + ct^2 = 0, // and this configuration of patch + ray direction is such that c = 0 - + // Ray with single intersection PointType ray_origin({0.0, 1.0, 1.25}); RayType ray(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {2./3.}, {0.25}, {sqrt(20. / 9.)}, eps, eps_test); + checkIntersections(ray, + bilinear_patch, + {sqrt(20. / 9.)}, + {2. / 3.}, + {0.25}, + eps, + eps_test); // Ray with no intersections ray_origin = PointType({0.0, 1.0, 1.75}); @@ -240,16 +325,15 @@ TEST(primal_surface_inter, difficult_garp_case) bilinear_patch(0, 1) = PointType({1.0, 1.0, 2.0}); // Double roots in the quadratic - ray_origin = PointType({2.0, 0.0, 1.5}); ray_direction = VectorType({-1.0, 0.0, 0.0}); ray = RayType(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {0.5}, {0.5}, {2.0}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {2.0}, {0.5}, {0.5}, eps, eps_test); ray_origin = PointType({0.0, 2.0, 1.5}); ray_direction = VectorType({0.0, -1.0, 0.0}); ray = RayType(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {0.5}, {0.5}, {2.0}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {2.0}, {0.5}, {0.5}, eps, eps_test); } //------------------------------------------------------------------------------ @@ -279,12 +363,12 @@ TEST(primal_surface_inter, flat_bilinear_intersect) VectorType ray_direction({1, 0.5, -1.0}); RayType ray(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {0.25}, {11. / 14.}, {1.5}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {1.5}, {0.25}, {11. / 14.}, eps, eps_test); // Ray with a single intersection that is coplanar with an isocurve ray_direction = VectorType({1.0, 0.0, -1.0}); ray = RayType(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {0.5}, {5. / 6.}, {sqrt(2)}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {sqrt(2)}, {0.5}, {5. / 6.}, eps, eps_test); // Ray with no intersections ray_direction = VectorType({1.0, -1.0, -0.5}); @@ -313,7 +397,213 @@ TEST(primal_surface_inter, flat_bilinear_intersect) ray_direction = VectorType({1.0, 0.0, 0.0}); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, bilinear_patch, {0.25}, {0.0}, {0.25}, eps, eps_test); + // Ray that is coplanar with a flat patch + // For ease of implementation, always return false + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); +} + +//------------------------------------------------------------------------------ +TEST(primal_surface_inter, flat_selfintersect_bilinear_intersect) +{ + static const int DIM = 3; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierPatchType = primal::BezierPatch; + using RayType = primal::Ray; + + const double eps = 1E-16; + const double eps_test = 1E-10; + + SLIC_INFO("primal: testing bilinear patch intersection"); + + // Set control points for square, hourglass patch + BezierPatchType bilinear_patch(1, 1); + bilinear_patch(0, 0) = PointType({-1.0, 1.0, 1.0}); + bilinear_patch(1, 0) = PointType({1.0, -1.0, 1.0}); + bilinear_patch(1, 1) = PointType({-1.0, -1.0, 1.0}); + bilinear_patch(0, 1) = PointType({1.0, 1.0, 1.0}); + + // Ray with single intersection at the overlap point + PointType ray_origin({0.0, 0.0, 2.0}); + VectorType ray_direction({0.0, 0.0, -1.0}); + RayType ray(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {1.0}, {0.5}, {0.5}, eps, eps_test); + + // Ray with single intersection not at the overlap point + ray_origin = PointType({0.0, 0.5, 2.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {1.0}, {0.25}, {0.5}, eps, eps_test); + + // Ray with no intersections + ray_origin = PointType({0.5, 0.0, 2.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + + // Change the patch to have a true self-overlapping region + bilinear_patch(1, 1) = PointType({-1.0, 0.0, 1.0}); + + // Has two intersections in parameter space + ray_origin = PointType({-0.1, 0.25, 2.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, + bilinear_patch, + {1.0, 1.0}, + {0.6, 5. / 12.}, + {0.75, 0.2}, + eps, + eps_test); + + // Is "tangent" to the overlap, resulting in a single intersection + ray_origin = PointType({0.0, 0.25, 2.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {1.0}, {0.5}, {0.5}, eps, eps_test); +} + +//------------------------------------------------------------------------------ +TEST(primal_surface_inter, bezier_surface_intersect) +{ + static const int DIM = 3; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using BezierPatchType = primal::BezierPatch; + using RayType = primal::Ray; + + double rt2 = sqrt(2), rt3 = sqrt(3), rt6 = sqrt(6); + + // Define the nodes and weights for one of six rational, biquartic Bezier patches + // that compose the unit sphere. This patch is centered at the +z axis. + // Nodes and weights obtained from the technical report + // "Tiling the Sphere with Rational Bezier Patches", + // James E. Cobb, University of Utah, 1988 + + // clang-format off + // These are the *homogeneous* coordinates + axom::Array node_data = { + PointType {4*(1-rt3), 4*(1-rt3), 4*(rt3-1)}, PointType {rt2*(rt3-4), -rt2, rt2*(4-rt3)}, PointType {4*(1-2*rt3)/3, 0, 4*(2*rt3-1)/3}, PointType {rt2*(rt3-4), rt2, rt2*(4-rt3)}, PointType {4*(1-rt3), 4*(rt3-1), 4*(rt3-1)}, + PointType { -rt2, rt2*(rt3 - 4), rt2*(4 - rt3)}, PointType {(2-3*rt3)/2, (2-3*rt3)/2, (rt3+6)/2}, PointType {rt2*(2*rt3-7)/3, 0, 5*rt6/3}, PointType {(2-3*rt3)/2, (3*rt3-2)/2, (rt3+6)/2}, PointType { -rt2, rt2*(4-rt3), rt2*(4-rt3)}, + PointType { 0, 4*(1-2*rt3)/3, 4*(2*rt3-1)/3}, PointType { 0, rt2*(2*rt3-7)/3, 5*rt6/3}, PointType {0, 0, 4*(5-rt3)/3}, PointType { 0, rt2*(7-2*rt3)/3, 5*rt6/3}, PointType { 0, 4*(2*rt3-1)/3, 4*(2*rt3-1)/3}, + PointType { rt2, rt2*(rt3 - 4), rt2*(4 - rt3)}, PointType {(3*rt3-2)/2, (2-3*rt3)/2, (rt3+6)/2}, PointType {rt2*(7-2*rt3)/3, 0, 5*rt6/3}, PointType {(3*rt3-2)/2, (3*rt3-2)/2, (rt3+6)/2}, PointType { rt2, rt2*(4-rt3), rt2*(4-rt3)}, + PointType {4*(rt3-1), 4*(1-rt3), 4*(rt3-1)}, PointType {rt2*(4-rt3), -rt2, rt2*(4-rt3)}, PointType {4*(2*rt3-1)/3, 0, 4*(2*rt3-1)/3}, PointType {rt2*(4-rt3), rt2, rt2*(4-rt3)}, PointType {4*(rt3-1), 4*(rt3-1), 4*(rt3-1)}}; + + axom::Array weight_data = { + 4*(3-rt3), rt2*(3*rt3-2), 4*(5-rt3)/3, rt2*(3*rt3-2), 4*(3-rt3), + rt2*(3*rt3-2), (rt3+6)/2, rt2*(rt3+6)/3, (rt3+6)/2, rt2*(3*rt3-2), + 4*(5-rt3)/3, rt2*(rt3+6)/3, 4*(5*rt3-1)/9, rt2*(rt3+6)/3, 4*(5-rt3)/3, + rt2*(3*rt3-2), (rt3+6)/2, rt2*(rt3+6)/3, (rt3+6)/2, rt2*(3*rt3-2), + 4*(3-rt3), rt2*(3*rt3-2), 4*(5-rt3)/3, rt2*(3*rt3-2), 4*(3-rt3)}; + // clang-format on + + BezierPatchType sphere_face_patch(node_data, weight_data, 4, 4); + for(int i = 0; i < 5; ++i) + { + for(int j = 0; j < 5; ++j) + { + sphere_face_patch(i, j).array() /= sphere_face_patch.getWeight(i, j); + } + } + + // Intersections with arbitrary patches aren't recorded + // with exact precision + const double eps = 1E-5; + const double eps_test = 1E-5; + + PointType ray_origin({0.0, 0.0, 0.0}); + + // Try points which will be interior to all subdivisions + double u_params[10], v_params[10]; + axom::numerics::linspace(0.0, 1.0, u_params, 10); + axom::numerics::linspace(0.0, 1.0, v_params, 10); + + for(int i = 0; i < 10; ++i) + { + for(int j = 0; j < 10; ++j) + { + VectorType ray_direction( + ray_origin, + sphere_face_patch.evaluate(u_params[i], v_params[j])); + RayType ray(ray_origin, ray_direction); + + // Points on the edge should not be recorded + if(i == 9 || j == 9) + { + continue; + checkIntersections(ray, sphere_face_patch, {}, {}, {}, eps, eps_test); + } + else + { + // continue; + checkIntersections(ray, + sphere_face_patch, + {1.0}, + {u_params[i]}, + {v_params[j]}, + eps, + eps_test); + } + } + } + + // Use different parameter values so that intersections + // are on the boundary of subdivisions + axom::numerics::linspace(0.0, 1.0, u_params, 9); + axom::numerics::linspace(0.0, 1.0, v_params, 9); + + for(int i = 0; i < 9; ++i) + { + for(int j = 0; j < 9; ++j) + { + VectorType ray_direction( + ray_origin, + sphere_face_patch.evaluate(u_params[i], v_params[j])); + RayType ray(ray_origin, ray_direction); + + // Points on the edge should not be recorded + if(i == 8 || j == 8) + { + continue; + checkIntersections(ray, sphere_face_patch, {}, {}, {}, eps, eps_test); + } + else + { + // continue; + // checkIntersections(ray, + // sphere_face_patch, + // {1.0}, + // {u_params[i]}, + // {v_params[j]}, + // eps, + // eps_test); + VectorType ray_direction(ray_origin, + sphere_face_patch.evaluate(u_params[i], v_params[j])); + + RayType ray(ray_origin, ray_direction); + axom::Array t, u, v; + bool ray_intersects = intersect(ray, sphere_face_patch, t, u, v); + + std::cout << "------------------" << std::endl; + for( int i = 0; i < t.size(); ++i ) + { + std::cout << "t: " << t[i] << ", u: " << u[i] << ", v: " << v[i] << std::endl; + } + + int xx= 12; + } + } + } + + VectorType ray_direction(ray_origin, + sphere_face_patch.evaluate(u_params[0], v_params[6])); + RayType ray(ray_origin, ray_direction); + + axom::Array t, u, v; + bool ray_intersects = intersect(ray, sphere_face_patch, t, u, v); } int main(int argc, char* argv[]) From 2637d7e54c7a9e7b22e580cff3458e7b78a165b8 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 4 Dec 2024 13:33:03 -0700 Subject: [PATCH 15/47] Fix oversight with unused variables --- src/axom/primal/operators/winding_number.hpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/axom/primal/operators/winding_number.hpp b/src/axom/primal/operators/winding_number.hpp index 820dddba74..3ae6b5f353 100644 --- a/src/axom/primal/operators/winding_number.hpp +++ b/src/axom/primal/operators/winding_number.hpp @@ -380,12 +380,10 @@ double winding_number(const Point& q, double edge_tol = 1e-8, double EPS = 1e-8) { - AXOM_UNUSED_VAR(EPS); - double ret_val = 0.0; for(int i = 0; i < carray.size(); i++) { - ret_val += winding_number(q, carray[i], false, edge_tol); + ret_val += winding_number(q, carray[i], edge_tol, EPS); } return ret_val; @@ -409,12 +407,10 @@ double winding_number(const Point& q, double edge_tol = 1e-8, double EPS = 1e-8) { - AXOM_UNUSED_VAR(EPS); - double ret_val = 0.0; for(int i = 0; i < narray.size(); i++) { - ret_val += winding_number(q, narray[i], edge_tol); + ret_val += winding_number(q, narray[i], edge_tol, EPS); } return ret_val; From de88b02b613da9893e27c767ace86527d3fb359c Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 4 Dec 2024 13:51:34 -0700 Subject: [PATCH 16/47] Add convenience method for getting knots --- src/axom/primal/geometry/KnotVector.hpp | 15 +++++++++++++++ src/axom/primal/geometry/NURBSCurve.hpp | 3 +++ 2 files changed, 18 insertions(+) diff --git a/src/axom/primal/geometry/KnotVector.hpp b/src/axom/primal/geometry/KnotVector.hpp index 36ea5396d0..b446d112ec 100644 --- a/src/axom/primal/geometry/KnotVector.hpp +++ b/src/axom/primal/geometry/KnotVector.hpp @@ -191,6 +191,21 @@ class KnotVector /// \brief Return the number of knots in the knot vector axom::IndexType getNumKnots() const { return m_knots.size(); } + /// \brief Return an array of the unique knot values + axom::Array getUniqueKnots() const + { + axom::Array unique_knots; + for(int i = 0; i < m_knots.size(); ++i) + { + if(i == 0 || m_knots[i] != m_knots[i - 1]) + { + unique_knots.push_back(m_knots[i]); + } + } + + return unique_knots; + } + /// \brief Return the number of valid knot spans axom::IndexType getNumKnotSpans() const { diff --git a/src/axom/primal/geometry/NURBSCurve.hpp b/src/axom/primal/geometry/NURBSCurve.hpp index b6f983aed7..589f55c3e2 100644 --- a/src/axom/primal/geometry/NURBSCurve.hpp +++ b/src/axom/primal/geometry/NURBSCurve.hpp @@ -1164,6 +1164,9 @@ class NURBSCurve /// \brief Return a copy of the knot vector as an array axom::Array getKnotsArray() const { return m_knotvec.getArray(); } + /// \brief Return an array of the unique knots in the knot vector + axom::Array getUniqueKnots() const { return m_knotvec.getUniqueKnots(); } + /// \brief Reverses the order of the NURBS curve's control points and weights void reverseOrientation() { From 0106a240297e1e0c4ba3f5aab89408b3126ff4c6 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 4 Dec 2024 16:12:39 -0700 Subject: [PATCH 17/47] Rework things to use axom::Arrays --- .../detail/intersect_bezier_impl.hpp | 28 +-- src/axom/primal/operators/intersect.hpp | 132 ++++++++++--- .../primal/tests/primal_bezier_intersect.cpp | 184 +++++++++++------- 3 files changed, 239 insertions(+), 105 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index 12c7f50afa..b258d9f456 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -60,8 +60,8 @@ namespace detail template bool intersect_bezier_curves(const BezierCurve &c1, const BezierCurve &c2, - std::vector &sp, - std::vector &tp, + axom::Array &sp, + axom::Array &tp, double sq_tol, int order1, int order2, @@ -111,8 +111,8 @@ bool intersect_2d_linear(const Point &a, * \param [in] r The input ray * \param [out] cp Parametric coordinates of intersections in \a c [0, 1) * \param [out] rp Parametric coordinates of intersections in \a r [0, inf) - * \param [in] sq_tol The squared tolerance parameter for determining if a - * Bezier curve is linear + * \param [in] sq_tol The squared tolerance parameter for distances in physical space + * \param [in] EPS The tolerance parameter for distances in parameter space * \param [in] order The order of \a c * \param s_offset The offset in parameter space for \a c * \param s_scale The scale in parameter space for \a c @@ -135,9 +135,10 @@ bool intersect_2d_linear(const Point &a, template bool intersect_ray_bezier(const Ray &r, const BezierCurve &c, - std::vector &rp, - std::vector &cp, + axom::Array &rp, + axom::Array &cp, double sq_tol, + double EPS, int order, double c_offset, double c_scale); @@ -146,8 +147,8 @@ bool intersect_ray_bezier(const Ray &r, template bool intersect_bezier_curves(const BezierCurve &c1, const BezierCurve &c2, - std::vector &sp, - std::vector &tp, + axom::Array &sp, + axom::Array &tp, double sq_tol, int order1, int order2, @@ -266,9 +267,10 @@ bool intersect_2d_linear(const Point &a, template bool intersect_ray_bezier(const Ray &r, const BezierCurve &c, - std::vector &rp, - std::vector &cp, + axom::Array &rp, + axom::Array &cp, double sq_tol, + double EPS, int order, double c_offset, double c_scale) @@ -296,7 +298,7 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear - if(intersect(r, seg, r0, s0, 0.0) && s0 < 1.0) + if(intersect(r, seg, r0, s0, 0.0) && s0 < 1.0 - EPS) { rp.push_back(r0); cp.push_back(c_offset + c_scale * s0); @@ -314,11 +316,11 @@ bool intersect_ray_bezier(const Ray &r, c_scale *= scaleFac; // Note: we want to find all intersections, so don't short-circuit - if(intersect_ray_bezier(r, c1, rp, cp, sq_tol, order, c_offset, c_scale)) + if(intersect_ray_bezier(r, c1, rp, cp, sq_tol, EPS, order, c_offset, c_scale)) { foundIntersection = true; } - if(intersect_ray_bezier(r, c2, rp, cp, sq_tol, order, c_offset + c_scale, c_scale)) + if(intersect_ray_bezier(r, c2, rp, cp, sq_tol, EPS, order, c_offset + c_scale, c_scale)) { foundIntersection = true; } diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index dc367c3559..dffd021092 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -538,8 +538,8 @@ bool intersect(const OrientedBoundingBox& b1, template bool intersect(const BezierCurve& c1, const BezierCurve& c2, - std::vector& sp, - std::vector& tp, + axom::Array& sp, + axom::Array& tp, double tol = 1E-8) { const double offset = 0.; @@ -561,12 +561,29 @@ bool intersect(const BezierCurve& c1, scale); } +/*! + * \brief Function to find intersections between a ray and a Bezier curve + * + * \param [in] r The input ray + * \param [in] c The input curve + * \param [out] rp Parametric coordinates of intersections in \a r [0, inf) + * \param [out] cp Parametric coordinates of intersections in \a c [0, 1) + * Bezier curve is linear + * \param [in] tol Tolerance parameter for physical distances + * \param [in] EPS Tolerance parameter for parameter-space distances + * + * \note A BezierCurve is parametrized in [0,1). This function assumes the all + * intersections have multiplicity one, i.e. the function does not find tangencies. + * + * \return True if the ray intersects the Bezier curve, False otherwise + */ template bool intersect(const Ray& r, const BezierCurve& c, - std::vector& rp, - std::vector& cp, - double tol = 1E-8) + axom::Array& rp, + axom::Array& cp, + double tol = 1E-8, + double EPS = 1E-8) { const double offset = 0.; const double scale = 1.; @@ -574,9 +591,64 @@ bool intersect(const Ray& r, // for efficiency, linearity check actually uses a squared tolerance const double sq_tol = tol * tol; - return detail::intersect_ray_bezier(r, c, rp, cp, sq_tol, c.getOrder(), offset, scale); + return detail::intersect_ray_bezier(r, c, rp, cp, sq_tol, EPS, c.getOrder(), offset, scale); } +/*! + * \brief Function to find intersections between a ray and a NURBS curve + * + * \param [in] r The input ray + * \param [in] n The input curve + * \param [out] rp Parametric coordinates of intersections in \a r [0, inf) + * \param [out] cp Parametric coordinates of intersections in the knot span of \a n + * \param [in] tol Tolerance parameter for physical distances + * \param [in] EPS Tolerance parameter for parameter-space distances + * + * \note Assumes the NURBS curve is parameterized on a half-open interval [a, b), + * and assumes the all intersections have multiplicity one, i.e. the function does not find tangencies. + * + * \return True if the ray intersects the NURBS curve, False otherwise + */ +template +bool intersect(const Ray& r, + const NURBSCurve& n, + axom::Array& rp, + axom::Array& np, + double tol = 1E-8, + double EPS = 1E-8) +{ + const double offset = 0.; + const double scale = 1.; + + // Check a bounding box of the entire NURBS first + Point ip; + if(!intersect(r, n.boundingBox(), ip)) + { + return false; + } + + // Decompose the NURBS curve into Bezier segments + auto beziers = n.extractBezier(); + const int deg = n.getDegree(); + axom::Array knot_vals = n.getUniqueKnots(); + + // Check each Bezier segment, and scale the intersection parameters + // back into the span of the original NURBS curve + for(int i = 0; i < beziers.size(); ++i) + { + axom::Array rc, nc; + intersect( r, beziers[i], rc, nc, tol, EPS ); + + // Scale the intersection parameters back into the span of the NURBS curve + for(int j = 0; j < rc.size(); ++j) + { + rp.push_back( rc[j] ); + np.push_back( knot_vals[i] + nc[j] * (knot_vals[i+1] - knot_vals[i]) ); + } + } + + return !rp.empty(); +} /// @} /// \name Plane Intersection Routines @@ -737,24 +809,40 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, true); } + // Remove duplicates from the (u, v) intersection points + // (Note it's not possible for (u_1, v_1) == (u_2, v_2) and t_1 != t_2) + const double EPS_sq = EPS * EPS; + + // The number of reported intersection points will be small, + // so we don't need to fully sort the list + for(int i = 0; i < tc.size(); ++i) { - t = tc; - u = uc; - v = vc; - - return foundIntersection; + // Also remove any intersections on the half-interval boundaries + if(isHalfOpen && (uc[i] >= 1.0 - EPS || vc[i] >= 1.0 - EPS)) + { + continue; + } + + Point uv({uc[i], vc[i]}); + + bool foundDuplicate = false; + for(int j = i + 1; !foundDuplicate && j < tc.size(); ++j) + { + if(squared_distance(uv, Point({uc[j], vc[j]})) < EPS_sq) + { + foundDuplicate = true; + } + } + + if(!foundDuplicate) + { + t.push_back(tc[i]); + u.push_back(uc[i]); + v.push_back(vc[i]); + } } - // else - // { - // for(int i = 0; i < tc.size(); ++i) - // { - // const T t0 = tc[i]; - // const T u0 = uc[i]; - // const T v0 = vc[i]; - - // if(u0 >) - // } - // } + + return !t.empty(); } } // namespace primal diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index 9dfd58d394..e54fb08307 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -16,6 +16,8 @@ #include "axom/primal/geometry/BezierCurve.hpp" #include "axom/primal/operators/intersect.hpp" +#include "axom/core/numerics/matvecops.hpp" + #include #include #include @@ -35,14 +37,14 @@ namespace primal = axom::primal; template void checkIntersections(const primal::BezierCurve& curve1, const primal::BezierCurve& curve2, - const std::vector& exp_s, - const std::vector& exp_t, + const axom::Array& exp_s, + const axom::Array& exp_t, double eps, double test_eps, bool shouldPrintIntersections = false) { constexpr int DIM = 2; - using Array = std::vector; + using Array = axom::Array; // Check validity of input data exp_s and exp_t. // They should have the same size and be sorted @@ -140,8 +142,8 @@ TEST(primal_bezier_inter, linear_bezier) PointType data2[order + 1] = {PointType {0.0, 1.0}, PointType {1.0, 0.0}}; BezierCurveType curve2(data2, order); - std::vector exp_intersections1 = {0.5}; - std::vector exp_intersections2 = {0.5}; + axom::Array exp_intersections1 = {0.5}; + axom::Array exp_intersections2 = {0.5}; const double eps = 1E-3; checkIntersections(curve1, @@ -163,8 +165,8 @@ TEST(primal_bezier_inter, linear_bezier) PointType data2[order + 1] = {PointType {1.0, 1.0}, PointType {2.0, 0.0}}; BezierCurveType curve2(data2, order); - std::vector exp_intersections1 = {0.0}; - std::vector exp_intersections2 = {0.0}; + axom::Array exp_intersections1 = {0.0}; + axom::Array exp_intersections2 = {0.0}; const double eps = 1E-3; checkIntersections(curve1, @@ -186,8 +188,8 @@ TEST(primal_bezier_inter, linear_bezier) PointType data2[order + 1] = {PointType {-2.0, 2.0}, PointType {2.0, 0.0}}; BezierCurveType curve2(data2, order); - std::vector exp_intersections1 = {.25}; - std::vector exp_intersections2 = {.75}; + axom::Array exp_intersections1 = {.25}; + axom::Array exp_intersections2 = {.75}; const double eps = 1E-3; checkIntersections(curve1, @@ -231,8 +233,8 @@ TEST(primal_bezier_inter, linear_bezier_interp_params) PointType data2[order + 1] = {PointType {t, 0.0}, PointType {t, 1.0}}; BezierCurveType curve2(data2, order); - std::vector exp_intersections1 = {t}; - std::vector exp_intersections2 = {s}; + axom::Array exp_intersections1 = {t}; + axom::Array exp_intersections2 = {s}; // test for intersections checkIntersections(curve1, @@ -281,7 +283,7 @@ TEST(primal_bezier_inter, no_intersections_bezier) PointType {3.0, 1.5}}; BezierCurveType curve2(data2, order); - std::vector exp_intersections; + axom::Array exp_intersections; const double eps = 1E-16; const double eps_test = 1E-10; @@ -321,7 +323,7 @@ TEST(primal_bezier_inter, cubic_quadratic_bezier) BezierCurveType curve2(data2, order2); // Note: same intersection params for curve and line - std::vector exp_intersections = {0.17267316464601146, + axom::Array exp_intersections = {0.17267316464601146, 0.5, 0.827326835353989}; @@ -379,7 +381,7 @@ TEST(primal_bezier_inter, cubic_bezier_varying_eps) BezierCurveType curve2(data2, order); // Note: same intersection params for curve and line - std::vector exp_intersections = {0.17267316464601146, + axom::Array exp_intersections = {0.17267316464601146, 0.5, 0.827326835353989}; @@ -429,7 +431,7 @@ TEST(primal_bezier_inter, cubic_bezier_nine_intersections) const double eps = 1E-16; const double eps_test = 1E-10; - std::vector exp_s = {0.04832125363145223, + axom::Array exp_s = {0.04832125363145223, 0.09691296096235966, 0.1149546049907844, 0.4525395443158645, @@ -439,7 +441,7 @@ TEST(primal_bezier_inter, cubic_bezier_nine_intersections) 0.937347647827529, 0.9747492689591496}; - std::vector exp_t = {0.05756422605799361, + axom::Array exp_t = {0.05756422605799361, 0.1237935342287382, 0.1676610724548464, 0.4229476589108138, @@ -461,41 +463,41 @@ TEST(primal_bezier_inter, cubic_bezier_nine_intersections) * Param \a shouldPrintIntersections is used for debugging and for generating * the initial array of expected intersections. */ -template +template void checkIntersectionsRay(const primal::Ray& ray, - const primal::BezierCurve& curve, - const std::vector& exp_s, - const std::vector& exp_t, + const CurveType& curve, + const axom::Array& exp_r, + const axom::Array& exp_c, double eps, double test_eps, bool shouldPrintIntersections = false) { constexpr int DIM = 2; - using Array = std::vector; + using Array = axom::Array; - // Check validity of input data exp_s and exp_t. + // Check validity of input data exp_c and exp_r. // They should have the same size - EXPECT_EQ(exp_s.size(), exp_t.size()); + EXPECT_EQ(exp_c.size(), exp_r.size()); - const int num_exp_intersections = static_cast(exp_s.size()); + const int num_exp_intersections = static_cast(exp_c.size()); const bool exp_intersect = (num_exp_intersections > 0); // Intersect the curve and ray, intersection parameters will be // in arrays s and t, for curve and ray, respectively - Array s, t; - bool curves_intersect = intersect(ray, curve, s, t, eps); + Array r, c; + bool curves_intersect = intersect(ray, curve, r, c, eps); EXPECT_EQ(exp_intersect, curves_intersect); - EXPECT_EQ(s.size(), t.size()); + EXPECT_EQ(r.size(), c.size()); // check that we found the expected number of intersection points - const int num_actual_intersections = static_cast(s.size()); + const int num_actual_intersections = static_cast(c.size()); EXPECT_EQ(num_exp_intersections, num_actual_intersections); // check that the evaluated intersection points are identical for(int i = 0; i < num_actual_intersections; ++i) { - auto p1 = curve.evaluate(t[i]); - auto p2 = ray.at(s[i]); + auto p1 = curve.evaluate(c[i]); + auto p2 = ray.at(r[i]); EXPECT_NEAR(0., primal::squared_distance(p1, p2), test_eps); @@ -505,10 +507,6 @@ void checkIntersectionsRay(const primal::Ray& ray, } } - // check that the intersections match our precomputed values - std::sort(s.begin(), s.end()); - std::sort(t.begin(), t.end()); - if(shouldPrintIntersections) { std::stringstream sstr; @@ -516,16 +514,16 @@ void checkIntersectionsRay(const primal::Ray& ray, sstr << "Intersections for curve and ray: " << "\n\t" << curve << "\n\t" << ray; - sstr << "\ns (" << s.size() << "): "; - for(auto i = 0u; i < s.size(); ++i) + sstr << "\ns (" << c.size() << "): "; + for(auto i = 0u; i < c.size(); ++i) { - sstr << std::setprecision(16) << s[i] << ","; + sstr << std::setprecision(16) << c[i] << ","; } - sstr << "\nt (" << t.size() << "): "; - for(auto i = 0u; i < t.size(); ++i) + sstr << "\nt (" << r.size() << "): "; + for(auto i = 0u; i < r.size(); ++i) { - sstr << std::setprecision(16) << t[i] << ","; + sstr << std::setprecision(16) << r[i] << ","; } SLIC_INFO(sstr.str()); @@ -533,14 +531,14 @@ void checkIntersectionsRay(const primal::Ray& ray, for(int i = 0; i < num_actual_intersections; ++i) { - EXPECT_NEAR(exp_s[i], s[i], test_eps); - EXPECT_NEAR(exp_t[i], t[i], test_eps); + EXPECT_NEAR(exp_c[i], c[i], test_eps); + EXPECT_NEAR(exp_r[i], r[i], test_eps); if(shouldPrintIntersections) { - SLIC_INFO("\t" << i << ": {s:" << s[i] << ", t:" << t[i] - << std::setprecision(16) << ", s_actual:" << exp_s[i] - << ", t_actual:" << exp_t[i] << "}"); + SLIC_INFO("\t" << i << ": {r:" << r[i] << ", c:" << c[i] + << std::setprecision(16) << ", s_actual:" << exp_c[i] + << ", t_actual:" << exp_r[i] << "}"); } } } @@ -569,8 +567,8 @@ TEST(primal_bezier_inter, ray_linear_bezier) PointType data[order + 1] = {PointType {1.0, 0.0}, PointType {0.0, 1.0}}; BezierCurveType curve(data, order); - std::vector exp_intersections1 = {std::sqrt(0.5)}; - std::vector exp_intersections2 = {0.5}; + axom::Array exp_intersections1 = {std::sqrt(0.5)}; + axom::Array exp_intersections2 = {0.5}; const double eps = 1E-3; checkIntersectionsRay(ray, @@ -596,8 +594,8 @@ TEST(primal_bezier_inter, ray_linear_bezier) const double eps = 1E-3; checkIntersectionsRay(ray1, curve, - std::vector({1.0}), - std::vector({0.0}), + axom::Array({1.0}), + axom::Array({0.0}), eps, eps); @@ -608,8 +606,8 @@ TEST(primal_bezier_inter, ray_linear_bezier) checkIntersectionsRay(ray2, curve, - std::vector(), - std::vector(), + axom::Array(), + axom::Array(), eps, eps); @@ -620,8 +618,8 @@ TEST(primal_bezier_inter, ray_linear_bezier) checkIntersectionsRay(ray3, curve, - std::vector({0.0}), - std::vector({0.5}), + axom::Array({0.0}), + axom::Array({0.5}), eps, eps); } @@ -637,8 +635,8 @@ TEST(primal_bezier_inter, ray_linear_bezier) VectorType ray_direction({1.0, 2.0}); RayType ray(ray_origin, ray_direction); - std::vector exp_intersections1 = {std::sqrt(5) / 3.0}; - std::vector exp_intersections2 = {2.0 / 3.0}; + axom::Array exp_intersections1 = {std::sqrt(5) / 3.0}; + axom::Array exp_intersections2 = {2.0 / 3.0}; const double eps = 1E-3; checkIntersectionsRay(ray, @@ -677,7 +675,7 @@ TEST(primal_bezier_inter, ray_no_intersections_bezier) PointType {3.0, 1.5}}; BezierCurveType curve(data, order); - std::vector exp_intersections; + axom::Array exp_intersections; const double eps = 1E-16; const double eps_test = 1E-10; @@ -726,8 +724,8 @@ TEST(primal_bezier_inter, ray_linear_bezier_interp_params) VectorType ray_direction1({0.0, 1.0}); RayType ray1(ray_origin1, ray_direction1); - std::vector exp_intersections_t = {t}; - std::vector exp_intersections_s = {s}; + axom::Array exp_intersections_t = {t}; + axom::Array exp_intersections_s = {s}; // test for intersections checkIntersectionsRay(ray1, @@ -779,7 +777,7 @@ TEST(primal_bezier_inter, ray_cubic_quadratic_bezier) PointType {3.0, -0.5}}; BezierCurveType curve(data, order); - std::vector all_intersections = {0.17267316464601146, + axom::Array all_intersections = {0.17267316464601146, 0.5, 0.827326835353989}; @@ -797,8 +795,8 @@ TEST(primal_bezier_inter, ray_cubic_quadratic_bezier) auto curve_pt_1 = curve.evaluate(all_intersections[1]); auto curve_pt_2 = curve.evaluate(all_intersections[2]); - std::vector exp_intersections; - std::vector ray_intersections; + axom::Array exp_intersections; + axom::Array ray_intersections; if(origin < curve_pt_0[0]) { exp_intersections.push_back(all_intersections[0]); @@ -846,14 +844,14 @@ TEST(primal_bezier_inter, ray_cubic_bezier_varying_eps) RayType ray(ray_origin, ray_direction); // Cubic curve - PointType data[order + 1] = {PointType {0.0, 0.5}, - PointType {1.0, -1.0}, - PointType {2.0, 1.0}, - PointType {3.0, -0.5}}; + PointType data[order + 1] = {PointType {0.0 / 3.0, 0.5}, + PointType {1.0 / 3.0, -1.0}, + PointType {2.0 / 3.0, 1.0}, + PointType {3.0 / 3.0, -0.5}}; BezierCurveType curve(data, order); // Note: same intersection params for curve and line - std::vector exp_intersections = {0.17267316464601146, + axom::Array exp_intersections = {0.17267316464601146, 0.5, 0.827326835353989}; @@ -876,7 +874,7 @@ TEST(primal_bezier_inter, ray_cubic_bezier_varying_eps) } //------------------------------------------------------------------------------ -TEST(primal_bezier_inter, ray_cubic_bezier_nine_intersections) +TEST(primal_bezier_inter, ray_cubic_bezier_four_intersections) { static const int DIM = 2; using CoordType = double; @@ -907,12 +905,12 @@ TEST(primal_bezier_inter, ray_cubic_bezier_nine_intersections) const double eps = 1E-16; const double eps_test = 1E-10; - std::vector exp_s = {21.19004780603474, + axom::Array exp_s = {21.19004780170474, 45.76845689117871, - 35.941606827, + 35.9416068276128, 45.76845689117871}; - std::vector exp_t = {0.0264232742968, + axom::Array exp_t = {0.0264232742968, 0.2047732691922508, 0.813490954734, 0.96880275626114684}; @@ -920,6 +918,52 @@ TEST(primal_bezier_inter, ray_cubic_bezier_nine_intersections) checkIntersectionsRay(ray, curve, exp_s, exp_t, eps, eps_test); } +//------------------------------------------------------------------------------ +TEST(primal_bezier_inter, ray_nurbs_intersections) +{ + static const int DIM = 2; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using NURBSCurveType = primal::NURBSCurve; + using RayType = primal::Ray; + + SLIC_INFO("primal: testing NURBS intersection"); + + const double eps = 1E-10; + const double eps_test = 1E-10; + + // NURBS Curve which defines an entire circle + PointType data[7] = {PointType {1.0, 0.0}, + PointType {1.0, 2.0}, + PointType {-1.0, 2.0}, + PointType {-1.0, 0.0}, + PointType {-1.0, -2.0}, + PointType {1.0, -2.0}, + PointType {1.0, 0.0}}; + double weights[7] = {1.0, 1. / 3., 1. / 3., 1.0, 1. / 3., 1. / 3., 1.0}; + + double knots[11] = {-1.0, -1.0, -1.0, -1.0, 0.5, 0.5, 0.5, 2.0, 2.0, 2.0, 2.0}; + NURBSCurveType circle(data, weights, 7, knots, 11); + + // Insert some extra knots to make the Bezier extraction more interesting + circle.insertKnot(0.0, 1); + circle.insertKnot(1.0, 2); + + // These parameters include the extra knots at 0.0 and 1.0 + double params[10]; + axom::numerics::linspace(-1.0, 2.0, params, 10); + + PointType ray_origin({0.0, 0.0}); + for(int i = 0; i < 9; ++i) // Skip the last parameter, which is equal to i=0 + { + VectorType ray_direction( ray_origin, circle.evaluate(params[i]) ); + RayType ray(ray_origin, ray_direction); + + checkIntersectionsRay( ray, circle, {1.0}, {params[i]}, eps, eps_test ); + } +} + int main(int argc, char* argv[]) { int result = 0; From 77d0b640f8754c170532975a0e312ffac9f370f8 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 4 Dec 2024 16:13:06 -0700 Subject: [PATCH 18/47] Finalize BezierPatch testing --- .../operators/detail/intersect_patch_impl.hpp | 32 ++++ .../primal/tests/primal_surface_intersect.cpp | 150 +++++++++++------- 2 files changed, 123 insertions(+), 59 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_patch_impl.hpp b/src/axom/primal/operators/detail/intersect_patch_impl.hpp index 9991f7ba7c..3602ff1820 100644 --- a/src/axom/primal/operators/detail/intersect_patch_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_patch_impl.hpp @@ -33,6 +33,38 @@ namespace detail { //---------------------------- FUNCTION DECLARATIONS --------------------------- +/*! + * \brief Recursive function to find the intersections between a line and a Bezier patch + * + * \param [in] line The input line + * \param [in] patch The input patch + * \param [out] tp Parametric coordinates of intersections in \a line + * \param [out] up Parametric coordinates of intersections in \a patch + * \param [out] vp Parametric coordinates of intersections in \a patch + * \param [in] order_u The order of \a line in the u direction + * \param [in] order_v The order of \a line in the v direction + * \param [in] u_offset The offset in parameter space for \a patch in the u direction + * \param [in] u_scale The scale in parameter space for \a patch in the u direction + * \param [in] v_offset The offset in parameter space for \a patch in the v direction + * \param [in] v_scale The scale in parameter space for \a patch in the v direction + * \param [in] sq_tol Numerical tolerance for physical distances + * \param [in] EPS Numerical tolerance in parameter space + * + * A ray can only intersect a Bezier patch if it intersects its bounding box. + * The base case of the recursion is when we can approximate the patch as + * bilinear, where we directly find their intersections. Otherwise, + * check for intersections recursively after bisecting the patch in each direction. + * + * \note This detial function returns all found intersections within EPS of parameter space, + * including identical intersections reported by each subdivision. + * The calling `intersect` routine should remove duplicates and enforce half-open behavior. + * + * \note This function assumes that all intersections have multiplicity + * one, i.e. does not find tangencies. + * + * \return True if the line intersects the patch, False otherwise + * \sa intersect_bezier + */ template bool intersect_line_patch(const Line &line, const BezierPatch &patch, diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp index c040731f00..5ec014d731 100644 --- a/src/axom/primal/tests/primal_surface_intersect.cpp +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -42,6 +42,7 @@ void checkIntersections(const primal::Ray& ray, const axom::Array& exp_v, double eps, double test_eps, + bool isHalfOpen = false, bool shouldPrintIntersections = false) { constexpr int DIM = 3; @@ -58,7 +59,7 @@ void checkIntersections(const primal::Ray& ray, // Intersect the ray and the patch, intersection parameters will be // in arrays (u, v) and t, for the patch and ray, respectively Array u, v, t; - bool ray_intersects = intersect(ray, patch, t, u, v); + bool ray_intersects = intersect(ray, patch, t, u, v, 1e-8, 1e-8, isHalfOpen); EXPECT_EQ(exp_intersect, ray_intersects); EXPECT_EQ(u.size(), v.size()); EXPECT_EQ(u.size(), t.size()); @@ -212,6 +213,7 @@ TEST(primal_surface_inter, bilinear_boundary_condition) const double eps = 1E-16; const double eps_test = 1E-10; + const bool isHalfOpen = true; SLIC_INFO("primal: testing bilinear patch intersection"); @@ -227,54 +229,81 @@ TEST(primal_surface_inter, bilinear_boundary_condition) VectorType ray_direction; RayType ray(ray_origin, ray_direction); + // Check both options for isHalfOpen + ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.0, 1.0)); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); + checkIntersections(ray, bilinear_patch, {sqrt(3.0)}, {0.0}, {1.0}, eps, eps_test, !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.5, 1.0)); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); + checkIntersections(ray, bilinear_patch, {sqrt(13.0) / 2.0}, {0.5}, {1.0}, eps, eps_test, !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.5)); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); + checkIntersections(ray, bilinear_patch, {sqrt(13.0) / 2.0}, {1.0}, {0.5}, eps, eps_test, !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.0)); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); + checkIntersections(ray, bilinear_patch, {sqrt(3.0)}, {1.0}, {0.0}, eps, eps_test, !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.5)); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); + checkIntersections(ray, bilinear_patch, {sqrt(13.0) / 2.0}, {1.0}, {0.5}, eps, eps_test, !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 1.0)); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); + checkIntersections(ray, bilinear_patch, {sqrt(6.0)}, {1.0}, {1.0}, eps, eps_test, !isHalfOpen); + + // These should record an intersection with both options ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.0, 0.0)); ray = RayType(ray_origin, ray_direction); - // Incorrect due to floating point arithmetic - // checkIntersections(ray, bilinear_patch, {sqrt(6.0)}, {0.0}, {0.0}, eps, eps_test); + for(bool option : {isHalfOpen, !isHalfOpen}) + { + checkIntersections(ray, + bilinear_patch, + {sqrt(6.0)}, + {0.0}, + {0.0}, + eps, + eps_test, + option); + } ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.5, 0.0)); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, - // bilinear_patch, - // {sqrt(13.0) / 2.0}, - // {0.5}, - // {0.0}, - // eps, - // eps_test); + for(bool option : {isHalfOpen, !isHalfOpen}) + { + checkIntersections(ray, + bilinear_patch, + {sqrt(13.0) / 2.0}, + {0.5}, + {0.0}, + eps, + eps_test, + option); + } ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.0, 0.5)); ray = RayType(ray_origin, ray_direction); - // checkIntersections(ray, - // bilinear_patch, - // {sqrt(13.0) / 2.0}, - // {0.0}, - // {0.5}, - // eps, - // eps_test); + for(bool option : {isHalfOpen, !isHalfOpen}) + { + checkIntersections(ray, + bilinear_patch, + {sqrt(13.0) / 2.0}, + {0.0}, + {0.5}, + eps, + eps_test, + option); + } } //------------------------------------------------------------------------------ @@ -513,17 +542,18 @@ TEST(primal_surface_inter, bezier_surface_intersect) // with exact precision const double eps = 1E-5; const double eps_test = 1E-5; + const bool isHalfOpen = true; PointType ray_origin({0.0, 0.0, 0.0}); // Try points which will be interior to all subdivisions - double u_params[10], v_params[10]; - axom::numerics::linspace(0.0, 1.0, u_params, 10); - axom::numerics::linspace(0.0, 1.0, v_params, 10); + double u_params[8], v_params[8]; + axom::numerics::linspace(0.0, 1.0, u_params, 8); + axom::numerics::linspace(0.0, 1.0, v_params, 8); - for(int i = 0; i < 10; ++i) + for(int i = 0; i < 8; ++i) { - for(int j = 0; j < 10; ++j) + for(int j = 0; j < 8; ++j) { VectorType ray_direction( ray_origin, @@ -533,8 +563,17 @@ TEST(primal_surface_inter, bezier_surface_intersect) // Points on the edge should not be recorded if(i == 9 || j == 9) { - continue; - checkIntersections(ray, sphere_face_patch, {}, {}, {}, eps, eps_test); + // Check both settings of isHalfOpen + checkIntersections(ray, sphere_face_patch, {}, {}, {}, eps, eps_test, isHalfOpen); + + checkIntersections(ray, + sphere_face_patch, + {1.0}, + {u_params[i]}, + {v_params[j]}, + eps, + eps_test, + !isHalfOpen); } else { @@ -552,12 +591,12 @@ TEST(primal_surface_inter, bezier_surface_intersect) // Use different parameter values so that intersections // are on the boundary of subdivisions - axom::numerics::linspace(0.0, 1.0, u_params, 9); - axom::numerics::linspace(0.0, 1.0, v_params, 9); + axom::numerics::linspace(0.0, 1.0, u_params, 5); + axom::numerics::linspace(0.0, 1.0, v_params, 5); - for(int i = 0; i < 9; ++i) + for(int i = 0; i < 5; ++i) { - for(int j = 0; j < 9; ++j) + for(int j = 0; j < 5; ++j) { VectorType ray_direction( ray_origin, @@ -567,39 +606,32 @@ TEST(primal_surface_inter, bezier_surface_intersect) // Points on the edge should not be recorded if(i == 8 || j == 8) { - continue; - checkIntersections(ray, sphere_face_patch, {}, {}, {}, eps, eps_test); + checkIntersections(ray, sphere_face_patch, {}, {}, {}, eps, eps_test, isHalfOpen); + + checkIntersections(ray, + sphere_face_patch, + {1.0}, + {u_params[i]}, + {v_params[j]}, + eps, + eps_test, + !isHalfOpen); } else { - // continue; - // checkIntersections(ray, - // sphere_face_patch, - // {1.0}, - // {u_params[i]}, - // {v_params[j]}, - // eps, - // eps_test); - VectorType ray_direction(ray_origin, - sphere_face_patch.evaluate(u_params[i], v_params[j])); - - RayType ray(ray_origin, ray_direction); - axom::Array t, u, v; - bool ray_intersects = intersect(ray, sphere_face_patch, t, u, v); - - std::cout << "------------------" << std::endl; - for( int i = 0; i < t.size(); ++i ) - { - std::cout << "t: " << t[i] << ", u: " << u[i] << ", v: " << v[i] << std::endl; - } - - int xx= 12; + checkIntersections(ray, + sphere_face_patch, + {1.0}, + {u_params[i]}, + {v_params[j]}, + eps, + eps_test); } } } VectorType ray_direction(ray_origin, - sphere_face_patch.evaluate(u_params[0], v_params[6])); + sphere_face_patch.evaluate(u_params[0], v_params[6])); RayType ray(ray_origin, ray_direction); axom::Array t, u, v; From 8a479ebc0968b15e3c51c4a33ef58d83ff0280c0 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 4 Dec 2024 18:14:33 -0700 Subject: [PATCH 19/47] Begin more general NURBS intersector --- src/axom/primal/CMakeLists.txt | 1 + src/axom/primal/geometry/NURBSPatch.hpp | 2764 +++++++++++++++++ src/axom/primal/operators/intersect.hpp | 121 +- .../primal/tests/primal_surface_intersect.cpp | 157 +- 4 files changed, 3024 insertions(+), 19 deletions(-) create mode 100644 src/axom/primal/geometry/NURBSPatch.hpp diff --git a/src/axom/primal/CMakeLists.txt b/src/axom/primal/CMakeLists.txt index 0dbcb2c52f..a160b823e6 100644 --- a/src/axom/primal/CMakeLists.txt +++ b/src/axom/primal/CMakeLists.txt @@ -31,6 +31,7 @@ set( primal_headers geometry/OrientationResult.hpp geometry/NumericArray.hpp geometry/NURBSCurve.hpp + geometry/NURBSPatch.hpp geometry/Plane.hpp geometry/Point.hpp geometry/Polygon.hpp diff --git a/src/axom/primal/geometry/NURBSPatch.hpp b/src/axom/primal/geometry/NURBSPatch.hpp new file mode 100644 index 0000000000..f9d71cbd5d --- /dev/null +++ b/src/axom/primal/geometry/NURBSPatch.hpp @@ -0,0 +1,2764 @@ +// Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/*! + * \file NURBSPatch.hpp + * + * \brief A (trimmed) NURBSPatch primitive + */ + +#ifndef AXOM_PRIMAL_NURBSPATCH_HPP_ +#define AXOM_PRIMAL_NURBSPATCH_HPP_ + +#include "axom/core.hpp" +#include "axom/slic.hpp" + +#include "axom/primal/geometry/NumericArray.hpp" +#include "axom/primal/geometry/Point.hpp" +#include "axom/primal/geometry/Vector.hpp" +#include "axom/primal/geometry/Segment.hpp" +#include "axom/primal/geometry/NURBSCurve.hpp" +#include "axom/primal/geometry/BezierPatch.hpp" +#include "axom/primal/geometry/BoundingBox.hpp" +#include "axom/primal/geometry/OrientedBoundingBox.hpp" + +#include "axom/primal/operators/squared_distance.hpp" + +#include + +namespace axom +{ +namespace primal +{ +// Forward declare the templated classes and operator functions +template +class NURBSPatch; + +/*! \brief Overloaded output operator for NURBS Patches*/ +template +std::ostream& operator<<(std::ostream& os, const NURBSPatch& nPatch); + +/*! + * \class NURBSPatch + * + * \brief Represents a 3D NURBS patch defined by a 2D array of control points + * \tparam T the coordinate type, e.g., double, float, etc. + * + * A NURBS patch has degrees `p` and `q` with knot vectors of length + * `r+1` and `s+1` respectively. There is a control net of (n + 1) * (m + 1) points + * with r+1 = n+p+2 and s+1 = m+q+2. + * Optionally has an equal number of weights for rational patches. + * + * The patch must be open (clamped on all boundaries) + * and continuous (unless p = 0 or q = 0) + * + * Nonrational NURBS patches are identified by an empty weights array. + */ +template +class NURBSPatch +{ +public: + using PointType = Point; + using VectorType = Vector; + using PlaneType = Plane; + + using CoordsVec = axom::Array; + using CoordsMat = axom::Array; + using WeightsVec = axom::Array; + using WeightsMat = axom::Array; + using KnotVectorType = KnotVector; + + using BoundingBoxType = BoundingBox; + using OrientedBoundingBoxType = OrientedBoundingBox; + using NURBSCurveType = primal::NURBSCurve; + + AXOM_STATIC_ASSERT_MSG( + (NDIMS == 1) || (NDIMS == 2) || (NDIMS == 3), + "A NURBS Patch object may be defined in 1-, 2-, or 3-D"); + + AXOM_STATIC_ASSERT_MSG( + std::is_arithmetic::value, + "A NURBS Patch must be defined using an arithmetic type"); + +public: + /*! + * \brief Default constructor for an empty (invalid) NURBS patch + * + * \note An empty NURBS patch is not valid + */ + NURBSPatch() + { + m_controlPoints.resize(0, 0); + m_weights.resize(0, 0); + m_knotvec_u = KnotVectorType(); + m_knotvec_v = KnotVectorType(); + + makeNonrational(); + } + + /*! + * \brief Constructor for a simple NURBS surface that reserves space for + * the minimum (sensible) number of points for the given degrees + * + * Constructs an empty patch by default (no nodes/weights on either axis) + * + * \param [in] deg_u The patch's degree on the first axis + * \param [in] deg_v The patch's degree on the second axis + * \pre deg_u, deg_v greater than or equal to 0. + */ + NURBSPatch(int deg_u, int deg_v) + { + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + m_controlPoints.resize(deg_u + 1, deg_v + 1); + m_knotvec_u = KnotVectorType(deg_u + 1, deg_u); + m_knotvec_v = KnotVectorType(deg_v + 1, deg_v); + + makeNonrational(); + } + + /*! + * \brief Constructor for an empty NURBS surface from its size + * + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] deg_u The patch's degree on the first axis + * \param [in] deg_v The patch's degree on the second axis + * + * \pre Requires npts_d > deg_d and deg_d >= 0 for d = u, v + */ + NURBSPatch(int npts_u, int npts_v, int deg_u, int deg_v) + { + SLIC_ASSERT(npts_u > deg_u && npts_v > deg_v); + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + m_knotvec_u = KnotVectorType(npts_u, deg_u); + m_knotvec_v = KnotVectorType(npts_v, deg_v); + + makeNonrational(); + } + + /*! + * \brief Constructor for a NURBS surface from a Bezier surface + * + * \param [in] bezierPatch the Bezier patch to convert to a NURBS patch + */ + explicit NURBSPatch(const BezierPatch& bezierPatch) + { + m_controlPoints = bezierPatch.getControlPoints(); + m_weights = bezierPatch.getWeights(); + + int deg_u = bezierPatch.getOrder_u(); + int deg_v = bezierPatch.getOrder_v(); + + m_knotvec_u = KnotVectorType(deg_u + 1, deg_u); + m_knotvec_v = KnotVectorType(deg_v + 1, deg_v); + } + + /*! + * \brief Constructor for a NURBS Patch from an array of coordinates and degrees + * + * \param [in] pts A 1D C-style array of npts_u*npts_v control points + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] deg_u The patch's degree on the first axis + * \param [in] deg_v The patch's degree on the second axis + * \pre Requires that npts_d >= deg_d + 1 and deg_d >= 0 for d = u, v + * + * The knot vectors are constructed such that the patch is uniform + * + * Elements of pts[k] are mapped to control nodes (p, q) lexicographically, i.e. + * pts[k] = nodes[ k // (npts_u + 1), k % npts_v ] + */ + NURBSPatch(const PointType* pts, int npts_u, int npts_v, int deg_u, int deg_v) + { + SLIC_ASSERT(pts != nullptr); + SLIC_ASSERT(npts_u >= deg_u + 1 && npts_v >= deg_v + 1); + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < npts_u * npts_v; ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + makeNonrational(); + + m_knotvec_u = KnotVectorType(npts_u, deg_u); + m_knotvec_v = KnotVectorType(npts_v, deg_v); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from arrays of coordinates and weights + * + * \param [in] pts A 1D C-style array of (ord_u+1)*(ord_v+1) control points + * \param [in] weights A 1D C-style array of (ord_u+1)*(ord_v+1) positive weights + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] deg_u The patch's degree on the first axis + * \param [in] deg_v The patch's degree on the second axis + * \pre Requires that npts_d >= deg_d + 1 and deg_d >= 0 for d = u, v + * + * The knot vectors are constructed such that the patch is uniform + * + * Elements of pts[k] are mapped to control nodes (p, q) lexicographically, i.e. + * pts[k] = nodes[ k // (npts_u + 1), k % npts_v ] + */ + NURBSPatch(const PointType* pts, + const T* weights, + int npts_u, + int npts_v, + int deg_u, + int deg_v) + { + SLIC_ASSERT(pts != nullptr); + SLIC_ASSERT(weights != nullptr); + SLIC_ASSERT(npts_u >= deg_u + 1 && npts_v >= deg_v + 1); + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < npts_u * npts_v; ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + m_weights.resize(npts_u, npts_v); + for(int t = 0; t < npts_u * npts_v; ++t) + { + m_weights.flatIndex(t) = weights[t]; + } + + m_knotvec_u = KnotVectorType(npts_u, deg_u); + m_knotvec_v = KnotVectorType(npts_v, deg_v); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 1D arrays of coordinates and degrees + * + * \param [in] pts A 1D axom::Array of npts_u*npts_v control points + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] deg_u The patch's degree on the first axis + * \param [in] deg_v The patch's degree on the second axis + * \pre Requires that npts_d >= deg_d + 1 and deg_d >= 0 for d = u, v + * + * The knot vectors are constructed such that the patch is uniform + * + * Elements of pts[k] are mapped to control nodes (p, q) lexicographically, i.e. + * pts[k] = nodes[ k // (npts_u + 1), k % npts_v ] + */ + NURBSPatch(const CoordsVec& pts, int npts_u, int npts_v, int deg_u, int deg_v) + { + SLIC_ASSERT(npts_u >= deg_u + 1 && npts_v >= deg_v + 1); + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < pts.size(); ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + makeNonrational(); + + m_knotvec_u = KnotVectorType(npts_u, deg_u); + m_knotvec_v = KnotVectorType(npts_v, deg_v); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 1D arrays of coordinates and weights + * + * \param [in] pts A 1D axom::Array of (ord_u+1)*(ord_v+1) control points + * \param [in] weights A 1D axom::Array of (ord_u+1)*(ord_v+1) positive weights + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] deg_u The patch's degree on the first axis + * \param [in] deg_v The patch's degree on the second axis + * \pre Requires that npts_d >= deg_d + 1 and deg_d >= 0 for d = u, v + * + * The knot vectors are constructed such that the patch is uniform + * + * Elements of pts[k] are mapped to control nodes (p, q) lexicographically, i.e. + * pts[k] = nodes[ k // (npts_u + 1), k % npts_v ] + */ + NURBSPatch(const CoordsVec& pts, + const WeightsVec& weights, + int npts_u, + int npts_v, + int deg_u, + int deg_v) + { + SLIC_ASSERT(npts_u > deg_u && npts_v > deg_v); + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < pts.size(); ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + m_weights.resize(npts_u, npts_v); + for(int t = 0; t < weights.size(); ++t) + { + m_weights.flatIndex(t) = weights[t]; + } + + m_knotvec_u = KnotVectorType(npts_u, deg_u); + m_knotvec_v = KnotVectorType(npts_v, deg_v); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 2D arrays of coordinates and degrees + * + * \param [in] pts A 2D axom::Array of (npts_u, npts_v) control points + * \param [in] deg_u The patch's degree on the first axis + * \param [in] deg_v The patch's degree on the second axis + * \pre Requires that npts_d >= deg_d + 1 and deg_d >= 0 for d = u, v + * + * The knot vectors are constructed such that the patch is uniform + */ + NURBSPatch(const CoordsMat& pts, int deg_u, int deg_v) : m_controlPoints(pts) + { + auto pts_shape = pts.shape(); + + SLIC_ASSERT(pts_shape[0] >= deg_u + 1 && pts_shape[1] >= deg_v + 1); + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + makeNonrational(); + + m_knotvec_u = KnotVectorType(pts_shape[0], deg_u); + m_knotvec_v = KnotVectorType(pts_shape[1], deg_v); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 2D arrays of coordinates and weights + * + * \param [in] pts A 2D axom::Array of (ord_u+1, ord_v+1) control points + * \param [in] weights A 2D axom::Array of (ord_u+1, ord_v+1) positive weights + * \param [in] deg_u The patch's degree on the first axis + * \param [in] deg_v The patch's degree on the second axis + * \pre Requires that npts_d >= deg_d + 1 and deg_d >= 0 for d = u, v + * + * The knot vectors are constructed such that the patch is uniform + */ + NURBSPatch(const CoordsMat& pts, const WeightsMat& weights, int deg_u, int deg_v) + : m_controlPoints(pts) + , m_weights(weights) + { + auto pts_shape = pts.shape(); + auto weights_shape = weights.shape(); + + SLIC_ASSERT(pts_shape[0] >= deg_u + 1 && pts_shape[1] >= deg_v + 1); + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + m_knotvec_u = KnotVectorType(pts_shape[0], deg_u); + m_knotvec_v = KnotVectorType(pts_shape[1], deg_v); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from C-style arrays of coordinates and knot vectors + * + * \param [in] pts A 1D C-style array of npts_u*npts_v control points + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] knots_u A 1D C-style array of npts_u + deg_u + 1 knots + * \param [in] nkts_u The number of knots in the u direction + * \param [in] knots_v A 1D C-style array of npts_v + deg_v + 1 knots + * \param [in] nkts_v The number of knots in the v direction + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires valid pointers and knot vectors + */ + NURBSPatch(const PointType* pts, + int npts_u, + int npts_v, + const T* knots_u, + int nkts_u, + const T* knots_v, + int nkts_v) + { + SLIC_ASSERT(pts != nullptr && knots_u != nullptr && knots_v != nullptr); + SLIC_ASSERT(npts_u >= 0 && npts_v >= 0); + SLIC_ASSERT(nkts_u >= 0 && nkts_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < npts_u * npts_v; ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + makeNonrational(); + + m_knotvec_u = KnotVectorType(knots_u, nkts_u, nkts_u - npts_u - 1); + m_knotvec_v = KnotVectorType(knots_v, nkts_v, nkts_v - npts_v - 1); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from C-style arrays of coordinates and weights + * + * \param [in] pts A 1D C-style array of npts_u*npts_v control points + * \param [in] weights A 1D C-style array of npts_u*npts_v positive weights + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] knots_u A 1D C-style array of npts_u + deg_u + 1 knots + * \param [in] nkts_u The number of knots in the u direction + * \param [in] knots_v A 1D C-style array of npts_v + deg_v + 1 knots + * \param [in] nkts_v The number of knots in the v direction + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires valid pointers and knot vectors + */ + NURBSPatch(const PointType* pts, + const T* weights, + int npts_u, + int npts_v, + const T* knots_u, + int nkts_u, + const T* knots_v, + int nkts_v) + { + SLIC_ASSERT(pts != nullptr && weights != nullptr && knots_u != nullptr && + knots_v != nullptr); + SLIC_ASSERT(npts_u >= 0 && npts_v >= 0); + SLIC_ASSERT(nkts_u >= 0 && nkts_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < npts_u * npts_v; ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + m_weights.resize(npts_u, npts_v); + for(int t = 0; t < npts_u * npts_v; ++t) + { + m_weights.flatIndex(t) = weights[t]; + } + + m_knotvec_u = KnotVectorType(knots_u, nkts_u, nkts_u - npts_u - 1); + m_knotvec_v = KnotVectorType(knots_v, nkts_v, nkts_v - npts_v - 1); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 1D axom::Array arrays of coordinates and knots + * + * \param [in] pts A 1D axom::Array of npts_u*npts_v control points + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] knots_u An axom::Array of npts_u + deg_u + 1 knots + * \param [in] knots_v An axom::Array of npts_v + deg_v + 1 knots + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires a valid knot vector and npts_d > deg_d + */ + NURBSPatch(const CoordsVec& pts, + int npts_u, + int npts_v, + const axom::Array& knots_u, + const axom::Array& knots_v) + { + SLIC_ASSERT(npts_u >= 0 && npts_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < pts.size(); ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + makeNonrational(); + + m_knotvec_u = KnotVectorType(knots_u, knots_u.size() - npts_u - 1); + m_knotvec_v = KnotVectorType(knots_v, knots_v.size() - npts_v - 1); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 1D axom::Array arrays of coordinates, weights, and knots + * + * \param [in] pts A 1D axom::Array of npts_u*npts_v control points + * \param [in] weights A 1D axom::Array of npts_u*npts_v positive weights + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] knots_u An axom::Array of npts_u + deg_u + 1 knots + * \param [in] knots_v An axom::Array of npts_v + deg_v + 1 knots + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires a valid knot vector and npts_d > deg_d + */ + NURBSPatch(const CoordsVec& pts, + const WeightsVec& weights, + int npts_u, + int npts_v, + const axom::Array& knots_u, + const axom::Array& knots_v) + { + SLIC_ASSERT(npts_u >= 0 && npts_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < pts.size(); ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + m_weights.resize(npts_u, npts_v); + for(int t = 0; t < weights.size(); ++t) + { + m_weights.flatIndex(t) = weights[t]; + } + + m_knotvec_u = KnotVectorType(knots_u, knots_u.size() - npts_u - 1); + m_knotvec_v = KnotVectorType(knots_v, knots_v.size() - npts_v - 1); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 1D axom::Array arrays of coordinates and KnotVectors + * + * \param [in] pts A 1D axom::Array of npts_u*npts_v control points + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] knotvec_u An KnotVector object for the first axis + * \param [in] knotvec_v An KnotVector object for the second axis + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires a valid knot vector and npts_d > deg_d + */ + NURBSPatch(const CoordsVec& pts, + int npts_u, + int npts_v, + const KnotVectorType& knotvec_u, + const KnotVectorType& knotvec_v) + : m_knotvec_u(knotvec_u) + , m_knotvec_v(knotvec_v) + { + SLIC_ASSERT(npts_u >= 0 && npts_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < pts.size(); ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + makeNonrational(); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 1D axom::Array arrays of coordinates, weights, and KnotVectors + * + * \param [in] pts A 1D axom::Array of npts_u*npts_v control points + * \param [in] weights A 1D axom::Array of npts_u*npts_v positive weights + * \param [in] npts_u The number of control points on the first axis + * \param [in] npts_v The number of control points on the second axis + * \param [in] knotvec_u An KnotVector object for the first axis + * \param [in] knotvec_v An KnotVector object for the second axis + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires a valid knot vector and npts_d > deg_d + */ + NURBSPatch(const CoordsVec& pts, + const WeightsVec& weights, + int npts_u, + int npts_v, + const KnotVectorType& knotvec_u, + const KnotVectorType& knotvec_v) + : m_knotvec_u(knotvec_u) + , m_knotvec_v(knotvec_v) + { + SLIC_ASSERT(npts_u >= 0 && npts_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + for(int t = 0; t < pts.size(); ++t) + { + m_controlPoints.flatIndex(t) = pts[t]; + } + + m_weights.resize(npts_u, npts_v); + for(int t = 0; t < weights.size(); ++t) + { + m_weights.flatIndex(t) = weights[t]; + } + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 2D axom::Array array of coordinates and array of knots + * + * \param [in] pts A 2D axom::Array of (npts_u, npts_v) control points + * \param [in] knots_u An axom::Array of npts_u + deg_u + 1 knots + * \param [in] knots_v An axom::Array of npts_v + deg_v + 1 knots + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires a valid knot vector and npts_d > deg_d + */ + NURBSPatch(const CoordsMat& pts, + const axom::Array& knots_u, + const axom::Array& knots_v) + : m_controlPoints(pts) + { + auto pts_shape = pts.shape(); + + SLIC_ASSERT(pts_shape[0] >= 0 && pts_shape[1] >= 0); + + makeNonrational(); + + m_knotvec_u = KnotVectorType(knots_u, knots_u.size() - pts_shape[0] - 1); + m_knotvec_v = KnotVectorType(knots_v, knots_v.size() - pts_shape[1] - 1); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 2D axom::Array array of coordinates, weights, and array of knots + * + * \param [in] pts A 2D axom::Array of (ord_u+1, ord_v+1) control points + * \param [in] weights A 2D axom::Array of (ord_u+1, ord_v+1) positive weights + * \param [in] knots_u An axom::Array of npts_u + deg_u + 1 knots + * \param [in] knots_v An axom::Array of npts_v + deg_v + 1 knots + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires a valid knot vector and npts_d > deg_d + */ + NURBSPatch(const CoordsMat& pts, + const WeightsMat& weights, + const axom::Array& knots_u, + const axom::Array& knots_v) + : m_controlPoints(pts) + , m_weights(weights) + { + auto pts_shape = pts.shape(); + + SLIC_ASSERT(pts_shape[0] >= 0 && pts_shape[1] >= 0); + + m_knotvec_u = KnotVectorType(knots_u, knots_u.size() - pts_shape[0] - 1); + m_knotvec_v = KnotVectorType(knots_v, knots_v.size() - pts_shape[1] - 1); + + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 1D axom::Array array of coordinates and KnotVector objects + * + * \param [in] pts A 2D axom::Array of (ord_u+1, ord_v+1) control points + * \param [in] knotvec_u A KnotVector object for the first axis + * \param [in] knotvec_v A KnotVector object for the second axis + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires a valid knot vector and npts_d > deg_d + */ + NURBSPatch(const CoordsMat& pts, + const KnotVectorType& knotvec_u, + const KnotVectorType& knotvec_v) + : m_controlPoints(pts) + , m_knotvec_u(knotvec_u) + , m_knotvec_v(knotvec_v) + { + makeNonrational(); + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Constructor for a NURBS Patch from 2D axom::Array array of coordinates, weights, and KnotVector objects + * + * \param [in] pts A 2D axom::Array of (ord_u+1, ord_v+1) control points + * \param [in] weights A 2D axom::Array of (ord_u+1, ord_v+1) positive weights + * \param [in] knotvec_u A KnotVector object for the first axis + * \param [in] knotvec_v A KnotVector object for the second axis + * + * For clamped and continuous curves, npts and the knot vector + * uniquely determine the degree + * + * \pre Requires a valid knot vector and npts_d > deg_d + */ + NURBSPatch(const CoordsMat& pts, + const WeightsMat& weights, + const KnotVectorType& knotvec_u, + const KnotVectorType& knotvec_v) + : m_controlPoints(pts) + , m_weights(weights) + , m_knotvec_u(knotvec_u) + , m_knotvec_v(knotvec_v) + { + SLIC_ASSERT(isValidNURBS()); + } + + /*! + * \brief Evaluate a NURBS surface at a particular parameter value \a t + * + * \param [in] u The parameter value on the first axis + * \param [in] v The parameter value on the second axis + * + * Adapted from Algorithm A3.5 on page 103 of "The NURBS Book" + * + * \pre Requires \a u, v in the span of each knot vector + */ + PointType evaluate(T u, T v) const + { + SLIC_ASSERT(u >= m_knotvec_u[0] && + u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + SLIC_ASSERT(v >= m_knotvec_v[0] && + v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + + const auto span_u = m_knotvec_u.findSpan(u); + const auto span_v = m_knotvec_v.findSpan(v); + + const auto basis_funs_u = + m_knotvec_u.calculateBasisFunctionsBySpan(span_u, u); + const auto basis_funs_v = + m_knotvec_v.calculateBasisFunctionsBySpan(span_v, v); + + const auto deg_u = getDegree_u(); + const auto deg_v = getDegree_v(); + + int ind_u = span_u - deg_u; + + PointType S = PointType::zero(); + + if(isRational()) + { + // Evaluate the homogeneous point + Point Sw = Point::zero(); + for(int l = 0; l <= deg_v; ++l) + { + Point temp = Point::zero(); + int ind_v = span_v - deg_v + l; + for(int k = 0; k <= deg_u; ++k) + { + auto& the_weight = m_weights(ind_u + k, ind_v); + auto& the_pt = m_controlPoints(ind_u + k, ind_v); + + for(int i = 0; i < NDIMS; ++i) + { + temp[i] += basis_funs_u[k] * the_weight * the_pt[i]; + } + temp[NDIMS] += basis_funs_u[k] * the_weight; + } + + for(int i = 0; i < NDIMS; ++i) + { + Sw[i] += basis_funs_v[l] * temp[i]; + } + Sw[NDIMS] += basis_funs_v[l] * temp[NDIMS]; + } + + // Project the point back to coordinate space + for(int i = 0; i < NDIMS; ++i) + { + S[i] = Sw[i] / Sw[NDIMS]; + } + } + else + { + for(int l = 0; l <= deg_v; ++l) + { + PointType temp = PointType::zero(); + int ind_v = span_v - deg_v + l; + for(int k = 0; k <= deg_u; ++k) + { + for(int i = 0; i < NDIMS; ++i) + { + temp[i] += basis_funs_u[k] * m_controlPoints(ind_u + k, ind_v)[i]; + } + } + for(int i = 0; i < NDIMS; ++i) + { + S[i] += basis_funs_v[l] * temp[i]; + } + } + } + + return S; + } + + /*! + * \brief Reset the degree and resize arrays of points (and weights) + * + * \param [in] npts_u The target number of control points on the first axis + * \param [in] npts_v The target number of control points on the second axis + * \param [in] deg_u The target degree on the first axis + * \param [in] deg_v The target degree on the second axis + * + * \warning This method will replace existing knot vectors with a uniform one. + */ + void setParameters(int npts_u, int npts_v, int deg_u, int deg_v) + { + SLIC_ASSERT(npts_u > deg_u && npts_v > deg_v); + SLIC_ASSERT(deg_u >= 0 && deg_v >= 0); + + m_controlPoints.resize(npts_u, npts_v); + + if(isRational()) + { + m_weights.resize(npts_u, npts_v); + } + + m_knotvec_u = KnotVectorType(npts_u, deg_u); + m_knotvec_v = KnotVectorType(npts_v, deg_v); + + makeNonrational(); + } + + /*! + * \brief Reset the knot vector in u + * + * \param [in] deg The target degree + * + * \warning This method does NOT change the existing control points, + * i.e. does not perform degree elevation/reduction. + * Will replace existing knot vector with a uniform one. + * + * \pre Requires deg_u < npts_u and deg >= 0 + */ + void setDegree_u(int deg) + { + SLIC_ASSERT(0 <= deg && deg < getNumControlPoints_u()); + + m_knotvec_u.makeUniform(getNumControlPoints_u(), deg); + } + + /*! + * \brief Reset the knot vector in v + * + * \param [in] deg The target degree + * + * \warning This method does NOT change the existing control points, + * i.e. does not perform degree elevation/reduction. + * Will replace existing knot vector with a uniform one. + * + * \pre Requires deg_v < npts_v and deg >= 0 + */ + void setDegree_v(int deg) + { + SLIC_ASSERT(0 <= deg && deg < getNumControlPoints_v()); + + m_knotvec_v.makeUniform(getNumControlPoints_v(), deg); + } + + /*! + * \brief Reset the knot vector and increase the number of control points + * + * \param [in] deg_u The target degree in u + * \param [in] deg_v The target degree in v + * + * \warning This method does NOT change the existing control points, + * i.e. is not performing degree elevation or reduction. + * \pre Requires deg_u < npts_u and deg_v < npts_v + */ + void setDegree(int deg_u, int deg_v) + { + setDegree_u(deg_u); + setDegree_v(deg_v); + } + + /*! + * \brief Set the number control points in u + * + * \param [in] npts The target number of control points + * + * \warning This method does NOT maintain the patch shape, + * i.e. is not performing knot insertion/removal. + * Will replace existing knot vectots with uniform ones. + */ + void setNumControlPoints(int npts_u, int npts_v) + { + SLIC_ASSERT(npts_u > getDegree_u()); + SLIC_ASSERT(npts_v > getDegree_v()); + + m_controlPoints.resize(npts_u, npts_v); + + if(isRational()) + { + m_weights.resize(npts_u, npts_v); + } + + m_knotvec_u.makeUniform(npts_u, getDegree_u()); + m_knotvec_v.makeUniform(npts_v, getDegree_v()); + } + + /*! + * \brief Set the number control points in u + * + * \param [in] npts The target number of control points + * + * \warning This method does NOT maintain the patch shape, + * i.e. is not performing knot insertion/removal. + */ + void setNumControlPoints_u(int npts) + { + SLIC_ASSERT(npts > getDegree_u()); + + m_controlPoints.resize(npts, getNumControlPoints_v()); + + if(isRational()) + { + m_weights.resize(npts, getNumControlPoints_v()); + } + + m_knotvec_u.makeUniform(npts, getDegree_u()); + } + + /*! + * \brief Set the number control points in v + * + * \param [in] npts The target number of control points + * + * \warning This method does NOT maintain the patch shape, + * i.e. is not performing knot insertion/removal. + */ + void setNumControlPoints_v(int npts) + { + SLIC_ASSERT(npts > getDegree_v()); + + m_controlPoints.resize(getNumControlPoints_u(), npts); + + if(isRational()) + { + m_weights.resize(getNumControlPoints_u(), npts); + } + + m_knotvec_v.makeUniform(npts, getDegree_v()); + } + + /*! + * \brief Set the knot value in the u vector at a specific index + * + * \param [in] idx The index of the knot + * \param [in] knot The updated value of the knot + */ + void setKnot_u(int idx, T knot) { m_knotvec_u[idx] = knot; } + + /*! + * \brief Set the knot value in the v vector at a specific index + * + * \param [in] idx The index of the knot + * \param [in] knot The updated value of the knot + */ + void setKnot_v(int idx, T knot) { m_knotvec_v[idx] = knot; } + + /*! + * \brief Set the u knot vector by an axom::Array + * + * \param [in] knots The new knot vector + */ + void setKnots_u(const axom::Array& knots, int degree) + { + m_knotvec_u = KnotVectorType(knots, degree); + } + + /*! + * \brief Set the v knot vector by an axom::Array + * + * \param [in] knots The new knot vector + */ + void setKnots_v(const axom::Array& knots, int degree) + { + m_knotvec_v = KnotVectorType(knots, degree); + } + + /*! + * \brief Set the u knot vector by a KnotVector object + * + * \param [in] knotVector The new knot vector + */ + void setKnots_u(const KnotVectorType& knotVector) + { + m_knotvec_u = knotVector; + } + + /*! + * \brief Set the v knot vector by a KnotVector object + * + * \param [in] knotVector The new knot vector + */ + void setKnots_v(const KnotVectorType& knotVector) + { + m_knotvec_v = knotVector; + } + + /// \brief Returns the degree of the NURBS Patch on the first axis + int getDegree_u() const { return m_knotvec_u.getDegree(); } + + /// \brief Returns the degree of the NURBS Patch on the second axis + int getDegree_v() const { return m_knotvec_v.getDegree(); } + + /// \brief Returns the order (degree + 1) of the NURBS Patch on the first axis + int getOrder_u() const { return m_knotvec_u.getDegree() + 1; } + + /// \brief Returns the order of the NURBS Patch on the second axis + int getOrder_v() const { return m_knotvec_v.getDegree() + 1; } + + /// \brief Return a copy of the KnotVector instance on the first axis + KnotVectorType getKnots_u() const { return m_knotvec_u; } + + /// \brief Return an array of knot values on the first axis + axom::Array getKnotsArray_u() const { return m_knotvec_u.getArray(); } + + /// \brief Return an array of the unique knots in the knot vector + axom::Array getUniqueKnots_u() const { return m_knotvec_u.getUniqueKnots(); } + + /// \brief Return a copy of the KnotVector instance on the second axis + KnotVectorType getKnots_v() const { return m_knotvec_v; } + + /// \brief Return an array of knot values on the second axis + axom::Array getKnotsArray_v() const { return m_knotvec_v.getArray(); } + + /// \brief Return an array of the unique knots in the knot vector + axom::Array getUniqueKnots_v() const { return m_knotvec_v.getUniqueKnots(); } + + /// \brief Returns the number of control points in the NURBS Patch on the first axis + int getNumControlPoints_u() const + { + return static_cast(m_controlPoints.shape()[0]); + } + + /// \brief Returns the number of control points in the NURBS Patch on the second axis + int getNumControlPoints_v() const + { + return static_cast(m_controlPoints.shape()[1]); + } + + /// \brief Return the length of the knot vector on the first axis + int getNumKnots_u() const { return m_knotvec_u.getNumKnots(); } + + /// \brief Return the length of the knot vector on the second axis + int getNumKnots_v() const { return m_knotvec_v.getNumKnots(); } + + /// Make trivially rational. If already rational, do nothing + void makeRational() + { + if(!isRational()) + { + auto patch_shape = m_controlPoints.shape(); + m_weights.resize(patch_shape[0], patch_shape[1]); + m_weights.fill(1.0); + } + } + + /// Make nonrational by shrinking array of weights + void makeNonrational() { m_weights.clear(); } + + /// Use array size as flag for rationality + bool isRational() const { return !m_weights.empty(); } + + /// Clears the list of control points, make nonrational + void clear() + { + m_controlPoints.clear(); + m_knotvec_u.clear(); + m_knotvec_v.clear(); + makeNonrational(); + } + + /// Retrieves the control point at index \a (idx_p, idx_q) + PointType& operator()(int ui, int vi) { return m_controlPoints(ui, vi); } + + /// Retrieves the vector of control points at index \a idx + const PointType& operator()(int ui, int vi) const + { + return m_controlPoints(ui, vi); + } + + /*! + * \brief Get a specific weight + * + * \param [in] ui The index of the weight on the first axis + * \param [in] vi The index of the weight on the second axis + * \pre Requires that the surface be rational + */ + const T& getWeight(int ui, int vi) const + { + SLIC_ASSERT(isRational()); + return m_weights(ui, vi); + } + + /*! + * \brief Set the weight at a specific index + * + * \param [in] ui The index of the weight in on the first axis + * \param [in] vi The index of the weight in on the second axis + * \param [in] weight The updated value of the weight + * \pre Requires that the surface be rational + * \pre Requires that the weight be positive + */ + void setWeight(int ui, int vi, T weight) + { + SLIC_ASSERT(isRational()); + SLIC_ASSERT(weight > 0); + + m_weights(ui, vi) = weight; + } + + /*! + * \brief Equality operator for NURBS patches + * + * \param [in] lhs The left-hand side NURBS patch + * \param [in] rhs The right-hand side NURBS patch + * + * \return True if the two patches are equal, false otherwise + */ + friend inline bool operator==(const NURBSPatch& lhs, + const NURBSPatch& rhs) + { + return (lhs.m_controlPoints == rhs.m_controlPoints) && + (lhs.m_weights == rhs.m_weights) && (lhs.m_knotvec_u == rhs.m_knotvec_u) && + (lhs.m_knotvec_v == rhs.m_knotvec_v); + } + + /*! + * \brief Inequality operator for NURBS patches + * + * \param [in] lhs The left-hand side NURBS patch + * \param [in] rhs The right-hand side NURBS patch + * + * \return True if the two patches are not equal, false otherwise + */ + friend inline bool operator!=(const NURBSPatch& lhs, + const NURBSPatch& rhs) + { + return !(lhs == rhs); + } + + /// Returns a copy of the NURBS patch's control points + CoordsMat getControlPoints() const { return m_controlPoints; } + + /// Returns a copy of the NURBS patch's weights + WeightsMat getWeights() const { return m_weights; } + + /*! + * \brief Reverses the order of one direction of the NURBS patch's control points and weights + * + * \param [in] axis orientation of patch. 0 to reverse in u, 1 for reverse in v + */ + void reverseOrientation(int axis) + { + if(axis == 0) + { + reverseOrientation_u(); + } + else + { + reverseOrientation_v(); + } + } + + /// \brief Reverses the order of the control points, weights, and knots on the first axis + void reverseOrientation_u() + { + auto patch_shape = m_controlPoints.shape(); + const int npts_u_mid = (patch_shape[0] + 1) / 2; + + for(int q = 0; q < patch_shape[1]; ++q) + { + for(int i = 0; i < npts_u_mid; ++i) + { + axom::utilities::swap(m_controlPoints(i, q), + m_controlPoints(patch_shape[0] - i - 1, q)); + } + + if(isRational()) + { + for(int i = 0; i < npts_u_mid; ++i) + { + axom::utilities::swap(m_weights(i, q), + m_weights(patch_shape[0] - i - 1, q)); + } + } + } + + m_knotvec_u.reverse(); + } + + /// \brief Reverses the order of the control points, weights, and knots on the second axis + void reverseOrientation_v() + { + auto patch_shape = m_controlPoints.shape(); + const int npts_v_mid = (patch_shape[1] + 1) / 2; + + for(int p = 0; p < patch_shape[0]; ++p) + { + for(int i = 0; i < npts_v_mid; ++i) + { + axom::utilities::swap(m_controlPoints(p, i), + m_controlPoints(p, patch_shape[1] - i - 1)); + } + + if(isRational()) + { + for(int i = 0; i < npts_v_mid; ++i) + { + axom::utilities::swap(m_weights(p, i), + m_weights(p, patch_shape[1] - i - 1)); + } + } + } + + m_knotvec_v.reverse(); + } + + /// \brief Swap the axes such that s(u, v) becomes s(v, u) + void swapAxes() + { + auto patch_shape = m_controlPoints.shape(); + + CoordsMat new_controlPoints(patch_shape[1], patch_shape[0]); + + for(int p = 0; p < patch_shape[0]; ++p) + { + for(int q = 0; q < patch_shape[1]; ++q) + { + new_controlPoints(q, p) = m_controlPoints(p, q); + } + } + + m_controlPoints = new_controlPoints; + + if(isRational()) + { + WeightsMat new_weights(patch_shape[1], patch_shape[0]); + + for(int p = 0; p < patch_shape[0]; ++p) + { + for(int q = 0; q < patch_shape[1]; ++q) + { + new_weights(q, p) = m_weights(p, q); + } + } + + m_weights = new_weights; + } + + std::swap(m_knotvec_u, m_knotvec_v); + } + + /// \brief Returns an axis-aligned bounding box containing the patch + BoundingBoxType boundingBox() const + { + return BoundingBoxType(m_controlPoints.data(), + static_cast(m_controlPoints.size())); + } + + /// \brief Returns an oriented bounding box containing the patch + OrientedBoundingBoxType orientedBoundingBox() const + { + return OrientedBoundingBoxType(m_controlPoints.data(), + static_cast(m_controlPoints.size())); + } + + /*! + * \brief Returns a NURBS patch isocurve for a fixed parameter value of \a u or \a v + * + * \param [in] uv parameter value at which to construct the isocurve + * \param [in] axis orientation of curve. 0 for fixed u, 1 for fixed v + * \return c The isocurve C(v) = S(u, v) for fixed u or C(u) = S(u, v) for fixed v + * + * \pre Requires \a uv be in the span of the relevant knot vector + */ + NURBSCurveType isocurve(T uv, int axis) const + { + SLIC_ASSERT((axis == 0) || (axis == 1)); + + if(axis == 0) + { + return isocurve_u(uv); + } + else + { + return isocurve_v(uv); + } + } + + /*! + * \brief Returns an isocurve with a fixed value of u + * + * \param [in] u Parameter value fixed in the isocurve + * \return c The isocurve C(v) = S(u, v) for fixed u + * + * \pre Requires \a u be in the span of the knot vector + */ + NURBSCurveType isocurve_u(T u) const + { + SLIC_ASSERT(u >= m_knotvec_u[0] && + u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + + using axom::utilities::lerp; + + bool isRationalPatch = isRational(); + + auto patch_shape = m_controlPoints.shape(); + const int deg_u = m_knotvec_u.getDegree(); + const int deg_v = m_knotvec_v.getDegree(); + + NURBSCurveType c(patch_shape[1], deg_v); + if(isRationalPatch) + { + c.makeRational(); + } + + // Find the control points by evaluating each column of the patch + const auto span_u = m_knotvec_u.findSpan(u); + const auto N_evals_u = m_knotvec_u.calculateBasisFunctionsBySpan(span_u, u); + for(int q = 0; q < patch_shape[1]; ++q) + { + Point H; + for(int j = 0; j <= deg_u; ++j) + { + const auto offset = span_u - deg_u + j; + const T weight = isRationalPatch ? m_weights(offset, q) : 1.0; + const auto& controlPoint = m_controlPoints(offset, q); + + for(int i = 0; i < NDIMS; ++i) + { + H[i] += N_evals_u[j] * weight * controlPoint[i]; + } + H[NDIMS] += N_evals_u[j] * weight; + } + + for(int i = 0; i < NDIMS; ++i) + { + c[q][i] = H[i] / H[NDIMS]; + } + + if(isRationalPatch) + { + c.setWeight(q, H[NDIMS]); + } + } + + c.setKnots(m_knotvec_v); + + return c; + } + + /*! + * \brief Returns an isocurve with a fixed value of v + * + * \param [in] v Parameter value fixed in the isocurve + * \return c The isocurve C(u) = S(u, v) for fixed v + * + * \pre Requires \a v be in the span of the knot vector + */ + NURBSCurveType isocurve_v(T v) const + { + SLIC_ASSERT(v >= m_knotvec_v[0] && + v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + + using axom::utilities::lerp; + + bool isRationalPatch = isRational(); + + auto patch_shape = m_controlPoints.shape(); + const int deg_u = m_knotvec_u.getDegree(); + const int deg_v = m_knotvec_v.getDegree(); + + NURBSCurveType c(patch_shape[0], deg_u); + if(isRationalPatch) + { + c.makeRational(); + } + + // Find the control points by evaluating each row of the patch + const auto span_v = m_knotvec_v.findSpan(v); + const auto N_evals_v = m_knotvec_v.calculateBasisFunctionsBySpan(span_v, v); + for(int p = 0; p < patch_shape[0]; ++p) + { + Point H; + for(int i = 0; i <= deg_v; ++i) + { + const auto offset = span_v - deg_v + i; + const T weight = isRationalPatch ? m_weights(p, offset) : 1.0; + const auto& controlPoint = m_controlPoints(p, offset); + + for(int j = 0; j < NDIMS; ++j) + { + H[j] += N_evals_v[i] * weight * controlPoint[j]; + } + H[NDIMS] += N_evals_v[i] * weight; + } + + for(int j = 0; j < NDIMS; ++j) + { + c[p][j] = H[j] / H[NDIMS]; + } + + if(isRationalPatch) + { + c.setWeight(p, H[NDIMS]); + } + } + + c.setKnots(m_knotvec_u); + + return c; + } + + /*! + * \brief Evaluate the surface and the first \a d derivatives at parameter \a u, \a v + * + * \param [in] u The parameter value on the first axis + * \param [in] v The parameter value on the second axis + * \param [in] d The number of derivatives to evaluate + * \param [out] ders A matrix of size d+1 x d+1 containing the derivatives + * + * ders[i][j] is the derivative of S with respect to u i times and v j times. + * For consistency, ders[0][0] contains the evaluation point stored as a vector + * + * Implementation adapted from Algorithm A3.6 on p. 111 of "The NURBS Book". + * Rational derivatives from Algorithm A4.4 on p. 137 of "The NURBS Book". + * + * \pre Requires \a u, v be in the span of the knots + */ + void evaluateDerivatives(T u, T v, int d, axom::Array& ders) const + { + SLIC_ASSERT(u >= m_knotvec_u[0] && + u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + SLIC_ASSERT(v >= m_knotvec_v[0] && + v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + + const int deg_u = getDegree_u(); + const int du = std::min(d, deg_u); + + const int deg_v = getDegree_v(); + const int dv = std::min(d, deg_v); + + // Matrix for derivatives + ders.resize(d + 1, d + 1); + ders.fill(VectorType(0.0)); + + // Matrix for derivatives of homogeneous surface + // Store w_{ui, uj} in Awders[i][j][NDIMS] + axom::Array, 2> Awders(d + 1, d + 1); + Awders.fill(Point::zero()); + + const bool isCurveRational = isRational(); + + // Find the span of the knot vectors and basis function derivatives + const auto span_u = m_knotvec_u.findSpan(u); + const auto N_evals_u = + m_knotvec_u.derivativeBasisFunctionsBySpan(span_u, u, du); + + const auto span_v = m_knotvec_v.findSpan(v); + const auto N_evals_v = + m_knotvec_v.derivativeBasisFunctionsBySpan(span_v, v, dv); + + for(int k = 0; k <= du; ++k) + { + axom::Array> temp(deg_v + 1); + + for(int s = 0; s <= deg_v; ++s) + { + temp[s] = Point::zero(); + for(int r = 0; r <= deg_u; ++r) + { + auto the_weight = isCurveRational + ? m_weights(span_u - deg_u + r, span_v - deg_v + s) + : 1.0; + auto& the_pt = m_controlPoints(span_u - deg_u + r, span_v - deg_v + s); + + for(int n = 0; n < NDIMS; ++n) + { + temp[s][n] += N_evals_u[k][r] * the_weight * the_pt[n]; + } + temp[s][NDIMS] += N_evals_u[k][r] * the_weight; + } + } + + int dd = std::min(d - k, dv); + for(int l = 0; l <= dd; ++l) + { + for(int s = 0; s <= deg_v; ++s) + { + for(int n = 0; n < NDIMS + 1; ++n) + { + Awders[k][l][n] += N_evals_v[l][s] * temp[s][n]; + } + } + } + } + + // Compute the derivatives of the homogeneous surface + for(int k = 0; k <= d; ++k) + { + for(int l = 0; l <= d - k; ++l) + { + auto v = Awders[k][l]; + + for(int j = 0; j <= l; ++j) + { + auto bin = axom::utilities::binomialCoefficient(l, j); + for(int n = 0; n < NDIMS; ++n) + { + v[n] -= bin * Awders[0][j][NDIMS] * ders[k][l - j][n]; + } + } + + for(int i = 1; i <= k; ++i) + { + auto bin = axom::utilities::binomialCoefficient(k, i); + for(int n = 0; n < NDIMS; ++n) + { + v[n] -= bin * Awders[i][0][NDIMS] * ders[k - i][l][n]; + } + + auto v2 = Point::zero(); + for(int j = 1; j <= l; ++j) + { + auto bin = axom::utilities::binomialCoefficient(l, j); + for(int n = 0; n < NDIMS; ++n) + { + v2[n] += bin * Awders[i][j][NDIMS] * ders[k - i][l - j][n]; + } + } + + for(int n = 0; n < NDIMS; ++n) + { + v[n] -= bin * v2[n]; + } + } + + for(int n = 0; n < NDIMS; ++n) + { + ders[k][l][n] = v[n] / Awders[0][0][NDIMS]; + } + } + } + } + + /*! + * \brief Evaluates all first derivatives of the NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * \param [out] eval The point value of the NURBS patch at (u, v) + * \param [out] Du The vector value of S_u(u, v) + * \param [out] Dv The vector value of S_v(u, v) + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + void evaluateFirstDerivatives(T u, + T v, + PointType& eval, + VectorType& Du, + VectorType& Dv) const + { + axom::Array ders; + evaluateDerivatives(u, v, 1, ders); + + eval = PointType(ders[0][0]); + Du = ders[1][0]; + Dv = ders[0][1]; + } + + /*! + * \brief Evaluates all linear derivatives of the NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * \param [out] eval The point value of the NURBS patch at (u, v) + * \param [out] Du The vector value of S_u(u, v) + * \param [out] Dv The vector value of S_v(u, v) + * \param [out] DuDv The vector value of S_uv(u, v) == S_vu(u, v) + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + void evaluateLinearDerivatives(T u, + T v, + PointType& eval, + VectorType& Du, + VectorType& Dv, + VectorType& DuDv) const + { + axom::Array ders; + evaluateDerivatives(u, v, 1, ders); + + eval = PointType(ders[0][0]); + Du = ders[1][0]; + Dv = ders[0][1]; + DuDv = ders[1][1]; + } + + /*! + * \brief Evaluates all second derivatives of the NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * \param [out] eval The point value of the NURBS patch at (u, v) + * \param [out] Du The vector value of S_u(u, v) + * \param [out] Dv The vector value of S_v(u, v) + * \param [out] DuDu The vector value of S_uu(u, v) + * \param [out] DvDv The vector value of S_vv(u, v) + * \param [out] DuDv The vector value of S_uu(u, v) + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + void evaluateSecondDerivatives(T u, + T v, + PointType& eval, + VectorType& Du, + VectorType& Dv, + VectorType& DuDu, + VectorType& DvDv, + VectorType& DuDv) const + { + axom::Array ders; + evaluateDerivatives(u, v, 2, ders); + + eval = PointType(ders[0][0]); + Du = ders[1][0]; + Dv = ders[0][1]; + DuDu = ders[2][0]; + DvDv = ders[0][2]; + DuDv = ders[1][1]; + } + + /*! + * \brief Computes a tangent in u of the NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + VectorType du(T u, T v) const + { + axom::Array ders; + evaluateDerivatives(u, v, 1, ders); + + return ders[1][0]; + } + + /*! + * \brief Computes a tangent in v of the NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + VectorType dv(T u, T v) const + { + axom::Array ders; + evaluateDerivatives(u, v, 1, ders); + + return ders[0][1]; + } + + /*! + * \brief Computes the second derivative in u of a NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + VectorType dudu(T u, T v) const + { + axom::Array ders; + evaluateDerivatives(u, v, 2, ders); + + return ders[2][0]; + } + + /*! + * \brief Computes the second derivative in v of a NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + VectorType dvdv(T u, T v) const + { + axom::Array ders; + evaluateDerivatives(u, v, 2, ders); + + return ders[0][2]; + } + + /*! + * \brief Computes the mixed second derivative in u and v of a NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + VectorType dudv(T u, T v) const + { + axom::Array ders; + evaluateDerivatives(u, v, 2, ders); + + return ders[1][1]; + } + + /*! + * \brief Computes the mixed second derivative in u and v of a NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + VectorType dvdu(T u, T v) const { return dudv(u, v); } + + /*! + * \brief Computes the normal vector to the NURBS patch at (\a u, \a v) + * + * \param [in] u Parameter value at which to evaluate on the first axis + * \param [in] v Parameter value at which to evaluate on the second axis + * + * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + */ + VectorType normal(T u, T v) const + { + PointType eval; + VectorType Du, Dv; + evaluateFirstDerivatives(u, v, eval, Du, Dv); + + return VectorType::cross_product(Du, Dv); + } + + /*! + * \brief Insert a knot to the u knot vector to have the given multiplicity + * + * \param [in] u The parameter value of the knot to insert + * \param [in] target_multiplicity The multiplicity of the knot to insert + * \return The index of the new knot + * + * Algorithm A5.3 on p. 155 of "The NURBS Book" + * + * \note If the knot is already present, it will be inserted + * up to the given multiplicity, or the maximum permitted by the degree + * + * \pre Requires \a u in the span of the knots + * + * \return The (maximum) index of the new knot + */ + axom::IndexType insertKnot_u(T u, int target_multiplicity = 1) + { + SLIC_ASSERT(u >= m_knotvec_u[0] && + u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + SLIC_ASSERT(target_multiplicity > 0); + + const bool isRationalPatch = isRational(); + + const int np = getNumControlPoints_u() - 1; + const int p = getDegree_u(); + + const int nq = getNumControlPoints_v() - 1; + + // Find the span and initial multiplicity of the knot + int s = 0; + const auto k = m_knotvec_u.findSpan(u, s); + + // Find how many knots we need to insert + int r = std::min(target_multiplicity - s, p - s); + if(r <= 0) + { + return k; + } + + // Temp variable + axom::IndexType L; + + // Compute the alphas, which depend only on the knot vector + axom::Array alpha(p - s, r + 1); + for(int j = 1; j <= r; ++j) + { + L = k - p + j; + for(int i = 0; i <= p - j - s; ++i) + { + alpha[i][j] = (u - m_knotvec_u[L + i]) / + (m_knotvec_u[i + k + 1] - m_knotvec_u[L + i]); + } + } + + // Store the new control points and weights + CoordsMat newControlPoints(np + 1 + r, nq + 1); + WeightsMat newWeights(0, 0); + if(isRationalPatch) + { + newWeights.resize(np + 1 + r, nq + 1); + } + + // Store a temporary array of points and weights + CoordsVec tempControlPoints(p + 1); + WeightsVec tempWeights(isRationalPatch ? p + 1 : 0); + + // Insert the knot for each row + for(int row = 0; row <= nq; ++row) + { + // Save unaltered control points + for(int i = 0; i <= k - p; ++i) + { + newControlPoints(i, row) = m_controlPoints(i, row); + if(isRationalPatch) + { + newWeights(i, row) = m_weights(i, row); + } + } + + for(int i = k - s; i <= np; ++i) + { + newControlPoints(i + r, row) = m_controlPoints(i, row); + if(isRationalPatch) + { + newWeights(i + r, row) = m_weights(i, row); + } + } + + // Load auxiliary control points + for(int i = 0; i <= p - s; ++i) + { + for(int n = 0; n < NDIMS; ++n) + { + tempControlPoints[i][n] = m_controlPoints(k - p + i, row)[n] * + (isRationalPatch ? m_weights(k - p + i, row) : 1.0); + } + + if(isRationalPatch) + { + tempWeights[i] = m_weights(k - p + i, row); + } + } + + // Insert the knot r times + for(int j = 1; j <= r; ++j) + { + L = k - p + j; + for(int i = 0; i <= p - j - s; ++i) + { + tempControlPoints[i].array() = + alpha(i, j) * tempControlPoints[i + 1].array() + + (1.0 - alpha(i, j)) * tempControlPoints[i].array(); + + if(isRationalPatch) + { + tempWeights[i] = alpha(i, j) * tempWeights[i + 1] + + (1.0 - alpha(i, j)) * tempWeights[i]; + } + } + + for(int n = 0; n < NDIMS; ++n) + { + newControlPoints(L, row)[n] = + tempControlPoints[0][n] / (isRationalPatch ? tempWeights[0] : 1.0); + newControlPoints(k + r - j - s, row)[n] = + tempControlPoints[p - j - s][n] / + (isRationalPatch ? tempWeights[p - j - s] : 1.0); + } + + if(isRationalPatch) + { + newWeights(L, row) = tempWeights[0]; + newWeights(k + r - j - s, row) = tempWeights[p - j - s]; + } + } + + // Load the remaining control points + for(int i = L + 1; i < k - s; ++i) + { + for(int n = 0; n < NDIMS; ++n) + { + newControlPoints(i, row)[n] = tempControlPoints[i - L][n] / + (isRationalPatch ? tempWeights[i - L] : 1.0); + } + + if(isRationalPatch) + { + newWeights(i, row) = tempWeights[i - L]; + } + } + } + + // Update the knot vector and control points + m_knotvec_u.insertKnotBySpan(k, u, r); + m_controlPoints = newControlPoints; + m_weights = newWeights; + + return k + r; + } + + /*! + * \brief Insert a knot to the v knot vector to have the given multiplicity + * + * \param [in] v The parameter value of the knot to insert + * \param [in] target_multiplicity The multiplicity of the knot to insert + * \return The index of the new knot + * + * Algorithm A5.3 on p. 155 of "The NURBS Book" + * + * \note If the knot is already present, it will be inserted + * up to the given multiplicity, or the maximum permitted by the degree + * + * \pre Requires \a v in the span of the knots + * + * \return The (maximum) index of the new knot + */ + axom::IndexType insertKnot_v(T v, int target_multiplicity = 1) + { + SLIC_ASSERT(v >= m_knotvec_v[0] && + v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + SLIC_ASSERT(target_multiplicity > 0); + + const bool isRationalPatch = isRational(); + + const int np = getNumControlPoints_u() - 1; + const int p = getDegree_u(); + + const int nq = getNumControlPoints_v() - 1; + const int q = getDegree_v(); + + // Find the span and initial multiplicity of the knot + int s = 0; + const auto k = m_knotvec_v.findSpan(v, s); + + // Find how many knots we need to insert + int r = std::min(target_multiplicity - s, q - s); + if(r <= 0) + { + return k; + } + + // Temp variable + axom::IndexType L; + + // Compute the alphas, which depend only on the knot vector + axom::Array alpha(p - s, r + 1); + for(int j = 1; j <= r; ++j) + { + L = k - q + j; + for(int i = 0; i <= q - j - s; ++i) + { + alpha[i][j] = (v - m_knotvec_v[L + i]) / + (m_knotvec_v[i + k + 1] - m_knotvec_v[L + i]); + } + } + + // Store the new control points and weights + CoordsMat newControlPoints(np + 1, nq + 1 + r); + WeightsMat newWeights(0, 0); + if(isRationalPatch) + { + newWeights.resize(np + 1, nq + 1 + r); + } + + // Store a temporary array of points and weights + CoordsVec tempControlPoints(q + 1); + WeightsVec tempWeights(isRationalPatch ? q + 1 : 0); + + // Insert the knot for each row + for(int col = 0; col <= np; ++col) + { + // Save unaltered control points + for(int i = 0; i <= k - q; ++i) + { + newControlPoints(col, i) = m_controlPoints(col, i); + if(isRationalPatch) + { + newWeights(col, i) = m_weights(col, i); + } + } + + for(int i = k - s; i <= nq; ++i) + { + newControlPoints(col, i + r) = m_controlPoints(col, i); + if(isRationalPatch) + { + newWeights(col, i + r) = m_weights(col, i); + } + } + + // Load auxiliary control points + for(int i = 0; i <= q - s; ++i) + { + for(int n = 0; n < NDIMS; ++n) + { + tempControlPoints[i][n] = m_controlPoints(col, k - q + i)[n] * + (isRationalPatch ? m_weights(col, k - q + i) : 1.0); + } + + if(isRationalPatch) + { + tempWeights[i] = m_weights(col, k - q + i); + } + } + + // Insert the knot r times + for(int j = 1; j <= r; ++j) + { + L = k - q + j; + for(int i = 0; i <= q - j - s; ++i) + { + tempControlPoints[i].array() = + alpha(i, j) * tempControlPoints[i + 1].array() + + (1.0 - alpha(i, j)) * tempControlPoints[i].array(); + + if(isRationalPatch) + { + tempWeights[i] = alpha(i, j) * tempWeights[i + 1] + + (1.0 - alpha(i, j)) * tempWeights[i]; + } + } + + for(int n = 0; n < NDIMS; ++n) + { + newControlPoints(col, L)[n] = + tempControlPoints[0][n] / (isRationalPatch ? tempWeights[0] : 1.0); + newControlPoints(col, k + r - j - s)[n] = + tempControlPoints[q - j - s][n] / + (isRationalPatch ? tempWeights[q - j - s] : 1.0); + } + + if(isRationalPatch) + { + newWeights(col, L) = tempWeights[0]; + newWeights(col, k + r - j - s) = tempWeights[q - j - s]; + } + } + + // Load the remaining control points + for(int i = L + 1; i < k - s; ++i) + { + for(int n = 0; n < NDIMS; ++n) + { + newControlPoints(col, i)[n] = tempControlPoints[i - L][n] / + (isRationalPatch ? tempWeights[i - L] : 1.0); + } + + if(isRationalPatch) + { + newWeights(col, i) = tempWeights[i - L]; + } + } + } + + // Update the knot vector and control points + m_knotvec_v.insertKnotBySpan(k, v, r); + m_controlPoints = newControlPoints; + m_weights = newWeights; + + return k + r; + } + + /*! + * \brief Splits a NURBS patch into four NURBS patches + * + * \param [in] u parameter value at which to bisect on the first axis + * \param [in] v parameter value at which to bisect on the second axis + * \param [out] p1 First output NURBS patch + * \param [out] p2 Second output NURBS patch + * \param [out] p3 Third output NURBS patch + * \param [out] p4 Fourth output NURBS patch + * + * v = 1 + * ---------------------- + * | | | + * | p3 | p4 | + * | | | + * --------(u,v)--------- + * | | | + * | p1 | p2 | + * | | | + * ---------------------- u = 1 + * + * \pre Parameter \a u and \a v must be in the knot span + */ + void split(T u, + T v, + NURBSPatch& p1, + NURBSPatch& p2, + NURBSPatch& p3, + NURBSPatch& p4) const + { + SLIC_ASSERT(u > m_knotvec_u[0] && + u < m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + SLIC_ASSERT(v > m_knotvec_v[0] && + v < m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + + // Bisect the patch along the u direction + split_u(u, p1, p2); + + // Temporarily store the result in each half and split again + NURBSPatch p0(p1); + p0.split_v(v, p1, p2); + + p0 = p2; + p0.split_v(v, p3, p4); + } + + /*! + * \brief Split the NURBS patch in two along the u direction + */ + void split_u(T u, NURBSPatch& p1, NURBSPatch& p2, bool normalize = false) const + { + SLIC_ASSERT(u > m_knotvec_u[0] && + u < m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + + const bool isRationalPatch = isRational(); + + const int p = getDegree_u(); + const int nq = getNumControlPoints_v() - 1; + + p1 = *this; + + // Will make the multiplicity of the knot at u equal to p + const auto k = p1.insertKnot_u(u, p); + auto nkts1 = p1.getNumKnots_u(); + auto npts1 = p1.getNumControlPoints_u(); + + // Split the knot vector, add to the returned curves + KnotVectorType k1, k2; + p1.getKnots_u().splitBySpan(k, k1, k2); + + p1.m_knotvec_u = k1; + p1.m_knotvec_v = m_knotvec_v; + + p2.m_knotvec_u = k2; + p2.m_knotvec_v = m_knotvec_v; + + // Copy the control points + p2.m_controlPoints.resize(nkts1 - k - 1, nq + 1); + if(isRationalPatch) + { + p2.m_weights.resize(nkts1 - k - 1, nq + 1); + } + else + { + p2.m_weights.resize(0, 0); + } + + for(int i = 0; i < p2.m_controlPoints.shape()[0]; ++i) + { + for(int j = 0; j < p2.m_controlPoints.shape()[1]; ++j) + { + p2.m_controlPoints(nkts1 - k - 2 - i, j) = p1(npts1 - 1 - i, j); + if(isRationalPatch) + { + p2.m_weights(nkts1 - k - 2 - i, j) = p1.getWeight(npts1 - 1 - i, j); + } + } + } + + // Assumes that the resizing is done on the *flattened* array + p1.m_controlPoints.resize(k - p + 1, nq + 1); + if(isRationalPatch) + { + p1.m_weights.resize(k - p + 1, nq + 1); + } + else + { + p1.m_weights.resize(0, 0); + } + + if(normalize) + { + p1.normalize(); + p2.normalize(); + } + } + + /*! + * \brief Split the NURBS patch in two along the v direction + */ + void split_v(T v, NURBSPatch& p1, NURBSPatch& p2, bool normalize = false) const + { + SLIC_ASSERT(v > m_knotvec_v[0] && + v < m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + + const bool isRationalPatch = isRational(); + + const int np = getNumControlPoints_u() - 1; + const int q = getDegree_v(); + + p1 = *this; + + // Will make the multiplicity of the knot at v equal to q + const auto k = p1.insertKnot_v(v, q); + auto nkts1 = p1.getNumKnots_v(); + auto npts1 = p1.getNumControlPoints_v(); + + // Split the knot vector, add to the returned curves + KnotVectorType k1, k2; + p1.getKnots_v().splitBySpan(k, k1, k2); + + p1.m_knotvec_u = m_knotvec_u; + p1.m_knotvec_v = k1; + + p2.m_knotvec_u = m_knotvec_u; + p2.m_knotvec_v = k2; + + // Copy the control points + p2.m_controlPoints.resize(np + 1, nkts1 - k - 1); + if(isRationalPatch) + { + p2.m_weights.resize(np + 1, nkts1 - k - 1); + } + else + { + p2.m_weights.resize(0, 0); + } + + for(int i = 0; i < p2.m_controlPoints.shape()[0]; ++i) + { + for(int j = 0; j < p2.m_controlPoints.shape()[1]; ++j) + { + p2.m_controlPoints(i, nkts1 - k - 2 - j) = p1(i, npts1 - 1 - j); + if(isRationalPatch) + { + p2.m_weights(i, nkts1 - k - 2 - j) = p1.getWeight(i, npts1 - 1 - j); + } + } + } + + // Rearrange the control points and weights by their flat index + // so that the `resize` method takes the correct submatrix + for(int i = 0; i < np + 1; ++i) + { + for(int j = 0; j < k - q + 1; ++j) + { + p1.m_controlPoints.flatIndex(j + i * (k - q + 1)) = + p1.m_controlPoints(i, j); + if(isRationalPatch) + { + p1.m_weights.flatIndex(j + i * (k - q + 1)) = p1.m_weights(i, j); + } + } + } + + // Resize the 2D arrays + p1.m_controlPoints.resize(np + 1, k - q + 1); + if(isRationalPatch) + { + p1.m_weights.resize(np + 1, k - q + 1); + } + else + { + p1.m_weights.resize(0, 0); + } + + if(normalize) + { + p1.normalize(); + p2.normalize(); + } + } + + /*! + * \brief Splits a NURBS surface (at each internal knot) into several Bezier patches + * + * If either degree_u or degree_v is zero, the resulting Bezier patches along + * that axis will be disconnected and order 0 + * + * Algorithm A5.7 on p. 177 of "The NURBS Book" + * + * \return An array of Bezier patches ordered lexicographically (in v, then u) + */ + axom::Array> extractBezier() const + { + const bool isRationalPatch = isRational(); + + const auto n = getNumControlPoints_u() - 1; + const auto p = getDegree_u(); + const auto kp = m_knotvec_u.getNumKnotSpans(); + + const auto m = getNumControlPoints_v() - 1; + const auto q = getDegree_v(); + const auto kq = m_knotvec_v.getNumKnotSpans(); + + axom::Array> strips(kp); + for(int i = 0; i < strips.size(); ++i) + { + strips[i].setParameters(p + 1, m + 1, p, q); + if(isRationalPatch) + { + strips[i].makeRational(); + } + } + + axom::Array alphas(std::max(0, std::max(p - 1, q - 1))); + + // Do Bezier extraction on the u-axis, which returns a collection of Bezier strips + if(p == 0) + { + for(int i = 0; i < n + 1; ++i) + { + for(int row = 0; row < m + 1; ++row) + { + strips[i](0, row) = m_controlPoints(i, row); + if(isRationalPatch) + { + strips[i].setWeight(0, row, m_weights(i, row)); + } + } + } + } + else + { + int a = p; + int b = p + 1; + int ns = 0; + + for(int i = 0; i <= p; ++i) + { + for(int row = 0; row <= m; ++row) + { + strips[ns](i, row) = m_controlPoints(i, row); + if(isRationalPatch) + { + strips[ns].setWeight(i, row, m_weights(i, row)); + } + } + } + + while(b < n + p + 1) + { + // Get multiplicity of the knot + int i = b; + while(b < n + p + 1 && m_knotvec_u[b] == m_knotvec_u[b + 1]) + { + ++b; + } + int mult = b - i + 1; + + if(mult < p) + { + // Get the numerator and the alphas + T numer = m_knotvec_u[b] - m_knotvec_u[a]; + + for(int j = p; j > mult; --j) + { + alphas[j - mult - 1] = numer / (m_knotvec_u[a + j] - m_knotvec_u[a]); + } + + // Do the knot insertion in place + for(int j = 1; j <= p - mult; ++j) + { + int save = p - mult - j; + int s = mult + j; + + for(int k = p; k >= s; --k) + { + T alpha = alphas[k - s]; + for(int row = 0; row <= m; ++row) + { + T weight_k = isRationalPatch ? strips[ns].getWeight(k, row) : 1.0; + T weight_km1 = + isRationalPatch ? strips[ns].getWeight(k - 1, row) : 1.0; + + if(isRationalPatch) + { + strips[ns].setWeight( + k, + row, + alpha * weight_k + (1.0 - alpha) * weight_km1); + } + + for(int N = 0; N < NDIMS; ++N) + { + strips[ns](k, row)[N] = + (alpha * strips[ns](k, row)[N] * weight_k + + (1.0 - alpha) * strips[ns](k - 1, row)[N] * weight_km1) / + (isRationalPatch ? strips[ns].getWeight(k, row) : 1.0); + } + } + } + + if(b < n + p + 1) + { + for(int row = 0; row <= m; ++row) + { + strips[ns + 1](save, row) = strips[ns](p, row); + if(isRationalPatch) + { + strips[ns + 1].setWeight(save, + row, + strips[ns].getWeight(p, row)); + } + } + } + } + } + + ++ns; + + if(b < n + p + 1) + { + for(int j = p - mult; j <= p; ++j) + { + for(int row = 0; row <= m; ++row) + { + strips[ns](j, row) = m_controlPoints(b - p + j, row); + if(isRationalPatch) + { + strips[ns].setWeight(j, row, m_weights(b - p + j, row)); + } + } + } + a = b; + b++; + } + } + } + + // For each strip, do Bezier extraction on the v-axis + axom::Array> beziers(kp * kq); + for(int i = 0; i < beziers.size(); ++i) + { + beziers[i].setOrder(p, q); + if(isRationalPatch) + { + beziers[i].makeRational(); + } + } + + for(int s_i = 0; s_i < strips.size(); ++s_i) + { + auto& strip = strips[s_i]; + int n_i = strip.getNumControlPoints_u() - 1; + int nb = s_i * m_knotvec_v.getNumKnotSpans(); + + // Handle this case separately + if(q == 0) + { + for(int i = 0; i < m + 1; ++i) + { + for(int col = 0; col < n_i + 1; ++col) + { + beziers[nb](col, 0) = strip(col, i); + if(isRationalPatch) + { + beziers[nb].setWeight(col, 0, strip.getWeight(col, i)); + } + } + + ++nb; + } + + continue; + } + + int a = q; + int b = q + 1; + + for(int i = 0; i <= q; ++i) + { + for(int col = 0; col <= n_i; ++col) + { + beziers[nb](col, i) = strip(col, i); + if(isRationalPatch) + { + beziers[nb].setWeight(col, i, strip.getWeight(col, i)); + } + } + } + + while(b < m + q + 1) + { + // Get multiplicity of the knot + int i = b; + while(b < m + q + 1 && m_knotvec_v[b] == m_knotvec_v[b + 1]) + { + ++b; + } + int mult = b - i + 1; + + if(mult < q) + { + // Get the numerator and the alphas + T numer = m_knotvec_v[b] - m_knotvec_v[a]; + + for(int j = q; j > mult; --j) + { + alphas[j - mult - 1] = numer / (m_knotvec_v[a + j] - m_knotvec_v[a]); + } + + // Do the knot insertion in place + for(int j = 1; j <= q - mult; ++j) + { + int save = q - mult - j; + int s = mult + j; + + for(int k = q; k >= s; --k) + { + T alpha = alphas[k - s]; + for(int col = 0; col <= n_i; ++col) + { + T weight_k = + isRationalPatch ? beziers[nb].getWeight(col, k) : 1.0; + T weight_km1 = + isRationalPatch ? beziers[nb].getWeight(col, k - 1) : 1.0; + + if(isRationalPatch) + { + beziers[nb].setWeight( + col, + k, + alpha * weight_k + (1.0 - alpha) * weight_km1); + } + + for(int N = 0; N < NDIMS; ++N) + { + beziers[nb](col, k)[N] = + (alpha * beziers[nb](col, k)[N] * weight_k + + (1.0 - alpha) * beziers[nb](col, k - 1)[N] * weight_km1) / + (isRationalPatch ? beziers[nb].getWeight(col, k) : 1.0); + } + } + } + + if(b < m + q + 1) + { + for(int col = 0; col <= n_i; ++col) + { + beziers[nb + 1](col, save) = beziers[nb](col, q); + if(isRationalPatch) + { + beziers[nb + 1].setWeight(col, + save, + beziers[nb].getWeight(col, q)); + } + } + } + } + } + + ++nb; + + if(b < m + q + 1) + { + for(int j = q - mult; j <= q; ++j) + { + for(int col = 0; col <= n_i; ++col) + { + beziers[nb](col, j) = strip(col, b - q + j); + if(isRationalPatch) + { + beziers[nb].setWeight(col, j, strip.getWeight(col, b - q + j)); + } + } + } + a = b; + b++; + } + } + } + + return beziers; + } + + /// \brief Normalize the knot vectors to the span [0, 1] + void normalize() + { + m_knotvec_u.normalize(); + m_knotvec_v.normalize(); + } + + /// \brief Normalize the knot vector in u to the span [0, 1] + void normalize_u() { m_knotvec_u.normalize(); } + + /// \brief Normalize the knot vector in v to the span [0, 1] + void normalize_v() { m_knotvec_v.normalize(); } + + /*! + * \brief Rescale both knot vectors to the span of [a, b] + * + * \param [in] a The lower bound of the new knot vector + * \param [in] b The upper bound of the new knot vector + * + * \pre Requires a < b + */ + void rescale(T a, T b) + { + SLIC_ASSERT(a < b); + m_knotvec_u.rescale(a, b); + m_knotvec_v.rescale(a, b); + } + + /*! + * \brief Rescale the knot vector in u to the span of [a, b] + * + * \param [in] a The lower bound of the new knot vector + * \param [in] b The upper bound of the new knot vector + * + * \pre Requires a < b + */ + void rescale_u(T a, T b) + { + SLIC_ASSERT(a < b); + m_knotvec_u.rescale(a, b); + } + + /*! + * \brief Rescale the knot vector in v to the span of [a, b] + * + * \param [in] a The lower bound of the new knot vector + * \param [in] b The upper bound of the new knot vector + * + * \pre Requires a < b + */ + void rescale_v(T a, T b) + { + SLIC_ASSERT(a < b); + m_knotvec_v.rescale(a, b); + } + + /*! + * \brief Simple formatted print of a NURBS Patch instance + * + * \param os The output stream to write to + * \return A reference to the modified ostream + */ + std::ostream& print(std::ostream& os) const + { + auto patch_shape = m_controlPoints.shape(); + + int deg_u = m_knotvec_u.getDegree(); + int deg_v = m_knotvec_v.getDegree(); + + int nkts_u = m_knotvec_u.getNumKnots(); + int nkts_v = m_knotvec_v.getNumKnots(); + + os << "{ degree (" << deg_u << ", " << deg_v << ") NURBS Patch, "; + os << "control points ["; + for(int p = 0; p < patch_shape[0]; ++p) + { + for(int q = 0; q < patch_shape[1]; ++q) + { + os << m_controlPoints(p, q) + << ((p < patch_shape[0] - 1 || q < patch_shape[1] - 1) ? "," : "]"); + } + } + + if(isRational()) + { + os << ", weights ["; + for(int p = 0; p < patch_shape[0]; ++p) + { + for(int q = 0; q < patch_shape[1]; ++q) + { + os << m_weights(p, q) + << ((p < patch_shape[0] - 1 || q < patch_shape[1] - 1) ? "," : "]"); + } + } + } + + os << ", knot vector u ["; + for(int i = 0; i < nkts_u; ++i) + { + os << m_knotvec_u[i] << ((i < nkts_u - 1) ? "," : "]"); + } + + os << ", knot vector v ["; + for(int i = 0; i < nkts_v; ++i) + { + os << m_knotvec_v[i] << ((i < nkts_v - 1) ? "," : "]"); + } + + return os; + } + + /// \brief Function to check if the NURBS surface is valid + bool isValidNURBS() const + { + // Check monotonicity, open-ness, continuity of each knot vector + if(!m_knotvec_u.isValid() || !m_knotvec_v.isValid()) + { + return false; + } + + // Number of knots must match the number of control points + int deg_u = m_knotvec_u.getDegree(); + int deg_v = m_knotvec_v.getDegree(); + + // Number of knots must match the number of control points + auto patch_shape = m_controlPoints.shape(); + if(m_knotvec_u.getNumKnots() != patch_shape[0] + deg_u + 1 || + m_knotvec_v.getNumKnots() != patch_shape[1] + deg_v + 1) + { + return false; + } + + if(isRational()) + { + // Number of control points must match number of weights + auto weights_shape = m_weights.shape(); + if(weights_shape[0] != patch_shape[0] || weights_shape[1] != patch_shape[1]) + { + return false; + } + + // Weights must be positive + for(int i = 0; i < weights_shape[0]; ++i) + { + for(int j = 0; j < weights_shape[1]; ++j) + { + if(m_weights(i, j) <= 0.0) + { + return false; + } + } + } + } + + return true; + } + +private: + CoordsMat m_controlPoints; + WeightsMat m_weights; + KnotVectorType m_knotvec_u, m_knotvec_v; +}; + +//------------------------------------------------------------------------------ +/// Free functions related to NURBSPatch +//------------------------------------------------------------------------------ +template +std::ostream& operator<<(std::ostream& os, const NURBSPatch& nPatch) +{ + nPatch.print(os); + return os; +} + +} // namespace primal +} // namespace axom + +#endif // AXOM_PRIMAL_NURBSPATCH_HPP_ diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index dffd021092..5c527e3b93 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -30,6 +30,8 @@ #include "axom/primal/geometry/Triangle.hpp" #include "axom/primal/geometry/BezierCurve.hpp" #include "axom/primal/geometry/BezierPatch.hpp" +#include "axom/primal/geometry/NURBSCurve.hpp" +#include "axom/primal/geometry/NURBSPatch.hpp" #include "axom/primal/operators/detail/intersect_impl.hpp" #include "axom/primal/operators/detail/intersect_ray_impl.hpp" @@ -591,7 +593,15 @@ bool intersect(const Ray& r, // for efficiency, linearity check actually uses a squared tolerance const double sq_tol = tol * tol; - return detail::intersect_ray_bezier(r, c, rp, cp, sq_tol, EPS, c.getOrder(), offset, scale); + return detail::intersect_ray_bezier(r, + c, + rp, + cp, + sq_tol, + EPS, + c.getOrder(), + offset, + scale); } /*! @@ -617,9 +627,6 @@ bool intersect(const Ray& r, double tol = 1E-8, double EPS = 1E-8) { - const double offset = 0.; - const double scale = 1.; - // Check a bounding box of the entire NURBS first Point ip; if(!intersect(r, n.boundingBox(), ip)) @@ -631,19 +638,19 @@ bool intersect(const Ray& r, auto beziers = n.extractBezier(); const int deg = n.getDegree(); axom::Array knot_vals = n.getUniqueKnots(); - + // Check each Bezier segment, and scale the intersection parameters // back into the span of the original NURBS curve for(int i = 0; i < beziers.size(); ++i) { axom::Array rc, nc; - intersect( r, beziers[i], rc, nc, tol, EPS ); + intersect(r, beziers[i], rc, nc, tol, EPS); // Scale the intersection parameters back into the span of the NURBS curve for(int j = 0; j < rc.size(); ++j) { - rp.push_back( rc[j] ); - np.push_back( knot_vals[i] + nc[j] * (knot_vals[i+1] - knot_vals[i]) ); + rp.push_back(rc[j]); + np.push_back(knot_vals[i] + nc[j] * (knot_vals[i + 1] - knot_vals[i])); } } @@ -811,7 +818,7 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, // Remove duplicates from the (u, v) intersection points // (Note it's not possible for (u_1, v_1) == (u_2, v_2) and t_1 != t_2) - const double EPS_sq = EPS * EPS; + const double sq_EPS = EPS * EPS; // The number of reported intersection points will be small, // so we don't need to fully sort the list @@ -828,7 +835,101 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, bool foundDuplicate = false; for(int j = i + 1; !foundDuplicate && j < tc.size(); ++j) { - if(squared_distance(uv, Point({uc[j], vc[j]})) < EPS_sq) + if(squared_distance(uv, Point({uc[j], vc[j]})) < sq_EPS) + { + foundDuplicate = true; + } + } + + if(!foundDuplicate) + { + t.push_back(tc[i]); + u.push_back(uc[i]); + v.push_back(vc[i]); + } + } + + return !t.empty(); +} + +template +AXOM_HOST_DEVICE bool intersect(const Ray& ray, + const NURBSPatch& patch, + axom::Array& t, + axom::Array& u, + axom::Array& v, + double tol = 1e-8, + double EPS = 1e-8, + bool isHalfOpen = false) +{ + // Check a bounding box of the entire NURBS first + Point ip; + if(!intersect(ray, patch.boundingBox(), ip)) + { + return false; + } + + // Decompose the NURBS patch into Bezier patches + auto beziers = patch.extractBezier(); + const int deg_u = patch.getDegree_u(); + const int deg_v = patch.getDegree_v(); + + const int num_knot_span_u = patch.getKnots_u().getNumKnotSpans(); + const int num_knot_span_v = patch.getKnots_v().getNumKnotSpans(); + + axom::Array knot_vals_u = patch.getUniqueKnots_u(); + axom::Array knot_vals_v = patch.getUniqueKnots_v(); + + // Store candidate intersections + axom::Array tc, uc, vc; + + // Check each Bezier patch, and scale the intersection parameters + // back into the span of the original NURBS patch + for(int i = 0; i < num_knot_span_u; ++i) + { + for(int j = 0; j < num_knot_span_v; ++j) + { + auto& bezier = beziers[i * num_knot_span_v + j]; + + // Store candidate intersections from each Bezier patch + axom::Array tcc, ucc, vcc; + intersect(ray, bezier, tcc, ucc, vcc, tol, EPS); + + // Scale the intersection parameters back into the span of the NURBS patch + for(int k = 0; k < tcc.size(); ++k) + { + tc.push_back(tcc[k]); + uc.push_back(knot_vals_u[i] + + ucc[k] * (knot_vals_u[i + 1] - knot_vals_u[i])); + vc.push_back(knot_vals_v[j] + + vcc[k] * (knot_vals_v[j + 1] - knot_vals_v[j])); + } + } + } + + // Do a second pass to remove duplicates from uc, vc + const double sq_EPS = EPS * EPS; + + // The number of reported intersection points will be small, + // so we don't need to fully sort the list + + double max_u_knot = patch.getKnots_u()[patch.getKnots_u().getNumKnots() - 1]; + double max_v_knot = patch.getKnots_v()[patch.getKnots_v().getNumKnots() - 1]; + + for(int i = 0; i < tc.size(); ++i) + { + // Also remove any intersections on the half-interval boundaries + if(isHalfOpen && (uc[i] >= max_u_knot - EPS || vc[i] >= max_v_knot - EPS)) + { + continue; + } + + Point uv({uc[i], vc[i]}); + + bool foundDuplicate = false; + for(int j = i + 1; !foundDuplicate && j < tc.size(); ++j) + { + if(squared_distance(uv, Point({uc[j], vc[j]})) < sq_EPS) { foundDuplicate = true; } diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp index 5ec014d731..6613fa81e0 100644 --- a/src/axom/primal/tests/primal_surface_intersect.cpp +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -34,9 +34,9 @@ namespace primal = axom::primal; * Param \a shouldPrintIntersections is used for debugging and for generating * the initial array of expected intersections. */ -template +template void checkIntersections(const primal::Ray& ray, - const primal::BezierPatch& patch, + const SurfaceType& patch, const axom::Array& exp_t, const axom::Array& exp_u, const axom::Array& exp_v, @@ -234,32 +234,74 @@ TEST(primal_surface_inter, bilinear_boundary_condition) ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.0, 1.0)); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); - checkIntersections(ray, bilinear_patch, {sqrt(3.0)}, {0.0}, {1.0}, eps, eps_test, !isHalfOpen); + checkIntersections(ray, + bilinear_patch, + {sqrt(3.0)}, + {0.0}, + {1.0}, + eps, + eps_test, + !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(0.5, 1.0)); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); - checkIntersections(ray, bilinear_patch, {sqrt(13.0) / 2.0}, {0.5}, {1.0}, eps, eps_test, !isHalfOpen); + checkIntersections(ray, + bilinear_patch, + {sqrt(13.0) / 2.0}, + {0.5}, + {1.0}, + eps, + eps_test, + !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.5)); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); - checkIntersections(ray, bilinear_patch, {sqrt(13.0) / 2.0}, {1.0}, {0.5}, eps, eps_test, !isHalfOpen); + checkIntersections(ray, + bilinear_patch, + {sqrt(13.0) / 2.0}, + {1.0}, + {0.5}, + eps, + eps_test, + !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.0)); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); - checkIntersections(ray, bilinear_patch, {sqrt(3.0)}, {1.0}, {0.0}, eps, eps_test, !isHalfOpen); + checkIntersections(ray, + bilinear_patch, + {sqrt(3.0)}, + {1.0}, + {0.0}, + eps, + eps_test, + !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 0.5)); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); - checkIntersections(ray, bilinear_patch, {sqrt(13.0) / 2.0}, {1.0}, {0.5}, eps, eps_test, !isHalfOpen); + checkIntersections(ray, + bilinear_patch, + {sqrt(13.0) / 2.0}, + {1.0}, + {0.5}, + eps, + eps_test, + !isHalfOpen); ray_direction = VectorType(ray_origin, bilinear_patch.evaluate(1.0, 1.0)); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test, isHalfOpen); - checkIntersections(ray, bilinear_patch, {sqrt(6.0)}, {1.0}, {1.0}, eps, eps_test, !isHalfOpen); + checkIntersections(ray, + bilinear_patch, + {sqrt(6.0)}, + {1.0}, + {1.0}, + eps, + eps_test, + !isHalfOpen); // These should record an intersection with both options @@ -539,7 +581,7 @@ TEST(primal_surface_inter, bezier_surface_intersect) } // Intersections with arbitrary patches aren't recorded - // with exact precision + // with as much precision as the base bilinear case const double eps = 1E-5; const double eps_test = 1E-5; const bool isHalfOpen = true; @@ -638,6 +680,103 @@ TEST(primal_surface_inter, bezier_surface_intersect) bool ray_intersects = intersect(ray, sphere_face_patch, t, u, v); } +//------------------------------------------------------------------------------ +TEST(primal_surface_inter, NURBS_surface_intersect) +{ + static const int DIM = 3; + using CoordType = double; + using PointType = primal::Point; + using VectorType = primal::Vector; + using NURBSPatchType = primal::NURBSPatch; + using RayType = primal::Ray; + + // Represent the sphere with a single NURBS patch + axom::Array knotvec_u = {-2.0, -2.0, -2.0, -2.0, 1.0, 1.0, 1.0, 1.0}; + axom::Array knotvec_v = + {0.0, 0.0, 0.0, 0.0, 1.5, 1.5, 1.5, 3.0, 3.0, 3.0, 3.0}; + + // clang-format off + axom::Array node_data = { + PointType {0, 0, 1}, PointType {0, 0, 1}, PointType { 0, 0, 1}, PointType { 0, 0, 1}, PointType { 0, 0, 1}, PointType {0, 0, 1}, PointType {0, 0, 1}, + PointType {2, 0, 1}, PointType {2, 4, 1}, PointType {-2, 4, 1}, PointType {-2, 0, 1}, PointType {-2, -4, 1}, PointType {2, -4, 1}, PointType {2, 0, 1}, + PointType {2, 0, -1}, PointType {2, 4, -1}, PointType {-2, 4, -1}, PointType {-2, 0, -1}, PointType {-2, -4, -1}, PointType {2, -4, -1}, PointType {2, 0, -1}, + PointType {0, 0, -1}, PointType {0, 0, -1}, PointType { 0, 0, -1}, PointType { 0, 0, -1}, PointType { 0, 0, -1}, PointType {0, 0, -1}, PointType {0, 0, -1}}; + + axom::Array weight_data = { + 1.0, 1.0/3.0, 1.0/3.0, 1.0, 1.0/3.0, 1.0/3.0, 1.0, + 1.0/3.0, 1.0/9.0, 1.0/9.0, 1.0/3.0, 1.0/9.0, 1.0/9.0, 1.0/3.0, + 1.0/3.0, 1.0/9.0, 1.0/9.0, 1.0/3.0, 1.0/9.0, 1.0/9.0, 1.0/3.0, + 1.0, 1.0/3.0, 1.0/3.0, 1.0, 1.0/3.0, 1.0/3.0, 1.0}; + // clang-format on + + NURBSPatchType sphere_patch(node_data, weight_data, 4, 7, knotvec_u, knotvec_v); + + // Add extra knots to make the Bezier extraction more interesting + sphere_patch.insertKnot_u(0.0, 1); + sphere_patch.insertKnot_u(0.5, 2); + sphere_patch.insertKnot_v(2.0, 3); + + // Test some parameter values that are at boundaries, knot values, + // subdivision boundaries, and interior points to subdivisions. + // Also, skip the points on the u-edges of the patch, + // as they're degenerate in physical space. + double params_u[6] = {-1.0, -0.9, 0.0, 0.3, 0.5, 0.75}; + double params_v[8] = {0.0, 0.3, 1.0, 1.6, 2.0, 2.5, 2.7, 3.0}; + + // Intersections with arbitrary patches aren't recorded + // with as much precision as the base bilinear case + const double eps = 1E-5; + const double eps_test = 1E-5; + const bool isHalfOpen = true; + + PointType ray_origin({0.0, 0.0, 0.0}); + + for(int i = 0; i < 6; ++i) + { + for(int j = 0; j < 8; ++j) + { + VectorType ray_direction(ray_origin, + sphere_patch.evaluate(params_u[i], params_v[j])); + RayType ray(ray_origin, ray_direction); + + std::cout << i << "< " << j << std::endl; + // The sphere meets itself at the v-edges + if(j == 0 || j == 7) + { + // Once if the surface is half-open + checkIntersections(ray, + sphere_patch, + {1.0}, + {params_u[i]}, + {0.0}, + eps, + eps_test, + isHalfOpen); + + // Twice if the surface is not half-open + checkIntersections(ray, + sphere_patch, + {1.0, 1.0}, + {params_u[i], params_u[i]}, + {0.0, 3.0}, + eps, + eps_test, + !isHalfOpen); + } + else + { + checkIntersections(ray, + sphere_patch, + {1.0}, + {params_u[i]}, + {params_v[j]}, + eps, + eps_test); + } + } + } +} + int main(int argc, char* argv[]) { int result = 0; From 02026f066ea5a0623c047fb9c1cb4f5bfa185f74 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Fri, 6 Dec 2024 14:33:59 -0700 Subject: [PATCH 20/47] Get changes from other PR --- src/axom/primal/geometry/KnotVector.hpp | 112 ++++++++++------- src/axom/primal/geometry/NURBSPatch.hpp | 154 ++++++++++++++++-------- 2 files changed, 176 insertions(+), 90 deletions(-) diff --git a/src/axom/primal/geometry/KnotVector.hpp b/src/axom/primal/geometry/KnotVector.hpp index b446d112ec..36b419b6c9 100644 --- a/src/axom/primal/geometry/KnotVector.hpp +++ b/src/axom/primal/geometry/KnotVector.hpp @@ -191,21 +191,6 @@ class KnotVector /// \brief Return the number of knots in the knot vector axom::IndexType getNumKnots() const { return m_knots.size(); } - /// \brief Return an array of the unique knot values - axom::Array getUniqueKnots() const - { - axom::Array unique_knots; - for(int i = 0; i < m_knots.size(); ++i) - { - if(i == 0 || m_knots[i] != m_knots[i - 1]) - { - unique_knots.push_back(m_knots[i]); - } - } - - return unique_knots; - } - /// \brief Return the number of valid knot spans axom::IndexType getNumKnotSpans() const { @@ -242,19 +227,28 @@ class KnotVector * For knot vector {u_0, ..., u_n}, returns i such that u_i <= t < u_i+1 * if t == u_n, returns i such that u_i < t <= u_i+1 (i.e. i = n - degree - 1) * - * \pre Assumes that the input t is within the knot vector - * * Implementation adapted from Algorithm A2.1 on page 68 of "The NURBS Book" * + * \pre Assumes that the input t is in the span [u_0, u_n] (up to some tolerance) + * + * \note If t is outside the knot span up to this tolerance, it is clamped to the span + * * \return The index of the knot span containing t */ axom::IndexType findSpan(T t) const { - SLIC_ASSERT(t >= m_knots[0] && t <= m_knots[m_knots.size() - 1]); + SLIC_ASSERT(isValidParameter(t)); const axom::IndexType nkts = m_knots.size(); - if(t == m_knots[nkts - 1]) + // Handle cases where t is outside the knot span within a tolerance + // by implicitly clamping it to the nearest span + if(t <= m_knots[0]) + { + return m_deg; + } + + if(t >= m_knots[nkts - 1]) { return nkts - m_deg - 2; } @@ -281,19 +275,29 @@ class KnotVector * For knot vector {u_0, ..., u_n}, returns i such that u_i <= t < u_i+1 * if t == u_n, returns i such that u_i < t <= u_i+1 (i.e. i = n - degree - 1) * - * \pre Assumes that the input t is within the knot vector + * \pre Assumes that the input t is within the knot vector (up to some tolerance) + * + * \note If t is outside the knot span up to this tolerance, the returned multiplicity + * will be equal to the degree + 1 (required for clamped curves) * * \return The index of the knot span containing t */ axom::IndexType findSpan(T t, int& multiplicity) const { - SLIC_ASSERT(t >= m_knots[0] && t <= m_knots[m_knots.size() - 1]); + SLIC_ASSERT(isValidParameter(t)); const auto nkts = m_knots.size(); const auto span = findSpan(t); + // Early exit for known multiplicities + if(t <= m_knots[0] || t >= m_knots[nkts - 1]) + { + multiplicity = m_deg + 1; + return span; + } + multiplicity = 0; - for(auto i = (t == m_knots[nkts - 1]) ? nkts - 1 : span; i >= 0; --i) + for(auto i = span; i >= 0; --i) { if(m_knots[i] == t) { @@ -371,20 +375,22 @@ class KnotVector * \param [in] t The value of the knot to insert * \param [in] target_mutliplicity The number of times the knot will be present * - * \pre Assumes that the input t is within the knot vector + * \pre Assumes that the input t is within the knot vector (up to some tolerance) * * \note If the knot is already present, it will be inserted * up to the given multiplicity, or the maximum permitted by the degree */ void insertKnot(T t, int target_multiplicity) { - SLIC_ASSERT(t >= m_knots[0] && t <= m_knots[m_knots.size() - 1]); + SLIC_ASSERT(isValidParameter(t)); int multiplicity; auto span = findSpan(t, multiplicity); - int r = - axom::utilities::clampVal(target_multiplicity - multiplicity, 0, m_deg); + // Compute how many knots should be inserted + int r = axom::utilities::clampVal(target_multiplicity - multiplicity, + 0, + m_deg - multiplicity); insertKnotBySpan(span, t, r); } @@ -439,8 +445,8 @@ class KnotVector k2.normalize(); } - // SLIC_ASSERT(k1.isValid()); - // SLIC_ASSERT(k2.isValid()); + SLIC_ASSERT(k1.isValid()); + SLIC_ASSERT(k2.isValid()); } /*! @@ -451,11 +457,11 @@ class KnotVector * \param [out] k2 The second knot vector * \param [in] normalize Whether to normalize the output knot vectors * - * \pre Assumes that the input t is within the knot vector + * \pre Assumes that the input t is *interior* to the knot vector */ void split(T t, KnotVector& k1, KnotVector& k2, bool normalize = false) const { - SLIC_ASSERT(t > m_knots[0] && t < m_knots[m_knots.size() - 1]); + SLIC_ASSERT(isValidInteriorParameter(t)); int multiplicity; axom::IndexType span = findSpan(t, multiplicity); @@ -472,7 +478,7 @@ class KnotVector * \param [in] span The span in which to evaluate the basis functions * \param [in] t The parameter value * - * \pre Assumes that the input t is within the knot vector and that the span is valid + * \pre Assumes that the input t is within the correct span * Implementation adapted from Algorithm A2.2 on page 70 of "The NURBS Book". * * \return An array of the `m_deg + 1` non-zero basis functions evaluated at t @@ -511,13 +517,13 @@ class KnotVector * * \param [in] t The parameter value * - * \pre Assumes that the input t is within the knot vector + * \pre Assumes that the input t is within the knot vector (up to a tolerance) * * \return An array of the `m_deg + 1` non-zero basis functions evaluated at t */ axom::Array calculateBasisFunctions(T t) const { - SLIC_ASSERT(t >= m_knots[0] && t <= m_knots[m_knots.size() - 1]); + SLIC_ASSERT(isValidParameter(t)); return calculateBasisFunctionsBySpan(findSpan(t), t); } @@ -527,19 +533,23 @@ class KnotVector * \param [in] span The span in which to evaluate the basis functions * \param [in] t The parameter value * \param [in] n The number of derivatives to compute - * \param [out] ders An array of the `n + 1` derivatives evaluated at t * * Implementation adapted from Algorithm A2.2 on page 70 of "The NURBS Book". + * + * \pre Assumes that the input t is within the provided knot span + * + * \return An array of the `n + 1` derivatives evaluated at t */ - void derivativeBasisFunctionsBySpan(axom::IndexType span, - T t, - int n, - axom::Array>& ders) const + axom::Array> derivativeBasisFunctionsBySpan(axom::IndexType span, + T t, + int n) const { SLIC_ASSERT(isValidSpan(span, t)); const int m_deg = getDegree(); + axom::Array> ders(n + 1); + axom::Array> ndu(m_deg + 1), a(2); axom::Array left(m_deg + 1), right(m_deg + 1); for(int j = 0; j <= m_deg; j++) @@ -611,6 +621,7 @@ class KnotVector std::swap(s1, s2); } } + // Multiply through by the correct factors (Eq. [2.9]) T r = static_cast(m_deg); for(int k = 1; k <= n; k++) @@ -621,6 +632,8 @@ class KnotVector } r *= static_cast(m_deg - k); } + + return ders; } /*! @@ -628,14 +641,15 @@ class KnotVector * * \param [in] t The parameter value * \param [in] n The number of derivatives to compute - * \param [out] ders An array of the `n + 1` derivatives evaluated at t * - * \pre Assumes that the input t is within the knot vector + * \pre Assumes that the input t is within the knot vector (up to a tolerance) + * + * \return An array of the `n + 1` derivatives evaluated at t */ - void derivativeBasisFunctions(T t, int n, axom::Array>& ders) const + axom::Array> derivativeBasisFunctions(T t, int n) const { - SLIC_ASSERT(t >= m_knots[0] && t <= m_knots[m_knots.size() - 1]); - derivativeBasisFunctionsBySpan(findSpan(t), t, n, ders); + SLIC_ASSERT(isValidParameter(t)); + return derivativeBasisFunctionsBySpan(findSpan(t), t, n); } /// \brief Reverse the knot vector @@ -770,6 +784,18 @@ class KnotVector return !(lhs == rhs); } + /// \brief Checks if given parameter is in knot span (to a tolerance) + bool isValidParameter(T t, T EPS = 1e-5) const + { + return t >= m_knots[0] - EPS && t <= m_knots[m_knots.size() - 1] + EPS; + } + + /// \brief Checks if given parameter is *interior* to knot span (to a tolerance) + bool isValidInteriorParameter(T t) const + { + return t > m_knots[0] && t < m_knots[m_knots.size() - 1]; + } + /*! * \brief Simple formatted print of a knot vector instance * diff --git a/src/axom/primal/geometry/NURBSPatch.hpp b/src/axom/primal/geometry/NURBSPatch.hpp index f9d71cbd5d..35a41d6478 100644 --- a/src/axom/primal/geometry/NURBSPatch.hpp +++ b/src/axom/primal/geometry/NURBSPatch.hpp @@ -729,14 +729,21 @@ class NURBSPatch * * Adapted from Algorithm A3.5 on page 103 of "The NURBS Book" * - * \pre Requires \a u, v in the span of each knot vector + * \pre Requires \a u, v in the span of each knot vector (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ PointType evaluate(T u, T v) const { - SLIC_ASSERT(u >= m_knotvec_u[0] && - u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); - SLIC_ASSERT(v >= m_knotvec_v[0] && - v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_u.isValidParameter(u)); + SLIC_ASSERT(m_knotvec_v.isValidParameter(v)); + + u = axom::utilities::clampVal(u, + m_knotvec_u[0], + m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + v = axom::utilities::clampVal(v, + m_knotvec_v[0], + m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); const auto span_u = m_knotvec_u.findSpan(u); const auto span_v = m_knotvec_v.findSpan(v); @@ -1032,18 +1039,12 @@ class NURBSPatch /// \brief Return an array of knot values on the first axis axom::Array getKnotsArray_u() const { return m_knotvec_u.getArray(); } - /// \brief Return an array of the unique knots in the knot vector - axom::Array getUniqueKnots_u() const { return m_knotvec_u.getUniqueKnots(); } - /// \brief Return a copy of the KnotVector instance on the second axis KnotVectorType getKnots_v() const { return m_knotvec_v; } /// \brief Return an array of knot values on the second axis axom::Array getKnotsArray_v() const { return m_knotvec_v.getArray(); } - /// \brief Return an array of the unique knots in the knot vector - axom::Array getUniqueKnots_v() const { return m_knotvec_v.getUniqueKnots(); } - /// \brief Returns the number of control points in the NURBS Patch on the first axis int getNumControlPoints_u() const { @@ -1312,12 +1313,16 @@ class NURBSPatch * \param [in] u Parameter value fixed in the isocurve * \return c The isocurve C(v) = S(u, v) for fixed u * - * \pre Requires \a u be in the span of the knot vector + * \pre Requires \a u be in the span of the knot vector (up to a small tolerance) + * + * \note If u is outside the knot span up this tolerance, it is clamped to the span */ NURBSCurveType isocurve_u(T u) const { - SLIC_ASSERT(u >= m_knotvec_u[0] && - u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_u.isValidParameter(u)); + u = axom::utilities::clampVal(u, + m_knotvec_u[0], + m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); using axom::utilities::lerp; @@ -1374,12 +1379,16 @@ class NURBSPatch * \param [in] v Parameter value fixed in the isocurve * \return c The isocurve C(u) = S(u, v) for fixed v * - * \pre Requires \a v be in the span of the knot vector + * \pre Requires \a v be in the span of the knot vector (up to a small tolerance) + * + * \note If v is outside the knot span up this tolerance, it is clamped to the span */ NURBSCurveType isocurve_v(T v) const { - SLIC_ASSERT(v >= m_knotvec_v[0] && - v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_v.isValidParameter(v)); + v = axom::utilities::clampVal(v, + m_knotvec_v[0], + m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); using axom::utilities::lerp; @@ -1444,14 +1453,21 @@ class NURBSPatch * Implementation adapted from Algorithm A3.6 on p. 111 of "The NURBS Book". * Rational derivatives from Algorithm A4.4 on p. 137 of "The NURBS Book". * - * \pre Requires \a u, v be in the span of the knots + * \pre Requires \a u, v be in the span of the knots (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ void evaluateDerivatives(T u, T v, int d, axom::Array& ders) const { - SLIC_ASSERT(u >= m_knotvec_u[0] && - u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); - SLIC_ASSERT(v >= m_knotvec_v[0] && - v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_u.isValidParameter(u)); + SLIC_ASSERT(m_knotvec_v.isValidParameter(v)); + + u = axom::utilities::clampVal(u, + m_knotvec_u[0], + m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + v = axom::utilities::clampVal(v, + m_knotvec_v[0], + m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); const int deg_u = getDegree_u(); const int du = std::min(d, deg_u); @@ -1597,7 +1613,9 @@ class NURBSPatch * \param [out] Dv The vector value of S_v(u, v) * \param [out] DuDv The vector value of S_uv(u, v) == S_vu(u, v) * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ void evaluateLinearDerivatives(T u, T v, @@ -1627,7 +1645,9 @@ class NURBSPatch * \param [out] DvDv The vector value of S_vv(u, v) * \param [out] DuDv The vector value of S_uu(u, v) * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ void evaluateSecondDerivatives(T u, T v, @@ -1655,7 +1675,9 @@ class NURBSPatch * \param [in] u Parameter value at which to evaluate on the first axis * \param [in] v Parameter value at which to evaluate on the second axis * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ VectorType du(T u, T v) const { @@ -1671,7 +1693,9 @@ class NURBSPatch * \param [in] u Parameter value at which to evaluate on the first axis * \param [in] v Parameter value at which to evaluate on the second axis * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ VectorType dv(T u, T v) const { @@ -1687,7 +1711,9 @@ class NURBSPatch * \param [in] u Parameter value at which to evaluate on the first axis * \param [in] v Parameter value at which to evaluate on the second axis * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ VectorType dudu(T u, T v) const { @@ -1703,7 +1729,9 @@ class NURBSPatch * \param [in] u Parameter value at which to evaluate on the first axis * \param [in] v Parameter value at which to evaluate on the second axis * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ VectorType dvdv(T u, T v) const { @@ -1719,7 +1747,9 @@ class NURBSPatch * \param [in] u Parameter value at which to evaluate on the first axis * \param [in] v Parameter value at which to evaluate on the second axis * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ VectorType dudv(T u, T v) const { @@ -1735,7 +1765,9 @@ class NURBSPatch * \param [in] u Parameter value at which to evaluate on the first axis * \param [in] v Parameter value at which to evaluate on the second axis * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ VectorType dvdu(T u, T v) const { return dudv(u, v); } @@ -1745,7 +1777,9 @@ class NURBSPatch * \param [in] u Parameter value at which to evaluate on the first axis * \param [in] v Parameter value at which to evaluate on the second axis * - * \pre We require evaluation of the patch at \a u and \a v between 0 and 1 + * \pre We require evaluation of the patch at \a u and \a v (up to a small tolerance) + * + * \note If u/v is outside the knot span up this tolerance, it is clamped to the span */ VectorType normal(T u, T v) const { @@ -1768,14 +1802,19 @@ class NURBSPatch * \note If the knot is already present, it will be inserted * up to the given multiplicity, or the maximum permitted by the degree * - * \pre Requires \a u in the span of the knots + * \pre Requires \a u in the span of the knots (up to a small tolerance) + * + * \note If u is outside the knot span up this tolerance, it is clamped to the span * * \return The (maximum) index of the new knot */ axom::IndexType insertKnot_u(T u, int target_multiplicity = 1) { - SLIC_ASSERT(u >= m_knotvec_u[0] && - u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_u.isValidParameter(u)); + u = axom::utilities::clampVal(u, + m_knotvec_u[0], + m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + SLIC_ASSERT(target_multiplicity > 0); const bool isRationalPatch = isRational(); @@ -1929,14 +1968,19 @@ class NURBSPatch * \note If the knot is already present, it will be inserted * up to the given multiplicity, or the maximum permitted by the degree * - * \pre Requires \a v in the span of the knots + * \pre Requires \a v in the span of the knots (up to a small tolerance) + * + * \note If v is outside the knot span up this tolerance, it is clamped to the span * * \return The (maximum) index of the new knot */ axom::IndexType insertKnot_v(T v, int target_multiplicity = 1) { - SLIC_ASSERT(v >= m_knotvec_v[0] && - v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_v.isValidParameter(v)); + v = axom::utilities::clampVal(v, + m_knotvec_v[0], + m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + SLIC_ASSERT(target_multiplicity > 0); const bool isRationalPatch = isRational(); @@ -2100,7 +2144,7 @@ class NURBSPatch * | | | * ---------------------- u = 1 * - * \pre Parameter \a u and \a v must be in the knot span + * \pre Parameter \a u and \a v must be *strictly interior* to the knot span */ void split(T u, T v, @@ -2109,10 +2153,8 @@ class NURBSPatch NURBSPatch& p3, NURBSPatch& p4) const { - SLIC_ASSERT(u > m_knotvec_u[0] && - u < m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); - SLIC_ASSERT(v > m_knotvec_v[0] && - v < m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_u.isValidInteriorParameter(u)); + SLIC_ASSERT(m_knotvec_v.isValidInteriorParameter(v)); // Bisect the patch along the u direction split_u(u, p1, p2); @@ -2130,8 +2172,7 @@ class NURBSPatch */ void split_u(T u, NURBSPatch& p1, NURBSPatch& p2, bool normalize = false) const { - SLIC_ASSERT(u > m_knotvec_u[0] && - u < m_knotvec_u[m_knotvec_u.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_u.isValidInteriorParameter(u)); const bool isRationalPatch = isRational(); @@ -2201,8 +2242,7 @@ class NURBSPatch */ void split_v(T v, NURBSPatch& p1, NURBSPatch& p2, bool normalize = false) const { - SLIC_ASSERT(v > m_knotvec_v[0] && - v < m_knotvec_v[m_knotvec_v.getNumKnots() - 1]); + SLIC_ASSERT(m_knotvec_v.isValidInteriorParameter(v)); const bool isRationalPatch = isRational(); @@ -2742,6 +2782,26 @@ class NURBSPatch return true; } + /// \brief Function to check if the u parameter is within the knot span + bool isValidParameter_u(T u, T EPS = 1e-8) const + { + return u >= m_knotvec_u[0] - EPS && + u <= m_knotvec_u[m_knotvec_u.getNumKnots() - 1] + EPS; + } + + /// \brief Function to check if the v parameter is within the knot span + bool isValidParameter_v(T v, T EPS = 1e-8) const + { + return v >= m_knotvec_v[0] - EPS && + v <= m_knotvec_v[m_knotvec_v.getNumKnots() - 1] + EPS; + } + + /// \brief Checks if given u parameter is *interior* to the knot span + bool isValidInteriorParameter(T t) const + { + return m_knotvec_u.isValidInteriorParameter(t); + } + private: CoordsMat m_controlPoints; WeightsMat m_weights; @@ -2761,4 +2821,4 @@ std::ostream& operator<<(std::ostream& os, const NURBSPatch& nPatch) } // namespace primal } // namespace axom -#endif // AXOM_PRIMAL_NURBSPATCH_HPP_ +#endif // AXOM_PRIMAL_NURBSPATCH_HPP_ \ No newline at end of file From 43d4514877107db629572e7f0ab1fdc6ca048279 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 17:58:56 -0700 Subject: [PATCH 21/47] Improve comment on isBilinear --- src/axom/primal/geometry/BezierPatch.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/axom/primal/geometry/BezierPatch.hpp b/src/axom/primal/geometry/BezierPatch.hpp index b4c3f946c5..62b8dc2af3 100644 --- a/src/axom/primal/geometry/BezierPatch.hpp +++ b/src/axom/primal/geometry/BezierPatch.hpp @@ -1987,8 +1987,10 @@ class BezierPatch /*! * \brief Predicate to check if the Bezier patch is approximately bilinear * - * This function checks if the non-corner control points of the patch form a grid - * with respect to the corner control points, up to a tolerance `sq_tol` + * This function checks if the patch is (nearly) bilinear in terms of its shape + * and it's parameterization. A necessary (but possibly not sufficient) condition + * for this is that the control points are coincident with the surface of the + * bilinear patch defined by its corners evaluated at uniform parameter values. * * * \param [in] sq_tol Threshold for absolute squared distances * \return True if patch is bilinear From 488b8fbfe29387fae4055f3a4fbd3451d8c31a26 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 17:59:33 -0700 Subject: [PATCH 22/47] Move "get unique knot values" to KnotVector --- src/axom/primal/geometry/KnotVector.hpp | 15 +++++++++++++++ src/axom/primal/geometry/NURBSCurve.hpp | 3 --- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/axom/primal/geometry/KnotVector.hpp b/src/axom/primal/geometry/KnotVector.hpp index 36b419b6c9..280f058444 100644 --- a/src/axom/primal/geometry/KnotVector.hpp +++ b/src/axom/primal/geometry/KnotVector.hpp @@ -191,6 +191,21 @@ class KnotVector /// \brief Return the number of knots in the knot vector axom::IndexType getNumKnots() const { return m_knots.size(); } + /// \brief Return an array of the unique knot values + axom::Array getUniqueKnots() const + { + axom::Array unique_knots; + for(int i = 0; i < m_knots.size(); ++i) + { + if(i == 0 || m_knots[i] != m_knots[i - 1]) + { + unique_knots.push_back(m_knots[i]); + } + } + + return unique_knots; + } + /// \brief Return the number of valid knot spans axom::IndexType getNumKnotSpans() const { diff --git a/src/axom/primal/geometry/NURBSCurve.hpp b/src/axom/primal/geometry/NURBSCurve.hpp index 589f55c3e2..b6f983aed7 100644 --- a/src/axom/primal/geometry/NURBSCurve.hpp +++ b/src/axom/primal/geometry/NURBSCurve.hpp @@ -1164,9 +1164,6 @@ class NURBSCurve /// \brief Return a copy of the knot vector as an array axom::Array getKnotsArray() const { return m_knotvec.getArray(); } - /// \brief Return an array of the unique knots in the knot vector - axom::Array getUniqueKnots() const { return m_knotvec.getUniqueKnots(); } - /// \brief Reverses the order of the NURBS curve's control points and weights void reverseOrientation() { From aed9364ef1e35cb3bf20478e97f3a9820cada63a Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 17:59:49 -0700 Subject: [PATCH 23/47] Tidy up implementation --- .../primal/operators/detail/intersect_impl.hpp | 10 +++++----- src/axom/primal/operators/intersect.hpp | 15 +++++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index 45cdde04e5..60ffdc20f9 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -1709,17 +1709,17 @@ AXOM_HOST_DEVICE bool intersect_plane_tet3d(const Plane& p, */ AXOM_HOST_DEVICE inline bool intersect_line_bilinear_patch(const Line& line, - const Point3& p0, - const Point3& p1, - const Point3& p2, - const Point3& p3, + const Point3& p00, + const Point3& p10, + const Point3& p11, + const Point3& p01, axom::Array& t, axom::Array& u, axom::Array& v, double EPS = 1e-8, bool isRay = false) { - Vector3 q00(p0), q10(p1), q11(p2), q01(p3); + Vector3 q00(p00), q10(p10), q11(p11), q01(p01); Vector3 e10 = q10 - q00; Vector3 e11 = q11 - q10; diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index 5c527e3b93..cb949387ac 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -637,7 +637,7 @@ bool intersect(const Ray& r, // Decompose the NURBS curve into Bezier segments auto beziers = n.extractBezier(); const int deg = n.getDegree(); - axom::Array knot_vals = n.getUniqueKnots(); + axom::Array knot_vals = n.getKnots().getUniqueKnots(); // Check each Bezier segment, and scale the intersection parameters // back into the span of the original NURBS curve @@ -749,7 +749,10 @@ AXOM_HOST_DEVICE bool intersect(const Plane& p, * For bilinear patches, implements GARP algorithm from Chapter 8 of Ray Tracing Gems (2019) * For higher order patches, intersections are found through recursive subdivison * until the subpatch is approximated by a bilinear patch. - * Assumes that the ray is not tangent to the patch + * Assumes that the ray is not tangent to the patch, and that the intersection + * is not at a point of degeneracy for which there are *infinitely* many intersections. + * For such intersections, the method will hang as it tries records an arbitrarily high + * number of intersections with distinct parameter values * * \return true iff the ray intersects the patch, otherwise false. */ @@ -874,11 +877,11 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, const int deg_u = patch.getDegree_u(); const int deg_v = patch.getDegree_v(); - const int num_knot_span_u = patch.getKnots_u().getNumKnotSpans(); - const int num_knot_span_v = patch.getKnots_v().getNumKnotSpans(); + axom::Array knot_vals_u = patch.getKnots_u().getUniqueKnots(); + axom::Array knot_vals_v = patch.getKnots_v().getUniqueKnots(); - axom::Array knot_vals_u = patch.getUniqueKnots_u(); - axom::Array knot_vals_v = patch.getUniqueKnots_v(); + const auto num_knot_span_u = knot_vals_u.size() - 1; + const auto num_knot_span_v = knot_vals_v.size() - 1; // Store candidate intersections axom::Array tc, uc, vc; From 73388403464294e7133fbccf7bbf16e84e9a0f3e Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 18:00:13 -0700 Subject: [PATCH 24/47] Add tests for self-coincident intersections --- .../primal/tests/primal_surface_intersect.cpp | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp index 6613fa81e0..2da9b0f5c3 100644 --- a/src/axom/primal/tests/primal_surface_intersect.cpp +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -202,7 +202,7 @@ TEST(primal_surface_inter, bilinear_intersect) } //------------------------------------------------------------------------------ -TEST(primal_surface_inter, bilinear_boundary_condition) +TEST(primal_surface_inter, bilinear_boundary_treatment) { static const int DIM = 3; using CoordType = double; @@ -405,6 +405,28 @@ TEST(primal_surface_inter, difficult_garp_case) ray_direction = VectorType({0.0, -1.0, 0.0}); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {2.0}, {0.5}, {0.5}, eps, eps_test); + + // Give patch a degeneracy at the point of intersection + // at which there are infinitely many parameters of intersection + bilinear_patch(1, 1) = PointType({-1.0, -1.0, 2.0}); + + ray_origin = PointType({-2.0, 0.0, 2.0}); + ray_direction = VectorType({1.0, -1.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + + // Current behavior is to return a single point of intersection, + // as in the above cases with infinitely many intersections + checkIntersections(ray, bilinear_patch, {sqrt(2)}, {1.0}, {0.5}, eps, eps_test); + + // Set ray origin to the point of degeneracy + ray_origin = PointType({-1.0, -1.0, 2.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {0.0}, {1.0}, {0.5}, eps, eps_test); + + // Set ray origin past the point of degeneracy (no intersections) + ray_origin = PointType({0.0, -2.0, 2.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); } //------------------------------------------------------------------------------ @@ -671,13 +693,6 @@ TEST(primal_surface_inter, bezier_surface_intersect) } } } - - VectorType ray_direction(ray_origin, - sphere_face_patch.evaluate(u_params[0], v_params[6])); - RayType ray(ray_origin, ray_direction); - - axom::Array t, u, v; - bool ray_intersects = intersect(ray, sphere_face_patch, t, u, v); } //------------------------------------------------------------------------------ @@ -703,10 +718,10 @@ TEST(primal_surface_inter, NURBS_surface_intersect) PointType {0, 0, -1}, PointType {0, 0, -1}, PointType { 0, 0, -1}, PointType { 0, 0, -1}, PointType { 0, 0, -1}, PointType {0, 0, -1}, PointType {0, 0, -1}}; axom::Array weight_data = { - 1.0, 1.0/3.0, 1.0/3.0, 1.0, 1.0/3.0, 1.0/3.0, 1.0, + 1.0, 1.0/3.0, 1.0/3.0, 1.0, 1.0/3.0, 1.0/3.0, 1.0, 1.0/3.0, 1.0/9.0, 1.0/9.0, 1.0/3.0, 1.0/9.0, 1.0/9.0, 1.0/3.0, 1.0/3.0, 1.0/9.0, 1.0/9.0, 1.0/3.0, 1.0/9.0, 1.0/9.0, 1.0/3.0, - 1.0, 1.0/3.0, 1.0/3.0, 1.0, 1.0/3.0, 1.0/3.0, 1.0}; + 1.0, 1.0/3.0, 1.0/3.0, 1.0, 1.0/3.0, 1.0/3.0, 1.0}; // clang-format on NURBSPatchType sphere_patch(node_data, weight_data, 4, 7, knotvec_u, knotvec_v); @@ -739,7 +754,6 @@ TEST(primal_surface_inter, NURBS_surface_intersect) sphere_patch.evaluate(params_u[i], params_v[j])); RayType ray(ray_origin, ray_direction); - std::cout << i << "< " << j << std::endl; // The sphere meets itself at the v-edges if(j == 0 || j == 7) { From b38472fc725e67f7b021767c0c3d137d5e52fdce Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 18:38:04 -0700 Subject: [PATCH 25/47] Update usage of axom::Array --- src/axom/primal/tests/primal_rational_bezier.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axom/primal/tests/primal_rational_bezier.cpp b/src/axom/primal/tests/primal_rational_bezier.cpp index 632a794f37..a09e6b66bb 100644 --- a/src/axom/primal/tests/primal_rational_bezier.cpp +++ b/src/axom/primal/tests/primal_rational_bezier.cpp @@ -497,7 +497,7 @@ TEST(primal_rationalbezier, rational_intersection) Bezier bottom_arc(bot_nodes, weights, 2); Bezier top_arc(top_nodes, weights, 2); - std::vector sp, tp; + axom::Array sp, tp; EXPECT_TRUE(intersect(bottom_arc, top_arc, sp, tp)); EXPECT_NEAR(bottom_arc.evaluate(sp[0])[0], top_arc.evaluate(tp[0])[0], abs_tol); From 19288ace11c8a7ba4bcb5eb898bd245b512030f9 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 18:58:25 -0700 Subject: [PATCH 26/47] Fix some minor bugs --- src/axom/primal/geometry/BezierPatch.hpp | 4 ++-- src/axom/primal/operators/detail/intersect_ray_impl.hpp | 1 + src/axom/primal/tests/primal_bezier_intersect.cpp | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/axom/primal/geometry/BezierPatch.hpp b/src/axom/primal/geometry/BezierPatch.hpp index 62b8dc2af3..e530a02a77 100644 --- a/src/axom/primal/geometry/BezierPatch.hpp +++ b/src/axom/primal/geometry/BezierPatch.hpp @@ -1903,11 +1903,11 @@ class BezierPatch // Find three points that produce a nonzero normal Vector3D plane_normal = VectorType::cross_product(v1, v2); - if(axom::utilities::isNearlyEqual(plane_normal.norm(), 0.0, tol)) + if(axom::utilities::isNearlyEqual(plane_normal.norm(), 0.0, EPS)) { plane_normal = VectorType::cross_product(v1, v3); } - if(axom::utilities::isNearlyEqual(plane_normal.norm(), 0.0, tol)) + if(axom::utilities::isNearlyEqual(plane_normal.norm(), 0.0, EPS)) { plane_normal = VectorType::cross_product(v2, v3); } diff --git a/src/axom/primal/operators/detail/intersect_ray_impl.hpp b/src/axom/primal/operators/detail/intersect_ray_impl.hpp index 8340bf5689..70d3ee18a6 100644 --- a/src/axom/primal/operators/detail/intersect_ray_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_ray_impl.hpp @@ -12,6 +12,7 @@ // primal includes #include "axom/primal/geometry/Point.hpp" #include "axom/primal/geometry/Ray.hpp" +#include "axom/primal/geometry/Line.hpp" #include "axom/primal/geometry/Segment.hpp" #include "axom/primal/geometry/BoundingBox.hpp" diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index e54fb08307..b946f82465 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -955,12 +955,12 @@ TEST(primal_bezier_inter, ray_nurbs_intersections) axom::numerics::linspace(-1.0, 2.0, params, 10); PointType ray_origin({0.0, 0.0}); - for(int i = 0; i < 9; ++i) // Skip the last parameter, which is equal to i=0 + for(int i = 0; i < 9; ++i) // Skip the last parameter, which is equal to i=0 { - VectorType ray_direction( ray_origin, circle.evaluate(params[i]) ); + VectorType ray_direction(ray_origin, circle.evaluate(params[i])); RayType ray(ray_origin, ray_direction); - checkIntersectionsRay( ray, circle, {1.0}, {params[i]}, eps, eps_test ); + checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test); } } From c13e49715718db1cc3a7c092f7b04fd2728ee4e3 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 20:14:35 -0700 Subject: [PATCH 27/47] Remove some unused variables --- src/axom/primal/operators/intersect.hpp | 53 +++++++++---------- .../primal/tests/primal_bezier_intersect.cpp | 2 +- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index cb949387ac..a80d0508f3 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -636,7 +636,6 @@ bool intersect(const Ray& r, // Decompose the NURBS curve into Bezier segments auto beziers = n.extractBezier(); - const int deg = n.getDegree(); axom::Array knot_vals = n.getKnots().getUniqueKnots(); // Check each Bezier segment, and scale the intersection parameters @@ -774,7 +773,6 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, // Store the candidate intersections axom::Array tc, uc, vc; - bool foundIntersection = false; if(order_u < 1 || order_v < 1) { @@ -784,17 +782,16 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, else if(order_u == 1 && order_v == 1) { primal::Line line(ray.origin(), ray.direction()); - foundIntersection = - detail::intersect_line_bilinear_patch(line, - patch(0, 0), - patch(order_u, 0), - patch(order_u, order_v), - patch(0, order_v), - tc, - uc, - vc, - EPS, - true); + detail::intersect_line_bilinear_patch(line, + patch(0, 0), + patch(order_u, 0), + patch(order_u, order_v), + patch(0, order_v), + tc, + uc, + vc, + EPS, + true); } else { @@ -803,20 +800,20 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, double u_offset = 0., v_offset = 0.; double u_scale = 1., v_scale = 1.; - foundIntersection = detail::intersect_line_patch(line, - patch, - tc, - uc, - vc, - order_u, - order_v, - u_offset, - u_scale, - v_offset, - v_scale, - sq_tol, - EPS, - true); + detail::intersect_line_patch(line, + patch, + tc, + uc, + vc, + order_u, + order_v, + u_offset, + u_scale, + v_offset, + v_scale, + sq_tol, + EPS, + true); } // Remove duplicates from the (u, v) intersection points @@ -874,8 +871,6 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, // Decompose the NURBS patch into Bezier patches auto beziers = patch.extractBezier(); - const int deg_u = patch.getDegree_u(); - const int deg_v = patch.getDegree_v(); axom::Array knot_vals_u = patch.getKnots_u().getUniqueKnots(); axom::Array knot_vals_v = patch.getKnots_v().getUniqueKnots(); diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index b946f82465..27099ccea3 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -960,7 +960,7 @@ TEST(primal_bezier_inter, ray_nurbs_intersections) VectorType ray_direction(ray_origin, circle.evaluate(params[i])); RayType ray(ray_origin, ray_direction); - checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test); + checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test, true); } } From d62231ac1dd9d4195195bb0193877bfd29a23cfc Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 23:13:05 -0700 Subject: [PATCH 28/47] Minor changes to help debugging --- .../primal/operators/detail/intersect_bezier_impl.hpp | 2 ++ src/axom/primal/tests/primal_bezier_intersect.cpp | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index b258d9f456..dbbeedfda5 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -298,6 +298,8 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear + bool isIntersect = intersect(r, seg, r0, s0, 0.0); + SLIC_INFO( "isIntersect: " << isIntersect << " r0: " << r0 << " s0: " << s0); if(intersect(r, seg, r0, s0, 0.0) && s0 < 1.0 - EPS) { rp.push_back(r0); diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index 27099ccea3..62bed5d2d1 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -92,13 +92,13 @@ void checkIntersections(const primal::BezierCurve& curve1, << "\n\t" << curve1 << "\n\t" << curve2; sstr << "\ns (" << s.size() << "): "; - for(auto i = 0u; i < s.size(); ++i) + for(auto i = 0; i < s.size(); ++i) { sstr << std::setprecision(16) << s[i] << ","; } sstr << "\nt (" << t.size() << "): "; - for(auto i = 0u; i < t.size(); ++i) + for(auto i = 0; i < t.size(); ++i) { sstr << std::setprecision(16) << t[i] << ","; } @@ -955,8 +955,10 @@ TEST(primal_bezier_inter, ray_nurbs_intersections) axom::numerics::linspace(-1.0, 2.0, params, 10); PointType ray_origin({0.0, 0.0}); - for(int i = 0; i < 9; ++i) // Skip the last parameter, which is equal to i=0 + for(int i = 6; i < 7; ++i) // Skip the last parameter, which is equal to i=0 { + std::cout << "Testing with parameter " << i << " " << params[i] << std::endl; + VectorType ray_direction(ray_origin, circle.evaluate(params[i])); RayType ray(ray_origin, ray_direction); From 3ebe85d316798c1d06a329c88ab2c5ffefa1b696 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Sun, 8 Dec 2024 23:15:03 -0700 Subject: [PATCH 29/47] Fix style --- src/axom/primal/operators/detail/intersect_bezier_impl.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index dbbeedfda5..d2e7a4ba12 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -299,7 +299,7 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear bool isIntersect = intersect(r, seg, r0, s0, 0.0); - SLIC_INFO( "isIntersect: " << isIntersect << " r0: " << r0 << " s0: " << s0); + SLIC_INFO("isIntersect: " << isIntersect << " r0: " << r0 << " s0: " << s0); if(intersect(r, seg, r0, s0, 0.0) && s0 < 1.0 - EPS) { rp.push_back(r0); From d7311751cfdfd6a332cff0d68720478e64686253 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 9 Dec 2024 09:27:31 -0700 Subject: [PATCH 30/47] Fix tolerances on half-open curves --- src/axom/primal/operators/detail/intersect_bezier_impl.hpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index d2e7a4ba12..8866351dd6 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -298,9 +298,7 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear - bool isIntersect = intersect(r, seg, r0, s0, 0.0); - SLIC_INFO("isIntersect: " << isIntersect << " r0: " << r0 << " s0: " << s0); - if(intersect(r, seg, r0, s0, 0.0) && s0 < 1.0 - EPS) + if(intersect(r, seg, r0, s0, 0.0) && s0 > 0.0 - EPS && s0 < 1.0 - EPS) { rp.push_back(r0); cp.push_back(c_offset + c_scale * s0); From bb1a9b61cd0dc88f2ed4115af6d146e38b15e8ae Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 9 Dec 2024 09:28:55 -0700 Subject: [PATCH 31/47] Restore debugging test to full version --- src/axom/primal/tests/primal_bezier_intersect.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index 62bed5d2d1..fada57d950 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -955,10 +955,8 @@ TEST(primal_bezier_inter, ray_nurbs_intersections) axom::numerics::linspace(-1.0, 2.0, params, 10); PointType ray_origin({0.0, 0.0}); - for(int i = 6; i < 7; ++i) // Skip the last parameter, which is equal to i=0 + for(int i = 0; i < 9; ++i) // Skip the last parameter, which is equal to i=0 { - std::cout << "Testing with parameter " << i << " " << params[i] << std::endl; - VectorType ray_direction(ray_origin, circle.evaluate(params[i])); RayType ray(ray_origin, ray_direction); From 47a865cd354b20e2a02cefe32924a3c97227489b Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 9 Dec 2024 19:16:02 -0700 Subject: [PATCH 32/47] Rework ray-segment intersection logic --- .../operators/detail/intersect_impl.hpp | 88 ++++++++++++++----- .../operators/detail/intersect_ray_impl.hpp | 48 ++++++++-- 2 files changed, 105 insertions(+), 31 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index 60ffdc20f9..88b56dada1 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -1806,25 +1806,47 @@ inline bool intersect_line_bilinear_patch(const Line& line, // Parameters of intersection are non-unique, // so take the center of the segment the intersection // (this avoids any inclusion issues at the boundary) - double t1 = Vector3::dot_product(pa, line.direction()); - double t2 = Vector3::dot_product(pa + pb, line.direction()); - if(t1 * t2 < 0) + const double t1 = Vector3::dot_product(pa, line.direction()); + const double t2 = Vector3::dot_product(pa + pb, line.direction()); + + if(!isRay || std::min(t1, t2) > 0.0) { - // Means the origin is inside the segment - t.push_back(0.5 * ((isRay ? 0.0 : t1) + t2)); + // Always an intersection in this case + t.push_back(0.5 * (t1 + t2)); + v.push_back(0.5); u.push_back(u0); - v.push_back(isRay ? (t1 - 0.5 * t2) / (t1 - t2) : 0.5); + return true; } - else if(t1 >= 0 || !isRay) + else if(t1 * t2 <= 0) { - // The origin is outside the segment, but the ray intersects - // (the line always intersects in this case) - t.push_back(0.5 * (t1 + t2)); + // Means the origin is inside the segment + + // Switch based on orientation of segment + if(t1 == t2) + { + t.push_back(0.5 * t1); + v.push_back(0.5); + } + else if(t1 < t2) + { + t.push_back(0.5 * t2); + v.push_back((t1 - 0.5 * t2) / (t1 - t2)); + } + else + { + t.push_back(0.5 * t1); + v.push_back(-0.5 * t1 / (t2 - t1)); + } u.push_back(u0); - v.push_back(0.5); + return true; } + else + { + // No intersection + return false; + } } } } @@ -1916,25 +1938,47 @@ inline bool intersect_line_bilinear_patch(const Line& line, // Parameters of intersection are non-unique, // so take the center of the segment the intersection // (this avoids any inclusion issues at the boundary) - double t1 = Vector3::dot_product(pa, line.direction()); - double t2 = Vector3::dot_product(pa + pb, line.direction()); - if(t1 * t2 < 0) + const double t1 = Vector3::dot_product(pa, line.direction()); + const double t2 = Vector3::dot_product(pa + pb, line.direction()); + + if(!isRay || std::min(t1, t2) > 0.0) { - // Means the origin is inside the segment - t.push_back(0.5 * ((isRay ? 0.0 : t1) + t2)); - u.push_back(isRay ? (t1 - 0.5 * t2) / (t1 - t2) : 0.5); + // Always an intersection in this case + t.push_back(0.5 * (t1 + t2)); + u.push_back(0.5); v.push_back(v0); + return true; } - else if(t1 >= 0.0 || !isRay) + else if(t1 * t2 <= 0) { - // The origin is outside the segment, but the ray intersects - // (the line always intersects in this case) - t.push_back(0.5 * (t1 + t2)); - u.push_back(0.5); + // Means the origin is inside the segment + + // Switch based on orientation of segment + if(t1 == t2) + { + t.push_back(0.5 * t1); + v.push_back(0.5); + } + else if(t1 < t2) + { + t.push_back(0.5 * t2); + u.push_back((t1 - 0.5 * t2) / (t1 - t2)); + } + else + { + t.push_back(0.5 * t1); + u.push_back(-0.5 * t1 / (t2 - t1)); + } v.push_back(v0); + return true; } + else + { + // No intersection + return false; + } } } } diff --git a/src/axom/primal/operators/detail/intersect_ray_impl.hpp b/src/axom/primal/operators/detail/intersect_ray_impl.hpp index 70d3ee18a6..9be2575a6f 100644 --- a/src/axom/primal/operators/detail/intersect_ray_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_ray_impl.hpp @@ -31,7 +31,7 @@ namespace detail * \a ray_param returns the parametric coordinate of the intersection point along \a R * and \a seg_param returns the parametric coordinate of the intersection point along \a S. * If the intersection point is nonunique, \a seg_param and \a ray_param return only - * a single point of intersection + * a single point of intersection at the center of the region. * \return status true iff R intersects with S, otherwise, false. */ template @@ -68,14 +68,44 @@ inline bool intersect_ray(const primal::Ray& R, if(axom::utilities::isNearlyEqual(cross, 0.0, EPS)) { // Check orientation of segment relative to ray - const double t0 = col[0] * ray_dir[0] + col[1] * ray_dir[1]; - const double t1 = t0 + seg_dir.dot(ray_dir); - - // Assign (nonunique) parameters - ray_param = (t1 > t0) ? t1 : t0; - seg_param = (t1 > t0) ? 1.0 : 0.0; - - return ((ray_param >= tlow) && (seg_param >= tlow) && (seg_param <= thigh)); + const double t1 = col[0] * ray_dir[0] + col[1] * ray_dir[1]; + const double t2 = t1 + seg_dir.dot(ray_dir); + + if(std::min(t1, t2) > 0.0) + { + // The origin is outside the segment, + // but the ray intersects + ray_param = 0.5 * (t1 + t2); + seg_param = 0.5; + } + else if(t1 * t2 <= 0) + { + // Means the origin is inside the segment + + // Switch based on orientation of segment + if(t1 == t2) + { + ray_param = 0.5 * t1; + seg_param = 0.5; + } + else if(t1 < t2) + { + ray_param = 0.5 * t2; + seg_param = (t1 - 0.5 * t2) / (t1 - t2); + } + else + { + ray_param = 0.5 * t1; + seg_param = -0.5 * t1 / (t2 - t1); + } + } + else + { + // No intersection + return false; + } + + return (ray_param >= tlow); } else { // Not collinear, no intersection From b2bcd64029402c4fb862109ebf4c7d3e590a7210 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 9 Dec 2024 19:16:23 -0700 Subject: [PATCH 33/47] Add new tests for new edge cases --- src/axom/primal/tests/primal_ray_intersect.cpp | 15 ++++++++------- .../primal/tests/primal_surface_intersect.cpp | 7 ++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/axom/primal/tests/primal_ray_intersect.cpp b/src/axom/primal/tests/primal_ray_intersect.cpp index c63ea2d2ed..42ae51d118 100644 --- a/src/axom/primal/tests/primal_ray_intersect.cpp +++ b/src/axom/primal/tests/primal_ray_intersect.cpp @@ -182,7 +182,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test TOP (+z) @@ -207,7 +207,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test LEFT (-y) @@ -232,7 +232,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test RIGHT (+y) @@ -257,7 +257,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test BACK (-x) @@ -282,7 +282,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test FRONT (+x) @@ -307,7 +307,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // Test a bunch of rays emitted from the box center @@ -408,7 +408,8 @@ TEST(primal_ray_intersect, ray_segment_edge_cases) SegmentType(PointType({2, 4.1}), PointType({1, 2.1})), SegmentType(PointType({1, 2.1}), PointType({0, 0.1})), SegmentType(PointType({1, 2.1}), PointType({-1, -1.9})), - SegmentType(PointType({0, 0.1}), PointType({-1, -1.9}))}; + SegmentType(PointType({0, 0.1}), PointType({-1, -1.9})), + SegmentType(PointType({1, 2.1}), PointType({1, 2.1}))}; for(auto& seg : intersecting_segs) { diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp index 2da9b0f5c3..3266d52759 100644 --- a/src/axom/primal/tests/primal_surface_intersect.cpp +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -193,12 +193,17 @@ TEST(primal_surface_inter, bilinear_intersect) checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); - // Ray with infinitely many intersections, but the origin is on the patch + // Rays with infinitely many intersections, but the origin is on the patch ray_origin = PointType({0.4, 0.0, 1.5}); ray_direction = VectorType({1.0, 0.0, 0.0}); ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {0.3}, {0.5}, {0.85}, eps, eps_test); + + ray_direction = VectorType({-1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + + checkIntersections(ray, bilinear_patch, {0.7}, {0.5}, {0.35}, eps, eps_test); } //------------------------------------------------------------------------------ From 26ece1568c1a8c0367bafd8f80a89ec92f52ad79 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 9 Dec 2024 19:17:45 -0700 Subject: [PATCH 34/47] Revert to old tolerance --- src/axom/primal/operators/detail/intersect_bezier_impl.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index 8866351dd6..b9dd03a4c0 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -298,7 +298,7 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear - if(intersect(r, seg, r0, s0, 0.0) && s0 > 0.0 - EPS && s0 < 1.0 - EPS) + if(intersect(r, seg, r0, s0, EPS) && s0 > 0.0 - EPS && s0 < 1.0 - EPS) { rp.push_back(r0); cp.push_back(c_offset + c_scale * s0); From 3dda94b05298d1db337387ce50c34e0be358d12a Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 9 Dec 2024 19:31:20 -0700 Subject: [PATCH 35/47] Check the style again --- src/axom/primal/tests/primal_ray_intersect.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/axom/primal/tests/primal_ray_intersect.cpp b/src/axom/primal/tests/primal_ray_intersect.cpp index 42ae51d118..42448a17bf 100644 --- a/src/axom/primal/tests/primal_ray_intersect.cpp +++ b/src/axom/primal/tests/primal_ray_intersect.cpp @@ -182,7 +182,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test TOP (+z) @@ -207,7 +207,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test LEFT (-y) @@ -232,7 +232,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test RIGHT (+y) @@ -257,7 +257,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test BACK (-x) @@ -282,7 +282,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // test FRONT (+x) @@ -307,7 +307,7 @@ TEST(primal_ray_intersect, ray_aabb_intersection_3D) EXPECT_FALSE( intersect_ray(sx, -nx, sy, -ny, sz, -nz, TEST_BOX3D, tmin, tmax)); } // END for all j - } // END for all i + } // END for all i } // Test a bunch of rays emitted from the box center From 543a609ebba701a0179c2042690f46dd2169c24a Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 9 Dec 2024 22:35:08 -0700 Subject: [PATCH 36/47] Fuss with some numerical tolerances --- src/axom/primal/operators/detail/intersect_bezier_impl.hpp | 2 +- src/axom/primal/tests/primal_bezier_intersect.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index b9dd03a4c0..8652dae79b 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -298,7 +298,7 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear - if(intersect(r, seg, r0, s0, EPS) && s0 > 0.0 - EPS && s0 < 1.0 - EPS) + if(intersect(r, seg, r0, s0, primal::PRIMAL_TINY) && s0 > 0.0 - EPS && s0 < 1.0 - EPS) { rp.push_back(r0); cp.push_back(c_offset + c_scale * s0); diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index fada57d950..8b81047608 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -960,7 +960,7 @@ TEST(primal_bezier_inter, ray_nurbs_intersections) VectorType ray_direction(ray_origin, circle.evaluate(params[i])); RayType ray(ray_origin, ray_direction); - checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test, true); + checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test); } } From d9cb8d59009308e52f03ff01966e55351c33336a Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 9 Dec 2024 23:38:12 -0700 Subject: [PATCH 37/47] Fix style --- src/axom/primal/operators/detail/intersect_bezier_impl.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index 8652dae79b..a73f58dc8e 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -298,7 +298,8 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear - if(intersect(r, seg, r0, s0, primal::PRIMAL_TINY) && s0 > 0.0 - EPS && s0 < 1.0 - EPS) + if(intersect(r, seg, r0, s0, primal::PRIMAL_TINY) && s0 > 0.0 - EPS && + s0 < 1.0 - EPS) { rp.push_back(r0); cp.push_back(c_offset + c_scale * s0); From fc7e752a9c22b53d4f0a43227c1579f7a92f423d Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 11 Dec 2024 13:22:10 -0700 Subject: [PATCH 38/47] Add back more debugging statements --- .../primal/operators/detail/intersect_bezier_impl.hpp | 5 ++++- .../primal/operators/detail/intersect_ray_impl.hpp | 11 +++++++---- src/axom/primal/tests/primal_bezier_intersect.cpp | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index a73f58dc8e..4e1d184d09 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -298,7 +298,10 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear - if(intersect(r, seg, r0, s0, primal::PRIMAL_TINY) && s0 > 0.0 - EPS && + bool intersects = intersect(r, seg, r0, s0, 0.0); + SLIC_INFO( c ); + SLIC_INFO( "\tintersects: " << intersects << " " << r0 << " " << s0 ); + if(intersects && s0 > 0.0 - EPS && s0 < 1.0 - EPS) { rp.push_back(r0); diff --git a/src/axom/primal/operators/detail/intersect_ray_impl.hpp b/src/axom/primal/operators/detail/intersect_ray_impl.hpp index 9be2575a6f..ddf91600b5 100644 --- a/src/axom/primal/operators/detail/intersect_ray_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_ray_impl.hpp @@ -56,16 +56,19 @@ inline bool intersect_ray(const primal::Ray& R, const double denom = numerics::determinant(ray_dir[0], -seg_dir[0], ray_dir[1], -seg_dir[1]); - // If denom is (nearly) zero (within tolerance EPS), the ray and segment are parallel + // If denom is (nearly) zero (within a numerical tolerance), the ray and segment are parallel if(axom::utilities::isNearlyEqual(denom, 0.0, EPS)) { // Check if ray and segment are collinear - const auto col = S.source().array() - R.origin().array(); + const primal::Vector col(R.origin(), S.source()); + const double col_norm = col.norm(); const double cross = numerics::determinant(col[0], ray_dir[0], col[1], ray_dir[1]); - if(axom::utilities::isNearlyEqual(cross, 0.0, EPS)) + // Normalize the cross product by the norm of col + if(axom::utilities::isNearlyEqual(col_norm, 0.0, primal::PRIMAL_TINY) || + axom::utilities::isNearlyEqual(cross / col_norm, 0.0, EPS)) { // Check orientation of segment relative to ray const double t1 = col[0] * ray_dir[0] + col[1] * ray_dir[1]; @@ -105,7 +108,7 @@ inline bool intersect_ray(const primal::Ray& R, return false; } - return (ray_param >= tlow); + return ((ray_param >= tlow) && (seg_param >= tlow) && (seg_param <= thigh)); } else { // Not collinear, no intersection diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index 8b81047608..fada57d950 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -960,7 +960,7 @@ TEST(primal_bezier_inter, ray_nurbs_intersections) VectorType ray_direction(ray_origin, circle.evaluate(params[i])); RayType ray(ray_origin, ray_direction); - checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test); + checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test, true); } } From ccded5e11dadc16fa79fd1ed50682551f8a2d04f Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Thu, 12 Dec 2024 14:08:49 -0700 Subject: [PATCH 39/47] Make tests more consistent --- .../primal/operators/detail/intersect_bezier_impl.hpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index 4e1d184d09..ed758dd2e8 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -298,11 +298,12 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear - bool intersects = intersect(r, seg, r0, s0, 0.0); - SLIC_INFO( c ); - SLIC_INFO( "\tintersects: " << intersects << " " << r0 << " " << s0 ); - if(intersects && s0 > 0.0 - EPS && - s0 < 1.0 - EPS) + intersect(r, seg, r0, s0, 0.0); + SLIC_INFO(c); + SLIC_INFO("\tintersects: " + << (r0 > 0.0 - EPS && s0 > 0.0 - EPS && s0 < 1.0 - EPS) << " " + << r0 << " " << s0); + if(r0 > 0.0 - EPS && s0 > 0.0 - EPS && s0 < 1.0 - EPS) { rp.push_back(r0); cp.push_back(c_offset + c_scale * s0); From d2009786416d66ddf760f86b23ee84124615ad54 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Mon, 16 Dec 2024 15:09:47 -0700 Subject: [PATCH 40/47] Fixed a typo! --- src/axom/primal/operators/detail/intersect_patch_impl.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axom/primal/operators/detail/intersect_patch_impl.hpp b/src/axom/primal/operators/detail/intersect_patch_impl.hpp index 3602ff1820..e0c36e3817 100644 --- a/src/axom/primal/operators/detail/intersect_patch_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_patch_impl.hpp @@ -55,7 +55,7 @@ namespace detail * bilinear, where we directly find their intersections. Otherwise, * check for intersections recursively after bisecting the patch in each direction. * - * \note This detial function returns all found intersections within EPS of parameter space, + * \note This detail function returns all found intersections within EPS of parameter space, * including identical intersections reported by each subdivision. * The calling `intersect` routine should remove duplicates and enforce half-open behavior. * From 3ea874eaaf2013fc78efded72e2495cd0fc52d2d Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Tue, 17 Dec 2024 23:52:14 -0700 Subject: [PATCH 41/47] Fixed some comments/descriptions --- src/axom/primal/geometry/BezierPatch.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/axom/primal/geometry/BezierPatch.hpp b/src/axom/primal/geometry/BezierPatch.hpp index e530a02a77..6c4e07a3b9 100644 --- a/src/axom/primal/geometry/BezierPatch.hpp +++ b/src/axom/primal/geometry/BezierPatch.hpp @@ -1869,7 +1869,7 @@ class BezierPatch * * \param [in] sq_tol Threshold for sum of squared distances * \param [in] EPS Threshold for nearness to zero - * \return True if c1 is near-planar + * \return True if c1 is planar up to tolerance \a sq_tol */ bool isPlanar(double sq_tol = 1E-8, double EPS = 1e-8) const { @@ -1933,11 +1933,11 @@ class BezierPatch * \brief Predicate to check if the patch can be approximated by a polygon * * This function checks if a BezierPatch lies in a plane - * and that the edged are linear up to tolerance `sq_tol` + * and that the edges are linear up to tolerance `sq_tol` * * \param [in] tol Threshold for sum of squared distances * \param [in] EPS Threshold for nearness to zero - * \return True if c1 is near-planar-polygonal + * \return True if c1 is planar-polygonal up to tolerance \a sq_tol */ bool isPolygonal(double sq_tol = 1E-8, double EPS = 1e-8) const { @@ -1993,7 +1993,7 @@ class BezierPatch * bilinear patch defined by its corners evaluated at uniform parameter values. * * * \param [in] sq_tol Threshold for absolute squared distances - * \return True if patch is bilinear + * \return True if patch is bilinear up to tolerance \a sq_tol */ bool isBilinear(double sq_tol = 1e-8) const { From 4bac34482b287c40a74ac20dc077f23f74929344 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Tue, 17 Dec 2024 23:59:17 -0700 Subject: [PATCH 42/47] Remove debug statements --- .../primal/operators/detail/intersect_bezier_impl.hpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index ed758dd2e8..7661029d4c 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -114,8 +114,8 @@ bool intersect_2d_linear(const Point &a, * \param [in] sq_tol The squared tolerance parameter for distances in physical space * \param [in] EPS The tolerance parameter for distances in parameter space * \param [in] order The order of \a c - * \param s_offset The offset in parameter space for \a c - * \param s_scale The scale in parameter space for \a c + * \param c_offset The offset in parameter space for \a c + * \param c_scale The scale in parameter space for \a c * * A ray can only intersect a Bezier curve if it intersects its bounding box * The base case of the recursion is when we can approximate the curves with @@ -299,10 +299,6 @@ bool intersect_ray_bezier(const Ray &r, // Need to check intersection with zero tolerance // to handle cases where `intersect` treats the ray as collinear intersect(r, seg, r0, s0, 0.0); - SLIC_INFO(c); - SLIC_INFO("\tintersects: " - << (r0 > 0.0 - EPS && s0 > 0.0 - EPS && s0 < 1.0 - EPS) << " " - << r0 << " " << s0); if(r0 > 0.0 - EPS && s0 > 0.0 - EPS && s0 < 1.0 - EPS) { rp.push_back(r0); From 7161682d7740a419ae2929a754e20371b933a983 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 18 Dec 2024 00:59:27 -0700 Subject: [PATCH 43/47] Remove unnecessary variables, make parameter checks consistent. --- .../operators/detail/intersect_impl.hpp | 37 ++++++++++--------- .../operators/detail/intersect_patch_impl.hpp | 31 +++++++--------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_impl.hpp b/src/axom/primal/operators/detail/intersect_impl.hpp index 88b56dada1..1235224a10 100644 --- a/src/axom/primal/operators/detail/intersect_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_impl.hpp @@ -1689,21 +1689,25 @@ AXOM_HOST_DEVICE bool intersect_plane_tet3d(const Plane& p, } /*! \brief Determines if a line intersects a bilinear patch. + * \param [in] line The line to intersect with the bilinear patch. * \param [in] p0 The first corner of the bilinear patch. * \param [in] p1 The second corner in ccw order. * \param [in] p2 The third corner. * \param [in] p3 The fourth corner. - * \param [in] line The line to intersect with the bilinear patch. - * \param [out] u The u parameter(s) of the intersection point. - * \param [out] v The v parameter(s) of the intersection point. - * \param [out] t The t parameter(s) of the intersection point. + * \param [out] t The t parameter(s) of the intersection point wrt the ray. + * \param [out] u The u parameter(s) of the intersection point wrt the patch. + * \param [out] v The v parameter(s) of the intersection point wrt the patch. + * \param [in] EPS The parameter space tolerance for intersection. * \param [in] isRay If true, only return intersections with t >= 0. * * Implements GARP algorithm from Chapter 8 of Ray Tracing Gems (2019) * - * \note A bilinear patch is parameterized in [0, 1) x [0, 1) + * \note A bilinear patch is parameterized in [0 - EPS, 1 + EPS]^2 * - * \warning Always returns false if the line is coplanar to a planar polygon + * \note There can be either 0, 1, 2 discrete intersections, or a continuous segment + * of intersections, in which case the method returns the ceneter of this segment as one point. + * + * \note Always returns false if the line is coplanar to a planar polygon * * \return true iff the line intersects the bilinear patch, otherwise false. */ @@ -1719,16 +1723,13 @@ inline bool intersect_line_bilinear_patch(const Line& line, double EPS = 1e-8, bool isRay = false) { - Vector3 q00(p00), q10(p10), q11(p11), q01(p01); - - Vector3 e10 = q10 - q00; - Vector3 e11 = q11 - q10; - Vector3 e00 = q01 - q00; + Vector3 e10(p00, p10); + Vector3 e11(p10, p11); + Vector3 e00(p00, p01); - Vector3 qn = Vector3::cross_product(e10, q01 - q11); + Vector3 qn = Vector3::cross_product(e10, Vector3(p11, p01)); - q00.array() -= line.origin().array(); - q10.array() -= line.origin().array(); + Vector3 q00(line.origin(), p00), q10(line.origin(), p10); // Solve a quadratic to find the parameters u0 of the B(u0, v) isocurves // that are closest to the line @@ -1774,7 +1775,7 @@ inline bool intersect_line_bilinear_patch(const Line& line, // Find the point on the isocurve that is closest to the ray for(auto u0 : {u1, u2}) { - if(u0 < -EPS || u0 >= 1.0 + EPS) continue; + if(u0 < -EPS || u0 > 1.0 + EPS) continue; Vector3 pa = (1 - u0) * q00 + u0 * q10; Vector3 pb = (1 - u0) * e00 + u0 * e11; // actually stores pb - pa @@ -1854,9 +1855,9 @@ inline bool intersect_line_bilinear_patch(const Line& line, else { // Switch to finding B(u, v0) isocurves instead - Vector3 e01 = q11 - q01; + Vector3 e01(p01, p11); Vector3 qm = Vector3::cross_product(e00, -e11); - q01.array() -= line.origin().array(); + Vector3 q01(line.origin(), p01); // Find the analogous coefficients for B(u, v0) isocurves double av = Vector3::scalar_triple_product(q00, line.direction(), e10); @@ -1905,7 +1906,7 @@ inline bool intersect_line_bilinear_patch(const Line& line, // Find the point on the isocurve that is closest to the ray for(auto v0 : {v1, v2}) { - if(v0 < -EPS || v0 >= 1.0 + EPS) continue; + if(v0 < -EPS || v0 > 1.0 + EPS) continue; Vector3 pa = (1.0 - v0) * q00 + v0 * q01; Vector3 pb = (1.0 - v0) * e10 + v0 * e01; // actually stores pb - pa diff --git a/src/axom/primal/operators/detail/intersect_patch_impl.hpp b/src/axom/primal/operators/detail/intersect_patch_impl.hpp index e0c36e3817..61278331f1 100644 --- a/src/axom/primal/operators/detail/intersect_patch_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_patch_impl.hpp @@ -105,7 +105,7 @@ bool intersect_line_patch(const Line &line, // Need to expand the box a bit so that intersections near subdivision boundaries // are accurately recorded Point ip; - if(!intersect(line, patch.boundingBox().scale(1.5), ip)) + if(!intersect(line, patch.boundingBox().expand(sq_tol), ip)) { return false; } @@ -116,23 +116,18 @@ bool intersect_line_patch(const Line &line, // Store candidate intersection points axom::Array tc, uc, vc; - foundIntersection = - detail::intersect_line_bilinear_patch(line, - patch(0, 0), - patch(order_u, 0), - patch(order_u, order_v), - patch(0, order_v), - tc, - uc, - vc, - EPS, - isRay); - - if(!foundIntersection) - { - return false; - } - + detail::intersect_line_bilinear_patch(line, + patch(0, 0), + patch(order_u, 0), + patch(order_u, order_v), + patch(0, order_v), + tc, + uc, + vc, + EPS, + isRay); + + // Check intersections based on subdivision-scaled tolerances foundIntersection = false; for(int i = 0; i < tc.size(); ++i) { From 3392880a403ccae3439d24f93ce9c0bbd15a9f25 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 18 Dec 2024 01:32:07 -0700 Subject: [PATCH 44/47] Add more consistent handling of line-/ray-bb routines --- .../detail/intersect_bezier_impl.hpp | 3 +- .../operators/detail/intersect_patch_impl.hpp | 3 +- src/axom/primal/operators/intersect.hpp | 73 ++++++++++++++++++- .../primal/tests/primal_bezier_intersect.cpp | 2 +- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp index 7661029d4c..979f3a8bb4 100644 --- a/src/axom/primal/operators/detail/intersect_bezier_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_bezier_impl.hpp @@ -279,12 +279,11 @@ bool intersect_ray_bezier(const Ray &r, // Check bounding box to short-circuit the intersection T r0, s0; - Point ip; constexpr T factor = 1e-8; // Need to expand the bounding box, since this ray-bb intersection routine // only parameterizes the ray on (0, inf) - if(!intersect(r, c.boundingBox().expand(factor), ip)) + if(!intersect(r, c.boundingBox().expand(factor))) { return false; } diff --git a/src/axom/primal/operators/detail/intersect_patch_impl.hpp b/src/axom/primal/operators/detail/intersect_patch_impl.hpp index 61278331f1..65c23020ab 100644 --- a/src/axom/primal/operators/detail/intersect_patch_impl.hpp +++ b/src/axom/primal/operators/detail/intersect_patch_impl.hpp @@ -104,8 +104,7 @@ bool intersect_line_patch(const Line &line, // Check bounding box to short-circuit the intersection // Need to expand the box a bit so that intersections near subdivision boundaries // are accurately recorded - Point ip; - if(!intersect(line, patch.boundingBox().expand(sq_tol), ip)) + if(!intersect(line, patch.boundingBox().expand(sq_tol))) { return false; } diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index a80d0508f3..0e37af68a5 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -309,7 +309,7 @@ bool intersect(const Ray& R, * \param [in] R the specified ray * \param [in] bb the user-supplied axis-aligned bounding box * - * \param [out] ip the intersection point where R intersects bb. + * \param [out] ip the intersection point with minimum parameter value where R intersects bb. * * \return status true iff bb intersects with R, otherwise, false. * @@ -328,13 +328,46 @@ AXOM_HOST_DEVICE bool intersect(const Ray& R, return detail::intersect_ray(R, bb, ip); } +/*! + * \brief Computes the intersection of the given ray, R, with the Box, bb. + * + * \param [in] R the specified ray + * \param [in] bb the user-supplied axis-aligned bounding box + * + * \return status true iff bb intersects with R, otherwise, false. + * + * \see primal::Ray + * \see primal::Segment + * \see primal::BoundingBox + * + * \note Computes Ray Box intersection using the slab method from pg 180 of + * Real Time Collision Detection by Christer Ericson. + */ +template +AXOM_HOST_DEVICE bool intersect(const Ray& R, + const BoundingBox& bb) +{ + AXOM_STATIC_ASSERT(std::is_floating_point::value); + + T tmin = axom::numerics::floating_point_limits::min(); + T tmax = axom::numerics::floating_point_limits::max(); + + const T EPS = numerics::floating_point_limits::epsilon(); + + return detail::intersect_ray(Ray(L.origin(), L.direction()), + bb, + tmin, + tmax, + EPS); +} + /*! * \brief Computes the intersection of the given line, L, with the Box, bb. * * \param [in] L the specified line (two-sided ray) * \param [in] bb the user-supplied axis-aligned bounding box * - * \param [out] ip the intersection point where L intersects bb. + * \param [out] ip the intersection point with minimum parameter value where L intersects bb. * * \return status true iff bb intersects with R, otherwise, false. * @@ -352,6 +385,38 @@ AXOM_HOST_DEVICE bool intersect(const Line& L, { return detail::intersect_line(L, bb, ip); } + +/*! + * \brief Computes the intersection of the given line, L, with the Box, bb. + * + * \param [in] L the specified line (two-sided ray) + * \param [in] bb the user-supplied axis-aligned bounding box + * + * \return status true iff bb intersects with R, otherwise, false. + * + * \see primal::Line + * \see primal::BoundingBox + * + * \note Computes Ray Box intersection using the slab method from pg 180 of + * Real Time Collision Detection by Christer Ericson. + */ +template +AXOM_HOST_DEVICE bool intersect(const Line& L, + const BoundingBox& bb) +{ + AXOM_STATIC_ASSERT(std::is_floating_point::value); + + T tmin = -axom::numerics::floating_point_limits::max(); + T tmax = axom::numerics::floating_point_limits::max(); + + const T EPS = numerics::floating_point_limits::epsilon(); + + return detail::intersect_ray(Ray(L.origin(), L.direction()), + bb, + tmin, + tmax, + EPS); +} /// @} /// \name Segment-BoundingBox Intersection Routines @@ -822,6 +887,10 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, // The number of reported intersection points will be small, // so we don't need to fully sort the list + SLIC_WARNING_IF(tc.size() > 10, + "Large number of intersections detected, eliminating " + "duplicates may be slow"); + for(int i = 0; i < tc.size(); ++i) { // Also remove any intersections on the half-interval boundaries diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index fada57d950..8b81047608 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -960,7 +960,7 @@ TEST(primal_bezier_inter, ray_nurbs_intersections) VectorType ray_direction(ray_origin, circle.evaluate(params[i])); RayType ray(ray_origin, ray_direction); - checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test, true); + checkIntersectionsRay(ray, circle, {1.0}, {params[i]}, eps, eps_test); } } From 849930630b4485daf579dfd40e0fd81a1c11b299 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 18 Dec 2024 01:40:27 -0700 Subject: [PATCH 45/47] Fix descriptions --- src/axom/primal/operators/intersect.hpp | 17 +++++++---------- .../primal/tests/primal_bezier_intersect.cpp | 4 ++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/axom/primal/operators/intersect.hpp b/src/axom/primal/operators/intersect.hpp index 0e37af68a5..aed487ee31 100644 --- a/src/axom/primal/operators/intersect.hpp +++ b/src/axom/primal/operators/intersect.hpp @@ -354,11 +354,7 @@ AXOM_HOST_DEVICE bool intersect(const Ray& R, const T EPS = numerics::floating_point_limits::epsilon(); - return detail::intersect_ray(Ray(L.origin(), L.direction()), - bb, - tmin, - tmax, - EPS); + return detail::intersect_ray(R, bb, tmin, tmax, EPS); } /*! @@ -714,7 +710,8 @@ bool intersect(const Ray& r, for(int j = 0; j < rc.size(); ++j) { rp.push_back(rc[j]); - np.push_back(knot_vals[i] + nc[j] * (knot_vals[i + 1] - knot_vals[i])); + np.push_back(axom::utilities::lerp(knot_vals[i], knot_vals[i + 1], nc[j])); + // knot_vals[i] + nc[j] * (knot_vals[i + 1] - knot_vals[i])); } } @@ -966,10 +963,10 @@ AXOM_HOST_DEVICE bool intersect(const Ray& ray, for(int k = 0; k < tcc.size(); ++k) { tc.push_back(tcc[k]); - uc.push_back(knot_vals_u[i] + - ucc[k] * (knot_vals_u[i + 1] - knot_vals_u[i])); - vc.push_back(knot_vals_v[j] + - vcc[k] * (knot_vals_v[j + 1] - knot_vals_v[j])); + uc.push_back( + axom::utilities::lerp(knot_vals_u[i], knot_vals_u[i + 1], ucc[k])); + vc.push_back( + axom::utilities::lerp(knot_vals_v[j], knot_vals_v[j + 1], vcc[k])); } } } diff --git a/src/axom/primal/tests/primal_bezier_intersect.cpp b/src/axom/primal/tests/primal_bezier_intersect.cpp index 8b81047608..c3991b2aa6 100644 --- a/src/axom/primal/tests/primal_bezier_intersect.cpp +++ b/src/axom/primal/tests/primal_bezier_intersect.cpp @@ -297,7 +297,7 @@ TEST(primal_bezier_inter, no_intersections_bezier) } //------------------------------------------------------------------------------ -TEST(primal_bezier_inter, cubic_quadratic_bezier) +TEST(primal_bezier_inter, cubic_bezier) { static const int DIM = 2; using CoordType = double; @@ -754,7 +754,7 @@ TEST(primal_bezier_inter, ray_linear_bezier_interp_params) } //------------------------------------------------------------------------------ -TEST(primal_bezier_inter, ray_cubic_quadratic_bezier) +TEST(primal_bezier_inter, ray_cubic_bezier) { static const int DIM = 2; using CoordType = double; From 989844da632d651a812db94a160a34de0ca31d72 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 18 Dec 2024 01:51:58 -0700 Subject: [PATCH 46/47] Fix static variable, move a test. --- .../primal/tests/primal_surface_intersect.cpp | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/axom/primal/tests/primal_surface_intersect.cpp b/src/axom/primal/tests/primal_surface_intersect.cpp index 3266d52759..1cbead5ad5 100644 --- a/src/axom/primal/tests/primal_surface_intersect.cpp +++ b/src/axom/primal/tests/primal_surface_intersect.cpp @@ -123,7 +123,7 @@ void checkIntersections(const primal::Ray& ray, //------------------------------------------------------------------------------ TEST(primal_surface_inter, bilinear_intersect) { - static const int DIM = 3; + constexpr int DIM = 3; using CoordType = double; using PointType = primal::Point; using VectorType = primal::Vector; @@ -160,12 +160,6 @@ TEST(primal_surface_inter, bilinear_intersect) ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); - // Ray with no intersections, but in a way that is difficult for - // the standard GARP implementation - ray_direction = VectorType({1.0, 0.0, 0.0}); - ray = RayType(ray_origin, ray_direction); - checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); - // Ray with two intersections ray_origin = PointType({-1.0, -1.0, 1.75}); ray_direction = VectorType({1.0, 1.0, 0.0}); @@ -209,7 +203,7 @@ TEST(primal_surface_inter, bilinear_intersect) //------------------------------------------------------------------------------ TEST(primal_surface_inter, bilinear_boundary_treatment) { - static const int DIM = 3; + constexpr int DIM = 3; using CoordType = double; using PointType = primal::Point; using VectorType = primal::Vector; @@ -356,7 +350,7 @@ TEST(primal_surface_inter, bilinear_boundary_treatment) //------------------------------------------------------------------------------ TEST(primal_surface_inter, difficult_garp_case) { - static const int DIM = 3; + constexpr int DIM = 3; using CoordType = double; using PointType = primal::Point; using VectorType = primal::Vector; @@ -411,6 +405,11 @@ TEST(primal_surface_inter, difficult_garp_case) ray = RayType(ray_origin, ray_direction); checkIntersections(ray, bilinear_patch, {2.0}, {0.5}, {0.5}, eps, eps_test); + ray_origin = PointType({0.0, 0.0, 1.75}); + ray_direction = VectorType({1.0, 0.0, 0.0}); + ray = RayType(ray_origin, ray_direction); + checkIntersections(ray, bilinear_patch, {}, {}, {}, eps, eps_test); + // Give patch a degeneracy at the point of intersection // at which there are infinitely many parameters of intersection bilinear_patch(1, 1) = PointType({-1.0, -1.0, 2.0}); @@ -437,7 +436,7 @@ TEST(primal_surface_inter, difficult_garp_case) //------------------------------------------------------------------------------ TEST(primal_surface_inter, flat_bilinear_intersect) { - static const int DIM = 3; + constexpr int DIM = 3; using CoordType = double; using PointType = primal::Point; using VectorType = primal::Vector; @@ -503,7 +502,7 @@ TEST(primal_surface_inter, flat_bilinear_intersect) //------------------------------------------------------------------------------ TEST(primal_surface_inter, flat_selfintersect_bilinear_intersect) { - static const int DIM = 3; + constexpr int DIM = 3; using CoordType = double; using PointType = primal::Point; using VectorType = primal::Vector; @@ -566,7 +565,7 @@ TEST(primal_surface_inter, flat_selfintersect_bilinear_intersect) //------------------------------------------------------------------------------ TEST(primal_surface_inter, bezier_surface_intersect) { - static const int DIM = 3; + constexpr int DIM = 3; using CoordType = double; using PointType = primal::Point; using VectorType = primal::Vector; @@ -703,7 +702,7 @@ TEST(primal_surface_inter, bezier_surface_intersect) //------------------------------------------------------------------------------ TEST(primal_surface_inter, NURBS_surface_intersect) { - static const int DIM = 3; + constexpr int DIM = 3; using CoordType = double; using PointType = primal::Point; using VectorType = primal::Vector; From 5758e7a035ffa045720a7452797233e194b8fc38 Mon Sep 17 00:00:00 2001 From: Jacob Spainhour Date: Wed, 18 Dec 2024 01:55:11 -0700 Subject: [PATCH 47/47] Update release notes! --- RELEASE-NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 938309dcb3..32dd3c82d6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -41,6 +41,7 @@ to use Open CASCADE's file I/O capabilities in support of Quest applications. ### Fixed - Fixes compilation issue with RAJA@2024.07 on 32-bit Windows configurations. +- Minor bugfix to `primal::intersect(segment, ray)` to better handle cases when segment and ray overlap. This required a [RAJA fix to avoid 64-bit intrinsics](https://github.com/LLNL/RAJA/pull/1746), as well as support for 32-bit `Word`s in Slam's `BitSet` class.