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:
1 parent
792c8c964f
commit
6e8ab4bc6d
6 files changed
+545
-10
No files matched your search
@@ -89,6 +89,7 @@ public class MeshPainter : MonoBehaviour
|
||||
private int[] _fillStamp; // transient marker for padding texels per dilation call
|
||||
private int _dilateStamp; // incremented per call -> padding from previous frames does not count as a source
|
||||
private readonly List<int> _dilateScratch = new List<int>(512);
|
||||
private readonly RaycastHit[] _rayHits = new RaycastHit[32]; // buffer so other colliders (player capsule, etc.) cannot block painting
|
||||
private int _dx0, _dy0, _dx1, _dy1; // dirty rectangle (CPU upload)
|
||||
|
||||
// --- GPU backend ---
|
||||
@@ -480,8 +481,27 @@ public class MeshPainter : MonoBehaviour
|
||||
private void PaintAtScreen(Vector2 screenPos)
|
||||
{
|
||||
Ray ray = paintCamera.ScreenPointToRay(screenPos);
|
||||
if (!(Physics.Raycast(ray, out RaycastHit hit, 1000f) && hit.collider == _collider))
|
||||
return;
|
||||
|
||||
// Collect all hits and pick the nearest one that belongs to the paint mesh.
|
||||
// A plain Physics.Raycast returns only the closest collider, so anything in
|
||||
// front of the mesh (the player's CharacterController capsule, future props,
|
||||
// the camera-collision proxy) would block painting. Filtering by collider
|
||||
// keeps painting working from every camera angle.
|
||||
int hitCount = Physics.RaycastNonAlloc(ray, _rayHits, 1000f, ~0, QueryTriggerInteraction.Ignore);
|
||||
RaycastHit hit = default;
|
||||
float nearest = float.MaxValue;
|
||||
bool found = false;
|
||||
for (int i = 0; i < hitCount; i++)
|
||||
{
|
||||
if (_rayHits[i].collider != _collider) continue;
|
||||
if (_rayHits[i].distance < nearest)
|
||||
{
|
||||
nearest = _rayHits[i].distance;
|
||||
hit = _rayHits[i];
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) return;
|
||||
if (_verts == null || hit.triangleIndex < 0) return;
|
||||
|
||||
// Bring the hit point into local mesh space (= collider/vertex space).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df129be8fadab79498bd429a59bb98e4
|
||||
Reference in new issue
Block a user