渲染管线中三道"关卡"的工作原理、调用顺序,以及在实际项目中的典型应用案例。

Pipeline Overview
渲染管线中的位置
三项测试都发生在片元着色器之后、帧缓冲写入之前,它们共同决定一个片元是否最终可见。

💡标准顺序:Alpha测试 → 模板测试 → 深度测试。早期剔除越多片元,GPU 后续开销越低。在现代渲染管线(OpenGL/Vulkan/Metal)中顺序略有差异,但逻辑语义保持一致。
🔴
Alpha 测试(Alpha Test)
基于片元透明度值决定是否丢弃该片元 · 顺序 ①
Alpha 测试对每个片元的 alpha 分量 与一个参考值进行比较,若不满足条件则直接 discard 丢弃,完全不写入任何缓冲区。与 Alpha 混合不同,Alpha 测试是一种硬性裁决------没有半透明,只有"通过"与"丢弃"。
典型场景:树叶、铁丝网、镂空纹理------纹理中有些区域根本不应该存在,用 Alpha 测试直接裁掉,比 Alpha 混合更高效,且不受绘制顺序影响。

案例
树叶 / 镂空遮罩纹理
树叶纹理中大量像素 alpha = 0(透明区域),使用 Alpha 测试直接丢弃,无需排序,写入深度缓冲正确。
cs
// ① Alpha 测试:丢弃 alpha 低于阈值的片元
// 常用于树叶、铁网、镂空纹理等硬边透明效果
uniform sampler2D _MainTex;
uniform float _Cutoff; // 裁剪阈值,例如 0.5
void main() {
vec4 col = texture2D(_MainTex, vTexCoord);
// Alpha Test:低于阈值 → 丢弃片元
if (col.a < _Cutoff) discard;
// 通过则正常输出颜色
gl_FragColor = col;
}
🟢
模板测试(Stencil Test)
基于模板缓冲区的值决定片元命运 · 顺序 ②
模板缓冲区(Stencil Buffer)是一张与帧缓冲等大的整型图像,每个像素存储一个整数值(通常 8-bit,范围 0~255)。模板测试将片元对应位置的缓冲值与参考值进行比较,决定是否通过;同时可根据测试结果修改缓冲区的值,从而影响后续渲染。
常见操作:KEEP(保持)、REPLACE(替换)、INCR(加 1)、DECR(减 1)、INVERT(按位取反)......

案例
描边效果(Outline) / 后处理遮罩
游戏中鼠标悬停时物体高亮描边:先将物体写入模板缓冲=1,再用稍大的模型在模板=0区域渲染描边色。
cs
// ShaderLab 示例:两 Pass 实现描边
// ━━ Pass 1:渲染物体本体,同时写模板值=1 ━━
Pass {
Stencil {
Ref 1
Comp Always // 永远通过模板测试
Pass Replace // 通过后写入 Ref=1
}
// 正常渲染物体颜色...
}
// ━━ Pass 2:描边,只在模板值≠1 的像素着色 ━━
Pass {
Stencil {
Ref 1
Comp NotEqual // 模板值≠1 才通过
Pass Keep
}
ZWrite Off
// 稍微放大顶点,输出描边颜色...
}
🔵
深度测试(Depth Test)
基于深度缓冲区决定前后遮挡关系 · 顺序 ③
深度缓冲区(Depth Buffer / Z-Buffer)存储每个像素当前最近片元的深度值(通常归一化到 [0,1])。深度测试将新片元的深度值与缓冲区内已有值对比,若新片元更靠近相机 (深度值更小,默认 LESS 函数)则通过并更新缓冲区,否则丢弃------这就是最基础的遮挡消除。

