diff --git a/AUTHORS.md b/AUTHORS.md index 2c77e522e1..0f0522c443 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -155,6 +155,7 @@ Programmers Paul McElroy (Greendogo) pchan3 Perry Hugh + Petr Mikheev (ptmikheev) Phillip Andrews (PhillipAnd) Pi03k Pieter van der Kloet (pvdk) diff --git a/CHANGELOG.md b/CHANGELOG.md index 060c857e58..cc06df61e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,9 +29,11 @@ Bug #5441: Enemies can't push a player character when in critical strike stance Bug #5451: Magic projectiles don't disappear with the caster Bug #5452: Autowalk is being included in savegames + Feature #390: 3rd person look "over the shoulder" Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher Feature #5362: Show the soul gems' trapped soul in count dialog Feature #5445: Handle NiLines + Feature #5457: Realistic diagonal movement Task #5480: Drop Qt4 support 0.46.0 diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 88defbaa09..24f25e508e 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -536,10 +536,11 @@ namespace MWClass moveSpeed = getSwimSpeed(ptr); else moveSpeed = getWalkSpeed(ptr); - if(getMovementSettings(ptr).mPosition[0] != 0 && getMovementSettings(ptr).mPosition[1] == 0) - moveSpeed *= 0.75f; - moveSpeed *= ptr.getClass().getMovementSettings(ptr).mSpeedFactor; + const MWMechanics::Movement& movementSettings = ptr.getClass().getMovementSettings(ptr); + if (movementSettings.mIsStrafing) + moveSpeed *= 0.75f; + moveSpeed *= movementSettings.mSpeedFactor; return moveSpeed; } diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 319d5f0144..b7084aff04 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -966,13 +966,14 @@ namespace MWClass moveSpeed = getRunSpeed(ptr); else moveSpeed = getWalkSpeed(ptr); - if(getMovementSettings(ptr).mPosition[0] != 0 && getMovementSettings(ptr).mPosition[1] == 0) - moveSpeed *= 0.75f; if(npcdata->mNpcStats.isWerewolf() && running && npcdata->mNpcStats.getDrawState() == MWMechanics::DrawState_Nothing) moveSpeed *= gmst.fWereWolfRunMult->mValue.getFloat(); - moveSpeed *= ptr.getClass().getMovementSettings(ptr).mSpeedFactor; + const MWMechanics::Movement& movementSettings = ptr.getClass().getMovementSettings(ptr); + if (movementSettings.mIsStrafing) + moveSpeed *= 0.75f; + moveSpeed *= movementSettings.mSpeedFactor; return moveSpeed; } diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index e4f870ed0d..24d1ebf368 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1939,64 +1939,76 @@ void CharacterController::update(float duration, bool animationOnly) bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying; bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; CreatureStats &stats = cls.getCreatureStats(mPtr); + Movement& movementSettings = cls.getMovementSettings(mPtr); //Force Jump Logic - bool isMoving = (std::abs(cls.getMovementSettings(mPtr).mPosition[0]) > .5 || std::abs(cls.getMovementSettings(mPtr).mPosition[1]) > .5); + bool isMoving = (std::abs(movementSettings.mPosition[0]) > .5 || std::abs(movementSettings.mPosition[1]) > .5); if(!inwater && !flying && solid) { //Force Jump if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)) - { - if(onground) - { - cls.getMovementSettings(mPtr).mPosition[2] = 1; - } - else - cls.getMovementSettings(mPtr).mPosition[2] = 0; - } + movementSettings.mPosition[2] = onground ? 1 : 0; //Force Move Jump, only jump if they're otherwise moving if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving) - { - - if(onground) - { - cls.getMovementSettings(mPtr).mPosition[2] = 1; - } - else - cls.getMovementSettings(mPtr).mPosition[2] = 0; - } + movementSettings.mPosition[2] = onground ? 1 : 0; } - osg::Vec3f vec(cls.getMovementSettings(mPtr).asVec3()); - vec.normalize(); - - if(mHitState != CharState_None && mJumpState == JumpState_None) - vec = osg::Vec3f(0.f, 0.f, 0.f); osg::Vec3f rot = cls.getRotationVector(mPtr); + osg::Vec3f vec(movementSettings.asVec3()); + vec.normalize(); - speed = cls.getSpeed(mPtr); - float analogueMult = 1.f; - if(isPlayer) + float analogueMult = 1.0f; + if (isPlayer) { + // TODO: Move this code to mwinput. // Joystick analogue movement. - float xAxis = std::abs(cls.getMovementSettings(mPtr).mPosition[0]); - float yAxis = std::abs(cls.getMovementSettings(mPtr).mPosition[1]); - analogueMult = ((xAxis > yAxis) ? xAxis : yAxis); - - // If Strafing, our max speed is slower so multiply by X axis instead. - if(std::abs(vec.x()/2.0f) > std::abs(vec.y())) - analogueMult = xAxis; + float xAxis = std::abs(movementSettings.mPosition[0]); + float yAxis = std::abs(movementSettings.mPosition[1]); + analogueMult = std::max(xAxis, yAxis); // Due to the half way split between walking/running, we multiply speed by 2 while walking, unless a keyboard was used. if(!isrunning && !sneak && !flying && analogueMult <= 0.5f) analogueMult *= 2.f; + + movementSettings.mSpeedFactor = analogueMult; } - speed *= analogueMult; + float effectiveRotation = rot.z(); + static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game"); + static const float turnToMovementDirectionSpeedCoef = Settings::Manager::getFloat("turn to movement direction speed coef", "Game"); + if (turnToMovementDirection && !(isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson())) + { + float targetMovementAngle = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y()); + movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState_Nothing || inwater) + && std::abs(targetMovementAngle) > osg::DegreesToRadians(60.0f); + if (movementSettings.mIsStrafing) + targetMovementAngle = 0; + float delta = targetMovementAngle - stats.getSideMovementAngle(); + float cosDelta = cosf(delta); + movementSettings.mSpeedFactor *= std::min(std::max(cosDelta, 0.f) + 0.3f, 1.f); // slow down when turn + float maxDelta = turnToMovementDirectionSpeedCoef * osg::PI * duration * (2.5f - cosDelta); + delta = std::min(delta, maxDelta); + delta = std::max(delta, -maxDelta); + stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); + effectiveRotation += delta; + } + else + movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; + + mAnimation->setLegsYawRadians(stats.getSideMovementAngle()); + if (stats.getDrawState() == MWMechanics::DrawState_Nothing || inwater) + mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2); + else + mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4); + + speed = cls.getSpeed(mPtr); vec.x() *= speed; vec.y() *= speed; + if(mHitState != CharState_None && mJumpState == JumpState_None) + vec = osg::Vec3f(); + CharacterState movestate = CharState_None; CharacterState idlestate = CharState_SpecialIdle; JumpingState jumpstate = JumpState_None; @@ -2158,7 +2170,7 @@ void CharacterController::update(float duration, bool animationOnly) inJump = false; - if(std::abs(vec.x()/2.0f) > std::abs(vec.y())) + if (movementSettings.mIsStrafing) { if(vec.x() > 0.0f) movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) @@ -2169,18 +2181,18 @@ void CharacterController::update(float duration, bool animationOnly) : (sneak ? CharState_SneakLeft : (isrunning ? CharState_RunLeft : CharState_WalkLeft))); } - else if(vec.y() != 0.0f) + else if (vec.length2() > 0.0f) { - if(vec.y() > 0.0f) + if (vec.y() >= 0.0f) movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward) : (sneak ? CharState_SneakForward : (isrunning ? CharState_RunForward : CharState_WalkForward))); - else if(vec.y() < 0.0f) + else movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack) : (sneak ? CharState_SneakBack : (isrunning ? CharState_RunBack : CharState_WalkBack))); } - else if(rot.z() != 0.0f) + else if (effectiveRotation != 0.0f) { // Do not play turning animation for player if rotation speed is very slow. // Actual threshold should take framerate in account. @@ -2193,9 +2205,9 @@ void CharacterController::update(float duration, bool animationOnly) bool isFirstPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); if (!sneak && jumpstate == JumpState_None && !isFirstPlayer && mPtr.getClass().isBipedal(mPtr)) { - if(rot.z() > rotationThreshold) + if(effectiveRotation > rotationThreshold) movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; - else if(rot.z() < -rotationThreshold) + else if(effectiveRotation < -rotationThreshold) movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; } } @@ -2317,9 +2329,9 @@ void CharacterController::update(float duration, bool animationOnly) world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); movement = vec; - cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0; + movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0; if (movement.z() == 0.f) - cls.getMovementSettings(mPtr).mPosition[2] = 0; + movementSettings.mPosition[2] = 0; // Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicSystem will actually handle it in this frame // due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled. @@ -2355,15 +2367,11 @@ void CharacterController::update(float duration, bool animationOnly) if(speed > 0.f) { float l = moved.length(); - - if((movement.x() < 0.0f && movement.x() < moved.x()*2.0f) || - (movement.x() > 0.0f && movement.x() > moved.x()*2.0f)) + if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2) moved.x() = movement.x(); - if((movement.y() < 0.0f && movement.y() < moved.y()*2.0f) || - (movement.y() > 0.0f && movement.y() > moved.y()*2.0f)) + if (std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2) moved.y() = movement.y(); - if((movement.z() < 0.0f && movement.z() < moved.z()*2.0f) || - (movement.z() > 0.0f && movement.z() > moved.z()*2.0f)) + if (std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2) moved.z() = movement.z(); // but keep the original speed float newLength = moved.length(); diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 0f11b8b2e4..79b8e23de4 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -23,7 +23,7 @@ namespace MWMechanics mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), - mDeathAnimation(-1), mTimeOfDeath(), mLevel (0) + mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0) { for (int i=0; i<4; ++i) mAiSettings[i] = 0; diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index b35c1e3b6e..5e91a1b5a0 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -80,6 +80,9 @@ namespace MWMechanics MWWorld::TimeStamp mTimeOfDeath; + // The difference between view direction and lower body direction. + float mSideMovementAngle; + public: typedef std::pair SummonKey; // private: @@ -298,6 +301,9 @@ namespace MWMechanics void addCorprusSpell(const std::string& sourceId, CorprusStats& stats); void removeCorprusSpell(const std::string& sourceId); + + float getSideMovementAngle() const { return mSideMovementAngle; } + void setSideMovementAngle(float angle) { mSideMovementAngle = angle; } }; } diff --git a/apps/openmw/mwmechanics/movement.hpp b/apps/openmw/mwmechanics/movement.hpp index cb9087359a..86b970e602 100644 --- a/apps/openmw/mwmechanics/movement.hpp +++ b/apps/openmw/mwmechanics/movement.hpp @@ -11,12 +11,14 @@ namespace MWMechanics float mPosition[3]; float mRotation[3]; float mSpeedFactor; + bool mIsStrafing; Movement() { mPosition[0] = mPosition[1] = mPosition[2] = 0.0f; mRotation[0] = mRotation[1] = mRotation[2] = 0.0f; mSpeedFactor = 1.f; + mIsStrafing = false; } osg::Vec3f asVec3() diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 594713a1cf..69136bac31 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -621,6 +621,8 @@ namespace MWRender , mTextKeyListener(nullptr) , mHeadYawRadians(0.f) , mHeadPitchRadians(0.f) + , mUpperBodyYawRadians(0.f) + , mLegsYawRadians(0.f) , mHasMagicEffects(false) , mAlpha(1.f) { @@ -1334,13 +1336,36 @@ namespace MWRender updateEffects(); + const float epsilon = 0.001f; + float yawOffset = 0; + if (mRootController) + { + bool enable = std::abs(mLegsYawRadians) > epsilon; + mRootController->setEnabled(enable); + if (enable) + { + mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0,0,1))); + yawOffset = mLegsYawRadians; + } + } + if (mSpineController) + { + float yaw = mUpperBodyYawRadians - yawOffset; + bool enable = std::abs(yaw) > epsilon; + mSpineController->setEnabled(enable); + if (enable) + { + mSpineController->setRotate(osg::Quat(yaw, osg::Vec3f(0,0,1))); + yawOffset = mUpperBodyYawRadians; + } + } if (mHeadController) { - const float epsilon = 0.001f; - bool enable = (std::abs(mHeadPitchRadians) > epsilon || std::abs(mHeadYawRadians) > epsilon); + float yaw = mHeadYawRadians - yawOffset; + bool enable = (std::abs(mHeadPitchRadians) > epsilon || std::abs(yaw) > epsilon); mHeadController->setEnabled(enable); if (enable) - mHeadController->setRotate(osg::Quat(mHeadPitchRadians, osg::Vec3f(1,0,0)) * osg::Quat(mHeadYawRadians, osg::Vec3f(0,0,1))); + mHeadController->setRotate(osg::Quat(mHeadPitchRadians, osg::Vec3f(1,0,0)) * osg::Quat(yaw, osg::Vec3f(0,0,1))); } // Scripted animations should not cause movement @@ -1801,13 +1826,17 @@ namespace MWRender void Animation::addControllers() { - mHeadController = nullptr; - - NodeMap::const_iterator found = getNodeMap().find("bip01 head"); - if (found == getNodeMap().end()) - return; + mHeadController = addRotateController("bip01 head"); + mSpineController = addRotateController("bip01 spine1"); + mRootController = addRotateController("bip01"); + } - osg::MatrixTransform* node = found->second; + RotateController* Animation::addRotateController(std::string bone) + { + auto iter = getNodeMap().find(bone); + if (iter == getNodeMap().end()) + return nullptr; + osg::MatrixTransform* node = iter->second; bool foundKeyframeCtrl = false; osg::Callback* cb = node->getUpdateCallback(); @@ -1820,13 +1849,15 @@ namespace MWRender } cb = cb->getNestedCallback(); } - + // Without KeyframeController the orientation will not be reseted each frame, so + // RotateController shouldn't be used for such nodes. if (!foundKeyframeCtrl) - return; + return nullptr; - mHeadController = new RotateController(mObjectRoot.get()); - node->addUpdateCallback(mHeadController); - mActiveControllers.emplace_back(node, mHeadController); + RotateController* controller = new RotateController(mObjectRoot.get()); + node->addUpdateCallback(controller); + mActiveControllers.emplace_back(node, controller); + return controller; } void Animation::setHeadPitch(float pitchRadians) diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index c53cf98a95..564952a905 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -267,8 +267,15 @@ protected: TextKeyListener* mTextKeyListener; osg::ref_ptr mHeadController; + osg::ref_ptr mSpineController; + osg::ref_ptr mRootController; float mHeadYawRadians; float mHeadPitchRadians; + float mUpperBodyYawRadians; + float mLegsYawRadians; + + RotateController* addRotateController(std::string bone); + bool mHasMagicEffects; osg::ref_ptr mGlowLight; @@ -477,6 +484,12 @@ public: virtual void setHeadYaw(float yawRadians); virtual float getHeadPitch() const; virtual float getHeadYaw() const; + + virtual void setUpperBodyYawRadians(float v) { mUpperBodyYawRadians = v; } + virtual void setLegsYawRadians(float v) { mLegsYawRadians = v; } + virtual float getUpperBodyYawRadians() const { return mUpperBodyYawRadians; } + virtual float getLegsYawRadians() const { return mLegsYawRadians; } + virtual void setAccurateAiming(bool enabled) {} virtual bool canBeHarvested() const { return false; } diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index e0818101d5..251f7f5939 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -7,9 +7,13 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/refdata.hpp" +#include "../mwmechanics/drawstate.hpp" +#include "../mwmechanics/npcstats.hpp" + #include "npcanimation.hpp" namespace @@ -52,11 +56,14 @@ namespace MWRender mFurthest(800.f), mIsNearest(false), mHeight(124.f), - mMaxCameraDistance(192.f), + mBaseCameraDistance(192.f), mVanityToggleQueued(false), mVanityToggleQueuedValue(false), mViewModeToggleQueued(false), - mCameraDistance(0.f) + mCameraDistance(0.f), + mThirdPersonMode(ThirdPersonViewMode::Standard), + mOverShoulderHorizontalOffset(30.0f), + mSmoothTransitionToCombatMode(0.f) { mVanity.enabled = false; mVanity.allowed = true; @@ -68,7 +75,7 @@ namespace MWRender mMainCam.yaw = 0.f; mMainCam.offset = 400.f; - mCameraDistance = mMaxCameraDistance; + mCameraDistance = mBaseCameraDistance; mUpdateCallback = new UpdateRenderCameraCallback(this); mCamera->addUpdateCallback(mUpdateCallback); @@ -84,7 +91,7 @@ namespace MWRender return mTrackingPtr; } - osg::Vec3d Camera::getFocalPoint() + osg::Vec3d Camera::getFocalPoint() const { const osg::Node* trackNode = mTrackingNode; if (!trackNode) @@ -96,22 +103,54 @@ namespace MWRender osg::Vec3d position = worldMat.getTrans(); if (!isFirstPerson()) + { position.z() += mHeight * mHeightScale; + + // We subtract 10.f here and add it within focalPointOffset in order to avoid camera clipping through ceiling. + // Needed because character's head can be a bit higher than collision area. + position.z() -= 10.f; + + position += getFocalPointOffset() + mFocalPointAdjustment; + } return position; } + osg::Vec3d Camera::getFocalPointOffset() const + { + osg::Vec3d offset(0, 0, 10.f); + if (mThirdPersonMode == ThirdPersonViewMode::OverShoulder && !mPreviewMode && !mVanity.enabled) + { + float horizontalOffset = mOverShoulderHorizontalOffset * (1.f - mSmoothTransitionToCombatMode); + float verticalOffset = mSmoothTransitionToCombatMode * 15.f + (1.f - mSmoothTransitionToCombatMode) * -10.f; + + offset.x() += horizontalOffset * cos(getYaw()); + offset.y() += horizontalOffset * sin(getYaw()); + offset.z() += verticalOffset; + } + return offset; + } + + void Camera::getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const + { + focal = getFocalPoint(); + osg::Vec3d offset(0,0,0); + if (!isFirstPerson()) + { + osg::Quat orient = osg::Quat(getPitch(), osg::Vec3d(1,0,0)) * osg::Quat(getYaw(), osg::Vec3d(0,0,1)); + offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); + } + camera = focal + offset; + } + void Camera::updateCamera(osg::Camera *cam) { if (mTrackingPtr.isEmpty()) return; - osg::Vec3d position = getFocalPoint(); + osg::Vec3d focal, position; + getPosition(focal, position); osg::Quat orient = osg::Quat(getPitch(), osg::Vec3d(1,0,0)) * osg::Quat(getYaw(), osg::Vec3d(0,0,1)); - - osg::Vec3d offset = orient * osg::Vec3d(0, isFirstPerson() ? 0 : -mCameraDistance, 0); - position += offset; - osg::Vec3d forward = orient * osg::Vec3d(0,1,0); osg::Vec3d up = orient * osg::Vec3d(0,0,1); @@ -164,14 +203,33 @@ namespace MWRender if (paused) return; - // only show the crosshair in game mode and in first person mode. + // only show the crosshair in game mode MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); - wm->showCrosshair(!wm->isGuiMode() && (mFirstPersonView && !mVanity.enabled && !mPreviewMode)); + wm->showCrosshair(!wm->isGuiMode() && !mVanity.enabled && !mPreviewMode + && (mFirstPersonView || mThirdPersonMode != ThirdPersonViewMode::Standard)); if(mVanity.enabled) { rotateCamera(0.f, osg::DegreesToRadians(3.f * duration), true); } + + updateSmoothTransitionToCombatMode(duration); + } + + void Camera::updateSmoothTransitionToCombatMode(float duration) + { + bool combatMode = true; + if (mTrackingPtr.getClass().isActor()) + combatMode = mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).getDrawState() != MWMechanics::DrawState_Nothing; + float speed = ((combatMode ? 1.f : 0.f) - mSmoothTransitionToCombatMode) * 5; + if (speed != 0) + speed += speed > 0 ? 1 : -1; + + mSmoothTransitionToCombatMode += speed * duration; + if (mSmoothTransitionToCombatMode > 1) + mSmoothTransitionToCombatMode = 1; + if (mSmoothTransitionToCombatMode < 0) + mSmoothTransitionToCombatMode = 0; } void Camera::toggleViewMode(bool force) @@ -186,6 +244,9 @@ namespace MWRender else mViewModeToggleQueued = false; + if (mTrackingPtr.getClass().isActor()) + mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).setSideMovementAngle(0); + mFirstPersonView = !mFirstPersonView; processViewChange(); } @@ -259,7 +320,7 @@ namespace MWRender mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset)); } - float Camera::getYaw() + float Camera::getYaw() const { if(mVanity.enabled || mPreviewMode) return mPreviewCam.yaw; @@ -280,7 +341,7 @@ namespace MWRender } } - float Camera::getPitch() + float Camera::getPitch() const { if (mVanity.enabled || mPreviewMode) { return mPreviewCam.pitch; @@ -314,7 +375,7 @@ namespace MWRender return mCameraDistance; } - void Camera::setCameraDistance(float dist, bool adjust, bool override) + void Camera::setBaseCameraDistance(float dist, bool adjust) { if(mFirstPersonView && !mPreviewMode && !mVanity.enabled) return; @@ -322,34 +383,55 @@ namespace MWRender mIsNearest = false; if (adjust) - dist += mCameraDistance; + { + if (mVanity.enabled || mPreviewMode) + dist += mCameraDistance; + else + dist += std::min(mCameraDistance - getCameraDistanceCorrection(), mBaseCameraDistance); + } + - if (dist >= mFurthest) { + if (dist >= mFurthest) dist = mFurthest; - } else if (!override && dist < 10.f) { - dist = 10.f; - } else if (override && dist <= mNearest) { + else if (dist <= mNearest) + { dist = mNearest; mIsNearest = true; } + + if (mVanity.enabled || mPreviewMode) + mPreviewCam.offset = dist; + else if (!mFirstPersonView) + mBaseCameraDistance = dist; + setCameraDistance(); + } + + void Camera::setCameraDistance(float dist, bool adjust) + { + if(mFirstPersonView && !mPreviewMode && !mVanity.enabled) + return; + + if (adjust) dist += mCameraDistance; + + if (dist >= mFurthest) + dist = mFurthest; + else if (dist < 10.f) + dist = 10.f; mCameraDistance = dist; + } - if (override) { - if (mVanity.enabled || mPreviewMode) { - mPreviewCam.offset = mCameraDistance; - } else if (!mFirstPersonView) { - mMaxCameraDistance = mCameraDistance; - } - } + float Camera::getCameraDistanceCorrection() const + { + return mThirdPersonMode != ThirdPersonViewMode::Standard ? std::max(-getPitch(), 0.f) * 50.f : 0; } void Camera::setCameraDistance() { - if (mVanity.enabled || mPreviewMode) { + if (mVanity.enabled || mPreviewMode) mCameraDistance = mPreviewCam.offset; - } else if (!mFirstPersonView) { - mCameraDistance = mMaxCameraDistance; - } + else if (!mFirstPersonView) + mCameraDistance = mBaseCameraDistance + getCameraDistanceCorrection(); + mFocalPointAdjustment = osg::Vec3d(); } void Camera::setAnimation(NpcAnimation *anim) @@ -382,22 +464,12 @@ namespace MWRender rotateCamera(getPitch(), getYaw(), false); } - void Camera::getPosition(osg::Vec3f &focal, osg::Vec3f &camera) - { - focal = getFocalPoint(); - - osg::Quat orient = osg::Quat(getPitch(), osg::Vec3d(1,0,0)) * osg::Quat(getYaw(), osg::Vec3d(0,0,1)); - - osg::Vec3d offset = orient * osg::Vec3d(0, isFirstPerson() ? 0 : -mCameraDistance, 0); - camera = focal + offset; - } - - bool Camera::isVanityOrPreviewModeEnabled() + bool Camera::isVanityOrPreviewModeEnabled() const { return mPreviewMode || mVanity.enabled; } - bool Camera::isNearest() + bool Camera::isNearest() const { return mIsNearest; } diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index 573cf936fd..bdcb692972 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -23,6 +23,10 @@ namespace MWRender /// \brief Camera control class Camera { + public: + enum class ThirdPersonViewMode {Standard, OverShoulder}; + + private: struct CamData { float pitch, yaw, offset; }; @@ -45,7 +49,7 @@ namespace MWRender bool enabled, allowed; } mVanity; - float mHeight, mMaxCameraDistance; + float mHeight, mBaseCameraDistance; CamData mMainCam, mPreviewCam; bool mVanityToggleQueued; @@ -54,6 +58,16 @@ namespace MWRender float mCameraDistance; + ThirdPersonViewMode mThirdPersonMode; + float mOverShoulderHorizontalOffset; + osg::Vec3d mFocalPointAdjustment; + + // Makes sense only if mThirdPersonMode is OverShoulder. Can be in range [0, 1]. + // Used for smooth transition from non-combat camera position (0) to combat camera position (1). + float mSmoothTransitionToCombatMode; + void updateSmoothTransitionToCombatMode(float duration); + float getCameraDistanceCorrection() const; + osg::ref_ptr mUpdateCallback; public: @@ -62,6 +76,9 @@ namespace MWRender MWWorld::Ptr getTrackingPtr() const; + void setThirdPersonViewMode(ThirdPersonViewMode mode) { mThirdPersonMode = mode; } + void setOverShoulderHorizontalOffset(float v) { mOverShoulderHorizontalOffset = v; } + /// Update the view matrix of \a cam void updateCamera(osg::Camera* cam); @@ -72,10 +89,10 @@ namespace MWRender /// \param rot Rotation angles in radians void rotateCamera(float pitch, float yaw, bool adjust); - float getYaw(); + float getYaw() const; void setYaw(float angle); - float getPitch(); + float getPitch() const; void setPitch(float angle); /// Attach camera to object @@ -100,27 +117,32 @@ namespace MWRender void update(float duration, bool paused=false); + /// Set base camera distance for current mode. Don't work on 1st person view. + /// \param adjust Indicates should distance be adjusted or set. + void setBaseCameraDistance(float dist, bool adjust = false); + /// Set camera distance for current mode. Don't work on 1st person view. /// \param adjust Indicates should distance be adjusted or set. - /// \param override If true new distance will be used as default. - /// If false, default distance can be restored with setCameraDistance(). - void setCameraDistance(float dist, bool adjust = false, bool override = true); + /// Default distance can be restored with setCameraDistance(). + void setCameraDistance(float dist, bool adjust = false); - /// Restore default camera distance for current mode. + /// Restore default camera distance and offset for current mode. void setCameraDistance(); float getCameraDistance() const; void setAnimation(NpcAnimation *anim); - osg::Vec3d getFocalPoint(); + osg::Vec3d getFocalPoint() const; + osg::Vec3d getFocalPointOffset() const; + void adjustFocalPoint(osg::Vec3d adjustment) { mFocalPointAdjustment = adjustment; } /// Stores focal and camera world positions in passed arguments - void getPosition(osg::Vec3f &focal, osg::Vec3f &camera); + void getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const; - bool isVanityOrPreviewModeEnabled(); + bool isVanityOrPreviewModeEnabled() const; - bool isNearest(); + bool isNearest() const; }; } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index d214828942..6997fee03e 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -364,6 +364,7 @@ namespace MWRender float firstPersonFov = Settings::Manager::getFloat("first person field of view", "Camera"); mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f); mStateUpdater->setFogEnd(mViewDistance); + updateThirdPersonViewMode(); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("near", mNearClip)); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("far", mViewDistance)); @@ -379,6 +380,15 @@ namespace MWRender mWorkQueue = nullptr; } + void RenderingManager::updateThirdPersonViewMode() + { + if (Settings::Manager::getBool("view over shoulder", "Camera")) + mCamera->setThirdPersonViewMode(Camera::ThirdPersonViewMode::OverShoulder); + else + mCamera->setThirdPersonViewMode(Camera::ThirdPersonViewMode::Standard); + mCamera->setOverShoulderHorizontalOffset(Settings::Manager::getFloat("view over shoulder horizontal offset", "Camera")); + } + osgUtil::IncrementalCompileOperation* RenderingManager::getIncrementalCompileOperation() { return mViewer->getIncrementalCompileOperation(); @@ -616,7 +626,7 @@ namespace MWRender mCamera->update(dt, paused); - osg::Vec3f focal, cameraPos; + osg::Vec3d focal, cameraPos; mCamera->getPosition(focal, cameraPos); mCurrentCameraPos = cameraPos; @@ -1323,13 +1333,18 @@ namespace MWRender { if(mCamera->isNearest() && dist > 0.f) mCamera->toggleViewMode(); + else if (override) + mCamera->setBaseCameraDistance(-dist / 120.f * 10, adjust); else - mCamera->setCameraDistance(-dist / 120.f * 10, adjust, override); + mCamera->setCameraDistance(-dist / 120.f * 10, adjust); } else if(mCamera->isFirstPerson() && dist < 0.f) { mCamera->toggleViewMode(); - mCamera->setCameraDistance(0.f, false, override); + if (override) + mCamera->setBaseCameraDistance(0.f, false); + else + mCamera->setCameraDistance(0.f, false); } } @@ -1376,7 +1391,7 @@ namespace MWRender void RenderingManager::changeVanityModeScale(float factor) { if(mCamera->isVanityOrPreviewModeEnabled()) - mCamera->setCameraDistance(-factor/120.f*10, true, true); + mCamera->setBaseCameraDistance(-factor/120.f*10, true); } void RenderingManager::overrideFieldOfView(float val) diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index db4788970e..6700f5ce6c 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -252,6 +252,7 @@ namespace MWRender void updateTextureFiltering(); void updateAmbient(); void setFogColor(const osg::Vec4f& color); + void updateThirdPersonViewMode(); void reportStats() const; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 20fce74bf8..6788a7fa29 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1857,15 +1857,35 @@ namespace MWWorld int nightEye = static_cast(player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::NightEye).getMagnitude()); mRendering->setNightEyeFactor(std::min(1.f, (nightEye/100.f))); - mRendering->getCamera()->setCameraDistance(); + auto* camera = mRendering->getCamera(); + camera->setCameraDistance(); if(!mRendering->getCamera()->isFirstPerson()) { - osg::Vec3f focal, camera; - mRendering->getCamera()->getPosition(focal, camera); - float radius = mRendering->getNearClipDistance()*2.5f; - MWPhysics::PhysicsSystem::RayResult result = mPhysics->castSphere(focal, camera, radius); + float cameraObstacleLimit = mRendering->getNearClipDistance() * 2.5f; + float focalObstacleLimit = std::max(cameraObstacleLimit, 10.0f); + + // Adjust focal point. + osg::Vec3d focal = camera->getFocalPoint(); + osg::Vec3d focalOffset = camera->getFocalPointOffset(); + float offsetLen = focalOffset.length(); + if (offsetLen > 0) + { + MWPhysics::PhysicsSystem::RayResult result = mPhysics->castSphere(focal - focalOffset, focal, focalObstacleLimit); + if (result.mHit) + { + double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; + if (adjustmentCoef < -1) + adjustmentCoef = -1; + camera->adjustFocalPoint(focalOffset * adjustmentCoef); + } + } + + // Adjust camera position. + osg::Vec3d cameraPos; + camera->getPosition(focal, cameraPos); + MWPhysics::PhysicsSystem::RayResult result = mPhysics->castSphere(focal, cameraPos, cameraObstacleLimit); if (result.mHit) - mRendering->getCamera()->setCameraDistance((result.mHitPos - focal).length() - radius, false, false); + mRendering->getCamera()->setCameraDistance((result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(), false); } } diff --git a/docs/source/reference/modding/settings/camera.rst b/docs/source/reference/modding/settings/camera.rst index 8bc36d8fb4..756db39992 100644 --- a/docs/source/reference/modding/settings/camera.rst +++ b/docs/source/reference/modding/settings/camera.rst @@ -113,3 +113,29 @@ because the Bethesda provided Morrowind assets do not adapt well to large values while small values can result in the hands not being visible. This setting can only be configured by editing the settings configuration file. + +view over shoulder +------------------ + +:Type: boolean +:Range: True/False +:Default: False + +This setting controls third person view mode. +False: View is centered on the character's head. Crosshair is hidden. +True: In non-combat mode camera is positioned behind the character's shoulder. Crosshair is visible in third person mode as well. + +This setting can only be configured by editing the settings configuration file. + +view over shoulder horizontal offset +------------------------------------ + +:Type: floating point +:Range: Any +:Default: 30 + +This setting makes sense only if 'view over shoulder' is enabled. Controls horizontal offset of the camera in third person mode. Negative value means offset to the left, positive - to the right. +Recommened values: 30 for the right shoulder, -30 for the left shoulder. + +This setting can only be configured by editing the settings configuration file. + diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 201704f351..4166883216 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -315,3 +315,30 @@ Setting the value of this setting to true will remove the 0 lower cap from the v allowing Damage Fatigue to reduce Fatigue to a value below zero. This setting can be controlled in Advanced tab of the launcher. + +turn to movement direction +-------------------------- + +:Type: boolean +:Range: True/False +:Default: False + +Affects side and diagonal movement. Enabling this setting makes movement more realistic. + +If disabled then the whole character's body is pointed to the direction of view. Diagonal movement has no special animation and causes sliding. + +If enabled then the character turns lower body to the direction of movement. Upper body is turned partially. Head is always pointed to the direction of view. In combat mode it works only for diagonal movement. In non-combat mode it also changes straight right and straight left movement. + +This setting can only be configured by editing the settings configuration file. + +turn to movement direction speed coef +------------------------------------- + +:Type: floating point +:Range: >0 +:Default: 1.0 + +Makes difference only if 'turn to movement direction' is enabled. Modifies turning speed. + +This setting can only be configured by editing the settings configuration file. + diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 617aca3f11..3a7c9a8b48 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -33,6 +33,12 @@ field of view = 60.0 # Best to leave this at the default since vanilla assets are not complete enough to adapt to high FoV's. Too low FoV would clip the hands off screen. first person field of view = 60.0 +# If enabled then third person camera is positioned above character's shoulder and crosshair is visible. +view over shoulder = false + +# Makes sense only if 'view over shoulder' is true. Negative value means offset to the left. +view over shoulder horizontal offset = 30 + [Cells] # Preload cells in a background thread. All settings starting with 'preload' have no effect unless this is enabled. @@ -296,6 +302,12 @@ projectiles enchant multiplier = 0 # This means that unlike Morrowind you will be able to knock down actors using this effect. uncapped damage fatigue = false +# Turn lower body to movement direction. 'true' makes diagonal movement more realistic. +turn to movement direction = false + +# Turning speed multiplier. Makes difference only if 'turn to movement direction' is enabled. +turn to movement direction speed coef = 1.0 + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).