深入解析 URP 原生不支持半透明物体投射阴影的原因,以及 Alpha Test 与 Dither 两种常用 workaround 的实现原理与实践
一、问题引入:为什么半透明物体没有阴影?
在现实世界中,玻璃、水晶、植物叶片等半透明物体都会投射阴影------即使光线能部分穿透,它们的轮廓依然会在地面上形成深浅不一的暗区。然而在 Unity URP 中,如果你直接将材质的 Rendering Mode 设为 Transparent,会发现这些物体在默认情况下不会投射任何阴影到其他物体上。

这个现象并非 URP 的 bug,而是由 Shadow Mapping 技术的基本原理所决定的。要理解这一点,我们需要深入 URP 的阴影渲染管线。
二、URP 阴影渲染管线解析
URP 使用Shadow Mapping(阴影贴图)技术来计算阴影。整个过程分为两个关键阶段:

2.1 Shadow Caster Pass(阴影投射阶段)
在这个阶段,URP 从光源视角 渲染场景的深度信息。所有参与阴影计算的物体都需要将自己的深度写入阴影贴图(Shadow Map)。

2.2 核心问题:ZWrite 与半透明
URP 的 Shadow Caster Pass 有一个硬性要求 :物体必须开启 ZWrite 才能写入深度贴图。这与渲染排序有关:
| 渲染模式 | ZWrite | 深度测试 | 能否投射阴影 | 典型用途 |
|---|---|---|---|---|
| Opaque | On | Less / LEqual | ✓ 支持 | 实体材质 |
| Transparent | Off | Always | ✗ 不支持 | 玻璃、水体 |
| Alpha Test | On | Less / LEqual | ✓ 支持 | 树叶、栏杆 |
| Transparent (Cutout) | On | Less / LEqual | ✓ 支持 | 镂空材质 |
⚠️ 为什么透明材质要关闭 ZWrite?
透明物体需要混合背景颜色来实现透明效果。如果开启 ZWrite,后续的透明物体就无法正确与之前的像素混合,会产生错误的遮挡关系。因此 Transparent 模式下 ZWrite 必须关闭,导致阴影投射能力丢失。
三、Workaround 1:Alpha Test + Alpha-to-Coverage
3.1 原理概述
Alpha Test (也称 Cutout)是一种"硬透明"技术:像素着色器根据 alpha 值决定是完全丢弃 还是完全保留 。由于保留的像素具有完整的不透明度,可以正常写入深度,从而投射阴影。
Alpha-to-Coverage (A2C) 是 A2C 是一种 MSAA 多重采样抗锯齿技术。在 MSAA 开启时,Alpha Test 的边缘像素会被转换为采样覆盖率信息,实现比普通 Alpha Test 更平滑的边缘效果。

3.2 Shader Graph 实现
在 Shader Graph 中,设置 Rendering Mode 为 Transparent (Cutout) 或 All,然后添加 Alpha Test 逻辑:
cs
// Shader Graph Property Settings
"m_RenderingMode": "Transparent",
"m_TransparentCullMode": "Back",
"m_TransparentZWrite": true, // 关键:开启透明物体的 ZWrite
"m_AlphaCutoff": 0.5, // Alpha 阈值
"m_AlphaCutoffEnable": true, // 启用 Alpha Test
// 同时需要在 Shader Graph 中添加以下节点逻辑:
// Sample Texture 2D → Split (获取 Alpha) → Threshold (Cutoff)
// Alpha → Master Node
3.3 实际应用:树叶阴影
这是 Alpha Test 最经典的应用场景------树叶需要在地面投射剪影阴影:
cs
Shader "Custom/LeafShadow"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Range(0,1)) = 0.5
}
SubShader
{
// Tags 用于支持透明渲染和阴影投射
Tags {
"RenderType"="TransparentCutout"
"Queue"="AlphaTest"
"IgnoreProjector"="True"
}
// ========================================
// Shadow Caster Pass - 投射阴影
// ========================================
Pass
{
Name "ShadowCaster"
Tags { "LightMode"="ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#pragma multi_compile _ _ALPHATEST_ON
// 启用 Alpha-to-Coverage (需要 MSAA)
#pragma multi_compile _ _ENABLE_FAST_VARIANT
sampler2D _MainTex;
float4 _MainTex_ST;
half _Cutoff;
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata v) {
v2f o;
o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
o.pos = UnityApplyLinearShadowBias(o.pos);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
half4 frag(v2f i) : SV_Target {
half4 col = tex2D(_MainTex, i.uv);
// 关键:Alpha Test - 丢弃透明像素
// 这样可以写入深度,投射阴影
#if _ALPHATEST_ON
clip(col.a - _Cutoff);
#endif
SHADOW_CASTER_FRAGMENT(i);
}
ENDCG
}
// ========================================
// Forward Pass - 正常渲染
// ========================================
Pass
{
Tags { "LightMode"="ForwardBase" }
// ... Forward 渲染代码
}
}
}
💡 注意事项
使用 Alpha-to-Coverage 需要在 Quality Settings 中开启 MSAA (至少 2x)。同时该技术仅在延迟渲染路径下效果最佳,前向渲染可能有差异。
四、Workaround 2:Dither 透明模拟
4.1 原理概述
**Dither(抖动)**是一种利用人眼视觉特性模拟半透明的技术。通过在像素级别以特定图案交错显示"有"和"无",人眼会将其感知为中间色调。
在阴影场景中,Dither 的核心思想是:即使材质本身是半透明的,我们也可以在 Shadow Caster Pass 中注入一个规则抖动图案,让阴影贴图记录"部分穿透"的效果。

