Add radial hero-cutout shader for third-person camera

Reveal the player when geometry sits between the orbit camera and the
character: a screen-space circular mask in a URP/Lit clone (HeroCutoutLit)
clips occluders in front of the player, depth-gated so only geometry nearer
than the hero is cut. The cutout lives outside UnityPerMaterial (SRP batcher
intact) and is a no-op when disarmed, so occluders look identical to URP/Lit.

A runtime driver (HeroCutout.cs on Main/GameManager) feeds the player's
screen position, radius, feather and depth into global shader properties and
only arms while the third-person orbit vcam is live; it disarms in
first-person, free-cam and the pause menu. Applied to the lobby walls via a
dedicated occluder material (floor left solid). Shipped but shelved for now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-28 17:18:32 +02:00
1 parent b6aaf69235
commit ac25f59232
10 files changed
+896 -7

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
using UnityEngine;
using Unity.Cinemachine;
/// <summary>
/// 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 (<c>_HeroCutoutPosRadius</c>, <c>_HeroCutoutDepthOn</c>) that the
/// <c>Custom/HeroCutoutLit</c> 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 <see cref="CinemachineCamera"/> 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).
/// </summary>
[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<ThirdPersonOrbitInput>(FindObjectsInactive.Include);
if (!modes) modes = FindFirstObjectByType<GameModeManager>();
if (orbitInput) _orbitCam = orbitInput.GetComponent<CinemachineCamera>();
}
private void OnDisable() => Disarm();
private void LateUpdate()
{
Camera cam = Camera.main;
if (!cam) { Disarm(); return; }
if (!_brain || _brain.gameObject != cam.gameObject)
_brain = cam.GetComponent<CinemachineBrain>();
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));
}
/// <summary>True only when the orbit vcam is the live Cinemachine camera (third-person modes).</summary>
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;
}
/// <summary>Clears the "enabled" flag so cutout materials render as plain URP/Lit.</summary>
private void Disarm() => Shader.SetGlobalVector(DepthOnId, Vector4.zero);
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c7d67030b8dcdec489471b88c35dd78b