Fix NanoVDB volume loading and rendering

This commit is contained in:
2026-04-09 01:11:59 +08:00
parent b839fd98af
commit fde99a4d34
13 changed files with 628024 additions and 55 deletions

View File

@@ -3,33 +3,100 @@
#include <XCEngine/Core/Asset/AssetDatabase.h>
#include <XCEngine/Core/Asset/AssetRef.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Math/Bounds.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Resources/Volume/VolumeField.h>
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
#if defined(XCENGINE_HAS_NANOVDB)
#include <nanovdb/GridHandle.h>
#include <nanovdb/HostBuffer.h>
#include <nanovdb/io/IO.h>
#endif
#include <chrono>
#include <filesystem>
#include <fstream>
#include <thread>
#include <vector>
using namespace XCEngine::Resources;
namespace {
std::vector<unsigned char> MakeTestNanoVDBPayload() {
return {
0x4E, 0x56, 0x44, 0x42,
0x10, 0x20, 0x30, 0x40,
0x01, 0x03, 0x05, 0x07,
0xAA, 0xBB, 0xCC, 0xDD
};
struct GeneratedNanoVDBVolume {
size_t payloadSize = 0u;
XCEngine::Math::Bounds bounds;
XCEngine::Math::Vector3 voxelSize = XCEngine::Math::Vector3::Zero();
VolumeIndexBounds indexBounds = {};
XCEngine::Core::uint32 gridType = 0u;
XCEngine::Core::uint32 gridClass = 0u;
};
std::filesystem::path GetCloudVolumePath() {
return std::filesystem::path(XCENGINE_TEST_CLOUD_NVDB_PATH);
}
void WriteBinaryFile(const std::filesystem::path& path, const std::vector<unsigned char>& bytes) {
std::ofstream output(path, std::ios::binary | std::ios::trunc);
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
void ExpectVector3Near(
const XCEngine::Math::Vector3& actual,
const XCEngine::Math::Vector3& expected,
float epsilon = 1e-4f) {
EXPECT_NEAR(actual.x, expected.x, epsilon);
EXPECT_NEAR(actual.y, expected.y, epsilon);
EXPECT_NEAR(actual.z, expected.z, epsilon);
}
void ExpectIndexBoundsEq(
const VolumeIndexBounds& actual,
const VolumeIndexBounds& expected) {
EXPECT_EQ(actual.minX, expected.minX);
EXPECT_EQ(actual.minY, expected.minY);
EXPECT_EQ(actual.minZ, expected.minZ);
EXPECT_EQ(actual.maxX, expected.maxX);
EXPECT_EQ(actual.maxY, expected.maxY);
EXPECT_EQ(actual.maxZ, expected.maxZ);
}
#if defined(XCENGINE_HAS_NANOVDB)
XCEngine::Math::Vector3 ToVector3(const nanovdb::Vec3d& value) {
return XCEngine::Math::Vector3(
static_cast<float>(value[0]),
static_cast<float>(value[1]),
static_cast<float>(value[2]));
}
VolumeIndexBounds ToIndexBounds(const nanovdb::CoordBBox& value) {
VolumeIndexBounds bounds = {};
bounds.minX = value.min()[0];
bounds.minY = value.min()[1];
bounds.minZ = value.min()[2];
bounds.maxX = value.max()[0];
bounds.maxY = value.max()[1];
bounds.maxZ = value.max()[2];
return bounds;
}
GeneratedNanoVDBVolume ReadTestNanoVDBFileMetadata(const std::filesystem::path& path) {
nanovdb::GridHandle<nanovdb::HostBuffer> handle =
nanovdb::io::readGrid<nanovdb::HostBuffer>(path.string());
GeneratedNanoVDBVolume generated;
generated.payloadSize = static_cast<size_t>(handle.bufferSize());
const nanovdb::GridMetaData* metadata = handle.gridMetaData();
if (metadata != nullptr) {
generated.bounds.SetMinMax(
ToVector3(metadata->worldBBox().min()),
ToVector3(metadata->worldBBox().max()));
generated.voxelSize = ToVector3(metadata->voxelSize());
generated.indexBounds = ToIndexBounds(metadata->indexBBox());
generated.gridType = static_cast<XCEngine::Core::uint32>(metadata->gridType());
generated.gridClass = static_cast<XCEngine::Core::uint32>(metadata->gridClass());
}
return generated;
}
#endif
TEST(VolumeFieldLoader, GetResourceType) {
VolumeFieldLoader loader;
EXPECT_EQ(loader.GetResourceType(), ResourceType::VolumeField);
@@ -48,12 +115,14 @@ TEST(VolumeFieldLoader, LoadInvalidPath) {
EXPECT_FALSE(result);
}
TEST(VolumeFieldLoader, LoadSourceNanoVDBBlob) {
#if defined(XCENGINE_HAS_NANOVDB)
TEST(VolumeFieldLoader, LoadSourceNanoVDBGridPayload) {
namespace fs = std::filesystem;
const fs::path volumePath = fs::temp_directory_path() / "xc_volume_loader_source_test.nvdb";
const std::vector<unsigned char> bytes = MakeTestNanoVDBPayload();
WriteBinaryFile(volumePath, bytes);
const fs::path volumePath = GetCloudVolumePath();
ASSERT_TRUE(fs::exists(volumePath));
const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(volumePath);
VolumeFieldLoader loader;
LoadResult result = loader.Load(volumePath.string().c_str());
@@ -62,13 +131,19 @@ TEST(VolumeFieldLoader, LoadSourceNanoVDBBlob) {
auto* volumeField = static_cast<VolumeField*>(result.resource);
EXPECT_EQ(volumeField->GetStorageKind(), VolumeStorageKind::NanoVDB);
EXPECT_EQ(volumeField->GetPayloadSize(), bytes.size());
EXPECT_EQ(static_cast<const unsigned char*>(volumeField->GetPayloadData())[0], bytes[0]);
EXPECT_EQ(volumeField->GetPayloadSize(), generated.payloadSize);
ExpectVector3Near(volumeField->GetBounds().GetMin(), generated.bounds.GetMin());
ExpectVector3Near(volumeField->GetBounds().GetMax(), generated.bounds.GetMax());
ExpectVector3Near(volumeField->GetVoxelSize(), generated.voxelSize);
ExpectIndexBoundsEq(volumeField->GetIndexBounds(), generated.indexBounds);
EXPECT_EQ(volumeField->GetGridType(), generated.gridType);
EXPECT_EQ(volumeField->GetGridClass(), generated.gridClass);
delete volumeField;
fs::remove(volumePath);
}
#endif
#if defined(XCENGINE_HAS_NANOVDB)
TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReimport) {
namespace fs = std::filesystem;
using namespace std::chrono_literals;
@@ -76,10 +151,13 @@ TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReim
const fs::path projectRoot = fs::temp_directory_path() / "xc_volume_library_cache_test";
const fs::path assetsDir = projectRoot / "Assets";
const fs::path volumePath = assetsDir / "cloud.nvdb";
const fs::path fixturePath = GetCloudVolumePath();
fs::remove_all(projectRoot);
fs::create_directories(assetsDir);
WriteBinaryFile(volumePath, MakeTestNanoVDBPayload());
ASSERT_TRUE(fs::exists(fixturePath));
fs::copy_file(fixturePath, volumePath, fs::copy_options::overwrite_existing);
const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(fixturePath);
AssetDatabase database;
database.Initialize(projectRoot.string().c_str());
@@ -97,7 +175,13 @@ TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReim
ASSERT_NE(artifactLoad.resource, nullptr);
auto* artifactVolume = static_cast<VolumeField*>(artifactLoad.resource);
EXPECT_EQ(artifactVolume->GetStorageKind(), VolumeStorageKind::NanoVDB);
EXPECT_EQ(artifactVolume->GetPayloadSize(), MakeTestNanoVDBPayload().size());
EXPECT_EQ(artifactVolume->GetPayloadSize(), generated.payloadSize);
ExpectVector3Near(artifactVolume->GetBounds().GetMin(), generated.bounds.GetMin());
ExpectVector3Near(artifactVolume->GetBounds().GetMax(), generated.bounds.GetMax());
ExpectVector3Near(artifactVolume->GetVoxelSize(), generated.voxelSize);
ExpectIndexBoundsEq(artifactVolume->GetIndexBounds(), generated.indexBounds);
EXPECT_EQ(artifactVolume->GetGridType(), generated.gridType);
EXPECT_EQ(artifactVolume->GetGridClass(), generated.gridClass);
delete artifactVolume;
const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr());
@@ -111,7 +195,13 @@ TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReim
database.Shutdown();
fs::remove_all(projectRoot);
}
#else
TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReimport) {
GTEST_SKIP() << "NanoVDB headers are unavailable in this build";
}
#endif
#if defined(XCENGINE_HAS_NANOVDB)
TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
namespace fs = std::filesystem;
@@ -121,11 +211,13 @@ TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
const fs::path projectRoot = fs::temp_directory_path() / "xc_volume_asset_ref_test";
const fs::path assetsDir = projectRoot / "Assets";
const fs::path volumePath = assetsDir / "cloud.nvdb";
const std::vector<unsigned char> bytes = MakeTestNanoVDBPayload();
const fs::path fixturePath = GetCloudVolumePath();
fs::remove_all(projectRoot);
fs::create_directories(assetsDir);
WriteBinaryFile(volumePath, bytes);
ASSERT_TRUE(fs::exists(fixturePath));
fs::copy_file(fixturePath, volumePath, fs::copy_options::overwrite_existing);
const GeneratedNanoVDBVolume generated = ReadTestNanoVDBFileMetadata(fixturePath);
manager.SetResourceRoot(projectRoot.string().c_str());
@@ -133,7 +225,13 @@ TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
const auto firstHandle = manager.Load<VolumeField>("Assets/cloud.nvdb");
ASSERT_TRUE(firstHandle.IsValid());
EXPECT_EQ(firstHandle->GetStorageKind(), VolumeStorageKind::NanoVDB);
EXPECT_EQ(firstHandle->GetPayloadSize(), bytes.size());
EXPECT_EQ(firstHandle->GetPayloadSize(), generated.payloadSize);
ExpectVector3Near(firstHandle->GetBounds().GetMin(), generated.bounds.GetMin());
ExpectVector3Near(firstHandle->GetBounds().GetMax(), generated.bounds.GetMax());
ExpectVector3Near(firstHandle->GetVoxelSize(), generated.voxelSize);
ExpectIndexBoundsEq(firstHandle->GetIndexBounds(), generated.indexBounds);
EXPECT_EQ(firstHandle->GetGridType(), generated.gridType);
EXPECT_EQ(firstHandle->GetGridClass(), generated.gridClass);
AssetRef assetRef;
ASSERT_TRUE(manager.TryGetAssetRef("Assets/cloud.nvdb", ResourceType::VolumeField, assetRef));
@@ -144,12 +242,23 @@ TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
const auto secondHandle = manager.Load<VolumeField>(assetRef);
ASSERT_TRUE(secondHandle.IsValid());
EXPECT_EQ(secondHandle->GetStorageKind(), VolumeStorageKind::NanoVDB);
EXPECT_EQ(secondHandle->GetPayloadSize(), bytes.size());
EXPECT_EQ(secondHandle->GetPayloadSize(), generated.payloadSize);
ExpectVector3Near(secondHandle->GetBounds().GetMin(), generated.bounds.GetMin());
ExpectVector3Near(secondHandle->GetBounds().GetMax(), generated.bounds.GetMax());
ExpectVector3Near(secondHandle->GetVoxelSize(), generated.voxelSize);
ExpectIndexBoundsEq(secondHandle->GetIndexBounds(), generated.indexBounds);
EXPECT_EQ(secondHandle->GetGridType(), generated.gridType);
EXPECT_EQ(secondHandle->GetGridClass(), generated.gridClass);
}
manager.SetResourceRoot("");
manager.Shutdown();
fs::remove_all(projectRoot);
}
#else
TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) {
GTEST_SKIP() << "NanoVDB headers are unavailable in this build";
}
#endif
} // namespace