|
23 | 23 | #include <nanovdb/tools/cuda/CoarsenGrid.cuh> |
24 | 24 | #include <nanovdb/tools/cuda/RefineGrid.cuh> |
25 | 25 | #include <nanovdb/util/cuda/Injection.cuh> |
| 26 | +#include <nanovdb/tools/cuda/MeshToGrid.cuh> |
| 27 | +#include <nanovdb/math/Proximity.h> |
26 | 28 | #include <nanovdb/util/cuda/Timer.h> |
27 | 29 | #include <nanovdb/util/Timer.h> |
28 | 30 | #include <nanovdb/io/IO.h> |
|
34 | 36 | #include <cuda_runtime_api.h> |
35 | 37 | #include <gtest/gtest.h> |
36 | 38 | #include <algorithm>// for std::sort |
| 39 | +#include <unordered_set> |
37 | 40 | #include <iomanip> // for std::setw, std::setfill |
38 | 41 | #include <thread> // for std::thread |
39 | 42 |
|
@@ -3746,3 +3749,178 @@ TEST(TestNanoVDBCUDA, GridHandle_from_HostBuffer) |
3746 | 3749 | } |
3747 | 3750 | } |
3748 | 3751 |
|
| 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