Add VolumeField NanoVDB asset pipeline
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<class BinaryResource>() { return
|
||||
template<> inline ResourceType GetResourceType<class UIView>() { return ResourceType::UIView; }
|
||||
template<> inline ResourceType GetResourceType<class UITheme>() { return ResourceType::UITheme; }
|
||||
template<> inline ResourceType GetResourceType<class UISchema>() { return ResourceType::UISchema; }
|
||||
template<> inline ResourceType GetResourceType<class VolumeField>() { return ResourceType::VolumeField; }
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||
#include <XCEngine/Resources/Shader/Shader.h>
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Volume/VolumeField.h>
|
||||
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
|
||||
#include <XCEngine/Resources/AudioClip/AudioClip.h>
|
||||
#include <XCEngine/Resources/AudioClip/AudioLoader.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
|
||||
|
||||
52
engine/include/XCEngine/Resources/Volume/VolumeField.h
Normal file
52
engine/include/XCEngine/Resources/Volume/VolumeField.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
|
||||
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<Core::uint8> m_payload;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
21
engine/include/XCEngine/Resources/Volume/VolumeFieldLoader.h
Normal file
21
engine/include/XCEngine/Resources/Volume/VolumeFieldLoader.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class VolumeFieldLoader : public IResourceLoader {
|
||||
public:
|
||||
VolumeFieldLoader();
|
||||
virtual ~VolumeFieldLoader() override;
|
||||
|
||||
ResourceType GetResourceType() const override { return ResourceType::VolumeField; }
|
||||
Containers::Array<Containers::String> 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
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
#include <XCEngine/Resources/Volume/VolumeField.h>
|
||||
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -366,6 +368,27 @@ bool WriteTextureArtifactFile(const fs::path& artifactPath, const Texture& textu
|
||||
return static_cast<bool>(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<Core::uint32>(volumeField.GetStorageKind());
|
||||
header.boundsMin = volumeField.GetBounds().GetMin();
|
||||
header.boundsMax = volumeField.GetBounds().GetMax();
|
||||
header.voxelSize = volumeField.GetVoxelSize();
|
||||
header.payloadSize = static_cast<Core::uint64>(volumeField.GetPayloadSize());
|
||||
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
if (volumeField.GetPayloadSize() > 0) {
|
||||
output.write(static_cast<const char*>(volumeField.GetPayloadData()), volumeField.GetPayloadSize());
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
std::vector<MaterialProperty> 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<VolumeField*>(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,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
|
||||
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
|
||||
#include <exception>
|
||||
|
||||
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);
|
||||
|
||||
43
engine/src/Resources/Volume/VolumeField.cpp
Normal file
43
engine/src/Resources/Volume/VolumeField.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include <XCEngine/Resources/Volume/VolumeField.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
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
|
||||
144
engine/src/Resources/Volume/VolumeFieldLoader.cpp
Normal file
144
engine/src/Resources/Volume/VolumeFieldLoader.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Volume/VolumeField.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
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<char*>(&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<Core::uint8> payload;
|
||||
payload.Resize(static_cast<size_t>(header.payloadSize));
|
||||
input.read(reinterpret_cast<char*>(payload.Data()), static_cast<std::streamsize>(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<VolumeStorageKind>(header.storageKind),
|
||||
bounds,
|
||||
header.voxelSize,
|
||||
payload.Data(),
|
||||
payload.Size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
VolumeFieldLoader::VolumeFieldLoader() = default;
|
||||
|
||||
VolumeFieldLoader::~VolumeFieldLoader() = default;
|
||||
|
||||
Containers::Array<Containers::String> VolumeFieldLoader::GetSupportedExtensions() const {
|
||||
Containers::Array<Containers::String> 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<Core::uint8> 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
|
||||
@@ -24,6 +24,7 @@ TEST(Resources_Types, ResourceType_EnumValues) {
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UIView), 13);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UITheme), 14);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UISchema), 15);
|
||||
EXPECT_EQ(static_cast<uint8_t>(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<UIView>(), ResourceType::UIView);
|
||||
EXPECT_EQ(GetResourceType<UITheme>(), ResourceType::UITheme);
|
||||
EXPECT_EQ(GetResourceType<UISchema>(), ResourceType::UISchema);
|
||||
EXPECT_EQ(GetResourceType<VolumeField>(), ResourceType::VolumeField);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -8,3 +8,4 @@ add_subdirectory(Material)
|
||||
add_subdirectory(Shader)
|
||||
add_subdirectory(AudioClip)
|
||||
add_subdirectory(UI)
|
||||
add_subdirectory(Volume)
|
||||
|
||||
35
tests/Resources/Volume/CMakeLists.txt
Normal file
35
tests/Resources/Volume/CMakeLists.txt
Normal file
@@ -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)
|
||||
42
tests/Resources/Volume/test_volume_field.cpp
Normal file
42
tests/Resources/Volume/test_volume_field.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Resources/Volume/VolumeField.h>
|
||||
|
||||
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<const unsigned char*>(volumeField.GetPayloadData())[0], 1u);
|
||||
EXPECT_GT(volumeField.GetMemorySize(), sizeof(VolumeField));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
155
tests/Resources/Volume/test_volume_field_loader.cpp
Normal file
155
tests/Resources/Volume/test_volume_field_loader.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Volume/VolumeField.h>
|
||||
#include <XCEngine/Resources/Volume/VolumeFieldLoader.h>
|
||||
|
||||
#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
|
||||
};
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
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<unsigned char> 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<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]);
|
||||
|
||||
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<VolumeField*>(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<unsigned char> bytes = MakeTestNanoVDBPayload();
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
WriteBinaryFile(volumePath, bytes);
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
{
|
||||
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());
|
||||
|
||||
AssetRef assetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/cloud.nvdb", ResourceType::VolumeField, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
const auto secondHandle = manager.Load<VolumeField>(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
|
||||
Reference in New Issue
Block a user