summaryrefslogtreecommitdiff
path: root/indra
diff options
context:
space:
mode:
authorHadet <hadet@Mac.lan>2026-05-28 21:27:52 -0500
committerHadet <hadet@Mac.lan>2026-05-29 18:03:54 -0500
commit4c6584d1ecc57e8a34e078935b03851b59d4f9eb (patch)
tree5c8a3edc3a5ae79c93a0d35becdc510606cfdec1 /indra
parent184cd85f01b7654e8dd3e476b2c268849d97ee78 (diff)
add Over-The-Shoulder mouse look alternative
Diffstat (limited to 'indra')
-rw-r--r--indra/newview/app_settings/settings.xml57
-rw-r--r--indra/newview/llagent.cpp18
-rw-r--r--indra/newview/llagentcamera.cpp76
-rw-r--r--indra/newview/llagentcamera.h9
-rw-r--r--indra/newview/llquickprefs.cpp23
-rw-r--r--indra/newview/llquickprefs.h5
-rw-r--r--indra/newview/llviewermenu.cpp24
-rw-r--r--indra/newview/skins/default/xui/en/floater_quick_prefs.xml126
-rw-r--r--indra/newview/skins/default/xui/en/panel_preferences_move.xml11
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"