From d09a79d47c1354dc53143147380e1ac725b8d5a1 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Sat, 29 Nov 2025 23:25:16 +0900 Subject: [PATCH] ADD: glTF instance rotation, animation --- docs/GameAPI.md | 29 +++++- docs/Scene.md | 45 +++++++--- src/core/engine.cpp | 49 ++++++++++- src/scene/vk_loader.cpp | 54 +++++++----- src/scene/vk_loader.h | 16 ++-- src/scene/vk_scene.cpp | 191 ++++++++++++++++++++++++++++++---------- src/scene/vk_scene.h | 17 ++++ 7 files changed, 310 insertions(+), 91 deletions(-) diff --git a/docs/GameAPI.md b/docs/GameAPI.md index fa4287a..2cdcc36 100644 --- a/docs/GameAPI.md +++ b/docs/GameAPI.md @@ -81,7 +81,33 @@ Docs: `docs/Scene.md` - `bool setGLTFInstanceAnimationLoop(const std::string &instanceName, bool loop);` - Notes: - All functions return `bool` indicating whether the named scene/instance exists. - - `SceneManager::update_scene()` advances active animations each frame using engine delta time. + - Animation state is **independent per scene and per instance**: + - Each named scene has its own `AnimationState`. + - Each glTF instance has its own `AnimationState`, even when sharing the same `LoadedGLTF`. + - An index `< 0` (e.g. `-1`) disables animation for that scene/instance (pose is frozen at the last evaluated state). + - `SceneManager::update_scene()` advances each active animation state every frame using engine delta time. + +### Per‑Instance Node / Joint Control (Non‑Skinned) + +For rigid models and simple “joints” (e.g. flaps, doors, turrets), you can apply local‑space pose offsets to individual glTF nodes per instance: + +- `bool setGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName, const glm::mat4 &offset);` +- `bool clearGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName);` +- `void clearGLTFInstanceNodeOffsets(const std::string &instanceName);` + +Typical usage: + +- Use glTF animation for the base motion (e.g. gear deployment). +- Layer game‑driven offsets on top for per‑instance control: + + ```cpp + // Rotate a control surface on one aircraft instance + glm::mat4 offset = + glm::rotate(glm::mat4(1.f), + glm::radians(aileronDegrees), + glm::vec3(1.f, 0.f, 0.f)); + sceneMgr->setGLTFInstanceNodeOffset("plane01", "LeftAileron", offset); + ``` ### Point Lights @@ -163,4 +189,3 @@ These are primarily debug/editor features but can be kept in a game build to pro - Point‑light editor UI built on `SceneManager` light APIs. - Object gizmo (ImGuizmo): - Uses last pick / hover pick as the current target and manipulates transforms via `setMeshInstanceTransform` / `setGLTFInstanceTransform`. - diff --git a/docs/Scene.md b/docs/Scene.md index 336ff26..72bad91 100644 --- a/docs/Scene.md +++ b/docs/Scene.md @@ -43,9 +43,12 @@ Thin scene layer that produces `RenderObject`s for the renderer. It gathers opaq ### 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. +GLTF files can contain one or more animation clips (e.g. `Idle`, `Walk`, `Run`). The loader (`LoadedGLTF`) parses these into `LoadedGLTF::Animation` objects. Animation *state* (which clip, time, loop flag) is stored outside the glTF asset: -> 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. +- One `AnimationState` per named static scene (for `loadScene`). +- One `AnimationState` per runtime glTF instance (`SceneManager::GLTFInstance`). + +This means that **animation is independent per scene and per instance**, even if they share the same underlying `LoadedGLTF` asset and meshes. **Static scenes (loaded via `loadScene`)** @@ -54,19 +57,19 @@ Example: engine default scene in `VulkanEngine::init()`: - `structure` is loaded and registered via: - `sceneManager->loadScene("structure", structureFile);` -To control its animation: +To control its animation state: -- By index: +- By index (per‑scene state): - `scene->setSceneAnimation("structure", 0); // first clip` - `scene->setSceneAnimation("structure", 1, true); // second clip, reset time` -- By name (matches glTF animation name): +- By name (per‑scene state; matches glTF animation name): - `scene->setSceneAnimation("structure", "Idle");` - `scene->setSceneAnimation("structure", "Run");` -- Looping: +- Looping (per‑scene state): - `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. +All functions return `bool` to indicate whether the scene name was found. A negative index (e.g. `-1`) disables animation for that scene (pose stays at the last evaluated frame). **Runtime GLTF instances** @@ -74,17 +77,35 @@ 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: +You can treat each instance as an “actor” and drive its current action from your game state. Each instance has its own `AnimationState`, even if multiple instances share the same `LoadedGLTF`. -- By index: +- By index (per‑instance state): - `scene->setGLTFInstanceAnimation("player", 0);` -- By name: + - `scene->setGLTFInstanceAnimation("player", -1); // disable animation for this actor` +- By name (per‑instance state): - `scene->setGLTFInstanceAnimation("player", "Idle");` - `scene->setGLTFInstanceAnimation("player", "Run");` -- Looping: +- Looping (per‑instance state): - `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. +These helpers update the instance’s `AnimationState`. `SceneManager::update_scene()` advances each instance’s state every frame using a per‑frame `dt` before drawing, so once you select an action, it will keep playing automatically until you change it or disable looping for that instance. + +### Per‑Instance Node / Joint Overrides + +For non‑skinned models (rigid parts), you can apply local‑space pose offsets to specific glTF nodes on a **per‑instance** basis. This is useful for things like control surfaces, doors, or turrets layered on top of an existing animation. + +- API (on `SceneManager`): + - `bool setGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName, const glm::mat4 &offset);` + - `bool clearGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName);` + - `void clearGLTFInstanceNodeOffsets(const std::string &instanceName);` + +Notes: + +- Offsets are **local‑space** post‑multipliers: + - Effective local transform = `node.localTransform * offset`. +- Offsets are *per instance*: + - Different instances of the same glTF can have different joint poses at the same animation time. +- Overrides are applied during draw via `DrawContext::gltfNodeLocalOverrides` and `MeshNode::Draw`, without modifying the shared glTF asset. ### GPU Scene Data diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 58918d5..d340aa3 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -1044,9 +1044,56 @@ void VulkanEngine::init_pipelines() metalRoughMaterial.build_pipelines(this); } +namespace +{ + // Rebuild a node's world transform in glTF local space, layering per-instance + // local offsets on top of the base localTransform at each node in the chain. + glm::mat4 build_node_world_with_overrides(const Node *node, + const std::unordered_map &overrides) + { + if (!node) + { + return glm::mat4(1.0f); + } + + std::vector chain; + const Node *cur = node; + while (cur) + { + chain.push_back(cur); + std::shared_ptr parent = cur->parent.lock(); + cur = parent ? parent.get() : nullptr; + } + + glm::mat4 world(1.0f); + for (auto it = chain.rbegin(); it != chain.rend(); ++it) + { + const Node *n = *it; + glm::mat4 local = n->localTransform; + auto ovIt = overrides.find(n); + if (ovIt != overrides.end()) + { + // Layer the override in local space for this instance. + local = local * ovIt->second; + } + world = world * local; + } + return world; + } +} + void MeshNode::Draw(const glm::mat4 &topMatrix, DrawContext &ctx) { - glm::mat4 nodeMatrix = topMatrix * worldTransform; + glm::mat4 nodeMatrix; + if (ctx.gltfNodeLocalOverrides && !ctx.gltfNodeLocalOverrides->empty()) + { + glm::mat4 world = build_node_world_with_overrides(this, *ctx.gltfNodeLocalOverrides); + nodeMatrix = topMatrix * world; + } + else + { + nodeMatrix = topMatrix * worldTransform; + } if (!mesh) { diff --git a/src/scene/vk_loader.cpp b/src/scene/vk_loader.cpp index 09ded33..4cfc15e 100644 --- a/src/scene/vk_loader.cpp +++ b/src/scene/vk_loader.cpp @@ -832,12 +832,8 @@ std::optional > loadGltf(VulkanEngine *engine, std:: } } - if (!file.animations.empty()) - { - file.activeAnimation = 0; - file.animationTime = 0.0f; - file.animationLoop = true; - } + // Default animation state is now owned by SceneManager per static scene / instance. + // LoadedGLTF only stores shared animation clips. } // We no longer need glTF-owned buffer payloads; free any large vectors @@ -896,62 +892,72 @@ void LoadedGLTF::refreshAllTransforms() } } -void LoadedGLTF::setActiveAnimation(int index, bool resetTime) +void LoadedGLTF::setActiveAnimation(AnimationState &state, int index, bool resetTime) { if (animations.empty()) { - activeAnimation = -1; + state.activeAnimation = -1; return; } - if (index < 0 || index >= static_cast(animations.size())) + if (index < 0) + { + state.activeAnimation = -1; + if (resetTime) + { + state.animationTime = 0.0f; + } + return; + } + + if (index >= static_cast(animations.size())) { index = 0; } - activeAnimation = index; + state.activeAnimation = index; if (resetTime) { - animationTime = 0.0f; + state.animationTime = 0.0f; } } -void LoadedGLTF::setActiveAnimation(const std::string &name, bool resetTime) +void LoadedGLTF::setActiveAnimation(AnimationState &state, 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); + setActiveAnimation(state, static_cast(i), resetTime); return; } } } -void LoadedGLTF::updateAnimation(float dt) +void LoadedGLTF::updateAnimation(float dt, AnimationState &state) { if (animations.empty()) return; - if (activeAnimation < 0 || activeAnimation >= static_cast(animations.size())) return; + if (state.activeAnimation < 0 || state.activeAnimation >= static_cast(animations.size())) return; if (dt <= 0.0f) return; - Animation &clip = animations[activeAnimation]; + Animation &clip = animations[state.activeAnimation]; if (clip.duration <= 0.0f) return; - animationTime += dt; - if (animationLoop) + state.animationTime += dt; + if (state.animationLoop) { - animationTime = std::fmod(animationTime, clip.duration); - if (animationTime < 0.0f) + state.animationTime = std::fmod(state.animationTime, clip.duration); + if (state.animationTime < 0.0f) { - animationTime += clip.duration; + state.animationTime += clip.duration; } } - else if (animationTime > clip.duration) + else if (state.animationTime > clip.duration) { - animationTime = clip.duration; + state.animationTime = clip.duration; } - float t = animationTime; + float t = state.animationTime; for (auto &ch: clip.channels) { diff --git a/src/scene/vk_loader.h b/src/scene/vk_loader.h index e8dc1a6..1e87082 100644 --- a/src/scene/vk_loader.h +++ b/src/scene/vk_loader.h @@ -98,20 +98,24 @@ struct LoadedGLTF : public IRenderable std::vector channels; }; + struct AnimationState + { + int activeAnimation = -1; + float animationTime = 0.f; + bool animationLoop = true; + }; + std::vector animations; - int activeAnimation = -1; - float animationTime = 0.f; - bool animationLoop = true; // Optional debug name (e.g., key used when loaded into SceneManager) std::string debugName; // Animation helpers - void updateAnimation(float dt); + void updateAnimation(float dt, AnimationState &state); 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); + void setActiveAnimation(AnimationState &state, int index, bool resetTime = true); + void setActiveAnimation(AnimationState &state, const std::string &name, bool resetTime = true); ~LoadedGLTF() { diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 67d62df..a247b10 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -123,6 +123,7 @@ void SceneManager::update_scene() mainDrawContext.OpaqueSurfaces.clear(); mainDrawContext.TransparentSurfaces.clear(); mainDrawContext.nextID = 1; + mainDrawContext.gltfNodeLocalOverrides = nullptr; mainCamera.update(); @@ -140,30 +141,6 @@ void SceneManager::update_scene() 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); - } - } - auto tagOwner = [&](RenderObject::OwnerType type, const std::string &name, size_t opaqueBegin, size_t transpBegin) { @@ -179,29 +156,57 @@ void SceneManager::update_scene() } }; - // Draw all loaded GLTF scenes (static world) + // Draw all loaded GLTF scenes (static world), advancing their independent animation states. for (auto &[name, scene] : loadedScenes) { - if (scene) + if (!scene) { - const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size(); - const size_t transpStart = mainDrawContext.TransparentSurfaces.size(); - scene->Draw(glm::mat4{1.f}, mainDrawContext); - tagOwner(RenderObject::OwnerType::StaticGLTF, name, opaqueStart, transpStart); + continue; } + + // Advance this scene's animation state (independent of instances). + if (dt > 0.f) + { + auto &animState = sceneAnimations[name]; + scene->updateAnimation(dt, animState); + } + + const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size(); + const size_t transpStart = mainDrawContext.TransparentSurfaces.size(); + mainDrawContext.gltfNodeLocalOverrides = nullptr; + scene->Draw(glm::mat4{1.f}, mainDrawContext); + mainDrawContext.gltfNodeLocalOverrides = nullptr; + tagOwner(RenderObject::OwnerType::StaticGLTF, name, opaqueStart, transpStart); } - // dynamic GLTF instances - for (const auto &kv: dynamicGLTFInstances) + // dynamic GLTF instances (each with its own animation state) + for (auto &kv : dynamicGLTFInstances) { - const GLTFInstance &inst = kv.second; - if (inst.scene) + GLTFInstance &inst = kv.second; + if (!inst.scene) { - const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size(); - const size_t transpStart = mainDrawContext.TransparentSurfaces.size(); - inst.scene->Draw(inst.transform, mainDrawContext); - tagOwner(RenderObject::OwnerType::GLTFInstance, kv.first, opaqueStart, transpStart); + continue; } + + if (dt > 0.f) + { + inst.scene->updateAnimation(dt, inst.animation); + } + + const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size(); + const size_t transpStart = mainDrawContext.TransparentSurfaces.size(); + // Enable per-instance node pose overrides while drawing this instance. + if (!inst.nodeLocalOverrides.empty()) + { + mainDrawContext.gltfNodeLocalOverrides = &inst.nodeLocalOverrides; + } + else + { + mainDrawContext.gltfNodeLocalOverrides = nullptr; + } + inst.scene->Draw(inst.transform, mainDrawContext); + mainDrawContext.gltfNodeLocalOverrides = nullptr; + tagOwner(RenderObject::OwnerType::GLTFInstance, kv.first, opaqueStart, transpStart); } // Default primitives are added as dynamic instances by the engine. @@ -371,7 +376,21 @@ void SceneManager::loadScene(const std::string &name, std::shared_ptrdebugName = name; } - loadedScenes[name] = std::move(scene); + loadedScenes[name] = scene; + + // Initialize default animation state for this named scene (play first clip if present). + if (scene && !scene->animations.empty()) + { + LoadedGLTF::AnimationState st{}; + st.activeAnimation = 0; + st.animationTime = 0.0f; + st.animationLoop = true; + sceneAnimations[name] = st; + } + else + { + sceneAnimations.erase(name); + } } std::shared_ptr SceneManager::getScene(const std::string &name) @@ -400,6 +419,7 @@ void SceneManager::cleanup() // Drop our references to GLTF scenes. Their destructors call clearAll() // exactly once to release GPU resources. loadedScenes.clear(); + sceneAnimations.clear(); loadedNodes.clear(); } @@ -453,7 +473,16 @@ void SceneManager::addGLTFInstance(const std::string &name, std::shared_ptrdebugName.empty() ? "" : scene->debugName.c_str()); - dynamicGLTFInstances[name] = GLTFInstance{std::move(scene), transform}; + GLTFInstance inst{}; + inst.scene = std::move(scene); + inst.transform = transform; + if (inst.scene && !inst.scene->animations.empty()) + { + inst.animation.activeAnimation = 0; + inst.animation.animationTime = 0.0f; + inst.animation.animationLoop = true; + } + dynamicGLTFInstances[name] = std::move(inst); } bool SceneManager::removeGLTFInstance(const std::string &name) @@ -519,6 +548,71 @@ void SceneManager::clearGLTFInstances() pendingGLTFRelease.size()); } +bool SceneManager::setGLTFInstanceNodeOffset(const std::string &instanceName, + const std::string &nodeName, + const glm::mat4 &offset) +{ + auto it = dynamicGLTFInstances.find(instanceName); + if (it == dynamicGLTFInstances.end()) + { + return false; + } + GLTFInstance &inst = it->second; + if (!inst.scene) + { + return false; + } + + auto nodePtr = inst.scene->getNode(nodeName); + if (!nodePtr) + { + return false; + } + + inst.nodeLocalOverrides[nodePtr.get()] = offset; + return true; +} + +bool SceneManager::clearGLTFInstanceNodeOffset(const std::string &instanceName, + const std::string &nodeName) +{ + auto it = dynamicGLTFInstances.find(instanceName); + if (it == dynamicGLTFInstances.end()) + { + return false; + } + GLTFInstance &inst = it->second; + if (!inst.scene) + { + return false; + } + + auto nodePtr = inst.scene->getNode(nodeName); + if (!nodePtr) + { + return false; + } + + auto ovIt = inst.nodeLocalOverrides.find(nodePtr.get()); + if (ovIt == inst.nodeLocalOverrides.end()) + { + return false; + } + + inst.nodeLocalOverrides.erase(ovIt); + return true; +} + +void SceneManager::clearGLTFInstanceNodeOffsets(const std::string &instanceName) +{ + auto it = dynamicGLTFInstances.find(instanceName); + if (it == dynamicGLTFInstances.end()) + { + return; + } + it->second.nodeLocalOverrides.clear(); +} + bool SceneManager::setSceneAnimation(const std::string &sceneName, int animationIndex, bool resetTime) { auto it = loadedScenes.find(sceneName); @@ -527,7 +621,8 @@ bool SceneManager::setSceneAnimation(const std::string &sceneName, int animation return false; } - it->second->setActiveAnimation(animationIndex, resetTime); + auto &animState = sceneAnimations[sceneName]; + it->second->setActiveAnimation(animState, animationIndex, resetTime); return true; } @@ -539,7 +634,8 @@ bool SceneManager::setSceneAnimation(const std::string &sceneName, const std::st return false; } - it->second->setActiveAnimation(animationName, resetTime); + auto &animState = sceneAnimations[sceneName]; + it->second->setActiveAnimation(animState, animationName, resetTime); return true; } @@ -551,7 +647,8 @@ bool SceneManager::setSceneAnimationLoop(const std::string &sceneName, bool loop return false; } - it->second->animationLoop = loop; + auto &animState = sceneAnimations[sceneName]; + animState.animationLoop = loop; return true; } @@ -563,7 +660,8 @@ bool SceneManager::setGLTFInstanceAnimation(const std::string &instanceName, int return false; } - it->second.scene->setActiveAnimation(animationIndex, resetTime); + LoadedGLTF::AnimationState &animState = it->second.animation; + it->second.scene->setActiveAnimation(animState, animationIndex, resetTime); return true; } @@ -575,7 +673,8 @@ bool SceneManager::setGLTFInstanceAnimation(const std::string &instanceName, con return false; } - it->second.scene->setActiveAnimation(animationName, resetTime); + LoadedGLTF::AnimationState &animState = it->second.animation; + it->second.scene->setActiveAnimation(animState, animationName, resetTime); return true; } @@ -587,6 +686,6 @@ bool SceneManager::setGLTFInstanceAnimationLoop(const std::string &instanceName, return false; } - it->second.scene->animationLoop = loop; + it->second.animation.animationLoop = loop; return true; } diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index bc9ffe5..4b4a325 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -50,6 +50,9 @@ struct DrawContext std::vector TransparentSurfaces; // Monotonic counter used to assign stable per-frame object IDs. uint32_t nextID = 1; + // Optional per-instance glTF node local overrides (additive layer in local space). + // When non-null, MeshNode::Draw will rebuild world transforms using these offsets. + const std::unordered_map *gltfNodeLocalOverrides = nullptr; }; class SceneManager @@ -105,6 +108,10 @@ public: { std::shared_ptr scene; glm::mat4 transform{1.f}; + LoadedGLTF::AnimationState animation; + // Per-instance local-space pose offsets for nodes in this glTF scene. + // The offset matrix is post-multiplied onto the node's localTransform. + std::unordered_map nodeLocalOverrides; }; void addGLTFInstance(const std::string &name, std::shared_ptr scene, @@ -113,6 +120,14 @@ public: bool getGLTFInstanceTransform(const std::string &name, glm::mat4 &outTransform); bool setGLTFInstanceTransform(const std::string &name, const glm::mat4 &transform); void clearGLTFInstances(); + // Per-instance glTF node pose overrides (local-space, layered on top of animation/base TRS). + // 'offset' is post-multiplied onto the node's localTransform for this instance only. + bool setGLTFInstanceNodeOffset(const std::string &instanceName, + const std::string &nodeName, + const glm::mat4 &offset); + bool clearGLTFInstanceNodeOffset(const std::string &instanceName, + const std::string &nodeName); + void clearGLTFInstanceNodeOffsets(const std::string &instanceName); // Animation control helpers (glTF) // Note: a LoadedGLTF may be shared by multiple instances; changing @@ -167,6 +182,8 @@ private: std::vector pointLights; std::unordered_map > loadedScenes; + // Per-named static glTF scene animation state (independent of instances). + std::unordered_map sceneAnimations; std::unordered_map > loadedNodes; std::unordered_map dynamicMeshInstances; std::unordered_map dynamicGLTFInstances;