diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 97460a9..3a9ec4d 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -3,9 +3,6 @@ name: Pip on: workflow_dispatch: pull_request: - push: - branches: - - master concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/release.yml similarity index 66% rename from .github/workflows/wheels.yml rename to .github/workflows/release.yml index d329c7f..86b3bd4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,10 @@ name: Wheels on: - workflow_dispatch: - pull_request: push: branches: - - master - release: - types: - - published - + - main + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -38,15 +33,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macos-latest, macos-14] steps: - uses: actions/checkout@v3 - - uses: pypa/cibuildwheel@v2.10.1 - env: - CIBW_ARCHS_MACOS: auto universal2 - CIBW_SKIP: pp37-win_amd64 + - uses: pypa/cibuildwheel@v2.17.0 - name: Verify clean directory run: git diff --exit-code @@ -59,22 +51,19 @@ jobs: upload_all: - name: Upload if release + name: Release To PyPi needs: [build_wheels, build_sdist] runs-on: ubuntu-latest - if: github.event_name == 'release' && github.event.action == 'published' - + permissions: + id-token: write steps: - uses: actions/setup-python@v4 with: - python-version: "3.x" + python-version: "3.12" - uses: actions/download-artifact@v3 with: name: artifact path: dist - - uses: pypa/gh-action-pypi-publish@v1.5.1 - with: - user: thomwolf - password: ${{ secrets.pypi_password }} \ No newline at end of file + - uses: pypa/gh-action-pypi-publish@v1.8.10 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index cb899ff..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "files.associations": { - "array": "cpp", - "functional": "cpp", - "atomic": "cpp", - "bit": "cpp", - "*.tcc": "cpp", - "cctype": "cpp", - "chrono": "cpp", - "clocale": "cpp", - "cmath": "cpp", - "compare": "cpp", - "complex": "cpp", - "concepts": "cpp", - "condition_variable": "cpp", - "cstdarg": "cpp", - "cstddef": "cpp", - "cstdint": "cpp", - "cstdio": "cpp", - "cstdlib": "cpp", - "ctime": "cpp", - "cwchar": "cpp", - "cwctype": "cpp", - "deque": "cpp", - "list": "cpp", - "string": "cpp", - "unordered_map": "cpp", - "unordered_set": "cpp", - "vector": "cpp", - "exception": "cpp", - "algorithm": "cpp", - "iterator": "cpp", - "memory": "cpp", - "memory_resource": "cpp", - "numeric": "cpp", - "random": "cpp", - "ratio": "cpp", - "string_view": "cpp", - "system_error": "cpp", - "tuple": "cpp", - "type_traits": "cpp", - "utility": "cpp", - "future": "cpp", - "initializer_list": "cpp", - "iosfwd": "cpp", - "iostream": "cpp", - "istream": "cpp", - "limits": "cpp", - "mutex": "cpp", - "new": "cpp", - "numbers": "cpp", - "ostream": "cpp", - "semaphore": "cpp", - "sstream": "cpp", - "stdexcept": "cpp", - "stop_token": "cpp", - "streambuf": "cpp", - "thread": "cpp", - "typeinfo": "cpp" - } -} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 2d37936..6a234c9 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,34 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Additionally, the V-HACD header-only C++ library (src/vhacdx/VHACD.h) is: + + +Copyright (c) 2011 Khaled Mamou (kmamou at gmail dot com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. The names of the contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index c05a619..3b9675b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -# pyVHACD +# vhacdx Python bindings for V-HACD -A very simple and raw python binding for V-HACD (https://github.com/kmammou/v-hacd) - -Generate a set of convex hulls for a triangulated mesh. +A very simple and raw python binding for [V-HACD](https://github.com/kmammou/v-hacd) that is forked from [thomwolf/pyVHACD](https://github.com/thomwolf/pyVHACD) which generates an approximate convex decomposition of a triangle mesh. Contains a single method: `output = compute_vhacd(points, faces)` which take as inputs: - points: a (N, 3) Numpy array of double containing the coordinates of the N vertex of the mesh @@ -13,23 +11,5 @@ Gives as output a list (number of convex hulls) of pairs of points-faces for eac # To install ``` -pip install pyVHACD -``` - -# To use - -Examples usage with pyvista: -``` -import pyvista -import pyVHACD - -mesh = pyvista.examples.download_bunny().triangulate() - -outputs = pyVHACD.compute_vhacd(mesh.points, mesh.faces) - -plotter = pyvista.Plotter(window_size=(1500, 1100)) -for i, (mesh_points, mesh_faces) in enumerate(outputs): - plotter.add_mesh(pyvista.PolyData(mesh_points, mesh_faces), color=list(pyvista.hexcolors.keys())[i]) - -plotter.show() +pip install vhacdx ``` diff --git a/pyproject.toml b/pyproject.toml index ad9c0c3..20b4c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,65 @@ [build-system] requires = [ "setuptools>=42", - "pybind11>=2.10.0", + "wheel", + "pybind11>=2.12.0", ] build-backend = "setuptools.build_meta" + +[project] +name = "vhacdx" +version = "0.0.7" +requires-python = ">=3.7" + +authors = [{name = "Thomas Wolf", email = "thomas@huggingface.co"}] +description = "Python bindings for VHACD" +urls = {Homepage = "https://github.com/trimesh/vhacdx"} +dependencies = ["numpy"] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[options] +zip_safe = false + +[project.optional-dependencies] +test = ["pytest"] + [tool.cibuildwheel] -test-command = "python {project}/tests/test.py" -test-skip = "*universal2:arm64" \ No newline at end of file +test-requires = "pytest" +test-command = "pytest {project}/tests" + +# skip platforms that don't have numpy wheels +test-skip = "*universal2:arm64 pp31* *i686 *musllinux*" +skip = "pp37-win_amd64" + +[tool.ruff] +target-version = "py37" +# See https://github.com/charliermarsh/ruff#rules for error code definitions. +select = [ + # "ANN", # annotations + "B", # bugbear + "C", # comprehensions + "E", # style errors + "F", # flakes + "I", # import sorting + "RUF100", # meta + "U", # upgrade + "W", # style warnings + "YTT", # sys.version +] + +ignore = [ + "C901", # Comprehension is too complex (11 > 10) + "N802", # Function name should be lowercase + "N806", # Variable in function should be lowercase + "E501", # Line too long ({width} > {limit} characters) + "B904", # raise ... from err + "B905", # zip() without an explicit strict= parameter + "ANN101", # type hint for `self` + "ANN002", # type hint for *args + "ANN003", # type hint for **kwargs +] +line-length = 90 diff --git a/setup.py b/setup.py index f0c9f97..c6e3f0d 100644 --- a/setup.py +++ b/setup.py @@ -1,41 +1,29 @@ -import sys - -from pybind11 import get_cmake_dir # Available at setup time due to pyproject.toml +import os + from pybind11.setup_helpers import Pybind11Extension, build_ext from setuptools import setup -__version__ = "0.0.2" +# get version from pyproject.toml to pass in to build macro +with open( + os.path.abspath(os.path.join(os.path.dirname(__file__), "pyproject.toml")) +) as f: + __version__ = next(L.split("=")[1].strip().strip('"') for L in f if "version" in L) + # The main interface is through Pybind11Extension. # * You can add cxx_std=11/14/17, and then build_ext can be removed. # * You can set include_pybind11=false to add the include directory yourself, # say from a submodule. -# -# Note: -# Sort input source files if you glob sources to ensure bit-for-bit -# reproducible builds (https://github.com/pybind/python_example/pull/53) - -ext_modules = [ - Pybind11Extension("pyVHACD", - ["src/pyVHACD/main.cpp"], - # Example: passing in the version to the compiled code - define_macros = [('VERSION_INFO', __version__)], - ), -] setup( - name='pyVHACD', - version=__version__, - author='Thomas Wolf', - author_email='thomas@huggingface.co', - url='https://github.com/thomwolf/pyVHACD', - description='Python bindings for VHACD', - long_description='Python bindings for VHACD', - ext_modules=ext_modules, - extras_require={"test": ["pytest", "numpy"]}, - install_requires=['pybind11>=2.2', 'numpy'], - cmdclass={'build_ext': build_ext}, - zip_safe=False, - python_requires=">=3.6", + ext_modules=[ + Pybind11Extension( + "vhacdx", + ["src/vhacdx/main.cpp"], + # Example: passing in the version to the compiled code + define_macros=[("VERSION_INFO", __version__)], + ), + ], + cmdclass={"build_ext": build_ext}, ) diff --git a/src/pyVHACD/main.cpp b/src/pyVHACD/main.cpp deleted file mode 100644 index e94c5ba..0000000 --- a/src/pyVHACD/main.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include -#include -#include -#include -#define ENABLE_VHACD_IMPLEMENTATION 1 -#define VHACD_DISABLE_THREADING 0 -#include "VHACD.h" - -#define STRINGIFY(x) #x -#define MACRO_STRINGIFY(x) STRINGIFY(x) - -namespace py = pybind11; - -// Return a list of convex hulls -// Each convex hull is a tuple of (vertices, indices) -// vertices is a numpy array of shape (n, 3) -// indices is a numpy array of shape (m,) -std::vector, py::array_t>> compute_vhacd(py::array_t points, py::array_t faces) { - - /* read input arrays buffer_info */ - auto buf_points = points.request(); - auto buf_faces = faces.request(); - - /* variables */ - double *ptr_points = (double *) buf_points.ptr; - uint32_t *ptr_faces = (uint32_t *) buf_faces.ptr; - size_t num_points = buf_points.shape[0]; - size_t num_faces = buf_faces.shape[0] / 4; - size_t num_triangle_indices = num_faces * 3; - - // Prepare our input arrays for VHACD - uint32_t *triangles = new uint32_t[num_triangle_indices]; - for (uint32_t i=0; iCompute(ptr_points, num_points, triangles, num_faces, p); - - while ( !iface->IsReady() ) - { - std::this_thread::sleep_for(std::chrono::nanoseconds(10000)); // s - } - - // Build our output arrays from VHACD outputs - std::vector, py::array_t>> res; - const int nConvexHulls = iface->GetNConvexHulls(); - res.reserve(nConvexHulls); - - if (nConvexHulls) - { - // Exporting Convex Decomposition results of convex hulls - - for (uint32_t i=0; iGetNConvexHulls(); i++) - { - VHACD::IVHACD::ConvexHull ch; - iface->GetConvexHull(i, ch); - - /* allocate the output buffers */ - py::array_t res_vertices = py::array_t(ch.m_points.size() * 3); - py::array_t res_faces = py::array_t(ch.m_triangles.size() * 4); - - py::buffer_info buf_res_vertices = res_vertices.request(); - py::buffer_info buf_res_faces = res_faces.request(); - - double *ptr_res_vertices = (double *) buf_res_vertices.ptr; - uint32_t *ptr_res_faces = (uint32_t *) buf_res_faces.ptr; - - for (uint32_t j = 0; j < ch.m_points.size(); j++) - { - const VHACD::Vertex& pos = ch.m_points[j]; - ptr_res_vertices[3*j] = pos.mX; - ptr_res_vertices[3*j+1] = pos.mY; - ptr_res_vertices[3*j+2] = pos.mZ; - } - - const uint32_t num_v = 3; - for (uint32_t j = 0; j < ch.m_triangles.size(); j++) - { - uint32_t i1 = ch.m_triangles[j].mI0; - uint32_t i2 = ch.m_triangles[j].mI1; - uint32_t i3 = ch.m_triangles[j].mI2; - ptr_res_faces[4*j] = num_v; - ptr_res_faces[4*j+1] = i1; - ptr_res_faces[4*j+2] = i2; - ptr_res_faces[4*j+3] = i3; - } - - // Reshape the vertices array to be (n, 3) - const long width = 3; - res_vertices.resize({(long)ch.m_points.size(), width}); - - // Push on our list - res.emplace_back(std::move(res_vertices), std::move(res_faces)); - } - } - return res; -} - - -/* Wrapping routines with PyBind */ -PYBIND11_MODULE(pyVHACD, m) { - m.doc() = "Python bindings for the V-HACD algorithm"; // optional module docstring - m.def("compute_vhacd", &compute_vhacd, "Compute convex hulls"); - -#ifdef VERSION_INFO - m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); -#else - m.attr("__version__") = "dev"; -#endif -} diff --git a/src/pyVHACD/VHACD.h b/src/vhacdx/VHACD.h similarity index 96% rename from src/pyVHACD/VHACD.h rename to src/vhacdx/VHACD.h index 45be09f..6c8b869 100644 --- a/src/pyVHACD/VHACD.h +++ b/src/vhacdx/VHACD.h @@ -29,7 +29,11 @@ // ImGui and StbLib and other popular open source libraries. # define VHACD_VERSION_MAJOR 4 -# define VHACD_VERSION_MINOR 0 +# define VHACD_VERSION_MINOR 1 + +// Changes for version 4.1 +// +// Various minor tweaks mostly to the test application and some default values. // Changes for version 4.0 // @@ -112,6 +116,8 @@ #include #include +#include +#include namespace VHACD { @@ -485,13 +491,403 @@ class IVHACD { } }; +/* + * Out of line definitions + */ + + template + T clamp(const T& v, const T& lo, const T& hi) + { + if (v < lo) + { + return lo; + } + if (v > hi) + { + return hi; + } + return v ; + } + +/* + * Getters + */ + template + inline T& Vector3::operator[](size_t i) + { + return m_data[i]; + } + + template + inline const T& Vector3::operator[](size_t i) const + { + return m_data[i]; + } + + template + inline T& Vector3::GetX() + { + return m_data[0]; + } + + template + inline T& Vector3::GetY() + { + return m_data[1]; + } + + template + inline T& Vector3::GetZ() + { + return m_data[2]; + } + + template + inline const T& Vector3::GetX() const + { + return m_data[0]; + } + + template + inline const T& Vector3::GetY() const + { + return m_data[1]; + } + + template + inline const T& Vector3::GetZ() const + { + return m_data[2]; + } + +/* + * Normalize and norming + */ + template + inline T Vector3::Normalize() + { + T n = GetNorm(); + if (n != T(0.0)) (*this) /= n; + return n; + } + + template + inline Vector3 Vector3::Normalized() + { + Vector3 ret = *this; + T n = GetNorm(); + if (n != T(0.0)) ret /= n; + return ret; + } + + template + inline T Vector3::GetNorm() const + { + return std::sqrt(GetNormSquared()); + } + + template + inline T Vector3::GetNormSquared() const + { + return this->Dot(*this); + } + + template + inline int Vector3::LongestAxis() const + { + auto it = std::max_element(m_data.begin(), m_data.end()); + return int(std::distance(m_data.begin(), it)); + } + +/* + * Vector-vector operations + */ + template + inline Vector3& Vector3::operator=(const Vector3& rhs) + { + GetX() = rhs.GetX(); + GetY() = rhs.GetY(); + GetZ() = rhs.GetZ(); + return *this; + } + + template + inline Vector3& Vector3::operator+=(const Vector3& rhs) + { + GetX() += rhs.GetX(); + GetY() += rhs.GetY(); + GetZ() += rhs.GetZ(); + return *this; + } + + template + inline Vector3& Vector3::operator-=(const Vector3& rhs) + { + GetX() -= rhs.GetX(); + GetY() -= rhs.GetY(); + GetZ() -= rhs.GetZ(); + return *this; + } + + template + inline Vector3 Vector3::CWiseMul(const Vector3& rhs) const + { + return Vector3(GetX() * rhs.GetX(), + GetY() * rhs.GetY(), + GetZ() * rhs.GetZ()); + } + + template + inline Vector3 Vector3::Cross(const Vector3& rhs) const + { + return Vector3(GetY() * rhs.GetZ() - GetZ() * rhs.GetY(), + GetZ() * rhs.GetX() - GetX() * rhs.GetZ(), + GetX() * rhs.GetY() - GetY() * rhs.GetX()); + } + + template + inline T Vector3::Dot(const Vector3& rhs) const + { + return GetX() * rhs.GetX() + + GetY() * rhs.GetY() + + GetZ() * rhs.GetZ(); + } + + template + inline Vector3 Vector3::operator+(const Vector3& rhs) const + { + return Vector3(GetX() + rhs.GetX(), + GetY() + rhs.GetY(), + GetZ() + rhs.GetZ()); + } + + template + inline Vector3 Vector3::operator-(const Vector3& rhs) const + { + return Vector3(GetX() - rhs.GetX(), + GetY() - rhs.GetY(), + GetZ() - rhs.GetZ()); + } + + template + inline Vector3 operator*(T lhs, const Vector3& rhs) + { + return Vector3(lhs * rhs.GetX(), + lhs * rhs.GetY(), + lhs * rhs.GetZ()); + } + +/* + * Vector-scalar operations + */ + template + inline Vector3& Vector3::operator-=(T a) + { + GetX() -= a; + GetY() -= a; + GetZ() -= a; + return *this; + } + + template + inline Vector3& Vector3::operator+=(T a) + { + GetX() += a; + GetY() += a; + GetZ() += a; + return *this; + } + + template + inline Vector3& Vector3::operator/=(T a) + { + GetX() /= a; + GetY() /= a; + GetZ() /= a; + return *this; + } + + template + inline Vector3& Vector3::operator*=(T a) + { + GetX() *= a; + GetY() *= a; + GetZ() *= a; + return *this; + } + + template + inline Vector3 Vector3::operator*(T rhs) const + { + return Vector3(GetX() * rhs, + GetY() * rhs, + GetZ() * rhs); + } + + template + inline Vector3 Vector3::operator/(T rhs) const + { + return Vector3(GetX() / rhs, + GetY() / rhs, + GetZ() / rhs); + } + +/* + * Unary operations + */ + template + inline Vector3 Vector3::operator-() const + { + return Vector3(-GetX(), + -GetY(), + -GetZ()); + } + +/* + * Comparison operators + */ + template + inline bool Vector3::operator<(const Vector3& rhs) const + { + if (GetX() == rhs.GetX()) + { + if (GetY() == rhs.GetY()) + { + return (GetZ() < rhs.GetZ()); + } + return (GetY() < rhs.GetY()); + } + return (GetX() < rhs.GetX()); + } + + template + inline bool Vector3::operator>(const Vector3& rhs) const + { + if (GetX() == rhs.GetX()) + { + if (GetY() == rhs.GetY()) + { + return (GetZ() > rhs.GetZ()); + } + return (GetY() > rhs.GetY()); + } + return (GetX() > rhs.GetZ()); + } + + template + inline bool Vector3::CWiseAllGE(const Vector3& rhs) const + { + return GetX() >= rhs.GetX() + && GetY() >= rhs.GetY() + && GetZ() >= rhs.GetZ(); + } + + template + inline bool Vector3::CWiseAllLE(const Vector3& rhs) const + { + return GetX() <= rhs.GetX() + && GetY() <= rhs.GetY() + && GetZ() <= rhs.GetZ(); + } + + template + inline Vector3 Vector3::CWiseMin(const Vector3& rhs) const + { + return Vector3(std::min(GetX(), rhs.GetX()), + std::min(GetY(), rhs.GetY()), + std::min(GetZ(), rhs.GetZ())); + } + + template + inline Vector3 Vector3::CWiseMax(const Vector3& rhs) const + { + return Vector3(std::max(GetX(), rhs.GetX()), + std::max(GetY(), rhs.GetY()), + std::max(GetZ(), rhs.GetZ())); + } + + template + inline T Vector3::MinCoeff() const + { + return *std::min_element(m_data.begin(), m_data.end()); + } + + template + inline T Vector3::MaxCoeff() const + { + return *std::max_element(m_data.begin(), m_data.end()); + } + + template + inline T Vector3::MinCoeff(uint32_t& idx) const + { + auto it = std::min_element(m_data.begin(), m_data.end()); + idx = uint32_t(std::distance(m_data.begin(), it)); + return *it; + } + + template + inline T Vector3::MaxCoeff(uint32_t& idx) const + { + auto it = std::max_element(m_data.begin(), m_data.end()); + idx = uint32_t(std::distance(m_data.begin(), it)); + return *it; + } + +/* + * Constructors + */ + template + inline Vector3::Vector3(T a) + : m_data{a, a, a} + { + } + + template + inline Vector3::Vector3(T x, T y, T z) + : m_data{x, y, z} + { + } + + template + inline Vector3::Vector3(const Vector3& rhs) + : m_data{rhs.m_data} + { + } + + template + template + inline Vector3::Vector3(const Vector3& rhs) + : m_data{T(rhs.GetX()), T(rhs.GetY()), T(rhs.GetZ())} + { + } + + template + inline Vector3::Vector3(const VHACD::Vertex& rhs) + : Vector3(rhs.mX, rhs.mY, rhs.mZ) + { + static_assert(std::is_same::value, "Vertex to Vector3 constructor only enabled for double"); + } + + template + inline Vector3::Vector3(const VHACD::Triangle& rhs) + : Vector3(rhs.mI0, rhs.mI1, rhs.mI2) + { + static_assert(std::is_same::value, "Triangle to Vector3 constructor only enabled for uint32_t"); + } + + template + inline Vector3::operator VHACD::Vertex() const + { + static_assert(std::is_same::value, "Vector3 to Vertex conversion only enable for double"); + return ::VHACD::Vertex( GetX(), GetY(), GetZ()); + } IVHACD* CreateVHACD(); // Create a synchronous (blocking) implementation of V-HACD IVHACD* CreateVHACD_ASYNC(); // Create an asynchronous (non-blocking) implementation of V-HACD } // namespace VHACD - #if ENABLE_VHACD_IMPLEMENTATION #include #include @@ -500,7 +896,6 @@ IVHACD* CreateVHACD_ASYNC(); // Create an asynchronous (non-blocking) impleme #include #include -#include #include #include #include @@ -597,402 +992,9 @@ class ScopedTime Timer m_timer; VHACD::IVHACD::IUserLogger* m_logger{ nullptr }; }; - -/* - * Out of line definitions - */ - -template -T clamp(const T& v, const T& lo, const T& hi) -{ - if (v < lo) - { - return lo; - } - if (v > hi) - { - return hi; - } - return v ; -} - -/* - * Getters - */ -template -inline T& Vector3::operator[](size_t i) -{ - return m_data[i]; -} - -template -inline const T& Vector3::operator[](size_t i) const -{ - return m_data[i]; -} - -template -inline T& Vector3::GetX() -{ - return m_data[0]; -} - -template -inline T& Vector3::GetY() -{ - return m_data[1]; -} - -template -inline T& Vector3::GetZ() -{ - return m_data[2]; -} - -template -inline const T& Vector3::GetX() const -{ - return m_data[0]; -} - -template -inline const T& Vector3::GetY() const -{ - return m_data[1]; -} - -template -inline const T& Vector3::GetZ() const -{ - return m_data[2]; -} - -/* - * Normalize and norming - */ -template -inline T Vector3::Normalize() -{ - T n = GetNorm(); - if (n != T(0.0)) (*this) /= n; - return n; -} - -template -inline Vector3 Vector3::Normalized() -{ - Vector3 ret = *this; - T n = GetNorm(); - if (n != T(0.0)) ret /= n; - return ret; -} - -template -inline T Vector3::GetNorm() const -{ - return std::sqrt(GetNormSquared()); -} - -template -inline T Vector3::GetNormSquared() const -{ - return this->Dot(*this); -} - -template -inline int Vector3::LongestAxis() const -{ - auto it = std::max_element(m_data.begin(), m_data.end()); - return int(std::distance(m_data.begin(), it)); -} - -/* - * Vector-vector operations - */ -template -inline Vector3& Vector3::operator=(const Vector3& rhs) -{ - GetX() = rhs.GetX(); - GetY() = rhs.GetY(); - GetZ() = rhs.GetZ(); - return *this; -} - -template -inline Vector3& Vector3::operator+=(const Vector3& rhs) -{ - GetX() += rhs.GetX(); - GetY() += rhs.GetY(); - GetZ() += rhs.GetZ(); - return *this; -} - -template -inline Vector3& Vector3::operator-=(const Vector3& rhs) -{ - GetX() -= rhs.GetX(); - GetY() -= rhs.GetY(); - GetZ() -= rhs.GetZ(); - return *this; -} - -template -inline Vector3 Vector3::CWiseMul(const Vector3& rhs) const -{ - return Vector3(GetX() * rhs.GetX(), - GetY() * rhs.GetY(), - GetZ() * rhs.GetZ()); -} - -template -inline Vector3 Vector3::Cross(const Vector3& rhs) const -{ - return Vector3(GetY() * rhs.GetZ() - GetZ() * rhs.GetY(), - GetZ() * rhs.GetX() - GetX() * rhs.GetZ(), - GetX() * rhs.GetY() - GetY() * rhs.GetX()); -} - -template -inline T Vector3::Dot(const Vector3& rhs) const -{ - return GetX() * rhs.GetX() - + GetY() * rhs.GetY() - + GetZ() * rhs.GetZ(); -} - -template -inline Vector3 Vector3::operator+(const Vector3& rhs) const -{ - return Vector3(GetX() + rhs.GetX(), - GetY() + rhs.GetY(), - GetZ() + rhs.GetZ()); -} - -template -inline Vector3 Vector3::operator-(const Vector3& rhs) const -{ - return Vector3(GetX() - rhs.GetX(), - GetY() - rhs.GetY(), - GetZ() - rhs.GetZ()); -} - -template -inline Vector3 operator*(T lhs, const Vector3& rhs) -{ - return Vector3(lhs * rhs.GetX(), - lhs * rhs.GetY(), - lhs * rhs.GetZ()); -} - -/* - * Vector-scalar operations - */ -template -inline Vector3& Vector3::operator-=(T a) -{ - GetX() -= a; - GetY() -= a; - GetZ() -= a; - return *this; -} - -template -inline Vector3& Vector3::operator+=(T a) -{ - GetX() += a; - GetY() += a; - GetZ() += a; - return *this; -} - -template -inline Vector3& Vector3::operator/=(T a) -{ - GetX() /= a; - GetY() /= a; - GetZ() /= a; - return *this; -} - -template -inline Vector3& Vector3::operator*=(T a) -{ - GetX() *= a; - GetY() *= a; - GetZ() *= a; - return *this; -} - -template -inline Vector3 Vector3::operator*(T rhs) const -{ - return Vector3(GetX() * rhs, - GetY() * rhs, - GetZ() * rhs); -} - -template -inline Vector3 Vector3::operator/(T rhs) const -{ - return Vector3(GetX() / rhs, - GetY() / rhs, - GetZ() / rhs); -} - -/* - * Unary operations - */ -template -inline Vector3 Vector3::operator-() const -{ - return Vector3(-GetX(), - -GetY(), - -GetZ()); -} - -/* - * Comparison operators - */ -template -inline bool Vector3::operator<(const Vector3& rhs) const -{ - if (GetX() == rhs.GetX()) - { - if (GetY() == rhs.GetY()) - { - return (GetZ() < rhs.GetZ()); - } - return (GetY() < rhs.GetY()); - } - return (GetX() < rhs.GetX()); -} - -template -inline bool Vector3::operator>(const Vector3& rhs) const -{ - if (GetX() == rhs.GetX()) - { - if (GetY() == rhs.GetY()) - { - return (GetZ() > rhs.GetZ()); - } - return (GetY() > rhs.GetY()); - } - return (GetX() > rhs.GetZ()); -} - -template -inline bool Vector3::CWiseAllGE(const Vector3& rhs) const -{ - return GetX() >= rhs.GetX() - && GetY() >= rhs.GetY() - && GetZ() >= rhs.GetZ(); -} - -template -inline bool Vector3::CWiseAllLE(const Vector3& rhs) const -{ - return GetX() <= rhs.GetX() - && GetY() <= rhs.GetY() - && GetZ() <= rhs.GetZ(); -} - -template -inline Vector3 Vector3::CWiseMin(const Vector3& rhs) const -{ - return Vector3(std::min(GetX(), rhs.GetX()), - std::min(GetY(), rhs.GetY()), - std::min(GetZ(), rhs.GetZ())); -} - -template -inline Vector3 Vector3::CWiseMax(const Vector3& rhs) const -{ - return Vector3(std::max(GetX(), rhs.GetX()), - std::max(GetY(), rhs.GetY()), - std::max(GetZ(), rhs.GetZ())); -} - -template -inline T Vector3::MinCoeff() const -{ - return *std::min_element(m_data.begin(), m_data.end()); -} - -template -inline T Vector3::MaxCoeff() const -{ - return *std::max_element(m_data.begin(), m_data.end()); -} - -template -inline T Vector3::MinCoeff(uint32_t& idx) const -{ - auto it = std::min_element(m_data.begin(), m_data.end()); - idx = uint32_t(std::distance(m_data.begin(), it)); - return *it; -} - -template -inline T Vector3::MaxCoeff(uint32_t& idx) const -{ - auto it = std::max_element(m_data.begin(), m_data.end()); - idx = uint32_t(std::distance(m_data.begin(), it)); - return *it; -} - -/* - * Constructors - */ -template -inline Vector3::Vector3(T a) - : m_data{a, a, a} -{ -} - -template -inline Vector3::Vector3(T x, T y, T z) - : m_data{x, y, z} -{ -} - -template -inline Vector3::Vector3(const Vector3& rhs) - : m_data{rhs.m_data} -{ -} - -template -template -inline Vector3::Vector3(const Vector3& rhs) - : m_data{T(rhs.GetX()), T(rhs.GetY()), T(rhs.GetZ())} -{ -} - -template -inline Vector3::Vector3(const VHACD::Vertex& rhs) - : Vector3(rhs.mX, rhs.mY, rhs.mZ) -{ - static_assert(std::is_same::value, "Vertex to Vector3 constructor only enabled for double"); -} - -template -inline Vector3::Vector3(const VHACD::Triangle& rhs) - : Vector3(rhs.mI0, rhs.mI1, rhs.mI2) -{ - static_assert(std::is_same::value, "Triangle to Vector3 constructor only enabled for uint32_t"); -} - -template -inline Vector3::operator VHACD::Vertex() const -{ - static_assert(std::is_same::value, "Vector3 to Vertex conversion only enable for double"); - return ::VHACD::Vertex( GetX(), GetY(), GetZ()); -} - -inline BoundsAABB::BoundsAABB(const std::vector& points) - : m_min(points[0]) - , m_max(points[0]) +BoundsAABB::BoundsAABB(const std::vector& points) + : m_min(points[0]) + , m_max(points[0]) { for (uint32_t i = 1; i < points.size(); ++i) { @@ -1002,10 +1004,10 @@ inline BoundsAABB::BoundsAABB(const std::vector& points) } } -inline BoundsAABB::BoundsAABB(const VHACD::Vect3& min, +BoundsAABB::BoundsAABB(const VHACD::Vect3& min, const VHACD::Vect3& max) - : m_min(min) - , m_max(max) + : m_min(min) + , m_max(max) { } @@ -1015,16 +1017,16 @@ BoundsAABB BoundsAABB::Union(const BoundsAABB& b) GetMax().CWiseMax(b.GetMax())); } -inline bool VHACD::BoundsAABB::Intersects(const VHACD::BoundsAABB& b) const +bool VHACD::BoundsAABB::Intersects(const VHACD::BoundsAABB& b) const { if ( ( GetMin().GetX() > b.GetMax().GetX()) - || (b.GetMin().GetX() > GetMax().GetX())) + || (b.GetMin().GetX() > GetMax().GetX())) return false; if ( ( GetMin().GetY() > b.GetMax().GetY()) - || (b.GetMin().GetY() > GetMax().GetY())) + || (b.GetMin().GetY() > GetMax().GetY())) return false; if ( ( GetMin().GetZ() > b.GetMax().GetZ()) - || (b.GetMin().GetZ() > GetMax().GetZ())) + || (b.GetMin().GetZ() > GetMax().GetZ())) return false; return true; } @@ -1035,30 +1037,30 @@ double BoundsAABB::SurfaceArea() const return double(2.0) * (d.GetX() * d.GetY() + d.GetX() * d.GetZ() + d.GetY() * d.GetZ()); } -inline double VHACD::BoundsAABB::Volume() const +double VHACD::BoundsAABB::Volume() const { VHACD::Vect3 d = GetMax() - GetMin(); return d.GetX() * d.GetY() * d.GetZ(); } -inline BoundsAABB VHACD::BoundsAABB::Inflate(double ratio) const +BoundsAABB VHACD::BoundsAABB::Inflate(double ratio) const { double inflate = (GetMin() - GetMax()).GetNorm() * double(0.5) * ratio; return BoundsAABB(GetMin() - inflate, GetMax() + inflate); } -inline VHACD::Vect3 VHACD::BoundsAABB::ClosestPoint(const VHACD::Vect3& p) const +VHACD::Vect3 VHACD::BoundsAABB::ClosestPoint(const VHACD::Vect3& p) const { return p.CWiseMax(GetMin()).CWiseMin(GetMax()); } -inline VHACD::Vect3& VHACD::BoundsAABB::GetMin() +VHACD::Vect3& VHACD::BoundsAABB::GetMin() { return m_min; } -inline VHACD::Vect3& VHACD::BoundsAABB::GetMax() +VHACD::Vect3& VHACD::BoundsAABB::GetMax() { return m_max; } @@ -1068,17 +1070,17 @@ inline const VHACD::Vect3& VHACD::BoundsAABB::GetMin() const return m_min; } -inline const VHACD::Vect3& VHACD::BoundsAABB::GetMax() const +const VHACD::Vect3& VHACD::BoundsAABB::GetMax() const { return m_max; } -inline VHACD::Vect3 VHACD::BoundsAABB::GetSize() const +VHACD::Vect3 VHACD::BoundsAABB::GetSize() const { return GetMax() - GetMin(); } -inline VHACD::Vect3 VHACD::BoundsAABB::GetCenter() const +VHACD::Vect3 VHACD::BoundsAABB::GetCenter() const { return (GetMin() + GetMax()) * double(0.5); } diff --git a/src/vhacdx/main.cpp b/src/vhacdx/main.cpp new file mode 100644 index 0000000..2811253 --- /dev/null +++ b/src/vhacdx/main.cpp @@ -0,0 +1,155 @@ +#include +#include +#include +#include +#define ENABLE_VHACD_IMPLEMENTATION 1 +#define VHACD_DISABLE_THREADING 0 +#include "VHACD.h" + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +namespace py = pybind11; + +// Return a list of convex hulls +// Each convex hull is a tuple of (vertices, indices) +// vertices is a numpy array of shape (n, 3) +// indices is a numpy array of shape (m,) +std::vector, py::array_t>> +compute_vhacd(py::array_t points, py::array_t faces, + uint32_t maxConvexHulls, uint32_t resolution, + double minimumVolumePercentErrorAllowed, + uint32_t maxRecursionDepth, bool shrinkWrap, std::string fillMode, + uint32_t maxNumVerticesPerCH, bool asyncACD, + uint32_t minEdgeLength, bool findBestPlane) { + + /* read input arrays buffer_info */ + auto buf_points = points.request(); + auto buf_faces = faces.request(); + + /* variables */ + double *ptr_points = (double *)buf_points.ptr; + uint32_t *ptr_faces = (uint32_t *)buf_faces.ptr; + size_t num_points = buf_points.shape[0]; + size_t num_faces = buf_faces.shape[0] / 4; + size_t num_triangle_indices = num_faces * 3; + + // Prepare our input arrays for VHACD + uint32_t *triangles = new uint32_t[num_triangle_indices]; + for (uint32_t i = 0; i < num_faces; i++) { + // We skip the first index of the face array, which is the number of + // vertices in the face + triangles[3 * i] = ptr_faces[4 * i + 1]; + triangles[3 * i + 1] = ptr_faces[4 * i + 2]; + triangles[3 * i + 2] = ptr_faces[4 * i + 3]; + } + + // Create the VHACD parameters + VHACD::IVHACD::Parameters p; + p.m_maxConvexHulls = maxConvexHulls; + p.m_resolution = resolution; + p.m_minimumVolumePercentErrorAllowed = minimumVolumePercentErrorAllowed; + p.m_maxRecursionDepth = maxRecursionDepth; + p.m_shrinkWrap = shrinkWrap; + p.m_maxNumVerticesPerCH = maxNumVerticesPerCH; + p.m_asyncACD = asyncACD; + p.m_minEdgeLength = minEdgeLength; + p.m_findBestPlane = findBestPlane; + if (fillMode == "flood") { + p.m_fillMode = VHACD::FillMode::FLOOD_FILL; + } else if (fillMode == "raycast") { + p.m_fillMode = VHACD::FillMode::RAYCAST_FILL; + } else if (fillMode == "surface") { + p.m_fillMode = VHACD::FillMode::SURFACE_ONLY; + } else { + printf("Invalid fill mode, only valid options are 'flood', 'raycast', and " + "'surface'\n"); + } + +#if VHACD_DISABLE_THREADING + VHACD::IVHACD *iface = VHACD::CreateVHACD(); +#else + VHACD::IVHACD *iface = + p.m_asyncACD ? VHACD::CreateVHACD_ASYNC() : VHACD::CreateVHACD(); +#endif + + /// The main computation /////////////////////////////////////////////// + iface->Compute(ptr_points, num_points, triangles, num_faces, p); + + while (!iface->IsReady()) { + std::this_thread::sleep_for(std::chrono::nanoseconds(10000)); // s + } + + // Build our output arrays from VHACD outputs + std::vector, py::array_t>> res; + const int nConvexHulls = iface->GetNConvexHulls(); + res.reserve(nConvexHulls); + + if (nConvexHulls) { + // Exporting Convex Decomposition results of convex hulls + + for (uint32_t i = 0; i < iface->GetNConvexHulls(); i++) { + VHACD::IVHACD::ConvexHull ch; + iface->GetConvexHull(i, ch); + + /* allocate the output buffers */ + py::array_t res_vertices = + py::array_t(ch.m_points.size() * 3); + py::array_t res_faces = + py::array_t(ch.m_triangles.size() * 3); + + py::buffer_info buf_res_vertices = res_vertices.request(); + py::buffer_info buf_res_faces = res_faces.request(); + + double *ptr_res_vertices = (double *)buf_res_vertices.ptr; + uint32_t *ptr_res_faces = (uint32_t *)buf_res_faces.ptr; + + for (uint32_t j = 0; j < ch.m_points.size(); j++) { + const VHACD::Vertex &pos = ch.m_points[j]; + ptr_res_vertices[3 * j] = pos.mX; + ptr_res_vertices[3 * j + 1] = pos.mY; + ptr_res_vertices[3 * j + 2] = pos.mZ; + } + + const uint32_t num_v = 3; + for (uint32_t j = 0; j < ch.m_triangles.size(); j++) { + uint32_t i1 = ch.m_triangles[j].mI0; + uint32_t i2 = ch.m_triangles[j].mI1; + uint32_t i3 = ch.m_triangles[j].mI2; + ptr_res_faces[3 * j] = i1; + ptr_res_faces[3 * j + 1] = i2; + ptr_res_faces[3 * j + 2] = i3; + } + + // Reshape the vertices array to be (n, 3) + const long width = 3; + res_vertices.resize({(long)ch.m_points.size(), width}); + // Resize faces to be (m, 3) + res_faces.resize({(long)ch.m_triangles.size(), width}); + + // Push on our list + res.emplace_back(std::move(res_vertices), std::move(res_faces)); + } + } + return res; +} + +/* Wrapping routines with PyBind */ +PYBIND11_MODULE(vhacdx, m) { + m.doc() = + "Python bindings for the V-HACD algorithm"; // optional module docstring + m.def("compute_vhacd", &compute_vhacd, "Compute convex hulls", + py::arg("points"), py::arg("faces"), py::arg("maxConvexHulls") = 64, + py::arg("resolution") = 400000, + py::arg("minimumVolumePercentErrorAllowed") = 1.0, + py::arg("maxRecursionDepth") = 10, py::arg("shrinkWrap") = true, + py::arg("fillMode") = "flood", py::arg("maxNumVerticesPerCH") = 64, + py::arg("asyncACD") = true, py::arg("minEdgeLength") = 2, + py::arg("findBestPlane") = false); + +#ifdef VERSION_INFO + m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO); +#else + m.attr("__version__") = "dev"; +#endif +} diff --git a/tests/test.py b/tests/test.py deleted file mode 100644 index 0f1c4fc..0000000 --- a/tests/test.py +++ /dev/null @@ -1,26 +0,0 @@ -import pyVHACD as pv -import numpy as np - -points = np.array([[-0.5, -0.5, -0.5], - [-0.5, -0.5, 0.5], - [-0.5, 0.5, 0.5], - [-0.5, 0.5, -0.5], - [ 0.5, -0.5, -0.5], - [ 0.5, 0.5, -0.5], - [ 0.5, 0.5, 0.5], - [ 0.5, -0.5, 0.5]], dtype=np.float32) - -faces = np.array([3, 0, 1, 3, 3, 2, 3, 1, 3, 4, 5, 7, 3, 6, 7, 5, 3, 0, 4, 1, 3, 7, - 1, 4, 3, 3, 2, 5, 3, 6, 5, 2, 3, 0, 3, 4, 3, 5, 4, 3, 3, 1, 7, 2, - 3, 6, 2, 7], dtype=np.int64) - -output = pv.compute_vhacd(points, faces) - -assert pv.__version__ == '0.0.2' -assert isinstance(output, list) -assert len(output) == 1 -assert len(output[0]) == 2 -assert isinstance(output[0][0], np.ndarray) -assert isinstance(output[0][1], np.ndarray) -assert output[0][0].shape == (8, 3) -assert output[0][1].shape == (48,) diff --git a/tests/test_vhacd.py b/tests/test_vhacd.py new file mode 100644 index 0000000..fc81ad0 --- /dev/null +++ b/tests/test_vhacd.py @@ -0,0 +1,102 @@ +import numpy as np +import vhacdx as pv + + +def test_basic(): + points = np.array( + [ + [-0.5, -0.5, -0.5], + [-0.5, -0.5, 0.5], + [-0.5, 0.5, 0.5], + [-0.5, 0.5, -0.5], + [0.5, -0.5, -0.5], + [0.5, 0.5, -0.5], + [0.5, 0.5, 0.5], + [0.5, -0.5, 0.5], + ], + dtype=np.float32, + ) + + faces = np.array( + [ + 3, + 0, + 1, + 3, + 3, + 2, + 3, + 1, + 3, + 4, + 5, + 7, + 3, + 6, + 7, + 5, + 3, + 0, + 4, + 1, + 3, + 7, + 1, + 4, + 3, + 3, + 2, + 5, + 3, + 6, + 5, + 2, + 3, + 0, + 3, + 4, + 3, + 5, + 4, + 3, + 3, + 1, + 7, + 2, + 3, + 6, + 2, + 7, + ], + dtype=np.int64, + ) + + output = pv.compute_vhacd(points, faces) + + assert isinstance(output, list) + assert len(output) == 1 + assert len(output[0]) == 2 + + # expand the first result + vertices, faces = output[0] + + # should be floating point ndarray + assert isinstance(vertices, np.ndarray) + assert vertices.dtype.kind == "f" + + # should be unsigned int ndarray + assert isinstance(faces, np.ndarray) + assert faces.dtype.kind == "u" + + assert vertices.shape == (8, 3) + assert faces.shape == (12, 3) + + +def test_import(): + # check that the version is a string and roughly semantic-version + assert pv.__version__.count(".") == 2 + + +if __name__ == "__main__": + test_basic() + test_import() diff --git a/usage.py b/usage.py deleted file mode 100644 index 387ba19..0000000 --- a/usage.py +++ /dev/null @@ -1,14 +0,0 @@ -# -# christian.jaques@gmail.com -# -# demo code for the use of PyBind with the inclusion of a C module -# check the use of "extern "C"{" in display_array.h -# -import numpy as np -import wrapper - - -# instantiate a Python array that'll be sent to C++ and C -b = np.ones(10)*3 -wrapper.display(b) # example function that displays the array -