Drive the hero cutout from a single camera-to-target ray
Replace the multi-sample silhouette probe with one camera->target raycast: any world geometry in the line of sight arms the cut, a clear line keeps occluders solid. The shader carves a world-space cone (apex at the camera, radius growing toward the target) so the screen-space hole stays a constant size regardless of occluder depth, with a soft radial fade plus a symmetric axial fade around the target so geometry just behind the player feathers out instead of hard-cutting along a plane. Anything well past the target stays solid. Also suppresses the deoccluder pull-in so the camera holds its orbit distance and the cut takes over. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
1aa1f6d869
commit
06f5f33e63
2 files changed
+228
-52
No files matched your search
+170
-27
@@ -2,17 +2,32 @@ using UnityEngine;
|
|||||||
using Unity.Cinemachine;
|
using Unity.Cinemachine;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Drives the screen-space radial "hero cutout". Every late frame it projects the local player to
|
/// Drives the world-space "hero cutout": occluder geometry that sits inside a cone running from the
|
||||||
/// screen space and feeds that position + a circle radius/feather + the player's eye-space depth into
|
/// third-person camera (apex) to the player is clipped away, punching a clean see-through funnel toward
|
||||||
/// two global shader properties (<c>_HeroCutoutPosRadius</c>, <c>_HeroCutoutDepthOn</c>) that the
|
/// the character. Every late frame this feeds two global shader properties (<c>_HeroCutoutTarget</c>,
|
||||||
/// <c>Custom/HeroCutoutLit</c> shader reads to clip occluder geometry between the camera and the
|
/// <c>_HeroCutoutParams</c>) that the <c>Custom/HeroCutoutLit</c> shader reads.
|
||||||
/// 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
|
/// Arming is deliberately simple: a single ray is cast FROM the camera TOWARD the player. If it hits
|
||||||
/// Cinemachine camera (i.e. Player Control / Paint mode). In first-person, free-cam, or while a menu
|
/// world geometry before reaching the player, the cut is armed; with a clear line of sight the
|
||||||
/// is open a different vcam is live, so the cutout disarms and occluders render solid. Disarming sets
|
/// occluders stay solid. There is no multi-sample / silhouette estimation - one ray, one answer.
|
||||||
/// 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.
|
/// The cut volume is defined entirely in the shader as a cone along the camera->player axis: everything
|
||||||
|
/// BETWEEN camera and player (within the cone) is cut; geometry well behind the player stays solid - so a
|
||||||
|
/// wall the player stands in front of renders completely normally. The far end fades out across a soft
|
||||||
|
/// depth band (<see cref="depthFeather"/>) straddling the player rather than snapping off at a hard
|
||||||
|
/// plane, so an occluder running past the player fades just behind them with no hard cut line. The cone's
|
||||||
|
/// radius grows with distance from the camera, so the hole keeps a constant on-screen size regardless of
|
||||||
|
/// occluder depth.
|
||||||
|
///
|
||||||
|
/// The effect is only active while the third-person orbit <see cref="CinemachineCamera"/> is the live
|
||||||
|
/// Cinemachine camera (Player Control / Paint mode). In first-person, free-cam or while a menu is open
|
||||||
|
/// a different vcam is live and the cutout disarms. Disarming clears the global "enabled" flag (also
|
||||||
|
/// the zero-initialised default), so any scene without this driver - or any frame with a clear view -
|
||||||
|
/// leaves cutout materials looking exactly like URP/Lit.
|
||||||
|
///
|
||||||
|
/// This replaces the camera pulling in toward the player (Cinemachine Deoccluder): instead of closing
|
||||||
|
/// the distance to dodge an obstacle, the camera holds its orbit distance and the obstacle is punched
|
||||||
|
/// see-through.
|
||||||
///
|
///
|
||||||
/// Attach to a persistent scene object (e.g. the GameModeManager host).
|
/// Attach to a persistent scene object (e.g. the GameModeManager host).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -25,36 +40,86 @@ public class HeroCutout : MonoBehaviour
|
|||||||
[Tooltip("Game mode manager, used as a fallback source for the live player. Empty = found automatically.")]
|
[Tooltip("Game mode manager, used as a fallback source for the live player. Empty = found automatically.")]
|
||||||
public GameModeManager modes;
|
public GameModeManager modes;
|
||||||
|
|
||||||
[Header("Circle")]
|
[Tooltip("Switch off the orbit camera's Cinemachine Deoccluder obstacle avoidance at runtime so the " +
|
||||||
[Tooltip("Radius of the see-through circle, as a fraction of screen height (0.13 = 13% of height).")]
|
"camera holds its orbit distance and this cutout handles occlusion - instead of the camera " +
|
||||||
[Range(0.02f, 0.6f)] public float radius = 0.13f;
|
"pulling in toward the player. The scene's Deoccluder asset is left untouched; only its live " +
|
||||||
|
"pull-in is disabled (and restored if this driver is disabled).")]
|
||||||
|
public bool replaceDeoccluderPullIn = true;
|
||||||
|
|
||||||
[Tooltip("Soft-edge width of the circle, as a fraction of screen height.")]
|
[Header("Cutout cone")]
|
||||||
[Range(0f, 0.3f)] public float feather = 0.05f;
|
[Tooltip("Radius of the see-through cone AT THE PLAYER, in metres. The cone tapers to nothing at the " +
|
||||||
|
"camera, so the hole keeps a constant on-screen size at every occluder depth.")]
|
||||||
|
[Range(0.05f, 3f)] public float radius = 0.5f;
|
||||||
|
|
||||||
[Tooltip("Metres added to the player's depth so geometry right at the player isn't clipped away.")]
|
[Tooltip("Soft-edge width of the cone wall at the player, in metres (also tapers toward the camera).")]
|
||||||
public float depthBias = 0.5f;
|
[Range(0f, 1f)] public float feather = 0.15f;
|
||||||
|
|
||||||
[Tooltip("Vertical offset (m) from the follow target where the circle centres (chest/head height).")]
|
[Tooltip("Soft depth band (m) straddling the player: instead of stopping at a hard plane at the " +
|
||||||
|
"player's depth, the cut fades out across +/- this distance around the player, so an occluder " +
|
||||||
|
"running past the player (a shelf board) fades just behind them too instead of showing a hard " +
|
||||||
|
"straight cut line. Geometry beyond this band stays fully solid.")]
|
||||||
|
[Range(0.01f, 2f)] public float depthFeather = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("Vertical offset (m) from the follow target the cone aims at (chest/head height).")]
|
||||||
public float heightOffset = 0f;
|
public float heightOffset = 0f;
|
||||||
|
|
||||||
static readonly int PosRadiusId = Shader.PropertyToID("_HeroCutoutPosRadius");
|
[Header("Occlusion ray (camera -> player)")]
|
||||||
static readonly int DepthOnId = Shader.PropertyToID("_HeroCutoutDepthOn");
|
[Tooltip("Layers that count as occluders. Leave empty (Nothing) to fall back to all default raycast layers.")]
|
||||||
|
public LayerMask occluderMask = ~0;
|
||||||
|
|
||||||
|
[Tooltip("Collider tag ignored by the ray (the player's own colliders). Must be an existing tag; " +
|
||||||
|
"empty = ignore nothing.")]
|
||||||
|
public string ignoreTag = "Player";
|
||||||
|
|
||||||
|
[Tooltip("Metres the ray stops short of the player. Keep at 0 so a wall the player stands right " +
|
||||||
|
"behind still arms the cut (everything between camera and player counts). The ray already " +
|
||||||
|
"ends AT the player, so geometry behind them is never hit regardless. Raise this only if a " +
|
||||||
|
"low camera grazes the floor at the player's feet (better fixed via Height Offset).")]
|
||||||
|
[Range(0f, 1.5f)] public float minDistanceFromTarget = 0f;
|
||||||
|
|
||||||
|
[Tooltip("Seconds the cutout stays armed after the view clears again, to stop flicker at occluder edges.")]
|
||||||
|
public float occlusionHold = 0.1f;
|
||||||
|
|
||||||
|
static readonly int TargetId = Shader.PropertyToID("_HeroCutoutTarget");
|
||||||
|
static readonly int ParamsId = Shader.PropertyToID("_HeroCutoutParams");
|
||||||
|
|
||||||
private CinemachineCamera _orbitCam;
|
private CinemachineCamera _orbitCam;
|
||||||
private CinemachineBrain _brain;
|
private CinemachineBrain _brain;
|
||||||
|
private CinemachineDeoccluder _deoccluder;
|
||||||
|
private bool _deoccluderResolved;
|
||||||
|
private bool _deoccluderOriginalAvoid;
|
||||||
|
private bool _pullInSuppressed;
|
||||||
|
private float _lastOccludedTime = -999f;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
if (!orbitInput) orbitInput = FindFirstObjectByType<ThirdPersonOrbitInput>(FindObjectsInactive.Include);
|
if (!orbitInput) orbitInput = FindFirstObjectByType<ThirdPersonOrbitInput>(FindObjectsInactive.Include);
|
||||||
if (!modes) modes = FindFirstObjectByType<GameModeManager>();
|
if (!modes) modes = FindFirstObjectByType<GameModeManager>();
|
||||||
if (orbitInput) _orbitCam = orbitInput.GetComponent<CinemachineCamera>();
|
EnsureRefs();
|
||||||
|
|
||||||
|
// CompareTag throws on an undefined tag - validate once so the per-frame ray never does.
|
||||||
|
if (!string.IsNullOrEmpty(ignoreTag))
|
||||||
|
{
|
||||||
|
try { _ = gameObject.CompareTag(ignoreTag); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[HeroCutout] Ignore Tag '{ignoreTag}' is not defined; the ray will ignore nothing.", this);
|
||||||
|
ignoreTag = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisable() => Disarm();
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
Disarm();
|
||||||
|
RestoreDeoccluderPullIn();
|
||||||
|
}
|
||||||
|
|
||||||
private void LateUpdate()
|
private void LateUpdate()
|
||||||
{
|
{
|
||||||
|
EnsureRefs();
|
||||||
|
ApplyPullInSuppression();
|
||||||
|
|
||||||
Camera cam = Camera.main;
|
Camera cam = Camera.main;
|
||||||
if (!cam) { Disarm(); return; }
|
if (!cam) { Disarm(); return; }
|
||||||
|
|
||||||
@@ -67,11 +132,42 @@ public class HeroCutout : MonoBehaviour
|
|||||||
if (!target) { Disarm(); return; }
|
if (!target) { Disarm(); return; }
|
||||||
|
|
||||||
Vector3 heroPos = target.position + Vector3.up * heightOffset;
|
Vector3 heroPos = target.position + Vector3.up * heightOffset;
|
||||||
Vector3 vp = cam.WorldToViewportPoint(heroPos);
|
Vector3 camPos = cam.transform.position;
|
||||||
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)));
|
// Only punch the tube when something actually stands between the player and the camera; with a
|
||||||
Shader.SetGlobalVector(DepthOnId, new Vector4(vp.z + depthBias, 1f, 0f, 0f));
|
// clear view the occluders stay solid. A short hold after the last blocked frame avoids flicker
|
||||||
|
// when the single ray grazes an occluder edge.
|
||||||
|
if (IsViewBlocked(camPos, heroPos)) _lastOccludedTime = Time.time;
|
||||||
|
if (Time.time - _lastOccludedTime > Mathf.Max(0f, occlusionHold)) { Disarm(); return; }
|
||||||
|
|
||||||
|
// Same hero point feeds the ray and the cone axis, so the gate and the cut volume agree.
|
||||||
|
Shader.SetGlobalVector(TargetId, new Vector4(heroPos.x, heroPos.y, heroPos.z, 1f));
|
||||||
|
Shader.SetGlobalVector(ParamsId, new Vector4(Mathf.Max(0f, radius), Mathf.Max(1e-4f, feather), Mathf.Max(1e-4f, depthFeather), 0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single ray, camera -> player. Returns true when a non-ignored collider blocks the view before
|
||||||
|
/// the ray reaches the player (stopped short by <see cref="minDistanceFromTarget"/> so the wall the
|
||||||
|
/// player stands against / the floor at their feet never counts). Anything "between" the camera and
|
||||||
|
/// the player is necessarily the first thing the ray hits, so checking the first hit is enough.
|
||||||
|
/// </summary>
|
||||||
|
private bool IsViewBlocked(Vector3 camPos, Vector3 heroPos)
|
||||||
|
{
|
||||||
|
Vector3 delta = heroPos - camPos;
|
||||||
|
float dist = delta.magnitude;
|
||||||
|
if (dist <= 1e-3f) return false;
|
||||||
|
Vector3 dir = delta / dist;
|
||||||
|
|
||||||
|
float maxDist = dist - Mathf.Clamp(minDistanceFromTarget, 0f, dist);
|
||||||
|
if (maxDist <= 1e-3f) return false;
|
||||||
|
|
||||||
|
int mask = occluderMask.value != 0 ? occluderMask.value : Physics.DefaultRaycastLayers;
|
||||||
|
|
||||||
|
if (!Physics.Raycast(camPos, dir, out RaycastHit hit, maxDist, mask, QueryTriggerInteraction.Ignore))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// First (closest) hit decides: if it's the ignored player, nothing real is in between.
|
||||||
|
return string.IsNullOrEmpty(ignoreTag) || !hit.collider.CompareTag(ignoreTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>True only when the orbit vcam is the live Cinemachine camera (third-person modes).</summary>
|
/// <summary>True only when the orbit vcam is the live Cinemachine camera (third-person modes).</summary>
|
||||||
@@ -90,5 +186,52 @@ public class HeroCutout : MonoBehaviour
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Clears the "enabled" flag so cutout materials render as plain URP/Lit.</summary>
|
/// <summary>Clears the "enabled" flag so cutout materials render as plain URP/Lit.</summary>
|
||||||
private void Disarm() => Shader.SetGlobalVector(DepthOnId, Vector4.zero);
|
private void Disarm() => Shader.SetGlobalVector(TargetId, Vector4.zero);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lazily resolves the orbit vcam and its Cinemachine Deoccluder (they may live in a scene that
|
||||||
|
/// loads after this driver). Captures the deoccluder's authored obstacle-avoidance state once so it
|
||||||
|
/// can be restored later.
|
||||||
|
/// </summary>
|
||||||
|
private void EnsureRefs()
|
||||||
|
{
|
||||||
|
if (!_orbitCam && orbitInput) _orbitCam = orbitInput.GetComponent<CinemachineCamera>();
|
||||||
|
if (!_deoccluderResolved && _orbitCam)
|
||||||
|
{
|
||||||
|
_deoccluder = _orbitCam.GetComponent<CinemachineDeoccluder>();
|
||||||
|
if (_deoccluder) _deoccluderOriginalAvoid = _deoccluder.AvoidObstacles.Enabled;
|
||||||
|
_deoccluderResolved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switches the deoccluder's obstacle avoidance (the camera pull-in) off while this driver replaces
|
||||||
|
/// it with the cutout, or restores the authored state when the replacement is turned off. Only
|
||||||
|
/// toggles on transitions, so it never fights a value changed elsewhere mid-frame.
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyPullInSuppression()
|
||||||
|
{
|
||||||
|
if (!_deoccluder) return;
|
||||||
|
|
||||||
|
if (replaceDeoccluderPullIn && !_pullInSuppressed)
|
||||||
|
{
|
||||||
|
_deoccluder.AvoidObstacles.Enabled = false;
|
||||||
|
_pullInSuppressed = true;
|
||||||
|
}
|
||||||
|
else if (!replaceDeoccluderPullIn && _pullInSuppressed)
|
||||||
|
{
|
||||||
|
_deoccluder.AvoidObstacles.Enabled = _deoccluderOriginalAvoid;
|
||||||
|
_pullInSuppressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Restores the deoccluder's authored pull-in if this driver had suppressed it.</summary>
|
||||||
|
private void RestoreDeoccluderPullIn()
|
||||||
|
{
|
||||||
|
if (_pullInSuppressed && _deoccluder)
|
||||||
|
{
|
||||||
|
_deoccluder.AvoidObstacles.Enabled = _deoccluderOriginalAvoid;
|
||||||
|
_pullInSuppressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,34 @@
|
|||||||
#ifndef HERO_CUTOUT_INCLUDED
|
#ifndef HERO_CUTOUT_INCLUDED
|
||||||
#define HERO_CUTOUT_INCLUDED
|
#define HERO_CUTOUT_INCLUDED
|
||||||
|
|
||||||
// Screen-space radial "hero cutout": clips occluder fragments that sit inside a circle around the
|
// World-space "hero cutout": clips occluder fragments that lie inside a CONE running from the camera
|
||||||
// on-screen player AND in front of it, punching a clean see-through hole toward the character.
|
// (apex) to the player (target). Everything BETWEEN the camera and the target within the cone is
|
||||||
|
// punched see-through; geometry well behind the target stays solid - so a wall the player stands in
|
||||||
|
// front of still renders completely normally. The "between vs. behind" split is purely geometric
|
||||||
|
// (position along the camera->target axis), not a depth-buffer bias.
|
||||||
//
|
//
|
||||||
// These two globals are driven every frame by HeroCutout.cs (Shader.SetGlobalVector). They live
|
// That far split is FEATHERED on BOTH sides, never a hard plane: the cut fades out smoothly across a
|
||||||
// OUTSIDE UnityPerMaterial, so the SRP Batcher is unaffected. When the effect is disabled
|
// band of +/- 'depthFeather' metres AROUND the target. So an occluder that crosses the player's depth
|
||||||
// (_HeroCutoutDepthOn.y < 0.5, also the zero-initialised default) ApplyHeroCutout early-outs and the
|
// (a shelf board or wall running away from the camera) keeps fading just behind the player instead of
|
||||||
// shader renders exactly like URP/Lit.
|
// snapping to solid - no hard straight cut line where a depth plane would slice the surface. Only
|
||||||
// _HeroCutoutPosRadius : xy = hero viewport pos (0..1, bottom-left), z = radius, w = feather
|
// geometry beyond that band (more than depthFeather behind the target) stays fully solid.
|
||||||
// radius/feather are in normalized viewport-HEIGHT units
|
//
|
||||||
// _HeroCutoutDepthOn : x = hero eye-space depth (incl. bias), y = enabled (0/1)
|
// Why a cone and not a cylinder: the cone's radius grows linearly with distance from the camera
|
||||||
float4 _HeroCutoutPosRadius;
|
// (radius = paramRadius * along/len), so its *screen-space* footprint is constant regardless of how
|
||||||
float4 _HeroCutoutDepthOn;
|
// far the occluder sits from the camera. A constant-world-radius cylinder would project bigger near
|
||||||
|
// the lens and smaller near the target - a distorted hole. paramRadius is therefore the radius at the
|
||||||
|
// target plane (metres); the cone tapers to 0 at the camera apex.
|
||||||
|
//
|
||||||
|
// Driven every frame by HeroCutout.cs (Shader.SetGlobalVector). These globals live OUTSIDE
|
||||||
|
// UnityPerMaterial, so the SRP Batcher is unaffected. When disabled (_HeroCutoutTarget.w < 0.5, also
|
||||||
|
// the zero-initialised default) ApplyHeroCutout early-outs and the shader renders exactly like URP/Lit.
|
||||||
|
// _HeroCutoutTarget : xyz = player/target world position, w = enabled (0/1)
|
||||||
|
// _HeroCutoutParams : x = cone radius at the target plane (m), y = edge feather at the target plane (m),
|
||||||
|
// z = depth feather: half-width (m) of the soft fade band straddling the target
|
||||||
|
float4 _HeroCutoutTarget;
|
||||||
|
float4 _HeroCutoutParams;
|
||||||
|
|
||||||
// Interleaved-gradient noise -> screen-door dither, gives the circle a soft edge with no extra
|
// Interleaved-gradient noise -> screen-door dither, gives the cone wall a soft edge with no extra
|
||||||
// textures or includes.
|
// textures or includes.
|
||||||
float HeroCutoutDither(float2 positionSS)
|
float HeroCutoutDither(float2 positionSS)
|
||||||
{
|
{
|
||||||
@@ -23,24 +37,43 @@ float HeroCutoutDither(float2 positionSS)
|
|||||||
|
|
||||||
void ApplyHeroCutout(float4 positionCS)
|
void ApplyHeroCutout(float4 positionCS)
|
||||||
{
|
{
|
||||||
if (_HeroCutoutDepthOn.y < 0.5)
|
if (_HeroCutoutTarget.w < 0.5)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Flip-correct screen UV (matches Camera.WorldToViewportPoint's bottom-left convention).
|
// Reconstruct this fragment's world position from screen UV + device depth.
|
||||||
float2 uv = GetNormalizedScreenSpaceUV(positionCS);
|
float2 screenUV = GetNormalizedScreenSpaceUV(positionCS);
|
||||||
float2 delta = uv - _HeroCutoutPosRadius.xy;
|
float3 posWS = ComputeWorldSpacePosition(screenUV, positionCS.z, UNITY_MATRIX_I_VP);
|
||||||
delta.x *= _ScreenParams.x / max(_ScreenParams.y, 1.0); // aspect-correct -> a round circle
|
|
||||||
float dist = length(delta);
|
|
||||||
|
|
||||||
float radius = _HeroCutoutPosRadius.z;
|
float3 camPos = _WorldSpaceCameraPos.xyz;
|
||||||
float feather = max(_HeroCutoutPosRadius.w, 1e-4);
|
float3 axis = _HeroCutoutTarget.xyz - camPos;
|
||||||
float inside = 1.0 - smoothstep(radius - feather, radius, dist); // 1 inside, 0 outside
|
float len = length(axis);
|
||||||
|
if (len < 1e-4)
|
||||||
|
return;
|
||||||
|
float3 dir = axis / len;
|
||||||
|
|
||||||
// Only hole geometry between the camera and the hero; anything at/behind the player stays solid.
|
// Distance along the camera->target axis (metres from the camera).
|
||||||
float eyeDepth = LinearEyeDepth(positionCS.z, _ZBufferParams);
|
float along = dot(posWS - camPos, dir);
|
||||||
float inFront = step(eyeDepth, _HeroCutoutDepthOn.x);
|
float depthFeather = max(_HeroCutoutParams.z, 1e-4);
|
||||||
|
if (along <= 0.0 || along >= len + depthFeather) // behind the camera, or well behind the target -> solid
|
||||||
|
return;
|
||||||
|
|
||||||
float cut = inside * inFront;
|
// Perpendicular distance to the axis -> the cone test.
|
||||||
|
float3 closest = camPos + dir * along;
|
||||||
|
float radial = length(posWS - closest);
|
||||||
|
|
||||||
|
// Cone: radius scales with distance from the camera (apex), so the on-screen hole stays the same
|
||||||
|
// size at every occluder depth. t in (0,1]: 0 at the camera, 1 at the target plane.
|
||||||
|
float t = along / len;
|
||||||
|
float radius = _HeroCutoutParams.x * t;
|
||||||
|
float feather = max(_HeroCutoutParams.y * t, 1e-4);
|
||||||
|
float radialCut = 1.0 - smoothstep(radius - feather, radius, radial); // 1 inside the cone, 0 outside
|
||||||
|
|
||||||
|
// Soft depth cap instead of a hard plane at the target: the cut fades out across a band straddling
|
||||||
|
// the target - full strength up to (len - depthFeather), gone by (len + depthFeather). So an occluder
|
||||||
|
// crossing the player's depth keeps fading just behind them rather than showing a hard straight edge.
|
||||||
|
float axialCut = 1.0 - smoothstep(len - depthFeather, len + depthFeather, along);
|
||||||
|
|
||||||
|
float cut = radialCut * axialCut;
|
||||||
clip(HeroCutoutDither(positionCS.xy) - cut);
|
clip(HeroCutoutDither(positionCS.xy) - cut);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Reference in new issue
Block a user