Merge branch 'camera2' into 'master'

Dehardcode camera

See merge request OpenMW/openmw!1030
fix-static-urls
AnyOldName3 3 years ago
commit a9d7598532

@ -293,6 +293,10 @@ bool OMW::Engine::frame(float frametime)
// Main menu opened? Then scripts are also paused.
bool paused = mEnvironment.getWindowManager()->containsMode(MWGui::GM_MainMenu);
// Should be called after input manager update and before any change to the game world.
// It applies to the game world queued changes from the previous frame.
mLuaManager->synchronizedUpdate(paused, frametime);
// update game state
{
ScopedProfile<UserStatsType::State> profile(frameStart, frameNumber, *timer, *stats);
@ -874,7 +878,6 @@ public:
}
else
update();
mEngine->mLuaManager->applyQueuedChanges();
};
void join()

@ -62,6 +62,7 @@ namespace MWPhysics
namespace MWRender
{
class Animation;
class Camera;
}
namespace MWMechanics
@ -433,14 +434,12 @@ namespace MWBase
virtual osg::Matrixf getActorHeadTransform(const MWWorld::ConstPtr& actor) const = 0;
virtual MWRender::Camera* getCamera() = 0;
virtual void togglePOV(bool force = false) = 0;
virtual bool isFirstPerson() const = 0;
virtual bool isPreviewModeEnabled() const = 0;
virtual void togglePreviewMode(bool enable) = 0;
virtual bool toggleVanityMode(bool enable) = 0;
virtual void allowVanityMode(bool allow) = 0;
virtual bool vanityRotateCamera(float * rot) = 0;
virtual void adjustCameraDistance(float dist) = 0;
virtual void applyDeferredPreviewRotationToPlayer(float dt) = 0;
virtual void disableDeferredPreviewRotation() = 0;

@ -27,7 +27,6 @@
namespace MWInput
{
const float ZOOM_SCALE = 10.f; /// Used for scrolling camera in and out
ActionManager::ActionManager(BindingsManager* bindingsManager,
osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation,
@ -41,7 +40,6 @@ namespace MWInput
, mSneaking(false)
, mAttemptJump(false)
, mOverencumberedMessageDelay(0.f)
, mPreviewPOVDelay(0.f)
, mTimeIdle(0.f)
{
}
@ -109,27 +107,6 @@ namespace MWInput
}
}
if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch"))
{
const float switchLimit = 0.25;
MWBase::World* world = MWBase::Environment::get().getWorld();
if (mBindingsManager->actionIsActive(A_TogglePOV))
{
if (world->isFirstPerson() ? mPreviewPOVDelay > switchLimit : mPreviewPOVDelay == 0)
world->togglePreviewMode(true);
mPreviewPOVDelay += dt;
}
else
{
//disable preview mode
if (mPreviewPOVDelay > 0)
world->togglePreviewMode(false);
if (mPreviewPOVDelay > 0.f && mPreviewPOVDelay <= switchLimit)
world->togglePOV();
mPreviewPOVDelay = 0.f;
}
}
if (triedToMove)
MWBase::Environment::get().getInputManager()->resetIdleTime();
@ -162,38 +139,16 @@ namespace MWInput
resetIdleTime();
}
else
{
updateIdleTime(dt);
}
mTimeIdle += dt;
mAttemptJump = false;
}
bool ActionManager::isPreviewModeEnabled()
{
return MWBase::Environment::get().getWorld()->isPreviewModeEnabled();
}
void ActionManager::resetIdleTime()
{
if (mTimeIdle < 0)
MWBase::Environment::get().getWorld()->toggleVanityMode(false);
mTimeIdle = 0.f;
}
void ActionManager::updateIdleTime(float dt)
{
static const float vanityDelay = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
.find("fVanityDelay")->mValue.getFloat();
if (mTimeIdle >= 0.f)
mTimeIdle += dt;
if (mTimeIdle > vanityDelay)
{
MWBase::Environment::get().getWorld()->toggleVanityMode(true);
mTimeIdle = -1.f;
}
}
void ActionManager::executeAction(int action)
{
MWBase::Environment::get().getLuaManager()->inputEvent({MWBase::LuaManager::InputEvent::Action, action});
@ -281,14 +236,6 @@ namespace MWInput
case A_ToggleDebug:
windowManager->toggleDebugWindow();
break;
case A_ZoomIn:
if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode())
MWBase::Environment::get().getWorld()->adjustCameraDistance(-ZOOM_SCALE);
break;
case A_ZoomOut:
if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode())
MWBase::Environment::get().getWorld()->adjustCameraDistance(ZOOM_SCALE);
break;
case A_QuickSave:
quickSave();
break;

@ -55,13 +55,9 @@ namespace MWInput
void setAttemptJump(bool enabled) { mAttemptJump = enabled; }
bool isPreviewModeEnabled();
private:
void handleGuiArrowKey(int action);
void updateIdleTime(float dt);
BindingsManager* mBindingsManager;
osg::ref_ptr<osgViewer::Viewer> mViewer;
osg::ref_ptr<osgViewer::ScreenCaptureHandler> mScreenCaptureHandler;
@ -72,7 +68,6 @@ namespace MWInput
bool mAttemptJump;
float mOverencumberedMessageDelay;
float mPreviewPOVDelay;
float mTimeIdle;
};
}

@ -34,12 +34,10 @@ namespace MWInput
, mJoystickEnabled (Settings::Manager::getBool("enable controller", "Input"))
, mGamepadCursorSpeed(Settings::Manager::getFloat("gamepad cursor speed", "Input"))
, mSneakToggleShortcutTimer(0.f)
, mGamepadZoom(0)
, mGamepadGuiCursorEnabled(true)
, mGuiCursorEnabled(true)
, mJoystickLastUsed(false)
, mSneakGamepadShortcut(false)
, mGamepadPreviewMode(false)
{
if (!controllerBindingsFile.empty())
{
@ -85,8 +83,6 @@ namespace MWInput
bool ControllerManager::update(float dt)
{
mGamepadPreviewMode = mActionManager->isPreviewModeEnabled();
if (mGuiCursorEnabled && !(mJoystickLastUsed && !mGamepadGuiCursorEnabled))
{
float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight) * 2.0f - 1.0f;
@ -115,7 +111,6 @@ namespace MWInput
if (MWBase::Environment::get().getWindowManager()->isGuiMode()
|| MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running)
{
mGamepadZoom = 0;
return false;
}
@ -182,15 +177,6 @@ namespace MWInput
}
}
if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch"))
{
if (!mBindingsManager->actionIsActive(A_TogglePOV))
mGamepadZoom = 0;
if (mGamepadZoom)
MWBase::Environment::get().getWorld()->adjustCameraDistance(-mGamepadZoom);
}
return triedToMove;
}
@ -289,21 +275,11 @@ namespace MWInput
{
gamepadToGuiControl(arg);
}
else
else if (MWBase::Environment::get().getWorld()->isPreviewModeEnabled() &&
(arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT || arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT))
{
if (mGamepadPreviewMode) // Preview Mode Gamepad Zooming
{
if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
{
mGamepadZoom = arg.value * 0.85f / 1000.f / 12.f;
return; // Do not propagate event.
}
else if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT)
{
mGamepadZoom = -arg.value * 0.85f / 1000.f / 12.f;
return; // Do not propagate event.
}
}
// Preview Mode Gamepad Zooming; do not propagate to mBindingsManager
return;
}
mBindingsManager->controllerAxisMoved(deviceID, arg);
}

@ -56,12 +56,10 @@ namespace MWInput
bool mJoystickEnabled;
float mGamepadCursorSpeed;
float mSneakToggleShortcutTimer;
float mGamepadZoom;
bool mGamepadGuiCursorEnabled;
bool mGuiCursorEnabled;
bool mJoystickLastUsed;
bool mSneakGamepadShortcut;
bool mGamepadPreviewMode;
};
}
#endif

@ -54,10 +54,6 @@ namespace MWInput
/// \fixme maybe crouching at this time
player.setUpDown(0);
}
else if (key == "vanitymode")
{
MWBase::Environment::get().getWorld()->allowVanityMode(value);
}
else if (key == "playerlooking" && !value)
{
MWBase::Environment::get().getWorld()->rotateObject(player.getPlayer(), osg::Vec3f());

@ -101,8 +101,6 @@ namespace MWInput
mMouseManager->update(dt);
mSensorManager->update(dt);
mActionManager->update(dt, controllerMove);
MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(dt);
}
void InputManager::setDragDrop(bool dragDrop)

