From 662940793eb89f01ed5042db1df43836c41b1a7a Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Tue, 11 Aug 2026 01:52:14 +0200 Subject: [PATCH] feat: fix scene loading that it loads from disk if not already loaded --- destrum/CMakeLists.txt | 2 + destrum/include/destrum/Assets/AssetManager.h | 33 ++ .../include/destrum/Assets/AssetReference.h | 35 ++ destrum/include/destrum/Components/Animator.h | 14 + .../destrum/Graphics/Caches/MaterialCache.h | 6 + destrum/include/destrum/Graphics/Material.h | 16 + .../destrum/Graphics/RenderResources.h | 7 +- .../include/destrum/Graphics/Resources/Mesh.h | 4 +- .../destrum/Graphics/SkeletalAnimation.h | 5 +- destrum/include/destrum/Graphics/Skeleton.h | 5 +- destrum/include/destrum/Util/ModelDoc.h | 28 +- destrum/src/Assets/AssetManager.cpp | 76 +++ destrum/src/Components/Animator.cpp | 394 ++++++++------ .../src/Components/MeshRendererComponent.cpp | 41 +- destrum/src/Graphics/Caches/MaterialCache.cpp | 63 ++- destrum/src/Graphics/MeshCache.cpp | 11 +- destrum/src/Graphics/RenderResources.cpp | 79 +++ lightkeeper/include/Lightkeeper.h | 2 +- lightkeeper/src/Lightkeeper.cpp | 486 ++++++++---------- tests/destrum_tests.cpp | 91 ++++ 20 files changed, 964 insertions(+), 434 deletions(-) create mode 100644 destrum/include/destrum/Assets/AssetManager.h create mode 100644 destrum/include/destrum/Assets/AssetReference.h create mode 100644 destrum/src/Assets/AssetManager.cpp diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index ad13d47..906ec16 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -58,6 +58,8 @@ set(SRC_FILES "src/FS/AssetFS.cpp" "src/FS/Manifest.cpp" + "src/Assets/AssetManager.cpp" + "src/Serialization/SceneSerializer.cpp" "src/Serialization/ComponentRegistry.cpp" diff --git a/destrum/include/destrum/Assets/AssetManager.h b/destrum/include/destrum/Assets/AssetManager.h new file mode 100644 index 0000000..8e4c3da --- /dev/null +++ b/destrum/include/destrum/Assets/AssetManager.h @@ -0,0 +1,33 @@ +#ifndef DESTRUM_ASSETMANAGER_H +#define DESTRUM_ASSETMANAGER_H + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +class AssetManager final : public Singleton { +public: + friend class Singleton; + + [[nodiscard]] std::filesystem::path Resolve(std::string_view reference) const; + + [[nodiscard]] ModelDoc::ModelAsset LoadModel( + std::string_view reference, + const ModelDoc::LoadOptions& options = {}) const; + + [[nodiscard]] std::vector LoadAnimationClips( + std::string_view reference, + const Skeleton& targetSkeleton) const; + +private: + AssetManager() = default; +}; + +#endif // DESTRUM_ASSETMANAGER_H diff --git a/destrum/include/destrum/Assets/AssetReference.h b/destrum/include/destrum/Assets/AssetReference.h new file mode 100644 index 0000000..0a0ca81 --- /dev/null +++ b/destrum/include/destrum/Assets/AssetReference.h @@ -0,0 +1,35 @@ +#ifndef DESTRUM_ASSETREFERENCE_H +#define DESTRUM_ASSETREFERENCE_H + +#include +#include +#include + +struct AssetReference { + std::string path; + std::string subresource; + + [[nodiscard]] bool empty() const { return path.empty(); } + + [[nodiscard]] std::string cacheKey() const { + return path.empty() ? std::string{} : path + "#" + subresource; + } + + [[nodiscard]] static std::optional fromCacheKey( + std::string_view key) + { + const auto separator = key.rfind('#'); + if (separator == std::string_view::npos || + separator == 0 || + separator + 1 >= key.size()) { + return std::nullopt; + } + + return AssetReference{ + .path = std::string{key.substr(0, separator)}, + .subresource = std::string{key.substr(separator + 1)}, + }; + } +}; + +#endif // DESTRUM_ASSETREFERENCE_H diff --git a/destrum/include/destrum/Components/Animator.h b/destrum/include/destrum/Components/Animator.h index 77273d4..59f4ec7 100644 --- a/destrum/include/destrum/Components/Animator.h +++ b/destrum/include/destrum/Components/Animator.h @@ -28,6 +28,7 @@ public: void Deserialize(const nlohmann::json& data) override; void ImGuiInspector() override; + // The source reference is carried by the AssetManager-loaded clip. void addClip(std::shared_ptr clip); void play(const std::string& name, float blendTime = 0.f); void stop(); @@ -41,10 +42,15 @@ public: return &m_skeleton; } + // A source reference is required when this Animator is saved to a scene. void setSkeleton(Skeleton skeleton) { + m_skeletonAsset = skeleton.assetReference; m_skeleton = std::move(skeleton); + m_referencesResolved = true; } + void ResolveReferences(const ObjectMap& objects) override; + private: struct PlaybackState { SkeletalAnimation* clip = nullptr; @@ -59,14 +65,22 @@ private: float m_blendT = 0.f; float m_blendDuration = 0.f; std::string m_currentClipName; + std::string m_previousClipName; std::unordered_map> m_clips; + std::vector m_clipOrder; + std::unordered_map m_clipAssets; + AssetReference m_skeletonAsset; + bool m_referencesResolved{true}; std::vector computeJointMatrices(const Skeleton& skeleton); glm::vec3 sampleTranslation(const SkeletalAnimation::Track& track, float t); glm::quat sampleRotation (const SkeletalAnimation::Track& track, float t); glm::vec3 sampleScale (const SkeletalAnimation::Track& track, float t); + + void RestorePlaybackState(const nlohmann::json& data); + void LoadAssetReferences(); }; #endif // ANIMATOR_H diff --git a/destrum/include/destrum/Graphics/Caches/MaterialCache.h b/destrum/include/destrum/Graphics/Caches/MaterialCache.h index 23d1d1f..7ec751d 100644 --- a/destrum/include/destrum/Graphics/Caches/MaterialCache.h +++ b/destrum/include/destrum/Graphics/Caches/MaterialCache.h @@ -28,10 +28,16 @@ public: void cleanup(GfxDevice& gfxDevice); MaterialID addMaterial(Material material); + MaterialID addSimpleColorMaterial( + glm::vec3 color, + std::string name = {}); MaterialID addSimpleTextureMaterial(ImageID textureID); [[nodiscard]] const Material& getMaterial(MaterialID id) const; [[nodiscard]] const std::string& getMaterialKey(MaterialID id) const; [[nodiscard]] std::optional findMaterialByKey(std::string_view key) const; + [[nodiscard]] std::optional findMaterialByName( + std::string_view name, + std::string_view sourceReference = {}) const; [[nodiscard]] MaterialID getFreeMaterialId() const; [[nodiscard]] MaterialID getPlaceholderMaterialId() const; diff --git a/destrum/include/destrum/Graphics/Material.h b/destrum/include/destrum/Graphics/Material.h index 890e400..96f6e83 100644 --- a/destrum/include/destrum/Graphics/Material.h +++ b/destrum/include/destrum/Graphics/Material.h @@ -2,7 +2,9 @@ #define MATERIAL_H #include +#include #include +#include struct alignas(16) MaterialData { alignas(16) glm::vec4 baseColor; @@ -22,6 +24,19 @@ enum class TextureFilteringMode : std::uint32_t { }; struct Material { + [[nodiscard]] static Material SimpleColor( + glm::vec3 color, + std::string materialName = {}) + { + Material material{}; + material.baseColor = color; + material.metallicFactor = 0.0f; + material.roughnessFactor = 1.0f; + material.emissiveFactor = 0.0f; + material.name = std::move(materialName); + return material; + } + glm::vec3 baseColor{1.f, 1.f, 1.f}; float metallicFactor{0.f}; float roughnessFactor{0.7f}; @@ -35,6 +50,7 @@ struct Material { // ImageId emissiveTexture{NULL_IMAGE_ID}; std::string name; + AssetReference assetReference; }; #endif //MATERIAL_H diff --git a/destrum/include/destrum/Graphics/RenderResources.h b/destrum/include/destrum/Graphics/RenderResources.h index b1a8b56..dccdca4 100644 --- a/destrum/include/destrum/Graphics/RenderResources.h +++ b/destrum/include/destrum/Graphics/RenderResources.h @@ -9,6 +9,7 @@ #include #include +#include class GfxDevice; @@ -44,6 +45,10 @@ public: ImageID addImageToCache(GPUImage image); + // Imports a file-backed model and uploads its mesh/material resources. + // References are keyed as "source#subresource" in the resource caches. + void loadModelAsset(GfxDevice& gfxDevice, std::string_view sourceReference); + [[nodiscard]] const GPUImage& getImage(ImageID id) const; [[nodiscard]] ImageID getWhiteTextureID() const { return whiteImageId; } @@ -72,4 +77,4 @@ private: ImageID defaultNormalImageId{NULL_IMAGE_ID}; }; -#endif \ No newline at end of file +#endif diff --git a/destrum/include/destrum/Graphics/Resources/Mesh.h b/destrum/include/destrum/Graphics/Resources/Mesh.h index 71b06cd..96d96aa 100644 --- a/destrum/include/destrum/Graphics/Resources/Mesh.h +++ b/destrum/include/destrum/Graphics/Resources/Mesh.h @@ -15,6 +15,7 @@ #include #include #include +#include // ─── CPU Mesh ───────────────────────────────────────────────────────────────── struct CPUMesh { @@ -36,6 +37,7 @@ struct CPUMesh { std::vector skinningData; // empty if no skeleton std::string name; + AssetReference assetReference; glm::vec3 minPos; glm::vec3 maxPos; @@ -115,4 +117,4 @@ inline std::vector indices = { } // namespace CubeMesh -#endif // MESH_H \ No newline at end of file +#endif // MESH_H diff --git a/destrum/include/destrum/Graphics/SkeletalAnimation.h b/destrum/include/destrum/Graphics/SkeletalAnimation.h index 67feed7..d0d2c27 100644 --- a/destrum/include/destrum/Graphics/SkeletalAnimation.h +++ b/destrum/include/destrum/Graphics/SkeletalAnimation.h @@ -7,8 +7,11 @@ #include #include +#include struct SkeletalAnimation { + AssetReference assetReference; + struct Keyframe { float time; glm::vec3 translation; @@ -32,4 +35,4 @@ struct SkeletalAnimation { const std::vector& getEventsForFrame(int frame) const; }; -#endif // SKELETALANIMATION_H \ No newline at end of file +#endif // SKELETALANIMATION_H diff --git a/destrum/include/destrum/Graphics/Skeleton.h b/destrum/include/destrum/Graphics/Skeleton.h index d01c352..7fc66c7 100644 --- a/destrum/include/destrum/Graphics/Skeleton.h +++ b/destrum/include/destrum/Graphics/Skeleton.h @@ -10,6 +10,7 @@ #include #include +#include struct Joint { JointId id{NULL_JOINT_ID}; @@ -19,6 +20,8 @@ struct Joint { }; struct Skeleton { + AssetReference assetReference; + struct JointNode { JointId id{NULL_JOINT_ID}; std::vector children; @@ -55,4 +58,4 @@ inline void buildParentIndex(Skeleton& skeleton) { } } -#endif // SKELETON_H \ No newline at end of file +#endif // SKELETON_H diff --git a/destrum/include/destrum/Util/ModelDoc.h b/destrum/include/destrum/Util/ModelDoc.h index ac674ec..703b172 100644 --- a/destrum/include/destrum/Util/ModelDoc.h +++ b/destrum/include/destrum/Util/ModelDoc.h @@ -31,6 +31,7 @@ #include #include +#include #include #include @@ -91,6 +92,7 @@ struct TextureInfo { struct MaterialInfo { std::string name; + AssetReference assetReference; glm::vec4 baseColor{1.0f}; glm::vec3 emissiveColor{0.0f}; @@ -1018,6 +1020,24 @@ static ModelAsset LoadModel(const std::string& path, const LoadOptions& options CollectMergedPerNodeMeshes(scene, model.skeleton, options, model.primitives); } + model.skeleton.assetReference.path = model.sourcePath; + for (std::size_t index = 0; index < model.primitives.size(); ++index) { + auto& primitive = model.primitives[index]; + primitive.mesh.assetReference.path = model.sourcePath; + primitive.mesh.assetReference.subresource = primitive.sourceMeshIndex != InvalidIndex + ? "mesh:" + std::to_string(primitive.sourceMeshIndex) + : "primitive:" + std::to_string(index); + } + for (std::size_t index = 0; index < model.materials.size(); ++index) { + model.materials[index].assetReference.path = model.sourcePath; + model.materials[index].assetReference.subresource = "material:" + std::to_string(index); + } + for (std::size_t index = 0; index < model.animations.size(); ++index) { + auto& animation = model.animations[index]; + animation.assetReference.path = model.sourcePath; + animation.assetReference.subresource = "animation:" + std::to_string(index); + } + std::cout << "[ModelDoc] Loaded model: " << path << " | primitives: " << model.primitives.size() << " | materials: " << model.materials.size() @@ -1121,7 +1141,13 @@ static const aiScene* LoadAnimationScene( Assimp::Importer importer; const aiScene* scene = LoadAnimationScene(importer, path); - return LoadAnimations(scene, targetSkeleton); + auto clips = LoadAnimations(scene, targetSkeleton); + for (std::size_t index = 0; index < clips.size(); ++index) { + auto& clip = clips[index]; + clip.assetReference.path = path; + clip.assetReference.subresource = "animation:" + std::to_string(index); + } + return clips; } } // namespace ModelDoc diff --git a/destrum/src/Assets/AssetManager.cpp b/destrum/src/Assets/AssetManager.cpp new file mode 100644 index 0000000..c02d59f --- /dev/null +++ b/destrum/src/Assets/AssetManager.cpp @@ -0,0 +1,76 @@ +#include + +#include + +#include + +namespace { + std::string ToReference(std::string_view reference) { + if (reference.empty()) { + throw std::invalid_argument("Asset reference cannot be empty"); + } + return std::string{reference}; + } +} + +std::filesystem::path AssetManager::Resolve(std::string_view reference) const { + const std::string value = ToReference(reference); + if (value.find("://") != std::string::npos) { + return AssetFS::GetInstance().GetFullPath(value); + } + + const std::filesystem::path path{value}; + if (path.is_absolute()) { + return path; + } + throw std::invalid_argument( + "Non-virtual asset references must be absolute: " + value); +} + +ModelDoc::ModelAsset AssetManager::LoadModel( + std::string_view reference, + const ModelDoc::LoadOptions& options) const +{ + const std::string sourceReference = ToReference(reference); + ModelDoc::ModelAsset model = ModelDoc::LoadModel( + Resolve(sourceReference).generic_string(), + options); + + model.sourcePath = sourceReference; + model.skeleton.assetReference.path = sourceReference; + for (std::size_t index = 0; index < model.primitives.size(); ++index) { + auto& primitive = model.primitives[index]; + primitive.mesh.assetReference.path = sourceReference; + primitive.mesh.assetReference.subresource = primitive.sourceMeshIndex != ModelDoc::InvalidIndex + ? "mesh:" + std::to_string(primitive.sourceMeshIndex) + : "primitive:" + std::to_string(index); + } + for (std::size_t index = 0; index < model.materials.size(); ++index) { + auto& material = model.materials[index]; + material.assetReference.path = sourceReference; + material.assetReference.subresource = "material:" + std::to_string(index); + } + for (std::size_t index = 0; index < model.animations.size(); ++index) { + auto& animation = model.animations[index]; + animation.assetReference.path = sourceReference; + animation.assetReference.subresource = "animation:" + std::to_string(index); + } + + return model; +} + +std::vector AssetManager::LoadAnimationClips( + std::string_view reference, + const Skeleton& targetSkeleton) const +{ + const std::string sourceReference = ToReference(reference); + auto clips = ModelDoc::LoadAnimationClips( + Resolve(sourceReference).generic_string(), + targetSkeleton); + for (std::size_t index = 0; index < clips.size(); ++index) { + auto& clip = clips[index]; + clip.assetReference.path = sourceReference; + clip.assetReference.subresource = "animation:" + std::to_string(index); + } + return clips; +} diff --git a/destrum/src/Components/Animator.cpp b/destrum/src/Components/Animator.cpp index e8cc486..c8d81b3 100644 --- a/destrum/src/Components/Animator.cpp +++ b/destrum/src/Components/Animator.cpp @@ -1,56 +1,45 @@ #include +#include #include #include +#include "spdlog/spdlog.h" #include #include #include - -#include "spdlog/spdlog.h" +#include +#include +#include namespace { - nlohmann::json Vec3Json(const glm::vec3& value) { - return {value.x, value.y, value.z}; - } - - glm::vec3 ReadVec3(const nlohmann::json& value) { - return {value.at(0).get(), value.at(1).get(), value.at(2).get()}; - } - - nlohmann::json QuatJson(const glm::quat& value) { - return {value.x, value.y, value.z, value.w}; - } - - glm::quat ReadQuat(const nlohmann::json& value) { - return { - value.at(3).get(), - value.at(0).get(), - value.at(1).get(), - value.at(2).get() - }; - } - - nlohmann::json Mat4Json(const glm::mat4& value) { - nlohmann::json result = nlohmann::json::array(); - for (std::size_t column = 0; column < 4; ++column) { - for (std::size_t row = 0; row < 4; ++row) { - result.push_back(value[static_cast(column)] - [static_cast(row)]); - } + std::optional ParseAnimationIndex(const AssetReference& asset) + { + constexpr std::string_view prefix = "animation:"; + if (asset.subresource.rfind(prefix, 0) != 0 || + asset.subresource.size() == prefix.size()) { + return std::nullopt; + } + + try { + return std::stoull(asset.subresource.substr(prefix.size())); + } catch (const std::exception&) { + return std::nullopt; } - return result; } - glm::mat4 ReadMat4(const nlohmann::json& value) { - glm::mat4 result{1.0f}; - for (std::size_t column = 0; column < 4; ++column) { - for (std::size_t row = 0; row < 4; ++row) { - result[static_cast(column)] - [static_cast(row)] = - value.at(column * 4 + row).get(); + std::string FormatAvailableClips( + const std::vector& clips) + { + std::ostringstream result; + result << "["; + for (std::size_t index = 0; index < clips.size(); ++index) { + if (index != 0) { + result << ", "; } + result << '\'' << clips[index].name << '\''; } - return result; + result << "]"; + return result.str(); } } @@ -58,60 +47,40 @@ Animator::Animator(GameObject& parent) : Component(parent, "Animator") {} nlohmann::json Animator::Serialize() const { - nlohmann::json skeleton; - skeleton["hierarchy"] = nlohmann::json::array(); - for (const auto& node : m_skeleton.hierarchy) { - skeleton["hierarchy"].push_back({ - {"id", node.id}, - {"children", node.children} - }); + if (!m_skeleton.joints.empty() && m_skeletonAsset.empty()) { + throw std::runtime_error( + "Animator cannot be saved without a skeleton asset reference"); } - skeleton["inverseBindMatrices"] = nlohmann::json::array(); - for (const auto& matrix : m_skeleton.inverseBindMatrices) { - skeleton["inverseBindMatrices"].push_back(Mat4Json(matrix)); - } - skeleton["joints"] = nlohmann::json::array(); - for (const auto& joint : m_skeleton.joints) { - skeleton["joints"].push_back({ - {"id", joint.id}, - {"translation", Vec3Json(joint.localTranslation)}, - {"rotation", QuatJson(joint.localRotation)}, - {"scale", Vec3Json(joint.localScale)} - }); - } - skeleton["jointNames"] = m_skeleton.jointNames; - skeleton["parentIndex"] = m_skeleton.parentIndex; - skeleton["rootPreTransform"] = Mat4Json(m_skeleton.rootPreTransform); - nlohmann::json clips = nlohmann::json::array(); + nlohmann::json animations = nlohmann::json::array(); + const auto appendAnimation = [&animations, this](const std::string& name) { + const auto clipIt = m_clips.find(name); + if (clipIt == m_clips.end()) { + return; + } + + const auto& clip = clipIt->second; + if (clip->assetReference.empty()) { + throw std::runtime_error( + "Animator clip has no asset reference: " + name); + } + animations.push_back({ + {"clip", name}, + {"asset", { + {"path", clip->assetReference.path}, + {"subresource", clip->assetReference.subresource} + }} + }); + }; + + for (const auto& name : m_clipOrder) { + appendAnimation(name); + } for (const auto& [name, clip] : m_clips) { - nlohmann::json clipJson{ - {"name", name}, - {"duration", clip->duration}, - {"looped", clip->looped}, - {"startFrame", clip->startFrame}, - {"tracks", nlohmann::json::array()}, - {"events", nlohmann::json::object()} - }; - for (const auto& track : clip->tracks) { - nlohmann::json trackJson{ - {"jointIndex", track.jointIndex}, - {"keyframes", nlohmann::json::array()} - }; - for (const auto& keyframe : track.keyframes) { - trackJson["keyframes"].push_back({ - {"time", keyframe.time}, - {"translation", Vec3Json(keyframe.translation)}, - {"rotation", QuatJson(keyframe.rotation)}, - {"scale", Vec3Json(keyframe.scale)} - }); - } - clipJson["tracks"].push_back(std::move(trackJson)); + (void)clip; + if (std::find(m_clipOrder.begin(), m_clipOrder.end(), name) == m_clipOrder.end()) { + appendAnimation(name); } - for (const auto& [frame, events] : clip->events) { - clipJson["events"][std::to_string(frame)] = events; - } - clips.push_back(std::move(clipJson)); } const auto playback = [](const PlaybackState& state, const std::string& name) { @@ -123,10 +92,15 @@ nlohmann::json Animator::Serialize() const { }; return { - {"skeleton", std::move(skeleton)}, - {"clips", std::move(clips)}, + {"assetReferences", { + {"skeleton", { + {"path", m_skeletonAsset.path}, + {"subresource", m_skeletonAsset.subresource} + }}, + {"animations", std::move(animations)} + }}, {"current", playback(m_current, m_currentClipName)}, - {"previous", playback(m_previous, m_previous.clip ? m_previous.clip->name : std::string{})}, + {"previous", playback(m_previous, m_previousClipName)}, {"blendT", m_blendT}, {"blendDuration", m_blendDuration} }; @@ -135,94 +109,196 @@ nlohmann::json Animator::Serialize() const { void Animator::Deserialize(const nlohmann::json& data) { m_skeleton = {}; m_clips.clear(); + m_clipOrder.clear(); + m_clipAssets.clear(); + m_skeletonAsset = {}; m_current = {}; m_previous = {}; m_currentClipName.clear(); + m_previousClipName.clear(); + m_referencesResolved = true; - if (data.contains("skeleton")) { - const auto& skeleton = data.at("skeleton"); - for (const auto& node : skeleton.value("hierarchy", nlohmann::json::array())) { - m_skeleton.hierarchy.push_back({ - node.at("id").get(), - node.value("children", std::vector{}) + if (!data.contains("assetReferences") || !data.at("assetReferences").is_object()) { + throw std::runtime_error("Animator data must contain assetReferences"); + } + const auto& references = data.at("assetReferences"); + const auto& skeleton = references.at("skeleton"); + m_skeletonAsset.path = skeleton.value("path", std::string{}); + m_skeletonAsset.subresource = skeleton.value("subresource", std::string{}); + for (const auto& animation : references.value("animations", nlohmann::json::array())) { + const std::string name = animation.at("clip").get(); + const auto& asset = animation.at("asset"); + m_clipAssets.insert_or_assign( + name, + AssetReference{ + asset.at("path").get(), + asset.value("subresource", std::string{}) }); - } - for (const auto& matrix : skeleton.value("inverseBindMatrices", nlohmann::json::array())) { - m_skeleton.inverseBindMatrices.push_back(ReadMat4(matrix)); - } - for (const auto& joint : skeleton.value("joints", nlohmann::json::array())) { - m_skeleton.joints.push_back({ - joint.at("id").get(), - ReadVec3(joint.at("translation")), - ReadQuat(joint.at("rotation")), - ReadVec3(joint.at("scale")) - }); - } - m_skeleton.jointNames = skeleton.value("jointNames", std::vector{}); - m_skeleton.parentIndex = skeleton.value("parentIndex", std::vector{}); - if (skeleton.contains("rootPreTransform")) { - m_skeleton.rootPreTransform = ReadMat4(skeleton.at("rootPreTransform")); - } - if (m_skeleton.parentIndex.size() != m_skeleton.joints.size()) { - buildParentIndex(m_skeleton); + if (std::find(m_clipOrder.begin(), m_clipOrder.end(), name) == m_clipOrder.end()) { + m_clipOrder.push_back(name); } } + m_referencesResolved = m_skeletonAsset.empty() && m_clipAssets.empty(); + RestorePlaybackState(data); +} - for (const auto& clipJson : data.value("clips", nlohmann::json::array())) { - auto clip = std::make_shared(); - clip->name = clipJson.at("name").get(); - clip->duration = clipJson.value("duration", 0.0f); - clip->looped = clipJson.value("looped", true); - clip->startFrame = clipJson.value("startFrame", 0); - for (const auto& trackJson : clipJson.value("tracks", nlohmann::json::array())) { - SkeletalAnimation::Track track; - track.jointIndex = trackJson.at("jointIndex").get(); - for (const auto& keyframeJson : trackJson.value("keyframes", nlohmann::json::array())) { - track.keyframes.push_back({ - keyframeJson.at("time").get(), - ReadVec3(keyframeJson.at("translation")), - ReadQuat(keyframeJson.at("rotation")), - ReadVec3(keyframeJson.at("scale")) - }); - } - clip->tracks.push_back(std::move(track)); - } - if (clipJson.contains("events")) { - for (auto it = clipJson.at("events").begin(); it != clipJson.at("events").end(); ++it) { - clip->events[std::stoi(it.key())] = it.value().get>(); - } - } - m_clips[clip->name] = std::move(clip); - } - +void Animator::RestorePlaybackState(const nlohmann::json& data) { const auto restorePlayback = [this](const nlohmann::json& playback, PlaybackState& state, - std::string* clipName) { + std::string& clipName) { const std::string name = playback.value("clip", std::string{}); state.time = playback.value("time", 0.0f); state.speed = playback.value("speed", 1.0f); state.clip = nullptr; - if (!name.empty()) { + if (!name.empty() && m_referencesResolved) { const auto it = m_clips.find(name); if (it == m_clips.end()) { throw std::runtime_error("Animator clip not found: " + name); } state.clip = it->second.get(); } - if (clipName != nullptr) { - *clipName = name; - } + clipName = name; }; if (data.contains("current")) { - restorePlayback(data.at("current"), m_current, &m_currentClipName); + restorePlayback(data.at("current"), m_current, m_currentClipName); } if (data.contains("previous")) { - restorePlayback(data.at("previous"), m_previous, nullptr); + restorePlayback(data.at("previous"), m_previous, m_previousClipName); } m_blendT = data.value("blendT", 0.0f); m_blendDuration = data.value("blendDuration", 0.0f); } +void Animator::ResolveReferences(const ObjectMap&) { + LoadAssetReferences(); +} + +void Animator::LoadAssetReferences() { + if (m_referencesResolved) { + return; + } + + if (m_skeletonAsset.empty()) { + throw std::runtime_error("Animator asset references have no skeleton source"); + } + + ModelDoc::LoadOptions options{}; + options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; + options.loadMaterials = false; + options.loadSkeleton = true; + options.loadAnimations = false; + + auto skeletonAsset = AssetManager::GetInstance().LoadModel(m_skeletonAsset.path, options); + if (skeletonAsset.skeleton.joints.empty()) { + throw std::runtime_error("Animator skeleton source has no joints: " + m_skeletonAsset.path); + } + m_skeleton = std::move(skeletonAsset.skeleton); + m_skeleton.assetReference = m_skeletonAsset; + + std::unordered_map> requestedBySource; + for (const auto& name : m_clipOrder) { + const auto assetIt = m_clipAssets.find(name); + if (assetIt != m_clipAssets.end()) { + requestedBySource[assetIt->second.path].push_back(name); + } + } + for (const auto& [name, asset] : m_clipAssets) { + if (std::find(m_clipOrder.begin(), m_clipOrder.end(), name) == m_clipOrder.end()) { + requestedBySource[asset.path].push_back(name); + } + } + + m_clips.clear(); + for (const auto& [source, requestedNames] : requestedBySource) { + auto clips = AssetManager::GetInstance().LoadAnimationClips(source, m_skeleton); + std::unordered_map clipIndicesByName; + for (std::size_t index = 0; index < clips.size(); ++index) { + clipIndicesByName.emplace(clips[index].name, index); + } + + for (const std::string& requestedName : requestedNames) { + const auto sourceIt = m_clipAssets.find(requestedName); + if (sourceIt == m_clipAssets.end()) { + continue; + } + + std::optional clipIndex = + ParseAnimationIndex(sourceIt->second); + + if (clipIndex && *clipIndex >= clips.size()) { + clipIndex = std::nullopt; + } + + if (!clipIndex) { + const auto nameIt = clipIndicesByName.find(requestedName); + if (nameIt != clipIndicesByName.end()) { + clipIndex = nameIt->second; + } + } + + // Older scene files did not store animation subresources. If the + // source contains exactly one clip, it is unambiguous even when + // the imported name changed between loader versions. + if (!clipIndex && clips.size() == 1 && requestedNames.size() == 1) { + clipIndex = 0; + } + + if (!clipIndex && clips.size() == requestedNames.size()) { + const auto requestedIt = std::find( + requestedNames.begin(), + requestedNames.end(), + requestedName); + if (requestedIt != requestedNames.end()) { + clipIndex = static_cast( + std::distance(requestedNames.begin(), requestedIt)); + } + } + + if (!clipIndex) { + throw std::runtime_error( + "Animator clip was not found in its source: " + requestedName + + ". Available clips: " + FormatAvailableClips(clips)); + } + + auto clip = std::move(clips[*clipIndex]); + AssetReference resolvedReference = sourceIt->second; + if (resolvedReference.subresource.empty()) { + resolvedReference.subresource = clip.assetReference.subresource; + } + + // The serialized name is the stable scene-facing name. Keep it + // even when the importer supplied a different display name. + clip.name = requestedName; + clip.assetReference = std::move(resolvedReference); + m_clips[requestedName] = + std::make_shared(std::move(clip)); + } + } + + for (const auto& [name, asset] : m_clipAssets) { + (void)asset; + if (!m_clips.contains(name)) { + throw std::runtime_error("Animator clip was not found in its source: " + name); + } + } + + m_referencesResolved = true; + RestorePlaybackState({ + {"current", { + {"clip", m_currentClipName}, + {"time", m_current.time}, + {"speed", m_current.speed} + }}, + {"previous", { + {"clip", m_previousClipName}, + {"time", m_previous.time}, + {"speed", m_previous.speed} + }}, + {"blendT", m_blendT}, + {"blendDuration", m_blendDuration} + }); +} + void Animator::Update(float dt) { if (!m_current.clip) return; @@ -245,6 +321,7 @@ void Animator::Update(float dt) { if (m_blendT >= 1.f) { m_blendT = 1.f; m_previous = {}; + m_previousClipName.clear(); } } } @@ -271,7 +348,17 @@ void Animator::ImGuiInspector() { } void Animator::addClip(std::shared_ptr clip) { - m_clips[clip->name] = std::move(clip); + if (!clip) { + return; + } + const std::string name = clip->name; + if (!m_clips.contains(name)) { + m_clipOrder.push_back(name); + } + if (!clip->assetReference.empty()) { + m_clipAssets[name] = clip->assetReference; + } + m_clips[name] = std::move(clip); } void Animator::play(const std::string& name, float blendTime) { @@ -290,10 +377,12 @@ void Animator::play(const std::string& name, float blendTime) { if (m_current.clip && blendTime > 0.f) { m_previous = m_current; + m_previousClipName = m_currentClipName; m_blendT = 0.f; m_blendDuration = blendTime; } else { m_previous = {}; + m_previousClipName.clear(); m_blendT = 1.f; } @@ -305,6 +394,7 @@ void Animator::stop() { m_current = {}; m_previous = {}; m_currentClipName = {}; + m_previousClipName = {}; } std::size_t Animator::uploadJointMatrices(const RenderContext& ctx, const Skeleton& skeleton, std::size_t frameIndex) { diff --git a/destrum/src/Components/MeshRendererComponent.cpp b/destrum/src/Components/MeshRendererComponent.cpp index d1e4a97..44dd7de 100644 --- a/destrum/src/Components/MeshRendererComponent.cpp +++ b/destrum/src/Components/MeshRendererComponent.cpp @@ -1,4 +1,7 @@ #include +#include +#include +#include #include #include "destrum/Components/Animator.h" @@ -79,19 +82,53 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) { return; } + const auto loadAssetForKey = [&](const std::string& key) { + const auto asset = AssetReference::fromCacheKey(key); + if (!asset) { + return; + } + if (!GameState::GetInstance().HasGfxDevice()) { + throw std::runtime_error( + "Cannot load mesh asset without a graphics device: " + asset->path); + } + + resources->loadModelAsset(GameState::GetInstance().Gfx(), asset->path); + }; + if (!meshKey.empty()) { - const auto resolved = resources->meshes().findMeshByKey(meshKey); + auto resolved = resources->meshes().findMeshByKey(meshKey); + if (!resolved) { + loadAssetForKey(meshKey); + resolved = resources->meshes().findMeshByKey(meshKey); + } if (!resolved) { throw std::runtime_error("Mesh resource not found: " + meshKey); } meshID = *resolved; } if (!materialKey.empty()) { - const auto resolved = resources->materials().findMaterialByKey(materialKey); + auto resolved = resources->materials().findMaterialByKey(materialKey); + if (!resolved) { + loadAssetForKey(materialKey); + resolved = resources->materials().findMaterialByKey(materialKey); + } + + if (!resolved) { + const auto meshAsset = AssetReference::fromCacheKey(meshKey); + resolved = resources->materials().findMaterialByName( + materialKey, + meshAsset ? meshAsset->path : std::string_view{}); + } + + if (!resolved) { + resolved = resources->materials().findMaterialByName(materialKey); + } + if (!resolved) { throw std::runtime_error("Material resource not found: " + materialKey); } materialID = *resolved; + materialKey = resources->materials().getMaterialKey(*resolved); } if (meshID != NULL_MESH_ID && meshKey.empty()) { diff --git a/destrum/src/Graphics/Caches/MaterialCache.cpp b/destrum/src/Graphics/Caches/MaterialCache.cpp index 9ff33d4..6e52ebe 100644 --- a/destrum/src/Graphics/Caches/MaterialCache.cpp +++ b/destrum/src/Graphics/Caches/MaterialCache.cpp @@ -7,6 +7,7 @@ #include #include #include +#include void MaterialCache::init( GfxDevice& gfxDevice, @@ -26,9 +27,9 @@ void MaterialCache::init( materialDataBuffer.buffer, "material data"); - Material placeholderMaterial{}; - placeholderMaterial.name = "PLACEHOLDER_MATERIAL"; - placeholderMaterial.diffuseTexture = defaultTextures.white; + Material placeholderMaterial = Material::SimpleColor( + glm::vec3{1.0f}, + "PLACEHOLDER_MATERIAL"); placeholderMaterialId = addMaterial(placeholderMaterial); gfxDevice.getMemoryManager().flushAllocation(materialDataBuffer); @@ -104,9 +105,14 @@ MaterialID MaterialCache::addMaterial(Material material) // exact range after batching updates. const auto materialId = static_cast(materials.size()); - std::string key = material.name.empty() - ? "material:" + std::to_string(materialId) - : material.name; + std::string key; + if (!material.assetReference.empty()) { + key = material.assetReference.cacheKey(); + } else { + key = material.name.empty() + ? "material:" + std::to_string(materialId) + : material.name; + } const std::string baseKey = key; std::size_t suffix = 1; while (std::find(materialKeys.begin(), materialKeys.end(), key) != materialKeys.end()) { @@ -121,13 +127,17 @@ MaterialID MaterialCache::addMaterial(Material material) MaterialID MaterialCache::addSimpleTextureMaterial(ImageID textureID) { - Material material{}; - material.name = "simple texture material"; + Material material = Material::SimpleColor( + glm::vec3{1.0f}, + "simple texture material"); material.diffuseTexture = textureID; - material.metallicFactor = 0.0f; - material.roughnessFactor = 1.0f; - return addMaterial(material); + return addMaterial(std::move(material)); +} + +MaterialID MaterialCache::addSimpleColorMaterial(glm::vec3 color, std::string name) +{ + return addMaterial(Material::SimpleColor(std::move(color), std::move(name))); } const Material& MaterialCache::getMaterial(MaterialID id) const @@ -149,6 +159,37 @@ std::optional MaterialCache::findMaterialByKey(std::string_view key) return static_cast(std::distance(materialKeys.begin(), it)); } +std::optional MaterialCache::findMaterialByName( + std::string_view name, + std::string_view sourceReference) const +{ + std::optional fallback; + for (MaterialID id = 0; id < materials.size(); ++id) { + const auto& material = materials[id]; + if (material.name != name) { + continue; + } + + if (sourceReference.empty()) { + return id; + } + + if (!material.assetReference.empty() && + material.assetReference.path == sourceReference) { + return id; + } + + // A manually-created material may have the same name but no source + // reference. Keep it as a fallback for scenes saved before canonical + // file-backed material keys were introduced. + if (!fallback.has_value() && material.assetReference.empty()) { + fallback = id; + } + } + + return fallback; +} + MaterialID MaterialCache::getFreeMaterialId() const { return static_cast(materials.size()); diff --git a/destrum/src/Graphics/MeshCache.cpp b/destrum/src/Graphics/MeshCache.cpp index 401650b..4171a61 100644 --- a/destrum/src/Graphics/MeshCache.cpp +++ b/destrum/src/Graphics/MeshCache.cpp @@ -32,9 +32,14 @@ MeshID MeshCache::addMesh(GfxDevice& gfxDevice, const CPUMesh& cpuMesh) const auto id = meshes.size(); meshes.push_back(std::move(gpuMesh)); cpuMeshes.push_back(cpuMesh); // store a copy of the CPU mesh - std::string key = cpuMesh.name.empty() - ? "mesh:" + std::to_string(id) - : cpuMesh.name; + std::string key; + if (!cpuMesh.assetReference.empty()) { + key = cpuMesh.assetReference.cacheKey(); + } else { + key = cpuMesh.name.empty() + ? "mesh:" + std::to_string(id) + : cpuMesh.name; + } const std::string baseKey = key; std::size_t suffix = 1; while (std::find(meshKeys.begin(), meshKeys.end(), key) != meshKeys.end()) { diff --git a/destrum/src/Graphics/RenderResources.cpp b/destrum/src/Graphics/RenderResources.cpp index 561496a..5add01c 100644 --- a/destrum/src/Graphics/RenderResources.cpp +++ b/destrum/src/Graphics/RenderResources.cpp @@ -1,11 +1,35 @@ #include +#include #include #include #include "volk.h" #include "spdlog/spdlog.h" +#include +#include +#include + +namespace { + const ModelDoc::TextureInfo* FindDiffuseTexture( + const ModelDoc::MaterialInfo& material) + { + for (const auto semantic : std::array{ + ModelDoc::TextureSemantic::BaseColor, + ModelDoc::TextureSemantic::Diffuse, + }) { + for (const auto& texture : material.textures) { + if (texture.semantic == semantic) { + return &texture; + } + } + } + + return nullptr; + } +} + RenderResources::RenderResources() = default; void RenderResources::init(GfxDevice& gfxDevice) @@ -175,6 +199,61 @@ ImageID RenderResources::addImageToCache(GPUImage image) { return imageCache->addImage(std::move(image)); } +void RenderResources::loadModelAsset( + GfxDevice& gfxDevice, + std::string_view sourceReference) +{ + ModelDoc::LoadOptions options{}; + options.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; + options.loadMaterials = true; + options.loadSkeleton = true; + options.loadAnimations = false; + + const auto model = AssetManager::GetInstance().LoadModel( + sourceReference, + options); + + for (const auto& primitive : model.primitives) { + const std::string key = primitive.mesh.assetReference.cacheKey(); + if (!key.empty() && meshes().findMeshByKey(key).has_value()) { + continue; + } + + meshes().addMesh(gfxDevice, primitive.mesh); + } + + for (const auto& sourceMaterial : model.materials) { + const std::string key = sourceMaterial.assetReference.cacheKey(); + if (!key.empty() && materials().findMaterialByKey(key).has_value()) { + continue; + } + + Material material = Material::SimpleColor( + glm::vec3{sourceMaterial.baseColor}, + sourceMaterial.name); + material.assetReference = sourceMaterial.assetReference; + material.metallicFactor = sourceMaterial.metallicFactor; + material.roughnessFactor = sourceMaterial.roughnessFactor; + material.emissiveFactor = std::max({ + sourceMaterial.emissiveColor.x, + sourceMaterial.emissiveColor.y, + sourceMaterial.emissiveColor.z, + }); + + if (const auto* texture = FindDiffuseTexture(sourceMaterial); + texture != nullptr && !texture->embedded && !texture->path.empty()) { + material.diffuseTexture = loadImageFromFile( + gfxDevice, + std::filesystem::path{texture->path}, + VK_IMAGE_USAGE_SAMPLED_BIT, + false, + TextureIntent::ColorSrgb); + } + + materials().addMaterial(std::move(material)); + } +} + BindlessSetManager& RenderResources::getBindlessSetManager() { return imageCache->bindlessSetManager; } diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index c29faec..0428234 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -31,7 +31,7 @@ private: GameObject* capybara = nullptr; - char scenePath[512]{"game://scenes/debug_scene.json"}; + char scenePath[512]{"scenes/debug_scene.json"}; char newSceneName[128]{"EmptyScene"}; std::string sceneStatus; }; diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 80a1de8..a760a82 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -1,6 +1,7 @@ #include "Lightkeeper.h" #include +#include #include "glm/gtx/transform.hpp" #include "spdlog/spdlog.h" #include @@ -27,7 +28,9 @@ #include "destrum/Util/ModelDocUtils.h" namespace { - std::filesystem::path ResolveScenePath(std::string_view value) { + std::filesystem::path ResolveScenePath( + std::string_view value, + const std::filesystem::path& exeDir) { if (value.empty()) { throw std::invalid_argument("Scene path cannot be empty"); } @@ -36,7 +39,8 @@ namespace { return AssetFS::GetInstance().GetFullPath(value); } - return std::filesystem::path{value}; + const std::filesystem::path path{value}; + return path.is_absolute() ? path : exeDir / path; } } @@ -68,37 +72,6 @@ void LightKeeper::customInit() staticModelOptions.loadSkeleton = false; staticModelOptions.loadAnimations = false; - auto kittyModel = ModelDoc::LoadModel( - AssetFS::GetInstance().GetFullPath("game://kitty.glb").generic_string(), - staticModelOptions - ); - ModelDocUtils::LogModelDocSummary(kittyModel, "kitty.glb"); - - const auto& kittyPrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(kittyModel, "game://kitty.glb"); - auto testMesh = kittyPrimitive.mesh; - testMesh.name = "Test Mesh"; - - auto testMeshID = resources.meshes().addMesh(gfxDevice, testMesh); - spdlog::info("TestMesh uploaded with id: {}", testMeshID); - - const auto testTexturePath = ModelDocUtils::PickTexturePath( - kittyModel, - kittyPrimitive, - AssetFS::GetInstance().GetFullPath("game://kitty.png") - ); - - const auto testimgID = resources.loadImageFromFile(gfxDevice, testTexturePath); - spdlog::info("Test image loaded from '{}' with id: {}", testTexturePath.generic_string(), testimgID); - - auto testMaterialID = resources.materials().addMaterial({ - .baseColor = ModelDocUtils::GetImportedBaseColor( - kittyModel, kittyPrimitive), - .diffuseTexture = testimgID, - .name = ModelDocUtils::GetImportedMaterialName( - kittyModel, kittyPrimitive, "TestMaterial"), - }); - spdlog::info("Test material created with id: {}", testMaterialID); - camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f))); auto& scene = SceneManager::GetInstance().CreateScene("Main"); @@ -168,64 +141,64 @@ void LightKeeper::customInit() characterModelOptions.loadSkeleton = true; characterModelOptions.loadAnimations = true; - auto charModel = ModelDoc::LoadModel( - AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string(), - characterModelOptions - ); - ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx"); - - const auto& charPrimitive = ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow( - charModel, - "engine://cotw-capybara-male/source/capybara.fbx" - ); - - const auto charMeshID = resources.meshes().addMesh(gfxDevice, charPrimitive.mesh); - - const auto charTexturePath = ModelDocUtils::PickTexturePath( - charModel, - charPrimitive, - AssetFS::GetInstance().GetFullPath( - "engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.pngballs") - ); - - const auto charTextureID = resources.loadImageFromFile(gfxDevice, charTexturePath); - // const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg")); - const auto charMaterialID = resources.materials().addMaterial({ - .baseColor = ModelDocUtils::GetImportedBaseColor( - charModel, charPrimitive), - .diffuseTexture = charTextureID, - .name = ModelDocUtils::GetImportedMaterialName( - charModel, charPrimitive, "CharacterMaterial"), - }); - - const auto charMeshComp = CharObj->AddComponent(); - charMeshComp->SetMeshID(charMeshID); - charMeshComp->SetMaterialID(charMaterialID); - - const auto animator = CharObj->AddComponent(); - animator->setSkeleton(std::move(charModel.skeleton)); - - std::string firstAnimationName; - - for (auto& clip : charModel.animations) - { - spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); - - if (firstAnimationName.empty()) - firstAnimationName = clip.name; - - animator->addClip(std::make_shared(std::move(clip))); - } - - if (!firstAnimationName.empty()) - { - spdlog::info("Playing animation: '{}'", firstAnimationName); - animator->play(firstAnimationName); - } - - animator->play("capybara_canter_fwd_01|capybara_canter_fwd_01|run"); - - CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); + // auto charModel = ModelDoc::LoadModel( + // AssetFS::GetInstance().GetFullPath("engine://cotw-capybara-male/source/capybara.fbx").generic_string(), + // characterModelOptions + // ); + // ModelDocUtils::LogModelDocSummary(charModel, "capybara.fbx"); + // + // const auto& charPrimitive = ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow( + // charModel, + // "engine://cotw-capybara-male/source/capybara.fbx" + // ); + // + // const auto charMeshID = resources.meshes().addMesh(gfxDevice, charPrimitive.mesh); + // + // const auto charTexturePath = ModelDocUtils::PickTexturePath( + // charModel, + // charPrimitive, + // AssetFS::GetInstance().GetFullPath( + // "engine://cotw-capybara-male/textures/capybara_male_light_brown_dif.ddsc.DECA.RE.pngballs") + // ); + // + // const auto charTextureID = resources.loadImageFromFile(gfxDevice, charTexturePath); + // // const auto charTextureID = gfxDevice.loadImageFromFile(AssetFS::GetInstance().GetFullPath("engine://char.jpg")); + // const auto charMaterialID = resources.materials().addMaterial({ + // .baseColor = ModelDocUtils::GetImportedBaseColor( + // charModel, charPrimitive), + // .diffuseTexture = charTextureID, + // .name = ModelDocUtils::GetImportedMaterialName( + // charModel, charPrimitive, "CharacterMaterial"), + // }); + // + // const auto charMeshComp = CharObj->AddComponent(); + // charMeshComp->SetMeshID(charMeshID); + // charMeshComp->SetMaterialID(charMaterialID); + // + // const auto animator = CharObj->AddComponent(); + // animator->setSkeleton(std::move(charModel.skeleton)); + // + // std::string firstAnimationName; + // + // for (auto& clip : charModel.animations) + // { + // spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); + // + // if (firstAnimationName.empty()) + // firstAnimationName = clip.name; + // + // animator->addClip(std::make_shared(std::move(clip))); + // } + // + // if (!firstAnimationName.empty()) + // { + // spdlog::info("Playing animation: '{}'", firstAnimationName); + // animator->play(firstAnimationName); + // } + // + // animator->play("capybara_canter_fwd_01|capybara_canter_fwd_01|run"); + // + // CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 0.f, 0.f)); // CharObj->GetTransform().SetWorldScale(0.01f, 0.01f, 0.01f); // ModelDoc::LoadOptions options{}; @@ -282,184 +255,177 @@ void LightKeeper::customInit() // } - { - const auto CharObj = scene.CreateGameObject("Character"); - - ModelDoc::LoadOptions characterOptions{}; - characterOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; - characterOptions.loadMaterials = true; - characterOptions.loadSkeleton = true; - characterOptions.loadAnimations = false; - - auto charModel = ModelDoc::LoadModel( - AssetFS::GetInstance() - .GetFullPath("engine://characterMedium.fbx") - .generic_string(), - characterOptions - ); - - const auto& charPrimitive = - ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow( - charModel, - "engine://characterMedium.fbx" - ); - - const auto charMeshID = resources.meshes().addMesh(gfxDevice, charPrimitive.mesh); - - const auto charTextureID = resources.loadImageFromFile(gfxDevice, - AssetFS::GetInstance().GetFullPath("engine://textures/criminalMaleA.png") - ); - - const auto charMaterialID = resources.materials().addMaterial({ - .baseColor = ModelDocUtils::GetImportedBaseColor( - charModel, charPrimitive), - .diffuseTexture = charTextureID, - .name = ModelDocUtils::GetImportedMaterialName( - charModel, - charPrimitive, - "CharacterMaterial" - ), - }); - - const auto charMeshComp = CharObj->AddComponent(); - charMeshComp->SetMeshID(charMeshID); - charMeshComp->SetMaterialID(charMaterialID); - - const auto animator = CharObj->AddComponent(); - animator->setSkeleton(charModel.skeleton); - - auto runClips = ModelDoc::LoadAnimationClips( - AssetFS::GetInstance() - .GetFullPath("engine://run.fbx") - .generic_string(), - charModel.skeleton - ); - - for (auto& clip : runClips) - { - spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); - animator->addClip(std::make_shared(std::move(clip))); - } - - if (!runClips.empty()) - { - animator->play("Root|Run"); - } - - CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f)); - CharObj->GetTransform().SetWorldPosition(glm::vec3(5, 0, 0)); - } - - auto cubeModel = ModelDoc::LoadModel( - AssetFS::GetInstance() - .GetFullPath("engine://cube.fbx") - .generic_string(), - staticModelOptions - ); - ModelDocUtils::LogModelDocSummary(cubeModel, "cube.fbx"); - - const auto& cubePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(cubeModel, "game://cube.fbx"); - - - const auto eliasTextuerPath = ModelDocUtils::PickTexturePath( - cubeModel, - cubePrimitive, - AssetFS::GetInstance().GetFullPath("game://grass.png") - ); - - const auto eliasTextueID = resources.loadImageFromFile(gfxDevice, eliasTextuerPath); - const auto eliasMaterialID = resources.materials().addMaterial({ - .baseColor = ModelDocUtils::GetImportedBaseColor( - planeModel, planePrimitive), - .textureFilteringMode = - TextureFilteringMode::Anisotropic, - .diffuseTexture = eliasTextueID, - .name = ModelDocUtils::GetImportedMaterialName( - planeModel, planePrimitive, "GroundPlaneMaterial"), - }); - - const auto cubeMeshID = resources.meshes().addMesh(gfxDevice, cubePrimitive.mesh); - // - // for (int i{0}; i < 100; i++) // { - // auto cube = std::make_shared("Cube"); + // const auto CharObj = scene.CreateGameObject("Character"); // - // cube->AddComponent(glm::vec3{0.5f}); - // cube->AddComponent(); + // ModelDoc::LoadOptions characterOptions{}; + // characterOptions.meshImportMode = ModelDoc::MeshImportMode::MergedPerNode; + // characterOptions.loadMaterials = true; + // characterOptions.loadSkeleton = true; + // characterOptions.loadAnimations = false; // - // auto meshComp = cube->AddComponent(); - // meshComp->SetMeshID(cubeMeshID); - // meshComp->SetMaterialID(eliasMaterialID); + // auto charModel = AssetManager::GetInstance().LoadModel("engine://characterMedium.fbx", characterOptions); // - // cube->GetTransform().SetWorldPosition(glm::vec3(0.0f, i, 0.0f)); - // cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); + // const auto& charPrimitive = + // ModelDocUtils::GetFirstSkinnedPrimitiveOrFirstOrThrow( + // charModel, + // "engine://characterMedium.fbx" + // ); // - // scene.Add(cube); - // scene.GetPhysics().RegisterGameObject(*cube); - // } - - // const int cubeCount = 10; - // const float spacing = 1.0f; + // const auto charMeshID = resources.meshes().addMesh(gfxDevice, charPrimitive.mesh); // - // for (int x = 0; x < cubeCount; x++) - // { - // for (int y = 0; y < 3; y++) + // const auto charTextureID = resources.loadImageFromFile(gfxDevice, + // AssetFS::GetInstance().GetFullPath("engine://textures/criminalMaleA.png") + // ); + // + // const auto charMaterialID = resources.materials().addMaterial({ + // .baseColor = ModelDocUtils::GetImportedBaseColor( + // charModel, charPrimitive), + // .diffuseTexture = charTextureID, + // .name = ModelDocUtils::GetImportedMaterialName( + // charModel, + // charPrimitive, + // "CharacterMaterial" + // ), + // }); + // + // const auto charMeshComp = CharObj->AddComponent(); + // charMeshComp->SetMeshID(charMeshID); + // charMeshComp->SetMaterialID(charMaterialID); + // + // const auto animator = CharObj->AddComponent(); + // animator->setSkeleton(charModel.skeleton); + // + // auto runClips = AssetManager::GetInstance().LoadAnimationClips( + // "engine://run.fbx", + // charModel.skeleton + // ); + // + // for (auto& clip : runClips) // { - // for (int z = 0; z < cubeCount; z++) - // { - // auto cube = std::make_shared("Cube"); - // - // cube->AddComponent(glm::vec3{0.5f}); - // cube->AddComponent(); - // - // auto meshComp = cube->AddComponent(); - // meshComp->SetMeshID(cubeMeshID); - // meshComp->SetMaterialID(eliasMaterialID); - // - // cube->GetTransform().SetWorldPosition(glm::vec3( - // (x - cubeCount / 2.0f) * spacing, - // y * spacing + 0, - // (z - cubeCount / 2.0f) * spacing - // )); - // - // cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); - // - // scene.Add(cube); - // scene.GetPhysics().RegisterGameObject(*cube); - // } + // spdlog::info("Loaded animation: '{}' ({:.2f}s)", clip.name, clip.duration); + // animator->addClip(std::make_shared(std::move(clip))); // } + // + // if (!runClips.empty()) + // { + // animator->play("Root|Run"); + // } + // + // CharObj->GetTransform().SetWorldPosition(glm::vec3(0.f)); + // CharObj->GetTransform().SetWorldPosition(glm::vec3(5, 0, 0)); + // } + // + // auto cubeModel = ModelDoc::LoadModel( + // AssetFS::GetInstance() + // .GetFullPath("engine://cube.fbx") + // .generic_string(), + // staticModelOptions + // ); + // ModelDocUtils::LogModelDocSummary(cubeModel, "cube.fbx"); + // + // const auto& cubePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(cubeModel, "game://cube.fbx"); + // + // + // const auto eliasTextuerPath = ModelDocUtils::PickTexturePath( + // cubeModel, + // cubePrimitive, + // AssetFS::GetInstance().GetFullPath("game://grass.png") + // ); + // + // const auto eliasTextueID = resources.loadImageFromFile(gfxDevice, eliasTextuerPath); + // const auto eliasMaterialID = resources.materials().addMaterial({ + // .baseColor = ModelDocUtils::GetImportedBaseColor( + // planeModel, planePrimitive), + // .textureFilteringMode = + // TextureFilteringMode::Anisotropic, + // .diffuseTexture = eliasTextueID, + // .name = ModelDocUtils::GetImportedMaterialName( + // planeModel, planePrimitive, "GroundPlaneMaterial"), + // }); + // + // const auto cubeMeshID = resources.meshes().addMesh(gfxDevice, cubePrimitive.mesh); + // // + // // for (int i{0}; i < 100; i++) + // // { + // // auto cube = std::make_shared("Cube"); + // // + // // cube->AddComponent(glm::vec3{0.5f}); + // // cube->AddComponent(); + // // + // // auto meshComp = cube->AddComponent(); + // // meshComp->SetMeshID(cubeMeshID); + // // meshComp->SetMaterialID(eliasMaterialID); + // // + // // cube->GetTransform().SetWorldPosition(glm::vec3(0.0f, i, 0.0f)); + // // cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); + // // + // // scene.Add(cube); + // // scene.GetPhysics().RegisterGameObject(*cube); + // // } + // + // // const int cubeCount = 10; + // // const float spacing = 1.0f; + // // + // // for (int x = 0; x < cubeCount; x++) + // // { + // // for (int y = 0; y < 3; y++) + // // { + // // for (int z = 0; z < cubeCount; z++) + // // { + // // auto cube = std::make_shared("Cube"); + // // + // // cube->AddComponent(glm::vec3{0.5f}); + // // cube->AddComponent(); + // // + // // auto meshComp = cube->AddComponent(); + // // meshComp->SetMeshID(cubeMeshID); + // // meshComp->SetMaterialID(eliasMaterialID); + // // + // // cube->GetTransform().SetWorldPosition(glm::vec3( + // // (x - cubeCount / 2.0f) * spacing, + // // y * spacing + 0, + // // (z - cubeCount / 2.0f) * spacing + // // )); + // // + // // cube->GetTransform().SetWorldScale(glm::vec3(0.005f)); + // // + // // scene.Add(cube); + // // scene.GetPhysics().RegisterGameObject(*cube); + // // } + // // } + // // } + + + // { + auto sphereModel = AssetManager::GetInstance().LoadModel("engine://sphere.fbx", staticModelOptions); + ModelDocUtils::LogModelDocSummary(sphereModel, "sphere.fbx"); + // + const auto& spherePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(sphereModel, "game://sphere.fbx"); + // + // + // const auto sphereTexturePath = ModelDocUtils::PickTexturePath( + // sphereModel, + // spherePrimitive, + // AssetFS::GetInstance().GetFullPath("game://238882.png") + // ); + // + // const auto sphereTextureID = resources.loadImageFromFile(gfxDevice, sphereTexturePath); + // sphereMaterial = resources.materials().addMaterial({ + // .baseColor = ModelDocUtils::GetImportedBaseColor(planeModel, planePrimitive), + // .textureFilteringMode =TextureFilteringMode::Anisotropic, + // .diffuseTexture = sphereTextureID, + // .name = ModelDocUtils::GetImportedMaterialName( + // planeModel, planePrimitive, + // "GroundPlaneMaterial"), + // }); + // + sphereMesh = resources.meshes().addMesh(gfxDevice, spherePrimitive.mesh); + // // } + sphereMaterial = resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Blue"); - { - auto sphereModel = ModelDoc::LoadModel( - AssetFS::GetInstance().GetFullPath("engine://sphere.fbx").generic_string(), - staticModelOptions - ); - ModelDocUtils::LogModelDocSummary(sphereModel, "sphere.fbx"); - - const auto& spherePrimitive = ModelDocUtils::GetFirstPrimitiveOrThrow(sphereModel, "game://sphere.fbx"); - - - const auto sphereTexturePath = ModelDocUtils::PickTexturePath( - sphereModel, - spherePrimitive, - AssetFS::GetInstance().GetFullPath("game://238882.png") - ); - - const auto sphereTextureID = resources.loadImageFromFile(gfxDevice, sphereTexturePath); - sphereMaterial = resources.materials().addMaterial({ - .baseColor = ModelDocUtils::GetImportedBaseColor(planeModel, planePrimitive), - .textureFilteringMode =TextureFilteringMode::Anisotropic, - .diffuseTexture = sphereTextureID, - .name = ModelDocUtils::GetImportedMaterialName( - planeModel, planePrimitive, - "GroundPlaneMaterial"), - }); - - sphereMesh = resources.meshes().addMesh(gfxDevice, spherePrimitive.mesh); - - } } void LightKeeper::customUpdate(float dt) @@ -501,7 +467,7 @@ void LightKeeper::customUpdate(float dt) if (ImGui::Button("Save Current Scene")) { try { - const auto path = ResolveScenePath(scenePath); + const auto path = ResolveScenePath(scenePath, m_params.exeDir); const auto parent = path.parent_path(); if (!parent.empty()) { std::filesystem::create_directories(parent); @@ -522,7 +488,7 @@ void LightKeeper::customUpdate(float dt) ImGui::SameLine(); if (ImGui::Button("Load Scene")) { try { - const auto path = ResolveScenePath(scenePath); + const auto path = ResolveScenePath(scenePath, m_params.exeDir); if (SceneSerializer::Load( SceneManager::GetInstance().GetCurrentScene(), path)) { diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp index b3949d7..6253001 100644 --- a/tests/destrum_tests.cpp +++ b/tests/destrum_tests.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -17,7 +18,9 @@ #include #include #include +#include #include +#include namespace { class TestComponent final : public Component { @@ -343,6 +346,91 @@ namespace { SceneManager::GetInstance().Destroy(); } + + void testAnimatorAssetReferences() + { + GameObject object{"animated"}; + auto* animator = object.AddComponent(); + + Skeleton skeleton; + Joint joint; + joint.id = 0; + joint.localTranslation = glm::vec3{0.0f}; + joint.localRotation = glm::identity(); + joint.localScale = glm::vec3{1.0f}; + skeleton.joints.push_back(joint); + skeleton.inverseBindMatrices.push_back(glm::mat4{1.0f}); + skeleton.jointNames.push_back("root"); + skeleton.assetReference.path = "engine://character.fbx"; + buildParentIndex(skeleton); + animator->setSkeleton(std::move(skeleton)); + + auto clip = std::make_shared(); + clip->name = "walk"; + clip->assetReference.path = "engine://walk.fbx"; + clip->assetReference.subresource = "animation:0"; + clip->duration = 1.0f; + SkeletalAnimation::Keyframe keyframe; + keyframe.time = 0.0f; + keyframe.translation = glm::vec3{0.0f}; + keyframe.rotation = glm::identity(); + keyframe.scale = glm::vec3{1.0f}; + SkeletalAnimation::Track track; + track.jointIndex = 0; + track.keyframes.push_back(keyframe); + clip->tracks.push_back(std::move(track)); + animator->addClip(std::move(clip)); + animator->play("walk"); + + const auto data = animator->Serialize(); + check(data.contains("assetReferences"), + "Animator scenes must store asset references"); + check(data.at("assetReferences").at("skeleton").at("path") == "engine://character.fbx", + "Animator serialization must store the skeleton source"); + check(data.at("assetReferences").at("animations").at(0).at("asset").at("subresource") == + "animation:0", + "Animator serialization must store the animation subresource"); + check(data.dump().find("keyframes") == std::string::npos, + "Animator scene data must not embed animation keyframes"); + } + + void testSimpleColorMaterial() + { + const auto material = Material::SimpleColor( + glm::vec3{0.2f, 0.4f, 0.8f}, + "blue material"); + + check(material.baseColor == glm::vec3{0.2f, 0.4f, 0.8f}, + "simple materials must preserve their color"); + check(material.metallicFactor == 0.0f && + material.roughnessFactor == 1.0f && + material.emissiveFactor == 0.0f, + "simple materials must use non-metallic default factors"); + check(material.diffuseTexture == NULL_IMAGE_ID, + "simple materials must not require a diffuse image"); + check(material.name == "blue material", + "simple materials must preserve their optional name"); + } + + void testAssetReferenceCacheKeys() + { + const AssetReference reference{ + .path = "game://CharacterMedium.fbx", + .subresource = "mesh:0", + }; + + const auto key = reference.cacheKey(); + check(key == "game://CharacterMedium.fbx#mesh:0", + "asset references must produce stable cache keys"); + + const auto parsed = AssetReference::fromCacheKey(key); + check(parsed.has_value() && + parsed->path == reference.path && + parsed->subresource == reference.subresource, + "asset cache keys must recover their source reference"); + check(!AssetReference::fromCacheKey("CharacterMedium.fbx").has_value(), + "plain cache names must not be treated as file-backed assets"); + } } int main() @@ -358,6 +446,9 @@ int main() testPhysicsWorldUnits(); testJoltSphereCollisions(); testSceneJoltSphereCollisions(); + testAnimatorAssetReferences(); + testSimpleColorMaterial(); + testAssetReferenceCacheKeys(); std::cout << "destrum tests passed\n"; return EXIT_SUCCESS; }