US3S3L4——透明度测试

透明测试

在游戏开发中,对象的某些部位完全透明而其他部位完全不透明
这种透明需求往往不需要半透明效果,相对比较极端,只有看得见和看不见之分
比如树叶、草、栅栏等等,如下图:

因为单纯的透明测试是不需要进行混合操作的,因此无需去关闭深度写入

透明测试的基本原理

基本原理:通过一个阈值来决定哪些像素应该被保留,哪些应该被丢弃
具体实现:通过片元携带的颜色信息中的透明度(A值)来计算

  • 不满足条件时(通常是小于某个阈值)

    该片元就会被舍弃,被舍弃的片元不会在进行任何处理,不会对颜色缓冲区产生任何影响

  • 满足条件时(通常是大于等于某个阈值)

    该片元会按照不透明物体的处理方式来处理

阈值判断使用的方法:利用 CG 中的内置函数:clip()

该函数有重载,参数类型可以是 float4​,float3​,float2​,float​ 等等
如果传入的参数任何一个分量是负数就会舍弃当前片元
它的内部实现会用到一个 discard​ 指令,代表剔除该片元 不再参与渲染

它的实现大致为:

1
2
3
4
5
void clip(float4 x)
{
if (any(x < 0))
discard;
}

透明测试实现

  1. 复用 颜色纹理结合光照模型 的Shader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    Shader "TeachShader/Lesson56"
    {
    Properties
    {
    _MainTex("MainTex", 2D) = ""{} // 纹理贴图
    _MainColor("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
    _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
    _SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
    }
    SubShader
    {
    Pass
    {
    Tags { "LightMode" = "ForwardBase" }

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"
    #include "Lighting.cginc"

    // 贴图纹理对应的映射成员
    sampler2D _MainTex;
    float4 _MainTex_ST;
    // 漫反射颜色、高光反射颜色、光泽度
    fixed4 _MainColor;
    fixed4 _SpecularColor;
    float _SpecularNum;

    struct v2f
    {
    float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
    float2 uv: TEXCOORD0; // 纹理UV坐标
    float3 wNormal: NORMAL; // 世界空间下的法线
    float3 wPos: TEXCOORD1; // 世界空间下的顶点坐标
    };

    v2f vert (appdata_base v)
    {
    v2f data;
    data.pos = UnityObjectToClipPos(v.vertex); // 将模型空间下的法线转换到世界空间下
    data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 计算UV
    data.wNormal = UnityObjectToWorldNormal(v.normal); // 法线转换到世界空间
    data.wPos = mul(unity_ObjectToWorld, v.vertex); // 顶点转换到世界空间

    return data;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _MainColor.rgb; // 反射率,即纹理颜色和漫反射材质颜色乘法叠加共同决定的颜色

    // 漫反射颜色
    float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 指向光源的方向
    fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(i.wNormal, lightDir));

    // 高光反射颜色
    float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos)); // 视角方向
    float3 halfA = normalize(viewDir + lightDir); // 半角向量
    fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(i.wNormal, halfA)), _SpecularNum);

    // 最终颜色 = 环境光 * 反射颜色 + 漫反射颜色 + 高光反射颜色
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

    return fixed4(color.rgb, 1);
    }
    ENDCG
    }
    }
    }
  2. 在属性中加一个阈值 _Cutoff​,取值范围为 0~1,用来设定用来判断的阈值。并在CG中添加属性的映射成员(使用 fixed​)

    1
    2
    3
    4
    5
    6
    7
    8
    Properties
    {
    _MainTex("MainTex", 2D) = ""{} // 纹理贴图
    _MainColor("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
    _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
    _SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
    _Cutoff("Curoff", Range(0, 1)) = 0 // 透明测试阈值
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 贴图纹理对应的映射成员
    sampler2D _MainTex;
    float4 _MainTex_ST;
    // 漫反射颜色、高光反射颜色、光泽度
    fixed4 _MainColor;
    fixed4 _SpecularColor;
    float _SpecularNum;
    // 透明测试阈值
    fixed _Cutoff;
  3. 将渲染队列设置为 AlphaTest​,并配合 IgnoreProjector​ 和 RenderType​ 一起设置

    1
    2
    3
    4
    5
    SubShader
    {
    Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
    Pass {/*...*/}
    }

    指的一提的是,仅进行透明测试是不需要进行混合操作的,因此无需去关闭深度写入

  4. 在片元着色器中获取了颜色贴图颜色后,就进行阈值判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    fixed4 frag (v2f i) : SV_Target
    {
    fixed4 texColor = tex2D(_MainTex, i.uv); // 颜色纹理的颜色信息
    clip(texColor.a - _Cutoff); // A通道减去阈值传入到Clip函数内,若A值小于阈值就会被裁剪

    fixed3 albedo = texColor.rgb * _MainColor.rgb; // 反射率,即纹理颜色和漫反射材质颜色乘法叠加共同决定的颜色

    // 漫反射颜色
    float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 指向光源的方向
    fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(i.wNormal, lightDir));

    // 高光反射颜色
    float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos)); // 视角方向
    float3 halfA = normalize(viewDir + lightDir); // 半角向量
    fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(i.wNormal, halfA)), _SpecularNum);

    // 最终颜色 = 环境光 * 反射颜色 + 漫反射颜色 + 高光反射颜色
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

    return fixed4(color.rgb, 1);
    }

    其中 clip(texColor.a - _Cutoff)​ 写法等同于:

    1
    2
    3
    4
    5
    if (​texColor.a - _Cutoff​ < 0)
    discard;
    // 或者
    if (texColor.a < _Cutoff)
    discard;

