Unity Shader Alpha测试 · 模板测试 · 深度测试

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

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 测试靠的是"值",模板/深度测试靠的是"缓冲区"。

相关推荐
2601_956002814 小时前
冬日狂想曲(赠去马赛克补丁)2026.5.13最新版免费下载 转存后自动更新 (看到请立即转存 资源随时失效)pc手机版通用
智能手机·游戏引擎·电脑·游戏程序·动画·游戏美术
Sator14 小时前
unity解决粒子与物体接触时的硬边缘问题
unity·游戏引擎
RPGMZ7 小时前
RPGMZ NPC头顶自动显示一段消息
前端·游戏引擎·rpgmz·rpgmakermz
程序员JerrySUN8 小时前
Jetson边缘嵌入式实战课程第三讲:L4T 与 Jetson 系统架构
linux·服务器·人工智能·安全·unity·系统架构·游戏引擎
萌萌的提莫队长10 小时前
Unity HDRP 渲染管线 Camera 输出到RenderTexture没有Alpha通道
unity·游戏引擎
winlife_10 小时前
Unity Editor 工具不该用反射写组件字段:SerializedObject 在 4 个场景里非用不可
unity·自动化·游戏引擎
星河耀银海10 小时前
Unity C#入门:变量的定义与访问权限(public/private)
unity·c#·lucene
郝学胜-神的一滴11 小时前
中级OpenGL教程 005:为球体&平面注入法线灵魂
c++·unity·图形渲染·three.js·opengl·unreal
那个村的李富贵11 小时前
unity编辑器工具,输出使用的字体
unity·编辑器·游戏引擎