ADD: Scene animation

This commit is contained in:
2025-11-17 13:54:19 +09:00
parent 84ba26ee2b
commit 24089dc325
6 changed files with 569 additions and 9 deletions

View File

@@ -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 percharacter 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 perframe `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.

View File

@@ -20,6 +20,9 @@
#include <glm/mat4x4.hpp>
#include <glm/vec4.hpp>
#include <glm/vec3.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>
#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;

View File

@@ -1,5 +1,7 @@
#include "stb_image.h"
#include <iostream>
#include <algorithm>
#include <cmath>
#include "vk_loader.h"
#include "core/texture_cache.h"
@@ -623,11 +625,29 @@ std::optional<std::shared_ptr<LoadedGLTF> > 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<std::shared_ptr<LoadedGLTF> > 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<std::shared_ptr<LoadedGLTF> > 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<float>(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<glm::vec4>(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<glm::vec3>(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<Node> 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<int>(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<int>(i), resetTime);
return;
}
}
}
void LoadedGLTF::updateAnimation(float dt)
{
if (animations.empty()) return;
if (activeAnimation < 0 || activeAnimation >= static_cast<int>(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();

View File

@@ -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> node;
std::vector<float> times;
std::vector<glm::vec3> vec3Values; // translation / scale
std::vector<glm::vec4> vec4Values; // rotation (x,y,z,w)
};
struct Animation
{
std::string name;
float duration = 0.f;
std::vector<AnimationChannel> channels;
};
std::vector<Animation> animations;
int activeAnimation = -1;
float animationTime = 0.f;
bool animationLoop = true;
// Animation helpers
void updateAnimation(float dt);
void refreshAllTransforms();
std::shared_ptr<Node> 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(); };

View File

@@ -1,6 +1,8 @@
#include "vk_scene.h"
#include <utility>
#include <unordered_set>
#include <chrono>
#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<float>(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<LoadedGLTF *> animatedScenes;
auto updateSceneAnim = [&](std::shared_ptr<LoadedGLTF> &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;
}

View File

@@ -66,8 +66,21 @@ public:
void addGLTFInstance(const std::string &name, std::shared_ptr<LoadedGLTF> 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;