diff --git a/Volt/Sandbox/src/Sandbox/Window/AssetBrowser/AssetBrowserPanel.cpp b/Volt/Sandbox/src/Sandbox/Window/AssetBrowser/AssetBrowserPanel.cpp index 7101c3b77..37f34772d 100644 --- a/Volt/Sandbox/src/Sandbox/Window/AssetBrowser/AssetBrowserPanel.cpp +++ b/Volt/Sandbox/src/Sandbox/Window/AssetBrowser/AssetBrowserPanel.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include @@ -316,6 +317,7 @@ void AssetBrowserPanel::UpdateMainContent() CreateNewShaderModal(); CreateNewMonoScriptModal(); + CreateNewMotionWeaveGraphModal(); DeleteFilesModal(); } @@ -926,20 +928,29 @@ void AssetBrowserPanel::RenderWindowRightClickPopup() if (ImGui::BeginMenu("Animation##Menu")) { - if (ImGui::MenuItem("Animated Character")) + if (ImGui::MenuItem("Motion Weave Graph")) { - CreateNewAssetInCurrentDirectory(Volt::AssetType::AnimatedCharacter); + CreateNewAssetInCurrentDirectory(Volt::AssetType::MotionWeave); } - if (ImGui::MenuItem("Animation Graph")) + if (ImGui::BeginMenu("Legacy##Menu")) { - CreateNewAssetInCurrentDirectory(Volt::AssetType::AnimationGraph); - } + if (ImGui::MenuItem("Animated Character")) + { + CreateNewAssetInCurrentDirectory(Volt::AssetType::AnimatedCharacter); + } - if (ImGui::MenuItem("Blend Space")) - { - CreateNewAssetInCurrentDirectory(Volt::AssetType::BlendSpace); + if (ImGui::MenuItem("Animation Graph")) + { + CreateNewAssetInCurrentDirectory(Volt::AssetType::AnimationGraph); + } + + if (ImGui::MenuItem("Blend Space")) + { + CreateNewAssetInCurrentDirectory(Volt::AssetType::BlendSpace); + } } + ImGui::EndMenu(); } @@ -1316,6 +1327,7 @@ void AssetBrowserPanel::CreateNewAssetInCurrentDirectory(Volt::AssetType type) case Volt::AssetType::MonoScript: originalName = "idk.cs"; break; case Volt::AssetType::PostProcessingStack: originalName = "PPS_NewPostStack"; break; case Volt::AssetType::PostProcessingMaterial: originalName = "PPM_NewPostMaterial"; break; + case Volt::AssetType::MotionWeave: originalName = "MW_NewMotionWeave"; break; } tempName = originalName; @@ -1422,6 +1434,13 @@ void AssetBrowserPanel::CreateNewAssetInCurrentDirectory(Volt::AssetType type) newAssetHandle = postStack->handle; break; } + + case Volt::AssetType::MotionWeave: + { + UI::OpenModal("New Motion Weave Graph##assetBrowser"); + m_MotionWeaveTargetSkeleton = 0; + break; + } } Reload(); @@ -1691,3 +1710,54 @@ void AssetBrowserPanel::CreateNewMonoScriptModal() UI::EndModal(); } } + +void AssetBrowserPanel::CreateNewMotionWeaveGraphModal() +{ + if (UI::BeginModal("New Motion Weave Graph##assetBrowser")) + { + static std::string name; + + if (UI::BeginProperties("MotionWeaveProperties")) + { + UI::Property("Name", name); + EditorUtils::Property("Target Skeleton", m_MotionWeaveTargetSkeleton, Volt::AssetType::Skeleton); + + UI::EndProperties(); + } + + const bool cantCreate = m_MotionWeaveTargetSkeleton == 0; + ImGui::BeginDisabled(cantCreate); + if (ImGui::Button("Create")) + { + const std::string extension = Volt::AssetManager::GetExtensionFromAssetType(Volt::AssetType::MotionWeave); + Ref motionWeave = Volt::AssetManager::CreateAsset( + Volt::AssetManager::GetRelativePath(myCurrentDirectory->path), name + extension, m_MotionWeaveTargetSkeleton); + Volt::AssetManager::SaveAsset(motionWeave); + + ImGui::CloseCurrentPopup(); + } + ImGui::EndDisabled(); + if (cantCreate) + { + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + { + ImGui::BeginTooltip(); + if (m_MotionWeaveTargetSkeleton == 0) + { + ImGui::Text("Need to select a skeleton to create"); + } + ImGui::EndTooltip(); + } + + } + ImGui::SameLine(); + + if (ImGui::Button("Cancel")) + { + name = ""; + ImGui::CloseCurrentPopup(); + } + + UI::EndModal(); + } +} diff --git a/Volt/Sandbox/src/Sandbox/Window/AssetBrowser/AssetBrowserPanel.h b/Volt/Sandbox/src/Sandbox/Window/AssetBrowser/AssetBrowserPanel.h index 58361aa4b..3b51b5f35 100644 --- a/Volt/Sandbox/src/Sandbox/Window/AssetBrowser/AssetBrowserPanel.h +++ b/Volt/Sandbox/src/Sandbox/Window/AssetBrowser/AssetBrowserPanel.h @@ -70,6 +70,7 @@ class AssetBrowserPanel : public EditorWindow void CreateNewAssetInCurrentDirectory(Volt::AssetType type); void CreateNewShaderModal(); void CreateNewMonoScriptModal(); + void CreateNewMotionWeaveGraphModal(); struct NewShaderData { @@ -119,6 +120,9 @@ class AssetBrowserPanel : public EditorWindow ///// Animation Graph creation ///// NewAnimationGraphData myNewAnimationGraphData{}; + ///// Motion Weave creation ///// + Volt::AssetHandle m_MotionWeaveTargetSkeleton = 0; + Ref ProcessDirectory(const std::filesystem::path& path, AssetBrowser::DirectoryItem* parent); std::unordered_map > myDirectories; Ref mySelectionManager; diff --git a/Volt/Volt/src/Volt/Animation/MotionWeaver.cpp b/Volt/Volt/src/Volt/Animation/MotionWeaver.cpp new file mode 100644 index 000000000..2ebe65d6c --- /dev/null +++ b/Volt/Volt/src/Volt/Animation/MotionWeaver.cpp @@ -0,0 +1,86 @@ +#include "vtpch.h" +#include "MotionWeaver.h" +#include "Volt/Log/Log.h" + +#include "Volt/Animation/AnimationManager.h" + +#include "Volt/Asset/Animation/Animation.h" +#include "Volt/Asset/Animation/Skeleton.h" +#include "Volt/Asset/AssetManager.h" + +namespace Volt +{ + + MotionWeaver::MotionWeaver(Ref motionWeaveAsset, Ref skeleton) + : m_MotionWeaveAsset(motionWeaveAsset), m_Skeleton(skeleton) + { + assert(m_MotionWeaveAsset); + assert(m_Skeleton); + } + + MotionWeaver::~MotionWeaver() + { + } + + void MotionWeaver::Update(float deltaTime) + { + if (m_MotionWeaveAsset->GetMotionWeaveAssetEntries().empty()) + { + return; + } + + if (m_Entries.empty()) + { + MotionWeaveAssetEntry& entry = m_MotionWeaveAsset->GetMotionWeaveAssetEntries()[0]; + + MotionWeaveEntry newEntry; + newEntry.animation = AssetManager::GetAsset(entry.animation); + newEntry.speed = 1; + newEntry.weight = 1; + newEntry.looping = true; + newEntry.startTime = 0; + + m_Entries.push_back(newEntry); + } + } + + const std::vector MotionWeaver::Sample() + { + if (m_Entries.empty()) + { + return std::vector(); + } + + auto targetEntry = m_Entries.front(); + const Animation::Pose sample = targetEntry.animation->SamplePose(targetEntry.startTime, AnimationManager::globalClock, m_Skeleton, targetEntry.looping, targetEntry.speed); + const auto& invBindPose = m_Skeleton->GetInverseBindPose(); + + if(sample.localTRS.size() != invBindPose.size()) + { + VT_CORE_ERROR("Sampled pose size does not match inverse bind pose size"); + return std::vector(); + } + if (sample.localTRS.empty()) + { + VT_CORE_ERROR("Sampled pose is empty"); + return std::vector(); + } + + std::vector result{}; + result.resize(sample.localTRS.size()); + + glm::vec3 rootMotion = sample.localTRS[0].position - m_PrevRootPosition; + m_PrevRootPosition = sample.localTRS[0].position; + + for (size_t i = 0; i < sample.localTRS.size(); i++) + { + const auto& trs = sample.localTRS.at(i); + + const glm::mat4 transform = glm::translate(glm::mat4{ 1.f }, trs.position)* glm::mat4_cast(trs.rotation)* glm::scale(glm::mat4{ 1.f }, trs.scale); + result[i] = transform * invBindPose[i]; + } + + return result; + } + +} diff --git a/Volt/Volt/src/Volt/Animation/MotionWeaver.h b/Volt/Volt/src/Volt/Animation/MotionWeaver.h new file mode 100644 index 000000000..4f333eb74 --- /dev/null +++ b/Volt/Volt/src/Volt/Animation/MotionWeaver.h @@ -0,0 +1,43 @@ +#pragma once + +#include "Volt/Asset/Animation/MotionWeaveAsset.h" + +#include + +//this class will be used to control the animation of a character +//It works by having a main animation and a list of all the previous animations, the main animation has a weight that increases to 1 over time, the previous animations have a weight that decreases to 0 over time +//when the weight of a previous animation reaches 0, it is removed from the list +namespace Volt +{ + class Animation; + class Skeleton; + + struct MotionWeaveEntry + { + Ref animation; + float weight = 0.f; + float speed = 1.f; + float startTime = 0.f; + bool looping = true; + }; + class MotionWeaver + { + public: + MotionWeaver(Ref motionWeaveAsset, Ref skeleton); + ~MotionWeaver(); + + void Update(float deltaTime); + + const std::vector Sample(); + + private: + Ref m_MotionWeaveAsset; + Ref m_Skeleton; + + glm::vec3 m_PrevRootPosition; + + std::vector m_Entries; + + }; + +} diff --git a/Volt/Volt/src/Volt/Asset/Animation/Animation.cpp b/Volt/Volt/src/Volt/Asset/Animation/Animation.cpp index f0677d05b..6e6239ed4 100644 --- a/Volt/Volt/src/Volt/Asset/Animation/Animation.cpp +++ b/Volt/Volt/src/Volt/Asset/Animation/Animation.cpp @@ -200,6 +200,50 @@ namespace Volt return result; } + const Animation::Pose Animation::SamplePose(float aStartTime, float aCurrentTime, Ref aSkeleton, bool looping, float speed) const + { + VT_PROFILE_FUNCTION(); + + const float finalDuration = myDuration / speed; + + const float localTime = aCurrentTime - aStartTime; + const float normalizedTime = localTime / finalDuration; + + const int32_t frameCount = (int32_t)myFrames.size(); + int32_t currentFrameIndex = (int32_t)(std::floor(normalizedTime * (float)frameCount)) % frameCount; + + if (normalizedTime > 1.f && !looping) + { + currentFrameIndex = frameCount - 1; + } + + currentFrameIndex = std::clamp(currentFrameIndex, 0, frameCount - 1); + + const float blendValue = (fmodf(normalizedTime, 1.f) * ((float)frameCount)) - (float)currentFrameIndex; + + int32_t nextFrameIndex = currentFrameIndex + 1; + + if (nextFrameIndex >= frameCount) + { + if (looping) + { + nextFrameIndex = 0; + } + else + { + nextFrameIndex = currentFrameIndex; + } + } + + const Pose& currentFrame = myFrames.at(currentFrameIndex); + const Pose& nextFrame = myFrames.at(nextFrameIndex); + + Pose result = currentFrame; + result.BlendWith(nextFrame, blendValue); + + return result; + } + const bool Animation::IsAtEnd(float startTime, float speed) { const float localTime = AnimationManager::globalClock - startTime; @@ -293,5 +337,31 @@ namespace Volt return animData; } + + void Animation::Pose::BlendWith(const Pose& other, float weight) + { + assert(localTRS.size() == other.localTRS.size()); + for (size_t i = 0; i < localTRS.size(); i++) + { + // Blend + localTRS[i].position = glm::mix(localTRS[i].position, other.localTRS[i].position, weight); + localTRS[i].rotation = glm::slerp(glm::normalize(localTRS[i].rotation), glm::normalize(other.localTRS[i].rotation), weight); + localTRS[i].scale = glm::mix(localTRS[i].scale, other.localTRS[i].scale, weight); + } + } + std::vector Animation::Pose::GetGlobalTransforms(const std::vector& invBindPose) const + { + std::vector result{}; + result.resize(localTRS.size()); + + for (size_t i = 0; i < localTRS.size(); i++) + { + const Animation::TRS& trs = localTRS.at(i); + + const glm::mat4 transform = glm::translate(glm::mat4{ 1.f }, trs.position)* glm::mat4_cast(trs.rotation)* glm::scale(glm::mat4{ 1.f }, trs.scale); + result[i] = transform * invBindPose[i]; + } + return result; + } } diff --git a/Volt/Volt/src/Volt/Asset/Animation/Animation.h b/Volt/Volt/src/Volt/Asset/Animation/Animation.h index 0f4d12a97..1a626d49f 100644 --- a/Volt/Volt/src/Volt/Asset/Animation/Animation.h +++ b/Volt/Volt/src/Volt/Asset/Animation/Animation.h @@ -20,12 +20,18 @@ namespace Volt struct Pose { std::vector localTRS; + + void BlendWith(const Pose& other, float weight); + std::vector GetGlobalTransforms(const std::vector& invBindPose) const; }; const std::vector Sample(float aStartTime, Ref aSkeleton, bool looping); const std::vector Sample(uint32_t frameIndex, Ref aSkeleton); const std::vector SampleTRS(float aStartTime, Ref aSkeleton, bool looping, float speed = 1.f) const; + + const Pose SamplePose(float aStartTime,float aCurrentTime, Ref aSkeleton, bool looping, float speed = 1.f) const; + const bool IsAtEnd(float startTime, float speed); const bool HasPassedTime(float startTime, float speed, float time); diff --git a/Volt/Volt/src/Volt/Asset/Animation/MotionWeaveAsset.cpp b/Volt/Volt/src/Volt/Asset/Animation/MotionWeaveAsset.cpp new file mode 100644 index 000000000..a8bab803d --- /dev/null +++ b/Volt/Volt/src/Volt/Asset/Animation/MotionWeaveAsset.cpp @@ -0,0 +1,14 @@ +#include "vtpch.h" +#include "MotionWeaveAsset.h" + +namespace Volt +{ + MotionWeaveAsset::MotionWeaveAsset(Volt::AssetHandle targetSkeletonHandle) + :m_TargetSkeletonHandle(targetSkeletonHandle) + { + } + std::vector Volt::MotionWeaveAsset::GetMotionWeaveAssetEntries() + { + return m_MotionWeaveAssetEntries; + } +} diff --git a/Volt/Volt/src/Volt/Asset/Animation/MotionWeaveAsset.h b/Volt/Volt/src/Volt/Asset/Animation/MotionWeaveAsset.h new file mode 100644 index 000000000..863c0e7b4 --- /dev/null +++ b/Volt/Volt/src/Volt/Asset/Animation/MotionWeaveAsset.h @@ -0,0 +1,28 @@ +#pragma once +#include "Volt/Asset/Asset.h" + +namespace Volt +{ + struct MotionWeaveAssetEntry + { + Volt::AssetHandle animation; + }; + + class MotionWeaveAsset : public Asset + { + public: + MotionWeaveAsset() = default; + MotionWeaveAsset(Volt::AssetHandle targetSkeletonHandle); + static AssetType GetStaticType() { return AssetType::MotionWeave; } + AssetType GetType() override { return GetStaticType(); }; + + std::vector GetMotionWeaveAssetEntries(); + + private: + friend class MotionWeaveImporter; + Volt::AssetHandle m_TargetSkeletonHandle = Volt::Asset::Null(); + + std::vector m_MotionWeaveAssetEntries; + + }; +} diff --git a/Volt/Volt/src/Volt/Asset/Asset.h b/Volt/Volt/src/Volt/Asset/Asset.h index 3d63c8708..ca0cc0ee1 100644 --- a/Volt/Volt/src/Volt/Asset/Asset.h +++ b/Volt/Volt/src/Volt/Asset/Asset.h @@ -82,6 +82,7 @@ namespace Volt { ".vtanim", AssetType::Animation }, { ".vtchr", AssetType::AnimatedCharacter }, { ".vtanimgraph", AssetType::AnimationGraph }, + { ".vtweave", AssetType::MotionWeave }, { ".png", AssetType::Texture }, { ".jpg", AssetType::Texture }, diff --git a/Volt/Volt/src/Volt/Asset/AssetManager.cpp b/Volt/Volt/src/Volt/Asset/AssetManager.cpp index f168eab48..ea402c4b0 100644 --- a/Volt/Volt/src/Volt/Asset/AssetManager.cpp +++ b/Volt/Volt/src/Volt/Asset/AssetManager.cpp @@ -19,6 +19,7 @@ #include "Volt/Asset/Importers/BehaviorTreeImporter.h" #include "Volt/Asset/Importers/NetContractImporter.h" #include "Volt/Asset/Importers/ParticlePresetImporter.h" +#include "Volt/Asset/Importers/MotionWeaveImporter.h" #include "Volt/Platform/ThreadUtility.h" @@ -68,6 +69,7 @@ namespace Volt m_assetImporters.emplace(AssetType::PostProcessingStack, CreateScope()); m_assetImporters.emplace(AssetType::PostProcessingMaterial, CreateScope()); m_assetImporters.emplace(AssetType::NetContract, CreateScope()); + m_assetImporters.emplace(AssetType::MotionWeave, CreateScope()); LoadAssetMetafiles(); } diff --git a/Volt/Volt/src/Volt/Asset/Importers/MotionWeaveImporter.cpp b/Volt/Volt/src/Volt/Asset/Importers/MotionWeaveImporter.cpp new file mode 100644 index 000000000..47a2457a1 --- /dev/null +++ b/Volt/Volt/src/Volt/Asset/Importers/MotionWeaveImporter.cpp @@ -0,0 +1,59 @@ +#include "vtpch.h" +#include "MotionWeaveImporter.h" +#include "Volt/Asset/AssetManager.h" +#include "Volt/Asset/Animation/MotionWeaveAsset.h" + +#include +#include "Volt/Utility/SerializationMacros.h" +#include "Volt/Utility/YAMLSerializationHelpers.h" + +bool Volt::MotionWeaveImporter::Load(const AssetMetadata& metadata, Ref& asset) const +{ + const auto filePath = AssetManager::GetFilesystemPath(metadata.filePath); + + if (!std::filesystem::exists(filePath)) + { + VT_CORE_ERROR("File {0} not found!", metadata.filePath); + asset->SetFlag(AssetFlag::Missing, true); + return false; + } + + std::ifstream file(filePath); + if (!file.is_open()) + { + VT_CORE_ERROR("Failed to open file: {0}!", metadata.filePath); + asset->SetFlag(AssetFlag::Invalid, true); + return false; + } + asset = CreateRef(); + auto rAsset = reinterpret_pointer_cast(asset); + + std::stringstream sstream; + sstream << file.rdbuf(); + + YAML::Node root = YAML::Load(sstream.str()); + + rAsset->m_TargetSkeletonHandle = root["TargetSkeleton"].as(AssetHandle(0)); + + if (AssetManager::GetMetadataFromHandle(rAsset->m_TargetSkeletonHandle).IsValid() == false) + { + VT_CORE_ERROR("Target skeleton handle is not valid: {0}!", rAsset->m_TargetSkeletonHandle); + asset->SetFlag(AssetFlag::Invalid, true); + return false; + } + + return true; +} + +void Volt::MotionWeaveImporter::Save(const AssetMetadata& metadata, const Ref& asset) const +{ + Ref motionWeaveAsset = std::reinterpret_pointer_cast(asset); + YAML::Emitter out; + + out << YAML::BeginMap; + out << YAML::Key << "TargetSkeleton" << YAML::Value << motionWeaveAsset->m_TargetSkeletonHandle; + out << YAML::EndMap; + std::ofstream fout(AssetManager::GetFilesystemPath(metadata.filePath)); + fout << out.c_str(); + fout.close(); +} diff --git a/Volt/Volt/src/Volt/Asset/Importers/MotionWeaveImporter.h b/Volt/Volt/src/Volt/Asset/Importers/MotionWeaveImporter.h new file mode 100644 index 000000000..2c08c527a --- /dev/null +++ b/Volt/Volt/src/Volt/Asset/Importers/MotionWeaveImporter.h @@ -0,0 +1,11 @@ +#pragma once +#include + +namespace Volt +{ + class MotionWeaveImporter : public AssetImporter + { + bool Load(const AssetMetadata& metadata, Ref& asset) const override; + void Save(const AssetMetadata& metadata, const Ref& asset) const override; + }; +} diff --git a/Volt/Volt/src/Volt/Components/RenderingComponents.h b/Volt/Volt/src/Volt/Components/RenderingComponents.h index c030cb68f..5a1e724e0 100644 --- a/Volt/Volt/src/Volt/Components/RenderingComponents.h +++ b/Volt/Volt/src/Volt/Components/RenderingComponents.h @@ -10,6 +10,7 @@ namespace Volt { class Camera; class AnimationController; + class MotionWeaver; struct MeshComponent { @@ -149,6 +150,7 @@ namespace Volt { AssetHandle motionWeave = Asset::Null(); + Ref weaver; static void ReflectType(TypeDesc& reflect) { reflect.SetGUID("{5D3B2C0D-5457-43D8-9623-98730E1556F4}"_guid);