From 416e091498aad85a5bb87f10422ab1b2e717e1dc Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 28 Mar 2026 11:39:37 -0700 Subject: [PATCH] feat: add Camera Stiffness and Pivot Height settings for motion comfort Camera Stiffness (default 20, range 5-100): controls how tightly the camera follows the player. Higher values = less sway/lag. Users who experience motion sickness can increase this to reduce floaty camera. Camera Pivot Height (default 1.8, range 0-3): height of the camera orbit point above the player's feet. Lower values reduce the "detached/floating" feel that can cause nausea. Setting to 0 puts the pivot at foot level (ground-locked camera). Both settings saved to settings file and applied via sliders in the Gameplay tab of the Settings window. --- include/rendering/camera_controller.hpp | 12 ++++++++++-- include/ui/game_screen.hpp | 2 ++ src/rendering/camera_controller.cpp | 16 ++++++++-------- src/ui/game_screen.cpp | 22 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 3bc64218..572b3877 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -192,8 +192,16 @@ private: static constexpr float MAX_DISTANCE_INTERIOR = 12.0f; // Max zoom inside WMOs bool extendedZoom_ = false; static constexpr float ZOOM_SMOOTH_SPEED = 15.0f; // How fast zoom eases - static constexpr float CAM_SMOOTH_SPEED = 20.0f; // How fast camera position smooths - static constexpr float PIVOT_HEIGHT = 1.8f; // Pivot at head height + static constexpr float CAM_SMOOTH_SPEED_DEFAULT = 20.0f; + float camSmoothSpeed_ = CAM_SMOOTH_SPEED_DEFAULT; // User-configurable camera smoothing (higher = tighter) +public: + void setCameraSmoothSpeed(float speed) { camSmoothSpeed_ = std::clamp(speed, 5.0f, 100.0f); } + float getCameraSmoothSpeed() const { return camSmoothSpeed_; } + void setPivotHeight(float h) { pivotHeight_ = std::clamp(h, 0.0f, 3.0f); } + float getPivotHeight() const { return pivotHeight_; } +private: + static constexpr float PIVOT_HEIGHT_DEFAULT = 1.8f; + float pivotHeight_ = PIVOT_HEIGHT_DEFAULT; // User-configurable pivot height static constexpr float CAM_SPHERE_RADIUS = 0.32f; // Keep camera farther from geometry to avoid clipping-through surfaces static constexpr float CAM_EPSILON = 0.22f; // Extra wall offset to avoid near-plane clipping artifacts static constexpr float COLLISION_FOCUS_RADIUS_THIRD_PERSON = 20.0f; // Reduced for performance diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 3a974846..4bd9e92d 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -201,6 +201,8 @@ private: float pendingMouseSensitivity = 0.2f; bool pendingInvertMouse = false; bool pendingExtendedZoom = false; + float pendingCameraStiffness = 20.0f; // Camera smooth speed (higher = tighter, less sway) + float pendingPivotHeight = 1.8f; // Camera pivot height above feet (lower = less detached feel) float pendingFov = 70.0f; // degrees, default matches WoW's ~70° horizontal FOV int pendingUiOpacity = 65; bool pendingMinimapRotate = false; diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 7d30a00f..8b23a8bf 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -181,7 +181,7 @@ void CameraController::update(float deltaTime) { // Pivot point at upper chest/neck float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; - glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset); + glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, pivotHeight_ + mountedOffset); // Camera direction from yaw/pitch glm::vec3 camDir = -forward3D; @@ -201,7 +201,7 @@ void CameraController::update(float deltaTime) { if (glm::dot(smoothedCamPos, smoothedCamPos) < 1e-4f) { smoothedCamPos = actualCam; } - float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime); + float camLerp = 1.0f - std::exp(-camSmoothSpeed_ * deltaTime); smoothedCamPos += (actualCam - smoothedCamPos) * camLerp; camera->setPosition(smoothedCamPos); @@ -1450,7 +1450,7 @@ void CameraController::update(float deltaTime) { if (terrainAtCam) { // Keep pivot high enough so near-hill camera rays don't cut through terrain. constexpr float kMinRayClearance = 2.0f; - float basePivotZ = targetPos.z + PIVOT_HEIGHT + mountedOffset; + float basePivotZ = targetPos.z + pivotHeight_ + mountedOffset; float rayClearance = basePivotZ - *terrainAtCam; if (rayClearance < kMinRayClearance) { desiredLift = std::clamp(kMinRayClearance - rayClearance, 0.0f, 1.4f); @@ -1468,7 +1468,7 @@ void CameraController::update(float deltaTime) { // are not relevant for camera pivoting. cachedPivotLift_ = 0.0f; } - glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset + pivotLift); + glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, pivotHeight_ + mountedOffset + pivotLift); // Camera direction from yaw/pitch (already computed as forward3D) glm::vec3 camDir = -forward3D; // Camera looks at pivot, so it's behind @@ -1549,7 +1549,7 @@ void CameraController::update(float deltaTime) { if (glm::dot(smoothedCamPos, smoothedCamPos) < 1e-4f) { smoothedCamPos = actualCam; // Initialize } - float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime); + float camLerp = 1.0f - std::exp(-camSmoothSpeed_ * deltaTime); smoothedCamPos += (actualCam - smoothedCamPos) * camLerp; // ===== Final floor clearance check ===== @@ -2090,7 +2090,7 @@ void CameraController::reset() { currentDistance = userTargetDistance; collisionDistance = currentDistance; float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; - glm::vec3 pivot = spawnPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset); + glm::vec3 pivot = spawnPos + glm::vec3(0.0f, 0.0f, pivotHeight_ + mountedOffset); glm::vec3 camDir = -forward3D; glm::vec3 camPos = pivot + camDir * currentDistance; smoothedCamPos = camPos; @@ -2232,7 +2232,7 @@ void CameraController::reset() { collisionDistance = currentDistance; float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; - glm::vec3 pivot = spawnPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset); + glm::vec3 pivot = spawnPos + glm::vec3(0.0f, 0.0f, pivotHeight_ + mountedOffset); glm::vec3 camDir = -forward3D; glm::vec3 camPos = pivot + camDir * currentDistance; smoothedCamPos = camPos; @@ -2271,7 +2271,7 @@ void CameraController::teleportTo(const glm::vec3& pos) { camera->setRotation(yaw, pitch); glm::vec3 forward3D = camera->getForward(); float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; - glm::vec3 pivot = pos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset); + glm::vec3 pivot = pos + glm::vec3(0.0f, 0.0f, pivotHeight_ + mountedOffset); glm::vec3 camDir = -forward3D; glm::vec3 camPos = pivot + camDir * currentDistance; smoothedCamPos = camPos; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index dd436e16..16028ce4 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -18447,6 +18447,24 @@ if (ImGui::Checkbox("Extended Camera Zoom", &pendingExtendedZoom)) { } saveSettings(); } +if (ImGui::SliderFloat("Camera Stiffness", &pendingCameraStiffness, 5.0f, 100.0f, "%.0f")) { + if (renderer) { + if (auto* cameraController = renderer->getCameraController()) { + cameraController->setCameraSmoothSpeed(pendingCameraStiffness); + } + } + saveSettings(); +} +ImGui::SetItemTooltip("Higher = tighter camera with less sway. Default: 20"); +if (ImGui::SliderFloat("Camera Pivot Height", &pendingPivotHeight, 0.0f, 3.0f, "%.1f")) { + if (renderer) { + if (auto* cameraController = renderer->getCameraController()) { + cameraController->setPivotHeight(pendingPivotHeight); + } + } + saveSettings(); +} +ImGui::SetItemTooltip("Height of camera orbit point above feet. Lower = less detached feel. Default: 1.8"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Allow the camera to zoom out further than normal"); @@ -21294,6 +21312,8 @@ void GameScreen::saveSettings() { out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n"; out << "invert_mouse=" << (pendingInvertMouse ? 1 : 0) << "\n"; out << "extended_zoom=" << (pendingExtendedZoom ? 1 : 0) << "\n"; + out << "camera_stiffness=" << pendingCameraStiffness << "\n"; + out << "camera_pivot_height=" << pendingPivotHeight << "\n"; out << "fov=" << pendingFov << "\n"; // Quest tracker position/size @@ -21452,6 +21472,8 @@ void GameScreen::loadSettings() { else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f); else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0); else if (key == "extended_zoom") pendingExtendedZoom = (std::stoi(val) != 0); + else if (key == "camera_stiffness") pendingCameraStiffness = std::clamp(std::stof(val), 5.0f, 100.0f); + else if (key == "camera_pivot_height") pendingPivotHeight = std::clamp(std::stof(val), 0.0f, 3.0f); else if (key == "fov") { pendingFov = std::clamp(std::stof(val), 45.0f, 110.0f); if (auto* renderer = core::Application::getInstance().getRenderer()) {