diff --git a/CHANGELOG.md b/CHANGELOG.md index 645bc59e9e..26957e6c8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Bug #4754: Stack of ammunition cannot be equipped partially Bug #4816: GetWeaponDrawn returns 1 before weapon is attached Bug #5057: Weapon swing sound plays at same pitch whether it hits or misses + Bug #5062: Root bone rotations for NPC animation don't work the same as for creature animation Bug #5129: Stuttering animation on Centurion Archer Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place Bug #5371: Keyframe animation tracks are used for any file that begins with an X diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 5869cc3a73..b8539671b5 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -191,6 +191,7 @@ bool Launcher::SettingsPage::loadSettings() } loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox); loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox); + loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox); distantLandCheckBox->setCheckState( Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging ? Qt::Checked : Qt::Unchecked); @@ -338,6 +339,7 @@ void Launcher::SettingsPage::saveSettings() saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing); saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection); saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement); + saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation); const bool wantDistantLand = distantLandCheckBox->checkState() == Qt::Checked; if (wantDistantLand != (Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging)) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 068d91ab42..dd7b97b6a5 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2386,49 +2386,55 @@ namespace MWMechanics } } - osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration); - if (duration > 0.0f) - moved /= duration; - else - moved = osg::Vec3f(0.f, 0.f, 0.f); + osg::Vec3f movementFromAnimation + = mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration); - moved.x() *= scale; - moved.y() *= scale; - - // Ensure we're moving in generally the right direction... - if (speed > 0.f && moved != osg::Vec3f()) + if (mPtr.getClass().isActor() && isMovementAnimationControlled() && !isScriptedAnimPlaying()) { - float l = moved.length(); - if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 - || std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2 - || std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2) - { - moved = movement; - // For some creatures getSpeed doesn't work, so we adjust speed to the animation. - // TODO: Fix Creature::getSpeed. - float newLength = moved.length(); - if (newLength > 0 && !cls.isNpc()) - moved *= (l / newLength); - } - } + if (duration > 0.0f) + movementFromAnimation /= duration; + else + movementFromAnimation = osg::Vec3f(0.f, 0.f, 0.f); - if (mFloatToSurface && cls.isActor()) - { - if (cls.getCreatureStats(mPtr).isDead() - || (!godmode - && cls.getCreatureStats(mPtr) - .getMagicEffects() - .getOrDefault(ESM::MagicEffect::Paralyze) - .getModifier() - > 0)) - { - moved.z() = 1.0; - } - } + movementFromAnimation.x() *= scale; + movementFromAnimation.y() *= scale; - // Update movement - if (isMovementAnimationControlled() && mPtr.getClass().isActor() && !isScriptedAnimPlaying()) - world->queueMovement(mPtr, moved); + if (speed > 0.f && movementFromAnimation != osg::Vec3f()) + { + // Ensure we're moving in the right general direction. In vanilla, all horizontal movement is taken from + // animations, even when moving diagonally (which doesn't have a corresponding animation). So to acheive + // diagonal movement, we have to rotate the movement taken from the animation to the intended + // direction. + // + // Note that while a complete movement animation cycle will have a well defined direction, no individual + // frame will, and therefore we have to determine the direction based on the currently playing cycle + // instead. + float animMovementAngle = getAnimationMovementDirection(); + float targetMovementAngle = std::atan2(-movement.x(), movement.y()); + float diff = targetMovementAngle - animMovementAngle; + movementFromAnimation = osg::Quat(diff, osg::Vec3f(0, 0, 1)) * movementFromAnimation; + } + + if (!(isPlayer && Settings::game().mPlayerMovementIgnoresAnimation)) + movement = movementFromAnimation; + + if (mFloatToSurface) + { + if (cls.getCreatureStats(mPtr).isDead() + || (!godmode + && cls.getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(ESM::MagicEffect::Paralyze) + .getModifier() + > 0)) + { + movement.z() = 1.0; + } + } + + // Update movement + world->queueMovement(mPtr, movement); + } mSkipAnim = false; @@ -2909,6 +2915,39 @@ namespace MWMechanics MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, *soundId, volume, pitch); } + float CharacterController::getAnimationMovementDirection() const + { + switch (mMovementState) + { + case CharState_RunLeft: + case CharState_SneakLeft: + case CharState_SwimWalkLeft: + case CharState_SwimRunLeft: + case CharState_WalkLeft: + return osg::PI_2f; + case CharState_RunRight: + case CharState_SneakRight: + case CharState_SwimWalkRight: + case CharState_SwimRunRight: + case CharState_WalkRight: + return -osg::PI_2f; + case CharState_RunForward: + case CharState_SneakForward: + case CharState_SwimRunForward: + case CharState_SwimWalkForward: + case CharState_WalkForward: + return mAnimation->getLegsYawRadians(); + case CharState_RunBack: + case CharState_SneakBack: + case CharState_SwimWalkBack: + case CharState_SwimRunBack: + case CharState_WalkBack: + return mAnimation->getLegsYawRadians() - osg::PIf; + default: + return 0.0f; + } + } + void CharacterController::updateHeadTracking(float duration) { const osg::Node* head = mAnimation->getNode("Bip01 Head"); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index c3d45fe0fb..63491ec776 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -319,6 +319,8 @@ namespace MWMechanics void playSwishSound() const; + float getAnimationMovementDirection() const; + MWWorld::MovementDirectionFlags getSupportedMovementDirections() const; }; } diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 0741e24a69..bac9dbb56c 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1235,9 +1235,11 @@ namespace MWRender mRootController->setEnabled(enable); if (enable) { - mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1)) - * osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0))); + osg::Quat legYaw = osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1)); + mRootController->setRotate(legYaw * osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0))); yawOffset = mLegsYawRadians; + // When yawing the root, also update the accumulated movement. + movement = legYaw * movement; } } if (mSpineController) diff --git a/components/settings/categories/game.hpp b/components/settings/categories/game.hpp index 5b400acb50..4aec92d0b8 100644 --- a/components/settings/categories/game.hpp +++ b/components/settings/categories/game.hpp @@ -74,6 +74,7 @@ namespace Settings "unarmed creature attacks damage armor" }; SettingValue mActorCollisionShapeType{ mIndex, "Game", "actor collision shape type" }; + SettingValue mPlayerMovementIgnoresAnimation{ mIndex, "Game", "player movement ignores animation" }; }; } diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index c88ae4e28f..31cc2703f2 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -517,3 +517,15 @@ will not be useful with another. * 0: Axis-aligned bounding box * 1: Rotating box * 2: Cylinder + +player movement ignores animation +--------------------------------- + +:Type: boolean +:Range: True/False +:Default: False + +In third person, the camera will sway along with the movement animations of the player. +Enabling this option disables this swaying by having the player character move independently of its animation. + +This setting can be controlled in the Settings tab of the launcher, under Visuals. diff --git a/files/settings-default.cfg b/files/settings-default.cfg index bbc6b4d1c8..da1c97519a 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -365,6 +365,11 @@ unarmed creature attacks damage armor = false # 2 = Cylinder actor collision shape type = 0 +# When false the player character will base movement on animations. This will sway the camera +# while moving in third person like in vanilla, and reproduce movement bugs caused by glitchy +# vanilla animations. +player movement ignores animation = false + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index c61b4f4229..1f5f206f67 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -14,7 +14,7 @@ - 0 + 1 @@ -293,8 +293,8 @@ 0 0 - 680 - 882 + 671 + 774 @@ -377,6 +377,16 @@ + + + + <html><head/><body><p>In third person, the camera will sway along with the movement animations of the player. Enabling this option disables this swaying by having the player character move independently of its animation. This was the default behavior of OpenMW 0.48 and earlier.</p></body></html> + + + Player movement ignores animation + + +