【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题

【Unity Shader学习笔记】(三) ---------------- 光照模型原理及漫反射和高光反射的实现
【Unity Shader】(四) ------ 纹理之法线纹理、单张纹理及遮罩纹理的实现

 

前言

相信读者对透明效果都不陌生,因为透明效果是游戏中经常使用的一种效果。要实现透明效果,通常会在渲染模型时控制它的透明通道。而其透明度则控制是其是否会显示,0 表示完全不显示,1 表示完全显示。

Unity 中通常使用两种方法来实现透明效果:透明度测试(Alpha Test)透明度混合(Alpha Blending)

  • 透明度测试。透明度测试是一种十分 “简单粗暴” 的机制,当有一个片元的透明度不符合条件时,就直接舍弃,不再任何处理(不会对颜色缓冲有影响);如果符合,就进行正常的处理(深度测试,深度写入等);所以这带来的效果也是两极分化的,要么完全透明,要么完全不透明。
  • 透明度混合。透明度混合可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与颜色缓冲中的值进行混合,得到新的颜色。需要注意的是,此方法需要关闭深度写入,而因此带来的问题就是要 十分十分十分 地注意渲染顺序。

 

为了方便读者理解,先解释一下深度缓冲,深度测试和深度写入

  • 深度缓冲。用于解决可见性问题的强大存在。决定了哪个物体的哪些部分会被渲染,哪些部分会被遮挡
  • 深度测试。开启后,当渲染一个片元时,根据它的深度值判断该片元距离摄像机的距离,然后将它的深度值和深度缓冲中的值进行比较
  • 深度写入。开启后,当一个片元进行了深度测试后,如果它的值距离更远,则说明有物体挡在了它前面,那么它就不会被渲染,如果更近,那么这个片元就应该覆盖掉颜色缓冲中的值,并把它的深度值更新到深度缓冲中。

 

可能会有读者会问:为什么透明度混合需要关闭深度写入呢?我们可以同过一张图来解释

 【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

平面 1 和 平面 2 都是在摄像机视线上,平面 1 是透明的而平面 2 是不透明的且平面 1 挡住了平面 2。理论上我们应该可以透过平面 1 来看到平面 2。事实上,如果没有关闭深度写入,平面 1 和 平面 2在渲染时进行深度测试,测试结果为平面 2更远,所以平面 2不会被渲染到屏幕上,即我们看不到平面 2。这很显然是不符合我们所要的。

 

一. 渲染顺序

 

1.1 渲染顺序的重要性

前文说过,关闭了深度写入后,渲染顺序就变得十分重要,为什么这么说呢

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 

如图,A 和 B渲染顺序不一样。有两种情况

    • 先渲染 B 再 渲染 A。此时深度缓冲中没有数据,B 直接写入它的颜色缓冲和深度缓冲;然后渲染 A,A进行深度测试,结果为 A 更近,所以此时会用 A 的透明度与颜色缓冲中值进行混合,得到正确的半透明效果。
    • 先渲染 A 再 渲染 B。此时深度缓冲中并没有数据,A 会写入颜色缓冲,但不会写入深度缓冲(因为关闭了深度写入);然后渲染 B ,B 进行深度测试,而此时深度缓冲中并没有数据,所以  B 会直接写进颜色缓冲和深度缓冲,就会覆盖掉颜色缓冲中 A 的颜色,所以最终渲染出来,从视觉上是 B 在 A 的前面

 

1.2 渲染队列

Unity 中提供了 渲染队列,并用整数索引表示渲染队列,索引越小,越早渲染

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 也可以使用 SubShader 中的 Queue 标签来决定该模型属于哪个渲染队列

名称 队列索引号 描述
Background 1000 这个队列会在任何队列 之前被渲染,通常用来渲染绘制在背景的物体
Geometry 2000 默认的队列,非透明物体使用此队列
Alpha Test 2450 进行透明度测试的物体使用的队列
Overlay 3000 按后往前顺序渲染,使用透明度混合的物体应该使用此队列
  4000 在最后渲染的物体使用此队列

 

 

二. 透明度测试

