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

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

相关推荐
魔士于安3 天前
unity 丑陋哥布林 哥布林有humannoid 动画没 带动画
unity·游戏引擎·材质·贴图·模型
小贺儿开发4 天前
Unity 家居视频遥控(细节优化)
科技·unity·程序员·udp·视频·工具·通信
淡海水4 天前
13-04-面试-源码级深度追问链
数据结构·unity·面试·c#·游戏引擎·源码·il2cpp
小小数媒成员4 天前
UGUI 性能优化(详细,全,深入)
开发语言·unity·游戏引擎
林川~015 天前
Unity新输入系统使用笔记
unity·input system
小贺儿开发5 天前
Unity 结合百度AI开放平台 手写文字识别
人工智能·科技·学习·unity·云服务·文字识别·手写文字
goujunwe5 天前
GEO 评估方法:如何自测你的内容,会不会被 AI 引用
搜索引擎·网络安全·游戏引擎·全文检索·中文分词
林川~016 天前
Unity 万能物理检测工具:射线检测 / 范围检测 / 层级过滤 / 编辑器可视化(可直接拿去用)
游戏·unity·c#·射线检测·通用工具
yi碗汤园6 天前
MQTT消息队列遥测传输协议
网络·网络协议·unity
林川~016 天前
Unity 常用 API 与核心类全解:从生命周期到协程,附性能避坑清单(Unity 6 适配)
unity·游戏引擎·常用api