diff options
| author | Hadet <hadet@Mac.lan> | 2026-05-28 21:27:52 -0500 |
|---|---|---|
| committer | Hadet <hadet@Mac.lan> | 2026-05-29 18:03:54 -0500 |
| commit | 4c6584d1ecc57e8a34e078935b03851b59d4f9eb (patch) | |
| tree | 5c8a3edc3a5ae79c93a0d35becdc510606cfdec1 /indra | |
| parent | 184cd85f01b7654e8dd3e476b2c268849d97ee78 (diff) | |
add Over-The-Shoulder mouse look alternative
Diffstat (limited to 'indra')
| -rw-r--r-- | indra/newview/app_settings/settings.xml | 57 | ||||
| -rw-r--r-- | indra/newview/llagent.cpp | 18 | ||||
| -rw-r--r-- | indra/newview/llagentcamera.cpp | 76 | ||||
| -rw-r--r-- | indra/newview/llagentcamera.h | 9 | ||||
| -rw-r--r-- | indra/newview/llquickprefs.cpp | 23 | ||||
| -rw-r--r-- | indra/newview/llquickprefs.h | 5 | ||||
| -rw-r--r-- | indra/newview/llviewermenu.cpp | 24 | ||||
| -rw-r--r-- | indra/newview/skins/default/xui/en/floater_quick_prefs.xml | 126 | ||||
| -rw-r--r-- | indra/newview/skins/default/xui/en/panel_preferences_move.xml | 11 |
9 files changed, 335 insertions, 14 deletions
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 481cafafd1..dbe10a528a 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -1365,6 +1365,63 @@ <integer>1</integer> </map> <!-- End NaCl/Firestorm port --> + <!-- OTS over-the-shoulder aim settings --> + <key>OTSEnabled</key> + <map> + <key>Comment</key> + <string>When true, M key enters OTS shoulder cam instead of first-person mouselook</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>OTSCameraDistance</key> + <map> + <key>Comment</key> + <string>OTS camera distance behind avatar (meters)</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>3.0</real> + </map> + <key>OTSCameraSide</key> + <map> + <key>Comment</key> + <string>OTS camera side offset (negative = right of avatar)</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>-0.5</real> + </map> + <key>OTSCameraHeight</key> + <map> + <key>Comment</key> + <string>OTS camera height above avatar root (meters)</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.5</real> + </map> + <key>OTSFocusDistance</key> + <map> + <key>Comment</key> + <string>OTS focus point distance in front of avatar (meters)</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>10.0</real> + </map> + <!-- End OTS settings --> <key>CameraOffset</key> <map> <key>Comment</key> diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index 23647487b0..75a9ef58fc 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -2123,6 +2123,13 @@ std::ostream& operator<<(std::ostream &s, const LLAgent &agent) //----------------------------------------------------------------------------- bool LLAgent::needsRenderAvatar() { + // OTS mode: always render avatar — we are in third-person even though + // mouselook input is active. + if (gAgentCamera.cameraOTS()) + { + return mShowAvatar && mOutfitChosen; + } + if (gAgentCamera.cameraMouselook() && !LLVOAvatar::sVisibleInFirstPerson) { return false; @@ -2134,6 +2141,11 @@ bool LLAgent::needsRenderAvatar() // true if we need to render your own avatar's head. bool LLAgent::needsRenderHead() { + // OTS mode: always render head — avatar is fully visible. + if (gAgentCamera.cameraOTS()) + { + return mShowAvatar; + } return (LLVOAvatar::sVisibleInFirstPerson && LLPipeline::sReflectionRender) || (mShowAvatar && !gAgentCamera.cameraMouselook()); } @@ -2247,7 +2259,8 @@ void LLAgent::endAnimationUpdateUI() } // clean up UI from mode we're leaving - if (gAgentCamera.getLastCameraMode() == CAMERA_MODE_MOUSELOOK ) + if (gAgentCamera.getLastCameraMode() == CAMERA_MODE_MOUSELOOK + || gAgentCamera.getLastCameraMode() == CAMERA_MODE_OTS) { gToolBarView->setToolBarsVisible(true); // show mouse cursor @@ -2369,7 +2382,8 @@ void LLAgent::endAnimationUpdateUI() //--------------------------------------------------------------------- // Set up UI for mode we're entering //--------------------------------------------------------------------- - if (gAgentCamera.getCameraMode() == CAMERA_MODE_MOUSELOOK) + if (gAgentCamera.getCameraMode() == CAMERA_MODE_MOUSELOOK + || gAgentCamera.getCameraMode() == CAMERA_MODE_OTS) { // clean up UI // first show anything hidden by UI toggle diff --git a/indra/newview/llagentcamera.cpp b/indra/newview/llagentcamera.cpp index 369a6d3697..80dfeac2d2 100644 --- a/indra/newview/llagentcamera.cpp +++ b/indra/newview/llagentcamera.cpp @@ -1176,7 +1176,7 @@ void LLAgentCamera::updateLookAt(const S32 mouse_x, const S32 mouse_y) LLVector3 headLookAxis; LLCoordFrame frameCamera = *((LLCoordFrame*)LLViewerCamera::getInstance()); - if (cameraMouselook()) + if (cameraMouselook() || cameraOTS()) { lookAtType = LOOKAT_TARGET_MOUSELOOK; } @@ -1409,7 +1409,7 @@ void LLAgentCamera::updateCamera() gAgent.setShowAvatar(true); } - if (isAgentAvatarValid() && (mCameraMode != CAMERA_MODE_MOUSELOOK)) + if (isAgentAvatarValid() && mCameraMode != CAMERA_MODE_MOUSELOOK) { gAgentAvatarp->updateAttachmentVisibility(mCameraMode); } @@ -1497,7 +1497,8 @@ void LLAgentCamera::updateCamera() } gAgent.setLastPositionGlobal(global_pos); - if (LLVOAvatar::sVisibleInFirstPerson && isAgentAvatarValid() && !gAgentAvatarp->isSitting() && cameraMouselook()) + // Exclude OTS — shoulder camera position must not be overridden by head-tracking. + if (LLVOAvatar::sVisibleInFirstPerson && isAgentAvatarValid() && !gAgentAvatarp->isSitting() && cameraMouselook() && !cameraOTS()) { LLVector3 head_pos = gAgentAvatarp->mHeadp->getWorldPosition() + LLVector3(0.08f, 0.f, 0.05f) * gAgentAvatarp->mHeadp->getWorldRotation() + @@ -1598,6 +1599,19 @@ LLVector3d LLAgentCamera::calcFocusPositionTargetGlobal() mFocusTargetGlobal = gAgent.getPosGlobalFromAgent(mFollowCam.getSimulatedFocus()); return mFocusTargetGlobal; } + else if (mCameraMode == CAMERA_MODE_OTS) + { + // Focus in front of avatar at aim height + static LLCachedControl<F32> ots_focus_dist(gSavedSettings, "OTSFocusDistance", 10.0f); + static LLCachedControl<F32> ots_height(gSavedSettings, "OTSCameraHeight", 0.5f); + static LLCachedControl<F32> ots_side(gSavedSettings, "OTSCameraSide", -0.5f); + LLVector3 focus_local((F32)ots_focus_dist, (F32)ots_side * 0.3f, (F32)ots_height * 0.5f); + LLQuaternion agent_rot = gAgent.getFrameAgent().getQuaternion(); + LLVector3 focus_world = focus_local * agent_rot; + LLVector3d avatar_pos = gAgent.getPosGlobalFromAgent(getAvatarRootPosition()); + mFocusTargetGlobal = avatar_pos + LLVector3d(focus_world); + return mFocusTargetGlobal; + } else if (mCameraMode == CAMERA_MODE_MOUSELOOK) { LLVector3d at_axis(1.0, 0.0, 0.0); @@ -1776,6 +1790,18 @@ LLVector3d LLAgentCamera::calcCameraPositionTargetGlobal(bool *hit_limit) { camera_position_global = gAgent.getPosGlobalFromAgent(mFollowCam.getSimulatedPosition()); } + else if (mCameraMode == CAMERA_MODE_OTS) + { + // Shoulder offset camera — avatar-local space: X=forward, Y=left, Z=up + static LLCachedControl<F32> ots_dist(gSavedSettings, "OTSCameraDistance", 3.0f); + static LLCachedControl<F32> ots_side(gSavedSettings, "OTSCameraSide", -0.5f); + static LLCachedControl<F32> ots_height(gSavedSettings, "OTSCameraHeight", 0.5f); + LLVector3 local_offset(-(F32)ots_dist, (F32)ots_side, (F32)ots_height); + LLQuaternion agent_rot = gAgent.getFrameAgent().getQuaternion(); + LLVector3 world_offset = local_offset * agent_rot; + LLVector3d avatar_pos = gAgent.getPosGlobalFromAgent(getAvatarRootPosition()); + camera_position_global = avatar_pos + LLVector3d(world_offset); + } else if (mCameraMode == CAMERA_MODE_MOUSELOOK) { if (!isAgentAvatarValid() || gAgentAvatarp->mDrawable.isNull()) @@ -2244,6 +2270,50 @@ void LLAgentCamera::changeCameraToDefault() //----------------------------------------------------------------------------- +// changeCameraToOTS() +// Over-the-shoulder aim mode. +// Calls changeCameraToMouselook() to inherit ALL of its input setup +// (cursor hiding, mouse capture, control flags, focus management), +// then immediately overrides mCameraMode to CAMERA_MODE_OTS so that +// calcCameraPositionTargetGlobal places the camera at the shoulder offset +// instead of the avatar's eye position. +// Avatar rendering is handled explicitly in needsRenderAvatar() and needsRenderHead(). +//----------------------------------------------------------------------------- +void LLAgentCamera::changeCameraToOTS() +{ + if (mCameraMode != CAMERA_MODE_OTS) + { + // Inherit everything from mouselook: cursor lock, mouse capture, + // AGENT_CONTROL_MOUSELOOK flag, keyboard focus clear, etc. + changeCameraToMouselook(false); + + // Override mCameraMode to OTS so position/focus calculations + // use the shoulder offset instead of the eye position. + mCameraMode = CAMERA_MODE_OTS; + + // changeCameraToMouselook hid attachments via updateAttachmentVisibility + // with CAMERA_MODE_MOUSELOOK. Restore full visibility for OTS mode. + if (isAgentAvatarValid()) + { + gAgentAvatarp->updateAttachmentVisibility(CAMERA_MODE_THIRD_PERSON); + } + } +} + +//----------------------------------------------------------------------------- +// changeCameraFromOTS() +//----------------------------------------------------------------------------- +void LLAgentCamera::changeCameraFromOTS() +{ + if (mCameraMode == CAMERA_MODE_OTS) + { + // changeCameraToDefault handles clearing AGENT_CONTROL_MOUSELOOK, + // showing the cursor, and restoring the normal camera mode. + changeCameraToDefault(); + } +} + +//----------------------------------------------------------------------------- // changeCameraToFollow() //----------------------------------------------------------------------------- void LLAgentCamera::changeCameraToFollow(bool animate) diff --git a/indra/newview/llagentcamera.h b/indra/newview/llagentcamera.h index d277fd6158..ac1a36a6c2 100644 --- a/indra/newview/llagentcamera.h +++ b/indra/newview/llagentcamera.h @@ -43,7 +43,8 @@ enum ECameraMode CAMERA_MODE_THIRD_PERSON, CAMERA_MODE_MOUSELOOK, CAMERA_MODE_CUSTOMIZE_AVATAR, - CAMERA_MODE_FOLLOW + CAMERA_MODE_FOLLOW, + CAMERA_MODE_OTS // Over-the-shoulder: mouselook input + third-person camera }; /** Camera Presets for CAMERA_MODE_THIRD_PERSON */ @@ -93,10 +94,14 @@ public: void changeCameraToThirdPerson(bool animate = true); void changeCameraToCustomizeAvatar(); // Trigger transition animation void changeCameraToFollow(bool animate = true); // Ventrella + void changeCameraToOTS(); // Over-the-shoulder aim mode + void changeCameraFromOTS(); // Exit OTS back to third person bool cameraThirdPerson() const { return (mCameraMode == CAMERA_MODE_THIRD_PERSON && mLastCameraMode == CAMERA_MODE_THIRD_PERSON); } - bool cameraMouselook() const { return (mCameraMode == CAMERA_MODE_MOUSELOOK && mLastCameraMode == CAMERA_MODE_MOUSELOOK); } + // Also true for OTS — reuses mouselook input and UI behaviour; camera position handled separately. + bool cameraMouselook() const { return (mCameraMode == CAMERA_MODE_MOUSELOOK && mLastCameraMode == CAMERA_MODE_MOUSELOOK) || mCameraMode == CAMERA_MODE_OTS; } bool cameraCustomizeAvatar() const { return (mCameraMode == CAMERA_MODE_CUSTOMIZE_AVATAR /*&& !mCameraAnimating*/); } bool cameraFollow() const { return (mCameraMode == CAMERA_MODE_FOLLOW && mLastCameraMode == CAMERA_MODE_FOLLOW); } + bool cameraOTS() const { return mCameraMode == CAMERA_MODE_OTS; } ECameraMode getCameraMode() const { return mCameraMode; } ECameraMode getLastCameraMode() const { return mLastCameraMode; } void updateCamera(); // Call once per frame to update camera location/orientation diff --git a/indra/newview/llquickprefs.cpp b/indra/newview/llquickprefs.cpp index 800aa7abac..9e83857a15 100644 --- a/indra/newview/llquickprefs.cpp +++ b/indra/newview/llquickprefs.cpp @@ -26,6 +26,8 @@ #include "llquickprefs.h" #include "llagent.h" +#include "llagentcamera.h" +#include "llcheckboxctrl.h" #include "llsliderctrl.h" #include "lltextbox.h" #include "llviewercontrol.h" @@ -97,6 +99,14 @@ bool LLFloaterQuickPrefs::postBuild() } onRegionChanged(); // evaluate current region immediately + // OTS aim mode checkbox + mOTSEnabledCheck = getChild<LLCheckBoxCtrl>("ots_enabled"); + if (mOTSEnabledCheck) + { + mOTSEnabledCheck->setCommitCallback( + boost::bind(&LLFloaterQuickPrefs::onOTSEnabledChanged, this)); + } + return true; } @@ -203,3 +213,16 @@ void LLFloaterQuickPrefs::syncAvatarZOffsetFromPreferenceSetting() F32 value = gSavedPerAccountSettings.getF32("AvatarHoverOffsetZ"); mAvatarZOffsetSlider->setValue(value, false); // false = no commit signal } + +void LLFloaterQuickPrefs::onOTSEnabledChanged() +{ + if (mOTSEnabledCheck) + { + gSavedSettings.setBOOL("OTSEnabled", mOTSEnabledCheck->get()); + // If currently in OTS mode and checkbox was unchecked, exit OTS + if (!mOTSEnabledCheck->get() && gAgentCamera.cameraOTS()) + { + gAgentCamera.changeCameraFromOTS(); + } + } +} diff --git a/indra/newview/llquickprefs.h b/indra/newview/llquickprefs.h index 0ef56a4299..b90333831c 100644 --- a/indra/newview/llquickprefs.h +++ b/indra/newview/llquickprefs.h @@ -27,6 +27,7 @@ #include "llfloater.h" class LLSliderCtrl; +class LLCheckBoxCtrl; class LLTextBox; /** @@ -76,6 +77,10 @@ private: void syncAvatarZOffsetFromPreferenceSetting(); boost::signals2::connection mRegionChangedSlot; + + // OTS over-the-shoulder aim + LLCheckBoxCtrl* mOTSEnabledCheck { nullptr }; + void onOTSEnabledChanged(); }; #endif // LL_LLQUICKPREFS_H diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index 4632800e2d..833a3cde6f 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -4778,25 +4778,37 @@ class LLViewMouselook : public view_listener_t { bool handleEvent(const LLSD& userdata) { + // When OTS is enabled M toggles OTS mode (shoulder cam). + // When disabled M toggles normal first-person mouselook. + if (gSavedSettings.getBOOL("OTSEnabled")) + { + if (!gAgentCamera.cameraOTS()) + { + gAgentCamera.changeCameraToOTS(); + } + else + { + gAgentCamera.changeCameraFromOTS(); + } + return true; + } + if (!gAgentCamera.cameraMouselook()) { gAgentCamera.changeCameraToMouselook(); } else { - // NaCl: Right-click + scroll wheel zoom in mouselook (ported from Firestorm). - // If we were zoomed when the user toggles out of mouselook, restore the - // normal (pre-zoom) FOV before switching back to the default camera. + // NaCl: restore FOV on mouselook exit LLVector3 mlFovValues = gSavedSettings.getVector3("_NACL_MLFovValues"); F32 cameraAngle = gSavedSettings.getF32("CameraAngle"); if (mlFovValues.mV[VZ] > 0.0f) { - mlFovValues.mV[VY] = cameraAngle; // preserve last zoomed FOV + mlFovValues.mV[VY] = cameraAngle; mlFovValues.mV[VZ] = 0.0f; gSavedSettings.setVector3("_NACL_MLFovValues", mlFovValues); - gSavedSettings.setF32("CameraAngle", mlFovValues.mV[VX]); // restore normal FOV + gSavedSettings.setF32("CameraAngle", mlFovValues.mV[VX]); } - // NaCl End gAgentCamera.changeCameraToDefault(); } return true; diff --git a/indra/newview/skins/default/xui/en/floater_quick_prefs.xml b/indra/newview/skins/default/xui/en/floater_quick_prefs.xml index 8b20fda6f5..f5bdcf2156 100644 --- a/indra/newview/skins/default/xui/en/floater_quick_prefs.xml +++ b/indra/newview/skins/default/xui/en/floater_quick_prefs.xml @@ -9,7 +9,7 @@ can_minimize="true" can_close="true" can_resize="false" - height="96" + height="240" width="320" layout="topleft" name="quick_prefs" @@ -82,4 +82,128 @@ name="max_bandwidth" tool_tip="Network bandwidth in Kbps (50 – 3000)" /> + <view_border + follows="left|right" + layout="topleft" + left="2" + right="-2" + top_pad="10" + height="2" + name="ots_divider" /> + + <text + type="string" + follows="left|top" + height="16" + layout="topleft" + left="5" + top_pad="6" + width="290" + text_color="EmphasisColor" + name="OTSLabel"> + Over-the-Shoulder Cam: + </text> + + <check_box + control_name="OTSEnabled" + follows="left|top" + height="16" + label="Use Over-The-Shoulder Cam" + layout="topleft" + left="10" + top_pad="4" + width="290" + name="ots_enabled" + tool_tip="When checked, M enters shoulder cam instead of first-person mouselook" /> + + <text + type="string" + follows="left|top" + height="16" + layout="topleft" + left="10" + top_pad="6" + width="100" + name="OTSDistLabel"> + Distance: + </text> + + <slider + control_name="OTSCameraDistance" + decimal_digits="1" + can_edit_text="true" + follows="left|right|top" + height="16" + increment="0.1" + initial_value="3.0" + max_val="10.0" + min_val="1.0" + label_width="0" + layout="topleft" + left="110" + right="-10" + top_delta="-2" + name="ots_distance" + tool_tip="Camera distance behind avatar (1 – 10 m)" /> + + <text + type="string" + follows="left|top" + height="16" + layout="topleft" + left="10" + top_pad="4" + width="100" + name="OTSSideLabel"> + Side offset: + </text> + + <slider + control_name="OTSCameraSide" + decimal_digits="2" + can_edit_text="true" + follows="left|right|top" + height="16" + increment="0.05" + initial_value="-0.5" + max_val="1.0" + min_val="-1.0" + label_width="0" + layout="topleft" + left="110" + right="-10" + top_delta="-2" + name="ots_side" + tool_tip="Side offset: negative = right shoulder, positive = left shoulder" /> + + <text + type="string" + follows="left|top" + height="16" + layout="topleft" + left="10" + top_pad="4" + width="100" + name="OTSHeightLabel"> + Height: + </text> + + <slider + control_name="OTSCameraHeight" + decimal_digits="2" + can_edit_text="true" + follows="left|right|top" + height="16" + increment="0.05" + initial_value="0.5" + max_val="2.0" + min_val="0.0" + label_width="0" + layout="topleft" + left="110" + right="-10" + top_delta="-2" + name="ots_height" + tool_tip="Camera height above avatar root (0 – 2 m)" /> + </floater> diff --git a/indra/newview/skins/default/xui/en/panel_preferences_move.xml b/indra/newview/skins/default/xui/en/panel_preferences_move.xml index eee55bd7bc..d59591a7fe 100644 --- a/indra/newview/skins/default/xui/en/panel_preferences_move.xml +++ b/indra/newview/skins/default/xui/en/panel_preferences_move.xml @@ -161,6 +161,17 @@ name="first_person_avatar_visible" top_pad="5" width="256" /> + <check_box + control_name="OTSEnabled" + follows="left|top" + height="20" + label="Use Over-The-Shoulder Cam" + layout="topleft" + left_delta="5" + name="ots_enabled" + top_pad="5" + tool_tip="When checked, pressing M enters over-the-shoulder cam instead of first-person mouselook" + width="320" /> <text type="string" length="1" |
