Unity URP 半透明阴影的局限性

深入解析 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. 在远处使用更低的抖动
相关推荐
空中海2 小时前
第四篇:Unity高级阶段(架构级开发能力)
unity·架构·游戏引擎
小贺儿开发3 小时前
【MediaPipe】Unity3D 虚拟面具互动演示
unity·人机交互·shader·摄像头·面具·互动·脸部捕捉
DaLiangChen4 小时前
Unity URP 绘制参考网格 Shader 教程(抗锯齿 + 渐变淡出)
unity·游戏引擎
空中海5 小时前
第三篇:Unity进阶阶段(商业项目能力)
unity·游戏引擎
Yuk丶9 小时前
Procedural Dialogue Engine - UE4程序化对话系统的技术实现
c++·游戏引擎·ue4·游戏程序·虚幻
RReality10 小时前
【Unity Shader URP】屏幕空间扭曲后处理(Screen Space Distortion)实战教程
ui·unity·游戏引擎·图形渲染·材质
zcc85807976212 小时前
Unity 事件驱动架构
unity
心之所向,自强不息12 小时前
VSCode + EmmyLua 调试 Unity Lua(最简接入 + 不阻塞运行版)
vscode·unity·lua
空中海13 小时前
第六篇:Unity专项方向
unity·游戏引擎