Unity Shader Alpha Test 与 Alpha Blend:透明度测试与混合的实现及排序问题

在 Unity Universal Render Pipeline 中,透明渲染是开发中绕不开的话题。本文从底层原理出发,深入讲解 Alpha Test(硬边缘透明度测试)和 Alpha Blend(半透明度混合)的实现方式、Shader 编写,以及在实践中的排序问题与优化策略。

一、为什么需要透明渲染

在 3D 渲染中,不透明物体可以简单依赖 深度缓冲(Z-Buffer) 来正确遮挡------离摄像机近的像素覆盖远的像素。但现实世界中大量物体并非完全不透明:玻璃、水面、烟雾、树叶、UI 元素......它们要么有明确的切割边缘 (如树叶贴图中的镂空部分),要么呈现连续的半透明效果(如毛玻璃)。这就是透明渲染要解决的两类问题。

在 Unity URP 中,透明渲染的核心分为两种策略:

  • Alpha Test(透明度测试):通过阈值判断像素的"去留",产生硬边缘的镂空效果
  • Alpha Blend(透明度混合):将像素颜色与帧缓冲中已有颜色按比例混合,产生平滑的半透明效果

二、Alpha Test 透明度测试(硬边缘)

2.1 基本原理

Alpha Test 的核心逻辑非常简单:在片元着色器(Fragment Shader)中,取当前像素的 Alpha 值,与一个阈值(Cutoff)比较。如果 Alpha 值低于阈值,就**丢弃(discard)**该片元,不写入颜色缓冲和深度缓冲;否则正常写入。

if (alpha < cutoff) → discard(丢弃片元)

if (alpha ≥ cutoff) → 正常渲染(写入 Color + Depth)

关键特性: Alpha Test 的每个像素最终只有两种结果------完全显示或完全丢弃,不存在中间状态。因此它产生的是硬边缘效果,常见于树叶、栅栏、字符等贴图的镂空处理。

2.2 HLSL 实现

在 URP 中,Alpha Test 可以在片元着色器中使用 clip() 函数实现,也可以使用 ShaderLab 的 AlphaToMask On 指令(对应旧的 AlphaTest)。

以下是一个完整的 URP Alpha Test Shader 示例:

cs 复制代码
Shader "Custom/URP/AlphaTest"
{
    Properties
    {
        _BaseMap("Base Map", 2D)    = "white" {}
        _BaseColor("Base Color", Color) = (1,1,1,1)
        _Cutoff("Alpha Cutoff", Range(0,1)) = 0.5
    }
    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType"     = "TransparentCutout"
            "Queue"           = "AlphaTest"
        }
 
        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }
 
            // 开启 AlphaToMask(等效旧版 AlphaTest)
            AlphaToMask On
            Cull Back
            ZWrite On
            ZTest LEqual
 
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
 
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv        : TEXCOORD0;
            };
 
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv        : TEXCOORD0;
                half   fogFactor : TEXCOORD1;
            };
 
            TEXTURE2D(_BaseMap);    SAMPLER(sampler_BaseMap);
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                float4 _BaseColor;
                half   _Cutoff;
            CBUFFER_END
 
            Varyings vert(Attributes input)
            {
                Varyings output;
                VERTEX_SETUP(input);
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                output.fogFactor = ComputeFogFactor(output.positionCS.z);
                return output;
            }
 
            half4 frag(Varyings input) : SV_Target
            {
                half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
 
                // 核心:Alpha Test
                // clip() 在参数 < 0 时丢弃当前片元
                clip(baseColor.a - _Cutoff);
 
                baseColor.rgb = MixFog(baseColor.rgb, input.fogFactor);
                return baseColor;
            }
            ENDHLSL
        }
    }
}

代码要点: 第 24 行的 AlphaToMask On 告诉 GPU 在片元着色器之后执行 Alpha 测试;第 71 行的 clip(baseColor.a - _Cutoff) 是 HLSL 中实现 Alpha Test 的标准写法。当 baseColor.a - _Cutoff < 0 时,片元被丢弃。

2.3 AlphaToMask 与 Multi-Sample Anti-Aliasing

当启用 AlphaToMask On 时,GPU 不会简单地使用 clip(),而是利用 MSAA(多重采样抗锯齿) 的覆盖率(Coverage)机制。在 MSAA 启用的条件下,每个像素的多个采样点会根据 Alpha 值独立决定是否通过测试,从而在边缘处产生近似抗锯齿的效果。这是一种"免费的"边缘柔化方案。

