diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 47b19e4..c52489c 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -119,6 +119,238 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1 &172071430 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 172071436} + - component: {fileID: 172071435} + - component: {fileID: 172071434} + - component: {fileID: 172071433} + - component: {fileID: 172071432} + - component: {fileID: 172071431} + m_Layer: 0 + m_Name: ThirdPersonCamera + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &172071431 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 172071430} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: df129be8fadab79498bd429a59bb98e4, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::ThirdPersonOrbitInput + horizontalSensitivity: 0.2 + verticalSensitivity: 0.15 + invertY: 0 + toggleKey: 3 + startInLookMode: 1 + enableZoom: 1 + zoomSpeed: 0.5 + minRadius: 1.5 + maxRadius: 8 + lockCursorInLookMode: 1 +--- !u!114 &172071432 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 172071430} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dbe43d662878a7c43bcd44d43c9e2094, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineDeoccluder + CollideAgainst: + serializedVersion: 2 + m_Bits: 1 + IgnoreTag: Player + TransparentLayers: + serializedVersion: 2 + m_Bits: 0 + MinimumDistanceFromTarget: 0.5 + AvoidObstacles: + Enabled: 1 + DistanceLimit: 0 + MinimumOcclusionTime: 0 + CameraRadius: 0.2 + UseFollowTarget: + Enabled: 0 + YOffset: 0 + Strategy: 0 + MaximumEffort: 4 + SmoothingTime: 0 + Damping: 0.2 + DampingWhenOccluded: 0 + ShotQualityEvaluation: + Enabled: 0 + OptimalDistance: 10 + NearLimit: 5 + FarLimit: 30 + MaxQualityBoost: 0.2 +--- !u!114 &172071433 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 172071430} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f38bda98361e1de48a4ca2bd86ea3c17, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineRotationComposer + Composition: + ScreenPosition: {x: 0, y: 0} + DeadZone: + Enabled: 0 + Size: {x: 0.2, y: 0.2} + HardLimits: + Enabled: 0 + Size: {x: 0.8, y: 0.8} + Offset: {x: 0, y: 0} + CenterOnActivate: 1 + TargetOffset: {x: 0, y: 0, z: 0} + Damping: {x: 0.5, y: 0.5} + Lookahead: + Enabled: 0 + Time: 0 + Smoothing: 0 + IgnoreY: 0 +--- !u!114 &172071434 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 172071430} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3b5d7c088409d9a40b7b09aa707777f8, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineOrbitalFollow + TargetOffset: {x: 0, y: 0, z: 0} + TrackerSettings: + BindingMode: 4 + PositionDamping: {x: 1, y: 1, z: 1} + AngularDampingMode: 0 + RotationDamping: {x: 1, y: 1, z: 1} + QuaternionDamping: 1 + OrbitStyle: 0 + Radius: 4.5 + Orbits: + Top: + Radius: 2 + Height: 5 + Center: + Radius: 4 + Height: 2.25 + Bottom: + Radius: 2.5 + Height: 0.1 + SplineCurvature: 0.5 + RecenteringTarget: 2 + HorizontalAxis: + Value: 0 + Center: 0 + Range: {x: -180, y: 180} + Wrap: 1 + Recentering: + Enabled: 0 + Wait: 1 + Time: 2 + Restrictions: 0 + VerticalAxis: + Value: 15 + Center: 12 + Range: {x: -20, y: 60} + Wrap: 0 + Recentering: + Enabled: 0 + Wait: 1 + Time: 2 + Restrictions: 0 + RadialAxis: + Value: 1 + Center: 1 + Range: {x: 1, y: 1} + Wrap: 0 + Recentering: + Enabled: 0 + Wait: 1 + Time: 2 + Restrictions: 0 +--- !u!114 &172071435 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 172071430} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f9dfa5b682dcd46bda6128250e975f58, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineCamera + Priority: + Enabled: 0 + m_Value: 0 + OutputChannel: 1 + StandbyUpdate: 2 + m_StreamingVersion: 20241001 + m_LegacyPriority: 0 + Target: + TrackingTarget: {fileID: 1774267802} + LookAtTarget: {fileID: 1774267802} + CustomLookAtTarget: 0 + Lens: + FieldOfView: 40 + OrthographicSize: 10 + NearClipPlane: 0.1 + FarClipPlane: 5000 + Dutch: 0 + ModeOverride: 0 + PhysicalProperties: + GateFit: 2 + SensorSize: {x: 21.946, y: 16.002} + LensShift: {x: 0, y: 0} + FocusDistance: 10 + Iso: 200 + ShutterSpeed: 0.005 + Aperture: 16 + BladeCount: 5 + Curvature: {x: 2, y: 11} + BarrelClipping: 0.25 + Anamorphism: 0 + BlendHint: 0 +--- !u!4 &172071436 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 172071430} + serializedVersion: 2 + m_LocalRotation: {x: 0.1305262, y: 0.00000002667958, z: -0.0000000035124335, w: 0.9914449} + m_LocalPosition: {x: 0, y: 3.1646857, z: -4.3466663} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &200502118 GameObject: m_ObjectHideFlags: 0 @@ -243,6 +475,7 @@ GameObject: - component: {fileID: 330585545} - component: {fileID: 330585544} - component: {fileID: 330585547} + - component: {fileID: 330585548} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera @@ -289,11 +522,11 @@ Camera: y: 0 width: 1 height: 1 - near clip plane: 0.3 - far clip plane: 1000 - field of view: 60 + near clip plane: 0.1 + far clip plane: 5000 + field of view: 40 orthographic: 0 - orthographic size: 5 + orthographic size: 10 m_Depth: -1 m_CullingMask: serializedVersion: 2 @@ -317,8 +550,8 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 330585543} serializedVersion: 2 - m_LocalRotation: {x: 0.3420201, y: 0, z: 0, w: 0.9396927} - m_LocalPosition: {x: 0, y: 4.43, z: -7.8} + m_LocalRotation: {x: 0.1305262, y: 0, z: 0, w: 0.9914449} + m_LocalPosition: {x: 0, y: 3.1646857, z: -4.3466663} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -368,6 +601,38 @@ MonoBehaviour: m_VarianceClampScale: 0.9 m_ContrastAdaptiveSharpening: 0 m_Version: 2 +--- !u!114 &330585548 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 72ece51f2901e7445ab60da3685d6b5f, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineBrain + ShowDebugText: 0 + ShowCameraFrustum: 1 + IgnoreTimeScale: 0 + WorldUpOverride: {fileID: 0} + ChannelMask: -1 + UpdateMethod: 2 + BlendUpdateMethod: 1 + LensModeOverride: + Enabled: 0 + DefaultMode: 2 + DefaultBlend: + Style: 1 + Time: 2 + CustomCurve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + CustomBlends: {fileID: 0} --- !u!1 &410087039 GameObject: m_ObjectHideFlags: 0 @@ -517,7 +782,7 @@ MonoBehaviour: brushSize: 2 textureSize: 1024 baseColor: {r: 0.8, g: 0.8, b: 0.8, a: 1} - paddingTexels: 1 + paddingTexels: 2 paintCamera: {fileID: 0} updateColliderEveryFrame: 0 brushShader: {fileID: 0} @@ -869,7 +1134,7 @@ GameObject: - component: {fileID: 1141039642} m_Layer: 0 m_Name: Player - m_TagString: Untagged + m_TagString: Player m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 @@ -888,6 +1153,7 @@ Transform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1944972203} + - {fileID: 1774267802} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!143 &1141039641 @@ -939,6 +1205,37 @@ MonoBehaviour: slideAcceleration: 20 maxSlideSpeed: 8 slideControl: 0.3 +--- !u!1 &1774267801 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1774267802} + m_Layer: 0 + m_Name: CameraTarget + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1774267802 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1774267801} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1141039639} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!4 &1944972203 stripped Transform: m_CorrespondingSourceObject: {fileID: -8679921383154817045, guid: 554c0bdf86263754d862d7effd04045b, type: 3} @@ -1004,3 +1301,4 @@ SceneRoots: - {fileID: 721670973} - {fileID: 963088882} - {fileID: 200502122} + - {fileID: 172071436} diff --git a/Assets/Scripts/MeshPainter.cs b/Assets/Scripts/MeshPainter.cs index 0841fd4..2c24ae2 100644 --- a/Assets/Scripts/MeshPainter.cs +++ b/Assets/Scripts/MeshPainter.cs @@ -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 _dilateScratch = new List(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). diff --git a/Assets/Scripts/ThirdPersonOrbitInput.cs b/Assets/Scripts/ThirdPersonOrbitInput.cs new file mode 100644 index 0000000..158983d --- /dev/null +++ b/Assets/Scripts/ThirdPersonOrbitInput.cs @@ -0,0 +1,186 @@ +using UnityEngine; +using Unity.Cinemachine; +#if ENABLE_INPUT_SYSTEM +using UnityEngine.InputSystem; +#endif + +/// +/// Feeds mouse input into a 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). +/// +[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(); + 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); + } + + /// + /// Adds to an input axis, respecting its range: + /// wrapping axes (e.g. yaw) wrap around, non-wrapping axes (e.g. pitch) clamp. + /// + 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 + } +} diff --git a/Assets/Scripts/ThirdPersonOrbitInput.cs.meta b/Assets/Scripts/ThirdPersonOrbitInput.cs.meta new file mode 100644 index 0000000..cdf7ba4 --- /dev/null +++ b/Assets/Scripts/ThirdPersonOrbitInput.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: df129be8fadab79498bd429a59bb98e4 \ No newline at end of file diff --git a/Packages/manifest.json b/Packages/manifest.json index be79f88..06b812b 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,6 +1,7 @@ { "dependencies": { "com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main", + "com.unity.cinemachine": "3.1.7", "com.unity.ide.visualstudio": "2.0.27", "com.unity.inputsystem": "1.19.0", "com.unity.multiplayer.center": "1.0.1", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 455e5aa..19963e3 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -27,6 +27,16 @@ }, "url": "https://packages.unity.com" }, + "com.unity.cinemachine": { + "version": "3.1.7", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.splines": "2.0.0", + "com.unity.modules.imgui": "1.0.0" + }, + "url": "https://packages.unity.com" + }, "com.unity.collections": { "version": "2.6.2", "depth": 2, @@ -132,6 +142,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.settings-manager": { + "version": "2.1.1", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.shadergraph": { "version": "17.3.0", "depth": 1, @@ -141,6 +158,17 @@ "com.unity.searcher": "4.9.3" } }, + "com.unity.splines": { + "version": "2.8.3", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1", + "com.unity.modules.imgui": "1.0.0", + "com.unity.settings-manager": "1.0.3" + }, + "url": "https://packages.unity.com" + }, "com.unity.test-framework": { "version": "1.6.0", "depth": 1,