#include "camera.hpp" #include #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 SceneUtil::NodeCallback { public: UpdateRenderCameraCallback(MWRender::Camera* cam) : mCamera(cam) { } void operator()(osg::Camera* cam, osg::NodeVisitor* nv) { // traverse first to update animations, in case the camera is attached to an animated node traverse(cam, nv); mCamera->updateCamera(cam); } private: MWRender::Camera* mCamera; }; } namespace MWRender { Camera::Camera (osg::Camera* camera) : mHeightScale(1.f), mCollisionType((MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor) | MWPhysics::CollisionType_CameraOnly), mCamera(camera), mAnimation(nullptr), mFirstPersonView(true), mMode(Mode::FirstPerson), mVanityAllowed(true), mDeferredRotationAllowed(true), mProcessViewChange(false), mHeight(124.f), mPitch(0.f), mYaw(0.f), mRoll(0.f), mCameraDistance(0.f), mPreferredCameraDistance(0.f), mFocalPointCurrentOffset(osg::Vec2d()), mFocalPointTargetOffset(osg::Vec2d()), mFocalPointTransitionSpeedCoef(1.f), mSkipFocalPointTransition(true), mPreviousTransitionInfluence(0.f), mShowCrosshair(false), mDeferredRotation(osg::Vec3f()), mDeferredRotationDisabled(false) { mUpdateCallback = new UpdateRenderCameraCallback(this); mCamera->addUpdateCallback(mUpdateCallback); } Camera::~Camera() { mCamera->removeUpdateCallback(mUpdateCallback); } osg::Vec3d Camera::calculateTrackedPosition() 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 res = worldMat.getTrans(); if (mMode != Mode::FirstPerson) res.z() += mHeight * mHeightScale; return res; } osg::Vec3d Camera::getFocalPointOffset() const { osg::Vec3d offset; offset.x() = mFocalPointCurrentOffset.x() * cos(mYaw); offset.y() = mFocalPointCurrentOffset.x() * sin(mYaw); offset.z() = mFocalPointCurrentOffset.y(); return offset; } void Camera::updateCamera(osg::Camera *cam) { osg::Quat orient = osg::Quat(mRoll + mExtraRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0, 0, 1)); osg::Vec3d forward = orient * osg::Vec3d(0,1,0); osg::Vec3d up = orient * osg::Vec3d(0,0,1); osg::Vec3d pos = mPosition; if (mMode == Mode::FirstPerson) { // It is a hack. Camera position depends on neck animation. // Animations are updated in OSG cull traversal and in order to avoid 1 frame delay we // recalculate the position here. Note that it becomes different from mPosition that // is used in other parts of the code. // TODO: detach camera from OSG animation and get rid of this hack. osg::Vec3d recalculatedTrackedPosition = calculateTrackedPosition(); pos = calculateFirstPersonPosition(recalculatedTrackedPosition); } cam->setViewMatrixAsLookAt(pos, pos + forward, up); mViewMatrix = cam->getViewMatrix(); } void Camera::update(float duration, bool paused) { mLockPitch = mLockYaw = false; if (mQueuedMode && mAnimation->upperBodyReady()) setMode(*mQueuedMode); if (mProcessViewChange) processViewChange(); if (paused) return; // only show the crosshair in game mode MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); wm->showCrosshair(!wm->isGuiMode() && mShowCrosshair); updateFocalPointOffset(duration); updatePosition(); } osg::Vec3d Camera::calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const { osg::Vec3d res = trackedPosition; osg::Vec2f horizontalOffset = Misc::rotateVec2f(osg::Vec2f(mFirstPersonOffset.x(), mFirstPersonOffset.y()), mYaw); res.x() += horizontalOffset.x(); res.y() += horizontalOffset.y(); res.z() += mFirstPersonOffset.z(); return res; } void Camera::updatePosition() { mTrackedPosition = calculateTrackedPosition(); if (mMode == Mode::Static) return; if (mMode == Mode::FirstPerson) { mPosition = calculateFirstPersonPosition(mTrackedPosition); mCameraDistance = 0; return; } constexpr float cameraObstacleLimit = 5.0f; constexpr float focalObstacleLimit = 10.f; const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); // Adjust focal point to prevent clipping. osg::Vec3d focalOffset = getFocalPointOffset(); osg::Vec3d focal = mTrackedPosition + focalOffset; focalOffset.z() += 10.f; // Needed to avoid camera clipping through the ceiling because // character's head can be a bit higher than the collision area. float offsetLen = focalOffset.length(); if (offsetLen > 0) { MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit, mCollisionType); if (result.mHit) { double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; focal += focalOffset * std::max(-1.0, adjustmentCoef); } } // Adjust camera distance. mCameraDistance = mPreferredCameraDistance; osg::Quat orient = osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1,0,0)) * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0,0,1)); osg::Vec3d offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, focal + offset, cameraObstacleLimit, mCollisionType); if (result.mHit) { mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(); offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); } mPosition = focal + offset; } void Camera::setMode(Mode newMode, bool force) { if (mMode == newMode) return; Mode oldMode = mMode; if (!force && (newMode == Mode::FirstPerson || oldMode == Mode::FirstPerson) && mAnimation && !mAnimation->upperBodyReady()) { // Changing the view will stop all playing animations, so if we are playing // anything important, queue the view change for later mQueuedMode = newMode; return; } mMode = newMode; mQueuedMode = std::nullopt; if (newMode == Mode::FirstPerson) mFirstPersonView = true; else if (newMode == Mode::ThirdPerson) mFirstPersonView = false; calculateDeferredRotation(); if (oldMode == Mode::FirstPerson || newMode == Mode::FirstPerson) { instantTransition(); mProcessViewChange = true; } } void Camera::setFocalPointTargetOffset(const 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) { setMode(mFirstPersonView ? Mode::ThirdPerson : Mode::FirstPerson, force); } bool Camera::toggleVanityMode(bool enable) { if (!enable) setMode(mFirstPersonView ? Mode::FirstPerson : Mode::ThirdPerson, false); else if (mVanityAllowed) setMode(Mode::Vanity, false); return (mMode == Mode::Vanity) == enable; } void Camera::setSneakOffset(float offset) { mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset)); } void Camera::setYaw(float angle, bool force) { if (!mLockYaw || force) mYaw = Misc::normalizeAngle(angle); if (force) mLockYaw = true; } void Camera::setPitch(float angle, bool force) { const float epsilon = 0.000001f; float limit = static_cast(osg::PI_2) - epsilon; if (!mLockPitch || force) mPitch = std::clamp(angle, -limit, limit); if (force) mLockPitch = true; } void Camera::setStaticPosition(const osg::Vec3d& pos) { if (mMode != Mode::Static) throw std::runtime_error("setStaticPosition can be used only if camera is in Static mode"); mPosition = pos; } void Camera::setAnimation(NpcAnimation *anim) { mAnimation = anim; mProcessViewChange = true; } void Camera::processViewChange() { if (mTrackingPtr.isEmpty()) return; if (mMode == Mode::FirstPerson) { 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; } mProcessViewChange = false; } void Camera::applyDeferredPreviewRotationToPlayer(float dt) { if (mMode != Mode::ThirdPerson || 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() { if (mMode == Mode::Static || mTrackingPtr.isEmpty()) return; 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() { if (mMode == Mode::Static) { mDeferredRotation = osg::Vec3f(); return; } MWWorld::Ptr ptr = mTrackingPtr; if (mMode == Mode::Preview || mMode == Mode::Vanity || 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); if (!mDeferredRotationAllowed) mDeferredRotationDisabled = true; } }