三、Alpha Blend 透明度混合(半透明)

3.1 基本原理

Alpha Blend 不丢弃任何片元,而是将片元的颜色与帧缓冲中已有的颜色进行加权混合。混合公式为:

Cresult = Csrc × αsrc + Cdst × (1 − αsrc)

其中 Csrc 是当前片元颜色,αsrc 是片元的 Alpha 值,Cdst 是帧缓冲中已有的颜色。混合的最终结果取决于片元渲染的先后顺序------这是 Alpha Blend 产生排序问题的根源。

3.2 ShaderLab 混合指令

Unity ShaderLab 中通过 Blend 指令配置混合参数:

cs 复制代码
// 标准 Alpha 混合(最常用)
Blend SrcAlpha OneMinusSrcAlpha
 
// 预乘 Alpha 混合(性能更好)
Blend One OneMinusSrcAlpha
 
// 加法混合(常用于粒子、发光效果)
Blend SrcAlpha One
 
// 完整语法:Blend SrcFactor DstFactor, SrcFactorA DstFactorA
// 前两个参数控制 RGB 混合,后两个控制 Alpha 混合

3.3 HLSL 实现

以下是 URP 中的完整 Alpha Blend Shader:

cs 复制代码
Shader "Custom/URP/AlphaBlend"
{
    Properties
    {
        _BaseMap("Base Map", 2D)    = "white" {}
        _BaseColor("Base Color", Color) = (1,1,1,0.5)
    }
    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType"     = "Transparent"
            "Queue"           = "Transparent"
        }
 
        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }
 
            // ★ 关键区别:AlphaBlend 需要 ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
            Cull Back
            ZTest LEqual
 
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
 
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv        : TEXCOORD0;
            };
 
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv        : TEXCOORD0;
                half   fogFactor : TEXCOORD1;
            };
 
            TEXTURE2D(_BaseMap);    SAMPLER(sampler_BaseMap);
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                float4 _BaseColor;
            CBUFFER_END
 
            Varyings vert(Attributes input)
            {
                Varyings output;
                VERTEX_SETUP(input);
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                output.fogFactor = ComputeFogFactor(output.positionCS.z);
                return output;
            }
 
            half4 frag(Varyings input) : SV_Target
            {
                half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv)
                                 * _BaseColor;
 
                // 注意:不需要 clip(),让所有片元都参与混合
                // 返回值中的 Alpha 将被 Blend 指令使用
 
                baseColor.rgb = MixFog(baseColor.rgb, input.fogFactor);
                return baseColor;
            }
            ENDHLSL
        }
    }
}

⚠️ 核心区别: Alpha Blend 必须设置 ZWrite Off(第 24 行)。因为如果透明物体写入深度,后面的物体(无论透明或不透明)都会被错误地遮挡。同时,片元着色器中不使用 clip()------所有片元都参与混合。

四、Alpha Test vs Alpha Blend 对比

特性 Alpha Test(透明度测试) Alpha Blend(透明度混合)
ShaderLab 指令 AlphaToMask On Blend SrcAlpha OneMinusSrcAlpha
RenderType TransparentCutout Transparent
Render Queue AlphaTest(2450) Transparent(3000)
深度写入 ZWrite On ZWrite Off
HLSL 关键函数 clip(alpha - cutoff) 无,直接输出颜色 + Alpha
视觉效果 硬边缘,像素级取舍 平滑半透明,连续过渡
排序依赖 不依赖排序(深度缓冲足够) 严格依赖绘制顺序
典型用途 树叶、栅栏、字符镂空 玻璃、水、烟雾、UI
性能 较高(Early-Z 仍有效) 较低(需要排序 + Overdraw)

五、透明渲染的排序问题

5.1 为什么需要排序

前面提到,Alpha Blend 的混合公式具有不可交换性

(A 混合 B) ≠ (B 混合 A)

例:红色(α=0.5) 混合 蓝色(α=0.5)

正序:C = 0.5×红 + 0.5×蓝 → 偏紫

反序:C = 0.5×蓝 + 0.5×红 → 同样偏紫(此例相同)

但当 α 不同时:

红(α=0.3)→蓝(α=0.8) ≠ 蓝(α=0.8)→红(α=0.3)

