Skip to content

Commit 0bcbb46

Browse files
JaeHyunLee94claude
andcommitted
ex_connected_components_cuda: merge multiple OBJs + --discard-surface-voxels
Accept one or more .obj files (concatenated into a single mesh) and add a --discard-surface-voxels switch: by default label the full narrow band (one component per closed surface), or prune the sqrt(3)/2-voxel barrier shell first when the switch is given. --voxel-size / --band-width replace the old positional args. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: JaeHyun Lee <jaehlee@nvidia.com>
1 parent a2e1c20 commit 0bcbb46

2 files changed

Lines changed: 91 additions & 51 deletions

File tree

nanovdb/nanovdb/examples/ex_connected_components_cuda/connected_components_cuda.cpp

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
///
66
/// @brief Host driver for the connected-components example (NanoVDB / CUDA only, no OpenVDB).
77
///
8-
/// Reads a triangle mesh from a Wavefront .obj file, builds the index<->world transform,
9-
/// and hands the mesh to the CUDA side, which rasterizes it into a ValueOnIndex narrow-band
10-
/// grid, discards the surface/barrier shell, and runs connected-components labeling
8+
/// Reads one or more Wavefront .obj meshes (concatenated into a single vertex/triangle list),
9+
/// builds the index<->world transform, and hands the mesh to the CUDA side, which rasterizes
10+
/// it into a ValueOnIndex narrow-band grid, optionally discards the surface/barrier shell
11+
/// (--discard-surface-voxels), and runs connected-components labeling
1112
/// (nanovdb::tools::cuda::ConnectedComponents) on the result. The device side also runs a
1213
/// CPU union-find oracle that independently verifies the GPU labeling.
1314
///
@@ -27,14 +28,17 @@
2728

2829
// ---- Host/device seam (implemented in connected_components_cuda_kernels.cu) -----------------------
2930
//
30-
// Rasterize the mesh -> ValueOnIndex narrow band, discard the barrier shell (unsigned distance
31-
// within sqrt(3)/2 voxels of the surface), and run connected-components labeling. Prints topology
32-
// diagnostics, the component count, and a CPU-oracle PASS/FAIL, and returns the number of
33-
// connected components.
31+
// Rasterize the mesh -> ValueOnIndex narrow band and run connected-components labeling. When
32+
// @a discardSurfaceVoxels is true, the surface/barrier shell (unsigned distance within sqrt(3)/2
33+
// voxels of the surface) is pruned first, splitting each closed surface's band into disjoint
34+
// inner/outer shells; otherwise labeling runs on the full narrow band (one component per closed
35+
// surface). Prints topology diagnostics, the component count, and a CPU-oracle PASS/FAIL, and
36+
// returns the number of connected components.
3437
uint64_t connectedComponentsFromMesh(const std::vector<nanovdb::Vec3f>& points,
3538
const std::vector<nanovdb::Vec3i>& triangles,
3639
const nanovdb::Map& map,
37-
float bandWidth);
40+
float bandWidth,
41+
bool discardSurfaceVoxels);
3842