@ -1,12 +1,82 @@
#include "luabindings.hpp"
#include "../mwrender/camera.hpp"
namespace MWLua
{
using CameraMode = MWRender::Camera::Mode;
sol::table initCameraPackage(const Context& context)
{
MWRender::Camera* camera = MWBase::Environment::get().getWorld()->getCamera();
sol::table api(context.mLua->sol(), sol::create);
// TODO
api["MODE"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with(
"Static", CameraMode::Static,
"FirstPerson", CameraMode::FirstPerson,
"ThirdPerson", CameraMode::ThirdPerson,
"Vanity", CameraMode::Vanity,
"Preview", CameraMode::Preview
));
api["getMode"] = [camera]() -> int { return static_cast<int>(camera->getMode()); };
api["getQueuedMode"] = [camera]() -> sol::optional<int>
{
std::optional<CameraMode> mode = camera->getQueuedMode();
if (mode)
return static_cast<int>(*mode);
else
return sol::nullopt;
};
api["setMode"] = [camera](int mode, sol::optional<bool> force)
{
camera->setMode(static_cast<CameraMode>(mode), force ? *force : false);
};
api["allowCharacterDeferredRotation"] = [camera](bool v) { camera->allowCharacterDeferredRotation(v); };
api["showCrosshair"] = [camera](bool v) { camera->showCrosshair(v); };
api["getTrackedPosition"] = [camera]() -> osg::Vec3f { return camera->getTrackedPosition(); };
api["getPosition"] = [camera]() -> osg::Vec3f { return camera->getPosition(); };
// All angles are negated in order to make camera rotation consistent with objects rotation.
// TODO: Fix the inconsistency of rotation direction in camera.cpp.
api["getPitch"] = [camera]() { return -camera->getPitch(); };
api["getYaw"] = [camera]() { return -camera->getYaw(); };
api["getRoll"] = [camera]() { return -camera->getRoll(); };
api["setStaticPosition"] = [camera](const osg::Vec3f& pos) { camera->setStaticPosition(pos); };
api["setPitch"] = [camera](float v)
{
camera->setPitch(-v, true);
if (camera->getMode() == CameraMode::ThirdPerson)
camera->calculateDeferredRotation();
};
api["setYaw"] = [camera](float v)
{
camera->setYaw(-v, true);
if (camera->getMode() == CameraMode::ThirdPerson)
camera->calculateDeferredRotation();
};
api["setRoll"] = [camera](float v) { camera->setRoll(-v); };
api["setExtraPitch"] = [camera](float v) { camera->setExtraPitch(-v); };
api["setExtraYaw"] = [camera](float v) { camera->setExtraYaw(-v); };
api["getExtraPitch"] = [camera]() { return -camera->getExtraPitch(); };
api["getExtraYaw"] = [camera]() { return -camera->getExtraYaw(); };
api["getThirdPersonDistance"] = [camera]() { return camera->getCameraDistance(); };
api["setPreferredThirdPersonDistance"] = [camera](float v) { camera->setPreferredCameraDistance(v); };
api["getFirstPersonOffset"] = [camera]() { return camera->getFirstPersonOffset(); };
api["setFirstPersonOffset"] = [camera](const osg::Vec3f& v) { camera->setFirstPersonOffset(v); };
api["getFocalPreferredOffset"] = [camera]() -> osg::Vec2f { return camera->getFocalPointTargetOffset(); };
api["setFocalPreferredOffset"] = [camera](const osg::Vec2f& v) { camera->setFocalPointTargetOffset(v); };
api["getFocalTransitionSpeed"] = [camera]() { return camera->getFocalPointTransitionSpeed(); };
api["setFocalTransitionSpeed"] = [camera](float v) { camera->setFocalPointTransitionSpeed(v); };
api["instantTransition"] = [camera]() { camera->instantTransition(); };
return LuaUtil::makeReadOnly(api);
}

@ -25,7 +25,7 @@ namespace MWLua
{
auto* lua = context.mLua;
sol::table api(lua->sol(), sol::create);
api["API_REVISION"] = 9;
api["API_REVISION"] = 10;
api["quit"] = [lua]()
{
Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback();

@ -132,14 +132,6 @@ namespace MWLua
mQueuedCallbacks.clear();
// Engine handlers in local scripts
PlayerScripts* playerScripts = dynamic_cast<PlayerScripts*>(mPlayer.getRefData().getLuaScripts());
if (playerScripts && !paused)
{
for (const auto& event : mInputEvents)
playerScripts->processInputEvent(event);
}
mInputEvents.clear();
for (const LocalEngineEvent& e : mLocalEngineEvents)
{
LObject obj(e.mDest, objectRegistry);
@ -180,8 +172,21 @@ namespace MWLua
mGlobalScripts.update(dt);
}
void LuaManager::applyQueuedChanges()
void LuaManager::synchronizedUpdate(bool paused, float dt)
{
if (mPlayer.isEmpty())
return; // The game is not started yet.
// We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency.
PlayerScripts* playerScripts = dynamic_cast<PlayerScripts*>(mPlayer.getRefData().getLuaScripts());
if (playerScripts && !paused)
{
for (const auto& event : mInputEvents)
playerScripts->processInputEvent(event);
playerScripts->inputUpdate(dt);
}
mInputEvents.clear();
MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager();
for (const std::string& message : mUIMessages)
windowManager->messageBox(message);

@ -32,7 +32,7 @@ namespace MWLua
void update(bool paused, float dt);
// Called by engine.cpp from the main thread. Can use scene graph.
void applyQueuedChanges();
void synchronizedUpdate(bool paused, float dt);
// Available everywhere through the MWBase::LuaManager interface.
// LuaManager queues these events and propagates to scripts on the next `update` call.

@ -17,7 +17,7 @@ namespace MWLua
{
registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers,
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers,
&mActionHandlers});
&mActionHandlers, &mInputUpdateHandlers});
}
void processInputEvent(const MWBase::LuaManager::InputEvent& event)
@ -43,12 +43,15 @@ namespace MWLua
}
}
void inputUpdate(float dt) { callEngineHandlers(mInputUpdateHandlers, dt); }
private:
EngineHandlerList mKeyPressHandlers{"onKeyPress"};
EngineHandlerList mKeyReleaseHandlers{"onKeyRelease"};
EngineHandlerList mControllerButtonPressHandlers{"onControllerButtonPress"};
EngineHandlerList mControllerButtonReleaseHandlers{"onControllerButtonRelease"};
EngineHandlerList mActionHandlers{"onInputAction"};
EngineHandlerList mInputUpdateHandlers{"onInputUpdate"};
};
}

@ -1598,6 +1598,7 @@ namespace MWMechanics
if (playerCharacter)
{
MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(duration);
playerCharacter->update(duration);
playerCharacter->setVisibility(1.f);
}

@ -56,41 +56,25 @@ namespace MWRender
mCamera(camera),
mAnimation(nullptr),
mFirstPersonView(true),
mMode(Mode::Normal),
mMode(Mode::FirstPerson),
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),
mDeferredRotationAllowed(true),
mProcessViewChange(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),
mPreferredCameraDistance(0.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),
mShowCrosshair(false),
mDeferredRotation(osg::Vec3f()),
mDeferredRotationDisabled(false)
{
mCameraDistance = mBaseCameraDistance;
mUpdateCallback = new UpdateRenderCameraCallback(this);
mCamera->addUpdateCallback(mUpdateCallback);
}
@ -100,7 +84,7 @@ namespace MWRender
mCamera->removeUpdateCallback(mUpdateCallback);
}
osg::Vec3d Camera::getFocalPoint() const
osg::Vec3d Camera::calculateTrackedPosition() const
{
if (!mTrackingNode)
return osg::Vec3d();
@ -108,155 +92,95 @@ namespace MWRender
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 res = worldMat.getTrans();
if (mMode != Mode::FirstPerson)
res.z() += mHeight * mHeightScale;
return res;
}
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();
osg::Vec3d offset;
offset.x() = mFocalPointCurrentOffset.x() * cos(mYaw);
offset.y() = mFocalPointCurrentOffset.x() * sin(mYaw);
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::Quat orient = osg::Quat(mRoll, 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);
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)
osg::Vec3d pos = mPosition;
if (mMode == Mode::FirstPerson)
{
pitch += getPitch();
yaw += getYaw();
// 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);
}
setYaw(yaw);
setPitch(pitch);
cam->setViewMatrixAsLookAt(pos, pos + forward, up);
}
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;
}
}
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() && 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;
wm->showCrosshair(!wm->isGuiMode() && mShowCrosshair);
updateFocalPointOffset(duration);
updatePosition();
}
float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr);
mTotalMovement += speed * duration;
speed /= (1.f + speed / 500.f);
float maxDelta = 300.f * duration;
mSmoothedSpeed += std::clamp(speed - mSmoothedSpeed, -maxDelta, maxDelta);
mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance);
updateStandingPreviewMode();
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()
{
mFocalPointAdjustment = osg::Vec3d();
if (isFirstPerson())
mTrackedPosition = calculateTrackedPosition();
if (mMode == Mode::Static)
return;
if (mMode == Mode::FirstPerson)
{
mPosition = calculateFirstPersonPosition(mTrackedPosition);
mCameraDistance = 0;
return;
}
const float cameraObstacleLimit = 5.0f;
const float focalObstacleLimit = 10.f;
const int collisionType = (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor);
constexpr float cameraObstacleLimit = 5.0f;
constexpr float focalObstacleLimit = 10.f;
const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting();
constexpr int collisionType = (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor);
// Adjust focal point to prevent clipping.
osg::Vec3d focal = getFocalPoint();
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)
{
@ -264,39 +188,51 @@ namespace MWRender
if (result.mHit)
{
double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen;
mFocalPointAdjustment = focalOffset * std::max(-1.0, adjustmentCoef);
focal += 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, collisionType);
// 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, collisionType);
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::updateStandingPreviewMode()
void Camera::setMode(Mode newMode, bool force)
{
if (!mStandingPreviewAllowed)
if (mMode == newMode)
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)
Mode oldMode = mMode;
if (!force && (newMode == Mode::FirstPerson || oldMode == Mode::FirstPerson) && !mAnimation->upperBodyReady())
{
mMode = Mode::Normal;
calculateDeferredRotation();
// 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;
}
else if (standingStill && mMode == Mode::Normal)
mMode = Mode::StandingPreview;
}
void Camera::setFocalPointTargetOffset(osg::Vec2d v)
void Camera::setFocalPointTargetOffset(const osg::Vec2d& v)
{
mFocalPointTargetOffset = v;
mPreviousTransitionSpeed = mFocalPointTransitionSpeed;
@ -346,78 +282,16 @@ namespace MWRender
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;
setMode(mFirstPersonView ? Mode::ThirdPerson : Mode::FirstPerson, force);
}
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();
setMode(mFirstPersonView ? Mode::FirstPerson : Mode::ThirdPerson, false);
else if (mVanityAllowed)
setMode(Mode::Vanity, false);
return (mMode == Mode::Vanity) == enable;
}
void Camera::setSneakOffset(float offset)
@ -425,67 +299,42 @@ namespace MWRender
mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset));
}
void Camera::setYaw(float angle)
void Camera::setYaw(float angle, bool force)
{
mYaw = Misc::normalizeAngle(angle);
if (!mLockYaw || force)
mYaw = Misc::normalizeAngle(angle);
if (force)
mLockYaw = true;
}
void Camera::setPitch(float angle)
void Camera::setPitch(float angle, bool force)
{
const float epsilon = 0.000001f;
float limit = static_cast<float>(osg::PI_2) - epsilon;
mPitch = std::clamp(angle, -limit, limit);
}
float Camera::getCameraDistance() const
{
if (isFirstPerson())
return 0.f;
return mCameraDistance;
if (!mLockPitch || force)
mPitch = std::clamp(angle, -limit, limit);
if (force)
mLockPitch = true;
}
void Camera::adjustCameraDistance(float delta)
void Camera::setStaticPosition(const osg::Vec3d& pos)
{
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 = std::clamp(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;
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;
processViewChange();
mProcessViewChange = true;
}
void Camera::processViewChange()
{
if(isFirstPerson())
if (mTrackingPtr.isEmpty())
return;
if (mMode == Mode::FirstPerson)
{
mAnimation->setViewMode(NpcAnimation::VM_FirstPerson);
mTrackingNode = mAnimation->getNode("Camera");
@ -503,12 +352,12 @@ namespace MWRender
else
mHeightScale = 1.f;
}
rotateCamera(getPitch(), getYaw(), false);
mProcessViewChange = false;
}
void Camera::applyDeferredPreviewRotationToPlayer(float dt)
{
if (isVanityOrPreviewModeEnabled() || mTrackingPtr.isEmpty())
if (mMode != Mode::ThirdPerson || mTrackingPtr.isEmpty())
return;
osg::Vec3f rot = mDeferredRotation;
@ -541,6 +390,8 @@ namespace MWRender
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());
}
@ -555,8 +406,13 @@ namespace MWRender
void Camera::calculateDeferredRotation()
{
if (mMode == Mode::Static)
{
mDeferredRotation = osg::Vec3f();
return;
}
MWWorld::Ptr ptr = mTrackingPtr;
if (isVanityOrPreviewModeEnabled() || ptr.isEmpty())
if (mMode == Mode::Preview || mMode == Mode::Vanity || ptr.isEmpty())
return;
if (mFirstPersonView)
{
@ -566,6 +422,8 @@ namespace MWRender
mDeferredRotation.x() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[0] - mPitch);
mDeferredRotation.z() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[2] - mYaw);
if (!mDeferredRotationAllowed)
mDeferredRotationDisabled = true;
}
}