案例
ZWrite Off + ZTest Always --- UI 永远在最前
UI 元素通常设置为不写深度、不受深度遮挡,始终渲染在场景之上;另一个典型是水面渲染需要自定义深度比较规则。
cs
// ShaderLab (Unity URP) 深度测试配置示例
// ── 案例 A:UI 永远置顶(不读深度,不写深度) ──
Pass {
ZWrite Off // 不写入深度缓冲
ZTest Always // 不进行深度比较,永远通过
// UI 着色器...
}
// ── 案例 B:标准不透明物体 ──
Pass {
ZWrite On // 写入深度
ZTest LEqual // 深度 ≤ 缓冲区值时通过(默认)
// 标准着色器...
}
// ── 案例 C:半透明物体(排序绘制,不写深度) ──
Pass {
ZWrite Off // 半透明不写深度,避免遮挡后面物体
ZTest LEqual // 仍然读深度,被不透明物体遮挡
Blend SrcAlpha OneMinusSrcAlpha
}
Execution Order
三者调用顺序详解
每个片元在写入帧缓冲前都要经历以下关卡,任意一关失败即被丢弃,不再进入后续步骤。
0
片元着色器输出
片元着色器计算完颜色和 alpha 值,准备进入测试流程。此时片元尚未写入任何缓冲区。
1
① Alpha 测试
比较片元 alpha 与参考值,不满足条件立即 discard。成本最低,丢弃最早,优先执行可节省后续模板/深度读写开销。
⚡ 在移动端 GPU 上,Alpha Test 可能破坏 Early-Z 优化(HSR),需权衡使用。
2
② 模板测试
读取模板缓冲区对应值,与参考值比较。通过则可选择更新缓冲区(KEEP / REPLACE / INCR ...),不通过则丢弃片元。
📌 模板缓冲区更新分三种情况:sfail(模板失败)、dpfail(深度失败)、dppass(深度通过)。
3
③ 深度测试
与深度缓冲区当前值比较,通过则写入新深度(如 ZWrite On),不通过则丢弃。深度测试是遮挡消除的核心机制。
🔍 Early-Z 是深度测试的提前版本,在光栅化阶段就做预剔除,但仅在着色器不含 discard / 修改深度时生效。
4
④ 混合 & 写入帧缓冲
三项测试全部通过后,片元进入混合阶段,按混合方程计算最终颜色,写入颜色缓冲区。
| 特性 | Alpha 测试 | 模板测试 | 深度测试 |
|---|---|---|---|
| 比较基准 | 片元 alpha 值 | 模板缓冲区整数 | 深度缓冲区浮点 |
| 缓冲区 | 无(直接丢弃) | Stencil Buffer(8-bit) | Depth Buffer(16/24/32-bit) |
| 主要用途 | 硬边镂空、透明裁剪 | 遮罩、描边、portal、反射裁剪 | 遮挡消除、前后关系 |
| 性能影响 | 可能破坏 Early-Z | 需读写额外缓冲区 | 核心机制,Early-Z 加速 |
| 典型场景 | 树叶、铁丝网、头发 | 描边、镂空 UI、阴影体 | 所有不透明物体 |
Combined Case
综合案例:带描边的镂空树叶
同时运用三种测试:Alpha 测试裁剪透明区域,模板测试实现描边,深度测试保证正确遮挡。

cs
// 综合案例:镂空树叶 + 描边,三项测试全用上
Shader "Custom/LeafOutline" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Range(0,1)) = 0.5
_OutlineW ("Outline Width", Float) = 0.03
_OutlineC ("Outline Color", Color) = (1,1,0,1)
}
SubShader {
Tags { "Queue" = "AlphaTest" }
// ═══ Pass 1:渲染树叶本体 ═══
Pass {
Stencil {
Ref 1
Comp Always
Pass Replace // 树叶区域写 stencil=1
}
ZWrite On
ZTest LEqual
CGPROGRAM
// ① Alpha 测试:丢弃透明像素
fixed4 col = tex2D(_MainTex, i.uv);
clip(col.a - _Cutoff); // alpha < _Cutoff → discard
// ③ 深度测试由 ZTest LEqual 自动处理
return col;
ENDCG
}
// ═══ Pass 2:描边(stencil≠1 才渲染) ═══
Pass {
Stencil {
Ref 1
Comp NotEqual // ② 模板测试:只在树叶外围通过
Pass Keep
}
ZWrite Off
ZTest LEqual
CGPROGRAM
// 顶点外扩 _OutlineW,输出描边颜色
v.vertex.xyz += v.normal * _OutlineW;
return _OutlineC;
ENDCG
}
}
}
Summary
总结一句话记住它们
🔴
Alpha 测试
片元太透明就直接扔掉,硬边镂空的利器,顺序第一,代价最小。
🟢
模板测试
用一张整数图做"通行证",可以画出任意形状的渲染范围,特效万能胶。
🔵
深度测试
近的盖住远的,Z-Buffer 记录最近深度,是 3D 场景不穿帮的根基。
===============================================
问题来了,那么Alpha测试,对应的是哪个缓冲区呢?
Alpha 测试没有对应的缓冲区
Alpha 测试是一种纯逻辑判断,它只读取片元着色器输出的 alpha 值,与一个参考值比较,然后做二选一:
if (alpha < cutoff) → discard(直接销毁这个片元)
else → 继续往下走
没有任何缓冲区参与,不读也不写。片元要么活着进入下一关,要么就地消失。
对比三者的缓冲区关系
| 测试 | 依赖的缓冲区 | 读? | 写? |
|---|---|---|---|
| Alpha 测试 | 无 | ✗ | ✗ |
| 模板测试 | Stencil Buffer(8-bit 整型图) | ✓ | ✓(可选) |
| 深度测试 | Depth Buffer(24/32-bit 浮点图) | ✓ | ✓(ZWrite On时) |
为什么 Alpha 测试排第一?
正因为它不涉及任何缓冲区读写,成本最低------只是一个 CPU 级别的数值比较,所以放在最前面,能早早剔除不需要的片元,减少后续模板缓冲和深度缓冲的读写压力。
一句话记住:Alpha 测试靠的是"值",模板/深度测试靠的是"缓冲区"。