因此,要获得正确的透明混合结果,必须按照从远到近(Back-to-Front) 的顺序渲染半透明物体。这个约束在只有一个半透明物体时不构成问题,但当多个半透明物体相互交叉时,就变得非常棘手。

5.2 排序的三种级别

  1. 物体级排序(Object Sorting):按物体包围盒中心到摄像机的距离排序。简单但粗糙,无法处理交叉物体。
  2. 面级排序(Triangle Sorting):按每个三角面到摄像机的距离排序。更精确但开销更大。
  3. 像素级排序(Per-Pixel Sorting):逐像素排序所有重叠的片元。最精确但性能开销最大(OIT 技术)。

5.3 物体交叉问题

核心矛盾: 基于深度值的排序(无论物体级还是面级)只能处理不交叉的情况。当两个半透明物体的几何体相互穿插时,不存在一个正确的整体排序------A 的某些部分在 B 前面,某些在 B 后面。这就是经典透明排序问题的本质。

5.4 ZWrite Off 带来的连锁问题

Alpha Blend 关闭深度写入后,还会引发一系列衍生问题:

  • 不透明物体错误遮挡透明物体 :如果渲染顺序不当,后渲染的不透明物体可能覆盖已渲染的透明物体。URP 通过 Render Queue 机制(不透明 Geometry = 2000,透明 Transparent = 3000)来保证不透明物体先渲染。
  • 透明物体之间的深度测试失效:由于不写入深度,两个透明物体之间的遮挡关系完全依赖排序,深度测试(ZTest)只能用于裁剪在完全不透明物体背后的透明片元。
  • 后处理效果异常:某些后处理效果(如 SSAO、深度重建)依赖深度缓冲的完整性,ZWrite Off 会导致透明物体在这些效果中"消失"。

六、URP 中的透明渲染管线

Unity URP 通过 Render Queue 值来控制渲染顺序,确保不透明物体在透明物体之前渲染:

cs 复制代码
// URP 默认 Render Queue 值
 
"Background"        → Queue 1000    // 天空盒、背景
"Geometry"          → Queue 2000    // 不透明物体(默认)
"AlphaTest"          → Queue 2450    // 透明度测试(在不透明之后)
"Transparent"        → Queue 3000    // 半透明物体
"Overlay"            → Queue 4000    // UI、镜头光晕等覆盖层
 
// URP Forward Renderer 渲染流程:
// 1. Setup → 2. Main Light Shadow → 3. Additional Shadows
// 4. Depth Prepass → 5. Opaque Objects → 6. Skybox
// 7. Transparent Objects(自动按距离排序)→ 8. Post Processing

URP 的透明排序: 在 URP 的 Forward Rendering 路径中,透明物体(Queue ≥ 3000)会按照物体中心点到摄像机的距离自动进行 Back-to-Front 排序。这是物体级排序,对于大多数简单场景已经足够,但无法处理物体交叉的情况。

七、排序问题的解决方案

7.1 常规优化策略

  1. 避免不必要的透明:能用 Alpha Test 解决的场景(如树叶镂空),不要使用 Alpha Blend。Alpha Test 写入深度,不依赖排序。
  2. 拆分交叉物体:将交叉的半透明物体拆分为不交叉的子网格,减少排序冲突。
  3. 使用双 Pass 渲染 :第一 Pass 用 Cull Front 渲染背面,第二 Pass 用 Cull Back 渲染正面,保证每个物体内部的面排序正确。
  4. 手动设置 Render Queue :通过 "Queue" = "Transparent+100" 手动调整透明物体的渲染顺序。
  5. 启用 Depth Prepass:在 URP 中启用 Depth Prepass 可以提前构建深度缓冲,帮助裁剪被遮挡的透明片元。

7.2 双 Pass 透明渲染示例

对于凸面半透明物体(如玻璃球),使用双 Pass 可以完美解决物体自身的排序问题:

cs 复制代码
   // Pass 1: 渲染背面(先渲染远处的面)
        Pass
        {
            Name "Back"
            Cull Front    // 剔除正面,只渲染背面
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            // ... HLSL 省略(同上)
        }
 
        // Pass 2: 渲染正面(后渲染近处的面)
        Pass
        {
            Name "Front"
            Cull Back     // 剔除背面,只渲染正面
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            // ... HLSL 省略(同上)
        }

7.3 OIT:排序无关透明

