From c6815fa80901ea5d3844b72cc819cae2302b1e87 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 8 Apr 2026 19:45:53 +0800 Subject: [PATCH] Add VolumeField NanoVDB asset pipeline --- engine/CMakeLists.txt | 4 + .../XCEngine/Core/Asset/ArtifactFormats.h | 13 ++ .../XCEngine/Core/Asset/AssetDatabase.h | 2 + .../XCEngine/Core/Asset/ResourceTypes.h | 5 +- engine/include/XCEngine/Resources/Resources.h | 2 + .../XCEngine/Resources/Volume/VolumeField.h | 52 ++++++ .../Resources/Volume/VolumeFieldLoader.h | 21 +++ engine/src/Core/Asset/AssetDatabase.cpp | 84 ++++++++++ engine/src/Core/Asset/ResourceManager.cpp | 3 + engine/src/Resources/Volume/VolumeField.cpp | 43 +++++ .../Resources/Volume/VolumeFieldLoader.cpp | 144 ++++++++++++++++ tests/Core/Asset/test_resource_types.cpp | 3 + tests/Resources/CMakeLists.txt | 1 + tests/Resources/Volume/CMakeLists.txt | 35 ++++ tests/Resources/Volume/test_volume_field.cpp | 42 +++++ .../Volume/test_volume_field_loader.cpp | 155 ++++++++++++++++++ 16 files changed, 608 insertions(+), 1 deletion(-) create mode 100644 engine/include/XCEngine/Resources/Volume/VolumeField.h create mode 100644 engine/include/XCEngine/Resources/Volume/VolumeFieldLoader.h create mode 100644 engine/src/Resources/Volume/VolumeField.cpp create mode 100644 engine/src/Resources/Volume/VolumeFieldLoader.cpp create mode 100644 tests/Resources/Volume/CMakeLists.txt create mode 100644 tests/Resources/Volume/test_volume_field.cpp create mode 100644 tests/Resources/Volume/test_volume_field_loader.cpp diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index dcaa939d..e4d113b2 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -370,6 +370,8 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Material/MaterialLoader.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Shader/Shader.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Shader/ShaderLoader.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Volume/VolumeField.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Volume/VolumeFieldLoader.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/AudioClip/AudioClip.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/AudioClip/AudioLoader.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/UI/UIDocumentTypes.h @@ -405,6 +407,8 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderSourceUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderAuthoringParser.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderAuthoringParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Volume/VolumeField.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Volume/VolumeFieldLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioClip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp ${XCENGINE_OPTIONAL_UI_RESOURCE_SOURCES} diff --git a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h index bfc378a4..b095c7e5 100644 --- a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h +++ b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h @@ -15,6 +15,7 @@ constexpr Core::uint32 kMaterialArtifactSchemaVersion = 6; constexpr Core::uint32 kMeshArtifactSchemaVersion = 2; constexpr Core::uint32 kShaderArtifactSchemaVersion = 5; constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2; +constexpr Core::uint32 kVolumeFieldArtifactSchemaVersion = 1; struct TextureArtifactHeader { char magic[8] = { 'X', 'C', 'T', 'E', 'X', '0', '1', '\0' }; @@ -129,5 +130,17 @@ struct UIDocumentArtifactDiagnosticHeader { Core::uint32 column = 1; }; +struct VolumeFieldArtifactHeader { + char magic[8] = { 'X', 'C', 'V', 'O', 'L', '0', '1', '\0' }; + Core::uint32 schemaVersion = kVolumeFieldArtifactSchemaVersion; + Core::uint32 storageKind = 0; + Math::Vector3 boundsMin = Math::Vector3::Zero(); + Math::Vector3 boundsMax = Math::Vector3::Zero(); + Math::Vector3 voxelSize = Math::Vector3::Zero(); + Core::uint64 payloadSize = 0; + Core::uint32 reserved0 = 0; + Core::uint32 reserved1 = 0; +}; + } // namespace Resources } // namespace XCEngine diff --git a/engine/include/XCEngine/Core/Asset/AssetDatabase.h b/engine/include/XCEngine/Core/Asset/AssetDatabase.h index 1fae970f..04e38d8b 100644 --- a/engine/include/XCEngine/Core/Asset/AssetDatabase.h +++ b/engine/include/XCEngine/Core/Asset/AssetDatabase.h @@ -139,6 +139,8 @@ private: ArtifactRecord& outRecord); bool ImportShaderAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord); + bool ImportVolumeFieldAsset(const SourceAssetRecord& sourceRecord, + ArtifactRecord& outRecord); bool ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord, UIDocumentKind kind, const char* artifactFileName, diff --git a/engine/include/XCEngine/Core/Asset/ResourceTypes.h b/engine/include/XCEngine/Core/Asset/ResourceTypes.h index 4d26e282..87a2f25c 100644 --- a/engine/include/XCEngine/Core/Asset/ResourceTypes.h +++ b/engine/include/XCEngine/Core/Asset/ResourceTypes.h @@ -25,7 +25,8 @@ enum class ResourceType : Core::uint8 { Prefab, UIView, UITheme, - UISchema + UISchema, + VolumeField }; constexpr const char* GetResourceTypeName(ResourceType type) { @@ -45,6 +46,7 @@ constexpr const char* GetResourceTypeName(ResourceType type) { case ResourceType::UIView: return "UIView"; case ResourceType::UITheme: return "UITheme"; case ResourceType::UISchema: return "UISchema"; + case ResourceType::VolumeField: return "VolumeField"; default: return "Unknown"; } } @@ -98,6 +100,7 @@ template<> inline ResourceType GetResourceType() { return template<> inline ResourceType GetResourceType() { return ResourceType::UIView; } template<> inline ResourceType GetResourceType() { return ResourceType::UITheme; } template<> inline ResourceType GetResourceType() { return ResourceType::UISchema; } +template<> inline ResourceType GetResourceType() { return ResourceType::VolumeField; } } // namespace Resources } // namespace XCEngine diff --git a/engine/include/XCEngine/Resources/Resources.h b/engine/include/XCEngine/Resources/Resources.h index 5ebb4200..f91ac02d 100644 --- a/engine/include/XCEngine/Resources/Resources.h +++ b/engine/include/XCEngine/Resources/Resources.h @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/engine/include/XCEngine/Resources/Volume/VolumeField.h b/engine/include/XCEngine/Resources/Volume/VolumeField.h new file mode 100644 index 00000000..d54e1fd0 --- /dev/null +++ b/engine/include/XCEngine/Resources/Volume/VolumeField.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Resources { + +enum class VolumeStorageKind : Core::uint32 { + Unknown = 0, + NanoVDB = 1 +}; + +class VolumeField : public IResource { +public: + VolumeField(); + virtual ~VolumeField() override; + + ResourceType GetType() const override { return ResourceType::VolumeField; } + const Containers::String& GetName() const override { return m_name; } + const Containers::String& GetPath() const override { return m_path; } + ResourceGUID GetGUID() const override { return m_guid; } + bool IsValid() const override { return m_isValid; } + size_t GetMemorySize() const override { return m_memorySize; } + void Release() override; + + bool Create(VolumeStorageKind storageKind, + const void* payload, + size_t payloadSize, + const Math::Bounds& bounds = Math::Bounds(), + const Math::Vector3& voxelSize = Math::Vector3::Zero()); + + VolumeStorageKind GetStorageKind() const { return m_storageKind; } + const Math::Bounds& GetBounds() const { return m_bounds; } + const Math::Vector3& GetVoxelSize() const { return m_voxelSize; } + const void* GetPayloadData() const { return m_payload.Data(); } + size_t GetPayloadSize() const { return m_payload.Size(); } + +private: + void UpdateMemorySize(); + + VolumeStorageKind m_storageKind = VolumeStorageKind::Unknown; + Math::Bounds m_bounds; + Math::Vector3 m_voxelSize = Math::Vector3::Zero(); + Containers::Array m_payload; +}; + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/include/XCEngine/Resources/Volume/VolumeFieldLoader.h b/engine/include/XCEngine/Resources/Volume/VolumeFieldLoader.h new file mode 100644 index 00000000..dac8380c --- /dev/null +++ b/engine/include/XCEngine/Resources/Volume/VolumeFieldLoader.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace XCEngine { +namespace Resources { + +class VolumeFieldLoader : public IResourceLoader { +public: + VolumeFieldLoader(); + virtual ~VolumeFieldLoader() override; + + ResourceType GetResourceType() const override { return ResourceType::VolumeField; } + Containers::Array GetSupportedExtensions() const override; + bool CanLoad(const Containers::String& path) const override; + LoadResult Load(const Containers::String& path, const ImportSettings* settings = nullptr) override; + ImportSettings* GetDefaultSettings() const override; +}; + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index bcc5ba88..0160a683 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include @@ -366,6 +368,27 @@ bool WriteTextureArtifactFile(const fs::path& artifactPath, const Texture& textu return static_cast(output); } +bool WriteVolumeFieldArtifactFile(const fs::path& artifactPath, const VolumeField& volumeField) { + std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + return false; + } + + VolumeFieldArtifactHeader header; + header.storageKind = static_cast(volumeField.GetStorageKind()); + header.boundsMin = volumeField.GetBounds().GetMin(); + header.boundsMax = volumeField.GetBounds().GetMax(); + header.voxelSize = volumeField.GetVoxelSize(); + header.payloadSize = static_cast(volumeField.GetPayloadSize()); + + output.write(reinterpret_cast(&header), sizeof(header)); + if (volumeField.GetPayloadSize() > 0) { + output.write(static_cast(volumeField.GetPayloadData()), volumeField.GetPayloadSize()); + } + + return static_cast(output); +} + std::vector GatherMaterialProperties(const Material& material) { return material.GetProperties(); } @@ -1258,6 +1281,7 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, const fs::path metaPath(sourcePath.string() + ".meta"); outRecord.metaPath = NormalizeRelativePath(metaPath); + const Containers::String expectedImporterName = GetImporterNameForPath(relativePath, isFolder); bool shouldRewriteMeta = false; if (!fs::exists(metaPath) || !ReadMetaFile(metaPath, outRecord) || !outRecord.guid.IsValid()) { @@ -1266,6 +1290,10 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, } shouldRewriteMeta = true; } + if (outRecord.importerName != expectedImporterName) { + outRecord.importerName = expectedImporterName; + shouldRewriteMeta = true; + } if (outRecord.importerVersion != kCurrentImporterVersion) { outRecord.importerVersion = kCurrentImporterVersion; shouldRewriteMeta = true; @@ -1409,6 +1437,9 @@ Containers::String AssetDatabase::GetImporterNameForPath(const Containers::Strin if (ext == ".shader") { return Containers::String("ShaderImporter"); } + if (ext == ".nvdb") { + return Containers::String("VolumeFieldImporter"); + } if (ext == ".mat" || ext == ".material" || ext == ".json") { return Containers::String("MaterialImporter"); } @@ -1437,6 +1468,9 @@ ResourceType AssetDatabase::GetPrimaryResourceTypeForImporter(const Containers:: if (importerName == "ShaderImporter") { return ResourceType::Shader; } + if (importerName == "VolumeFieldImporter") { + return ResourceType::VolumeField; + } return ResourceType::Unknown; } @@ -1487,6 +1521,8 @@ bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord, return ImportModelAsset(sourceRecord, outRecord); case ResourceType::Shader: return ImportShaderAsset(sourceRecord, outRecord); + case ResourceType::VolumeField: + return ImportVolumeFieldAsset(sourceRecord, outRecord); default: SetLastErrorMessage(Containers::String("No importer available for asset: ") + sourceRecord.relativePath); return false; @@ -1875,6 +1911,54 @@ bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord, return true; } +bool AssetDatabase::ImportVolumeFieldAsset(const SourceAssetRecord& sourceRecord, + ArtifactRecord& outRecord) { + VolumeFieldLoader loader; + const Containers::String absolutePath = + NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr()); + LoadResult result = loader.Load(absolutePath); + if (!result || result.resource == nullptr) { + return false; + } + + VolumeField* volumeField = static_cast(result.resource); + const Containers::String artifactKey = BuildArtifactKey(sourceRecord); + const Containers::String artifactDir = BuildArtifactDirectory(artifactKey); + const Containers::String mainArtifactPath = + NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcvol"); + + std::error_code ec; + fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + ec.clear(); + fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec); + if (ec) { + delete volumeField; + return false; + } + + const bool writeOk = + WriteVolumeFieldArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *volumeField); + delete volumeField; + if (!writeOk) { + return false; + } + + outRecord.artifactKey = artifactKey; + outRecord.assetGuid = sourceRecord.guid; + outRecord.importerName = sourceRecord.importerName; + outRecord.importerVersion = sourceRecord.importerVersion; + outRecord.resourceType = ResourceType::VolumeField; + outRecord.artifactDirectory = artifactDir; + outRecord.mainArtifactPath = mainArtifactPath; + outRecord.sourceHash = sourceRecord.sourceHash; + outRecord.metaHash = sourceRecord.metaHash; + outRecord.sourceFileSize = sourceRecord.sourceFileSize; + outRecord.sourceWriteTime = sourceRecord.sourceWriteTime; + outRecord.mainLocalID = kMainAssetLocalID; + outRecord.dependencies.clear(); + return true; +} + bool AssetDatabase::ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord, UIDocumentKind kind, const char* artifactFileName, diff --git a/engine/src/Core/Asset/ResourceManager.cpp b/engine/src/Core/Asset/ResourceManager.cpp index cd816ac4..58b6de3f 100644 --- a/engine/src/Core/Asset/ResourceManager.cpp +++ b/engine/src/Core/Asset/ResourceManager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace XCEngine { @@ -49,6 +50,7 @@ TextureLoader g_textureLoader; UIViewLoader g_uiViewLoader; UIThemeLoader g_uiThemeLoader; UISchemaLoader g_uiSchemaLoader; +VolumeFieldLoader g_volumeFieldLoader; } // namespace @@ -92,6 +94,7 @@ void ResourceManager::EnsureInitialized() { RegisterBuiltinLoader(*this, g_uiViewLoader); RegisterBuiltinLoader(*this, g_uiThemeLoader); RegisterBuiltinLoader(*this, g_uiSchemaLoader); + RegisterBuiltinLoader(*this, g_volumeFieldLoader); m_assetImportService.Initialize(); m_asyncLoader = std::move(asyncLoader); diff --git a/engine/src/Resources/Volume/VolumeField.cpp b/engine/src/Resources/Volume/VolumeField.cpp new file mode 100644 index 00000000..71c667bf --- /dev/null +++ b/engine/src/Resources/Volume/VolumeField.cpp @@ -0,0 +1,43 @@ +#include + +#include + +namespace XCEngine { +namespace Resources { + +VolumeField::VolumeField() = default; + +VolumeField::~VolumeField() = default; + +void VolumeField::Release() { + delete this; +} + +bool VolumeField::Create(VolumeStorageKind storageKind, + const void* payload, + size_t payloadSize, + const Math::Bounds& bounds, + const Math::Vector3& voxelSize) { + if (payload == nullptr || payloadSize == 0) { + return false; + } + + m_storageKind = storageKind; + m_bounds = bounds; + m_voxelSize = voxelSize; + m_payload.Resize(payloadSize); + std::memcpy(m_payload.Data(), payload, payloadSize); + m_isValid = true; + UpdateMemorySize(); + return true; +} + +void VolumeField::UpdateMemorySize() { + m_memorySize = sizeof(VolumeField) + + m_name.Length() + + m_path.Length() + + m_payload.Size(); +} + +} // namespace Resources +} // namespace XCEngine diff --git a/engine/src/Resources/Volume/VolumeFieldLoader.cpp b/engine/src/Resources/Volume/VolumeFieldLoader.cpp new file mode 100644 index 00000000..9e26312e --- /dev/null +++ b/engine/src/Resources/Volume/VolumeFieldLoader.cpp @@ -0,0 +1,144 @@ +#include + +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Resources { + +namespace { + +Containers::String GetResourceNameFromPath(const Containers::String& path) { + const std::filesystem::path filePath(path.CStr()); + const std::string fileName = filePath.filename().string(); + if (!fileName.empty()) { + return Containers::String(fileName.c_str()); + } + return path; +} + +LoadResult CreateVolumeFieldResource(const Containers::String& path, + VolumeStorageKind storageKind, + const Math::Bounds& bounds, + const Math::Vector3& voxelSize, + const void* payload, + size_t payloadSize) { + auto* volumeField = new VolumeField(); + + IResource::ConstructParams params; + params.name = GetResourceNameFromPath(path); + params.path = path; + params.guid = ResourceGUID::Generate(path); + params.memorySize = payloadSize; + volumeField->Initialize(params); + + if (!volumeField->Create(storageKind, payload, payloadSize, bounds, voxelSize)) { + delete volumeField; + return LoadResult(Containers::String("Failed to create volume field resource: ") + path); + } + + return LoadResult(volumeField); +} + +LoadResult LoadVolumeFieldArtifact(const Containers::String& path) { + std::filesystem::path resolvedPath(path.CStr()); + if (!resolvedPath.is_absolute() && !std::filesystem::exists(resolvedPath)) { + const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); + if (!resourceRoot.Empty()) { + resolvedPath = std::filesystem::path(resourceRoot.CStr()) / resolvedPath; + } + } + + std::ifstream input(resolvedPath, std::ios::binary); + if (!input.is_open()) { + return LoadResult(Containers::String("Failed to read volume artifact: ") + path); + } + + VolumeFieldArtifactHeader header; + input.read(reinterpret_cast(&header), sizeof(header)); + if (!input) { + return LoadResult(Containers::String("Failed to parse volume artifact header: ") + path); + } + + const bool validHeader = + std::memcmp(header.magic, "XCVOL01", 7) == 0 && + header.schemaVersion == kVolumeFieldArtifactSchemaVersion && + header.payloadSize > 0; + if (!validHeader) { + return LoadResult(Containers::String("Invalid volume artifact header: ") + path); + } + + Containers::Array payload; + payload.Resize(static_cast(header.payloadSize)); + input.read(reinterpret_cast(payload.Data()), static_cast(header.payloadSize)); + if (!input) { + return LoadResult(Containers::String("Failed to read volume artifact payload: ") + path); + } + + Math::Bounds bounds; + bounds.SetMinMax(header.boundsMin, header.boundsMax); + + return CreateVolumeFieldResource(path, + static_cast(header.storageKind), + bounds, + header.voxelSize, + payload.Data(), + payload.Size()); +} + +} // namespace + +VolumeFieldLoader::VolumeFieldLoader() = default; + +VolumeFieldLoader::~VolumeFieldLoader() = default; + +Containers::Array VolumeFieldLoader::GetSupportedExtensions() const { + Containers::Array extensions; + extensions.PushBack("nvdb"); + extensions.PushBack("xcvol"); + return extensions; +} + +bool VolumeFieldLoader::CanLoad(const Containers::String& path) const { + const Containers::String ext = GetExtension(path).ToLower(); + return ext == "nvdb" || ext == "xcvol"; +} + +LoadResult VolumeFieldLoader::Load(const Containers::String& path, const ImportSettings* settings) { + (void)settings; + + if (!CanLoad(path)) { + return LoadResult(Containers::String("Unsupported volume format: ") + GetExtension(path).ToLower()); + } + + const Containers::String ext = GetExtension(path).ToLower(); + if (ext == "xcvol") { + return LoadVolumeFieldArtifact(path); + } + + Containers::Array payload = ReadFileData(path); + if (payload.Empty()) { + return LoadResult(Containers::String("Failed to read file: ") + path); + } + + return CreateVolumeFieldResource(path, + VolumeStorageKind::NanoVDB, + Math::Bounds(), + Math::Vector3::Zero(), + payload.Data(), + payload.Size()); +} + +ImportSettings* VolumeFieldLoader::GetDefaultSettings() const { + return nullptr; +} + +REGISTER_RESOURCE_LOADER(VolumeFieldLoader); + +} // namespace Resources +} // namespace XCEngine diff --git a/tests/Core/Asset/test_resource_types.cpp b/tests/Core/Asset/test_resource_types.cpp index ae6be4e1..1171fdab 100644 --- a/tests/Core/Asset/test_resource_types.cpp +++ b/tests/Core/Asset/test_resource_types.cpp @@ -24,6 +24,7 @@ TEST(Resources_Types, ResourceType_EnumValues) { EXPECT_EQ(static_cast(ResourceType::UIView), 13); EXPECT_EQ(static_cast(ResourceType::UITheme), 14); EXPECT_EQ(static_cast(ResourceType::UISchema), 15); + EXPECT_EQ(static_cast(ResourceType::VolumeField), 16); } TEST(Resources_Types, GetResourceTypeName) { @@ -33,6 +34,7 @@ TEST(Resources_Types, GetResourceTypeName) { EXPECT_STREQ(GetResourceTypeName(ResourceType::UIView), "UIView"); EXPECT_STREQ(GetResourceTypeName(ResourceType::UITheme), "UITheme"); EXPECT_STREQ(GetResourceTypeName(ResourceType::UISchema), "UISchema"); + EXPECT_STREQ(GetResourceTypeName(ResourceType::VolumeField), "VolumeField"); EXPECT_STREQ(GetResourceTypeName(ResourceType::Unknown), "Unknown"); } @@ -46,6 +48,7 @@ TEST(Resources_Types, GetResourceType_TemplateSpecializations) { EXPECT_EQ(GetResourceType(), ResourceType::UIView); EXPECT_EQ(GetResourceType(), ResourceType::UITheme); EXPECT_EQ(GetResourceType(), ResourceType::UISchema); + EXPECT_EQ(GetResourceType(), ResourceType::VolumeField); } } // namespace diff --git a/tests/Resources/CMakeLists.txt b/tests/Resources/CMakeLists.txt index 9f6309e1..dbc6174a 100644 --- a/tests/Resources/CMakeLists.txt +++ b/tests/Resources/CMakeLists.txt @@ -8,3 +8,4 @@ add_subdirectory(Material) add_subdirectory(Shader) add_subdirectory(AudioClip) add_subdirectory(UI) +add_subdirectory(Volume) diff --git a/tests/Resources/Volume/CMakeLists.txt b/tests/Resources/Volume/CMakeLists.txt new file mode 100644 index 00000000..1af2f3ed --- /dev/null +++ b/tests/Resources/Volume/CMakeLists.txt @@ -0,0 +1,35 @@ +# ============================================================ +# Volume Tests +# ============================================================ + +set(VOLUME_TEST_SOURCES + test_volume_field.cpp + test_volume_field_loader.cpp +) + +add_executable(volume_tests ${VOLUME_TEST_SOURCES}) + +if(MSVC) + set_target_properties(volume_tests PROPERTIES + LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" + ) +endif() + +target_link_libraries(volume_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main +) + +target_include_directories(volume_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/tests/Fixtures +) + +target_compile_definitions(volume_tests PRIVATE + XCENGINE_TEST_FIXTURES_DIR="${CMAKE_SOURCE_DIR}/tests/Fixtures" +) + +include(GoogleTest) +gtest_discover_tests(volume_tests) diff --git a/tests/Resources/Volume/test_volume_field.cpp b/tests/Resources/Volume/test_volume_field.cpp new file mode 100644 index 00000000..4b0930f6 --- /dev/null +++ b/tests/Resources/Volume/test_volume_field.cpp @@ -0,0 +1,42 @@ +#include + +#include + +using namespace XCEngine::Resources; + +namespace { + +TEST(VolumeField, CreatePreservesPayloadAndMetadata) { + const unsigned char payload[] = { 1, 2, 3, 4, 5, 6 }; + + VolumeField volumeField; + IResource::ConstructParams params; + params.name = "cloud.nvdb"; + params.path = "Assets/cloud.nvdb"; + params.guid = ResourceGUID::Generate(params.path); + volumeField.Initialize(params); + + XCEngine::Math::Bounds bounds; + bounds.SetMinMax( + XCEngine::Math::Vector3(-1.0f, -2.0f, -3.0f), + XCEngine::Math::Vector3(4.0f, 5.0f, 6.0f)); + + ASSERT_TRUE(volumeField.Create( + VolumeStorageKind::NanoVDB, + payload, + sizeof(payload), + bounds, + XCEngine::Math::Vector3(0.5f, 0.25f, 0.125f))); + + EXPECT_TRUE(volumeField.IsValid()); + EXPECT_EQ(volumeField.GetType(), ResourceType::VolumeField); + EXPECT_EQ(volumeField.GetStorageKind(), VolumeStorageKind::NanoVDB); + EXPECT_EQ(volumeField.GetPayloadSize(), sizeof(payload)); + EXPECT_EQ(volumeField.GetBounds().GetMin(), XCEngine::Math::Vector3(-1.0f, -2.0f, -3.0f)); + EXPECT_EQ(volumeField.GetBounds().GetMax(), XCEngine::Math::Vector3(4.0f, 5.0f, 6.0f)); + EXPECT_EQ(volumeField.GetVoxelSize(), XCEngine::Math::Vector3(0.5f, 0.25f, 0.125f)); + EXPECT_EQ(static_cast(volumeField.GetPayloadData())[0], 1u); + EXPECT_GT(volumeField.GetMemorySize(), sizeof(VolumeField)); +} + +} // namespace diff --git a/tests/Resources/Volume/test_volume_field_loader.cpp b/tests/Resources/Volume/test_volume_field_loader.cpp new file mode 100644 index 00000000..a0c2fa68 --- /dev/null +++ b/tests/Resources/Volume/test_volume_field_loader.cpp @@ -0,0 +1,155 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace XCEngine::Resources; + +namespace { + +std::vector MakeTestNanoVDBPayload() { + return { + 0x4E, 0x56, 0x44, 0x42, + 0x10, 0x20, 0x30, 0x40, + 0x01, 0x03, 0x05, 0x07, + 0xAA, 0xBB, 0xCC, 0xDD + }; +} + +void WriteBinaryFile(const std::filesystem::path& path, const std::vector& bytes) { + std::ofstream output(path, std::ios::binary | std::ios::trunc); + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); +} + +TEST(VolumeFieldLoader, GetResourceType) { + VolumeFieldLoader loader; + EXPECT_EQ(loader.GetResourceType(), ResourceType::VolumeField); +} + +TEST(VolumeFieldLoader, CanLoad) { + VolumeFieldLoader loader; + EXPECT_TRUE(loader.CanLoad("cloud.nvdb")); + EXPECT_TRUE(loader.CanLoad("cloud.xcvol")); + EXPECT_FALSE(loader.CanLoad("cloud.txt")); +} + +TEST(VolumeFieldLoader, LoadInvalidPath) { + VolumeFieldLoader loader; + LoadResult result = loader.Load("invalid/path/cloud.nvdb"); + EXPECT_FALSE(result); +} + +TEST(VolumeFieldLoader, LoadSourceNanoVDBBlob) { + namespace fs = std::filesystem; + + const fs::path volumePath = fs::temp_directory_path() / "xc_volume_loader_source_test.nvdb"; + const std::vector bytes = MakeTestNanoVDBPayload(); + WriteBinaryFile(volumePath, bytes); + + VolumeFieldLoader loader; + LoadResult result = loader.Load(volumePath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* volumeField = static_cast(result.resource); + EXPECT_EQ(volumeField->GetStorageKind(), VolumeStorageKind::NanoVDB); + EXPECT_EQ(volumeField->GetPayloadSize(), bytes.size()); + EXPECT_EQ(static_cast(volumeField->GetPayloadData())[0], bytes[0]); + + delete volumeField; + fs::remove(volumePath); +} + +TEST(VolumeFieldLoader, AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReimport) { + namespace fs = std::filesystem; + using namespace std::chrono_literals; + + 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"; + + fs::remove_all(projectRoot); + fs::create_directories(assetsDir); + WriteBinaryFile(volumePath, MakeTestNanoVDBPayload()); + + AssetDatabase database; + database.Initialize(projectRoot.string().c_str()); + + AssetDatabase::ResolvedAsset firstResolve; + ASSERT_TRUE(database.EnsureArtifact("Assets/cloud.nvdb", ResourceType::VolumeField, firstResolve)); + ASSERT_TRUE(firstResolve.exists); + ASSERT_TRUE(firstResolve.artifactReady); + EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr())); + EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().generic_string(), ".xcvol"); + + VolumeFieldLoader loader; + LoadResult artifactLoad = loader.Load(firstResolve.artifactMainPath); + ASSERT_TRUE(artifactLoad); + ASSERT_NE(artifactLoad.resource, nullptr); + auto* artifactVolume = static_cast(artifactLoad.resource); + EXPECT_EQ(artifactVolume->GetStorageKind(), VolumeStorageKind::NanoVDB); + EXPECT_EQ(artifactVolume->GetPayloadSize(), MakeTestNanoVDBPayload().size()); + delete artifactVolume; + + const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr()); + std::this_thread::sleep_for(50ms); + + AssetDatabase::ResolvedAsset secondResolve; + ASSERT_TRUE(database.EnsureArtifact("Assets/cloud.nvdb", ResourceType::VolumeField, secondResolve)); + EXPECT_EQ(firstResolve.artifactMainPath, secondResolve.artifactMainPath); + EXPECT_EQ(originalArtifactWriteTime, fs::last_write_time(secondResolve.artifactMainPath.CStr())); + + database.Shutdown(); + fs::remove_all(projectRoot); +} + +TEST(VolumeFieldLoader, ResourceManagerLoadsVolumeByAssetRefFromProjectAssets) { + namespace fs = std::filesystem; + + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + 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 bytes = MakeTestNanoVDBPayload(); + + fs::remove_all(projectRoot); + fs::create_directories(assetsDir); + WriteBinaryFile(volumePath, bytes); + + manager.SetResourceRoot(projectRoot.string().c_str()); + + { + const auto firstHandle = manager.Load("Assets/cloud.nvdb"); + ASSERT_TRUE(firstHandle.IsValid()); + EXPECT_EQ(firstHandle->GetStorageKind(), VolumeStorageKind::NanoVDB); + EXPECT_EQ(firstHandle->GetPayloadSize(), bytes.size()); + + AssetRef assetRef; + ASSERT_TRUE(manager.TryGetAssetRef("Assets/cloud.nvdb", ResourceType::VolumeField, assetRef)); + EXPECT_TRUE(assetRef.IsValid()); + + manager.UnloadAll(); + + const auto secondHandle = manager.Load(assetRef); + ASSERT_TRUE(secondHandle.IsValid()); + EXPECT_EQ(secondHandle->GetStorageKind(), VolumeStorageKind::NanoVDB); + EXPECT_EQ(secondHandle->GetPayloadSize(), bytes.size()); + } + + manager.SetResourceRoot(""); + manager.Shutdown(); + fs::remove_all(projectRoot); +} + +} // namespace