Add Cinemachine third-person orbit camera and fix paint raycast

- Install Cinemachine 3.1.7 and set up an orbital third-person camera rig
  (CinemachineBrain, OrbitalFollow, RotationComposer, Deoccluder).
- Add ThirdPersonOrbitInput: mouse-driven orbit and wheel zoom with a Tab
  toggle that unlocks the cursor and pauses rotation.
- Fix MeshPainter so the paint ray skips the player's CharacterController
  capsule (and any other collider) and always targets the mesh collider,
  so painting works from every camera angle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-22 14:31:45 +02:00
1 parent 792c8c964f
commit 6e8ab4bc6d
6 files changed
+545 -10

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
using UnityEngine;
using Unity.Cinemachine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
/// <summary>
/// Feeds mouse input into a <see cref="CinemachineOrbitalFollow"/> so the camera
/// orbits around the follow target like a classic third-person camera.
///
/// Rotation is always on by default and the cursor is locked/hidden while looking.
/// Pressing the toggle key (Tab) unlocks the cursor and disables camera rotation,
/// so you can freely use the mouse (e.g. paint the mesh with the left button or
/// click UI). Pressing it again re-locks the cursor and re-enables rotation.
///
/// Horizontal/vertical mouse movement drives the orbital axes (yaw/pitch); the
/// mouse wheel zooms by changing the orbit radius. The script reads the new Input
/// System (Mouse.current / Keyboard.current) when enabled and falls back to the
/// legacy Input Manager otherwise. The project currently runs the new Input System only.
///
/// Attach this to the same GameObject as the CinemachineCamera /
/// CinemachineOrbitalFollow (the camera rig, not the player).
/// </summary>
[RequireComponent(typeof(CinemachineOrbitalFollow))]
[DisallowMultipleComponent]
public class ThirdPersonOrbitInput : MonoBehaviour
{
[Header("Rotation")]
[Tooltip("Horizontal look sensitivity in degrees of yaw per pixel of mouse movement.")]
public float horizontalSensitivity = 0.2f;
[Tooltip("Vertical look sensitivity in degrees of pitch per pixel of mouse movement.")]
public float verticalSensitivity = 0.15f;
[Tooltip("If ON, moving the mouse up tilts the camera down (inverted pitch).")]
public bool invertY = false;
[Header("Toggle")]
[Tooltip("Key that toggles between look mode (cursor locked, camera rotates) and " +
"free mode (cursor unlocked, camera rotation disabled).")]
#if ENABLE_INPUT_SYSTEM
public Key toggleKey = Key.Tab;
#else
public KeyCode toggleKey = KeyCode.Tab;
#endif
[Tooltip("If ON, the camera starts in look mode (rotation enabled, cursor locked).")]
public bool startInLookMode = true;
[Header("Zoom")]
[Tooltip("If ON, the mouse wheel zooms the camera in and out (changes orbit radius).")]
public bool enableZoom = true;
[Tooltip("How much the orbit radius changes per mouse wheel notch.")]
public float zoomSpeed = 0.5f;
[Tooltip("Closest the camera can zoom in (orbit radius in units).")]
public float minRadius = 1.5f;
[Tooltip("Farthest the camera can zoom out (orbit radius in units).")]
public float maxRadius = 8f;
[Header("Cursor")]
[Tooltip("If ON, the cursor is locked and hidden while in look mode (rotation " +
"enabled), and freed in free mode so you can use the mouse normally.")]
public bool lockCursorInLookMode = true;
CinemachineOrbitalFollow _orbit;
// True = look mode (cursor locked, camera rotates). False = free mode.
bool _rotationEnabled = true;
void Awake()
{
_orbit = GetComponent<CinemachineOrbitalFollow>();
if (_orbit == null)
Debug.LogError("[ThirdPersonOrbitInput] No CinemachineOrbitalFollow found on this GameObject.", this);
}
void OnEnable()
{
_rotationEnabled = startInLookMode;
ApplyCursorState();
}
void OnDisable()
{
// Make sure we never leave the cursor locked/hidden when this is turned off.
SetCursorLocked(false);
}
void Update()
{
if (_orbit == null)
return;
// Tab toggles between look mode and free mode.
if (WasTogglePressed())
{
_rotationEnabled = !_rotationEnabled;
ApplyCursorState();
}
if (_rotationEnabled)
{
Vector2 delta = ReadMouseDelta();
if (delta.sqrMagnitude > 0f)
{
ApplyAxisDelta(ref _orbit.HorizontalAxis, delta.x * horizontalSensitivity);
float pitch = delta.y * verticalSensitivity * (invertY ? 1f : -1f);
ApplyAxisDelta(ref _orbit.VerticalAxis, pitch);
}
}
if (enableZoom)
{
float scroll = ReadScroll();
if (!Mathf.Approximately(scroll, 0f))
_orbit.Radius = Mathf.Clamp(_orbit.Radius - scroll * zoomSpeed, minRadius, maxRadius);
}
}
void ApplyCursorState()
{
if (lockCursorInLookMode)
SetCursorLocked(_rotationEnabled);
}
/// <summary>
/// Adds <paramref name="amount"/> to an input axis, respecting its range:
/// wrapping axes (e.g. yaw) wrap around, non-wrapping axes (e.g. pitch) clamp.
/// </summary>
static void ApplyAxisDelta(ref InputAxis axis, float amount)
{
float value = axis.Value + amount;
float min = axis.Range.x;
float max = axis.Range.y;
if (axis.Wrap && max > min)
{
float span = max - min;
value = ((value - min) % span + span) % span + min;
}
else
{
value = Mathf.Clamp(value, min, max);
}
axis.Value = value;
}
static void SetCursorLocked(bool locked)
{
Cursor.lockState = locked ? CursorLockMode.Locked : CursorLockMode.None;
Cursor.visible = !locked;
}
bool WasTogglePressed()
{
#if ENABLE_INPUT_SYSTEM
return Keyboard.current != null && Keyboard.current[toggleKey].wasPressedThisFrame;
#else
return Input.GetKeyDown(toggleKey);
#endif
}
Vector2 ReadMouseDelta()
{
#if ENABLE_INPUT_SYSTEM
// Mouse.current.delta is already the per-frame pixel movement (no deltaTime).
return Mouse.current != null ? Mouse.current.delta.ReadValue() : Vector2.zero;
#else
return new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y")) * 10f;
#endif
}
float ReadScroll()
{
#if ENABLE_INPUT_SYSTEM
// Scroll y is reported in steps of ~120 per notch; normalize to ~1 per notch.
return Mouse.current != null ? Mouse.current.scroll.ReadValue().y / 120f : 0f;
#else
return Input.GetAxis("Mouse ScrollWheel") * 10f;
#endif
}
}