Skip to content

Commit a1237c8

Browse files
sifakisclaude
andcommitted
Add unit tests for MeshToGrid: empty mesh and unit tetrahedron
MeshToGrid_EmptyMesh: verifies that getHandle() returns a valid grid with zero active voxels when the triangle list is empty. MeshToGrid_UnitTetrahedron: constructs a unit tetrahedron (4 vertices, 4 faces) with voxel size 0.1. Tests both getHandle() (topology-only) and getHandleAndUDF() (with sidecar). Checks: - Topology: checksum from getHandle() equals that from getHandleAndUDF(), confirming both paths produce identical grids. - Per-voxel UDF: sidecar values match CPU brute-force (index-space pointToTriangleDistSqr * voxelSize) within 1e-3 voxels. - No false positives: every active voxel lies within the narrow band. - No false negatives: every voxel with CPU UDF strictly inside the band is active (checked via leaf-iteration active set). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3b2ad8f commit a1237c8

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

nanovdb/nanovdb/unittest/TestNanoVDB.cu

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
#include <nanovdb/tools/cuda/CoarsenGrid.cuh>
2424
#include <nanovdb/tools/cuda/RefineGrid.cuh>
2525
#include <nanovdb/util/cuda/Injection.cuh>
26+
#include <nanovdb/tools/cuda/MeshToGrid.cuh>
27+
#include <nanovdb/math/Proximity.h>
2628
#include <nanovdb/util/cuda/Timer.h>
2729
#include <nanovdb/util/Timer.h>
2830
#include <nanovdb/io/IO.h>
@@ -34,6 +36,7 @@
3436
#include <cuda_runtime_api.h>
3537
#include <gtest/gtest.h>
3638
#include <algorithm>// for std::sort
39+
#include <unordered_set>
3740
#include <iomanip> // for std::setw, std::setfill
3841
#include <thread> // for std::thread
3942

@@ -3746,3 +3749,178 @@ TEST(TestNanoVDBCUDA, GridHandle_from_HostBuffer)
37463749
}
37473750
}
37483751

