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>
95 lines
4.1 KiB
C#
95 lines
4.1 KiB
C#
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);
|
|
}
|