新建一个工程,去掉天空盒;新建一个Material 和 shader ,命名为 Alpha Test;新建一个 cube

 

需要提前了解的是:

  •  ZWrite Off 用于关闭深度写入,可以写在 Pass里面,也可以写在 SubShader 里,如果是后者,那么就会对所有的 Pass 产生效果,即所有的 Pass 都会关闭深度写入。
  • 我们在后面的代码将会使用 clip 函数进行透明度测试。参数为裁剪时使用的标量或矢量,如果参数的任一分量为负数,就舍弃当前像素的输出颜色。我们同样可以在MSDN上找到它的定义

 【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 

I. 创建一个场景,去掉天空盒;新建一个 Material 和 shader ,命名为 Alpha Test;创建一个 Cube;准备一张不同区域透明度不同的透明纹理(读者可以在本文最下方下载)。

 

II. 定义 Propreties 语义块

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 Properites 语义块并没有什么特别的属性,_Cutoff 属性用来控制透明度,范围为【0,1】,因为纹理像素的透明度范围就在此范围。

 

III.  指定渲染队列

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

在 SubShader 中定义一个 Tags,IgnoreProjector 决定 shader 是否会受投影器的影响,RenderType 可以让 shader 归入提前定义的组(这里是 TransparentCutout)。

 

IV. 定义与 Properties 中相匹配的变量

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 

 

V. 定义输入输出结构体

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 

VI. 定义顶点着色器

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 TRANSFORM_TEX 函数我们在之前已经解释过了,如果读者对此不太了解,可以翻看我的上一篇文章

 

VII. 定义片元着色器

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 这些代码相信读者都不陌生,这里 clip 函数对不符合条件的片元舍弃了,即不渲染了。

 

VIII. 最后设置 FallBack

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 

 

