From 43e9fb330f13658104b423d83b1bbc9cb0f975f9 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Sun, 19 Apr 2026 19:24:55 +0300 Subject: [PATCH] Character Animation --- src/features/editScene/CMakeLists.txt | 6 + src/features/editScene/EditorApp.cpp | 16 + src/features/editScene/EditorApp.hpp | 2 + .../editScene/components/AnimationTree.hpp | 22 ++ .../components/AnimationTreeModule.cpp | 30 ++ .../editScene/components/CharacterSlots.hpp | 3 + .../editScene/systems/AnimationTreeSystem.cpp | 358 ++++++++++++++++++ .../editScene/systems/AnimationTreeSystem.hpp | 81 ++++ .../editScene/systems/CharacterSlotSystem.cpp | 1 + .../editScene/systems/EditorUISystem.cpp | 12 +- .../editScene/systems/SceneSerializer.cpp | 37 ++ .../editScene/systems/SceneSerializer.hpp | 2 + .../editScene/ui/AnimationTreeEditor.cpp | 92 +++++ .../editScene/ui/AnimationTreeEditor.hpp | 25 ++ 14 files changed, 686 insertions(+), 1 deletion(-) create mode 100644 src/features/editScene/components/AnimationTree.hpp create mode 100644 src/features/editScene/components/AnimationTreeModule.cpp create mode 100644 src/features/editScene/systems/AnimationTreeSystem.cpp create mode 100644 src/features/editScene/systems/AnimationTreeSystem.hpp create mode 100644 src/features/editScene/ui/AnimationTreeEditor.cpp create mode 100644 src/features/editScene/ui/AnimationTreeEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index ef95662..d84772c 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -26,6 +26,7 @@ set(EDITSCENE_SOURCES systems/RoomLayoutSystem.cpp systems/FurnitureLibrary.cpp systems/CharacterSlotSystem.cpp + systems/AnimationTreeSystem.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp ui/PhysicsColliderEditor.cpp @@ -41,6 +42,7 @@ set(EDITSCENE_SOURCES ui/PrimitiveEditor.cpp ui/TriangleBufferEditor.cpp ui/CharacterSlotsEditor.cpp + ui/AnimationTreeEditor.cpp ui/CellGridEditor.cpp ui/LotEditor.cpp ui/DistrictEditor.cpp @@ -61,6 +63,7 @@ set(EDITSCENE_SOURCES components/PrimitiveModule.cpp components/TriangleBufferModule.cpp components/CharacterSlotsModule.cpp + components/AnimationTreeModule.cpp components/CellGridModule.cpp components/CellGridEditorsModule.cpp components/CellGrid.cpp @@ -88,6 +91,7 @@ set(EDITSCENE_HEADERS components/Primitive.hpp components/TriangleBuffer.hpp components/CharacterSlots.hpp + components/AnimationTree.hpp components/CellGrid.hpp systems/EditorUISystem.hpp systems/CellGridSystem.hpp @@ -96,6 +100,7 @@ set(EDITSCENE_HEADERS systems/ProceduralMaterialSystem.hpp systems/ProceduralMeshSystem.hpp systems/CharacterSlotSystem.hpp + systems/AnimationTreeSystem.hpp systems/ProceduralTextureSystem.hpp systems/StaticGeometrySystem.hpp systems/SceneSerializer.hpp @@ -121,6 +126,7 @@ set(EDITSCENE_HEADERS ui/PrimitiveEditor.hpp ui/TriangleBufferEditor.hpp ui/CharacterSlotsEditor.hpp + ui/AnimationTreeEditor.hpp ui/CellGridEditor.hpp ui/LotEditor.hpp ui/DistrictEditor.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 804f990..518f7fb 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -10,6 +10,7 @@ #include "systems/ProceduralMaterialSystem.hpp" #include "systems/ProceduralMeshSystem.hpp" #include "systems/CharacterSlotSystem.hpp" +#include "systems/AnimationTreeSystem.hpp" #include "systems/CellGridSystem.hpp" #include "systems/RoomLayoutSystem.hpp" #include "camera/EditorCamera.hpp" @@ -31,6 +32,7 @@ #include "components/Primitive.hpp" #include "components/TriangleBuffer.hpp" #include "components/CharacterSlots.hpp" +#include "components/AnimationTree.hpp" #include "components/CellGrid.hpp" #include "components/CellGridModule.hpp" #include @@ -122,6 +124,7 @@ EditorApp::~EditorApp() // Release all systems m_characterSlotSystem.reset(); + m_animationTreeSystem.reset(); m_proceduralMeshSystem.reset(); m_proceduralMaterialSystem.reset(); m_proceduralTextureSystem.reset(); @@ -231,6 +234,11 @@ void EditorApp::setup() m_world, m_sceneMgr); m_characterSlotSystem->initialize(); + // Setup AnimationTree system + m_animationTreeSystem = std::make_unique( + m_world, m_sceneMgr); + m_animationTreeSystem->initialize(); + // Setup CellGrid system m_cellGridSystem = std::make_unique(m_world, m_sceneMgr); @@ -300,6 +308,9 @@ void EditorApp::setupECS() // Register CharacterSlots component m_world.component(); + // Register AnimationTree component + m_world.component(); + // Register CellGrid/Town components CellGridModule::registerComponents(m_world); } @@ -465,6 +476,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_characterSlotSystem->update(); } + // Update AnimationTree system + if (m_animationTreeSystem) { + m_animationTreeSystem->update(evt.timeSinceLastFrame); + } + // Update ProceduralMesh system if (m_proceduralMeshSystem) { m_proceduralMeshSystem->update(); diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index bfe854c..6eb83a2 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -22,6 +22,7 @@ class ProceduralTextureSystem; class ProceduralMaterialSystem; class ProceduralMeshSystem; class CharacterSlotSystem; +class AnimationTreeSystem; class CellGridSystem; class RoomLayoutSystem; @@ -118,6 +119,7 @@ private: std::unique_ptr m_proceduralMaterialSystem; std::unique_ptr m_proceduralMeshSystem; std::unique_ptr m_characterSlotSystem; + std::unique_ptr m_animationTreeSystem; std::unique_ptr m_cellGridSystem; std::unique_ptr m_roomLayoutSystem; diff --git a/src/features/editScene/components/AnimationTree.hpp b/src/features/editScene/components/AnimationTree.hpp new file mode 100644 index 0000000..0d1bf06 --- /dev/null +++ b/src/features/editScene/components/AnimationTree.hpp @@ -0,0 +1,22 @@ +#ifndef EDITSCENE_ANIMATIONTREE_HPP +#define EDITSCENE_ANIMATIONTREE_HPP +#pragma once +#include + +/** + * Animation tree component for playing skeletal animations on entities. + * Works with any entity that has an Ogre::Entity with a skeleton + * (either via RenderableComponent or CharacterSlotsComponent). + */ +struct AnimationTreeComponent { + Ogre::String currentAnimation; + float speed = 1.0f; + bool loop = true; + bool enabled = true; + bool useRootMotion = false; + bool dirty = true; + + AnimationTreeComponent() = default; +}; + +#endif // EDITSCENE_ANIMATIONTREE_HPP diff --git a/src/features/editScene/components/AnimationTreeModule.cpp b/src/features/editScene/components/AnimationTreeModule.cpp new file mode 100644 index 0000000..f82a84b --- /dev/null +++ b/src/features/editScene/components/AnimationTreeModule.cpp @@ -0,0 +1,30 @@ +#include "AnimationTree.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/AnimationTreeEditor.hpp" +#include "Transform.hpp" + +REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering", + AnimationTreeComponent, AnimationTreeEditor) +{ + registry.registerComponent( + "Animation Tree", "Rendering", + std::make_unique(sceneMgr), + /* Adder */ + [sceneMgr](flecs::entity e) { + if (!e.has()) { + TransformComponent transform; + transform.node = + sceneMgr->getRootSceneNode() + ->createChildSceneNode(); + e.set(transform); + } + AnimationTreeComponent at; + at.dirty = true; + e.set(at); + }, + /* Remover */ + [sceneMgr](flecs::entity e) { + (void)sceneMgr; + e.remove(); + }); +} diff --git a/src/features/editScene/components/CharacterSlots.hpp b/src/features/editScene/components/CharacterSlots.hpp index 914b9be..65ac45f 100644 --- a/src/features/editScene/components/CharacterSlots.hpp +++ b/src/features/editScene/components/CharacterSlots.hpp @@ -14,6 +14,9 @@ struct CharacterSlotsComponent { std::unordered_map slots; bool dirty = true; + /* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */ + Ogre::Entity *masterEntity = nullptr; + CharacterSlotsComponent() = default; }; diff --git a/src/features/editScene/systems/AnimationTreeSystem.cpp b/src/features/editScene/systems/AnimationTreeSystem.cpp new file mode 100644 index 0000000..3f8d4a0 --- /dev/null +++ b/src/features/editScene/systems/AnimationTreeSystem.cpp @@ -0,0 +1,358 @@ +#include "AnimationTreeSystem.hpp" +#include "../components/Transform.hpp" +#include "../components/Renderable.hpp" +#include "../components/CharacterSlots.hpp" +#include +#include +#include +#include + +//============================================================================= +// RootMotionTracker +//============================================================================= + +RootMotionTracker::RootMotionTracker(Ogre::AnimationState *animationState, + Ogre::Entity *entity) + : mEntity(entity) + , mTrack(nullptr) + , mAppliedTranslation(Ogre::Vector3::ZERO) + , mAppliedRotation(Ogre::Quaternion::IDENTITY) +{ + assert(animationState); + assert(entity); + + Ogre::SkeletonInstance *skel = entity->getSkeleton(); + Ogre::Animation *anim = nullptr; + + try { + anim = skel->getAnimation(animationState->getAnimationName()); + } catch (...) { + return; + } + + /* Try known root bone names */ + const char *rootNames[] = {"Root", "mixamorig:Hips", "Spineroot"}; + for (const char *name : rootNames) { + try { + Ogre::Bone *bone = skel->getBone(name); + mTrack = anim->getNodeTrack(bone->getHandle()); + break; + } catch (...) { + } + } + + /* Fallback: first node track */ + if (!mTrack) { + try { + const auto &trackList = anim->_getNodeTrackList(); + if (!trackList.empty()) + mTrack = trackList.begin()->second; + } catch (...) { + } + } + + if (!mTrack) + return; + + /* Get root bone binding pose */ + try { + Ogre::Bone *rootBone = skel->getBone(mTrack->getHandle()); + mRootBindingPosition = rootBone->getInitialPosition(); + mRootBindingOrientation = rootBone->getInitialOrientation(); + mRootBindingOrientationInverse = mRootBindingOrientation.Inverse(); + } catch (...) { + mTrack = nullptr; + return; + } + + /* Compute loop deltas */ + if (mTrack->getNumKeyFrames() >= 2) { + Ogre::TransformKeyFrame *tkfBeg = mTrack->getNodeKeyFrame(0); + Ogre::TransformKeyFrame *tkfEnd = + mTrack->getNodeKeyFrame(mTrack->getNumKeyFrames() - 1); + + Ogre::Quaternion begRotation = + mRootBindingOrientation * tkfBeg->getRotation() * + mRootBindingOrientationInverse; + Ogre::Quaternion endRotation = + mRootBindingOrientation * tkfEnd->getRotation() * + mRootBindingOrientationInverse; + mLoopRotation = endRotation * begRotation.Inverse(); + + /* Limit rotation to Y-axis */ + Ogre::Matrix3 mat; + mLoopRotation.ToRotationMatrix(mat); + Ogre::Radian yAngle, zAngle, xAngle; + mat.ToEulerAnglesYZX(yAngle, zAngle, xAngle); + mat.FromEulerAnglesYZX(yAngle, Ogre::Radian(0.0f), + Ogre::Radian(0.0f)); + mLoopRotation.FromRotationMatrix(mat); + + mLoopRotationInverse = mLoopRotation.Inverse(); + + Ogre::Vector3 begTranslation = + mRootBindingPosition + tkfBeg->getTranslate() - + begRotation * mRootBindingPosition; + Ogre::Vector3 endTranslation = + mRootBindingPosition + tkfEnd->getTranslate() - + endRotation * mRootBindingPosition; + mLoopTranslation = + endTranslation - mLoopRotation * begTranslation; + mLoopTranslation.y = 0.0f; + } else { + mLoopRotation = Ogre::Quaternion::IDENTITY; + mLoopRotationInverse = Ogre::Quaternion::IDENTITY; + mLoopTranslation = Ogre::Vector3::ZERO; + } + + /* Suppress root bone movement in skeleton */ + if (!animationState->hasBlendMask()) { + animationState->createBlendMask( + entity->getSkeleton()->getNumBones(), 1.0f); + } + animationState->setBlendMaskEntry(mTrack->getHandle(), 0.0f); +} + +void RootMotionTracker::apply(int loops, float thisTime) +{ + if (!mTrack) + return; + + Ogre::SceneNode *sceneNode = mEntity->getParentSceneNode(); + if (!sceneNode) + return; + + Ogre::TransformKeyFrame tkf(nullptr, 0.0f); + + /* Unapply transform from last frame */ + sceneNode->rotate(mAppliedRotation.Inverse()); + sceneNode->translate(-mAppliedTranslation, Ogre::Node::TS_LOCAL); + + /* Apply periodic loop transforms */ + while (loops < 0) { + sceneNode->rotate(mLoopRotationInverse); + sceneNode->translate(-mLoopTranslation, Ogre::Node::TS_LOCAL); + loops++; + } + while (loops > 0) { + sceneNode->translate(mLoopTranslation, Ogre::Node::TS_LOCAL); + sceneNode->rotate(mLoopRotation); + loops--; + } + + /* Apply transform from this frame */ + mTrack->getInterpolatedKeyFrame(thisTime, &tkf); + + mAppliedRotation = mRootBindingOrientation * tkf.getRotation() * + mRootBindingOrientationInverse; + mAppliedTranslation = mRootBindingPosition + tkf.getTranslate() - + mAppliedRotation * mRootBindingPosition; + + sceneNode->translate(mAppliedTranslation, Ogre::Node::TS_LOCAL); + sceneNode->rotate(mAppliedRotation); +} + +void RootMotionTracker::unapply() +{ + if (!mTrack) + return; + + Ogre::SceneNode *sceneNode = mEntity->getParentSceneNode(); + if (!sceneNode) + return; + + sceneNode->rotate(mAppliedRotation.Inverse()); + sceneNode->translate(-mAppliedTranslation, Ogre::Node::TS_LOCAL); + + mAppliedRotation = Ogre::Quaternion::IDENTITY; + mAppliedTranslation = Ogre::Vector3::ZERO; +} + +//============================================================================= +// AnimationTreeSystem +//============================================================================= + +AnimationTreeSystem::AnimationTreeSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) +{ + m_world.observer("AnimationTreeCleanup") + .event(flecs::OnRemove) + .each([this](flecs::entity e, AnimationTreeComponent &) { + teardownEntity(e); + }); +} + +AnimationTreeSystem::~AnimationTreeSystem() +{ + std::vector toRemove; + for (auto &pair : m_states) + toRemove.push_back(pair.first); + for (auto id : toRemove) + teardownEntity(m_world.entity(id)); +} + +void AnimationTreeSystem::initialize() +{ + m_initialized = true; +} + +Ogre::Entity *AnimationTreeSystem::findAnimatedEntity(flecs::entity e) +{ + if (!e.is_alive()) + return nullptr; + + if (e.has()) { + auto &cs = e.get(); + if (cs.masterEntity && cs.masterEntity->hasSkeleton()) + return cs.masterEntity; + } + + if (e.has()) { + auto &rc = e.get(); + if (rc.entity && rc.entity->hasSkeleton()) + return rc.entity; + } + + if (e.has()) { + auto &t = e.get(); + if (t.node) { + for (unsigned int i = 0; + i < t.node->numAttachedObjects(); ++i) { + Ogre::MovableObject *obj = + t.node->getAttachedObject(i); + if (obj->getMovableType() == "Entity") { + Ogre::Entity *ent = + static_cast(obj); + if (ent->hasSkeleton()) + return ent; + } + } + } + } + + return nullptr; +} + +void AnimationTreeSystem::setupEntity(flecs::entity e, + AnimationTreeComponent &at) +{ + Ogre::Entity *ent = findAnimatedEntity(e); + if (!ent || !ent->hasSkeleton()) { + teardownEntity(e); + return; + } + + auto it = m_states.find(e.id()); + if (it != m_states.end() && it->second.animState) { + if (it->second.currentAnimName != at.currentAnimation) { + it->second.animState->setEnabled(false); + if (it->second.rootMotion) + it->second.rootMotion->unapply(); + it->second.animState = nullptr; + it->second.rootMotion.reset(); + } + } + + if (at.currentAnimation.empty()) { + teardownEntity(e); + return; + } + + EntityAnimState &state = m_states[e.id()]; + + try { + state.animState = ent->getAnimationState(at.currentAnimation); + } catch (const Ogre::Exception &) { + state.animState = nullptr; + state.rootMotion.reset(); + state.currentAnimName.clear(); + return; + } + + state.currentAnimName = at.currentAnimation; + state.ogreEntity = ent; + + state.animState->setEnabled(at.enabled); + state.animState->setLoop(at.loop); + + if (at.useRootMotion) { + if (!state.rootMotion || !state.rootMotion->hasTrack()) { + state.rootMotion = std::make_unique( + state.animState, ent); + } + } else { + if (state.rootMotion) { + state.rootMotion->unapply(); + state.rootMotion.reset(); + } + } +} + +void AnimationTreeSystem::teardownEntity(flecs::entity e) +{ + auto it = m_states.find(e.id()); + if (it == m_states.end()) + return; + + if (it->second.animState) + it->second.animState->setEnabled(false); + if (it->second.rootMotion) + it->second.rootMotion->unapply(); + + m_states.erase(it); +} + +void AnimationTreeSystem::update(float deltaTime) +{ + if (!m_initialized) + return; + + m_world.query().each( + [this, deltaTime](flecs::entity e, AnimationTreeComponent &at) { + if (at.dirty) { + setupEntity(e, at); + at.dirty = false; + } + + auto it = m_states.find(e.id()); + if (it == m_states.end()) + return; + + EntityAnimState &state = it->second; + if (!state.animState) + return; + + /* Sync runtime toggles */ + if (state.animState->getEnabled() != at.enabled) { + state.animState->setEnabled(at.enabled); + if (!at.enabled && state.rootMotion) + state.rootMotion->unapply(); + } + if (state.animState->getLoop() != at.loop) + state.animState->setLoop(at.loop); + + if (!at.enabled) + return; + + float dt = deltaTime * at.speed; + if (dt == 0.0f) + return; + + float lastTime = state.animState->getTimePosition(); + state.animState->addTime(dt); + float thisTime = state.animState->getTimePosition(); + float length = state.animState->getLength(); + bool loop = state.animState->getLoop(); + + int loops = 0; + if (loop && length > 0.0f) { + loops = (int)std::round( + (lastTime + dt - thisTime) / length); + } + + if (at.useRootMotion && state.rootMotion) + state.rootMotion->apply(loops, thisTime); + }); +} diff --git a/src/features/editScene/systems/AnimationTreeSystem.hpp b/src/features/editScene/systems/AnimationTreeSystem.hpp new file mode 100644 index 0000000..b62f647 --- /dev/null +++ b/src/features/editScene/systems/AnimationTreeSystem.hpp @@ -0,0 +1,81 @@ +#ifndef EDITSCENE_ANIMATIONTREESYSTEM_HPP +#define EDITSCENE_ANIMATIONTREESYSTEM_HPP +#pragma once + +#include +#include +#include +#include + +#include "../components/AnimationTree.hpp" + +/** + * RootMotionTracker applies transforms from a NodeAnimationTrack to an + * Entity's parent SceneNode instead of to its Skeleton's Bone. + * + * Based on OGRE Sample_SkeletalAnimation's RootMotionApplier. + */ +class RootMotionTracker { +public: + RootMotionTracker(Ogre::AnimationState *animationState, + Ogre::Entity *entity); + + void apply(int loops, float thisTime); + void unapply(); + + bool hasTrack() const { return mTrack != nullptr; } + +private: + Ogre::Entity *mEntity; + Ogre::NodeAnimationTrack *mTrack; + + Ogre::Vector3 mRootBindingPosition; + Ogre::Quaternion mRootBindingOrientation; + Ogre::Quaternion mRootBindingOrientationInverse; + + Ogre::Vector3 mLoopTranslation; + Ogre::Quaternion mLoopRotation; + Ogre::Quaternion mLoopRotationInverse; + + Ogre::Vector3 mAppliedTranslation; + Ogre::Quaternion mAppliedRotation; +}; + +/** + * System that manages skeletal animation playback and root motion + * for entities with AnimationTreeComponent. + */ +class AnimationTreeSystem { +public: + AnimationTreeSystem(flecs::world &world, Ogre::SceneManager *sceneMgr); + ~AnimationTreeSystem(); + + void initialize(); + void update(float deltaTime); + + /** + * Find the Ogre entity with a skeleton for a given flecs entity. + * Checks CharacterSlotsComponent master, RenderableComponent, then + * attached objects on the transform node. + */ + static Ogre::Entity *findAnimatedEntity(flecs::entity e); + +private: + struct EntityAnimState { + Ogre::AnimationState *animState = nullptr; + Ogre::Entity *ogreEntity = nullptr; + std::unique_ptr rootMotion; + Ogre::String currentAnimName; + }; + + void setupEntity(flecs::entity e, AnimationTreeComponent &at); + void teardownEntity(flecs::entity e); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; + bool m_initialized = false; + + std::unordered_map m_states; +}; + +#endif // EDITSCENE_ANIMATIONTREESYSTEM_HPP diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index 2863b88..b585875 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -215,6 +215,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, masterEnt = m_sceneMgr->createEntity(cs.slots.at(masterSlot)); transform.node->attachObject(masterEnt); m_entities[e.id()].parts[masterSlot] = masterEnt; + cs.masterEntity = masterEnt; std::cout << " master loaded: " << masterEnt->getName() << std::endl; } catch (const Ogre::Exception &ex) { diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 6166f03..1a8ae65 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -17,6 +17,7 @@ #include "../components/TriangleBuffer.hpp" #include "../components/LodSettings.hpp" #include "../components/CharacterSlots.hpp" +#include "../components/AnimationTree.hpp" #include "../components/CellGrid.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" @@ -412,6 +413,8 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth) indicators += " [Tex]"; if (entity.has()) indicators += " [Mat]"; + if (entity.has()) + indicators += " [Anim]"; snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(), indicators.c_str(), (unsigned long long)entity.id()); @@ -632,7 +635,14 @@ void EditorUISystem::renderComponentList(flecs::entity entity) m_componentRegistry.render(entity, cs); componentCount++; } - + + // Render AnimationTree if present + if (entity.has()) { + auto &at = entity.get_mut(); + m_componentRegistry.render(entity, at); + componentCount++; + } + // Render CellGrid if present if (entity.has()) { auto &grid = entity.get_mut(); diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index b130a65..54f4ae3 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -16,6 +16,7 @@ #include "../components/Primitive.hpp" #include "../components/TriangleBuffer.hpp" #include "../components/CharacterSlots.hpp" +#include "../components/AnimationTree.hpp" #include "../components/CellGrid.hpp" #include "../components/GeneratedPhysicsTag.hpp" #include "EditorUISystem.hpp" @@ -182,6 +183,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["characterSlots"] = serializeCharacterSlots(entity); } + + if (entity.has()) { + json["animationTree"] = serializeAnimationTree(entity); + } // CellGrid/Town components if (entity.has()) { @@ -303,6 +308,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit deserializeCharacterSlots(entity, json["characterSlots"]); } + if (json.contains("animationTree")) { + deserializeAnimationTree(entity, json["animationTree"]); + } + if (json.contains("triangleBuffer")) { deserializeTriangleBuffer(entity, json["triangleBuffer"]); } @@ -1299,6 +1308,34 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, const nloh entity.set(cs); } +nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity) +{ + auto& at = entity.get(); + nlohmann::json json; + + json["currentAnimation"] = at.currentAnimation; + json["speed"] = at.speed; + json["loop"] = at.loop; + json["enabled"] = at.enabled; + json["useRootMotion"] = at.useRootMotion; + + return json; +} + +void SceneSerializer::deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json) +{ + AnimationTreeComponent at; + + at.currentAnimation = json.value("currentAnimation", ""); + at.speed = json.value("speed", 1.0f); + at.loop = json.value("loop", true); + at.enabled = json.value("enabled", true); + at.useRootMotion = json.value("useRootMotion", false); + at.dirty = true; + + entity.set(at); +} + // ============================================================================ // CellGrid/Town Component Serialization diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index ff49c6e..0b9c22e 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -55,6 +55,7 @@ private: nlohmann::json serializePrimitive(flecs::entity entity); nlohmann::json serializeTriangleBuffer(flecs::entity entity); nlohmann::json serializeCharacterSlots(flecs::entity entity); + nlohmann::json serializeAnimationTree(flecs::entity entity); // CellGrid/Town component serialization nlohmann::json serializeCellGrid(flecs::entity entity); @@ -83,6 +84,7 @@ private: void deserializePrimitive(flecs::entity entity, const nlohmann::json& json); void deserializeTriangleBuffer(flecs::entity entity, const nlohmann::json& json); void deserializeCharacterSlots(flecs::entity entity, const nlohmann::json& json); + void deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json); // CellGrid/Town component deserialization void deserializeCellGrid(flecs::entity entity, const nlohmann::json& json); diff --git a/src/features/editScene/ui/AnimationTreeEditor.cpp b/src/features/editScene/ui/AnimationTreeEditor.cpp new file mode 100644 index 0000000..7bd1ac6 --- /dev/null +++ b/src/features/editScene/ui/AnimationTreeEditor.cpp @@ -0,0 +1,92 @@ +#include "AnimationTreeEditor.hpp" +#include "../systems/AnimationTreeSystem.hpp" +#include + +AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr) + : m_sceneMgr(sceneMgr) +{ +} + +bool AnimationTreeEditor::renderComponent(flecs::entity entity, + AnimationTreeComponent &at) +{ + bool modified = false; + (void)entity; + + if (ImGui::CollapsingHeader("Animation Tree", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + Ogre::Entity *ent = + AnimationTreeSystem::findAnimatedEntity(entity); + std::vector animNames; + + if (ent && ent->hasSkeleton()) { + Ogre::AnimationStateSet *states = + ent->getAllAnimationStates(); + if (states) { + for (const auto &pair : states->getAnimationStates()) + animNames.push_back(pair.first); + } + } + + if (animNames.empty()) { + ImGui::TextDisabled("No skeleton found on entity"); + } else { + /* Animation selector */ + Ogre::String currentAnim = at.currentAnimation; + Ogre::String preview = + currentAnim.empty() ? "(none)" : currentAnim; + if (ImGui::BeginCombo("Animation", preview.c_str())) { + bool noneSelected = currentAnim.empty(); + if (ImGui::Selectable("(none)", noneSelected)) { + at.currentAnimation = ""; + modified = true; + at.dirty = true; + } + if (noneSelected) + ImGui::SetItemDefaultFocus(); + + for (const auto &name : animNames) { + bool isSelected = (currentAnim == name); + if (ImGui::Selectable(name.c_str(), isSelected)) { + at.currentAnimation = name; + modified = true; + at.dirty = true; + } + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + /* Speed */ + if (ImGui::SliderFloat("Speed", &at.speed, -2.0f, 5.0f, + "%.2f")) + modified = true; + + /* Loop */ + if (ImGui::Checkbox("Loop", &at.loop)) + modified = true; + + /* Enabled (play/pause) */ + if (ImGui::Checkbox("Enabled", &at.enabled)) + modified = true; + + /* Root motion */ + if (ImGui::Checkbox("Use Root Motion", &at.useRootMotion)) { + modified = true; + at.dirty = true; + } + + if (!at.currentAnimation.empty()) { + ImGui::Text("State: %s", + at.enabled ? "Playing" : "Paused"); + } + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/AnimationTreeEditor.hpp b/src/features/editScene/ui/AnimationTreeEditor.hpp new file mode 100644 index 0000000..3eaac15 --- /dev/null +++ b/src/features/editScene/ui/AnimationTreeEditor.hpp @@ -0,0 +1,25 @@ +#ifndef EDITSCENE_ANIMATIONTREEEDITOR_HPP +#define EDITSCENE_ANIMATIONTREEEDITOR_HPP +#pragma once +#include "ComponentEditor.hpp" +#include "../components/AnimationTree.hpp" +#include + +/** + * Editor for AnimationTreeComponent + */ +class AnimationTreeEditor : public ComponentEditor { +public: + explicit AnimationTreeEditor(Ogre::SceneManager *sceneMgr); + + const char *getName() const override { return "Animation Tree"; } + +protected: + bool renderComponent(flecs::entity entity, + AnimationTreeComponent &at) override; + +private: + Ogre::SceneManager *m_sceneMgr; +}; + +#endif // EDITSCENE_ANIMATIONTREEEDITOR_HPP