@ -1,6 +1,7 @@
#ifndef GAME_MWRENDER_CAMERA_H
#define GAME_MWRENDER_CAMERA_H
#include <optional>
#include <string>
#include <osg/ref_ptr>
@ -24,73 +25,8 @@ namespace MWRender
class Camera
{
public:
enum class Mode { Normal, Vanity, Preview, StandingPreview };
enum class Mode : int {Static = 0, FirstPerson = 1, ThirdPerson = 2, Vanity = 3, Preview = 4};
private:
MWWorld::Ptr mTrackingPtr;
osg::ref_ptr<const osg::Node> mTrackingNode;
float mHeightScale;
osg::ref_ptr<osg::Camera> mCamera;
NpcAnimation *mAnimation;
bool mFirstPersonView;
Mode mMode;
bool mVanityAllowed;
bool mStandingPreviewAllowed;
bool mDeferredRotationAllowed;
float mNearest;
float mFurthest;
bool mIsNearest;
float mHeight, mBaseCameraDistance;
float mPitch, mYaw, mRoll;
bool mVanityToggleQueued;
bool mVanityToggleQueuedValue;
bool mViewModeToggleQueued;
float mCameraDistance;
float mMaxNextCameraDistance;
osg::Vec3d mFocalPointAdjustment;
osg::Vec2d mFocalPointCurrentOffset;
osg::Vec2d mFocalPointTargetOffset;
float mFocalPointTransitionSpeedCoef;
bool mSkipFocalPointTransition;
// This fields are used to make focal point transition smooth if previous transition was not finished.
float mPreviousTransitionInfluence;
osg::Vec2d mFocalPointTransitionSpeed;
osg::Vec2d mPreviousTransitionSpeed;
osg::Vec2d mPreviousExtraOffset;
float mSmoothedSpeed;
float mZoomOutWhenMoveCoef;
bool mDynamicCameraDistanceEnabled;
bool mShowCrosshairInThirdPersonMode;
bool mHeadBobbingEnabled;
float mHeadBobbingOffset;
float mHeadBobbingWeight; // Value from 0 to 1 for smooth enabling/disabling.
float mTotalMovement; // Needed for head bobbing.
void updateHeadBobbing(float duration);
void updateFocalPointOffset(float duration);
void updatePosition();
float getCameraDistanceCorrection() const;
osg::ref_ptr<osg::Callback> mUpdateCallback;
// Used to rotate player to the direction of view after exiting preview or vanity mode.
osg::Vec3f mDeferredRotation;
bool mDeferredRotationDisabled;
void calculateDeferredRotation();
void updateStandingPreviewMode();
public:
Camera(osg::Camera* camera);
~Camera();
@ -99,36 +35,36 @@ namespace MWRender
MWWorld::Ptr getTrackingPtr() const { return mTrackingPtr; }
void setFocalPointTransitionSpeed(float v) { mFocalPointTransitionSpeedCoef = v; }
void setFocalPointTargetOffset(osg::Vec2d v);
float getFocalPointTransitionSpeed() const { return mFocalPointTransitionSpeedCoef; }
void setFocalPointTargetOffset(const osg::Vec2d& v);
osg::Vec2d getFocalPointTargetOffset() const { return mFocalPointTargetOffset; }
void instantTransition();
void enableDynamicCameraDistance(bool v) { mDynamicCameraDistanceEnabled = v; }
void enableCrosshairInThirdPersonMode(bool v) { mShowCrosshairInThirdPersonMode = v; }
void showCrosshair(bool v) { mShowCrosshair = v; }
/// Update the view matrix of \a cam
void updateCamera(osg::Camera* cam);
/// Reset to defaults
void reset();
void reset() { setMode(Mode::FirstPerson); }
/// Set where the camera is looking at. Uses Morrowind (euler) angles
/// \param rot Rotation angles in radians
void rotateCamera(float pitch, float yaw, bool adjust);
void rotateCameraToTrackingPtr();
float getPitch() const { return mPitch; }
float getYaw() const { return mYaw; }
void setYaw(float angle);
float getRoll() const { return mRoll; }
float getPitch() const { return mPitch; }
void setPitch(float angle);
void setPitch(float angle, bool force = false);
void setYaw(float angle, bool force = false);
void setRoll(float angle) { mRoll = angle; }
float getExtraPitch() const { return mExtraPitch; }
float getExtraYaw() const { return mExtraYaw; }
void setExtraPitch(float angle) { mExtraPitch = angle; }
void setExtraYaw(float angle) { mExtraYaw = angle; }
/// @param Force view mode switch, even if currently not allowed by the animation.
void toggleViewMode(bool force=false);
bool toggleVanityMode(bool enable);
void allowVanityMode(bool allow);
/// @note this may be ignored if an important animation is currently playing
void togglePreviewMode(bool enable);
void applyDeferredPreviewRotationToPlayer(float dt);
void disableDeferredPreviewRotation() { mDeferredRotationDisabled = true; }
@ -136,29 +72,84 @@ namespace MWRender
/// \brief Lowers the camera for sneak.
void setSneakOffset(float offset);
bool isFirstPerson() const { return mFirstPersonView && mMode == Mode::Normal; }
void processViewChange();
void update(float duration, bool paused=false);
/// Adds distDelta to the camera distance. Switches 3rd/1st person view if distance is less than limit.
void adjustCameraDistance(float distDelta);
float getCameraDistance() const;
float getCameraDistance() const { return mCameraDistance; }
void setPreferredCameraDistance(float v) { mPreferredCameraDistance = v; }
void setAnimation(NpcAnimation *anim);
osg::Vec3d getFocalPoint() const;
osg::Vec3d getFocalPointOffset() const;
/// Stores focal and camera world positions in passed arguments
void getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const;
osg::Vec3d getTrackedPosition() const { return mTrackedPosition; }
const osg::Vec3d& getPosition() const { return mPosition; }
void setStaticPosition(const osg::Vec3d& pos);
bool isVanityOrPreviewModeEnabled() const { return mMode != Mode::Normal; }
bool isVanityOrPreviewModeEnabled() const { return mMode == Mode::Vanity || mMode == Mode::Preview; }
Mode getMode() const { return mMode; }
std::optional<Mode> getQueuedMode() const { return mQueuedMode; }
void setMode(Mode mode, bool force = true);
void allowCharacterDeferredRotation(bool v) { mDeferredRotationAllowed = v; }
void calculateDeferredRotation();
void setFirstPersonOffset(const osg::Vec3f& v) { mFirstPersonOffset = v; }
osg::Vec3f getFirstPersonOffset() const { return mFirstPersonOffset; }
private:
MWWorld::Ptr mTrackingPtr;
osg::ref_ptr<const osg::Node> mTrackingNode;
osg::Vec3d mTrackedPosition;
float mHeightScale;
osg::ref_ptr<osg::Camera> mCamera;
NpcAnimation *mAnimation;
// Always 'true' if mMode == `FirstPerson`. Also it is 'true' in `Vanity` or `Preview` modes if
// the camera should return to `FirstPerson` view after it.
bool mFirstPersonView;
Mode mMode;
std::optional<Mode> mQueuedMode;
bool mVanityAllowed;
bool mDeferredRotationAllowed;
bool mProcessViewChange;
float mHeight;
float mPitch, mYaw, mRoll;
float mExtraPitch = 0, mExtraYaw = 0;
bool mLockPitch = false, mLockYaw = false;
osg::Vec3d mPosition;
bool isNearest() const { return mIsNearest; }
float mCameraDistance, mPreferredCameraDistance;
osg::Vec3f mFirstPersonOffset{0, 0, 0};
osg::Vec2d mFocalPointCurrentOffset;
osg::Vec2d mFocalPointTargetOffset;
float mFocalPointTransitionSpeedCoef;
bool mSkipFocalPointTransition;
// This fields are used to make focal point transition smooth if previous transition was not finished.
float mPreviousTransitionInfluence;
osg::Vec2d mFocalPointTransitionSpeed;
osg::Vec2d mPreviousTransitionSpeed;
osg::Vec2d mPreviousExtraOffset;
bool mShowCrosshair;
osg::Vec3d calculateTrackedPosition() const;
osg::Vec3d calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const;
osg::Vec3d getFocalPointOffset() const;
void updateFocalPointOffset(float duration);
void updatePosition();
osg::ref_ptr<osg::Callback> mUpdateCallback;
// Used to rotate player to the direction of view after exiting preview or vanity mode.
osg::Vec3f mDeferredRotation;
bool mDeferredRotationDisabled;
};
}

