From 3dc97cafa7277755e988a93a4e24914ab467ebcd Mon Sep 17 00:00:00 2001 From: Antoine Pilote Date: Wed, 6 Sep 2023 17:49:05 -0400 Subject: [PATCH] Animation system --- Nuake/src/Rendering/Renderer.cpp | 2 +- Nuake/src/Resource/ModelLoader.cpp | 64 ++++++++++-- Nuake/src/Resource/SkeletalAnimation.cpp | 104 ++++++++++++++++++ Nuake/src/Resource/SkeletalAnimation.h | 110 +++++++++++++++++--- Nuake/src/Resource/SkinnedModel.cpp | 37 +++++++ Nuake/src/Resource/SkinnedModel.h | 9 ++ Nuake/src/Scene/Scene.cpp | 25 +++++ Nuake/src/Scene/Systems/AnimationSystem.cpp | 89 ++++++++++++++++ Nuake/src/Scene/Systems/AnimationSystem.h | 24 +++++ 9 files changed, 444 insertions(+), 20 deletions(-) create mode 100644 Nuake/src/Resource/SkeletalAnimation.cpp create mode 100644 Nuake/src/Scene/Systems/AnimationSystem.cpp create mode 100644 Nuake/src/Scene/Systems/AnimationSystem.h diff --git a/Nuake/src/Rendering/Renderer.cpp b/Nuake/src/Rendering/Renderer.cpp index da239a74..c6490846 100644 --- a/Nuake/src/Rendering/Renderer.cpp +++ b/Nuake/src/Rendering/Renderer.cpp @@ -53,7 +53,7 @@ namespace Nuake { Vector3(-0.5f, 0.5f, 0.5f), Vector2(1, 1), Vector3(-1, 0, 0) } }; - std::vector CubeIndices + std::vector CubeIndices { 0, 1, 3, 3, 1, 2, 1, 5, 2, 2, 5, 6, diff --git a/Nuake/src/Resource/ModelLoader.cpp b/Nuake/src/Resource/ModelLoader.cpp index 57cfe60d..855c368b 100644 --- a/Nuake/src/Resource/ModelLoader.cpp +++ b/Nuake/src/Resource/ModelLoader.cpp @@ -84,20 +84,70 @@ namespace Nuake ProcessSkinnedNode(scene->mRootNode, scene); if (scene->HasAnimations()) { - SkeletalAnimation animation; + std::vector> animations = std::vector>(); + // Parse animations for (uint32_t i = 0; i < scene->mNumAnimations; i++) { aiAnimation* aiAnim = scene->mAnimations[i]; - const float duration = aiAnim->mDuration; - const float ticksPerSecond = aiAnim->mTicksPerSecond; - // Read data - SkeletonNode rootSkeletonNode; - ProcessAnimationNode(rootSkeletonNode, scene->mRootNode); + const std::string animationName = aiAnim->mName.data; + const float animationDuration = aiAnim->mDuration; + const float animationTicksPerSecond = aiAnim->mTicksPerSecond; + auto animation = CreateRef(animationName, animationDuration, animationTicksPerSecond); - model->SetSkeletonRootNode(rootSkeletonNode); + // Here we iterate over every channel(each channel represents a bone) and fill the SkeletalAnimation + // object with every keyframe. We essentially just convert every assimp vector, quat, string to our own types. + for (uint32_t j = 0; j < aiAnim->mNumChannels; j++) + { + aiNodeAnim* animChannel = aiAnim->mChannels[j]; + const std::string channelName = animChannel->mNodeName.data; // this should be a bone name + + // Create the track + BoneTransformTrack& track = animation->GetTrack(channelName); + + // Create every keyframe in the current channel + // Position + for (uint32_t p = 0; p < animChannel->mNumPositionKeys; p++) + { + aiVectorKey positionKey = animChannel->mPositionKeys[p]; + const float keyTime = positionKey.mTime; + const aiVector3D assimpKeyValue = positionKey.mValue; + const Vector3 keyValue = Vector3(assimpKeyValue.x, assimpKeyValue.y, assimpKeyValue.z); + + track.PushPositionKeyframe(keyTime, keyValue); + } + + // Rotation + for (uint32_t r = 0; r < animChannel->mNumRotationKeys; r++) + { + aiQuatKey rotationKey = animChannel->mRotationKeys[r]; + const float keyTime = rotationKey.mTime; + const aiQuaterniont assimpKeyValue = rotationKey.mValue; + const Quat keyValue = Quat(assimpKeyValue.w, assimpKeyValue.x, assimpKeyValue.y, assimpKeyValue.z); + + track.PushRotationKeyframe(keyTime, keyValue); + } + + // Scaling + for (uint32_t s = 0; s < animChannel->mNumScalingKeys; s++) + { + aiVectorKey scaleKey = animChannel->mScalingKeys[s]; + const float keyTime = scaleKey.mTime; + const aiVector3D assimpKeyValue = scaleKey.mValue; + const Vector3 keyValue = Vector3(assimpKeyValue.x, assimpKeyValue.y, assimpKeyValue.z); + + track.PushScaleKeyframe(keyTime, keyValue); + } + } + + animations.push_back(animation); } + + SkeletonNode rootSkeletonNode; + ProcessAnimationNode(rootSkeletonNode, scene->mRootNode); + model->SetSkeletonRootNode(std::move(rootSkeletonNode)); + model->SetAnimations(std::move(animations)); } for (const auto& mesh : m_SkinnedMeshes) diff --git a/Nuake/src/Resource/SkeletalAnimation.cpp b/Nuake/src/Resource/SkeletalAnimation.cpp new file mode 100644 index 00000000..c2cfd7ee --- /dev/null +++ b/Nuake/src/Resource/SkeletalAnimation.cpp @@ -0,0 +1,104 @@ +#include "SkeletalAnimation.h" + +namespace Nuake +{ + BoneTransformTrack::BoneTransformTrack() + { + m_Positions = std::vector(); + m_Rotations = std::vector(); + m_Scales = std::vector(); + + m_PositionTimestamps = std::vector(); + m_RotationTimestamps = std::vector(); + m_ScaleTimestamps = std::vector(); + } + + float BoneTransformTrack::GetScaleFactor(float lastTime, float nextTime, float animationTime) + { + float scaleFactor = 0.0f; + float midWayLength = animationTime - lastTime; + float framesDiff = nextTime - lastTime; + scaleFactor = midWayLength / framesDiff; + return scaleFactor; + } + + Nuake::Matrix4 BoneTransformTrack::InterpolatePosition(float time) + { + if (m_Positions.size() == 0) + { + return Matrix4(1.0f); + } + + if (m_Positions.size() == 1) + { + return glm::translate(Matrix4(1.0f), m_Positions[0]); + } + + int p0Index = GetPositionIndex(time); + int p1Index = p0Index + 1; + float scaleFactor = GetScaleFactor(m_PositionTimestamps[p0Index], m_PositionTimestamps[p1Index], time); + glm::vec3 finalPosition = glm::mix(m_Positions[p0Index], m_Positions[p1Index], scaleFactor); + return glm::translate(Matrix4(1.0f), finalPosition); + } + + Nuake::Matrix4 BoneTransformTrack::InterpolateRotation(float time) + { + if (m_Rotations.size() == 0) + { + return Matrix4(1.0f); + } + + if (m_Rotations.size() == 1) + { + auto rotation = glm::normalize(m_Rotations[0]); + return glm::toMat4(rotation); + } + + int p0Index = GetRotationIndex(time); + int p1Index = p0Index + 1; + float scaleFactor = GetScaleFactor(m_RotationTimestamps[p0Index], + m_RotationTimestamps[p1Index], time); + glm::quat finalRotation = glm::slerp(m_Rotations[p0Index], + m_Rotations[p1Index], scaleFactor); + finalRotation = glm::normalize(finalRotation); + return glm::toMat4(finalRotation); + } + + Nuake::Matrix4 BoneTransformTrack::InterpolateScale(float time) + { + if (m_Scales.size() == 0) + { + return Matrix4(1.0f); + } + + if (1 == m_Scales.size()) + return glm::scale(glm::mat4(1.0f), m_Scales[0]); + + int p0Index = GetScaleIndex(time); + int p1Index = p0Index + 1; + float scaleFactor = GetScaleFactor(m_ScaleTimestamps[p0Index], + m_ScaleTimestamps[p1Index], time); + glm::vec3 finalScale = glm::mix(m_Scales[p0Index], m_Scales[p1Index], scaleFactor); + return glm::scale(glm::mat4(1.0f), finalScale); + } + + SkeletalAnimation::SkeletalAnimation(const std::string& name, float duration, float ticksPerSecond) + { + m_Name = name; + m_Duration = duration; + m_TicksPerSecond = ticksPerSecond; + m_CurrentTime = 0.0f; + m_Loop = true; + } + + BoneTransformTrack& SkeletalAnimation::GetTrack(const std::string& name) + { + if (m_Tracks.find(name) != m_Tracks.end()) + { + return m_Tracks[name]; + } + + m_Tracks[name] = BoneTransformTrack(); + return m_Tracks[name]; + } +} \ No newline at end of file diff --git a/Nuake/src/Resource/SkeletalAnimation.h b/Nuake/src/Resource/SkeletalAnimation.h index a08a1d2e..42c2e941 100644 --- a/Nuake/src/Resource/SkeletalAnimation.h +++ b/Nuake/src/Resource/SkeletalAnimation.h @@ -12,32 +12,118 @@ namespace Nuake Vector3 Scalings; }; - struct BoneTransformTrack + class BoneTransformTrack { - std::vector positionTimestamps = {}; - std::vector rotationTimestamps = {}; - std::vector scaleTimestamps = {}; + private: + std::vector m_PositionTimestamps = {}; + std::vector m_RotationTimestamps = {}; + std::vector m_ScaleTimestamps = {}; - std::vector positions = {}; - std::vector rotations = {}; - std::vector scales = {}; + std::vector m_Positions = {}; + std::vector m_Rotations = {}; + std::vector m_Scales = {}; + + public: + BoneTransformTrack(); + ~BoneTransformTrack() = default; + + void PushPositionKeyframe(float timestamp, const Vector3& position) + { + m_PositionTimestamps.push_back(timestamp); + m_Positions.push_back(position); + } + + void PushRotationKeyframe(float timestamp, const Quat& rotation) + { + m_RotationTimestamps.push_back(timestamp); + m_Rotations.push_back(rotation); + } + + void PushScaleKeyframe(float timestamp, const Vector3& scale) + { + m_ScaleTimestamps.push_back(timestamp); + m_Scales.push_back(scale); + } + + int GetPositionIndex(float animationTime) + { + if (m_Positions.size() == 0) + { + return 0; + } + + for (int index = 0; index < m_Positions.size() - 1; index++) + { + if (animationTime < m_PositionTimestamps[index + 1]) + { + return index; + } + } + assert(0); + } + + /* Gets the current index on mKeyRotations to interpolate to based on the + current animation time*/ + int GetRotationIndex(float animationTime) + { + for (int index = 0; index < m_Rotations.size() - 1; ++index) + { + if (animationTime < m_RotationTimestamps[index + 1]) + { + return index; + } + } + assert(0); + } + + /* Gets the current index on mKeyScalings to interpolate to based on the + current animation time */ + int GetScaleIndex(float animationTime) + { + for (int index = 0; index < m_Scales.size() - 1; ++index) + { + if (animationTime < m_ScaleTimestamps[index + 1]) + { + return index; + } + } + assert(0); + } + + float GetScaleFactor(float lastTime, float nextTime, float animationTime); + Matrix4 InterpolatePosition(float time); + Matrix4 InterpolateRotation(float time); + Matrix4 InterpolateScale(float time); }; class SkeletalAnimation { private: - float m_Duration; - int m_TicksPerSecond; - std::unordered_map m_Tracks; - std::vector m_Bones; + float m_CurrentTime; + float m_Duration; + float m_TicksPerSecond; + std::string m_Name; + bool m_Loop; public: SkeletalAnimation() = default; + SkeletalAnimation(const std::string& name, float duration, float ticksPerSecond); + ~SkeletalAnimation() = default; - Bone& FindBone(const std::string& boneName); + void SetCurrentTime(float time) + { + m_CurrentTime = fmod(time, m_Duration); + } + float GetCurrentTime() const { return m_CurrentTime; } + void SetDuration(float duration) { m_Duration = duration; } + float GetDuration() const { return m_Duration; } float GetTicksPerSecond() const { return m_TicksPerSecond; } + void SetTicksPerSecond(float ticks) { m_TicksPerSecond = ticks; } + + BoneTransformTrack& GetTrack(const std::string& name); + std::unordered_map& GetTracks() { return m_Tracks; } }; } diff --git a/Nuake/src/Resource/SkinnedModel.cpp b/Nuake/src/Resource/SkinnedModel.cpp index a638fa91..a6bf06d3 100644 --- a/Nuake/src/Resource/SkinnedModel.cpp +++ b/Nuake/src/Resource/SkinnedModel.cpp @@ -27,6 +27,43 @@ namespace Nuake return m_Meshes; } + void SkinnedModel::SetAnimations(const std::vector> animations) + { + m_NumAnimation = static_cast(animations.size()); + m_CurrentAnimation = 0; + m_Animations = animations; + } + + void SkinnedModel::AddAnimation(Ref animation) + { + m_NumAnimation++; + m_Animations.push_back(std::move(animation)); + } + + Ref SkinnedModel::GetCurrentAnimation() + { + if (m_CurrentAnimation < m_NumAnimation) + { + return m_Animations[m_CurrentAnimation]; + } + + Logger::Log("Cannot get animation if no animation exists", "skinned model", WARNING); + return nullptr; + } + + void SkinnedModel::PlayAnimation(uint32_t animationId) + { + if (animationId >= m_NumAnimation) + { + Logger::Log("Cannot play animation, index out of range", "skinned model", CRITICAL); + return; + } + + GetCurrentAnimation()->SetCurrentTime(0.0f); // Reset previous animation + + m_CurrentAnimation = animationId; + } + json SkinnedModel::Serialize() { BEGIN_SERIALIZE(); diff --git a/Nuake/src/Resource/SkinnedModel.h b/Nuake/src/Resource/SkinnedModel.h index 93930cec..99711080 100644 --- a/Nuake/src/Resource/SkinnedModel.h +++ b/Nuake/src/Resource/SkinnedModel.h @@ -15,6 +15,10 @@ namespace Nuake SkeletonNode m_SkeletonRoot; + uint32_t m_CurrentAnimation = 0; + uint32_t m_NumAnimation = 0; + std::vector> m_Animations; + public: SkinnedModel(); SkinnedModel(const std::string path); @@ -29,6 +33,11 @@ namespace Nuake void AddMesh(Ref mesh); std::vector>& GetMeshes(); + void SetAnimations(const std::vector> animations); + void AddAnimation(Ref animation); + Ref GetCurrentAnimation(); + void PlayAnimation(uint32_t animationId); + json Serialize() override; bool Deserialize(const json& j) override; }; diff --git a/Nuake/src/Scene/Scene.cpp b/Nuake/src/Scene/Scene.cpp index 2d888c34..b94d9973 100644 --- a/Nuake/src/Scene/Scene.cpp +++ b/Nuake/src/Scene/Scene.cpp @@ -4,6 +4,7 @@ #include "src/Scene/Systems/TransformSystem.h" #include "src/Scene/Systems/QuakeMapBuilder.h" #include "src/Scene/Systems/ParticleSystem.h" +#include "src/Scene/Systems/AnimationSystem.h" #include "src/Rendering/SceneRenderer.h" #include "Scene.h" @@ -49,6 +50,7 @@ namespace Nuake // Adding systems - Order is important m_Systems.push_back(CreateRef(this)); m_Systems.push_back(CreateRef(this)); + m_Systems.push_back(CreateRef(this)); m_Systems.push_back(CreateRef(this)); m_Systems.push_back(CreateRef(this)); @@ -473,6 +475,17 @@ namespace Nuake skeletonRootEntity.AddComponent(); entity.AddChild(skeletonRootEntity); + Vector3 bonePosition; + Quat boneRotation; + Vector3 boneScale; + Decompose(skeletonRoot.Transform, bonePosition, boneRotation, boneScale); + + auto& transformComponent = skeletonRootEntity.GetComponent(); + transformComponent.SetLocalPosition(bonePosition); + transformComponent.SetLocalRotation(boneRotation); + transformComponent.SetLocalScale(boneScale); + transformComponent.SetLocalTransform(skeletonRoot.Transform); + CreateSkeletonTraverse(skeletonRootEntity, skeletonRoot); } @@ -484,6 +497,18 @@ namespace Nuake boneEntity.AddComponent(); entity.AddChild(boneEntity); + Vector3 bonePosition; + Quat boneRotation; + Vector3 boneScale; + Decompose(c.Transform, bonePosition, boneRotation, boneScale); + + auto& transformComponent = boneEntity.GetComponent(); + transformComponent.SetLocalPosition(bonePosition); + transformComponent.SetLocalRotation(boneRotation); + transformComponent.SetLocalScale(boneScale); + transformComponent.SetLocalTransform(c.Transform); + transformComponent.Dirty = false; + CreateSkeletonTraverse(boneEntity, c); } } diff --git a/Nuake/src/Scene/Systems/AnimationSystem.cpp b/Nuake/src/Scene/Systems/AnimationSystem.cpp new file mode 100644 index 00000000..7c934f70 --- /dev/null +++ b/Nuake/src/Scene/Systems/AnimationSystem.cpp @@ -0,0 +1,89 @@ +#include "AnimationSystem.h" + +#include "src/Scene/Scene.h" +#include "src/Scene/Entities/Entity.h" +#include "src/Scene/Components/SkinnedModelComponent.h" + +namespace Nuake +{ + AnimationSystem::AnimationSystem(Scene* scene) + { + m_Scene = scene; + } + + bool AnimationSystem::Init() + { + return true; + } + + void AnimationSystem::Update(Timestep ts) + { + auto view = m_Scene->m_Registry.view(); + for (auto e : view) + { + auto [transformComponent, skinnedComponent] = view.get(e); + + auto& model = skinnedComponent.ModelResource; + if (!model) + { + continue; + } + + Ref animation = model->GetCurrentAnimation(); + float newAnimationTime = animation->GetCurrentTime() + (ts * animation->GetTicksPerSecond()); + animation->SetCurrentTime(newAnimationTime); + + auto& rootBone = model->GetSkeletonRootNode(); + UpdateBonePositionTraversal(rootBone, animation, animation->GetCurrentTime()); + } + } + + void AnimationSystem::UpdateBonePositionTraversal(SkeletonNode& bone, Ref animation, float time) + { + const std::string& boneName = bone.Name; + + + Entity& boneEntity = m_Scene->GetEntity(boneName); + if (boneEntity.GetHandle() != -1) + { + auto& animationTrack = animation->GetTrack(boneName); + auto& transformComponent = boneEntity.GetComponent(); + + // Get Update transform + const Matrix4 newPosition = animationTrack.InterpolatePosition(time); + const Matrix4 newRotation = animationTrack.InterpolateRotation(time); + const Matrix4 newScale = animationTrack.InterpolateScale(time); + const Matrix4 finalTransform = newPosition * newRotation * newScale; + + Vector3 localPosition; + Quat localRotation; + Vector3 localScale; + Decompose(finalTransform, localPosition, localRotation, localScale); + + transformComponent.SetLocalPosition(localPosition); + transformComponent.SetLocalRotation(localRotation); + transformComponent.SetLocalScale(localScale); + transformComponent.SetLocalTransform(finalTransform); + } + + for (auto& childBone : bone.Children) + { + UpdateBonePositionTraversal(childBone, animation, time); + } + } + + void AnimationSystem::FixedUpdate(Timestep ts) + { + + } + + void AnimationSystem::EditorUpdate() + { + + } + + void AnimationSystem::Exit() + { + + } +} diff --git a/Nuake/src/Scene/Systems/AnimationSystem.h b/Nuake/src/Scene/Systems/AnimationSystem.h new file mode 100644 index 00000000..57361dad --- /dev/null +++ b/Nuake/src/Scene/Systems/AnimationSystem.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include + +#include "src/Resource/SkeletonNode.h" +#include "src/Resource/SkeletalAnimation.h" + +namespace Nuake +{ + class AnimationSystem : public System + { + public: + AnimationSystem(Scene* scene); + bool Init() override; + void Update(Timestep ts) override; + void Draw() override {} + void EditorUpdate() override; + void FixedUpdate(Timestep ts) override; + void Exit() override; + + private: + void UpdateBonePositionTraversal(SkeletonNode& bone, Ref animation, float time); + }; +}