Character Animation
This commit is contained in:
@@ -26,6 +26,7 @@ set(EDITSCENE_SOURCES
|
||||
systems/RoomLayoutSystem.cpp
|
||||
systems/FurnitureLibrary.cpp
|
||||
systems/CharacterSlotSystem.cpp
|
||||
systems/AnimationTreeSystem.cpp
|
||||
ui/TransformEditor.cpp
|
||||
ui/RenderableEditor.cpp
|
||||
ui/PhysicsColliderEditor.cpp
|
||||
@@ -41,6 +42,7 @@ set(EDITSCENE_SOURCES
|
||||
ui/PrimitiveEditor.cpp
|
||||
ui/TriangleBufferEditor.cpp
|
||||
ui/CharacterSlotsEditor.cpp
|
||||
ui/AnimationTreeEditor.cpp
|
||||
ui/CellGridEditor.cpp
|
||||
ui/LotEditor.cpp
|
||||
ui/DistrictEditor.cpp
|
||||
@@ -61,6 +63,7 @@ set(EDITSCENE_SOURCES
|
||||
components/PrimitiveModule.cpp
|
||||
components/TriangleBufferModule.cpp
|
||||
components/CharacterSlotsModule.cpp
|
||||
components/AnimationTreeModule.cpp
|
||||
components/CellGridModule.cpp
|
||||
components/CellGridEditorsModule.cpp
|
||||
components/CellGrid.cpp
|
||||
@@ -88,6 +91,7 @@ set(EDITSCENE_HEADERS
|
||||
components/Primitive.hpp
|
||||
components/TriangleBuffer.hpp
|
||||
components/CharacterSlots.hpp
|
||||
components/AnimationTree.hpp
|
||||
components/CellGrid.hpp
|
||||
systems/EditorUISystem.hpp
|
||||
systems/CellGridSystem.hpp
|
||||
@@ -96,6 +100,7 @@ set(EDITSCENE_HEADERS
|
||||
systems/ProceduralMaterialSystem.hpp
|
||||
systems/ProceduralMeshSystem.hpp
|
||||
systems/CharacterSlotSystem.hpp
|
||||
systems/AnimationTreeSystem.hpp
|
||||
systems/ProceduralTextureSystem.hpp
|
||||
systems/StaticGeometrySystem.hpp
|
||||
systems/SceneSerializer.hpp
|
||||
@@ -121,6 +126,7 @@ set(EDITSCENE_HEADERS
|
||||
ui/PrimitiveEditor.hpp
|
||||
ui/TriangleBufferEditor.hpp
|
||||
ui/CharacterSlotsEditor.hpp
|
||||
ui/AnimationTreeEditor.hpp
|
||||
ui/CellGridEditor.hpp
|
||||
ui/LotEditor.hpp
|
||||
ui/DistrictEditor.hpp
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "systems/ProceduralMaterialSystem.hpp"
|
||||
#include "systems/ProceduralMeshSystem.hpp"
|
||||
#include "systems/CharacterSlotSystem.hpp"
|
||||
#include "systems/AnimationTreeSystem.hpp"
|
||||
#include "systems/CellGridSystem.hpp"
|
||||
#include "systems/RoomLayoutSystem.hpp"
|
||||
#include "camera/EditorCamera.hpp"
|
||||
@@ -31,6 +32,7 @@
|
||||
#include "components/Primitive.hpp"
|
||||
#include "components/TriangleBuffer.hpp"
|
||||
#include "components/CharacterSlots.hpp"
|
||||
#include "components/AnimationTree.hpp"
|
||||
#include "components/CellGrid.hpp"
|
||||
#include "components/CellGridModule.hpp"
|
||||
#include <OgreRTShaderSystem.h>
|
||||
@@ -122,6 +124,7 @@ EditorApp::~EditorApp()
|
||||
|
||||
// Release all systems
|
||||
m_characterSlotSystem.reset();
|
||||
m_animationTreeSystem.reset();
|
||||
m_proceduralMeshSystem.reset();
|
||||
m_proceduralMaterialSystem.reset();
|
||||
m_proceduralTextureSystem.reset();
|
||||
@@ -231,6 +234,11 @@ void EditorApp::setup()
|
||||
m_world, m_sceneMgr);
|
||||
m_characterSlotSystem->initialize();
|
||||
|
||||
// Setup AnimationTree system
|
||||
m_animationTreeSystem = std::make_unique<AnimationTreeSystem>(
|
||||
m_world, m_sceneMgr);
|
||||
m_animationTreeSystem->initialize();
|
||||
|
||||
// Setup CellGrid system
|
||||
m_cellGridSystem =
|
||||
std::make_unique<CellGridSystem>(m_world, m_sceneMgr);
|
||||
@@ -300,6 +308,9 @@ void EditorApp::setupECS()
|
||||
// Register CharacterSlots component
|
||||
m_world.component<CharacterSlotsComponent>();
|
||||
|
||||
// Register AnimationTree component
|
||||
m_world.component<AnimationTreeComponent>();
|
||||
|
||||
// Register CellGrid/Town components
|
||||
CellGridModule::registerComponents(m_world);
|
||||
}
|
||||
@@ -465,6 +476,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_characterSlotSystem->update();
|
||||
}
|
||||
|
||||
// Update AnimationTree system
|
||||
if (m_animationTreeSystem) {
|
||||
m_animationTreeSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
// Update ProceduralMesh system
|
||||
if (m_proceduralMeshSystem) {
|
||||
m_proceduralMeshSystem->update();
|
||||
|
||||
@@ -22,6 +22,7 @@ class ProceduralTextureSystem;
|
||||
class ProceduralMaterialSystem;
|
||||
class ProceduralMeshSystem;
|
||||
class CharacterSlotSystem;
|
||||
class AnimationTreeSystem;
|
||||
class CellGridSystem;
|
||||
class RoomLayoutSystem;
|
||||
|
||||
@@ -118,6 +119,7 @@ private:
|
||||
std::unique_ptr<ProceduralMaterialSystem> m_proceduralMaterialSystem;
|
||||
std::unique_ptr<ProceduralMeshSystem> m_proceduralMeshSystem;
|
||||
std::unique_ptr<CharacterSlotSystem> m_characterSlotSystem;
|
||||
std::unique_ptr<AnimationTreeSystem> m_animationTreeSystem;
|
||||
std::unique_ptr<CellGridSystem> m_cellGridSystem;
|
||||
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
|
||||
|
||||
|
||||
22
src/features/editScene/components/AnimationTree.hpp
Normal file
22
src/features/editScene/components/AnimationTree.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef EDITSCENE_ANIMATIONTREE_HPP
|
||||
#define EDITSCENE_ANIMATIONTREE_HPP
|
||||
#pragma once
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Animation tree component for playing skeletal animations on entities.
|
||||
* Works with any entity that has an Ogre::Entity with a skeleton
|
||||
* (either via RenderableComponent or CharacterSlotsComponent).
|
||||
*/
|
||||
struct AnimationTreeComponent {
|
||||
Ogre::String currentAnimation;
|
||||
float speed = 1.0f;
|
||||
bool loop = true;
|
||||
bool enabled = true;
|
||||
bool useRootMotion = false;
|
||||
bool dirty = true;
|
||||
|
||||
AnimationTreeComponent() = default;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREE_HPP
|
||||
30
src/features/editScene/components/AnimationTreeModule.cpp
Normal file
30
src/features/editScene/components/AnimationTreeModule.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#include "AnimationTree.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/AnimationTreeEditor.hpp"
|
||||
#include "Transform.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
|
||||
AnimationTreeComponent, AnimationTreeEditor)
|
||||
{
|
||||
registry.registerComponent<AnimationTreeComponent>(
|
||||
"Animation Tree", "Rendering",
|
||||
std::make_unique<AnimationTreeEditor>(sceneMgr),
|
||||
/* Adder */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
if (!e.has<TransformComponent>()) {
|
||||
TransformComponent transform;
|
||||
transform.node =
|
||||
sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode();
|
||||
e.set<TransformComponent>(transform);
|
||||
}
|
||||
AnimationTreeComponent at;
|
||||
at.dirty = true;
|
||||
e.set<AnimationTreeComponent>(at);
|
||||
},
|
||||
/* Remover */
|
||||
[sceneMgr](flecs::entity e) {
|
||||
(void)sceneMgr;
|
||||
e.remove<AnimationTreeComponent>();
|
||||
});
|
||||
}
|
||||
@@ -14,6 +14,9 @@ struct CharacterSlotsComponent {
|
||||
std::unordered_map<Ogre::String, Ogre::String> slots;
|
||||
bool dirty = true;
|
||||
|
||||
/* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */
|
||||
Ogre::Entity *masterEntity = nullptr;
|
||||
|
||||
CharacterSlotsComponent() = default;
|
||||
};
|
||||
|
||||
|
||||
358
src/features/editScene/systems/AnimationTreeSystem.cpp
Normal file
358
src/features/editScene/systems/AnimationTreeSystem.cpp
Normal file
@@ -0,0 +1,358 @@
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include <OgreEntity.h>
|
||||
#include <OgreLogManager.h>
|
||||
#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)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
{
|
||||
m_world.observer<AnimationTreeComponent>("AnimationTreeCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e, AnimationTreeComponent &) {
|
||||
teardownEntity(e);
|
||||
});
|
||||
}
|
||||
|
||||
AnimationTreeSystem::~AnimationTreeSystem()
|
||||
{
|
||||
std::vector<flecs::entity_t> toRemove;
|
||||
for (auto &pair : m_states)
|
||||
toRemove.push_back(pair.first);
|
||||
for (auto id : toRemove)
|
||||
teardownEntity(m_world.entity(id));
|
||||
}
|
||||
|
||||
void AnimationTreeSystem::initialize()
|
||||
{
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
Ogre::Entity *AnimationTreeSystem::findAnimatedEntity(flecs::entity e)
|
||||
{
|
||||
if (!e.is_alive())
|
||||
return nullptr;
|
||||
|
||||
if (e.has<CharacterSlotsComponent>()) {
|
||||
auto &cs = e.get<CharacterSlotsComponent>();
|
||||
if (cs.masterEntity && cs.masterEntity->hasSkeleton())
|
||||
return cs.masterEntity;
|
||||
}
|
||||
|
||||
if (e.has<RenderableComponent>()) {
|
||||
auto &rc = e.get<RenderableComponent>();
|
||||
if (rc.entity && rc.entity->hasSkeleton())
|
||||
return rc.entity;
|
||||
}
|
||||
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &t = e.get<TransformComponent>();
|
||||
if (t.node) {
|
||||
for (unsigned int i = 0;
|
||||
i < t.node->numAttachedObjects(); ++i) {
|
||||
Ogre::MovableObject *obj =
|
||||
t.node->getAttachedObject(i);
|
||||
if (obj->getMovableType() == "Entity") {
|
||||
Ogre::Entity *ent =
|
||||
static_cast<Ogre::Entity *>(obj);
|
||||
if (ent->hasSkeleton())
|
||||
return ent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AnimationTreeSystem::setupEntity(flecs::entity e,
|
||||
AnimationTreeComponent &at)
|
||||
{
|
||||
Ogre::Entity *ent = findAnimatedEntity(e);
|
||||
if (!ent || !ent->hasSkeleton()) {
|
||||
teardownEntity(e);
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = m_states.find(e.id());
|
||||
if (it != m_states.end() && it->second.animState) {
|
||||
if (it->second.currentAnimName != at.currentAnimation) {
|
||||
it->second.animState->setEnabled(false);
|
||||
if (it->second.rootMotion)
|
||||
it->second.rootMotion->unapply();
|
||||
it->second.animState = nullptr;
|
||||
it->second.rootMotion.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if (at.currentAnimation.empty()) {
|
||||
teardownEntity(e);
|
||||
return;
|
||||
}
|
||||
|
||||
EntityAnimState &state = m_states[e.id()];
|
||||
|
||||
try {
|
||||
state.animState = ent->getAnimationState(at.currentAnimation);
|
||||
} catch (const Ogre::Exception &) {
|
||||
state.animState = nullptr;
|
||||
state.rootMotion.reset();
|
||||
state.currentAnimName.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentAnimName = at.currentAnimation;
|
||||
state.ogreEntity = ent;
|
||||
|
||||
state.animState->setEnabled(at.enabled);
|
||||
state.animState->setLoop(at.loop);
|
||||
|
||||
if (at.useRootMotion) {
|
||||
if (!state.rootMotion || !state.rootMotion->hasTrack()) {
|
||||
state.rootMotion = std::make_unique<RootMotionTracker>(
|
||||
state.animState, ent);
|
||||
}
|
||||
} else {
|
||||
if (state.rootMotion) {
|
||||
state.rootMotion->unapply();
|
||||
state.rootMotion.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationTreeSystem::teardownEntity(flecs::entity e)
|
||||
{
|
||||
auto it = m_states.find(e.id());
|
||||
if (it == m_states.end())
|
||||
return;
|
||||
|
||||
if (it->second.animState)
|
||||
it->second.animState->setEnabled(false);
|
||||
if (it->second.rootMotion)
|
||||
it->second.rootMotion->unapply();
|
||||
|
||||
m_states.erase(it);
|
||||
}
|
||||
|
||||
void AnimationTreeSystem::update(float deltaTime)
|
||||
{
|
||||
if (!m_initialized)
|
||||
return;
|
||||
|
||||
m_world.query<AnimationTreeComponent>().each(
|
||||
[this, deltaTime](flecs::entity e, AnimationTreeComponent &at) {
|
||||
if (at.dirty) {
|
||||
setupEntity(e, at);
|
||||
at.dirty = false;
|
||||
}
|
||||
|
||||
auto it = m_states.find(e.id());
|
||||
if (it == m_states.end())
|
||||
return;
|
||||
|
||||
EntityAnimState &state = it->second;
|
||||
if (!state.animState)
|
||||
return;
|
||||
|
||||
/* Sync runtime toggles */
|
||||
if (state.animState->getEnabled() != at.enabled) {
|
||||
state.animState->setEnabled(at.enabled);
|
||||
if (!at.enabled && state.rootMotion)
|
||||
state.rootMotion->unapply();
|
||||
}
|
||||
if (state.animState->getLoop() != at.loop)
|
||||
state.animState->setLoop(at.loop);
|
||||
|
||||
if (!at.enabled)
|
||||
return;
|
||||
|
||||
float dt = deltaTime * at.speed;
|
||||
if (dt == 0.0f)
|
||||
return;
|
||||
|
||||
float lastTime = state.animState->getTimePosition();
|
||||
state.animState->addTime(dt);
|
||||
float thisTime = state.animState->getTimePosition();
|
||||
float length = state.animState->getLength();
|
||||
bool loop = state.animState->getLoop();
|
||||
|
||||
int loops = 0;
|
||||
if (loop && length > 0.0f) {
|
||||
loops = (int)std::round(
|
||||
(lastTime + dt - thisTime) / length);
|
||||
}
|
||||
|
||||
if (at.useRootMotion && state.rootMotion)
|
||||
state.rootMotion->apply(loops, thisTime);
|
||||
});
|
||||
}
|
||||
81
src/features/editScene/systems/AnimationTreeSystem.hpp
Normal file
81
src/features/editScene/systems/AnimationTreeSystem.hpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#ifndef EDITSCENE_ANIMATIONTREESYSTEM_HPP
|
||||
#define EDITSCENE_ANIMATIONTREESYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "../components/AnimationTree.hpp"
|
||||
|
||||
/**
|
||||
* RootMotionTracker applies transforms from a NodeAnimationTrack to an
|
||||
* Entity's parent SceneNode instead of to its Skeleton's Bone.
|
||||
*
|
||||
* Based on OGRE Sample_SkeletalAnimation's RootMotionApplier.
|
||||
*/
|
||||
class RootMotionTracker {
|
||||
public:
|
||||
RootMotionTracker(Ogre::AnimationState *animationState,
|
||||
Ogre::Entity *entity);
|
||||
|
||||
void apply(int loops, float thisTime);
|
||||
void unapply();
|
||||
|
||||
bool hasTrack() const { return mTrack != nullptr; }
|
||||
|
||||
private:
|
||||
Ogre::Entity *mEntity;
|
||||
Ogre::NodeAnimationTrack *mTrack;
|
||||
|
||||
Ogre::Vector3 mRootBindingPosition;
|
||||
Ogre::Quaternion mRootBindingOrientation;
|
||||
Ogre::Quaternion mRootBindingOrientationInverse;
|
||||
|
||||
Ogre::Vector3 mLoopTranslation;
|
||||
Ogre::Quaternion mLoopRotation;
|
||||
Ogre::Quaternion mLoopRotationInverse;
|
||||
|
||||
Ogre::Vector3 mAppliedTranslation;
|
||||
Ogre::Quaternion mAppliedRotation;
|
||||
};
|
||||
|
||||
/**
|
||||
* System that manages skeletal animation playback and root motion
|
||||
* for entities with AnimationTreeComponent.
|
||||
*/
|
||||
class AnimationTreeSystem {
|
||||
public:
|
||||
AnimationTreeSystem(flecs::world &world, Ogre::SceneManager *sceneMgr);
|
||||
~AnimationTreeSystem();
|
||||
|
||||
void initialize();
|
||||
void update(float deltaTime);
|
||||
|
||||
/**
|
||||
* Find the Ogre entity with a skeleton for a given flecs entity.
|
||||
* Checks CharacterSlotsComponent master, RenderableComponent, then
|
||||
* attached objects on the transform node.
|
||||
*/
|
||||
static Ogre::Entity *findAnimatedEntity(flecs::entity e);
|
||||
|
||||
private:
|
||||
struct EntityAnimState {
|
||||
Ogre::AnimationState *animState = nullptr;
|
||||
Ogre::Entity *ogreEntity = nullptr;
|
||||
std::unique_ptr<RootMotionTracker> rootMotion;
|
||||
Ogre::String currentAnimName;
|
||||
};
|
||||
|
||||
void setupEntity(flecs::entity e, AnimationTreeComponent &at);
|
||||
void teardownEntity(flecs::entity e);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
bool m_initialized = false;
|
||||
|
||||
std::unordered_map<flecs::entity_t, EntityAnimState> m_states;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREESYSTEM_HPP
|
||||
@@ -215,6 +215,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
|
||||
masterEnt = m_sceneMgr->createEntity(cs.slots.at(masterSlot));
|
||||
transform.node->attachObject(masterEnt);
|
||||
m_entities[e.id()].parts[masterSlot] = masterEnt;
|
||||
cs.masterEntity = masterEnt;
|
||||
std::cout << " master loaded: " << masterEnt->getName()
|
||||
<< std::endl;
|
||||
} catch (const Ogre::Exception &ex) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "../components/TriangleBuffer.hpp"
|
||||
#include "../components/LodSettings.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include "../components/CellGrid.hpp"
|
||||
#include "../ui/TransformEditor.hpp"
|
||||
#include "../ui/RenderableEditor.hpp"
|
||||
@@ -412,6 +413,8 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
indicators += " [Tex]";
|
||||
if (entity.has<ProceduralMaterialComponent>())
|
||||
indicators += " [Mat]";
|
||||
if (entity.has<AnimationTreeComponent>())
|
||||
indicators += " [Anim]";
|
||||
|
||||
snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(),
|
||||
indicators.c_str(), (unsigned long long)entity.id());
|
||||
@@ -632,7 +635,14 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
m_componentRegistry.render<CharacterSlotsComponent>(entity, cs);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
|
||||
// Render AnimationTree if present
|
||||
if (entity.has<AnimationTreeComponent>()) {
|
||||
auto &at = entity.get_mut<AnimationTreeComponent>();
|
||||
m_componentRegistry.render<AnimationTreeComponent>(entity, at);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render CellGrid if present
|
||||
if (entity.has<CellGridComponent>()) {
|
||||
auto &grid = entity.get_mut<CellGridComponent>();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "../components/Primitive.hpp"
|
||||
#include "../components/TriangleBuffer.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include "../components/CellGrid.hpp"
|
||||
#include "../components/GeneratedPhysicsTag.hpp"
|
||||
#include "EditorUISystem.hpp"
|
||||
@@ -182,6 +183,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
if (entity.has<CharacterSlotsComponent>()) {
|
||||
json["characterSlots"] = serializeCharacterSlots(entity);
|
||||
}
|
||||
|
||||
if (entity.has<AnimationTreeComponent>()) {
|
||||
json["animationTree"] = serializeAnimationTree(entity);
|
||||
}
|
||||
|
||||
// CellGrid/Town components
|
||||
if (entity.has<CellGridComponent>()) {
|
||||
@@ -303,6 +308,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit
|
||||
deserializeCharacterSlots(entity, json["characterSlots"]);
|
||||
}
|
||||
|
||||
if (json.contains("animationTree")) {
|
||||
deserializeAnimationTree(entity, json["animationTree"]);
|
||||
}
|
||||
|
||||
if (json.contains("triangleBuffer")) {
|
||||
deserializeTriangleBuffer(entity, json["triangleBuffer"]);
|
||||
}
|
||||
@@ -1299,6 +1308,34 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, const nloh
|
||||
entity.set<CharacterSlotsComponent>(cs);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity)
|
||||
{
|
||||
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;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json)
|
||||
{
|
||||
AnimationTreeComponent at;
|
||||
|
||||
at.currentAnimation = json.value("currentAnimation", "");
|
||||
at.speed = json.value("speed", 1.0f);
|
||||
at.loop = json.value("loop", true);
|
||||
at.enabled = json.value("enabled", true);
|
||||
at.useRootMotion = json.value("useRootMotion", false);
|
||||
at.dirty = true;
|
||||
|
||||
entity.set<AnimationTreeComponent>(at);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// CellGrid/Town Component Serialization
|
||||
|
||||
@@ -55,6 +55,7 @@ private:
|
||||
nlohmann::json serializePrimitive(flecs::entity entity);
|
||||
nlohmann::json serializeTriangleBuffer(flecs::entity entity);
|
||||
nlohmann::json serializeCharacterSlots(flecs::entity entity);
|
||||
nlohmann::json serializeAnimationTree(flecs::entity entity);
|
||||
|
||||
// CellGrid/Town component serialization
|
||||
nlohmann::json serializeCellGrid(flecs::entity entity);
|
||||
@@ -83,6 +84,7 @@ private:
|
||||
void deserializePrimitive(flecs::entity entity, const nlohmann::json& json);
|
||||
void deserializeTriangleBuffer(flecs::entity entity, const nlohmann::json& json);
|
||||
void deserializeCharacterSlots(flecs::entity entity, const nlohmann::json& json);
|
||||
void deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json);
|
||||
|
||||
// CellGrid/Town component deserialization
|
||||
void deserializeCellGrid(flecs::entity entity, const nlohmann::json& json);
|
||||
|
||||
92
src/features/editScene/ui/AnimationTreeEditor.cpp
Normal file
92
src/features/editScene/ui/AnimationTreeEditor.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "AnimationTreeEditor.hpp"
|
||||
#include "../systems/AnimationTreeSystem.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr)
|
||||
: m_sceneMgr(sceneMgr)
|
||||
{
|
||||
}
|
||||
|
||||
bool AnimationTreeEditor::renderComponent(flecs::entity entity,
|
||||
AnimationTreeComponent &at)
|
||||
{
|
||||
bool modified = false;
|
||||
(void)entity;
|
||||
|
||||
if (ImGui::CollapsingHeader("Animation Tree",
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::Indent();
|
||||
|
||||
Ogre::Entity *ent =
|
||||
AnimationTreeSystem::findAnimatedEntity(entity);
|
||||
std::vector<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);
|
||||
}
|
||||
}
|
||||
|
||||
if (animNames.empty()) {
|
||||
ImGui::TextDisabled("No skeleton found on entity");
|
||||
} else {
|
||||
/* Animation selector */
|
||||
Ogre::String currentAnim = at.currentAnimation;
|
||||
Ogre::String preview =
|
||||
currentAnim.empty() ? "(none)" : currentAnim;
|
||||
if (ImGui::BeginCombo("Animation", preview.c_str())) {
|
||||
bool noneSelected = currentAnim.empty();
|
||||
if (ImGui::Selectable("(none)", noneSelected)) {
|
||||
at.currentAnimation = "";
|
||||
modified = true;
|
||||
at.dirty = true;
|
||||
}
|
||||
if (noneSelected)
|
||||
ImGui::SetItemDefaultFocus();
|
||||
|
||||
for (const auto &name : animNames) {
|
||||
bool isSelected = (currentAnim == name);
|
||||
if (ImGui::Selectable(name.c_str(), isSelected)) {
|
||||
at.currentAnimation = name;
|
||||
modified = true;
|
||||
at.dirty = true;
|
||||
}
|
||||
if (isSelected)
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
/* Speed */
|
||||
if (ImGui::SliderFloat("Speed", &at.speed, -2.0f, 5.0f,
|
||||
"%.2f"))
|
||||
modified = true;
|
||||
|
||||
/* Loop */
|
||||
if (ImGui::Checkbox("Loop", &at.loop))
|
||||
modified = true;
|
||||
|
||||
/* Enabled (play/pause) */
|
||||
if (ImGui::Checkbox("Enabled", &at.enabled))
|
||||
modified = true;
|
||||
|
||||
/* Root motion */
|
||||
if (ImGui::Checkbox("Use Root Motion", &at.useRootMotion)) {
|
||||
modified = true;
|
||||
at.dirty = true;
|
||||
}
|
||||
|
||||
if (!at.currentAnimation.empty()) {
|
||||
ImGui::Text("State: %s",
|
||||
at.enabled ? "Playing" : "Paused");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
25
src/features/editScene/ui/AnimationTreeEditor.hpp
Normal file
25
src/features/editScene/ui/AnimationTreeEditor.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef EDITSCENE_ANIMATIONTREEEDITOR_HPP
|
||||
#define EDITSCENE_ANIMATIONTREEEDITOR_HPP
|
||||
#pragma once
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include <OgreSceneManager.h>
|
||||
|
||||
/**
|
||||
* Editor for AnimationTreeComponent
|
||||
*/
|
||||
class AnimationTreeEditor : public ComponentEditor<AnimationTreeComponent> {
|
||||
public:
|
||||
explicit AnimationTreeEditor(Ogre::SceneManager *sceneMgr);
|
||||
|
||||
const char *getName() const override { return "Animation Tree"; }
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
AnimationTreeComponent &at) override;
|
||||
|
||||
private:
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREEEDITOR_HPP
|
||||
Reference in New Issue
Block a user