@ -59,7 +59,6 @@
#include "vismask.hpp"
#include "pathgrid.hpp"
#include "camera.hpp"
#include "viewovershoulder.hpp"
#include "water.hpp"
#include "terrainstorage.hpp"
#include "navmesh.hpp"
@ -472,8 +471,6 @@ namespace MWRender
mWater.reset(new Water(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath));
mCamera.reset(new Camera(mViewer->getCamera()));
if (Settings::Manager::getBool("view over shoulder", "Camera"))
mViewOverShoulderController.reset(new ViewOverShoulderController(mCamera.get()));
mScreenshotManager.reset(new ScreenshotManager(viewer, mRootNode, sceneRoot, mResourceSystem, mWater.get()));
@ -826,15 +823,9 @@ namespace MWRender
updateNavMesh();
updateRecastMesh();
if (mViewOverShoulderController)
mViewOverShoulderController->update();
mCamera->update(dt, paused);
osg::Vec3d focal, cameraPos;
mCamera->getPosition(focal, cameraPos);
mCurrentCameraPos = cameraPos;
bool isUnderwater = mWater->isUnderwater(cameraPos);
bool isUnderwater = mWater->isUnderwater(mCamera->getPosition());
mStateUpdater->setFogStart(mFog->getFogStart(isUnderwater));
mStateUpdater->setFogEnd(mFog->getFogEnd(isUnderwater));
setFogColor(mFog->getFogColor(isUnderwater));

@ -79,7 +79,6 @@ namespace MWRender
class NpcAnimation;
class Pathgrid;
class Camera;
class ViewOverShoulderController;
class Water;
class TerrainStorage;
class LandManager;
@ -207,7 +206,6 @@ namespace MWRender
// camera stuff
Camera* getCamera() { return mCamera.get(); }
const osg::Vec3f& getCameraPosition() const { return mCurrentCameraPos; }
/// temporarily override the field of view with given value.
void overrideFieldOfView(float val);
@ -284,8 +282,6 @@ namespace MWRender
osg::ref_ptr<NpcAnimation> mPlayerAnimation;
osg::ref_ptr<SceneUtil::PositionAttitudeTransform> mPlayerNode;
std::unique_ptr<Camera> mCamera;
std::unique_ptr<ViewOverShoulderController> mViewOverShoulderController;
osg::Vec3f mCurrentCameraPos;
osg::ref_ptr<StateUpdater> mStateUpdater;
osg::ref_ptr<SharedUniformStateUpdater> mSharedUniformStateUpdater;

@ -1,110 +0,0 @@
#include "viewovershoulder.hpp"
#include <osg/Quat>
#include <components/settings/settings.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/ptr.hpp"
#include "../mwworld/refdata.hpp"
#include "../mwmechanics/drawstate.hpp"
namespace MWRender
{
ViewOverShoulderController::ViewOverShoulderController(Camera* camera) :
mCamera(camera), mMode(Mode::RightShoulder),
mAutoSwitchShoulder(Settings::Manager::getBool("auto switch shoulder", "Camera")),
mOverShoulderHorizontalOffset(30.f), mOverShoulderVerticalOffset(-10.f)
{
osg::Vec2f offset = Settings::Manager::getVector2("view over shoulder offset", "Camera");
mOverShoulderHorizontalOffset = std::abs(offset.x());
mOverShoulderVerticalOffset = offset.y();
mDefaultShoulderIsRight = offset.x() >= 0;
mCamera->enableDynamicCameraDistance(true);
mCamera->enableCrosshairInThirdPersonMode(true);
mCamera->setFocalPointTargetOffset(offset);
}
void ViewOverShoulderController::update()
{
if (mCamera->isFirstPerson())
return;
Mode oldMode = mMode;
auto ptr = mCamera->getTrackingPtr();
bool combat = ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getDrawState() != MWMechanics::DrawState_Nothing;
if (combat && !mCamera->isVanityOrPreviewModeEnabled())
mMode = Mode::Combat;
else if (MWBase::Environment::get().getWorld()->isSwimming(ptr))
mMode = Mode::Swimming;
else if (oldMode == Mode::Combat || oldMode == Mode::Swimming)
mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder;
if (mAutoSwitchShoulder && (mMode == Mode::LeftShoulder || mMode == Mode::RightShoulder))
trySwitchShoulder();
if (oldMode == mMode)
return;
if (mCamera->getMode() == Camera::Mode::Vanity)
// Player doesn't touch controls for a long time. Transition should be very slow.
mCamera->setFocalPointTransitionSpeed(0.2f);
else if ((oldMode == Mode::Combat || mMode == Mode::Combat) && mCamera->getMode() == Camera::Mode::Normal)
// Transition to/from combat mode and we are not it preview mode. Should be fast.
mCamera->setFocalPointTransitionSpeed(5.f);
else
mCamera->setFocalPointTransitionSpeed(1.f); // Default transition speed.
switch (mMode)
{
case Mode::RightShoulder:
mCamera->setFocalPointTargetOffset({mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset});
break;
case Mode::LeftShoulder:
mCamera->setFocalPointTargetOffset({-mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset});
break;
case Mode::Combat:
case Mode::Swimming:
default:
mCamera->setFocalPointTargetOffset({0, 15});
}
}
void ViewOverShoulderController::trySwitchShoulder()
{
if (mCamera->getMode() != Camera::Mode::Normal)
return;
const float limitToSwitch = 120; // switch to other shoulder if wall is closer than this limit
const float limitToSwitchBack = 300; // switch back to default shoulder if there is no walls at this distance
auto orient = osg::Quat(mCamera->getYaw(), osg::Vec3d(0,0,1));
osg::Vec3d playerPos = mCamera->getFocalPoint() - mCamera->getFocalPointOffset();
MWBase::World* world = MWBase::Environment::get().getWorld();
osg::Vec3d sideOffset = orient * osg::Vec3d(world->getHalfExtents(mCamera->getTrackingPtr()).x() - 1, 0, 0);
float rayRight = world->getDistToNearestRayHit(
playerPos + sideOffset, orient * osg::Vec3d(1, 0, 0), limitToSwitchBack + 1);
float rayLeft = world->getDistToNearestRayHit(
playerPos - sideOffset, orient * osg::Vec3d(-1, 0, 0), limitToSwitchBack + 1);
float rayRightForward = world->getDistToNearestRayHit(
playerPos + sideOffset, orient * osg::Vec3d(1, 3, 0), limitToSwitchBack + 1);
float rayLeftForward = world->getDistToNearestRayHit(
playerPos - sideOffset, orient * osg::Vec3d(-1, 3, 0), limitToSwitchBack + 1);
float distRight = std::min(rayRight, rayRightForward);
float distLeft = std::min(rayLeft, rayLeftForward);
if (distLeft < limitToSwitch && distRight > limitToSwitchBack)
mMode = Mode::RightShoulder;
else if (distRight < limitToSwitch && distLeft > limitToSwitchBack)
mMode = Mode::LeftShoulder;
else if (distRight > limitToSwitchBack && distLeft > limitToSwitchBack)
mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder;
}
}

@ -1,30 +0,0 @@
#ifndef VIEWOVERSHOULDER_H
#define VIEWOVERSHOULDER_H
#include "camera.hpp"
namespace MWRender
{
class ViewOverShoulderController
{
public:
ViewOverShoulderController(Camera* camera);
void update();
private:
void trySwitchShoulder();
enum class Mode { RightShoulder, LeftShoulder, Combat, Swimming };
Camera* mCamera;
Mode mMode;
bool mAutoSwitchShoulder;
float mOverShoulderHorizontalOffset;
float mOverShoulderVerticalOffset;
bool mDefaultShoulderIsRight;
};
}
#endif // VIEWOVERSHOULDER_H

