Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3296fd06e2 | ||
|
|
9102630227 | ||
|
|
e72bf3e86f | ||
|
|
4a97f065af | ||
|
|
0bc279c387 | ||
|
|
6ce0707aae | ||
|
|
7c172d99a4 | ||
|
|
86e975ba58 | ||
|
|
1a3a7b7b10 | ||
|
|
4d923768fa | ||
|
|
7a1e66396d | ||
|
|
88a4f90569 | ||
|
|
03ed5c8f00 | ||
|
|
f22bf6a8f7 | ||
|
|
3c6f121493 | ||
|
|
68b26c4579 | ||
|
|
3183842375 | ||
|
|
2dcabf97dd | ||
|
|
efa513a569 | ||
|
|
dd67710848 | ||
|
|
2523040b08 |
@@ -120,6 +120,9 @@ jobs:
|
||||
- name: install
|
||||
shell: powershell
|
||||
run: .\ci\install_windows.ps1
|
||||
- name: install_numpy
|
||||
shell: powershell
|
||||
run: .\ci\install_windows_numpy.ps1
|
||||
- name: build
|
||||
# nvcc doesn't set _WIN32 when run in bash so we need to set it manually
|
||||
shell: bash
|
||||
|
||||
@@ -7,6 +7,7 @@ A lightweight GPU friendly version of VDB initially targeting rendering applicat
|
||||
* [Frequently asked questions](../../doc/nanovdb/FAQ.md)
|
||||
* [Source tree](../../doc/nanovdb/SourceTree.md)
|
||||
* [Examples](../../doc/nanovdb/HelloWorld.md)
|
||||
* [Python examples](python/examples/)
|
||||
|
||||
### Copyright Contributors to the OpenVDB Project
|
||||
### SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// X-macro list of NanoVDB BuildT types currently exposed to Python.
|
||||
//
|
||||
// NanoVDB coding style says "avoid macro functions; use inline and templates
|
||||
// instead" (docs/codingstyle.txt). This file is a deliberate exception: it
|
||||
// must emit top-level template instantiations and binding registrations into
|
||||
// several translation units from one canonical list, which a C++ template
|
||||
// cannot do. The pattern is contained to this file plus the matching
|
||||
// per-consumer macro definitions; no runtime behavior is hidden behind a
|
||||
// macro function.
|
||||
//
|
||||
// Consumers define one or more of the kind-specific macros below before
|
||||
// including this file. Each macro is invoked once per matching type; any
|
||||
// macro the consumer does not define is treated as a no-op and reset after
|
||||
// the file is processed. This is the single point that must be edited when
|
||||
// adding (or removing) a Python-visible BuildT.
|
||||
//
|
||||
// Macros:
|
||||
// NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// Scalar value types with arithmetic semantics — exposed with full
|
||||
// NodeInfo + setVoxel accessors. Suffix forms Python class names
|
||||
// (e.g. "Float" -> "FloatGrid"). GridTypeEnum names the
|
||||
// nanovdb::GridType:: enumerator a grid of this BuildT carries
|
||||
// (used by the polymorphic handle.grid(n) dispatch).
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum)
|
||||
// Vector value types — exposed with a setVoxel accessor but no
|
||||
// NodeInfo. AccessorName is passed explicitly because the legacy
|
||||
// Python class names for the original two (Vec3f, RGBA8) are
|
||||
// inconsistent — kept as-is for backwards compatibility.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// The nanovdb::Point build type — exposed with a bare accessor.
|
||||
// Point grids carry GridType::PointIndex (NOT a GridType::Point;
|
||||
// that enumerator doesn't exist), so GridTypeEnum is given
|
||||
// explicitly. Polymorphic dispatch routes PointIndex grids to
|
||||
// NanoGrid<Point>.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// BuildTs whose nanovdb::SetVoxel<T> is unavailable (bool, the
|
||||
// quantized Fp4/Fp8/Fp16/FpN types, the index types ValueIndex /
|
||||
// ValueOnIndex, and ValueMask). nanovdb::BuildTraits<T>::is_special
|
||||
// is true for all of these. Exposed with a bare accessor only — no
|
||||
// setVoxel, no NodeInfo. The Python accessor's getValue() returns
|
||||
// the type given by nanovdb::BuildToValueMap<T>::Type — float for
|
||||
// Fp4 / Fp8 / Fp16 / FpN, uint64 for ValueIndex / ValueOnIndex,
|
||||
// bool for ValueMask and for the BuildT=bool (Boolean) grid.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix)
|
||||
// Subset that has C++ sampler specializations (used by PyMath
|
||||
// samplers and PyCreateNanoGrid create*Grid factories).
|
||||
|
||||
#ifndef NANOVDB_PY_FOR_EACH_SCALAR_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_VECTOR_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_POINT_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_READONLY_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#endif
|
||||
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(float, Float, Float)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(double, Double, Double)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int16_t, Int16, Int16)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int32_t, Int32, Int32)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int64_t, Int64, Int64)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(uint8_t, UInt8, UInt8)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(uint32_t, UInt32, UInt32)
|
||||
// nanovdb::Half is intentionally not bound. The source declares it as
|
||||
// `class Half{};` (an empty placeholder for IEEE 754 half-precision, see
|
||||
// NanoVDB.h around line 180) and the C++ ProbeValue<Half> chain is
|
||||
// inconsistent — leaf storage carries Half but the ProbeValue specialization
|
||||
// expects float, causing a type mismatch during instantiation. When the
|
||||
// upstream Half implementation lands this can be added in a follow-up.
|
||||
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3f, Vec3f,
|
||||
"Vec3fReadVectorAccessor", Vec3f)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3d, Vec3d,
|
||||
"Vec3dReadAccessor", Vec3d)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec4f, Vec4f,
|
||||
"Vec4fReadAccessor", Vec4f)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec4d, Vec4d,
|
||||
"Vec4dReadAccessor", Vec4d)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3u8, Vec3u8,
|
||||
"Vec3u8ReadAccessor", Vec3u8)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3u16, Vec3u16,
|
||||
"Vec3u16ReadAccessor", Vec3u16)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::math::Rgba8, RGBA8,
|
||||
"RGBA8ReadAccessor", RGBA8)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_POINT_BUILDT(::nanovdb::Point, Point, PointIndex)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(bool, Boolean, Boolean)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp4, Fp4, Fp4)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp8, Fp8, Fp8)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp16, Fp16, Fp16)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::FpN, FpN, FpN)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueIndex, Index, Index)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueOnIndex, OnIndex, OnIndex)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueMask, Mask, Mask)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(float, Float)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(double, Double)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(int32_t, Int32)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(::nanovdb::Vec3f, Vec3f)
|
||||
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#endif
|
||||
|
||||
#undef NANOVDB_PY_FOR_EACH_SCALAR_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_VECTOR_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_POINT_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_READONLY_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT
|
||||
@@ -4,8 +4,19 @@ option(NANOVDB_BUILD_PYTHON_UNITTESTS [=[
|
||||
"Include the NanoVDB Python unit test. Requires a python interpreter]=]
|
||||
${NANOVDB_BUILD_UNITTESTS})
|
||||
|
||||
# Stubs are only consumed from an installed wheel (they ship next to the .so
|
||||
# in nanovdb/), so default ON only when SKBUILD is driving the build. In an
|
||||
# in-source dev / OpenVDB-CI build the .pyi is not used and stub generation
|
||||
# can be skipped — which also dodges the macOS CI's sanitizer-runtime
|
||||
# DYLD_INSERT_LIBRARIES wrapper, under which Python dlopen'ing the .so
|
||||
# triggers TSan "Interceptors are not working" and aborts stubgen.
|
||||
option(NANOVDB_BUILD_PYTHON_STUBS
|
||||
"Generate .pyi type stubs for the nanovdb Python module (requires nanobind 2.x or newer)."
|
||||
${SKBUILD})
|
||||
|
||||
nanobind_add_module(nanovdb_python NB_STATIC
|
||||
NanoVDBModule.cc
|
||||
PyBuildGrid.cc
|
||||
PyCreateNanoGrid.cc
|
||||
PyGridChecksum.cc
|
||||
PyGridHandle.cc
|
||||
@@ -18,6 +29,8 @@ nanobind_add_module(nanovdb_python NB_STATIC
|
||||
PyPrimitives.cc
|
||||
PySampleFromVoxels.cc
|
||||
PyTools.cc
|
||||
PyTree.cc
|
||||
PyVoxelBlockManager.cc
|
||||
cuda/PyDeviceBuffer.cc
|
||||
cuda/PyDeviceGridHandle.cu
|
||||
cuda/PyPointsToGrid.cu
|
||||
@@ -33,10 +46,47 @@ if(SKBUILD)
|
||||
set_target_properties(nanovdb_python PROPERTIES INSTALL_RPATH "$ORIGIN/../../openvdb/lib")
|
||||
install(TARGETS nanovdb_python DESTINATION ${NANOVDB_INSTALL_LIBDIR})
|
||||
install(FILES __init__.py DESTINATION nanovdb)
|
||||
# Ship the nanovdb C/C++ headers inside the Python wheel so downstream
|
||||
# extension authors can compile against the same NanoVDB the wheel was
|
||||
# built with. nanovdb.get_include() resolves to this directory at runtime.
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../
|
||||
DESTINATION nanovdb/include/nanovdb
|
||||
FILES_MATCHING
|
||||
PATTERN "*.h"
|
||||
PATTERN "*.cuh"
|
||||
PATTERN "python" EXCLUDE
|
||||
PATTERN "examples" EXCLUDE
|
||||
PATTERN "unittest" EXCLUDE
|
||||
PATTERN "cmd" EXCLUDE
|
||||
PATTERN "docs" EXCLUDE)
|
||||
else()
|
||||
install(TARGETS nanovdb_python DESTINATION ${VDB_PYTHON_INSTALL_DIRECTORY})
|
||||
endif()
|
||||
|
||||
# .pyi type stubs for IDE / type-checker support. Driven by the CMake helper
|
||||
# that ships with nanobind 2.x; silently skipped on older nanobind where the
|
||||
# helper does not exist.
|
||||
if(NANOVDB_BUILD_PYTHON_STUBS AND COMMAND nanobind_add_stub)
|
||||
set(NANOVDB_STUB_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
nanobind_add_stub(nanovdb_python_stub
|
||||
MODULE nanovdb
|
||||
OUTPUT "${NANOVDB_STUB_OUTPUT_DIR}/nanovdb.pyi"
|
||||
PYTHON_PATH $<TARGET_FILE_DIR:nanovdb_python>
|
||||
DEPENDS nanovdb_python
|
||||
MARKER_FILE "${NANOVDB_STUB_OUTPUT_DIR}/py.typed"
|
||||
INCLUDE_PRIVATE)
|
||||
if(SKBUILD)
|
||||
install(FILES
|
||||
"${NANOVDB_STUB_OUTPUT_DIR}/nanovdb.pyi"
|
||||
"${NANOVDB_STUB_OUTPUT_DIR}/py.typed"
|
||||
DESTINATION nanovdb)
|
||||
endif()
|
||||
elseif(NANOVDB_BUILD_PYTHON_STUBS)
|
||||
message(STATUS
|
||||
"NANOVDB_BUILD_PYTHON_STUBS is ON but nanobind_add_stub is unavailable "
|
||||
"(nanobind >= 2.0 required). Skipping stub generation.")
|
||||
endif()
|
||||
|
||||
# pytest
|
||||
if(NANOVDB_BUILD_PYTHON_UNITTESTS)
|
||||
|
||||
@@ -49,12 +99,19 @@ if(NANOVDB_BUILD_PYTHON_UNITTESTS)
|
||||
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/TestNanoVDB.py -v
|
||||
WORKING_DIRECTORY "${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
|
||||
# Smoke-run every example script (they self-skip when optional
|
||||
# dependencies such as NumPy or OpenVDB are unavailable).
|
||||
add_test(NAME pytest_nanovdb_examples
|
||||
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/TestExamples.py -v
|
||||
WORKING_DIRECTORY "${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
set_tests_properties(pytest_nanovdb_examples PROPERTIES TIMEOUT 300)
|
||||
|
||||
if(WIN32)
|
||||
set(PYTHONPATH "$ENV{PYTHONPATH};${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
string(REPLACE "\\;" ";" PYTHONPATH "${PYTHONPATH}")
|
||||
string(REPLACE ";" "\\;" PYTHONPATH "${PYTHONPATH}")
|
||||
set_tests_properties(pytest_nanovdb PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHONPATH}")
|
||||
set_tests_properties(pytest_nanovdb pytest_nanovdb_examples PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHONPATH}")
|
||||
else()
|
||||
set_tests_properties(pytest_nanovdb PROPERTIES ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
set_tests_properties(pytest_nanovdb pytest_nanovdb_examples PROPERTIES ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+1001
-194
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyBuildGrid.h"
|
||||
|
||||
#include <nanobind/stl/array.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Bind nanovdb::tools::build::Grid<BuildT> together with its
|
||||
// ValueAccessor<BuildT> and Tree<BuildT>::WriteAccessor proxies, plus a
|
||||
// .to_nanovdb() shortcut that bakes the build grid into a host NanoGrid
|
||||
// via tools::createNanoGrid.
|
||||
//
|
||||
// One instantiation per writable BuildT in BuildTypes.def (scalars +
|
||||
// vectors). All three classes live under the nanovdb.tools.build submodule;
|
||||
// the Python class names mirror the existing typed-grid naming so
|
||||
// nanovdb.tools.build.FloatGrid is the mutable counterpart of the read-only
|
||||
// nanovdb.FloatGrid.
|
||||
template<typename BuildT>
|
||||
static void defineBuildGrid(nb::module_& m,
|
||||
const char* gridName,
|
||||
const char* valueAccName,
|
||||
const char* writeAccName)
|
||||
{
|
||||
using GridT = tools::build::Grid<BuildT>;
|
||||
using TreeT = tools::build::Tree<BuildT>;
|
||||
using AccT = tools::build::ValueAccessor<BuildT>;
|
||||
using WriteAccT = typename TreeT::WriteAccessor;
|
||||
using ValueT = typename GridT::ValueType;
|
||||
|
||||
// ----- build::Grid<BuildT> -----
|
||||
nb::class_<GridT>(m, gridName,
|
||||
"Mutable host-side grid builder for this BuildT. Write voxels via "
|
||||
"setValue / getAccessor / getWriteAccessor, then call to_nanovdb() "
|
||||
"to bake a frozen NanoGrid GridHandle.")
|
||||
.def(nb::init<const ValueT&, const std::string&, GridClass>(),
|
||||
"background"_a,
|
||||
"name"_a = std::string(""),
|
||||
"gridClass"_a = GridClass::Unknown,
|
||||
"Construct an empty mutable build grid. Voxels read as "
|
||||
"background until written.")
|
||||
.def("getValue",
|
||||
[](const GridT& self, const Coord& ijk) -> ValueT {
|
||||
return self.getValue(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return the value at ijk (background if no leaf covers it).")
|
||||
.def("setValue",
|
||||
[](GridT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Set the voxel value at ijk and mark the voxel active.")
|
||||
// build::Grid has no top-level isActive(ijk); the read path is via
|
||||
// ValueAccessor. Spin up a fresh accessor for the single query so
|
||||
// Python callers don't have to.
|
||||
.def("isActive",
|
||||
[](GridT& self, const Coord& ijk) {
|
||||
AccT acc = self.getAccessor();
|
||||
return acc.isActive(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return True iff ijk is in an active voxel. Equivalent to "
|
||||
"self.getAccessor().isActive(ijk), but allocates a fresh "
|
||||
"accessor for each call — for repeated queries use "
|
||||
"self.getAccessor() and reuse it.")
|
||||
.def("setValueOn",
|
||||
[](GridT& self, const Coord& ijk) {
|
||||
AccT acc = self.getAccessor();
|
||||
acc.setValueOn(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Mark ijk active without changing the stored value. Equivalent "
|
||||
"to self.getAccessor().setValueOn(ijk).")
|
||||
.def("nodeCount",
|
||||
[](const GridT& self) -> std::array<size_t, 3> {
|
||||
return self.nodeCount();
|
||||
},
|
||||
"Return a 3-tuple (leaf_count, lower_count, upper_count).")
|
||||
.def("gridType", &GridT::gridType,
|
||||
"Return the GridType enumerator this BuildT carries.")
|
||||
.def("gridClass", &GridT::gridClass,
|
||||
"Return the GridClass assigned at construction time.")
|
||||
.def("getName", &GridT::getName,
|
||||
"Return the grid name (as passed at construction).")
|
||||
.def("setName", &GridT::setName, "name"_a,
|
||||
"Replace the grid name.")
|
||||
.def("setTransform", &GridT::setTransform,
|
||||
"scale"_a = 1.0,
|
||||
"translation"_a = Vec3d(0.0),
|
||||
"Set an affine index-to-world map from a uniform scale and "
|
||||
"translation. Replaces any prior transform.")
|
||||
.def_prop_ro("background",
|
||||
[](const GridT& self) -> ValueT {
|
||||
return self.mRoot.background();
|
||||
},
|
||||
"The background value supplied at construction.")
|
||||
.def("getAccessor", &GridT::getAccessor,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a ValueAccessor wired to this grid's root. Thread-safe "
|
||||
"for reads, NOT thread-safe for writes. The accessor borrows "
|
||||
"from this grid — the grid must outlive it.")
|
||||
// WriteAccessor's defaulted move constructor would leave its
|
||||
// internal ValueAccessor's `mRoot&` reference dangling (it points
|
||||
// into the moved-from WriteAccessor's own mRoot field). Bypass
|
||||
// the move path entirely by heap-allocating and handing nanobind
|
||||
// ownership — the C++ object stays at a stable address for its
|
||||
// whole lifetime.
|
||||
.def("getWriteAccessor",
|
||||
[](GridT& self) -> WriteAccT* {
|
||||
return new WriteAccT(self.mRoot, self.mMutex);
|
||||
},
|
||||
nb::rv_policy::take_ownership,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a WriteAccessor for thread-safe writes; the accessor "
|
||||
"buffers changes into a private root and merges them into the "
|
||||
"parent grid on destruction (or on an explicit merge() call). "
|
||||
"Held by nanobind on the heap so the accessor's internal "
|
||||
"references stay valid.")
|
||||
// Baking a large grid is the most expensive operation on this
|
||||
// class; release the GIL so other Python threads can run during
|
||||
// the conversion (the lambda only touches C++ state).
|
||||
.def("to_nanovdb",
|
||||
[](const GridT& self,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
int verbose) {
|
||||
return tools::createNanoGrid<GridT, BuildT, HostBuffer>(
|
||||
self, sMode, cMode, verbose);
|
||||
},
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"verbose"_a = 0,
|
||||
"Bake this mutable grid into a host NanoGrid<BuildT> and "
|
||||
"return its GridHandle. The build grid is unchanged. "
|
||||
"Releases the GIL during conversion.");
|
||||
|
||||
// ----- build::ValueAccessor<BuildT> -----
|
||||
//
|
||||
// Move-only (copy is deleted) — returned by getAccessor(). Caches the
|
||||
// last leaf / lower / upper node it touched, so repeated access to
|
||||
// neighboring coordinates is fast.
|
||||
nb::class_<AccT>(m, valueAccName,
|
||||
"Move-only read/write accessor returned by build.Grid.getAccessor(). "
|
||||
"Caches the most recently visited tree path for fast neighbor access.")
|
||||
.def("getValue",
|
||||
[](const AccT& self, const Coord& ijk) -> ValueT {
|
||||
return self.getValue(ijk);
|
||||
},
|
||||
"ijk"_a, "Return the value at ijk (uses the cache).")
|
||||
.def("setValue",
|
||||
[](AccT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Set the value at ijk and mark it active (uses the cache).")
|
||||
.def("setValueOn",
|
||||
[](AccT& self, const Coord& ijk) { self.setValueOn(ijk); },
|
||||
"ijk"_a,
|
||||
"Mark ijk active without changing the stored value.")
|
||||
.def("isActive",
|
||||
[](const AccT& self, const Coord& ijk) {
|
||||
return self.isActive(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return True iff ijk is in an active voxel.")
|
||||
.def("isValueOn",
|
||||
[](const AccT& self, const Coord& ijk) {
|
||||
return self.isValueOn(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Alias for isActive(ijk).");
|
||||
|
||||
// ----- build::Tree<BuildT>::WriteAccessor -----
|
||||
//
|
||||
// Move-only. Holds its own root node + a reference to the parent root's
|
||||
// mutex; on destruction (or explicit merge()) it locks the mutex and
|
||||
// splices its buffered nodes into the parent. Designed for multi-thread
|
||||
// writes — one WriteAccessor per thread, no shared mutable state.
|
||||
nb::class_<WriteAccT>(m, writeAccName,
|
||||
"Thread-safe write accessor returned by build.Grid.getWriteAccessor(). "
|
||||
"Buffers writes into a private root and merges them into the parent "
|
||||
"grid on destruction or explicit merge().")
|
||||
.def("setValue",
|
||||
[](WriteAccT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Buffer a set into this accessor's private root.")
|
||||
.def("setValueOn",
|
||||
[](WriteAccT& self, const Coord& ijk) { self.setValueOn(ijk); },
|
||||
"ijk"_a,
|
||||
"Buffer an active-state set into this accessor's private root.")
|
||||
.def("merge", &WriteAccT::merge,
|
||||
"Lock the parent mutex and splice this accessor's buffered "
|
||||
"nodes into the parent grid. Called automatically when the "
|
||||
"accessor is destroyed; calling it explicitly is only "
|
||||
"necessary if you want the changes visible to the parent "
|
||||
"before the accessor goes out of scope.");
|
||||
}
|
||||
|
||||
void defineBuildGridModule(nb::module_& toolsModule)
|
||||
{
|
||||
nb::module_ buildModule = toolsModule.def_submodule("build");
|
||||
buildModule.doc() =
|
||||
"Mutable, voxel-by-voxel CPU grid builder mirroring "
|
||||
"nanovdb::tools::build::*. Construct a typed Grid (e.g. "
|
||||
"FloatGrid(0.0, 'mygrid')), populate it with setValue / "
|
||||
"ValueAccessor / WriteAccessor, then call .to_nanovdb() to bake "
|
||||
"a host NanoGrid handle.";
|
||||
|
||||
// X-macro instantiation over every writable BuildT: scalars (full
|
||||
// arithmetic) and vectors. Read-only special BuildTs (Boolean, Fp*,
|
||||
// Index, Mask) and Point are deliberately excluded — they have no
|
||||
// SetValue<T> specialization and can't be built voxel-by-voxel.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
defineBuildGrid<T>(buildModule, #Suffix "Grid", \
|
||||
#Suffix "ValueAccessor", #Suffix "WriteAccessor");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
defineBuildGrid<T>(buildModule, #Suffix "Grid", \
|
||||
#Suffix "ValueAccessor", #Suffix "WriteAccessor");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYBUILDGRID_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYBUILDGRID_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
/// @brief Register the nanovdb.tools.build submodule and its per-BuildT
|
||||
/// Grid / ValueAccessor / WriteAccessor classes (one set per writable
|
||||
/// scalar and vector BuildT in BuildTypes.def). Constructs the submodule
|
||||
/// as `toolsModule.def_submodule("build")`.
|
||||
void defineBuildGridModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -11,6 +11,10 @@
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
@@ -35,20 +39,553 @@ GridHandle<HostBuffer> createNanoGridFromFunc(const BuildT&
|
||||
|
||||
template<typename BuildT> void defineCreateNanoGrid(nb::module_& m, const char* name)
|
||||
{
|
||||
m.def(name, &createNanoGridFromFunc<BuildT>, nb::call_guard<nb::gil_scoped_release>(), "background"_a, "name"_a, "gridClass"_a, "func"_a, "bbox"_a);
|
||||
m.def(name, &createNanoGridFromFunc<BuildT>, nb::call_guard<nb::gil_scoped_release>(), "background"_a, "name"_a, "gridClass"_a, "func"_a, "bbox"_a,
|
||||
"Construct a NanoGrid by evaluating func(Coord) over every voxel in bbox. "
|
||||
"Returns a GridHandle owning the freshly-built grid.");
|
||||
}
|
||||
|
||||
template<typename BufferT> void defineOpenToNanoVDB(nb::module_& m)
|
||||
{
|
||||
#ifdef NANOVDB_USE_OPENVDB
|
||||
m.def("openToNanoVDB", &tools::openToNanoVDB<BufferT>, "base"_a, "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, "verbose"_a = 0);
|
||||
m.def("openToNanoVDB", &tools::openToNanoVDB<BufferT>,
|
||||
"base"_a,
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"verbose"_a = 0,
|
||||
"Convert an OpenVDB base grid to a NanoVDB GridHandle.");
|
||||
#endif
|
||||
}
|
||||
|
||||
template void defineCreateNanoGrid<float>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<double>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<int32_t>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<Vec3f>(nb::module_&, const char*);
|
||||
// ============================================================================
|
||||
// Conversion bindings: AbsDiff/RelDiff oracle classes, and the polymorphic
|
||||
// createNanoGrid free functions for quantized + index destination BuildTs.
|
||||
// Each accepts source = NanoGrid<SrcBuildT> OR build::Grid<SrcBuildT>.
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
// ----- Quantized (Fp4/Fp8/Fp16) -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, DstBuildT, BufferT>(srcGrid,
|
||||
// sMode, cMode, ditherOn, verbose, buffer).
|
||||
//
|
||||
// Try SrcBuildT against both NanoGrid<SrcBuildT> and build::Grid<SrcBuildT>
|
||||
// and return an empty nb::object on no match so the caller can fall through
|
||||
// to the next SrcBuildT.
|
||||
//
|
||||
// GIL is held for the isinstance / cast dispatch (which touches the Python
|
||||
// object's type and reference graph) but released around the underlying
|
||||
// tools::createNanoGrid traversal — the source data lives in stable C++
|
||||
// storage whose lifetime is anchored by the Python wrapper passed in via
|
||||
// py_src, so it's safe to read without holding the GIL.
|
||||
template<typename SrcBuildT, typename DstBuildT>
|
||||
nb::object tryQuantizeFpX(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, DstBuildT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, DstBuildT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename DstBuildT>
|
||||
nb::object createNanoGridFpX(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose,
|
||||
const char* pyFnName)
|
||||
{
|
||||
// The C++ Fp{4,8,16,N} preProcess static_asserts SrcValueT == float;
|
||||
// double sources hit a compile-time error, so we accept float only.
|
||||
if (auto r = tryQuantizeFpX<float, DstBuildT>(
|
||||
py_src, sMode, cMode, ditherOn, verbose); r.is_valid()) return r;
|
||||
std::string msg(pyFnName);
|
||||
msg += ": source must be a FloatGrid or nanovdb.tools.build.FloatGrid "
|
||||
"(Fp4/Fp8/Fp16/FpN require a float source value type).";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
|
||||
// ----- FpN (variable bit-width) -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, FpN, OracleT, BufferT>(srcGrid,
|
||||
// sMode, cMode, ditherOn, verbose, oracle, buffer). OracleT is AbsDiff or
|
||||
// RelDiff; the binding exposes both as separate Python overloads.
|
||||
template<typename SrcBuildT, typename OracleT>
|
||||
nb::object tryQuantizeFpN(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose,
|
||||
const OracleT& oracle)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
// Same GIL pattern as tryQuantizeFpX: hold the GIL through the
|
||||
// isinstance / cast dispatch, release it for the conversion.
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, FpN, OracleT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose, oracle);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, FpN, OracleT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose, oracle);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename OracleT>
|
||||
nb::object createNanoGridFpNImpl(nb::handle py_src,
|
||||
const OracleT& oracle,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose)
|
||||
{
|
||||
if (auto r = tryQuantizeFpN<float, OracleT>(
|
||||
py_src, sMode, cMode, ditherOn, verbose, oracle); r.is_valid()) return r;
|
||||
throw nb::type_error(
|
||||
"createNanoGridFpN: source must be a FloatGrid or "
|
||||
"nanovdb.tools.build.FloatGrid (FpN requires a float source value type).");
|
||||
}
|
||||
|
||||
// ----- Index / OnIndex -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, DstBuildT, BufferT>(srcGrid,
|
||||
// channels, includeStats, includeTiles, verbose, buffer). DstBuildT is
|
||||
// ValueIndex or ValueOnIndex; the binding exposes both as separate
|
||||
// named functions. Source set is wider than the quantized variants —
|
||||
// any arithmetic or vector source can be re-cast as an index grid.
|
||||
template<typename SrcBuildT, typename DstBuildT>
|
||||
nb::object tryIndexify(nb::handle py_src,
|
||||
uint32_t channels,
|
||||
bool includeStats,
|
||||
bool includeTiles,
|
||||
int verbose)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
// Same GIL pattern as tryQuantizeFpX.
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, DstBuildT, HostBuffer>(
|
||||
src, channels, includeStats, includeTiles, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, DstBuildT, HostBuffer>(
|
||||
src, channels, includeStats, includeTiles, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename DstBuildT>
|
||||
nb::object createIndexImpl(nb::handle py_src,
|
||||
uint32_t channels,
|
||||
bool includeStats,
|
||||
bool includeTiles,
|
||||
int verbose,
|
||||
const char* pyFnName)
|
||||
{
|
||||
if (auto r = tryIndexify<float, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<double, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<int32_t, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<Vec3f, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
std::string msg(pyFnName);
|
||||
msg += ": source must be a FloatGrid, DoubleGrid, Int32Grid, "
|
||||
"Vec3fGrid, or the matching nanovdb.tools.build.* mutable grid.";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
|
||||
// ----- tools.CreateNanoGrid: converter class with blind-data authoring -----
|
||||
//
|
||||
// Mirrors nanovdb::tools::CreateNanoGrid<SrcGridT>. The C++ class is
|
||||
// templated on the source grid type, so this binding stores the source
|
||||
// Python object plus the recorded settings and addBlindData() calls, then
|
||||
// dispatches over the supported SrcBuildTs when getHandle() is called —
|
||||
// constructing the C++ converter, replaying the recorded state, and baking
|
||||
// the handle. The destination BuildT is the source BuildT (the C++
|
||||
// default); quantized and index destinations remain on the
|
||||
// createNanoGridFp* / createNanoGridIndex free functions above. Authored
|
||||
// channels come back zeroed — fill them through the writable NumPy view
|
||||
// returned by grid.getBlindData(n).
|
||||
|
||||
// Byte size of one element of the given blind-data GridType, or 0 when the
|
||||
// size cannot be derived (the caller must then pass it explicitly). Matches
|
||||
// the per-type size table enforced by GridBlindMetaData::isValid().
|
||||
uint32_t blindDataTypeSize(GridType dataType)
|
||||
{
|
||||
switch (dataType) {
|
||||
case GridType::Float: return 4u;
|
||||
case GridType::Double: return 8u;
|
||||
case GridType::Int16: return 2u;
|
||||
case GridType::Int32: return 4u;
|
||||
case GridType::Int64: return 8u;
|
||||
case GridType::UInt8: return 1u;
|
||||
case GridType::UInt32: return 4u;
|
||||
case GridType::Half: return 2u;
|
||||
case GridType::RGBA8: return 4u;
|
||||
case GridType::Fp8: return 1u;
|
||||
case GridType::Fp16: return 2u;
|
||||
case GridType::Vec3f: return 12u;
|
||||
case GridType::Vec3d: return 24u;
|
||||
case GridType::Vec4f: return 16u;
|
||||
case GridType::Vec4d: return 32u;
|
||||
case GridType::Vec3u8: return 3u;
|
||||
case GridType::Vec3u16: return 6u;
|
||||
default: return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
class PyCreateNanoGrid
|
||||
{
|
||||
struct BlindDataSpec
|
||||
{
|
||||
std::string name;
|
||||
GridBlindDataSemantic semantic;
|
||||
GridBlindDataClass dataClass;
|
||||
GridType dataType;
|
||||
uint64_t count;
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit PyCreateNanoGrid(nb::object src)
|
||||
: mSrc(std::move(src))
|
||||
{
|
||||
if (!(matches<float>() || matches<double>() ||
|
||||
matches<int32_t>() || matches<Vec3f>())) {
|
||||
throw nb::type_error(
|
||||
"CreateNanoGrid: source must be a FloatGrid, DoubleGrid, "
|
||||
"Int32Grid, Vec3fGrid, or the matching "
|
||||
"nanovdb.tools.build.* mutable grid.");
|
||||
}
|
||||
}
|
||||
|
||||
// Validates eagerly (the C++ ctor only NANOVDB_ASSERTs, which release
|
||||
// builds skip) so mistakes surface here rather than as an invalid grid.
|
||||
uint64_t addBlindData(const std::string& name,
|
||||
uint64_t count,
|
||||
GridType dataType,
|
||||
GridBlindDataSemantic semantic,
|
||||
GridBlindDataClass dataClass,
|
||||
uint32_t size)
|
||||
{
|
||||
if (name.size() >= GridBlindMetaData::MaxNameSize) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: name exceeds the 255 character limit.");
|
||||
}
|
||||
if (size == 0u) size = blindDataTypeSize(dataType);
|
||||
if (size == 0u) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: the element size cannot be derived from this "
|
||||
"dataType — pass size explicitly.");
|
||||
}
|
||||
const GridBlindMetaData meta(0, count, size, semantic, dataClass, dataType);
|
||||
if (!meta.isValid()) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: invalid combination of dataSemantic, "
|
||||
"dataClass, dataType, and size.");
|
||||
}
|
||||
mBlind.push_back(BlindDataSpec{name, semantic, dataClass, dataType, count, size});
|
||||
return static_cast<uint64_t>(mBlind.size() - 1);
|
||||
}
|
||||
|
||||
void setStats(tools::StatsMode mode) { mStats = mode; }
|
||||
void setChecksum(CheckMode mode) { mChecksum = mode; }
|
||||
void setVerbose(int mode) { mVerbose = mode; }
|
||||
void enableDithering(bool on) { mDither = on; }
|
||||
|
||||
nb::object getHandle() const
|
||||
{
|
||||
if (auto r = tryGetHandle<float>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<double>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<int32_t>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<Vec3f>(); r.is_valid()) return r;
|
||||
throw nb::type_error("CreateNanoGrid: unsupported source grid type.");
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename SrcBuildT> bool matches() const
|
||||
{
|
||||
return nb::isinstance<NanoGrid<SrcBuildT>>(mSrc) ||
|
||||
nb::isinstance<tools::build::Grid<SrcBuildT>>(mSrc);
|
||||
}
|
||||
|
||||
template<typename SrcBuildT> nb::object tryGetHandle() const
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
if (nb::isinstance<NanoSrcT>(mSrc)) return this->bake(nb::cast<const NanoSrcT&>(mSrc));
|
||||
if (nb::isinstance<BuildSrcT>(mSrc)) return this->bake(nb::cast<const BuildSrcT&>(mSrc));
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
// Same GIL pattern as tryQuantizeFpX: the dispatch above runs with the
|
||||
// GIL held, the traversal runs without it (the source's lifetime is
|
||||
// anchored by mSrc).
|
||||
template<typename SrcGridT> nb::object bake(const SrcGridT& src) const
|
||||
{
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
tools::CreateNanoGrid<SrcGridT> converter(src);
|
||||
converter.setStats(mStats);
|
||||
converter.setChecksum(mChecksum);
|
||||
converter.setVerbose(mVerbose);
|
||||
converter.enableDithering(mDither);
|
||||
for (const auto& b : mBlind) {
|
||||
converter.addBlindData(b.name, b.semantic, b.dataClass, b.dataType,
|
||||
static_cast<size_t>(b.count),
|
||||
static_cast<size_t>(b.size));
|
||||
}
|
||||
handle = converter.getHandle();
|
||||
// The C++ converter allocates authored channels without clearing
|
||||
// them (C++ callers memcpy their payload in). Zero-fill here so
|
||||
// the NumPy view starts deterministic. The authored channels are
|
||||
// the first mBlind.size() blind-data entries — any converter-
|
||||
// added channel (e.g. a long grid name) is appended after them.
|
||||
if (!mBlind.empty()) {
|
||||
if (auto* dst = const_cast<GridData*>(handle.gridData())) {
|
||||
for (size_t i = 0; i < mBlind.size(); ++i) {
|
||||
const GridBlindMetaData* meta = dst->blindMetaData(uint32_t(i));
|
||||
std::memset(const_cast<void*>(meta->blindData()), 0,
|
||||
meta->blindDataSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
|
||||
nb::object mSrc;
|
||||
std::vector<BlindDataSpec> mBlind;
|
||||
tools::StatsMode mStats = tools::StatsMode::Default;
|
||||
CheckMode mChecksum = CheckMode::Default;
|
||||
int mVerbose = 0;
|
||||
bool mDither = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineCreateNanoGridConversions(nb::module_& toolsModule)
|
||||
{
|
||||
// ------ Oracle classes ------
|
||||
nb::class_<tools::AbsDiff>(toolsModule, "AbsDiff",
|
||||
"Compression oracle for FpN: accept the approximation when "
|
||||
"|exact - approx| <= tolerance. A tolerance of -1.0 (the "
|
||||
"default) means uninitialized; any non-negative value (including "
|
||||
"0.0) is treated as initialized by the operator bool() check, "
|
||||
"or the C++ create function can fill it in via init().")
|
||||
.def(nb::init<float>(), "tolerance"_a = -1.0f,
|
||||
"Construct an AbsDiff oracle with the given absolute tolerance.")
|
||||
.def("getTolerance", &tools::AbsDiff::getTolerance,
|
||||
"Return the current absolute tolerance.")
|
||||
.def("setTolerance", &tools::AbsDiff::setTolerance, "tolerance"_a,
|
||||
"Replace the current absolute tolerance.")
|
||||
.def("__bool__",
|
||||
[](const tools::AbsDiff& self) { return bool(self); },
|
||||
"True iff the tolerance has been initialized (>= 0).");
|
||||
|
||||
nb::class_<tools::RelDiff>(toolsModule, "RelDiff",
|
||||
"Compression oracle for FpN: accept the approximation when "
|
||||
"|exact - approx| / max(|exact|, |approx|) <= tolerance.")
|
||||
.def(nb::init<float>(), "tolerance"_a = -1.0f,
|
||||
"Construct a RelDiff oracle with the given relative tolerance.")
|
||||
.def("getTolerance", &tools::RelDiff::getTolerance,
|
||||
"Return the current relative tolerance.")
|
||||
.def("setTolerance", &tools::RelDiff::setTolerance, "tolerance"_a,
|
||||
"Replace the current relative tolerance.")
|
||||
.def("__bool__",
|
||||
[](const tools::RelDiff& self) { return bool(self); },
|
||||
"True iff the tolerance has been initialized (>= 0).");
|
||||
|
||||
// ------ Quantized fixed-width: Fp4 / Fp8 / Fp16 ------
|
||||
toolsModule.def("createNanoGridFp4",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp4>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp4");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a "
|
||||
"NanoGrid<Fp4> (4 bits per voxel). ditherOn adds sub-quantum "
|
||||
"noise to break up banding.");
|
||||
|
||||
toolsModule.def("createNanoGridFp8",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp8>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp8");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<Fp8> (8 bits "
|
||||
"per voxel). ditherOn adds sub-quantum noise.");
|
||||
|
||||
toolsModule.def("createNanoGridFp16",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp16>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp16");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<Fp16> (16 bits "
|
||||
"per voxel). ditherOn adds sub-quantum noise.");
|
||||
|
||||
// ------ Variable bit-width: FpN with AbsDiff or RelDiff oracle ------
|
||||
//
|
||||
// Two overloads — one per oracle type. Python dispatch picks the
|
||||
// right one from the oracle argument's type. The createNanoGrid C++
|
||||
// template's parameter order is (src, sMode, cMode, ditherOn, verbose,
|
||||
// oracle, buffer); we reorder for Python so oracle comes second
|
||||
// (most callers want to specify it explicitly), then mode/dither
|
||||
// parameters as kwargs with defaults.
|
||||
toolsModule.def("createNanoGridFpN",
|
||||
[](nb::handle src, const tools::AbsDiff& oracle,
|
||||
tools::StatsMode sMode, CheckMode cMode, bool ditherOn, int verbose) {
|
||||
return createNanoGridFpNImpl(src, oracle, sMode, cMode, ditherOn, verbose);
|
||||
},
|
||||
"src"_a, "oracle"_a = tools::AbsDiff(),
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<FpN> (variable "
|
||||
"bits per voxel; each leaf picks the smallest N that satisfies "
|
||||
"the oracle's tolerance). Pass an AbsDiff oracle for absolute "
|
||||
"error bound, or use the RelDiff overload for relative error.");
|
||||
|
||||
toolsModule.def("createNanoGridFpN",
|
||||
[](nb::handle src, const tools::RelDiff& oracle,
|
||||
tools::StatsMode sMode, CheckMode cMode, bool ditherOn, int verbose) {
|
||||
return createNanoGridFpNImpl(src, oracle, sMode, cMode, ditherOn, verbose);
|
||||
},
|
||||
"src"_a, "oracle"_a,
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"FpN overload accepting a RelDiff oracle for relative error.");
|
||||
|
||||
// ------ Index / OnIndex ------
|
||||
//
|
||||
// createNanoGridIndex / createNanoGridOnIndex are the canonical names
|
||||
// for the broad-source-coverage index conversion bindings. A narrower
|
||||
// createOnIndexGrid factory still lives in PyVoxelBlockManager.cc as
|
||||
// the test scaffolding entry point used by the VBM unit tests.
|
||||
toolsModule.def("createNanoGridIndex",
|
||||
[](nb::handle src, uint32_t channels, bool includeStats,
|
||||
bool includeTiles, int verbose) {
|
||||
return createIndexImpl<ValueIndex>(
|
||||
src, channels, includeStats, includeTiles, verbose,
|
||||
"createNanoGridIndex");
|
||||
},
|
||||
"src"_a, "channels"_a = 0u, "includeStats"_a = true,
|
||||
"includeTiles"_a = true, "verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueIndex>. Every voxel "
|
||||
"(active or inactive) gets a unique uint64 sequential index, with "
|
||||
"the original values stored as blind data when channels > 0.");
|
||||
|
||||
toolsModule.def("createNanoGridOnIndex",
|
||||
[](nb::handle src, uint32_t channels, bool includeStats,
|
||||
bool includeTiles, int verbose) {
|
||||
return createIndexImpl<ValueOnIndex>(
|
||||
src, channels, includeStats, includeTiles, verbose,
|
||||
"createNanoGridOnIndex");
|
||||
},
|
||||
"src"_a, "channels"_a = 0u, "includeStats"_a = true,
|
||||
"includeTiles"_a = true, "verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueOnIndex>. Only the "
|
||||
"active voxels get a sequential index — the canonical input to "
|
||||
"buildVoxelBlockManager.");
|
||||
|
||||
// ------ CreateNanoGrid converter class (blind-data authoring) ------
|
||||
nb::class_<PyCreateNanoGrid>(toolsModule, "CreateNanoGrid",
|
||||
"Reusable converter mirroring nanovdb::tools::CreateNanoGrid. "
|
||||
"Construct from a NanoGrid or nanovdb.tools.build.* grid (float, "
|
||||
"double, int32, or Vec3f), optionally declare blind-data channels "
|
||||
"with addBlindData(), then bake a fresh NanoGrid of the same BuildT "
|
||||
"with getHandle(). Authored channels come back zero-filled — write "
|
||||
"their contents through the writable NumPy view returned by "
|
||||
"grid.getBlindData(n). For quantized (Fp*) or index destinations "
|
||||
"use the createNanoGridFp* / createNanoGridIndex functions instead.")
|
||||
.def(nb::init<nb::object>(), "srcGrid"_a,
|
||||
"Construct a converter reading from srcGrid (a NanoGrid or "
|
||||
"nanovdb.tools.build.* grid of BuildT float, double, int32, or "
|
||||
"Vec3f). The converter keeps srcGrid alive.")
|
||||
.def("addBlindData", &PyCreateNanoGrid::addBlindData,
|
||||
"name"_a, "count"_a, "dataType"_a = GridType::Float,
|
||||
"dataSemantic"_a = GridBlindDataSemantic::Unknown,
|
||||
"dataClass"_a = GridBlindDataClass::AttributeArray,
|
||||
"size"_a = 0u,
|
||||
"Declare a blind-data channel of count elements of dataType to "
|
||||
"be allocated in the destination grid, and return its channel "
|
||||
"index. size (bytes per element) is derived from dataType when "
|
||||
"omitted; pass it explicitly for dataTypes without a fixed "
|
||||
"element size. The C++ signature orders the parameters (name, "
|
||||
"dataSemantic, dataClass, dataType, count, size) — reordered "
|
||||
"here so the common case reads addBlindData(name, count).")
|
||||
.def("setStats", &PyCreateNanoGrid::setStats, "mode"_a,
|
||||
"Set the StatsMode used when baking the destination grid.")
|
||||
.def("setChecksum", &PyCreateNanoGrid::setChecksum, "mode"_a,
|
||||
"Set the CheckMode used when baking the destination grid.")
|
||||
.def("setVerbose", &PyCreateNanoGrid::setVerbose, "mode"_a = 1,
|
||||
"Set the verbosity level used when baking the destination grid.")
|
||||
.def("enableDithering", &PyCreateNanoGrid::enableDithering, "on"_a = true,
|
||||
"Toggle dithering of the destination grid (only meaningful for "
|
||||
"quantized BuildTs; kept for parity with the C++ class).")
|
||||
.def("getHandle", &PyCreateNanoGrid::getHandle,
|
||||
"Bake and return a GridHandle owning a NanoGrid of the source's "
|
||||
"BuildT, including any channels declared via addBlindData(). "
|
||||
"Each call bakes a fresh grid.");
|
||||
}
|
||||
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
template void defineCreateNanoGrid<T>(nb::module_&, const char*);
|
||||
#include "BuildTypes.def"
|
||||
|
||||
template void defineOpenToNanoVDB<HostBuffer>(nb::module_&);
|
||||
|
||||
|
||||
@@ -15,6 +15,21 @@ template<typename BuildT> void defineCreateNanoGrid(nb::module_& m, const char*
|
||||
template<typename BufferT> void defineOpenToNanoVDB(nb::module_& m);
|
||||
#endif
|
||||
|
||||
/// @brief Bind the AbsDiff / RelDiff quantization oracle classes and the
|
||||
/// polymorphic createNanoGridFp4 / Fp8 / Fp16 / FpN / Index / OnIndex
|
||||
/// free functions on the nanovdb.tools submodule. Sources accepted
|
||||
/// include both NanoGrid<SrcBuildT> and tools::build::Grid<SrcBuildT>:
|
||||
/// the quantized createNanoGridFp* / FpN paths accept float only
|
||||
/// (C++ Fp{4,8,16,N}::preProcess static-asserts SrcValueT == float);
|
||||
/// the createNanoGridIndex / OnIndex paths accept float, double,
|
||||
/// int32_t, and Vec3f sources. Additional source BuildTs can be
|
||||
/// added by extending the explicit try-each-SrcBuildT chains in
|
||||
/// createNanoGridFpX / FpNImpl / createIndexImpl. Also binds the
|
||||
/// tools.CreateNanoGrid converter class, which mirrors the C++
|
||||
/// tools::CreateNanoGrid and adds blind-data authoring via
|
||||
/// addBlindData() (same source BuildT set as the index paths).
|
||||
void defineCreateNanoGridConversions(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineCheckMode(nb::module_& m)
|
||||
{
|
||||
nb::enum_<CheckMode>(m, "CheckMode")
|
||||
nb::enum_<CheckMode>(m, "CheckMode",
|
||||
"Selector controlling how aggressively a grid checksum is computed: "
|
||||
"Disable skips checksumming, Partial covers only the header, Full "
|
||||
"covers the whole grid, and Default picks the recommended mode.")
|
||||
.value("Disable", CheckMode::Disable)
|
||||
.value("Partial", CheckMode::Partial)
|
||||
.value("Full", CheckMode::Full)
|
||||
@@ -24,13 +27,65 @@ void defineCheckMode(nb::module_& m)
|
||||
|
||||
void defineChecksum(nb::module_& m)
|
||||
{
|
||||
nb::class_<Checksum>(m, "Checksum").def(nb::self == nb::self, "rhs"_a).def(nb::self != nb::self, "rhs"_a);
|
||||
nb::class_<Checksum>(m, "Checksum",
|
||||
"64-bit checksum value stored in a grid header. Produced by "
|
||||
"tools.evalChecksum and compared against the stored one by "
|
||||
"tools.validateChecksum.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of two Checksum values.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of two Checksum values.")
|
||||
.def("isEmpty", &Checksum::isEmpty,
|
||||
"True iff no checksum is stored (checksumming was disabled).")
|
||||
.def("isHalf", &Checksum::isHalf,
|
||||
"True iff only the header portion (grid + tree + root) is checksummed.")
|
||||
.def("isFull", &Checksum::isFull,
|
||||
"True iff both the header portion and all nodes are checksummed.")
|
||||
.def("mode", &Checksum::mode,
|
||||
"CheckMode this checksum was computed with (Disable, Partial, or Full).");
|
||||
}
|
||||
|
||||
void defineUpdateChecksum(nb::module_& m)
|
||||
{
|
||||
m.def(
|
||||
"updateChecksum", [](GridData* gridData, CheckMode mode) { tools::updateChecksum(gridData, mode); }, "gridData"_a, "mode"_a);
|
||||
"updateChecksum", [](GridData* gridData, CheckMode mode) { tools::updateChecksum(gridData, mode); }, "gridData"_a, "mode"_a,
|
||||
"Recompute and store the checksum of gridData using the given CheckMode.");
|
||||
}
|
||||
|
||||
void defineEvalChecksumModule(nb::module_& toolsModule)
|
||||
{
|
||||
// tools.evalChecksum(grid, mode) — compute a fresh checksum for the
|
||||
// given grid without writing it back. Mirrors the GridData* overload
|
||||
// in tools/GridChecksum.h; the polymorphism over BuildT is implicit
|
||||
// because every NanoGrid<T> is-a GridData in the C++ hierarchy (and
|
||||
// the Python class binding declares NanoGrid<T> as derived from
|
||||
// GridData).
|
||||
toolsModule.def("evalChecksum",
|
||||
[](const GridData* gridData, CheckMode mode) -> Checksum {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("evalChecksum: grid is None.");
|
||||
}
|
||||
return tools::evalChecksum(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Compute and return the Checksum for the given grid using the "
|
||||
"specified CheckMode. Does not modify the grid.");
|
||||
|
||||
// tools.validateChecksum(grid, mode) — compare the stored checksum
|
||||
// against a freshly computed one and return a bool.
|
||||
toolsModule.def("validateChecksum",
|
||||
[](const GridData* gridData, CheckMode mode) -> bool {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("validateChecksum: grid is None.");
|
||||
}
|
||||
return tools::validateChecksum(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Return True iff the grid's stored checksum matches a freshly "
|
||||
"computed one for the given CheckMode. A grid with no stored "
|
||||
"checksum (Checksum.isEmpty()) is considered valid.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -13,6 +13,11 @@ void defineCheckMode(nb::module_& m);
|
||||
void defineChecksum(nb::module_& m);
|
||||
void defineUpdateChecksum(nb::module_& m);
|
||||
|
||||
/// @brief Bind tools.evalChecksum and tools.validateChecksum on the
|
||||
/// nanovdb.tools submodule. Both accept any bound NanoGrid via
|
||||
/// GridData* upcast (handled by nanobind's class hierarchy).
|
||||
void defineEvalChecksumModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,7 @@ void defineHostGridHandle(nb::module_& m)
|
||||
new (&handle) GridHandle<BufferT>(std::move(buffer));
|
||||
},
|
||||
"t"_a.noconvert());
|
||||
defineGridHandleUtilities<BufferT>(m);
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -7,62 +7,226 @@
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/tools/GridChecksum.h> // for tools::updateGridCount
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Polymorphic host-side `handle.grid(n)`: dispatch on gridType(n) to the
|
||||
// matching NanoGrid<BuildT> subclass currently bound in Python. Returns
|
||||
// None when the underlying BuildT is not bound in this build — see the
|
||||
// list of bound types in BuildTypes.def. The returned object is parented
|
||||
// to the handle so the handle is kept alive at least as long as the grid.
|
||||
template<typename BufferT>
|
||||
inline nb::object pyHostGrid(nb::handle py_handle, uint32_t n)
|
||||
{
|
||||
auto& handle = nb::cast<nanovdb::GridHandle<BufferT>&>(py_handle);
|
||||
if (n >= handle.gridCount()) return nb::none();
|
||||
switch (handle.gridType(n)) {
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
default:
|
||||
return nb::none();
|
||||
}
|
||||
}
|
||||
|
||||
// Free functions splitGrids / mergeGrids exposed at module scope. Templated
|
||||
// on BufferT so the same definitions work for both the host GridHandle and
|
||||
// the device GridHandle bindings.
|
||||
template<typename BufferT> void defineGridHandleUtilities(nb::module_& m)
|
||||
{
|
||||
using HandleT = nanovdb::GridHandle<BufferT>;
|
||||
m.def("splitGrids", [](const HandleT& handle) {
|
||||
auto handles = nanovdb::splitGrids(handle);
|
||||
nb::list out;
|
||||
for (auto& h : handles) {
|
||||
out.append(std::move(h));
|
||||
}
|
||||
return out;
|
||||
}, nb::arg("handle"),
|
||||
"Split a multi-grid handle into a list of single-grid handles, "
|
||||
"each owning a freshly-allocated buffer.");
|
||||
// mergeGrids: walk the Python sequence by CONST ref to each handle and
|
||||
// concatenate buffer bytes into a freshly-allocated output. The original
|
||||
// nanovdb::mergeGrids takes a `const std::vector<GridHandle>&`, but
|
||||
// building such a vector from a Python list requires moving from each
|
||||
// wrapper (GridHandle is move-only) — which would silently empty the
|
||||
// caller's `h1`/`h2` Python objects. Instead we inline the merge logic
|
||||
// here so we never need to move from the inputs.
|
||||
m.def("mergeGrids", [](nb::sequence handles) {
|
||||
// Collect const refs so we touch each Python wrapper exactly once.
|
||||
std::vector<const HandleT*> sources;
|
||||
sources.reserve(nb::len(handles));
|
||||
for (nb::handle item : handles) {
|
||||
sources.push_back(&nb::cast<const HandleT&>(item));
|
||||
}
|
||||
|
||||
uint64_t totalSize = 0;
|
||||
uint32_t totalGrids = 0;
|
||||
for (const HandleT* h : sources) {
|
||||
totalGrids += h->gridCount();
|
||||
for (uint32_t n = 0; n < h->gridCount(); ++n) {
|
||||
totalSize += h->gridSize(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to merge (empty sequence, or only empty handles): return an
|
||||
// empty handle. BufferT::create(0) is ill-defined — for HostBuffer it
|
||||
// yields a non-null data() over a zero-byte region, and the
|
||||
// GridHandle(buffer) ctor would then read a full GridData header out of
|
||||
// it (heap overflow / "invalid host buffer" throw).
|
||||
if (totalGrids == 0) return HandleT();
|
||||
|
||||
auto buffer = BufferT::create(totalSize);
|
||||
uint8_t* dst = static_cast<uint8_t*>(buffer.data());
|
||||
uint32_t writeIndex = 0;
|
||||
for (const HandleT* h : sources) {
|
||||
for (uint32_t n = 0; n < h->gridCount(); ++n) {
|
||||
// gridData(n) is the authoritative per-grid start pointer (it
|
||||
// applies mMetaData[n].offset), so we don't assume the source
|
||||
// grids are laid out contiguously in the buffer.
|
||||
const uint64_t gs = h->gridSize(n);
|
||||
std::memcpy(dst, h->gridData(n), gs);
|
||||
auto* gd = reinterpret_cast<nanovdb::GridData*>(dst);
|
||||
nanovdb::tools::updateGridCount(gd, writeIndex++, totalGrids);
|
||||
dst += gs;
|
||||
}
|
||||
}
|
||||
return HandleT(std::move(buffer));
|
||||
}, nb::arg("handles"),
|
||||
"Combine a list of GridHandles into a single multi-grid GridHandle. "
|
||||
"Input handles are read by const reference; the new handle owns a "
|
||||
"freshly-allocated buffer and the inputs are left untouched.");
|
||||
}
|
||||
|
||||
template<typename BufferT> nb::class_<nanovdb::GridHandle<BufferT>> defineGridHandle(nb::module_& m, const char* name)
|
||||
{
|
||||
return nb::class_<nanovdb::GridHandle<BufferT>>(m, name)
|
||||
.def(nb::init<>())
|
||||
.def("reset", &nanovdb::GridHandle<BufferT>::reset)
|
||||
.def("size", &nanovdb::GridHandle<BufferT>::bufferSize)
|
||||
.def("isEmpty", &nanovdb::GridHandle<BufferT>::isEmpty)
|
||||
.def("empty", &nanovdb::GridHandle<BufferT>::empty)
|
||||
return nb::class_<nanovdb::GridHandle<BufferT>>(m, name,
|
||||
"Owns a buffer holding one or more serialized NanoVDB grids. "
|
||||
"Construct via nanovdb.tools.create* factories or nanovdb.io.readGrid(s); "
|
||||
"access individual grids via handle.grid(n).")
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty handle. Use the nanovdb.tools.create* "
|
||||
"factories or nanovdb.io.readGrid(s) instead in normal use.")
|
||||
.def("reset", &nanovdb::GridHandle<BufferT>::reset,
|
||||
"Drop the underlying buffer; the handle becomes empty.")
|
||||
.def("size", &nanovdb::GridHandle<BufferT>::bufferSize,
|
||||
"Total byte size of the buffer backing this handle (sum of "
|
||||
"every grid plus any internal padding).")
|
||||
.def("isEmpty", &nanovdb::GridHandle<BufferT>::isEmpty,
|
||||
"True iff the handle owns no buffer.")
|
||||
.def("empty", &nanovdb::GridHandle<BufferT>::empty,
|
||||
"Same as isEmpty(). Retained for parity with the C++ "
|
||||
"GridHandle::empty() member.")
|
||||
.def(
|
||||
"__bool__", [](const nanovdb::GridHandle<BufferT>& handle) { handle.empty(); }, nb::is_operator())
|
||||
.def("floatGrid", nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<float>), nb::arg("n") = 0, nb::rv_policy::reference_internal)
|
||||
.def("doubleGrid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<double>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("int32Grid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<int32_t>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("vec3fGrid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<nanovdb::Vec3f>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("rgba8Grid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<nanovdb::math::Rgba8>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("isPadded", &nanovdb::GridHandle<BufferT>::isPadded)
|
||||
.def("gridCount", &nanovdb::GridHandle<BufferT>::gridCount)
|
||||
.def("gridSize", &nanovdb::GridHandle<BufferT>::gridSize, nb::arg("n") = 0)
|
||||
.def("gridType", &nanovdb::GridHandle<BufferT>::gridType, nb::arg("n") = 0)
|
||||
"__bool__",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle) { return !handle.empty(); },
|
||||
nb::is_operator(),
|
||||
"True iff the handle owns a non-empty buffer (`not isEmpty()`).")
|
||||
.def("copy",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle) {
|
||||
return handle.template copy<BufferT>();
|
||||
},
|
||||
"Return a deep copy of this GridHandle backed by a freshly-allocated buffer.")
|
||||
.def("grid", &pyHostGrid<BufferT>, nb::arg("n") = 0,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the n-th grid as a typed Grid subclass selected by "
|
||||
"gridType(n), or None if the BuildT is not bound in Python. "
|
||||
"The returned grid keeps this handle alive.")
|
||||
.def("isPadded", &nanovdb::GridHandle<BufferT>::isPadded,
|
||||
"True iff this handle's buffer is aligned past the natural "
|
||||
"GridData alignment (used by the I/O code path).")
|
||||
.def("gridCount", &nanovdb::GridHandle<BufferT>::gridCount,
|
||||
"Number of grids stored in this handle.")
|
||||
.def(
|
||||
"gridSize",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// GridHandle::gridSize(n) indexes mMetaData[n] unchecked, so an
|
||||
// out-of-range n (including any n on an empty handle) is UB.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridSize: grid index out of range [0, gridCount()).");
|
||||
return handle.gridSize(n);
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
"Byte size of the n-th grid (without padding).")
|
||||
.def(
|
||||
"gridType",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// GridHandle::gridType(n) indexes mMetaData[n] unchecked, so an
|
||||
// out-of-range n (including any n on an empty handle) is UB.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridType: grid index out of range [0, gridCount()).");
|
||||
return handle.gridType(n);
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
"GridType enumerator of the n-th grid (e.g. GridType.Float). "
|
||||
"Cheap to query — does not require materializing the grid.")
|
||||
.def(
|
||||
"gridData",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, uint32_t n) { return nb::bytes(handle.gridData(n), handle.gridSize(n)); },
|
||||
[](nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// gridData(n) returns nullptr for out-of-range n, but gridSize(n)
|
||||
// below indexes mMetaData[n] unchecked — guard both here.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridData: grid index out of range [0, gridCount()).");
|
||||
return nb::bytes(handle.gridData(n), handle.gridSize(n));
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("write", nb::overload_cast<const std::string&>(&nanovdb::GridHandle<BufferT>::write, nb::const_), nb::arg("fileName"))
|
||||
.def("write", nb::overload_cast<const std::string&, uint32_t>(&nanovdb::GridHandle<BufferT>::write, nb::const_), nb::arg("fileName"), nb::arg("n"))
|
||||
nb::rv_policy::reference_internal,
|
||||
"Raw byte contents of the n-th grid as a Python bytes object. "
|
||||
"Useful for hashing or for handing off to non-NanoVDB tooling.")
|
||||
.def("write", nb::overload_cast<const std::string&>(&nanovdb::GridHandle<BufferT>::write, nb::const_),
|
||||
nb::arg("fileName"),
|
||||
"Write every grid in this handle to the given .nvdb file.")
|
||||
.def("write", nb::overload_cast<const std::string&, uint32_t>(&nanovdb::GridHandle<BufferT>::write, nb::const_),
|
||||
nb::arg("fileName"), nb::arg("n"),
|
||||
"Write just the n-th grid in this handle to the given .nvdb file.")
|
||||
.def(
|
||||
"read", [](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName) { handle.read(fileName); }, nb::arg("fileName"))
|
||||
"read", [](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName) { handle.read(fileName); },
|
||||
nb::arg("fileName"),
|
||||
"Replace this handle's contents with every grid read from the "
|
||||
"given .nvdb file.")
|
||||
.def(
|
||||
"read",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName, uint32_t n) { handle.read(fileName, n); },
|
||||
nb::arg("fileName"),
|
||||
nb::arg("n"))
|
||||
nb::arg("n"),
|
||||
"Replace this handle's contents with the n-th grid read from "
|
||||
"the given .nvdb file.")
|
||||
.def(
|
||||
"read",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName, const std::string& gridName) { handle.read(fileName, gridName); },
|
||||
nb::arg("fileName"),
|
||||
nb::arg("gridName"));
|
||||
|
||||
nb::arg("gridName"),
|
||||
"Replace this handle's contents with the grid of the given "
|
||||
"name read from the .nvdb file.");
|
||||
}
|
||||
|
||||
void defineHostGridHandle(nb::module_& m);
|
||||
|
||||
@@ -2,16 +2,26 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyGridStats.h"
|
||||
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/tools/GridStats.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
void defineStatsMode(nb::module_& m)
|
||||
{
|
||||
nb::enum_<tools::StatsMode>(m, "StatsMode")
|
||||
nb::enum_<tools::StatsMode>(m, "StatsMode",
|
||||
"Selector controlling which per-node statistics are computed by "
|
||||
"tools.updateGridStats: Disable skips stats, BBox refreshes only "
|
||||
"bounding boxes, MinMax adds min/max, and All adds average and "
|
||||
"standard deviation as well.")
|
||||
.value("Disable", tools::StatsMode::Disable)
|
||||
.value("BBox", tools::StatsMode::BBox)
|
||||
.value("MinMax", tools::StatsMode::MinMax)
|
||||
@@ -20,4 +30,201 @@ void defineStatsMode(nb::module_& m)
|
||||
.value("End", tools::StatsMode::End);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ----- Extrema<ValueT> binding (rank 0 and rank 1 share the same surface) -----
|
||||
template<typename BuildT>
|
||||
static void defineExtrema(nb::module_& m, const char* name)
|
||||
{
|
||||
using ValueT = typename NanoGrid<BuildT>::ValueType;
|
||||
using ExtremaT = tools::Extrema<ValueT>;
|
||||
|
||||
nb::class_<ExtremaT>(m, name,
|
||||
"Running minimum / maximum accumulator over a stream of values. "
|
||||
"Build via repeated add(v) calls or via tools.getExtrema(grid, bbox).")
|
||||
.def(nb::init<>(),
|
||||
"Default-construct an Extrema with min = numeric_limits::max and "
|
||||
"max = numeric_limits::lowest, so any subsequent .add(v) gives "
|
||||
"exact min/max.")
|
||||
.def("min",
|
||||
[](const ExtremaT& self) -> ValueT { return self.min(); },
|
||||
"Return the minimum value observed so far.")
|
||||
.def("max",
|
||||
[](const ExtremaT& self) -> ValueT { return self.max(); },
|
||||
"Return the maximum value observed so far.")
|
||||
.def("add",
|
||||
[](ExtremaT& self, const ValueT& v) { self.add(v); },
|
||||
"value"_a,
|
||||
"Update min/max with a single sample.")
|
||||
.def("__bool__",
|
||||
[](const ExtremaT& self) { return bool(self); },
|
||||
"True iff the Extrema has accumulated at least one sample "
|
||||
"(i.e. min <= max).")
|
||||
.def_static("hasMinMax", &ExtremaT::hasMinMax,
|
||||
"True for value types where min/max is meaningful "
|
||||
"(everything except bool).")
|
||||
.def_static("hasAverage", &ExtremaT::hasAverage,
|
||||
"Always False — Extrema does not compute averages; "
|
||||
"use Stats for that.")
|
||||
.def_static("hasStdDeviation", &ExtremaT::hasStdDeviation,
|
||||
"Always False — Extrema does not compute standard "
|
||||
"deviation; use Stats for that.")
|
||||
.def_static("hasStats", &ExtremaT::hasStats,
|
||||
"True iff the value type supports the min/max bookkeeping "
|
||||
"(everything except bool).");
|
||||
}
|
||||
|
||||
// ----- Stats<ValueT> binding (inherits Extrema) -----
|
||||
template<typename BuildT>
|
||||
static void defineStats(nb::module_& m, const char* name)
|
||||
{
|
||||
using ValueT = typename NanoGrid<BuildT>::ValueType;
|
||||
using BaseT = tools::Extrema<ValueT>;
|
||||
using StatsT = tools::Stats<ValueT>;
|
||||
|
||||
nb::class_<StatsT, BaseT>(m, name,
|
||||
"Running min/max/mean/variance/std accumulator over a stream of "
|
||||
"values. Extends Extrema with sample-count-weighted moments.")
|
||||
.def(nb::init<>(),
|
||||
"Default-construct a Stats accumulator with zero samples.")
|
||||
.def("add",
|
||||
[](StatsT& self, const ValueT& v) { self.add(v); },
|
||||
"value"_a,
|
||||
"Add a single sample.")
|
||||
.def("size",
|
||||
[](const StatsT& self) -> size_t { return self.size(); },
|
||||
"Number of samples accumulated so far.")
|
||||
.def("avg",
|
||||
[](const StatsT& self) -> double { return self.avg(); },
|
||||
"Arithmetic mean of all samples.")
|
||||
.def("mean",
|
||||
[](const StatsT& self) -> double { return self.mean(); },
|
||||
"Alias for avg().")
|
||||
.def("var",
|
||||
[](const StatsT& self) -> double { return self.var(); },
|
||||
"Population variance (Sum(x-mean)^2 / N). Returns 0 if "
|
||||
"fewer than two samples have been added.")
|
||||
.def("variance",
|
||||
[](const StatsT& self) -> double { return self.variance(); },
|
||||
"Alias for var().")
|
||||
.def("std",
|
||||
[](const StatsT& self) -> double { return self.std(); },
|
||||
"Standard deviation = sqrt(var()).")
|
||||
.def("stdDev",
|
||||
[](const StatsT& self) -> double { return self.stdDev(); },
|
||||
"Alias for std().")
|
||||
.def_static("hasMinMax", &StatsT::hasMinMax,
|
||||
"True for value types where min/max is meaningful.")
|
||||
.def_static("hasAverage", &StatsT::hasAverage,
|
||||
"True for value types that support mean/variance.")
|
||||
.def_static("hasStdDeviation", &StatsT::hasStdDeviation,
|
||||
"True for value types that support standard deviation.")
|
||||
.def_static("hasStats", &StatsT::hasStats,
|
||||
"True for value types where full statistics is meaningful.");
|
||||
}
|
||||
|
||||
// ----- updateGridStats polymorphic dispatch ----------------------------------
|
||||
//
|
||||
// Match the IsNanoGridValid/callNanoGrid pattern: an Op struct with `known<T>`
|
||||
// (called for every BuildT that's in scope) and `unknown` (fallback). Only
|
||||
// scalar + vector BuildTs have a meaningful Stats specialization, so the
|
||||
// other arms raise.
|
||||
struct UpdateGridStatsOp
|
||||
{
|
||||
template<typename BuildT>
|
||||
static void known(GridData* gridData, tools::StatsMode mode)
|
||||
{
|
||||
using GridT = NanoGrid<BuildT>;
|
||||
using ValueT = typename GridT::ValueType;
|
||||
if constexpr (BuildTraits<BuildT>::is_special &&
|
||||
!util::is_same<bool, BuildT>::value) {
|
||||
// Special / quantized / index / mask BuildTs don't have an
|
||||
// arithmetic ValueT, so tools::updateGridStats's MinMax / All
|
||||
// branches would instantiate Stats<ValueT> / Extrema<ValueT>
|
||||
// with no meaningful semantics (and may not even compile).
|
||||
// The Disable and BBox branches use NoopStats, which works
|
||||
// for any ValueT — drive that directly here so the BBox path
|
||||
// remains available on special grids (it just recomputes
|
||||
// node bounding boxes without touching min/max/avg).
|
||||
if (mode == tools::StatsMode::Disable) {
|
||||
return;
|
||||
} else if (mode == tools::StatsMode::BBox) {
|
||||
tools::GridStats<GridT, tools::NoopStats<ValueT>> stats;
|
||||
stats.update(*static_cast<GridT*>(gridData));
|
||||
} else {
|
||||
throw nb::value_error(
|
||||
"updateGridStats: this grid's BuildT (special / "
|
||||
"quantized / index / mask) has no arithmetic value "
|
||||
"type — only StatsMode.Disable and StatsMode.BBox "
|
||||
"are supported.");
|
||||
}
|
||||
} else {
|
||||
tools::updateGridStats(static_cast<GridT*>(gridData), mode);
|
||||
}
|
||||
}
|
||||
static void unknown(GridData*, tools::StatsMode) {
|
||||
throw nb::value_error(
|
||||
"updateGridStats: unsupported GridType / BuildT combination.");
|
||||
}
|
||||
};
|
||||
|
||||
// ----- getExtrema (per-BuildT factory) ---------------------------------------
|
||||
template<typename BuildT>
|
||||
static tools::Extrema<typename NanoGrid<BuildT>::ValueType>
|
||||
pyGetExtrema(const NanoGrid<BuildT>& grid, const CoordBBox& bbox)
|
||||
{
|
||||
return tools::getExtrema<BuildT>(grid, bbox);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineGridStatsModule(nb::module_& toolsModule)
|
||||
{
|
||||
// Per-BuildT Extrema + Stats. One pair per scalar/vector BuildT — the
|
||||
// value types are all distinct so we get N pairs of new Python classes.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
defineExtrema<T>(toolsModule, #Suffix "Extrema"); \
|
||||
defineStats<T>(toolsModule, #Suffix "Stats");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
defineExtrema<T>(toolsModule, #Suffix "Extrema"); \
|
||||
defineStats<T>(toolsModule, #Suffix "Stats");
|
||||
#include "BuildTypes.def"
|
||||
|
||||
// Polymorphic updateGridStats. Accepts any bound NanoGrid<BuildT> (via
|
||||
// upcast to GridData*) and dispatches on its mGridType.
|
||||
toolsModule.def("updateGridStats",
|
||||
[](GridData* gridData, tools::StatsMode mode) {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("updateGridStats: grid is None.");
|
||||
}
|
||||
callNanoGrid<UpdateGridStatsOp>(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = tools::StatsMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Recompute and write per-node statistics into the given grid in "
|
||||
"place. Polymorphic over BuildT. Scalar, vector, and Boolean "
|
||||
"grids accept every StatsMode (Disable / BBox / MinMax / All); "
|
||||
"Boolean grids use the C++ NoopStats path internally regardless "
|
||||
"of mode because there's no arithmetic min/max/avg/dev on bool. "
|
||||
"Other special (quantized / index / mask) grids accept Disable "
|
||||
"and BBox (the latter recomputes node bounding boxes only); "
|
||||
"MinMax and All raise ValueError because their value type has "
|
||||
"no arithmetic semantics.");
|
||||
|
||||
// Per-BuildT getExtrema. We expose one overload per scalar/vector
|
||||
// BuildT — they each return a Python-side Extrema<ValueT> of the
|
||||
// matching name.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
toolsModule.def("getExtrema", &pyGetExtrema<T>, \
|
||||
"grid"_a, "bbox"_a, nb::call_guard<nb::gil_scoped_release>(), \
|
||||
"Return the Extrema of all values in the grid that intersect " \
|
||||
"the given bbox.");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
toolsModule.def("getExtrema", &pyGetExtrema<T>, \
|
||||
"grid"_a, "bbox"_a, nb::call_guard<nb::gil_scoped_release>(), \
|
||||
"Return the Extrema of all values in the grid that intersect " \
|
||||
"the given bbox.");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -11,6 +11,12 @@ namespace pynanovdb {
|
||||
|
||||
void defineStatsMode(nb::module_& m);
|
||||
|
||||
/// @brief Register per-BuildT Extrema and Stats classes (one set per
|
||||
/// scalar / vector BuildT in BuildTypes.def) and the polymorphic
|
||||
/// tools.updateGridStats / tools.getExtrema helpers under the
|
||||
/// nanovdb.tools submodule.
|
||||
void defineGridStatsModule(nb::module_& toolsModule);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyGridValidator.h"
|
||||
|
||||
#include <nanobind/operators.h>
|
||||
#include <nanobind/stl/pair.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/GridValidator.h>
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#endif
|
||||
|
||||
#include <nanobind/operators.h>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -18,7 +26,8 @@ namespace pynanovdb {
|
||||
|
||||
template<typename BufferT> void defineValidateGrids(nb::module_& m)
|
||||
{
|
||||
m.def("validateGrids", &tools::validateGrids<GridHandle<BufferT>>, "handle"_a, "mode"_a, "verbose"_a);
|
||||
m.def("validateGrids", &tools::validateGrids<GridHandle<BufferT>>,
|
||||
"handle"_a, "mode"_a, "verbose"_a);
|
||||
}
|
||||
|
||||
template void defineValidateGrids<HostBuffer>(nb::module_&);
|
||||
@@ -26,4 +35,126 @@ template void defineValidateGrids<HostBuffer>(nb::module_&);
|
||||
template void defineValidateGrids<cuda::DeviceBuffer>(nb::module_&);
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// callNanoGrid op for checkGrid: writes into a fixed-size buffer and returns
|
||||
// it as a std::pair<bool, std::string> for nb::make_tuple consumption.
|
||||
//
|
||||
// tools::checkGrid writes error messages with util::sprint / util::strcpy,
|
||||
// neither of which is bounded — they trust the caller's buffer is big
|
||||
// enough. Size the buffer well above what any current error message
|
||||
// produces (the longest formatted message in GridValidator.h is on the
|
||||
// order of ~80 characters once both GridType and GridClass enumerator
|
||||
// names are stringified) and leave generous headroom for future
|
||||
// additions.
|
||||
struct CheckGridOp
|
||||
{
|
||||
static constexpr size_t kErrorBufSize = 4096;
|
||||
|
||||
template<typename BuildT>
|
||||
static std::pair<bool, std::string> known(const GridData* gridData,
|
||||
CheckMode mode)
|
||||
{
|
||||
char buf[kErrorBufSize];
|
||||
buf[0] = '\0';
|
||||
tools::checkGrid<BuildT>(
|
||||
static_cast<const NanoGrid<BuildT>*>(gridData), buf, mode);
|
||||
const bool ok = (buf[0] == '\0');
|
||||
return {ok, std::string(buf)};
|
||||
}
|
||||
static std::pair<bool, std::string> unknown(const GridData* gridData,
|
||||
CheckMode /*mode*/)
|
||||
{
|
||||
(void)gridData;
|
||||
return {false, "Unsupported GridType for checkGrid"};
|
||||
}
|
||||
};
|
||||
|
||||
// callNanoGrid op for isValid — wraps tools::isValid<BuildT> for every
|
||||
// switched-over BuildT.
|
||||
struct IsValidOp
|
||||
{
|
||||
template<typename BuildT>
|
||||
static bool known(const GridData* gridData, CheckMode mode, bool verbose)
|
||||
{
|
||||
return tools::isValid<BuildT>(
|
||||
static_cast<const NanoGrid<BuildT>*>(gridData), mode, verbose);
|
||||
}
|
||||
static bool unknown(const GridData* gridData, CheckMode /*mode*/, bool verbose)
|
||||
{
|
||||
if (verbose && gridData != nullptr) {
|
||||
char str[16];
|
||||
std::cerr << "Validation failed: Unsupported GridType: \""
|
||||
<< toStr(str, gridData->mGridType) << "\""
|
||||
<< std::endl;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineGridValidatorModule(nb::module_& toolsModule)
|
||||
{
|
||||
// Single-grid validate. Takes a handle, a grid index, and the
|
||||
// usual mode + verbose flags. Returns true iff the grid passes all
|
||||
// tests for the given mode. Bound for both host and device handles
|
||||
// (when CUDA is enabled), matching validateGrids' coverage —
|
||||
// tools::validateGrid does host-side dispatch via callNanoGrid on
|
||||
// the host-resident gridData() pointer that DeviceGridHandle also
|
||||
// exposes, so the same overload pair is appropriate.
|
||||
toolsModule.def("validateGrid",
|
||||
&tools::validateGrid<GridHandle<HostBuffer>>,
|
||||
"handle"_a, "gridID"_a,
|
||||
"mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Validate the gridID'th grid in the handle against the given "
|
||||
"CheckMode. Returns False (without raising) if gridID is out "
|
||||
"of range or the grid fails any check. CheckMode.Disable is a "
|
||||
"short-circuit that always returns True without inspecting "
|
||||
"the grid (even when gridID is out of range), matching the "
|
||||
"C++ behavior. Complements validateGrids() which checks the "
|
||||
"whole handle.");
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
toolsModule.def("validateGrid",
|
||||
&tools::validateGrid<GridHandle<cuda::DeviceBuffer>>,
|
||||
"handle"_a, "gridID"_a,
|
||||
"mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Validate the gridID'th grid in the device handle (uses the "
|
||||
"host-resident copy of the grid metadata for the actual "
|
||||
"checks). Same semantics as the host-handle overload.");
|
||||
#endif
|
||||
|
||||
// Polymorphic checkGrid — returns (ok, error_message). Mirrors the C++
|
||||
// char-buffer-out signature, but the buffer is hidden inside the
|
||||
// binding so Python callers get a Python str.
|
||||
toolsModule.def("checkGrid",
|
||||
[](const GridData* gridData, CheckMode mode)
|
||||
-> std::pair<bool, std::string> {
|
||||
if (gridData == nullptr) {
|
||||
return {false, "Grid is None"};
|
||||
}
|
||||
return callNanoGrid<CheckGridOp>(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Full,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Run structural validation checks on the grid for the given "
|
||||
"CheckMode. Returns a (ok, error_message) tuple — error_message "
|
||||
"is empty when ok is True.");
|
||||
|
||||
// Polymorphic isValid — convenience wrapper. Same as checkGrid + a
|
||||
// checksum check, returning just the bool.
|
||||
toolsModule.def("isValid",
|
||||
[](const GridData* gridData, CheckMode mode, bool verbose) {
|
||||
if (gridData == nullptr) return false;
|
||||
return callNanoGrid<IsValidOp>(gridData, mode, verbose);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Return True iff the grid passes structural validation AND its "
|
||||
"stored checksum matches a freshly computed one for the given "
|
||||
"CheckMode.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -11,6 +11,12 @@ namespace pynanovdb {
|
||||
|
||||
template<typename BufferT> void defineValidateGrids(nb::module_& m);
|
||||
|
||||
/// @brief Register tools.validateGrid (single grid in a handle),
|
||||
/// tools.checkGrid (polymorphic, returns (bool, error_str)) and
|
||||
/// tools.isValid (polymorphic shortcut) under the nanovdb.tools
|
||||
/// submodule. Bound once; polymorphic dispatch uses callNanoGrid.
|
||||
void defineGridValidatorModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,7 +11,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineHostBuffer(nb::module_& m)
|
||||
{
|
||||
nb::class_<HostBuffer>(m, "HostBuffer");
|
||||
nb::class_<HostBuffer>(m, "HostBuffer",
|
||||
"Default host-side buffer used to back a GridHandle. Memory is "
|
||||
"owned by this buffer and freed when the handle (and therefore "
|
||||
"the buffer) is destroyed.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -21,40 +21,67 @@ namespace {
|
||||
|
||||
void defineFileGridMetaData(nb::module_& m)
|
||||
{
|
||||
nb::class_<io::FileMetaData>(m, "FileMetaData")
|
||||
.def_ro("gridSize", &io::FileMetaData::gridSize)
|
||||
.def_ro("fileSize", &io::FileMetaData::fileSize)
|
||||
.def_ro("nameKey", &io::FileMetaData::nameKey)
|
||||
.def_ro("voxelCount", &io::FileMetaData::voxelCount)
|
||||
.def_ro("gridType", &io::FileMetaData::gridType)
|
||||
.def_ro("gridClass", &io::FileMetaData::gridClass)
|
||||
.def_ro("indexBBox", &io::FileMetaData::indexBBox)
|
||||
.def_ro("worldBBox", &io::FileMetaData::worldBBox)
|
||||
.def_ro("voxelSize", &io::FileMetaData::voxelSize)
|
||||
.def_ro("nameSize", &io::FileMetaData::nameSize)
|
||||
nb::class_<io::FileMetaData>(m, "FileMetaData",
|
||||
"Per-grid header read from the .nvdb file index. Mirrors the C++ "
|
||||
"io::FileMetaData layout; subclassed by FileGridMetaData which adds "
|
||||
"the grid name string.")
|
||||
.def_ro("gridSize", &io::FileMetaData::gridSize,
|
||||
"Uncompressed grid size in bytes.")
|
||||
.def_ro("fileSize", &io::FileMetaData::fileSize,
|
||||
"On-disk byte size of this grid (post-codec).")
|
||||
.def_ro("nameKey", &io::FileMetaData::nameKey,
|
||||
"Hash of the grid name used as a fast lookup key.")
|
||||
.def_ro("voxelCount", &io::FileMetaData::voxelCount,
|
||||
"Number of active voxels in this grid.")
|
||||
.def_ro("gridType", &io::FileMetaData::gridType,
|
||||
"GridType enumerator naming the BuildT of this grid.")
|
||||
.def_ro("gridClass", &io::FileMetaData::gridClass,
|
||||
"GridClass enumerator (LevelSet, FogVolume, ...).")
|
||||
.def_ro("indexBBox", &io::FileMetaData::indexBBox,
|
||||
"Axis-aligned bounding box of active voxels in index space.")
|
||||
.def_ro("worldBBox", &io::FileMetaData::worldBBox,
|
||||
"Axis-aligned bounding box of active voxels in world space.")
|
||||
.def_ro("voxelSize", &io::FileMetaData::voxelSize,
|
||||
"World-space size of a single voxel.")
|
||||
.def_ro("nameSize", &io::FileMetaData::nameSize,
|
||||
"Length of the grid name string including the null terminator.")
|
||||
.def_prop_ro("nodeCount",
|
||||
[](io::FileMetaData& metaData) {
|
||||
return std::make_tuple(metaData.nodeCount[0], metaData.nodeCount[1], metaData.nodeCount[2], metaData.nodeCount[3]);
|
||||
})
|
||||
},
|
||||
"Tuple (leaf, lower, upper, root) of node counts in this grid.")
|
||||
.def_prop_ro("tileCount",
|
||||
[](io::FileMetaData& metaData) { return std::make_tuple(metaData.tileCount[0], metaData.tileCount[1], metaData.tileCount[2]); })
|
||||
.def_ro("codec", &io::FileMetaData::codec)
|
||||
.def_ro("blindDataCount", &io::FileMetaData::blindDataCount)
|
||||
.def_ro("version", &io::FileMetaData::version);
|
||||
[](io::FileMetaData& metaData) { return std::make_tuple(metaData.tileCount[0], metaData.tileCount[1], metaData.tileCount[2]); },
|
||||
"Tuple (lower-tile, upper-tile, root-tile) of active-tile counts.")
|
||||
.def_ro("codec", &io::FileMetaData::codec,
|
||||
"Codec used to compress this grid on disk.")
|
||||
.def_ro("blindDataCount", &io::FileMetaData::blindDataCount,
|
||||
"Number of blind-data channels attached to this grid.")
|
||||
.def_ro("version", &io::FileMetaData::version,
|
||||
"NanoVDB version stored in the file when this grid was written.");
|
||||
|
||||
nb::bind_vector<std::vector<io::FileMetaData>>(m, "FileMetaDataVector");
|
||||
nb::bind_vector<std::vector<io::FileMetaData>>(m, "FileMetaDataVector",
|
||||
"List of FileMetaData entries, one per grid in a .nvdb file.");
|
||||
|
||||
nb::class_<io::FileGridMetaData, io::FileMetaData>(m, "FileGridMetaData")
|
||||
.def_ro("gridName", &io::FileGridMetaData::gridName)
|
||||
.def("memUsage", &io::FileGridMetaData::memUsage);
|
||||
nb::class_<io::FileGridMetaData, io::FileMetaData>(m, "FileGridMetaData",
|
||||
"FileMetaData extended with the grid name. Returned by "
|
||||
"readGridMetaData() so callers can identify grids by name without "
|
||||
"materializing them.")
|
||||
.def_ro("gridName", &io::FileGridMetaData::gridName,
|
||||
"Grid name as a Python string.")
|
||||
.def("memUsage", &io::FileGridMetaData::memUsage,
|
||||
"Byte size of this metadata record in memory.");
|
||||
|
||||
nb::bind_vector<std::vector<io::FileGridMetaData>>(m, "FileGridMetaDataVector");
|
||||
nb::bind_vector<std::vector<io::FileGridMetaData>>(m, "FileGridMetaDataVector",
|
||||
"List of FileGridMetaData entries, one per grid in a .nvdb file.");
|
||||
}
|
||||
|
||||
template<typename BufferT> void defineReadWriteGrid(nb::module_& m)
|
||||
{
|
||||
m.def("hasGrid", nb::overload_cast<const std::string&, const std::string&>(&io::hasGrid), "fileName"_a, "gridName"_a);
|
||||
m.def("readGridMetaData", nb::overload_cast<const std::string&>(&io::readGridMetaData), "fileName"_a);
|
||||
m.def("hasGrid", nb::overload_cast<const std::string&, const std::string&>(&io::hasGrid), "fileName"_a, "gridName"_a,
|
||||
"Return True iff the .nvdb file at fileName contains a grid named gridName.");
|
||||
m.def("readGridMetaData", nb::overload_cast<const std::string&>(&io::readGridMetaData), "fileName"_a,
|
||||
"Return a FileGridMetaDataVector describing every grid stored in the .nvdb file.");
|
||||
}
|
||||
|
||||
template<typename BufferT> nb::list readGrids(const std::string& fileName, int verbose, const BufferT& buffer)
|
||||
@@ -91,21 +118,26 @@ void defineHostReadWriteGrid(nb::module_& m)
|
||||
"fileName"_a,
|
||||
"handle"_a,
|
||||
"codec"_a = io::Codec::NONE,
|
||||
"verbose"_a = 0);
|
||||
m.def("writeGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0);
|
||||
"verbose"_a = 0,
|
||||
"Write the grids in handle to the .nvdb file at fileName using the given codec.");
|
||||
m.def("writeGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0,
|
||||
"Write every GridHandle in the handles list to the .nvdb file at fileName.");
|
||||
m.def("readGrid",
|
||||
nb::overload_cast<const std::string&, int, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"n"_a = 0,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the n-th grid from the .nvdb file at fileName into a fresh GridHandle.");
|
||||
m.def("readGrid",
|
||||
nb::overload_cast<const std::string&, const std::string&, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"gridName"_a,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
m.def("readGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the grid named gridName from the .nvdb file at fileName into a fresh GridHandle.");
|
||||
m.def("readGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(),
|
||||
"Read every grid from the .nvdb file at fileName, returning a list of GridHandles.");
|
||||
}
|
||||
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
@@ -119,21 +151,26 @@ void defineDeviceReadWriteGrid(nb::module_& m)
|
||||
"fileName"_a,
|
||||
"handle"_a,
|
||||
"codec"_a = io::Codec::NONE,
|
||||
"verbose"_a = 0);
|
||||
m.def("deviceWriteGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0);
|
||||
"verbose"_a = 0,
|
||||
"Write the grids in a device-backed handle to the .nvdb file at fileName.");
|
||||
m.def("deviceWriteGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0,
|
||||
"Write every device-backed GridHandle in handles to the .nvdb file at fileName.");
|
||||
m.def("deviceReadGrid",
|
||||
nb::overload_cast<const std::string&, int, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"n"_a = 0,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the n-th grid from the .nvdb file at fileName into a fresh DeviceGridHandle.");
|
||||
m.def("deviceReadGrid",
|
||||
nb::overload_cast<const std::string&, const std::string&, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"gridName"_a,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
m.def("deviceReadGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the grid named gridName from the .nvdb file at fileName into a fresh DeviceGridHandle.");
|
||||
m.def("deviceReadGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(),
|
||||
"Read every grid from the .nvdb file at fileName into device-backed handles.");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -141,16 +178,19 @@ void defineDeviceReadWriteGrid(nb::module_& m)
|
||||
|
||||
void defineIOModule(nb::module_& m)
|
||||
{
|
||||
nb::enum_<io::Codec>(m, "Codec")
|
||||
nb::enum_<io::Codec>(m, "Codec",
|
||||
"Compression codec selector used when writing a .nvdb file. NONE "
|
||||
"writes raw bytes; ZIP uses zlib; BLOSC uses the blosc codec when "
|
||||
"compiled in.")
|
||||
.value("NONE", io::Codec::NONE)
|
||||
.value("ZIP", io::Codec::ZIP)
|
||||
.value("BLOSC", io::Codec::BLOSC)
|
||||
.export_values();
|
||||
// .def("__repr__", [](const io::Codec& codec) {
|
||||
// char str[strlen<io::Codec>()];
|
||||
// toStr(str, codec);
|
||||
// return std::string(str);
|
||||
// });
|
||||
.export_values()
|
||||
.def("__repr__", [](const io::Codec& codec) {
|
||||
char str[strlen<io::Codec>()];
|
||||
toStr(str, codec);
|
||||
return std::string(str);
|
||||
});
|
||||
|
||||
defineFileGridMetaData(m);
|
||||
defineHostReadWriteGrid(m);
|
||||
|
||||
+325
-187
@@ -11,7 +11,6 @@
|
||||
#include <nanobind/stl/bind_vector.h>
|
||||
|
||||
#include "PySampleFromVoxels.h"
|
||||
#include "cuda/PySampleFromVoxels.h"
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -26,18 +25,27 @@ void defineCoord(nb::module_& m)
|
||||
using ValueType = math::Coord::ValueType;
|
||||
|
||||
nb::class_<math::Coord>(m, "Coord", "Signed (i, j, k) 32-bit integer coordinate class, similar to openvdb::math::Coord")
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<ValueType>(), "n"_a)
|
||||
.def(nb::init<ValueType, ValueType, ValueType>(), "i"_a, "j"_a, "k"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct (0, 0, 0).")
|
||||
.def(nb::init<ValueType>(), "n"_a,
|
||||
"Construct (n, n, n).")
|
||||
.def(nb::init<ValueType, ValueType, ValueType>(), "i"_a, "j"_a, "k"_a,
|
||||
"Construct (i, j, k).")
|
||||
.def_prop_rw(
|
||||
"x", [](const math::Coord& ijk) { return ijk.x(); }, [](math::Coord& ijk, int32_t i) { ijk.x() = i; })
|
||||
"x", [](const math::Coord& ijk) { return ijk.x(); }, [](math::Coord& ijk, int32_t i) { ijk.x() = i; },
|
||||
"First component of the (i, j, k) triple.")
|
||||
.def_prop_rw(
|
||||
"y", [](const math::Coord& ijk) { return ijk.y(); }, [](math::Coord& ijk, int32_t j) { ijk.y() = j; })
|
||||
"y", [](const math::Coord& ijk) { return ijk.y(); }, [](math::Coord& ijk, int32_t j) { ijk.y() = j; },
|
||||
"Second component of the (i, j, k) triple.")
|
||||
.def_prop_rw(
|
||||
"z", [](const math::Coord& ijk) { return ijk.z(); }, [](math::Coord& ijk, int32_t k) { ijk.z() = k; })
|
||||
.def_static("max", &math::Coord::max)
|
||||
.def_static("min", &math::Coord::min)
|
||||
.def_static("memUsage", &math::Coord::memUsage)
|
||||
"z", [](const math::Coord& ijk) { return ijk.z(); }, [](math::Coord& ijk, int32_t k) { ijk.z() = k; },
|
||||
"Third component of the (i, j, k) triple.")
|
||||
.def_static("max", &math::Coord::max,
|
||||
"Largest representable Coord (INT32_MAX in every component).")
|
||||
.def_static("min", &math::Coord::min,
|
||||
"Smallest representable Coord (INT32_MIN in every component).")
|
||||
.def_static("memUsage", &math::Coord::memUsage,
|
||||
"Byte size of a Coord instance.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Coord& ijk, size_t i) {
|
||||
@@ -46,7 +54,8 @@ void defineCoord(nb::module_& m)
|
||||
}
|
||||
return ijk[static_cast<math::Coord::IndexType>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Coord& ijk, size_t i, ValueType value) {
|
||||
@@ -56,41 +65,69 @@ void defineCoord(nb::module_& m)
|
||||
ijk[static_cast<math::Coord::IndexType>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__and__", [](const math::Coord& a, math::Coord::IndexType b) { return a & b; }, nb::is_operator(), "n"_a)
|
||||
"__and__", [](const math::Coord& a, math::Coord::IndexType b) { return a & b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise bitwise AND with the scalar n.")
|
||||
.def(
|
||||
"__lshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a << b; }, nb::is_operator(), "n"_a)
|
||||
"__lshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a << b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise left shift by n bits.")
|
||||
.def(
|
||||
"__rshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a >> b; }, nb::is_operator(), "n"_a)
|
||||
.def(nb::self < nb::self, "rhs"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
"__rshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a >> b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise right shift by n bits.")
|
||||
.def(nb::self < nb::self, "rhs"_a,
|
||||
"Lexicographic less-than comparison.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of all three components.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of any one component.")
|
||||
.def(
|
||||
"__iand__", [](math::Coord& a, int b) { return a &= b; }, nb::is_operator(), "n"_a)
|
||||
"__iand__", [](math::Coord& a, int b) { return a &= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise bitwise AND with the scalar n.")
|
||||
.def(
|
||||
"__ilshift__", [](math::Coord& a, uint32_t b) { return a <<= b; }, nb::is_operator(), "n"_a)
|
||||
"__ilshift__", [](math::Coord& a, uint32_t b) { return a <<= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise left shift by n bits.")
|
||||
.def(
|
||||
"__irshift__", [](math::Coord& a, uint32_t b) { return a >>= b; }, nb::is_operator(), "n"_a)
|
||||
"__irshift__", [](math::Coord& a, uint32_t b) { return a >>= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise right shift by n bits.")
|
||||
.def(
|
||||
"__iadd__", [](math::Coord& a, int b) { return a += b; }, nb::is_operator(), "n"_a)
|
||||
.def(nb::self + nb::self, "rhs"_a)
|
||||
.def(nb::self - nb::self, "rhs"_a)
|
||||
.def(-nb::self)
|
||||
.def(nb::self += nb::self, "rhs"_a)
|
||||
.def(nb::self -= nb::self, "rhs"_a)
|
||||
.def("minComponent", &math::Coord::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Coord::maxComponent, "other"_a)
|
||||
.def("offsetBy", nb::overload_cast<ValueType, ValueType, ValueType>(&math::Coord::offsetBy, nb::const_), "dx"_a, "dy"_a, "dz"_a)
|
||||
.def("offsetBy", nb::overload_cast<ValueType>(&math::Coord::offsetBy, nb::const_), "n"_a)
|
||||
.def_static("lessThan", &math::Coord::lessThan, "a"_a, "b"_a)
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<float>>, "xyz"_a)
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<double>>, "xyz"_a)
|
||||
.def("hash", &math::Coord::template hash<12>)
|
||||
.def("octant", &math::Coord::octant)
|
||||
.def("asVec3s", &math::Coord::asVec3s)
|
||||
.def("asVec3d", &math::Coord::asVec3d)
|
||||
.def("round", &math::Coord::round)
|
||||
"__iadd__", [](math::Coord& a, int b) { return a += b; }, nb::is_operator(), "n"_a,
|
||||
"In-place add the scalar n to every component.")
|
||||
.def(nb::self + nb::self, "rhs"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "rhs"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self += nb::self, "rhs"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self -= nb::self, "rhs"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def("minComponent", &math::Coord::minComponent, "other"_a,
|
||||
"Component-wise minimum with other. See nanovdb::math::Coord::minComponent in NanoVDB.h.")
|
||||
.def("maxComponent", &math::Coord::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other. See nanovdb::math::Coord::maxComponent in NanoVDB.h.")
|
||||
.def("offsetBy", nb::overload_cast<ValueType, ValueType, ValueType>(&math::Coord::offsetBy, nb::const_), "dx"_a, "dy"_a, "dz"_a,
|
||||
"Return a new Coord offset by (dx, dy, dz). See nanovdb::math::Coord::offsetBy in NanoVDB.h.")
|
||||
.def("offsetBy", nb::overload_cast<ValueType>(&math::Coord::offsetBy, nb::const_), "n"_a,
|
||||
"Return a new Coord offset by n in every component.")
|
||||
.def_static("lessThan", &math::Coord::lessThan, "a"_a, "b"_a,
|
||||
"Component-wise a < b returning a Coord of 0 / 1 flags.")
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<float>>, "xyz"_a,
|
||||
"Floor each component of a Vec3f to produce an integer Coord.")
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<double>>, "xyz"_a,
|
||||
"Floor each component of a Vec3d to produce an integer Coord.")
|
||||
.def("hash", &math::Coord::template hash<12>,
|
||||
"Spatial hash of this coordinate suited for hashed root-table lookups.")
|
||||
.def("octant", &math::Coord::octant,
|
||||
"Return the 0..7 octant index of this coordinate's sign bits.")
|
||||
.def("asVec3s", &math::Coord::asVec3s,
|
||||
"Convert to a Vec3f (float) with no scaling.")
|
||||
.def("asVec3d", &math::Coord::asVec3d,
|
||||
"Convert to a Vec3d (double) with no scaling.")
|
||||
.def("round", &math::Coord::round,
|
||||
"Component-wise round; for an integer Coord this is the identity.")
|
||||
.def("__repr__", [](const math::Coord& ijk) {
|
||||
std::stringstream ostr;
|
||||
ostr << ijk;
|
||||
@@ -101,13 +138,20 @@ void defineCoord(nb::module_& m)
|
||||
template<typename T> void defineVec3(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::Vec3<T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<T>(), "x"_a)
|
||||
.def(nb::init<T, T, T>(), "x"_a, "y"_a, "z"_a)
|
||||
.def(nb::init<math::Vec3<T>>(), "v"_a)
|
||||
.def(nb::init<math::Coord>(), "ijk"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a zero-initialized vector.")
|
||||
.def(nb::init<T>(), "x"_a,
|
||||
"Construct (x, x, x).")
|
||||
.def(nb::init<T, T, T>(), "x"_a, "y"_a, "z"_a,
|
||||
"Construct (x, y, z).")
|
||||
.def(nb::init<math::Vec3<T>>(), "v"_a,
|
||||
"Copy-construct from another Vec3.")
|
||||
.def(nb::init<math::Coord>(), "ijk"_a,
|
||||
"Construct from an integer Coord, casting each component.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Component-wise equality.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Component-wise inequality.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Vec3<T>& v, size_t i) {
|
||||
@@ -116,7 +160,8 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
}
|
||||
return v[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Vec3<T>& v, size_t i, T value) {
|
||||
@@ -126,38 +171,68 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
v[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
.def("dot", &math::Vec3<T>::template dot<math::Vec3<T>>, "v"_a)
|
||||
.def("cross", &math::Vec3<T>::template cross<math::Vec3<T>>, "v"_a)
|
||||
.def("lengthSqr", &math::Vec3<T>::lengthSqr)
|
||||
.def("length", &math::Vec3<T>::length)
|
||||
.def(-nb::self)
|
||||
.def(nb::self * nb::self, "v"_a)
|
||||
.def(nb::self / nb::self, "v"_a)
|
||||
.def(nb::self + nb::self, "v"_a)
|
||||
.def(nb::self - nb::self, "v"_a)
|
||||
.def(nb::self + math::Coord(), "ijk"_a)
|
||||
.def(nb::self - math::Coord(), "ijk"_a)
|
||||
.def(nb::self * T(), "s"_a)
|
||||
.def(nb::self / T(), "s"_a)
|
||||
.def(nb::self += nb::self, "v"_a)
|
||||
.def(nb::self += math::Coord(), "ijk"_a)
|
||||
.def(nb::self -= nb::self, "v"_a)
|
||||
.def(nb::self -= math::Coord(), "ijk"_a)
|
||||
.def(nb::self *= T(), "s"_a)
|
||||
.def(nb::self /= T(), "s"_a)
|
||||
.def("normalize", &math::Vec3<T>::normalize)
|
||||
.def("minComponent", &math::Vec3<T>::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Vec3<T>::maxComponent, "other"_a)
|
||||
.def("min", &math::Vec3<T>::min)
|
||||
.def("max", &math::Vec3<T>::max)
|
||||
.def("floor", &math::Vec3<T>::floor)
|
||||
.def("ceil", &math::Vec3<T>::ceil)
|
||||
.def("round", &math::Vec3<T>::round)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z).")
|
||||
.def("dot", &math::Vec3<T>::template dot<math::Vec3<T>>, "v"_a,
|
||||
"Dot product with another vector.")
|
||||
.def("cross", &math::Vec3<T>::template cross<math::Vec3<T>>, "v"_a,
|
||||
"Cross product with another vector.")
|
||||
.def("lengthSqr", &math::Vec3<T>::lengthSqr,
|
||||
"Squared Euclidean length (cheaper than length()).")
|
||||
.def("length", &math::Vec3<T>::length,
|
||||
"Euclidean length of this vector.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self * nb::self, "v"_a,
|
||||
"Component-wise multiplication.")
|
||||
.def(nb::self / nb::self, "v"_a,
|
||||
"Component-wise division.")
|
||||
.def(nb::self + nb::self, "v"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "v"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(nb::self + math::Coord(), "ijk"_a,
|
||||
"Add an integer Coord component-wise.")
|
||||
.def(nb::self - math::Coord(), "ijk"_a,
|
||||
"Subtract an integer Coord component-wise.")
|
||||
.def(nb::self * T(), "s"_a,
|
||||
"Multiply every component by the scalar s.")
|
||||
.def(nb::self / T(), "s"_a,
|
||||
"Divide every component by the scalar s.")
|
||||
.def(nb::self += nb::self, "v"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self += math::Coord(), "ijk"_a,
|
||||
"In-place add an integer Coord.")
|
||||
.def(nb::self -= nb::self, "v"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def(nb::self -= math::Coord(), "ijk"_a,
|
||||
"In-place subtract an integer Coord.")
|
||||
.def(nb::self *= T(), "s"_a,
|
||||
"In-place scalar multiply.")
|
||||
.def(nb::self /= T(), "s"_a,
|
||||
"In-place scalar divide.")
|
||||
.def("normalize", &math::Vec3<T>::normalize,
|
||||
"Scale this vector to unit length in place.")
|
||||
.def("minComponent", &math::Vec3<T>::minComponent, "other"_a,
|
||||
"Component-wise minimum with other.")
|
||||
.def("maxComponent", &math::Vec3<T>::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other.")
|
||||
.def("min", &math::Vec3<T>::min,
|
||||
"Smallest single component of this vector.")
|
||||
.def("max", &math::Vec3<T>::max,
|
||||
"Largest single component of this vector.")
|
||||
.def("floor", &math::Vec3<T>::floor,
|
||||
"Component-wise floor.")
|
||||
.def("ceil", &math::Vec3<T>::ceil,
|
||||
"Component-wise ceiling.")
|
||||
.def("round", &math::Vec3<T>::round,
|
||||
"Component-wise round.")
|
||||
.def(
|
||||
"__mul__", [](const T& a, math::Vec3<T> b) { return a * b; }, nb::is_operator(), "b"_a)
|
||||
"__mul__", [](const T& a, math::Vec3<T> b) { return a * b; }, nb::is_operator(), "b"_a,
|
||||
"Right-multiply: scalar * Vec3.")
|
||||
.def(
|
||||
"__truediv__", [](const T& a, math::Vec3<T> b) { return a / b; }, nb::is_operator(), "b"_a)
|
||||
"__truediv__", [](const T& a, math::Vec3<T> b) { return a / b; }, nb::is_operator(), "b"_a,
|
||||
"Right-divide: scalar / Vec3, component-wise.")
|
||||
.def("__repr__", [](const math::Vec3<T>& v) {
|
||||
std::stringstream ostr;
|
||||
ostr << v;
|
||||
@@ -168,12 +243,18 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
template<typename T> void defineVec4(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::Vec4<T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<T>(), "x"_a)
|
||||
.def(nb::init<T, T, T, T>(), "x"_a, "y"_a, "z"_a, "w"_a)
|
||||
.def(nb::init<math::Vec4<T>>(), "v"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a zero-initialized vector.")
|
||||
.def(nb::init<T>(), "x"_a,
|
||||
"Construct (x, x, x, x).")
|
||||
.def(nb::init<T, T, T, T>(), "x"_a, "y"_a, "z"_a, "w"_a,
|
||||
"Construct (x, y, z, w).")
|
||||
.def(nb::init<math::Vec4<T>>(), "v"_a,
|
||||
"Copy-construct from another Vec4.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Component-wise equality.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Component-wise inequality.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Vec4<T>& v, size_t i) {
|
||||
@@ -182,7 +263,8 @@ template<typename T> void defineVec4(nb::module_& m, const char* name, const cha
|
||||
}
|
||||
return v[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z, 3=w).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Vec4<T>& v, size_t i, T value) {
|
||||
@@ -192,28 +274,48 @@ template<typename T> void defineVec4(nb::module_& m, const char* name, const cha
|
||||
v[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
.def("dot", &math::Vec4<T>::template dot<math::Vec4<T>>, "v"_a)
|
||||
.def("lengthSqr", &math::Vec4<T>::lengthSqr)
|
||||
.def("length", &math::Vec4<T>::length)
|
||||
.def(-nb::self)
|
||||
.def(nb::self * nb::self, "v"_a)
|
||||
.def(nb::self / nb::self, "v"_a)
|
||||
.def(nb::self + nb::self, "v"_a)
|
||||
.def(nb::self - nb::self, "v"_a)
|
||||
.def(nb::self * T(), "s"_a)
|
||||
.def(nb::self / T(), "s"_a)
|
||||
.def(nb::self += nb::self, "v"_a)
|
||||
.def(nb::self -= nb::self, "v"_a)
|
||||
.def(nb::self *= T(), "s"_a)
|
||||
.def(nb::self /= T(), "s"_a)
|
||||
.def("normalize", &math::Vec4<T>::normalize)
|
||||
.def("minComponent", &math::Vec4<T>::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Vec4<T>::maxComponent, "other"_a)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z, 3=w).")
|
||||
.def("dot", &math::Vec4<T>::template dot<math::Vec4<T>>, "v"_a,
|
||||
"Dot product with another vector.")
|
||||
.def("lengthSqr", &math::Vec4<T>::lengthSqr,
|
||||
"Squared Euclidean length (cheaper than length()).")
|
||||
.def("length", &math::Vec4<T>::length,
|
||||
"Euclidean length of this vector.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self * nb::self, "v"_a,
|
||||
"Component-wise multiplication.")
|
||||
.def(nb::self / nb::self, "v"_a,
|
||||
"Component-wise division.")
|
||||
.def(nb::self + nb::self, "v"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "v"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(nb::self * T(), "s"_a,
|
||||
"Multiply every component by the scalar s.")
|
||||
.def(nb::self / T(), "s"_a,
|
||||
"Divide every component by the scalar s.")
|
||||
.def(nb::self += nb::self, "v"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self -= nb::self, "v"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def(nb::self *= T(), "s"_a,
|
||||
"In-place scalar multiply.")
|
||||
.def(nb::self /= T(), "s"_a,
|
||||
"In-place scalar divide.")
|
||||
.def("normalize", &math::Vec4<T>::normalize,
|
||||
"Scale this vector to unit length in place.")
|
||||
.def("minComponent", &math::Vec4<T>::minComponent, "other"_a,
|
||||
"Component-wise minimum with other.")
|
||||
.def("maxComponent", &math::Vec4<T>::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other.")
|
||||
.def(
|
||||
"__mul__", [](const T& a, math::Vec4<T> b) { return a * b; }, nb::is_operator(), "b"_a)
|
||||
"__mul__", [](const T& a, math::Vec4<T> b) { return a * b; }, nb::is_operator(), "b"_a,
|
||||
"Right-multiply: scalar * Vec4.")
|
||||
.def(
|
||||
"__truediv__", [](const T& a, math::Vec4<T> b) { return a / b; }, nb::is_operator(), "b"_a)
|
||||
"__truediv__", [](const T& a, math::Vec4<T> b) { return a / b; }, nb::is_operator(), "b"_a,
|
||||
"Right-divide: scalar / Vec4, component-wise.")
|
||||
.def("__repr__", [](const math::Vec4<T>& v) {
|
||||
std::stringstream ostr;
|
||||
ostr << v;
|
||||
@@ -226,18 +328,30 @@ void defineRgba8(nb::module_& m)
|
||||
using ValueType = math::Rgba8::ValueType;
|
||||
|
||||
nb::class_<math::Rgba8>(m, "Rgba8", "8-bit red, green, blue, alpha packed into 32 bit unsigned int")
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const math::Rgba8&>(), "other"_a)
|
||||
.def(nb::init<uint8_t, uint8_t, uint8_t, uint8_t>(), "r"_a, "g"_a, "b"_a, "a"_a = 255)
|
||||
.def(nb::init<uint8_t>(), "v"_a)
|
||||
.def(nb::init<float, float, float, float>(), "r"_a, "g"_a, "b"_a, "a"_a = 1.0)
|
||||
.def(nb::init<Vec3f>(), "rgb"_a)
|
||||
.def(nb::init<Vec4f>(), "rgba"_a)
|
||||
.def(nb::self < nb::self, "rhs"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def("lengthSqr", &math::Rgba8::lengthSqr)
|
||||
.def("length", &math::Rgba8::length)
|
||||
.def("asFloat", &math::Rgba8::asFloat, "n"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a fully transparent black Rgba8 (all components 0).")
|
||||
.def(nb::init<const math::Rgba8&>(), "other"_a,
|
||||
"Copy-construct from another Rgba8.")
|
||||
.def(nb::init<uint8_t, uint8_t, uint8_t, uint8_t>(), "r"_a, "g"_a, "b"_a, "a"_a = 255,
|
||||
"Construct from four 0..255 uint8 channels; a defaults to fully opaque.")
|
||||
.def(nb::init<uint8_t>(), "v"_a,
|
||||
"Construct a gray Rgba8 with every channel set to v.")
|
||||
.def(nb::init<float, float, float, float>(), "r"_a, "g"_a, "b"_a, "a"_a = 1.0,
|
||||
"Construct from four 0..1 floats, clamped and quantized to uint8.")
|
||||
.def(nb::init<Vec3f>(), "rgb"_a,
|
||||
"Construct from an RGB float triple; alpha defaults to opaque.")
|
||||
.def(nb::init<Vec4f>(), "rgba"_a,
|
||||
"Construct from an RGBA float quadruple.")
|
||||
.def(nb::self < nb::self, "rhs"_a,
|
||||
"Less-than comparison on the packed uint32 representation.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality on the packed uint32 representation.")
|
||||
.def("lengthSqr", &math::Rgba8::lengthSqr,
|
||||
"Squared length over (r, g, b, a) as integers.")
|
||||
.def("length", &math::Rgba8::length,
|
||||
"Euclidean length over (r, g, b, a) as floats.")
|
||||
.def("asFloat", &math::Rgba8::asFloat, "n"_a,
|
||||
"Return the n-th channel as a 0..1 float.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Rgba8& rgba, size_t i) {
|
||||
@@ -246,7 +360,8 @@ void defineRgba8(nb::module_& m)
|
||||
}
|
||||
return rgba[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th channel as a uint8 (0=r, 1=g, 2=b, 3=a).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Rgba8& rgba, size_t i, ValueType value) {
|
||||
@@ -256,26 +371,38 @@ void defineRgba8(nb::module_& m)
|
||||
rgba[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write the i-th channel as a uint8 (0=r, 1=g, 2=b, 3=a).")
|
||||
.def_prop_rw(
|
||||
"packed", [](const math::Rgba8& rgba) { return rgba.packed(); }, [](math::Rgba8& rgba, uint32_t packed) { rgba.packed() = packed; })
|
||||
"packed", [](const math::Rgba8& rgba) { return rgba.packed(); }, [](math::Rgba8& rgba, uint32_t packed) { rgba.packed() = packed; },
|
||||
"The raw 32-bit packed RGBA representation.")
|
||||
.def_prop_rw(
|
||||
"r", [](const math::Rgba8& rgba) { return rgba.r(); }, [](math::Rgba8& rgba, uint8_t r) { rgba.r() = r; })
|
||||
"r", [](const math::Rgba8& rgba) { return rgba.r(); }, [](math::Rgba8& rgba, uint8_t r) { rgba.r() = r; },
|
||||
"Red channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"g", [](const math::Rgba8& rgba) { return rgba.g(); }, [](math::Rgba8& rgba, uint8_t g) { rgba.g() = g; })
|
||||
"g", [](const math::Rgba8& rgba) { return rgba.g(); }, [](math::Rgba8& rgba, uint8_t g) { rgba.g() = g; },
|
||||
"Green channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"b", [](const math::Rgba8& rgba) { return rgba.b(); }, [](math::Rgba8& rgba, uint8_t b) { rgba.b() = b; })
|
||||
"b", [](const math::Rgba8& rgba) { return rgba.b(); }, [](math::Rgba8& rgba, uint8_t b) { rgba.b() = b; },
|
||||
"Blue channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"a", [](const math::Rgba8& rgba) { return rgba.a(); }, [](math::Rgba8& rgba, uint8_t a) { rgba.a() = a; })
|
||||
.def("asVec3f", [](const math::Rgba8& rgba) { return Vec3f(rgba); })
|
||||
.def("asVec4f", [](const math::Rgba8& rgba) { return Vec4f(rgba); });
|
||||
"a", [](const math::Rgba8& rgba) { return rgba.a(); }, [](math::Rgba8& rgba, uint8_t a) { rgba.a() = a; },
|
||||
"Alpha channel as a uint8 0..255.")
|
||||
.def("asVec3f", [](const math::Rgba8& rgba) { return Vec3f(rgba); },
|
||||
"Convert RGB channels to a Vec3f of 0..1 floats (alpha dropped).")
|
||||
.def("asVec4f", [](const math::Rgba8& rgba) { return Vec4f(rgba); },
|
||||
"Convert RGBA channels to a Vec4f of 0..1 floats.");
|
||||
}
|
||||
|
||||
template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
{
|
||||
nb::class_<math::BaseBBox<Vec3T>>(m, name)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
nb::class_<math::BaseBBox<Vec3T>>(m, name,
|
||||
"Axis-aligned bounding-box base class. Stores a min / max corner; "
|
||||
"concrete subclasses add the open-interval vs closed-interval semantics.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of both min and max corners.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of either min or max corner.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::BaseBBox<Vec3T>& bbox, size_t i) {
|
||||
@@ -284,7 +411,8 @@ template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
}
|
||||
return bbox[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read corner 0 (min) or corner 1 (max).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::BaseBBox<Vec3T>& bbox, size_t i, const Vec3T& value) {
|
||||
@@ -294,29 +422,45 @@ template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
bbox[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write corner 0 (min) or corner 1 (max).")
|
||||
.def_prop_rw(
|
||||
"min", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.min(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& min) { bbox.min() = min; })
|
||||
"min", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.min(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& min) { bbox.min() = min; },
|
||||
"Minimum corner of the bounding box.")
|
||||
.def_prop_rw(
|
||||
"max", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.max(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& max) { bbox.max() = max; })
|
||||
.def("translate", &math::BaseBBox<Vec3T>::translate, "xyz"_a)
|
||||
.def("expand", nb::overload_cast<const Vec3T&>(&math::BaseBBox<Vec3T>::expand), "xyz"_a)
|
||||
.def("expand", nb::overload_cast<const math::BaseBBox<Vec3T>&>(&math::BaseBBox<Vec3T>::expand), "bbox"_a)
|
||||
.def("intersect", &math::BaseBBox<Vec3T>::intersect, "bbox"_a)
|
||||
.def("isInside", &math::BaseBBox<Vec3T>::isInside, "xyz"_a);
|
||||
"max", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.max(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& max) { bbox.max() = max; },
|
||||
"Maximum corner of the bounding box.")
|
||||
.def("translate", &math::BaseBBox<Vec3T>::translate, "xyz"_a,
|
||||
"Translate this bounding box by xyz in place.")
|
||||
.def("expand", nb::overload_cast<const Vec3T&>(&math::BaseBBox<Vec3T>::expand), "xyz"_a,
|
||||
"Grow this bounding box to include the point xyz.")
|
||||
.def("expand", nb::overload_cast<const math::BaseBBox<Vec3T>&>(&math::BaseBBox<Vec3T>::expand), "bbox"_a,
|
||||
"Grow this bounding box to include another bbox in its entirety.")
|
||||
.def("intersect", &math::BaseBBox<Vec3T>::intersect, "bbox"_a,
|
||||
"Shrink this bounding box to the intersection with bbox.")
|
||||
.def("isInside", &math::BaseBBox<Vec3T>::isInside, "xyz"_a,
|
||||
"True iff xyz lies inside this bounding box.");
|
||||
}
|
||||
|
||||
template<typename Vec3T> void defineBBoxFloatingPoint(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::BBox<Vec3T, true>, math::BaseBBox<Vec3T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const Vec3T&, const Vec3T&>(), "min"_a, "max"_a)
|
||||
.def(nb::init<const math::Coord&, const math::Coord&>(), "min"_a, "max"_a)
|
||||
.def_static("createCube", &math::BBox<Vec3T>::createCube, "min"_a, "dim"_a)
|
||||
.def(nb::init<const math::BaseBBox<math::Coord>&>(), "bbox"_a)
|
||||
.def("empty", &math::BBox<Vec3T>::empty)
|
||||
.def("dim", &math::BBox<Vec3T>::dim)
|
||||
.def("isInside", &math::BBox<Vec3T>::isInside, "p"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty bounding box (min > max sentinel).")
|
||||
.def(nb::init<const Vec3T&, const Vec3T&>(), "min"_a, "max"_a,
|
||||
"Construct from explicit min and max corners.")
|
||||
.def(nb::init<const math::Coord&, const math::Coord&>(), "min"_a, "max"_a,
|
||||
"Construct from integer Coord corners, cast to floating-point.")
|
||||
.def_static("createCube", &math::BBox<Vec3T>::createCube, "min"_a, "dim"_a,
|
||||
"Construct an axis-aligned cube of side dim anchored at min.")
|
||||
.def(nb::init<const math::BaseBBox<math::Coord>&>(), "bbox"_a,
|
||||
"Construct from an integer CoordBBox, cast to floating-point.")
|
||||
.def("empty", &math::BBox<Vec3T>::empty,
|
||||
"True iff this bbox is empty (any min component > the matching max).")
|
||||
.def("dim", &math::BBox<Vec3T>::dim,
|
||||
"Return max - min as a Vec3 of side lengths.")
|
||||
.def("isInside", &math::BBox<Vec3T>::isInside, "p"_a,
|
||||
"True iff p lies inside this bounding box (half-open interval).")
|
||||
.def("__repr__", [](const math::BBox<Vec3T>& b) {
|
||||
std::stringstream ostr;
|
||||
ostr << b;
|
||||
@@ -329,24 +473,39 @@ template<typename CoordT> void defineBBoxInteger(nb::module_& m, const char* nam
|
||||
using ValueType = typename CoordT::ValueType;
|
||||
|
||||
nb::class_<math::BBox<CoordT, false>, math::BaseBBox<CoordT>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const CoordT&, const CoordT&>(), "min"_a, "max"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty CoordBBox (min > max sentinel).")
|
||||
.def(nb::init<const CoordT&, const CoordT&>(), "min"_a, "max"_a,
|
||||
"Construct from explicit min and max Coord corners (inclusive).")
|
||||
.def(
|
||||
"__iter__",
|
||||
[](const math::BBox<CoordT>& b) { return nb::make_iterator(nb::type<math::BBox<CoordT>>(), "CoordBBoxIterator", b.begin(), b.end()); },
|
||||
nb::keep_alive<0, 1>())
|
||||
.def_static("createCube", nb::overload_cast<const CoordT&, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "dim"_a)
|
||||
.def_static("createCube", nb::overload_cast<ValueType, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "max"_a)
|
||||
.def("is_divisible", &math::BBox<CoordT>::is_divisible)
|
||||
.def("empty", &math::BBox<CoordT>::empty)
|
||||
.def("dim", &math::BBox<CoordT>::dim)
|
||||
.def("volume", &math::BBox<CoordT>::volume)
|
||||
.def("isInside", nb::overload_cast<const CoordT&>(&math::BBox<CoordT>::isInside, nb::const_), "p"_a)
|
||||
.def("isInside", nb::overload_cast<const math::BBox<CoordT>&>(&math::BBox<CoordT>::isInside, nb::const_), "b"_a)
|
||||
.def("asFloat", &math::BBox<CoordT>::template asReal<float>)
|
||||
.def("asDouble", &math::BBox<CoordT>::template asReal<double>)
|
||||
.def("hasOverlap", &math::BBox<CoordT>::hasOverlap, "b"_a)
|
||||
.def("expandBy", &math::BBox<CoordT>::expandBy, "padding"_a)
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Iterate over every Coord in this CoordBBox in row-major order.")
|
||||
.def_static("createCube", nb::overload_cast<const CoordT&, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "dim"_a,
|
||||
"Construct a cube of side dim voxels anchored at the min Coord.")
|
||||
.def_static("createCube", nb::overload_cast<ValueType, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "max"_a,
|
||||
"Construct a cube spanning [min, max] in every axis.")
|
||||
.def("is_divisible", &math::BBox<CoordT>::is_divisible,
|
||||
"True iff this CoordBBox has more than one voxel in every axis.")
|
||||
.def("empty", &math::BBox<CoordT>::empty,
|
||||
"True iff this CoordBBox is empty (any min component > the matching max).")
|
||||
.def("dim", &math::BBox<CoordT>::dim,
|
||||
"Return max - min + 1 as a Coord of side lengths (inclusive).")
|
||||
.def("volume", &math::BBox<CoordT>::volume,
|
||||
"Total number of voxels enclosed by this CoordBBox.")
|
||||
.def("isInside", nb::overload_cast<const CoordT&>(&math::BBox<CoordT>::isInside, nb::const_), "p"_a,
|
||||
"True iff the integer point p lies inside this CoordBBox.")
|
||||
.def("isInside", nb::overload_cast<const math::BBox<CoordT>&>(&math::BBox<CoordT>::isInside, nb::const_), "b"_a,
|
||||
"True iff b lies entirely inside this CoordBBox.")
|
||||
.def("asFloat", &math::BBox<CoordT>::template asReal<float>,
|
||||
"Convert this CoordBBox to a floating-point BBox of Vec3f.")
|
||||
.def("asDouble", &math::BBox<CoordT>::template asReal<double>,
|
||||
"Convert this CoordBBox to a floating-point BBox of Vec3d.")
|
||||
.def("hasOverlap", &math::BBox<CoordT>::hasOverlap, "b"_a,
|
||||
"True iff this CoordBBox shares any voxel with b.")
|
||||
.def("expandBy", &math::BBox<CoordT>::expandBy, "padding"_a,
|
||||
"Grow this CoordBBox by the given padding in every direction.")
|
||||
.def("__repr__", [](const CoordBBox& b) {
|
||||
std::stringstream ostr;
|
||||
ostr << b;
|
||||
@@ -377,33 +536,12 @@ void defineMathModule(nb::module_& m)
|
||||
defineBaseBBox<math::Coord>(m, "CoordBaseBBox");
|
||||
defineBBoxInteger<math::Coord>(m, "CoordBBox", "Bounding box for Coord minimum and maximum");
|
||||
|
||||
defineNearestNeighborSampler<float>(m, "FloatNearestNeighborSampler");
|
||||
defineTrilinearSampler<float>(m, "FloatTrilinearSampler");
|
||||
defineTriquadraticSampler<float>(m, "FloatTriquadraticSampler");
|
||||
defineTricubicSampler<float>(m, "FloatTricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<double>(m, "DoubleNearestNeighborSampler");
|
||||
defineTrilinearSampler<double>(m, "DoubleTrilinearSampler");
|
||||
defineTriquadraticSampler<double>(m, "DoubleTriquadraticSampler");
|
||||
defineTricubicSampler<double>(m, "DoubleTricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<int32_t>(m, "Int32NearestNeighborSampler");
|
||||
defineTrilinearSampler<int32_t>(m, "Int32TrilinearSampler");
|
||||
defineTriquadraticSampler<int32_t>(m, "Int32TriquadraticSampler");
|
||||
defineTricubicSampler<int32_t>(m, "Int32TricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<Vec3f>(m, "Vec3fNearestNeighborSampler");
|
||||
defineTrilinearSampler<Vec3f>(m, "Vec3fTrilinearSampler");
|
||||
defineTriquadraticSampler<Vec3f>(m, "Vec3fTriquadraticSampler");
|
||||
defineTricubicSampler<Vec3f>(m, "Vec3fTricubicSampler");
|
||||
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
nb::module_ cudaModule = m.def_submodule("cuda");
|
||||
cudaModule.doc() = "A submodule that implements CUDA-accelerated math functions";
|
||||
|
||||
defineSampleFromVoxels<float>(cudaModule, "sampleFromVoxels");
|
||||
defineSampleFromVoxels<double>(cudaModule, "sampleFromVoxels");
|
||||
#endif
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
defineNearestNeighborSampler<T>(m, #Suffix "NearestNeighborSampler"); \
|
||||
defineTrilinearSampler<T>(m, #Suffix "TrilinearSampler"); \
|
||||
defineTriquadraticSampler<T>(m, #Suffix "TriquadraticSampler"); \
|
||||
defineTricubicSampler<T>(m, #Suffix "TricubicSampler");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -32,11 +32,9 @@ GridHandle<BufferT> createLevelSetSphere(GridType gridType,
|
||||
switch (gridType) {
|
||||
case GridType::Float: return createLevelSetSphere<float, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double: return createLevelSetSphere<double, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createLevelSetSphere for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetSphere: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +56,9 @@ GridHandle<BufferT> createLevelSetTorus(GridType gridType,
|
||||
return createLevelSetTorus<float, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return createLevelSetTorus<double, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createLevelSetTorus for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetTorus: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +77,9 @@ GridHandle<BufferT> createFogVolumeSphere(GridType gridType,
|
||||
switch (gridType) {
|
||||
case GridType::Float: return createFogVolumeSphere<float, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double: return createFogVolumeSphere<double, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createFogVolumeSphere for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeSphere: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,14 +101,218 @@ GridHandle<BufferT> createFogVolumeTorus(GridType gridType,
|
||||
return createFogVolumeTorus<float, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return createFogVolumeTorus<double, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createFogVolumeTorus for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeTorus: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- New primitives ----------
|
||||
// Same float/double switch pattern as the four existing primitives above.
|
||||
// The C++ templates also accept Fp4/Fp8/Fp16/FpN; those flavors live on
|
||||
// the createNanoGrid path with explicit oracle and dither parameters
|
||||
// rather than being expressed as primitive overloads here.
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetBox<float, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetBox<double, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetBBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
double thickness,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetBBox<float, BufferT>(
|
||||
width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetBBox<double, BufferT>(
|
||||
width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetBBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetOctahedron(GridType gridType,
|
||||
double scale,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetOctahedron<float, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetOctahedron<double, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetOctahedron: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createFogVolumeBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createFogVolumeBox<float, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createFogVolumeBox<double, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createFogVolumeOctahedron(GridType gridType,
|
||||
double scale,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createFogVolumeOctahedron<float, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createFogVolumeOctahedron<double, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeOctahedron: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
// Point primitives. The result is always a PointDataGrid (uint32 storage),
|
||||
// so unlike the level-set / fog-volume primitives there's no value-type
|
||||
// dispatch worth exposing — the intermediate level-set's precision is
|
||||
// not user-controllable in this binding. The C++ template also accepts
|
||||
// BuildT=double, but that path segfaults during scatter at least with the
|
||||
// current C++ implementation, so the binding stays on the float-only
|
||||
// instantiation that's exercised by the C++ unit tests.
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointSphere(int pointsPerVoxel,
|
||||
double radius,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointSphere<float, BufferT>(
|
||||
pointsPerVoxel, radius, center, voxelSize, origin, name, mode, buffer);
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointTorus(int pointsPerVoxel,
|
||||
double majorRadius,
|
||||
double minorRadius,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointTorus<float, BufferT>(
|
||||
pointsPerVoxel, majorRadius, minorRadius, center, voxelSize, origin, name, cMode, buffer);
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointBox(int pointsPerVoxel,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointBox<float, BufferT>(
|
||||
pointsPerVoxel, width, height, depth, center, voxelSize, origin, name, mode, buffer);
|
||||
}
|
||||
|
||||
// createPointScatter takes an existing level set as its source. We bind
|
||||
// the float source variant — the C++ template also accepts double, but
|
||||
// the existing primitives and tests use float, and the source grid is
|
||||
// the runtime-typed nanovdb::NanoGrid<float>, so a single overload keeps
|
||||
// the Python surface simple.
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointScatter(const NanoGrid<float>& srcGrid,
|
||||
int pointsPerVoxel,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointScatter<float, BufferT>(
|
||||
srcGrid, pointsPerVoxel, name, mode, buffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
@@ -131,7 +329,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "sphere_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a sphere of the given radius and center.");
|
||||
|
||||
m.def("createLevelSetTorus",
|
||||
nb::overload_cast<GridType,
|
||||
@@ -155,7 +354,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "torus_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a torus with the given major and minor radii.");
|
||||
|
||||
m.def("createFogVolumeSphere",
|
||||
nb::overload_cast<GridType, double, const Vec3d&, double, double, const Vec3d&, const std::string&, tools::StatsMode, CheckMode, const BufferT&>(
|
||||
@@ -169,7 +369,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "sphere_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a sphere of the given radius and center.");
|
||||
|
||||
m.def("createFogVolumeTorus",
|
||||
nb::overload_cast<GridType,
|
||||
@@ -193,7 +394,139 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "torus_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a torus with the given major and minor radii.");
|
||||
|
||||
// ---------- Level-set / fog-volume box / bbox / octahedron primitives ----
|
||||
m.def("createLevelSetBox", &createLevelSetBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of an axis-aligned box.");
|
||||
|
||||
m.def("createLevelSetBBox", &createLevelSetBBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"thickness"_a = 10.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "bbox_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a hollow box wireframe (BBox = bounding "
|
||||
"box edges with the given thickness).");
|
||||
|
||||
m.def("createLevelSetOctahedron", &createLevelSetOctahedron<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"scale"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
// Default name spells the shape correctly even though the
|
||||
// upstream C++ default still carries the historical
|
||||
// "octadedron_ls" typo. Callers can override either way.
|
||||
"name"_a = "octahedron_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of an octahedron.");
|
||||
|
||||
m.def("createFogVolumeBox", &createFogVolumeBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a box (exterior 0/inactive, interior active "
|
||||
"with values smoothly varying from 0 at the surface to 1 inside).");
|
||||
|
||||
m.def("createFogVolumeOctahedron", &createFogVolumeOctahedron<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"scale"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "octahedron_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of an octahedron.");
|
||||
|
||||
// ---------- Point primitives ----------
|
||||
m.def("createPointSphere", &createPointSphere<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"radius"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "sphere_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a sphere. "
|
||||
"The output grid is always a UInt32 PointDataGrid; the "
|
||||
"intermediate level-set's value type is hard-coded to float.");
|
||||
|
||||
m.def("createPointTorus", &createPointTorus<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"majorRadius"_a = 100.0,
|
||||
"minorRadius"_a = 50.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "torus_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a torus. "
|
||||
"Always returns a UInt32 PointDataGrid.");
|
||||
|
||||
m.def("createPointBox", &createPointBox<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a box. "
|
||||
"Always returns a UInt32 PointDataGrid.");
|
||||
|
||||
m.def("createPointScatter", &createPointScatter<BufferT>,
|
||||
"srcGrid"_a,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"name"_a = "point_scatter",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Scatter a PointDataGrid into the active voxels of a "
|
||||
"NanoGrid<float> level set. The source grid must satisfy "
|
||||
"srcGrid.isLevelSet() and have an active bounding box; "
|
||||
"non-level-set sources (e.g. fog volumes) raise RuntimeError. "
|
||||
"Point coordinates are stored as blind data in world space.");
|
||||
}
|
||||
|
||||
template void definePrimitives<HostBuffer>(nb::module_&);
|
||||
|
||||
@@ -15,20 +15,53 @@ namespace {
|
||||
|
||||
template<typename TreeT, int Order> void defineSampleFromVoxels(nb::module_& m, const char* name)
|
||||
{
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
nb::class_<math::SampleFromVoxels<TreeT, Order, false>>(m, name)
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using SamplerT = math::SampleFromVoxels<TreeT, Order, false>;
|
||||
auto cls = nb::class_<SamplerT>(m, name,
|
||||
"Callable sampler that reconstructs a grid value at an arbitrary "
|
||||
"index-space position. Build via the matching create*Sampler() factory.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const CoordT& ijk) { return sampler(ijk); }, nb::is_operator(), "ijk"_a)
|
||||
"__call__", [](const SamplerT& sampler, const CoordT& ijk) { return sampler(ijk); }, nb::is_operator(), "ijk"_a,
|
||||
"Sample the grid at integer voxel coordinate ijk.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const Vec3f& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a)
|
||||
"__call__", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a,
|
||||
"Sample the grid at fractional index-space position xyz.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const Vec3d& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a);
|
||||
"__call__", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a,
|
||||
"Sample the grid at fractional index-space position xyz (double).");
|
||||
// gradient() exists on the trilinear sampler only, zeroCrossing() on the
|
||||
// trilinear and triquadratic samplers, and both static_assert a
|
||||
// floating-point ValueT in C++ — mirror that gating here.
|
||||
if constexpr (Order == 1 && util::is_floating_point<ValueT>::value) {
|
||||
cls.def(
|
||||
"gradient", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler.gradient(xyz); }, "xyz"_a,
|
||||
"Return the index-space gradient of the trilinear reconstruction "
|
||||
"at fractional index-space position xyz. Use "
|
||||
"grid.indexToWorldGrad() to move it to world space.")
|
||||
.def(
|
||||
"gradient", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler.gradient(xyz); }, "xyz"_a,
|
||||
"Return the index-space gradient at fractional index-space "
|
||||
"position xyz (double).");
|
||||
}
|
||||
if constexpr ((Order == 1 || Order == 2) && util::is_floating_point<ValueT>::value) {
|
||||
cls.def(
|
||||
"zeroCrossing", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler.zeroCrossing(xyz); }, "xyz"_a,
|
||||
"True iff the reconstruction stencil at fractional index-space "
|
||||
"position xyz straddles the zero iso-surface.")
|
||||
.def(
|
||||
"zeroCrossing", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler.zeroCrossing(xyz); }, "xyz"_a,
|
||||
"True iff the reconstruction stencil at fractional index-space "
|
||||
"position xyz (double) straddles the zero iso-surface.");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename TreeT, int Order> void defineCreateSampler(nb::module_& m, const char* name)
|
||||
{
|
||||
m.def(
|
||||
name, [](const Grid<TreeT>& grid) { return math::createSampler<Order, TreeT, false>(grid.tree()); }, "grid"_a);
|
||||
name, [](const Grid<TreeT>& grid) { return math::createSampler<Order, TreeT, false>(grid.tree()); }, "grid"_a,
|
||||
"Build a sampler of the matching order that reads values from the "
|
||||
"given grid's tree.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -65,24 +98,11 @@ template<typename BuildT> void defineTricubicSampler(nb::module_& m, const char*
|
||||
defineCreateSampler<TreeT, 3>(m, "createTricubicSampler");
|
||||
}
|
||||
|
||||
template void defineNearestNeighborSampler<float>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<float>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<float>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<float>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<double>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<double>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<double>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<double>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<int32_t>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<Vec3f>(nb::module_&, const char*);
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
template void defineNearestNeighborSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTrilinearSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTriquadraticSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTricubicSampler<T>(nb::module_&, const char*);
|
||||
#include "BuildTypes.def"
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "PyNanoToOpenVDB.h"
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
#include "cuda/PyPointsToGrid.h"
|
||||
#include "cuda/PySampleFromVoxels.h"
|
||||
#include "cuda/PySignedFloodFill.h"
|
||||
#endif
|
||||
|
||||
@@ -31,12 +32,17 @@ void defineToolsModule(nb::module_& m)
|
||||
|
||||
defineStatsMode(m);
|
||||
|
||||
defineGridStatsModule(m);
|
||||
defineGridValidatorModule(m);
|
||||
defineEvalChecksumModule(m);
|
||||
|
||||
defineCreateNanoGridConversions(m);
|
||||
|
||||
definePrimitives<HostBuffer>(m);
|
||||
|
||||
defineCreateNanoGrid<float>(m, "createFloatGrid");
|
||||
defineCreateNanoGrid<double>(m, "createDoubleGrid");
|
||||
defineCreateNanoGrid<int32_t>(m, "createInt32Grid");
|
||||
defineCreateNanoGrid<Vec3f>(m, "createVec3fGrid");
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
defineCreateNanoGrid<T>(m, "create" #Suffix "Grid");
|
||||
#include "BuildTypes.def"
|
||||
|
||||
#ifdef NANOVDB_USE_OPENVDB
|
||||
defineOpenToNanoVDB<HostBuffer>(m);
|
||||
@@ -55,6 +61,9 @@ void defineToolsModule(nb::module_& m)
|
||||
defineSignedFloodFill<double>(cudaModule, "signedFloodFill");
|
||||
|
||||
definePointsToGrid<math::Rgba8>(cudaModule, "pointsToRGBA8Grid");
|
||||
|
||||
defineSampleFromVoxels<float>(cudaModule, "sampleFromVoxels");
|
||||
defineSampleFromVoxels<double>(cudaModule, "sampleFromVoxels");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyTree.h"
|
||||
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Polymorphic mgr() that returns the right typed NodeManager based on the
|
||||
// handle's stored gridType. Dispatch follows the same X-macro pattern as
|
||||
// pyHostGrid / pyDeviceGrid; unbound BuildTs return None rather than the
|
||||
// generic getMgr<BuildT>() ptr that would be reinterpreted.
|
||||
template<typename BufferT>
|
||||
static nb::object pyNodeMgr(nb::handle py_self)
|
||||
{
|
||||
using HandleT = NodeManagerHandle<BufferT>;
|
||||
auto& handle = nb::cast<HandleT&>(py_self);
|
||||
if (!handle.data()) return nb::none();
|
||||
// We need to read the stored gridType, but it's private. The public
|
||||
// mgr<BuildT>() returns NULL for type mismatch, so iterate by BuildT.
|
||||
// The X-macro produces one case per bound BuildT; first non-null wins.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
return nb::none();
|
||||
}
|
||||
|
||||
void defineNodeManagerHandle(nb::module_& m)
|
||||
{
|
||||
using HandleT = NodeManagerHandle<HostBuffer>;
|
||||
nb::class_<HandleT>(m, "NodeManagerHandle",
|
||||
"Owns the memory backing a NodeManager. Move-only. "
|
||||
"Obtain via nanovdb.createNodeManager(grid).")
|
||||
.def("size",
|
||||
[](const HandleT& h) { return h.size(); },
|
||||
"Byte size of the buffer backing this NodeManagerHandle.")
|
||||
.def(
|
||||
"__bool__",
|
||||
[](const HandleT& h) { return h.data() != nullptr; },
|
||||
nb::is_operator(),
|
||||
"True iff this handle owns a non-empty buffer.")
|
||||
.def("mgr", &pyNodeMgr<HostBuffer>,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the typed NodeManager for the grid this handle was "
|
||||
"built from, or None if the BuildT is not Python-visible. The "
|
||||
"returned NodeManager keeps this handle alive.");
|
||||
}
|
||||
|
||||
// createNodeManager has one template instantiation per BuildT. We expose a
|
||||
// single polymorphic `createNodeManager(grid)` that picks the right one
|
||||
// based on the runtime type of `grid` (any nb::class_-bound NanoGrid<T>).
|
||||
// nb::isinstance is a fast type check that avoids the exception-on-mismatch
|
||||
// overhead that would come from trying nb::cast and catching cast_error for
|
||||
// every non-matching BuildT.
|
||||
template<typename BuildT>
|
||||
static nb::object tryCreateNodeManager(nb::handle py_grid)
|
||||
{
|
||||
using GridT = NanoGrid<BuildT>;
|
||||
if (!nb::isinstance<GridT>(py_grid)) {
|
||||
return nb::object(); // sentinel: "not this BuildT, try next"
|
||||
}
|
||||
auto& grid = nb::cast<GridT&>(py_grid);
|
||||
return nb::cast(createNodeManager<BuildT, HostBuffer>(grid));
|
||||
}
|
||||
|
||||
void defineCreateNodeManager(nb::module_& m)
|
||||
{
|
||||
m.def("createNodeManager",
|
||||
[](nb::handle py_grid) -> nb::object {
|
||||
// Try every bound BuildT; first successful cast wins.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
throw nb::type_error(
|
||||
"createNodeManager: argument is not a NanoVDB grid of any "
|
||||
"bound BuildT");
|
||||
},
|
||||
"grid"_a,
|
||||
// The constructed NodeManager stores a raw pointer back to the
|
||||
// grid; the handle must therefore keep the grid (and transitively
|
||||
// the GridHandle that owns the grid's buffer) alive.
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Build a NodeManager for the given grid, returning a "
|
||||
"NodeManagerHandle that owns the underlying buffer. The handle's "
|
||||
"mgr() method returns the typed NodeManager. The handle keeps the "
|
||||
"source grid alive for as long as it lives.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,486 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYTREE_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYTREE_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/ndarray.h>
|
||||
#include <nanobind/stl/tuple.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/NodeManager.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
|
||||
#include <type_traits> // std::is_arithmetic_v, std::enable_if
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// -------------------- NanoLeaf<BuildT> --------------------
|
||||
//
|
||||
// Binds the 8^3 leaf node. Methods that don't depend on whether the leaf
|
||||
// stores a contiguous T[512] array are bound unconditionally; the
|
||||
// zero-copy 512-element NumPy values() view is only bound when the leaf
|
||||
// layout actually carries T mValues[512] (the BuildTraits<T>::is_special
|
||||
// types use packed / index / mask layouts where a 512-element T view is
|
||||
// either impossible or misleading).
|
||||
template<typename BuildT> void defineNanoLeaf(nb::module_& m, const char* name)
|
||||
{
|
||||
using LeafT = nanovdb::NanoLeaf<BuildT>;
|
||||
using ValueT = typename LeafT::ValueType;
|
||||
using CoordT = typename LeafT::CoordType;
|
||||
|
||||
auto cls = nb::class_<LeafT>(m, name,
|
||||
"Leaf node — 8x8x8 voxels. Inherits stats and bbox from the same "
|
||||
"leaf-data block bound across BuildTs.");
|
||||
|
||||
cls.def("origin", &LeafT::origin,
|
||||
"Index-space origin (minimum corner) of this leaf node.")
|
||||
.def("bbox", &LeafT::bbox,
|
||||
"Index-space bounding box of this leaf's active voxels.")
|
||||
.def("hasBBox", &LeafT::hasBBox,
|
||||
"True iff this leaf carries a cached active bounding box.")
|
||||
.def_static("dim", &LeafT::dim,
|
||||
"Side length of a leaf node in voxels (always 8).")
|
||||
.def_static("voxelCount", &LeafT::voxelCount,
|
||||
"Total voxel count in a leaf (always 512).")
|
||||
.def("memUsage", &LeafT::memUsage,
|
||||
"Byte size of this leaf node.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&LeafT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("isActive",
|
||||
[](const LeafT& leaf, uint32_t n) {
|
||||
// Underlying mValueMask.isOn(n) is unchecked; release builds
|
||||
// skip the C++ NANOVDB_ASSERT and would silently read OOB.
|
||||
if (n >= LeafT::voxelCount()) {
|
||||
throw nb::index_error(
|
||||
"Leaf.isActive(n): n out of range [0, voxelCount)");
|
||||
}
|
||||
return leaf.isActive(n);
|
||||
},
|
||||
nb::arg("n"),
|
||||
"True iff the n-th voxel (linear index into the 512-element leaf) is active.")
|
||||
.def("getValue",
|
||||
[](const LeafT& leaf, uint32_t offset) {
|
||||
// mValues[offset] is unchecked in C++; guard the Python side.
|
||||
if (offset >= LeafT::voxelCount()) {
|
||||
throw nb::index_error(
|
||||
"Leaf.getValue(offset): offset out of range [0, voxelCount)");
|
||||
}
|
||||
return leaf.getValue(offset);
|
||||
},
|
||||
nb::arg("offset"),
|
||||
"Return the value at linear offset (0..511) within this leaf.")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&LeafT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk; ijk must lie inside this leaf.")
|
||||
.def("getFirstValue", &LeafT::getFirstValue,
|
||||
"Value at the leaf's first voxel (linear offset 0).")
|
||||
.def("getLastValue", &LeafT::getLastValue,
|
||||
"Value at the leaf's last voxel (linear offset 511).")
|
||||
.def("minimum", &LeafT::minimum,
|
||||
"Minimum active value within this leaf.")
|
||||
.def("maximum", &LeafT::maximum,
|
||||
"Maximum active value within this leaf.")
|
||||
.def("average", &LeafT::average,
|
||||
"Average of active values within this leaf.")
|
||||
.def("stdDeviation", &LeafT::stdDeviation,
|
||||
"Standard deviation of active values within this leaf.")
|
||||
// NOTE: variance() omitted — NanoVDB.h line 4388 uses unqualified
|
||||
// Pow2() which fails ADL for non-float ValueTs (ValueIndex /
|
||||
// ValueMask / etc.). Users can compute it as stdDeviation() ** 2.
|
||||
.def("flags", &LeafT::flags,
|
||||
"Raw leaf flag bits.")
|
||||
.def("valueMask", &LeafT::valueMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the leaf's 512-bit active-value mask.")
|
||||
.def("probeValue",
|
||||
[](const LeafT& leaf, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = leaf.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
|
||||
// Zero-copy 512-element NumPy view of mValues. Only enabled for
|
||||
// BuildTs whose ValueType is a primitive arithmetic type (float,
|
||||
// double, int*). For Fp* / Index / Mask / bool / Point the leaf uses
|
||||
// packed / mask / void layouts. For Vec3f / Vec3d / Vec4f / Vec4d /
|
||||
// Vec3u8 / Vec3u16 / Rgba8 the leaf carries a struct array that
|
||||
// nanobind's ndarray can't represent directly — those would need a
|
||||
// flattened (count, dim) component-typed view which a follow-up can
|
||||
// add. Users can still walk every leaf via the bound getValue().
|
||||
if constexpr (std::is_arithmetic_v<ValueT>
|
||||
&& !nanovdb::BuildTraits<BuildT>::is_special) {
|
||||
cls.def("values",
|
||||
[](nb::handle py_self) {
|
||||
auto& leaf = nb::cast<LeafT&>(py_self);
|
||||
size_t shape[1] = {LeafT::voxelCount()};
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, ValueT, nb::ndim<1>, nb::c_contig, nb::device::cpu>(
|
||||
static_cast<void*>(leaf.data()->mValues),
|
||||
size_t(1), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy NumPy view of the 512 leaf values. The view "
|
||||
"keeps the leaf (and transitively the GridHandle that owns the "
|
||||
"underlying buffer) alive.");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- NanoUpper / NanoLower<BuildT> --------------------
|
||||
//
|
||||
// Both internal node levels share the same C++ API surface (just different
|
||||
// LOG2DIM). One helper templated on the concrete InternalNode type.
|
||||
template<typename InternalT>
|
||||
void defineInternalNodeBase(nb::class_<InternalT>& cls)
|
||||
{
|
||||
using ValueT = typename InternalT::ValueType;
|
||||
using CoordT = typename InternalT::CoordType;
|
||||
cls.def("origin", &InternalT::origin,
|
||||
"Index-space origin (minimum corner) of this internal node.")
|
||||
.def("bbox", &InternalT::bbox,
|
||||
"Index-space bounding box of this internal node's active voxels.")
|
||||
.def_static("dim", &InternalT::dim,
|
||||
"Side length in voxels covered by this internal node.")
|
||||
.def_static("memUsage", []() { return InternalT::memUsage(); },
|
||||
"Byte size of an internal node.")
|
||||
.def("minimum", &InternalT::minimum,
|
||||
"Minimum active value within this node's subtree.")
|
||||
.def("maximum", &InternalT::maximum,
|
||||
"Maximum active value within this node's subtree.")
|
||||
.def("average", &InternalT::average,
|
||||
"Average of active values within this node's subtree.")
|
||||
.def("stdDeviation", &InternalT::stdDeviation,
|
||||
"Standard deviation of active values within this node's subtree.")
|
||||
// variance() omitted for parity with the leaf binding; compute as
|
||||
// stdDeviation() ** 2 in Python.
|
||||
.def("valueMask", &InternalT::valueMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the node's active-tile mask.")
|
||||
.def("childMask", &InternalT::childMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the node's child-pointer mask (1 where a child node exists).")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&InternalT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by descending into the subtree.")
|
||||
.def("getFirstValue", &InternalT::getFirstValue,
|
||||
"Value at the first (lowest-indexed) tile in this node.")
|
||||
.def("getLastValue", &InternalT::getLastValue,
|
||||
"Value at the last (highest-indexed) tile in this node.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&InternalT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const InternalT& node, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = node.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
}
|
||||
|
||||
template<typename BuildT> void defineNanoUpper(nb::module_& m, const char* name)
|
||||
{
|
||||
using UpperT = nanovdb::NanoUpper<BuildT>;
|
||||
nb::class_<UpperT> cls(m, name,
|
||||
"Upper internal node — 32x32x32 (covers a 4096^3 region in index space).");
|
||||
defineInternalNodeBase(cls);
|
||||
}
|
||||
|
||||
template<typename BuildT> void defineNanoLower(nb::module_& m, const char* name)
|
||||
{
|
||||
using LowerT = nanovdb::NanoLower<BuildT>;
|
||||
nb::class_<LowerT> cls(m, name,
|
||||
"Lower internal node — 16x16x16 (covers a 128^3 region in index space).");
|
||||
defineInternalNodeBase(cls);
|
||||
}
|
||||
|
||||
// -------------------- NanoRoot<BuildT> --------------------
|
||||
template<typename BuildT> void defineNanoRoot(nb::module_& m, const char* name)
|
||||
{
|
||||
using RootT = nanovdb::NanoRoot<BuildT>;
|
||||
using ValueT = typename RootT::ValueType;
|
||||
using CoordT = typename RootT::CoordType;
|
||||
|
||||
nb::class_<RootT>(m, name, "Root node — top of the tree, holds the tile table.")
|
||||
.def("background", &RootT::background, nb::rv_policy::reference_internal,
|
||||
"Background value returned for inactive voxels.")
|
||||
.def("tileCount", &RootT::tileCount,
|
||||
"Number of active tiles in the root tile table.")
|
||||
.def("getTableSize", &RootT::getTableSize,
|
||||
"Total number of entries in the root tile table.")
|
||||
.def("isEmpty", &RootT::isEmpty,
|
||||
"True iff the root has no active tiles.")
|
||||
.def("bbox", &RootT::bbox, nb::rv_policy::reference_internal,
|
||||
"Index-space bounding box of every active value in this tree.")
|
||||
.def("minimum", &RootT::minimum, nb::rv_policy::reference_internal,
|
||||
"Minimum active value in the tree.")
|
||||
.def("maximum", &RootT::maximum, nb::rv_policy::reference_internal,
|
||||
"Maximum active value in the tree.")
|
||||
.def("average", &RootT::average, nb::rv_policy::reference_internal,
|
||||
"Average of active values in the tree.")
|
||||
.def("stdDeviation", &RootT::stdDeviation, nb::rv_policy::reference_internal,
|
||||
"Standard deviation of active values in the tree.")
|
||||
.def("memUsage",
|
||||
nb::overload_cast<>(&RootT::memUsage, nb::const_),
|
||||
"Byte size of the root node and its tile table.")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&RootT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by walking from the root.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&RootT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const RootT& root, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = root.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
}
|
||||
|
||||
// -------------------- NanoTree<BuildT> --------------------
|
||||
template<typename BuildT> void defineNanoTree(nb::module_& m, const char* name)
|
||||
{
|
||||
using TreeT = nanovdb::NanoTree<BuildT>;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
using RootT = typename TreeT::RootType;
|
||||
using UpperT = typename TreeT::UpperNodeType;
|
||||
using LowerT = typename TreeT::LowerNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
|
||||
nb::class_<TreeT>(m, name,
|
||||
"Tree — owns the root and provides bulk metadata queries "
|
||||
"(node counts, active voxel count, extrema).")
|
||||
.def("root",
|
||||
nb::overload_cast<>(&TreeT::root, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"Root node of this tree. Lifetime is anchored to the tree.")
|
||||
.def("background", &TreeT::background, nb::rv_policy::reference_internal,
|
||||
"Background value returned for inactive voxels.")
|
||||
.def("activeVoxelCount", &TreeT::activeVoxelCount,
|
||||
"Total number of active voxels in this tree.")
|
||||
// activeTileCount(level): valid range is 1..3 (lower / upper / root
|
||||
// tile counts). C++ uses NANOVDB_ASSERT(level > 0 && level <= 3)
|
||||
// which is a no-op in release builds — so guard explicitly.
|
||||
.def("activeTileCount",
|
||||
[](const TreeT& tree, uint32_t level) -> uint32_t {
|
||||
if (level < 1 || level > 3) {
|
||||
throw nb::value_error(
|
||||
"Tree.activeTileCount(level): level must be 1, 2, or 3");
|
||||
}
|
||||
return tree.activeTileCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of active tiles at the given tree level (1=lower, 2=upper, 3=root).")
|
||||
// nodeCount(level): valid range is 0..2 (leaf / lower / upper).
|
||||
// C++ uses NANOVDB_ASSERT(level < 3), again no-op in release.
|
||||
// The lambda's `int level` argument disambiguates the call against
|
||||
// Tree's templated nodeCount<NodeT>() overload at the C++ level,
|
||||
// so we don't need an overload_cast / static_cast wrapper here.
|
||||
.def("nodeCount",
|
||||
[](const TreeT& tree, int level) -> uint32_t {
|
||||
if (level < 0 || level >= 3) {
|
||||
throw nb::value_error(
|
||||
"Tree.nodeCount(level): level must be 0, 1, or 2");
|
||||
}
|
||||
return tree.nodeCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).")
|
||||
.def("totalNodeCount", &TreeT::totalNodeCount,
|
||||
"Sum of node counts across every tree level.")
|
||||
.def_static("memUsage", &TreeT::memUsage,
|
||||
"Byte size of a single tree structure (header only).")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&TreeT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by descending from the root.")
|
||||
.def("isActive", &TreeT::isActive, nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const TreeT& tree, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = tree.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.")
|
||||
.def("extrema",
|
||||
[](const TreeT& tree) {
|
||||
ValueT mn, mx;
|
||||
tree.extrema(mn, mx);
|
||||
return std::make_tuple(mn, mx);
|
||||
},
|
||||
"Return (min, max) of the active values over the whole tree.")
|
||||
.def("getFirstLeaf",
|
||||
nb::overload_cast<>(&TreeT::getFirstLeaf, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First leaf node in breadth-first order, or None if the tree is empty.")
|
||||
.def("getFirstLower",
|
||||
nb::overload_cast<>(&TreeT::getFirstLower, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First lower internal node in breadth-first order, or None if none exists.")
|
||||
.def("getFirstUpper",
|
||||
nb::overload_cast<>(&TreeT::getFirstUpper, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First upper internal node in breadth-first order, or None if none exists.");
|
||||
}
|
||||
|
||||
// -------------------- NodeManager<BuildT> --------------------
|
||||
//
|
||||
// NodeManager is heap-managed by a NodeManagerHandle (move-only, owns the
|
||||
// underlying memory). We bind one NodeManager class per BuildT and one
|
||||
// host-side NodeManagerHandle class. Users get a handle from
|
||||
// nanovdb.createNodeManager(grid); they then call handle.mgr() to obtain a
|
||||
// borrowed pointer to the typed NodeManager — its lifetime is anchored to
|
||||
// the handle via reference_internal.
|
||||
template<typename BuildT> void defineNodeManager(nb::module_& m, const char* name)
|
||||
{
|
||||
using NMT = nanovdb::NodeManager<BuildT>;
|
||||
nb::class_<NMT>(m, name,
|
||||
"Sequential breadth-first accessor for the leaf / lower / upper "
|
||||
"internal nodes of a NanoGrid. Construct via "
|
||||
"nanovdb.createNodeManager(grid).")
|
||||
.def("isLinear",
|
||||
nb::overload_cast<>(&NMT::isLinear, nb::const_),
|
||||
"True iff this NodeManager is laid out as a linear offset table over a breadth-first grid.")
|
||||
.def("memUsage",
|
||||
nb::overload_cast<>(&NMT::memUsage, nb::const_),
|
||||
"Byte size of this NodeManager.")
|
||||
.def("nodeCount",
|
||||
[](const NMT& nm, int level) -> uint64_t {
|
||||
// Mirror Tree.nodeCount bounds (NodeManager forwards to Tree).
|
||||
if (level < 0 || level >= 3) {
|
||||
throw nb::value_error(
|
||||
"NodeManager.nodeCount(level): level must be 0, 1, or 2");
|
||||
}
|
||||
return nm.nodeCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).")
|
||||
.def("leafCount", &NMT::leafCount,
|
||||
"Number of leaf nodes managed by this NodeManager.")
|
||||
.def("lowerCount", &NMT::lowerCount,
|
||||
"Number of lower internal nodes managed by this NodeManager.")
|
||||
.def("upperCount", &NMT::upperCount,
|
||||
"Number of upper internal nodes managed by this NodeManager.")
|
||||
// leaf / lower / upper: NANOVDB_ASSERT(i < nodeCount(LEVEL)) in C++ is
|
||||
// no-op in release, so guard explicitly to convert OOB access into a
|
||||
// Python IndexError instead of memory corruption.
|
||||
.def("leaf",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoLeaf<BuildT>& {
|
||||
if (i >= nm.leafCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.leaf(i): i out of range [0, leafCount)");
|
||||
}
|
||||
return nm.leaf(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th leaf node in breadth-first order.")
|
||||
.def("lower",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoLower<BuildT>& {
|
||||
if (i >= nm.lowerCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.lower(i): i out of range [0, lowerCount)");
|
||||
}
|
||||
return nm.lower(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th lower internal node in breadth-first order.")
|
||||
.def("upper",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoUpper<BuildT>& {
|
||||
if (i >= nm.upperCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.upper(i): i out of range [0, upperCount)");
|
||||
}
|
||||
return nm.upper(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th upper internal node in breadth-first order.");
|
||||
}
|
||||
|
||||
void defineNodeManagerHandle(nb::module_& m);
|
||||
void defineCreateNodeManager(nb::module_& m);
|
||||
|
||||
// -------------------- grid.leaf_values() bulk extractor --------------------
|
||||
//
|
||||
// For non-special BuildTs with breadth-first, fixed-size leaves, the leaf
|
||||
// values can be reached as a contiguous (N_leaves, 512) array — every leaf
|
||||
// occupies sizeof(NanoLeaf<T>) bytes and mValues starts at a known offset
|
||||
// inside each leaf. We bind this on NanoGrid<T> as leaf_values() for
|
||||
// efficient bulk analytics from Python.
|
||||
template<typename BuildT, typename = void>
|
||||
struct PyLeafValuesBinder
|
||||
{
|
||||
template<typename ClsT> static void apply(ClsT&) {}
|
||||
};
|
||||
|
||||
template<typename BuildT>
|
||||
struct PyLeafValuesBinder<BuildT,
|
||||
typename std::enable_if<
|
||||
std::is_arithmetic_v<typename nanovdb::NanoLeaf<BuildT>::ValueType>
|
||||
&& !nanovdb::BuildTraits<BuildT>::is_special>::type>
|
||||
{
|
||||
template<typename ClsT>
|
||||
static void apply(ClsT& cls)
|
||||
{
|
||||
using GridT = nanovdb::NanoGrid<BuildT>;
|
||||
using LeafT = nanovdb::NanoLeaf<BuildT>;
|
||||
using ValueT = typename LeafT::ValueType;
|
||||
cls.def("leaf_values",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& grid = nb::cast<GridT&>(py_self);
|
||||
const auto& tree = grid.tree();
|
||||
const uint32_t nLeaves = tree.template nodeCount<LeafT>();
|
||||
if (!grid.isBreadthFirst()) {
|
||||
throw nb::value_error(
|
||||
"leaf_values() requires a breadth-first grid "
|
||||
"layout; rebuild via "
|
||||
"nanovdb.tools.createNanoGrid(...).");
|
||||
}
|
||||
// For an empty grid (no leaves) we still return an ndarray
|
||||
// — shape (0, 512) — so callers can iterate / np.asarray()
|
||||
// / shape-test without branching on a None sentinel.
|
||||
LeafT* first = const_cast<LeafT*>(tree.getFirstLeaf());
|
||||
size_t shape[2] = {nLeaves, LeafT::voxelCount()};
|
||||
int64_t strides[2] = {
|
||||
static_cast<int64_t>(sizeof(LeafT) / sizeof(ValueT)),
|
||||
1
|
||||
};
|
||||
// first is non-null whenever nLeaves > 0; when nLeaves == 0
|
||||
// we pass a dummy non-null aligned pointer (the grid itself)
|
||||
// so nanobind has something to base the empty array on.
|
||||
// Nothing will be read since the leading shape is 0.
|
||||
void* data = (first != nullptr)
|
||||
? static_cast<void*>(first->data()->mValues)
|
||||
: static_cast<void*>(&grid);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, ValueT, nb::ndim<2>, nb::device::cpu>(
|
||||
data, size_t(2), shape, py_self, strides),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (N_leaves, 512) NumPy view of every leaf's "
|
||||
"values, in breadth-first leaf order. Available only for "
|
||||
"BuildTs whose leaf layout carries T mValues[512] (i.e. not "
|
||||
"Fp*, Index, Mask, bool, or Point) and only on breadth-first "
|
||||
"grids. Returns an empty (0, 512) array for grids with no "
|
||||
"leaves. The view keeps the grid alive.");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,547 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyVoxelBlockManager.h"
|
||||
|
||||
#include <nanobind/ndarray.h>
|
||||
#include <nanobind/stl/tuple.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/VoxelBlockManager.h>
|
||||
#include <nanovdb/util/Util.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
using nanovdb::tools::VoxelBlockManager;
|
||||
using nanovdb::tools::VoxelBlockManagerBase;
|
||||
using nanovdb::tools::VoxelBlockManagerHandle;
|
||||
using nanovdb::tools::buildVoxelBlockManager;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// ----------------------- Log2BlockWidth dispatch --------------------------
|
||||
//
|
||||
// Log2BlockWidth is a compile-time template parameter on every VBM helper.
|
||||
// We expose it to Python as a runtime int and dispatch via a switch that
|
||||
// instantiates the four useful widths (BlockWidth = 64, 128, 256, 512).
|
||||
// Larger widths are not bound by default; callers who need them can add a
|
||||
// new case below.
|
||||
|
||||
template<typename F>
|
||||
static auto dispatchLog2BlockWidth(int log2BlockWidth, F&& fn)
|
||||
{
|
||||
switch (log2BlockWidth) {
|
||||
case 6: return fn(std::integral_constant<int, 6>{});
|
||||
case 7: return fn(std::integral_constant<int, 7>{});
|
||||
case 8: return fn(std::integral_constant<int, 8>{});
|
||||
case 9: return fn(std::integral_constant<int, 9>{});
|
||||
default:
|
||||
throw nb::value_error(
|
||||
"VoxelBlockManager: log2_block_width must be 6, 7, 8, or 9 "
|
||||
"(BlockWidth = 64, 128, 256, or 512). Larger widths are not "
|
||||
"bound in Python by default.");
|
||||
}
|
||||
}
|
||||
|
||||
// PyVBMHandle wraps the C++ VoxelBlockManagerHandle and carries the
|
||||
// log2_block_width the handle was built with. The C++ handle does NOT store
|
||||
// log2_block_width itself, so without this wrapper the Python binding would
|
||||
// have to ask the caller every time — which the user can lie about and
|
||||
// trigger out-of-bounds reads of the metadata buffers. Storing it once at
|
||||
// build time and consulting it in every accessor closes that hole.
|
||||
struct PyVBMHandle
|
||||
{
|
||||
VoxelBlockManagerHandle<HostBuffer> handle;
|
||||
int log2BlockWidth = 6;
|
||||
|
||||
PyVBMHandle() = default;
|
||||
PyVBMHandle(VoxelBlockManagerHandle<HostBuffer>&& h, int lbw) noexcept
|
||||
: handle(std::move(h)), log2BlockWidth(lbw) {}
|
||||
|
||||
PyVBMHandle(const PyVBMHandle&) = delete;
|
||||
PyVBMHandle& operator=(const PyVBMHandle&) = delete;
|
||||
PyVBMHandle(PyVBMHandle&&) = default;
|
||||
PyVBMHandle& operator=(PyVBMHandle&&) = default;
|
||||
|
||||
uint64_t blockCount() const { return handle.blockCount(); }
|
||||
uint64_t firstOffset() const { return handle.firstOffset(); }
|
||||
uint64_t lastOffset() const { return handle.lastOffset(); }
|
||||
void reset() { handle.reset(); }
|
||||
int blockWidth() const { return 1 << log2BlockWidth; }
|
||||
int jumpMapLength() const { return 1 << (log2BlockWidth - 6); }
|
||||
};
|
||||
|
||||
// ----------------------- decodeInverseMaps helper -------------------------
|
||||
//
|
||||
// Common implementation used by both the free function and the
|
||||
// handle.decodeBlock(i) method. Allocates fresh leafIndex (uint32) and
|
||||
// voxelOffset (uint16) NumPy arrays of length BlockWidth and fills them.
|
||||
template<int Log2BlockWidth>
|
||||
static nb::object pyDecodeInverseMapsImpl(const NanoGrid<ValueOnIndex>& grid,
|
||||
uint32_t firstLeafID,
|
||||
const uint64_t* jumpMap,
|
||||
uint64_t blockFirstOffset)
|
||||
{
|
||||
constexpr int BlockWidth = 1 << Log2BlockWidth;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<Log2BlockWidth>::JumpMapLength;
|
||||
|
||||
// The C++ decodeInverseMaps iterates leafID = firstLeafID ..
|
||||
// firstLeafID + nExtraLeaves, where nExtraLeaves is the popcount of
|
||||
// this block's jumpMap (each set bit marks an additional leaf
|
||||
// boundary crossed within the block). If the jumpMap is corrupt or
|
||||
// was built against a different grid, the loop could read past
|
||||
// tree.getFirstNode<0>(). Pre-compute the upper bound and validate
|
||||
// it against grid.tree().nodeCount(0) before any allocation.
|
||||
uint32_t nExtraLeaves = 0;
|
||||
for (int i = 0; i < JumpMapLength; ++i)
|
||||
nExtraLeaves += util::countOn(jumpMap[i]);
|
||||
const uint32_t nLeaves = grid.tree().nodeCount(0);
|
||||
if (uint64_t(firstLeafID) + uint64_t(nExtraLeaves) >= nLeaves) {
|
||||
throw nb::value_error(
|
||||
"decodeInverseMaps: firstLeafID + popcount(jumpMap) would "
|
||||
"index past grid.tree().nodeCount(0) — the jumpMap is "
|
||||
"either corrupt or was paired with a different grid.");
|
||||
}
|
||||
|
||||
// Each call allocates fresh BlockWidth-sized output arrays for the
|
||||
// leaf-index and voxel-offset results. We use plain new[] (rather than
|
||||
// a numpy-allocated buffer) because the produced ndarrays are returned
|
||||
// by reference and Python owns them via the capsule deleters below —
|
||||
// when the ndarray is destroyed, the capsule's deleter runs delete[].
|
||||
//
|
||||
// The raw pointers live in std::unique_ptr until the matching capsule
|
||||
// has been constructed; that way if the second allocation, the
|
||||
// decodeInverseMaps call, or either capsule construction throws, the
|
||||
// unique_ptr unwinds the half-built state cleanly. After a capsule
|
||||
// takes ownership we release() so the unique_ptr no longer double-frees.
|
||||
std::unique_ptr<uint32_t[]> leafIndex(new uint32_t[BlockWidth]);
|
||||
std::unique_ptr<uint16_t[]> voxelOffset(new uint16_t[BlockWidth]);
|
||||
|
||||
using VBM = VoxelBlockManager<Log2BlockWidth>;
|
||||
{
|
||||
// Release the GIL around the pure-C++ decode kernel — the heavy part,
|
||||
// and the only part of this helper that touches no Python objects. The
|
||||
// GIL is re-acquired on scope exit (including during exception unwind)
|
||||
// before the capsules / ndarrays below are constructed.
|
||||
nb::gil_scoped_release release;
|
||||
VBM::template decodeInverseMaps<ValueOnIndex>(
|
||||
&grid, firstLeafID, jumpMap, blockFirstOffset,
|
||||
leafIndex.get(), voxelOffset.get());
|
||||
}
|
||||
|
||||
// nb::capsule wraps the raw pointer + matching delete[] so it can serve
|
||||
// as the ndarray's owner — the capsule lives as long as the ndarray and
|
||||
// its destruction runs the deleter.
|
||||
nb::capsule leafOwner(leafIndex.get(),
|
||||
[](void* p) noexcept { delete[] static_cast<uint32_t*>(p); });
|
||||
auto* leafRaw = leafIndex.release();
|
||||
nb::capsule offsetOwner(voxelOffset.get(),
|
||||
[](void* p) noexcept { delete[] static_cast<uint16_t*>(p); });
|
||||
auto* offsetRaw = voxelOffset.release();
|
||||
|
||||
size_t shape[1] = {static_cast<size_t>(BlockWidth)};
|
||||
nb::ndarray<nb::numpy, uint32_t, nb::ndim<1>, nb::c_contig, nb::device::cpu>
|
||||
leafArr(leafRaw, size_t(1), shape, leafOwner);
|
||||
nb::ndarray<nb::numpy, uint16_t, nb::ndim<1>, nb::c_contig, nb::device::cpu>
|
||||
offsetArr(offsetRaw, size_t(1), shape, offsetOwner);
|
||||
return nb::make_tuple(
|
||||
nb::cast(leafArr, nb::rv_policy::reference),
|
||||
nb::cast(offsetArr, nb::rv_policy::reference));
|
||||
}
|
||||
|
||||
// ------------------- VoxelBlockManagerHandle binding ----------------------
|
||||
|
||||
static const NanoGrid<ValueOnIndex>* castOnIndexGrid(nb::handle py_grid,
|
||||
const char* fn_name)
|
||||
{
|
||||
if (!nb::isinstance<NanoGrid<ValueOnIndex>>(py_grid)) {
|
||||
std::string msg(fn_name);
|
||||
msg += ": grid must be a NanoVDB grid of build type ValueOnIndex (OnIndexGrid)";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
return &nb::cast<const NanoGrid<ValueOnIndex>&>(py_grid);
|
||||
}
|
||||
|
||||
static void defineHandle(nb::module_& toolsModule)
|
||||
{
|
||||
nb::class_<PyVBMHandle>(toolsModule, "VoxelBlockManagerHandle",
|
||||
"Owns the firstLeafID / jumpMap metadata buffers backing a "
|
||||
"VoxelBlockManager. Constructed by nanovdb.tools.buildVoxelBlockManager.")
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty VoxelBlockManagerHandle with no buffers.")
|
||||
.def("blockCount", &PyVBMHandle::blockCount,
|
||||
"Number of voxel blocks managed by this handle.")
|
||||
.def("firstOffset", &PyVBMHandle::firstOffset,
|
||||
"Sequential voxel index of the first active voxel covered "
|
||||
"by this handle (1 by default when the handle covers the "
|
||||
"full grid).")
|
||||
.def("lastOffset", &PyVBMHandle::lastOffset,
|
||||
"Sequential voxel index of the last active voxel covered "
|
||||
"by this handle.")
|
||||
.def("reset", &PyVBMHandle::reset,
|
||||
"Release this handle's buffers and reset it to the empty state.")
|
||||
.def_prop_ro("log2_block_width", [](const PyVBMHandle& h) { return h.log2BlockWidth; },
|
||||
"The log2_block_width this handle was built with. The jumpMap "
|
||||
"and decodeBlock outputs derive their shapes from this value.")
|
||||
.def_prop_ro("block_width", &PyVBMHandle::blockWidth,
|
||||
"BlockWidth = 1 << log2_block_width (64, 128, 256, or 512).")
|
||||
.def_prop_ro("jump_map_length", &PyVBMHandle::jumpMapLength,
|
||||
"JumpMapLength = BlockWidth / 64 (1, 2, 4, or 8).")
|
||||
.def(
|
||||
"__bool__",
|
||||
[](const PyVBMHandle& h) { return h.blockCount() > 0; },
|
||||
nb::is_operator())
|
||||
// Zero-copy view of the (blockCount,) firstLeafID array.
|
||||
.def("firstLeafID",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& h = nb::cast<PyVBMHandle&>(py_self);
|
||||
size_t shape[1] = {static_cast<size_t>(h.blockCount())};
|
||||
// A default-constructed or reset() handle has a null
|
||||
// hostFirstLeafID(); we still return an empty (0,) ndarray
|
||||
// so callers don't have to branch on a None sentinel. The
|
||||
// dummy non-null pointer (the handle itself) keeps nanobind
|
||||
// happy; nothing is read since the leading shape is 0.
|
||||
uint32_t* raw = h.handle.hostFirstLeafID();
|
||||
void* data = (raw != nullptr) ? static_cast<void*>(raw)
|
||||
: static_cast<void*>(&h);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, uint32_t, nb::ndim<1>,
|
||||
nb::c_contig, nb::device::cpu>(
|
||||
data, size_t(1), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (blockCount,) uint32 NumPy view of the "
|
||||
"firstLeafID array. Returns an empty (0,) array on a "
|
||||
"default-constructed or reset() handle. The view keeps this "
|
||||
"handle alive.")
|
||||
// jumpMap is uint64_t[blockCount * JumpMapLength]. JumpMapLength is
|
||||
// determined by the log2_block_width recorded on the handle, not by
|
||||
// the caller — that way the returned view always covers exactly the
|
||||
// allocated buffer, with no risk of OOB reads.
|
||||
.def("jumpMap",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& h = nb::cast<PyVBMHandle&>(py_self);
|
||||
size_t shape[2] = {static_cast<size_t>(h.blockCount()),
|
||||
static_cast<size_t>(h.jumpMapLength())};
|
||||
// Same null-buffer guard as firstLeafID(): a
|
||||
// default-constructed / reset() handle has a null
|
||||
// hostJumpMap(); return an empty (0, jump_map_length)
|
||||
// ndarray rather than passing nullptr to nanobind.
|
||||
uint64_t* raw = h.handle.hostJumpMap();
|
||||
void* data = (raw != nullptr) ? static_cast<void*>(raw)
|
||||
: static_cast<void*>(&h);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, uint64_t, nb::ndim<2>,
|
||||
nb::c_contig, nb::device::cpu>(
|
||||
data, size_t(2), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (blockCount, jump_map_length) uint64 NumPy "
|
||||
"view of the jumpMap. The shape is determined by the "
|
||||
"log2_block_width the handle was built with. Returns an empty "
|
||||
"(0, jump_map_length) array on a default-constructed or reset() "
|
||||
"handle. The view keeps this handle alive.")
|
||||
// Decode the inverse maps for a single block of this VBM. The
|
||||
// log2_block_width is taken from the handle, so the caller cannot
|
||||
// request a width that doesn't match what was built.
|
||||
.def("decodeBlock",
|
||||
[](PyVBMHandle& self,
|
||||
nb::handle py_grid,
|
||||
uint64_t block_index) -> nb::object {
|
||||
const auto* grid = castOnIndexGrid(py_grid,
|
||||
"VoxelBlockManagerHandle.decodeBlock");
|
||||
if (block_index >= self.blockCount()) {
|
||||
throw nb::index_error(
|
||||
"VoxelBlockManagerHandle.decodeBlock(block_index): "
|
||||
"block_index out of range [0, blockCount).");
|
||||
}
|
||||
// Defensive: NanoVDB's buildVoxelBlockManager doesn't always
|
||||
// initialize firstLeafID for blocks where no leaf starts at
|
||||
// a block boundary AND no leaf's iteration sweep reaches
|
||||
// them (e.g. when the source grid is tile-compressed, so
|
||||
// some sequential offsets correspond to tile values rather
|
||||
// than leaf voxels). The slot is then uninitialized memory;
|
||||
// passing it into decodeInverseMaps would lead to an OOB
|
||||
// read of tree.getFirstNode<0>()[garbage]. Catch the case
|
||||
// and raise rather than segfault.
|
||||
const uint32_t firstLeafID =
|
||||
self.handle.hostFirstLeafID()[block_index];
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
if (firstLeafID >= nLeaves) {
|
||||
throw nb::value_error(
|
||||
"VoxelBlockManagerHandle.decodeBlock: the VBM's "
|
||||
"firstLeafID for this block was not initialized by "
|
||||
"buildVoxelBlockManager (the underlying algorithm "
|
||||
"doesn't cover blocks that no leaf reaches via its "
|
||||
"iteration). This typically happens on OnIndex "
|
||||
"grids built from tile-compressed source grids; "
|
||||
"until the issue is fixed upstream the workaround "
|
||||
"is to build the source grid voxel-by-voxel with "
|
||||
"build::Grid so it stays uncompressed.");
|
||||
}
|
||||
return dispatchLog2BlockWidth(self.log2BlockWidth, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
constexpr int BlockWidth = 1 << LBW;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<LBW>::JumpMapLength;
|
||||
const uint64_t blockFirstOffset =
|
||||
self.firstOffset() + block_index * BlockWidth;
|
||||
return pyDecodeInverseMapsImpl<LBW>(
|
||||
*grid,
|
||||
firstLeafID,
|
||||
self.handle.hostJumpMap() + block_index * JumpMapLength,
|
||||
blockFirstOffset);
|
||||
});
|
||||
},
|
||||
"grid"_a, "block_index"_a,
|
||||
"Decode the inverse maps for the block_index-th block of this "
|
||||
"VBM. Returns (leaf_index, voxel_offset) uint32 / uint16 NumPy "
|
||||
"arrays of length BlockWidth = 1<<log2_block_width, using the "
|
||||
"log2_block_width the handle was built with.");
|
||||
}
|
||||
|
||||
// ------------------- buildVoxelBlockManager binding -----------------------
|
||||
|
||||
static void defineBuild(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("buildVoxelBlockManager",
|
||||
[](nb::handle py_grid,
|
||||
int log2_block_width,
|
||||
uint64_t first_offset,
|
||||
uint64_t last_offset,
|
||||
uint64_t n_blocks) -> PyVBMHandle {
|
||||
const auto* grid = castOnIndexGrid(py_grid, "buildVoxelBlockManager");
|
||||
// The C++ implementation only NANOVDB_ASSERTs these preconditions,
|
||||
// which makes them no-ops in release builds. Validate them here
|
||||
// so Python callers get a clear error instead of UB / abort.
|
||||
if (!grid->isSequential()) {
|
||||
throw nb::value_error(
|
||||
"buildVoxelBlockManager: grid must satisfy "
|
||||
"grid.isSequential() (fixed-size, breadth-first node "
|
||||
"layout). NanoVDB grids constructed via "
|
||||
"tools.createOnIndexGrid satisfy this by default.");
|
||||
}
|
||||
return dispatchLog2BlockWidth(log2_block_width, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
using Base = VoxelBlockManagerBase<LBW>;
|
||||
constexpr uint64_t BlockWidth = Base::BlockWidth;
|
||||
constexpr uint64_t JumpMapLength = Base::JumpMapLength;
|
||||
// first_offset must be 1 (mod BlockWidth). The single-arg
|
||||
// C++ helper would normalize a zero input to 1; we do the
|
||||
// same here so the in-place builder below sees a valid
|
||||
// value. Validate the nonzero case ourselves.
|
||||
if (first_offset != 0 &&
|
||||
((first_offset - 1) & (BlockWidth - 1)) != 0) {
|
||||
throw nb::value_error(
|
||||
"buildVoxelBlockManager: first_offset must satisfy "
|
||||
"first_offset == 1 (mod BlockWidth). Pass 0 (the "
|
||||
"default) to let the implementation use 1.");
|
||||
}
|
||||
if (first_offset == 0) first_offset = 1;
|
||||
if (last_offset == 0) last_offset = grid->activeVoxelCount();
|
||||
if (last_offset < first_offset) return PyVBMHandle();
|
||||
// Capacity must hold at least ceil((last - first + 1) /
|
||||
// BlockWidth) blocks; otherwise the handle's lastOffset
|
||||
// would advertise more coverage than blockCount allows
|
||||
// and decodeBlock would silently truncate. The formula
|
||||
// below equals the ceil() above when BlockWidth is a
|
||||
// power of two.
|
||||
const uint64_t minBlocks =
|
||||
(last_offset - first_offset + BlockWidth) >> LBW;
|
||||
if (n_blocks != 0 && n_blocks < minBlocks) {
|
||||
std::string msg(
|
||||
"buildVoxelBlockManager: n_blocks must be at "
|
||||
"least ceil((last_offset - first_offset + 1) / "
|
||||
"BlockWidth) = ");
|
||||
msg += std::to_string(minBlocks);
|
||||
msg += ". Pass 0 (the default) to use the minimum "
|
||||
"required capacity.";
|
||||
throw nb::value_error(msg.c_str());
|
||||
}
|
||||
if (n_blocks == 0) n_blocks = minBlocks;
|
||||
// Allocate the metadata buffers ourselves so we can
|
||||
// pre-initialize firstLeafID with a sentinel value before
|
||||
// the in-place builder runs. The C++ allocating overload
|
||||
// calls HostBuffer::create() which returns uninitialized
|
||||
// memory; blocks that the algorithm doesn't touch would
|
||||
// then retain arbitrary values, and our decodeBlock guard
|
||||
// (firstLeafID >= nLeaves) might miss any garbage value
|
||||
// that happens to be < nLeaves. By prefilling with nLeaves
|
||||
// up front, every untouched slot deterministically trips
|
||||
// the guard.
|
||||
auto firstLeafIDBuf = HostBuffer::create(
|
||||
n_blocks * sizeof(uint32_t));
|
||||
auto jumpMapBuf = HostBuffer::create(
|
||||
n_blocks * JumpMapLength * sizeof(uint64_t));
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
{
|
||||
uint32_t* slots = static_cast<uint32_t*>(
|
||||
firstLeafIDBuf.data());
|
||||
for (uint64_t i = 0; i < n_blocks; ++i) {
|
||||
slots[i] = nLeaves;
|
||||
}
|
||||
}
|
||||
VoxelBlockManagerHandle<HostBuffer> handle(
|
||||
std::move(firstLeafIDBuf), std::move(jumpMapBuf),
|
||||
n_blocks, first_offset, last_offset);
|
||||
// In-place builder zeros the jumpMap itself and only
|
||||
// touches firstLeafID slots it actually visits. Release the
|
||||
// GIL around it — it's pure C++ (touches no Python objects)
|
||||
// and may parallelize internally via util::forEach.
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
buildVoxelBlockManager<LBW, HostBuffer>(grid, handle);
|
||||
}
|
||||
return PyVBMHandle(std::move(handle), LBW);
|
||||
});
|
||||
},
|
||||
"grid"_a,
|
||||
"log2_block_width"_a = 6,
|
||||
"first_offset"_a = 0,
|
||||
"last_offset"_a = 0,
|
||||
"n_blocks"_a = 0,
|
||||
"Build a host-side VoxelBlockManager from an OnIndexGrid. "
|
||||
"log2_block_width selects the per-block active-voxel count "
|
||||
"(6=64, 7=128, 8=256, 9=512). Pass 0 for first_offset / "
|
||||
"last_offset / n_blocks to use the full grid (first active "
|
||||
"voxel through grid.activeVoxelCount(), minimum block count). "
|
||||
"first_offset, if nonzero, must satisfy first_offset == 1 "
|
||||
"(mod BlockWidth).");
|
||||
}
|
||||
|
||||
// --------------------- decodeInverseMaps binding --------------------------
|
||||
|
||||
static void defineDecode(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("decodeInverseMaps",
|
||||
[](nb::handle py_grid,
|
||||
uint32_t first_leaf_id,
|
||||
nb::ndarray<const uint64_t, nb::ndim<1>,
|
||||
nb::c_contig, nb::device::cpu> jump_map,
|
||||
uint64_t block_first_offset,
|
||||
int log2_block_width) -> nb::object {
|
||||
const auto* grid = castOnIndexGrid(py_grid, "decodeInverseMaps");
|
||||
// The C++ helper indexes tree.getFirstNode<0>()[first_leaf_id]
|
||||
// without a bounds check, so a stray first_leaf_id leads to an
|
||||
// OOB read. Validate up front. (We also require isSequential();
|
||||
// getFirstNode only makes sense on a sequential tree.)
|
||||
if (!grid->isSequential()) {
|
||||
throw nb::value_error(
|
||||
"decodeInverseMaps: grid must satisfy "
|
||||
"grid.isSequential().");
|
||||
}
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
if (first_leaf_id >= nLeaves) {
|
||||
throw nb::index_error(
|
||||
"decodeInverseMaps: first_leaf_id out of range "
|
||||
"[0, grid.tree().nodeCount(0)).");
|
||||
}
|
||||
return dispatchLog2BlockWidth(log2_block_width, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<LBW>::JumpMapLength;
|
||||
if (jump_map.shape(0) != JumpMapLength) {
|
||||
std::string msg("decodeInverseMaps: jump_map must have "
|
||||
"length JumpMapLength = ");
|
||||
msg += std::to_string(JumpMapLength);
|
||||
msg += " for log2_block_width=";
|
||||
msg += std::to_string(LBW);
|
||||
throw nb::value_error(msg.c_str());
|
||||
}
|
||||
return pyDecodeInverseMapsImpl<LBW>(
|
||||
*grid, first_leaf_id, jump_map.data(),
|
||||
block_first_offset);
|
||||
});
|
||||
},
|
||||
"grid"_a,
|
||||
"first_leaf_id"_a,
|
||||
"jump_map"_a,
|
||||
"block_first_offset"_a,
|
||||
"log2_block_width"_a = 6,
|
||||
"Decode the inverse maps for a single voxel block of an OnIndexGrid. "
|
||||
"Returns a (leaf_index, voxel_offset) tuple of fresh NumPy arrays of "
|
||||
"length BlockWidth = 1<<log2_block_width. jump_map must have length "
|
||||
"BlockWidth/64. first_leaf_id must be in [0, grid.tree().nodeCount(0)).");
|
||||
}
|
||||
|
||||
// ----- createOnIndexGrid test-scaffold factory ----------------------------
|
||||
//
|
||||
// Narrow source-coverage factory used by the VoxelBlockManager unit tests.
|
||||
// New code should prefer tools.createNanoGridOnIndex (in PyCreateNanoGrid.cc)
|
||||
// which accepts a wider source set.
|
||||
|
||||
template<typename SrcBuildT>
|
||||
static nb::object tryCreateOnIndexGrid(nb::handle py_grid,
|
||||
uint32_t channels,
|
||||
bool include_stats,
|
||||
bool include_tiles,
|
||||
int verbose)
|
||||
{
|
||||
using SrcGridT = NanoGrid<SrcBuildT>;
|
||||
if (!nb::isinstance<SrcGridT>(py_grid)) {
|
||||
return nb::object();
|
||||
}
|
||||
const SrcGridT& src = nb::cast<const SrcGridT&>(py_grid);
|
||||
return nb::cast(
|
||||
tools::createNanoGrid<SrcGridT, ValueOnIndex, HostBuffer>(
|
||||
src, channels, include_stats, include_tiles, verbose));
|
||||
}
|
||||
|
||||
static void defineCreateOnIndexGrid(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("createOnIndexGrid",
|
||||
[](nb::handle py_grid,
|
||||
uint32_t channels,
|
||||
bool include_stats,
|
||||
bool include_tiles,
|
||||
int verbose) -> nb::object {
|
||||
// Try every source BuildT we accept.
|
||||
if (auto r = tryCreateOnIndexGrid<float>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<double>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<int32_t>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<Vec3f>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
throw nb::type_error(
|
||||
"createOnIndexGrid: source grid must be a FloatGrid, "
|
||||
"DoubleGrid, Int32Grid, or Vec3fGrid (other source BuildTs "
|
||||
"are not yet bound).");
|
||||
},
|
||||
"src_grid"_a,
|
||||
"channels"_a = 0u,
|
||||
"include_stats"_a = true,
|
||||
"include_tiles"_a = true,
|
||||
"verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueOnIndex> "
|
||||
"(OnIndexGrid). Accepts FloatGrid / DoubleGrid / Int32Grid / "
|
||||
"Vec3fGrid. This is a narrow helper kept alongside "
|
||||
"buildVoxelBlockManager; for general index conversion (broader "
|
||||
"source coverage, blind-data channels) prefer "
|
||||
"nanovdb.tools.createNanoGridOnIndex.");
|
||||
}
|
||||
|
||||
void defineVoxelBlockManagerModule(nb::module_& toolsModule)
|
||||
{
|
||||
defineHandle(toolsModule);
|
||||
defineBuild(toolsModule);
|
||||
defineDecode(toolsModule);
|
||||
defineCreateOnIndexGrid(toolsModule);
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYVOXELBLOCKMANAGER_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYVOXELBLOCKMANAGER_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
/// @brief Bind VoxelBlockManagerHandle<HostBuffer>,
|
||||
/// tools.buildVoxelBlockManager, tools.decodeInverseMaps, and the
|
||||
/// test-scaffold tools.createOnIndexGrid factory under the given
|
||||
/// Python submodule (expected to be the existing nanovdb.tools).
|
||||
void defineVoxelBlockManagerModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -1,9 +1,26 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import os
|
||||
import sys
|
||||
if sys.platform == "win32":
|
||||
import os
|
||||
openvdb_dll_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'openvdb', 'lib')
|
||||
os.add_dll_directory(directory)
|
||||
|
||||
from .lib.nanovdb import *
|
||||
if sys.platform == "win32":
|
||||
_openvdb_dll_directory = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), os.pardir, "openvdb", "lib"
|
||||
)
|
||||
if os.path.isdir(_openvdb_dll_directory):
|
||||
os.add_dll_directory(_openvdb_dll_directory)
|
||||
|
||||
|
||||
def get_include():
|
||||
"""Return the absolute path to the bundled NanoVDB C/C++ include directory.
|
||||
|
||||
Use this from a downstream Python extension's build system so the extension
|
||||
compiles against the same NanoVDB headers the installed wheel was built with::
|
||||
|
||||
import nanovdb
|
||||
ext_kwargs = dict(include_dirs=[nanovdb.get_include()])
|
||||
"""
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "include")
|
||||
|
||||
|
||||
from .lib.nanovdb import * # noqa: E402,F401,F403
|
||||
|
||||
@@ -13,7 +13,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineDeviceBuffer(nb::module_& m)
|
||||
{
|
||||
nb::class_<cuda::DeviceBuffer>(m, "DeviceBuffer");
|
||||
nb::class_<cuda::DeviceBuffer>(m, "DeviceBuffer",
|
||||
"CUDA device-side buffer used to back a DeviceGridHandle. Holds a "
|
||||
"host mirror and a device pointer; deviceUpload / deviceDownload on "
|
||||
"the handle move bytes between the two.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <nanobind/ndarray.h>
|
||||
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -13,6 +14,48 @@ using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Device-side polymorphic deviceGrid(n) — same dispatch shape as
|
||||
// pyHostGrid<BufferT> in PyGridHandle.h, but returns the device pointer.
|
||||
// gridType(n) is read from the host-side GridData header (the handle keeps
|
||||
// a host mirror), so this works whether or not the grid has been uploaded.
|
||||
// Returns None if the device-side grid is null (i.e. no deviceUpload yet)
|
||||
// or the BuildT is not Python-visible.
|
||||
static nb::object pyDeviceGrid(nb::handle py_handle, uint32_t n)
|
||||
{
|
||||
using BufferT = nanovdb::cuda::DeviceBuffer;
|
||||
auto& handle = nb::cast<GridHandle<BufferT>&>(py_handle);
|
||||
if (n >= handle.gridCount()) return nb::none();
|
||||
switch (handle.gridType(n)) {
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#include "../BuildTypes.def"
|
||||
default:
|
||||
return nb::none();
|
||||
}
|
||||
}
|
||||
|
||||
void defineDeviceGridHandle(nb::module_& m)
|
||||
{
|
||||
using BufferT = nanovdb::cuda::DeviceBuffer;
|
||||
@@ -27,19 +70,31 @@ void defineDeviceGridHandle(nb::module_& m)
|
||||
new (&handle) GridHandle<BufferT>(std::move(buffer));
|
||||
},
|
||||
"cpu_t"_a.noconvert(),
|
||||
"cuda_t"_a.noconvert())
|
||||
.def("deviceFloatGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<float>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceDoubleGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<double>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceInt32Grid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<int32_t>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceVec3fGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<Vec3f>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceRGBA8Grid",
|
||||
nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<math::Rgba8>),
|
||||
"n"_a = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
"cuda_t"_a.noconvert(),
|
||||
"Construct a DeviceGridHandle that wraps an existing pair of "
|
||||
"host and device uint32 arrays of equal length.")
|
||||
.def("deviceGrid", &pyDeviceGrid, "n"_a = 0,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the n-th device-resident grid as a typed Grid subclass "
|
||||
"selected by gridType(n), or None if the BuildT is not bound in "
|
||||
"Python or the device copy has not been uploaded yet. The "
|
||||
"returned grid keeps this handle alive.")
|
||||
.def(
|
||||
"deviceUpload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceUpload(nullptr, sync); }, "sync"_a = true)
|
||||
"deviceUpload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceUpload(nullptr, sync); }, "sync"_a = true,
|
||||
"Copy the host-side buffer to the device. If sync is True the "
|
||||
"call blocks until the transfer completes.")
|
||||
.def(
|
||||
"deviceDownload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceDownload(nullptr, sync); }, "sync"_a = true);
|
||||
"deviceDownload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceDownload(nullptr, sync); }, "sync"_a = true,
|
||||
"Copy the device-side buffer back to the host. If sync is True "
|
||||
"the call blocks until the transfer completes.");
|
||||
// NOTE: defineGridHandleUtilities<BufferT> intentionally NOT called for
|
||||
// DeviceBuffer. Registering nanovdb.splitGrids / nanovdb.mergeGrids as a
|
||||
// second overload taking a DeviceGridHandle list conflicts with the host
|
||||
// overload because both signatures take nb::list, and nanobind's
|
||||
// overload resolution can't disambiguate by element type — it picks one
|
||||
// and the inner cast fails with std::bad_cast. A properly typed device
|
||||
// variant (with its own name, or strongly-typed std::vector<HandleT>
|
||||
// args via nanobind/stl/vector.h) can land later if it's needed.
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -50,7 +50,8 @@ template<typename BuildT> void definePointsToGrid(nb::module_& m, const char* na
|
||||
auto handle = converter.getHandle(points, tensor.shape(0));
|
||||
return handle;
|
||||
},
|
||||
"tensor"_a);
|
||||
"tensor"_a,
|
||||
"Rasterize the given (N, 3) int32 device tensor of points into a fresh GridHandle.");
|
||||
}
|
||||
|
||||
template void definePointsToGrid<nanovdb::math::Rgba8>(nb::module_&, const char*);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# NanoVDB Python Examples
|
||||
|
||||
Runnable `.py` scripts demonstrating the NanoVDB Python bindings.
|
||||
Each script is self-contained, builds its own input data, and prints
|
||||
a small summary to stdout.
|
||||
|
||||
Run any example with:
|
||||
|
||||
```bash
|
||||
python load_inspect.py
|
||||
```
|
||||
|
||||
When working from the source tree, the `nanovdb` module needs to be
|
||||
on `PYTHONPATH` (e.g. via the build output directory):
|
||||
|
||||
```bash
|
||||
cd <build>/nanovdb/nanovdb/python
|
||||
PYTHONPATH=. python /path/to/<example>.py
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
| Script | What it shows |
|
||||
| --- | --- |
|
||||
| [`load_inspect.py`](load_inspect.py) | Polymorphic `handle.grid(n)` access, `GridMetaData` type-erased introspection, mixed-type handles via `mergeGrids`. |
|
||||
| [`build_grid.py`](build_grid.py) | Voxel-by-voxel construction with `nanovdb.tools.build.FloatGrid`, the cached `ValueAccessor`, the thread-safe `WriteAccessor`, and `.to_nanovdb()` to bake into a host `GridHandle`. |
|
||||
| [`bulk_leaf_numpy.py`](bulk_leaf_numpy.py) | Zero-copy `(N_leaves, 512)` NumPy view of every leaf's mValues via `grid.leaf_values()`. Includes a global-stats reduction and an in-place mutation that propagates back into the grid. Requires NumPy. |
|
||||
| [`quantize.py`](quantize.py) | Quantize a `NanoGrid<float>` through `nanovdb.tools.createNanoGridFp{4,8,16,N}`. Shows fixed-width quantization with dithering and variable-width `FpN` with both `AbsDiff` and `RelDiff` oracles. |
|
||||
| [`validate.py`](validate.py) | `nanovdb.tools.validateGrid` / `validateGrids`, `checkGrid`, `isValid`, and the `evalChecksum` / `validateChecksum` / `updateChecksum` round-trip. |
|
||||
| [`io_roundtrip.py`](io_roundtrip.py) | `nanovdb.io` write/read round-trip over five primitives: `writeGrids` with codec fallback, `readGridMetaData`, `hasGrid`, `readGrid` by name, `splitGrids`, and zero-copy point positions via `getBlindData`. Port of `ex_write_nanovdb_grids` + `ex_read_nanovdb_sphere_accessor`. |
|
||||
| [`make_funny_nanovdb.py`](make_funny_nanovdb.py) | Functor-based construction: `tools.createFloatGrid(background, name, gridClass, func, bbox)` evaluates a Python callback at every voxel. Port of `ex_make_funny_nanovdb` on a reduced domain. |
|
||||
| [`make_typed_grids.py`](make_typed_grids.py) | One solid sphere per value type via `tools.build.{Float,Double,Int16,Int32,Int64,UInt32,Vec3f}Grid`, written to a single file and re-read with polymorphic `handle.grid()` dispatch. Port of `ex_make_typed_grids`. |
|
||||
| [`raytrace_level_set.py`](raytrace_level_set.py) | CPU sphere-traced level-set render to PGM using `worldToIndex`, the trilinear sampler, `zeroCrossing()`, and normalized `gradient()` shading. Port of `ex_raytrace_level_set` (host path). |
|
||||
| [`raytrace_fog_volume.py`](raytrace_fog_volume.py) | CPU transmittance ray-march of a fog volume to PGM using a `ReadAccessor` and `Coord.Floor` — the accessor-based sampling idiom. Port of `ex_raytrace_fog_volume` (host path). |
|
||||
| [`collide_level_set.py`](collide_level_set.py) | Particles colliding with a level set: `worldToIndexF`, `tree.isActive` narrow-band test, accessor distance reads, and `sampler.gradient()` collision normals. Port of `ex_collide_level_set` (host path). |
|
||||
| [`index_grid_channels.py`](index_grid_channels.py) | `tools.createNanoGridOnIndex(src, channels=1)`, `grid.valueCount()`, coordinate reads through `createChannelAccessor`, and blind-data authoring with `tools.CreateNanoGrid.addBlindData` + the writable `getBlindData` view. Extends the host half of `ex_index_grid_cuda`. Requires NumPy for the authoring section. |
|
||||
| [`node_manager.py`](node_manager.py) | Linearized node iteration with `createNodeManager`: per-level counts, `leaf(i)` / `lower(i)` access, node origins, masks, and stats. Port of the host half of `ex_nodemanager_cuda`. |
|
||||
| [`openvdb_interop.py`](openvdb_interop.py) | `tools.openToNanoVDB` / `nanoToOpenVDB` round-trip with accessor comparison on both sides. Self-skips unless built with `NANOVDB_USE_OPENVDB` and `openvdb` is importable. Port of `ex_openvdb_to_nanovdb_accessor`. |
|
||||
|
||||
Scripts that produce files write them to a fresh temporary directory
|
||||
and print its path, so the source tree stays clean. The whole set is
|
||||
smoke-tested by [`../test/TestExamples.py`](../test/TestExamples.py)
|
||||
(ctest name `pytest_nanovdb_examples`) when the module is configured
|
||||
with `NANOVDB_BUILD_PYTHON_UNITTESTS=ON`.
|
||||
|
||||
For full API signatures and per-argument docstrings, use Python's
|
||||
`help()` on any symbol — e.g. `help(nanovdb.tools.createNanoGridFpN)`.
|
||||
The bindings ship `.pyi` type stubs for IDE / type-checker support.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Construct a NanoVDB grid voxel-by-voxel in pure Python.
|
||||
|
||||
nanovdb.tools.build.<Suffix>Grid is the mutable CPU grid builder
|
||||
that mirrors nanovdb::tools::build::Grid<T>. This example shows the
|
||||
three construction loops you'll typically reach for (setValue
|
||||
directly, the cached ValueAccessor, and the thread-safe
|
||||
WriteAccessor), then bakes each build grid into a host NanoGrid via
|
||||
.to_nanovdb() and reads it back through the regular polymorphic
|
||||
handle.grid() API.
|
||||
|
||||
Run with: python build_grid.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def fill_with_setValue():
|
||||
"""Simplest path: setValue() directly on the build grid."""
|
||||
g = nanovdb.tools.build.FloatGrid(
|
||||
background=0.0, name="setValue_demo",
|
||||
gridClass=nanovdb.GridClass.FogVolume)
|
||||
# Plant five active voxels along the x axis.
|
||||
for i in range(5):
|
||||
g.setValue(nanovdb.math.Coord(i, 0, 0), float(i + 1))
|
||||
return g
|
||||
|
||||
|
||||
def fill_with_accessor():
|
||||
"""Cached path: getAccessor() avoids re-walking the tree for
|
||||
each write when consecutive coordinates share a leaf."""
|
||||
g = nanovdb.tools.build.FloatGrid(background=0.0, name="accessor_demo")
|
||||
acc = g.getAccessor()
|
||||
# The accessor caches the last leaf / lower / upper node, so a
|
||||
# burst of writes in a 16^3 neighborhood only walks the tree once.
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
for z in range(8):
|
||||
if x + y + z == 7:
|
||||
acc.setValue(nanovdb.math.Coord(x, y, z),
|
||||
float(x * 64 + y * 8 + z))
|
||||
return g
|
||||
|
||||
|
||||
def fill_with_write_accessor():
|
||||
"""Thread-safe path: getWriteAccessor() buffers writes into a
|
||||
private root and merges them into the parent on destruction or
|
||||
on an explicit .merge() call. Useful when fanning out to multiple
|
||||
threads — one WriteAccessor per thread, no shared mutable state."""
|
||||
g = nanovdb.tools.build.FloatGrid(background=0.0, name="write_accessor_demo")
|
||||
wa = g.getWriteAccessor()
|
||||
wa.setValue(nanovdb.math.Coord(50, 50, 50), 9.0)
|
||||
# Before merge, the parent doesn't see the change yet.
|
||||
assert g.getValue(nanovdb.math.Coord(50, 50, 50)) == 0.0
|
||||
wa.merge()
|
||||
assert g.getValue(nanovdb.math.Coord(50, 50, 50)) == 9.0
|
||||
return g
|
||||
|
||||
|
||||
def main():
|
||||
for builder in (fill_with_setValue, fill_with_accessor,
|
||||
fill_with_write_accessor):
|
||||
g = builder()
|
||||
print(f"=== {g.getName()} ===")
|
||||
print(f" nodeCount (leaf, lower, upper) = {g.nodeCount()}")
|
||||
|
||||
# Bake into a host NanoGrid<float>.
|
||||
handle = g.to_nanovdb(sMode=nanovdb.tools.StatsMode.All)
|
||||
ng = handle.grid()
|
||||
print(f" baked NanoGrid: type={ng.gridType()}, "
|
||||
f"active={ng.activeVoxelCount()}, "
|
||||
f"worldBBox={ng.worldBBox()}")
|
||||
|
||||
# The build grid is left untouched — we can bake again.
|
||||
handle2 = g.to_nanovdb()
|
||||
assert handle2.grid().activeVoxelCount() == ng.activeVoxelCount()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Bulk per-leaf value access as a zero-copy NumPy array.
|
||||
|
||||
grid.leaf_values() is the highest-bandwidth path from NanoVDB into
|
||||
NumPy. It returns an (N_leaves, 512) view of every leaf's mValues
|
||||
without copying — modify it, slice it, feed it into a PyTorch tensor,
|
||||
hash it for cache lookup, whatever you need.
|
||||
|
||||
Run with: python bulk_leaf_numpy.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("This example requires numpy. Install it with: pip install numpy")
|
||||
return
|
||||
|
||||
# Build a fog volume sphere with stats so the leaves have meaningful
|
||||
# min/max attached (just for the printing below — not required by
|
||||
# leaf_values itself).
|
||||
handle = nanovdb.tools.createFogVolumeSphere(
|
||||
radius=20.0, name="bulk_demo")
|
||||
grid = handle.grid()
|
||||
print(f"Grid: {grid.gridType()}, active voxels = {grid.activeVoxelCount()}, "
|
||||
f"leaves = {grid.tree().nodeCount(0)}")
|
||||
|
||||
# leaf_values() is the zero-copy view. Modifying it modifies the grid.
|
||||
bulk = grid.leaf_values()
|
||||
# np.asarray adds a NumPy wrapper but doesn't copy.
|
||||
arr = np.asarray(bulk)
|
||||
print(f"leaf_values: shape={arr.shape}, dtype={arr.dtype}, "
|
||||
f"backed by grid memory (no copy).")
|
||||
|
||||
# Global statistics across every voxel in every leaf, computed in C.
|
||||
# 0.0 voxels (background) are excluded by using a mask.
|
||||
nonzero = arr[arr != 0.0]
|
||||
print(f" non-background voxels = {nonzero.size}")
|
||||
print(f" min = {nonzero.min()}, max = {nonzero.max()}, "
|
||||
f"mean = {nonzero.mean()}")
|
||||
|
||||
# Per-leaf reductions: each row of `arr` is one leaf's 512 voxels.
|
||||
per_leaf_max = arr.max(axis=1)
|
||||
print(f" per-leaf max (first 5): {per_leaf_max[:5]}")
|
||||
|
||||
# Zero-copy means writes propagate. Zero out the first leaf's values
|
||||
# and read one back through the regular accessor to confirm the grid
|
||||
# actually changed. (The active *mask* is unchanged — we wrote into
|
||||
# mValues only — so activeVoxelCount() stays the same.)
|
||||
arr[0] = 0.0
|
||||
leaf = grid.tree().getFirstLeaf()
|
||||
if leaf is not None:
|
||||
first_value_after = leaf.getFirstValue()
|
||||
print(f" zeroed first leaf's values in place: "
|
||||
f"leaf.getFirstValue() = {first_value_after}, "
|
||||
f"activeVoxelCount unchanged: {grid.activeVoxelCount()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Collide falling particles against a level-set surface on the CPU.
|
||||
|
||||
Python port of the host path of ex_collide_level_set: particles fall
|
||||
under gravity; each step transforms the candidate position to index
|
||||
space with grid.worldToIndexF, tests the narrow band with
|
||||
tree.isActive, reads the signed distance through a ReadAccessor, and
|
||||
on penetration reflects the velocity about the surface normal. The C++
|
||||
original builds the normal from 6-tap finite differences of the SDF;
|
||||
here the bound sampler.gradient() provides the same quantity in one
|
||||
call (index-space units — normalized before use).
|
||||
|
||||
Run with: python collide_level_set.py
|
||||
"""
|
||||
import random
|
||||
|
||||
import nanovdb
|
||||
|
||||
NUM_PARTICLES = 500
|
||||
NUM_STEPS = 40
|
||||
DT = 0.1
|
||||
GRAVITY = -9.8
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=100.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
tree = grid.tree()
|
||||
acc = grid.getAccessor()
|
||||
sampler = nanovdb.math.createTrilinearSampler(grid)
|
||||
|
||||
# Seed particles above the north pole of the sphere, falling down.
|
||||
rng = random.Random(42)
|
||||
particles = []
|
||||
for _ in range(NUM_PARTICLES):
|
||||
p = [rng.uniform(-30.0, 30.0), rng.uniform(115.0, 140.0),
|
||||
rng.uniform(-30.0, 30.0)]
|
||||
particles.append((p, [0.0, -20.0, 0.0]))
|
||||
|
||||
total_collisions = 0
|
||||
for step in range(NUM_STEPS):
|
||||
collisions = 0
|
||||
for p, v in particles:
|
||||
v[1] += GRAVITY * DT
|
||||
next_p = [p[i] + v[i] * DT for i in range(3)]
|
||||
|
||||
ijk = nanovdb.math.Coord.Floor(grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(next_p[0], next_p[1], next_p[2])))
|
||||
if tree.isActive(ijk): # inside the narrow band?
|
||||
d = acc.getValue(ijk)
|
||||
if d <= 0.0: # inside the level set?
|
||||
ipos = grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(next_p[0], next_p[1], next_p[2]))
|
||||
n = sampler.gradient(ipos)
|
||||
n.normalize()
|
||||
# Project the position back to the surface and
|
||||
# reflect the velocity, as in the C++ example.
|
||||
for i in range(3):
|
||||
next_p[i] -= d * n[i]
|
||||
v_dot_n = sum(v[i] * n[i] for i in range(3))
|
||||
for i in range(3):
|
||||
v[i] -= 2.0 * v_dot_n * n[i]
|
||||
collisions += 1
|
||||
p[:] = next_p
|
||||
total_collisions += collisions
|
||||
if collisions:
|
||||
print(f"step {step:2d}: {collisions} collisions")
|
||||
|
||||
print(f"total collisions over {NUM_STEPS} steps: {total_collisions}")
|
||||
assert total_collisions > 0
|
||||
|
||||
# No particle should end up inside the surface.
|
||||
worst = 0.0
|
||||
for p, _ in particles:
|
||||
ijk = nanovdb.math.Coord.Floor(grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(p[0], p[1], p[2])))
|
||||
if tree.isActive(ijk):
|
||||
worst = min(worst, acc.getValue(ijk))
|
||||
print(f"deepest final penetration: {worst:.3f} world units")
|
||||
assert worst > -2.0 * grid.voxelSize()[0]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Index grids, value channels, and blind-data authoring.
|
||||
|
||||
Part A ports the host half of ex_index_grid_cuda: a float level set is
|
||||
re-encoded as a NanoGrid<ValueOnIndex> whose voxels store sequential
|
||||
uint64 indices, with the original float values copied into a blind-data
|
||||
channel — then read back by coordinate through a ChannelAccessor,
|
||||
which the C++ example only does in a CUDA kernel. Part B authors a
|
||||
brand-new blind-data channel on a grid with tools.CreateNanoGrid and
|
||||
fills it in place through the writable getBlindData() NumPy view.
|
||||
|
||||
Run with: python index_grid_channels.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def index_grid_with_channel():
|
||||
src = nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere")
|
||||
src_grid = src.grid()
|
||||
|
||||
# channels=1 copies the source values into blind-data channel 0,
|
||||
# indexed by the per-voxel uint64 indices the OnIndex grid stores.
|
||||
handle = nanovdb.tools.createNanoGridOnIndex(src_grid, channels=1)
|
||||
grid = handle.grid()
|
||||
print(f"OnIndex grid: valueCount={grid.valueCount()}, "
|
||||
f"source activeVoxelCount={src_grid.activeVoxelCount()}")
|
||||
assert grid.valueCount() >= src_grid.activeVoxelCount()
|
||||
|
||||
# createChannelAccessor inspects the channel's recorded dataType and
|
||||
# returns the matching typed accessor (here: OnIndexFloat...).
|
||||
channel = nanovdb.createChannelAccessor(grid, 0)
|
||||
print(f"channel accessor: {type(channel).__name__}, "
|
||||
f"valueCount={channel.valueCount()}")
|
||||
|
||||
src_acc = src_grid.getAccessor()
|
||||
for ijk in (nanovdb.math.Coord(48, 0, 0), nanovdb.math.Coord(0, 50, 0),
|
||||
nanovdb.math.Coord(0, 0, 52)):
|
||||
via_channel = channel.getValue(ijk)
|
||||
via_source = src_acc.getValue(ijk)
|
||||
print(f" {ijk}: channel={via_channel:.3f} source={via_source:.3f} "
|
||||
f"(linear offset {channel.getIndex(ijk)})")
|
||||
assert via_channel == via_source
|
||||
|
||||
|
||||
def author_blind_data():
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("NumPy not found. Skipping the blind-data authoring section.")
|
||||
return
|
||||
src = nanovdb.tools.build.FloatGrid(0.0, "authored",
|
||||
nanovdb.GridClass.Unknown)
|
||||
for i in range(8):
|
||||
src.setValue(nanovdb.math.Coord(i, 0, 0), float(i))
|
||||
|
||||
# Declare a channel at conversion time; it is allocated zero-filled
|
||||
# in the baked grid and filled afterwards through the writable view.
|
||||
conv = nanovdb.tools.CreateNanoGrid(src)
|
||||
ch = conv.addBlindData("temperature", count=64,
|
||||
dataType=nanovdb.GridType.Float)
|
||||
handle = conv.getHandle()
|
||||
grid = handle.grid()
|
||||
print(f"authored grid has {grid.blindDataCount()} blind-data channel(s)")
|
||||
|
||||
view = grid.getBlindData(ch)
|
||||
view[:] = np.linspace(273.0, 373.0, num=64, dtype=np.float32)
|
||||
|
||||
# Re-resolve the channel by name and confirm the writes persisted.
|
||||
n = grid.findBlindData("temperature")
|
||||
meta = grid.blindMetaData(n)
|
||||
stored = grid.getBlindData(n)
|
||||
print(f" {meta.name()!r}: {meta.valueCount} x {meta.dataType}, "
|
||||
f"range [{stored.min():.1f}, {stored.max():.1f}]")
|
||||
assert n == ch
|
||||
assert stored[0] == np.float32(273.0)
|
||||
|
||||
|
||||
def main():
|
||||
index_grid_with_channel()
|
||||
print()
|
||||
author_blind_data()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Write a bundle of primitive grids to a .nvdb file and read it back.
|
||||
|
||||
Python port of ex_write_nanovdb_grids, ex_read_nanovdb_sphere, and
|
||||
ex_read_nanovdb_sphere_accessor, consolidated into one self-contained
|
||||
round trip: bake five primitives, write them to a single file (with
|
||||
codec fallback), inspect the file metadata without loading the grids,
|
||||
re-read one grid by name and all grids at once, and split a merged
|
||||
handle back apart. The point-sphere section reads the world-space
|
||||
point positions through the zero-copy getBlindData() NumPy view.
|
||||
|
||||
Run with: python io_roundtrip.py
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
|
||||
def write_primitives(path):
|
||||
handles = [
|
||||
nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere_ls"),
|
||||
nanovdb.tools.createLevelSetTorus(majorRadius=50.0, minorRadius=20.0,
|
||||
name="torus_ls"),
|
||||
nanovdb.tools.createLevelSetBox(width=40.0, height=60.0, depth=80.0,
|
||||
name="box_ls"),
|
||||
nanovdb.tools.createLevelSetBBox(width=40.0, height=60.0, depth=80.0,
|
||||
thickness=10.0, name="bbox_ls"),
|
||||
nanovdb.tools.createPointSphere(pointsPerVoxel=2, radius=50.0,
|
||||
name="sphere_points"),
|
||||
]
|
||||
# BLOSC gives the best compression but is a build-time option; fall
|
||||
# back to an uncompressed file when this module was built without it.
|
||||
try:
|
||||
nanovdb.io.writeGrids(path, handles, codec=nanovdb.io.Codec.BLOSC)
|
||||
codec = "BLOSC"
|
||||
except RuntimeError:
|
||||
nanovdb.io.writeGrids(path, handles, codec=nanovdb.io.Codec.NONE)
|
||||
codec = "NONE"
|
||||
print(f"Wrote {len(handles)} grids to {path} (codec={codec})")
|
||||
|
||||
|
||||
def inspect_file(path):
|
||||
# readGridMetaData parses the per-grid file headers without loading
|
||||
# any voxel data — the cheap way to answer "what's in this file?".
|
||||
for meta in nanovdb.io.readGridMetaData(path):
|
||||
print(f" {meta.gridName!r}: type={meta.gridType}, "
|
||||
f"class={meta.gridClass}, voxels={meta.voxelCount}")
|
||||
assert nanovdb.io.hasGrid(path, "sphere_ls")
|
||||
assert not nanovdb.io.hasGrid(path, "no_such_grid")
|
||||
|
||||
|
||||
def read_back(path):
|
||||
# Read a single grid by name, then print the recognizable
|
||||
# ex_read_nanovdb_sphere_accessor cross-section along the x-axis.
|
||||
handle = nanovdb.io.readGrid(path, "sphere_ls")
|
||||
acc = handle.grid().getAccessor()
|
||||
for i in range(47, 54):
|
||||
ijk = nanovdb.math.Coord(i, 0, 0)
|
||||
print(f" sphere_ls({i},0,0) = {acc.getValue(ijk):.2f}")
|
||||
|
||||
# Read every grid, merge them into one multi-grid handle, and split
|
||||
# that handle back into one handle per grid.
|
||||
handles = nanovdb.io.readGrids(path)
|
||||
merged = nanovdb.mergeGrids(handles)
|
||||
print(f" merged handle holds {merged.gridCount()} grids")
|
||||
parts = nanovdb.splitGrids(merged)
|
||||
assert len(parts) == len(handles)
|
||||
print(f" splitGrids -> {len(parts)} single-grid handles")
|
||||
return handles
|
||||
|
||||
|
||||
def point_positions(handles):
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("NumPy not found. Skipping the point-positions section.")
|
||||
return
|
||||
# The point sphere stores its world-space positions as a blind-data
|
||||
# channel; getBlindData() exposes it as a zero-copy (N, 3) view.
|
||||
points = next(h.grid() for h in handles
|
||||
if h.grid().gridName() == "sphere_points")
|
||||
positions = points.getBlindData(0)
|
||||
radii = np.linalg.norm(positions, axis=1)
|
||||
print(f" {positions.shape[0]} points, "
|
||||
f"|p| in [{radii.min():.2f}, {radii.max():.2f}]")
|
||||
# Points are jittered within their voxel, so allow ~1.5 voxels.
|
||||
assert np.all(np.abs(radii - 50.0) < 1.5)
|
||||
|
||||
|
||||
def main():
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "primitives.nvdb")
|
||||
write_primitives(path)
|
||||
inspect_file(path)
|
||||
handles = read_back(path)
|
||||
point_positions(handles)
|
||||
print(f"Output left in {out_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Load a NanoVDB grid and inspect it polymorphically.
|
||||
|
||||
handle.grid(i) returns the correct typed Python subclass for whatever
|
||||
GridType the grid carries, so a single call site can handle a mixed
|
||||
bundle of grid types. This example builds two grids of different
|
||||
value types into one handle, then walks the handle inspecting each
|
||||
grid via the polymorphic accessor plus GridMetaData (the type-erased
|
||||
introspector that answers "what's in this buffer?" without knowing
|
||||
BuildT at compile time).
|
||||
|
||||
Run with: python load_inspect.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def describe_handle(handle):
|
||||
print(f"Handle contains {handle.gridCount()} grid(s).")
|
||||
for i in range(handle.gridCount()):
|
||||
# GridType / gridSize are cheap to query on the handle itself.
|
||||
gtype = handle.gridType(i)
|
||||
gsize = handle.gridSize(i)
|
||||
print(f" [{i}] type={gtype}, size={gsize} bytes")
|
||||
|
||||
# handle.grid(i) returns the matching <Suffix>Grid subclass
|
||||
# at runtime — no isinstance dispatch needed at the call site.
|
||||
# The grid name lives on the grid itself, not the handle.
|
||||
grid = handle.grid(i)
|
||||
print(f" name={grid.gridName()!r}, "
|
||||
f"gridClass={grid.gridClass()}")
|
||||
print(f" activeVoxelCount={grid.activeVoxelCount()}")
|
||||
print(f" worldBBox={grid.worldBBox()}")
|
||||
|
||||
# GridMetaData is the type-erased introspector — answers
|
||||
# "what's in this buffer?" without knowing BuildT.
|
||||
meta = nanovdb.GridMetaData(grid)
|
||||
print(f" gridSize={meta.gridSize()}, "
|
||||
f"isLevelSet={meta.isLevelSet()}, "
|
||||
f"isFogVolume={meta.isFogVolume()}")
|
||||
|
||||
|
||||
def main():
|
||||
# Build two grids of different types into one handle so we can
|
||||
# exercise the polymorphic accessor.
|
||||
h_float = nanovdb.tools.createLevelSetSphere(
|
||||
radius=10.0, name="sphere_float")
|
||||
h_double = nanovdb.tools.createLevelSetSphere(
|
||||
gridType=nanovdb.GridType.Double, radius=10.0, name="sphere_double")
|
||||
handle = nanovdb.mergeGrids([h_float, h_double])
|
||||
|
||||
describe_handle(handle)
|
||||
|
||||
print()
|
||||
print("Polymorphic dispatch from a runtime GridType:")
|
||||
for i in range(handle.gridCount()):
|
||||
grid = handle.grid(i)
|
||||
# Each typed grid carries a getAccessor() that returns the
|
||||
# appropriate <Suffix>ReadAccessor — float for FloatGrid,
|
||||
# double for DoubleGrid, etc.
|
||||
acc = grid.getAccessor()
|
||||
v = acc.getValue(nanovdb.math.Coord(0, 0, 0))
|
||||
print(f" grid[{i}] accessor.getValue(0,0,0) = {v}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Build a level set from a Python function evaluated at every voxel.
|
||||
|
||||
Python port of ex_make_funny_nanovdb: a trigonometric interference
|
||||
pattern is CSG-intersected with a sphere and clamped to a narrow band,
|
||||
using the functor-based nanovdb.tools.createFloatGrid factory. The
|
||||
callback is one Python call per voxel in the bbox, so this port uses a
|
||||
65^3 domain (~275k calls, a few seconds) where the C++ original fills
|
||||
[-500,500]^3 — scale `SIZE` up only if you are willing to wait.
|
||||
|
||||
Run with: python make_funny_nanovdb.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
SIZE = 32 # half-width of the cubic domain, in voxels
|
||||
BACKGROUND = 5.0 # narrow-band half-width, in world units
|
||||
FREQ = 0.8 # rescaled from the C++ 0.1 to fit the smaller domain
|
||||
|
||||
|
||||
def funny(ijk):
|
||||
v = 4.0 + 5.0 * (math.cos(ijk.x * FREQ) * math.sin(ijk.y * FREQ) +
|
||||
math.cos(ijk.y * FREQ) * math.sin(ijk.z * FREQ) +
|
||||
math.cos(ijk.z * FREQ) * math.sin(ijk.x * FREQ))
|
||||
# CSG intersection with a sphere of radius SIZE.
|
||||
r = math.sqrt(ijk.x ** 2 + ijk.y ** 2 + ijk.z ** 2)
|
||||
v = max(v, r - SIZE)
|
||||
# Clamp to the narrow band.
|
||||
return max(-BACKGROUND, min(BACKGROUND, v))
|
||||
|
||||
|
||||
def main():
|
||||
bbox = nanovdb.math.CoordBBox(nanovdb.math.Coord(-SIZE),
|
||||
nanovdb.math.Coord(SIZE))
|
||||
print(f"Evaluating funny() over {bbox} ...")
|
||||
handle = nanovdb.tools.createFloatGrid(
|
||||
BACKGROUND, "funny", nanovdb.GridClass.LevelSet, funny, bbox)
|
||||
|
||||
grid = handle.grid()
|
||||
print(f"activeVoxelCount = {grid.activeVoxelCount()}")
|
||||
acc = grid.getAccessor()
|
||||
probe = nanovdb.math.Coord(0, 0, 0)
|
||||
print(f"value at {probe} = {acc.getValue(probe):.3f}")
|
||||
assert grid.isLevelSet()
|
||||
assert grid.activeVoxelCount() > 0
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "funny.nvdb")
|
||||
try:
|
||||
nanovdb.io.writeGrid(path, handle, codec=nanovdb.io.Codec.BLOSC)
|
||||
except RuntimeError:
|
||||
nanovdb.io.writeGrid(path, handle, codec=nanovdb.io.Codec.NONE)
|
||||
print(f"Wrote {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Build grids of many value types and read them back polymorphically.
|
||||
|
||||
Python port of ex_make_typed_grids: a small solid sphere is baked once
|
||||
per value type through the matching nanovdb.tools.build.<Suffix>Grid
|
||||
mutable builder, all handles are written to a single .nvdb file, and
|
||||
the file is re-read with handle.grid() returning the correct typed
|
||||
subclass for each grid at runtime.
|
||||
|
||||
Run with: python make_typed_grids.py
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RADIUS = 8 # voxels; ~2.1k active voxels per grid keeps this quick
|
||||
|
||||
|
||||
def solid_sphere_coords():
|
||||
r2 = RADIUS * RADIUS
|
||||
for i in range(-RADIUS, RADIUS + 1):
|
||||
for j in range(-RADIUS, RADIUS + 1):
|
||||
for k in range(-RADIUS, RADIUS + 1):
|
||||
if i * i + j * j + k * k < r2:
|
||||
yield nanovdb.math.Coord(i, j, k)
|
||||
|
||||
|
||||
def build_typed_grids():
|
||||
# (builder class, grid name, background, voxel value)
|
||||
specs = [
|
||||
(nanovdb.tools.build.FloatGrid, "float_grid", 0.0, 1.0),
|
||||
(nanovdb.tools.build.DoubleGrid, "double_grid", 0.0, 1.0),
|
||||
(nanovdb.tools.build.Int16Grid, "int16_grid", 0, 1),
|
||||
(nanovdb.tools.build.Int32Grid, "int32_grid", 0, 1),
|
||||
(nanovdb.tools.build.Int64Grid, "int64_grid", 0, 1),
|
||||
(nanovdb.tools.build.UInt32Grid, "uint32_grid", 0, 1),
|
||||
(nanovdb.tools.build.Vec3fGrid, "vec3f_grid",
|
||||
nanovdb.math.Vec3f(0.0), nanovdb.math.Vec3f(1.0, 0.0, 0.0)),
|
||||
]
|
||||
handles = []
|
||||
for cls, name, background, value in specs:
|
||||
grid = cls(background, name, nanovdb.GridClass.Unknown)
|
||||
for ijk in solid_sphere_coords():
|
||||
grid.setValue(ijk, value)
|
||||
handles.append(grid.to_nanovdb())
|
||||
print(f"built {name} ({cls.__name__})")
|
||||
return handles
|
||||
|
||||
|
||||
def main():
|
||||
handles = build_typed_grids()
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "custom_types.nvdb")
|
||||
nanovdb.io.writeGrids(path, handles)
|
||||
print(f"Wrote {len(handles)} grids to {path}")
|
||||
|
||||
# Re-read and dispatch: handle.grid() returns FloatGrid, Int16Grid,
|
||||
# Vec3fGrid, ... according to the GridType each grid carries.
|
||||
probe = nanovdb.math.Coord(0, 0, 0)
|
||||
for handle in nanovdb.io.readGrids(path):
|
||||
grid = handle.grid()
|
||||
value = grid.getAccessor().getValue(probe)
|
||||
print(f" {grid.gridName():<12} -> {type(grid).__name__:<12} "
|
||||
f"active={grid.activeVoxelCount()} value(0,0,0)={value}")
|
||||
assert grid.activeVoxelCount() > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Iterate a grid's nodes linearly through a NodeManager.
|
||||
|
||||
Python port of the host half of ex_nodemanager_cuda: createNodeManager
|
||||
builds linearized arrays of the tree's leaf, lower, and upper nodes so
|
||||
they can be visited by index instead of by tree traversal. Each node
|
||||
exposes its origin, per-node stats, and (on leaves) the raw 512-value
|
||||
buffer. For bulk NumPy analytics over every leaf at once, see
|
||||
bulk_leaf_numpy.py's grid.leaf_values() instead.
|
||||
|
||||
Run with: python node_manager.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
tree = grid.tree()
|
||||
|
||||
nmh = nanovdb.createNodeManager(grid)
|
||||
nm = nmh.mgr()
|
||||
print(f"NodeManager over {grid.gridName()!r} (linear={nm.isLinear()}):")
|
||||
print(f" leaves={nm.leafCount()}, lower={nm.lowerCount()}, "
|
||||
f"upper={nm.upperCount()}")
|
||||
|
||||
# The counts mirror the tree's per-level node counts.
|
||||
assert nm.leafCount() == tree.nodeCount(0)
|
||||
assert nm.lowerCount() == tree.nodeCount(1)
|
||||
assert nm.upperCount() == tree.nodeCount(2)
|
||||
|
||||
# Linear access agrees with tree traversal.
|
||||
assert nm.leaf(0).origin() == tree.getFirstLeaf().origin()
|
||||
|
||||
# Visit a few leaves by index: origin, activity, and value range.
|
||||
for i in range(min(5, nm.leafCount())):
|
||||
leaf = nm.leaf(i)
|
||||
print(f" leaf[{i}] origin={leaf.origin()} "
|
||||
f"on={leaf.valueMask().countOn()} "
|
||||
f"min={leaf.minimum():.3f} max={leaf.maximum():.3f}")
|
||||
|
||||
# Internal nodes are reachable the same way.
|
||||
lower = nm.lower(0)
|
||||
print(f" lower[0] origin={lower.origin()} "
|
||||
f"children={lower.childMask().countOn()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Round-trip a grid between OpenVDB and NanoVDB.
|
||||
|
||||
Python port of ex_openvdb_to_nanovdb and ex_openvdb_to_nanovdb_accessor:
|
||||
an OpenVDB level-set sphere is converted to NanoVDB with openToNanoVDB,
|
||||
values are compared through both libraries' accessors, and the NanoVDB
|
||||
grid is converted back with nanoToOpenVDB. Requires a build with
|
||||
NANOVDB_USE_OPENVDB and an importable `openvdb` module; the script
|
||||
skips (exit 0) when either is missing.
|
||||
|
||||
Run with: python openvdb_interop.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
if not hasattr(nanovdb.tools, "openToNanoVDB"):
|
||||
print("nanovdb was built without NANOVDB_USE_OPENVDB. Skipping...")
|
||||
return
|
||||
try:
|
||||
import openvdb
|
||||
except ImportError:
|
||||
print("openvdb not found. Skipping...")
|
||||
return
|
||||
|
||||
sphere = openvdb.createLevelSetSphere(100.0)
|
||||
sphere.name = "sphere"
|
||||
handle = nanovdb.tools.openToNanoVDB(sphere)
|
||||
grid = handle.grid()
|
||||
print(f"openToNanoVDB: {grid.gridName()!r}, class={grid.gridClass()}, "
|
||||
f"active={grid.activeVoxelCount()}")
|
||||
assert grid.gridClass() == nanovdb.GridClass.LevelSet
|
||||
|
||||
# Compare a cross-section through both accessors, as the C++
|
||||
# accessor example does.
|
||||
open_acc = sphere.getAccessor()
|
||||
nano_acc = grid.getAccessor()
|
||||
for i in range(97, 104):
|
||||
open_v = open_acc.getValue((i, 0, 0))
|
||||
nano_v = nano_acc.getValue(nanovdb.math.Coord(i, 0, 0))
|
||||
print(f" ({i},0,0): openvdb={open_v:.3f} nanovdb={nano_v:.3f}")
|
||||
assert abs(open_v - nano_v) < 1e-5
|
||||
|
||||
# And back: NanoVDB -> OpenVDB.
|
||||
back = nanovdb.tools.nanoToOpenVDB(handle)
|
||||
print(f"nanoToOpenVDB: {back.name!r}, empty={back.empty()}")
|
||||
assert back.name == "sphere"
|
||||
assert not back.empty()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Quantize a NanoGrid<float> down to a quantized BuildT.
|
||||
|
||||
nanovdb.tools.createNanoGridFp4 / Fp8 / Fp16 / FpN are the quantized
|
||||
counterparts of the regular grid types. Fp4 / Fp8 / Fp16 use a fixed
|
||||
bit-width per voxel. FpN picks the bit-width per leaf so each leaf
|
||||
hits a user-supplied tolerance (the "oracle"). Smaller bit widths
|
||||
give smaller files at the cost of approximation error.
|
||||
|
||||
Run with: python quantize.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def gridSize_in_kb(handle):
|
||||
return handle.gridSize(0) / 1024
|
||||
|
||||
|
||||
def main():
|
||||
# Source: a 50-radius sphere fog volume at full float precision.
|
||||
src_handle = nanovdb.tools.createFogVolumeSphere(radius=50.0)
|
||||
src_grid = src_handle.grid()
|
||||
print(f"Source FloatGrid: {gridSize_in_kb(src_handle):.1f} KB, "
|
||||
f"active voxels = {src_grid.activeVoxelCount()}")
|
||||
|
||||
# Fixed-width quantization. Each subsequent format roughly halves
|
||||
# the per-voxel storage cost; dithering optional.
|
||||
for fn_name, label in [
|
||||
("createNanoGridFp16", "Fp16 (16-bit fixed)"),
|
||||
("createNanoGridFp8", "Fp8 (8-bit fixed)"),
|
||||
("createNanoGridFp4", "Fp4 (4-bit fixed)"),
|
||||
]:
|
||||
h = getattr(nanovdb.tools, fn_name)(src_grid, ditherOn=True)
|
||||
print(f" {label}: {gridSize_in_kb(h):.1f} KB, "
|
||||
f"active voxels = {h.grid().activeVoxelCount()}")
|
||||
|
||||
# Variable-width FpN. The oracle picks the per-leaf bit-width to
|
||||
# meet a tolerance — AbsDiff for absolute error, RelDiff for
|
||||
# relative error. -1 tolerance means "uninitialized" so we pass
|
||||
# an explicit value.
|
||||
abs_oracle = nanovdb.tools.AbsDiff(0.05) # ±0.05 per voxel
|
||||
h_fpn_abs = nanovdb.tools.createNanoGridFpN(src_grid, abs_oracle)
|
||||
print(f" FpN (AbsDiff 0.05): {gridSize_in_kb(h_fpn_abs):.1f} KB")
|
||||
|
||||
rel_oracle = nanovdb.tools.RelDiff(0.1) # 10% relative error
|
||||
h_fpn_rel = nanovdb.tools.createNanoGridFpN(src_grid, rel_oracle)
|
||||
print(f" FpN (RelDiff 0.10): {gridSize_in_kb(h_fpn_rel):.1f} KB")
|
||||
|
||||
# The output is a regular NanoGrid<FpN> — read-only, but exposes
|
||||
# the standard surface (gridType, activeVoxelCount, accessor.getValue
|
||||
# returning a decoded float, etc).
|
||||
fpn = h_fpn_abs.grid()
|
||||
print(f"\nFpN grid type: {fpn.gridType()}, "
|
||||
f"voxel at (0,0,0) = {fpn.getAccessor().getValue(nanovdb.math.Coord(0, 0, 0))}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Ray-march a fog volume on the CPU and write a PGM image.
|
||||
|
||||
Python port of the host path of ex_raytrace_fog_volume: each ray is
|
||||
clipped to the grid's index bounding box, then integrated with fixed
|
||||
steps, accumulating transmittance from the density sampled through a
|
||||
ReadAccessor at Coord.Floor of the march position — the accessor-based
|
||||
idiom, in contrast to the sampler-based raytrace_level_set.py. Every
|
||||
step is one Python-to-C++ call, so the default RES is modest; raise it
|
||||
for a nicer image.
|
||||
|
||||
Run with: python raytrace_fog_volume.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RES = 64 # image is RES x RES pixels
|
||||
FOV_DEG = 45.0
|
||||
DT = 1.0 # march step, in voxels
|
||||
SIGMA = 0.2 # extinction scale applied to the sampled density
|
||||
|
||||
|
||||
def clip_to_bbox(eye, direction, bbox):
|
||||
"""Slab-clip a ray against a CoordBBox; returns (t0, t1) or None."""
|
||||
t0, t1 = 0.0, math.inf
|
||||
for axis in range(3):
|
||||
lo = bbox.min[axis] - eye[axis]
|
||||
hi = bbox.max[axis] + 1.0 - eye[axis]
|
||||
d = direction[axis]
|
||||
if abs(d) < 1e-12:
|
||||
if lo > 0.0 or hi < 0.0:
|
||||
return None
|
||||
continue
|
||||
near, far = lo / d, hi / d
|
||||
if near > far:
|
||||
near, far = far, near
|
||||
t0, t1 = max(t0, near), min(t1, far)
|
||||
if t0 > t1:
|
||||
return None
|
||||
return t0, t1
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createFogVolumeSphere(radius=50.0, name="fog")
|
||||
grid = handle.grid()
|
||||
bbox = grid.indexBBox()
|
||||
acc = grid.getAccessor()
|
||||
|
||||
# Perspective camera looking down -z, as in the C++ RayGenOp,
|
||||
# working directly in index space (the grid transform is uniform).
|
||||
dim = [bbox.max[i] + 1 - bbox.min[i] for i in range(3)]
|
||||
center = [bbox.min[i] + 0.5 * dim[i] for i in range(3)]
|
||||
eye = (center[0], center[1], center[2] + 2.0 * dim[2])
|
||||
tan_fov = math.tan(math.radians(FOV_DEG) * 0.5)
|
||||
|
||||
pixels = bytearray(RES * RES)
|
||||
for y in range(RES):
|
||||
for x in range(RES):
|
||||
px = (2.0 * (x + 0.5) / RES - 1.0) * tan_fov
|
||||
py = (2.0 * (y + 0.5) / RES - 1.0) * tan_fov
|
||||
norm = math.sqrt(px * px + py * py + 1.0)
|
||||
direction = (px / norm, py / norm, -1.0 / norm)
|
||||
|
||||
span = clip_to_bbox(eye, direction, bbox)
|
||||
if span is None:
|
||||
continue
|
||||
transmittance = 1.0
|
||||
t = span[0]
|
||||
while t < span[1]:
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t * direction[0],
|
||||
eye[1] + t * direction[1],
|
||||
eye[2] + t * direction[2])
|
||||
density = acc.getValue(nanovdb.math.Coord.Floor(pos))
|
||||
transmittance *= 1.0 - density * SIGMA * DT
|
||||
if transmittance < 0.005:
|
||||
break
|
||||
t += DT
|
||||
pixels[y * RES + x] = int(255 * (1.0 - transmittance))
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "raytrace_fog_volume.pgm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(f"P5\n{RES} {RES}\n255\n".encode("ascii"))
|
||||
f.write(bytes(pixels))
|
||||
lit = sum(1 for p in pixels if p > 0)
|
||||
print(f"Rendered {RES}x{RES} image, {lit} foggy pixels -> {path}")
|
||||
assert lit > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Ray-trace a narrow-band level set on the CPU and write a PGM image.
|
||||
|
||||
Python port of the host path of ex_raytrace_level_set. The C++ example
|
||||
finds surface hits with math::Ray + HDDA ZeroCrossing, which are not
|
||||
bound in Python; this port re-expresses the search as sphere tracing —
|
||||
the clamped SDF value itself bounds the safe step size — using the
|
||||
trilinear sampler, with sampler.zeroCrossing() confirming the interval
|
||||
and sampler.gradient() shading the hit. Every sample is one
|
||||
Python-to-C++ call, so the default RES is modest; raise it for a nicer
|
||||
image.
|
||||
|
||||
Run with: python raytrace_level_set.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RES = 64 # image is RES x RES pixels
|
||||
FOV_DEG = 45.0
|
||||
LIGHT = (0.577, 0.577, 0.577) # unit vector toward the light
|
||||
|
||||
|
||||
def clip_to_bbox(eye, direction, bbox):
|
||||
"""Slab-clip a ray against a CoordBBox; returns (t0, t1) or None."""
|
||||
t0, t1 = 0.0, math.inf
|
||||
for axis in range(3):
|
||||
lo = bbox.min[axis] - eye[axis]
|
||||
hi = bbox.max[axis] + 1.0 - eye[axis]
|
||||
d = direction[axis]
|
||||
if abs(d) < 1e-12:
|
||||
if lo > 0.0 or hi < 0.0:
|
||||
return None
|
||||
continue
|
||||
near, far = lo / d, hi / d
|
||||
if near > far:
|
||||
near, far = far, near
|
||||
t0, t1 = max(t0, near), min(t1, far)
|
||||
if t0 > t1:
|
||||
return None
|
||||
return t0, t1
|
||||
|
||||
|
||||
def trace(sampler, eye, direction, t0, t1, voxel_size):
|
||||
"""Sphere-trace from t0 to t1; returns (hit t, crossing seen) or None."""
|
||||
t = t0
|
||||
while t < t1:
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t * direction[0],
|
||||
eye[1] + t * direction[1],
|
||||
eye[2] + t * direction[2])
|
||||
d = sampler(pos) # world-unit SDF, clamped to the narrow band
|
||||
if d <= 0.0:
|
||||
# zeroCrossing() reports whether the reconstruction stencil
|
||||
# here straddles the iso-surface — the sampler-level analog
|
||||
# of the HDDA ZeroCrossing test in the C++ example.
|
||||
return t, sampler.zeroCrossing(pos)
|
||||
# The clamped SDF bounds the distance to the surface, so it is
|
||||
# a safe (index-space) step size; never step below half a voxel.
|
||||
t += max(d / voxel_size, 0.5)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=100.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
bbox = grid.indexBBox()
|
||||
voxel_size = grid.voxelSize()[0]
|
||||
sampler = nanovdb.math.createTrilinearSampler(grid)
|
||||
|
||||
# Perspective camera looking down -z, as in the C++ RayGenOp,
|
||||
# working directly in index space (the grid transform is uniform).
|
||||
dim = [bbox.max[i] + 1 - bbox.min[i] for i in range(3)]
|
||||
center = [bbox.min[i] + 0.5 * dim[i] for i in range(3)]
|
||||
eye = nanovdb.math.Vec3d(center[0], center[1], center[2] + 2.0 * dim[2])
|
||||
tan_fov = math.tan(math.radians(FOV_DEG) * 0.5)
|
||||
|
||||
pixels = bytearray(RES * RES)
|
||||
crossings = 0
|
||||
for y in range(RES):
|
||||
for x in range(RES):
|
||||
px = (2.0 * (x + 0.5) / RES - 1.0) * tan_fov
|
||||
py = (2.0 * (y + 0.5) / RES - 1.0) * tan_fov
|
||||
norm = math.sqrt(px * px + py * py + 1.0)
|
||||
direction = (px / norm, py / norm, -1.0 / norm)
|
||||
|
||||
span = clip_to_bbox(eye, direction, bbox)
|
||||
if span is None:
|
||||
continue
|
||||
hit = trace(sampler, eye, direction, span[0], span[1],
|
||||
voxel_size)
|
||||
if hit is None:
|
||||
continue
|
||||
t_hit, crossed = hit
|
||||
crossings += crossed
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t_hit * direction[0],
|
||||
eye[1] + t_hit * direction[1],
|
||||
eye[2] + t_hit * direction[2])
|
||||
# gradient() is in index-space units — normalize before use.
|
||||
n = sampler.gradient(pos)
|
||||
n.normalize()
|
||||
shade = max(0.0, n[0] * LIGHT[0] + n[1] * LIGHT[1]
|
||||
+ n[2] * LIGHT[2])
|
||||
pixels[y * RES + x] = int(255 * shade)
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "raytrace_level_set.pgm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(f"P5\n{RES} {RES}\n255\n".encode("ascii"))
|
||||
f.write(bytes(pixels))
|
||||
lit = sum(1 for p in pixels if p > 0)
|
||||
print(f"Rendered {RES}x{RES} image, {lit} lit pixels "
|
||||
f"({crossings} confirmed zero-crossings) -> {path}")
|
||||
assert lit > 0 and crossings > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Validate a NanoVDB grid and round-trip its checksum.
|
||||
|
||||
nanovdb.tools exposes the grid-quality surface: validateGrid (single
|
||||
grid), checkGrid -> (ok, error), isValid, plus the checksum helpers
|
||||
evalChecksum / validateChecksum / updateChecksum. This example walks
|
||||
through a typical "load and verify" workflow.
|
||||
|
||||
Run with: python validate.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
# A well-formed grid: createLevelSetSphere returns a NanoGrid<float>
|
||||
# with stats and checksum populated to the defaults.
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=10.0)
|
||||
grid = handle.grid()
|
||||
|
||||
# Whole-handle validation. validateGrids returns a single bool.
|
||||
# The verbose=True flag prints failure details to std::cerr
|
||||
# (visible from Python via std::cerr -> sys.stderr on most stdlibs).
|
||||
all_ok = nanovdb.tools.validateGrids(
|
||||
handle, nanovdb.CheckMode.Default, verbose=False)
|
||||
print(f"validateGrids(handle, Default) = {all_ok}")
|
||||
|
||||
# Per-grid validation, with a helpful message on failure.
|
||||
one_ok = nanovdb.tools.validateGrid(handle, 0, nanovdb.CheckMode.Full)
|
||||
print(f"validateGrid(handle, 0, Full) = {one_ok}")
|
||||
|
||||
# validateGrid returns False (no raise) on out-of-range gridID;
|
||||
# CheckMode.Disable short-circuits and always returns True.
|
||||
print(f"validateGrid(handle, 99) = "
|
||||
f"{nanovdb.tools.validateGrid(handle, 99)} (out of range)")
|
||||
print(f"validateGrid(handle, 99, Disable) = "
|
||||
f"{nanovdb.tools.validateGrid(handle, 99, nanovdb.CheckMode.Disable)}"
|
||||
f" (Disable short-circuit)")
|
||||
|
||||
# tools.checkGrid returns the structural check result plus a
|
||||
# human-readable error message (empty on success).
|
||||
ok, msg = nanovdb.tools.checkGrid(grid, nanovdb.CheckMode.Full)
|
||||
print(f"checkGrid(grid, Full) = ok={ok}, msg={msg!r}")
|
||||
|
||||
# tools.isValid is checkGrid + checksum verification, returning
|
||||
# one bool.
|
||||
print(f"isValid(grid, Default) = "
|
||||
f"{nanovdb.tools.isValid(grid, nanovdb.CheckMode.Default)}")
|
||||
|
||||
# Checksum round-trip. evalChecksum is non-mutating; updateChecksum
|
||||
# writes back into the grid header.
|
||||
cs1 = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Full)
|
||||
nanovdb.tools.updateChecksum(grid, nanovdb.CheckMode.Full)
|
||||
cs2 = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Full)
|
||||
print(f"evalChecksum equal after no-op update: {cs1 == cs2}")
|
||||
print(f"validateChecksum(grid, Full) = "
|
||||
f"{nanovdb.tools.validateChecksum(grid, nanovdb.CheckMode.Full)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Smoke test: run every example script and require a clean exit.
|
||||
|
||||
The examples are self-contained and degrade gracefully (exit 0 with a
|
||||
skip message) when an optional dependency such as NumPy or OpenVDB is
|
||||
missing, so a non-zero exit or an uncaught exception always indicates
|
||||
a real breakage — typically the bindings drifting under the examples.
|
||||
|
||||
The script list is deliberately explicit rather than a glob so that a
|
||||
missing listed example fails loudly and unrelated files placed under
|
||||
examples/ are not executed.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
os.pardir, "examples")
|
||||
|
||||
EXAMPLE_SCRIPTS = [
|
||||
"build_grid.py",
|
||||
"bulk_leaf_numpy.py",
|
||||
"collide_level_set.py",
|
||||
"index_grid_channels.py",
|
||||
"io_roundtrip.py",
|
||||
"load_inspect.py",
|
||||
"make_funny_nanovdb.py",
|
||||
"make_typed_grids.py",
|
||||
"node_manager.py",
|
||||
"openvdb_interop.py",
|
||||
"quantize.py",
|
||||
"raytrace_fog_volume.py",
|
||||
"raytrace_level_set.py",
|
||||
"validate.py",
|
||||
]
|
||||
|
||||
|
||||
class TestExamples(unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
def _make_test(script_name):
|
||||
def test(self):
|
||||
script = os.path.join(EXAMPLES_DIR, script_name)
|
||||
self.assertTrue(os.path.isfile(script),
|
||||
f"example script is missing: {script}")
|
||||
command = [sys.executable, script]
|
||||
env = os.environ.copy()
|
||||
# Preserve output up to a native crash and ask Python to report the
|
||||
# active frame for fatal signals and Windows exceptions.
|
||||
env["PYTHONFAULTHANDLER"] = "1"
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
if hasattr(os, "add_dll_directory"):
|
||||
# Match TestNanoVDB.py's in-tree OpenVDB DLL lookup. The child
|
||||
# starts in a temporary directory, so resolve this while the
|
||||
# parent is still in the CMake configuration directory.
|
||||
config = os.path.basename(os.getcwd())
|
||||
openvdb_dll_directory = os.path.abspath(os.path.join(
|
||||
os.getcwd(), os.pardir, os.pardir, os.pardir, os.pardir,
|
||||
"openvdb", "openvdb", config))
|
||||
dll_directories = [openvdb_dll_directory]
|
||||
cuda_root = env.get("CUDA_PATH")
|
||||
if cuda_root:
|
||||
dll_directories.append(os.path.join(cuda_root, "bin"))
|
||||
env["NANOVDB_TEST_DLL_DIRECTORIES"] = os.pathsep.join(
|
||||
dll_directories)
|
||||
# Python imports sitecustomize during startup. Add this test
|
||||
# directory to the child's module path so sitecustomize.py can
|
||||
# register only the required OpenVDB and CUDA DLL directories
|
||||
# before the example imports nanovdb, while still running the
|
||||
# example as a normal script.
|
||||
test_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
env["PYTHONPATH"] = os.pathsep.join(
|
||||
(test_directory, env.get("PYTHONPATH", "")))
|
||||
# A fresh cwd per run keeps any output files out of the source
|
||||
# tree (the examples themselves also write to tempdirs).
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=tempfile.mkdtemp(prefix="nanovdb_example_"),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
self.assertEqual(
|
||||
result.returncode, 0,
|
||||
f"{script_name} exited with {result.returncode}\n"
|
||||
f"--- stdout ---\n{result.stdout}\n"
|
||||
f"--- stderr ---\n{result.stderr}")
|
||||
|
||||
return test
|
||||
|
||||
|
||||
for _name in EXAMPLE_SCRIPTS:
|
||||
_test_name = "test_" + _name.replace(".py", "")
|
||||
setattr(TestExamples, _test_name, _make_test(_name))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Configure dependent-DLL lookup for in-tree Windows Python tests."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
# Keep these handles alive for the lifetime of the child Python process.
|
||||
_dll_directory_handles = []
|
||||
|
||||
if hasattr(os, "add_dll_directory"):
|
||||
for _path in os.environ.get(
|
||||
"NANOVDB_TEST_DLL_DIRECTORIES", "").split(os.pathsep):
|
||||
if os.path.isdir(_path):
|
||||
try:
|
||||
_dll_directory_handles.append(os.add_dll_directory(_path))
|
||||
except OSError:
|
||||
pass
|
||||
Reference in New Issue
Block a user