From 24089dc32598dc79d1ed55cbf53e36689a531548 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Mon, 17 Nov 2025 13:54:19 +0900 Subject: [PATCH] ADD: Scene animation --- docs/Scene.md | 45 ++++++ src/core/vk_types.h | 25 +++ src/scene/vk_loader.cpp | 335 +++++++++++++++++++++++++++++++++++++++- src/scene/vk_loader.h | 32 ++++ src/scene/vk_scene.cpp | 128 ++++++++++++++- src/scene/vk_scene.h | 13 ++ 6 files changed, 569 insertions(+), 9 deletions(-) diff --git a/docs/Scene.md b/docs/Scene.md index 86ad8c0..4113583 100644 --- a/docs/Scene.md +++ b/docs/Scene.md @@ -41,6 +41,51 @@ Thin scene layer that produces `RenderObject`s for the renderer. It gathers opaq - GLTF instances - `addGLTFInstance(name, LoadedGLTF, transform)`, `removeGLTFInstance(name)`, `clearGLTFInstances()`. +### GLTF Animation / “Actions” + +GLTF files can contain one or more animation clips (e.g. `Idle`, `Walk`, `Run`). The loader (`LoadedGLTF`) parses these into `LoadedGLTF::Animation` objects, and `SceneManager` exposes a thin API to pick which clip is currently playing. + +> Note: a `LoadedGLTF` is typically shared by multiple instances. Changing the active animation on a shared `LoadedGLTF` will affect all instances that point to it. If you want per‑character independent actions, load separate `LoadedGLTF` objects (one per character) or duplicate the asset in your game layer. + +**Static scenes (loaded via `loadScene`)** + +Example: engine default scene in `VulkanEngine::init()`: + +- `structure` is loaded and registered via: + - `sceneManager->loadScene("structure", structureFile);` + +To control its animation: + +- By index: + - `scene->setSceneAnimation("structure", 0); // first clip` + - `scene->setSceneAnimation("structure", 1, true); // second clip, reset time` +- By name (matches glTF animation name): + - `scene->setSceneAnimation("structure", "Idle");` + - `scene->setSceneAnimation("structure", "Run");` +- Looping: + - `scene->setSceneAnimationLoop("structure", true); // enable loop` + - `scene->setSceneAnimationLoop("structure", false); // play once and stop at end` + +All functions return `bool` to indicate whether the scene name was found. + +**Runtime GLTF instances** + +GLTF instances are created via: + +- `scene->addGLTFInstance("player", playerGltf, playerTransform);` + +You can treat each instance as an “actor” and drive its current action from your game state: + +- By index: + - `scene->setGLTFInstanceAnimation("player", 0);` +- By name: + - `scene->setGLTFInstanceAnimation("player", "Idle");` + - `scene->setGLTFInstanceAnimation("player", "Run");` +- Looping: + - `scene->setGLTFInstanceAnimationLoop("player", true);` + +These helpers forward to the underlying `LoadedGLTF`’s `setActiveAnimation(...)` and `animationLoop` fields. `SceneManager::update_scene()` advances animations every frame using a per‑frame `dt`, so once you select an action, it will keep playing automatically until you change it or disable looping. + ### GPU Scene Data - `GPUSceneData` carries camera matrices and lighting constants for the frame. diff --git a/src/core/vk_types.h b/src/core/vk_types.h index 4e34792..6ba7fbc 100644 --- a/src/core/vk_types.h +++ b/src/core/vk_types.h @@ -20,6 +20,9 @@ #include #include +#include +#include +#include #define VK_CHECK(x) \ @@ -148,6 +151,28 @@ struct Node : public IRenderable { glm::mat4 localTransform; glm::mat4 worldTransform; + glm::vec3 translation{0.0f, 0.0f, 0.0f}; + glm::vec3 scale{1.0f, 1.0f, 1.0f}; + glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f}; + bool hasTRS{false}; + + void updateLocalFromTRS() + { + glm::mat4 tm = glm::translate(glm::mat4(1.0f), translation); + glm::mat4 rm = glm::mat4_cast(rotation); + glm::mat4 sm = glm::scale(glm::mat4(1.0f), scale); + localTransform = tm * rm * sm; + } + + void setTRS(const glm::vec3 &t, const glm::quat &r, const glm::vec3 &s) + { + translation = t; + rotation = r; + scale = s; + hasTRS = true; + updateLocalFromTRS(); + } + void refreshTransform(const glm::mat4& parentMatrix) { worldTransform = parentMatrix * localTransform; diff --git a/src/scene/vk_loader.cpp b/src/scene/vk_loader.cpp index 2cbf178..91876a0 100644 --- a/src/scene/vk_loader.cpp +++ b/src/scene/vk_loader.cpp @@ -1,5 +1,7 @@ #include "stb_image.h" #include +#include +#include #include "vk_loader.h" #include "core/texture_cache.h" @@ -623,11 +625,29 @@ std::optional > loadGltf(VulkanEngine *engine, std:: } nodes.push_back(newNode); - file.nodes[node.name.c_str()]; + if (!node.name.empty()) + { + file.nodes[std::string(node.name)] = newNode; + } std::visit(fastgltf::visitor{ [&](fastgltf::Node::TransformMatrix matrix) { - memcpy(&newNode->localTransform, matrix.data(), sizeof(matrix)); + glm::mat4 m(1.0f); + memcpy(&m, matrix.data(), sizeof(matrix)); + + glm::vec3 t = glm::vec3(m[3]); + glm::vec3 col0 = glm::vec3(m[0]); + glm::vec3 col1 = glm::vec3(m[1]); + glm::vec3 col2 = glm::vec3(m[2]); + + glm::vec3 s(glm::length(col0), glm::length(col1), glm::length(col2)); + if (s.x != 0.0f) col0 /= s.x; + if (s.y != 0.0f) col1 /= s.y; + if (s.z != 0.0f) col2 /= s.z; + glm::mat3 rotMat(col0, col1, col2); + glm::quat r = glm::quat_cast(rotMat); + + newNode->setTRS(t, r, s); }, [&](fastgltf::Node::TRS transform) { glm::vec3 tl(transform.translation[0], transform.translation[1], @@ -636,11 +656,7 @@ std::optional > loadGltf(VulkanEngine *engine, std:: transform.rotation[2]); glm::vec3 sc(transform.scale[0], transform.scale[1], transform.scale[2]); - glm::mat4 tm = glm::translate(glm::mat4(1.f), tl); - glm::mat4 rm = glm::toMat4(rot); - glm::mat4 sm = glm::scale(glm::mat4(1.f), sc); - - newNode->localTransform = tm * rm * sm; + newNode->setTRS(tl, rot, sc); } }, node.transform); @@ -669,6 +685,125 @@ std::optional > loadGltf(VulkanEngine *engine, std:: node->refreshTransform(glm::mat4{1.f}); } } + + // Load animations (if present) + if (!gltf.animations.empty()) + { + file.animations.reserve(gltf.animations.size()); + + for (auto &anim: gltf.animations) + { + LoadedGLTF::Animation dstAnim; + dstAnim.name = anim.name.c_str(); + dstAnim.duration = 0.0f; + + dstAnim.channels.reserve(anim.channels.size()); + + for (auto &ch: anim.channels) + { + if (ch.nodeIndex >= nodes.size() || ch.samplerIndex >= anim.samplers.size()) + { + continue; + } + + LoadedGLTF::AnimationChannel channel{}; + channel.node = nodes[ch.nodeIndex]; + + switch (ch.path) + { + case fastgltf::AnimationPath::Translation: + channel.target = LoadedGLTF::AnimationChannel::Target::Translation; + break; + case fastgltf::AnimationPath::Rotation: + channel.target = LoadedGLTF::AnimationChannel::Target::Rotation; + break; + case fastgltf::AnimationPath::Scale: + channel.target = LoadedGLTF::AnimationChannel::Target::Scale; + break; + default: + // Weights and other paths not yet supported + continue; + } + + const fastgltf::AnimationSampler &sampler = anim.samplers[ch.samplerIndex]; + switch (sampler.interpolation) + { + case fastgltf::AnimationInterpolation::Step: + channel.interpolation = LoadedGLTF::AnimationChannel::Interpolation::Step; + break; + case fastgltf::AnimationInterpolation::Linear: + case fastgltf::AnimationInterpolation::CubicSpline: + default: + channel.interpolation = LoadedGLTF::AnimationChannel::Interpolation::Linear; + break; + } + + // Input times + const auto &timeAccessor = gltf.accessors[sampler.inputAccessor]; + channel.times.reserve(timeAccessor.count); + float maxTime = 0.0f; + + fastgltf::iterateAccessorWithIndex(gltf, timeAccessor, + [&](float value, size_t) { + channel.times.push_back(value); + if (value > maxTime) maxTime = value; + }); + + // Output values + const auto &valueAccessor = gltf.accessors[sampler.outputAccessor]; + const bool isCubic = sampler.interpolation == fastgltf::AnimationInterpolation::CubicSpline; + + if (channel.target == LoadedGLTF::AnimationChannel::Target::Rotation) + { + channel.vec4Values.clear(); + channel.vec4Values.reserve(valueAccessor.count); + + fastgltf::iterateAccessorWithIndex(gltf, valueAccessor, + [&](glm::vec4 v, size_t index) { + if (isCubic) + { + // For cubic-spline, values are [in, value, out]; keep only the middle one. + if (index % 3 != 1) return; + } + channel.vec4Values.push_back(v); + }); + } + else + { + channel.vec3Values.clear(); + channel.vec3Values.reserve(valueAccessor.count); + + fastgltf::iterateAccessorWithIndex(gltf, valueAccessor, + [&](glm::vec3 v, size_t index) { + if (isCubic) + { + if (index % 3 != 1) return; + } + channel.vec3Values.push_back(v); + }); + } + + if (!channel.times.empty()) + { + dstAnim.duration = std::max(dstAnim.duration, maxTime); + dstAnim.channels.push_back(std::move(channel)); + } + } + + if (!dstAnim.channels.empty()) + { + file.animations.push_back(std::move(dstAnim)); + } + } + + if (!file.animations.empty()) + { + file.activeAnimation = 0; + file.animationTime = 0.0f; + file.animationLoop = true; + } + } + // We no longer need glTF-owned buffer payloads; free any large vectors for (auto &buf : gltf.buffers) { @@ -701,6 +836,192 @@ void LoadedGLTF::Draw(const glm::mat4 &topMatrix, DrawContext &ctx) } } +std::shared_ptr LoadedGLTF::getNode(const std::string &name) +{ + auto it = nodes.find(name); + return (it != nodes.end()) ? it->second : nullptr; +} + +void LoadedGLTF::refreshAllTransforms() +{ + for (auto &n: topNodes) + { + if (n) + { + n->refreshTransform(glm::mat4{1.f}); + } + } +} + +void LoadedGLTF::setActiveAnimation(int index, bool resetTime) +{ + if (animations.empty()) + { + activeAnimation = -1; + return; + } + + if (index < 0 || index >= static_cast(animations.size())) + { + index = 0; + } + + activeAnimation = index; + if (resetTime) + { + animationTime = 0.0f; + } +} + +void LoadedGLTF::setActiveAnimation(const std::string &name, bool resetTime) +{ + for (size_t i = 0; i < animations.size(); ++i) + { + if (animations[i].name == name) + { + setActiveAnimation(static_cast(i), resetTime); + return; + } + } +} + +void LoadedGLTF::updateAnimation(float dt) +{ + if (animations.empty()) return; + if (activeAnimation < 0 || activeAnimation >= static_cast(animations.size())) return; + if (dt <= 0.0f) return; + + Animation &clip = animations[activeAnimation]; + if (clip.duration <= 0.0f) return; + + animationTime += dt; + if (animationLoop) + { + animationTime = std::fmod(animationTime, clip.duration); + if (animationTime < 0.0f) + { + animationTime += clip.duration; + } + } + else if (animationTime > clip.duration) + { + animationTime = clip.duration; + } + + float t = animationTime; + + for (auto &ch: clip.channels) + { + if (!ch.node) continue; + const size_t keyCount = ch.times.size(); + if (keyCount == 0) continue; + + size_t k1 = 0; + while (k1 < keyCount && ch.times[k1] < t) + { + ++k1; + } + + size_t k0; + if (k1 == 0) + { + k0 = k1 = 0; + } + else if (k1 >= keyCount) + { + k0 = keyCount - 1; + k1 = keyCount - 1; + } + else + { + k0 = k1 - 1; + } + + float t0 = ch.times[k0]; + float t1 = ch.times[k1]; + float alpha = 0.0f; + if (k0 != k1 && t1 > t0) + { + alpha = (t - t0) / (t1 - t0); + alpha = std::clamp(alpha, 0.0f, 1.0f); + } + + Node &node = *ch.node; + + switch (ch.target) + { + case AnimationChannel::Target::Translation: + { + if (ch.vec3Values.size() != keyCount) break; + glm::vec3 v0 = ch.vec3Values[k0]; + glm::vec3 v1 = ch.vec3Values[k1]; + glm::vec3 v; + if (ch.interpolation == AnimationChannel::Interpolation::Step || k0 == k1) + { + v = v0; + } + else + { + v = v0 * (1.0f - alpha) + v1 * alpha; + } + node.translation = v; + node.hasTRS = true; + break; + } + case AnimationChannel::Target::Scale: + { + if (ch.vec3Values.size() != keyCount) break; + glm::vec3 v0 = ch.vec3Values[k0]; + glm::vec3 v1 = ch.vec3Values[k1]; + glm::vec3 v; + if (ch.interpolation == AnimationChannel::Interpolation::Step || k0 == k1) + { + v = v0; + } + else + { + v = v0 * (1.0f - alpha) + v1 * alpha; + } + node.scale = v; + node.hasTRS = true; + break; + } + case AnimationChannel::Target::Rotation: + { + if (ch.vec4Values.size() != keyCount) break; + glm::vec4 v0 = ch.vec4Values[k0]; + glm::vec4 v1 = ch.vec4Values[k1]; + + glm::quat q0(v0.w, v0.x, v0.y, v0.z); + glm::quat q1(v1.w, v1.x, v1.y, v1.z); + glm::quat q; + if (ch.interpolation == AnimationChannel::Interpolation::Step || k0 == k1) + { + q = q0; + } + else + { + q = glm::slerp(q0, q1, alpha); + } + node.rotation = glm::normalize(q); + node.hasTRS = true; + break; + } + } + } + + // Rebuild local matrices from updated TRS and refresh world transforms + for (auto &[name, nodePtr]: nodes) + { + if (nodePtr && nodePtr->hasTRS) + { + nodePtr->updateLocalFromTRS(); + } + } + + refreshAllTransforms(); +} + void LoadedGLTF::clearAll() { VkDevice dv = creator->_deviceManager->device(); diff --git a/src/scene/vk_loader.h b/src/scene/vk_loader.h index b4e4f1e..ba94ade 100644 --- a/src/scene/vk_loader.h +++ b/src/scene/vk_loader.h @@ -59,6 +59,38 @@ struct LoadedGLTF : public IRenderable VulkanEngine *creator; + struct AnimationChannel + { + enum class Target { Translation, Rotation, Scale }; + enum class Interpolation { Linear, Step }; + + Target target = Target::Translation; + Interpolation interpolation = Interpolation::Linear; + std::shared_ptr node; + std::vector times; + std::vector vec3Values; // translation / scale + std::vector vec4Values; // rotation (x,y,z,w) + }; + + struct Animation + { + std::string name; + float duration = 0.f; + std::vector channels; + }; + + std::vector animations; + int activeAnimation = -1; + float animationTime = 0.f; + bool animationLoop = true; + + // Animation helpers + void updateAnimation(float dt); + void refreshAllTransforms(); + std::shared_ptr getNode(const std::string &name); + void setActiveAnimation(int index, bool resetTime = true); + void setActiveAnimation(const std::string &name, bool resetTime = true); + ~LoadedGLTF() { clearAll(); }; void clearMeshes(){ clearAll(); }; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 9934136..fd58e2e 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -1,6 +1,8 @@ #include "vk_scene.h" #include +#include +#include #include "vk_swapchain.h" #include "core/engine_context.h" @@ -36,9 +38,51 @@ void SceneManager::update_scene() mainCamera.update(); - if (loadedScenes.find("structure") != loadedScenes.end()) + // Simple per-frame dt (seconds) for animations + static auto lastFrameTime = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + float dt = std::chrono::duration(now - lastFrameTime).count(); + lastFrameTime = now; + if (dt < 0.f) { - loadedScenes["structure"]->Draw(glm::mat4{1.f}, mainDrawContext); + dt = 0.f; + } + if (dt > 0.1f) + { + dt = 0.1f; + } + + // Advance glTF animations once per unique LoadedGLTF + if (dt > 0.f) + { + std::unordered_set animatedScenes; + + auto updateSceneAnim = [&](std::shared_ptr &scene) { + if (!scene) return; + LoadedGLTF *ptr = scene.get(); + if (animatedScenes.insert(ptr).second) + { + ptr->updateAnimation(dt); + } + }; + + for (auto &[name, scene] : loadedScenes) + { + updateSceneAnim(scene); + } + for (auto &[name, inst] : dynamicGLTFInstances) + { + updateSceneAnim(inst.scene); + } + } + + // Draw all loaded GLTF scenes (static world) + for (auto &[name, scene] : loadedScenes) + { + if (scene) + { + scene->Draw(glm::mat4{1.f}, mainDrawContext); + } } // dynamic GLTF instances @@ -237,7 +281,87 @@ bool SceneManager::removeGLTFInstance(const std::string &name) return dynamicGLTFInstances.erase(name) > 0; } +bool SceneManager::setGLTFInstanceTransform(const std::string &name, const glm::mat4 &transform) +{ + auto it = dynamicGLTFInstances.find(name); + if (it == dynamicGLTFInstances.end()) return false; + it->second.transform = transform; + return true; +} + void SceneManager::clearGLTFInstances() { dynamicGLTFInstances.clear(); } + +bool SceneManager::setSceneAnimation(const std::string &sceneName, int animationIndex, bool resetTime) +{ + auto it = loadedScenes.find(sceneName); + if (it == loadedScenes.end() || !it->second) + { + return false; + } + + it->second->setActiveAnimation(animationIndex, resetTime); + return true; +} + +bool SceneManager::setSceneAnimation(const std::string &sceneName, const std::string &animationName, bool resetTime) +{ + auto it = loadedScenes.find(sceneName); + if (it == loadedScenes.end() || !it->second) + { + return false; + } + + it->second->setActiveAnimation(animationName, resetTime); + return true; +} + +bool SceneManager::setSceneAnimationLoop(const std::string &sceneName, bool loop) +{ + auto it = loadedScenes.find(sceneName); + if (it == loadedScenes.end() || !it->second) + { + return false; + } + + it->second->animationLoop = loop; + return true; +} + +bool SceneManager::setGLTFInstanceAnimation(const std::string &instanceName, int animationIndex, bool resetTime) +{ + auto it = dynamicGLTFInstances.find(instanceName); + if (it == dynamicGLTFInstances.end() || !it->second.scene) + { + return false; + } + + it->second.scene->setActiveAnimation(animationIndex, resetTime); + return true; +} + +bool SceneManager::setGLTFInstanceAnimation(const std::string &instanceName, const std::string &animationName, bool resetTime) +{ + auto it = dynamicGLTFInstances.find(instanceName); + if (it == dynamicGLTFInstances.end() || !it->second.scene) + { + return false; + } + + it->second.scene->setActiveAnimation(animationName, resetTime); + return true; +} + +bool SceneManager::setGLTFInstanceAnimationLoop(const std::string &instanceName, bool loop) +{ + auto it = dynamicGLTFInstances.find(instanceName); + if (it == dynamicGLTFInstances.end() || !it->second.scene) + { + return false; + } + + it->second.scene->animationLoop = loop; + return true; +} diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index b487ef2..ef768a7 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -66,8 +66,21 @@ public: void addGLTFInstance(const std::string &name, std::shared_ptr scene, const glm::mat4 &transform = glm::mat4(1.f)); bool removeGLTFInstance(const std::string &name); + bool setGLTFInstanceTransform(const std::string &name, const glm::mat4 &transform); void clearGLTFInstances(); + // Animation control helpers (glTF) + // Note: a LoadedGLTF may be shared by multiple instances; changing + // the active animation on a scene or instance affects all users + // of that shared LoadedGLTF. + bool setSceneAnimation(const std::string &sceneName, int animationIndex, bool resetTime = true); + bool setSceneAnimation(const std::string &sceneName, const std::string &animationName, bool resetTime = true); + bool setSceneAnimationLoop(const std::string &sceneName, bool loop); + + bool setGLTFInstanceAnimation(const std::string &instanceName, int animationIndex, bool resetTime = true); + bool setGLTFInstanceAnimation(const std::string &instanceName, const std::string &animationName, bool resetTime = true); + bool setGLTFInstanceAnimationLoop(const std::string &instanceName, bool loop); + struct SceneStats { float scene_update_time = 0.f;