3752+
TEST(TestNanoVDBCUDA, MeshToGrid_EmptyMesh)
3753+
{
3754+
using BuildT = nanovdb::ValueOnIndex;
3755+
3756+
nanovdb::Map map;
3757+
map.set(0.1, nanovdb::Vec3d(0.0));
3758+
3759+
nanovdb::tools::cuda::MeshToGrid<BuildT> converter(nullptr, 0u, nullptr, 0u, map);
3760+
converter.setVerbose(0);
3761+
auto handle = converter.getHandle();
3762+
handle.deviceDownload();
3763+
const auto* grid = handle.grid<BuildT>();
3764+
ASSERT_NE(grid, nullptr);
3765+
EXPECT_EQ(grid->tree().activeVoxelCount(), 0);
3766+
}// MeshToGrid_EmptyMesh
3767+
3768+
TEST(TestNanoVDBCUDA, MeshToGrid_UnitTetrahedron)
3769+
{
3770+
using BuildT = nanovdb::ValueOnIndex;
3771+
3772+
// Unit tetrahedron: four vertices, four triangular faces.
3773+
// Vertex coordinates are in world space.
3774+
const std::vector<nanovdb::Vec3f> hostPoints = {
3775+
{0.f, 0.f, 0.f}, // p0
3776+
{1.f, 0.f, 0.f}, // p1
3777+
{0.f, 1.f, 0.f}, // p2
3778+
{0.f, 0.f, 1.f}, // p3
3779+
};
3780+
// t0: z=0 face, t1: x=0 face, t2: y=0 face, t3: diagonal face (x+y+z=1)
3781+
const std::vector<nanovdb::Vec3i> hostTriangles = {
3782+
{0, 1, 2},
3783+
{0, 2, 3},
3784+
{0, 3, 1},
3785+
{1, 2, 3},
3786+
};
3787+
3788+
auto nPoints = hostPoints.size();
3789+
auto nTriangles = hostTriangles.size();
3790+
3791+
// Upload points and triangles to device
3792+
auto pointsBuf = nanovdb::cuda::DeviceBuffer::create(nPoints * sizeof(nanovdb::Vec3f), nullptr, false);
3793+
ASSERT_TRUE(pointsBuf.deviceData());
3794+
cudaCheck(cudaMemcpy(pointsBuf.deviceData(), hostPoints.data(),
3795+
nPoints * sizeof(nanovdb::Vec3f), cudaMemcpyHostToDevice));
3796+
3797+
auto trisBuf = nanovdb::cuda::DeviceBuffer::create(nTriangles * sizeof(nanovdb::Vec3i), nullptr, false);
3798+
ASSERT_TRUE(trisBuf.deviceData());
3799+
cudaCheck(cudaMemcpy(trisBuf.deviceData(), hostTriangles.data(),
3800+
nTriangles * sizeof(nanovdb::Vec3i), cudaMemcpyHostToDevice));
3801+
3802+
auto dPoints = static_cast<const nanovdb::Vec3f*>(pointsBuf.deviceData());
3803+
auto dTriangles = static_cast<const nanovdb::Vec3i*>(trisBuf.deviceData());
3804+
3805+
// Uniform-scale map: world = dx * index
3806+
const double dx = 0.1;
3807+
nanovdb::Map map;
3808+
map.set(dx, nanovdb::Vec3d(0.0));
3809+
3810+
const float bandWidth = 3.0f; // default, in voxels
3811+
const float bandWidthWorld = bandWidth * float(dx);
3812+
3813+
// CPU brute-force UDF in index space (matches GPU arithmetic exactly):
3814+
// transform world-space verts to index space, compute pointToTriangleDistSqr
3815+
// with integer voxel centers, scale result to world space.
3816+
std::array<nanovdb::Vec3f, 4> idxVerts;
3817+
for (uint32_t i = 0; i < nPoints; ++i)
3818+
idxVerts[i] = map.applyInverseMap(hostPoints[i]);
3819+
3820+
auto cpuUDF = [&](int ix, int iy, int iz) -> float {
3821+
const nanovdb::Vec3f p{float(ix), float(iy), float(iz)};
3822+
float minDistSqr = std::numeric_limits<float>::max();
3823+
for (const auto& tri : hostTriangles) {
3824+
const float d = nanovdb::math::pointToTriangleDistSqr<nanovdb::Vec3f>(
3825+
idxVerts[tri[0]], idxVerts[tri[1]], idxVerts[tri[2]], p);
3826+
minDistSqr = std::min(minDistSqr, d);
3827+
}
3828+
return std::sqrt(minDistSqr) * float(dx); // world-space distance
3829+
};
3830+
3831+
// --- Topology-only path ---
3832+
uint64_t topoChecksum = 0;
3833+
{
3834+
nanovdb::tools::cuda::MeshToGrid<BuildT> conv(dPoints, nPoints, dTriangles, nTriangles, map);
3835+
conv.setVerbose(0);
3836+
conv.setChecksum(nanovdb::CheckMode::Full);
3837+
auto handle = conv.getHandle();
3838+
handle.deviceDownload();
3839+
const auto* topoGrid = handle.grid<BuildT>();
3840+
ASSERT_NE(topoGrid, nullptr);
3841+
topoChecksum = topoGrid->mChecksum.full();
3842+
EXPECT_GT(topoGrid->tree().activeVoxelCount(), uint64_t(0));
3843+
}
3844+
3845+
// --- UDF path ---
3846+
nanovdb::tools::cuda::MeshToGrid<BuildT> converter(dPoints, nPoints, dTriangles, nTriangles, map);
3847+
converter.setVerbose(0);
3848+
converter.setChecksum(nanovdb::CheckMode::Full);
3849+
auto [handle, sidecarBuf] = converter.getHandleAndUDF();
3850+
3851+
handle.deviceDownload();
3852+
const auto* grid = handle.grid<BuildT>();
3853+
ASSERT_NE(grid, nullptr);
3854+
3855+
const uint64_t sidecarCount = sidecarBuf.size() / sizeof(float);
3856+
std::vector<float> hostSidecar(sidecarCount);
3857+
cudaCheck(cudaMemcpy(hostSidecar.data(), sidecarBuf.deviceData(),
3858+
sidecarBuf.size(), cudaMemcpyDeviceToHost));
3859+
3860+
// Per-voxel UDF correctness: every active voxel must lie inside the narrow band
3861+
// and its sidecar distance must match the CPU brute-force value within 1e-3 voxels.
3862+
const uint32_t nLeaves = grid->tree().nodeCount(0);
3863+
const auto* leaves = grid->tree().getFirstLeaf();
3864+
uint64_t activeCount = 0;
3865+
3866+
for (uint32_t li = 0; li < nLeaves; ++li) {
3867+
const auto& leaf = leaves[li];
3868+
const auto org = leaf.origin();
3869+
for (int vi = 0; vi < 512; ++vi) {
3870+
if (!leaf.isActive(vi)) continue;
3871+
++activeCount;
3872+
3873+
const int lx = vi & 7, ly = (vi >> 3) & 7, lz = (vi >> 6) & 7;
3874+
const int ix = org[0]+lx, iy = org[1]+ly, iz = org[2]+lz;
3875+
3876+
const float exactUDF = cpuUDF(ix, iy, iz);
3877+
ASSERT_LE(exactUDF, bandWidthWorld * (1.f + 1e-5f))
3878+
<< "Active voxel at (" << ix << "," << iy << "," << iz
3879+
<< ") is outside the narrow band (distance=" << exactUDF/float(dx) << " voxels)";
3880+
3881+
const uint64_t sIdx = leaf.getValue(vi);
3882+
ASSERT_LT(sIdx, sidecarCount) << "Sidecar index out of range";
3883+
3884+
const float ourUDF = hostSidecar[sIdx];
3885+
const float errVoxels = std::abs(ourUDF - exactUDF) / float(dx);
3886+
EXPECT_LT(errVoxels, 1e-3f)
3887+
<< "UDF error at (" << ix << "," << iy << "," << iz
3888+
<< "): ours=" << ourUDF/float(dx) << " exact=" << exactUDF/float(dx) << " voxels";
3889+
}
3890+
}
3891+
EXPECT_GT(activeCount, uint64_t(0));
3892+
3893+
// Build a flat set of active coord keys from the leaf iteration
3894+
// Encode each (ix,iy,iz) as a uint64_t with 21 bits per axis, offset by 2^20.
3895+
auto encodeCoord = [](int x, int y, int z) -> uint64_t {
3896+
return (uint64_t(x + (1<<20)))
3897+
| (uint64_t(y + (1<<20)) << 21)
3898+
| (uint64_t(z + (1<<20)) << 42);
3899+
};
3900+
std::unordered_set<uint64_t> activeSet;
3901+
activeSet.reserve(activeCount);
3902+
for (uint32_t li = 0; li < nLeaves; ++li) {
3903+
const auto& leaf = leaves[li];
3904+
const auto org = leaf.origin();
3905+
for (int vi = 0; vi < 512; ++vi) {
3906+
if (!leaf.isActive(vi)) continue;
3907+
const int lx = vi & 7, ly = (vi >> 3) & 7, lz = (vi >> 6) & 7;
3908+
activeSet.insert(encodeCoord(org[0]+lx, org[1]+ly, org[2]+lz));
3909+
}
3910+
}
3911+
3912+
// No false negatives: every voxel with CPU UDF strictly inside the band must be active.
3913+
const int ilo = (int)std::floor(-bandWidth) - 1;
3914+
const int ihi = (int)std::ceil(1.0 / dx + bandWidth) + 1;
3915+
uint64_t missedCount = 0;
3916+
for (int ix = ilo; ix <= ihi; ++ix)
3917+
for (int iy = ilo; iy <= ihi; ++iy)
3918+
for (int iz = ilo; iz <= ihi; ++iz)
3919+
if (cpuUDF(ix, iy, iz) < bandWidthWorld)
3920+
if (activeSet.count(encodeCoord(ix, iy, iz)) == 0)
3921+
++missedCount;
3922+
EXPECT_EQ(missedCount, uint64_t(0));
3923+
3924+
// getHandle() and getHandleAndUDF() must produce identical grids.
3925+
EXPECT_EQ(grid->mChecksum.full(), topoChecksum);
3926+
}// MeshToGrid_UnitTetrahedron

0 commit comments

Comments
 (0)