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); }