#ifndef HERO_CUTOUT_INCLUDED #define HERO_CUTOUT_INCLUDED // World-space "hero cutout": clips occluder fragments that lie inside a CONE running from the camera // (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. // // That far split is FEATHERED on BOTH sides, never a hard plane: the cut fades out smoothly across a // band of +/- 'depthFeather' metres AROUND the target. So an occluder that crosses the player's depth // (a shelf board or wall running away from the camera) keeps fading just behind the player instead of // snapping to solid - no hard straight cut line where a depth plane would slice the surface. Only // geometry beyond that band (more than depthFeather behind the target) stays fully solid. // // Why a cone and not a cylinder: the cone's radius grows linearly with distance from the camera // (radius = paramRadius * along/len), so its *screen-space* footprint is constant regardless of how // 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 cone wall a soft edge with no extra // textures or includes. float HeroCutoutDither(float2 positionSS) { return frac(52.9829189 * frac(dot(positionSS, float2(0.06711056, 0.00583715)))); } void ApplyHeroCutout(float4 positionCS) { if (_HeroCutoutTarget.w < 0.5) return; // Reconstruct this fragment's world position from screen UV + device depth. float2 screenUV = GetNormalizedScreenSpaceUV(positionCS); float3 posWS = ComputeWorldSpacePosition(screenUV, positionCS.z, UNITY_MATRIX_I_VP); float3 camPos = _WorldSpaceCameraPos.xyz; float3 axis = _HeroCutoutTarget.xyz - camPos; float len = length(axis); if (len < 1e-4) return; float3 dir = axis / len; // Distance along the camera->target axis (metres from the camera). float along = dot(posWS - camPos, dir); float depthFeather = max(_HeroCutoutParams.z, 1e-4); if (along <= 0.0 || along >= len + depthFeather) // behind the camera, or well behind the target -> solid return; // 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); } #endif