using UnityEngine;
using UnityEngine.Rendering;
///
/// Turns a flat textured card (Quad MeshRenderer) into a zero-thickness
/// "paper" cutout that casts and receives shadows. The colour pass is alpha
/// blended so anti-aliased sprite edges stay soft; the shadow caster pass uses
/// alpha clip so the shadow follows the silhouette. Works with URP/Lit and
/// the built-in Standard shader. Pure renderer setup; no gameplay logic.
///
[ExecuteAlways]
[DisallowMultipleComponent]
public class PaperCardShadows : MonoBehaviour
{
[Tooltip("Alpha below this is clipped in the shadow pass and discarded in the colour pass.")]
[Range(0f, 1f)] public float alphaClipThreshold = 0.15f;
[Tooltip("Reapply material settings on Awake/OnValidate. Turn off to manage the material by hand.")]
public bool enforceMaterialSettings = true;
Renderer rend;
void Awake() { Apply(); }
void OnValidate() { Apply(); }
void Reset() { Apply(); }
public void Apply()
{
if (rend == null) rend = GetComponent();
if (rend == null) return;
rend.shadowCastingMode = ShadowCastingMode.TwoSided;
rend.receiveShadows = true;
if (!enforceMaterialSettings) return;
var mat = Application.isPlaying ? rend.material : rend.sharedMaterial;
if (mat == null) return;
ConfigureSoftCutout(mat, alphaClipThreshold);
}
/// Alpha-blended colour with alpha-clipped shadows: soft edges, crisp silhouette shadow.
public static void ConfigureSoftCutout(Material mat, float cutoff)
{
bool urp = mat.HasProperty("_BaseMap");
if (urp)
{
// Opaque + alpha clip + alpha-to-coverage: MSAA turns the sprite's soft alpha
// into anti-aliased coverage with correct depth, so silhouettes never show
// the blend/depth hairline that transparent rendering leaves at the edge.
mat.SetFloat("_Surface", 0f); // Opaque
mat.SetFloat("_Blend", 0f);
mat.SetFloat("_SrcBlend", (float)BlendMode.One);
mat.SetFloat("_DstBlend", (float)BlendMode.Zero);
mat.SetFloat("_SrcBlendAlpha", (float)BlendMode.One);
mat.SetFloat("_DstBlendAlpha", (float)BlendMode.Zero);
mat.SetFloat("_ZWrite", 1f);
mat.SetFloat("_AlphaClip", 1f);
mat.SetFloat("_Cutoff", cutoff);
mat.SetFloat("_AlphaToMask", 1f);
mat.SetFloat("_Cull", (float)CullMode.Off); // draw both faces so a clipped rim can never look hollow
mat.SetFloat("_ReceiveShadows", 1f);
mat.DisableKeyword("_SURFACE_TYPE_TRANSPARENT");
mat.EnableKeyword("_ALPHATEST_ON");
mat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
mat.DisableKeyword("_ALPHAMODULATE_ON");
mat.SetOverrideTag("RenderType", "TransparentCutout");
mat.renderQueue = (int)RenderQueue.AlphaTest;
}
else
{
// Built-in Standard: Fade mode
mat.SetFloat("_Mode", 2f);
mat.SetFloat("_Cutoff", cutoff);
mat.SetInt("_SrcBlend", (int)BlendMode.SrcAlpha);
mat.SetInt("_DstBlend", (int)BlendMode.OneMinusSrcAlpha);
mat.SetInt("_ZWrite", 1);
mat.EnableKeyword("_ALPHABLEND_ON");
mat.EnableKeyword("_ALPHATEST_ON");
mat.SetOverrideTag("RenderType", "Transparent");
mat.renderQueue = (int)RenderQueue.Transparent;
}
}
}