Add model and GaussianSplat asset pipelines
This commit is contained in:
@@ -363,6 +363,13 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Texture/TextureLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Texture/TextureImportSettings.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/BuiltinResources.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Model/AssimpModelImporter.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Model/Model.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Model/ModelArtifactIO.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Model/ModelLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/GaussianSplat/GaussianSplat.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Mesh/Mesh.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Mesh/MeshLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Mesh/MeshImportSettings.h
|
||||
@@ -382,6 +389,14 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Texture/TextureLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Texture/TextureImportSettings.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/BuiltinResources.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Model/AssimpModelImporter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Model/Model.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Model/ModelArtifactIO.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Model/ModelLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/GaussianSplat/GaussianSplat.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/GaussianSplat/GaussianSplatLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Mesh/Mesh.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Mesh/MeshLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Mesh/MeshImportSettings.cpp
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
#include <XCEngine/Resources/Texture/Texture.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -16,6 +19,8 @@ constexpr Core::uint32 kMeshArtifactSchemaVersion = 2;
|
||||
constexpr Core::uint32 kShaderArtifactSchemaVersion = 5;
|
||||
constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2;
|
||||
constexpr Core::uint32 kVolumeFieldArtifactSchemaVersion = 2;
|
||||
constexpr Core::uint32 kModelArtifactSchemaVersion = 1;
|
||||
constexpr Core::uint32 kGaussianSplatArtifactSchemaVersion = 1;
|
||||
|
||||
struct TextureArtifactHeader {
|
||||
char magic[8] = { 'X', 'C', 'T', 'E', 'X', '0', '1', '\0' };
|
||||
@@ -66,6 +71,38 @@ struct MaterialPropertyArtifact {
|
||||
MaterialProperty::Value value = {};
|
||||
};
|
||||
|
||||
struct ModelArtifactFileHeader {
|
||||
char magic[8] = { 'X', 'C', 'M', 'O', 'D', '0', '1', '\0' };
|
||||
Core::uint32 schemaVersion = kModelArtifactSchemaVersion;
|
||||
};
|
||||
|
||||
struct ModelArtifactHeader {
|
||||
Core::uint32 nodeCount = 0;
|
||||
Core::uint32 meshBindingCount = 0;
|
||||
Core::uint32 materialBindingCount = 0;
|
||||
Core::uint32 rootNodeIndex = 0xffffffffu;
|
||||
};
|
||||
|
||||
struct ModelNodeArtifactHeader {
|
||||
Core::int32 parentIndex = -1;
|
||||
Core::uint32 meshBindingStart = 0;
|
||||
Core::uint32 meshBindingCount = 0;
|
||||
Math::Vector3 localPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion localRotation = Math::Quaternion::Identity();
|
||||
Math::Vector3 localScale = Math::Vector3(1.0f, 1.0f, 1.0f);
|
||||
};
|
||||
|
||||
struct ModelMeshBindingArtifact {
|
||||
LocalID meshLocalID = kInvalidLocalID;
|
||||
Core::uint32 materialBindingStart = 0;
|
||||
Core::uint32 materialBindingCount = 0;
|
||||
};
|
||||
|
||||
struct ModelMaterialBindingArtifact {
|
||||
Core::uint32 slotIndex = 0;
|
||||
LocalID materialLocalID = kInvalidLocalID;
|
||||
};
|
||||
|
||||
struct ShaderArtifactFileHeader {
|
||||
char magic[8] = { 'X', 'C', 'S', 'H', 'D', '0', '5', '\0' };
|
||||
Core::uint32 schemaVersion = kShaderArtifactSchemaVersion;
|
||||
@@ -144,5 +181,36 @@ struct VolumeFieldArtifactHeader {
|
||||
Core::uint64 payloadSize = 0;
|
||||
};
|
||||
|
||||
struct GaussianSplatArtifactFileHeader {
|
||||
char magic[8] = { 'X', 'C', 'G', 'S', 'P', '0', '1', '\0' };
|
||||
Core::uint32 schemaVersion = kGaussianSplatArtifactSchemaVersion;
|
||||
};
|
||||
|
||||
struct GaussianSplatArtifactHeader {
|
||||
Core::uint32 contentVersion = 1;
|
||||
Core::uint32 splatCount = 0;
|
||||
Core::uint32 chunkCount = 0;
|
||||
Core::uint32 cameraCount = 0;
|
||||
Math::Vector3 boundsMin = Math::Vector3::Zero();
|
||||
Math::Vector3 boundsMax = Math::Vector3::Zero();
|
||||
Core::uint32 positionFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::VectorFloat32);
|
||||
Core::uint32 otherFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::OtherFloat32);
|
||||
Core::uint32 colorFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::ColorRGBA32F);
|
||||
Core::uint32 shFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::SHFloat32);
|
||||
Core::uint32 chunkFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::Unknown);
|
||||
Core::uint32 cameraFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::Unknown);
|
||||
Core::uint32 sectionCount = 0;
|
||||
Core::uint64 payloadSize = 0;
|
||||
};
|
||||
|
||||
struct GaussianSplatArtifactSectionRecord {
|
||||
Core::uint32 sectionType = static_cast<Core::uint32>(GaussianSplatSectionType::Unknown);
|
||||
Core::uint32 format = static_cast<Core::uint32>(GaussianSplatSectionFormat::Unknown);
|
||||
Core::uint64 payloadOffset = 0;
|
||||
Core::uint64 dataSize = 0;
|
||||
Core::uint32 elementCount = 0;
|
||||
Core::uint32 elementStride = 0;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -82,6 +82,7 @@ public:
|
||||
bool TryGetAssetGuid(const Containers::String& requestPath, AssetGUID& outGuid) const;
|
||||
bool TryGetImportableResourceType(const Containers::String& requestPath, ResourceType& outType) const;
|
||||
bool TryGetAssetRef(const Containers::String& requestPath, ResourceType resourceType, AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath);
|
||||
bool ReimportAsset(const Containers::String& requestPath,
|
||||
ResolvedAsset& outAsset,
|
||||
MaintenanceStats* outStats = nullptr);
|
||||
@@ -139,6 +140,8 @@ private:
|
||||
ArtifactRecord& outRecord);
|
||||
bool ImportShaderAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord);
|
||||
bool ImportGaussianSplatAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord);
|
||||
bool ImportVolumeFieldAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord);
|
||||
bool ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord,
|
||||
@@ -161,7 +164,7 @@ private:
|
||||
ArtifactDependencyRecord& outRecord) const;
|
||||
bool AreDependenciesCurrent(const std::vector<ArtifactDependencyRecord>& dependencies) const;
|
||||
bool CollectModelDependencies(const SourceAssetRecord& sourceRecord,
|
||||
const Mesh& mesh,
|
||||
const std::vector<Containers::String>& importedTexturePaths,
|
||||
std::vector<ArtifactDependencyRecord>& outDependencies) const;
|
||||
bool CollectMaterialDependencies(const Material& material,
|
||||
std::vector<ArtifactDependencyRecord>& outDependencies) const;
|
||||
|
||||
@@ -75,6 +75,8 @@ public:
|
||||
bool TryGetAssetRef(const Containers::String& requestPath,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetRef& assetRef,
|
||||
Containers::String& outPath);
|
||||
bool TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const;
|
||||
void BuildLookupSnapshot(LookupSnapshot& outSnapshot) const;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public:
|
||||
const Containers::String& path,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetImportService& importService,
|
||||
bool TryResolveAssetPath(AssetImportService& importService,
|
||||
const AssetRef& assetRef,
|
||||
Containers::String& outPath) const;
|
||||
void RememberResolvedPath(const AssetGUID& assetGuid, const Containers::String& relativePath);
|
||||
|
||||
@@ -26,7 +26,9 @@ enum class ResourceType : Core::uint8 {
|
||||
UIView,
|
||||
UITheme,
|
||||
UISchema,
|
||||
VolumeField
|
||||
VolumeField,
|
||||
Model,
|
||||
GaussianSplat
|
||||
};
|
||||
|
||||
constexpr const char* GetResourceTypeName(ResourceType type) {
|
||||
@@ -47,6 +49,8 @@ constexpr const char* GetResourceTypeName(ResourceType type) {
|
||||
case ResourceType::UITheme: return "UITheme";
|
||||
case ResourceType::UISchema: return "UISchema";
|
||||
case ResourceType::VolumeField: return "VolumeField";
|
||||
case ResourceType::Model: return "Model";
|
||||
case ResourceType::GaussianSplat: return "GaussianSplat";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -101,6 +105,8 @@ template<> inline ResourceType GetResourceType<class UIView>() { return Resource
|
||||
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; }
|
||||
template<> inline ResourceType GetResourceType<class Model>() { return ResourceType::Model; }
|
||||
template<> inline ResourceType GetResourceType<class GaussianSplat>() { return ResourceType::GaussianSplat; }
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
|
||||
138
engine/include/XCEngine/Resources/GaussianSplat/GaussianSplat.h
Normal file
138
engine/include/XCEngine/Resources/GaussianSplat/GaussianSplat.h
Normal file
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Core/Math/Vector4.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
enum class GaussianSplatSectionType : Core::uint32 {
|
||||
Unknown = 0,
|
||||
Positions = 1,
|
||||
Other = 2,
|
||||
Color = 3,
|
||||
SH = 4,
|
||||
Chunks = 5,
|
||||
Cameras = 6
|
||||
};
|
||||
|
||||
enum class GaussianSplatSectionFormat : Core::uint32 {
|
||||
Unknown = 0,
|
||||
VectorFloat32 = 1,
|
||||
VectorFloat16 = 2,
|
||||
VectorNorm16 = 3,
|
||||
VectorNorm11 = 4,
|
||||
VectorNorm6 = 5,
|
||||
OtherFloat32 = 16,
|
||||
OtherPacked32 = 17,
|
||||
ColorRGBA32F = 32,
|
||||
ColorRGBA16F = 33,
|
||||
ColorRGBA8 = 34,
|
||||
ColorBC7 = 35,
|
||||
SHFloat32 = 48,
|
||||
SHFloat16 = 49,
|
||||
SHNorm11 = 50,
|
||||
SHNorm6 = 51,
|
||||
ChunkFloat32 = 64,
|
||||
CameraFloat32 = 80
|
||||
};
|
||||
|
||||
constexpr Core::uint32 kGaussianSplatSHCoefficientCount = 45;
|
||||
|
||||
struct GaussianSplatPositionRecord {
|
||||
Math::Vector3 position = Math::Vector3::Zero();
|
||||
};
|
||||
|
||||
struct GaussianSplatOtherRecord {
|
||||
Math::Quaternion rotation = Math::Quaternion::Identity();
|
||||
Math::Vector3 scale = Math::Vector3::Zero();
|
||||
float reserved = 0.0f;
|
||||
};
|
||||
|
||||
struct GaussianSplatColorRecord {
|
||||
Math::Vector4 colorOpacity = Math::Vector4::Zero();
|
||||
};
|
||||
|
||||
struct GaussianSplatSHRecord {
|
||||
float coefficients[kGaussianSplatSHCoefficientCount] = {};
|
||||
};
|
||||
|
||||
struct GaussianSplatSection {
|
||||
GaussianSplatSectionType type = GaussianSplatSectionType::Unknown;
|
||||
GaussianSplatSectionFormat format = GaussianSplatSectionFormat::Unknown;
|
||||
Core::uint64 dataOffset = 0;
|
||||
Core::uint64 dataSize = 0;
|
||||
Core::uint32 elementCount = 0;
|
||||
Core::uint32 elementStride = 0;
|
||||
};
|
||||
|
||||
struct GaussianSplatMetadata {
|
||||
Core::uint32 contentVersion = 1;
|
||||
Core::uint32 splatCount = 0;
|
||||
Core::uint32 chunkCount = 0;
|
||||
Core::uint32 cameraCount = 0;
|
||||
Math::Bounds bounds;
|
||||
GaussianSplatSectionFormat positionFormat = GaussianSplatSectionFormat::VectorFloat32;
|
||||
GaussianSplatSectionFormat otherFormat = GaussianSplatSectionFormat::OtherFloat32;
|
||||
GaussianSplatSectionFormat colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
|
||||
GaussianSplatSectionFormat shFormat = GaussianSplatSectionFormat::SHFloat32;
|
||||
GaussianSplatSectionFormat chunkFormat = GaussianSplatSectionFormat::Unknown;
|
||||
GaussianSplatSectionFormat cameraFormat = GaussianSplatSectionFormat::Unknown;
|
||||
};
|
||||
|
||||
class GaussianSplat : public IResource {
|
||||
public:
|
||||
GaussianSplat();
|
||||
~GaussianSplat() override;
|
||||
|
||||
ResourceType GetType() const override { return ResourceType::GaussianSplat; }
|
||||
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 CreateOwned(const GaussianSplatMetadata& metadata,
|
||||
Containers::Array<GaussianSplatSection>&& sections,
|
||||
Containers::Array<Core::uint8>&& payload);
|
||||
void Clear();
|
||||
|
||||
const GaussianSplatMetadata& GetMetadata() const { return m_metadata; }
|
||||
Core::uint32 GetContentVersion() const { return m_metadata.contentVersion; }
|
||||
Core::uint32 GetSplatCount() const { return m_metadata.splatCount; }
|
||||
Core::uint32 GetChunkCount() const { return m_metadata.chunkCount; }
|
||||
Core::uint32 GetCameraCount() const { return m_metadata.cameraCount; }
|
||||
const Math::Bounds& GetBounds() const { return m_metadata.bounds; }
|
||||
GaussianSplatSectionFormat GetPositionFormat() const { return m_metadata.positionFormat; }
|
||||
GaussianSplatSectionFormat GetOtherFormat() const { return m_metadata.otherFormat; }
|
||||
GaussianSplatSectionFormat GetColorFormat() const { return m_metadata.colorFormat; }
|
||||
GaussianSplatSectionFormat GetSHFormat() const { return m_metadata.shFormat; }
|
||||
GaussianSplatSectionFormat GetChunkFormat() const { return m_metadata.chunkFormat; }
|
||||
GaussianSplatSectionFormat GetCameraFormat() const { return m_metadata.cameraFormat; }
|
||||
const Containers::Array<GaussianSplatSection>& GetSections() const { return m_sections; }
|
||||
const GaussianSplatSection* FindSection(GaussianSplatSectionType type) const;
|
||||
const void* GetSectionData(GaussianSplatSectionType type) const;
|
||||
const GaussianSplatPositionRecord* GetPositionRecords() const;
|
||||
const GaussianSplatOtherRecord* GetOtherRecords() const;
|
||||
const GaussianSplatColorRecord* GetColorRecords() const;
|
||||
const GaussianSplatSHRecord* GetSHRecords() const;
|
||||
const Core::uint8* GetPayloadData() const { return m_payload.Data(); }
|
||||
size_t GetPayloadSize() const { return m_payload.Size(); }
|
||||
|
||||
private:
|
||||
bool ValidateSections(const Containers::Array<GaussianSplatSection>& sections, size_t payloadSize) const;
|
||||
void UpdateMemorySize();
|
||||
|
||||
GaussianSplatMetadata m_metadata = {};
|
||||
Containers::Array<GaussianSplatSection> m_sections;
|
||||
Containers::Array<Core::uint8> m_payload;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class GaussianSplat;
|
||||
|
||||
bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
|
||||
const GaussianSplat& gaussianSplat,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
LoadResult LoadGaussianSplatArtifact(const Containers::String& path);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class GaussianSplatLoader : public IResourceLoader {
|
||||
public:
|
||||
GaussianSplatLoader();
|
||||
~GaussianSplatLoader() override;
|
||||
|
||||
ResourceType GetResourceType() const override { return ResourceType::GaussianSplat; }
|
||||
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
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class Model;
|
||||
class Mesh;
|
||||
class Material;
|
||||
class Texture;
|
||||
|
||||
struct ImportedModelMesh {
|
||||
LocalID localID = kInvalidLocalID;
|
||||
Mesh* mesh = nullptr;
|
||||
std::vector<LocalID> materialLocalIDs;
|
||||
};
|
||||
|
||||
struct ImportedModelMaterial {
|
||||
LocalID localID = kInvalidLocalID;
|
||||
Material* material = nullptr;
|
||||
};
|
||||
|
||||
struct ImportedModelData {
|
||||
Model* model = nullptr;
|
||||
std::vector<ImportedModelMesh> meshes;
|
||||
std::vector<ImportedModelMaterial> materials;
|
||||
std::vector<Texture*> textures;
|
||||
|
||||
void Reset();
|
||||
};
|
||||
|
||||
bool ImportAssimpModel(const Containers::String& sourcePath,
|
||||
const MeshImportSettings& settings,
|
||||
ImportedModelData& outData,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
72
engine/include/XCEngine/Resources/Model/Model.h
Normal file
72
engine/include/XCEngine/Resources/Model/Model.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
constexpr Core::uint32 kInvalidModelNodeIndex = 0xffffffffu;
|
||||
|
||||
struct ModelMaterialBinding {
|
||||
Core::uint32 slotIndex = 0;
|
||||
LocalID materialLocalID = kInvalidLocalID;
|
||||
};
|
||||
|
||||
struct ModelMeshBinding {
|
||||
LocalID meshLocalID = kInvalidLocalID;
|
||||
Core::uint32 materialBindingStart = 0;
|
||||
Core::uint32 materialBindingCount = 0;
|
||||
};
|
||||
|
||||
struct ModelNode {
|
||||
Containers::String name;
|
||||
Core::int32 parentIndex = -1;
|
||||
Core::uint32 meshBindingStart = 0;
|
||||
Core::uint32 meshBindingCount = 0;
|
||||
Math::Vector3 localPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion localRotation = Math::Quaternion::Identity();
|
||||
Math::Vector3 localScale = Math::Vector3(1.0f, 1.0f, 1.0f);
|
||||
};
|
||||
|
||||
class Model : public IResource {
|
||||
public:
|
||||
Model();
|
||||
~Model() override;
|
||||
|
||||
ResourceType GetType() const override { return ResourceType::Model; }
|
||||
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;
|
||||
|
||||
void SetRootNodeIndex(Core::uint32 index);
|
||||
Core::uint32 GetRootNodeIndex() const { return m_rootNodeIndex; }
|
||||
bool HasRootNode() const { return m_rootNodeIndex != kInvalidModelNodeIndex; }
|
||||
|
||||
void AddNode(const ModelNode& node);
|
||||
void AddMeshBinding(const ModelMeshBinding& binding);
|
||||
void AddMaterialBinding(const ModelMaterialBinding& binding);
|
||||
void ClearGraph();
|
||||
|
||||
const Containers::Array<ModelNode>& GetNodes() const { return m_nodes; }
|
||||
const Containers::Array<ModelMeshBinding>& GetMeshBindings() const { return m_meshBindings; }
|
||||
const Containers::Array<ModelMaterialBinding>& GetMaterialBindings() const { return m_materialBindings; }
|
||||
|
||||
private:
|
||||
void UpdateMemorySize();
|
||||
|
||||
Core::uint32 m_rootNodeIndex = kInvalidModelNodeIndex;
|
||||
Containers::Array<ModelNode> m_nodes;
|
||||
Containers::Array<ModelMeshBinding> m_meshBindings;
|
||||
Containers::Array<ModelMaterialBinding> m_materialBindings;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
18
engine/include/XCEngine/Resources/Model/ModelArtifactIO.h
Normal file
18
engine/include/XCEngine/Resources/Model/ModelArtifactIO.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class Model;
|
||||
|
||||
bool WriteModelArtifactFile(const Containers::String& artifactPath,
|
||||
const Model& model,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
LoadResult LoadModelArtifact(const Containers::String& path);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
21
engine/include/XCEngine/Resources/Model/ModelLoader.h
Normal file
21
engine/include/XCEngine/Resources/Model/ModelLoader.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class ModelLoader : public IResourceLoader {
|
||||
public:
|
||||
ModelLoader();
|
||||
~ModelLoader() override;
|
||||
|
||||
ResourceType GetResourceType() const override { return ResourceType::Model; }
|
||||
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
|
||||
@@ -14,6 +14,12 @@
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureImportSettings.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Model/AssimpModelImporter.h>
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
#include "Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.h"
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
@@ -48,6 +53,13 @@ Containers::String ToContainersString(const std::string& value) {
|
||||
|
||||
Containers::String NormalizeArtifactPathString(const fs::path& path);
|
||||
Containers::String NormalizeArtifactPathString(const Containers::String& path);
|
||||
constexpr const char* kModelSubAssetManifestFileName = "subassets.tsv";
|
||||
|
||||
struct ModelSubAssetManifestEntry {
|
||||
LocalID localID = kInvalidLocalID;
|
||||
ResourceType resourceType = ResourceType::Unknown;
|
||||
Containers::String artifactPath;
|
||||
};
|
||||
|
||||
void PopulateResolvedAssetResult(const Containers::String& projectRoot,
|
||||
const AssetDatabase::SourceAssetRecord& sourceRecord,
|
||||
@@ -320,6 +332,85 @@ std::vector<std::string> SplitFields(const std::string& line) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
bool WriteModelSubAssetManifest(const fs::path& manifestPath,
|
||||
const std::vector<ModelSubAssetManifestEntry>& entries) {
|
||||
std::ofstream output(manifestPath, std::ios::out | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output << "# localID\tresourceType\tartifactPath\n";
|
||||
for (const ModelSubAssetManifestEntry& entry : entries) {
|
||||
if (entry.localID == kInvalidLocalID ||
|
||||
entry.resourceType == ResourceType::Unknown ||
|
||||
entry.artifactPath.Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
output << entry.localID << '\t'
|
||||
<< static_cast<Core::uint32>(entry.resourceType) << '\t'
|
||||
<< EscapeField(ToStdString(entry.artifactPath)) << '\n';
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
bool TryReadModelSubAssetManifest(const fs::path& manifestPath,
|
||||
std::vector<ModelSubAssetManifestEntry>& outEntries) {
|
||||
outEntries.clear();
|
||||
|
||||
std::ifstream input(manifestPath);
|
||||
if (!input.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::vector<std::string> fields = SplitFields(line);
|
||||
if (fields.size() < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelSubAssetManifestEntry entry;
|
||||
entry.localID = static_cast<LocalID>(std::stoull(fields[0]));
|
||||
entry.resourceType = static_cast<ResourceType>(std::stoul(fields[1]));
|
||||
entry.artifactPath = ToContainersString(fields[2]);
|
||||
if (entry.localID == kInvalidLocalID ||
|
||||
entry.resourceType == ResourceType::Unknown ||
|
||||
entry.artifactPath.Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
outEntries.push_back(entry);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryResolveModelSubAssetArtifactPath(const fs::path& manifestPath,
|
||||
const AssetRef& assetRef,
|
||||
Containers::String& outArtifactPath) {
|
||||
std::vector<ModelSubAssetManifestEntry> entries;
|
||||
if (!TryReadModelSubAssetManifest(manifestPath, entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ModelSubAssetManifestEntry& entry : entries) {
|
||||
if (entry.localID != assetRef.localID || entry.resourceType != assetRef.resourceType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
outArtifactPath = entry.artifactPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void WriteString(std::ofstream& stream, const Containers::String& value) {
|
||||
const Core::uint32 length = static_cast<Core::uint32>(value.Length());
|
||||
stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
|
||||
@@ -825,6 +916,74 @@ bool AssetDatabase::TryGetAssetRef(const Containers::String& requestPath,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetDatabase::TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath) {
|
||||
outPath.Clear();
|
||||
ClearLastErrorMessage();
|
||||
|
||||
if (!assetRef.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assetRef.localID == kMainAssetLocalID) {
|
||||
return TryGetPrimaryAssetPath(assetRef.assetGuid, outPath);
|
||||
}
|
||||
|
||||
const auto sourceIt = m_sourcesByGuid.find(assetRef.assetGuid);
|
||||
if (sourceIt == m_sourcesByGuid.end()) {
|
||||
SetLastErrorMessage(Containers::String("Unknown asset GUID for sub-asset path resolution: ") +
|
||||
assetRef.assetGuid.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
const SourceAssetRecord& sourceRecord = sourceIt->second;
|
||||
const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
|
||||
if (primaryType == ResourceType::Unknown) {
|
||||
SetLastErrorMessage(Containers::String("Asset does not have an importable primary type: ") +
|
||||
sourceRecord.relativePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto resolveFromArtifactRecord = [&](const ArtifactRecord& artifactRecord) -> bool {
|
||||
const fs::path manifestPath =
|
||||
fs::path(m_projectRoot.CStr()) /
|
||||
artifactRecord.artifactDirectory.CStr() /
|
||||
kModelSubAssetManifestFileName;
|
||||
|
||||
Containers::String artifactPath;
|
||||
if (!TryResolveModelSubAssetArtifactPath(manifestPath, assetRef, artifactPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outPath = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactPath.CStr());
|
||||
return true;
|
||||
};
|
||||
|
||||
auto artifactIt = m_artifactsByGuid.find(assetRef.assetGuid);
|
||||
if (artifactIt != m_artifactsByGuid.end() &&
|
||||
!ShouldReimport(sourceRecord, &artifactIt->second) &&
|
||||
resolveFromArtifactRecord(artifactIt->second)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ResolvedAsset resolvedAsset;
|
||||
if (!EnsureArtifact(sourceRecord.relativePath, primaryType, resolvedAsset)) {
|
||||
if (m_lastErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(Containers::String("Failed to import asset while resolving sub-asset path: ") +
|
||||
sourceRecord.relativePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
artifactIt = m_artifactsByGuid.find(assetRef.assetGuid);
|
||||
if (artifactIt != m_artifactsByGuid.end() && resolveFromArtifactRecord(artifactIt->second)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SetLastErrorMessage(Containers::String("Sub-asset localID was not found in artifact manifest: ") +
|
||||
sourceRecord.relativePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AssetDatabase::ReimportAsset(const Containers::String& requestPath,
|
||||
ResolvedAsset& outAsset,
|
||||
MaintenanceStats* outStats) {
|
||||
@@ -1442,6 +1601,9 @@ Containers::String AssetDatabase::GetImporterNameForPath(const Containers::Strin
|
||||
if (ext == ".obj" || ext == ".fbx" || ext == ".gltf" || ext == ".glb" || ext == ".dae" || ext == ".stl") {
|
||||
return Containers::String("ModelImporter");
|
||||
}
|
||||
if (ext == ".ply") {
|
||||
return Containers::String("GaussianSplatImporter");
|
||||
}
|
||||
if (ext == ".shader") {
|
||||
return Containers::String("ShaderImporter");
|
||||
}
|
||||
@@ -1468,7 +1630,10 @@ ResourceType AssetDatabase::GetPrimaryResourceTypeForImporter(const Containers::
|
||||
return ResourceType::Texture;
|
||||
}
|
||||
if (importerName == "ModelImporter") {
|
||||
return ResourceType::Mesh;
|
||||
return ResourceType::Model;
|
||||
}
|
||||
if (importerName == "GaussianSplatImporter") {
|
||||
return ResourceType::GaussianSplat;
|
||||
}
|
||||
if (importerName == "MaterialImporter") {
|
||||
return ResourceType::Material;
|
||||
@@ -1488,6 +1653,10 @@ bool AssetDatabase::ShouldReimport(const SourceAssetRecord& sourceRecord,
|
||||
return true;
|
||||
}
|
||||
|
||||
if (artifactRecord->resourceType != GetPrimaryResourceTypeForImporter(sourceRecord.importerName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (artifactRecord->artifactKey.Empty() ||
|
||||
artifactRecord->mainArtifactPath.Empty()) {
|
||||
return true;
|
||||
@@ -1525,8 +1694,10 @@ bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord,
|
||||
return ImportTextureAsset(sourceRecord, outRecord);
|
||||
case ResourceType::Material:
|
||||
return ImportMaterialAsset(sourceRecord, outRecord);
|
||||
case ResourceType::Mesh:
|
||||
case ResourceType::Model:
|
||||
return ImportModelAsset(sourceRecord, outRecord);
|
||||
case ResourceType::GaussianSplat:
|
||||
return ImportGaussianSplatAsset(sourceRecord, outRecord);
|
||||
case ResourceType::Shader:
|
||||
return ImportShaderAsset(sourceRecord, outRecord);
|
||||
case ResourceType::VolumeField:
|
||||
@@ -1764,34 +1935,51 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord,
|
||||
|
||||
bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord) {
|
||||
MeshLoader loader;
|
||||
const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
|
||||
LoadResult result = loader.Load(absolutePath);
|
||||
if (!result || result.resource == nullptr) {
|
||||
const Containers::String absolutePath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
|
||||
|
||||
MeshImportSettings importSettings;
|
||||
ImportedModelData importedModel;
|
||||
Containers::String importErrorMessage;
|
||||
if (!ImportAssimpModel(absolutePath, importSettings, importedModel, &importErrorMessage)) {
|
||||
if (!importErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(importErrorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Mesh* mesh = static_cast<Mesh*>(result.resource);
|
||||
std::vector<Containers::String> importedTexturePaths;
|
||||
importedTexturePaths.reserve(importedModel.textures.size());
|
||||
for (Texture* texture : importedModel.textures) {
|
||||
if (texture == nullptr || texture->GetPath().Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
importedTexturePaths.push_back(texture->GetPath());
|
||||
}
|
||||
|
||||
std::vector<ArtifactDependencyRecord> dependencies;
|
||||
CollectModelDependencies(sourceRecord, *mesh, dependencies);
|
||||
CollectModelDependencies(sourceRecord, importedTexturePaths, dependencies);
|
||||
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
|
||||
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
|
||||
const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmesh");
|
||||
const Containers::String mainArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmodel");
|
||||
|
||||
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) {
|
||||
DestroyImportedMesh(mesh);
|
||||
importedModel.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool writeOk = true;
|
||||
std::vector<ModelSubAssetManifestEntry> subAssetManifestEntries;
|
||||
std::unordered_map<const Texture*, Containers::String> textureArtifactPaths;
|
||||
std::unordered_map<const Texture*, AssetRef> textureAssetRefs;
|
||||
for (size_t textureIndex = 0; writeOk && textureIndex < mesh->GetTextures().Size(); ++textureIndex) {
|
||||
Texture* texture = mesh->GetTextures()[textureIndex];
|
||||
for (size_t textureIndex = 0; writeOk && textureIndex < importedModel.textures.size(); ++textureIndex) {
|
||||
Texture* texture = importedModel.textures[textureIndex];
|
||||
if (texture == nullptr) {
|
||||
continue;
|
||||
}
|
||||
@@ -1815,12 +2003,10 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Containers::String> materialArtifactPaths;
|
||||
materialArtifactPaths.reserve(mesh->GetMaterials().Size());
|
||||
for (size_t materialIndex = 0; writeOk && materialIndex < mesh->GetMaterials().Size(); ++materialIndex) {
|
||||
Material* material = mesh->GetMaterials()[materialIndex];
|
||||
if (material == nullptr) {
|
||||
materialArtifactPaths.emplace_back();
|
||||
std::unordered_map<LocalID, Containers::String> materialArtifactPathsByLocalID;
|
||||
for (size_t materialIndex = 0; writeOk && materialIndex < importedModel.materials.size(); ++materialIndex) {
|
||||
const ImportedModelMaterial& materialEntry = importedModel.materials[materialIndex];
|
||||
if (materialEntry.material == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1828,7 +2014,7 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("material_" + std::to_string(materialIndex) + ".xcmat"));
|
||||
writeOk = WriteMaterialArtifactFile(
|
||||
fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(),
|
||||
*material,
|
||||
*materialEntry.material,
|
||||
textureArtifactPaths,
|
||||
textureAssetRefs,
|
||||
this);
|
||||
@@ -1836,17 +2022,58 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
break;
|
||||
}
|
||||
|
||||
materialArtifactPaths.push_back(materialArtifactPath);
|
||||
materialArtifactPathsByLocalID.emplace(materialEntry.localID, materialArtifactPath);
|
||||
subAssetManifestEntries.push_back(
|
||||
ModelSubAssetManifestEntry{ materialEntry.localID, ResourceType::Material, materialArtifactPath });
|
||||
}
|
||||
|
||||
writeOk = writeOk &&
|
||||
WriteMeshArtifactFile(
|
||||
fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(),
|
||||
*mesh,
|
||||
materialArtifactPaths);
|
||||
for (size_t meshIndex = 0; writeOk && meshIndex < importedModel.meshes.size(); ++meshIndex) {
|
||||
const ImportedModelMesh& meshEntry = importedModel.meshes[meshIndex];
|
||||
if (meshEntry.mesh == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DestroyImportedMesh(mesh);
|
||||
std::vector<Containers::String> meshMaterialArtifactPaths;
|
||||
meshMaterialArtifactPaths.reserve(meshEntry.materialLocalIDs.size());
|
||||
for (LocalID materialLocalID : meshEntry.materialLocalIDs) {
|
||||
const auto materialPathIt = materialArtifactPathsByLocalID.find(materialLocalID);
|
||||
meshMaterialArtifactPaths.push_back(
|
||||
materialPathIt != materialArtifactPathsByLocalID.end()
|
||||
? materialPathIt->second
|
||||
: Containers::String());
|
||||
}
|
||||
|
||||
const Containers::String meshArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("mesh_" + std::to_string(meshIndex) + ".xcmesh"));
|
||||
writeOk = WriteMeshArtifactFile(
|
||||
fs::path(m_projectRoot.CStr()) / meshArtifactPath.CStr(),
|
||||
*meshEntry.mesh,
|
||||
meshMaterialArtifactPaths);
|
||||
if (writeOk) {
|
||||
subAssetManifestEntries.push_back(
|
||||
ModelSubAssetManifestEntry{ meshEntry.localID, ResourceType::Mesh, meshArtifactPath });
|
||||
}
|
||||
}
|
||||
|
||||
Containers::String modelWriteErrorMessage;
|
||||
if (writeOk) {
|
||||
writeOk = WriteModelArtifactFile(
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()),
|
||||
*importedModel.model,
|
||||
&modelWriteErrorMessage);
|
||||
}
|
||||
|
||||
if (writeOk) {
|
||||
writeOk = WriteModelSubAssetManifest(
|
||||
fs::path(m_projectRoot.CStr()) / artifactDir.CStr() / kModelSubAssetManifestFileName,
|
||||
subAssetManifestEntries);
|
||||
}
|
||||
|
||||
importedModel.Reset();
|
||||
if (!writeOk) {
|
||||
if (!modelWriteErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(modelWriteErrorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1854,7 +2081,7 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
outRecord.assetGuid = sourceRecord.guid;
|
||||
outRecord.importerName = sourceRecord.importerName;
|
||||
outRecord.importerVersion = sourceRecord.importerVersion;
|
||||
outRecord.resourceType = ResourceType::Mesh;
|
||||
outRecord.resourceType = ResourceType::Model;
|
||||
outRecord.artifactDirectory = artifactDir;
|
||||
outRecord.mainArtifactPath = mainArtifactPath;
|
||||
outRecord.sourceHash = sourceRecord.sourceHash;
|
||||
@@ -1919,6 +2146,65 @@ bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetDatabase::ImportGaussianSplatAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord) {
|
||||
const Containers::String absolutePath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
|
||||
|
||||
LoadResult result = Internal::ImportGaussianSplatPlyFile(absolutePath);
|
||||
if (!result || result.resource == nullptr) {
|
||||
if (!result.errorMessage.Empty()) {
|
||||
SetLastErrorMessage(result.errorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GaussianSplat* gaussianSplat = static_cast<GaussianSplat*>(result.resource);
|
||||
const Containers::String artifactKey = BuildArtifactKey(sourceRecord);
|
||||
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
|
||||
const Containers::String mainArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcgsplat");
|
||||
|
||||
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 gaussianSplat;
|
||||
return false;
|
||||
}
|
||||
|
||||
Containers::String writeErrorMessage;
|
||||
const Containers::String gaussianSplatArtifactWritePath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr());
|
||||
const bool writeOk = WriteGaussianSplatArtifactFile(
|
||||
gaussianSplatArtifactWritePath,
|
||||
*gaussianSplat,
|
||||
&writeErrorMessage);
|
||||
delete gaussianSplat;
|
||||
if (!writeOk) {
|
||||
if (!writeErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(writeErrorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
outRecord.artifactKey = artifactKey;
|
||||
outRecord.assetGuid = sourceRecord.guid;
|
||||
outRecord.importerName = sourceRecord.importerName;
|
||||
outRecord.importerVersion = sourceRecord.importerVersion;
|
||||
outRecord.resourceType = ResourceType::GaussianSplat;
|
||||
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::ImportVolumeFieldAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord) {
|
||||
VolumeFieldLoader loader;
|
||||
@@ -2157,7 +2443,7 @@ bool AssetDatabase::AreDependenciesCurrent(
|
||||
}
|
||||
|
||||
bool AssetDatabase::CollectModelDependencies(const AssetDatabase::SourceAssetRecord& sourceRecord,
|
||||
const Mesh& mesh,
|
||||
const std::vector<Containers::String>& importedTexturePaths,
|
||||
std::vector<AssetDatabase::ArtifactDependencyRecord>& outDependencies) const {
|
||||
outDependencies.clear();
|
||||
|
||||
@@ -2177,12 +2463,12 @@ bool AssetDatabase::CollectModelDependencies(const AssetDatabase::SourceAssetRec
|
||||
}
|
||||
}
|
||||
|
||||
for (Texture* texture : mesh.GetTextures()) {
|
||||
if (texture == nullptr || texture->GetPath().Empty()) {
|
||||
for (const Containers::String& texturePathValue : importedTexturePaths) {
|
||||
if (texturePathValue.Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string texturePath = ToStdString(texture->GetPath());
|
||||
const std::string texturePath = ToStdString(texturePathValue);
|
||||
if (texturePath.find('#') != std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -302,6 +302,16 @@ bool AssetImportService::TryGetAssetRef(const Containers::String& requestPath,
|
||||
return m_assetDatabase.TryGetAssetRef(requestPath, resourceType, outRef);
|
||||
}
|
||||
|
||||
bool AssetImportService::TryResolveAssetPath(const AssetRef& assetRef,
|
||||
Containers::String& outPath) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_assetDatabase.TryResolveAssetPath(assetRef, outPath);
|
||||
}
|
||||
|
||||
bool AssetImportService::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
|
||||
@@ -131,13 +131,17 @@ bool ProjectAssetIndex::TryGetAssetRef(AssetImportService& importService,
|
||||
return resolved;
|
||||
}
|
||||
|
||||
bool ProjectAssetIndex::TryResolveAssetPath(const AssetImportService& importService,
|
||||
bool ProjectAssetIndex::TryResolveAssetPath(AssetImportService& importService,
|
||||
const AssetRef& assetRef,
|
||||
Containers::String& outPath) const {
|
||||
if (!assetRef.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assetRef.localID != kMainAssetLocalID) {
|
||||
return importService.TryResolveAssetPath(assetRef, outPath);
|
||||
}
|
||||
|
||||
bool resolved = false;
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
@@ -149,7 +153,7 @@ bool ProjectAssetIndex::TryResolveAssetPath(const AssetImportService& importServ
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
resolved = importService.TryGetPrimaryAssetPath(assetRef.assetGuid, outPath);
|
||||
resolved = importService.TryResolveAssetPath(assetRef, outPath);
|
||||
if (resolved) {
|
||||
const_cast<ProjectAssetIndex*>(this)->RememberResolvedPath(assetRef.assetGuid, outPath);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/IO/ResourceFileSystem.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
|
||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
@@ -43,7 +45,9 @@ void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) {
|
||||
}
|
||||
}
|
||||
|
||||
GaussianSplatLoader g_gaussianSplatLoader;
|
||||
MaterialLoader g_materialLoader;
|
||||
ModelLoader g_modelLoader;
|
||||
MeshLoader g_meshLoader;
|
||||
ShaderLoader g_shaderLoader;
|
||||
TextureLoader g_textureLoader;
|
||||
@@ -87,7 +91,9 @@ void ResourceManager::EnsureInitialized() {
|
||||
Core::UniqueRef<AsyncLoader> asyncLoader = Core::MakeUnique<AsyncLoader>();
|
||||
asyncLoader->Initialize(2);
|
||||
|
||||
RegisterBuiltinLoader(*this, g_gaussianSplatLoader);
|
||||
RegisterBuiltinLoader(*this, g_materialLoader);
|
||||
RegisterBuiltinLoader(*this, g_modelLoader);
|
||||
RegisterBuiltinLoader(*this, g_meshLoader);
|
||||
RegisterBuiltinLoader(*this, g_shaderLoader);
|
||||
RegisterBuiltinLoader(*this, g_textureLoader);
|
||||
@@ -522,8 +528,6 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
ImportSettings* settings) {
|
||||
EnsureInitialized();
|
||||
|
||||
const ResourceGUID guid = ResourceGUID::Generate(path);
|
||||
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
@@ -535,15 +539,6 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
m_resourceRoot);
|
||||
}
|
||||
|
||||
if (IResource* cached = FindInCache(guid)) {
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource cache-hit path=") + path);
|
||||
}
|
||||
return LoadResult(cached);
|
||||
}
|
||||
|
||||
IResourceLoader* loader = FindLoader(type);
|
||||
if (loader == nullptr) {
|
||||
Debug::Logger::Get().Warning(Debug::LogCategory::FileSystem,
|
||||
@@ -552,6 +547,50 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
return LoadResult(false, "Loader not found");
|
||||
}
|
||||
|
||||
Containers::String loadPath = path;
|
||||
AssetImportService::ImportedAsset resolvedAsset;
|
||||
ResourceType importableType = ResourceType::Unknown;
|
||||
const bool shouldUseProjectArtifact =
|
||||
!m_resourceRoot.Empty() &&
|
||||
m_assetImportService.TryGetImportableResourceType(path, importableType) &&
|
||||
importableType == type;
|
||||
|
||||
if (shouldUseProjectArtifact &&
|
||||
m_assetImportService.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath);
|
||||
loadPath = resolvedAsset.runtimeLoadPath;
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource artifact path=") +
|
||||
path +
|
||||
" artifact=" +
|
||||
loadPath);
|
||||
}
|
||||
} else if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource direct path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
|
||||
const ResourceGUID guid = ResourceGUID::Generate(loadPath);
|
||||
|
||||
if (IResource* cached = FindInCache(guid)) {
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource cache-hit path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
return LoadResult(cached);
|
||||
}
|
||||
|
||||
const InFlightLoadKey inFlightKey{ guid, type };
|
||||
std::shared_ptr<InFlightLoadState> inFlightState;
|
||||
bool shouldExecuteLoad = false;
|
||||
@@ -610,36 +649,6 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
: Containers::String("In-flight load completed without cached resource"));
|
||||
}
|
||||
|
||||
Containers::String loadPath = path;
|
||||
AssetImportService::ImportedAsset resolvedAsset;
|
||||
ResourceType importableType = ResourceType::Unknown;
|
||||
const bool shouldUseProjectArtifact =
|
||||
!m_resourceRoot.Empty() &&
|
||||
m_assetImportService.TryGetImportableResourceType(path, importableType) &&
|
||||
importableType == type;
|
||||
|
||||
if (shouldUseProjectArtifact &&
|
||||
m_assetImportService.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath);
|
||||
loadPath = resolvedAsset.runtimeLoadPath;
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource artifact path=") +
|
||||
path +
|
||||
" artifact=" +
|
||||
loadPath);
|
||||
}
|
||||
} else if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource direct path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
|
||||
LoadResult result;
|
||||
try {
|
||||
result = loader->Load(loadPath, settings);
|
||||
|
||||
109
engine/src/Resources/GaussianSplat/GaussianSplat.cpp
Normal file
109
engine/src/Resources/GaussianSplat/GaussianSplat.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
GaussianSplat::GaussianSplat() = default;
|
||||
|
||||
GaussianSplat::~GaussianSplat() = default;
|
||||
|
||||
void GaussianSplat::Release() {
|
||||
delete this;
|
||||
}
|
||||
|
||||
bool GaussianSplat::CreateOwned(const GaussianSplatMetadata& metadata,
|
||||
Containers::Array<GaussianSplatSection>&& sections,
|
||||
Containers::Array<Core::uint8>&& payload) {
|
||||
if (!ValidateSections(sections, payload.Size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_metadata = metadata;
|
||||
m_sections = std::move(sections);
|
||||
m_payload = std::move(payload);
|
||||
m_isValid = true;
|
||||
UpdateMemorySize();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GaussianSplat::Clear() {
|
||||
m_metadata = {};
|
||||
m_sections.Clear();
|
||||
m_payload.Clear();
|
||||
m_isValid = false;
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
const GaussianSplatSection* GaussianSplat::FindSection(GaussianSplatSectionType type) const {
|
||||
for (const GaussianSplatSection& section : m_sections) {
|
||||
if (section.type == type) {
|
||||
return §ion;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const void* GaussianSplat::GetSectionData(GaussianSplatSectionType type) const {
|
||||
const GaussianSplatSection* section = FindSection(type);
|
||||
if (section == nullptr || section->dataSize == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return m_payload.Data() + static_cast<size_t>(section->dataOffset);
|
||||
}
|
||||
|
||||
const GaussianSplatPositionRecord* GaussianSplat::GetPositionRecords() const {
|
||||
return static_cast<const GaussianSplatPositionRecord*>(GetSectionData(GaussianSplatSectionType::Positions));
|
||||
}
|
||||
|
||||
const GaussianSplatOtherRecord* GaussianSplat::GetOtherRecords() const {
|
||||
return static_cast<const GaussianSplatOtherRecord*>(GetSectionData(GaussianSplatSectionType::Other));
|
||||
}
|
||||
|
||||
const GaussianSplatColorRecord* GaussianSplat::GetColorRecords() const {
|
||||
return static_cast<const GaussianSplatColorRecord*>(GetSectionData(GaussianSplatSectionType::Color));
|
||||
}
|
||||
|
||||
const GaussianSplatSHRecord* GaussianSplat::GetSHRecords() const {
|
||||
return static_cast<const GaussianSplatSHRecord*>(GetSectionData(GaussianSplatSectionType::SH));
|
||||
}
|
||||
|
||||
bool GaussianSplat::ValidateSections(const Containers::Array<GaussianSplatSection>& sections,
|
||||
size_t payloadSize) const {
|
||||
for (size_t index = 0; index < sections.Size(); ++index) {
|
||||
const GaussianSplatSection& section = sections[index];
|
||||
if (section.type == GaussianSplatSectionType::Unknown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (section.dataOffset > payloadSize || section.dataSize > payloadSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Core::uint64 sectionEnd = section.dataOffset + section.dataSize;
|
||||
if (sectionEnd < section.dataOffset || sectionEnd > payloadSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t otherIndex = index + 1; otherIndex < sections.Size(); ++otherIndex) {
|
||||
if (sections[otherIndex].type == section.type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GaussianSplat::UpdateMemorySize() {
|
||||
m_memorySize =
|
||||
sizeof(GaussianSplat) +
|
||||
m_name.Length() +
|
||||
m_path.Length() +
|
||||
(m_sections.Size() * sizeof(GaussianSplatSection)) +
|
||||
m_payload.Size();
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
202
engine/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp
Normal file
202
engine/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveArtifactPath(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;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPath.lexically_normal();
|
||||
}
|
||||
|
||||
LoadResult CreateOwnedGaussianSplatResource(const Containers::String& path,
|
||||
const GaussianSplatMetadata& metadata,
|
||||
Containers::Array<GaussianSplatSection>&& sections,
|
||||
Containers::Array<Core::uint8>&& payload) {
|
||||
auto* gaussianSplat = new GaussianSplat();
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = GetResourceNameFromPath(path);
|
||||
params.path = path;
|
||||
params.guid = ResourceGUID::Generate(path);
|
||||
params.memorySize = payload.Size();
|
||||
gaussianSplat->Initialize(params);
|
||||
|
||||
if (!gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))) {
|
||||
delete gaussianSplat;
|
||||
return LoadResult(Containers::String("Failed to create GaussianSplat resource: ") + path);
|
||||
}
|
||||
|
||||
return LoadResult(gaussianSplat);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
|
||||
const GaussianSplat& gaussianSplat,
|
||||
Containers::String* outErrorMessage) {
|
||||
const std::filesystem::path resolvedPath = ResolveArtifactPath(artifactPath);
|
||||
std::error_code ec;
|
||||
const std::filesystem::path parentPath = resolvedPath.parent_path();
|
||||
if (!parentPath.empty()) {
|
||||
std::filesystem::create_directories(parentPath, ec);
|
||||
if (ec) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage =
|
||||
Containers::String("Failed to create GaussianSplat artifact directory: ") +
|
||||
Containers::String(parentPath.generic_string().c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream output(resolvedPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to open GaussianSplat artifact for write: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GaussianSplatArtifactFileHeader fileHeader;
|
||||
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
|
||||
|
||||
const GaussianSplatMetadata& metadata = gaussianSplat.GetMetadata();
|
||||
GaussianSplatArtifactHeader header;
|
||||
header.contentVersion = metadata.contentVersion;
|
||||
header.splatCount = metadata.splatCount;
|
||||
header.chunkCount = metadata.chunkCount;
|
||||
header.cameraCount = metadata.cameraCount;
|
||||
header.boundsMin = metadata.bounds.GetMin();
|
||||
header.boundsMax = metadata.bounds.GetMax();
|
||||
header.positionFormat = static_cast<Core::uint32>(metadata.positionFormat);
|
||||
header.otherFormat = static_cast<Core::uint32>(metadata.otherFormat);
|
||||
header.colorFormat = static_cast<Core::uint32>(metadata.colorFormat);
|
||||
header.shFormat = static_cast<Core::uint32>(metadata.shFormat);
|
||||
header.chunkFormat = static_cast<Core::uint32>(metadata.chunkFormat);
|
||||
header.cameraFormat = static_cast<Core::uint32>(metadata.cameraFormat);
|
||||
header.sectionCount = static_cast<Core::uint32>(gaussianSplat.GetSections().Size());
|
||||
header.payloadSize = static_cast<Core::uint64>(gaussianSplat.GetPayloadSize());
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
|
||||
for (const GaussianSplatSection& section : gaussianSplat.GetSections()) {
|
||||
GaussianSplatArtifactSectionRecord sectionRecord;
|
||||
sectionRecord.sectionType = static_cast<Core::uint32>(section.type);
|
||||
sectionRecord.format = static_cast<Core::uint32>(section.format);
|
||||
sectionRecord.payloadOffset = section.dataOffset;
|
||||
sectionRecord.dataSize = section.dataSize;
|
||||
sectionRecord.elementCount = section.elementCount;
|
||||
sectionRecord.elementStride = section.elementStride;
|
||||
output.write(reinterpret_cast<const char*>(§ionRecord), sizeof(sectionRecord));
|
||||
}
|
||||
|
||||
if (gaussianSplat.GetPayloadSize() > 0) {
|
||||
output.write(reinterpret_cast<const char*>(gaussianSplat.GetPayloadData()), gaussianSplat.GetPayloadSize());
|
||||
}
|
||||
|
||||
if (!output && outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write GaussianSplat artifact: ") + artifactPath;
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
LoadResult LoadGaussianSplatArtifact(const Containers::String& path) {
|
||||
const std::filesystem::path resolvedPath = ResolveArtifactPath(path);
|
||||
|
||||
std::ifstream input(resolvedPath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return LoadResult(Containers::String("Failed to read GaussianSplat artifact: ") + path);
|
||||
}
|
||||
|
||||
GaussianSplatArtifactFileHeader fileHeader;
|
||||
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to parse GaussianSplat artifact file header: ") + path);
|
||||
}
|
||||
|
||||
const bool validFileHeader =
|
||||
std::memcmp(fileHeader.magic, "XCGSP01", 7) == 0 &&
|
||||
fileHeader.schemaVersion == kGaussianSplatArtifactSchemaVersion;
|
||||
if (!validFileHeader) {
|
||||
return LoadResult(Containers::String("Invalid GaussianSplat artifact file header: ") + path);
|
||||
}
|
||||
|
||||
GaussianSplatArtifactHeader header;
|
||||
input.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to parse GaussianSplat artifact header: ") + path);
|
||||
}
|
||||
|
||||
Containers::Array<GaussianSplatSection> sections;
|
||||
sections.Reserve(header.sectionCount);
|
||||
for (Core::uint32 index = 0; index < header.sectionCount; ++index) {
|
||||
GaussianSplatArtifactSectionRecord sectionRecord;
|
||||
input.read(reinterpret_cast<char*>(§ionRecord), sizeof(sectionRecord));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read GaussianSplat artifact section table: ") + path);
|
||||
}
|
||||
|
||||
GaussianSplatSection section;
|
||||
section.type = static_cast<GaussianSplatSectionType>(sectionRecord.sectionType);
|
||||
section.format = static_cast<GaussianSplatSectionFormat>(sectionRecord.format);
|
||||
section.dataOffset = sectionRecord.payloadOffset;
|
||||
section.dataSize = sectionRecord.dataSize;
|
||||
section.elementCount = sectionRecord.elementCount;
|
||||
section.elementStride = sectionRecord.elementStride;
|
||||
sections.PushBack(section);
|
||||
}
|
||||
|
||||
Containers::Array<Core::uint8> payload;
|
||||
payload.Resize(static_cast<size_t>(header.payloadSize));
|
||||
if (header.payloadSize > 0) {
|
||||
input.read(reinterpret_cast<char*>(payload.Data()), static_cast<std::streamsize>(header.payloadSize));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read GaussianSplat artifact payload: ") + path);
|
||||
}
|
||||
}
|
||||
|
||||
GaussianSplatMetadata metadata;
|
||||
metadata.contentVersion = header.contentVersion;
|
||||
metadata.splatCount = header.splatCount;
|
||||
metadata.chunkCount = header.chunkCount;
|
||||
metadata.cameraCount = header.cameraCount;
|
||||
metadata.bounds.SetMinMax(header.boundsMin, header.boundsMax);
|
||||
metadata.positionFormat = static_cast<GaussianSplatSectionFormat>(header.positionFormat);
|
||||
metadata.otherFormat = static_cast<GaussianSplatSectionFormat>(header.otherFormat);
|
||||
metadata.colorFormat = static_cast<GaussianSplatSectionFormat>(header.colorFormat);
|
||||
metadata.shFormat = static_cast<GaussianSplatSectionFormat>(header.shFormat);
|
||||
metadata.chunkFormat = static_cast<GaussianSplatSectionFormat>(header.chunkFormat);
|
||||
metadata.cameraFormat = static_cast<GaussianSplatSectionFormat>(header.cameraFormat);
|
||||
|
||||
return CreateOwnedGaussianSplatResource(path, metadata, std::move(sections), std::move(payload));
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
40
engine/src/Resources/GaussianSplat/GaussianSplatLoader.cpp
Normal file
40
engine/src/Resources/GaussianSplat/GaussianSplatLoader.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
GaussianSplatLoader::GaussianSplatLoader() = default;
|
||||
|
||||
GaussianSplatLoader::~GaussianSplatLoader() = default;
|
||||
|
||||
Containers::Array<Containers::String> GaussianSplatLoader::GetSupportedExtensions() const {
|
||||
Containers::Array<Containers::String> extensions;
|
||||
extensions.PushBack("xcgsplat");
|
||||
return extensions;
|
||||
}
|
||||
|
||||
bool GaussianSplatLoader::CanLoad(const Containers::String& path) const {
|
||||
return GetExtension(path).ToLower() == "xcgsplat";
|
||||
}
|
||||
|
||||
LoadResult GaussianSplatLoader::Load(const Containers::String& path, const ImportSettings* settings) {
|
||||
(void)settings;
|
||||
|
||||
if (!CanLoad(path)) {
|
||||
return LoadResult(Containers::String("Unsupported GaussianSplat format: ") + GetExtension(path).ToLower());
|
||||
}
|
||||
|
||||
return LoadGaussianSplatArtifact(path);
|
||||
}
|
||||
|
||||
ImportSettings* GaussianSplatLoader::GetDefaultSettings() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
REGISTER_RESOURCE_LOADER(GaussianSplatLoader);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,634 @@
|
||||
#include "Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.h"
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
namespace Internal {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSHC0 = 0.2820948f;
|
||||
|
||||
enum class PlyScalarType {
|
||||
Invalid = 0,
|
||||
Int8,
|
||||
UInt8,
|
||||
Int16,
|
||||
UInt16,
|
||||
Int32,
|
||||
UInt32,
|
||||
Float32,
|
||||
Float64
|
||||
};
|
||||
|
||||
enum class GaussianSplatSemantic {
|
||||
Ignore = 0,
|
||||
PositionX,
|
||||
PositionY,
|
||||
PositionZ,
|
||||
DC0R,
|
||||
DC0G,
|
||||
DC0B,
|
||||
Opacity,
|
||||
Scale0,
|
||||
Scale1,
|
||||
Scale2,
|
||||
Rot0,
|
||||
Rot1,
|
||||
Rot2,
|
||||
Rot3,
|
||||
SHRest0
|
||||
};
|
||||
|
||||
struct PlyPropertyDesc {
|
||||
std::string name;
|
||||
PlyScalarType type = PlyScalarType::Invalid;
|
||||
GaussianSplatSemantic semantic = GaussianSplatSemantic::Ignore;
|
||||
Core::uint32 shRestIndex = 0;
|
||||
};
|
||||
|
||||
struct ParsedPlyHeader {
|
||||
Core::uint32 vertexCount = 0;
|
||||
Core::uint32 vertexStride = 0;
|
||||
std::vector<PlyPropertyDesc> vertexProperties;
|
||||
};
|
||||
|
||||
Containers::String ToContainersString(const std::string& value) {
|
||||
return Containers::String(value.c_str());
|
||||
}
|
||||
|
||||
std::string TrimAscii(const std::string& value) {
|
||||
const auto begin = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
});
|
||||
const auto end = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
}).base();
|
||||
|
||||
if (begin >= end) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
return std::string(begin, end);
|
||||
}
|
||||
|
||||
PlyScalarType ParsePlyScalarType(const std::string& typeName) {
|
||||
if (typeName == "char" || typeName == "int8") {
|
||||
return PlyScalarType::Int8;
|
||||
}
|
||||
if (typeName == "uchar" || typeName == "uint8") {
|
||||
return PlyScalarType::UInt8;
|
||||
}
|
||||
if (typeName == "short" || typeName == "int16") {
|
||||
return PlyScalarType::Int16;
|
||||
}
|
||||
if (typeName == "ushort" || typeName == "uint16") {
|
||||
return PlyScalarType::UInt16;
|
||||
}
|
||||
if (typeName == "int" || typeName == "int32") {
|
||||
return PlyScalarType::Int32;
|
||||
}
|
||||
if (typeName == "uint" || typeName == "uint32") {
|
||||
return PlyScalarType::UInt32;
|
||||
}
|
||||
if (typeName == "float" || typeName == "float32") {
|
||||
return PlyScalarType::Float32;
|
||||
}
|
||||
if (typeName == "double" || typeName == "float64") {
|
||||
return PlyScalarType::Float64;
|
||||
}
|
||||
return PlyScalarType::Invalid;
|
||||
}
|
||||
|
||||
Core::uint32 GetPlyScalarTypeSize(PlyScalarType type) {
|
||||
switch (type) {
|
||||
case PlyScalarType::Int8:
|
||||
case PlyScalarType::UInt8:
|
||||
return 1u;
|
||||
case PlyScalarType::Int16:
|
||||
case PlyScalarType::UInt16:
|
||||
return 2u;
|
||||
case PlyScalarType::Int32:
|
||||
case PlyScalarType::UInt32:
|
||||
case PlyScalarType::Float32:
|
||||
return 4u;
|
||||
case PlyScalarType::Float64:
|
||||
return 8u;
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryParseSHRestIndex(const std::string& propertyName, Core::uint32& outIndex) {
|
||||
static constexpr const char* kPrefix = "f_rest_";
|
||||
if (propertyName.rfind(kPrefix, 0) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string suffix = propertyName.substr(std::char_traits<char>::length(kPrefix));
|
||||
if (suffix.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char ch : suffix) {
|
||||
if (!std::isdigit(static_cast<unsigned char>(ch))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
outIndex = static_cast<Core::uint32>(std::stoul(suffix));
|
||||
return true;
|
||||
}
|
||||
|
||||
GaussianSplatSemantic MapPropertySemantic(const std::string& propertyName, Core::uint32& outSHRestIndex) {
|
||||
outSHRestIndex = 0u;
|
||||
|
||||
if (propertyName == "x") return GaussianSplatSemantic::PositionX;
|
||||
if (propertyName == "y") return GaussianSplatSemantic::PositionY;
|
||||
if (propertyName == "z") return GaussianSplatSemantic::PositionZ;
|
||||
if (propertyName == "f_dc_0") return GaussianSplatSemantic::DC0R;
|
||||
if (propertyName == "f_dc_1") return GaussianSplatSemantic::DC0G;
|
||||
if (propertyName == "f_dc_2") return GaussianSplatSemantic::DC0B;
|
||||
if (propertyName == "opacity") return GaussianSplatSemantic::Opacity;
|
||||
if (propertyName == "scale_0") return GaussianSplatSemantic::Scale0;
|
||||
if (propertyName == "scale_1") return GaussianSplatSemantic::Scale1;
|
||||
if (propertyName == "scale_2") return GaussianSplatSemantic::Scale2;
|
||||
if (propertyName == "rot_0") return GaussianSplatSemantic::Rot0;
|
||||
if (propertyName == "rot_1") return GaussianSplatSemantic::Rot1;
|
||||
if (propertyName == "rot_2") return GaussianSplatSemantic::Rot2;
|
||||
if (propertyName == "rot_3") return GaussianSplatSemantic::Rot3;
|
||||
if (TryParseSHRestIndex(propertyName, outSHRestIndex)) return GaussianSplatSemantic::SHRest0;
|
||||
return GaussianSplatSemantic::Ignore;
|
||||
}
|
||||
|
||||
bool ReadScalarAsFloat(std::ifstream& input, PlyScalarType type, float& outValue) {
|
||||
switch (type) {
|
||||
case PlyScalarType::Int8: {
|
||||
std::int8_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::UInt8: {
|
||||
std::uint8_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::Int16: {
|
||||
std::int16_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::UInt16: {
|
||||
std::uint16_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::Int32: {
|
||||
std::int32_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::UInt32: {
|
||||
std::uint32_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::Float32: {
|
||||
float value = 0.0f;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = value;
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::Float64: {
|
||||
double value = 0.0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ParsePlyHeader(std::ifstream& input,
|
||||
ParsedPlyHeader& outHeader,
|
||||
Containers::String& outErrorMessage) {
|
||||
outHeader = {};
|
||||
outErrorMessage.Clear();
|
||||
|
||||
std::string line;
|
||||
if (!std::getline(input, line)) {
|
||||
outErrorMessage = "PLY header is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
line = TrimAscii(line);
|
||||
if (line != "ply") {
|
||||
outErrorMessage = "PLY header missing magic 'ply'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!std::getline(input, line)) {
|
||||
outErrorMessage = "PLY header missing format line";
|
||||
return false;
|
||||
}
|
||||
|
||||
line = TrimAscii(line);
|
||||
if (line != "format binary_little_endian 1.0") {
|
||||
outErrorMessage = Containers::String("Unsupported PLY format: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool seenVertexElement = false;
|
||||
bool sawNonVertexElementBeforeVertex = false;
|
||||
bool parsingVertexProperties = false;
|
||||
std::unordered_set<std::string> propertyNames;
|
||||
|
||||
while (std::getline(input, line)) {
|
||||
line = TrimAscii(line);
|
||||
if (line.empty() || line == "comment" || line.rfind("comment ", 0) == 0 || line.rfind("obj_info ", 0) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "end_header") {
|
||||
break;
|
||||
}
|
||||
|
||||
std::istringstream lineStream(line);
|
||||
std::string token;
|
||||
lineStream >> token;
|
||||
if (token == "element") {
|
||||
std::string elementName;
|
||||
std::uint64_t elementCount = 0;
|
||||
lineStream >> elementName >> elementCount;
|
||||
if (!lineStream) {
|
||||
outErrorMessage = Containers::String("Malformed PLY element line: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
parsingVertexProperties = (elementName == "vertex");
|
||||
if (parsingVertexProperties) {
|
||||
if (sawNonVertexElementBeforeVertex) {
|
||||
outErrorMessage = "Unsupported PLY layout: vertex element must be the first data element";
|
||||
return false;
|
||||
}
|
||||
outHeader.vertexCount = static_cast<Core::uint32>(elementCount);
|
||||
seenVertexElement = true;
|
||||
} else if (!seenVertexElement) {
|
||||
sawNonVertexElementBeforeVertex = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token == "property") {
|
||||
std::string typeName;
|
||||
lineStream >> typeName;
|
||||
if (!lineStream) {
|
||||
outErrorMessage = Containers::String("Malformed PLY property line: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeName == "list") {
|
||||
outErrorMessage = Containers::String("Unsupported PLY list property: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string propertyName;
|
||||
lineStream >> propertyName;
|
||||
if (!lineStream) {
|
||||
outErrorMessage = Containers::String("Malformed PLY property line: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parsingVertexProperties) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!propertyNames.insert(propertyName).second) {
|
||||
outErrorMessage = Containers::String("Duplicate PLY vertex property: ") + ToContainersString(propertyName);
|
||||
return false;
|
||||
}
|
||||
|
||||
PlyPropertyDesc desc;
|
||||
desc.name = propertyName;
|
||||
desc.type = ParsePlyScalarType(typeName);
|
||||
if (desc.type == PlyScalarType::Invalid) {
|
||||
outErrorMessage = Containers::String("Unsupported PLY property type: ") + ToContainersString(typeName);
|
||||
return false;
|
||||
}
|
||||
|
||||
desc.semantic = MapPropertySemantic(propertyName, desc.shRestIndex);
|
||||
outHeader.vertexStride += GetPlyScalarTypeSize(desc.type);
|
||||
outHeader.vertexProperties.push_back(desc);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
outErrorMessage = "Failed while parsing PLY header";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!seenVertexElement || outHeader.vertexCount == 0u) {
|
||||
outErrorMessage = "PLY file does not declare a valid vertex element";
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr const char* kRequiredProperties[] = {
|
||||
"x", "y", "z",
|
||||
"f_dc_0", "f_dc_1", "f_dc_2",
|
||||
"opacity",
|
||||
"scale_0", "scale_1", "scale_2",
|
||||
"rot_0", "rot_1", "rot_2", "rot_3"
|
||||
};
|
||||
for (const char* propertyName : kRequiredProperties) {
|
||||
if (propertyNames.find(propertyName) == propertyNames.end()) {
|
||||
outErrorMessage = Containers::String("PLY file is missing required 3DGS property: ") + propertyName;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
const std::string propertyName = "f_rest_" + std::to_string(index);
|
||||
if (propertyNames.find(propertyName) == propertyNames.end()) {
|
||||
outErrorMessage = Containers::String("PLY file is missing required 3DGS SH property: ") +
|
||||
ToContainersString(propertyName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveSourcePath(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;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPath.lexically_normal();
|
||||
}
|
||||
|
||||
float Sigmoid(float value) {
|
||||
return 1.0f / (1.0f + std::exp(-value));
|
||||
}
|
||||
|
||||
Math::Vector3 SH0ToColor(const std::array<float, 3>& dc0) {
|
||||
return Math::Vector3(
|
||||
dc0[0] * kSHC0 + 0.5f,
|
||||
dc0[1] * kSHC0 + 0.5f,
|
||||
dc0[2] * kSHC0 + 0.5f);
|
||||
}
|
||||
|
||||
Math::Vector3 LinearScale(const std::array<float, 3>& logScale) {
|
||||
return Math::Vector3(
|
||||
std::abs(std::exp(logScale[0])),
|
||||
std::abs(std::exp(logScale[1])),
|
||||
std::abs(std::exp(logScale[2])));
|
||||
}
|
||||
|
||||
Math::Quaternion NormalizeRotationWXYZ(const std::array<float, 4>& rotationWXYZ) {
|
||||
const float magnitudeSq =
|
||||
rotationWXYZ[0] * rotationWXYZ[0] +
|
||||
rotationWXYZ[1] * rotationWXYZ[1] +
|
||||
rotationWXYZ[2] * rotationWXYZ[2] +
|
||||
rotationWXYZ[3] * rotationWXYZ[3];
|
||||
if (magnitudeSq <= std::numeric_limits<float>::epsilon()) {
|
||||
return Math::Quaternion::Identity();
|
||||
}
|
||||
|
||||
const float inverseMagnitude = 1.0f / std::sqrt(magnitudeSq);
|
||||
return Math::Quaternion(
|
||||
rotationWXYZ[1] * inverseMagnitude,
|
||||
rotationWXYZ[2] * inverseMagnitude,
|
||||
rotationWXYZ[3] * inverseMagnitude,
|
||||
rotationWXYZ[0] * inverseMagnitude);
|
||||
}
|
||||
|
||||
bool IsFinite(const Math::Vector3& value) {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
|
||||
}
|
||||
|
||||
bool IsFinite(const Math::Vector4& value) {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w);
|
||||
}
|
||||
|
||||
bool IsFinite(const Math::Quaternion& value) {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w);
|
||||
}
|
||||
|
||||
void BuildSections(Core::uint32 vertexCount,
|
||||
Containers::Array<GaussianSplatSection>& outSections,
|
||||
size_t& outPayloadSize) {
|
||||
outSections.Clear();
|
||||
outSections.Reserve(4u);
|
||||
|
||||
size_t payloadOffset = 0u;
|
||||
auto appendSection = [&](GaussianSplatSectionType type,
|
||||
GaussianSplatSectionFormat format,
|
||||
size_t elementStride) {
|
||||
GaussianSplatSection section;
|
||||
section.type = type;
|
||||
section.format = format;
|
||||
section.dataOffset = payloadOffset;
|
||||
section.dataSize = elementStride * static_cast<size_t>(vertexCount);
|
||||
section.elementCount = vertexCount;
|
||||
section.elementStride = static_cast<Core::uint32>(elementStride);
|
||||
outSections.PushBack(section);
|
||||
payloadOffset += section.dataSize;
|
||||
};
|
||||
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Positions,
|
||||
GaussianSplatSectionFormat::VectorFloat32,
|
||||
sizeof(GaussianSplatPositionRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Other,
|
||||
GaussianSplatSectionFormat::OtherFloat32,
|
||||
sizeof(GaussianSplatOtherRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Color,
|
||||
GaussianSplatSectionFormat::ColorRGBA32F,
|
||||
sizeof(GaussianSplatColorRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::SH,
|
||||
GaussianSplatSectionFormat::SHFloat32,
|
||||
sizeof(GaussianSplatSHRecord));
|
||||
|
||||
outPayloadSize = payloadOffset;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LoadResult ImportGaussianSplatPlyFile(const Containers::String& path) {
|
||||
const std::filesystem::path resolvedPath = ResolveSourcePath(path);
|
||||
std::ifstream input(resolvedPath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return LoadResult(Containers::String("Failed to read GaussianSplat source file: ") + path);
|
||||
}
|
||||
|
||||
ParsedPlyHeader header;
|
||||
Containers::String headerError;
|
||||
if (!ParsePlyHeader(input, header, headerError)) {
|
||||
return LoadResult(Containers::String("Failed to parse GaussianSplat PLY header: ") + path + " - " + headerError);
|
||||
}
|
||||
|
||||
Containers::Array<GaussianSplatSection> sections;
|
||||
size_t payloadSize = 0u;
|
||||
BuildSections(header.vertexCount, sections, payloadSize);
|
||||
|
||||
Containers::Array<Core::uint8> payload;
|
||||
payload.Resize(payloadSize);
|
||||
|
||||
auto* positions = reinterpret_cast<GaussianSplatPositionRecord*>(
|
||||
payload.Data() + static_cast<size_t>(sections[0].dataOffset));
|
||||
auto* other = reinterpret_cast<GaussianSplatOtherRecord*>(
|
||||
payload.Data() + static_cast<size_t>(sections[1].dataOffset));
|
||||
auto* colors = reinterpret_cast<GaussianSplatColorRecord*>(
|
||||
payload.Data() + static_cast<size_t>(sections[2].dataOffset));
|
||||
auto* shRecords = reinterpret_cast<GaussianSplatSHRecord*>(
|
||||
payload.Data() + static_cast<size_t>(sections[3].dataOffset));
|
||||
|
||||
Math::Bounds bounds;
|
||||
bool hasBounds = false;
|
||||
|
||||
for (Core::uint32 vertexIndex = 0; vertexIndex < header.vertexCount; ++vertexIndex) {
|
||||
std::array<float, 3> position = { 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, 3> dc0 = { 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, 3> scale = { 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, 4> rotation = { 1.0f, 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, kGaussianSplatSHCoefficientCount> sh = {};
|
||||
float opacity = 0.0f;
|
||||
|
||||
for (const PlyPropertyDesc& property : header.vertexProperties) {
|
||||
float value = 0.0f;
|
||||
if (!ReadScalarAsFloat(input, property.type, value)) {
|
||||
return LoadResult(
|
||||
Containers::String("Failed to read GaussianSplat vertex data: ") +
|
||||
path +
|
||||
" at vertex " +
|
||||
Containers::String(std::to_string(vertexIndex).c_str()));
|
||||
}
|
||||
|
||||
switch (property.semantic) {
|
||||
case GaussianSplatSemantic::PositionX: position[0] = value; break;
|
||||
case GaussianSplatSemantic::PositionY: position[1] = value; break;
|
||||
case GaussianSplatSemantic::PositionZ: position[2] = value; break;
|
||||
case GaussianSplatSemantic::DC0R: dc0[0] = value; break;
|
||||
case GaussianSplatSemantic::DC0G: dc0[1] = value; break;
|
||||
case GaussianSplatSemantic::DC0B: dc0[2] = value; break;
|
||||
case GaussianSplatSemantic::Opacity: opacity = value; break;
|
||||
case GaussianSplatSemantic::Scale0: scale[0] = value; break;
|
||||
case GaussianSplatSemantic::Scale1: scale[1] = value; break;
|
||||
case GaussianSplatSemantic::Scale2: scale[2] = value; break;
|
||||
case GaussianSplatSemantic::Rot0: rotation[0] = value; break;
|
||||
case GaussianSplatSemantic::Rot1: rotation[1] = value; break;
|
||||
case GaussianSplatSemantic::Rot2: rotation[2] = value; break;
|
||||
case GaussianSplatSemantic::Rot3: rotation[3] = value; break;
|
||||
case GaussianSplatSemantic::SHRest0:
|
||||
if (property.shRestIndex < sh.size()) {
|
||||
sh[property.shRestIndex] = value;
|
||||
}
|
||||
break;
|
||||
case GaussianSplatSemantic::Ignore:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
positions[vertexIndex].position = Math::Vector3(position[0], position[1], position[2]);
|
||||
other[vertexIndex].rotation = NormalizeRotationWXYZ(rotation);
|
||||
other[vertexIndex].scale = LinearScale(scale);
|
||||
other[vertexIndex].reserved = 0.0f;
|
||||
|
||||
const Math::Vector3 color = SH0ToColor(dc0);
|
||||
colors[vertexIndex].colorOpacity = Math::Vector4(color, Sigmoid(opacity));
|
||||
|
||||
for (size_t shIndex = 0; shIndex < sh.size(); ++shIndex) {
|
||||
shRecords[vertexIndex].coefficients[shIndex] = sh[shIndex];
|
||||
}
|
||||
|
||||
if (!IsFinite(positions[vertexIndex].position) ||
|
||||
!IsFinite(other[vertexIndex].rotation) ||
|
||||
!IsFinite(other[vertexIndex].scale) ||
|
||||
!IsFinite(colors[vertexIndex].colorOpacity)) {
|
||||
return LoadResult(
|
||||
Containers::String("GaussianSplat source contains non-finite values: ") +
|
||||
path +
|
||||
" at vertex " +
|
||||
Containers::String(std::to_string(vertexIndex).c_str()));
|
||||
}
|
||||
|
||||
if (!hasBounds) {
|
||||
bounds.SetMinMax(positions[vertexIndex].position, positions[vertexIndex].position);
|
||||
hasBounds = true;
|
||||
} else {
|
||||
bounds.Encapsulate(positions[vertexIndex].position);
|
||||
}
|
||||
}
|
||||
|
||||
GaussianSplatMetadata metadata;
|
||||
metadata.contentVersion = 1u;
|
||||
metadata.splatCount = header.vertexCount;
|
||||
metadata.chunkCount = 0u;
|
||||
metadata.cameraCount = 0u;
|
||||
metadata.bounds = bounds;
|
||||
metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32;
|
||||
metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32;
|
||||
metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
|
||||
metadata.shFormat = GaussianSplatSectionFormat::SHFloat32;
|
||||
metadata.chunkFormat = GaussianSplatSectionFormat::Unknown;
|
||||
metadata.cameraFormat = GaussianSplatSectionFormat::Unknown;
|
||||
|
||||
auto* gaussianSplat = new GaussianSplat();
|
||||
IResource::ConstructParams params;
|
||||
params.name = GetResourceNameFromPath(path);
|
||||
params.path = path;
|
||||
params.guid = ResourceGUID::Generate(path);
|
||||
params.memorySize = payload.Size();
|
||||
gaussianSplat->Initialize(params);
|
||||
|
||||
if (!gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))) {
|
||||
delete gaussianSplat;
|
||||
return LoadResult(Containers::String("Failed to create GaussianSplat resource from PLY: ") + path);
|
||||
}
|
||||
|
||||
return LoadResult(gaussianSplat);
|
||||
}
|
||||
|
||||
} // namespace Internal
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
namespace Internal {
|
||||
|
||||
LoadResult ImportGaussianSplatPlyFile(const Containers::String& path);
|
||||
|
||||
} // namespace Internal
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
1054
engine/src/Resources/Model/AssimpModelImporter.cpp
Normal file
1054
engine/src/Resources/Model/AssimpModelImporter.cpp
Normal file
File diff suppressed because it is too large
Load Diff
57
engine/src/Resources/Model/Model.cpp
Normal file
57
engine/src/Resources/Model/Model.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
Model::Model() = default;
|
||||
|
||||
Model::~Model() {
|
||||
Release();
|
||||
}
|
||||
|
||||
void Model::Release() {
|
||||
ClearGraph();
|
||||
SetInvalid();
|
||||
}
|
||||
|
||||
void Model::SetRootNodeIndex(Core::uint32 index) {
|
||||
m_rootNodeIndex = index;
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::AddNode(const ModelNode& node) {
|
||||
m_nodes.PushBack(node);
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::AddMeshBinding(const ModelMeshBinding& binding) {
|
||||
m_meshBindings.PushBack(binding);
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::AddMaterialBinding(const ModelMaterialBinding& binding) {
|
||||
m_materialBindings.PushBack(binding);
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::ClearGraph() {
|
||||
m_rootNodeIndex = kInvalidModelNodeIndex;
|
||||
m_nodes.Clear();
|
||||
m_meshBindings.Clear();
|
||||
m_materialBindings.Clear();
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::UpdateMemorySize() {
|
||||
m_memorySize =
|
||||
m_nodes.Size() * sizeof(ModelNode) +
|
||||
m_meshBindings.Size() * sizeof(ModelMeshBinding) +
|
||||
m_materialBindings.Size() * sizeof(ModelMaterialBinding);
|
||||
|
||||
for (const ModelNode& node : m_nodes) {
|
||||
m_memorySize += node.name.Length();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
261
engine/src/Resources/Model/ModelArtifactIO.cpp
Normal file
261
engine/src/Resources/Model/ModelArtifactIO.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveArtifactPath(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;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPath.lexically_normal();
|
||||
}
|
||||
|
||||
void WriteString(std::ofstream& stream, const Containers::String& value) {
|
||||
const Core::uint32 length = static_cast<Core::uint32>(value.Length());
|
||||
stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
|
||||
if (length > 0) {
|
||||
stream.write(value.CStr(), length);
|
||||
}
|
||||
}
|
||||
|
||||
Containers::String ReadString(std::ifstream& stream) {
|
||||
Core::uint32 length = 0;
|
||||
stream.read(reinterpret_cast<char*>(&length), sizeof(length));
|
||||
if (!stream || length == 0) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
std::string buffer(length, '\0');
|
||||
stream.read(buffer.data(), length);
|
||||
if (!stream) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
return Containers::String(buffer.c_str());
|
||||
}
|
||||
|
||||
LoadResult CreateOwnedModelResource(const Containers::String& path,
|
||||
const ModelArtifactHeader& header,
|
||||
Containers::Array<ModelNode>&& nodes,
|
||||
Containers::Array<ModelMeshBinding>&& meshBindings,
|
||||
Containers::Array<ModelMaterialBinding>&& materialBindings) {
|
||||
auto* model = new Model();
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = GetResourceNameFromPath(path);
|
||||
params.path = path;
|
||||
params.guid = ResourceGUID::Generate(path);
|
||||
params.memorySize = 0;
|
||||
model->Initialize(params);
|
||||
|
||||
if (header.rootNodeIndex != kInvalidModelNodeIndex) {
|
||||
model->SetRootNodeIndex(header.rootNodeIndex);
|
||||
}
|
||||
|
||||
for (const ModelNode& node : nodes) {
|
||||
model->AddNode(node);
|
||||
}
|
||||
|
||||
for (const ModelMeshBinding& binding : meshBindings) {
|
||||
model->AddMeshBinding(binding);
|
||||
}
|
||||
|
||||
for (const ModelMaterialBinding& binding : materialBindings) {
|
||||
model->AddMaterialBinding(binding);
|
||||
}
|
||||
|
||||
return LoadResult(model);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WriteModelArtifactFile(const Containers::String& artifactPath,
|
||||
const Model& model,
|
||||
Containers::String* outErrorMessage) {
|
||||
const std::filesystem::path resolvedPath = ResolveArtifactPath(artifactPath);
|
||||
std::error_code ec;
|
||||
const std::filesystem::path parentPath = resolvedPath.parent_path();
|
||||
if (!parentPath.empty()) {
|
||||
std::filesystem::create_directories(parentPath, ec);
|
||||
if (ec) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage =
|
||||
Containers::String("Failed to create model artifact directory: ") +
|
||||
Containers::String(parentPath.generic_string().c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream output(resolvedPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to open model artifact for write: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ModelArtifactFileHeader fileHeader;
|
||||
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
|
||||
|
||||
ModelArtifactHeader header;
|
||||
header.nodeCount = static_cast<Core::uint32>(model.GetNodes().Size());
|
||||
header.meshBindingCount = static_cast<Core::uint32>(model.GetMeshBindings().Size());
|
||||
header.materialBindingCount = static_cast<Core::uint32>(model.GetMaterialBindings().Size());
|
||||
header.rootNodeIndex = model.GetRootNodeIndex();
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
|
||||
for (const ModelNode& node : model.GetNodes()) {
|
||||
WriteString(output, node.name);
|
||||
|
||||
ModelNodeArtifactHeader nodeHeader;
|
||||
nodeHeader.parentIndex = node.parentIndex;
|
||||
nodeHeader.meshBindingStart = node.meshBindingStart;
|
||||
nodeHeader.meshBindingCount = node.meshBindingCount;
|
||||
nodeHeader.localPosition = node.localPosition;
|
||||
nodeHeader.localRotation = node.localRotation;
|
||||
nodeHeader.localScale = node.localScale;
|
||||
output.write(reinterpret_cast<const char*>(&nodeHeader), sizeof(nodeHeader));
|
||||
}
|
||||
|
||||
for (const ModelMeshBinding& binding : model.GetMeshBindings()) {
|
||||
ModelMeshBindingArtifact bindingArtifact;
|
||||
bindingArtifact.meshLocalID = binding.meshLocalID;
|
||||
bindingArtifact.materialBindingStart = binding.materialBindingStart;
|
||||
bindingArtifact.materialBindingCount = binding.materialBindingCount;
|
||||
output.write(reinterpret_cast<const char*>(&bindingArtifact), sizeof(bindingArtifact));
|
||||
}
|
||||
|
||||
for (const ModelMaterialBinding& binding : model.GetMaterialBindings()) {
|
||||
ModelMaterialBindingArtifact bindingArtifact;
|
||||
bindingArtifact.slotIndex = binding.slotIndex;
|
||||
bindingArtifact.materialLocalID = binding.materialLocalID;
|
||||
output.write(reinterpret_cast<const char*>(&bindingArtifact), sizeof(bindingArtifact));
|
||||
}
|
||||
|
||||
if (!output && outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write model artifact: ") + artifactPath;
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
LoadResult LoadModelArtifact(const Containers::String& path) {
|
||||
const std::filesystem::path resolvedPath = ResolveArtifactPath(path);
|
||||
|
||||
std::ifstream input(resolvedPath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return LoadResult(Containers::String("Failed to read model artifact: ") + path);
|
||||
}
|
||||
|
||||
ModelArtifactFileHeader fileHeader;
|
||||
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to parse model artifact file header: ") + path);
|
||||
}
|
||||
|
||||
const bool validFileHeader =
|
||||
std::memcmp(fileHeader.magic, "XCMOD01", 7) == 0 &&
|
||||
fileHeader.schemaVersion == kModelArtifactSchemaVersion;
|
||||
if (!validFileHeader) {
|
||||
return LoadResult(Containers::String("Invalid model artifact file header: ") + path);
|
||||
}
|
||||
|
||||
ModelArtifactHeader header;
|
||||
input.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to parse model artifact header: ") + path);
|
||||
}
|
||||
|
||||
if (header.rootNodeIndex != kInvalidModelNodeIndex &&
|
||||
header.rootNodeIndex >= header.nodeCount) {
|
||||
return LoadResult(Containers::String("Invalid model artifact root node index: ") + path);
|
||||
}
|
||||
|
||||
Containers::Array<ModelNode> nodes;
|
||||
nodes.Reserve(header.nodeCount);
|
||||
for (Core::uint32 index = 0; index < header.nodeCount; ++index) {
|
||||
ModelNode node;
|
||||
node.name = ReadString(input);
|
||||
|
||||
ModelNodeArtifactHeader nodeHeader;
|
||||
input.read(reinterpret_cast<char*>(&nodeHeader), sizeof(nodeHeader));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read model node artifact: ") + path);
|
||||
}
|
||||
|
||||
node.parentIndex = nodeHeader.parentIndex;
|
||||
node.meshBindingStart = nodeHeader.meshBindingStart;
|
||||
node.meshBindingCount = nodeHeader.meshBindingCount;
|
||||
node.localPosition = nodeHeader.localPosition;
|
||||
node.localRotation = nodeHeader.localRotation;
|
||||
node.localScale = nodeHeader.localScale;
|
||||
nodes.PushBack(std::move(node));
|
||||
}
|
||||
|
||||
Containers::Array<ModelMeshBinding> meshBindings;
|
||||
meshBindings.Reserve(header.meshBindingCount);
|
||||
for (Core::uint32 index = 0; index < header.meshBindingCount; ++index) {
|
||||
ModelMeshBindingArtifact bindingArtifact;
|
||||
input.read(reinterpret_cast<char*>(&bindingArtifact), sizeof(bindingArtifact));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read model mesh binding artifact: ") + path);
|
||||
}
|
||||
|
||||
ModelMeshBinding binding;
|
||||
binding.meshLocalID = bindingArtifact.meshLocalID;
|
||||
binding.materialBindingStart = bindingArtifact.materialBindingStart;
|
||||
binding.materialBindingCount = bindingArtifact.materialBindingCount;
|
||||
meshBindings.PushBack(binding);
|
||||
}
|
||||
|
||||
Containers::Array<ModelMaterialBinding> materialBindings;
|
||||
materialBindings.Reserve(header.materialBindingCount);
|
||||
for (Core::uint32 index = 0; index < header.materialBindingCount; ++index) {
|
||||
ModelMaterialBindingArtifact bindingArtifact;
|
||||
input.read(reinterpret_cast<char*>(&bindingArtifact), sizeof(bindingArtifact));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read model material binding artifact: ") + path);
|
||||
}
|
||||
|
||||
ModelMaterialBinding binding;
|
||||
binding.slotIndex = bindingArtifact.slotIndex;
|
||||
binding.materialLocalID = bindingArtifact.materialLocalID;
|
||||
materialBindings.PushBack(binding);
|
||||
}
|
||||
|
||||
return CreateOwnedModelResource(
|
||||
path,
|
||||
header,
|
||||
std::move(nodes),
|
||||
std::move(meshBindings),
|
||||
std::move(materialBindings));
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
40
engine/src/Resources/Model/ModelLoader.cpp
Normal file
40
engine/src/Resources/Model/ModelLoader.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
ModelLoader::ModelLoader() = default;
|
||||
|
||||
ModelLoader::~ModelLoader() = default;
|
||||
|
||||
Containers::Array<Containers::String> ModelLoader::GetSupportedExtensions() const {
|
||||
Containers::Array<Containers::String> extensions;
|
||||
extensions.PushBack("xcmodel");
|
||||
return extensions;
|
||||
}
|
||||
|
||||
bool ModelLoader::CanLoad(const Containers::String& path) const {
|
||||
return GetExtension(path).ToLower() == "xcmodel";
|
||||
}
|
||||
|
||||
LoadResult ModelLoader::Load(const Containers::String& path, const ImportSettings* settings) {
|
||||
(void)settings;
|
||||
|
||||
if (!CanLoad(path)) {
|
||||
return LoadResult(Containers::String("Unsupported model format: ") + GetExtension(path).ToLower());
|
||||
}
|
||||
|
||||
return LoadModelArtifact(path);
|
||||
}
|
||||
|
||||
ImportSettings* ModelLoader::GetDefaultSettings() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
REGISTER_RESOURCE_LOADER(ModelLoader);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
Reference in New Issue
Block a user