Animation Tree was implemented
This commit is contained in:
@@ -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
|
||||
|
||||
9
src/features/editScene/components/AnimationTree.cpp
Normal file
9
src/features/editScene/components/AnimationTree.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#include "AnimationTree.hpp"
|
||||
|
||||
AnimationTreeComponent::AnimationTreeComponent()
|
||||
: root()
|
||||
, enabled(true)
|
||||
, useRootMotion(false)
|
||||
, dirty(true)
|
||||
{
|
||||
}
|
||||
@@ -2,21 +2,158 @@
|
||||
#define EDITSCENE_ANIMATIONTREE_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* 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<AnimationTreeNode> children;
|
||||
/* For stateMachine nodes: auto-transition when animation ends */
|
||||
std::unordered_map<Ogre::String, Ogre::String> 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<AnimationTreeNode *> &out)
|
||||
{
|
||||
if (type == "stateMachine")
|
||||
out.push_back(this);
|
||||
for (auto &child : children)
|
||||
child.collectStateMachines(out);
|
||||
}
|
||||
|
||||
void collectStateMachines(
|
||||
std::vector<const AnimationTreeNode *> &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<Ogre::String, Ogre::String> currentStates;
|
||||
|
||||
AnimationTreeComponent();
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREE_HPP
|
||||
|
||||
@@ -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<TransformComponent>(transform);
|
||||
}
|
||||
AnimationTreeComponent at;
|
||||
AnimationTreeComponent at = createDefaultTree();
|
||||
at.dirty = true;
|
||||
e.set<AnimationTreeComponent>(at);
|
||||
},
|
||||
|
||||
@@ -7,171 +7,6 @@
|
||||
#include <OgreSceneNode.h>
|
||||
#include <cmath>
|
||||
|
||||
//=============================================================================
|
||||
// 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<RootMotionTracker>(
|
||||
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<AnimationTreeComponent>().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<Ogre::String> 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<Ogre::String> 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<AnimationTreeComponent>())
|
||||
return;
|
||||
auto &at = e.get_mut<AnimationTreeComponent>();
|
||||
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<AnimationTreeComponent>())
|
||||
return "";
|
||||
auto &at = e.get<AnimationTreeComponent>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6,44 +6,18 @@
|
||||
#include <Ogre.h>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <set>
|
||||
|
||||
#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<Ogre::String> fadeIn;
|
||||
std::set<Ogre::String> fadeOut;
|
||||
std::unordered_map<Ogre::String, float> weights;
|
||||
};
|
||||
|
||||
struct EntityAnimTreeState {
|
||||
Ogre::Entity *ogreEntity = nullptr;
|
||||
std::unique_ptr<RootMotionTracker> rootMotion;
|
||||
Ogre::String currentAnimName;
|
||||
Ogre::Bone *rootBone = nullptr;
|
||||
Ogre::Vector3 rootBindingPosition;
|
||||
Ogre::Quaternion rootBindingOrientation;
|
||||
Ogre::Vector3 rootBindingScale;
|
||||
|
||||
std::unordered_map<Ogre::String, AnimationRuntimeInfo> animations;
|
||||
std::unordered_map<Ogre::String, FadeInfo> fadeStates;
|
||||
/* Track previous currentStates to detect external changes */
|
||||
std::unordered_map<Ogre::String, Ogre::String> prevStates;
|
||||
};
|
||||
|
||||
struct AnimEvalData {
|
||||
float weight = 0.0f;
|
||||
float timeDelta = 0.0f;
|
||||
};
|
||||
|
||||
struct EvalContext {
|
||||
float deltaTime = 0.0f;
|
||||
std::unordered_map<Ogre::String, AnimEvalData> animData;
|
||||
std::vector<std::pair<Ogre::String, Ogre::String>> 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<flecs::entity_t, EntityAnimState> m_states;
|
||||
std::unordered_map<flecs::entity_t, EntityAnimTreeState> m_states;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREESYSTEM_HPP
|
||||
|
||||
@@ -1308,31 +1308,76 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, const nloh
|
||||
entity.set<CharacterSlotsComponent>(cs);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity)
|
||||
static nlohmann::json serializeAnimationTreeNode(
|
||||
const AnimationTreeNode &node)
|
||||
{
|
||||
auto& at = entity.get<AnimationTreeComponent>();
|
||||
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<std::string>();
|
||||
}
|
||||
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<AnimationTreeComponent>();
|
||||
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<AnimationTreeComponent>(at);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,84 +7,511 @@ AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr)
|
||||
{
|
||||
}
|
||||
|
||||
bool AnimationTreeEditor::renderComponent(flecs::entity entity,
|
||||
AnimationTreeComponent &at)
|
||||
std::vector<Ogre::String> AnimationTreeEditor::getAvailableAnimations(
|
||||
flecs::entity entity)
|
||||
{
|
||||
std::vector<Ogre::String> 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<Ogre::String> 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<AnimationTreeNode *> 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<Ogre::String> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include <OgreSceneManager.h>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* 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<AnimationTreeComponent> {
|
||||
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<Ogre::String> getAvailableAnimations(
|
||||
flecs::entity entity);
|
||||
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
std::vector<TreeEditOp> m_editOps;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREEEDITOR_HPP
|
||||
|
||||
Reference in New Issue
Block a user