完整 Shader 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Shader "TeachShader/Lesson56"
{
Properties
{
_MainTex("MainTex", 2D) = ""{} // 纹理贴图
_MainColor("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
_Cutoff("Curoff", Range(0, 1)) = 0 // 透明测试阈值
}
SubShader
{
Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}

Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"

// 贴图纹理对应的映射成员
sampler2D _MainTex;
float4 _MainTex_ST;
// 漫反射颜色、高光反射颜色、光泽度
fixed4 _MainColor;
fixed4 _SpecularColor;
float _SpecularNum;
// 透明测试阈值
fixed _Cutoff;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float2 uv: TEXCOORD0; // 纹理UV坐标
float3 wNormal: NORMAL; // 世界空间下的法线
float3 wPos: TEXCOORD1; // 世界空间下的顶点坐标
};

v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 将模型空间下的法线转换到世界空间下
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 计算UV
data.wNormal = UnityObjectToWorldNormal(v.normal); // 法线转换到世界空间
data.wPos = mul(unity_ObjectToWorld, v.vertex); // 顶点转换到世界空间

return data;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 texColor = tex2D(_MainTex, i.uv); // 颜色纹理的颜色信息
clip(texColor.a - _Cutoff); // A通道减去阈值传入到Clip函数内,若A值小于阈值就会被裁剪

fixed3 albedo = texColor.rgb * _MainColor.rgb; // 反射率,即纹理颜色和漫反射材质颜色乘法叠加共同决定的颜色

// 漫反射颜色
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 指向光源的方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(i.wNormal, lightDir));

// 高光反射颜色
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos)); // 视角方向
float3 halfA = normalize(viewDir + lightDir); // 半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(i.wNormal, halfA)), _SpecularNum);

// 最终颜色 = 环境光 * 反射颜色 + 漫反射颜色 + 高光反射颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

return fixed4(color.rgb, 1);
}
ENDCG
}
}
}

以下图作为示例(下图的四个色块透明度各不相同,其中左上角A值为80%,右上角A值为70%,左下角A值为60%,右下角A值为50%)

transparent_texture_small

分别调节使用此 Shader 的材质的 Cutoff​ 值为 0,0.55,0.65,0.75,0.85,得到效果如下:

imageimageimageimageimage

可以看到,随着 阈值 Cutoff​ 值越来越高,A值低于阈值的像素会被丢弃不被渲染