Keep the paint-mode pan on release and scale it by zoom

The middle-mouse pan now persists when the button is released and only glides back to center on leaving paint mode. Pan sensitivity and max distance scale with the orbit zoom distance, so panning stays consistent on screen at any zoom and unchanged at the default zoom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-30 19:25:45 +02:00
1 parent 4e8a907a8c
commit 3fa99f32fe
2 files changed
+124 -2

No files matched your search

+112 -1
View File
@@ -68,6 +68,25 @@ public class ThirdPersonOrbitInput : MonoBehaviour
"(size-1 design value; scaled by the target). Set so the bound player is comfortably framed.")]
public float defaultRadius = 4.5f;
[Header("Pan (middle mouse)")]
[Tooltip("Hold the middle mouse button and drag to pan the camera framing - a temporary offset to bring an " +
"awkward spot of the model in front of the lens for painting. The pan stays put on release and only " +
"glides back to center when paint mode is left. Enabled only while PaintMode is active.")]
public bool enablePan = true;
[Tooltip("World units the pan offset moves per pixel of mouse drag, measured at the default zoom. It then " +
"scales with the live zoom distance so the model slides the same amount on screen per pixel at any " +
"zoom (closer in = finer). Body scale is folded in automatically via the orbit radius.")]
public float panSensitivity = 0.004f;
[Tooltip("Farthest the framing can pan from center, measured at the default zoom. It scales with the zoom " +
"distance too, so the maximum on-screen pan reach stays the same at every zoom level.")]
public float panMaxDistance = 1f;
[Tooltip("How snappily the pan glides back to center when paint mode is left (1/second; 0 = snaps home " +
"instantly). The pan stays put on middle-button release - it only returns on leaving paint mode.")]
public float panReturnSpeed = 14f;
[Header("Cursor")]
[Tooltip("Lock and hide the cursor while the camera is actively rotating.")]
public bool manageCursor = true;
@@ -76,6 +95,8 @@ public class ThirdPersonOrbitInput : MonoBehaviour
Vector2 _smoothedLook; // low-pass-filtered look delta (deg/frame) for eased rotation
float _targetRadius; // zoom radius the actual radius eases toward
bool _zoomInit; // _targetRadius seeded from the live radius yet?
Vector2 _pan; // accumulated pan in camera-space units (x=right, y=up); stays put on release, eases to 0 on leaving paint mode
CinemachineCameraOffset _camOffset; // post-aim extension that slides the whole framing (true pan, not an orbit arc)
/// <summary>Current orbit mode. Setting it also refreshes the cursor state.</summary>
public OrbitLookMode LookMode
@@ -91,16 +112,40 @@ public class ThirdPersonOrbitInput : MonoBehaviour
/// </summary>
public bool LookSuppressed { get; set; }
/// <summary>While true, holding the middle mouse button and dragging pans the camera framing - a temporary
/// look-point offset that stays put on release. Set by PaintMode so the pan is only available while painting;
/// clearing it (on leaving paint mode) is what glides the framing back home.</summary>
public bool PanEnabled { get; set; }
void Awake()
{
_orbit = GetComponent<CinemachineOrbitalFollow>();
if (!_orbit)
Debug.LogError("[ThirdPersonOrbitInput] No CinemachineOrbitalFollow found on this GameObject.", this);
// The pan rides a CinemachineCameraOffset extension applied AFTER aim with PreserveComposition off: it
// shifts the camera position AND the look-at point by the same world vector, so the framing translates
// (the model slides across the screen) instead of re-aiming back to center like a body TargetOffset
// would. Offset is camera-local (x=right, y=up). Added at runtime so no scene/prefab authoring is needed.
if (enablePan)
{
_camOffset = GetComponent<CinemachineCameraOffset>();
if (!_camOffset) _camOffset = gameObject.AddComponent<CinemachineCameraOffset>();
_camOffset.ApplyAfter = CinemachineCore.Stage.Aim;
_camOffset.PreserveComposition = false;
_camOffset.Offset = Vector3.zero;
}
}
void OnEnable() => ApplyCursorState();
void OnDisable() => SetCursorLocked(false);
void OnDisable()
{
SetCursorLocked(false);
// Drop any live pan so a disabled rig doesn't leave a stale framing offset behind.
_pan = Vector2.zero;
if (_camOffset) _camOffset.Offset = Vector3.zero;
}
/// <summary>Uniform scale of the current follow target (1 when none, or when Scale With Target is off).
/// The zoom range, step and default radius are multiplied by this so the framing is body-relative across
@@ -112,6 +157,15 @@ public class ThirdPersonOrbitInput : MonoBehaviour
return t ? Mathf.Max(0.0001f, t.lossyScale.x) : 1f;
}
/// <summary>How much the pan scales with the live zoom distance. Dividing the orbit radius by the default
/// radius makes this exactly 1 at the default zoom (so the authored panSensitivity/panMaxDistance keep their
/// feel there), smaller when zoomed in (finer pan, less reach) and larger when zoomed out. The body scale
/// cancels out because the radius already embeds it - a bigger player orbits proportionally farther - so the
/// pan stays body-relative across player sizes at every zoom, just like CurrentScale() did at the default
/// radius. The result: the model slides the same amount ON SCREEN per pixel, and the same maximum reach
/// across the screen, regardless of how far the wheel has zoomed.</summary>
float PanZoomScale() => _orbit ? _orbit.Radius / Mathf.Max(0.0001f, defaultRadius) : 1f;
/// <summary>Snaps the zoom to the scaled default radius. Called when a new avatar binds (see
/// GameModeManager.BindLocalPlayer) so each player starts at its body-relative default zoom instead of
/// inheriting the previous avatar's radius.</summary>
@@ -211,6 +265,8 @@ public class ThirdPersonOrbitInput : MonoBehaviour
? Mathf.Lerp(_orbit.Radius, _targetRadius, 1f - Mathf.Exp(-zoomSmoothing * Time.deltaTime))
: _targetRadius;
}
UpdatePan(Time.deltaTime);
}
bool ShouldRotate()
@@ -267,6 +323,61 @@ public class ThirdPersonOrbitInput : MonoBehaviour
#endif
}
bool IsMiddleMouseHeld()
{
#if ENABLE_INPUT_SYSTEM
return Mouse.current != null && Mouse.current.middleButton.isPressed;
#else
return Input.GetMouseButton(2);
#endif
}
/// <summary>Middle-mouse pan: while PaintMode enables it, holding the middle button and dragging slides the
/// whole framing along the screen right/up axes (via the camera-offset extension), then eases the offset
/// back to center on release. A temporary framing offset to line up an awkward spot of the model for
/// painting - the player body and its paint collider are untouched. Disabled (eases home) outside paint mode
/// and while the orbit is parked in <see cref="OrbitLookMode.Disabled"/> (pause / no-player).</summary>
void UpdatePan(float dt)
{
if (!enablePan || !_camOffset)
return;
bool active = PanEnabled && lookMode != OrbitLookMode.Disabled;
float zoom = PanZoomScale();
if (active && IsMiddleMouseHeld())
{
Vector2 d = ReadMouseDelta() * MouseSensitivity.Multiplier;
// Subtracting the drag gives a hand-tool feel (the model follows the cursor: drag right -> model
// slides right). Both the step and the clamp scale with the orbit distance so the pan feels the same
// on screen at every zoom - finer when zoomed in, and the same maximum reach across the screen.
_pan -= d * (panSensitivity * zoom);
_pan = Vector2.ClampMagnitude(_pan, panMaxDistance * zoom);
}
else if (active)
{
return; // still painting, middle button released - hold the pan where it was left
}
else if (_pan != Vector2.zero)
{
// Left paint mode (or look disabled) - glide the framing back to center.
if (panReturnSpeed > 0f)
_pan = Vector2.Lerp(_pan, Vector2.zero, 1f - Mathf.Exp(-panReturnSpeed * dt));
else
_pan = Vector2.zero;
// Snap the last sliver so the offset settles exactly on center and this branch stops running.
if (_pan.sqrMagnitude < 1e-8f)
_pan = Vector2.zero;
}
else
{
return; // at rest and not panning - nothing to write
}
// Offset is camera-local (x=right, y=up); the extension translates eye + look-at together -> true pan.
_camOffset.Offset = new Vector3(_pan.x, _pan.y, 0f);
}
static bool ShiftHeld()
{
#if ENABLE_INPUT_SYSTEM