US3S3L6——开启深度写入的半透明

开启深度写入的半透明效果用来处理的需求

对于本身结构较为复杂的模型,使用之前的透明度混合Shader会由于关闭了深度写入,会产生错误的渲染效果
虽然我们可以通过拆分模型的方式解决部分问题,但是对于一些结构复杂的模型,拆分模型的方式会增加工作量

以下面的模型为例,左边不使用 半透明Shader 的材质,右边使用了 关闭深度写入的半透明Shader 的材质

image

可以看到,对于这种自带遮挡关系的复杂模型,关闭深度写入会出现遮挡关系错误的渲染问题

对于这种情况,我们可以采用 开启深度写入的半透明 Shader 来优化效果

开启深度写入的半透明效果的基本原理

基本原理:使用两个Pass渲染通道来处理渲染逻辑

  • 第一个Pass:开启深度写入,不输出颜色,目的是让该模型各片元的深度值能写入深度缓冲
  • 第二个Pass:进行正常的透明度混合(和上节课一样)

这样做的话,当执行第一个Pass时,会执行深度测试,并进行深度写入
如果此时该片元没有通过深度测试会直接丢弃,不会再执行第二个Pass
对于同一个模型中处于屏幕同一位置的片元们,会进行该位置的深度测试再决定渲染哪个片元

如何做到不输出颜色?使用 ColorMask​ 颜色遮罩 渲染状态(命令)

它主要用于控制颜色分量是否写入到颜色缓冲区中

  • ColorMask RGBA​ —— 表示写入颜色的RGBA通道
  • ColorMask 0​ —— 表示不写入颜色
  • ColorMask RB​ —— 表示只写入红色和蓝色通道

注意!

  1. 开启深度写入的半透明效果,模型内部之间不会有任何半透明效果(因为模型内部深度较大的片元会被丢弃掉)
  2. 由于有两个Pass渲染通道,因此它会带来一定的性能开销

实现开启深度写入的半透明效果

  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
    71
    72
    73
    74
    75
    76
    77
    78
    Shader "TeachShader/Lesson58_Transparent"
    {
    Properties
    {
    _MainTex("MainTex", 2D) = ""{} // 纹理贴图
    _MainColor("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
    _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
    _SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
    _AlphaScale("AlphaScale", Range(0, 1)) = 1 // 对象总体透明度
    }
    SubShader
    {
    Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }

    Pass
    {
    Tags { "LightMode" = "ForwardBase" }
    ZWrite Off //半透明效果关闭深度写入
    Blend SrcAlpha OneMinusSrcAlpha //设置混合因子

    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 _AlphaScale;

    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); // 取出纹理的颜色
    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;
    // 最终透明度 = A通道值 * 整体透明度
    return fixed4(color.rgb, texColor.a * _AlphaScale);
    }
    ENDCG
    }
    }
    }
  2. SubShader​ 中之前的 Pass​ 渲染通道前面加一个 Pass​ 渲染通道

  3. 在新加 Pass​ 渲染通道中开启深度写入,并且使用 ColorMask 0​ 颜色遮罩 渲染命令,不输出颜色

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    SubShader
    {
    Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }

    // 进行深度写入,不输出颜色,如果此片元后续在深度测试中被丢弃,后面渲染颜色的Pass是不会输出的
    Pass
    {
    ZWrite On
    ColorMask 0
    }

    Pass { /* 渲染并混合颜色的Pass */ }
    }

完整 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
79
80
81
82
83
84
85
Shader "TeachShader/Lesson58_Transparent"
{
Properties
{
_MainTex("MainTex", 2D) = ""{} // 纹理贴图
_MainColor("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
_AlphaScale("AlphaScale", Range(0, 1)) = 1 // 对象总体透明度
}
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }

// 进行深度写入,不输出颜色,如果此片元后续在深度测试中被丢弃,后面渲染颜色的Pass是不会输出的
Pass
{
ZWrite On
ColorMask 0
}

Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off //半透明效果关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha //设置混合因子

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 _AlphaScale;

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); // 取出纹理的颜色
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;
// 最终透明度 = A通道值 * 整体透明度
return fixed4(color.rgb, texColor.a * _AlphaScale);
}
ENDCG
}
}
}

显示效果(左为不使用半透明,中间为关闭深度写入的半透明,右为开启深度写入的半透明):

image

可以看见,使用开启深度写入的半透明的遮挡关系是正确的