Skip to content

Commit 78ee495

Browse files
committed
Add MeshToGrid: CUDA triangle mesh to NanoVDB IndexGrid/UDF converter
Introduces nanovdb::tools::cuda::MeshToGrid<BuildT>, a GPU-accelerated rasterizer that converts a triangle soup (vertex list + index list) into a sparse NanoVDB grid on the device. Two output modes are implemented: - getHandle() — produces a ValueOnIndex (topology-only) grid - getHandleAndUDF() — produces the same grid plus a device sidecar buffer of per-active-voxel unsigned distances (world-space UDF, ordered by active voxel index) SDF (signed distance field) output is planned future work. The pipeline uses a hierarchical top-down subdivision approach: 1. transformTriangles() — vertices transformed to NanoVDB index space via nanovdb::Map::applyInverseMap() 2. processRootTrianglePairs() — enumerates (root tile, triangle) pairs whose padded AABBs overlap, via CUB prefix-sum + scatter (no atomics) 3. processLeafTrianglePairs() — 3-level 8x subdivision (4096→512→64→8), refining the pair list at each level using triangle-AABB / SAT intersection tests (evaluateAndCountSubBoxesKernel: 1 CTA per pair, 512 threads, warp ballot + CUB reduce) 4. Topology assembly — unique leaf origins extracted, NanoVDB ValueOnIndex tree built via TopologyBuilder, empty leaves pruned via PruneGrid 5. UDF computation (optional) — ComputeUDFFunctor computes per-voxel point-to-triangle distances via atomic-min, finalized to world-space in a second pass The example driver (ex_mesh_to_grid_cuda) reads an OBJ file, builds an OpenVDB UDF as reference, and runs a thorough correctness validation: - Active voxel set comparison (false negatives, false positive rate) - Per-voxel UDF error histogram vs. OpenVDB reference (in voxel units) - CPU brute-force ground-truth check on the worst-error voxels New files: nanovdb/nanovdb/tools/cuda/MeshToGrid.cuh nanovdb/nanovdb/math/Proximity.h — point/triangle proximity utils nanovdb/nanovdb/util/cuda/Rasterization.cuh — ComputeUDFFunctor and helpers Modified files: nanovdb/nanovdb/tools/cuda/TopologyBuilder.cuh — adds InitGridTreeRootFunctor for constructing grids from scratch (no source grid to copy metadata from) nanovdb/nanovdb/util/cuda/Util.h — adds operatorKernelInstance for launching pre-constructed device functors carrying data members nanovdb/nanovdb/examples/CMakeLists.txt — registers ex_mesh_to_grid_cuda Signed-off-by: Efty Sifakis <esifakis@nvidia.com>
1 parent d23549f commit 78ee495

8 files changed

Lines changed: 2120 additions & 0 deletions

File tree

nanovdb/nanovdb/examples/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ nanovdb_example(NAME "ex_dilate_nanovdb_cuda" OPENVDB)
113113
nanovdb_example(NAME "ex_merge_nanovdb_cuda" OPENVDB)
114114
nanovdb_example(NAME "ex_refine_nanovdb_cuda" OPENVDB)
115115
nanovdb_example(NAME "ex_coarsen_nanovdb_cuda" OPENVDB)
116+
nanovdb_example(NAME "ex_mesh_to_grid_cuda" OPENVDB)
116117

