diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index e0818101d..f5d63343b 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) @@ -259,7 +317,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 +338,7 @@ namespace MWRender } } - float Camera::getPitch() + float Camera::getPitch() const { if (mVanity.enabled || mPreviewMode) { return mPreviewCam.pitch; @@ -314,7 +372,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 +380,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; } - mCameraDistance = dist; - if (override) { - if (mVanity.enabled || mPreviewMode) { - mPreviewCam.offset = mCameraDistance; - } else if (!mFirstPersonView) { - mMaxCameraDistance = mCameraDistance; - } - } + 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; + } + + 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 +461,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 573cf936f..bdcb69297 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 d21482894..6997fee03 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 db4788970..6700f5ce6 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 20fce74bf..6788a7fa2 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/files/settings-default.cfg b/files/settings-default.cfg index 617aca3f1..b42924de0 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.