From 998984f75a390d31c569f6aba1db5cf9809d0c59 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Wed, 29 Apr 2026 18:45:37 +0300 Subject: [PATCH] Root motion fixed now --- .../editScene/components/Character.hpp | 6 ++ src/features/editScene/lua/LuaState.cpp | 2 +- src/features/editScene/physics/physics.cpp | 18 ++++- src/features/editScene/physics/physics.h | 7 ++ .../editScene/systems/AnimationTreeSystem.cpp | 73 +++++++++++++------ .../editScene/systems/AnimationTreeSystem.hpp | 10 +++ .../editScene/systems/CharacterSystem.cpp | 22 ++++-- .../editScene/systems/PathFollowingSystem.cpp | 37 ++++++---- .../systems/PlayerControllerSystem.cpp | 26 +++---- 9 files changed, 141 insertions(+), 60 deletions(-) diff --git a/src/features/editScene/components/Character.hpp b/src/features/editScene/components/Character.hpp index 32f85c5..21bd1e9 100644 --- a/src/features/editScene/components/Character.hpp +++ b/src/features/editScene/components/Character.hpp @@ -37,6 +37,12 @@ struct CharacterComponent { /* Dirty flag — triggers rebuild of the Jolt character */ bool dirty = true; + /* When true, the scene node position is driven by root motion + * (AnimationTreeSystem), not by physics. The physics character + * position is synced to match the scene node each frame, and + * physics does NOT write its position back to the scene node. */ + bool useRootMotion = false; + /* Floor detection: raycast downward to find ground before enabling gravity */ bool hasFloor = false; float floorCheckDistance = 2.0f; diff --git a/src/features/editScene/lua/LuaState.cpp b/src/features/editScene/lua/LuaState.cpp index dce55eb..1b7c7db 100644 --- a/src/features/editScene/lua/LuaState.cpp +++ b/src/features/editScene/lua/LuaState.cpp @@ -165,7 +165,7 @@ void LuaState::lateSetup() { Ogre::DataStreamPtr stream = Ogre::ResourceGroupManager::getSingleton().openResource( - "data.lua", "LuaScripts"); + "data2.lua", "LuaScripts"); std::cout << "stream: " << stream->getAsString() << "\n"; if (luaL_dostring(L, stream->getAsString().c_str()) != LUA_OK) { std::cout << "error: " << lua_tostring(L, -1) << "\n"; diff --git a/src/features/editScene/physics/physics.cpp b/src/features/editScene/physics/physics.cpp index 60dd254..4eb424a 100644 --- a/src/features/editScene/physics/physics.cpp +++ b/src/features/editScene/physics/physics.cpp @@ -790,10 +790,10 @@ public: node->_setDerivedOrientation(JoltPhysics::convert(q)); } for (JPH::Character *ch : characters) { - if (body_interface.IsAdded(ch->GetBodyID())) { + JPH::BodyID bID = ch->GetBodyID(); + if (body_interface.IsAdded(bID)) { ch->PostSimulation(0.1f); - Ogre::SceneNode *node = - id2node[ch->GetBodyID()]; + Ogre::SceneNode *node = id2node[bID]; if (node) node->_setDerivedPosition( JoltPhysics::convert( @@ -1633,6 +1633,13 @@ public: return it->second; return nullptr; } + void setRootMotionCharacter(JPH::BodyID id, bool enabled) + { + (void)id; + (void)enabled; + /* No longer needed - root motion drives physics velocity, + * and physics writes position back to scene node normally. */ + } }; void physics() @@ -1976,5 +1983,10 @@ JoltPhysicsWrapper::getSceneNodeFromBodyID(JPH::BodyID id) const return phys->getSceneNodeFromBodyID(id); } +void JoltPhysicsWrapper::setRootMotionCharacter(JPH::BodyID id, bool enabled) +{ + phys->setRootMotionCharacter(id, enabled); +} + template <> JoltPhysicsWrapper *Ogre::Singleton::msSingleton = 0; diff --git a/src/features/editScene/physics/physics.h b/src/features/editScene/physics/physics.h index 100c802..bc08ade 100644 --- a/src/features/editScene/physics/physics.h +++ b/src/features/editScene/physics/physics.h @@ -230,5 +230,12 @@ public: bool bodyIsCharacter(JPH::BodyID id) const; void destroyCharacter(std::shared_ptr ch); Ogre::SceneNode *getSceneNodeFromBodyID(JPH::BodyID id) const; + + /* Mark a character body as root-motion-driven. + * When true, Physics::update() will NOT write the character's + * position back to the scene node after the physics step, + * because the scene node position is driven by root motion + * from AnimationTreeSystem. */ + void setRootMotionCharacter(JPH::BodyID id, bool enabled); }; #endif diff --git a/src/features/editScene/systems/AnimationTreeSystem.cpp b/src/features/editScene/systems/AnimationTreeSystem.cpp index d311c25..d4ae7e4 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.cpp +++ b/src/features/editScene/systems/AnimationTreeSystem.cpp @@ -300,6 +300,12 @@ void AnimationTreeSystem::update(float deltaTime) flecs::entity e, AnimationTreeComponent &at) { + /* Reset root-motion velocity for all entities. + * Root motion velocity will be set below only for + * entities with active root motion. */ + if (e.has()) + e.get_mut().linearVelocity = + Ogre::Vector3::ZERO; resolveTemplate(at); if (at.dirty) { @@ -369,39 +375,58 @@ void AnimationTreeSystem::update(float deltaTime) 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; + + Ogre::Vector3 delta; + if (info.hasPrevRootPos) { + /* + * Compute delta from previous + * root bone position. Detect + * animation wrapping by checking + * if time decreased (wrapped + * around). When wrapping, add + * the loop translation to + * compensate for the jump from + * end back to start. + */ + delta = thisPos - + info.prevRootPos; + if (thisTime < lastTime) { + /* Animation wrapped */ + delta += + info.loopTranslation; + } + } else { + delta = Ogre::Vector3::ZERO; + } + info.prevRootPos = thisPos; + info.hasPrevRootPos = true; + totalRootMotion += delta * data.weight; } } } if (at.useRootMotion && sceneNode) { + /* + * Compute root motion velocity from the animation + * displacement. Do NOT move the scene node directly - + * the physics character's velocity drives movement, + * and physics writes the position back to the scene + * node naturally. This avoids jitter caused by + * teleporting the physics character to match a + * root-motion-driven scene node position. + */ if (e.has()) { auto &cc = e.get_mut(); + cc.useRootMotion = true; if (deltaTime > 0.0000001f) { float safeDelta = Ogre::Math::Clamp( deltaTime, 0.005f, 0.99f); @@ -421,9 +446,6 @@ void AnimationTreeSystem::update(float deltaTime) cc.linearVelocity.y, -10.5f, 10.0f); } - } else { - sceneNode->translate(totalRootMotion, - Ogre::Node::TS_LOCAL); } } @@ -637,9 +659,18 @@ void AnimationTreeSystem::setStateInternal(flecs::entity e, auto itAnim = state.animations.find( animNode->animationName); if (itAnim != state.animations.end() && - itAnim->second.ogreAnimState) + itAnim->second.ogreAnimState) { itAnim->second.ogreAnimState ->setTimePosition(0.0f); + /* Reset root motion tracking + * so the first frame of the + * new animation doesn't + * produce a large delta from + * the previous animation's + * root position. */ + itAnim->second.hasPrevRootPos = + false; + } } } } diff --git a/src/features/editScene/systems/AnimationTreeSystem.hpp b/src/features/editScene/systems/AnimationTreeSystem.hpp index 264ff7f..dd338ca 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.hpp +++ b/src/features/editScene/systems/AnimationTreeSystem.hpp @@ -51,6 +51,11 @@ private: Ogre::AnimationState *ogreAnimState = nullptr; Ogre::NodeAnimationTrack *rootTrack = nullptr; Ogre::Vector3 loopTranslation = Ogre::Vector3::ZERO; + /* Previous root bone position for root motion delta + * computation. Used to detect animation wrapping and + * compute smooth deltas across loop boundaries. */ + Ogre::Vector3 prevRootPos = Ogre::Vector3::ZERO; + bool hasPrevRootPos = false; }; struct FadeInfo { @@ -67,6 +72,11 @@ private: Ogre::Quaternion rootBindingOrientation; Ogre::Vector3 rootBindingScale; + /* Root motion unapply/reapply state */ + Ogre::Vector3 appliedRootTranslation = Ogre::Vector3::ZERO; + Ogre::Quaternion appliedRootRotation = + Ogre::Quaternion::IDENTITY; + std::unordered_map animations; std::unordered_map fadeStates; diff --git a/src/features/editScene/systems/CharacterSystem.cpp b/src/features/editScene/systems/CharacterSystem.cpp index fbb1804..806b4c2 100644 --- a/src/features/editScene/systems/CharacterSystem.cpp +++ b/src/features/editScene/systems/CharacterSystem.cpp @@ -165,9 +165,10 @@ void CharacterSystem::setupEntity(flecs::entity e, CharacterComponent &cc) cc.hasFloor = false; std::cout << "CharacterSystem::setupEntity: entity=" << e.id() << " nodePos=" << transform.node->_getDerivedPosition() - << " parent=" << (transform.node->getParent() ? - transform.node->getParent()->getName() : - "") + << " parent=" + << (transform.node->getParent() ? + transform.node->getParent()->getName() : + "") << " radius=" << cc.radius << " height=" << cc.height << " dirty=" << cc.dirty << " hasFloor=" << cc.hasFloor << std::endl; @@ -280,13 +281,24 @@ void CharacterSystem::update(float deltaTime) Ogre::Vector3 nodePos = state.sceneNode->_getDerivedPosition(); - /* If scene node was moved externally (editor gizmo), - * teleport character there */ + /* + * Root motion drives physics velocity (AnimationTreeSystem + * computes it from the animation displacement). The physics + * character moves naturally via velocity, and physics writes + * the position back to the scene node. No teleportation or + * position sync needed - this avoids jitter. + * + * If the scene node was moved externally (editor gizmo), + * teleport the physics character to match. + */ Ogre::Vector3 diff = nodePos - charPos; if (diff.squaredLength() > 0.001f) { state.character->SetPosition( JoltPhysics::convert(nodePos)); } + /* Root motion velocity is applied via linear velocity + * above. No special physics handling needed - physics + * writes position back to scene node normally. */ /* Apply velocity via Jolt linear velocity. * Preserve physics-driven Y velocity when no explicit diff --git a/src/features/editScene/systems/PathFollowingSystem.cpp b/src/features/editScene/systems/PathFollowingSystem.cpp index b8cad44..0e3e85d 100644 --- a/src/features/editScene/systems/PathFollowingSystem.cpp +++ b/src/features/editScene/systems/PathFollowingSystem.cpp @@ -10,8 +10,8 @@ #include PathFollowingSystem::PathFollowingSystem(flecs::world &world, - Ogre::SceneManager *sceneMgr, - NavMeshSystem *navSystem) + Ogre::SceneManager *sceneMgr, + NavMeshSystem *navSystem) : m_world(world) , m_sceneMgr(sceneMgr) , m_navSystem(navSystem) @@ -32,8 +32,8 @@ Ogre::Vector3 PathFollowingSystem::getEntityPosition(flecs::entity e) } void PathFollowingSystem::rotateTowards(flecs::entity e, - const Ogre::Vector3 &direction, - float deltaTime) + const Ogre::Vector3 &direction, + float deltaTime) { if (!e.has()) return; @@ -95,8 +95,8 @@ void PathFollowingSystem::update(float deltaTime) navmeshEntity = e; }); - m_world - .query() + m_world.query() .each([&](flecs::entity e, PathFollowingComponent &pf, CharacterComponent &cc, TransformComponent &trans) { (void)trans; @@ -112,7 +112,8 @@ void PathFollowingSystem::update(float deltaTime) toFinal.y = 0; float distToFinal = toFinal.length(); if (distToFinal < 0.5f) { - cc.linearVelocity = Ogre::Vector3::ZERO; + if (!cc.useRootMotion) + cc.linearVelocity = Ogre::Vector3::ZERO; pf.hasTarget = false; pf.currentLocomotionState = "idle"; pf.path.clear(); @@ -128,8 +129,8 @@ void PathFollowingSystem::update(float deltaTime) if (navmeshEntity.is_alive() && m_navSystem) { std::vector newPath; bool found = m_navSystem->findPath( - navmeshEntity, charPos, targetPos, - newPath); + navmeshEntity, charPos, + targetPos, newPath); if (found && !newPath.empty()) { pf.path = std::move(newPath); pf.pathIndex = 0; @@ -160,9 +161,13 @@ void PathFollowingSystem::update(float deltaTime) if (pf.pathIndex >= (int)pf.path.size()) { // Last waypoint - check if close to final target if (distToFinal < 0.5f) { - cc.linearVelocity = Ogre::Vector3::ZERO; + if (!cc.useRootMotion) + cc.linearVelocity = + Ogre::Vector3:: + ZERO; pf.hasTarget = false; - pf.currentLocomotionState = "idle"; + pf.currentLocomotionState = + "idle"; pf.path.clear(); pf.pathIndex = 0; applyLocomotionState(e); @@ -192,12 +197,18 @@ void PathFollowingSystem::update(float deltaTime) pf.currentLocomotionState = "walk"; } - cc.linearVelocity = toTarget * speed; + /* Only set velocity if root motion is not + * active. When root motion is active, + * AnimationTreeSystem already computed the + * velocity from the animation displacement. */ + if (!cc.useRootMotion) + cc.linearVelocity = toTarget * speed; // Rotate character to face movement direction rotateTowards(e, toTarget, deltaTime); } else { - cc.linearVelocity = Ogre::Vector3::ZERO; + if (!cc.useRootMotion) + cc.linearVelocity = Ogre::Vector3::ZERO; } applyLocomotionState(e); diff --git a/src/features/editScene/systems/PlayerControllerSystem.cpp b/src/features/editScene/systems/PlayerControllerSystem.cpp index d007757..f9ecea2 100644 --- a/src/features/editScene/systems/PlayerControllerSystem.cpp +++ b/src/features/editScene/systems/PlayerControllerSystem.cpp @@ -303,14 +303,10 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc, return; // Skip locomotion if input is locked by an executing action - if (pc.inputLocked) { - auto &cc = state.targetEntity.get_mut(); - cc.linearVelocity = Ogre::Vector3::ZERO; + if (pc.inputLocked) return; - } GameInputState &input = m_editorApp->getGameInputState(); - auto &cc = state.targetEntity.get_mut(); // Get camera yaw for relative movement Ogre::Quaternion yawRot(Ogre::Degree(state.yaw), Ogre::Vector3::UNIT_Y); @@ -325,28 +321,26 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc, if (right.squaredLength() > 0.0001f) right.normalise(); - Ogre::Vector3 desiredVel = Ogre::Vector3::ZERO; + Ogre::Vector3 desiredDir = Ogre::Vector3::ZERO; if (input.w) - desiredVel += forward; + desiredDir += forward; if (input.s) - desiredVel -= forward; + desiredDir -= forward; if (input.a) - desiredVel -= right; + desiredDir -= right; if (input.d) - desiredVel += right; + desiredDir += right; - bool isMoving = desiredVel.squaredLength() > 0.0001f; - float speed = input.shift ? 5.0f : 2.5f; + bool isMoving = desiredDir.squaredLength() > 0.0001f; if (isMoving) { - desiredVel.normalise(); - cc.linearVelocity = desiredVel * speed; + desiredDir.normalise(); // Rotate character to face movement direction auto &transform = state.targetEntity.get_mut(); if (transform.node) { - Ogre::Vector3 flatForward = desiredVel; + Ogre::Vector3 flatForward = desiredDir; flatForward.y = 0; if (flatForward.squaredLength() > 0.0001f) { flatForward.normalise(); @@ -361,8 +355,6 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc, targetRot, true)); } } - } else { - cc.linearVelocity = Ogre::Vector3::ZERO; } // Update animation state