4.2 URP 中的 Dither 实现
URP 提供了内置的 Dither 函数和抖动纹理,配合自定义 Shadow Caster Pass 可以实现此效果:
cs
// ========================================
// Dither 透明阴影着色器
// ========================================
// 4x4 Bayer 抖动矩阵 - 用于决定哪些像素"投射阴影"
static const half4 _BayerMatrix = half4(0.0, 0.5, 0.125, 0.625);
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 screenPos : TEXCOORD0; // 屏幕空间位置用于 Dither
float3 normal : TEXCOORD1;
};
// 获取屏幕空间的抖动值
float GetDitherValue(float4 screenPos) {
// 计算屏幕像素坐标
float2 screenUV = screenPos.xy / screenPos.w;
screenUV *= float2(_ScreenParams.x, _ScreenParams.y);
// Bayer 4x4 抖动
int x = (int)screenUV.x % 4;
int y = (int)screenUV.y % 4;
// 查找抖动阈值
float dither[16] = {
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5
};
return dither[y * 4 + x] / 16.0;
}
// Shadow Caster Pass
v2f vert(appdata v) {
v2f o;
o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
o.screenPos = ComputeScreenPos(o.pos); // 传递屏幕位置
o.normal = UnityObjectToWorldNormal(v.normal);
return o;
}
half4 frag(v2f i, uniform half _Alpha, uniform half _ShadowStrength) : SV_Target {
float dither = GetDitherValue(i.screenPos);
// 根据透明度比例决定哪些像素投射阴影
// alpha=0.8 表示 80% 的像素投射阴影,20% 穿透
half effectiveAlpha = _Alpha * _ShadowStrength;
// 抖动比较:只有当 dither 值小于有效透明度时才投射阴影
if (dither > effectiveAlpha) {
// 丢弃像素 - 光线穿透,不投射阴影
discard;
}
// 投射阴影
SHADOW_CASTER_FRAGMENT(i);
}
4.3 URP 内置 Dither 函数
URP 提供了一个更简洁的内置函数 UnityDither,可以直接使用:
cs
// URP 内置的抖动函数
// 位置: com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl
// 使用方法:
void UnityDither(float3 vClipPos, half fAlpha) {
// fAlpha: 透明度值 (0-1)
half threshold = UNITY_DITHER(vClipPos);
if (fAlpha < threshold) {
discard;
}
}
// 在 Shadow Caster Pass 中使用示例:
half4 frag(v2f i) : SV_Target {
half4 col = tex2D(_MainTex, i.uv);
// 在阴影 Pass 中应用抖动
// 这样阴影会呈现半透明效果
half3 clipPos = i.pos.xyz / i.pos.w;
UnityDither(clipPos, col.a);
SHADOW_CASTER_FRAGMENT(i);
}
五、两种方案对比与选择
| 特性 | Alpha Test + A2C | Dither 透明 |
|---|---|---|
| 阴影质量 | 硬边缘 + MSAA 抗锯齿 | 模拟半透明,图案可见 |
| 视觉效果 | 边缘可能有锯齿(无 A2C) | 更柔和的阴影渐变 |
| 性能开销 | 低(标准深度写入) | 中等(额外抖动计算) |
| 适用场景 | 树叶、栏杆、镂空物体 | 玻璃、毛玻璃、烟雾 |
| MSAA 要求 | 需要(开启 A2C) | 可选 |
| 实现复杂度 | 低(Shader Graph 即可) | 中(需自定义 Pass) |
✓ 选型建议
- 如果目标是 树叶、布料栏杆 等有明显轮廓的物体 → 选择 Alpha Test + A2C
- 如果目标是 玻璃、水晶、烟雾 等需要柔和半透明阴影 → 选择 Dither
- 如果项目不支持 MSAA → 只能使用 Dither
六、实践注意事项
6.1 性能优化建议
- 避免过度使用:半透明阴影计算成本高于普通阴影,只在必要场景使用
- 考虑阴影级联:对于远距离物体,可以降低半透明阴影的精度或完全关闭
- 使用级联阴影贴图:避免半透明物体投射到近处精细阴影贴图上
6.2 常见问题排查
cs
// 问题:半透明物体不投射阴影
// 检查清单:
□ Rendering Mode 是否设置为 Transparent (Cutout)?
□ 是否添加了自定义 ShadowCaster Pass?
□ Surface Type 是否为 Opaque? (Transparent 会跳过阴影)
□ ZWrite 是否开启?
// 问题:阴影边缘有锯齿
// 解决方案:
1. 开启 MSAA (Quality Settings → Anti Aliasing → 4x)
2. 启用 Alpha-to-Coverage
3. 适当提高阴影贴图分辨率
// 问题:Dither 图案太明显
// 解决方案:
1. 使用更高阶的抖动矩阵 (8x8, 16x16)
2. 结合噪声纹理增加随机性
3. 在远处使用更低的抖动