#include "camera.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/refdata.hpp" #include "../mwmechanics/drawstate.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwphysics/raycasting.hpp" #include "npcanimation.hpp" namespace { class UpdateRenderCameraCallback : public osg::NodeCallback { public: UpdateRenderCameraCallback(MWRender::Camera* cam) : mCamera(cam) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) override { osg::Camera* cam = static_cast(node); // traverse first to update animations, in case the camera is attached to an animated node traverse(node, nv); mCamera->updateCamera(cam); } private: MWRender::Camera* mCamera; }; } namespace MWRender { Camera::Camera (osg::Camera* camera) : mHeightScale(1.f), mCamera(camera), mAnimation(nullptr), mFirstPersonView(true), mMode(Mode::Normal), mVanityAllowed(true), mStandingPreviewAllowed(Settings::Manager::getBool("preview if stand still", "Camera")), mDeferredRotationAllowed(Settings::Manager::getBool("deferred preview rotation", "Camera")), mNearest(30.f), mFurthest(800.f), mIsNearest(false), mHeight(124.f), mBaseCameraDistance(Settings::Manager::getFloat("third person camera distance", "Camera")), mPitch(0.f), mYaw(0.f), mRoll(0.f), mVanityToggleQueued(false), mVanityToggleQueuedValue(false), mViewModeToggleQueued(false), mCameraDistance(0.f), mMaxNextCameraDistance(800.f), mFocalPointCurrentOffset(osg::Vec2d()), mFocalPointTargetOffset(osg::Vec2d()), mFocalPointTransitionSpeedCoef(1.f), mSkipFocalPointTransition(true), mPreviousTransitionInfluence(0.f), mSmoothedSpeed(0.f), mZoomOutWhenMoveCoef(Settings::Manager::getFloat("zoom out when move coef", "Camera")), mDynamicCameraDistanceEnabled(false), mShowCrosshairInThirdPersonMode(false), mHeadBobbingEnabled(Settings::Manager::getBool("head bobbing", "Camera")), mHeadBobbingOffset(0.f), mHeadBobbingWeight(0.f), mTotalMovement(0.f), mDeferredRotation(osg::Vec3f()), mDeferredRotationDisabled(false) { mCameraDistance = mBaseCameraDistance; mUpdateCallback = new UpdateRenderCameraCallback(this); mCamera->addUpdateCallback(mUpdateCallback); } Camera::~Camera() { mCamera->removeUpdateCallback(mUpdateCallback); } osg::Vec3d Camera::getFocalPoint() const { if (!mTrackingNode) return osg::Vec3d(); osg::NodePathList nodepaths = mTrackingNode->getParentalNodePaths(); if (nodepaths.empty()) return osg::Vec3d(); osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); osg::Vec3d position = worldMat.getTrans(); if (isFirstPerson()) position.z() += mHeadBobbingOffset; else { 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); offset.x() += mFocalPointCurrentOffset.x() * cos(getYaw()); offset.y() += mFocalPointCurrentOffset.x() * sin(getYaw()); offset.z() += mFocalPointCurrentOffset.y(); 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) { osg::Vec3d focal, position; getPosition(focal, position); osg::Quat orient = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw, osg::Vec3d(0, 0, 1)); osg::Vec3d forward = orient * osg::Vec3d(0,1,0); osg::Vec3d up = orient * osg::Vec3d(0,0,1); cam->setViewMatrixAsLookAt(position, position + forward, up); } void Camera::updateHeadBobbing(float duration) { static const float doubleStepLength = Settings::Manager::getFloat("head bobbing step", "Camera") * 2; static const float stepHeight = Settings::Manager::getFloat("head bobbing height", "Camera"); static const float maxRoll = osg::DegreesToRadians(Settings::Manager::getFloat("head bobbing roll", "Camera")); if (MWBase::Environment::get().getWorld()->isOnGround(mTrackingPtr)) mHeadBobbingWeight = std::min(mHeadBobbingWeight + duration * 5, 1.f); else mHeadBobbingWeight = std::max(mHeadBobbingWeight - duration * 5, 0.f); float doubleStepState = mTotalMovement / doubleStepLength - std::floor(mTotalMovement / doubleStepLength); // from 0 to 1 during 2 steps float stepState = std::abs(doubleStepState * 4 - 2) - 1; // from -1 to 1 on even steps and from 1 to -1 on odd steps float effect = (1 - std::cos(stepState * osg::DegreesToRadians(30.f))) * 7.5f; // range from 0 to 1 float coef = std::min(mSmoothedSpeed / 300.f, 1.f) * mHeadBobbingWeight; mHeadBobbingOffset = (0.5f - effect) * coef * stepHeight; // range from -stepHeight/2 to stepHeight/2 mRoll = osg::sign(stepState) * effect * coef * maxRoll; // range from -maxRoll to maxRoll } void Camera::reset() { togglePreviewMode(false); toggleVanityMode(false); if (!mFirstPersonView) toggleViewMode(); } void Camera::rotateCamera(float pitch, float yaw, bool adjust) { if (adjust) { pitch += getPitch(); yaw += getYaw(); } setYaw(yaw); setPitch(pitch); } void Camera::update(float duration, bool paused) { if (mAnimation->upperBodyReady()) { // Now process the view changes we queued earlier if (mVanityToggleQueued) { toggleVanityMode(mVanityToggleQueuedValue); mVanityToggleQueued = false; } if (mViewModeToggleQueued) { togglePreviewMode(false); toggleViewMode(); mViewModeToggleQueued = false; } } if (paused) return; // only show the crosshair in game mode MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); wm->showCrosshair(!wm->isGuiMode() && mMode != Mode::Preview && mMode != Mode::Vanity && (mFirstPersonView || mShowCrosshairInThirdPersonMode)); if(mMode == Mode::Vanity) rotateCamera(0.f, osg::DegreesToRadians(3.f * duration), true); if (isFirstPerson() && mHeadBobbingEnabled) updateHeadBobbing(duration); else mRoll = mHeadBobbingOffset = 0; updateFocalPointOffset(duration); updatePosition(); float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr); mTotalMovement += speed * duration; speed /= (1.f + speed / 500.f); float maxDelta = 300.f * duration; mSmoothedSpeed += osg::clampBetween(speed - mSmoothedSpeed, -maxDelta, maxDelta); mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance); updateStandingPreviewMode(); } void Camera::updatePosition() { mFocalPointAdjustment = osg::Vec3d(); if (isFirstPerson()) return; const float cameraObstacleLimit = 5.0f; const float focalObstacleLimit = 10.f; const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); // Adjust focal point to prevent clipping. osg::Vec3d focal = getFocalPoint(); osg::Vec3d focalOffset = getFocalPointOffset(); float offsetLen = focalOffset.length(); if (offsetLen > 0) { MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit); if (result.mHit) { double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; mFocalPointAdjustment = focalOffset * std::max(-1.0, adjustmentCoef); } } // Calculate camera distance. mCameraDistance = mBaseCameraDistance + getCameraDistanceCorrection(); if (mDynamicCameraDistanceEnabled) mCameraDistance = std::min(mCameraDistance, mMaxNextCameraDistance); osg::Vec3d cameraPos; getPosition(focal, cameraPos); MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit); if (result.mHit) mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(); } void Camera::updateStandingPreviewMode() { if (!mStandingPreviewAllowed) return; float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr); bool combat = mTrackingPtr.getClass().isActor() && mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).getDrawState() != MWMechanics::DrawState_Nothing; bool standingStill = speed == 0 && !combat && !mFirstPersonView; if (!standingStill && mMode == Mode::StandingPreview) { mMode = Mode::Normal; calculateDeferredRotation(); } else if (standingStill && mMode == Mode::Normal) mMode = Mode::StandingPreview; } void Camera::setFocalPointTargetOffset(osg::Vec2d v) { mFocalPointTargetOffset = v; mPreviousTransitionSpeed = mFocalPointTransitionSpeed; mPreviousTransitionInfluence = 1.0f; } void Camera::updateFocalPointOffset(float duration) { if (duration <= 0) return; if (mSkipFocalPointTransition) { mSkipFocalPointTransition = false; mPreviousExtraOffset = osg::Vec2d(); mPreviousTransitionInfluence = 0.f; mFocalPointCurrentOffset = mFocalPointTargetOffset; } osg::Vec2d oldOffset = mFocalPointCurrentOffset; if (mPreviousTransitionInfluence > 0) { mFocalPointCurrentOffset -= mPreviousExtraOffset; mPreviousExtraOffset = mPreviousExtraOffset / mPreviousTransitionInfluence + mPreviousTransitionSpeed * duration; mPreviousTransitionInfluence = std::max(0.f, mPreviousTransitionInfluence - duration * mFocalPointTransitionSpeedCoef); mPreviousExtraOffset *= mPreviousTransitionInfluence; mFocalPointCurrentOffset += mPreviousExtraOffset; } osg::Vec2d delta = mFocalPointTargetOffset - mFocalPointCurrentOffset; if (delta.length2() > 0) { float coef = duration * (1.0 + 5.0 / delta.length()) * mFocalPointTransitionSpeedCoef * (1.0f - mPreviousTransitionInfluence); mFocalPointCurrentOffset += delta * std::min(coef, 1.0f); } else { mPreviousExtraOffset = osg::Vec2d(); mPreviousTransitionInfluence = 0.f; } mFocalPointTransitionSpeed = (mFocalPointCurrentOffset - oldOffset) / duration; } void Camera::toggleViewMode(bool force) { // Changing the view will stop all playing animations, so if we are playing // anything important, queue the view change for later if (!mAnimation->upperBodyReady() && !force) { mViewModeToggleQueued = true; return; } else mViewModeToggleQueued = false; mFirstPersonView = !mFirstPersonView; updateStandingPreviewMode(); instantTransition(); processViewChange(); } void Camera::allowVanityMode(bool allow) { if (!allow && mMode == Mode::Vanity) { disableDeferredPreviewRotation(); toggleVanityMode(false); } mVanityAllowed = allow; } bool Camera::toggleVanityMode(bool enable) { // Changing the view will stop all playing animations, so if we are playing // anything important, queue the view change for later if (mFirstPersonView && !mAnimation->upperBodyReady()) { mVanityToggleQueued = true; mVanityToggleQueuedValue = enable; return false; } if (!mVanityAllowed && enable) return false; if ((mMode == Mode::Vanity) == enable) return true; mMode = enable ? Mode::Vanity : Mode::Normal; if (!mDeferredRotationAllowed) disableDeferredPreviewRotation(); if (!enable) calculateDeferredRotation(); processViewChange(); return true; } void Camera::togglePreviewMode(bool enable) { if (mFirstPersonView && !mAnimation->upperBodyReady()) return; if((mMode == Mode::Preview) == enable) return; mMode = enable ? Mode::Preview : Mode::Normal; if (mMode == Mode::Normal) updateStandingPreviewMode(); else if (mFirstPersonView) instantTransition(); if (mMode == Mode::Normal) { if (!mDeferredRotationAllowed) disableDeferredPreviewRotation(); calculateDeferredRotation(); } processViewChange(); } void Camera::setSneakOffset(float offset) { mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset)); } void Camera::setYaw(float angle) { mYaw = Misc::normalizeAngle(angle); } void Camera::setPitch(float angle) { const float epsilon = 0.000001f; float limit = osg::PI_2 - epsilon; mPitch = osg::clampBetween(angle, -limit, limit); } float Camera::getCameraDistance() const { if (isFirstPerson()) return 0.f; return mCameraDistance; } void Camera::adjustCameraDistance(float delta) { if (!isFirstPerson()) { if(isNearest() && delta < 0.f && getMode() != Mode::Preview && getMode() != Mode::Vanity) toggleViewMode(); else mBaseCameraDistance = std::min(mCameraDistance - getCameraDistanceCorrection(), mBaseCameraDistance) + delta; } else if (delta > 0.f) { toggleViewMode(); mBaseCameraDistance = 0; } mIsNearest = mBaseCameraDistance <= mNearest; mBaseCameraDistance = osg::clampBetween(mBaseCameraDistance, mNearest, mFurthest); Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance); } float Camera::getCameraDistanceCorrection() const { if (!mDynamicCameraDistanceEnabled) return 0; float pitchCorrection = std::max(-getPitch(), 0.f) * 50.f; float smoothedSpeedSqr = mSmoothedSpeed * mSmoothedSpeed; float speedCorrection = smoothedSpeedSqr / (smoothedSpeedSqr + 300.f*300.f) * mZoomOutWhenMoveCoef; return pitchCorrection + speedCorrection; } void Camera::setAnimation(NpcAnimation *anim) { mAnimation = anim; processViewChange(); } void Camera::processViewChange() { if(isFirstPerson()) { mAnimation->setViewMode(NpcAnimation::VM_FirstPerson); mTrackingNode = mAnimation->getNode("Camera"); if (!mTrackingNode) mTrackingNode = mAnimation->getNode("Head"); mHeightScale = 1.f; } else { mAnimation->setViewMode(NpcAnimation::VM_Normal); SceneUtil::PositionAttitudeTransform* transform = mTrackingPtr.getRefData().getBaseNode(); mTrackingNode = transform; if (transform) mHeightScale = transform->getScale().z(); else mHeightScale = 1.f; } rotateCamera(getPitch(), getYaw(), false); } void Camera::applyDeferredPreviewRotationToPlayer(float dt) { if (isVanityOrPreviewModeEnabled() || mTrackingPtr.isEmpty()) return; osg::Vec3f rot = mDeferredRotation; float delta = rot.normalize(); delta = std::min(delta, (delta + 1.f) * 3 * dt); rot *= delta; mDeferredRotation -= rot; if (mDeferredRotationDisabled) { mDeferredRotationDisabled = delta > 0.0001; rotateCameraToTrackingPtr(); return; } auto& movement = mTrackingPtr.getClass().getMovementSettings(mTrackingPtr); movement.mRotation[0] += rot.x(); movement.mRotation[1] += rot.y(); movement.mRotation[2] += rot.z(); if (std::abs(mDeferredRotation.z()) > 0.0001) { float s = std::sin(mDeferredRotation.z()); float c = std::cos(mDeferredRotation.z()); float x = movement.mPosition[0]; float y = movement.mPosition[1]; movement.mPosition[0] = x * c + y * s; movement.mPosition[1] = x * -s + y * c; } } void Camera::rotateCameraToTrackingPtr() { setPitch(-mTrackingPtr.getRefData().getPosition().rot[0] - mDeferredRotation.x()); setYaw(-mTrackingPtr.getRefData().getPosition().rot[2] - mDeferredRotation.z()); } void Camera::instantTransition() { mSkipFocalPointTransition = true; mDeferredRotationDisabled = false; mDeferredRotation = osg::Vec3f(); rotateCameraToTrackingPtr(); } void Camera::calculateDeferredRotation() { MWWorld::Ptr ptr = mTrackingPtr; if (isVanityOrPreviewModeEnabled() || ptr.isEmpty()) return; if (mFirstPersonView) { instantTransition(); return; } mDeferredRotation.x() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[0] - mPitch); mDeferredRotation.z() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[2] - mYaw); } }