@ -165,9 +165,15 @@ namespace MWWorld
listener->loadingOff();
// insert records that may not be present in all versions of MW
if (mEsm[0].getFormat() == 0)
ensureNeededRecords();
// Find main game file
for (const ESM::ESMReader& reader : mEsm)
{
if (!Misc::StringUtils::ciEndsWith(reader.getName(), ".esm") && !Misc::StringUtils::ciEndsWith(reader.getName(), ".omwgame"))
continue;
if (reader.getFormat() == 0)
ensureNeededRecords(); // and insert records that may not be present in all versions of MW.
break;
}
mCurrentDate.reset(new DateTimeManager());
@ -595,13 +601,7 @@ namespace MWWorld
void World::useDeathCamera()
{
if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() )
{
mRendering->getCamera()->togglePreviewMode(false);
mRendering->getCamera()->toggleVanityMode(false);
}
if(mRendering->getCamera()->isFirstPerson())
mRendering->getCamera()->toggleViewMode(true);
mRendering->getCamera()->setMode(MWRender::Camera::Mode::ThirdPerson);
}
MWWorld::Player& World::getPlayer()
@ -1858,7 +1858,7 @@ namespace MWWorld
}
bool isWerewolf = player.getClass().getNpcStats(player).isWerewolf();
bool isFirstPerson = mRendering->getCamera()->isFirstPerson();
bool isFirstPerson = this->isFirstPerson();
if (isWerewolf && isFirstPerson)
{
float werewolfFov = Fallback::Map::getFloat("General_Werewolf_FOV");
@ -1928,11 +1928,12 @@ namespace MWWorld
void World::updateSoundListener()
{
osg::Vec3f cameraPosition = mRendering->getCamera()->getPosition();
const ESM::Position& refpos = getPlayerPtr().getRefData().getPosition();
osg::Vec3f listenerPos;
if (isFirstPerson())
listenerPos = mRendering->getCameraPosition();
listenerPos = cameraPosition;
else
listenerPos = refpos.asVec3() + osg::Vec3f(0, 0, 1.85f * mPhysics->getHalfExtents(getPlayerPtr()).z());
@ -1943,7 +1944,7 @@ namespace MWWorld
osg::Vec3f forward = listenerOrient * osg::Vec3f(0,1,0);
osg::Vec3f up = listenerOrient * osg::Vec3f(0,0,1);
bool underwater = isUnderwater(getPlayerPtr().getCell(), mRendering->getCameraPosition());
bool underwater = isUnderwater(getPlayerPtr().getCell(), cameraPosition);
MWBase::Environment::get().getSoundManager()->setListenerPosDir(listenerPos, forward, up, underwater);
}
@ -2395,7 +2396,7 @@ namespace MWWorld
bool World::isFirstPerson() const
{
return mRendering->getCamera()->isFirstPerson();
return mRendering->getCamera()->getMode() == MWRender::Camera::Mode::FirstPerson;
}
bool World::isPreviewModeEnabled() const
@ -2403,11 +2404,6 @@ namespace MWWorld
return mRendering->getCamera()->getMode() == MWRender::Camera::Mode::Preview;
}
void World::togglePreviewMode(bool enable)
{
mRendering->getCamera()->togglePreviewMode(enable);
}
bool World::toggleVanityMode(bool enable)
{
return mRendering->getCamera()->toggleVanityMode(enable);
@ -2423,25 +2419,19 @@ namespace MWWorld
mRendering->getCamera()->applyDeferredPreviewRotationToPlayer(dt);
}
void World::allowVanityMode(bool allow)
{
mRendering->getCamera()->allowVanityMode(allow);
}
MWRender::Camera* World::getCamera() { return mRendering->getCamera(); }
bool World::vanityRotateCamera(float * rot)
{
if(!mRendering->getCamera()->isVanityOrPreviewModeEnabled())
auto* camera = mRendering->getCamera();
if(!camera->isVanityOrPreviewModeEnabled())
return false;
mRendering->getCamera()->rotateCamera(rot[0], rot[2], true);
camera->setPitch(camera->getPitch() + rot[0]);
camera->setYaw(camera->getYaw() + rot[2]);
return true;
}
void World::adjustCameraDistance(float dist)
{
mRendering->getCamera()->adjustCameraDistance(dist);
}
void World::saveLoaded()
{
mStore.validateDynamic();

@ -529,13 +529,10 @@ namespace MWWorld
bool isFirstPerson() const override;
bool isPreviewModeEnabled() const override;
void togglePreviewMode(bool enable) override;
bool toggleVanityMode(bool enable) override;
void allowVanityMode(bool allow) override;
MWRender::Camera* getCamera() override;
bool vanityRotateCamera(float * rot) override;
void adjustCameraDistance(float dist) override;
void applyDeferredPreviewRotationToPlayer(float dt) override;
void disableDeferredPreviewRotation() override;

@ -93,13 +93,13 @@ namespace
EXPECT_EQ(getAsString(lua, "moveAndScale * v(300, 200, 100)"), "(156, 222, 68)");
EXPECT_THAT(getAsString(lua, "moveAndScale"), AllOf(StartsWith("TransformM{ move(6, 22, 18) scale(0.5, 1, 0.5) "), EndsWith(" }")));
EXPECT_EQ(getAsString(lua, "T.identity"), "TransformM{ }");
lua.safe_script("rx = T.rotateX(math.pi / 2)");
lua.safe_script("ry = T.rotateY(math.pi / 2)");
lua.safe_script("rz = T.rotateZ(math.pi / 2)");
lua.safe_script("rx = T.rotateX(-math.pi / 2)");
lua.safe_script("ry = T.rotateY(-math.pi / 2)");
lua.safe_script("rz = T.rotateZ(-math.pi / 2)");
EXPECT_LT(get<float>(lua, "(rx * v(1, 2, 3) - v(1, -3, 2)):length()"), 1e-6);
EXPECT_LT(get<float>(lua, "(ry * v(1, 2, 3) - v(3, 2, -1)):length()"), 1e-6);
EXPECT_LT(get<float>(lua, "(rz * v(1, 2, 3) - v(-2, 1, 3)):length()"), 1e-6);
lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(-math.pi / 4)");
lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(math.pi / 4)");
EXPECT_THAT(getAsString(lua, "rot"), HasSubstr("TransformQ"));
EXPECT_LT(get<float>(lua, "(rot * v(1, 0, 0) - v(0, 0, 1)):length()"), 1e-6);
EXPECT_LT(get<float>(lua, "(rot * rot:inverse() * v(1, 0, 0) - v(1, 0, 0)):length()"), 1e-6);

@ -430,6 +430,10 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path)
if (item(info.fileName()))
continue;
// Enabled by default in system openmw.cfg; shouldn't be shown in content list.
if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0)
continue;
if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive))
{
EsmFile *file = new EsmFile(path2);

@ -32,14 +32,14 @@ public:
int getVer() const { return mHeader.mData.version; }
int getRecordCount() const { return mHeader.mData.records; }
float getFVer() const { return (mHeader.mData.version == VER_12) ? 1.2f : 1.3f; }
const std::string getAuthor() const { return mHeader.mData.author; }
const std::string getDesc() const { return mHeader.mData.desc; }
const std::string& getAuthor() const { return mHeader.mData.author; }
const std::string& getDesc() const { return mHeader.mData.desc; }
const std::vector<Header::MasterData> &getGameFiles() const { return mHeader.mMaster; }
const Header& getHeader() const { return mHeader; }
int getFormat() const { return mHeader.mFormat; };
const NAME &retSubName() const { return mCtx.subName; }
uint32_t getSubSize() const { return mCtx.leftSub; }
std::string getName() const {return mCtx.filename; };
const std::string& getName() const { return mCtx.filename; };
/*************************************************************************
*

@ -105,9 +105,9 @@ namespace LuaUtil
[](const Vec3& v) { return TransformM{osg::Matrixf::scale(v)}; },
[](float x, float y, float z) { return TransformM{osg::Matrixf::scale(x, y, z)}; });
transforms["rotate"] = [](float angle, const Vec3& axis) { return TransformQ{osg::Quat(angle, axis)}; };
transforms["rotateX"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(1, 0, 0))}; };
transforms["rotateY"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 1, 0))}; };
transforms["rotateZ"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 0, 1))}; };
transforms["rotateX"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(-1, 0, 0))}; };
transforms["rotateY"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, -1, 0))}; };
transforms["rotateZ"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 0, -1))}; };
transMType[sol::meta_function::multiplication] = sol::overload(
[](const TransformM& a, const Vec3& b) { return a.mM.preMult(b); },

@ -65,4 +65,5 @@ $DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw/*lua
cd $FILES_DIR/builtin_scripts
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw_aux/*lua
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/camera.lua

@ -17,7 +17,9 @@ Lua API reference
openmw_nearby
openmw_input
openmw_ui
openmw_camera
openmw_aux_util
interface_camera
- :ref:`Engine handlers reference`
@ -56,11 +58,11 @@ Player scripts are local scripts that are attached to a player.
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.nearby <Package openmw.nearby>` | by local scripts | | Read-only access to the nearest area of the game world. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.input <Package openmw.input>` | by player scripts | | User input |
|:ref:`openmw.input <Package openmw.input>` | by player scripts | | User input. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls :ref:`user interface <User interface reference>` |
|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls :ref:`user interface <User interface reference>`. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|openmw.camera | by player scripts | | Controls camera (not implemented) |
|:ref:`openmw.camera <Package openmw.camera>` | by player scripts | | Controls camera. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
**openmw_aux**
@ -73,3 +75,13 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid
+=========================================================+====================+===============================================================+
|:ref:`openmw_aux.util <Package openmw_aux.util>` | everywhere | | Miscellaneous utils |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
**Interfaces of built-in scripts**
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
| Interface | Can be used | Description |
+=========================================================+====================+===============================================================+
|:ref:`Camera <Interface Camera>` | by player scripts | | Allows to alter behavior of the built-in camera script |
| | | | without overriding the script completely. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+

@ -10,7 +10,7 @@ Engine handler is a function defined by a script, that can be called by the engi
| | | `be assigned to a script in openmw-cs (not yet implemented)`. |
| | | ``onInterfaceOverride`` can be called before ``onInit``. |
+----------------------------------+----------------------------------------------------------------------+
| onUpdate(dt) | | Called every frame if game not paused. `dt` is the time |
| onUpdate(dt) | | Called every frame if the game is not paused. `dt` is the time |
| | | from the last update in seconds. |
+----------------------------------+----------------------------------------------------------------------+
| onSave() -> savedData | | Called when the game is saving. May be called in inactive |
@ -44,6 +44,9 @@ Engine handler is a function defined by a script, that can be called by the engi
+----------------------------------+----------------------------------------------------------------------+
| **Only for local scripts attached to a player** |
+----------------------------------+----------------------------------------------------------------------+
| onInputUpdate(dt) | | Called every frame (if the game is not paused) right after |
| | | processing user input. Use it only for latency-critical stuff. |
+----------------------------------+----------------------------------------------------------------------+
| onKeyPress(key) | | `Key <openmw_input.html##(KeyboardEvent)>`_ is pressed. |
| | | Usage example: ``if key.symbol == 'z' and key.withShift then ...`` |
+----------------------------------+----------------------------------------------------------------------+

@ -0,0 +1,6 @@
Interface Camera
================
.. raw:: html
:file: generated_html/scripts_omw_camera.html

@ -0,0 +1,5 @@
Package openmw.camera
=====================
.. raw:: html
:file: generated_html/openmw_camera.html

@ -352,7 +352,7 @@ Player scripts are local scripts that are attached to a player.
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls :ref:`user interface <User interface reference>` |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|openmw.camera | by player scripts | | Controls camera (not implemented) |
|:ref:`openmw.camera <Package openmw.camera>` | by player scripts | | Controls camera |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
openmw_aux
@ -439,6 +439,15 @@ Using the interface:
The order in which the scripts are started is important. So if one mod should override an interface provided by another mod, make sure that load order (i.e. the sequence of `lua-scripts=...` in `openmw.cfg`) is correct.
**Interfaces of built-in scripts**
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
| Interface | Can be used | Description |
+=========================================================+====================+===============================================================+
|:ref:`Camera <Interface Camera>` | by player scripts | | Allows to alter behavior of the built-in camera script |
| | | | without overriding the script completely. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
Event system
============
@ -719,8 +728,7 @@ you can import these files to get code autocompletion and integrated OpenMW API
.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-project-settings.png
- Press `Next`, choose the `Libraries` tab, and click `Add External Source Folder`.
- Specify there the path to ``resources/lua_api`` in your OpenMW installation.
- If you use `openmw_aux`_, add ``resources/vfs`` as an additional external source folder.
- Specify there paths to ``resources/lua_api`` and ``resources/vfs`` in your OpenMW installation.
.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-import-api.png
@ -749,4 +757,17 @@ You can add special hints to give LDT more information:
.. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-code-completion2.png
In order to have autocompletion for script interfaces the information where to find these interfaces should be provided.
For example for the camera interface (defined in ``resources/vfs/scripts/omw/camera.lua``):
.. code-block:: Lua
--- @type Interfaces
-- @field scripts.omw.camera#Interface Camera
-- ... other interfaces here
--- @field #Interfaces I
local I = require('openmw.interfaces')
I.Camera.disableZoom()
See `LDT Documentation Language <https://wiki.eclipse.org/LDT/User_Area/Documentation_Language>`__ for more details.

@ -0,0 +1 @@
PLAYER: scripts/omw/camera.lua

@ -0,0 +1,243 @@
local camera = require('openmw.camera')
local input = require('openmw.input')
local settings = require('openmw.settings')
local util = require('openmw.util')
local self = require('openmw.self')
local head_bobbing = require('scripts.omw.head_bobbing')
local third_person = require('scripts.omw.third_person')
local MODE = camera.MODE
local previewIfStandSill = settings._getBoolFromSettingsCfg('Camera', 'preview if stand still')
local showCrosshairInThirdPerson = settings._getBoolFromSettingsCfg('Camera', 'view over shoulder')
local primaryMode
local noModeControl = 0
local noStandingPreview = 0
local noHeadBobbing = 0
local noZoom = 0
local function init()
camera.allowCharacterDeferredRotation(settings._getBoolFromSettingsCfg('Camera', 'deferred preview rotation'))
if camera.getMode() == MODE.FirstPerson then
primaryMode = MODE.FirstPerson
else
primaryMode = MODE.ThirdPerson
camera.setMode(MODE.ThirdPerson)
end
end
local smoothedSpeed = 0
local previewTimer = 0
local function updatePOV(dt)
local switchLimit = 0.25
if input.isActionPressed(input.ACTION.TogglePOV) and input.getControlSwitch(input.CONTROL_SWITCH.ViewMode) then
previewTimer = previewTimer + dt
if primaryMode == MODE.ThirdPerson or previewTimer >= switchLimit then
third_person.standingPreview = false
camera.setMode(MODE.Preview)
end
elseif previewTimer > 0 then
if previewTimer <= switchLimit then
if primaryMode == MODE.FirstPerson then
primaryMode = MODE.ThirdPerson
else
primaryMode = MODE.FirstPerson
end
end
camera.setMode(primaryMode)
previewTimer = 0
end
end
local idleTimer = 0
local vanityDelay = settings.getGMST('fVanityDelay')
local function updateVanity(dt)
if input.isIdle() then
idleTimer = idleTimer + dt
else
idleTimer = 0
end
local vanityAllowed = input.getControlSwitch(input.CONTROL_SWITCH.VanityMode)
if vanityAllowed and idleTimer > vanityDelay and camera.getMode() ~= MODE.Vanity then
camera.setMode(MODE.Vanity)
end
if camera.getMode() == MODE.Vanity then
if not vanityAllowed or idleTimer == 0 then
camera.setMode(primaryMode)
else
camera.setYaw(camera.getYaw() + math.rad(3) * dt)
end
end
end
local function updateSmoothedSpeed(dt)
local speed = self:getCurrentSpeed()
speed = speed / (1 + speed / 500)
local maxDelta = 300 * dt
smoothedSpeed = smoothedSpeed + util.clamp(speed - smoothedSpeed, -maxDelta, maxDelta)
end
local minDistance = 30
local maxDistance = 800
local function zoom(delta)
if not input.getControlSwitch(input.CONTROL_SWITCH.ViewMode) or
not input.getControlSwitch(input.CONTROL_SWITCH.Controls) or
camera.getMode() == MODE.Static or noZoom > 0 then
return
end
if camera.getMode() ~= MODE.FirstPerson then
local obstacleDelta = third_person.preferredDistance - camera.getThirdPersonDistance()
if delta > 0 and third_person.baseDistance == minDistance and
(camera.getMode() ~= MODE.Preview or third_person.standingPreview) and noModeControl == 0 then
primaryMode = MODE.FirstPerson
camera.setMode(primaryMode)
elseif delta > 0 or obstacleDelta < -delta then
third_person.baseDistance = util.clamp(third_person.baseDistance - delta - obstacleDelta, minDistance, maxDistance)
end
elseif delta < 0 and noModeControl == 0 then
primaryMode = MODE.ThirdPerson
camera.setMode(primaryMode)
third_person.baseDistance = minDistance
end
end
local function applyControllerZoom(dt)
if camera.getMode() == MODE.Preview then
local triggerLeft = input.getAxisValue(input.CONTROLLER_AXIS.TriggerLeft)
local triggerRight = input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight)
local controllerZoom = (triggerRight - triggerLeft) * 100 * dt
if controllerZoom ~= 0 then
zoom(controllerZoom)
end
end
end
local function updateStandingPreview()
local mode = camera.getMode()
if not previewIfStandSill or noStandingPreview > 0
or mode == MODE.FirstPerson or mode == MODE.Static or mode == MODE.Vanity then
third_person.standingPreview = false
return
end
local standingStill = self:getCurrentSpeed() == 0 and not self:isInWeaponStance() and not self:isInMagicStance()
if standingStill and mode == MODE.ThirdPerson then
third_person.standingPreview = true
camera.setMode(MODE.Preview)
elseif not standingStill and third_person.standingPreview then
third_person.standingPreview = false
camera.setMode(primaryMode)
end
end
local function updateCrosshair()
camera.showCrosshair(
camera.getMode() == MODE.FirstPerson or
(showCrosshairInThirdPerson and (camera.getMode() == MODE.ThirdPerson or third_person.standingPreview)))
end
local function onUpdate(dt)
camera.setExtraPitch(0)
camera.setExtraYaw(0)
camera.setRoll(0)
camera.setFirstPersonOffset(util.vector3(0, 0, 0))
updateSmoothedSpeed(dt)
end
local function onInputUpdate(dt)
local mode = camera.getMode()
if mode == MODE.FirstPerson or mode == MODE.ThirdPerson then
primaryMode = mode
end
if mode ~= MODE.Static then
if not camera.getQueuedMode() or camera.getQueuedMode() == MODE.Preview then
if noModeControl == 0 then
updatePOV(dt)
updateVanity(dt)
end
updateStandingPreview()
end
updateCrosshair()
end
applyControllerZoom(dt)
third_person.update(dt, smoothedSpeed)
if noHeadBobbing == 0 then head_bobbing.update(dt, smoothedSpeed) end
end
return {
interfaceName = 'Camera',
--- @module Camera
-- @usage require('openmw.interfaces').Camera
interface = {
--- @field [parent=#Camera] #number version Interface version
version = 0,
--- @function [parent=#Camera] getPrimaryMode Returns primary mode (MODE.FirstPerson or MODE.ThirdPerson).
getPrimaryMode = function() return primaryMode end,
--- @function [parent=#Camera] getBaseThirdPersonDistance
getBaseThirdPersonDistance = function() return third_person.baseDistance end,
--- @function [parent=#Camera] setBaseThirdPersonDistance
setBaseThirdPersonDistance = function(v) third_person.baseDistance = v end,
--- @function [parent=#Camera] getTargetThirdPersonDistance
getTargetThirdPersonDistance = function() return third_person.preferredDistance end,
--- @function [parent=#Camera] isModeControlEnabled
isModeControlEnabled = function() return noModeControl == 0 end,
--- @function [parent=#Camera] disableModeControl
disableModeControl = function() noModeControl = noModeControl + 1 end,
--- @function [parent=#Camera] enableModeControl
enableModeControl = function() noModeControl = math.max(0, noModeControl - 1) end,
--- @function [parent=#Camera] isStandingPreviewEnabled
isStandingPreviewEnabled = function() return previewIfStandSill and noStandingPreview == 0 end,
--- @function [parent=#Camera] disableStandingPreview
disableStandingPreview = function() noStandingPreview = noStandingPreview + 1 end,
--- @function [parent=#Camera] enableStandingPreview
enableStandingPreview = function() noStandingPreview = math.max(0, noStandingPreview - 1) end,
--- @function [parent=#Camera] isHeadBobbingEnabled
isHeadBobbingEnabled = function() return head_bobbing.enabled and noHeadBobbing == 0 end,
--- @function [parent=#Camera] disableHeadBobbing
disableHeadBobbing = function() noHeadBobbing = noHeadBobbing + 1 end,
--- @function [parent=#Camera] enableHeadBobbing
enableHeadBobbing = function() noHeadBobbing = math.max(0, noHeadBobbing - 1) end,
--- @function [parent=#Camera] isZoomEnabled
isZoomEnabled = function() return noZoom == 0 end,
--- @function [parent=#Camera] disableZoom
disableZoom = function() noZoom = noZoom + 1 end,
--- @function [parent=#Camera] enableZoom
enableZoom = function() noZoom = math.max(0, noZoom - 1) end,
--- @function [parent=#Camera] isThirdPersonOffsetControlEnabled
isThirdPersonOffsetControlEnabled = function() return third_person.noOffsetControl == 0 end,
--- @function [parent=#Camera] disableThirdPersonOffsetControl
disableThirdPersonOffsetControl = function() third_person.noOffsetControl = third_person.noOffsetControl + 1 end,
--- @function [parent=#Camera] enableThirdPersonOffsetControl
enableThirdPersonOffsetControl = function() third_person.noOffsetControl = math.max(0, third_person.noOffsetControl - 1) end,
},
engineHandlers = {
onUpdate = onUpdate,
onInputUpdate = onInputUpdate,
onInputAction = function(action)
if action == input.ACTION.ZoomIn then
zoom(10)
elseif action == input.ACTION.ZoomOut then
zoom(-10)
end
end,
onActive = init,
onLoad = function(data)
if data and data.distance then third_person.baseDistance = data.distance end
end,
onSave = function()
return {version = 0, distance = third_person.baseDistance}
end,
},
}

@ -0,0 +1,51 @@
local camera = require('openmw.camera')
local self = require('openmw.self')
local settings = require('openmw.settings')
local util = require('openmw.util')
local doubleStepLength = settings._getFloatFromSettingsCfg('Camera', 'head bobbing step') * 2
local stepHeight = settings._getFloatFromSettingsCfg('Camera', 'head bobbing height')
local maxRoll = math.rad(settings._getFloatFromSettingsCfg('Camera', 'head bobbing roll'))
local effectWeight = 0
local totalMovement = 0
local M = {
enabled = settings._getBoolFromSettingsCfg('Camera', 'head bobbing')
}
-- Trajectory of each step is a scaled arc of 60 degrees.
local halfArc = math.rad(30)
local sampleArc = function(x) return 1 - math.cos(x * halfArc) end
local arcHeight = sampleArc(1)
function M.update(dt, smoothedSpeed)
local speed = self:getCurrentSpeed()
speed = speed / (1 + speed / 500) -- limit bobbing frequency if the speed is very high
totalMovement = totalMovement + speed * dt
if not M.enabled or camera.getMode() ~= camera.MODE.FirstPerson then
effectWeight = 0
return
end
if self:isOnGround() then
effectWeight = math.min(1, effectWeight + dt * 5)
else
effectWeight = math.max(0, effectWeight - dt * 5)
end
local doubleStepState = totalMovement / doubleStepLength
doubleStepState = doubleStepState - math.floor(doubleStepState) -- from 0 to 1 during 2 steps
local stepState = math.abs(doubleStepState * 4 - 2) - 1 -- from -1 to 1 on even steps and from 1 to -1 on odd steps
local effect = sampleArc(stepState) / arcHeight -- range from 0 to 1
-- Smoothly reduce the effect to zero when the player stops
local coef = math.min(smoothedSpeed / 300, 1) * effectWeight
local zOffset = (0.5 - effect) * coef * stepHeight -- range from -stepHeight/2 to stepHeight/2
local roll = ((stepState > 0 and 1) or -1) * effect * coef * maxRoll -- range from -maxRoll to maxRoll
camera.setFirstPersonOffset(camera.getFirstPersonOffset() + util.vector3(0, 0, zOffset))
camera.setRoll(camera.getRoll() + roll)
end
return M

@ -0,0 +1,139 @@
local camera = require('openmw.camera')
local settings = require('openmw.settings')
local util = require('openmw.util')
local self = require('openmw.self')
local nearby = require('openmw.nearby')
local MODE = camera.MODE
local STATE = { RightShoulder = 0, LeftShoulder = 1, Combat = 2, Swimming = 3 }
local M = {
baseDistance = settings._getFloatFromSettingsCfg('Camera', 'third person camera distance'),
preferredDistance = 0,
standingPreview = false,
noOffsetControl = 0,
}
local viewOverShoulder = settings._getBoolFromSettingsCfg('Camera', 'view over shoulder')
local autoSwitchShoulder = settings._getBoolFromSettingsCfg('Camera', 'auto switch shoulder')
local shoulderOffset = settings._getVector2FromSettingsCfg('Camera', 'view over shoulder offset')
local zoomOutWhenMoveCoef = settings._getFloatFromSettingsCfg('Camera', 'zoom out when move coef')
local defaultShoulder = (shoulderOffset.x > 0 and STATE.RightShoulder) or STATE.LeftShoulder
local rightShoulderOffset = util.vector2(math.abs(shoulderOffset.x), shoulderOffset.y)
local leftShoulderOffset = util.vector2(-math.abs(shoulderOffset.x), shoulderOffset.y)
local combatOffset = util.vector2(0, 15)
local state = defaultShoulder
local rayOptions = {collisionType = nearby.COLLISION_TYPE.Default - nearby.COLLISION_TYPE.Actor}
local function ray(from, angle, limit)
local to = from + util.transform.rotateZ(angle) * util.vector3(0, limit, 0)
local res = nearby.castRay(from, to, rayOptions)
if res.hit then
return (res.hitPos - from):length()
else
return limit
end
end
local function trySwitchShoulder()
local limitToSwitch = 120 -- switch to other shoulder if wall is closer than this limit
local limitToSwitchBack = 300 -- switch back to default shoulder if there is no walls at this distance
local pos = camera.getTrackedPosition()
local rayRight = ray(pos, camera.getYaw() + math.rad(90), limitToSwitchBack + 1)
local rayLeft = ray(pos, camera.getYaw() - math.rad(90), limitToSwitchBack + 1)
local rayRightForward = ray(pos, camera.getYaw() + math.rad(30), limitToSwitchBack + 1)
local rayLeftForward = ray(pos, camera.getYaw() - math.rad(30), limitToSwitchBack + 1)
local distRight = math.min(rayRight, rayRightForward)
local distLeft = math.min(rayLeft, rayLeftForward)
if distLeft < limitToSwitch and distRight > limitToSwitchBack then
state = STATE.RightShoulder
elseif distRight < limitToSwitch and distLeft > limitToSwitchBack then
state = STATE.LeftShoulder
elseif distRight > limitToSwitchBack and distLeft > limitToSwitchBack then
state = defaultShoulder
end
end
local function calculateDistance(smoothedSpeed)
local smoothedSpeedSqr = smoothedSpeed * smoothedSpeed
return (M.baseDistance + math.max(camera.getPitch(), 0) * 50
+ smoothedSpeedSqr / (smoothedSpeedSqr + 300*300) * zoomOutWhenMoveCoef)
end
local noThirdPersonLastFrame = true
local function updateState()
local mode = camera.getMode()
local oldState = state
if (self:isInWeaponStance() or self:isInMagicStance()) and mode == MODE.ThirdPerson then
state = STATE.Combat
elseif self:isSwimming() then
state = STATE.Swimming
elseif oldState == STATE.Combat or oldState == STATE.Swimming then
state = defaultShoulder
end
if autoSwitchShoulder and (mode == MODE.ThirdPerson or state ~= oldState or noThirdPersonLastFrame)
and (state == STATE.LeftShoulder or state == STATE.RightShoulder) then
trySwitchShoulder()
end
if oldState ~= state or noThirdPersonLastFrame then
-- State was changed, start focal point transition.
if mode == MODE.Vanity then
-- Player doesn't touch controls for a long time. Transition should be very slow.
camera.setFocalTransitionSpeed(0.2)
elseif (oldState == STATE.Combat or state == STATE.Combat) and
(mode ~= MODE.Preview or M.standingPreview) then
-- Transition to/from combat mode and we are not in preview mode. Should be fast.
camera.setFocalTransitionSpeed(5.0)
else
camera.setFocalTransitionSpeed(1.0) -- Default transition speed.
end
if state == STATE.RightShoulder then
camera.setFocalPreferredOffset(rightShoulderOffset)
elseif state == STATE.LeftShoulder then
camera.setFocalPreferredOffset(leftShoulderOffset)
else
camera.setFocalPreferredOffset(combatOffset)
end
end
end
function M.update(dt, smoothedSpeed)
local mode = camera.getMode()
if mode == MODE.FirstPerson or mode == MODE.Static then
noThirdPersonLastFrame = true
return
end
if not viewOverShoulder then
M.preferredDistance = M.baseDistance
camera.setPreferredThirdPersonDistance(M.baseDistance)
noThirdPersonLastFrame = false
return
end
if M.noOffsetControl == 0 then
updateState()
else
state = nil
end
M.preferredDistance = calculateDistance(smoothedSpeed)
if noThirdPersonLastFrame then -- just switched to third person view
camera.setPreferredThirdPersonDistance(M.preferredDistance)
camera.instantTransition()
noThirdPersonLastFrame = false
else
local maxIncrease = dt * (100 + M.baseDistance)
camera.setPreferredThirdPersonDistance(math.min(
M.preferredDistance, camera.getThirdPersonDistance() + maxIncrease))
end
end
return M

@ -0,0 +1,171 @@
-------------------------------------------------------------------------------
-- `openmw.camera` controls camera.
-- Can be used only by player scripts.
-- @module camera
-- @usage local camera = require('openmw.camera')
-------------------------------------------------------------------------------
-- @type MODE Camera modes.
-- @field #number Static Camera doesn't track player; player inputs doesn't affect camera; use `setStaticPosition` to move the camera.
-- @field #number FirstPerson First person mode.
-- @field #number ThirdPerson Third person mode; player character turns to the view direction.
-- @field #number Vanity Similar to Preview; camera slowly moves around the player.
-- @field #number Preview Third person mode, but player character doesn't turn to the view direction.
-------------------------------------------------------------------------------
-- Camera modes.
-- @field [parent=#camera] #MODE MODE
-------------------------------------------------------------------------------
-- Return the current @{openmw.camera#MODE}.
-- @function [parent=#camera] getMode
-- @return #MODE
-------------------------------------------------------------------------------
-- Return the mode the camera will switch to after the end of the current animation. Can be nil.
-- @function [parent=#camera] getQueuedMode
-- @return #MODE
-------------------------------------------------------------------------------
-- Change @{openmw.camera#MODE}; if the second (optional, true by default) argument is set to false, the switching can be delayed (see `getQueuedMode`).
-- @function [parent=#camera] setMode
-- @param #MODE mode
-- @param #boolean force
-------------------------------------------------------------------------------
-- If set to true then after switching from Preview to ThirdPerson the player character turns to the camera view direction. Otherwise the camera turns to the character view direction.
-- @function [parent=#camera] allowCharacterDeferredRotation
-- @param #boolean boolValue
-------------------------------------------------------------------------------
-- Show/hide crosshair.
-- @function [parent=#camera] showCrosshair
-- @param #boolean boolValue
-------------------------------------------------------------------------------
-- Current position of the tracked object (the characters head if there is no animation).
-- @function [parent=#camera] getTrackedPosition
-- @return openmw.util#Vector3
-------------------------------------------------------------------------------
-- Current position of the camera.
-- @function [parent=#camera] getPosition
-- @return openmw.util#Vector3
-------------------------------------------------------------------------------
-- Camera pitch angle (radians) without taking extraPitch into account.
-- Full pitch is `getPitch()+getExtraPitch()`.
-- @function [parent=#camera] getPitch
-- @return #number
-------------------------------------------------------------------------------
-- Force the pitch angle to the given value (radians); player input on this axis is ignored in this frame.
-- @function [parent=#camera] setPitch
-- @param #number value
-------------------------------------------------------------------------------
-- Camera yaw angle (radians) without taking extraYaw into account.
-- Full yaw is `getYaw()+getExtraYaw()`.
-- @function [parent=#camera] getYaw
-- @return #number
-------------------------------------------------------------------------------
-- Force the yaw angle to the given value (radians); player input on this axis is ignored in this frame.
-- @function [parent=#camera] setYaw
-- @param #number value
-------------------------------------------------------------------------------
-- Get camera roll angle (radians).
-- @function [parent=#camera] getRoll
-- @return #number
-------------------------------------------------------------------------------
-- Set camera roll angle (radians).
-- @function [parent=#camera] setRoll
-- @param #number value
-------------------------------------------------------------------------------
-- Additional summand for the pitch angle that is not affected by player input.
-- Full pitch is `getPitch()+getExtraPitch()`.
-- @function [parent=#camera] getExtraPitch
-- @return #number
-------------------------------------------------------------------------------
-- Additional summand for the pitch angle; useful for camera shaking effects.
-- Setting extra pitch doesn't block player input.
-- Full pitch is `getPitch()+getExtraPitch()`.
-- @function [parent=#camera] setExtraPitch
-- @param #number value
-------------------------------------------------------------------------------
-- Additional summand for the yaw angle that is not affected by player input.
-- Full yaw is `getYaw()+getExtraYaw()`.
-- @function [parent=#camera] getExtraYaw
-- @return #number
-------------------------------------------------------------------------------
-- Additional summand for the yaw angle; useful for camera shaking effects.
-- Setting extra pitch doesn't block player input.
-- Full yaw is `getYaw()+getExtraYaw()`.
-- @function [parent=#camera] setExtraYaw
-- @param #number value
-------------------------------------------------------------------------------
-- Set camera position; can be used only if camera is in Static mode.
-- @function [parent=#camera] setStaticPosition
-- @param openmw.util#Vector3 pos
-------------------------------------------------------------------------------
-- The offset between the characters head and the camera in first person mode (3d vector).
-- @function [parent=#camera] getFirstPersonOffset
-- @return openmw.util#Vector3
-------------------------------------------------------------------------------
-- Set the offset between the characters head and the camera in first person mode (3d vector).
-- @function [parent=#camera] setFirstPersonOffset
-- @param openmw.util#Vector3 offset
-------------------------------------------------------------------------------
-- Preferred offset between tracked position (see `getTrackedPosition`) and the camera focal point (the center of the screen) in third person mode.
-- See `setFocalPreferredOffset`.
-- @function [parent=#camera] getFocalPreferredOffset
-- @return openmw.util#Vector2
-------------------------------------------------------------------------------
-- Set preferred offset between tracked position (see `getTrackedPosition`) and the camera focal point (the center of the screen) in third person mode.
-- The offset is a 2d vector (X, Y) where X is horizontal (to the right from the character) and Y component is vertical (upward).
-- The real offset can differ from the preferred one during smooth transition of if blocked by an obstacle.
-- Smooth transition happens by default every time when the preferred offset was changed. Use `instantTransition()` to skip the current transition.
-- @function [parent=#camera] setFocalPreferredOffset
-- @param openmw.util#Vector2 offset
-------------------------------------------------------------------------------
-- The actual distance between the camera and the character in third person mode; can differ from the preferred one if there is an obstacle.
-- @function [parent=#camera] getThirdPersonDistance
-- @return #number
-------------------------------------------------------------------------------
-- Set preferred distance between the camera and the character in third person mode.
-- @function [parent=#camera] setPreferredThirdPersonDistance
-- @param #number distance
-------------------------------------------------------------------------------
-- The current speed coefficient of focal point (the center of the screen in third person mode) smooth transition.
-- @function [parent=#camera] getFocalTransitionSpeed
-- @return #number
-------------------------------------------------------------------------------
-- Set the speed coefficient of focal point (the center of the screen in third person mode) smooth transition.
-- Smooth transition happens by default every time when the preferred offset was changed. Use `instantTransition()` to skip the current transition.
-- @function [parent=#camera] setFocalTransitionSpeed
-- Set the speed coefficient
-- @param #number speed
-------------------------------------------------------------------------------
-- Make instant the current transition of camera focal point and the current deferred rotation (see `allowCharacterDeferredRotation`).
-- @function [parent=#camera] instantTransition
return nil

@ -32,7 +32,7 @@
-- v:length() -- 5.0 length
-- v:length2() -- 25.0 square of the length
-- v:normalize() -- vector2(3/5, 4/5)
-- v:rotate(radians) -- rotate clockwise (returns rotated vector)
-- v:rotate(radians) -- rotate counterclockwise (returns rotated vector)
-- v1:dot(v2) -- dot product (returns a number)
-- v1 * v2 -- dot product
-- v1 + v2 -- vector addition
@ -183,26 +183,26 @@
-------------------------------------------------------------------------------
-- Rotation (any axis).
-- Rotation around a vector (counterclockwise if the vector points to us).
-- @function [parent=#TRANSFORM] rotate
-- @param #number angle
-- @param #Vector3 axis.
-- @return #Transform.
-------------------------------------------------------------------------------
-- X-axis rotation.
-- X-axis rotation (equivalent to `rotate(angle, vector3(-1, 0, 0))`).
-- @function [parent=#TRANSFORM] rotateX
-- @param #number angle
-- @return #Transform.
-------------------------------------------------------------------------------
-- Y-axis rotation.
-- Y-axis rotation (equivalent to `rotate(angle, vector3(0, -1, 0))`).
-- @function [parent=#TRANSFORM] rotateY
-- @param #number angle
-- @return #Transform.
-------------------------------------------------------------------------------
-- Z-axis rotation.
-- Z-axis rotation (equivalent to `rotate(angle, vector3(0, 0, -1))`).
-- @function [parent=#TRANSFORM] rotateZ
-- @param #number angle
-- @return #Transform.

@ -2,6 +2,7 @@
# Modifications should be done on the user openmw.cfg file instead
# (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html)
content=builtin.omwscripts
data=${MORROWIND_DATA_FILES}
data-local="?userdata?data"
resources=${OPENMW_RESOURCE_FILES}

@ -2,6 +2,7 @@
# Modifications should be done on the user openmw.cfg file instead
# (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html)
content=builtin.omwscripts
data="?global?data"
data=./data
data-local="?userdata?data"

Loading…
Cancel
Save