diff --git a/openvdb/openvdb/CMakeLists.txt b/openvdb/openvdb/CMakeLists.txt index 3db0e4e144..a0b999ab28 100644 --- a/openvdb/openvdb/CMakeLists.txt +++ b/openvdb/openvdb/CMakeLists.txt @@ -473,6 +473,7 @@ set(OPENVDB_LIBRARY_TOOLS_INCLUDE_FILES tools/ChangeBackground.h tools/Clip.h tools/Composite.h + tools/ConvexVoxelizer.h tools/Count.h tools/Dense.h tools/DenseSparseTools.h @@ -484,6 +485,7 @@ set(OPENVDB_LIBRARY_TOOLS_INCLUDE_FILES tools/GridTransformer.h tools/Interpolation.h tools/LevelSetAdvect.h + tools/LevelSetDilatedMesh.h tools/LevelSetFilter.h tools/LevelSetFracture.h tools/LevelSetMeasure.h @@ -492,6 +494,7 @@ set(OPENVDB_LIBRARY_TOOLS_INCLUDE_FILES tools/LevelSetRebuild.h tools/LevelSetSphere.h tools/LevelSetTracker.h + tools/LevelSetTubes.h tools/LevelSetUtil.h tools/Mask.h tools/Merge.h diff --git a/openvdb/openvdb/math/Vec2.h b/openvdb/openvdb/math/Vec2.h index a4abf85a74..fccc7c27fa 100644 --- a/openvdb/openvdb/math/Vec2.h +++ b/openvdb/openvdb/math/Vec2.h @@ -246,7 +246,7 @@ class Vec2: public Tuple<2, T> Vec2 unitSafe() const { T l2 = lengthSqr(); - return l2 ? *this/static_cast(sqrt(l2)) : Vec2(1,0); + return l2 != T(0) ? *this/static_cast(sqrt(l2)) : Vec2(1,0); } /// Multiply each element of this vector by @a scalar. diff --git a/openvdb/openvdb/math/Vec3.h b/openvdb/openvdb/math/Vec3.h index 76759fbbbc..fbcf048612 100644 --- a/openvdb/openvdb/math/Vec3.h +++ b/openvdb/openvdb/math/Vec3.h @@ -392,7 +392,7 @@ class Vec3: public Tuple<3, T> Vec3 unitSafe() const { T l2 = lengthSqr(); - return l2 ? *this / static_cast(sqrt(l2)) : Vec3(1, 0 ,0); + return l2 != T(0) ? *this / static_cast(sqrt(l2)) : Vec3(1, 0 ,0); } // Number of cols, rows, elements diff --git a/openvdb/openvdb/tools/ConvexVoxelizer.h b/openvdb/openvdb/tools/ConvexVoxelizer.h new file mode 100644 index 0000000000..d39c6a4ea2 --- /dev/null +++ b/openvdb/openvdb/tools/ConvexVoxelizer.h @@ -0,0 +1,1360 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: Apache-2.0 +/// +/// @author Greg Hurst +/// +/// @file ConvexVoxelizer.h +/// +/// @brief Base class used to generate the narrow-band level set of a convex region. +/// +/// @note By definition a level set has a fixed narrow band width +/// (the half width is defined by LEVEL_SET_HALF_WIDTH in Types.h), +/// whereas an SDF can have a variable narrow band width. + +#ifndef OPENVDB_TOOLS_CONVEXVOXELIZER_HAS_BEEN_INCLUDED +#define OPENVDB_TOOLS_CONVEXVOXELIZER_HAS_BEEN_INCLUDED + +#include +#include +#include + +#include "Merge.h" +#include "SignedFloodFill.h" + +#include +#include +#include +#include + +#include +#include + +namespace openvdb { +OPENVDB_USE_VERSION_NAMESPACE +namespace OPENVDB_VERSION_NAME { +namespace tools { + +namespace lvlset { + +/// @brief Internal class used by derived @c ConvexVoxelizer classes that make use of PointPartitioner. +template +struct PointArray +{ + using PosType = VectorType; + + PointArray() = default; + + PointArray(const std::vector& vec) + : mVec(vec) + { + } + + inline const VectorType& operator[](const Index& i) { return mVec[i]; } + + inline size_t size() const { return mVec.size(); }; + + inline void getPos(size_t n, VectorType& xyz) const { xyz = mVec[n]; } + +private: + + const std::vector& mVec; + +}; // struct PointArray + +} // namespace lvlset + +/// @brief Base class used to generate a grid of type @c GridType containing a narrow-band level set +/// representation of a convex region. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c Derived is the derived class that implements the base class (curiously recurring template pattern). +/// +/// @par Example of derived level set sphere class +/// @code +/// template +/// class SphereVoxelizer : public ConvexVoxelizer> +/// { +/// using GridPtr = typename GridType::Ptr; +/// +/// using BaseT = ConvexVoxelizer>; +/// using BaseT::mXYData; +/// using BaseT::tileCeil; +/// +/// using ValueT = typename BaseT::ValueT; +/// using Vec3T = typename BaseT::Vec3T; +/// +/// public: +/// +/// friend class ConvexVoxelizer>; +/// +/// SphereVoxelizer(GridPtr& grid, const bool& threaded = true) +/// : BaseT(grid, threaded) +/// { +/// } +/// +/// template +/// void +/// operator()(const math::Vec3& pt, const ScalarT& r) +/// { +/// static_assert(std::is_floating_point::value); +/// +/// if (r <= 0) +/// return; +/// +/// initialize(pt, r); +/// +/// BaseT::iterate(); +/// } +/// +/// private: +/// +/// inline ValueT +/// signedDistance(const Vec3T& p) const +/// { +/// return (p - mPt).length() - mRad; +/// } +/// +/// inline void +/// setXYRangeData(const Index& step = 1) +/// { +/// mXYData.reset(mX - mORad, mX + mORad, step); +/// +/// for (ValueT x = tileCeil(mX - mORad, step); x <= mX + mORad; x += step) +/// mXYData.expandYRange(x, BaseT::circleBottom(mX, mY, mORad, x), +/// BaseT::circleTop(mX, mY, mORad, x)); +/// } +/// +/// std::function sphereBottomTop = +/// [this](ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) +/// { +/// zb = BaseT::sphereBottom(mX, mY, mZ, mORad, x, y); +/// zt = BaseT::sphereTop(mX, mY, mZ, mORad, x, y); +/// +/// return std::isfinite(zb) && std::isfinite(zt); +/// }; +/// +/// template +/// inline void +/// initialize(const math::Vec3& pt, const ScalarT& r) +/// { +/// const ValueT vx = BaseT::voxelSize(), +/// hw = BaseT::halfWidth(); +/// +/// // sphere data in index space +/// mPt = Vec3T(pt)/vx; +/// mRad = ValueT(r)/vx; +/// +/// mX = mPt.x(); mY = mPt.y(); mZ = mPt.z(); +/// +/// // padded radius used to populate the outer halfwidth of the sdf +/// mORad = mRad + hw; +/// +/// BaseT::bottomTop = sphereBottomTop; +/// } +/// +/// Vec3T mPt; +/// ValueT mRad, mORad, mX, mY, mZ; +/// }; +/// +/// // usage: +/// +/// // initialize level set grid with voxel size 0.1 and half width 3.0 +/// FloatGrid::Ptr grid = createLevelSet(0.1f, 3.0f); +/// +/// // populate grid with a sphere centered at (0, 1, 2) and radius 5 +/// SphereVoxelizer op(grid); +/// op(Vec3s(0.0f, 1.0f, 2.0f), 5.0f); +/// +/// @endcode +template +class ConvexVoxelizer +{ + using GridPtr = typename GridType::Ptr; + + using TreeT = typename GridType::TreeType; + using RootT = typename TreeT::RootNodeType; + using LeafT = typename TreeT::LeafNodeType; + + using NodeChainT = typename RootT::NodeChainType; + + using AccessorT = typename GridType::Accessor; + +protected: + + using ValueT = typename GridType::ValueType; + using Vec3T = math::Vec3; + using Vec2T = math::Vec2; + + static_assert(std::is_floating_point::value); + +public: + + /// @brief Constructor + /// + /// @param grid scalar grid to populate the level set in + /// @param threaded center of the sphere in world units + /// @param interrupter pointer to optional interrupter. Use template + /// argument util::NullInterrupter if no interruption is desired. + /// + /// @note The voxel size and half width are determined from the input grid, + /// meaning the voxel size and background value need to be set prior to voxelization + ConvexVoxelizer(GridPtr& grid, const bool& threaded = false, InterruptType* interrupter = nullptr) + : mGrid(grid) + , mVox(ValueT((grid->voxelSize())[0])) + , mHw(ValueT(grid->background()/(grid->voxelSize())[0])) + , mBg(grid->background()) + , mNegBg(-(grid->background())) + , mSerial(!threaded) + , mInterrupter(interrupter) + { + } + + virtual ~ConvexVoxelizer() = default; + + /// @brief Return the voxel size of the grid. + inline ValueT voxelSize() const { return mVox; } + + /// @brief Return the half width of the narrow-band level set. + inline ValueT halfWidth() const { return mHw; } + +private: + + class CacheLastLeafAccessor; + +protected: + + // ------------ Main APIs for derived classes ------------ + + /// @brief The function the derived class calls to create the level set, + /// working in index space other than setting signed distance values. + /// + /// @note This function handles both parallel and serial iterations. If running in serial mode, + /// it flood fills the tile topology immediately; otherwise, it avoids duplicating nontrivial + /// tree topology over multiple threads. This method also checks for background tiles + /// that are too thin to fit and delegates accordingly. + inline void + iterate() + { + static const Index LEAFDIM = LeafT::DIM; + + // objects too thin to have negative background tiles + if (!invokeTileCanFit(LEAFDIM)) { + thinIterate(); + return; + } + + // iterate over all non root nodes + using ChainT = typename NodeChainT::PopBack; + + // if we're working in parallel, we avoid flood filling the tile topology until the end + // this avoids duplicating nontrivial tree topology over multiple threads + if (mSerial) + iterateTile(); + + iterateLeaf(); + + if (!checkInterrupter()) + return; + + if (!mSerial) + tools::signedFloodFill(getTree()); + } + + // ------------ derived classes need to implement these ------------ + + /// @brief Determines the x bounds in index space of the convex region dilated by the half width. + /// For each x value in index space, the y range in index space of the dilated region is computed. + /// This function should store the data in @c mXYData. + /// + /// @param step The step size for setting the XY range data, defaults to 1. + /// @note Function to be implemented by derived classes to set XY range data. + /// This function is called at most 4 times within @c iterate(). + void setXYRangeData(const Index&) {} + + /// @brief Checks if the tile of a given dimension can possibly fit within the region. + /// + /// The derived class does not need to implement it if the default behavior is acceptable, + /// which assumes a tile can always possibly fit. + /// + /// @param dim The dimension of the tile in which to check if the tile fits. + /// @note This is meant as a short-circuting method: if a tile of a given dimension + /// can't fit then @c iterate will not try to populate the level set with background + /// tiles of this dimension. + /// @return true if the tile can possibly fit; otherwise false. + inline bool tileCanFit(const Index&) const { return true; } + + // distance in index space + /// @brief Computes the signed distance from a point to the convex region in index space. + /// + /// @param p The point in 3D space for which to compute the signed distance. + inline ValueT signedDistance(const Vec3T&) const { return ValueT(0); } + + /// @brief Computes the signed distance for tiles in index space, + /// considering the center of the tile. + /// This method is optional to override and defaults to @c signedDistance. + /// + /// @param p The point at the center of the tile in 3D space. + /// @note This can be useful for cases that build objects from multiple primitives, e.g. + /// dilated mesh is built by constructing and unioning _open_ prisms and _open_ tube wedges. + /// A tile might not fully fit in an open prism but might fit in the union of a prism and wedge, + /// and so in this case it might make sense to use the sdf for an offset triangle on tiles + /// during the open prism scan. + inline ValueT + tilePointSignedDistance(const Vec3T& p) const + { + return static_cast(this)->signedDistance(p); + } + + /// @brief Find where a vertical infinite line intersects + /// a convex region dilated by the half width. + /// + /// @param[out] zb Reference to the z ordinate where the bottom intersection occurs. + /// @param[out] zt Reference to the z ordinate where the top intersection occurs. + /// @param[in] x The x ordinate of the infinte line. + /// @param[in] y The y ordinate of the infinte line. + /// @return true if an intersection occurs; otherwise false. + /// @note The derived class can override this lambda to implement different behavior for degenerate cases. + std::function bottomTop = + [](ValueT&, ValueT&, const ValueT&, const ValueT&) { return false; }; + + // ------------ utilities ------------ + + /// @brief Rounds an input scalar up to the nearest valid ordinate of tile of a specified size. + /// @param x Input value. + /// @param step Tile step size. + /// @return The ceiling of the value based on the tile size. + inline static ValueT + tileCeil(const ValueT& x, const ValueT& step) + { + const ValueT offset = ValueT(0.5) * (step - ValueT(1)); + + return step == ValueT(1) + ? static_cast(math::Ceil(perturbDown(x))) + : step * static_cast(math::Ceil(perturbDown((x - offset)/step))) + offset; + } + + /// @brief Rounds an input scalar up to the nearest valid ordinate of tile of a specified size. + /// @tparam T Any integral type (int, unsigned int, size_t, etc.) + /// @param x Input value. + /// @param step Tile step size. + /// @return The ceiling of the value based on the tile size. + template + inline static ValueT + tileCeil(const ValueT& x, const T& step) + { + static_assert(std::is_integral::value, "Index must be an integral type"); + + const ValueT s = static_cast(step); + + return tileCeil(x, s); + } + + /// @brief Rounds an input scalar down to the nearest valid ordinate of tile of a specified size. + /// @param x Input value. + /// @param step Tile step size. + /// @return The ceiling of the value based on the tile size. + inline static ValueT + tileFloor(const ValueT& x, const ValueT& step) + { + const ValueT offset = ValueT(0.5) * (step - ValueT(1)); + + return step == ValueT(1) + ? static_cast(math::Floor(perturbUp(x))) + : step * static_cast(math::Floor(perturbUp((x - offset)/step))) + offset; + } + + /// @brief Rounds an input scalar down to the nearest valid ordinate of tile of a specified size. + /// @tparam T Any integral type (int, unsigned int, size_t, etc.) + /// @param x Input value. + /// @param step Tile step size. + /// @return The ceiling of the value based on the tile size. + template + inline static ValueT + tileFloor(const ValueT& x, const T& step) + { + static_assert(std::is_integral::value, "Index must be an integral type"); + + const ValueT s = static_cast(step); + + return tileFloor(x, s); + } + + /// @brief Computes the bottom y-coordinate of a circle at a given x position. + /// @param x0 X-coordinate of the circle's center. + /// @param y0 Y-coordinate of the circle's center. + /// @param r Radius of the circle. + /// @param x X-coordinate for which to compute the bottom y-coordinate. + /// @return The y-coordinate at the bottom of the circle for the given x position. + inline static ValueT + circleBottom(const ValueT& x0, const ValueT& y0, + const ValueT& r, const ValueT& x) + { + return y0 - math::Sqrt(math::Pow2(r) - math::Pow2(x-x0)); + } + + /// @brief Computes the top y-coordinate of a circle at a given x position. + /// @param x0 X-coordinate of the circle's center. + /// @param y0 Y-coordinate of the circle's center. + /// @param r Radius of the circle. + /// @param x X-coordinate for which to compute the top y-coordinate. + /// @return The y-coordinate at the top of the circle for the given x position. + inline static ValueT + circleTop(const ValueT& x0, const ValueT& y0, + const ValueT& r, const ValueT& x) + { + return y0 + math::Sqrt(math::Pow2(r) - math::Pow2(x-x0)); + } + + /// @brief Computes the bottom z-coordinate of a sphere at a given (x, y) position. + /// @param x0 X-coordinate of the sphere's center. + /// @param y0 Y-coordinate of the sphere's center. + /// @param z0 Z-coordinate of the sphere's center. + /// @param r Radius of the sphere. + /// @param x X-coordinate for which to compute the bottom z-coordinate. + /// @param y Y-coordinate for which to compute the bottom z-coordinate. + /// @return The z-coordinate at the bottom of the sphere for the given (x, y) position. + inline static ValueT + sphereBottom(const ValueT& x0, const ValueT& y0, const ValueT& z0, + const ValueT& r, const ValueT& x, const ValueT& y) + { + return z0 - math::Sqrt(math::Pow2(r) - math::Pow2(x-x0) - math::Pow2(y-y0)); + } + + /// @brief Computes the top z-coordinate of a sphere at a given (x, y) position. + /// @param x0 X-coordinate of the sphere's center. + /// @param y0 Y-coordinate of the sphere's center. + /// @param z0 Z-coordinate of the sphere's center. + /// @param r Radius of the sphere. + /// @param x X-coordinate for which to compute the top z-coordinate. + /// @param y Y-coordinate for which to compute the top z-coordinate. + /// @return The z-coordinate at the top of the sphere for the given (x, y) position. + inline static ValueT + sphereTop(const ValueT& x0, const ValueT& y0, const ValueT& z0, + const ValueT& r, const ValueT& x, const ValueT& y) + { + return z0 + math::Sqrt(math::Pow2(r) - math::Pow2(x-x0) - math::Pow2(y-y0)); + } + + // ------------ nested classes ------------ + + /// @brief Class that stores endpoints of a y range for each x value within a specified range and step size. + /// @details This class tracks y ranges (defined by ymin and ymax) for each x value over a defined interval, + /// using a configurable step size. + /// It allows updating, expanding, and resetting the y ranges, as well as merging data from other instances + /// and trimming invalid entries. + class XYRangeData + { + + public: + + XYRangeData() = default; + + /// @brief Constructor that sets the x range to span a given interval with a specific step size. + /// This initializes all y ranges as empty. + /// @param xmin The lower bound of the x range. + /// @param xmax The upper bound of the x range. + /// @param step The step size between x values. Defaults to 1. + XYRangeData(const ValueT& xmin, const ValueT& xmax, const Index& step = 1) + { + reset(xmin, xmax, step); + } + + /// @brief Expands the y range for a given x value by updating the minimum and maximum + /// y values if the new ones extend the current range. + /// @param x The x value. + /// @param ymin The new minimum y value to compare with and possibly update + /// the current minimum at x. + /// @param ymax The new maximum y value to compare with and possibly update + /// the current maximum at x. + inline void + expandYRange(const ValueT& x, const ValueT& ymin, const ValueT& ymax) + { + expandYMin(x, ymin); + expandYMax(x, ymax); + } + + /// @brief Sets the minimum y value for a given x value, if the provided ymin + /// is smaller than the current value. + /// @param x The x value. + /// @param ymin The minimum y value to possibly be set. + inline void + expandYMin(const ValueT& x, const ValueT& ymin) + { + const Index i = worldToIndex(x); + + if (std::isfinite(ymin) && ymin < mYMins[i]) + mYMins[i] = ymin; + } + + /// @brief Sets the maximum y value for a given x value, if the provided ymax + /// is larger than the current value. + /// @param x The x value. + /// @param ymax The maximum y value to possibly be set. + inline void + expandYMax(const ValueT& x, const ValueT& ymax) + { + const Index i = worldToIndex(x); + + if (std::isfinite(ymax) && ymax > mYMaxs[i]) + mYMaxs[i] = ymax; + } + + /// @brief Expands the y range for a given x by adjusting ymin or ymax if the + /// given y is smaller or larger. + /// @param x The x value. + /// @param y The y value to use for expanding the range. + inline void + expandYRange(const ValueT& x, const ValueT& y) + { + if (std::isfinite(y)) { + const Index i = worldToIndex(x); + + if (y < mYMins[i]) + mYMins[i] = y; + + if (y > mYMaxs[i]) + mYMaxs[i] = y; + } + } + + /// @brief Set the minimum y value for a given x value, + /// even if it is larger than the current value. + /// @param x The x value. + /// @param ymin The minimum y value to reset. + inline void + setYMin(const ValueT& x, const ValueT& ymin) + { + const Index i = worldToIndex(x); + + mYMins[i] = ymin; + } + + /// @brief Set the maximum y value for a given x value, + /// even if it is smaller than the current value. + /// @param x The x value. + /// @param ymax The maximum y value to reset. + inline void + setYMax(const ValueT& x, const ValueT& ymax) + { + const Index i = worldToIndex(x); + + mYMaxs[i] = ymax; + } + + /// @brief Clears the y range for a given x value, setting it to an empty interval. + /// @param x The x value. + inline void + clearYRange(const ValueT& x) + { + const Index i = worldToIndex(x); + + mYMins[i] = MAXVALUE; + mYMaxs[i] = MINVALUE; + } + + /// @brief Resets the x range to span a given interval with a specific step size. + /// This initializes all y ranges as empty. + /// @param xmin The lower bound of the x range. + /// @param xmax The upper bound of the x range. + /// @param step The step size between x values. Defaults to 1. + inline void + reset(const ValueT& xmin, const ValueT& xmax, const Index& step = 1) + { + assert(step != 0); + + mStep = step; + mStepInv = ValueT(1)/static_cast(mStep); + + mXStart = tileCeil(xmin, mStep); + mXEnd = tileFloor(xmax, mStep); + + mSize = 1 + indexDistance(mXEnd, mXStart); + + mYMins.assign(mSize, MAXVALUE); + mYMaxs.assign(mSize, MINVALUE); + } + + /// @brief Retrieves the step size used for the x values. + /// @return The step size. + inline Index step() const { return mStep; } + + /// @brief Returns the number of x points in the current range. + /// @return The size of the x range. + inline Index size() const { return mSize; } + + /// @brief Retrieves the starting x value in the range. + /// @return The start of the x range. + inline ValueT start() const { return mXStart; } + + /// @brief Retrieves the ending x value in the range. + /// @return The end of the x range. + inline ValueT end() const { return mXEnd; } + + /// @brief Converts an index to its corresponding x value. + /// @param i The index value. + /// @return The corresponding x value. + inline ValueT getX(const Index& i) const { return indexToWorld(i); } + + /// @brief Gets the minimum y value for a given index. + /// @param i The index value. + /// @return The minimum y value. + inline ValueT getYMin(const Index& i) const { assert(i < mSize); return mYMins[i]; } + + /// @brief Gets the maximum y value for a given index. + /// @param i The index value. + /// @return The maximum y value. + inline ValueT getYMax(const Index& i) const { assert(i < mSize); return mYMaxs[i]; } + + /// @brief Gets the minimum y value for a given x value. + /// @param x The x value. + /// @return The minimum y value at the given x. + /// @note @c x is rounded to the nearest value in the x range. + inline ValueT getYMin(const ValueT& x) const { return mYMins[worldToIndex(x)]; } + + /// @brief Gets the maximum y value for a given x value. + /// @param x The x value. + /// @return The maximum y value at the given x. + /// @note @c x is rounded to the nearest value in the x range. + inline ValueT getYMax(const ValueT& x) const { return mYMaxs[worldToIndex(x)]; } + + /// @brief Retrieves the x, ymin, and ymax values for a given index. + /// @param x Output parameter for the x value. + /// @param ymin Output parameter for the minimum y value. + /// @param ymax Output parameter for the maximum y value. + /// @param i The index to query. + inline void + XYData(ValueT& x, ValueT& ymin, ValueT& ymax, const Index& i) const + { + x = indexToWorld(i); + ymin = mYMins[i]; + ymax = mYMaxs[i]; + } + + /// @brief Merges another XYRangeData into the current instance by combining y ranges + /// over the overlapping x range. + /// @param xydata The XYRangeData to merge with. + inline void + merge(const XYRangeData& xydata) + { + assert(mStep == xydata.step()); + + const ValueT start = xydata.start(), end = xydata.end(); + + const std::vector& ymins = xydata.mYMins; + const std::vector& ymaxs = xydata.mYMaxs; + + if (start < mXStart) { + const Index n = indexDistance(mXStart, start); + mYMins.insert(mYMins.begin(), n, MAXVALUE); + mYMaxs.insert(mYMaxs.begin(), n, MINVALUE); + mXStart = start; + } + + if (mXEnd < end) { + const Index m = indexDistance(end, mXEnd); + mYMins.insert(mYMins.end(), m, MAXVALUE); + mYMaxs.insert(mYMaxs.end(), m, MINVALUE); + mXEnd = end; + } + + mSize = 1 + indexDistance(mXEnd, mXStart); + + const Index offset = start < mXStart ? 0 : indexDistance(start, mXStart); + for (Index i = 0, j = offset; i < ymins.size(); ++i, ++j) { + if (mYMins[j] > ymins[i]) { mYMins[j] = ymins[i]; } + if (mYMaxs[j] < ymaxs[i]) { mYMaxs[j] = ymaxs[i]; } + } + } + + /// @brief Trims the x range by removing empty or invalid y ranges from the beginning and end. + /// Truncates the range if all values are invalid. + inline void + trim() + { + Index i = 0; + while(i < mSize) { + if (mYMins[i] > mYMaxs[i]) i++; + else break; + } + + if (i == mSize) { + mSize = 0; mXStart = ValueT(0); mXEnd = ValueT(0); + mYMins.clear(); mYMaxs.clear(); + return; + } + + Index j = 0; + while(j < mSize) { + const Index k = mSize - (j + 1); + if (mYMins[k] > mYMaxs[k]) j++; + else break; + } + + if (i == 0 && j == 0) + return; + + mSize -= i + j; + mXStart += ValueT(i * mStep); + mXEnd -= ValueT(j * mStep); + + if (i > 0) { + mYMins.erase(mYMins.begin(), mYMins.begin() + i); + mYMaxs.erase(mYMaxs.begin(), mYMaxs.begin() + i); + } + + if (j > 0) { + mYMins.erase(mYMins.end() - j, mYMins.end()); + mYMaxs.erase(mYMaxs.end() - j, mYMaxs.end()); + } + } + + private: + + inline static const ValueT + MINVALUE = std::numeric_limits::lowest(), + MAXVALUE = std::numeric_limits::max(); + + inline Index + indexDistance(const ValueT& a, const ValueT& b) + { + return Index(math::Round(mStepInv*math::Abs(a - b))); + } + + inline Index + worldToIndex(const ValueT& x) const + { + const Index i = Index(math::Round(mStepInv*(x - mXStart))); + assert(i < mSize); + + return i; + } + + inline ValueT + indexToWorld(const Index i) const + { + assert(i < mSize); + + return mXStart + ValueT(i * mStep); + } + + Index mStep, mSize; + + ValueT mStepInv, mXStart, mXEnd; + + std::vector mYMins, mYMaxs; + + }; // class XYRangeData + + // ------------ protected members ------------ + + XYRangeData mXYData; + +private: + +#define EPS 0.0005f + inline static ValueT perturbDown(const ValueT& x) { return x - ValueT(EPS); } + inline static ValueT perturbUp(const ValueT& x) { return x + ValueT(EPS); } +#undef EPS + + inline static ValueT + voxelCeil(const ValueT& x) + { + return static_cast(math::Ceil(perturbDown(x))); + } + + inline static ValueT + voxelFloor(const ValueT& x) + { + return static_cast(math::Floor(perturbUp(x))); + } + + // skips the need for negative tile population and internal leap frogging + inline void thinIterate() + { + invokeSetXYRangeData(); + + // false means disable internal leap frogging + iterateXYZ(); + } + + template + inline void iterateTile() + { + using NodeT = typename ChainT::Back; + using PoppedChainT = typename ChainT::PopBack; + + static const Index DIM = NodeT::DIM; + + // only attempt to add negative background tiles at this level if they can fit + if (invokeTileCanFit(DIM)) { + invokeSetXYRangeData(DIM); + + tileIterateXYZ(); + } + + if constexpr (!std::is_same_v>) { + iterateTile(); + } + } + + inline void + iterateLeaf() + { + invokeSetXYRangeData(); + + // true means enable internal leap frogging + iterateXYZ(); + } + + template + void + iterateXYZ() + { + // borrowing parallel logic from tools/LevelSetSphere.h + + const Index n = mXYData.size(); + + if (mSerial) { + CacheLastLeafAccessor acc(getTree()); + for (Index i = 0; i < n; ++i) { + if (mInterrupter && !(i & ((1 << 7) - 1)) && !checkInterrupter()) + return; + + iterateYZ(i, acc); + } + } else { + tbb::enumerable_thread_specific pool(getTree()); + + auto kernel = [&](const tbb::blocked_range& rng) { + TreeT &tree = pool.local(); + CacheLastLeafAccessor acc(tree); + + if (!checkInterrupter()) + return; + + for (Index i = rng.begin(); i != rng.end(); ++i) { + if constexpr (LeapFrog) + iterateNoTilesYZ(i, acc); + else + iterateYZ(i, acc); + } + }; + + tbb::parallel_for(tbb::blocked_range(Index(0), n, Index(128)), kernel); + using RangeT = tbb::blocked_range::iterator>; + struct Op { + const bool mDelete; + TreeT *mTree; + Op(TreeT &tree) : mDelete(false), mTree(&tree) {} + Op(const Op& other, tbb::split) : mDelete(true), mTree(new TreeT(other.mTree->background())) {} + ~Op() { if (mDelete) delete mTree; } + void operator()(RangeT &r) { for (auto i=r.begin(); i!=r.end(); ++i) this->merge(*i);} + void join(Op &other) { this->merge(*(other.mTree)); } + void merge(TreeT &tree) { mTree->merge(tree, MERGE_ACTIVE_STATES); } + } op( getTree() ); + tbb::parallel_reduce(RangeT(pool.begin(), pool.end(), 4), op); + } + } + + + // for a given x value, create a filled slice of the object by populating + // active voxels and inactive negative background voxels + // if we're leap frogging, we may assume the negative background tiles are already populated + // for each x ordinate and y-scan range + // find the z-range for each y and then populate the grid with distance values + template + inline void + iterateYZ(const Index& i, CacheLastLeafAccessor& acc) + { + // initialize x value and y-range + ValueT x, yb, yt; + mXYData.XYData(x, yb, yt, i); + + if (!std::isfinite(yb) || !std::isfinite(yt)) + return; + + ValueT zb, zt; + + for (ValueT y = voxelCeil(yb); y <= perturbUp(yt); ++y) { + if (!bottomTop(zb, zt, x, y)) + continue; + + Coord ijk(Int32(x), Int32(y), Int32(0)); + Vec3T p(x, y, ValueT(0)); + + ijk[2] = Int32(voxelCeil(zb))-1; + acc.reset(ijk); + + for (ValueT z = voxelCeil(zb); z <= perturbUp(zt); ++z) { + ijk[2] = Int32(z); + const ValueT val = acc.template getValue<1>(ijk); + + if (val == mNegBg) { + if constexpr (LeapFrog) acc.template leapUp(ijk, z); + continue; + } + + p[2] = z; + const ValueT dist = mVox * invokeSignedDistance(p); + + if (dist <= mNegBg) { + acc.template setValueOff<1,false>(ijk, mNegBg); + } else if (dist < val) { + acc.template setValueOn<1,false>(ijk, dist); + } else { // dist >= val + acc.template checkReset<1>(ijk); + } + } + } + } + + // for a given x value, create a hollow slice of the object by only populating active voxels + // for each x ordinate and y-scan range + // find the z-range for each y and then populate the grid with distance values + inline void + iterateNoTilesYZ(const Index& i, CacheLastLeafAccessor& acc) + { + // initialize x value and y-range + ValueT x, yb, yt; + mXYData.XYData(x, yb, yt, i); + + if (!std::isfinite(yb) || !std::isfinite(yt)) + return; + + ValueT zb, zt; + + for (ValueT y = voxelCeil(yb); y <= perturbUp(yt); ++y) { + if (!bottomTop(zb, zt, x, y)) + continue; + + Coord ijk(Int32(x), Int32(y), Int32(0)); + Vec3T p(x, y, ValueT(0)); + + bool early_break = false; + ValueT z_stop; + + ijk[2] = Int32(voxelCeil(zb))-1; + acc.reset(ijk); + for (ValueT z = voxelCeil(zb); z <= perturbUp(zt); ++z) { + ijk[2] = Int32(z); + p[2] = z; + const ValueT dist = mVox * invokeSignedDistance(p); + + if (dist <= mNegBg) { + early_break = true; + z_stop = z; + break; + } else if (dist < mBg) { + acc.template setValueOn<1>(ijk, dist); + } else { // dist >= mBg + acc.template checkReset<1>(ijk); + } + } + if (early_break) { + ijk[2] = Int32(voxelFloor(zt))+1; + acc.reset(ijk); + for (ValueT z = voxelFloor(zt); z > z_stop; --z) { + ijk[2] = Int32(z); + p[2] = z; + const ValueT dist = mVox * invokeSignedDistance(p); + + if (dist <= mNegBg) { + break; + } else if (dist < mBg) { + acc.template setValueOn<-1>(ijk, dist); + } else { // dist >= mBg + acc.template checkReset<-1>(ijk); + } + } + } + } + } + + template + void + tileIterateXYZ() + { + AccessorT acc(getTree()); + for (Index i = 0; i < mXYData.size(); ++i) { + if (mInterrupter && !(i & ((1 << 7) - 1)) && !checkInterrupter()) + return; + + tileIterateYZ(i, acc); + } + } + + template + inline void + tileIterateYZ(const Index& i, AccessorT& acc) + { + // initialize x value and y-range + ValueT x, yb, yt; + mXYData.XYData(x, yb, yt, i); + + if (!std::isfinite(yb) || !std::isfinite(yt)) + return; + + static const Index TILESIZE = NodeT::DIM; + + ValueT zb, zt; + + for (ValueT y = tileCeil(yb, TILESIZE); y <= perturbUp(yt); y += TILESIZE) { + if (!bottomTop(zb, zt, x, y)) + continue; + + Coord ijk(Int32(x), Int32(y), Int32(0)); + Vec3T p(x, y, ValueT(0)); + + bool tiles_added = false; + ValueT z = tileCeil(zb, TILESIZE) - 2*TILESIZE; + while (z <= tileFloor(zt, TILESIZE) + TILESIZE) { + ijk[2] = Int32(z); + p[2] = z; + + if (leapFrogToNextTile(ijk, z, acc)) + continue; + + if (addTile(p, ijk, acc)) tiles_added = true; + else if (tiles_added) break; + } + } + } + + template + inline bool + leapFrogToNextTile(const Coord& ijk, ValueT& z, AccessorT& acc) const + { + static const int offset = NodeT::DIM; + static const int nodeDepth = int(TreeT::DEPTH - NodeT::LEVEL - 1); + + // we have not encountered an already populated tile + if (acc.getValue(ijk) != mNegBg) { + z += dir*offset; + return false; + } + + const int depth = acc.getValueDepth(ijk); + + // tile is not larger than current node + if (depth >= nodeDepth) { + z += dir*offset; + return false; + } + + const ValueT sz = ValueT(mTileSizes[depth]); + + z = dir > 0 + ? sz * ValueT(math::Ceil(z/sz)) + ValueT(0.5)*(offset-1) + : sz * ValueT(math::Floor(z/sz)) - ValueT(0.5)*(offset+1); + + return true; + } + + // add negative background tile inside the object if it fits and return true iff it was added + template + inline bool + addTile(const Vec3T& p, const Coord& ijk, AccessorT& acc) + { + static const Index LEVEL = NodeT::LEVEL + 1; + + if (tileFits(p)) { + acc.addTile(LEVEL, ijk, mNegBg, false); + return true; + } else { + return false; + } + } + + template + inline bool + tileFits(const Vec3T& p) const + { + static const Index TILESIZE = NodeT::DIM; + + static const ValueT R1 = ValueT(0.500)*(TILESIZE-1), + R2 = ValueT(0.866)*(TILESIZE-1); + + const ValueT dist = invokeTilePointSignedDistance(p); + + // fast positive criterion: circumsribed ball is in the object + if (dist <= -R2-mHw) + return true; + + // fast negative criterion: inscribed ball is not in the object + else if (dist >= -R1-mHw) + return false; + + // convexity: the tile is in the object iff all corners are in the object + return invokeTilePointSignedDistance(p + Vec3T(-R1, -R1, -R1)) < -mHw + && invokeTilePointSignedDistance(p + Vec3T(-R1, -R1, R1)) < -mHw + && invokeTilePointSignedDistance(p + Vec3T(-R1, R1, -R1)) < -mHw + && invokeTilePointSignedDistance(p + Vec3T(-R1, R1, R1)) < -mHw + && invokeTilePointSignedDistance(p + Vec3T(R1, -R1, -R1)) < -mHw + && invokeTilePointSignedDistance(p + Vec3T(R1, -R1, R1)) < -mHw + && invokeTilePointSignedDistance(p + Vec3T(R1, R1, -R1)) < -mHw + && invokeTilePointSignedDistance(p + Vec3T(R1, R1, R1)) < -mHw; + } + + inline void + invokeSetXYRangeData(const Index& step = 1) + { + static_cast(this)->setXYRangeData(step); + } + + inline bool + invokeTileCanFit(const Index& dim) const + { + return static_cast(this)->tileCanFit(dim); + } + + inline ValueT + invokeSignedDistance(const Vec3T& p) const + { + return static_cast(this)->signedDistance(p); + } + + inline ValueT + invokeTilePointSignedDistance(const Vec3T& p) const + { + return static_cast(this)->tilePointSignedDistance(p); + } + + // misc + + static inline std::vector + treeTileSizes() + { + // iterate over all non root nodes + using ChainT = typename NodeChainT::PopBack; + + std::vector sizes; + doTreeTileSizes(sizes); + + return sizes; + } + + template + static inline void + doTreeTileSizes(std::vector& sizes) + { + using NodeT = typename ChainT::Back; + using PoppedChainT = typename ChainT::PopBack; + + sizes.push_back(NodeT::DIM); + + if constexpr (!std::is_same_v>) { + doTreeTileSizes(sizes); + } + } + + inline bool + checkInterrupter() + { + if (util::wasInterrupted(mInterrupter)) { + openvdb::thread::cancelGroupExecution(); + return false; + } + return true; + } + + inline TreeT& getTree() { return mGrid->tree(); } + + // ------------ private nested classes ------------ + + /// @brief A class that caches access to the last leaf node. + /// @tparam TreeT The type of the tree being accessed. + /// @note This class optimizes repeated accesses to the same + /// leaf node by caching the last accessed leaf. + class CacheLastLeafAccessor + { + using NodeMaskType = util::NodeMask; + + public: + + /// @brief Constructs the CacheLastLeafAccessor and initializes + /// the internal accessor with the provided tree. + /// @param tree Reference to the tree being accessed. + CacheLastLeafAccessor(TreeT& tree) + : mAcc(tree), mTileSizes(treeTileSizes()) + { + } + + /// @brief Resets the cache by caching new leaf data for the given coordinates. + /// @param ijk The coordinate for which to cache the new leaf data. + inline void reset(const Coord& ijk) { cacheNewLeafData(ijk); } + + /// @brief Checks if the given coordinates are at a new leaf position, + /// and resets the cache if needed. + /// @tparam ZDir The direction in the Z-axis (default: 0, other choices are -1 and 1). + /// @param ijk The coordinate to check and potentially reset. + template + inline void checkReset(const Coord& ijk) + { + if (atNewLeafPos(ijk)) + cacheNewLeafData(ijk); + } + + /// @brief Retrieves the value at the given coordinate, + /// checking and resetting the cache if needed. + /// @tparam ZDir The direction in the Z-axis (default: 0, other choices are -1 and 1). + /// @tparam Check If true, checks if the coordinate is at a new leaf position (default: true). + /// @param ijk The coordinate for which to retrieve the value. + /// @return The value at the specified coordinate. + template + inline ValueT getValue(const Coord& ijk) + { + if (Check && atNewLeafPos(ijk)) + cacheNewLeafData(ijk); + + return mLeaf + ? mLeafData[coordToOffset(ijk)] + : mAcc.getValue(ijk); + } + + /// @brief Sets the value at the given coordinates and marks the voxel as active, + /// checking the cache if needed. + /// @tparam ZDir The direction in the Z-axis (default: 0, other choices are -1 and 1). + /// @tparam Check If true, checks if the coordinate is at a new leaf position (default: true). + /// @param ijk The coordinate where the value is to be set. + /// @param val The value to set at the specified coordinate. + template + inline void setValueOn(const Coord& ijk, const ValueT& val) + { + if (Check && atNewLeafPos(ijk)) + cacheNewLeafData(ijk); + + if (mLeaf) { + const Index n = coordToOffset(ijk); + mLeafData[n] = val; + mValueMask->setOn(n); + } else { + mAcc.setValueOn(ijk, val); + cacheNewLeafData(ijk); + } + } + + /// @brief Sets the value at the given coordinate and marks the voxel as inactive, + /// checking the cache if needed. + /// @tparam ZDir The direction in the Z-axis (default: 0, other choices are -1 and 1). + /// @tparam Check If true, checks if the coordinate is at a new leaf position (default: true). + /// @param ijk The coordinates where the value is to be set. + /// @param val The value to set at the specified coordinate. + template + inline void setValueOff(const Coord& ijk, const ValueT& val) + { + if (Check && atNewLeafPos(ijk)) + cacheNewLeafData(ijk); + + if (mLeaf) { + const Index n = coordToOffset(ijk); + mLeafData[n] = val; + mValueMask->setOff(n); + } else { + mAcc.setValueOff(ijk, val); + cacheNewLeafData(ijk); + } + } + + /// @brief Return @c true if the value of a voxel resides at the (possibly cached) + /// leaf level of the tree, i.e., if it is not a tile value. + /// @tparam ZDir The direction in the Z-axis (default: 0, other choices are -1 and 1). + /// @tparam Check If true, checks if the coordinate is at a new leaf position (default: true). + /// @param ijk The coordinate of the voxel to check. + /// @return True if the voxel exists, otherwise false. + template + inline bool isVoxel(const Coord& ijk) + { + return Check && atNewLeafPos(ijk) ? mLeaf != nullptr : mAcc.isVoxel(ijk); + } + + /// @brief If the input coordinate lies within a tile, + /// the input z value is set to the top of the tile bounding box, in index space. + /// @tparam Check If true, checks if the voxel exists before leaping (default: true). + /// @param ijk The coordinate to be examined. + /// @param z The Z-coordinate to be adjusted. + template + inline void leapUp(const Coord& ijk, ValueT& z) + { + if (isVoxel<1,Check>(ijk)) + return; + + const int depth = mAcc.getValueDepth(ijk); + const ValueT sz = ValueT(mTileSizes[depth]); + + z = sz * ValueT(math::Ceil((z+ValueT(1))/sz)) - ValueT(1); + } + + private: + + static const Index + DIM = LeafT::DIM, + LOG2DIM = LeafT::LOG2DIM; + + template + bool atNewLeafPos(const Coord& ijk) const + { + if constexpr (ZDir == -1) { + // assumes value just above has been cached already! + return (ijk[2] & (DIM-1u)) == DIM-1u; + } else if constexpr (ZDir == 1) { + // assumes value just below has been cached already! + return (ijk[2] & (DIM-1u)) == 0; + } else { + return Coord::lessThan(ijk, mOrigin) + || Coord::lessThan(mOrigin.offsetBy(DIM-1u), ijk); + } + } + + inline void cacheNewLeafData(const Coord& ijk) + { + mLeaf = mAcc.probeLeaf(ijk); + mOrigin = ijk & (~(DIM - 1)); + + if (mLeaf) { + mLeafData = mLeaf->buffer().data(); + mValueMask = &(mLeaf->getValueMask()); + } else { + mLeafData = nullptr; + } + } + + inline static Index coordToOffset(const Coord& ijk) + { + return ((ijk[0] & (DIM-1u)) << 2*LOG2DIM) + + ((ijk[1] & (DIM-1u)) << LOG2DIM) + + (ijk[2] & (DIM-1u)); + } + + AccessorT mAcc; + LeafT* mLeaf; + + ValueT* mLeafData; + NodeMaskType* mValueMask; + Coord mOrigin; + + const std::vector mTileSizes; + + }; // class CacheLastLeafAccessor + + // ------------ private members ------------ + + GridPtr mGrid; + + const std::vector mTileSizes = treeTileSizes(); + + const ValueT mVox, mHw, mBg, mNegBg; + + // misc + + const bool mSerial; + + InterruptType* mInterrupter; + +}; // class ConvexVoxelizer + + +} // namespace tools +} // namespace OPENVDB_VERSION_NAME +} // namespace openvdb + +#endif // OPENVDB_TOOLS_CONVEXVOXELIZER_HAS_BEEN_INCLUDED diff --git a/openvdb/openvdb/tools/LevelSetDilatedMesh.h b/openvdb/openvdb/tools/LevelSetDilatedMesh.h new file mode 100644 index 0000000000..9a3a60c079 --- /dev/null +++ b/openvdb/openvdb/tools/LevelSetDilatedMesh.h @@ -0,0 +1,1723 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: Apache-2.0 +/// +/// @author Greg Hurst +/// +/// @file LevelSetDilatedMesh.h +/// +/// @brief Generate a narrow-band level set of a dilated surface mesh. +/// +/// @note By definition a level set has a fixed narrow band width +/// (the half width is defined by LEVEL_SET_HALF_WIDTH in Types.h), +/// whereas an SDF can have a variable narrow band width. + +#ifndef OPENVDB_TOOLS_LEVELSETDILATEDMESH_HAS_BEEN_INCLUDED +#define OPENVDB_TOOLS_LEVELSETDILATEDMESH_HAS_BEEN_INCLUDED + +#include "ConvexVoxelizer.h" +#include "LevelSetTubes.h" +#include "PointPartitioner.h" +#include "Prune.h" + +#include +#include +#include + +#include +#include + +#include +#include + + +namespace openvdb { +OPENVDB_USE_VERSION_NAMESPACE +namespace OPENVDB_VERSION_NAME { +namespace tools { + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a dilated triangle surface mesh (dilated by a radius in all directions). +/// +/// @param vertices Vertices of the mesh in world units. +/// @param triangles Triangle indices of the mesh. +/// @param radius Dilation radius in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param interrupter Interrupter adhering to the util::NullInterrupter interface. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the mesh vertex and radius type +/// and must be a floating-point scalar. +/// @note The input mesh is always treated as a surface, and so dilation occurs in every direction. +/// This includes meshes that could represent valid BRep solids, dilation occurs both +/// inward and outward, forming a 'shell' rather than only expanding outward. +template +typename GridType::Ptr +createLevelSetDilatedMesh( + const std::vector>& vertices, const std::vector& triangles, + ScalarType radius, float voxelSize, float halfWidth = float(LEVEL_SET_HALF_WIDTH), + InterruptT* interrupter = nullptr); + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a dilated quad surface mesh (dilated by a radius in all directions). +/// +/// @param vertices Vertices of the mesh in world units. +/// @param quads Quad indices of the mesh. +/// @param radius Dilation radius in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param interrupter Interrupter adhering to the util::NullInterrupter interface. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the mesh vertex and radius type +/// and must be a floating-point scalar. +/// @note The input mesh is always treated as a surface, and so dilation occurs in every direction. +/// This includes meshes that could represent valid BRep solids, dilation occurs both +/// inward and outward, forming a 'shell' rather than only expanding outward. +template +typename GridType::Ptr +createLevelSetDilatedMesh( + const std::vector>& vertices, const std::vector& quads, + ScalarType radius, float voxelSize, float halfWidth = float(LEVEL_SET_HALF_WIDTH), + InterruptT* interrupter = nullptr); + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a dilated triangle & quad surface mesh (dilated by a radius in all directions). +/// +/// @param vertices Vertices of the mesh in world units. +/// @param triangles Triangle indices of the mesh. +/// @param quads Quad indices of the mesh. +/// @param radius Dilation radius in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param interrupter Interrupter adhering to the util::NullInterrupter interface. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the mesh vertex and radius type +/// and must be a floating-point scalar. +/// @note The input mesh is always treated as a surface, and so dilation occurs in every direction. +/// This includes meshes that could represent valid BRep solids, dilation occurs both +/// inward and outward, forming a 'shell' rather than only expanding outward. +template +typename GridType::Ptr +createLevelSetDilatedMesh(const std::vector>& vertices, + const std::vector& triangles, const std::vector& quads, + ScalarType radius, float voxelSize, float halfWidth = float(LEVEL_SET_HALF_WIDTH), + InterruptT* interrupter = nullptr); + +namespace lvlset { + +/// @brief Class used to generate a grid of type @c GridType containing a narrow-band level set +/// representation of an _open_ prism. +/// The only parts of the level set populated are along both normals of the triangle. +/// Negative background tiles that fit inside the closed dilated triangle are also populated. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +template +class OpenTriangularPrismVoxelizer + : public ConvexVoxelizer< + GridType, + OpenTriangularPrismVoxelizer, + InterruptT> +{ + using GridPtr = typename GridType::Ptr; + + using BaseT = ConvexVoxelizer< + GridType, + OpenTriangularPrismVoxelizer, + InterruptT + >; + + using BaseT::mXYData; + using BaseT::tileCeil; + + using ValueT = typename BaseT::ValueT; + using Vec3T = typename BaseT::Vec3T; + +public: + + friend class ConvexVoxelizer< + GridType, + OpenTriangularPrismVoxelizer, + InterruptT + >; + + /// @brief Constructor + /// + /// @param grid scalar grid to populate the level set in + /// @param threaded center of the sphere in world units + /// @param interrupter pointer to optional interrupter. Use template + /// argument util::NullInterrupter if no interruption is desired. + /// + /// @note The voxel size and half width are determined from the input grid, + /// meaning the voxel size and background value need to be set prior to voxelization + OpenTriangularPrismVoxelizer(GridPtr& grid, + const bool& threaded = false, + InterruptT* interrupter = nullptr) + : BaseT(grid, threaded, interrupter) + { + } + + /// @brief Create an open prism + /// + /// @param pt1 point 1 of the triangle in world units + /// @param pt2 point 2 of the triangle in world units + /// @param pt3 point 3 of the triangle in world units + /// @param radius radius of the open prism in world units + template + void + operator()(const math::Vec3& pt1, const math::Vec3& pt2, + const math::Vec3& pt3, const ScalarType& radius) + { + static_assert(std::is_floating_point::value); + + if (initialize(pt1, pt2, pt3, radius)) + BaseT::iterate(); + } + +private: + + inline void + setXYRangeData(const Index& step = 1) + { + const ValueT &x1 = mPts[0].x(), &x2 = mPts[1].x(), &x3 = mPts[2].x(), + &x4 = mPts[3].x(), &x5 = mPts[4].x(), &x6 = mPts[5].x(); + + const ValueT xmin = math::Min(x1, x2, x3, x4, x5, x6); + const ValueT xmax = math::Max(x1, x2, x3, x4, x5, x6); + mXYData.reset(xmin, xmax, step); + + // TODO add logic to ignore edges in the interior of the projection + // TODO add logic that classifies each segment as being either on 'top' or 'bottom' + + setXYSegmentRangeData<0,1,0>(step); + setXYSegmentRangeData<1,2,0>(step); + setXYSegmentRangeData<2,0,0>(step); + + setXYSegmentRangeData<3,4,0>(step); + setXYSegmentRangeData<4,5,0>(step); + setXYSegmentRangeData<5,3,0>(step); + + setXYSegmentRangeData<0,3,0>(step); + setXYSegmentRangeData<1,4,0>(step); + setXYSegmentRangeData<2,5,0>(step); + } + + template + inline void + setXYSegmentRangeData(const Index& step = 1) + { + const ValueT &x1 = mPts[i].x(), &x2 = mPts[j].x(); + + // nothing to do if segment does not span across more than on voxel in x + // other segments will handle this segment's range + if (tileCeil(x1, step) == tileCeil(x2, step)) + return; + + const ValueT x_start = tileCeil(math::Min(x1, x2), step), + x_end = math::Max(x1, x2), + stepv = ValueT(step); + + for (ValueT x = x_start; x <= x_end; x += stepv) { + if constexpr (MinMax <= 0) + mXYData.expandYMin(x, line2D(x)); + if constexpr (MinMax >= 0) + mXYData.expandYMax(x, line2D(x)); + } + } + + // simply offset distance to the center plane, we may assume any CPQ falls in inside the prism + inline ValueT + signedDistance(const Vec3T& p) const + { + return math::Abs(mTriNrml.dot(p - mA)) - mRad; + } + + // allows for tiles to poke outside of the open prism into the tubes + // adaptation of udTriangle at https://iquilezles.org/articles/distfunctions/ + inline ValueT + tilePointSignedDistance(const Vec3T& p) const + { + const Vec3T pa = p - mA, + pb = p - mB, + pc = p - mC; + + const ValueT udist = + math::Sign(mBAXNrml.dot(pa)) + + math::Sign(mCBXNrml.dot(pb)) + + math::Sign(mACXNrml.dot(pc)) < 2 + ? + math::Sqrt(math::Min( + (mBA * math::Clamp01(mBANorm2.dot(pa)) - pa).lengthSqr(), + (mCB * math::Clamp01(mCBNorm2.dot(pb)) - pb).lengthSqr(), + (mAC * math::Clamp01(mACNorm2.dot(pc)) - pc).lengthSqr() + )) + : + math::Abs(mTriNrml.dot(p - mA)); + + return udist - mRad; + } + + inline bool + tileCanFit(const Index& dim) const + { + return mRad >= BaseT::halfWidth() + ValueT(0.5) * (ValueT(dim)-ValueT(1)); + } + + std::function prismBottomTop = + [this](ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) + { + zb = std::numeric_limits::lowest(); + zt = std::numeric_limits::max(); + + // TODO with proper book keeping we can know apriori which 2 indexes will set zb & zt + // basically figure out a poor man's cylindrical decomposition... + setPlaneBottomTop<0>(zb, zt, x, y); + setPlaneBottomTop<1>(zb, zt, x, y); + setPlaneBottomTop<2>(zb, zt, x, y); + setPlaneBottomTop<3>(zb, zt, x, y); + setPlaneBottomTop<4>(zb, zt, x, y); + + return true; + }; + + template + inline void + setPlaneBottomTop(ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) const + { + if (math::isApproxZero(mFaceNrmls[i].z())) + return; + + const ValueT z = mPlaneXCoeffs[i]*x + mPlaneYCoeffs[i]*y + mPlaneOffsets[i]; + + if (mFaceNrmls[i].z() < 0) { + if (zb < z) + zb = z; + } else { + if (zt > z) + zt = z; + } + } + + // world space points and radius inputs + // initializes class members in index space + template + inline bool + initialize(const math::Vec3& pt1, const math::Vec3& pt2, + const math::Vec3& pt3, const ScalarType& r) + { + const ValueT vx = BaseT::voxelSize(), + hw = BaseT::halfWidth(); + + mA = Vec3T(pt1)/vx; + mB = Vec3T(pt2)/vx; + mC = Vec3T(pt3)/vx; + + mRad = ValueT(r)/vx; + if (math::isApproxZero(mRad) || mRad < 0) + return false; // nothing to voxelize, prism has no volume + + mBA = mB-mA; + mCB = mC-mB; + mAC = mA-mC; + + mTriNrml = mBA.cross(mC-mA); + + mBAXNrml = mTriNrml.cross(mBA); + mCBXNrml = mTriNrml.cross(mCB); + mACXNrml = mTriNrml.cross(mAC); + + mBANorm2 = math::isApproxZero(mBA.lengthSqr()) ? mBA : mBA/mBA.lengthSqr(); + mCBNorm2 = math::isApproxZero(mCB.lengthSqr()) ? mCB : mCB/mCB.lengthSqr(); + mACNorm2 = math::isApproxZero(mAC.lengthSqr()) ? mAC : mAC/mAC.lengthSqr(); + + const ValueT len = mTriNrml.length(); + if (math::isApproxZero(len)) { + return false; // nothing to voxelize, prism has no volume + } else { + mTriNrml /= len; + } + + const ValueT hwRad = mRad + hw; + mPts = { + mA + hwRad * mTriNrml, mB + hwRad * mTriNrml, mC + hwRad * mTriNrml, + mA - hwRad * mTriNrml, mB - hwRad * mTriNrml, mC - hwRad * mTriNrml + }; + + // tri1, tri2, quad1, quad2, quad3 + mFaceNrmls = { + mTriNrml, + -mTriNrml, + mTriNrml.cross(mA-mB).unitSafe(), + mTriNrml.cross(mB-mC).unitSafe(), + mTriNrml.cross(mC-mA).unitSafe() + }; + + { + static const std::vector p_ind = {0, 3, 0, 1, 2}; + + mPlaneXCoeffs.assign(5, ValueT(0)); + mPlaneYCoeffs.assign(5, ValueT(0)); + mPlaneOffsets.assign(5, ValueT(0)); + + for (Index i = 0; i < 5; ++i) { + if (!math::isApproxZero(mFaceNrmls[i].z())) { + const ValueT cx = mFaceNrmls[i].x()/mFaceNrmls[i].z(), + cy = mFaceNrmls[i].y()/mFaceNrmls[i].z(); + const Vec3T p = mPts[p_ind[i]]; + mPlaneXCoeffs[i] = -cx; + mPlaneYCoeffs[i] = -cy; + mPlaneOffsets[i] = p.x()*cx + p.y()*cy + p.z(); + } + } + } + + BaseT::bottomTop = prismBottomTop; + + return true; + } + + // ------------ general utilities ------------ + + template + ValueT + line2D(const ValueT& x) const + { + const ValueT &x1 = mPts[i].x(), &y1 = mPts[i].y(), + &x2 = mPts[j].x(), &y2 = mPts[j].y(); + + const ValueT m = (y2-y1)/(x2-x1); + + return y1 + m * (x-x1); + } + + // ------------ private members ------------ + + Vec3T mA, mB, mC; + ValueT mRad; + + Vec3T mBA, mCB, mAC; + Vec3T mBAXNrml, mCBXNrml, mACXNrml; + Vec3T mBANorm2, mCBNorm2, mACNorm2; + + std::vector mPts = std::vector(6); + + Vec3T mTriNrml; + std::vector mFaceNrmls = std::vector(5); + + std::vector mPlaneXCoeffs = std::vector(5), + mPlaneYCoeffs = std::vector(5), + mPlaneOffsets = std::vector(5); + +}; // class OpenTriangularPrismVoxelizer + +/// @brief Class used to generate a grid of type @c GridType containing a narrow-band level set +/// representation of an _open_ wedge. +/// The only parts of the level set populated are within a sector of a capsule. +/// The sector is defined by the intersection of two half spaces. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +template +class OpenCapsuleWedgeVoxelizer + : public ConvexVoxelizer< + GridType, + OpenCapsuleWedgeVoxelizer, + InterruptT> +{ + using GridPtr = typename GridType::Ptr; + + using BaseT = ConvexVoxelizer< + GridType, + OpenCapsuleWedgeVoxelizer, + InterruptT + >; + + using BaseT::mXYData; + using BaseT::tileCeil; + + using ValueT = typename BaseT::ValueT; + using Vec3T = typename BaseT::Vec3T; + using Vec2T = typename BaseT::Vec2T; + +public: + + friend class ConvexVoxelizer< + GridType, + OpenCapsuleWedgeVoxelizer, + InterruptT + >; + + /// @brief Constructor + /// + /// @param grid scalar grid to populate the level set in + /// @param threaded center of the sphere in world units + /// @param interrupter pointer to optional interrupter. Use template + /// argument util::NullInterrupter if no interruption is desired. + /// + /// @note The voxel size and half width are determined from the input grid, + /// meaning the voxel size and background value need to be set prior to voxelization + OpenCapsuleWedgeVoxelizer(GridPtr& grid, const bool& threaded = false, + InterruptT* interrupter = nullptr) + : BaseT(grid, threaded, interrupter) + { + } + + /// @brief Create an open wedge + /// + /// @param pt1 first endpoint open wedge in world units + /// @param pt2 second endpoint open wedge in world units + /// @param radius radius of the open prism in world units + /// @param nrml1 normal of a half space the the capsule is clipped with to form the open wedge + /// @param nrml2 normal of the other half space the the capsule is clipped with to form the open wedge + /// + /// @note The normal vectors @f$n @f$ point outward from the open wedge, + /// and the clipping half space is defined by the set of points @f$p @f$ that satisfy @f$n . (p - pt1) \leq 0@f$. + template + void + operator()(const math::Vec3& pt1, const math::Vec3& pt2, + const ScalarType& radius, const math::Vec3& nrml1, + const math::Vec3& nrml2) + { + static_assert(std::is_floating_point::value); + + if (initialize(pt1, pt2, radius, nrml1, nrml2)) + BaseT::iterate(); + } + +private: + + // computes *approximate* xy-range data: the projected caps might contain over-inclusive values + inline void + setXYRangeData(const Index& step = 1) + { + const ValueT stepv = ValueT(step); + + // short circuit a vertical cylinder + if (mIsVertical) { + mXYData.reset(mX1 - mORad, mX1 + mORad, step); + + for (ValueT x = tileCeil(mX1 - mORad, step); x <= mX1 + mORad; x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), circle1Top(x)); + + intersectWithXYWedgeLines(); + return; + } + + const ValueT v = math::Min(mORad, mORad * math::Abs(mYdiff)/mXYNorm); + + const ValueT a0 = mX1 - mORad, + a1 = mX1 - v, + a2 = mX1 + v, + a3 = mX2 - v, + a4 = mX2 + v, + a5 = mX2 + mORad; + + const ValueT tc0 = tileCeil(a0, step), + tc1 = tileCeil(a1, step), + tc2 = tileCeil(a2, step), + tc3 = tileCeil(a3, step), + tc4 = tileCeil(a4, step); + + mXYData.reset(a0, a5, step); + + for (ValueT x = tc0; x <= a1; x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), circle1Top(x)); + + if (!math::isApproxZero(mXdiff)) { + if (mY1 > mY2) { + for (ValueT x = tc1; x <= math::Min(a2, a3); x += stepv) + mXYData.expandYRange(x, lineBottom(x), circle1Top(x)); + } else { + for (ValueT x = tc1; x <= math::Min(a2, a3); x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), lineTop(x)); + } + } + + if (a2 < a3) { + for (ValueT x = tc2; x <= a3; x += stepv) + mXYData.expandYRange(x, lineBottom(x), lineTop(x)); + } else { + if (mY2 <= mY1) { + for (ValueT x = tc3; x <= a2; x += stepv) + mXYData.expandYRange(x, circle2Bottom(x), circle1Top(x)); + } else { + for (ValueT x = tc3; x <= a2; x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), circle2Top(x)); + } + } + + if (!math::isApproxZero(mXdiff)) { + if (mY1 > mY2) { + for (ValueT x = math::Max(tc2, tc3); x <= a4; x += stepv) + mXYData.expandYRange(x, circle2Bottom(x), lineTop(x)); + } else { + for (ValueT x = math::Max(tc2, tc3); x <= a4; x += stepv) + mXYData.expandYRange(x, lineBottom(x), circle2Top(x)); + } + } + + for (ValueT x = tc4; x <= a5; x += stepv) + mXYData.expandYRange(x, circle2Bottom(x), circle2Top(x)); + + intersectWithXYStrip(); + intersectWithXYWedgeLines(); + } + + inline void + intersectWithXYStrip() + { + // these strips are vertical when the capsule is + if (mIsVertical) + return; + + const Vec3T &pp1 = mPlanePts[0], &pp2 = mPlanePts[1]; + const ValueT &vx = mV.x(), &vy = mV.y(); + + Vec2T n = Vec2T(-vy, vx).unitSafe(); + Vec3T cvec = mORad * Vec3T(-vy, vx, ValueT(0)).unitSafe(); + + if (math::isApproxZero(vy)) + cvec.y() = math::Abs(cvec.y()); + else if (vy > 0) + cvec *= ValueT(-1); + + const Vec3T cpmin(mPt1 - cvec), cpmax(mPt1 + cvec); + + if (math::isApproxZero(mXdiff)) { + const ValueT px = mPt1.x(), + xmin = math::Min(px, pp1.x(), pp2.x()), + xmax = math::Max(px, pp1.x(), pp2.x()); + + if (!inWedge(cpmin)) + intersectWithXYHalfSpace(n.x() < 0 ? n : -n, Vec2T(xmin, ValueT(0))); + + if (!inWedge(cpmax)) + intersectWithXYHalfSpace(n.x() > 0 ? n : -n, Vec2T(xmax, ValueT(0))); + } else { + const ValueT m = mYdiff/mXdiff; + const ValueT y1 = mPt1.y() - m * mPt1.x(), + y2 = pp1.y() - m * pp1.x(), + y3 = pp2.y() - m * pp2.x(); + const ValueT ymin = math::Min(y1, y2, y3), + ymax = math::Max(y1, y2, y3); + + if (!inWedge(vy <= 0 ? cpmin : cpmax)) + intersectWithXYHalfSpace(n.y() < 0 ? n : -n, Vec2T(ValueT(0), ymin)); + + if (!inWedge(vy > 0 ? cpmin : cpmax)) + intersectWithXYHalfSpace(n.y() > 0 ? n : -n, Vec2T(ValueT(0), ymax)); + } + } + + inline void + intersectWithXYWedgeLines() + { + const Vec3T v(mORad * mV.unitSafe()), + p1(mPt1 - v), + p2(mPt2 + v); + + const Vec2T p1_2d(p1.x(), p1.y()), p2_2d(p2.x(), p2.y()); + + Vec2T d(-mPlaneNrmls[0].x() - mPlaneNrmls[1].x(), + -mPlaneNrmls[0].y() - mPlaneNrmls[1].y()); + + Vec2T n0(-mDirVectors[0].y(), mDirVectors[0].x()), + n1(-mDirVectors[1].y(), mDirVectors[1].x()); + + if (n0.dot(d) > 0) + n0 *= ValueT(-1); + if (n1.dot(d) > 0) + n1 *= ValueT(-1); + + if (!math::isApproxZero(n0.lengthSqr())) + intersectWithXYHalfSpace(n0, n0.dot(p2_2d - p1_2d) < 0 ? p1_2d : p2_2d); + + if (!math::isApproxZero(n1.lengthSqr())) + intersectWithXYHalfSpace(n1, n1.dot(p2_2d - p1_2d) < 0 ? p1_2d : p2_2d); + } + + inline void + intersectWithXYHalfSpace(const Vec2T& n, const Vec2T& p) + { + if (mXYData.size() == 0) + return; + + if (math::isApproxZero(n.y())) { + const ValueT &px = p.x(); + if (n.x() < 0) { + const Index m = mXYData.size(); + for (Index i = 0; i < m; ++i) { + const ValueT x = mXYData.getX(i); + + if (x < px) mXYData.clearYRange(x); + else break; + } + } else { + Index i = mXYData.size()-1; + while (true) { + const ValueT x = mXYData.getX(i); + + if (x > px) mXYData.clearYRange(x); + else break; + + if (i != 0) --i; + else break; + } + } + } else { + const bool set_min = n.y() < 0; + const Index m = mXYData.size(); + + const ValueT b = -n.x()/n.y(); + const ValueT a = p.y() - b * p.x(); + + ValueT x, ymin, ymax; + for (Index i = 0; i < m; ++i) { + mXYData.XYData(x, ymin, ymax, i); + const ValueT yint = a + b * x; + + if (ymin <= yint && yint <= ymax) { + if (set_min) mXYData.setYMin(x, yint); + else mXYData.setYMax(x, yint); + } else { + if (set_min ? yint > ymax : yint < ymin) + mXYData.clearYRange(x); + } + } + } + + mXYData.trim(); + } + + // distance in index space + inline ValueT + signedDistance(const Vec3T& p) const + { + const Vec3T w = p - mPt1; + const ValueT dot = w.dot(mV); + + // carefully short circuit with a fuzzy tolerance, which avoids division by small mVLenSqr + if (dot <= math::Tolerance::value()) + return w.length() - mRad; + + if (dot >= mVLenSqr) + return (p - mPt2).length() - mRad; + + const ValueT t = w.dot(mV)/mVLenSqr; + + return (w - t * mV).length() - mRad; + } + + inline bool + tileCanFit(const Index& dim) const + { + return mRad >= BaseT::halfWidth() + ValueT(0.5) * (ValueT(dim)-ValueT(1)); + } + + std::function capsuleBottomTopVertical = + [this](ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) + { + zb = BaseT::sphereBottom(mX1, mY1, math::Min(mZ1, mZ2), mORad, x, y); + zt = BaseT::sphereTop(mX2, mY2, math::Max(mZ1, mZ2), mORad, x, y); + + return std::isfinite(zb) && std::isfinite(zt); + }; + + std::function capsuleBottomTop = + [this](ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) + { + ValueT cylptb, cylptt; + if (!infiniteCylinderBottomTop(cylptb, cylptt, x, y)) + return false; + + const ValueT dotb = (Vec3T(x, y, cylptb) - mPt1).dot(mV); + const ValueT dott = (Vec3T(x, y, cylptt) - mPt1).dot(mV); + + if (dotb < 0) + zb = sphere1Bottom(x, y); + else if (dotb > mVLenSqr) + zb = sphere2Bottom(x, y); + else + zb = cylptb; + + if (dott < 0) + zt = sphere1Top(x, y); + else if (dott > mVLenSqr) + zt = sphere2Top(x, y); + else + zt = cylptt; + + if (!std::isfinite(zb) || !std::isfinite(zt)) + return false; + + intersectWedge<0,1>(zb, zt, x, y); + intersectWedge<1,0>(zb, zt, x, y); + + return inWedge(x, y, ValueT(0.5)*(zb+zt)); + }; + + template + inline void + intersectWedge(ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) + { + const Vec3T& n0 = mPlaneNrmls[i]; + + if (math::isApproxZero(n0.z())) + return; + + const ValueT zp = mPlaneXCoeffs[i]*x + mPlaneYCoeffs[i]*y + mPlaneOffsets[i]; + + if (zb <= zp && zp <= zt && inHalfSpace(Vec3T(x, y, zp))) { + if (n0.z() < 0) + zb = zp; + else + zt = zp; + } + } + + inline bool + inWedge(const ValueT& x, const ValueT& y, const ValueT& z) + { + return inWedge(Vec3T(x, y, z)); + } + + inline bool + inWedge(const Vec3T& pt) + { + return inHalfSpace<0>(pt) && inHalfSpace<1>(pt); + } + + template + inline bool + inHalfSpace(const Vec3T& pt) + { + // allow points within a fuzzy fractional (index space) distance to the halfspace + // this ensures the seams between open wedges and open prisms are completely filled in + // assumes mPlaneNrmls[i] is a unit vector + static const ValueT VOXFRAC = 0.125; + + return mPlaneNrmls[i].dot(pt-mPt1) <= VOXFRAC; + } + + // assumes tube is not vertical! + inline bool + infiniteCylinderBottomTop(ValueT& cylptb, ValueT& cylptt, + const ValueT& x, const ValueT& y) const + { + const Vec2T q(x, y); + + const Vec2T qproj = mPt12d + mV2d*((q - mPt12d).dot(mV2d))/mXYNorm2; + + const ValueT t = mX1 != mX2 ? (qproj[0] - mX1)/mXdiff : (qproj[1] - mY1)/mYdiff; + + const Vec3T qproj3D = mPt1 + t * mV; + + const ValueT d2 = (q - qproj).lengthSqr(); + + // outside of cylinder's 2D projection + if (mORad2 < d2) + return false; + + const ValueT h = math::Sqrt((mORad2 - d2) * mVLenSqr/mXYNorm2); + + cylptb = qproj3D[2] - h; + cylptt = qproj3D[2] + h; + + return true; + } + + inline ValueT + lineBottom(const ValueT& x) const + { + return mY1 + (mYdiff*(x-mX1) - mORad * mXYNorm)/mXdiff; + } + + inline ValueT + lineTop(const ValueT& x) const + { + return mY1 + (mYdiff*(x-mX1) + mORad * mXYNorm)/mXdiff; + } + + inline ValueT + circle1Bottom(const ValueT& x) const + { + return BaseT::circleBottom(mX1, mY1, mORad, x); + } + + inline ValueT + circle1Top(const ValueT& x) const + { + return BaseT::circleTop(mX1, mY1, mORad, x); + } + + inline ValueT + circle2Bottom(const ValueT& x) const + { + return BaseT::circleBottom(mX2, mY2, mORad, x); + } + + inline ValueT + circle2Top(const ValueT& x) const + { + return BaseT::circleTop(mX2, mY2, mORad, x); + } + + inline ValueT + sphere1Bottom(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereBottom(mX1, mY1, mZ1, mORad, x, y); + } + + inline ValueT + sphere1Top(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereTop(mX1, mY1, mZ1, mORad, x, y); + } + + inline ValueT + sphere2Bottom(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereBottom(mX2, mY2, mZ2, mORad, x, y); + } + + inline ValueT + sphere2Top(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereTop(mX2, mY2, mZ2, mORad, x, y); + } + + // world space points and radius inputs + // initializes class members in index space + template + inline bool + initialize(const math::Vec3& pt1, const math::Vec3& pt2, + const ScalarType& r, const math::Vec3& nrml1, + const math::Vec3& nrml2) + { + const ValueT vx = BaseT::voxelSize(), + hw = BaseT::halfWidth(); + + // forces x1 <= x2 + if (pt1[0] <= pt2[0]) { + mPt1 = Vec3T(pt1)/vx; + mPt2 = Vec3T(pt2)/vx; + } else { + mPt1 = Vec3T(pt2)/vx; + mPt2 = Vec3T(pt1)/vx; + } + + mRad = ValueT(r)/vx; + + // tube has no volume + if (math::isApproxZero(mRad)) + return false; + + // padded radius used to populate the outer halfwidth of the sdf + mORad = mRad + hw; + mORad2 = mORad * mORad; + + mV = mPt2 - mPt1; + mVLenSqr = mV.lengthSqr(); + + // no direction to form the wedge on a sphere + if (math::isApproxZero(mVLenSqr)) + return false; + + mX1 = mPt1[0]; mY1 = mPt1[1]; mZ1 = mPt1[2]; + mX2 = mPt2[0]; mY2 = mPt2[1]; mZ2 = mPt2[2]; + + mXdiff = mX2 - mX1; + mYdiff = mY2 - mY1; + mZdiff = mZ2 - mZ1; + + mPt12d = Vec2T(mX1, mY1); + mPt22d = Vec2T(mX2, mY2); + mV2d = mPt22d - mPt12d; + + mXYNorm2 = math::Pow2(mXdiff) + math::Pow2(mYdiff); + mXYNorm = math::Sqrt(mXYNorm2); + mIsVertical = math::isApproxZero(mXYNorm); + + { + const Vec3T n1 = Vec3T(nrml1), n2 = Vec3T(nrml2); + + // no direction to form the wedge + if (math::isApproxZero(n1.lengthSqr()) || math::isApproxZero(n2.lengthSqr())) + return false; + + mPlaneNrmls[0] = (n1 - n1.projection(mV)).unitSafe(); + mPlaneNrmls[1] = (n2 - n2.projection(mV)).unitSafe(); + + // degenerate wedge + if (approxAntiParallel(mPlaneNrmls[0], mPlaneNrmls[1])) + return false; + + mDirVectors[0] = mORad * mV.cross(mPlaneNrmls[0]).unitSafe(); + mDirVectors[1] = mORad * mV.cross(mPlaneNrmls[1]).unitSafe(); + + if (approxParallel(mPlaneNrmls[0], mPlaneNrmls[1])) { + mDirVectors[1] = -mDirVectors[0]; + } else { + if (mPlaneNrmls[1].dot(mDirVectors[0]) > 0) + mDirVectors[0] *= ValueT(-1); + if (mPlaneNrmls[0].dot(mDirVectors[1]) > 0) + mDirVectors[1] *= ValueT(-1); + } + + mPlanePts[0] = mPt1 + mDirVectors[0] + ValueT(0.025) * mPlaneNrmls[0]; + mPlanePts[1] = mPt1 + mDirVectors[1] + ValueT(0.025) * mPlaneNrmls[1]; + } + + { + mPlaneXCoeffs.assign(2, ValueT(0)); + mPlaneYCoeffs.assign(2, ValueT(0)); + mPlaneOffsets.assign(2, ValueT(0)); + + for (Index i = 0; i < 2; ++i) { + if (!math::isApproxZero(mPlaneNrmls[i].z())) { + const ValueT cx = mPlaneNrmls[i].x()/mPlaneNrmls[i].z(), + cy = mPlaneNrmls[i].y()/mPlaneNrmls[i].z(); + const Vec3T p = mPlanePts[i]; + mPlaneXCoeffs[i] = -cx; + mPlaneYCoeffs[i] = -cy; + mPlaneOffsets[i] = p.x()*cx + p.y()*cy + p.z(); + } + } + } + + BaseT::bottomTop = mIsVertical ? capsuleBottomTopVertical : capsuleBottomTop; + + return true; + } + + inline bool + approxAntiParallel(const Vec3T& n1, const Vec3T& n2) + { + return approxParallel(n1, -n2); + } + + inline bool + approxParallel(const Vec3T& n1, const Vec3T& n2) + { + return n1.unitSafe().eq(n2.unitSafe()); + } + + // ------------ private members ------------ + + // tube data -- populated via initialize() + + Vec3T mPt1, mPt2, mV; + + Vec2T mPt12d, mPt22d, mV2d; + + ValueT mORad, mORad2, mRad, mVLenSqr, mXdiff, mYdiff, mZdiff, mXYNorm, mXYNorm2; + + ValueT mX1, mY1, mZ1, mX2, mY2, mZ2; + + bool mIsVertical; + + std::vector mPlaneNrmls = std::vector(2), + mDirVectors = std::vector(2), + mPlanePts = std::vector(2); + + std::vector mPlaneXCoeffs = std::vector(2), + mPlaneYCoeffs = std::vector(2), + mPlaneOffsets = std::vector(2); + +}; // class OpenCapsuleWedgeVoxelizer + + +/// @brief Class representing the connectivity of edges in a triangle mesh, +/// where each edge is associated with the cells (triangles) sharing it. +/// Provides methods to retrieve adjacent cells, +/// vertex coordinates, normals, and other geometric properties. +template +class TriangleMeshEdgeConnectivity { + + static_assert(std::is_floating_point::value); + + using Vec3T = math::Vec3; + +public: + + /// @brief Constructs the TriangleMeshEdgeConnectivity object with given coordinates and cell data. + /// Populates edge-to-cell adjacency and computes cell normals. + /// + /// @param coords Vector of vertex coordinates. + /// @param cells Vector of cell (triangle) indices. + TriangleMeshEdgeConnectivity(const std::vector& coords, + const std::vector& cells) + : mCoords(coords), mCells(cells) + { + const Index n = Index(coords.size()); + + mNormals.resize(cells.size()); + + for (Index i = 0; i < cells.size(); ++i) { + const Vec3I& cell = mCells[i]; + + Edge edges[3] = { + Edge(cell[0], cell[1]), + Edge(cell[1], cell[2]), + Edge(cell[2], cell[0]) + }; + + for (const Edge& edge : edges) { + mEdgeMap[edge].push_back(i); + } + + if (cell[0] >= n || cell[1] >= n || cell[2] >= n) + OPENVDB_THROW(ValueError, "out of bounds index"); + + const Vec3T &p1 = mCoords[cell[0]], + &p2 = mCoords[cell[1]], + &p3 = mCoords[cell[2]]; + + mNormals[i] = (p2 - p1).cross(p3 - p1).unitSafe(); + } + + for (auto& [edge, cells] : mEdgeMap) + sortAdjacentCells(edge, cells); + } + + /// @brief Retrieves the IDs of cells adjacent to an edge formed by two vertices. + /// + /// @param v1 First vertex index. + /// @param v2 Second vertex index. + /// @param cellIds Output vector to hold the IDs of adjacent cells. + /// @return True if adjacent cells are found, false otherwise. + bool + getAdjacentCells(const Index& v1, const Index& v2, std::vector& cellIds) const + { + Edge edge(v1, v2); + auto it = mEdgeMap.find(edge); + if (it != mEdgeMap.end()) { + cellIds = it->second; + return true; + } + return false; + } + + /// @brief Retrieves the 3D coordinate at a given index. + /// @tparam T Any integral type (int, unsigned int, size_t, etc.) + /// @param i Index of the vertex. + /// @return The 3D coordinate as a constant reference to Vec3T. + template + inline const Vec3T& + getCoord(const T& i) const + { + static_assert(std::is_integral::value, "Index must be an integral type"); + + return mCoords[i]; + } + + /// @brief Retrieves the cell (triangle) at a given index. + /// @tparam T Any integral type (int, unsigned int, size_t, etc.) + /// @param i Index of the cell. + /// @return Constant reference to the triangle's vertex indices. + template + inline const Vec3I& + getCell(const T& i) const + { + static_assert(std::is_integral::value, "Index must be an integral type"); + + return mCells[i]; + } + + /// @brief Retrieves the 3D coordinates of the vertices forming a + /// primitive (triangle) at a given cell index. + /// @tparam T Any integral type (int, unsigned int, size_t, etc.) + /// @param i Index of the cell (triangle). + /// @return A vector of three Vec3T representing the coordinates of the triangle's vertices. + template + inline std::vector + getPrimitive(const T& i) const + { + static_assert(std::is_integral::value, "Index must be an integral type"); + + const Vec3I cell = mCells[i]; + + return {mCoords[cell[0]], mCoords[cell[1]], mCoords[cell[2]]}; + } + + /// @brief Retrieves the unit normal vector of a cell (triangle) at a given index. + /// @tparam T Any integral type (int, unsigned int, size_t, etc.) + /// @param i Index of the cell. + /// @return The normal vector of the triangle as a Vec3T. + template + inline Vec3T + getNormal(const T& i) const + { + static_assert(std::is_integral::value, "Index must be an integral type"); + + return mNormals[i]; + } + + /// @brief Retrieves the total number of coordinates in the mesh. + /// + /// @return The number of coordinates as an Index. + inline Index64 + coordCount() const + { + return mCoords.size(); + } + + /// @brief Retrieves the total number of cells (triangles) in the mesh. + /// + /// @return The number of cells as an Index. + inline Index64 + cellCount() const + { + return mCells.size(); + } + +private: + struct Edge { + Index mV1, mV2; + + Edge(Index v1, Index v2) + : mV1(std::min(v1, v2)), mV2(std::max(v1, v2)) + { + } + + bool operator<(const Edge& e) const + { + return mV1 < e.mV1 || (mV1 == e.mV1 && mV2 < e.mV2); + } + }; + + inline Vec3T + centroid(Index cellIdx) const + { + const Vec3I cell = mCells[cellIdx]; + return (mCoords[cell[0]] + mCoords[cell[1]] + mCoords[cell[2]]) / 3.0; + } + + inline bool + onSameHalfPlane(const Vec3T &n, const Vec3T& p0, const Vec3T &p1, const Vec3T &p2) + { + return math::Abs(math::Sign(n.dot(p1-p0)) - math::Sign(n.dot(p2-p0))) != 2; + } + + inline void + sortAdjacentCells(const Edge& edge, std::vector& cells) + { + if (cells.size() <= 2) return; + + const Vec3I &base_cell = mCells[cells[0]]; + const Index offset = edge.mV1 + edge.mV2; + + const Index p1Ind = base_cell[0] + base_cell[1] + base_cell[2] - offset; + + const Vec3T &p1 = mCoords[p1Ind], + &n1 = mNormals[cells[0]]; + + const Vec3T p0 = mCoords[edge.mV1]; + + Vec3T bi_nrml = n1.cross(p0 - mCoords[edge.mV2]); + if (bi_nrml.dot(p1 - p0) > 0) + bi_nrml *= ValueT(-1); + + auto windingamount = [&](Index cellIdx) + { + if (cellIdx == 0) return 0.0f; + + const Vec3I &cell = mCells[cellIdx]; + const Index p2Ind = cell[0] + cell[1] + cell[2] - offset; + + const Vec3T &p2 = mCoords[p2Ind], + &n2 = mNormals[cellIdx]; + + const ValueT cos_theta = math::Abs(n1.dot(n2)); + const int sgn = math::Sign(n1.dot(p2 - p1)), + sgn2 = math::Sign(bi_nrml.dot(p2 - p0)); + + return sgn != 0 + ? (sgn == 1 + ? ValueT(1) + ValueT(sgn2) * cos_theta + : ValueT(3) - ValueT(sgn2) * cos_theta + ) + : (onSameHalfPlane(bi_nrml, p0, p1, p2) ? ValueT(0) : ValueT(2)); + }; + + std::sort(cells.begin(), cells.end(), [&](const Index& t1, const Index& t2) { + return windingamount(t1) < windingamount(t2); + }); + } + + // ------------ private members ------------ + + const std::vector& mCoords; + const std::vector& mCells; + + std::vector mNormals; + + std::map> mEdgeMap; + +}; // class TriangleMeshEdgeConnectivity + + +/// @brief Class used to generate a grid of type @c GridType containing a narrow-band level set +/// representation of a dilated mesh (surface mesh dilated by a radius in all directions). +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the mesh vertex and radius type +/// and must be a floating-point scalar. +template +class DilatedMeshVoxelizer { + + using GridPtr = typename GridType::Ptr; + using TreeT = typename GridType::TreeType; + using LeafT = typename TreeT::LeafNodeType; + + using PartitionerT = tools::PointPartitioner; + + using PrismVoxelizer = OpenTriangularPrismVoxelizer; + using WedgeVoxelizer = OpenCapsuleWedgeVoxelizer; + + using MeshConnectivity = TriangleMeshEdgeConnectivity; + + using Vec3T = math::Vec3; + + static_assert(std::is_floating_point::value); + +public: + + /// @brief Constructor for constant radius + /// + /// @param vertices vertices of the mesh in world units + /// @param triangles triangle indices indices in the mesh + /// @param radius radius of all tubes in world units + /// @param voxelSize voxel size in world units + /// @param halfWidth half-width in voxel units + /// @param interrupter pointer to optional interrupter. Use template + /// argument util::NullInterrupter if no interruption is desired. + DilatedMeshVoxelizer(const std::vector& vertices, const std::vector& triangles, + ScalarType radius, float voxelSize, float halfWidth, InterruptT* interrupter) + : mMesh(std::make_shared(MeshConnectivity(vertices, triangles))) + , mVox(voxelSize), mHw(halfWidth), mRad(radius) + , mInterrupter(interrupter) + { + initializeGrid(); + + if constexpr (PtPartition) + initializePartitioner(); + + mPVoxelizer = std::make_shared(mGrid, false); + mWVoxelizer = std::make_shared(mGrid, false); + } + + DilatedMeshVoxelizer(DilatedMeshVoxelizer& other, tbb::split) + : mMesh(other.mMesh), mVox(other.mVox), mHw(other.mHw) + , mRad(other.mRad), mInterrupter(other.mInterrupter) + , mPtPartitioner(other.mPtPartitioner) + { + initializeGrid(); + + mPVoxelizer = std::make_shared(mGrid, false); + mWVoxelizer = std::make_shared(mGrid, false); + } + + void operator()(const tbb::blocked_range& rng) + { + if (!checkInterrupter()) + return; + + if constexpr (PtPartition) { + for (size_t i = rng.begin(); i < rng.end(); ++i) + for (auto it = mPtPartitioner->indices(i); it; ++it) + voxelizeTriangle(*it); + } else { + for (size_t i = rng.begin(); i < rng.end(); ++i) + voxelizeTriangle(i); + } + } + + void join(DilatedMeshVoxelizer& other) + { + tools::CsgUnionOp op(other.mGrid->tree(), Steal()); + tree::DynamicNodeManager nodeManager(mGrid->tree()); + nodeManager.foreachTopDown(op, true); + + other.mGrid = nullptr; + } + + inline Index64 bucketSize() const { return mPtPartitioner->size(); } + + inline Index64 cellSize() const { return mMesh->cellCount(); } + + inline GridPtr getGrid() const { return mGrid; } + +private: + + inline bool + affinelyIndependent(const Vec3T& p1, const Vec3T& p2, const Vec3T& p3) const + { + const Vec3T n = (p2-p1).cross(p3-p1); + return !math::isApproxZero(n.x()) + || !math::isApproxZero(n.y()) + || !math::isApproxZero(n.z()); + } + + inline void + voxelizeTriangle(const size_t& i) + { + const Vec3I &cell = mMesh->getCell(i); + const std::vector pts = mMesh->getPrimitive(i); + + // degenerate triangle + if (!affinelyIndependent(pts[0], pts[1], pts[2])) { + voxelizeCapsule(pts[0], pts[1], pts[2]); + return; + } + + // prism + (*mPVoxelizer)(pts[0], pts[1], pts[2], mRad); + + std::vector cellIds; + Vec3T n1, n2; + + // wedges + for (Index j = 0; j < 3; ++j) { + const bool success = mMesh->getAdjacentCells(cell[j], cell[(j+1) % 3], cellIds); + if (success && cellIds[0] == i) { + if (findWedgeNormals(Index(i), j, cellIds, n1, n2)) + (*mWVoxelizer)(pts[j], pts[(j+1) % 3], mRad, n1, n2); + } + } + } + + inline void + voxelizeCapsule(const Vec3T& p1, const Vec3T& p2, const Vec3T& p3) + { + lvlset::CapsuleVoxelizer voxelizer(mGrid, false); + + ScalarType d1 = (p2-p1).lengthSqr(), + d2 = (p3-p2).lengthSqr(), + d3 = (p1-p3).lengthSqr(); + + ScalarType maxd = math::Max(d1, d2, d3); + + if (maxd == d1) + voxelizer(p1, p2, mRad); + else if (maxd == d2) + voxelizer(p2, p3, mRad); + else + voxelizer(p3, p1, mRad); + } + + inline bool + findWedgeNormals(const Index& cellIdx, const Index& vIdx, + const std::vector& cellIds, Vec3T& n1, Vec3T& n2) const + { + if (cellIds.size() == 1) + return findWedgeNormals1(cellIdx, vIdx, n1, n2); + else if (cellIds.size() == 2) + return findWedgeNormals2(cellIdx, vIdx, cellIds[1], n1, n2); + else if (cellIds.size() > 2) + return findWedgeNormals3(cellIdx, vIdx, cellIds, n1, n2); + + return false; + } + + inline bool + findWedgeNormals1(const Index& cellIdx, const Index& vIdx, + Vec3T& n1, Vec3T& n2) const + { + const Vec3I &cell = mMesh->getCell(cellIdx); + const Vec3T &p1 = mMesh->getCoord(cell[vIdx]), + &p2 = mMesh->getCoord(cell[(vIdx+1) % 3]), + &p3 = mMesh->getCoord(cell[(vIdx+2) % 3]); + + const Vec3T &n = mMesh->getNormal(cellIdx); + + n1 = n.cross(p2-p1).unitSafe(); + if (n1.dot(p3-p1) < 0) n1 *= -1.0f; + + n2 = n1; + + return true; + } + + inline bool + findWedgeNormals2(const Index& cellIdx, const Index& vIdx, + const Index& cellIdx2, Vec3T& n1, Vec3T& n2) const + { + const Vec3I &cell = mMesh->getCell(cellIdx), + &cell2 = mMesh->getCell(cellIdx2); + + const Index cIdx2 = cell2[0] + cell2[1] + cell2[2] - cell[vIdx] - cell[(vIdx+1) % 3]; + + const Vec3T &p1 = mMesh->getCoord(cell[vIdx]), + &p2 = mMesh->getCoord(cell[(vIdx+1) % 3]), + &p3 = mMesh->getCoord(cell[(vIdx+2) % 3]), + &p4 = mMesh->getCoord(cIdx2); + + const Vec3T &nrml1 = mMesh->getNormal(cellIdx), + &nrml2 = mMesh->getNormal(cellIdx2); + + n1 = nrml1.cross(p2-p1).unitSafe(), + n2 = nrml2.cross(p2-p1).unitSafe(); + + if (n1.dot(p3-p1) < 0) n1 *= -1.0f; + if (n2.dot(p4-p1) < 0) n2 *= -1.0f; + + return true; + } + + inline bool + findWedgeNormals3(const Index& cellIdx, const Index& vIdx, + const std::vector& cellIds, Vec3T& n1, Vec3T& n2) const + { + const Vec3I &cell = mMesh->getCell(cellIdx); + + const Index64 n = cellIds.size(); + const Index offset = cell[vIdx] + cell[(vIdx+1) % 3]; + + for (Index64 i = 0; i < n; ++i) { + const Vec3I &cell0 = mMesh->getCell(cellIds[i]), + &cell1 = mMesh->getCell(cellIds[(i+1) % n]), + &cell2 = mMesh->getCell(cellIds[(i+2) % n]); + + const Index cIdx0 = cell0[0] + cell0[1] + cell0[2] - offset, + cIdx1 = cell1[0] + cell1[1] + cell1[2] - offset, + cIdx2 = cell2[0] + cell2[1] + cell2[2] - offset; + + const Vec3T &p0 = mMesh->getCoord(cIdx0), + &p1 = mMesh->getCoord(cIdx1), + &p2 = mMesh->getCoord(cIdx2); + + Vec3T nrml0 = mMesh->getNormal(cellIds[i]), + nrml1 = mMesh->getNormal(cellIds[(i+1) % n]); + + if (nrml0.dot(p1-p0) > 0) nrml0 *= ScalarType(-1); + if (nrml1.dot(p0-p1) > 0) nrml1 *= ScalarType(-1); + + if (nrml0.dot(p2-p0) > 0 || nrml1.dot(p2-p1) > 0) + continue; + + Index vIdxi; + if (cell0[0] == cell[vIdx]) + vIdxi = cell0[1] == cell[(vIdx+1) % 3] ? 0 : 2; + else if (cell0[1] == cell[vIdx]) + vIdxi = cell0[2] == cell[(vIdx+1) % 3] ? 1 : 0; + else + vIdxi = cell0[0] == cell[(vIdx+1) % 3] ? 2 : 1; + + return findWedgeNormals2(cellIds[i], vIdxi, cellIds[(i+1) % n], n1, n2); + } + + return false; + } + + inline void + computeCentroids(std::vector& centroids) + { + centroids.resize(mMesh->cellCount()); + + tbb::parallel_for(tbb::blocked_range(0, centroids.size()), + [&](const tbb::blocked_range& r) { + for (size_t i = r.begin(); i != r.end(); ++i) { + const std::vector prim = mMesh->getPrimitive(i); + centroids[i] = (prim[0] + prim[1] + prim[2]) / ScalarType(3); + } + }); + } + + inline void + initializeGrid() + { + mGrid = createLevelSet(mVox, mHw); + } + + inline void + initializePartitioner() + { + std::vector centroids; + computeCentroids(centroids); + + lvlset::PointArray points(centroids); + + mPtPartitioner = std::make_shared(); + mPtPartitioner->construct(points, mGrid->transform()); + } + + inline bool + checkInterrupter() + { + if (util::wasInterrupted(mInterrupter)) { + openvdb::thread::cancelGroupExecution(); + return false; + } + return true; + } + + // ------------ private members ------------ + + std::shared_ptr mMesh; + + const float mVox, mHw; + + const ScalarType mRad; + + InterruptT* mInterrupter; + + GridPtr mGrid; + + std::shared_ptr mPtPartitioner; + + std::shared_ptr mPVoxelizer; + std::shared_ptr mWVoxelizer; + +}; // class DilatedMeshVoxelizer + +} // namespace lvlset + + +// ------------ createLevelSetDilatedMesh ------------- // + +template +typename GridType::Ptr +createLevelSetDilatedMesh( + const std::vector>& vertices, const std::vector& triangles, + ScalarType radius, float voxelSize, float halfWidth, InterruptT* interrupter) +{ + static_assert(std::is_floating_point::value); + + using GridPtr = typename GridType::Ptr; + using ValueT = typename GridType::ValueType; + + using Voxelizer = typename lvlset::DilatedMeshVoxelizer; + + static_assert(std::is_floating_point::value, + "createLevelSetDilatedMesh must return a scalar grid"); + + if (voxelSize <= 0) OPENVDB_THROW(ValueError, "voxel size must be positive"); + if (halfWidth <= 0) OPENVDB_THROW(ValueError, "half-width must be positive"); + + Voxelizer op(vertices, triangles, radius, voxelSize, halfWidth, interrupter); + + const tbb::blocked_range triangleRange(0, op.bucketSize()); + tbb::parallel_reduce(triangleRange, op); + + GridPtr grid = op.getGrid(); + tools::pruneLevelSet(grid->tree()); + + return grid; +} + +template +typename GridType::Ptr +createLevelSetDilatedMesh( + const std::vector>& vertices, const std::vector& quads, + ScalarType radius, float voxelSize, float halfWidth, InterruptT* interrupter) +{ + static_assert(std::is_floating_point::value); + + using ValueT = typename GridType::ValueType; + + static_assert(std::is_floating_point::value, + "createLevelSetDilatedMesh must return a scalar grid"); + + if (voxelSize <= 0) OPENVDB_THROW(ValueError, "voxel size must be positive"); + if (halfWidth <= 0) OPENVDB_THROW(ValueError, "half-width must be positive"); + + const Index64 n = quads.size(); + std::vector triangles(2*n); + + tbb::parallel_for(tbb::blocked_range(0, n), + [&](const tbb::blocked_range& r) { + for (Index64 i = r.begin(); i != r.end(); ++i) { + const Vec4I& q = quads[i]; + triangles[i] = Vec3I(q.x(), q.y(), q.z()); + triangles[i + n] = Vec3I(q.x(), q.z(), q.w()); + } + }); + + return createLevelSetDilatedMesh( + vertices, triangles, radius, voxelSize, halfWidth, interrupter); +} + +template +typename GridType::Ptr +createLevelSetDilatedMesh(const std::vector>& vertices, + const std::vector& triangles, const std::vector& quads, + ScalarType radius, float voxelSize, float halfWidth, InterruptT* interrupter) +{ + static_assert(std::is_floating_point::value); + + using ValueT = typename GridType::ValueType; + + static_assert(std::is_floating_point::value, + "createLevelSetDilatedMesh must return a scalar grid"); + + if (voxelSize <= 0) OPENVDB_THROW(ValueError, "voxel size must be positive"); + if (halfWidth <= 0) OPENVDB_THROW(ValueError, "half-width must be positive"); + + if (quads.empty()) + return createLevelSetDilatedMesh( + vertices, triangles, radius, voxelSize, halfWidth, interrupter); + + const Index64 tn = triangles.size(), qn = quads.size(); + const Index64 qn2 = tn + qn; + std::vector tris(tn + 2*qn); + + tbb::parallel_for(tbb::blocked_range(0, tn), + [&](const tbb::blocked_range& r) { + for (Index64 i = r.begin(); i != r.end(); ++i) { + tris[i] = triangles[i]; + } + }); + + tbb::parallel_for(tbb::blocked_range(0, qn), + [&](const tbb::blocked_range& r) { + for (Index64 i = r.begin(); i != r.end(); ++i) { + const Vec4I& q = quads[i]; + tris[i + tn] = Vec3I(q.x(), q.y(), q.z()); + tris[i + qn2] = Vec3I(q.x(), q.z(), q.w()); + } + }); + + return createLevelSetDilatedMesh( + vertices, tris, radius, voxelSize, halfWidth, interrupter); +} + + +//////////////////////////////////////// + + +// Explicit Template Instantiation + +#ifdef OPENVDB_USE_EXPLICIT_INSTANTIATION + +#ifdef OPENVDB_INSTANTIATE_LEVELSETDILATEDMESH +#include +#endif + +#define _FUNCTION(TreeT) \ + Grid::Ptr createLevelSetDilatedMesh>(const std::vector&, \ + const std::vector&, float, float, float, util::NullInterrupter*) +OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION) +#undef _FUNCTION + +#define _FUNCTION(TreeT) \ + Grid::Ptr createLevelSetDilatedMesh>(const std::vector&, \ + const std::vector&, float, float, float, util::NullInterrupter*) +OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION) +#undef _FUNCTION + +#define _FUNCTION(TreeT) \ + Grid::Ptr createLevelSetDilatedMesh>(const std::vector&, \ + const std::vector&, const std::vector&, float, float, float, \ + util::NullInterrupter*) +OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION) +#undef _FUNCTION + +#endif // OPENVDB_USE_EXPLICIT_INSTANTIATION + +} // namespace tools +} // namespace OPENVDB_VERSION_NAME +} // namespace openvdb + +#endif // OPENVDB_TOOLS_LEVELSETDILATEDMESH_HAS_BEEN_INCLUDED diff --git a/openvdb/openvdb/tools/LevelSetTubes.h b/openvdb/openvdb/tools/LevelSetTubes.h new file mode 100644 index 0000000000..d088508285 --- /dev/null +++ b/openvdb/openvdb/tools/LevelSetTubes.h @@ -0,0 +1,1652 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: Apache-2.0 +/// +/// @author Greg Hurst +/// +/// @file LevelSetTubes.h +/// +/// @brief Generate a narrow-band level set of a capsule, tapered capsule, and tube complex. +/// +/// @note By definition a level set has a fixed narrow band width +/// (the half width is defined by LEVEL_SET_HALF_WIDTH in Types.h), +/// whereas an SDF can have a variable narrow band width. + +#ifndef OPENVDB_TOOLS_LEVELSETTUBES_HAS_BEEN_INCLUDED +#define OPENVDB_TOOLS_LEVELSETTUBES_HAS_BEEN_INCLUDED + +#include "ConvexVoxelizer.h" +#include "PointPartitioner.h" +#include "Prune.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + + +namespace openvdb { +OPENVDB_USE_VERSION_NAMESPACE +namespace OPENVDB_VERSION_NAME { +namespace tools { + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a capsule (tube with constant radius and sphere caps). +/// +/// @param pt1 First capsule endpoint in world units. +/// @param pt2 Second capsule endpoint in world units. +/// @param radius Radius of the capsule in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param interrupter Interrupter adhering to the util::NullInterrupter interface. +/// @param threaded If true multi-threading is enabled (true by default). +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the capsule endpoint and radius type +/// and must be a floating-point scalar. +template +typename GridType::Ptr +createLevelSetCapsule(const math::Vec3& pt1, const math::Vec3& pt2, + ScalarType radius, float voxelSize, float halfWidth = float(LEVEL_SET_HALF_WIDTH), + InterruptT* interrupter = nullptr, bool threaded = true); + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a capsule (tube with constant radius and sphere caps). +/// +/// @param pt1 First capsule endpoint in world units. +/// @param pt2 Second capsule endpoint in world units. +/// @param radius Radius of the capsule in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param threaded If true multi-threading is enabled (true by default). +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the capsule endpoint and radius type +/// and must be a floating-point scalar. +template +typename GridType::Ptr +createLevelSetCapsule(const math::Vec3& pt1, const math::Vec3& pt2, + ScalarType radius, float voxelSize, float halfWidth = float(LEVEL_SET_HALF_WIDTH), + bool threaded = true); + + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a tapered capsule (tube with sphere caps and different radii at both ends, +/// or equivalently the convex hull of two spheres with possibly different centers and radii). +/// +/// @param pt1 First tapered capsule endpoint in world units. +/// @param pt2 Second tapered capsule endpoint in world units. +/// @param radius1 Radius of the tapered capsule at @c pt1 in world units. +/// @param radius2 Radius of the tapered capsule at @c pt2 in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param interrupter Interrupter adhering to the util::NullInterrupter interface. +/// @param threaded If true multi-threading is enabled (true by default). +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the tapered capsule endpoint and radius type +/// and must be a floating-point scalar. +template +typename GridType::Ptr +createLevelSetTaperedCapsule(const math::Vec3& pt1, const math::Vec3& pt2, + ScalarType radius1, ScalarType radius2, + float voxelSize, float halfWidth = float(LEVEL_SET_HALF_WIDTH), + InterruptT* interrupter = nullptr, bool threaded = true); + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a tapered capsule (tube with sphere caps and different radii at both ends, +/// or equivalently the convex hull of two spheres with possibly different centers and radii). +/// +/// @param pt1 First tapered capsule endpoint in world units. +/// @param pt2 Second tapered capsule endpoint in world units. +/// @param radius1 Radius of the tapered capsule at @c pt1 in world units. +/// @param radius2 Radius of the tapered capsule at @c pt2 in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param threaded If true multi-threading is enabled (true by default). +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the tapered capsule endpoint and radius type +/// and must be a floating-point scalar. +template +typename GridType::Ptr +createLevelSetTaperedCapsule(const math::Vec3& pt1, const math::Vec3& pt2, + ScalarType radius1, ScalarType radius2, float voxelSize, + float halfWidth = float(LEVEL_SET_HALF_WIDTH), bool threaded = true); + +/// @brief Different policies when creating a tube complex with varying radii +/// @details +///
+///
TUBE_VERTEX_RADII +///
Specify that the tube complex radii are per-vertex, +/// meaning each tube has different radii at its two endpoints +/// and the complex is a collection of tapered capsules. +/// +///
TUBE_SEGMENT_RADII +///
Specify that the tube complex radii are per-segment, +/// meaning each tube has a constant radius and the complex is a collection of capsules. +/// +///
TUBE_AUTOMATIC +///
Specify that the only valid setting is to be chosen, +/// defaulting to the per-vertex policy if both are valid. +///
+enum TubeRadiiPolicy { TUBE_AUTOMATIC = 0, TUBE_VERTEX_RADII, TUBE_SEGMENT_RADII }; + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a tube complex (a collection of capsules defined by endpoint coordinates and segment indices). +/// +/// @param vertices Endpoint vertices in the tube complex in world units. +/// @param segments Segment indices in the tube complex. +/// @param radius Radius of all tubes in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param interrupter Interrupter adhering to the util::NullInterrupter interface. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the capsule complex vertex and radius type +/// and must be a floating-point scalar. +template +typename GridType::Ptr +createLevelSetTubeComplex(const std::vector>& vertices, + const std::vector& segments, ScalarType radius, float voxelSize, + float halfWidth = float(LEVEL_SET_HALF_WIDTH), InterruptT* interrupter = nullptr); + +/// @brief Return a grid of type @c GridType containing a narrow-band level set +/// representation of a tube complex (a collection of tubes defined by endpoint coordinates, segment indices, and radii). +/// +/// @param vertices Endpoint vertices in the tube complex in world units. +/// @param segments Segment indices in the tube complex. +/// @param radii Radii specification for all tubes in world units. +/// @param voxelSize Voxel size in world units. +/// @param halfWidth Half the width of the narrow band, in voxel units. +/// @param TubeRadiiPolicy Policies: per-segment, per-vertex, or automatic (default). +/// @param interrupter Interrupter adhering to the util::NullInterrupter interface. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the capsule complex vertex and radius type +/// and must be a floating-point scalar. +/// @note The automatic @c TubeRadiiPolicy chooses the valid per-segment or per-vertex policy, +/// defaulting to per-vertex if both are valid. +template +typename GridType::Ptr +createLevelSetTubeComplex(const std::vector>& vertices, + const std::vector& segments, const std::vector& radii, float voxelSize, + float halfWidth = float(LEVEL_SET_HALF_WIDTH), TubeRadiiPolicy radii_policy = TUBE_AUTOMATIC, + InterruptT* interrupter = nullptr); + + +//////////////////////////////////////// + + +namespace lvlset { + +/// @brief Class used to generate a grid of type @c GridType containing a narrow-band level set +/// representation of a capsule. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +template +class CapsuleVoxelizer + : public ConvexVoxelizer< + GridType, + CapsuleVoxelizer, + InterruptT> +{ + using GridPtr = typename GridType::Ptr; + + using BaseT = ConvexVoxelizer< + GridType, + CapsuleVoxelizer, + InterruptT + >; + + using BaseT::mXYData; + using BaseT::tileCeil; + + using ValueT = typename BaseT::ValueT; + using Vec3T = typename BaseT::Vec3T; + using Vec2T = typename BaseT::Vec2T; + +public: + + friend class ConvexVoxelizer< + GridType, + CapsuleVoxelizer, + InterruptT + >; + + /// @brief Constructor + /// + /// @param grid scalar grid to populate the level set in + /// @param threaded center of the sphere in world units + /// @param interrupter pointer to optional interrupter. Use template + /// argument util::NullInterrupter if no interruption is desired. + /// + /// @note The voxel size and half width are determined from the input grid, + /// meaning the voxel size and background value need to be set prior to voxelization + CapsuleVoxelizer(GridPtr& grid, const bool& threaded = true, + InterruptT* interrupter = nullptr) + : BaseT(grid, threaded, interrupter) + { + } + + /// @brief Create a capsule + /// + /// @param pt1 first endpoint of the capsule in world units + /// @param pt2 second endpoint of the capsule in world units + /// @param radius radius of the capsule in world units + template + void + operator()(const math::Vec3& pt1, + const math::Vec3& pt2, const ScalarType& r) + { + static_assert(std::is_floating_point::value); + + initialize(pt1, pt2, r); + + BaseT::iterate(); + } + +private: + + // setXYRangeData inputs: + // step: step size in index space (default is 1) + // + // setXYRangeData class member inputs: + // mPt1, mPt2, mORad: a tube with points p1, p2 and radius r (padded for halfwidth) + // + // setXYRangeData class member outputs: + // mXs: list of x ordinates to scan over + // mYbs: list of bottom y ordinates to start scanning with, for each x + // mYts: list of top y ordinates to stop scanning at, for each x + // + // + // This routine projects the tube on the xy-plane, giving a stadium shape. + // It detemines the x-scan range, and the y-range for each x-value. + // The x-range is divided into intervals depending on if each y endpoint hits a circle or line. + // + // The x-range is partitioned by ordinates a0, a1, a2, a3, a4, a5 + // and based on some cases listed below, some ai will be permuted. + // + // The x-range intervals have a few flavors depending on + // * if the stadium points right-down or right-up + // * the bottom circle ends before the top circle starts, or not + // + // 2D projection of capsule onto xy-plane, giving a stadium: + // + // ∧ y ******** + // | *** *** + // | * * + // | / | * + // | / | * + // | / | * + // | / | p2 * + // | / | / \ r * + // | / | / \ *| + // | / | / \ *| + // | / |/ * | + // | / /| / | | + // | / / | / | | + // | / / | / | | + // | * / | / | | + // | *| / | / | | + // | *| / | / | | + // | * | / | / | | + // | * | p1 | | | + // | * | / | | | + // | |*| / | | | + // | |*| / | | | + // | | | * | | | + // | | |*** *** | | | | + // | | | ******** | | | | + // | | | | | | | + // | | | | | | | + // | a0 a1 a2 a3 a4 a5 + // | x + // └-----------------------------------------------------------> + // + // In this schematic, we have a0 < a1 < a2 < a3 < a4 < a5, + // but, for examples, it's possible for a2 and a3 to swap if the stadium is more vertical + + inline void + setXYRangeData(const Index& step = 1) + { + const ValueT stepv = ValueT(step); + + // short circuit a vertical cylinder + if (mIsVertical) { + mXYData.reset(mX1 - mORad, mX1 + mORad, step); + + for (ValueT x = tileCeil(mX1 - mORad, step); x <= mX1 + mORad; x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), circle1Top(x)); + return; + } + + const ValueT v = math::Min(mORad, mORad * math::Abs(mYdiff)/mXYNorm); + + const ValueT a0 = mX1 - mORad, + a1 = mX1 - v, + a2 = mX1 + v, + a3 = mX2 - v, + a4 = mX2 + v, + a5 = mX2 + mORad; + + const ValueT tc0 = tileCeil(a0, step), + tc1 = tileCeil(a1, step), + tc2 = tileCeil(a2, step), + tc3 = tileCeil(a3, step), + tc4 = tileCeil(a4, step); + + mXYData.reset(a0, a5, step); + + for (ValueT x = tc0; x <= a1; x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), circle1Top(x)); + + if (!math::isApproxZero(mXdiff)) { + if (mY1 > mY2) { + for (ValueT x = tc1; x <= math::Min(a2, a3); x += stepv) + mXYData.expandYRange(x, lineBottom(x), circle1Top(x)); + } else { + for (ValueT x = tc1; x <= math::Min(a2, a3); x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), lineTop(x)); + } + } + + if (a2 < a3) { + for (ValueT x = tc2; x <= a3; x += stepv) + mXYData.expandYRange(x, lineBottom(x), lineTop(x)); + } else { + if (mY2 <= mY1) { + for (ValueT x = tc3; x <= a2; x += stepv) + mXYData.expandYRange(x, circle2Bottom(x), circle1Top(x)); + } else { + for (ValueT x = tc3; x <= a2; x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), circle2Top(x)); + } + } + + if (!math::isApproxZero(mXdiff)) { + if (mY1 > mY2) { + for (ValueT x = math::Max(tc2, tc3); x <= a4; x += stepv) + mXYData.expandYRange(x, circle2Bottom(x), lineTop(x)); + } else { + for (ValueT x = math::Max(tc2, tc3); x <= a4; x += stepv) + mXYData.expandYRange(x, lineBottom(x), circle2Top(x)); + } + } + + for (ValueT x = tc4; x <= a5; x += stepv) + mXYData.expandYRange(x, circle2Bottom(x), circle2Top(x)); + + mXYData.trim(); + } + + // distance in index space + inline ValueT + signedDistance(const Vec3T& p) const + { + const Vec3T w = p - mPt1; + const ValueT dot = w.dot(mV); + + // carefully short circuit with a fuzzy tolerance, which avoids division by small mVLenSqr + if (dot <= math::Tolerance::value()) + return w.length() - mRad; + + if (dot >= mVLenSqr) + return (p - mPt2).length() - mRad; + + const ValueT t = w.dot(mV)/mVLenSqr; + + return (w - t * mV).length() - mRad; + } + + inline bool + tileCanFit(const Index& dim) const + { + return mRad >= BaseT::halfWidth() + ValueT(0.70711) * (ValueT(dim)-ValueT(1)); + } + + // vertical capsule + // for a given x,y pair, find the z-range of a tube + // z-range is bottom sphere cap to the top sphere cap in vertical case + std::function capsuleBottomTopVertical = + [this](ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) + { + zb = BaseT::sphereBottom(mX1, mY1, math::Min(mZ1, mZ2), mORad, x, y); + zt = BaseT::sphereTop(mX2, mY2, math::Max(mZ1, mZ2), mORad, x, y); + + return std::isfinite(zb) && std::isfinite(zt); + }; + + // non vertical capsule + // for a given x,y pair, find the z-range of a tube + // first find the z-range as if its an infinite cylinder + // then for each z-range endpoint, determine if it should be on a sphere cap + std::function capsuleBottomTop = + [this](ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) + { + ValueT cylptb, cylptt; + if (!infiniteCylinderBottomTop(cylptb, cylptt, x, y)) + return false; + + const ValueT dotb = (Vec3T(x, y, cylptb) - mPt1).dot(mV); + const ValueT dott = (Vec3T(x, y, cylptt) - mPt1).dot(mV); + + if (dotb < 0) + zb = sphere1Bottom(x, y); + else if (dotb > mVLenSqr) + zb = sphere2Bottom(x, y); + else + zb = cylptb; + + if (dott < 0) + zt = sphere1Top(x, y); + else if (dott > mVLenSqr) + zt = sphere2Top(x, y); + else + zt = cylptt; + + return std::isfinite(zb) && std::isfinite(zt); + }; + + // assumes capsule is not vertical! + inline bool + infiniteCylinderBottomTop(ValueT& cylptb, ValueT& cylptt, const ValueT& x, const ValueT& y) const + { + const Vec2T q(x, y); + + const Vec2T qproj = mPt12d + mV2d*((q - mPt12d).dot(mV2d))/mXYNorm2; + + const ValueT t = mX1 != mX2 ? (qproj[0] - mX1)/mXdiff : (qproj[1] - mY1)/mYdiff; + + const Vec3T qproj3D = mPt1 + t * mV; + + const ValueT d2 = (q - qproj).lengthSqr(); + + // outside of cylinder's 2D projection + if (mORad2 < d2) + return false; + + const ValueT h = math::Sqrt((mORad2 - d2) * mVLenSqr/mXYNorm2); + + cylptb = qproj3D[2] - h; + cylptt = qproj3D[2] + h; + + return true; + } + + inline ValueT + lineBottom(const ValueT& x) const + { + return mY1 + (mYdiff*(x-mX1) - mORad * mXYNorm)/mXdiff; + } + + inline ValueT + lineTop(const ValueT& x) const + { + return mY1 + (mYdiff*(x-mX1) + mORad * mXYNorm)/mXdiff; + } + + inline ValueT + circle1Bottom(const ValueT& x) const + { + return BaseT::circleBottom(mX1, mY1, mORad, x); + } + + inline ValueT + circle1Top(const ValueT& x) const + { + return BaseT::circleTop(mX1, mY1, mORad, x); + } + + inline ValueT + circle2Bottom(const ValueT& x) const + { + return BaseT::circleBottom(mX2, mY2, mORad, x); + } + + inline ValueT + circle2Top(const ValueT& x) const + { + return BaseT::circleTop(mX2, mY2, mORad, x); + } + + inline ValueT + sphere1Bottom(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereBottom(mX1, mY1, mZ1, mORad, x, y); + } + + inline ValueT + sphere1Top(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereTop(mX1, mY1, mZ1, mORad, x, y); + } + + inline ValueT + sphere2Bottom(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereBottom(mX2, mY2, mZ2, mORad, x, y); + } + + inline ValueT + sphere2Top(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereTop(mX2, mY2, mZ2, mORad, x, y); + } + + // world space points and radius inputs + // initializes class members in index space + template + inline void + initialize(const math::Vec3& pt1, + const math::Vec3& pt2, const ScalarType& r) + { + const ValueT vx = BaseT::voxelSize(), + hw = BaseT::halfWidth(); + + if (pt1[0] <= pt2[0]) { + mPt1 = Vec3T(pt1)/vx; + mPt2 = Vec3T(pt2)/vx; + } else { + mPt1 = Vec3T(pt2)/vx; + mPt2 = Vec3T(pt1)/vx; + } + + mRad = ValueT(r)/vx; + + // padded radius used to populate the outer halfwidth of the sdf + mORad = mRad + hw; + mORad2 = mORad * mORad; + + mV = mPt2 - mPt1; + mVLenSqr = mV.lengthSqr(); + + mX1 = mPt1[0]; mY1 = mPt1[1]; mZ1 = mPt1[2]; + mX2 = mPt2[0]; mY2 = mPt2[1]; mZ2 = mPt2[2]; + + mXdiff = mX2 - mX1; + mYdiff = mY2 - mY1; + mZdiff = mZ2 - mZ1; + + mPt12d = Vec2T(mX1, mY1); + mPt22d = Vec2T(mX2, mY2); + mV2d = mPt22d - mPt12d; + + mXYNorm2 = math::Pow2(mXdiff) + math::Pow2(mYdiff); + mXYNorm = math::Sqrt(mXYNorm2); + mIsVertical = math::isApproxZero(mXYNorm); + + BaseT::bottomTop = mIsVertical ? capsuleBottomTopVertical : capsuleBottomTop; + } + + // ------------ private members ------------ + + // tube data -- populated via initialize() + + Vec3T mPt1, mPt2, mV; + + Vec2T mPt12d, mPt22d, mV2d; + + ValueT mORad, mORad2, mRad, mVLenSqr, mXdiff, mYdiff, mZdiff, mXYNorm, mXYNorm2; + + ValueT mX1, mY1, mZ1, mX2, mY2, mZ2; + + bool mIsVertical; + +}; // class CapsuleVoxelizer + + +/// @brief Class used to generate a grid of type @c GridType containing a narrow-band level set +/// representation of a tapered capsule. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +template +class TaperedCapsuleVoxelizer + : public ConvexVoxelizer< + GridType, + TaperedCapsuleVoxelizer, + InterruptT> +{ + using GridPtr = typename GridType::Ptr; + + using BaseT = ConvexVoxelizer< + GridType, + TaperedCapsuleVoxelizer, + InterruptT + >; + + using BaseT::mXYData; + using BaseT::tileCeil; + + using ValueT = typename BaseT::ValueT; + using Vec3T = typename BaseT::Vec3T; + using Vec2T = typename BaseT::Vec2T; + +public: + + friend class ConvexVoxelizer< + GridType, + TaperedCapsuleVoxelizer, + InterruptT + >; + + /// @brief Constructor + /// + /// @param grid scalar grid to populate the level set in + /// @param threaded center of the sphere in world units + /// @param interrupter pointer to optional interrupter. Use template + /// argument util::NullInterrupter if no interruption is desired. + /// + /// @note The voxel size and half width are determined from the input grid, + /// meaning the voxel size and background value need to be set prior to voxelization + TaperedCapsuleVoxelizer(GridPtr& grid, const bool& threaded = true, + InterruptT* interrupter = nullptr) + : BaseT(grid, threaded, interrupter) + { + } + + /// @brief Create a tapered capsule + /// + /// @param pt1 first endpoint of the tapered capsule in world units + /// @param pt2 second endpoint of the tapered capsule in world units + /// @param radius1 radius of the tapered capsule at @c pt1 in world units + /// @param radius2 radius of the tapered capsule at @c pt2 in world units + template + void + operator()(const math::Vec3& pt1, const math::Vec3& pt2, + const ScalarType& radius1, const ScalarType& radius2) + { + static_assert(std::is_floating_point::value); + + // fail on degenerate inputs for now + + // ball + if ((pt1 - pt2).lengthSqr() <= math::Pow2(radius1 - radius2)) { + OPENVDB_THROW(RuntimeError, + "The tapered capsule is degenerate, in this case it is a ball. Consider using the CapsuleVoxelizer class instead."); + } + + // capsule + if (math::Abs(radius1 - radius2) < 0.001f*BaseT::voxelSize()) { + OPENVDB_THROW(RuntimeError, + "The tapered capsule is degenerate, in this case it is a capsule. Consider using the CapsuleVoxelizer class instead."); + } + + initialize(pt1, pt2, radius1, radius2); + + BaseT::iterate(); + } + +private: + + inline void + setXYRangeData(const Index& step = 1) + { + const ValueT stepv = ValueT(step); + + // short circuit when one circle is in the other + if (mXYNorm2 <= mRdiff2) { + if (mX1 - mORad1 <= mX2 - mORad2) { + mXYData.reset(mX1 - mORad1, mX1 + mORad1, step); + for (ValueT x = tileCeil(mX1 - mORad1, step); x <= mX1 + mORad1; x += stepv) + mXYData.expandYRange(x, circle1Bottom(x), circle1Top(x)); + } else { + mXYData.reset(mX2 - mORad2, mX2 + mORad2, step); + for (ValueT x = tileCeil(mX2 - mORad2, step); x <= mX2 + mORad2; x += stepv) + mXYData.expandYRange(x, circle2Bottom(x), circle2Top(x)); + } + return; + } + + mXYData.reset( + math::Min(mX1 - mORad1, mX2 - mORad2), + math::Max(mX1 + mORad1, mX2 + mORad2), + step + ); + + Vec2T p1t, p2t, p1b, p2b; + const bool success = pullyPoints(p1t, p2t, p1b, p2b); + + if (success) { + setLineXYData(p1t, p2t, stepv); + setLineXYData(p1b, p2b, stepv); + + setCircleXYData(p1t, p1b, stepv, true); // mPt1 + setCircleXYData(p2t, p2b, stepv, false); // mPt2 + } + + mXYData.trim(); + } + + // https://en.wikipedia.org/wiki/Belt_problem#Pulley_problem + inline bool + pullyPoints(Vec2T& p1t, Vec2T& p2t, Vec2T& p1b, Vec2T& p2b) const + { + const ValueT diff = mXYNorm2 - mRdiff2; + if (diff < 0) + return false; + + const ValueT alpha = std::atan2(mYdiff, mXdiff), + theta = std::atan2(math::Sqrt(diff), mRdiff); + + const ValueT sin1 = math::Sin(theta + alpha), sin2 = math::Sin(theta - alpha), + cos1 = math::Cos(theta + alpha), cos2 = math::Cos(theta - alpha); + + p1t.x() = mX1 + mORad1*cos1; p1t.y() = mY1 + mORad1*sin1; + p2t.x() = mX2 + mORad2*cos1; p2t.y() = mY2 + mORad2*sin1; + p2b.x() = mX2 + mORad2*cos2; p2b.y() = mY2 - mORad2*sin2; + p1b.x() = mX1 + mORad1*cos2; p1b.y() = mY1 - mORad1*sin2; + + return true; + } + + inline void + setLineXYData(const Vec2T& q1, const Vec2T& q2, const ValueT& step) + { + if (math::Abs(q1.x() - q2.x()) < math::Tolerance::value()) { + ValueT x = tileCeil(q1.x(), step); + if (q1.x() == x) { + mXYData.expandYRange(x, q1.y()); + mXYData.expandYRange(x, q2.y()); + } + } else { + const bool q1_left = q1.x() < q2.x(); + const ValueT &x1 = q1_left ? q1.x() : q2.x(), + &y1 = q1_left ? q1.y() : q2.y(), + &x2 = q1_left ? q2.x() : q1.x(), + &y2 = q1_left ? q2.y() : q1.y(); + + ValueT m = (y2 - y1)/(x2 - x1), + x = tileCeil(x1, step), + y = y1 + m * (x-x1), + delta = m * step; + for (; x <= x2; x += step, y += delta) + mXYData.expandYRange(x, y); + } + } + + inline void + setCircleXYData(const Vec2T& q1, const Vec2T& q2, + const ValueT& step, const bool is_pt1) + { + const Vec3T &p1 = is_pt1 ? mPt1 : mPt2; + const ValueT &r1 = is_pt1 ? mORad1 : mORad2; + + const std::vector xs = { + tileCeil(p1.x() - r1, step), + tileCeil(math::Min(q1.x(), q2.x()), step), + tileCeil(math::Max(q1.x(), q2.x()), step), + tileCeil(p1.x() + r1, step) + }; + + for (int i = 0; i < int(xs.size()-1); ++i) { + setCircleHiXYData(xs[i], xs[i+1], step, is_pt1); + setCircleLoXYData(xs[i], xs[i+1], step, is_pt1); + } + } + + inline void + setCircleHiXYData(const ValueT& x1, const ValueT& x2, + const ValueT& step, const bool& is_pt1) + { + const ValueT x_test = static_cast(math::Floor(ValueT(0.5)*(x1+x2))); + + if (is_pt1) { + // if |x2-x1| is small, our test point might be too close to the pulley point + if (math::Abs(x2-x1) < 5 || mXYData.getYMax(x_test) <= circle1Top(x_test)) { + for (ValueT x = x1; x < x2; x += step) + mXYData.expandYMax(x, circle1Top(x)); + } + } else { + if (math::Abs(x2-x1) < 5 || mXYData.getYMax(x_test) <= circle2Top(x_test)) { + for (ValueT x = x1; x < x2; x += step) + mXYData.expandYMax(x, circle2Top(x)); + } + } + } + + inline void + setCircleLoXYData(const ValueT& x1, const ValueT& x2, + const ValueT& step, const bool& is_pt1) + { + const ValueT x_test = static_cast(math::Floor(ValueT(0.5)*(x1+x2))); + + if (is_pt1) { + // if |x2-x1| is small, our test point might be too close to the pulley point + if (math::Abs(x2-x1) < 5 || mXYData.getYMin(x_test) >= circle1Bottom(x_test)) { + for (ValueT x = x1; x < x2; x += step) + mXYData.expandYMin(x, circle1Bottom(x)); + } + } else { + if (math::Abs(x2-x1) < 5 || mXYData.getYMin(x_test) >= circle2Bottom(x_test)) { + for (ValueT x = x1; x < x2; x += step) + mXYData.expandYMin(x, circle2Bottom(x)); + } + } + } + + // Round Cone: https://iquilezles.org/articles/distfunctions/ + // distance in index space + inline ValueT + signedDistance(const Vec3T& p) const + { + const Vec3T w = p - mPt1; + const ValueT y = w.dot(mV), + z = y - mVLenSqr, + x2 = (w*mVLenSqr - mV*y).lengthSqr(), + y2 = y*y*mVLenSqr, + z2 = z*z*mVLenSqr, + k = mRdiff2*x2; // should multiply by sgn(mRdiff), but it's always positive + + if (ValueT(math::Sign(z))*mA2*z2 >= k) + return math::Sqrt(x2 + z2)*mInvVLenSqr - mRad2; + + if (ValueT(math::Sign(y))*mA2*y2 <= k) + return math::Sqrt(x2 + y2)*mInvVLenSqr - mRad1; + + return (math::Sqrt(x2*mA2*mInvVLenSqr) + y*mRdiff)*mInvVLenSqr - mRad1; + } + + inline bool + tileCanFit(const Index& dim) const + { + // we know mRad1 >= mRad2 + return mRad1 >= BaseT::halfWidth() + ValueT(0.70711) * (ValueT(dim)-ValueT(1)); + } + + std::function taperedCapsuleBottomTop = + [this](ValueT& zb, ValueT& zt, const ValueT& x, const ValueT& y) + { + const Vec2T q(x, y); + + const bool in_ball1 = (q - mPt12d).lengthSqr() <= mORad1Sqr, + in_ball2 = (q - mPt22d).lengthSqr() <= mORad2Sqr; + + if (in_ball1) { + zt = sphere1Top(x, y); + zb = 2.0f*mZ1 - zt; + } + + if (in_ball2) { + if (in_ball1) { + const ValueT zt2 = sphere2Top(x, y), + zb2 = 2.0f*mZ2 - zt2; + + zt = math::Max(zt, zt2); + zb = math::Min(zb, zb2); + } else { + zt = sphere2Top(x, y); + zb = 2.0f*mZ2 - zt; + } + } + + // attempt to short circuit when top and bottom hits are on sphere caps + if (in_ball1 || in_ball2) { + const double ht = mConeD.dot(Vec3d(x,y,zt) - mConeV); + // top point is in one of the half spaces pointing away from the cone + if (mH1 > ht || ht > mH2) { + const double hb = mConeD.dot(Vec3d(x,y,zb) - mConeV); + // bottom point is in one of the half spaces pointing away from the cone + if (mH1 > hb || hb > mH2) + return true; + } + } + + ValueT conezb = 0.0f, conezt = 0.0f; + int cint_cnt; + openConeFrustumBottomTop(conezb, conezt, cint_cnt, x, y); + + if (in_ball1 && in_ball2) { + if (cint_cnt == 2) { + zb = math::Min(zb, conezb); + zt = math::Max(zt, conezt); + } else if (cint_cnt == 1) { + zb = math::Min(zb, conezb); + zt = math::Max(zt, conezb); + } + + // cint_cnt == 0 implies zb and zt are already set correctly + return true; + } + + if (cint_cnt == 2 || (!in_ball1 && !in_ball2)) { + zb = conezb; zt = conezt; + return cint_cnt == 2; + } + + // we know at this point that in_ball1 ^ in_ball2 + + // zt and zb have been assigned values already based on the ball their in + if (cint_cnt == 0) + return true; + + // cint_cnt == 1 and we're only in one ball + + zt = math::Max(zt, conezb); + zb = math::Min(zb, conezb); + + return true; + }; + + // https://www.geometrictools.com/Documentation/IntersectionLineCone.pdf + // works in double precision in case the cone tapers very slowly (r1 ~ r2) + inline void + openConeFrustumBottomTop(ValueT& conezb, ValueT& conezt, int& cint_cnt, + const ValueT& x, const ValueT& y) const + { + cint_cnt = 0; + const Vec3d p(double(x), double(y), mRayZ); + const Vec3d diff = p - mConeV; + + const double ddotdiff = mConeD.dot(diff); + + const double c1 = mGamma * diff.z() - mConeD.z() * ddotdiff; + const double c0 = ddotdiff * ddotdiff - mGamma * diff.lengthSqr(); + + if (mC2 != 0.0f) { + const double delta = c1*c1 - c0*mC2; + if (delta >= 0.0f) { + const double sqrt = math::Sqrt(delta); + const double t1 = mC2Inv*(-c1 + sqrt); + if (validFrustumRange(t1, ddotdiff)) { + cint_cnt++; + conezb = ValueT(mRayZ - t1); + } + const double t2 = mC2Inv*(-c1 - sqrt); + if (validFrustumRange(t2, ddotdiff)) { + cint_cnt++; + if (cint_cnt == 2 && t1 > t2) + conezt = ValueT(mRayZ - t2); + else { + conezt = conezb; + conezb = ValueT(mRayZ - t2); + } + } + } + } else if (c1 != 0.0f) { + const double t = -c0/(2.0f*c1); + if (validFrustumRange(t, ddotdiff)) { + cint_cnt = 1; + conezb = ValueT(mRayZ - t); + } + } + + // ignore the c2 == c1 == 0 case, where the ray is on the boundary of the cone + } + + inline bool + validFrustumRange(const double& t, const double& ddotdiff) const + { + const double h = ddotdiff - t * mConeD.z(); + + return mH1 <= h && h <= mH2; + } + + inline ValueT + circle1Bottom(const ValueT& x) const + { + return BaseT::circleBottom(mX1, mY1, mORad1, x); + } + + inline ValueT + circle1Top(const ValueT& x) const + { + return BaseT::circleTop(mX1, mY1, mORad1, x); + } + + inline ValueT + circle2Bottom(const ValueT& x) const + { + return BaseT::circleBottom(mX2, mY2, mORad2, x); + } + + inline ValueT + circle2Top(const ValueT& x) const + { + return BaseT::circleTop(mX2, mY2, mORad2, x); + } + + inline ValueT + sphere1Bottom(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereBottom(mX1, mY1, mZ1, mORad1, x, y); + } + + inline ValueT + sphere1Top(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereTop(mX1, mY1, mZ1, mORad1, x, y); + } + + inline ValueT + sphere2Bottom(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereBottom(mX2, mY2, mZ2, mORad2, x, y); + } + + inline ValueT + sphere2Top(const ValueT& x, const ValueT& y) const + { + return BaseT::sphereTop(mX2, mY2, mZ2, mORad2, x, y); + } + + // world space points and radius inputs + // initializes class members in index space + template + inline void + initialize(const math::Vec3& pt1, const math::Vec3& pt2, + const ScalarType& r1, const ScalarType& r2) + { + const ValueT vx = BaseT::voxelSize(), + hw = BaseT::halfWidth(); + + // enforce mRad1 > mRad2 + if (r2 <= r1) { + mPt1 = Vec3T(pt1)/vx; + mPt2 = Vec3T(pt2)/vx; + mRad1 = ValueT(r1)/vx; + mRad2 = ValueT(r2)/vx; + } else { + mPt1 = Vec3T(pt2)/vx; + mPt2 = Vec3T(pt1)/vx; + mRad1 = ValueT(r2)/vx; + mRad2 = ValueT(r1)/vx; + } + + // padded radii used to populate the outer halfwidth of the sdf + mORad1 = mRad1 + hw; + mORad2 = mRad2 + hw; + mORad1Sqr = mORad1 * mORad1; + mORad2Sqr = mORad2 * mORad2; + + mV = mPt2 - mPt1; + mVLenSqr = mV.lengthSqr(); + mInvVLenSqr = mVLenSqr != ValueT(0) ? ValueT(1)/mVLenSqr : ValueT(1); + + mX1 = mPt1[0]; mY1 = mPt1[1]; mZ1 = mPt1[2]; + mX2 = mPt2[0]; mY2 = mPt2[1]; mZ2 = mPt2[2]; + + mXdiff = mX2 - mX1; + mYdiff = mY2 - mY1; + mZdiff = mZ2 - mZ1; + + mPt12d = Vec2T(mX1, mY1); + mPt22d = Vec2T(mX2, mY2); + mV2d = mPt22d - mPt12d; + + mXYNorm2 = math::Pow2(mXdiff) + math::Pow2(mYdiff); + mXYNorm = math::Sqrt(mXYNorm2); + mIXYNorm2 = mXYNorm2 != ValueT(0) ? ValueT(1)/mXYNorm2 : ValueT(1); + + // mRdiff is non negative + mRdiff = mRad1 - mRad2; + mRdiff2 = mRdiff * mRdiff; + mA2 = mVLenSqr - mRdiff2; + + // we assume + // alpha is solid angle of cone + // r1 != r2, since the object is not a capsule + // P > abs(r1-r2), since one ball is not contained in the other + // we work in double precision in case csc is large + const double P = mV.length(), + csc = P/mRdiff, // csc(alpha/2) + sin = mRdiff/P; // sin(alpha/2) + mGamma = 1.0 - mRdiff2/(P*P); // cos(alpha/2)^2 + mH1 = mORad2*(csc-sin); + mH2 = mORad1*(csc-sin); + + mConeD = -((Vec3d)mV).unitSafe(); + mConeV = (Vec3d)mPt1 - (double)mORad1 * csc * mConeD; + + mRayZ = math::Max(mZ1 + mORad1, mZ2 + mORad2) + 2.0; + mC2 = math::Pow2(mConeD.z()) - mGamma; + mC2Inv = mC2 != 0.0 ? 1.0/mC2 : 1.0; + + BaseT::bottomTop = taperedCapsuleBottomTop; + } + + // ------------ private members ------------ + + // tapered capsule data -- populated via initialize() + + Vec3T mPt1, mPt2, mV; + + Vec2T mPt12d, mPt22d, mV2d; + + ValueT mORad1, mORad2, mORad1Sqr, mORad2Sqr, mRad1, mRad2, mVLenSqr, mInvVLenSqr, + mXdiff, mYdiff, mZdiff, mXYNorm, mXYNorm2, mIXYNorm2, mRdiff, mRdiff2, mA2; + + ValueT mX1, mY1, mZ1, mX2, mY2, mZ2; + + // some members are stored explicitly as double because when + // the cone tapers very slowly (r1 ~ r2), then csc(cone_angle) can be very large + + Vec3d mConeV, mConeD; + + double mRayZ, mGamma, mC2, mC2Inv, mH1, mH2; + +}; // class TaperedCapsuleVoxelizer + + +/// @brief Class used to generate a grid of type @c GridType containing a narrow-band level set +/// representation of a tube complex. +/// +/// @note @c GridType::ValueType must be a floating-point scalar. +/// @note @c ScalarType represents the capsule complex vertex and radius type +/// and must be a floating-point scalar. +/// @note Setting @c PerSegmentRadii to @c true gives a complex of capsules and a complex of tapered capsules otherwise. +template +class TubeComplexVoxelizer { + + using GridPtr = typename GridType::Ptr; + using TreeT = typename GridType::TreeType; + using LeafT = typename TreeT::LeafNodeType; + + using PartitionerT = tools::PointPartitioner; + + using Vec3T = math::Vec3; + + static_assert(std::is_floating_point::value); + +public: + + /// @brief Constructor for constant radius + /// + /// @param vertices endpoint vertices in the tube complex in world units + /// @param segments segment indices in the tube complex + /// @param radius radius of all tubes in world units + /// @param voxelSize voxel size in world units + /// @param background background value in voxel units + /// @param interrupter pointer to optional interrupter. Use template + /// argument util::NullInterrupter if no interruption is desired. + TubeComplexVoxelizer(const std::vector& vertices, const std::vector& segments, + ScalarType radius, float voxelSize, float halfWidth, InterruptT* interrupter) + : mVox(voxelSize), mHw(halfWidth), mRad(radius) + , mCoords(vertices), mCells(segments), mRadii(getEmptyVector()) + , mInterrupter(interrupter) + { + initializeGrid(); + initializePartitioner(); + } + + /// @brief Constructor for varying radii + /// + /// @param vertices endpoint vertices in the tube complex in world units + /// @param segments segment indices in the tube complex + /// @param radii radii specification for all tubes in world units + /// @param voxelSize voxel size in world units + /// @param halfWidth half-width value in voxel units + /// @param interrupter pointer to optional interrupter. Use template + /// argument util::NullInterrupter if no interruption is desired. + /// + /// @note If @c PerSegmentRadii is set to @c true then @c segments and @c radii must have + /// the same size. If @c PerSegmentRadii is set to @c false then @c vertices and @c radii + /// must have the same size. + TubeComplexVoxelizer(const std::vector& vertices, const std::vector& segments, + const std::vector& radii, float voxelSize, float halfWidth, + InterruptT* interrupter) + : mVox(voxelSize), mHw(halfWidth), mRad(0.0) + , mCoords(vertices), mCells(segments), mRadii(radii) + , mInterrupter(interrupter) + { + if constexpr (PerSegmentRadii) { + if (mCells.size() != mRadii.size()) + OPENVDB_THROW(RuntimeError, + "TubeComplexVoxelizer needs the same number of segments and radii"); + } else { + if (mCoords.size() != mRadii.size()) + OPENVDB_THROW(RuntimeError, + "TubeComplexVoxelizer needs the same number of coordinates and radii"); + } + + initializeGrid(); + initializePartitioner(); + } + + TubeComplexVoxelizer(TubeComplexVoxelizer& other, tbb::split) + : mVox(other.mVox), mHw(other.mHw), mRad(other.mRad) + , mCoords(other.mCoords), mCells(other.mCells), mRadii(other.mRadii) + , mPtPartitioner(other.mPtPartitioner), mInterrupter(other.mInterrupter) + { + initializeGrid(); + } + + template + inline typename std::enable_if_t + operator()(const tbb::blocked_range& rng) + { + if (!checkInterrupter()) + return; + + if (mRadii.size() == 0) + constantRadiusVoxelize(rng); + else + perSegmentRadiusVoxelize(rng); + } + + template + inline typename std::enable_if_t + operator()(const tbb::blocked_range& rng) + { + if (!checkInterrupter()) + return; + + if (mRadii.size() == 0) + constantRadiusVoxelize(rng); + else + perVertexRadiusVoxelize(rng); + } + + void join(TubeComplexVoxelizer& other) + { + tools::CsgUnionOp op(other.mGrid->tree(), Steal()); + tree::DynamicNodeManager nodeManager(mGrid->tree()); + nodeManager.foreachTopDown(op, true); + + other.mGrid = nullptr; + } + + inline Index64 bucketSize() const { return mPtPartitioner->size(); } + + inline GridPtr getGrid() const { return mGrid; } + +private: + + /// TODO increase performance by not creating parts of caps that overlap with other tubes: + /// + /// * Determine segment adjacency + /// * Create _open_ cylinders and conical frustums + /// * Create _open_ & _partial_ caps + /// + /// This should help speed up creation of complexes that contain + /// a bunch of short segments that approximate a smooth curve. + /// + /// Idea is similar to _open_ prisms and _open_ wedges speeding up + /// creation of dilated mesh level sets from finely triangulated meshes. + + inline void + constantRadiusVoxelize(const tbb::blocked_range& rng) + { + CapsuleVoxelizer voxelizer(mGrid, false); + + for (size_t i = rng.begin(); i < rng.end(); ++i) { + for (auto it = mPtPartitioner->indices(i); it; ++it) { + const Index k = *it; + const Vec2I& cell = mCells[k]; + voxelizer(mCoords[cell[0]], mCoords[cell[1]], mRad); + } + } + } + + inline void + perSegmentRadiusVoxelize(const tbb::blocked_range& rng) + { + CapsuleVoxelizer voxelizer(mGrid, false); + + for (size_t i = rng.begin(); i < rng.end(); ++i) { + for (auto it = mPtPartitioner->indices(i); it; ++it) { + const Index k = *it; + const Vec2I& cell = mCells[k]; + voxelizer(mCoords[cell[0]], mCoords[cell[1]], mRadii[k]); + } + } + } + + inline void + perVertexRadiusVoxelize(const tbb::blocked_range& rng) + { + TaperedCapsuleVoxelizer tc_voxelizer(mGrid, false); + + CapsuleVoxelizer c_voxelizer(mGrid, false); + + for (size_t i = rng.begin(); i < rng.end(); ++i) { + for (auto it = mPtPartitioner->indices(i); it; ++it) { + const Index k = *it; + const Vec2I& cell = mCells[k]; + const Index32 &i = cell.x(), &j = cell.y(); + + const Vec3T &pt1 = mCoords[i], &pt2 = mCoords[j]; + const ScalarType &r1 = mRadii[i], &r2 = mRadii[j]; + + if ((pt1 - pt2).lengthSqr() <= math::Pow2(r1-r2)) { // ball + if (r1 >= r2) + c_voxelizer(pt1, pt1, r1); + else + c_voxelizer(pt2, pt2, r2); + } else if (math::Abs(r1-r2) < 0.001*mVox) { // capsule + c_voxelizer(pt1, pt2, r1); + } else { + tc_voxelizer(pt1, pt2, r1, r2); + } + } + } + } + + inline void + computeCentroids(std::vector& centroids) + { + const Index n = Index(mCoords.size()); + + centroids.resize(mCells.size()); + + tbb::parallel_for(tbb::blocked_range(0, centroids.size()), + [&](const tbb::blocked_range& r) { + for (size_t i = r.begin(); i != r.end(); ++i) { + const Vec2I &cell = mCells[i]; + + if (cell[0] >= n || cell[1] >= n) + OPENVDB_THROW(ValueError, "out of bounds index"); + + centroids[i] = ScalarType(0.5) * (mCoords[cell[0]] + mCoords[cell[1]]); + } + }); + } + + inline void + initializeGrid() + { + mGrid = createLevelSet(mVox, mHw); + } + + inline void + initializePartitioner() + { + std::vector centroids; + computeCentroids(centroids); + + lvlset::PointArray points(centroids); + + mPtPartitioner = std::make_shared(); + mPtPartitioner->construct(points, mGrid->transform()); + } + + inline bool + checkInterrupter() + { + if (util::wasInterrupted(mInterrupter)) { + openvdb::thread::cancelGroupExecution(); + return false; + } + return true; + } + + static const std::vector& + getEmptyVector() { + static const std::vector empty; + return empty; + } + + // ------------ private members ------------ + + const float mVox, mHw; + + const ScalarType mRad; + + const std::vector& mCoords; + const std::vector& mCells; + const std::vector& mRadii; + + std::shared_ptr mPtPartitioner; + + InterruptT* mInterrupter; + + GridPtr mGrid; + +}; // class TubeComplexVoxelizer + +} // namespace lvlset + + +// ------------ createLevelSetTubeComplex ------------- // + +// constant radius + +template +typename GridType::Ptr +createLevelSetTubeComplex(const std::vector>& vertices, + const std::vector& segments, ScalarType radius, + float voxelSize, float halfWidth, InterruptT* interrupter) +{ + static_assert(std::is_floating_point::value); + + using GridPtr = typename GridType::Ptr; + using ValueT = typename GridType::ValueType; + + using ComplexVoxelizer = typename lvlset::TubeComplexVoxelizer; + + static_assert(std::is_floating_point::value, + "createLevelSetTubeComplex must return a scalar grid"); + + if (voxelSize <= 0) OPENVDB_THROW(ValueError, "voxel size must be positive"); + if (halfWidth <= 0) OPENVDB_THROW(ValueError, "half-width must be positive"); + + ComplexVoxelizer op(vertices, segments, radius, voxelSize, halfWidth, interrupter); + + const tbb::blocked_range segmentRange(0, op.bucketSize()); + tbb::parallel_reduce(segmentRange, op); + + GridPtr tubegrid = op.getGrid(); + tools::pruneLevelSet(tubegrid->tree()); + + return tubegrid; +} + +// varying radii + +template +typename GridType::Ptr +createLevelSetTubeComplex(const std::vector>& vertices, + const std::vector& segments, const std::vector& radii, + float voxelSize, float halfWidth, TubeRadiiPolicy radii_policy, InterruptT* interrupter) +{ + static_assert(std::is_floating_point::value); + + using GridPtr = typename GridType::Ptr; + using ValueT = typename GridType::ValueType; + + using CapsuleComplexVoxelizer = typename lvlset::TubeComplexVoxelizer; + using TaperedCapsuleComplexVoxelizer = typename lvlset::TubeComplexVoxelizer; + + static_assert(std::is_floating_point::value, + "createLevelSetTubeComplex must return a scalar grid"); + + if (voxelSize <= 0) OPENVDB_THROW(ValueError, "voxel size must be positive"); + if (halfWidth <= 0) OPENVDB_THROW(ValueError, "half-width must be positive"); + + if (radii_policy == TUBE_AUTOMATIC) { + if (vertices.size() != radii.size() && segments.size() != radii.size()) + OPENVDB_THROW(ValueError, + "createLevelSetTubeComplex requires equal number of vertices and radii, or segments and radii, with automatic radii policy."); + } + + TubeRadiiPolicy resolved_radii_policy = radii_policy == TUBE_AUTOMATIC + ? (vertices.size() == radii.size() ? TUBE_VERTEX_RADII : TUBE_SEGMENT_RADII) + : radii_policy; + + GridPtr tubegrid; + + switch(resolved_radii_policy) { + case TUBE_VERTEX_RADII : { + if (vertices.size() != radii.size()) + OPENVDB_THROW(ValueError, + "createLevelSetTubeComplex requires equal number of vertices and radii with per-vertex radii policy."); + + TaperedCapsuleComplexVoxelizer op(vertices, segments, radii, + voxelSize, halfWidth, interrupter); + + const tbb::blocked_range segmentRange(0, op.bucketSize()); + tbb::parallel_reduce(segmentRange, op); + + tubegrid = op.getGrid(); + break; + } + case TUBE_SEGMENT_RADII : { + if (segments.size() != radii.size()) + OPENVDB_THROW(ValueError, + "createLevelSetTubeComplex requires equal number of segments and radii with per-segment radii policy."); + + CapsuleComplexVoxelizer op(vertices, segments, radii, + voxelSize, halfWidth, interrupter); + + const tbb::blocked_range segmentRange(0, op.bucketSize()); + tbb::parallel_reduce(segmentRange, op); + + tubegrid = op.getGrid(); + break; + } + default: + OPENVDB_THROW(ValueError, "Invalid tube radii policy."); + } + + tools::pruneLevelSet(tubegrid->tree()); + + return tubegrid; +} + + +// ------------ createLevelSetCapsule ------------- // + +template +typename GridType::Ptr +createLevelSetCapsule(const math::Vec3& pt1, const math::Vec3& pt2, + ScalarType radius, float voxelSize, float halfWidth, + InterruptT* interrupter, bool threaded) +{ + static_assert(std::is_floating_point::value); + + using GridPtr = typename GridType::Ptr; + using ValueT = typename GridType::ValueType; + + using CapsuleVoxelizer = typename lvlset::CapsuleVoxelizer; + + static_assert(std::is_floating_point::value, + "createLevelSetCapsule must return a scalar grid"); + + if (voxelSize <= 0) OPENVDB_THROW(ValueError, "voxel size must be positive"); + if (halfWidth <= 0) OPENVDB_THROW(ValueError, "half-width must be positive"); + + GridPtr grid = createLevelSet(voxelSize, halfWidth); + + CapsuleVoxelizer voxelizer(grid, threaded, interrupter); + voxelizer(pt1, pt2, radius); + + return grid; +} + +template +typename GridType::Ptr +createLevelSetCapsule(const math::Vec3& pt1, const math::Vec3& pt2, + ScalarType radius, float voxelSize, float halfWidth, bool threaded) +{ + return createLevelSetCapsule( + pt1, pt2, radius, voxelSize, halfWidth, nullptr, threaded); +} + + +// ------------ createLevelSetTaperedCapsule ------------- // + +template +typename GridType::Ptr +createLevelSetTaperedCapsule(const math::Vec3& pt1, const math::Vec3& pt2, + ScalarType radius1, ScalarType radius2, + float voxelSize, float halfWidth, InterruptT* interrupter, bool threaded) +{ + static_assert(std::is_floating_point::value); + + using GridPtr = typename GridType::Ptr; + using ValueT = typename GridType::ValueType; + + using CapsuleVoxelizer = typename lvlset::CapsuleVoxelizer; + using TaperedCapsuleVoxelizer = typename lvlset::TaperedCapsuleVoxelizer; + + static_assert(std::is_floating_point::value, + "createLevelSetTaperedCapsule must return a scalar grid"); + + if (voxelSize <= 0) OPENVDB_THROW(ValueError, "voxel size must be positive"); + if (halfWidth <= 0) OPENVDB_THROW(ValueError, "half-width must be positive"); + + GridPtr grid = createLevelSet(voxelSize, halfWidth); + + if ((pt1 - pt2).lengthSqr() <= math::Pow2(radius1 - radius2)) { // ball + + CapsuleVoxelizer voxelizer(grid, threaded, interrupter); + if (radius1 >= radius2) + voxelizer(pt1, pt1, radius1); + else + voxelizer(pt2, pt2, radius2); + + } else if (math::Abs(radius1 - radius2) < 0.001*voxelSize) { // capsule + + CapsuleVoxelizer voxelizer(grid, threaded, interrupter); + voxelizer(pt1, pt2, radius1); + + } else { // tapered capsule + + TaperedCapsuleVoxelizer voxelizer(grid, threaded, interrupter); + voxelizer(pt1, pt2, radius1, radius2); + } + + return grid; +} + +template +typename GridType::Ptr +createLevelSetTaperedCapsule(const math::Vec3& pt1, const math::Vec3& pt2, + ScalarType radius1, ScalarType radius2, float voxelSize, float halfWidth, bool threaded) +{ + return createLevelSetTaperedCapsule( + pt1, pt2, radius1, radius2, voxelSize, halfWidth, nullptr, threaded); +} + + +//////////////////////////////////////// + + +// Explicit Template Instantiation + +#ifdef OPENVDB_USE_EXPLICIT_INSTANTIATION + +#ifdef OPENVDB_INSTANTIATE_LEVELSETTUBES +#include +#endif + +#define _FUNCTION(TreeT) \ + Grid::Ptr createLevelSetTubeComplex>(const std::vector&, \ + const std::vector&, float, float, float, util::NullInterrupter*) +OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION) +#undef _FUNCTION + +#define _FUNCTION(TreeT) \ + Grid::Ptr createLevelSetTubeComplex>(const std::vector&, \ + const std::vector&, const std::vector&, float, float, TubeRadiiPolicy, \ + util::NullInterrupter*) +OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION) +#undef _FUNCTION + +#define _FUNCTION(TreeT) \ + Grid::Ptr createLevelSetCapsule>(const Vec3s&, const Vec3s&, \ + float, float, float, util::NullInterrupter*, bool) +OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION) +#undef _FUNCTION + +#define _FUNCTION(TreeT) \ + Grid::Ptr createLevelSetTaperedCapsule>(const Vec3s&, const Vec3s&, \ + float, float, float, float, util::NullInterrupter*, bool) +OPENVDB_REAL_TREE_INSTANTIATE(_FUNCTION) +#undef _FUNCTION + +#endif // OPENVDB_USE_EXPLICIT_INSTANTIATION + +} // namespace tools +} // namespace OPENVDB_VERSION_NAME +} // namespace openvdb + +#endif // OPENVDB_TOOLS_LEVELSETTUBES_HAS_BEEN_INCLUDED diff --git a/openvdb/openvdb/unittest/CMakeLists.txt b/openvdb/openvdb/unittest/CMakeLists.txt index b1e04ebcfe..f42019cdd2 100644 --- a/openvdb/openvdb/unittest/CMakeLists.txt +++ b/openvdb/openvdb/unittest/CMakeLists.txt @@ -121,6 +121,7 @@ else() TestLeafOrigin.cc TestLevelSetFilter.cc TestLevelSetRayIntersector.cc + TestLevelSetTubes.cc TestLevelSetUtil.cc TestLinearInterp.cc TestMaps.cc diff --git a/openvdb/openvdb/unittest/TestLevelSetTubes.cc b/openvdb/openvdb/unittest/TestLevelSetTubes.cc new file mode 100644 index 0000000000..9ba9bf23bf --- /dev/null +++ b/openvdb/openvdb/unittest/TestLevelSetTubes.cc @@ -0,0 +1,620 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include +#include +#include +#include + +#include + +class TestLevelSetTubes: public ::testing::Test +{ +public: + void SetUp() override { openvdb::initialize(); } + void TearDown() override { openvdb::uninitialize(); } +}; + +namespace { + +template +void +testCapsuleMeasures(const openvdb::Vec3s& p1, const openvdb::Vec3s& p2, const float& r, + typename GridT::Ptr& grid, float error = 0.01f) +{ + using namespace openvdb; + + const float pi = math::pi(), + l = (p2-p1).length(); + + const float area = 2.0f*l*pi*r + 4.0f*pi*math::Pow2(r), + volume = l*pi*math::Pow2(r) + 4.0f*pi*math::Pow(r,3)/3.0f, + totGaussCurv = 4.0f*pi, // Gauss-Bonnet + totMeanCurv = pi*l + 4.0f*pi*r; + + tools::LevelSetMeasure m(*grid); + + EXPECT_NEAR(m.area(true), area, area*error); + EXPECT_NEAR(m.volume(true), volume, volume*error); + EXPECT_NEAR(m.totGaussianCurvature(true), totGaussCurv, totGaussCurv*error); + EXPECT_NEAR(m.totMeanCurvature(true), totMeanCurv, totMeanCurv*error); +} + +template +void +testTaperedCapsuleMeasures(const openvdb::Vec3s& p1, const openvdb::Vec3s& p2, + const float& r1, const float& r2, typename GridT::Ptr& grid, + float error = 0.01f, bool test_gauss_curvature = true) +{ + using namespace openvdb; + + const float pi = math::pi(), epsilon = math::Tolerance().value(), + l = (p2-p1).length(), R = math::Max(r1, r2); + + const float l2 = math::Pow2(l), r12 = math::Pow2(r1), + r22 = math::Pow2(r2), rdiff2 = math::Pow2(r1-r2); + + float area, volume, totGaussCurv, totMeanCurv; + + if (l <= math::Max(epsilon, R)) { + // degenerate case: sphere + area = 4.0f*pi*math::Pow2(R), + volume = 4.0f/3.0f*pi*math::Pow3(R), + totGaussCurv = 4.0f*pi, // Gauss-Bonnet + totMeanCurv = 4.0f*pi*R; + } else { + // formulas assume non-degenerate object, i.e. l > 0 && r1 > 0 && r2 > 0 + area = pi/l * (l + r1 + r2) * (rdiff2 + l*(r1 + r2)), + volume = pi/(3.0f*l) * ((l2 + rdiff2)*(r12 + r1*r2 + r22) + 2.0f*l*(r1*r12 + r2*r22)), + totGaussCurv = 4.0f*pi, // Gauss-Bonnet + totMeanCurv = pi/l * (math::Pow2(l) + math::Pow2(r1 - r2) + 2.0f*l*(r1 + r2)); + } + + tools::LevelSetMeasure m(*grid); + + EXPECT_NEAR(m.area(true), area, area*error); + EXPECT_NEAR(m.volume(true), volume, volume*error); + EXPECT_NEAR(m.totMeanCurvature(true), totMeanCurv, totMeanCurv*error); + + // objects with sharp corners or edges tend to give answers far from the analytical solution + // in cases like these, e.g. cone-tip, we can toggle this test off + if (test_gauss_curvature) { + EXPECT_NEAR(m.totGaussianCurvature(true), totGaussCurv, totGaussCurv*error); + } +} + +} + + +TEST_F(TestLevelSetTubes, testCapsule) +{ + using namespace openvdb; + + using GridT = FloatGrid; + using GridPtr = typename GridT::Ptr; + + // generic tests + { + const Vec3s p1(15.8f, 13.2f, 16.7f), p2(4.3f, 7.9f, -4.8f); + const float r = 4.3f; + + const float voxelSize = 0.1f, width = 3.25f; + const Coord ijk(int(p1[0]/voxelSize), + int(p1[1]/voxelSize), + int(p1[2]/voxelSize));// inside + + GridPtr ls = tools::createLevelSetCapsule(p1, p2, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_TRUE(ls->tree().isValueOff(ijk)); + EXPECT_NEAR(-ls->background(), ls->tree().getValue(ijk), 1e-6); + EXPECT_NEAR(voxelSize*width, ls->background(), 1e-6); + EXPECT_NEAR(ls->background(),ls->tree().getValue(Coord(0)), 1e-6); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + + // test measures + { + const Vec3s p1(15.8f, 13.2f, 16.7f), p2(4.3f, 7.9f, -4.8f), + p3(-3.0f, -7.4f, 8.9f), p4(-2.7f, 8.9f, 30.4f); + const float r1 = 4.3f, r2 = 1.2f, r3 = 2.1f, r4 = 3.6f; + + const float voxelSize = 0.1f; + + GridPtr ls1 = tools::createLevelSetCapsule(p1, p2, r1, voxelSize); + GridPtr ls2 = tools::createLevelSetCapsule(p1, p3, r2, voxelSize); + GridPtr ls3 = tools::createLevelSetCapsule(p3, p2, r3, voxelSize); + GridPtr ls4 = tools::createLevelSetCapsule(p2, p4, r4, voxelSize); + + testCapsuleMeasures(p1, p2, r1, ls1); + testCapsuleMeasures(p1, p3, r2, ls2); + testCapsuleMeasures(p3, p2, r3, ls3); + testCapsuleMeasures(p2, p4, r4, ls4); + } + + // degenerate case: sphere + { + const Vec3s p1(4.3f, 7.9f, -4.8f), p2(4.3f, 7.9f, -4.8f); + const float r = 4.3f; + + const float voxelSize = 0.1f; + + GridPtr ls = tools::createLevelSetCapsule(p1, p2, r, voxelSize); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + testCapsuleMeasures(p1, p2, r, ls); + } + + // degenerate case: line + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(1.0f, 0.0f, 0.0f); + const float r = 0.0f; + + const float voxelSize = 0.1f, width = 2.0f; + + GridPtr ls = tools::createLevelSetCapsule(p1, p2, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() == 117 /* 90 + 27 */); + EXPECT_TRUE(tools::minMax(ls->tree()).min() >= 0.0f); // no zero crossings + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + + // degenerate case: point + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(0.0f, 0.0f, 0.0f); + const float r = 0.0f; + + const float voxelSize = 0.1f, width = 2.0f; + + GridPtr ls = tools::createLevelSetCapsule(p1, p2, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() == 27 /* 3x3x3 grid */); + EXPECT_TRUE(tools::minMax(ls->tree()).min() >= 0.0f); // no zero crossings + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + + // degenerate case: negative radius --> empty grid + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(0.0f, 0.0f, 0.0f); + const float r = -10.0f; + + const float voxelSize = 0.1f, width = 2.0f; + + GridPtr ls = tools::createLevelSetCapsule(p1, p2, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() == 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + +}// testCapsule + +TEST_F(TestLevelSetTubes, testTaperedCapsule) +{ + using namespace openvdb; + + using GridT = FloatGrid; + using GridPtr = typename GridT::Ptr; + + // generic tests + { + const Vec3s p1(15.8f, 13.2f, 16.7f), p2(4.3f, 7.9f, -4.8f); + const float r1 = 4.3f, r2 = 1.2f; + + const float voxelSize = 0.1f, width = 3.25f; + const Coord ijk(int(p1[0]/voxelSize), + int(p1[1]/voxelSize), + int(p1[2]/voxelSize));// inside + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r1, r2, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_TRUE(ls->tree().isValueOff(ijk)); + EXPECT_NEAR(-ls->background(), ls->tree().getValue(ijk), 1e-6); + EXPECT_NEAR(voxelSize*width, ls->background(), 1e-6); + EXPECT_NEAR(ls->background(),ls->tree().getValue(Coord(0)), 1e-6); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + + // test measures + { + const Vec3s p1(15.8f, 13.2f, 16.7f), p2(4.3f, 7.9f, -4.8f), + p3(-3.0f, -7.4f, 8.9f), p4(-2.7f, 8.9f, 30.4f); + const float r1 = 4.3f, r2 = 1.2f, r3 = 2.1f, r4 = 3.6f; + + const float voxelSize = 0.1f; + + GridPtr ls1 = tools::createLevelSetTaperedCapsule(p1, p2, r1, r2, voxelSize); + GridPtr ls2 = tools::createLevelSetTaperedCapsule(p1, p3, r2, r3, voxelSize); + GridPtr ls3 = tools::createLevelSetTaperedCapsule(p3, p2, r3, r4, voxelSize); + GridPtr ls4 = tools::createLevelSetTaperedCapsule(p2, p4, r4, r3, voxelSize); + + testTaperedCapsuleMeasures(p1, p2, r1, r2, ls1); + testTaperedCapsuleMeasures(p1, p3, r2, r3, ls2); + testTaperedCapsuleMeasures(p3, p2, r3, r4, ls3); + testTaperedCapsuleMeasures(p2, p4, r4, r3, ls4); + } + + // degenerate case: capsule + { + const Vec3s p1(4.3f, 7.9f, -4.8f), p2(4.3f, 7.9f, -4.8f); + const float r = 4.3f; + + const float voxelSize = 0.1f; + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r, r, voxelSize); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + testTaperedCapsuleMeasures(p1, p2, r, r, ls); + } + + // degenerate case: sphere by equivalent endpoints + { + const Vec3s p1(4.3f, 7.9f, -4.8f), p2(4.3f, 7.9f, -4.8f); + const float r1 = 4.3f, r2 = 1.2f; + + const float voxelSize = 0.1f; + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r1, r2, voxelSize); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + testTaperedCapsuleMeasures(p1, p2, r1, r2, ls); + } + + // degenerate case: sphere by nested spheres + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(1.0f, 0.0f, 0.0f); + const float r1 = 1.5f, r2 = 0.5f; + + const float voxelSize = 0.1f; + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r1, r2, voxelSize); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + testTaperedCapsuleMeasures(p1, p2, r1, r2, ls); + } + + // degenerate case: cone with sphere cap (tear drop) + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(4.0f, 0.0f, 0.0f); + const float r1 = 1.0f, r2 = 0.0f; + + const float voxelSize = 0.1f; + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r1, r2, voxelSize); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + // don't test total Gaussian curvature + testTaperedCapsuleMeasures(p1, p2, r1, r2, ls, 0.05f, false); + } + + // degenerate case: cone with sphere cap (tear drop) and tip between endpoints + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(4.0f, 0.0f, 0.0f), p2_equiv(2.0f, 0.0f, 0.0f); + const float r1 = 1.0f, r2 = -1.0f, r2_equiv = 0.0f; + + const float voxelSize = 0.1f; + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r1, r2, voxelSize); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + // don't test total Gaussian curvature + testTaperedCapsuleMeasures(p1, p2_equiv, r1, r2_equiv, ls, 0.05f, false); + } + + // degenerate case: line + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(1.0f, 0.0f, 0.0f); + const float r = 0.0f; + + const float voxelSize = 0.1f, width = 2.0f; + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() == 117 /* 90 + 27 */); + EXPECT_TRUE(tools::minMax(ls->tree()).min() >= 0.0f); // no zero crossings + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + + // degenerate case: point + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(0.0f, 0.0f, 0.0f); + const float r = 0.0f; + + const float voxelSize = 0.1f, width = 2.0f; + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() == 27 /* 3x3x3 grid */); + EXPECT_TRUE(tools::minMax(ls->tree()).min() >= 0.0f); // no zero crossings + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + + // degenerate case: negative radius --> empty grid + { + const Vec3s p1(0.0f, 0.0f, 0.0f), p2(0.0f, 0.0f, 0.0f); + const float r = -10.0f; + + const float voxelSize = 0.1f, width = 2.0f; + + GridPtr ls = tools::createLevelSetTaperedCapsule(p1, p2, r, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() == 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + +}// testTaperedCapsule + +TEST_F(TestLevelSetTubes, testTubeComplexConstantRadius) +{ + using namespace openvdb; + + using GridT = FloatGrid; + using GridPtr = typename GridT::Ptr; + + // generic tests + { + const Vec3s p1(15.8f, 13.2f, 16.7f), p2(4.3f, 7.9f, -4.8f), + p3(-3.0f, -7.4f, 8.9f), p4(-2.7f, 8.9f, 30.4f); + const float r = 2.1f; + + const std::vector vertices({p1, p2, p3, p4}); + const std::vector segments({Vec2I(0, 1), Vec2I(0, 2), Vec2I(0, 3)}); + + const float voxelSize = 0.1f, width = 3.25f; + const Coord ijk(int(p1[0]/voxelSize), + int(p1[1]/voxelSize), + int(p1[2]/voxelSize));// inside + + GridPtr ls = tools::createLevelSetTubeComplex(vertices, segments, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_TRUE(ls->tree().isValueOff(ijk)); + EXPECT_NEAR(-ls->background(), ls->tree().getValue(ijk), 1e-6); + EXPECT_NEAR(voxelSize*width, ls->background(), 1e-6); + EXPECT_NEAR(ls->background(),ls->tree().getValue(Coord(0)), 1e-6); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + + // test measures of complex that forms a single straight capsule + { + const std::vector vertices({Vec3s(0.0f, 0.0f, 0.0f), Vec3s(1.0f, 0.0f, 0.0f), + Vec3s(1.5f, 0.0f, 0.0f), Vec3s(3.0f, 0.0f, 0.0f)}); + const std::vector segments({Vec2I(0, 1), Vec2I(1, 2), Vec2I(2, 3)}); + + const float r = 0.6f, voxelSize = 0.1f; + + GridPtr ls = tools::createLevelSetTubeComplex(vertices, segments, r, voxelSize); + + // this complex is equivalent to a capsule from first vertex to last vertex with radius r + testCapsuleMeasures(vertices[0], vertices[3], r, ls); + } + + // test measures of an approximate a torus + { + const float pi = math::pi(), error = 0.05f, + a = 1.2f, c = 3.0f, voxelSize = 0.1f; + + const Index32 n = 128; + std::vector vertices; + std::vector segments; + const float delta = 2.0f*pi/static_cast(n); + + float theta = 0.0f; + for (Index32 i = 0; i < n; ++i, theta += delta) { + vertices.push_back(Vec3s(c*math::Cos(theta), c*math::Sin(theta), 0.0f)); + segments.push_back(Vec2I(i, (i+1) % n)); + } + + GridPtr ls = tools::createLevelSetTubeComplex(vertices, segments, a, voxelSize); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + const float area = 4.0f*math::Pow2(pi)*a*c, + volume = 2.0f*math::Pow2(pi)*math::Pow2(a)*c, + totGaussCurv = 0.0f, // Gauss-Bonnet + totMeanCurv = 2.0f*c*math::Pow2(pi); + + tools::LevelSetMeasure m(*ls); + + EXPECT_NEAR(m.area(true), area, area*error); + EXPECT_NEAR(m.volume(true), volume, volume*error); + EXPECT_NEAR(m.totGaussianCurvature(true), totGaussCurv, 0.1f); + EXPECT_NEAR(m.totMeanCurvature(true), totMeanCurv, totMeanCurv*error); + } + + // genus of a cube wireframe + { + using FilterT = tools::LevelSetFilter; + + const std::vector vertices({ + Vec3s(-0.5, 0.5, -0.5f), Vec3s(0.5, 0.5, -0.5f), Vec3s(0.5, -0.5, -0.5f), + Vec3s(-0.5, -0.5, -0.5f), Vec3s(-0.5, 0.5, 0.5f), Vec3s(0.5, 0.5, 0.5f), + Vec3s(0.5, -0.5, 0.5f), Vec3s(-0.5, -0.5, 0.5f) + }); + + const std::vector segments({ + Vec2I(0, 1), Vec2I(0, 3), Vec2I(0, 4), Vec2I(1, 2), Vec2I(1, 5), Vec2I(2, 3), + Vec2I(2, 6), Vec2I(3, 7), Vec2I(4, 5), Vec2I(4, 7), Vec2I(5, 6), Vec2I(6, 7) + }); + + const float r = 0.1f, voxelSize = 0.025f; + + GridPtr ls = tools::createLevelSetTubeComplex(vertices, segments, r, voxelSize); + + // smooth slightly to ensure numerical integration when determining the genus is correct + FilterT filter(*ls); + for (Index i = 0; i < 5; ++i) + filter.meanCurvature(); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + EXPECT_EQ(tools::levelSetGenus(*ls), int(5)); + } + +}// testTubeComplexConstantRadius + +TEST_F(TestLevelSetTubes, testTubeComplexVariableRadius) +{ + using namespace openvdb; + + using GridT = FloatGrid; + using GridPtr = typename GridT::Ptr; + + // generic tests + { + const Vec3s p1(15.8f, 13.2f, 16.7f), p2(4.3f, 7.9f, -4.8f), + p3(-3.0f, -7.4f, 8.9f), p4(-2.7f, 8.9f, 30.4f); + const float r1 = 4.3f, r2 = 1.2f, r3 = 2.1f, r4 = 3.6f; + + const std::vector vertices({p1, p2, p3, p4}); + const std::vector segments({Vec2I(0, 1), Vec2I(0, 2), Vec2I(0, 3)}); + const std::vector radii({r1, r2, r3, r4}); + + const float voxelSize = 0.1f, width = 3.25f; + const Coord ijk(int(p1[0]/voxelSize), + int(p1[1]/voxelSize), + int(p1[2]/voxelSize));// inside + + GridPtr ls = tools::createLevelSetTubeComplex(vertices, segments, radii, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_TRUE(ls->tree().isValueOff(ijk)); + EXPECT_NEAR(-ls->background(), ls->tree().getValue(ijk), 1e-6); + EXPECT_NEAR(voxelSize*width, ls->background(), 1e-6); + EXPECT_NEAR(ls->background(),ls->tree().getValue(Coord(0)), 1e-6); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + + // test measures of complex that forms a single straight tapered capsule + { + const std::vector vertices({Vec3s(0.0f, 0.0f, 0.0f), Vec3s(1.0f, 0.0f, 0.0f), + Vec3s(1.5f, 0.0f, 0.0f), Vec3s(3.0f, 0.0f, 0.0f)}); + const std::vector segments({Vec2I(0, 1), Vec2I(1, 2), Vec2I(2, 3)}); + const std::vector radii({0.3f, 0.4f, 0.45f, 0.6f}); + + const float voxelSize = 0.1f; + + GridPtr ls = tools::createLevelSetTubeComplex(vertices, segments, radii, voxelSize); + + // this complex is equivalent to a tapered capsule from first vertex to last vertex + testTaperedCapsuleMeasures(vertices[0], vertices[3], radii[0], radii[3], ls, 0.02f); + } + + // test genus of a torus with per-vertex radii + { + using FilterT = tools::LevelSetFilter; + + const float pi = math::pi(), + a = 1.2f, c = 3.0f, voxelSize = 0.1f, width = 3.0f; + + const Index32 n = 128; + std::vector vertices; + std::vector segments; + std::vector radii; + const float delta = 2.0f*pi/static_cast(n); + + float theta = 0.0f; + for (Index32 i = 0; i < n; ++i, theta += delta) { + vertices.push_back(Vec3s(c*math::Cos(theta), c*math::Sin(theta), 0.0f)); + segments.push_back(Vec2I(i, (i+1) % n)); + radii.push_back(a + 0.15f*a*math::Sin(5.0f*theta)); + } + + GridPtr ls = tools::createLevelSetTubeComplex( + vertices, segments, radii, voxelSize, width, tools::TUBE_VERTEX_RADII); + + // smooth slightly to ensure numerical integration of the genus is correct + FilterT filter(*ls); + for (Index i = 0; i < 5; ++i) + filter.meanCurvature(); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + EXPECT_EQ(tools::levelSetGenus(*ls), int(1)); + } + + // test genus of a torus with per-segment radii + { + using FilterT = tools::LevelSetFilter; + + const float pi = math::pi(), + a = 1.2f, c = 3.0f, voxelSize = 0.1f, width = 3.0f; + + const Index32 n = 10; + std::vector vertices; + std::vector segments; + std::vector radii; + const float delta = 2.0f*pi/static_cast(n); + + float theta = 0.0f; + for (Index32 i = 0; i < n; ++i, theta += delta) { + vertices.push_back(Vec3s(c*math::Cos(theta), c*math::Sin(theta), 0.0f)); + segments.push_back(Vec2I(i, (i+1) % n)); + radii.push_back(0.7f*a + 0.3f*a*math::Sin(9.0f*theta)); + } + + GridPtr ls = tools::createLevelSetTubeComplex( + vertices, segments, radii, voxelSize, width, tools::TUBE_SEGMENT_RADII); + + // smooth slightly to ensure numerical integration of the genus is correct + FilterT filter(*ls); + for (Index i = 0; i < 5; ++i) + filter.meanCurvature(); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + EXPECT_EQ(tools::levelSetGenus(*ls), int(1)); + } + + // genus of a cube wireframe + { + using FilterT = tools::LevelSetFilter; + + const std::vector vertices({ + Vec3s(-0.5, 0.5, -0.5f), Vec3s(0.5, 0.5, -0.5f), Vec3s(0.5, -0.5, -0.5f), + Vec3s(-0.5, -0.5, -0.5f), Vec3s(-0.5, 0.5, 0.5f), Vec3s(0.5, 0.5, 0.5f), + Vec3s(0.5, -0.5, 0.5f), Vec3s(-0.5, -0.5, 0.5f) + }); + + const std::vector segments({ + Vec2I(0, 1), Vec2I(0, 3), Vec2I(0, 4), Vec2I(1, 2), Vec2I(1, 5), Vec2I(2, 3), + Vec2I(2, 6), Vec2I(3, 7), Vec2I(4, 5), Vec2I(4, 7), Vec2I(5, 6), Vec2I(6, 7) + }); + + const std::vector radii({0.1f, 0.15f, 0.2f, 0.175f, 0.08f, 0.11f, 0.3f, 0.09f}); + + const float voxelSize = 0.025f; + + GridPtr ls = tools::createLevelSetTubeComplex(vertices, segments, radii, voxelSize); + + // smooth slightly to ensure numerical integration of the genus is correct + FilterT filter(*ls); + for (Index i = 0; i < 5; ++i) + filter.meanCurvature(); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + + EXPECT_EQ(tools::levelSetGenus(*ls), int(5)); + } + +}// testTubeComplexConstantRadius diff --git a/openvdb/openvdb/unittest/TestTools.cc b/openvdb/openvdb/unittest/TestTools.cc index df17050a06..24842be73b 100644 --- a/openvdb/openvdb/unittest/TestTools.cc +++ b/openvdb/openvdb/unittest/TestTools.cc @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -217,6 +219,63 @@ TEST_F(TestTools, testLevelSetPlatonic) }// testLevelSetPlatonic +TEST_F(TestTools, testLevelSetDilatedMesh) +{ + using namespace openvdb; + + const float r = 2.9f; + const Vec3s p0(15.8f, 13.2f, 16.7f), p1(4.3f, 7.9f, -4.8f); + const Vec3s p2(-3.0f, -7.4f, 8.9f), p3(-2.7f, 8.9f, 30.4f); + const Vec3s p4(23.0f, 17.4f, -10.9f), p5(5.2f, -5.7f, 29.0f); + const Vec3s p6(-14.6f, 3.7f, 10.9f), p7(35.8f, 23.4f, 5.8f); + + const std::vector vertices({p0, p1, p2, p3, p4, p5, p6, p7}); + const std::vector triangles1({Vec3I(0, 1, 2), Vec3I(0, 1, 3), Vec3I(0, 1, 4)}); + const std::vector triangles2({Vec3I(0, 1, 4)}); + const std::vector quads1({Vec4I(0, 1, 2, 5), Vec4I(0, 1, 6, 3), Vec4I(0, 1, 4, 7)}); + const std::vector quads2({Vec4I(0, 1, 4, 7)}); + + const float voxelSize = 0.1f, width = 3.25f; + const Coord ijk(int(p1[0]/voxelSize), + int(p1[1]/voxelSize), + int(p1[2]/voxelSize));// inside + + {// test dilated triangle mesh + FloatGrid::Ptr ls = tools::createLevelSetDilatedMesh( + vertices, triangles1, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_TRUE(ls->tree().isValueOff(ijk)); + EXPECT_NEAR(-ls->background(), ls->tree().getValue(ijk), 1e-6); + EXPECT_NEAR(voxelSize*width, ls->background(), 1e-6); + EXPECT_NEAR(ls->background(),ls->tree().getValue(Coord(30, 0, -50)), 1e-6); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + {// test dilated quad mesh + FloatGrid::Ptr ls = tools::createLevelSetDilatedMesh( + vertices, quads1, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_TRUE(ls->tree().isValueOff(ijk)); + EXPECT_NEAR(-ls->background(), ls->tree().getValue(ijk), 1e-6); + EXPECT_NEAR(voxelSize*width, ls->background(), 1e-6); + EXPECT_NEAR(ls->background(),ls->tree().getValue(Coord(30, 0, -50)), 1e-6); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + {// test dilated mixed triangle & quad mesh + FloatGrid::Ptr ls = tools::createLevelSetDilatedMesh( + vertices, triangles2, quads2, r, voxelSize, width); + + EXPECT_TRUE(ls->activeVoxelCount() > 0); + EXPECT_TRUE(ls->tree().isValueOff(ijk)); + EXPECT_NEAR(-ls->background(), ls->tree().getValue(ijk), 1e-6); + EXPECT_NEAR(voxelSize*width, ls->background(), 1e-6); + EXPECT_NEAR(ls->background(),ls->tree().getValue(Coord(30, 0, -50)), 1e-6); + EXPECT_EQ(int(GRID_LEVEL_SET), int(ls->getGridClass())); + } + +}// testLevelSetDilatedMesh + TEST_F(TestTools, testLevelSetAdvect) { // Uncomment sections below to run this (time-consuming) test diff --git a/pendingchanges/level_set_constructors.txt b/pendingchanges/level_set_constructors.txt new file mode 100644 index 0000000000..693a3e2807 --- /dev/null +++ b/pendingchanges/level_set_constructors.txt @@ -0,0 +1,8 @@ +OpenVDB: + New Features: + - Various level set constructors. + - tools::createLevelSetCapsule creates a grid containing a narrow-band level set representation of a capsule (tube with constant radius and sphere caps). + - tools::createLevelSetTaperedCapsule creates a grid containing a narrow-band level set representation of a tapered capsule (tube with sphere caps and different radii at both ends, or equivalently the convex hull of two spheres with possibly different centers and radii). + - tools::createLevelSetTubeComplex creates a grid containing a narrow-band level set representation of a tube complex (a collection of capsules or tapered capsules defined by endpoint coordinates and segment indices). + - tools::createLevelSetDilatedMesh creates a grid containing a narrow-band level set representation of a dilated surface mesh (dilated by a radius in all directions). + - Base class tools::ConvexVoxelizer, which can be inherited to implement a convex level set constructor.