Add Nahida model import and preview pipeline
This commit is contained in:
@@ -325,6 +325,7 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetGUID.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetRef.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ArtifactFormats.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ArtifactContainer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetDatabase.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetImportService.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ProjectAssetIndex.h
|
||||
@@ -336,6 +337,7 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AsyncLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceDependencyGraph.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetGUID.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ArtifactContainer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetDatabase.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetImportService.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ProjectAssetIndex.cpp
|
||||
@@ -543,10 +545,12 @@ add_library(XCEngine STATIC
|
||||
|
||||
# Scene
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/ModelSceneInstantiation.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneRuntime.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/RuntimeLoop.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/Scene.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/ModelSceneInstantiation.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneRuntime.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/RuntimeLoop.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneManager.cpp
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
const Resources::AssetRef& GetMeshAssetRef() const { return m_meshRef; }
|
||||
|
||||
void SetMeshPath(const std::string& meshPath);
|
||||
void SetMeshAssetRef(const Resources::AssetRef& meshRef);
|
||||
void SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh);
|
||||
void SetMesh(Resources::Mesh* mesh);
|
||||
void ClearMesh();
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
const std::vector<Resources::AssetRef>& GetMaterialAssetRefs() const { return m_materialRefs; }
|
||||
|
||||
void SetMaterialPath(size_t index, const std::string& materialPath);
|
||||
void SetMaterialAssetRef(size_t index, const Resources::AssetRef& materialRef);
|
||||
void SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material);
|
||||
void SetMaterial(size_t index, Resources::Material* material);
|
||||
void SetMaterials(const std::vector<Resources::ResourceHandle<Resources::Material>>& materials);
|
||||
|
||||
105
engine/include/XCEngine/Core/Asset/ArtifactContainer.h
Normal file
105
engine/include/XCEngine/Core/Asset/ArtifactContainer.h
Normal file
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
constexpr Core::uint32 kArtifactContainerSchemaVersion = 1;
|
||||
|
||||
enum class ArtifactContainerCompression : Core::uint32 {
|
||||
None = 0
|
||||
};
|
||||
|
||||
struct ArtifactContainerEntry {
|
||||
Containers::String name;
|
||||
ResourceType resourceType = ResourceType::Unknown;
|
||||
LocalID localID = kInvalidLocalID;
|
||||
Core::uint32 flags = 0;
|
||||
ArtifactContainerCompression compression = ArtifactContainerCompression::None;
|
||||
Containers::Array<Core::uint8> payload;
|
||||
};
|
||||
|
||||
struct ArtifactContainerEntryView {
|
||||
Containers::String name;
|
||||
ResourceType resourceType = ResourceType::Unknown;
|
||||
LocalID localID = kInvalidLocalID;
|
||||
Core::uint32 flags = 0;
|
||||
ArtifactContainerCompression compression = ArtifactContainerCompression::None;
|
||||
Core::uint64 payloadOffset = 0;
|
||||
Core::uint64 payloadSize = 0;
|
||||
};
|
||||
|
||||
class ArtifactContainerWriter {
|
||||
public:
|
||||
void Clear();
|
||||
void AddEntry(const ArtifactContainerEntry& entry);
|
||||
void AddEntry(ArtifactContainerEntry&& entry);
|
||||
|
||||
const Containers::Array<ArtifactContainerEntry>& GetEntries() const { return m_entries; }
|
||||
|
||||
bool WriteToFile(const Containers::String& path,
|
||||
Containers::String* outErrorMessage = nullptr) const;
|
||||
|
||||
private:
|
||||
Containers::Array<ArtifactContainerEntry> m_entries;
|
||||
};
|
||||
|
||||
class ArtifactContainerReader {
|
||||
public:
|
||||
bool Open(const Containers::String& path,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
void Close();
|
||||
|
||||
bool IsOpen() const { return !m_path.Empty(); }
|
||||
const Containers::String& GetPath() const { return m_path; }
|
||||
const Containers::Array<ArtifactContainerEntryView>& GetEntries() const { return m_entries; }
|
||||
Core::uint32 GetEntryCount() const { return static_cast<Core::uint32>(m_entries.Size()); }
|
||||
|
||||
const ArtifactContainerEntryView* FindEntryByName(const Containers::String& name) const;
|
||||
const ArtifactContainerEntryView* FindEntry(ResourceType resourceType,
|
||||
LocalID localID) const;
|
||||
|
||||
bool ReadEntryPayload(const ArtifactContainerEntryView& entry,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage = nullptr) const;
|
||||
bool ReadEntryPayload(const Containers::String& name,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage = nullptr) const;
|
||||
|
||||
private:
|
||||
Containers::String m_path;
|
||||
Containers::Array<ArtifactContainerEntryView> m_entries;
|
||||
Core::uint64 m_payloadStart = 0;
|
||||
Core::uint64 m_payloadSize = 0;
|
||||
};
|
||||
|
||||
bool WriteArtifactContainer(const Containers::String& path,
|
||||
const Containers::Array<ArtifactContainerEntry>& entries,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
bool IsArtifactContainerFile(const Containers::String& path);
|
||||
Containers::String BuildArtifactContainerEntryPath(const Containers::String& containerPath,
|
||||
const Containers::String& entryName);
|
||||
bool TryParseArtifactContainerEntryPath(const Containers::String& path,
|
||||
Containers::String& outContainerPath,
|
||||
Containers::String& outEntryName);
|
||||
bool ReadArtifactContainerEntryPayload(const Containers::String& containerPath,
|
||||
const Containers::String& entryName,
|
||||
ResourceType expectedType,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
bool ReadArtifactContainerPayloadByPath(const Containers::String& path,
|
||||
ResourceType expectedType,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
bool ReadArtifactContainerMainEntryPayload(const Containers::String& path,
|
||||
ResourceType expectedType,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -175,6 +175,7 @@ private:
|
||||
Core::uint64 materialVersion = 0;
|
||||
RHI::RHIResourceView* baseColorTextureView = nullptr;
|
||||
RHI::RHIResourceView* shadowMapTextureView = nullptr;
|
||||
std::vector<RHI::RHIResourceView*> materialTextureViews;
|
||||
};
|
||||
|
||||
struct ResolvedShaderPass {
|
||||
@@ -305,6 +306,9 @@ private:
|
||||
const Resources::Texture* ResolveTexture(const Resources::Material* material) const;
|
||||
RHI::RHIResourceView* ResolveTextureView(const Resources::Texture* texture);
|
||||
RHI::RHIResourceView* ResolveTextureView(const VisibleRenderItem& visibleItem);
|
||||
RHI::RHIResourceView* ResolveMaterialTextureView(
|
||||
const Resources::Material* material,
|
||||
const BuiltinPassResourceBindingDesc& binding);
|
||||
static LightingConstants BuildLightingConstants(const RenderLightingData& lightingData);
|
||||
static AdditionalLightConstants BuildAdditionalLightConstants(const RenderAdditionalLightData& lightData);
|
||||
bool HasSkybox(const RenderSceneData& sceneData) const;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Core/Math/Vector4.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -38,6 +39,8 @@ struct StaticMeshVertex {
|
||||
Math::Vector3 tangent = Math::Vector3::Zero();
|
||||
Math::Vector3 bitangent = Math::Vector3::Zero();
|
||||
Math::Vector2 uv0 = Math::Vector2::Zero();
|
||||
Math::Vector2 uv1 = Math::Vector2::Zero();
|
||||
Math::Vector4 color = Math::Vector4::One();
|
||||
};
|
||||
|
||||
struct MeshSection {
|
||||
|
||||
37
engine/include/XCEngine/Scene/ModelSceneInstantiation.h
Normal file
37
engine/include/XCEngine/Scene/ModelSceneInstantiation.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
|
||||
namespace Components {
|
||||
class GameObject;
|
||||
class Scene;
|
||||
}
|
||||
|
||||
struct ModelSceneInstantiationResult {
|
||||
Components::GameObject* rootObject = nullptr;
|
||||
std::vector<Components::GameObject*> nodeObjects;
|
||||
std::vector<Components::GameObject*> meshObjects;
|
||||
};
|
||||
|
||||
bool InstantiateModelHierarchy(
|
||||
Components::Scene& scene,
|
||||
const Resources::Model& model,
|
||||
const Resources::AssetRef& modelAssetRef,
|
||||
Components::GameObject* parent = nullptr,
|
||||
ModelSceneInstantiationResult* outResult = nullptr,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
bool InstantiateModelHierarchy(
|
||||
Components::Scene& scene,
|
||||
const Containers::String& modelPath,
|
||||
Components::GameObject* parent = nullptr,
|
||||
ModelSceneInstantiationResult* outResult = nullptr,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
} // namespace XCEngine
|
||||
@@ -106,6 +106,31 @@ void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
|
||||
}
|
||||
}
|
||||
|
||||
void MeshFilterComponent::SetMeshAssetRef(const Resources::AssetRef& meshRef) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_asyncMeshLoadRequested = false;
|
||||
m_mesh.Reset();
|
||||
m_meshPath.clear();
|
||||
m_meshRef = meshRef;
|
||||
if (!m_meshRef.IsValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Containers::String resolvedPath;
|
||||
if (Resources::ResourceManager::Get().TryResolveAssetPath(m_meshRef, resolvedPath)) {
|
||||
m_meshPath = ToStdString(resolvedPath);
|
||||
}
|
||||
|
||||
if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_mesh = Resources::ResourceManager::Get().Load<Resources::Mesh>(m_meshRef);
|
||||
if (m_mesh.Get() != nullptr && m_meshPath.empty()) {
|
||||
m_meshPath = ToStdString(m_mesh->GetPath());
|
||||
}
|
||||
}
|
||||
|
||||
void MeshFilterComponent::SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh) {
|
||||
m_pendingMeshLoad.reset();
|
||||
m_asyncMeshLoadRequested = false;
|
||||
@@ -271,7 +296,8 @@ void MeshFilterComponent::ResolvePendingMesh() {
|
||||
}
|
||||
|
||||
m_meshPath = ToStdString(m_mesh->GetPath());
|
||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
|
||||
if (!m_meshRef.IsValid() &&
|
||||
!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
|
||||
m_meshRef.Reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,32 @@ void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& mat
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::SetMaterialAssetRef(size_t index, const Resources::AssetRef& materialRef) {
|
||||
EnsureMaterialSlot(index);
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
m_asyncMaterialLoadRequested[index] = false;
|
||||
m_materials[index].Reset();
|
||||
m_materialPaths[index].clear();
|
||||
m_materialRefs[index] = materialRef;
|
||||
if (!m_materialRefs[index].IsValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Containers::String resolvedPath;
|
||||
if (Resources::ResourceManager::Get().TryResolveAssetPath(m_materialRefs[index], resolvedPath)) {
|
||||
m_materialPaths[index] = ToStdString(resolvedPath);
|
||||
}
|
||||
|
||||
if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_materials[index] = Resources::ResourceManager::Get().Load<Resources::Material>(m_materialRefs[index]);
|
||||
if (m_materials[index].Get() != nullptr && m_materialPaths[index].empty()) {
|
||||
m_materialPaths[index] = MaterialPathFromHandle(m_materials[index]);
|
||||
}
|
||||
}
|
||||
|
||||
void MeshRendererComponent::SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material) {
|
||||
EnsureMaterialSlot(index);
|
||||
m_pendingMaterialLoads[index].reset();
|
||||
@@ -445,7 +471,8 @@ void MeshRendererComponent::ResolvePendingMaterials() {
|
||||
}
|
||||
|
||||
m_materialPaths[index] = MaterialPathFromHandle(m_materials[index]);
|
||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_materialPaths[index].c_str(),
|
||||
if (!m_materialRefs[index].IsValid() &&
|
||||
!Resources::ResourceManager::Get().TryGetAssetRef(m_materialPaths[index].c_str(),
|
||||
Resources::ResourceType::Material,
|
||||
m_materialRefs[index])) {
|
||||
m_materialRefs[index].Reset();
|
||||
|
||||
571
engine/src/Core/Asset/ArtifactContainer.cpp
Normal file
571
engine/src/Core/Asset/ArtifactContainer.cpp
Normal file
@@ -0,0 +1,571 @@
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kArtifactContainerEntryPathToken = "@entry=";
|
||||
|
||||
struct ArtifactContainerFileHeader {
|
||||
char magic[8] = { 'X', 'C', 'A', 'R', 'T', '0', '1', '\0' };
|
||||
Core::uint32 schemaVersion = kArtifactContainerSchemaVersion;
|
||||
Core::uint32 entryCount = 0;
|
||||
Core::uint64 directorySize = 0;
|
||||
Core::uint64 payloadSize = 0;
|
||||
Core::uint64 contentHashHigh = 0;
|
||||
Core::uint64 contentHashLow = 0;
|
||||
};
|
||||
|
||||
struct ArtifactContainerEntryHeader {
|
||||
Core::uint32 resourceType = 0;
|
||||
Core::uint32 compression = 0;
|
||||
Core::uint32 flags = 0;
|
||||
Core::uint32 nameLength = 0;
|
||||
Core::uint64 localID = 0;
|
||||
Core::uint64 payloadOffset = 0;
|
||||
Core::uint64 payloadSize = 0;
|
||||
};
|
||||
|
||||
class IncrementalArtifactHasher {
|
||||
public:
|
||||
void Append(const void* data, size_t size) {
|
||||
if (data == nullptr || size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hasBytes = true;
|
||||
const auto* bytes = static_cast<const Core::uint8*>(data);
|
||||
for (size_t index = 0; index < size; ++index) {
|
||||
m_high ^= static_cast<Core::uint64>(bytes[index]);
|
||||
m_high *= 1099511628211ULL;
|
||||
|
||||
m_low ^= static_cast<Core::uint64>(bytes[index]);
|
||||
m_low *= 1099511628211ULL;
|
||||
}
|
||||
}
|
||||
|
||||
AssetGUID Finish() const {
|
||||
if (!m_hasBytes) {
|
||||
return AssetGUID();
|
||||
}
|
||||
return AssetGUID(m_high, m_low);
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_hasBytes = false;
|
||||
Core::uint64 m_high = 14695981039346656037ULL;
|
||||
Core::uint64 m_low = 1099511628211ULL ^ 0x9e3779b97f4a7c15ULL;
|
||||
};
|
||||
|
||||
Containers::String MakeError(const char* message) {
|
||||
return Containers::String(message == nullptr ? "" : message);
|
||||
}
|
||||
|
||||
bool TryWriteBytes(std::ofstream& output, const void* data, size_t size) {
|
||||
output.write(static_cast<const char*>(data), static_cast<std::streamsize>(size));
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
void AppendBytes(const void* data, size_t size, std::vector<Core::uint8>& outBytes) {
|
||||
if (data == nullptr || size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* begin = static_cast<const Core::uint8*>(data);
|
||||
outBytes.insert(outBytes.end(), begin, begin + size);
|
||||
}
|
||||
|
||||
bool IsExpectedMagic(const ArtifactContainerFileHeader& header) {
|
||||
return std::memcmp(header.magic, "XCART01", 7) == 0 &&
|
||||
header.schemaVersion == kArtifactContainerSchemaVersion;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ArtifactContainerWriter::Clear() {
|
||||
m_entries.Clear();
|
||||
}
|
||||
|
||||
void ArtifactContainerWriter::AddEntry(const ArtifactContainerEntry& entry) {
|
||||
m_entries.PushBack(entry);
|
||||
}
|
||||
|
||||
void ArtifactContainerWriter::AddEntry(ArtifactContainerEntry&& entry) {
|
||||
m_entries.PushBack(std::move(entry));
|
||||
}
|
||||
|
||||
bool ArtifactContainerWriter::WriteToFile(const Containers::String& path,
|
||||
Containers::String* outErrorMessage) const {
|
||||
return WriteArtifactContainer(path, m_entries, outErrorMessage);
|
||||
}
|
||||
|
||||
bool WriteArtifactContainer(const Containers::String& path,
|
||||
const Containers::Array<ArtifactContainerEntry>& entries,
|
||||
Containers::String* outErrorMessage) {
|
||||
std::vector<Core::uint8> directoryBytes;
|
||||
Core::uint64 payloadBytes = 0;
|
||||
|
||||
for (const ArtifactContainerEntry& entry : entries) {
|
||||
if (entry.name.Length() > std::numeric_limits<Core::uint32>::max()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer entry name is too long.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const Core::uint64 entryPayloadSize = static_cast<Core::uint64>(entry.payload.Size());
|
||||
if (payloadBytes > std::numeric_limits<Core::uint64>::max() - entryPayloadSize) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer payload size overflow.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ArtifactContainerEntryHeader entryHeader = {};
|
||||
entryHeader.resourceType = static_cast<Core::uint32>(entry.resourceType);
|
||||
entryHeader.compression = static_cast<Core::uint32>(entry.compression);
|
||||
entryHeader.flags = entry.flags;
|
||||
entryHeader.nameLength = static_cast<Core::uint32>(entry.name.Length());
|
||||
entryHeader.localID = entry.localID;
|
||||
entryHeader.payloadOffset = payloadBytes;
|
||||
entryHeader.payloadSize = entryPayloadSize;
|
||||
AppendBytes(&entryHeader, sizeof(entryHeader), directoryBytes);
|
||||
AppendBytes(entry.name.CStr(), entry.name.Length(), directoryBytes);
|
||||
|
||||
payloadBytes += entryPayloadSize;
|
||||
}
|
||||
|
||||
IncrementalArtifactHasher hasher;
|
||||
if (!directoryBytes.empty()) {
|
||||
hasher.Append(directoryBytes.data(), directoryBytes.size());
|
||||
}
|
||||
for (const ArtifactContainerEntry& entry : entries) {
|
||||
if (!entry.payload.Empty()) {
|
||||
hasher.Append(entry.payload.Data(), entry.payload.Size());
|
||||
}
|
||||
}
|
||||
|
||||
ArtifactContainerFileHeader fileHeader = {};
|
||||
fileHeader.entryCount = static_cast<Core::uint32>(entries.Size());
|
||||
fileHeader.directorySize = static_cast<Core::uint64>(directoryBytes.size());
|
||||
fileHeader.payloadSize = payloadBytes;
|
||||
const AssetGUID contentHash = hasher.Finish();
|
||||
fileHeader.contentHashHigh = contentHash.high;
|
||||
fileHeader.contentHashLow = contentHash.low;
|
||||
|
||||
std::error_code ec;
|
||||
const fs::path targetPath(path.CStr());
|
||||
if (!targetPath.parent_path().empty()) {
|
||||
fs::create_directories(targetPath.parent_path(), ec);
|
||||
if (ec) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to create ArtifactContainer parent directory.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream output(targetPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to open ArtifactContainer output file.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteBytes(output, &fileHeader, sizeof(fileHeader)) ||
|
||||
(!directoryBytes.empty() &&
|
||||
!TryWriteBytes(output, directoryBytes.data(), directoryBytes.size()))) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to write ArtifactContainer header or directory.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ArtifactContainerEntry& entry : entries) {
|
||||
if (!entry.payload.Empty() &&
|
||||
!TryWriteBytes(output, entry.payload.Data(), entry.payload.Size())) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to write ArtifactContainer payload.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsArtifactContainerFile(const Containers::String& path) {
|
||||
std::ifstream input(path.CStr(), std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ArtifactContainerFileHeader fileHeader = {};
|
||||
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
|
||||
return input && IsExpectedMagic(fileHeader);
|
||||
}
|
||||
|
||||
Containers::String BuildArtifactContainerEntryPath(const Containers::String& containerPath,
|
||||
const Containers::String& entryName) {
|
||||
if (containerPath.Empty() || entryName.Empty()) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
Containers::String result = containerPath;
|
||||
result += kArtifactContainerEntryPathToken;
|
||||
result += entryName;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool TryParseArtifactContainerEntryPath(const Containers::String& path,
|
||||
Containers::String& outContainerPath,
|
||||
Containers::String& outEntryName) {
|
||||
outContainerPath.Clear();
|
||||
outEntryName.Clear();
|
||||
|
||||
const std::string text(path.CStr());
|
||||
const std::string token(kArtifactContainerEntryPathToken);
|
||||
const size_t tokenPos = text.rfind(token);
|
||||
if (tokenPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t entryNamePos = tokenPos + token.length();
|
||||
if (tokenPos == 0 || entryNamePos >= text.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outContainerPath = Containers::String(text.substr(0, tokenPos).c_str());
|
||||
outEntryName = Containers::String(text.substr(entryNamePos).c_str());
|
||||
return !outContainerPath.Empty() && !outEntryName.Empty();
|
||||
}
|
||||
|
||||
bool ArtifactContainerReader::Open(const Containers::String& path,
|
||||
Containers::String* outErrorMessage) {
|
||||
Close();
|
||||
|
||||
std::ifstream input(path.CStr(), std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to open ArtifactContainer file.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ArtifactContainerFileHeader fileHeader = {};
|
||||
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
|
||||
if (!input || !IsExpectedMagic(fileHeader)) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer header is invalid.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
input.seekg(0, std::ios::end);
|
||||
const std::streamoff fileSize = input.tellg();
|
||||
const Core::uint64 expectedFileSize =
|
||||
static_cast<Core::uint64>(sizeof(fileHeader)) +
|
||||
fileHeader.directorySize +
|
||||
fileHeader.payloadSize;
|
||||
if (fileSize < static_cast<std::streamoff>(sizeof(fileHeader)) ||
|
||||
expectedFileSize != static_cast<Core::uint64>(fileSize)) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer file size does not match header.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
input.seekg(static_cast<std::streamoff>(sizeof(fileHeader)), std::ios::beg);
|
||||
std::vector<Core::uint8> directoryBytes(static_cast<size_t>(fileHeader.directorySize));
|
||||
if (!directoryBytes.empty()) {
|
||||
input.read(reinterpret_cast<char*>(directoryBytes.data()),
|
||||
static_cast<std::streamsize>(directoryBytes.size()));
|
||||
if (!input) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to read ArtifactContainer directory.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Containers::Array<ArtifactContainerEntryView> parsedEntries;
|
||||
parsedEntries.Reserve(fileHeader.entryCount);
|
||||
|
||||
size_t cursor = 0;
|
||||
for (Core::uint32 entryIndex = 0; entryIndex < fileHeader.entryCount; ++entryIndex) {
|
||||
if (cursor + sizeof(ArtifactContainerEntryHeader) > directoryBytes.size()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer directory is truncated.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ArtifactContainerEntryHeader entryHeader = {};
|
||||
std::memcpy(&entryHeader, directoryBytes.data() + cursor, sizeof(entryHeader));
|
||||
cursor += sizeof(entryHeader);
|
||||
|
||||
if (cursor + entryHeader.nameLength > directoryBytes.size()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer entry name is truncated.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entryHeader.payloadOffset > fileHeader.payloadSize ||
|
||||
entryHeader.payloadSize > fileHeader.payloadSize - entryHeader.payloadOffset) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer entry payload range is invalid.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ArtifactContainerEntryView& view = parsedEntries.EmplaceBack();
|
||||
view.name = Containers::String(
|
||||
reinterpret_cast<const char*>(directoryBytes.data() + cursor),
|
||||
entryHeader.nameLength);
|
||||
view.resourceType = static_cast<ResourceType>(entryHeader.resourceType);
|
||||
view.localID = entryHeader.localID;
|
||||
view.flags = entryHeader.flags;
|
||||
view.compression = static_cast<ArtifactContainerCompression>(entryHeader.compression);
|
||||
view.payloadOffset = entryHeader.payloadOffset;
|
||||
view.payloadSize = entryHeader.payloadSize;
|
||||
cursor += entryHeader.nameLength;
|
||||
}
|
||||
|
||||
if (cursor != directoryBytes.size()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer directory has trailing garbage.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
IncrementalArtifactHasher hasher;
|
||||
if (!directoryBytes.empty()) {
|
||||
hasher.Append(directoryBytes.data(), directoryBytes.size());
|
||||
}
|
||||
|
||||
std::array<Core::uint8, 4096> buffer = {};
|
||||
Core::uint64 remainingPayloadBytes = fileHeader.payloadSize;
|
||||
while (remainingPayloadBytes > 0) {
|
||||
const size_t chunkSize = static_cast<size_t>(std::min<Core::uint64>(
|
||||
remainingPayloadBytes,
|
||||
static_cast<Core::uint64>(buffer.size())));
|
||||
input.read(reinterpret_cast<char*>(buffer.data()), static_cast<std::streamsize>(chunkSize));
|
||||
if (!input) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to hash ArtifactContainer payload.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
hasher.Append(buffer.data(), chunkSize);
|
||||
remainingPayloadBytes -= static_cast<Core::uint64>(chunkSize);
|
||||
}
|
||||
|
||||
const AssetGUID expectedHash(fileHeader.contentHashHigh, fileHeader.contentHashLow);
|
||||
const AssetGUID actualHash = hasher.Finish();
|
||||
if (expectedHash != actualHash) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer content hash mismatch.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
m_path = path;
|
||||
m_entries = std::move(parsedEntries);
|
||||
m_payloadStart =
|
||||
static_cast<Core::uint64>(sizeof(ArtifactContainerFileHeader)) +
|
||||
fileHeader.directorySize;
|
||||
m_payloadSize = fileHeader.payloadSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ArtifactContainerReader::Close() {
|
||||
m_path.Clear();
|
||||
m_entries.Clear();
|
||||
m_payloadStart = 0;
|
||||
m_payloadSize = 0;
|
||||
}
|
||||
|
||||
const ArtifactContainerEntryView* ArtifactContainerReader::FindEntryByName(
|
||||
const Containers::String& name) const {
|
||||
for (const ArtifactContainerEntryView& entry : m_entries) {
|
||||
if (entry.name == name) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ArtifactContainerEntryView* ArtifactContainerReader::FindEntry(
|
||||
ResourceType resourceType,
|
||||
LocalID localID) const {
|
||||
for (const ArtifactContainerEntryView& entry : m_entries) {
|
||||
if (entry.resourceType == resourceType && entry.localID == localID) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ArtifactContainerReader::ReadEntryPayload(const ArtifactContainerEntryView& entry,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage) const {
|
||||
outPayload.Clear();
|
||||
if (!IsOpen()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer is not open.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.payloadOffset > m_payloadSize ||
|
||||
entry.payloadSize > m_payloadSize - entry.payloadOffset) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer entry payload range is invalid.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.payloadSize == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.payloadSize > static_cast<Core::uint64>(std::numeric_limits<size_t>::max())) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer entry payload is too large.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream input(m_path.CStr(), std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to reopen ArtifactContainer file.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
input.seekg(static_cast<std::streamoff>(m_payloadStart + entry.payloadOffset), std::ios::beg);
|
||||
if (!input) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to seek ArtifactContainer payload.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
outPayload.ResizeUninitialized(static_cast<size_t>(entry.payloadSize));
|
||||
input.read(reinterpret_cast<char*>(outPayload.Data()),
|
||||
static_cast<std::streamsize>(entry.payloadSize));
|
||||
if (!input) {
|
||||
outPayload.Clear();
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("Failed to read ArtifactContainer payload.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ArtifactContainerReader::ReadEntryPayload(const Containers::String& name,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage) const {
|
||||
const ArtifactContainerEntryView* entry = FindEntryByName(name);
|
||||
if (entry == nullptr) {
|
||||
outPayload.Clear();
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer entry was not found.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return ReadEntryPayload(*entry, outPayload, outErrorMessage);
|
||||
}
|
||||
|
||||
bool ReadArtifactContainerEntryPayload(const Containers::String& containerPath,
|
||||
const Containers::String& entryName,
|
||||
ResourceType expectedType,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage) {
|
||||
outPayload.Clear();
|
||||
|
||||
ArtifactContainerReader reader;
|
||||
if (!reader.Open(containerPath, outErrorMessage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ArtifactContainerEntryView* entry = reader.FindEntryByName(entryName);
|
||||
if (entry == nullptr) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer entry was not found.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedType != ResourceType::Unknown && entry->resourceType != expectedType) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer entry resource type did not match.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return reader.ReadEntryPayload(*entry, outPayload, outErrorMessage);
|
||||
}
|
||||
|
||||
bool ReadArtifactContainerPayloadByPath(const Containers::String& path,
|
||||
ResourceType expectedType,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage) {
|
||||
Containers::String containerPath;
|
||||
Containers::String entryName;
|
||||
if (TryParseArtifactContainerEntryPath(path, containerPath, entryName)) {
|
||||
return ReadArtifactContainerEntryPayload(
|
||||
containerPath,
|
||||
entryName,
|
||||
expectedType,
|
||||
outPayload,
|
||||
outErrorMessage);
|
||||
}
|
||||
|
||||
return ReadArtifactContainerMainEntryPayload(path, expectedType, outPayload, outErrorMessage);
|
||||
}
|
||||
|
||||
bool ReadArtifactContainerMainEntryPayload(const Containers::String& path,
|
||||
ResourceType expectedType,
|
||||
Containers::Array<Core::uint8>& outPayload,
|
||||
Containers::String* outErrorMessage) {
|
||||
outPayload.Clear();
|
||||
|
||||
ArtifactContainerReader reader;
|
||||
if (!reader.Open(path, outErrorMessage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ArtifactContainerEntryView* entry = reader.FindEntry(expectedType, kMainAssetLocalID);
|
||||
if (entry == nullptr) {
|
||||
entry = reader.FindEntryByName("main");
|
||||
}
|
||||
if (entry == nullptr) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = MakeError("ArtifactContainer main entry was not found.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return reader.ReadEntryPayload(*entry, outPayload, outErrorMessage);
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -75,6 +75,9 @@ bool SerializeMaterialArtifactPayload(
|
||||
const AssetDatabase* assetDatabase);
|
||||
bool SerializeShaderArtifactPayload(const Shader& shader,
|
||||
Containers::Array<Core::uint8>& outPayload);
|
||||
bool SerializeMeshArtifactPayload(const Mesh& mesh,
|
||||
const std::vector<Containers::String>& materialArtifactPaths,
|
||||
Containers::Array<Core::uint8>& outPayload);
|
||||
bool WriteSingleEntryArtifactContainerFile(const fs::path& artifactPath,
|
||||
ResourceType resourceType,
|
||||
const Containers::Array<Core::uint8>& payload);
|
||||
@@ -1225,11 +1228,28 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader)
|
||||
bool WriteMeshArtifactFile(const fs::path& artifactPath,
|
||||
const Mesh& mesh,
|
||||
const std::vector<Containers::String>& materialArtifactPaths) {
|
||||
Containers::Array<Core::uint8> payload;
|
||||
if (!SerializeMeshArtifactPayload(mesh, materialArtifactPaths, payload)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!payload.Empty()) {
|
||||
output.write(reinterpret_cast<const char*>(payload.Data()), static_cast<std::streamsize>(payload.Size()));
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
bool SerializeMeshArtifactPayload(const Mesh& mesh,
|
||||
const std::vector<Containers::String>& materialArtifactPaths,
|
||||
Containers::Array<Core::uint8>& outPayload) {
|
||||
std::ostringstream output(std::ios::binary | std::ios::out);
|
||||
|
||||
MeshArtifactHeader header;
|
||||
header.vertexCount = mesh.GetVertexCount();
|
||||
header.vertexStride = mesh.GetVertexStride();
|
||||
@@ -1259,7 +1279,13 @@ bool WriteMeshArtifactFile(const fs::path& artifactPath,
|
||||
WriteString(output, NormalizeArtifactPathString(materialArtifactPath));
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
if (!output) {
|
||||
outPayload.Clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
outPayload = ToByteArray(output.str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void DestroyImportedMesh(Mesh* mesh) {
|
||||
@@ -1438,6 +1464,20 @@ bool AssetDatabase::TryResolveAssetPath(const AssetRef& assetRef, Containers::St
|
||||
}
|
||||
|
||||
auto resolveFromArtifactRecord = [&](const ArtifactRecord& artifactRecord) -> bool {
|
||||
const Containers::String absoluteMainArtifactPath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord.mainArtifactPath.CStr());
|
||||
|
||||
ArtifactContainerReader reader;
|
||||
Containers::String containerError;
|
||||
if (reader.Open(absoluteMainArtifactPath, &containerError)) {
|
||||
const ArtifactContainerEntryView* entry =
|
||||
reader.FindEntry(assetRef.resourceType, assetRef.localID);
|
||||
if (entry != nullptr) {
|
||||
outPath = BuildArtifactContainerEntryPath(absoluteMainArtifactPath, entry->name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const fs::path manifestPath =
|
||||
fs::path(m_projectRoot.CStr()) /
|
||||
artifactRecord.artifactDirectory.CStr() /
|
||||
@@ -2170,7 +2210,7 @@ Core::uint32 AssetDatabase::GetCurrentImporterVersion(const Containers::String&
|
||||
}
|
||||
|
||||
if (importerName == "ModelImporter") {
|
||||
return 10;
|
||||
return 12;
|
||||
}
|
||||
|
||||
if (importerName == "GaussianSplatImporter") {
|
||||
@@ -2557,21 +2597,24 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
std::vector<ArtifactDependencyRecord> 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.xcmodel");
|
||||
const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey);
|
||||
const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, ".xcmodel");
|
||||
const Containers::String artifactDir =
|
||||
NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path());
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec);
|
||||
ec.clear();
|
||||
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
fs::create_directories(
|
||||
(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(),
|
||||
ec);
|
||||
if (ec) {
|
||||
importedModel.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
ArtifactContainerWriter writer;
|
||||
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 < importedModel.textures.size(); ++textureIndex) {
|
||||
@@ -2580,16 +2623,23 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
continue;
|
||||
}
|
||||
|
||||
const Containers::String textureArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("texture_" + std::to_string(textureIndex) + ".xctex"));
|
||||
writeOk = WriteTextureArtifactFile(
|
||||
fs::path(m_projectRoot.CStr()) / textureArtifactPath.CStr(),
|
||||
*texture);
|
||||
const Containers::String entryName =
|
||||
Containers::String(("texture_" + std::to_string(textureIndex) + ".xctex").c_str());
|
||||
Containers::Array<Core::uint8> payload;
|
||||
writeOk = SerializeTextureArtifactPayload(*texture, payload);
|
||||
if (!writeOk) {
|
||||
break;
|
||||
}
|
||||
|
||||
textureArtifactPaths.emplace(texture, textureArtifactPath);
|
||||
ArtifactContainerEntry entry;
|
||||
entry.name = entryName;
|
||||
entry.resourceType = ResourceType::Texture;
|
||||
entry.payload = std::move(payload);
|
||||
writer.AddEntry(std::move(entry));
|
||||
|
||||
textureArtifactPaths.emplace(
|
||||
texture,
|
||||
BuildArtifactContainerEntryPath(mainArtifactPath, entryName));
|
||||
|
||||
if (!texture->GetPath().Empty()) {
|
||||
AssetRef textureAssetRef;
|
||||
@@ -2606,11 +2656,12 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
continue;
|
||||
}
|
||||
|
||||
const Containers::String materialArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("material_" + std::to_string(materialIndex) + ".xcmat"));
|
||||
writeOk = WriteMaterialArtifactFile(
|
||||
fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(),
|
||||
const Containers::String entryName =
|
||||
Containers::String(("material_" + std::to_string(materialIndex) + ".xcmat").c_str());
|
||||
Containers::Array<Core::uint8> payload;
|
||||
writeOk = SerializeMaterialArtifactPayload(
|
||||
*materialEntry.material,
|
||||
payload,
|
||||
textureArtifactPaths,
|
||||
textureAssetRefs,
|
||||
this);
|
||||
@@ -2618,9 +2669,16 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
break;
|
||||
}
|
||||
|
||||
materialArtifactPathsByLocalID.emplace(materialEntry.localID, materialArtifactPath);
|
||||
subAssetManifestEntries.push_back(
|
||||
ModelSubAssetManifestEntry{ materialEntry.localID, ResourceType::Material, materialArtifactPath });
|
||||
ArtifactContainerEntry entry;
|
||||
entry.name = entryName;
|
||||
entry.resourceType = ResourceType::Material;
|
||||
entry.localID = materialEntry.localID;
|
||||
entry.payload = std::move(payload);
|
||||
writer.AddEntry(std::move(entry));
|
||||
|
||||
materialArtifactPathsByLocalID.emplace(
|
||||
materialEntry.localID,
|
||||
BuildArtifactContainerEntryPath(mainArtifactPath, entryName));
|
||||
}
|
||||
|
||||
for (size_t meshIndex = 0; writeOk && meshIndex < importedModel.meshes.size(); ++meshIndex) {
|
||||
@@ -2639,30 +2697,37 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
: 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 });
|
||||
const Containers::String entryName =
|
||||
Containers::String(("mesh_" + std::to_string(meshIndex) + ".xcmesh").c_str());
|
||||
Containers::Array<Core::uint8> payload;
|
||||
writeOk = SerializeMeshArtifactPayload(*meshEntry.mesh, meshMaterialArtifactPaths, payload);
|
||||
if (!writeOk) {
|
||||
break;
|
||||
}
|
||||
|
||||
ArtifactContainerEntry entry;
|
||||
entry.name = entryName;
|
||||
entry.resourceType = ResourceType::Mesh;
|
||||
entry.localID = meshEntry.localID;
|
||||
entry.payload = std::move(payload);
|
||||
writer.AddEntry(std::move(entry));
|
||||
}
|
||||
|
||||
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);
|
||||
Containers::Array<Core::uint8> modelPayload;
|
||||
writeOk = SerializeModelArtifactPayload(*importedModel.model, modelPayload, &modelWriteErrorMessage);
|
||||
if (writeOk) {
|
||||
ArtifactContainerEntry mainEntry;
|
||||
mainEntry.name = "main";
|
||||
mainEntry.resourceType = ResourceType::Model;
|
||||
mainEntry.localID = kMainAssetLocalID;
|
||||
mainEntry.payload = std::move(modelPayload);
|
||||
writer.AddEntry(std::move(mainEntry));
|
||||
writeOk = writer.WriteToFile(
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()),
|
||||
&modelWriteErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
importedModel.Reset();
|
||||
@@ -2940,14 +3005,19 @@ bool AssetDatabase::ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord,
|
||||
}
|
||||
|
||||
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
|
||||
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
|
||||
const Containers::String mainArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / artifactFileName);
|
||||
const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey);
|
||||
const Containers::String extension =
|
||||
Containers::String(fs::path(artifactFileName == nullptr ? "" : artifactFileName).extension().string().c_str());
|
||||
const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, extension.CStr());
|
||||
const Containers::String artifactDir =
|
||||
NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path());
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec);
|
||||
ec.clear();
|
||||
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
fs::create_directories(
|
||||
(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(),
|
||||
ec);
|
||||
if (ec) {
|
||||
SetLastErrorMessage(
|
||||
Containers::String("Failed to create UI artifact directory: ") + artifactDir);
|
||||
|
||||
@@ -106,6 +106,38 @@ RHI::InputLayoutDesc BuiltinForwardPipeline::BuildInputLayout() {
|
||||
texcoord.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, uv0));
|
||||
inputLayout.elements.push_back(texcoord);
|
||||
|
||||
RHI::InputElementDesc backTexcoord = {};
|
||||
backTexcoord.semanticName = "TEXCOORD";
|
||||
backTexcoord.semanticIndex = 1;
|
||||
backTexcoord.format = static_cast<uint32_t>(RHI::Format::R32G32_Float);
|
||||
backTexcoord.inputSlot = 0;
|
||||
backTexcoord.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, uv1));
|
||||
inputLayout.elements.push_back(backTexcoord);
|
||||
|
||||
RHI::InputElementDesc tangent = {};
|
||||
tangent.semanticName = "TEXCOORD";
|
||||
tangent.semanticIndex = 2;
|
||||
tangent.format = static_cast<uint32_t>(RHI::Format::R32G32B32_Float);
|
||||
tangent.inputSlot = 0;
|
||||
tangent.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, tangent));
|
||||
inputLayout.elements.push_back(tangent);
|
||||
|
||||
RHI::InputElementDesc bitangent = {};
|
||||
bitangent.semanticName = "TEXCOORD";
|
||||
bitangent.semanticIndex = 3;
|
||||
bitangent.format = static_cast<uint32_t>(RHI::Format::R32G32B32_Float);
|
||||
bitangent.inputSlot = 0;
|
||||
bitangent.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, bitangent));
|
||||
inputLayout.elements.push_back(bitangent);
|
||||
|
||||
RHI::InputElementDesc color = {};
|
||||
color.semanticName = "COLOR";
|
||||
color.semanticIndex = 0;
|
||||
color.format = static_cast<uint32_t>(RHI::Format::R32G32B32A32_Float);
|
||||
color.inputSlot = 0;
|
||||
color.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, color));
|
||||
inputLayout.elements.push_back(color);
|
||||
|
||||
return inputLayout;
|
||||
}
|
||||
|
||||
@@ -132,17 +164,26 @@ bool BuiltinForwardPipeline::Render(
|
||||
const RenderSurface& surface,
|
||||
const RenderSceneData& sceneData) {
|
||||
if (!Initialize(context)) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::Rendering,
|
||||
"BuiltinForwardPipeline::Render failed: Initialize returned false");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_volumetricPass != nullptr &&
|
||||
!sceneData.visibleVolumes.empty() &&
|
||||
!m_volumetricPass->PrepareVolumeResources(context, sceneData)) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::Rendering,
|
||||
"BuiltinForwardPipeline::Render failed: PrepareVolumeResources returned false");
|
||||
return false;
|
||||
}
|
||||
if (m_gaussianSplatPass != nullptr &&
|
||||
!sceneData.visibleGaussianSplats.empty() &&
|
||||
!m_gaussianSplatPass->PrepareGaussianSplatResources(context, sceneData)) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::Rendering,
|
||||
"BuiltinForwardPipeline::Render failed: PrepareGaussianSplatResources returned false");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -156,6 +197,9 @@ bool BuiltinForwardPipeline::Render(
|
||||
};
|
||||
|
||||
if (!BeginForwardScenePass(passContext)) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::Rendering,
|
||||
"BuiltinForwardPipeline::Render failed: BeginForwardScenePass returned false");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -167,15 +211,35 @@ bool BuiltinForwardPipeline::Render(
|
||||
bool renderResult = ExecuteForwardOpaquePass(passContext);
|
||||
if (renderResult) {
|
||||
renderResult = ExecuteForwardSkyboxPass(passContext);
|
||||
if (!renderResult) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::Rendering,
|
||||
"BuiltinForwardPipeline::Render failed: ExecuteForwardSkyboxPass returned false");
|
||||
}
|
||||
}
|
||||
if (renderResult && m_gaussianSplatPass != nullptr) {
|
||||
renderResult = m_gaussianSplatPass->Execute(passContext);
|
||||
if (!renderResult) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::Rendering,
|
||||
"BuiltinForwardPipeline::Render failed: BuiltinGaussianSplatPass::Execute returned false");
|
||||
}
|
||||
}
|
||||
if (renderResult && m_volumetricPass != nullptr) {
|
||||
renderResult = m_volumetricPass->Execute(passContext);
|
||||
if (!renderResult) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::Rendering,
|
||||
"BuiltinForwardPipeline::Render failed: BuiltinVolumetricPass::Execute returned false");
|
||||
}
|
||||
}
|
||||
if (renderResult) {
|
||||
renderResult = ExecuteForwardTransparentPass(passContext);
|
||||
if (!renderResult) {
|
||||
Debug::Logger::Get().Error(
|
||||
Debug::LogCategory::Rendering,
|
||||
"BuiltinForwardPipeline::Render failed: ExecuteForwardTransparentPass returned false");
|
||||
}
|
||||
}
|
||||
|
||||
if (sampledDirectionalShadow) {
|
||||
|
||||
@@ -438,6 +438,37 @@ BuiltinForwardPipeline::CachedDescriptorSet* BuiltinForwardPipeline::GetOrCreate
|
||||
}
|
||||
}
|
||||
|
||||
if (setLayout.usesMaterialTextures) {
|
||||
if (material == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (cachedDescriptorSet.materialTextureViews.size() != setLayout.materialTextureBindings.size()) {
|
||||
cachedDescriptorSet.materialTextureViews.assign(
|
||||
setLayout.materialTextureBindings.size(),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
for (size_t bindingIndex = 0; bindingIndex < setLayout.materialTextureBindings.size(); ++bindingIndex) {
|
||||
const BuiltinPassResourceBindingDesc& textureBinding =
|
||||
setLayout.materialTextureBindings[bindingIndex];
|
||||
RHI::RHIResourceView* resolvedTextureView =
|
||||
ResolveMaterialTextureView(material, textureBinding);
|
||||
if (resolvedTextureView == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (cachedDescriptorSet.materialTextureViews[bindingIndex] != resolvedTextureView) {
|
||||
cachedDescriptorSet.descriptorSet.set->Update(
|
||||
textureBinding.location.binding,
|
||||
resolvedTextureView);
|
||||
cachedDescriptorSet.materialTextureViews[bindingIndex] = resolvedTextureView;
|
||||
}
|
||||
}
|
||||
} else if (!cachedDescriptorSet.materialTextureViews.empty()) {
|
||||
cachedDescriptorSet.materialTextureViews.clear();
|
||||
}
|
||||
|
||||
if (setLayout.usesLighting) {
|
||||
if (!passLayout.lighting.IsValid() || passLayout.lighting.set != setIndex) {
|
||||
return nullptr;
|
||||
@@ -556,6 +587,33 @@ RHI::RHIResourceView* BuiltinForwardPipeline::ResolveTextureView(
|
||||
return textureView != nullptr ? textureView : m_fallbackTexture2DView;
|
||||
}
|
||||
|
||||
RHI::RHIResourceView* BuiltinForwardPipeline::ResolveMaterialTextureView(
|
||||
const Resources::Material* material,
|
||||
const BuiltinPassResourceBindingDesc& binding) {
|
||||
const bool expectsCubemap = binding.resourceType == Resources::ShaderResourceType::TextureCube;
|
||||
RHI::RHIResourceView* fallbackView = expectsCubemap ? m_fallbackTextureCubeView : m_fallbackTexture2DView;
|
||||
if (material == nullptr) {
|
||||
return fallbackView;
|
||||
}
|
||||
|
||||
const Resources::ResourceHandle<Resources::Texture> textureHandle = material->GetTexture(binding.name);
|
||||
const Resources::Texture* texture = textureHandle.Get();
|
||||
if (texture == nullptr) {
|
||||
return fallbackView;
|
||||
}
|
||||
|
||||
const Resources::TextureType textureType = texture->GetTextureType();
|
||||
const bool isCubemap =
|
||||
textureType == Resources::TextureType::TextureCube ||
|
||||
textureType == Resources::TextureType::TextureCubeArray;
|
||||
if (expectsCubemap != isCubemap) {
|
||||
return fallbackView;
|
||||
}
|
||||
|
||||
RHI::RHIResourceView* textureView = ResolveTextureView(texture);
|
||||
return textureView != nullptr ? textureView : fallbackView;
|
||||
}
|
||||
|
||||
BuiltinForwardPipeline::LightingConstants BuiltinForwardPipeline::BuildLightingConstants(
|
||||
const RenderLightingData& lightingData) {
|
||||
LightingConstants lightingConstants = {};
|
||||
@@ -664,7 +722,7 @@ bool BuiltinForwardPipeline::DrawVisibleItem(
|
||||
? sceneData.lighting.mainDirectionalShadow.shadowParams
|
||||
: Math::Vector4::Zero(),
|
||||
sceneData.lighting.HasMainDirectionalShadow()
|
||||
? Math::Vector4(1.0f, 0.0f, 0.0f, 0.0f)
|
||||
? sceneData.lighting.mainDirectionalShadow.shadowOptions
|
||||
: Math::Vector4::Zero()
|
||||
};
|
||||
|
||||
@@ -730,7 +788,8 @@ bool BuiltinForwardPipeline::DrawVisibleItem(
|
||||
const Resources::Material* materialKey =
|
||||
(setLayout.usesMaterial ||
|
||||
setLayout.usesBaseColorTexture ||
|
||||
setLayout.usesMaterialBuffers)
|
||||
setLayout.usesMaterialBuffers ||
|
||||
setLayout.usesMaterialTextures)
|
||||
? material
|
||||
: nullptr;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||
@@ -229,12 +230,27 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
|
||||
}
|
||||
|
||||
std::filesystem::path dependencyFsPath(dependencyPath.CStr());
|
||||
Containers::String containerPathText;
|
||||
Containers::String entryName;
|
||||
const bool isContainerEntryPath =
|
||||
TryParseArtifactContainerEntryPath(dependencyPath, containerPathText, entryName);
|
||||
if (isContainerEntryPath) {
|
||||
dependencyFsPath = std::filesystem::path(containerPathText.CStr());
|
||||
}
|
||||
|
||||
auto rebuildResolvedPath = [&entryName, isContainerEntryPath](const std::filesystem::path& resolvedPath) {
|
||||
const Containers::String normalizedPath = NormalizePathString(resolvedPath);
|
||||
return isContainerEntryPath
|
||||
? BuildArtifactContainerEntryPath(normalizedPath, entryName)
|
||||
: normalizedPath;
|
||||
};
|
||||
|
||||
if (dependencyFsPath.is_absolute() && std::filesystem::exists(dependencyFsPath)) {
|
||||
return NormalizePathString(dependencyFsPath);
|
||||
return rebuildResolvedPath(dependencyFsPath);
|
||||
}
|
||||
|
||||
if (std::filesystem::exists(dependencyFsPath)) {
|
||||
return NormalizePathString(dependencyFsPath);
|
||||
return rebuildResolvedPath(dependencyFsPath);
|
||||
}
|
||||
|
||||
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
@@ -242,7 +258,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
|
||||
const std::filesystem::path projectRelativeCandidate =
|
||||
std::filesystem::path(resourceRoot.CStr()) / dependencyFsPath;
|
||||
if (std::filesystem::exists(projectRelativeCandidate)) {
|
||||
return NormalizePathString(projectRelativeCandidate);
|
||||
return rebuildResolvedPath(projectRelativeCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +270,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
|
||||
const std::filesystem::path ownerRelativeCandidate =
|
||||
ownerArtifactFsPath.parent_path() / dependencyFsPath;
|
||||
if (std::filesystem::exists(ownerRelativeCandidate)) {
|
||||
return NormalizePathString(ownerRelativeCandidate);
|
||||
return rebuildResolvedPath(ownerRelativeCandidate);
|
||||
}
|
||||
|
||||
std::filesystem::path current = ownerArtifactFsPath.parent_path();
|
||||
@@ -264,7 +280,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
|
||||
if (!projectRoot.empty()) {
|
||||
const std::filesystem::path projectRelativeCandidate = projectRoot / dependencyFsPath;
|
||||
if (std::filesystem::exists(projectRelativeCandidate)) {
|
||||
return NormalizePathString(projectRelativeCandidate);
|
||||
return rebuildResolvedPath(projectRelativeCandidate);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -675,42 +691,66 @@ ImportedMeshData ImportSingleMesh(const aiMesh& mesh,
|
||||
result.vertices.reserve(mesh.mNumVertices);
|
||||
result.indices.reserve(mesh.mNumFaces * 3);
|
||||
|
||||
const bool hasNormals = mesh.HasNormals();
|
||||
const bool hasTangentsAndBitangents = mesh.HasTangentsAndBitangents();
|
||||
const bool hasUv0 = mesh.HasTextureCoords(0);
|
||||
const bool hasUv1 = mesh.HasTextureCoords(1);
|
||||
const bool hasVertexColors = mesh.HasVertexColors(0);
|
||||
const bool useFallbackUv1 = hasUv0 && !hasUv1;
|
||||
|
||||
VertexAttribute attributes = VertexAttribute::Position;
|
||||
if (mesh.HasNormals()) {
|
||||
if (hasNormals) {
|
||||
attributes = attributes | VertexAttribute::Normal;
|
||||
}
|
||||
if (mesh.HasTangentsAndBitangents()) {
|
||||
if (hasTangentsAndBitangents) {
|
||||
attributes = attributes | VertexAttribute::Tangent | VertexAttribute::Bitangent;
|
||||
}
|
||||
if (mesh.HasTextureCoords(0)) {
|
||||
if (hasUv0) {
|
||||
attributes = attributes | VertexAttribute::UV0;
|
||||
}
|
||||
if (hasUv1 || useFallbackUv1) {
|
||||
attributes = attributes | VertexAttribute::UV1;
|
||||
}
|
||||
if (hasVertexColors) {
|
||||
attributes = attributes | VertexAttribute::Color;
|
||||
}
|
||||
|
||||
const Math::Matrix4 normalTransform = worldTransform.Inverse().Transpose();
|
||||
const float appliedScale = globalScale;
|
||||
|
||||
for (Core::uint32 vertexIndex = 0; vertexIndex < mesh.mNumVertices; ++vertexIndex) {
|
||||
StaticMeshVertex vertex;
|
||||
StaticMeshVertex vertex = {};
|
||||
|
||||
const aiVector3D& position = mesh.mVertices[vertexIndex];
|
||||
const Math::Vector3 transformedPosition = worldTransform.MultiplyPoint(Math::Vector3(position.x, position.y, position.z));
|
||||
vertex.position = transformedPosition * appliedScale + offset;
|
||||
|
||||
if (mesh.HasNormals()) {
|
||||
if (hasNormals) {
|
||||
const aiVector3D& normal = mesh.mNormals[vertexIndex];
|
||||
vertex.normal = normalTransform.MultiplyVector(Math::Vector3(normal.x, normal.y, normal.z)).Normalized();
|
||||
}
|
||||
|
||||
if (mesh.HasTangentsAndBitangents()) {
|
||||
if (hasTangentsAndBitangents) {
|
||||
const aiVector3D& tangent = mesh.mTangents[vertexIndex];
|
||||
const aiVector3D& bitangent = mesh.mBitangents[vertexIndex];
|
||||
vertex.tangent = normalTransform.MultiplyVector(Math::Vector3(tangent.x, tangent.y, tangent.z)).Normalized();
|
||||
vertex.bitangent = normalTransform.MultiplyVector(Math::Vector3(bitangent.x, bitangent.y, bitangent.z)).Normalized();
|
||||
}
|
||||
|
||||
if (mesh.HasTextureCoords(0)) {
|
||||
if (hasUv0) {
|
||||
const aiVector3D& uv = mesh.mTextureCoords[0][vertexIndex];
|
||||
vertex.uv0 = Math::Vector2(uv.x, uv.y);
|
||||
vertex.uv1 = vertex.uv0;
|
||||
}
|
||||
|
||||
if (hasUv1) {
|
||||
const aiVector3D& uv = mesh.mTextureCoords[1][vertexIndex];
|
||||
vertex.uv1 = Math::Vector2(uv.x, uv.y);
|
||||
}
|
||||
|
||||
if (hasVertexColors) {
|
||||
const aiColor4D& color = mesh.mColors[0][vertexIndex];
|
||||
vertex.color = Math::Vector4(color.r, color.g, color.b, color.a);
|
||||
}
|
||||
|
||||
result.vertices.push_back(vertex);
|
||||
@@ -831,14 +871,72 @@ void ApplyMaterialProperty(Material& material, const MaterialProperty& property)
|
||||
}
|
||||
|
||||
LoadResult LoadMeshArtifact(const Containers::String& path) {
|
||||
std::ifstream input(path.CStr(), std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
Containers::Array<Core::uint8> data;
|
||||
|
||||
auto tryRead = [&data](const std::filesystem::path& filePath, bool& opened) {
|
||||
opened = false;
|
||||
data.Clear();
|
||||
|
||||
Containers::String containerError;
|
||||
if (ReadArtifactContainerPayloadByPath(
|
||||
Containers::String(filePath.generic_string().c_str()),
|
||||
ResourceType::Mesh,
|
||||
data,
|
||||
&containerError)) {
|
||||
opened = true;
|
||||
return;
|
||||
}
|
||||
|
||||
std::ifstream input(filePath, std::ios::binary | std::ios::ate);
|
||||
if (!input.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
const std::streamsize size = input.tellg();
|
||||
if (size < 0) {
|
||||
data.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
input.seekg(0, std::ios::beg);
|
||||
data.Resize(static_cast<size_t>(size));
|
||||
if (size > 0 &&
|
||||
!input.read(reinterpret_cast<char*>(data.Data()), size)) {
|
||||
data.Clear();
|
||||
}
|
||||
};
|
||||
|
||||
bool opened = false;
|
||||
std::filesystem::path resolvedPath(path.CStr());
|
||||
tryRead(resolvedPath, opened);
|
||||
if (!opened && !resolvedPath.is_absolute()) {
|
||||
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
if (!resourceRoot.Empty()) {
|
||||
resolvedPath = std::filesystem::path(resourceRoot.CStr()) / resolvedPath;
|
||||
tryRead(resolvedPath, opened);
|
||||
}
|
||||
}
|
||||
|
||||
if (!opened || data.Size() < sizeof(MeshArtifactHeader)) {
|
||||
return LoadResult(Containers::String("Failed to read mesh artifact: ") + path);
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
auto readBytes = [&data, &offset](void* destination, size_t byteCount) -> bool {
|
||||
if (offset + byteCount > data.Size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (byteCount > 0) {
|
||||
std::memcpy(destination, data.Data() + offset, byteCount);
|
||||
offset += byteCount;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
MeshArtifactHeader header;
|
||||
input.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!input) {
|
||||
if (!readBytes(&header, sizeof(header))) {
|
||||
return LoadResult(Containers::String("Failed to parse mesh artifact header: ") + path);
|
||||
}
|
||||
|
||||
@@ -859,28 +957,29 @@ LoadResult LoadMeshArtifact(const Containers::String& path) {
|
||||
Containers::Array<MeshSection> sections;
|
||||
sections.Resize(header.sectionCount);
|
||||
for (Core::uint32 index = 0; index < header.sectionCount; ++index) {
|
||||
input.read(reinterpret_cast<char*>(§ions[index]), sizeof(MeshSection));
|
||||
if (!input) {
|
||||
if (!readBytes(§ions[index], sizeof(MeshSection))) {
|
||||
return LoadResult(Containers::String("Failed to read mesh sections: ") + path);
|
||||
}
|
||||
}
|
||||
|
||||
Containers::Array<Core::uint8> vertexData;
|
||||
vertexData.Resize(static_cast<size_t>(header.vertexDataSize));
|
||||
if (header.vertexDataSize > 0) {
|
||||
input.read(reinterpret_cast<char*>(vertexData.Data()), static_cast<std::streamsize>(header.vertexDataSize));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read mesh vertex data: ") + path);
|
||||
}
|
||||
if (header.vertexDataSize > static_cast<Core::uint64>(data.Size() - offset)) {
|
||||
return LoadResult(Containers::String("Failed to read mesh vertex data: ") + path);
|
||||
}
|
||||
if (header.vertexDataSize > 0 &&
|
||||
!readBytes(vertexData.Data(), static_cast<size_t>(header.vertexDataSize))) {
|
||||
return LoadResult(Containers::String("Failed to read mesh vertex data: ") + path);
|
||||
}
|
||||
|
||||
Containers::Array<Core::uint8> indexData;
|
||||
indexData.Resize(static_cast<size_t>(header.indexDataSize));
|
||||
if (header.indexDataSize > 0) {
|
||||
input.read(reinterpret_cast<char*>(indexData.Data()), static_cast<std::streamsize>(header.indexDataSize));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read mesh index data: ") + path);
|
||||
}
|
||||
if (header.indexDataSize > static_cast<Core::uint64>(data.Size() - offset)) {
|
||||
return LoadResult(Containers::String("Failed to read mesh index data: ") + path);
|
||||
}
|
||||
if (header.indexDataSize > 0 &&
|
||||
!readBytes(indexData.Data(), static_cast<size_t>(header.indexDataSize))) {
|
||||
return LoadResult(Containers::String("Failed to read mesh index data: ") + path);
|
||||
}
|
||||
|
||||
mesh->SetVertexData(vertexData.Data(),
|
||||
@@ -901,11 +1000,31 @@ LoadResult LoadMeshArtifact(const Containers::String& path) {
|
||||
bounds.SetMinMax(header.boundsMin, header.boundsMax);
|
||||
mesh->SetBounds(bounds);
|
||||
|
||||
auto readBinaryString = [&data, &offset, &readBytes](Containers::String& outValue) -> bool {
|
||||
Core::uint32 length = 0;
|
||||
if (!readBytes(&length, sizeof(length))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
outValue.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (offset + length > data.Size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outValue = Containers::String(reinterpret_cast<const char*>(data.Data() + offset), length);
|
||||
offset += length;
|
||||
return true;
|
||||
};
|
||||
|
||||
MaterialLoader materialLoader;
|
||||
|
||||
for (Core::uint32 materialIndex = 0; materialIndex < header.materialCount; ++materialIndex) {
|
||||
const Containers::String materialArtifactPath = ReadBinaryString(input);
|
||||
if (!input) {
|
||||
Containers::String materialArtifactPath;
|
||||
if (!readBinaryString(materialArtifactPath)) {
|
||||
return LoadResult(Containers::String("Failed to read mesh material artifact path: ") + path);
|
||||
}
|
||||
|
||||
|
||||
@@ -640,37 +640,61 @@ ImportedMeshData ImportSingleMesh(const aiMesh& mesh) {
|
||||
result.vertices.reserve(mesh.mNumVertices);
|
||||
result.indices.reserve(mesh.mNumFaces * 3);
|
||||
|
||||
const bool hasNormals = mesh.HasNormals();
|
||||
const bool hasTangentsAndBitangents = mesh.HasTangentsAndBitangents();
|
||||
const bool hasUv0 = mesh.HasTextureCoords(0);
|
||||
const bool hasUv1 = mesh.HasTextureCoords(1);
|
||||
const bool hasVertexColors = mesh.HasVertexColors(0);
|
||||
const bool useFallbackUv1 = hasUv0 && !hasUv1;
|
||||
|
||||
VertexAttribute attributes = VertexAttribute::Position;
|
||||
if (mesh.HasNormals()) {
|
||||
if (hasNormals) {
|
||||
attributes = attributes | VertexAttribute::Normal;
|
||||
}
|
||||
if (mesh.HasTangentsAndBitangents()) {
|
||||
if (hasTangentsAndBitangents) {
|
||||
attributes = attributes | VertexAttribute::Tangent | VertexAttribute::Bitangent;
|
||||
}
|
||||
if (mesh.HasTextureCoords(0)) {
|
||||
if (hasUv0) {
|
||||
attributes = attributes | VertexAttribute::UV0;
|
||||
}
|
||||
if (hasUv1 || useFallbackUv1) {
|
||||
attributes = attributes | VertexAttribute::UV1;
|
||||
}
|
||||
if (hasVertexColors) {
|
||||
attributes = attributes | VertexAttribute::Color;
|
||||
}
|
||||
|
||||
for (Core::uint32 vertexIndex = 0; vertexIndex < mesh.mNumVertices; ++vertexIndex) {
|
||||
StaticMeshVertex vertex;
|
||||
StaticMeshVertex vertex = {};
|
||||
const aiVector3D& position = mesh.mVertices[vertexIndex];
|
||||
vertex.position = Math::Vector3(position.x, position.y, position.z);
|
||||
|
||||
if (mesh.HasNormals()) {
|
||||
if (hasNormals) {
|
||||
const aiVector3D& normal = mesh.mNormals[vertexIndex];
|
||||
vertex.normal = Math::Vector3(normal.x, normal.y, normal.z).Normalized();
|
||||
}
|
||||
|
||||
if (mesh.HasTangentsAndBitangents()) {
|
||||
if (hasTangentsAndBitangents) {
|
||||
const aiVector3D& tangent = mesh.mTangents[vertexIndex];
|
||||
const aiVector3D& bitangent = mesh.mBitangents[vertexIndex];
|
||||
vertex.tangent = Math::Vector3(tangent.x, tangent.y, tangent.z).Normalized();
|
||||
vertex.bitangent = Math::Vector3(bitangent.x, bitangent.y, bitangent.z).Normalized();
|
||||
}
|
||||
|
||||
if (mesh.HasTextureCoords(0)) {
|
||||
if (hasUv0) {
|
||||
const aiVector3D& uv = mesh.mTextureCoords[0][vertexIndex];
|
||||
vertex.uv0 = Math::Vector2(uv.x, uv.y);
|
||||
vertex.uv1 = vertex.uv0;
|
||||
}
|
||||
|
||||
if (hasUv1) {
|
||||
const aiVector3D& uv = mesh.mTextureCoords[1][vertexIndex];
|
||||
vertex.uv1 = Math::Vector2(uv.x, uv.y);
|
||||
}
|
||||
|
||||
if (hasVertexColors) {
|
||||
const aiColor4D& color = mesh.mColors[0][vertexIndex];
|
||||
vertex.color = Math::Vector4(color.r, color.g, color.b, color.a);
|
||||
}
|
||||
|
||||
result.vertices.push_back(vertex);
|
||||
|
||||
228
engine/src/Scene/ModelSceneInstantiation.cpp
Normal file
228
engine/src/Scene/ModelSceneInstantiation.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
#include <XCEngine/Scene/ModelSceneInstantiation.h>
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
namespace XCEngine {
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace Components;
|
||||
using namespace Resources;
|
||||
|
||||
Containers::String MakeNodeName(const ModelNode& node, Core::uint32 nodeIndex) {
|
||||
if (!node.name.Empty()) {
|
||||
return node.name;
|
||||
}
|
||||
|
||||
return Containers::String(("ModelNode_" + std::to_string(nodeIndex)).c_str());
|
||||
}
|
||||
|
||||
Containers::String MakeMeshObjectName(const Containers::String& nodeName, Core::uint32 meshBindingOffset) {
|
||||
return Containers::String(
|
||||
(std::string(nodeName.CStr()) + "_Mesh" + std::to_string(meshBindingOffset)).c_str());
|
||||
}
|
||||
|
||||
AssetRef MakeSubAssetRef(const AssetRef& modelAssetRef, LocalID localID, ResourceType resourceType) {
|
||||
AssetRef ref;
|
||||
ref.assetGuid = modelAssetRef.assetGuid;
|
||||
ref.localID = localID;
|
||||
ref.resourceType = resourceType;
|
||||
return ref;
|
||||
}
|
||||
|
||||
void SetErrorMessage(Containers::String* outErrorMessage, const Containers::String& message) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
bool AttachMeshBinding(
|
||||
GameObject& targetObject,
|
||||
const Model& model,
|
||||
const AssetRef& modelAssetRef,
|
||||
const ModelMeshBinding& meshBinding,
|
||||
Containers::String* outErrorMessage,
|
||||
std::vector<GameObject*>* outMeshObjects) {
|
||||
if (!modelAssetRef.IsValid()) {
|
||||
SetErrorMessage(outErrorMessage, "Model asset ref is required when instantiating mesh bindings.");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* meshFilter = targetObject.GetComponent<MeshFilterComponent>();
|
||||
if (meshFilter == nullptr) {
|
||||
meshFilter = targetObject.AddComponent<MeshFilterComponent>();
|
||||
}
|
||||
|
||||
auto* meshRenderer = targetObject.GetComponent<MeshRendererComponent>();
|
||||
if (meshRenderer == nullptr) {
|
||||
meshRenderer = targetObject.AddComponent<MeshRendererComponent>();
|
||||
}
|
||||
|
||||
meshFilter->SetMeshAssetRef(MakeSubAssetRef(modelAssetRef, meshBinding.meshLocalID, ResourceType::Mesh));
|
||||
|
||||
const auto& materialBindings = model.GetMaterialBindings();
|
||||
const Core::uint32 materialBindingEnd = meshBinding.materialBindingStart + meshBinding.materialBindingCount;
|
||||
if (materialBindingEnd > materialBindings.Size()) {
|
||||
SetErrorMessage(outErrorMessage, "Model mesh binding references material bindings outside the model range.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Core::uint32 materialBindingIndex = meshBinding.materialBindingStart;
|
||||
materialBindingIndex < materialBindingEnd;
|
||||
++materialBindingIndex) {
|
||||
const ModelMaterialBinding& materialBinding = materialBindings[materialBindingIndex];
|
||||
meshRenderer->SetMaterialAssetRef(
|
||||
static_cast<size_t>(materialBinding.slotIndex),
|
||||
MakeSubAssetRef(modelAssetRef, materialBinding.materialLocalID, ResourceType::Material));
|
||||
}
|
||||
|
||||
if (outMeshObjects != nullptr) {
|
||||
outMeshObjects->push_back(&targetObject);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool InstantiateModelHierarchy(
|
||||
Components::Scene& scene,
|
||||
const Resources::Model& model,
|
||||
const Resources::AssetRef& modelAssetRef,
|
||||
Components::GameObject* parent,
|
||||
ModelSceneInstantiationResult* outResult,
|
||||
Containers::String* outErrorMessage) {
|
||||
if (!model.IsValid()) {
|
||||
SetErrorMessage(outErrorMessage, "Model is invalid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!model.HasRootNode()) {
|
||||
SetErrorMessage(outErrorMessage, "Model does not have a root node.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& nodes = model.GetNodes();
|
||||
const auto& meshBindings = model.GetMeshBindings();
|
||||
if (model.GetRootNodeIndex() >= nodes.Size()) {
|
||||
SetErrorMessage(outErrorMessage, "Model root node index is outside the node range.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ModelSceneInstantiationResult localResult;
|
||||
localResult.nodeObjects.resize(nodes.Size(), nullptr);
|
||||
for (Core::uint32 nodeIndex = 0; nodeIndex < nodes.Size(); ++nodeIndex) {
|
||||
const ModelNode& node = nodes[nodeIndex];
|
||||
localResult.nodeObjects[nodeIndex] = scene.CreateGameObject(MakeNodeName(node, nodeIndex).CStr(), nullptr);
|
||||
if (localResult.nodeObjects[nodeIndex] == nullptr) {
|
||||
SetErrorMessage(outErrorMessage, "Failed to create a model node game object.");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* transform = localResult.nodeObjects[nodeIndex]->GetTransform();
|
||||
transform->SetLocalPosition(node.localPosition);
|
||||
transform->SetLocalRotation(node.localRotation);
|
||||
transform->SetLocalScale(node.localScale);
|
||||
}
|
||||
|
||||
for (Core::uint32 nodeIndex = 0; nodeIndex < nodes.Size(); ++nodeIndex) {
|
||||
const ModelNode& node = nodes[nodeIndex];
|
||||
GameObject* nodeObject = localResult.nodeObjects[nodeIndex];
|
||||
if (nodeObject == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.parentIndex >= 0) {
|
||||
const Core::uint32 parentIndex = static_cast<Core::uint32>(node.parentIndex);
|
||||
if (parentIndex >= localResult.nodeObjects.size() || localResult.nodeObjects[parentIndex] == nullptr) {
|
||||
SetErrorMessage(outErrorMessage, "Model node references an invalid parent index.");
|
||||
return false;
|
||||
}
|
||||
|
||||
nodeObject->SetParent(localResult.nodeObjects[parentIndex], false);
|
||||
} else if (parent != nullptr) {
|
||||
nodeObject->SetParent(parent, false);
|
||||
}
|
||||
}
|
||||
|
||||
for (Core::uint32 nodeIndex = 0; nodeIndex < nodes.Size(); ++nodeIndex) {
|
||||
const ModelNode& node = nodes[nodeIndex];
|
||||
GameObject* nodeObject = localResult.nodeObjects[nodeIndex];
|
||||
if (nodeObject == nullptr || node.meshBindingCount == 0u) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Core::uint32 meshBindingEnd = node.meshBindingStart + node.meshBindingCount;
|
||||
if (meshBindingEnd > meshBindings.Size()) {
|
||||
SetErrorMessage(outErrorMessage, "Model node references mesh bindings outside the model range.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.meshBindingCount == 1u) {
|
||||
if (!AttachMeshBinding(
|
||||
*nodeObject,
|
||||
model,
|
||||
modelAssetRef,
|
||||
meshBindings[node.meshBindingStart],
|
||||
outErrorMessage,
|
||||
&localResult.meshObjects)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const Containers::String nodeName = MakeNodeName(node, nodeIndex);
|
||||
for (Core::uint32 meshBindingIndex = node.meshBindingStart; meshBindingIndex < meshBindingEnd; ++meshBindingIndex) {
|
||||
GameObject* meshObject = scene.CreateGameObject(
|
||||
MakeMeshObjectName(nodeName, meshBindingIndex - node.meshBindingStart).CStr(),
|
||||
nodeObject);
|
||||
if (meshObject == nullptr) {
|
||||
SetErrorMessage(outErrorMessage, "Failed to create an auxiliary mesh binding game object.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AttachMeshBinding(
|
||||
*meshObject,
|
||||
model,
|
||||
modelAssetRef,
|
||||
meshBindings[meshBindingIndex],
|
||||
outErrorMessage,
|
||||
&localResult.meshObjects)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
localResult.rootObject = localResult.nodeObjects[model.GetRootNodeIndex()];
|
||||
if (outResult != nullptr) {
|
||||
*outResult = std::move(localResult);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InstantiateModelHierarchy(
|
||||
Components::Scene& scene,
|
||||
const Containers::String& modelPath,
|
||||
Components::GameObject* parent,
|
||||
ModelSceneInstantiationResult* outResult,
|
||||
Containers::String* outErrorMessage) {
|
||||
ResourceManager& resourceManager = ResourceManager::Get();
|
||||
const ResourceHandle<Resources::Model> modelHandle = resourceManager.Load<Resources::Model>(modelPath);
|
||||
if (!modelHandle.IsValid()) {
|
||||
SetErrorMessage(outErrorMessage, Containers::String("Failed to load model asset: ") + modelPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
AssetRef modelAssetRef;
|
||||
if (!resourceManager.TryGetAssetRef(modelPath, ResourceType::Model, modelAssetRef)) {
|
||||
SetErrorMessage(outErrorMessage, Containers::String("Failed to resolve model asset ref: ") + modelPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return InstantiateModelHierarchy(scene, *modelHandle, modelAssetRef, parent, outResult, outErrorMessage);
|
||||
}
|
||||
|
||||
} // namespace XCEngine
|
||||
Reference in New Issue
Block a user