Added skeletal animations and other fixes

This commit is contained in:
2026-03-16 12:57:53 +01:00
parent 2cc18b3329
commit 28c6703892
51 changed files with 1964 additions and 602 deletions

View File

@@ -0,0 +1,189 @@
#include <destrum/Components/Animator.h>
#include <destrum/Graphics/Pipelines/SkinningPipeline.h>
#include <destrum/ObjectModel/GameObject.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include "spdlog/spdlog.h"
Animator::Animator(GameObject& parent)
: Component(parent, "Animator") {}
// ─── Component interface ──────────────────────────────────────────────────────
void Animator::Update() {
if (!m_current.clip) return;
// Time delta comes from your engine's time system
// const float dt = Time::GetDeltaTime();
const float dt = 0.016f; // ~60 FPS
m_current.time += dt * m_current.speed;
if (m_current.clip->looped)
m_current.time = std::fmod(m_current.time, m_current.clip->duration);
else
m_current.time = std::min(m_current.time, m_current.clip->duration);
if (m_previous.clip) {
m_previous.time += dt * m_previous.speed;
if (m_previous.clip->looped)
m_previous.time = std::fmod(m_previous.time, m_previous.clip->duration);
m_blendT += dt / m_blendDuration;
if (m_blendT >= 1.f) {
m_blendT = 1.f;
m_previous = {};
}
}
spdlog::info("Playing '{}': time = {:.2f}s, blend = {:.2f}",
m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str(),
m_current.time,
m_blendT);
}
void Animator::ImGuiInspector() {
// ImGui::Text("Clip: %s", m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str());
// ImGui::Text("Time: %.2f", m_current.time);
// ImGui::Text("Blend: %.2f", m_blendT);
//
// if (!isPlaying()) {
// ImGui::BeginDisabled();
// ImGui::Button("Stop");
// ImGui::EndDisabled();
// } else if (ImGui::Button("Stop")) {
// stop();
// }
//
// ImGui::Separator();
// ImGui::Text("Clips:");
// for (auto& [name, _] : m_clips) {
// if (ImGui::Selectable(name.c_str(), name == m_currentClipName))
// play(name);
// }
}
// ─── Animation control ────────────────────────────────────────────────────────
void Animator::addClip(std::shared_ptr<SkeletalAnimation> clip) {
m_clips[clip->name] = std::move(clip);
}
void Animator::play(const std::string& name, float blendTime) {
auto it = m_clips.find(name);
if (it == m_clips.end()) return;
if (m_current.clip && blendTime > 0.f) {
m_previous = m_current;
m_blendT = 0.f;
m_blendDuration = blendTime;
} else {
m_previous = {};
m_blendT = 1.f;
}
m_current = { it->second.get(), 0.f, 1.f };
m_currentClipName = name;
}
void Animator::stop() {
m_current = {};
m_previous = {};
m_currentClipName = {};
}
// ─── Joint matrix upload ──────────────────────────────────────────────────────
std::size_t Animator::uploadJointMatrices(SkinningPipeline& pipeline,
const Skeleton& skeleton,
std::size_t frameIndex) {
auto matrices = computeJointMatrices(skeleton);
return pipeline.appendJointMatrices(matrices, frameIndex);
}
// ─── Private: pose evaluation ─────────────────────────────────────────────────
std::vector<glm::mat4> Animator::computeJointMatrices(const Skeleton& skeleton) {
const std::size_t numJoints = skeleton.joints.size();
std::vector<glm::mat4> global(numJoints, glm::mat4{1.f});
std::vector<glm::mat4> result(numJoints, glm::mat4{1.f});
auto findTrack = [](const SkeletalAnimation* clip,
std::uint32_t idx) -> const SkeletalAnimation::Track* {
if (!clip) return nullptr;
for (auto& t : clip->tracks)
if (t.jointIndex == idx) return &t;
return nullptr;
};
for (std::uint32_t i = 0; i < numJoints; ++i) {
glm::vec3 tr = {0.f, 0.f, 0.f};
glm::quat rot = glm::identity<glm::quat>();
glm::vec3 sc = {1.f, 1.f, 1.f};
if (const auto* track = findTrack(m_current.clip, i)) {
tr = sampleTranslation(*track, m_current.time);
rot = sampleRotation (*track, m_current.time);
sc = sampleScale (*track, m_current.time);
}
if (m_previous.clip && m_blendT < 1.f) {
if (const auto* prev = findTrack(m_previous.clip, i)) {
tr = glm::mix (sampleTranslation(*prev, m_previous.time), tr, m_blendT);
rot = glm::slerp(sampleRotation (*prev, m_previous.time), rot, m_blendT);
sc = glm::mix (sampleScale (*prev, m_previous.time), sc, m_blendT);
}
}
glm::mat4 local = glm::translate(glm::mat4{1.f}, tr)
* glm::mat4_cast(rot)
* glm::scale(glm::mat4{1.f}, sc);
const int parent = skeleton.parentIndex[i];
global[i] = (parent < 0) ? local : global[parent] * local;
result[i] = global[i] * skeleton.inverseBindMatrices[i];
}
return result;
}
// ─── Keyframe sampling ────────────────────────────────────────────────────────
glm::vec3 Animator::sampleTranslation(const SkeletalAnimation::Track& track, float t) {
const auto& kf = track.keyframes;
if (kf.size() == 1) return kf[0].translation;
for (std::size_t i = 0; i + 1 < kf.size(); ++i) {
if (t <= kf[i + 1].time) {
float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time);
return glm::mix(kf[i].translation, kf[i + 1].translation, f);
}
}
return kf.back().translation;
}
glm::quat Animator::sampleRotation(const SkeletalAnimation::Track& track, float t) {
const auto& kf = track.keyframes;
if (kf.size() == 1) return kf[0].rotation;
for (std::size_t i = 0; i + 1 < kf.size(); ++i) {
if (t <= kf[i + 1].time) {
float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time);
return glm::slerp(kf[i].rotation, kf[i + 1].rotation, f);
}
}
return kf.back().rotation;
}
glm::vec3 Animator::sampleScale(const SkeletalAnimation::Track& track, float t) {
const auto& kf = track.keyframes;
if (kf.size() == 1) return kf[0].scale;
for (std::size_t i = 0; i + 1 < kf.size(); ++i) {
if (t <= kf[i + 1].time) {
float f = (t - kf[i].time) / (kf[i + 1].time - kf[i].time);
return glm::mix(kf[i].scale, kf[i + 1].scale, f);
}
}
return kf.back().scale;
}

