using UnityEngine;
using Unity.Cinemachine;
///
/// Drives the screen-space radial "hero cutout". Every late frame it projects the local player to
/// screen space and feeds that position + a circle radius/feather + the player's eye-space depth into
/// two global shader properties (_HeroCutoutPosRadius, _HeroCutoutDepthOn) that the
/// Custom/HeroCutoutLit shader reads to clip occluder geometry between the camera and the
/// player - punching a soft see-through hole toward the character.
///
/// The effect is only armed while the third-person orbit is the live
/// Cinemachine camera (i.e. Player Control / Paint mode). In first-person, free-cam, or while a menu
/// is open a different vcam is live, so the cutout disarms and occluders render solid. Disarming sets
/// the global "enabled" flag to 0, which also matches the zero-initialised default - so any scene
/// without this driver leaves cutout materials looking exactly like URP/Lit.
///
/// Attach to a persistent scene object (e.g. the GameModeManager host).
///
[DisallowMultipleComponent]
public class HeroCutout : MonoBehaviour
{
[Tooltip("Orbit camera input whose CinemachineCamera must be live for the cutout to arm. Empty = found automatically.")]
public ThirdPersonOrbitInput orbitInput;
[Tooltip("Game mode manager, used as a fallback source for the live player. Empty = found automatically.")]
public GameModeManager modes;
[Header("Circle")]
[Tooltip("Radius of the see-through circle, as a fraction of screen height (0.13 = 13% of height).")]
[Range(0.02f, 0.6f)] public float radius = 0.13f;
[Tooltip("Soft-edge width of the circle, as a fraction of screen height.")]
[Range(0f, 0.3f)] public float feather = 0.05f;
[Tooltip("Metres added to the player's depth so geometry right at the player isn't clipped away.")]
public float depthBias = 0.5f;
[Tooltip("Vertical offset (m) from the follow target where the circle centres (chest/head height).")]
public float heightOffset = 0f;
static readonly int PosRadiusId = Shader.PropertyToID("_HeroCutoutPosRadius");
static readonly int DepthOnId = Shader.PropertyToID("_HeroCutoutDepthOn");
private CinemachineCamera _orbitCam;
private CinemachineBrain _brain;
private void Awake()
{
if (!orbitInput) orbitInput = FindFirstObjectByType(FindObjectsInactive.Include);
if (!modes) modes = FindFirstObjectByType();
if (orbitInput) _orbitCam = orbitInput.GetComponent();
}
private void OnDisable() => Disarm();
private void LateUpdate()
{
Camera cam = Camera.main;
if (!cam) { Disarm(); return; }
if (!_brain || _brain.gameObject != cam.gameObject)
_brain = cam.GetComponent();
if (!IsThirdPersonLive()) { Disarm(); return; }
Transform target = ResolveHeroTarget();
if (!target) { Disarm(); return; }
Vector3 heroPos = target.position + Vector3.up * heightOffset;
Vector3 vp = cam.WorldToViewportPoint(heroPos);
if (vp.z <= 0f) { Disarm(); return; } // hero is behind the camera
Shader.SetGlobalVector(PosRadiusId, new Vector4(vp.x, vp.y, radius, Mathf.Max(feather, 1e-4f)));
Shader.SetGlobalVector(DepthOnId, new Vector4(vp.z + depthBias, 1f, 0f, 0f));
}
/// True only when the orbit vcam is the live Cinemachine camera (third-person modes).
private bool IsThirdPersonLive()
{
if (!_orbitCam || !_brain) return false;
var live = _brain.ActiveVirtualCamera as CinemachineCamera;
return live && live == _orbitCam;
}
private Transform ResolveHeroTarget()
{
if (_orbitCam && _orbitCam.Follow) return _orbitCam.Follow;
if (modes && modes.Player) return modes.Player.transform;
return null;
}
/// Clears the "enabled" flag so cutout materials render as plain URP/Lit.
private void Disarm() => Shader.SetGlobalVector(DepthOnId, Vector4.zero);
}