diff --git a/CMakeLists.txt b/CMakeLists.txt index 63fb90c..fa5d6bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,9 @@ add_library(AnimTestbedCode OBJECT src/AnimGraph/AnimGraphResource.cc src/AnimGraph/AnimGraphResource.h src/AnimGraph/SyncTrack.cc - src/AnimGraph/SyncTrack.h) + src/AnimGraph/SyncTrack.h + src/AnimGraph/AnimLibrary.h +) target_include_directories( AnimTestbedCode @@ -150,6 +152,9 @@ target_sources(runtests PRIVATE tests/AnimGraphEvalTests.cc tests/NodeDescriptorTests.cc tests/SyncTrackTests.cc + tests/AnimDataTests.cc + tests/TestAnimData.cc + tests/TestAnimData.h tests/main.cc ${ozz_offline_test_objs} ) diff --git a/src/AnimGraph/AnimGraphData.h b/src/AnimGraph/AnimGraphData.h index 6a82d87..af37321 100644 --- a/src/AnimGraph/AnimGraphData.h +++ b/src/AnimGraph/AnimGraphData.h @@ -34,6 +34,8 @@ struct AnimDataRef { struct AnimationResource { std::string m_name; + std::string m_filename; + ozz::animation::Animation* m_animation; SyncTrack m_sync_track; }; @@ -42,8 +44,9 @@ inline void to_json( nlohmann::json& j, const AnimationResource& animation_resource) { j["type"] = "AnimationResource"; - j["synctrack"] = animation_resource.m_sync_track; j["name"] = animation_resource.m_name; + j["filename"] = animation_resource.m_filename; + j["synctrack"] = animation_resource.m_sync_track; } inline void from_json( @@ -51,8 +54,9 @@ inline void from_json( AnimationResource& animation_resource) { assert(j["type"] == "AnimationResource"); - animation_resource.m_sync_track = j["synctrack"]; animation_resource.m_name = j["name"]; + animation_resource.m_filename = j["filename"]; + animation_resource.m_sync_track = j["synctrack"]; } struct AnimDataAllocator { diff --git a/src/AnimGraph/AnimGraphNodes.cc b/src/AnimGraph/AnimGraphNodes.cc index e9672ba..390a2ef 100644 --- a/src/AnimGraph/AnimGraphNodes.cc +++ b/src/AnimGraph/AnimGraphNodes.cc @@ -152,6 +152,7 @@ bool AnimSamplerNode::Init(AnimGraphContext& context) { archive >> *m_animation; context.m_animation_map[m_filename] = { + m_filename, m_filename, m_animation, SyncTrack()}; diff --git a/src/AnimGraph/AnimLibrary.h b/src/AnimGraph/AnimLibrary.h new file mode 100644 index 0000000..bef2e34 --- /dev/null +++ b/src/AnimGraph/AnimLibrary.h @@ -0,0 +1,133 @@ +// +// Created by martin on 11.04.25. +// + +#ifndef ANIMATIONLIBRARY_H +#define ANIMATIONLIBRARY_H + +#include + +#include "AnimGraph/AnimGraphData.h" +#include "ozz/base/io/archive.h" +#include "ozz/base/io/stream.h" +#include "ozz/base/log.h" + +struct AnimLibrary { + typedef std::map AnimationResourceMap; + AnimationResourceMap mAnimations = {}; + std::vector mManagedAnimations = {}; + + AnimLibrary() = default; + ~AnimLibrary() { Reset(); } + + // Move operators. Needed to avoid duplicate frees on the managed animations. + AnimLibrary(AnimLibrary&& other) + : mAnimations(std::move(other.mAnimations)), + mManagedAnimations(std::move(other.mManagedAnimations)) {}; + AnimLibrary& operator=(AnimLibrary&& other) { + mAnimations = other.mAnimations; + mManagedAnimations = std::move(other.mManagedAnimations); + return *this; + } + + bool AddAnimation( + const std::string& name, + ozz::animation::Animation* animation) { + AnimationResource animation_resource; + animation_resource.m_name = name; + animation_resource.m_animation = animation; + mAnimations[name] = animation_resource; + + return true; + } + + bool AddAnimationFile(const std::string& name, const std::string& filename) { + if (mAnimations.find(name) != mAnimations.end()) { + std::cerr << "Cannot add animation '" << name + << "' to library. Animation already exists." << std::endl; + return false; + } + + ozz::animation::Animation* animation = new ozz::animation::Animation; + + assert(!filename.empty()); + ozz::io::File file(filename.c_str(), "rb"); + if (!file.opened()) { + ozz::log::Err() << "Failed to open animation file " << filename << "." + << std::endl; + return false; + } + ozz::io::IArchive archive(&file); + if (!archive.TestTag()) { + ozz::log::Err() << "Failed to load animation instance from file " + << filename << "." << std::endl; + return false; + } + + // Once the tag is validated, reading cannot fail. + archive >> *animation; + + mManagedAnimations.push_back(animation); + + AnimationResource animation_resource; + animation_resource.m_name = name; + animation_resource.m_animation = animation; + animation_resource.m_filename = filename; + + mAnimations[name] = animation_resource; + + return true; + } + + void Reset() { + for (ozz::animation::Animation* animation : mManagedAnimations) { + assert(animation->num_tracks() < 5); + delete animation; + } + + mManagedAnimations.clear(); + mAnimations.clear(); + } +}; + +inline void to_json(nlohmann::json& j, const AnimLibrary& animation_library) { + j["type"] = "AnimationLibrary"; + + for (AnimLibrary::AnimationResourceMap::const_iterator iter = + animation_library.mAnimations.cbegin(); + iter != animation_library.mAnimations.cend(); + ++iter) { + j["animations"][iter->first] = iter->second; + } +} + +inline void from_json(const nlohmann::json& j, AnimLibrary& animation_library) { + animation_library.Reset(); + + if (!j.contains("type") || j["type"] != "AnimationLibrary") { + std::cerr << "Invalid type. Expected 'AnimationLibrary'." << std::endl; + } + + if (!j.contains("animations")) { + std::cerr << "Invalid AnimationLibrary. Expected 'animations' key." + << std::endl; + } + + for (nlohmann::json::const_iterator iter = j["animations"].begin(); + iter != j["animations"].cend(); + ++iter) { + AnimationResource animation_resource = *iter; + + if (!animation_resource.m_filename.empty()) { + animation_library.AddAnimationFile( + iter.key(), + j["animations"][iter.key()]["filename"]); + animation_library.mAnimations[iter.key()].m_sync_track = + animation_resource.m_sync_track; + } else { + animation_library.mAnimations[iter.key()] = *iter; + } + } +} + +#endif //ANIMATIONLIBRARY_H diff --git a/tests/AnimDataTests.cc b/tests/AnimDataTests.cc new file mode 100644 index 0000000..d323fbd --- /dev/null +++ b/tests/AnimDataTests.cc @@ -0,0 +1,41 @@ +// +// Created by martin on 11.04.25. +// + +#include "3rdparty/json/json.hpp" +#include "AnimGraph/AnimLibrary.h" +#include "TestAnimData.h" +#include "catch.hpp" + +using namespace nlohmann; + +TEST_CASE("Serialize AnimLibrary", "[AnimLibrary]") { + AnimLibrary library; + + TestAnimData::SingleBoneSkeleton single_bone_testdata; + + REQUIRE(library.AddAnimationFile( + "translation_x", + single_bone_testdata.animation_translate_x_resource.m_filename)); + REQUIRE(library.AddAnimationFile( + "translation_y", + single_bone_testdata.animation_translate_y_resource.m_filename)); + + // serialize + json library_data; + library_data = library; + + // deserialize + AnimLibrary library_deserialized; + library_deserialized = library_data; + + CHECK(library_deserialized.mAnimations.size() == library.mAnimations.size()); + for (AnimLibrary::AnimationResourceMap::const_iterator iter = + library.mAnimations.cbegin(); + iter != library.mAnimations.cend(); + ++iter) { + CHECK( + library_deserialized.mAnimations.find(iter->first) + != library_deserialized.mAnimations.end()); + } +} \ No newline at end of file diff --git a/tests/AnimGraphEvalTests.cc b/tests/AnimGraphEvalTests.cc index 1c8383b..f1771de 100644 --- a/tests/AnimGraphEvalTests.cc +++ b/tests/AnimGraphEvalTests.cc @@ -200,10 +200,12 @@ TEST_CASE_METHOD( graph_context.m_skeleton = skeleton.get(); graph_context.m_animation_map["trans_x"] = { "trans_x", + "", animation_translate_x.get(), animation_translate_x_sync_track}; graph_context.m_animation_map["trans_y"] = { "trans_y", + "", animation_translate_y.get(), animation_translate_y_sync_track}; diff --git a/tests/TestAnimData.cc b/tests/TestAnimData.cc new file mode 100644 index 0000000..199eea4 --- /dev/null +++ b/tests/TestAnimData.cc @@ -0,0 +1,117 @@ +// +// Created by martin on 11.04.25. +// + +#include "TestAnimData.h" + +#include + +#include "ozz/animation/offline/animation_builder.h" +#include "ozz/animation/offline/raw_animation.h" +#include "ozz/animation/offline/raw_skeleton.h" +#include "ozz/base/io/archive.h" +#include "ozz/base/log.h" + +namespace TestAnimData { + +SingleBoneSkeleton::SingleBoneSkeleton() { + using namespace ozz::animation::offline; + + RawSkeleton raw_skeleton; + RawSkeleton::Joint raw_joint; + + raw_joint.name = "Bone0"; + raw_joint.transform.translation.x = 1.f; + raw_joint.transform.translation.y = 2.f; + raw_joint.transform.translation.z = 3.f; + + raw_skeleton.roots.push_back(raw_joint); + + SkeletonBuilder skeleton_builder; + skeleton = skeleton_builder(raw_skeleton); + + // SingleBoneSkeleton Animations + ozz::animation::offline::RawAnimation raw_animation_translation_x; + raw_animation_translation_x.name = "TranslationX"; + RawAnimation::JointTrack bone0_track; + RawAnimation::JointTrack::Translations bone0_translations; + + // animation_translate_x + RawAnimation::TranslationKey translation_key; + translation_key.time = 0.f; + translation_key.value = ozz::math::Float3(0.f, 0.f, 0.f); + bone0_translations.push_back(translation_key); + + translation_key.time = 1.f; + translation_key.value = ozz::math::Float3(1.f, 0.f, 0.f); + bone0_translations.push_back(translation_key); + + bone0_track.translations = bone0_translations; + raw_animation_translation_x.tracks.push_back(bone0_track); + raw_animation_translation_x.duration = 1.f; + if (!raw_animation_translation_x.Validate()) { + std::cerr << "Error: could animation raw data invalid!" << std::endl; + } + + AnimationBuilder animation_builder; + animation_translate_x = animation_builder(raw_animation_translation_x); + + animation_translate_x_resource.m_animation = animation_translate_x.get(); + SaveAnimation("single_bone_translation_z.ozz", animation_translate_x.get()); + animation_translate_x_resource.m_name = "single_bone_translation_z"; + animation_translate_x_resource.m_filename = "single_bone_translation_z.ozz"; + + // animation_translate_y + ozz::animation::offline::RawAnimation raw_animation_translation_y; + raw_animation_translation_y.name = "TranslationY"; + bone0_translations.clear(); + + translation_key.time = 0.f; + translation_key.value = ozz::math::Float3(0.f, 0.f, 0.f); + bone0_translations.push_back(translation_key); + + translation_key.time = 1.f; + translation_key.value = ozz::math::Float3(0.f, 1.f, 0.f); + bone0_translations.push_back(translation_key); + + bone0_track.translations = bone0_translations; + raw_animation_translation_y.tracks.push_back(bone0_track); + raw_animation_translation_y.duration = 1.f; + if (!raw_animation_translation_y.Validate()) { + std::cerr << "Error: could animation raw data invalid!" << std::endl; + } + + animation_translate_y = animation_builder(raw_animation_translation_y); + + animation_translate_y_resource.m_animation = animation_translate_y.get(); + SaveAnimation("single_bone_translation_y.ozz", animation_translate_y.get()); + animation_translate_y_resource.m_name = "single_bone_translation_y"; + animation_translate_y_resource.m_filename = "single_bone_translation_y.ozz"; +} + +bool SingleBoneSkeleton::SaveSkeleton( + const char* filename, + ozz::animation::Skeleton* skeleton) { + assert(false); + return false; +} + +bool SingleBoneSkeleton::SaveAnimation( + const char* filename, + ozz::animation::Animation* animation) { + ozz::io::File file(filename, "wb"); + if (!file.opened()) { + ozz::log::Err() << "Failed to create animation file " << filename << "." + << std::endl; + delete animation; + return false; + } + + ozz::io::OArchive archive(&file); + + archive << *animation; + + return true; +} + +} // namespace TestAnimData diff --git a/tests/TestAnimData.h b/tests/TestAnimData.h new file mode 100644 index 0000000..eac8254 --- /dev/null +++ b/tests/TestAnimData.h @@ -0,0 +1,37 @@ +// +// Created by martin on 11.04.25. +// + +#ifndef TESTANIMDATA_H +#define TESTANIMDATA_H + +#include "AnimGraph/AnimGraphData.h" +#include "AnimGraph/SyncTrack.h" +#include "ozz/animation/offline/skeleton_builder.h" +#include "ozz/animation/runtime/animation.h" +#include "ozz/animation/runtime/skeleton.h" + +namespace TestAnimData { + +struct SingleBoneSkeleton { + SingleBoneSkeleton(); + + ozz::unique_ptr skeleton = nullptr; + + ozz::unique_ptr animation_translate_x = nullptr; + AnimationResource animation_translate_x_resource; + SyncTrack animation_translate_x_sync_track = {}; + + ozz::unique_ptr animation_translate_y = nullptr; + AnimationResource animation_translate_y_resource; + SyncTrack animation_translate_y_sync_track = {}; + + bool SaveSkeleton(const char* filename, ozz::animation::Skeleton* skeleton); + bool SaveAnimation( + const char* filename, + ozz::animation::Animation* animation); +}; + +} // namespace TestAnimData + +#endif //TESTANIMDATA_H