对于复杂的透明场景,传统的排序方法可能完全不够用。这时可以考虑 **OIT(Order-Independent Transparency,排序无关透明)**技术。OIT 的核心思想是:不依赖绘制顺序,在像素级别正确地混合所有透明片元。

各方案的优缺点:

技术 精度 性能开销 实现难度 适用场景
Depth Peeling 精确 高(N 层 = N 个 Pass) 透明层数少的场景
Weighted Blended 近似 低(单 Pass) 粒子、体积雾等大量半透明
Per-Pixel Linked List 精确 中~高(显存占用大) 复杂透明场景

八、实战建议与性能优化

8.1 选择正确的透明策略

  1. 能用不透明就用不透明:不透明物体性能最好,有完整的深度缓冲和 Early-Z 优化。
  2. 需要镂空 → Alpha Test :树叶、栅栏、字符等场景优先使用 Alpha Test + clip()。它写入深度,不依赖排序。
  3. 需要半透明 → Alpha Blend:玻璃、水、烟雾等必须使用 Alpha Blend,但要注意排序问题。
  4. 两者结合 :有些材质既有镂空又有半透明(如带镂空的半透明旗帜),可以用 Alpha Test 处理镂空,Alpha Blend 处理半透明区域。在片元着色器中先 clip() 再输出混合颜色。

8.2 性能注意事项

  • Alpha Test 的 Early-Z 破坏 :虽然 Alpha Test 写入深度,但 clip() 在片元着色器中执行,可能导致部分 GPU 的 Early-Z 优化失效。现代 GPU(NVIDIA Turing+、AMD RDNA)有 Early DepthStencil TestConservative Depth 等机制来缓解。
  • Alpha Blend 的 Overdraw:透明物体不会因深度测试而跳过片元,导致大量 Overdraw。控制屏幕上透明物体的覆盖面积是关键。
  • 排序开销:URP 每帧对透明物体进行 CPU 端排序(Quick Sort),透明物体数量过多时排序本身成为瓶颈。建议控制透明物体总数。
  • 预乘 Alpha(Pre-multiplied Alpha) :使用 Blend One OneMinusSrcAlpha 可以在某些情况下避免渲染顺序问题,是移动端推荐的半透明方案。

8.3 URP 调试透明问题

URP 提供了多种调试透明渲染问题的方法:

cs 复制代码
// 1. Frame Debugger(Window → Analysis → Frame Debugger)
//    可以逐步查看每个 Draw Call 的渲染结果和排序顺序
 
// 2. Wireframe 模式查看透明物体的面片排序
//    Scene 视图左上角 → Wireframe
 
// 3. 在 Shader 中临时输出深度值来验证深度缓冲
half4 frag(Varyings input) : SV_Target
{
    // 调试:可视化深度值
    float depth = input.positionCS.z;
    return half4(depth, depth, depth, 1.0);
}
 
// 4. Render Doc 捕获帧,逐像素查看深度和模板值

九、总结

Alpha TestAlpha Blend 是图形渲染中最基础也最重要的两种透明处理方式。理解它们的底层原理、正确配置 ShaderLab 参数,以及清醒地认识排序问题的本质,是每个图形程序员必备的能力。

在实际项目中,没有"银弹"------每种技术都有其适用范围和局限性。关键是根据场景需求做出正确的选择,并通过合理的排序策略和优化手段来弥补不足。在 URP 中,合理利用 Render Queue、双 Pass 渲染、以及 Frame Debugger 调试工具,可以解决绝大多数透明渲染问题。

相关推荐
小贺儿开发2 小时前
Unity3D 拼图互动游戏
游戏·unity·人机交互·2d·拼图·互动
mxwin3 小时前
Unity Mask 贴图:用一张纹理的 RGBA 通道分别控制 PBR 材质参数
unity·材质·贴图
FairGuard手游加固4 小时前
FairGuard支持HybridCLR热更DLL加密
游戏·unity·游戏引擎
海海不瞌睡(捏捏王子)4 小时前
Unity GUI优化
unity·游戏引擎
心前阳光6 小时前
Unity之Luban表格配置
unity
mascon7 小时前
unity mcp 使用
unity·游戏引擎
心前阳光8 小时前
Unity之语音提问,语音答复
unity·游戏引擎
mxwin9 小时前
Unity Shader UV 坐标与纹理平铺Tiling & Offset 深度解析
unity·游戏引擎·shader·uv
chao18984410 小时前
基于STM32F1的声源定位系统设计与实现
stm32·嵌入式硬件·unity