完整代码:

 1 Shader "Unity/01-AlphaTest" {
 2     Properties {
 3         _Color ("Main Tint", Color) = (1,1,1,1)
 4         _MainTex ("Main Tex", 2D) = "white" {}
 5         _Cutoff("Alpha Cutoff",Range(0,1)) = 0.5
 6     }
 7     SubShader {
 8         Tags{"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
 9         Cull Off
10         Pass
11         {
12             Tags{"LightMode" = "ForwardBase"}
13 
14             CGPROGRAM
15             #pragma vertex vert
16             #pragma fragment frag
17             #include "Lighting.cginc"
18             #include "UnityCG.cginc"
19 
20             fixed4 _Color;
21             sampler2D _MainTex;
22             float4 _MainTex_ST;
23             fixed _Cutoff;
24 
25             struct a2v
26             {
27                 float4 vertex : POSITION;
28                 float3 normal : NORMAL;
29                 float4 texcoord : TEXCOORD0;
30             };
31 
32             struct v2f
33             {
34                 float4 pos : SV_POSITION;
35                 float3 worldNormal : TEXCOORD0;
36                 float3 worldPos : TEXCOORD1;
37                 float2 uv : TEXCOORD2;
38             };
39 
40             v2f vert(a2v v)
41             {
42                 v2f o;
43                 o.pos = UnityObjectToClipPos(v.vertex);
44                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
45                 o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
46 
47                 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
48 
49                 return o;
50 
51             }
52 
53             fixed4 frag(v2f i) : SV_TARGET0
54             {
55                 fixed3 worldNormal = normalize(i.worldNormal);
56                 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
57 
58                 fixed4 texcolor = tex2D(_MainTex,i.uv);
59 
60                 clip(texcolor.a - _Cutoff);
61 
62                 fixed3 albedo = texcolor.rgb * _Color.rgb;
63                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
64                 fixed3 diffues = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
65 
66                 return fixed4(ambient + diffues,1.0);
67             }
68             ENDCG
69         }
70     }
71         FallBack "Transparent/Cutout/VertexLit"
72 
73 }

 

 

IX. 保存,回到Unity,查看效果

不同 Cutoff 的效果:

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

我们可以看到通过透明度测试实现的效果在边界处并不理想,有锯齿,而为了解决这个问题,我们就应该使用透明度混合,来得到更柔和的效果。

 

 

三. 透明度混合

 

3.1 透明度混合的实现

回想一下,我们前面所说的透明度混合的原理:把自身的颜色和颜色缓冲中的颜色进行混合,得到新的颜色。既然要混合,那就需要混合命令 Blend。混合语义有许多,我们稍后会具体地介绍,在这里,我们使用 Blend SrcFactor DstFactor 这条语义,其中 Blend 是操作,SrcFactor,DstFactor 是因子;我们把 SrcFactor 设为 SrcAlpha,DstFactor 设为 OneMinusSrcAlpha。即我们即将使用的混合语义代码为 Blend SrcAlpha OneMinusSrcAlpha,这相当于,混合后颜色为:

               【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

不明白这条公式的读者不用着急,我们稍后会具体解释,这里先知道我们即将使用这条式子便可。

 代码和透明度测试类似,所以这里只列出需要注意的修改的地方。

 

 

 

I. 新建一个 Material 和 shader ,命名为 Alpha Blend;创建一个 Cube;使用同一张透明纹理。

 

II.修改 Properties 语义块

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

其中 _AlphaScale 用来控制整体的透明度。当然也要在CG代码片中定义与其对应的变量。

 

III. 修改 Tags

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 

 

IV. 关闭深度写入和开启混合

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 

 

V.修改片元着色器

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 我们用透明纹理的透明通道和 _AlphaScale 来控制整体透明度

 

VI. 修改 FallBack

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 

 

完整代码:

 1 Shader "Unity/02-AlphaBlend" {
 2     Properties {
 3         _Color ("Main Tint", Color) = (1,1,1,1)
 4         _MainTex ("Main Tex", 2D) = "white" {}
 5         _AlphaScale("Alpha Scale",Range(0,1)) = 1
 6     }
 7     SubShader {
 8         Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
 9 
10         Pass
11         {
12             Tags{"LightMode" = "ForwardBase"}
13             ZWrite Off
14             Blend SrcAlpha OneMinusSrcAlpha
15 
16 
17             CGPROGRAM
18             #pragma vertex vert
19             #pragma fragment frag
20             #include "Lighting.cginc"
21             #include "UnityCG.cginc"
22 
23             fixed4 _Color;
24             sampler2D _MainTex;
25             float4 _MainTex_ST;
26             fixed _AlphaScale;
27 
28             struct a2v
29             {
30                 float4 vertex : POSITION;
31                 float3 normal : NORMAL;
32                 float4 texcoord : TEXCOORD0;
33             };
34 
35             struct v2f
36             {
37                 float4 pos : SV_POSITION;
38                 float3 worldNormal : TEXCOORD0;
39                 float3 worldPos : TEXCOORD1;
40                 float2 uv : TEXCOORD2;
41             };
42 
43             v2f vert(a2v v)
44             {
45                 v2f o;
46                 o.pos = UnityObjectToClipPos(v.vertex);
47                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
48                 o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
49 
50                 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
51 
52                 return o;
53 
54             }
55 
56             fixed4 frag(v2f i) : SV_TARGET0
57             {
58                 fixed3 worldNormal = normalize(i.worldNormal);
59                 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
60 
61                 fixed4 texcolor = tex2D(_MainTex,i.uv);
62 
63                 fixed3 albedo = texcolor.rgb * _Color.rgb;
64                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
65                 fixed3 diffues = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
66 
67                 return fixed4(ambient + diffues,texcolor.a * _AlphaScale);
68             }
69             ENDCG
70         }
71     }
72         FallBack "Transparent/VertexLit"
73 
74 }

 

 

 VII. 保存,回到Unity,查看效果

 不同  _AlphaScale 的效果:

 【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 对比透明度测试,我们可以看到透明度混合更加柔和平滑。

 

 3.2 混合命令

Blend SrcFactor DstFactor 开启混合,设置因子。源颜色 x ScrFacor  + 目标颜色 x DstFactor,结构存入颜色缓冲
Blend SrcFactor DstFactor,SrcFactorA DstFactorA 和上面类似,只是混合透明通道的因子不同

 

 混合有两个操作数:源颜色(source color) 目标颜色(destination color)

  • 源颜色。指片元着色器产生的颜色值,用 S 表示。
  • 目标颜色。指颜色缓冲中的值,用 D 表示。
  • 两者混合后,得到的新颜色用 O 表示。

而上面三者都包含了 RGBA 通道。

除了 Blend Off 以外使用Blend 命令,Unity 会为我们开启混合,因为只有开启了混合,混合命令才起效。

混合命令由 操作因子 组成,操作默认是使用 加操作,而为了混合RGB 通道 和 A通道,所以我们需要 4 个因子

以混合命令 Blend SrcFactor DstFactor 为例,默认为加操作,SrcFactor 为源颜色, DstFactor 为目标颜色,然后计算

                              【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

                              【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 下面是 ShaderLab 支持的一些混合因子:

 混合有两个操作数:源颜色(source color) 目标颜色(destination color)

  • 源颜色。指片元着色器产生的颜色值,用 S 表示。
  • 目标颜色。指颜色缓冲中的值,用 D 表示。
  • 两者混合后,得到的新颜色用 O 表示。

而上面三者都包含了 RGBA 通道。

除了 Blend Off 以外使用Blend 命令,Unity 会为我们开启混合,因为只有开启了混合,混合命令才起效。

混合命令由 操作因子 组成,操作默认是使用 加操作,而为了混合RGB 通道 和 A通道,所以我们需要 4 个因子

以混合命令 Blend SrcFactor DstFactor 为例,默认为加操作,SrcFactor 为源颜色, DstFactor 为目标颜色,然后计算

                              【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

                              【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 下面是 ShaderLab 支持的一些混合因子:

 

 混合有两个操作数:源颜色(source color) 目标颜色(destination color)

  • 源颜色。指片元着色器产生的颜色值,用 S 表示。
  • 目标颜色。指颜色缓冲中的值,用 D 表示。
  • 两者混合后,得到的新颜色用 O 表示。

而上面三者都包含了 RGBA 通道。

除了 Blend Off 以外使用Blend 命令,Unity 会为我们开启混合,因为只有开启了混合,混合命令才起效。

混合命令由 操作因子 组成,操作默认是使用 加操作,而为了混合RGB 通道 和 A通道,所以我们需要 4 个因子

以混合命令 Blend SrcFactor DstFactor 为例,默认为加操作,SrcFactor 为源颜色, DstFactor 为目标颜色,然后计算

                              【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

                              【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 下面是 ShaderLab 支持的一些混合因子:

 

  

读者可以自行选择因子来试试效果

 

 

四. 双面渲染

一般来说,如果一个物体是透明的,要么我们应该可以看到它的内部和它的任一个面,但前面我们实现的透明中并没有实现这个效果,因为 Unity 在默认引擎下剔除了物体背面(是相对于摄像机方向的背面,而不是世界坐标中前后左右的背面),不渲染。而剔除的指令为

Cull Back | Front | Off

为了实现双面渲染,我们可以这样实现:设置两个 Pass ,一个只渲染前面,一个只渲染背面。不过需要注意的是,由于开启了深度测试,所以要注意渲染顺序,要先渲染背面,再渲染正面,这样就能确保背面会被渲染出来。

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理 

两个 Pass 中,除了 Cull 指令不一样外,其余代码都是和透明度混合中的代码一样,所以,这里直接给出完整代码

  1 // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
  2 
  3 Shader "Unity/04-AlphaBlendBothSide" {
  4     Properties {
  5         _Color ("Main Tint", Color) = (1,1,1,1)
  6         _MainTex ("Main Tex", 2D) = "white" {}
  7         _AlphaScale("Alpha Scale",Range(0,1)) = 1
  8     }
  9     SubShader {
 10         Tags{"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
 11 
 12         Pass
 13         {
 14             Tags{"LightMode" = "ForwardBase"}
 15 
 16             Cull Front
 17 
 18             ZWrite Off
 19             Blend SrcAlpha OneMinusSrcAlpha
 20 
 21 
 22             CGPROGRAM
 23             #pragma vertex vert
 24             #pragma fragment frag
 25             #include "Lighting.cginc"
 26             #include "UnityCG.cginc"
 27 
 28             fixed4 _Color;
 29             sampler2D _MainTex;
 30             float4 _MainTex_ST;
 31             fixed _AlphaScale;
 32 
 33             struct a2v
 34             {
 35                 float4 vertex : POSITION;
 36                 float3 normal : NORMAL;
 37                 float4 texcoord : TEXCOORD0;
 38             };
 39 
 40             struct v2f
 41             {
 42                 float4 pos : SV_POSITION;
 43                 float3 worldNormal : TEXCOORD0;
 44                 float3 worldPos : TEXCOORD1;
 45                 float2 uv : TEXCOORD2;
 46             };
 47 
 48             v2f vert(a2v v)
 49             {
 50                 v2f o;
 51                 o.pos = UnityObjectToClipPos(v.vertex);
 52                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
 53                 o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
 54 
 55                 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
 56 
 57                 return o;
 58 
 59             }
 60 
 61             fixed4 frag(v2f i) : SV_TARGET0
 62             {
 63                 fixed3 worldNormal = normalize(i.worldNormal);
 64                 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
 65 
 66                 fixed4 texcolor = tex2D(_MainTex,i.uv);
 67 
 68                 fixed3 albedo = texcolor.rgb * _Color.rgb;
 69                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
 70                 fixed3 diffues = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
 71 
 72                 return fixed4(ambient + diffues,texcolor.a * _AlphaScale);
 73             }
 74 
 75 
 76 
 77 
 78 
 79             ENDCG
 80         }
 81 
 82 
 83         Pass
 84         {
 85             Tags{"LightMode" = "ForwardBase"}
 86 
 87             Cull Back
 88 
 89             ZWrite Off
 90             Blend SrcAlpha OneMinusSrcAlpha
 91 
 92 
 93             CGPROGRAM
 94             #pragma vertex vert
 95             #pragma fragment frag
 96             #include "Lighting.cginc"
 97             #include "UnityCG.cginc"
 98 
 99             fixed4 _Color;
100             sampler2D _MainTex;
101             float4 _MainTex_ST;
102             fixed _AlphaScale;
103 
104             struct a2v
105             {
106                 float4 vertex : POSITION;
107                 float3 normal : NORMAL;
108                 float4 texcoord : TEXCOORD0;
109             };
110 
111             struct v2f
112             {
113                 float4 pos : SV_POSITION;
114                 float3 worldNormal : TEXCOORD0;
115                 float3 worldPos : TEXCOORD1;
116                 float2 uv : TEXCOORD2;
117             };
118 
119             v2f vert(a2v v)
120             {
121                 v2f o;
122                 o.pos = UnityObjectToClipPos(v.vertex);
123                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
124                 o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
125 
126                 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
127 
128                 return o;
129 
130             }
131 
132             fixed4 frag(v2f i) : SV_TARGET0
133             {
134                 fixed3 worldNormal = normalize(i.worldNormal);
135                 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
136 
137                 fixed4 texcolor = tex2D(_MainTex,i.uv);
138 
139                 fixed3 albedo = texcolor.rgb * _Color.rgb;
140                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
141                 fixed3 diffues = _LightColor0.rgb * albedo * max(0,dot(worldNormal,worldLightDir));
142 
143                 return fixed4(ambient + diffues,texcolor.a * _AlphaScale);
144             }
145 
146             ENDCG
147 
148     }
149     }
150         FallBack "Transparent/VertexLit"
151 
152 }

 

 现在来查看下效果:

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

 现在我们可以清楚地看到物体的内部了。

 

 

五. 总结

透明效果是十分常见且有用的一种实现,我们可以利用它来实现很多有趣的效果。要实现透明,更多地是对渲染的一种理解。本文只是对Unity中渲染的一些基础解释,希望能对读者有所帮助。

 

本文所用透明纹理及shader

 

 

【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理

上一篇:精通MySQL之锁篇


下一篇:Vue项目上this.$set的用法