3943
/// @brief Minimal Wavefront .obj reader (vertices + faces) using NanoVDB types.
4044
///
@@ -86,29 +90,59 @@ static void readOBJ(const std::string& filename,
8690
int main(int argc, char* argv[])
8791
{
8892
try {
89-
if (argc < 2)
90-
throw std::runtime_error("usage: " + std::string(argv[0]) +
91-
" <input.obj> [voxelSize] [bandWidth]");
93+
// Parse args: any non-option token is an input .obj; options set the transform and the
94+
// barrier-discard switch.
95+
std::vector<std::string> objFiles;
96+
float voxelSize = 0.01f;
97+
float bandWidth = 3.0f;
98+
bool discardSurfaceVoxels = false;
9299

93-
const std::string inputFile = argv[1];
94-
const float voxelSize = (argc > 2) ? std::stof(argv[2]) : 0.01f;
95-
const float bandWidth = (argc > 3) ? std::stof(argv[3]) : 3.0f;
100+
auto nextValue = [&](int& i, const char* opt) -> const char* {
101+
if (i + 1 >= argc) throw std::runtime_error(std::string("missing value for ") + opt);
102+
return argv[++i];
103+
};
104+
for (int i = 1; i < argc; ++i) {
105+
const std::string a = argv[i];
106+
if (a == "--discard-surface-voxels") discardSurfaceVoxels = true;
107+
else if (a == "--voxel-size") voxelSize = std::stof(nextValue(i, "--voxel-size"));
108+
else if (a == "--band-width") bandWidth = std::stof(nextValue(i, "--band-width"));
109+
else if (a.rfind("--", 0) == 0) throw std::runtime_error("unknown option: " + a);
110+
else objFiles.push_back(a);
111+
}
112+
if (objFiles.empty())
113+
throw std::runtime_error(
114+
"usage: " + std::string(argv[0]) +
115+
" <input.obj> [more.obj ...] [--voxel-size S] [--band-width W] [--discard-surface-voxels]");
96116

117+
// Read and merge all input meshes into one vertex/triangle list. Merging simply concatenates
118+
// the meshes in their own coordinates (no repositioning), offsetting each mesh's triangle
119+
// indices past the vertices already added.
97120
std::vector<nanovdb::Vec3f> points;
98121
std::vector<nanovdb::Vec3i> triangles;
99-
std::cout << "Reading " << inputFile << "...\n";
100-
readOBJ(inputFile, points, triangles);
101-
std::cout << "Loaded " << points.size() << " vertices, "
102-
<< triangles.size() << " triangles.\n";
122+
for (const std::string& file : objFiles) {
123+
std::vector<nanovdb::Vec3f> filePoints;
124+
std::vector<nanovdb::Vec3i> fileTriangles;
125+
std::cout << "Reading " << file << "...\n";
126+
readOBJ(file, filePoints, fileTriangles);
127+
const int offset = int(points.size());
128+
points.insert(points.end(), filePoints.begin(), filePoints.end());
129+
for (const nanovdb::Vec3i& t : fileTriangles)
130+
triangles.emplace_back(t[0] + offset, t[1] + offset, t[2] + offset);
131+
}
132+
std::cout << "Loaded " << points.size() << " vertices, " << triangles.size()
133+
<< " triangles from " << objFiles.size() << " mesh(es).\n";
103134
if (points.empty() || triangles.empty())
104135
throw std::runtime_error("mesh has no triangles");
105136

106137
// Index<->world transform: uniform voxel size, no translation.
107138
nanovdb::Map map;
108139
map.set(double(voxelSize), nanovdb::Vec3d(0.0), 1.0);
109140

141+
std::cout << "Surface voxels: "
142+
<< (discardSurfaceVoxels ? "discarded (barrier shell pruned)"
143+
: "kept (full narrow band)") << "\n";
110144
const uint64_t numComponents =
111-
connectedComponentsFromMesh(points, triangles, map, bandWidth);
145+
connectedComponentsFromMesh(points, triangles, map, bandWidth, discardSurfaceVoxels);
112146
std::cout << "Connected components: " << numComponents << "\n";
113147

114148
return 0;

nanovdb/nanovdb/examples/ex_connected_components_cuda/connected_components_cuda_kernels.cu

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
/// @file connected_components_cuda_kernels.cu
55
///
66
/// @brief CUDA / NanoVDB side of the connected-components example. Rasterizes a triangle mesh into a
7-
/// ValueOnIndex narrow-band grid (nanovdb::tools::cuda::MeshToGrid), discards the
7+
/// ValueOnIndex narrow-band grid (nanovdb::tools::cuda::MeshToGrid), optionally discards the
88
/// surface/barrier shell (unsigned distance within sqrt(3)/2 voxels of the surface) with
99
/// nanovdb::tools::cuda::PruneGrid, and runs connected-components labeling with
1010
/// nanovdb::tools::cuda::ConnectedComponents. A CPU union-find oracle independently verifies
@@ -172,7 +172,8 @@ bool validateAgainstOracle(const GridHandleT& derivedHandle, uint32_t leafCount,
172172
uint64_t connectedComponentsFromMesh(const std::vector<nanovdb::Vec3f>& points,
173173
const std::vector<nanovdb::Vec3i>& triangles,
174174
const nanovdb::Map& map,
175-
float bandWidth)
175+
float bandWidth,
176+
bool discardSurfaceVoxels)
176177
{
177178
const cudaStream_t stream = 0;
178179

@@ -188,41 +189,46 @@ uint64_t connectedComponentsFromMesh(const std::vector<nanovdb::Vec3f>& points,
188189
auto [origHandle, udfSidecar] = converter.getHandleAndUDF();
189190
const auto* d_orig = origHandle.template deviceGrid<BuildT>();
190191

191-
// World-space voxel size from the map (uniform scale here, but read it generically).
192-
const nanovdb::Vec3d w0 = map.applyMap(nanovdb::Vec3d(0.0, 0.0, 0.0));
193-
const nanovdb::Vec3d wx = map.applyMap(nanovdb::Vec3d(1.0, 0.0, 0.0));
194-
const float voxelSize = float(wx[0] - w0[0]);
195-
196-
// ---- Step 2: discard the surface/barrier shell -> derived topology. ----
197-
const float barrierSqWorld = 0.75f * voxelSize * voxelSize; // (sqrt(3)/2 * voxelSize)^2
198-
const uint32_t srcLeafCount = Traits::getTreeData(d_orig).mNodeCount[0];
199-
200-
auto retainMask = nanovdb::cuda::DeviceBuffer::create(
201-
std::size_t(srcLeafCount) * sizeof(nanovdb::Mask<3>), nullptr, false);
202-
auto* d_retainMask = static_cast<nanovdb::Mask<3>*>(retainMask.deviceData());
203-
204-
nanovdb::util::cuda::operatorKernel<UDFBarrierPruneMaskFunctor>
205-
<<<srcLeafCount, UDFBarrierPruneMaskFunctor::MaxThreadsPerBlock, 0, stream>>>(
206-
d_orig, static_cast<const float*>(udfSidecar.deviceData()), barrierSqWorld, d_retainMask);
207-
cudaCheckError();
208-
209-
nanovdb::tools::cuda::PruneGrid<BuildT> pruner(d_orig, d_retainMask, stream);
210-
auto derivedHandle = pruner.getHandle();
211-
const auto* d_derived = derivedHandle.template deviceGrid<BuildT>();
192+
// ---- Step 2 (optional): discard the surface/barrier shell -> derived topology. ----
193+
// With the shell removed, each closed surface's band splits into disjoint inner/outer shells;
194+
// without it, connected components run on the full narrow band (one component per closed surface).
195+
GridHandleT derivedHandle; // stays empty unless we prune
196+
const nanovdb::NanoGrid<BuildT>* d_cc = d_orig; // grid connected components will label
197+
if (discardSurfaceVoxels) {
198+
// World-space voxel size from the map (uniform scale here, but read it generically).
199+
const nanovdb::Vec3d w0 = map.applyMap(nanovdb::Vec3d(0.0, 0.0, 0.0));
200+
const nanovdb::Vec3d wx = map.applyMap(nanovdb::Vec3d(1.0, 0.0, 0.0));
201+
const float voxelSize = float(wx[0] - w0[0]);
202+
const float barrierSqWorld = 0.75f * voxelSize * voxelSize; // (sqrt(3)/2 * voxelSize)^2
203+
const uint32_t srcLeafCount = Traits::getTreeData(d_orig).mNodeCount[0];
204+
205+
auto retainMask = nanovdb::cuda::DeviceBuffer::create(
206+
std::size_t(srcLeafCount) * sizeof(nanovdb::Mask<3>), nullptr, false);
207+
auto* d_retainMask = static_cast<nanovdb::Mask<3>*>(retainMask.deviceData());
208+
209+
nanovdb::util::cuda::operatorKernel<UDFBarrierPruneMaskFunctor>
210+
<<<srcLeafCount, UDFBarrierPruneMaskFunctor::MaxThreadsPerBlock, 0, stream>>>(
211+
d_orig, static_cast<const float*>(udfSidecar.deviceData()), barrierSqWorld, d_retainMask);
212+
cudaCheckError();
213+
214+
nanovdb::tools::cuda::PruneGrid<BuildT> pruner(d_orig, d_retainMask, stream);
215+
derivedHandle = pruner.getHandle();
216+
d_cc = derivedHandle.template deviceGrid<BuildT>();
217+
}
212218

213-
// ---- Step 3: connected-components labeling on the derived grid. ----
214-
nanovdb::tools::cuda::ConnectedComponents<BuildT> cc(d_derived, stream);
219+
// ---- Step 3: connected-components labeling on the selected grid. ----
220+
nanovdb::tools::cuda::ConnectedComponents<BuildT> cc(d_cc, stream);
215221
auto [d_labels, numComponents] = cc.getVoxelLabelsAndCount();
216222
cudaCheck(cudaStreamSynchronize(stream));
217223

218-
// Diagnostics.
219-
const uint64_t derivedActive = Traits::getActiveVoxelCount(d_derived);
220-
const uint32_t derivedLeaves = Traits::getTreeData(d_derived).mNodeCount[0];
221-
std::cout << "Derived (barrier-removed) grid: " << derivedActive << " active voxels, "
222-
<< derivedLeaves << " leaves.\n";
224+
// Diagnostics + CPU-oracle self-check (on whichever grid was labeled).
225+
const GridHandleT& ccHandle = discardSurfaceVoxels ? derivedHandle : origHandle;
226+
const uint64_t ccActive = Traits::getActiveVoxelCount(d_cc);
227+
const uint32_t ccLeaves = Traits::getTreeData(d_cc).mNodeCount[0];
228+
std::cout << (discardSurfaceVoxels ? "Derived (barrier-removed) grid: " : "Full narrow-band grid: ")
229+
<< ccActive << " active voxels, " << ccLeaves << " leaves.\n";
223230

224-
// CPU-oracle self-check.
225-
validateAgainstOracle(derivedHandle, derivedLeaves, derivedActive, d_labels, numComponents);
231+
validateAgainstOracle(ccHandle, ccLeaves, ccActive, d_labels, numComponents);
226232

227233
return numComponents;
228234
}

0 commit comments

Comments
 (0)