View File

@@ -1,19 +1,56 @@
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/ObjectModel/Transform.h>
#include "destrum/Components/Animator.h"
#include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/GameState.h"
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
}
void MeshRendererComponent::Start() {
Component::Start();
if (auto* animator = GetGameObject()->GetComponent<Animator>()) {
const auto& gfxDevice = GameState::GetInstance().Gfx();
const auto& mesh = GameState::GetInstance().Renderer().GetMeshCache().getMesh(meshID);
m_skinnedMesh = std::make_unique<SkinnedMesh>();
m_skinnedMesh->skinnedVertexBuffer = gfxDevice.createBuffer(
mesh.numVertices * sizeof(CPUMesh::Vertex),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
);
}
}
void MeshRendererComponent::Update() {
}
void MeshRendererComponent::Render(const RenderContext& ctx) {
if (meshID != NULL_MESH_ID && materialID != NULL_MATERIAL_ID) {
if (meshID == NULL_MESH_ID || materialID == NULL_MATERIAL_ID) return;
if (auto* animator = GetGameObject()->GetComponent<Animator>(); animator && m_skinnedMesh) {
const auto& mesh = ctx.renderer.GetMeshCache().getCPUMesh(meshID);
const auto skeleton = GetGameObject()->GetComponent<Animator>()->getSkeleton();
std::uint32_t frameIdx = GameState::GetInstance().Gfx().getCurrentFrameIndex();
const std::size_t jointMatricesStartIndex = animator->uploadJointMatrices(
ctx.renderer.getSkinningPipeline(),
*skeleton, // skeleton stored on GPUMesh (or pass from CPUMesh)
frameIdx
);
ctx.renderer.drawSkinnedMesh(
meshID,
GetTransform().GetWorldMatrix(),
materialID,
m_skinnedMesh.get(),
jointMatricesStartIndex
);
} else {
ctx.renderer.drawMesh(meshID, GetTransform().GetWorldMatrix(), materialID);
}
}

View File

@@ -98,13 +98,13 @@ void OrbitAndSpin::Update()
// GetTransform().SetLocalScale(glm::vec3(std::sin(m_GrowPhase)));
// material color
auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
mat.baseColor = glm::vec3(
0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
);
GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
// auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
// mat.baseColor = glm::vec3(
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
// );
// GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
}
void OrbitAndSpin::Start() {