diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index d84772c..7706e27 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -64,6 +64,7 @@ set(EDITSCENE_SOURCES components/TriangleBufferModule.cpp components/CharacterSlotsModule.cpp components/AnimationTreeModule.cpp + components/AnimationTree.cpp components/CellGridModule.cpp components/CellGridEditorsModule.cpp components/CellGrid.cpp diff --git a/src/features/editScene/components/AnimationTree.cpp b/src/features/editScene/components/AnimationTree.cpp new file mode 100644 index 0000000..8ac684f --- /dev/null +++ b/src/features/editScene/components/AnimationTree.cpp @@ -0,0 +1,9 @@ +#include "AnimationTree.hpp" + +AnimationTreeComponent::AnimationTreeComponent() + : root() + , enabled(true) + , useRootMotion(false) + , dirty(true) +{ +} diff --git a/src/features/editScene/components/AnimationTree.hpp b/src/features/editScene/components/AnimationTree.hpp index 0d1bf06..099ded2 100644 --- a/src/features/editScene/components/AnimationTree.hpp +++ b/src/features/editScene/components/AnimationTree.hpp @@ -2,21 +2,158 @@ #define EDITSCENE_ANIMATIONTREE_HPP #pragma once #include +#include +#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). + * A node in the animation tree. + * + * Node types: + * "output" - Root output node, optional speed multiplier (1 child) + * "stateMachine" - Cross-fades between child states, named for lookup + * "state" - A named state within a state machine (1 child) + * "speed" - Playback speed multiplier (1 child) + * "animation" - Leaf referencing an Ogre animation by name + */ +struct AnimationTreeNode { + Ogre::String type = "animation"; + Ogre::String name; + Ogre::String animationName; + float speed = 1.0f; + float fadeSpeed = 7.5f; + std::vector children; + /* For stateMachine nodes: auto-transition when animation ends */ + std::unordered_map endTransitions; + + AnimationTreeNode() = default; + + AnimationTreeNode *findChild(const Ogre::String &childName) + { + for (auto &child : children) { + if (child.name == childName) + return &child; + } + return nullptr; + } + + const AnimationTreeNode *findChild( + const Ogre::String &childName) const + { + for (const auto &child : children) { + if (child.name == childName) + return &child; + } + return nullptr; + } + + AnimationTreeNode *findStateMachine(const Ogre::String &smName) + { + if (type == "stateMachine" && name == smName) + return this; + for (auto &child : children) { + auto *found = child.findStateMachine(smName); + if (found) + return found; + } + return nullptr; + } + + const AnimationTreeNode *findStateMachine( + const Ogre::String &smName) const + { + if (type == "stateMachine" && name == smName) + return this; + for (const auto &child : children) { + auto *found = child.findStateMachine(smName); + if (found) + return found; + } + return nullptr; + } + + AnimationTreeNode *findState(const Ogre::String &stateName) + { + if (type == "state" && name == stateName) + return this; + for (auto &child : children) { + auto *found = child.findState(stateName); + if (found) + return found; + } + return nullptr; + } + + const AnimationTreeNode *findState( + const Ogre::String &stateName) const + { + if (type == "state" && name == stateName) + return this; + for (const auto &child : children) { + auto *found = child.findState(stateName); + if (found) + return found; + } + return nullptr; + } + + AnimationTreeNode *findAnimationLeaf() + { + if (type == "animation") + return this; + for (auto &child : children) { + auto *found = child.findAnimationLeaf(); + if (found) + return found; + } + return nullptr; + } + + const AnimationTreeNode *findAnimationLeaf() const + { + if (type == "animation") + return this; + for (const auto &child : children) { + auto *found = child.findAnimationLeaf(); + if (found) + return found; + } + return nullptr; + } + + void collectStateMachines(std::vector &out) + { + if (type == "stateMachine") + out.push_back(this); + for (auto &child : children) + child.collectStateMachines(out); + } + + void collectStateMachines( + std::vector &out) const + { + if (type == "stateMachine") + out.push_back(this); + for (const auto &child : children) + child.collectStateMachines(out); + } +}; + +/** + * Animation tree component for hierarchical state-machine-based animation. + * + * The tree is evaluated each frame by AnimationTreeSystem to determine + * which Ogre AnimationStates are active and their blend weights. */ struct AnimationTreeComponent { - Ogre::String currentAnimation; - float speed = 1.0f; - bool loop = true; + AnimationTreeNode root; bool enabled = true; bool useRootMotion = false; bool dirty = true; - AnimationTreeComponent() = default; + /* Runtime: current state of each state machine (not serialized) */ + std::unordered_map currentStates; + + AnimationTreeComponent(); }; #endif // EDITSCENE_ANIMATIONTREE_HPP diff --git a/src/features/editScene/components/AnimationTreeModule.cpp b/src/features/editScene/components/AnimationTreeModule.cpp index f82a84b..88300bf 100644 --- a/src/features/editScene/components/AnimationTreeModule.cpp +++ b/src/features/editScene/components/AnimationTreeModule.cpp @@ -3,6 +3,33 @@ #include "../ui/AnimationTreeEditor.hpp" #include "Transform.hpp" +static AnimationTreeComponent createDefaultTree() +{ + AnimationTreeComponent at; + + at.root.type = "output"; + at.root.speed = 1.0f; + + AnimationTreeNode sm; + sm.type = "stateMachine"; + sm.name = "main"; + sm.fadeSpeed = 7.5f; + + AnimationTreeNode state; + state.type = "state"; + state.name = "idle"; + + AnimationTreeNode anim; + anim.type = "animation"; + anim.animationName = "idle"; + + state.children.push_back(anim); + sm.children.push_back(state); + at.root.children.push_back(sm); + + return at; +} + REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering", AnimationTreeComponent, AnimationTreeEditor) { @@ -18,7 +45,7 @@ REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering", ->createChildSceneNode(); e.set(transform); } - AnimationTreeComponent at; + AnimationTreeComponent at = createDefaultTree(); at.dirty = true; e.set(at); }, diff --git a/src/features/editScene/systems/AnimationTreeSystem.cpp b/src/features/editScene/systems/AnimationTreeSystem.cpp index 3f8d4a0..02a3d6b 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.cpp +++ b/src/features/editScene/systems/AnimationTreeSystem.cpp @@ -7,171 +7,6 @@ #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) @@ -244,50 +79,88 @@ void AnimationTreeSystem::setupEntity(flecs::entity 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; + EntityAnimTreeState &state = m_states[e.id()]; state.ogreEntity = ent; - state.animState->setEnabled(at.enabled); - state.animState->setLoop(at.loop); + Ogre::SkeletonInstance *skel = ent->getSkeleton(); - 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(); + /* Find and freeze root bone */ + const char *rootNames[] = {"Root", "mixamorig:Hips", "Spineroot"}; + for (const char *name : rootNames) { + try { + state.rootBone = skel->getBone(name); + break; + } catch (...) { } } + if (state.rootBone) { + state.rootBindingPosition = state.rootBone->getInitialPosition(); + state.rootBindingOrientation = + state.rootBone->getInitialOrientation(); + state.rootBindingScale = state.rootBone->getInitialScale(); + state.rootBone->setManuallyControlled(true); + state.rootBone->setPosition(state.rootBindingPosition); + state.rootBone->setOrientation(state.rootBindingOrientation); + state.rootBone->setScale(state.rootBindingScale); + } + + /* Discover all animations and precompute root tracks */ + Ogre::AnimationStateSet *states = ent->getAllAnimationStates(); + if (states) { + for (const auto &pair : states->getAnimationStates()) { + const Ogre::String &animName = pair.first; + Ogre::AnimationState *as = pair.second; + + /* Use blend mask to exclude root bone from animation + * (setManuallyControlled alone does not prevent + * Animation::apply() in OGRE 14) */ + if (state.rootBone) { + as->destroyBlendMask(); + as->createBlendMask(skel->getNumBones(), + 1.0f); + as->setBlendMaskEntry( + state.rootBone->getHandle(), + 0.0f); + } + + AnimationRuntimeInfo info; + info.ogreAnimState = as; + + try { + Ogre::Animation *anim = + skel->getAnimation(animName); + if (state.rootBone) { + unsigned short handle = + state.rootBone->getHandle(); + if (anim->hasNodeTrack(handle)) { + info.rootTrack = + anim->getNodeTrack(handle); + if (info.rootTrack->getNumKeyFrames() >= + 2) { + Ogre::TransformKeyFrame *tkfBeg = + info.rootTrack + ->getNodeKeyFrame(0); + Ogre::TransformKeyFrame *tkfEnd = + info.rootTrack + ->getNodeKeyFrame( + info.rootTrack + ->getNumKeyFrames() - + 1); + info.loopTranslation = + tkfEnd->getTranslate() - + tkfBeg->getTranslate(); + } + } + } + } catch (...) { + } + + state.animations[animName] = info; + } + } + + /* Initialize default state machine states */ + initializeTreeStates(at.root, at); } void AnimationTreeSystem::teardownEntity(flecs::entity e) @@ -296,21 +169,61 @@ void AnimationTreeSystem::teardownEntity(flecs::entity e) if (it == m_states.end()) return; - if (it->second.animState) - it->second.animState->setEnabled(false); - if (it->second.rootMotion) - it->second.rootMotion->unapply(); + EntityAnimTreeState &state = it->second; + + for (auto &pair : state.animations) { + if (pair.second.ogreAnimState) + pair.second.ogreAnimState->destroyBlendMask(); + } + + disableAllAnimations(state); + + if (state.rootBone) { + state.rootBone->setManuallyControlled(false); + state.rootBone = nullptr; + } + + state.animations.clear(); + state.fadeStates.clear(); + state.prevStates.clear(); m_states.erase(it); } +void AnimationTreeSystem::disableAllAnimations(EntityAnimTreeState &state) +{ + for (auto &pair : state.animations) { + if (pair.second.ogreAnimState) + pair.second.ogreAnimState->setEnabled(false); + } +} + +void AnimationTreeSystem::initializeTreeStates( + const AnimationTreeNode &node, AnimationTreeComponent &at) +{ + if (node.type == "stateMachine") { + if (at.currentStates.find(node.name) == + at.currentStates.end()) { + for (const auto &child : node.children) { + if (child.type == "state") { + at.currentStates[node.name] = child.name; + break; + } + } + } + } + for (const auto &child : node.children) + initializeTreeStates(child, at); +} + void AnimationTreeSystem::update(float deltaTime) { if (!m_initialized) return; m_world.query().each( - [this, deltaTime](flecs::entity e, AnimationTreeComponent &at) { + [this, deltaTime](flecs::entity e, + AnimationTreeComponent &at) { if (at.dirty) { setupEntity(e, at); at.dirty = false; @@ -320,39 +233,374 @@ void AnimationTreeSystem::update(float deltaTime) if (it == m_states.end()) return; - EntityAnimState &state = it->second; - if (!state.animState) + EntityAnimTreeState &state = it->second; + if (!state.ogreEntity) 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) + if (!at.enabled) { + disableAllAnimations(state); 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); + /* Sync external state changes (editor) */ + syncStateChanges(at, state); + + /* Evaluate tree */ + EvalContext ctx; + ctx.deltaTime = deltaTime; + evaluateNode(at.root, 1.0f, 1.0f, at, state, ctx); + + /* Apply animation weights and advance */ + Ogre::Vector3 totalRootMotion = + Ogre::Vector3::ZERO; + Ogre::SceneNode *sceneNode = + state.ogreEntity->getParentSceneNode(); + + for (auto &pair : ctx.animData) { + const Ogre::String &animName = pair.first; + AnimEvalData &data = pair.second; + + auto itAnim = + state.animations.find(animName); + if (itAnim == state.animations.end()) + continue; + AnimationRuntimeInfo &info = + itAnim->second; + + if (!info.ogreAnimState) + continue; + + bool active = data.weight > 0.001f; + info.ogreAnimState->setEnabled(active); + info.ogreAnimState->setWeight( + Ogre::Math::Clamp(data.weight, 0.0f, + 1.0f)); + + if (active && data.timeDelta != 0.0f) { + float lastTime = info.ogreAnimState + ->getTimePosition(); + info.ogreAnimState->addTime( + data.timeDelta); + float thisTime = + info.ogreAnimState + ->getTimePosition(); + float length = + info.ogreAnimState + ->getLength(); + bool loop = + info.ogreAnimState + ->getLoop(); + + int loops = 0; + if (loop && length > 0.0f) { + loops = (int)std::round( + (lastTime + data.timeDelta - + thisTime) / + length); + } + + if (at.useRootMotion && + info.rootTrack) { + Ogre::TransformKeyFrame tkf( + nullptr, 0.0f); + info.rootTrack + ->getInterpolatedKeyFrame( + lastTime, &tkf); + Ogre::Vector3 lastPos = + tkf.getTranslate(); + info.rootTrack + ->getInterpolatedKeyFrame( + thisTime, &tkf); + Ogre::Vector3 thisPos = + tkf.getTranslate(); + Ogre::Vector3 delta = + thisPos - lastPos + + loops * + info.loopTranslation; + totalRootMotion += + delta * data.weight; + } + } + } + + if (at.useRootMotion && sceneNode) + sceneNode->translate(totalRootMotion, + Ogre::Node::TS_LOCAL); + + /* Reset root bone to binding pose */ + if (state.rootBone) { + state.rootBone->setPosition( + state.rootBindingPosition); + state.rootBone->setOrientation( + state.rootBindingOrientation); + state.rootBone->setScale( + state.rootBindingScale); + } + + /* Handle end-of-animation transitions */ + checkEndTransitions(e, at, state, ctx); }); } + +void AnimationTreeSystem::evaluateNode( + const AnimationTreeNode &node, float parentWeight, float timeMul, + const AnimationTreeComponent &at, EntityAnimTreeState &state, + EvalContext &ctx) +{ + if (parentWeight < 0.001f) + return; + + if (node.type == "output") { + if (!node.children.empty()) + evaluateNode(node.children[0], parentWeight, + timeMul * node.speed, at, state, ctx); + } else if (node.type == "speed") { + if (!node.children.empty()) + evaluateNode(node.children[0], parentWeight, + timeMul * node.speed, at, state, ctx); + } else if (node.type == "stateMachine") { + auto itCurrent = at.currentStates.find(node.name); + if (itCurrent == at.currentStates.end()) + return; + + auto &fade = state.fadeStates[node.name]; + const Ogre::String ¤tName = itCurrent->second; + + /* Ensure weight entries exist */ + for (const auto &child : node.children) { + if (child.type == "state" && + fade.weights.find(child.name) == + fade.weights.end()) { + fade.weights[child.name] = + (child.name == currentName) ? 1.0f : + 0.0f; + } + } + + /* Update fades */ + std::vector doneIn; + for (const auto &inName : fade.fadeIn) { + fade.weights[inName] += + ctx.deltaTime * node.fadeSpeed; + if (fade.weights[inName] >= 1.0f) { + fade.weights[inName] = 1.0f; + doneIn.push_back(inName); + } + } + for (const auto &n : doneIn) + fade.fadeIn.erase(n); + + std::vector doneOut; + for (const auto &outName : fade.fadeOut) { + fade.weights[outName] -= + ctx.deltaTime * node.fadeSpeed; + if (fade.weights[outName] <= 0.0f) { + fade.weights[outName] = 0.0f; + doneOut.push_back(outName); + } + } + for (const auto &n : doneOut) + fade.fadeOut.erase(n); + + /* Evaluate active states */ + for (const auto &child : node.children) { + if (child.type != "state") + continue; + float w = fade.weights[child.name]; + if (w > 0.001f) + evaluateNode(child, parentWeight * w, + timeMul, at, state, ctx); + } + + /* Queue end-transition check */ + if (node.endTransitions.find(currentName) != + node.endTransitions.end()) + ctx.endChecks.push_back( + {node.name, currentName}); + } else if (node.type == "state") { + if (!node.children.empty()) + evaluateNode(node.children[0], parentWeight, + timeMul, at, state, ctx); + } else if (node.type == "animation") { + auto &data = ctx.animData[node.animationName]; + data.weight += parentWeight; + data.timeDelta = ctx.deltaTime * timeMul; + } +} + +void AnimationTreeSystem::syncStateChanges( + AnimationTreeComponent &at, EntityAnimTreeState &state) +{ + for (auto &pair : at.currentStates) { + auto itPrev = state.prevStates.find(pair.first); + if (itPrev == state.prevStates.end() || + itPrev->second != pair.second) { + /* State changed externally (editor); set up fade */ + auto &fade = state.fadeStates[pair.first]; + if (itPrev != state.prevStates.end()) + fade.fadeOut.insert(itPrev->second); + fade.fadeIn.insert(pair.second); + fade.weights[pair.second] = 0.0f; + state.prevStates[pair.first] = pair.second; + } + } + /* Also track newly added state machines */ + for (auto &pair : at.currentStates) { + if (state.prevStates.find(pair.first) == + state.prevStates.end()) + state.prevStates[pair.first] = pair.second; + } +} + +void AnimationTreeSystem::checkEndTransitions( + flecs::entity e, AnimationTreeComponent &at, + EntityAnimTreeState &state, EvalContext &ctx) +{ + for (auto &pair : ctx.endChecks) { + const Ogre::String &smName = pair.first; + const Ogre::String &stateName = pair.second; + + const AnimationTreeNode *smNode = + findStateMachineNode(at.root, smName); + if (!smNode) + continue; + + auto itTrans = smNode->endTransitions.find(stateName); + if (itTrans == smNode->endTransitions.end()) + continue; + + const AnimationTreeNode *stNode = + findStateNode(*smNode, stateName); + if (!stNode) + continue; + const AnimationTreeNode *animNode = + findAnimationNode(*stNode); + if (!animNode) + continue; + + auto itAnim = state.animations.find( + animNode->animationName); + if (itAnim == state.animations.end()) + continue; + Ogre::AnimationState *as = + itAnim->second.ogreAnimState; + if (!as) + continue; + + float t = as->getTimePosition(); + float len = as->getLength(); + if (len > 0.0f && t >= len * 0.99f) + setStateInternal(e, at, state, smName, + itTrans->second, true); + } +} + +void AnimationTreeSystem::setState(flecs::entity e, + const Ogre::String &stateMachineName, + const Ogre::String &stateName, + bool reset) +{ + auto it = m_states.find(e.id()); + if (it == m_states.end()) + return; + if (!e.has()) + return; + auto &at = e.get_mut(); + setStateInternal(e, at, it->second, stateMachineName, + stateName, reset); +} + +void AnimationTreeSystem::setStateInternal( + flecs::entity e, AnimationTreeComponent &at, + EntityAnimTreeState &state, + const Ogre::String &stateMachineName, + const Ogre::String &stateName, bool reset) +{ + (void)e; + auto itCurrent = at.currentStates.find(stateMachineName); + if (itCurrent == at.currentStates.end()) + return; + if (itCurrent->second == stateName) + return; + + auto &fade = state.fadeStates[stateMachineName]; + fade.fadeOut.insert(itCurrent->second); + fade.fadeIn.insert(stateName); + fade.weights[stateName] = 0.0f; + + itCurrent->second = stateName; + state.prevStates[stateMachineName] = stateName; + + if (reset) { + /* Reset the new state's animation */ + const AnimationTreeNode *smNode = + findStateMachineNode(at.root, stateMachineName); + if (smNode) { + const AnimationTreeNode *stNode = + findStateNode(*smNode, stateName); + if (stNode) { + const AnimationTreeNode *animNode = + findAnimationNode(*stNode); + if (animNode) { + auto itAnim = state.animations.find( + animNode->animationName); + if (itAnim != state.animations.end() && + itAnim->second.ogreAnimState) + itAnim->second.ogreAnimState + ->setTimePosition(0.0f); + } + } + } + } +} + +Ogre::String AnimationTreeSystem::getCurrentState( + flecs::entity e, const Ogre::String &stateMachineName) +{ + if (!e.has()) + return ""; + auto &at = e.get(); + auto it = at.currentStates.find(stateMachineName); + if (it != at.currentStates.end()) + return it->second; + return ""; +} + +const AnimationTreeNode *AnimationTreeSystem::findStateMachineNode( + const AnimationTreeNode &root, const Ogre::String &name) const +{ + if (root.type == "stateMachine" && root.name == name) + return &root; + for (const auto &child : root.children) { + const AnimationTreeNode *found = + findStateMachineNode(child, name); + if (found) + return found; + } + return nullptr; +} + +const AnimationTreeNode *AnimationTreeSystem::findStateNode( + const AnimationTreeNode &smNode, + const Ogre::String &stateName) const +{ + for (const auto &child : smNode.children) { + if (child.type == "state" && child.name == stateName) + return &child; + } + return nullptr; +} + +const AnimationTreeNode *AnimationTreeSystem::findAnimationNode( + const AnimationTreeNode &stateNode) const +{ + if (stateNode.type == "animation") + return &stateNode; + for (const auto &child : stateNode.children) { + const AnimationTreeNode *found = + findAnimationNode(child); + if (found) + return found; + } + return nullptr; +} diff --git a/src/features/editScene/systems/AnimationTreeSystem.hpp b/src/features/editScene/systems/AnimationTreeSystem.hpp index b62f647..f4393e4 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.hpp +++ b/src/features/editScene/systems/AnimationTreeSystem.hpp @@ -6,44 +6,18 @@ #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. + * System that evaluates an AnimationTreeComponent each frame. * - * 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. + * Walks the tree recursively to: + * - Determine active animation states and their blend weights + * - Advance Ogre::AnimationState::addTime() for active animations + * - Compute and apply root motion (optional) + * - Handle end-of-animation auto-transitions */ class AnimationTreeSystem { public: @@ -53,29 +27,93 @@ public: void initialize(); void update(float deltaTime); + /** + * Manually switch a state machine to a new state. + * Sets up cross-fade automatically. + */ + void setState(flecs::entity e, const Ogre::String &stateMachineName, + const Ogre::String &stateName, bool reset = false); + /** * 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); + /** + * Get current state name of a state machine (for editor display). + */ + Ogre::String getCurrentState(flecs::entity e, + const Ogre::String &stateMachineName); + private: - struct EntityAnimState { - Ogre::AnimationState *animState = nullptr; + struct AnimationRuntimeInfo { + Ogre::AnimationState *ogreAnimState = nullptr; + Ogre::NodeAnimationTrack *rootTrack = nullptr; + Ogre::Vector3 loopTranslation = Ogre::Vector3::ZERO; + }; + + struct FadeInfo { + std::set fadeIn; + std::set fadeOut; + std::unordered_map weights; + }; + + struct EntityAnimTreeState { Ogre::Entity *ogreEntity = nullptr; - std::unique_ptr rootMotion; - Ogre::String currentAnimName; + Ogre::Bone *rootBone = nullptr; + Ogre::Vector3 rootBindingPosition; + Ogre::Quaternion rootBindingOrientation; + Ogre::Vector3 rootBindingScale; + + std::unordered_map animations; + std::unordered_map fadeStates; + /* Track previous currentStates to detect external changes */ + std::unordered_map prevStates; + }; + + struct AnimEvalData { + float weight = 0.0f; + float timeDelta = 0.0f; + }; + + struct EvalContext { + float deltaTime = 0.0f; + std::unordered_map animData; + std::vector> endChecks; }; void setupEntity(flecs::entity e, AnimationTreeComponent &at); void teardownEntity(flecs::entity e); + void disableAllAnimations(EntityAnimTreeState &state); + void initializeTreeStates(const AnimationTreeNode &node, + AnimationTreeComponent &at); + void evaluateNode(const AnimationTreeNode &node, float parentWeight, + float timeMul, const AnimationTreeComponent &at, + EntityAnimTreeState &state, EvalContext &ctx); + void checkEndTransitions(flecs::entity e, AnimationTreeComponent &at, + EntityAnimTreeState &state, + EvalContext &ctx); + void syncStateChanges(AnimationTreeComponent &at, + EntityAnimTreeState &state); + void setStateInternal(flecs::entity e, AnimationTreeComponent &at, + EntityAnimTreeState &state, + const Ogre::String &stateMachineName, + const Ogre::String &stateName, bool reset); + + const AnimationTreeNode *findStateMachineNode( + const AnimationTreeNode &root, + const Ogre::String &name) const; + const AnimationTreeNode *findStateNode( + const AnimationTreeNode &smNode, + const Ogre::String &stateName) const; + const AnimationTreeNode *findAnimationNode( + const AnimationTreeNode &stateNode) const; flecs::world &m_world; Ogre::SceneManager *m_sceneMgr; bool m_initialized = false; - std::unordered_map m_states; + std::unordered_map m_states; }; #endif // EDITSCENE_ANIMATIONTREESYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 54f4ae3..e6a17e0 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -1308,31 +1308,76 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, const nloh entity.set(cs); } -nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity) +static nlohmann::json serializeAnimationTreeNode( + const AnimationTreeNode &node) { - 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; - + json["type"] = node.type; + if (!node.name.empty()) + json["name"] = node.name; + if (!node.animationName.empty()) + json["animationName"] = node.animationName; + if (node.speed != 1.0f) + json["speed"] = node.speed; + if (node.fadeSpeed != 7.5f) + json["fadeSpeed"] = node.fadeSpeed; + if (!node.endTransitions.empty()) { + json["endTransitions"] = nlohmann::json::object(); + for (const auto &pair : node.endTransitions) + json["endTransitions"][pair.first] = pair.second; + } + if (!node.children.empty()) { + json["children"] = nlohmann::json::array(); + for (const auto &child : node.children) + json["children"].push_back( + serializeAnimationTreeNode(child)); + } return json; } -void SceneSerializer::deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json) +static void deserializeAnimationTreeNode(AnimationTreeNode &node, + const nlohmann::json &json) +{ + node.type = json.value("type", "animation"); + node.name = json.value("name", ""); + node.animationName = json.value("animationName", ""); + node.speed = json.value("speed", 1.0f); + node.fadeSpeed = json.value("fadeSpeed", 7.5f); + node.endTransitions.clear(); + if (json.contains("endTransitions") && + json["endTransitions"].is_object()) { + for (auto &[key, val] : json["endTransitions"].items()) + node.endTransitions[key] = val.get(); + } + node.children.clear(); + if (json.contains("children") && json["children"].is_array()) { + for (const auto &childJson : json["children"]) { + AnimationTreeNode child; + deserializeAnimationTreeNode(child, childJson); + node.children.push_back(child); + } + } +} + +nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity) +{ + auto &at = entity.get(); + nlohmann::json json; + json["enabled"] = at.enabled; + json["useRootMotion"] = at.useRootMotion; + json["root"] = serializeAnimationTreeNode(at.root); + 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); + if (json.contains("root")) + deserializeAnimationTreeNode(at.root, json["root"]); at.dirty = true; - entity.set(at); } diff --git a/src/features/editScene/ui/AnimationTreeEditor.cpp b/src/features/editScene/ui/AnimationTreeEditor.cpp index 7bd1ac6..926f326 100644 --- a/src/features/editScene/ui/AnimationTreeEditor.cpp +++ b/src/features/editScene/ui/AnimationTreeEditor.cpp @@ -7,84 +7,511 @@ AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr) { } -bool AnimationTreeEditor::renderComponent(flecs::entity entity, - AnimationTreeComponent &at) +std::vector AnimationTreeEditor::getAvailableAnimations( + flecs::entity entity) +{ + std::vector result; + Ogre::Entity *ent = + AnimationTreeSystem::findAnimatedEntity(entity); + if (ent && ent->hasSkeleton()) { + Ogre::AnimationStateSet *states = + ent->getAllAnimationStates(); + if (states) { + for (const auto &pair : states->getAnimationStates()) + result.push_back(pair.first); + } + } + return result; +} + +static ImU32 nodeTypeColorU32(const Ogre::String &type) +{ + if (type == "output") + return IM_COL32(0xFF, 0x88, 0x88, 0xFF); + if (type == "stateMachine") + return IM_COL32(0x88, 0xBB, 0xFF, 0xFF); + if (type == "state") + return IM_COL32(0x88, 0xFF, 0x88, 0xFF); + if (type == "speed") + return IM_COL32(0xFF, 0xEE, 0x88, 0xFF); + return IM_COL32(0xFF, 0xFF, 0xFF, 0xFF); +} + +static void makeLabel(const AnimationTreeNode &node, char *buf, + size_t bufSize) +{ + if (node.type == "animation") { + snprintf(buf, bufSize, "[%s] %s", node.type.c_str(), + node.animationName.empty() ? + "(none)" : + node.animationName.c_str()); + } else if (node.type == "speed") { + snprintf(buf, bufSize, "[%s] %.2fx %s", node.type.c_str(), + node.speed, + node.name.empty() ? "" : node.name.c_str()); + } else if (node.type == "stateMachine") { + snprintf(buf, bufSize, "[%s] %s (fade %.1f)", + node.type.c_str(), node.name.c_str(), + node.fadeSpeed); + } else { + snprintf(buf, bufSize, "[%s] %s", node.type.c_str(), + node.name.empty() ? "" : node.name.c_str()); + } +} + +static int countChildrenOfType(const AnimationTreeNode &node, + const Ogre::String &type) +{ + int n = 0; + for (const auto &c : node.children) + if (c.type == type) + n++; + return n; +} + +static size_t findChildIndex(const AnimationTreeNode &parent, + const AnimationTreeNode *child) +{ + for (size_t i = 0; i < parent.children.size(); i++) + if (&parent.children[i] == child) + return i; + return (size_t)-1; +} + +void AnimationTreeEditor::queueAddChild(AnimationTreeNode *parent, + const Ogre::String &type) +{ + TreeEditOp op; + op.type = TreeEditOp::AddChild; + op.targetNode = parent; + op.childType = type; + m_editOps.push_back(op); +} + +void AnimationTreeEditor::queueRemoveNode(AnimationTreeNode *parent, + size_t childIndex) +{ + TreeEditOp op; + op.type = TreeEditOp::RemoveNode; + op.targetNode = parent; + op.childIndex = childIndex; + m_editOps.push_back(op); +} + +void AnimationTreeEditor::queueMoveNode(AnimationTreeNode *parent, + size_t childIndex, + int delta) +{ + TreeEditOp op; + op.type = TreeEditOp::MoveNode; + op.targetNode = parent; + op.childIndex = childIndex; + op.moveDelta = delta; + m_editOps.push_back(op); +} + +void AnimationTreeEditor::applyEditOps(AnimationTreeComponent &at) +{ + bool changed = false; + for (auto &op : m_editOps) { + if (!op.targetNode) + continue; + auto &vec = op.targetNode->children; + switch (op.type) { + case TreeEditOp::AddChild: { + AnimationTreeNode child; + child.type = op.childType; + if (op.childType == "stateMachine") { + child.name = "sm" + + std::to_string(countChildrenOfType( + *op.targetNode, + "stateMachine")); + child.fadeSpeed = 7.5f; + } else if (op.childType == "state") { + child.name = "state" + + std::to_string(countChildrenOfType( + *op.targetNode, "state")); + } else if (op.childType == "animation") { + child.animationName = ""; + } else if (op.childType == "speed") { + child.speed = 1.0f; + } + vec.push_back(child); + changed = true; + break; + } + case TreeEditOp::RemoveNode: { + if (op.childIndex < vec.size()) { + vec.erase(vec.begin() + op.childIndex); + changed = true; + } + break; + } + case TreeEditOp::MoveNode: { + if (op.childIndex < vec.size()) { + int newIdx = (int)op.childIndex + op.moveDelta; + if (newIdx >= 0 && + newIdx < (int)vec.size()) { + std::swap(vec[op.childIndex], + vec[newIdx]); + changed = true; + } + } + break; + } + } + } + m_editOps.clear(); + if (changed) + at.dirty = true; +} + +static bool canHaveChildren(const Ogre::String &type) +{ + return type == "output" || type == "stateMachine" || + type == "state" || type == "speed"; +} + +void AnimationTreeEditor::renderTree(AnimationTreeNode &node, + AnimationTreeNode *parent, + AnimationTreeNode *&selectedNode, + AnimationTreeNode *&selectedParent, + int &id) +{ + int myId = id++; + bool isLeaf = node.children.empty(); + ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (isLeaf) + flags |= ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_NoTreePushOnOpen; + if (selectedNode == &node) + flags |= ImGuiTreeNodeFlags_Selected; + + char label[256]; + makeLabel(node, label, sizeof(label)); + + /* Color the whole tree node label */ + ImGui::PushStyleColor(ImGuiCol_Text, + ImGui::ColorConvertU32ToFloat4( + nodeTypeColorU32(node.type))); + bool open = ImGui::TreeNodeEx( + ("##animnode" + std::to_string(myId)).c_str(), flags, + "%s", label); + ImGui::PopStyleColor(); + + if (ImGui::IsItemClicked()) { + selectedNode = &node; + selectedParent = parent; + } + + /* Right-click context menu — must be attached to TreeNodeEx + * (the last item), before we add buttons on the same line */ + if (ImGui::BeginPopupContextItem()) { + if (canHaveChildren(node.type)) { + if (node.type == "output" || + node.type == "stateMachine") { + if (ImGui::MenuItem("Add State Machine")) + queueAddChild(&node, "stateMachine"); + } + if (node.type == "stateMachine" || + node.type == "output" || + node.type == "state") { + if (ImGui::MenuItem("Add State")) + queueAddChild(&node, "state"); + } + if (node.type == "output" || + node.type == "state" || + node.type == "speed") { + if (ImGui::MenuItem("Add Animation")) + queueAddChild(&node, "animation"); + } + if (node.type == "output" || + node.type == "state") { + if (ImGui::MenuItem("Add Speed")) + queueAddChild(&node, "speed"); + } + } + if (parent) { + size_t idx = findChildIndex(*parent, &node); + if (ImGui::MenuItem("Remove")) { + queueRemoveNode(parent, idx); + if (selectedNode == &node) { + selectedNode = nullptr; + selectedParent = nullptr; + } + } + if (ImGui::MenuItem("Move Up")) + queueMoveNode(parent, idx, -1); + if (ImGui::MenuItem("Move Down")) + queueMoveNode(parent, idx, +1); + } + ImGui::EndPopup(); + } + + /* Inline buttons on the same line as the tree node label */ + ImGui::PushID(myId); + if (canHaveChildren(node.type)) { + ImGui::SameLine(); + if (ImGui::SmallButton("+")) { + ImGui::OpenPopup("AddChild"); + } + if (ImGui::BeginPopup("AddChild")) { + if (node.type == "output" || + node.type == "stateMachine") { + if (ImGui::MenuItem("State Machine")) + queueAddChild(&node, "stateMachine"); + } + if (node.type == "stateMachine" || + node.type == "output" || + node.type == "state") { + if (ImGui::MenuItem("State")) + queueAddChild(&node, "state"); + } + if (node.type == "output" || + node.type == "state" || + node.type == "speed") { + if (ImGui::MenuItem("Animation")) + queueAddChild(&node, "animation"); + } + if (node.type == "output" || + node.type == "state") { + if (ImGui::MenuItem("Speed")) + queueAddChild(&node, "speed"); + } + ImGui::EndPopup(); + } + } + if (parent) { + size_t myIndex = findChildIndex(*parent, &node); + + ImGui::SameLine(); + if (ImGui::SmallButton("-")) + queueRemoveNode(parent, myIndex); + + if (myIndex > 0) { + ImGui::SameLine(); + if (ImGui::SmallButton("^")) + queueMoveNode(parent, myIndex, -1); + } + if (myIndex + 1 < parent->children.size()) { + ImGui::SameLine(); + if (ImGui::SmallButton("v")) + queueMoveNode(parent, myIndex, +1); + } + } + ImGui::PopID(); + + /* Render children and TreePop (only when node is open) */ + if (!isLeaf && open) { + for (auto &child : node.children) + renderTree(child, &node, selectedNode, + selectedParent, id); + ImGui::TreePop(); + } +} + +void AnimationTreeEditor::renderProperties( + AnimationTreeNode *node, AnimationTreeComponent &at, + flecs::entity entity) +{ + if (!node) + return; + + ImGui::Separator(); + ImGui::Text("Node Properties"); + + bool modified = false; + + /* Type selector */ + const char *types[] = {"output", "stateMachine", "state", + "speed", "animation"}; + int typeIdx = 0; + for (int i = 0; i < IM_ARRAYSIZE(types); i++) { + if (node->type == types[i]) { + typeIdx = i; + break; + } + } + if (ImGui::Combo("Type", &typeIdx, types, + IM_ARRAYSIZE(types))) { + node->type = types[typeIdx]; + modified = true; + } + + /* Name */ + if (node->type == "stateMachine" || node->type == "state") { + char buf[256]; + strncpy(buf, node->name.c_str(), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + if (ImGui::InputText("Name", buf, sizeof(buf))) { + node->name = buf; + modified = true; + } + } + + /* Animation name */ + if (node->type == "animation") { + std::vector anims = + getAvailableAnimations(entity); + Ogre::String current = node->animationName; + Ogre::String preview = current.empty() ? "(none)" : + current; + if (ImGui::BeginCombo("Animation", preview.c_str())) { + bool noneSel = current.empty(); + if (ImGui::Selectable("(none)", noneSel)) { + node->animationName = ""; + modified = true; + } + for (const auto &name : anims) { + bool sel = (current == name); + if (ImGui::Selectable(name.c_str(), sel)) { + node->animationName = name; + modified = true; + } + } + ImGui::EndCombo(); + } + } + + /* Speed */ + if (node->type == "output" || node->type == "speed") { + if (ImGui::SliderFloat("Speed", &node->speed, -2.0f, + 10.0f, "%.2f")) + modified = true; + } + + /* Fade speed */ + if (node->type == "stateMachine") { + if (ImGui::SliderFloat("Fade Speed", &node->fadeSpeed, + 0.1f, 30.0f, "%.1f")) + modified = true; + } + + /* End transitions (for state machines) */ + if (node->type == "stateMachine" && !node->children.empty()) { + ImGui::Separator(); + ImGui::Text("End Transitions"); + ImGui::TextDisabled( + "Auto-switch state when animation ends"); + + for (const auto &child : node->children) { + if (child.type != "state") + continue; + char buf[256]; + auto it = node->endTransitions.find(child.name); + if (it != node->endTransitions.end()) + strncpy(buf, it->second.c_str(), + sizeof(buf) - 1); + else + buf[0] = '\0'; + buf[sizeof(buf) - 1] = '\0'; + + Ogre::String lbl = "On '" + child.name + + "' end →"; + if (ImGui::InputText(lbl.c_str(), buf, + sizeof(buf))) { + if (buf[0] == '\0') + node->endTransitions.erase( + child.name); + else + node->endTransitions[child.name] = + buf; + modified = true; + } + } + } + + if (modified) + at.dirty = true; +} + +void AnimationTreeEditor::renderStatePreview( + AnimationTreeComponent &at) +{ + std::vector stateMachines; + at.root.collectStateMachines(stateMachines); + if (stateMachines.empty()) + return; + + ImGui::Separator(); + ImGui::Text("State Preview"); + + for (auto *sm : stateMachines) { + if (!sm) + continue; + + auto itCurrent = at.currentStates.find(sm->name); + Ogre::String currentState = + (itCurrent != at.currentStates.end()) ? + itCurrent->second : + ""; + + Ogre::String label = sm->name + "##" + sm->name; + Ogre::String preview = currentState.empty() ? + "(none)" : + currentState; + + if (ImGui::BeginCombo(label.c_str(), preview.c_str())) { + for (const auto &child : sm->children) { + if (child.type != "state") + continue; + bool sel = (currentState == child.name); + if (ImGui::Selectable(child.name.c_str(), + sel)) { + at.currentStates[sm->name] = + child.name; + at.dirty = true; + } + } + ImGui::EndCombo(); + } + } +} + +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); - } + /* Global toggles */ + if (ImGui::Checkbox("Enabled", &at.enabled)) + modified = true; + if (ImGui::Checkbox("Use Root Motion", + &at.useRootMotion)) { + modified = true; + at.dirty = true; } - 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(); + ImGui::Separator(); - 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(); - } + /* Tree view */ + static AnimationTreeNode *selectedNode = nullptr; + static AnimationTreeNode *selectedParent = nullptr; + int id = 0; + renderTree(at.root, nullptr, selectedNode, + selectedParent, id); - /* 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"); - } + /* Apply all deferred edits. Vector reallocations may + * invalidate pointers, so clear selection afterwards. */ + bool hadEdits = !m_editOps.empty(); + applyEditOps(at); + if (hadEdits) { + selectedNode = nullptr; + selectedParent = nullptr; } + /* Node properties */ + renderProperties(selectedNode, at, entity); + + /* State preview */ + renderStatePreview(at); + ImGui::Unindent(); } diff --git a/src/features/editScene/ui/AnimationTreeEditor.hpp b/src/features/editScene/ui/AnimationTreeEditor.hpp index 3eaac15..5b2a340 100644 --- a/src/features/editScene/ui/AnimationTreeEditor.hpp +++ b/src/features/editScene/ui/AnimationTreeEditor.hpp @@ -4,9 +4,13 @@ #include "ComponentEditor.hpp" #include "../components/AnimationTree.hpp" #include +#include /** - * Editor for AnimationTreeComponent + * Editor for AnimationTreeComponent. + * + * Provides a recursive tree view for editing the animation hierarchy + * and state-machine current-state selectors for preview. */ class AnimationTreeEditor : public ComponentEditor { public: @@ -14,12 +18,42 @@ public: const char *getName() const override { return "Animation Tree"; } + /* Deferred edit operations — safe to call during UI rendering + * since they are applied only after the tree is fully drawn. */ + struct TreeEditOp { + enum OpType { AddChild, RemoveNode, MoveNode } type; + AnimationTreeNode *targetNode = nullptr; + size_t childIndex = 0; + Ogre::String childType; + int moveDelta = 0; + }; + + void queueAddChild(AnimationTreeNode *parent, + const Ogre::String &type); + void queueRemoveNode(AnimationTreeNode *parent, size_t childIndex); + void queueMoveNode(AnimationTreeNode *parent, size_t childIndex, + int delta); + protected: bool renderComponent(flecs::entity entity, AnimationTreeComponent &at) override; private: + void renderTree(AnimationTreeNode &node, + AnimationTreeNode *parent, + AnimationTreeNode *&selectedNode, + AnimationTreeNode *&selectedParent, + int &id); + void renderProperties(AnimationTreeNode *node, + AnimationTreeComponent &at, + flecs::entity entity); + void renderStatePreview(AnimationTreeComponent &at); + void applyEditOps(AnimationTreeComponent &at); + std::vector getAvailableAnimations( + flecs::entity entity); + Ogre::SceneManager *m_sceneMgr; + std::vector m_editOps; }; #endif // EDITSCENE_ANIMATIONTREEEDITOR_HPP