Custom mouse cursor: normal/paint/pipette with recorder-aware software mode
- CursorController: polls painter state in LateUpdate; paint cursor only while the brush preview decal actually draws (MeshPainter.BrushPreviewVisible), pipette while held/toggled, fallback to normal over UI (InputUtil.PointerOverUi) - Auto ForceSoftware while Unity Recorder is recording (reflection into editor-only assembly, 0.5s poll, editor-only) so the cursor is captured; back to hardware cursor when recording stops - 3 placeholder cursor PNGs (32x32, cursor import type) with per-cursor hotspots Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9decea3501
commit
ae5fa63141
10 files changed
+465
No files matched your search
@@ -422,6 +422,8 @@ public partial class MeshPainter : MonoBehaviour
|
||||
private bool _rayHitsSaturated; // log-once: a saturated buffer can silently drop our own collider
|
||||
|
||||
// --- preview overlay ---
|
||||
/// <summary>True while the brush preview decal is actually drawn this frame (hover hits the paintable surface).</summary>
|
||||
public bool BrushPreviewVisible { get; private set; }
|
||||
public Shader previewShader; // optional; else Shader.Find("Hidden/MeshPaintPreview")
|
||||
private Material _previewMat;
|
||||
private Mesh _previewQuad; // unit quad in the tangent plane (flat decal mode)
|
||||
@@ -785,6 +787,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
// the same local space as the raycast hit and valid for skinned (baked) and static meshes.
|
||||
Mesh previewMesh = _collider ? _collider.sharedMesh : null;
|
||||
if (!previewMesh) return;
|
||||
BrushPreviewVisible = true;
|
||||
Matrix4x4 m = transform.localToWorldMatrix;
|
||||
int sub = previewMesh.subMeshCount;
|
||||
for (int i = 0; i < sub; i++)
|
||||
@@ -804,6 +807,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
frame.SetColumn(3, new Vector4(pos.x, pos.y, pos.z, 1f));
|
||||
|
||||
Graphics.DrawMesh(_previewQuad, transform.localToWorldMatrix * frame, _previewMat, layer, paintCamera, 0);
|
||||
BrushPreviewVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,6 +815,8 @@ public partial class MeshPainter : MonoBehaviour
|
||||
|
||||
private void Update()
|
||||
{
|
||||
BrushPreviewVisible = false; // set again below only when the decal really draws this frame
|
||||
|
||||
if (!_setupValid) return;
|
||||
|
||||
if (useGpu != _gpuActive) SwitchBackend(useGpu);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the hardware mouse cursor with the paint state of the local painter: normal outside
|
||||
/// paint mode, brush while painting, eyedropper while the pipette is active. Lives once in the
|
||||
/// Main scene; remote painters never have PaintingEnabled, so polling the manager's local
|
||||
/// painter is unambiguous. While a camera rig locks/hides the cursor this is simply invisible.
|
||||
/// </summary>
|
||||
public class CursorController : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Source of the local painter. Found automatically when left empty.")]
|
||||
[SerializeField] private GameModeManager modeManager;
|
||||
|
||||
[Header("Cursors (import type: Cursor)")]
|
||||
[SerializeField] private Texture2D normalCursor;
|
||||
[Tooltip("Click point inside the texture, in pixels from the top-left.")]
|
||||
[SerializeField] private Vector2 normalHotspot = Vector2.zero;
|
||||
[SerializeField] private Texture2D paintCursor;
|
||||
[SerializeField] private Vector2 paintHotspot = new(16f, 16f);
|
||||
[SerializeField] private Texture2D pipetteCursor;
|
||||
[SerializeField] private Vector2 pipetteHotspot = new(1f, 30f);
|
||||
|
||||
private enum State { None, Normal, Paint, Pipette }
|
||||
|
||||
private State _applied = State.None;
|
||||
private CursorMode _appliedMode = CursorMode.Auto;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Hardware cursors are composited by the OS and never end up in Unity Recorder captures.
|
||||
// While a recording runs we force the software cursor (rendered by Unity -> captured).
|
||||
// RecorderWindow lives in the editor-only Unity.Recorder.Editor assembly, hence reflection.
|
||||
private static readonly System.Type RecorderWindowType =
|
||||
System.Type.GetType("UnityEditor.Recorder.RecorderWindow, Unity.Recorder.Editor");
|
||||
private static readonly System.Reflection.MethodInfo RecorderIsRecording =
|
||||
RecorderWindowType?.GetMethod("IsRecording");
|
||||
private Object _recorderWindow;
|
||||
private float _nextRecorderPoll;
|
||||
private bool _recorderActive;
|
||||
|
||||
private bool RecorderActive()
|
||||
{
|
||||
if (RecorderIsRecording == null) return false;
|
||||
if (Time.unscaledTime < _nextRecorderPoll) return _recorderActive;
|
||||
_nextRecorderPoll = Time.unscaledTime + 0.5f;
|
||||
if (!_recorderWindow)
|
||||
{
|
||||
var windows = Resources.FindObjectsOfTypeAll(RecorderWindowType);
|
||||
_recorderWindow = windows.Length > 0 ? windows[0] : null;
|
||||
}
|
||||
_recorderActive = _recorderWindow && (bool)RecorderIsRecording.Invoke(_recorderWindow, null);
|
||||
return _recorderActive;
|
||||
}
|
||||
#endif
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (!modeManager) modeManager = FindFirstObjectByType<GameModeManager>();
|
||||
}
|
||||
|
||||
// LateUpdate: MeshPainter.Update settles this frame's pipette state (Shift/Ctrl momentary
|
||||
// pipette) first, so the cursor never lags a frame behind the modifier.
|
||||
private void LateUpdate()
|
||||
{
|
||||
var painter = modeManager ? modeManager.Painter : null;
|
||||
State want = State.Normal;
|
||||
// Over UI neither the pipette nor the brush can act -> normal arrow.
|
||||
if (painter && painter.PaintingEnabled && !InputUtil.PointerOverUi())
|
||||
{
|
||||
if (painter.pipette) want = State.Pipette;
|
||||
// Paint cursor only while painting is actually possible, i.e. the brush preview
|
||||
// decal is drawn at the hover point this frame; hovering off the mesh -> normal.
|
||||
else if (painter.BrushPreviewVisible) want = State.Paint;
|
||||
}
|
||||
|
||||
var mode = CursorMode.Auto;
|
||||
#if UNITY_EDITOR
|
||||
if (RecorderActive()) mode = CursorMode.ForceSoftware;
|
||||
#endif
|
||||
|
||||
if (want == _applied && mode == _appliedMode) return;
|
||||
_applied = want;
|
||||
_appliedMode = mode;
|
||||
|
||||
switch (want)
|
||||
{
|
||||
case State.Paint: Cursor.SetCursor(paintCursor, paintHotspot, mode); break;
|
||||
case State.Pipette: Cursor.SetCursor(pipetteCursor, pipetteHotspot, mode); break;
|
||||
default: Cursor.SetCursor(normalCursor, normalHotspot, mode); break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto); // back to the OS cursor
|
||||
_applied = State.None;
|
||||
_appliedMode = CursorMode.Auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa11882355996204c8d14bb57c6a1838
|
||||
Reference in new issue
Block a user