117118
if(CUDAToolkit_FOUND)
118119
nanovdb_example(NAME "ex_make_mgpu_nanovdb") # requires cuRAND
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright Contributors to the OpenVDB Project
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// the following files are from OpenVDB
5+
#include <openvdb/tools/Morphology.h>
6+
#include <openvdb/util/CpuTimer.h>
7+
#include <openvdb/tools/MeshToVolume.h>
8+
9+
// the following files are from NanoVDB
10+
#include <nanovdb/NanoVDB.h>
11+
12+
#include <thrust/universal_vector.h>
13+
14+
#include <fstream>
15+
#include <iostream>
16+
#include <sstream>
17+
#include <string>
18+
#include <vector>
19+
20+
template<typename BuildT>
21+
void mainMeshToGrid(
22+
const nanovdb::Vec3f *devicePoints,
23+
const int pointCount,
24+
const nanovdb::Vec3i *deviceTriangles,
25+
const int triangleCount,
26+
const nanovdb::Map map,
27+
const openvdb::FloatGrid::Ptr refGrid);
28+
29+
void readOBJ(const std::string& filename,
30+
std::vector<openvdb::Vec3s>& points,
31+
std::vector<openvdb::Vec3I>& triangles,
32+
std::vector<openvdb::Vec4I>& quads)
33+
{
34+
std::ifstream file(filename);
35+
if (!file.is_open()) {
36+
OPENVDB_THROW(openvdb::IoError, "Failed to open OBJ file: " + filename);
37+
}
38+
39+
std::string line;
40+
int lineNumber = 0;
41+
42+
while (std::getline(file, line)) {
43+
lineNumber++;
44+
std::istringstream iss(line);
45+
std::string type;
46+
iss >> type;
47+
48+
if (type == "v") {
49+
float x, y, z;
50+
iss >> x >> y >> z;
51+
points.push_back(openvdb::Vec3s(x, y, z));
52+
} else if (type == "f") {
53+
std::vector<int> faceIndices;
54+
std::string vertexData;
55+
56+
while (iss >> vertexData) {
57+
// Isolate the vertex index (everything before the first slash)
58+
size_t slashPos = vertexData.find('/');
59+
std::string indexStr = vertexData.substr(0, slashPos);
60+
61+
if (indexStr.empty()) continue;
62+
63+
int raw_idx = std::stoi(indexStr);
64+
int actual_idx = 0;
65+
66+
// Handle negative indices: relative to the number of points parsed so far
67+
if (raw_idx < 0) {
68+
actual_idx = points.size() + raw_idx;
69+
} else {
70+
// Standard positive indices: OBJ is 1-based, convert to 0-based for C++
71+
actual_idx = raw_idx - 1;
72+
}
73+
74+
// Strict bounds checking to prevent segfaults
75+
if (actual_idx < 0 || actual_idx >= points.size()) {
76+
OPENVDB_THROW(openvdb::ValueError,
77+
"OBJ parse error on line " + std::to_string(lineNumber) +
78+
": Face index out of bounds (Raw: " + std::to_string(raw_idx) +
79+
", Computed: " + std::to_string(actual_idx) + ", Total Points: " +
80+
std::to_string(points.size()) + ")");
81+
}
82+
83+
faceIndices.push_back(actual_idx);
84+
}
85+
86+
// Add to the appropriate OpenVDB list
87+
if (faceIndices.size() == 3) {
88+
triangles.push_back(openvdb::Vec3I(faceIndices[0], faceIndices[1], faceIndices[2]));
89+
} else if (faceIndices.size() == 4) {
90+
quads.push_back(openvdb::Vec4I(faceIndices[0], faceIndices[1], faceIndices[2], faceIndices[3]));
91+
} else if (faceIndices.size() > 4) {
92+
std::cerr << "Warning on line " << lineNumber << ": Skipping face with "
93+
<< faceIndices.size() << " vertices. Triangulate your mesh!" << std::endl;
94+
}
95+
}
96+
}
97+
}
98+
99+
/// @brief This example depends on OpenVDB, NanoVDB, and CUDA
100+
int main(int argc, char *argv[])
101+
{
102+
using BuildT = nanovdb::ValueOnIndex;
103+
104+
openvdb::util::CpuTimer cpuTimer;
105+
106+
try {
107+
108+
if (argc<2) OPENVDB_THROW(openvdb::ValueError, "usage: "+std::string(argv[0])+" input.obj [output.vdb]\n");
109+
std::string inputFile = argv[1];
110+
std::string outputFile = "output.vdb";
111+
if (argc > 2)
112+
outputFile = argv[2];
113+
float voxelSize = 0.001f;
114+
if (argc > 3)
115+
voxelSize = atof(argv[3]);
116+
117+
std::vector<openvdb::Vec3s> openvdb_points;
118+
std::vector<openvdb::Vec3I> openvdb_triangles;
119+
std::vector<openvdb::Vec4I> quads;
120+
121+
// Read the OBJ file
122+
std::cout << "Reading " << inputFile << "..." << std::endl;
123+
readOBJ(inputFile, openvdb_points, openvdb_triangles, quads);
124+
std::cout << "Loaded " << openvdb_points.size() << " vertices, "
125+
<< openvdb_triangles.size() << " openvdb_triangles, and "
126+
<< quads.size() << " quads." << std::endl;
127+
128+
// Initialize OpenVDB
129+
openvdb::initialize();
130+
131+
// Setup Grid Transform (Voxel Size)
132+
openvdb::math::Transform::Ptr transform =
133+
openvdb::math::Transform::createLinearTransform(voxelSize);
134+
135+
// Convert Mesh to Unsigned Distance Field (UDF)
136+
// halfband specifies the half-width of the narrow band in voxel units
137+
float halfband = 3.0f;
138+
cpuTimer.start("Converting mesh to OpenVDB unsigned distance field");
139+
openvdb::FloatGrid::Ptr grid = openvdb::tools::meshToUnsignedDistanceField<openvdb::FloatGrid>(
140+
*transform, openvdb_points, openvdb_triangles, quads, halfband);
141+
cpuTimer.stop();
142+
143+
144+
// Write the Grid to a VDB File
145+
grid->setName("UnsignedDistanceField");
146+
grid->print(std::cout, 2);
147+
std::cout << "Writing to " << outputFile << "..." << std::endl;
148+
openvdb::GridPtrVec grids;
149+
grids.push_back(grid);
150+
openvdb::io::File file(outputFile);
151+
file.write(grids);
152+
file.close();
153+
154+
// Cast the raw pointers from the std::vector data
155+
const auto* nano_pts_data = reinterpret_cast<const nanovdb::Vec3f*>(openvdb_points.data());
156+
const auto* nano_tris_data = reinterpret_cast<const nanovdb::Vec3i*>(openvdb_triangles.data());
157+
158+
// Initialize the thrust vectors using the casted pointer ranges
159+
thrust::universal_vector<nanovdb::Vec3f> nanovdb_points(nano_pts_data, nano_pts_data + openvdb_points.size());
160+
thrust::universal_vector<nanovdb::Vec3i> nanovdb_triangles(nano_tris_data, nano_tris_data + openvdb_triangles.size());
161+
162+
// Convert OpenVDB transform to nanovdb::Map
163+
164+
const auto openvdb_mat4 = transform->baseMap()->getAffineMap()->getMat4();
165+
nanovdb::Map map;
166+
map.set(openvdb_mat4, openvdb_mat4.inverse());
167+
168+
mainMeshToGrid<BuildT>(
169+
nanovdb_points.data().get(),
170+
nanovdb_points.size(),
171+
nanovdb_triangles.data().get(),
172+
nanovdb_triangles.size(),
173+
map,
174+
grid);
175+
176+
return 0;
177+
}
178+
catch (const std::exception& e) {
179+
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
180+
}
181+
return 0;
182+
}

0 commit comments

Comments
 (0)