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.
This commit is contained in:
Kelsi 2026-03-28 11:39:37 -07:00
parent 5a8ab87a78
commit 416e091498
4 changed files with 42 additions and 10 deletions

View file

@ -192,8 +192,16 @@ private:
static constexpr float MAX_DISTANCE_INTERIOR = 12.0f; // Max zoom inside WMOs static constexpr float MAX_DISTANCE_INTERIOR = 12.0f; // Max zoom inside WMOs
bool extendedZoom_ = false; bool extendedZoom_ = false;
static constexpr float ZOOM_SMOOTH_SPEED = 15.0f; // How fast zoom eases 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 CAM_SMOOTH_SPEED_DEFAULT = 20.0f;
static constexpr float PIVOT_HEIGHT = 1.8f; // Pivot at head height 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_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 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 static constexpr float COLLISION_FOCUS_RADIUS_THIRD_PERSON = 20.0f; // Reduced for performance

View file

@ -201,6 +201,8 @@ private:
float pendingMouseSensitivity = 0.2f; float pendingMouseSensitivity = 0.2f;
bool pendingInvertMouse = false; bool pendingInvertMouse = false;
bool pendingExtendedZoom = 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 float pendingFov = 70.0f; // degrees, default matches WoW's ~70° horizontal FOV
int pendingUiOpacity = 65; int pendingUiOpacity = 65;
bool pendingMinimapRotate = false; bool pendingMinimapRotate = false;

View file

@ -181,7 +181,7 @@ void CameraController::update(float deltaTime) {
// Pivot point at upper chest/neck // Pivot point at upper chest/neck
float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; 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 // Camera direction from yaw/pitch
glm::vec3 camDir = -forward3D; glm::vec3 camDir = -forward3D;
@ -201,7 +201,7 @@ void CameraController::update(float deltaTime) {
if (glm::dot(smoothedCamPos, smoothedCamPos) < 1e-4f) { if (glm::dot(smoothedCamPos, smoothedCamPos) < 1e-4f) {
smoothedCamPos = actualCam; smoothedCamPos = actualCam;
} }
float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime); float camLerp = 1.0f - std::exp(-camSmoothSpeed_ * deltaTime);
smoothedCamPos += (actualCam - smoothedCamPos) * camLerp; smoothedCamPos += (actualCam - smoothedCamPos) * camLerp;
camera->setPosition(smoothedCamPos); camera->setPosition(smoothedCamPos);
@ -1450,7 +1450,7 @@ void CameraController::update(float deltaTime) {
if (terrainAtCam) { if (terrainAtCam) {
// Keep pivot high enough so near-hill camera rays don't cut through terrain. // Keep pivot high enough so near-hill camera rays don't cut through terrain.
constexpr float kMinRayClearance = 2.0f; constexpr float kMinRayClearance = 2.0f;
float basePivotZ = targetPos.z + PIVOT_HEIGHT + mountedOffset; float basePivotZ = targetPos.z + pivotHeight_ + mountedOffset;
float rayClearance = basePivotZ - *terrainAtCam; float rayClearance = basePivotZ - *terrainAtCam;
if (rayClearance < kMinRayClearance) { if (rayClearance < kMinRayClearance) {
desiredLift = std::clamp(kMinRayClearance - rayClearance, 0.0f, 1.4f); desiredLift = std::clamp(kMinRayClearance - rayClearance, 0.0f, 1.4f);
@ -1468,7 +1468,7 @@ void CameraController::update(float deltaTime) {
// are not relevant for camera pivoting. // are not relevant for camera pivoting.
cachedPivotLift_ = 0.0f; 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) // Camera direction from yaw/pitch (already computed as forward3D)
glm::vec3 camDir = -forward3D; // Camera looks at pivot, so it's behind 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) { if (glm::dot(smoothedCamPos, smoothedCamPos) < 1e-4f) {
smoothedCamPos = actualCam; // Initialize 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; smoothedCamPos += (actualCam - smoothedCamPos) * camLerp;
// ===== Final floor clearance check ===== // ===== Final floor clearance check =====
@ -2090,7 +2090,7 @@ void CameraController::reset() {
currentDistance = userTargetDistance; currentDistance = userTargetDistance;
collisionDistance = currentDistance; collisionDistance = currentDistance;
float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; 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 camDir = -forward3D;
glm::vec3 camPos = pivot + camDir * currentDistance; glm::vec3 camPos = pivot + camDir * currentDistance;
smoothedCamPos = camPos; smoothedCamPos = camPos;
@ -2232,7 +2232,7 @@ void CameraController::reset() {
collisionDistance = currentDistance; collisionDistance = currentDistance;
float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; 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 camDir = -forward3D;
glm::vec3 camPos = pivot + camDir * currentDistance; glm::vec3 camPos = pivot + camDir * currentDistance;
smoothedCamPos = camPos; smoothedCamPos = camPos;
@ -2271,7 +2271,7 @@ void CameraController::teleportTo(const glm::vec3& pos) {
camera->setRotation(yaw, pitch); camera->setRotation(yaw, pitch);
glm::vec3 forward3D = camera->getForward(); glm::vec3 forward3D = camera->getForward();
float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; 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 camDir = -forward3D;
glm::vec3 camPos = pivot + camDir * currentDistance; glm::vec3 camPos = pivot + camDir * currentDistance;
smoothedCamPos = camPos; smoothedCamPos = camPos;

View file

@ -18447,6 +18447,24 @@ if (ImGui::Checkbox("Extended Camera Zoom", &pendingExtendedZoom)) {
} }
saveSettings(); 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()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Allow the camera to zoom out further than normal"); ImGui::SetTooltip("Allow the camera to zoom out further than normal");
@ -21294,6 +21312,8 @@ void GameScreen::saveSettings() {
out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n"; out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n";
out << "invert_mouse=" << (pendingInvertMouse ? 1 : 0) << "\n"; out << "invert_mouse=" << (pendingInvertMouse ? 1 : 0) << "\n";
out << "extended_zoom=" << (pendingExtendedZoom ? 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"; out << "fov=" << pendingFov << "\n";
// Quest tracker position/size // 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 == "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 == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0);
else if (key == "extended_zoom") pendingExtendedZoom = (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") { else if (key == "fov") {
pendingFov = std::clamp(std::stof(val), 45.0f, 110.0f); pendingFov = std::clamp(std::stof(val), 45.0f, 110.0f);
if (auto* renderer = core::Application::getInstance().getRenderer()) { if (auto* renderer = core::Application::getInstance().getRenderer()) {