Add VolumeField NanoVDB asset pipeline

This commit is contained in:
2026-04-08 19:45:53 +08:00
parent 6bf9203eec
commit c6815fa809
16 changed files with 608 additions and 1 deletions

View File

@@ -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}

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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>

View 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

View 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

View File

@@ -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,

View File

@@ -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);

View 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

View 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