笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题
【Unity Shader学习笔记】(三) ---------------- 光照模型原理及漫反射和高光反射的实现 【Unity Shader】(四) ------ 纹理之法线纹理、单张纹理及遮罩纹理的实现 前言
相信读者对透明效果都不陌生,因为透明效果是游戏中经常使用的一种效果。要实现透明效果,通常会在渲染模型时控制它的透明通道。而其透明度则控制是其是否会显示,0 表示完全不显示,1 表示完全显示。
Unity 中通常使用两种方法来实现透明效果:透明度测试(Alpha Test)和 透明度混合(Alpha Blending)。
- 透明度测试。透明度测试是一种十分 “简单粗暴” 的机制,当有一个片元的透明度不符合条件时,就直接舍弃,不再任何处理(不会对颜色缓冲有影响);如果符合,就进行正常的处理(深度测试,深度写入等);所以这带来的效果也是两极分化的,要么完全透明,要么完全不透明。
- 透明度混合。透明度混合可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与颜色缓冲中的值进行混合,得到新的颜色。需要注意的是,此方法需要关闭深度写入,而因此带来的问题就是要 十分十分十分 地注意渲染顺序。
为了方便读者理解,先解释一下深度缓冲,深度测试和深度写入
- 深度缓冲。用于解决可见性问题的强大存在。决定了哪个物体的哪些部分会被渲染,哪些部分会被遮挡
- 深度测试。开启后,当渲染一个片元时,根据它的深度值判断该片元距离摄像机的距离,然后将它的深度值和深度缓冲中的值进行比较
- 深度写入。开启后,当一个片元进行了深度测试后,如果它的值距离更远,则说明有物体挡在了它前面,那么它就不会被渲染,如果更近,那么这个片元就应该覆盖掉颜色缓冲中的值,并把它的深度值更新到深度缓冲中。
可能会有读者会问:为什么透明度混合需要关闭深度写入呢?我们可以同过一张图来解释
平面 1 和 平面 2 都是在摄像机视线上,平面 1 是透明的而平面 2 是不透明的且平面 1 挡住了平面 2。理论上我们应该可以透过平面 1 来看到平面 2。事实上,如果没有关闭深度写入,平面 1 和 平面 2在渲染时进行深度测试,测试结果为平面 2更远,所以平面 2不会被渲染到屏幕上,即我们看不到平面 2。这很显然是不符合我们所要的。
一. 渲染顺序
1.1 渲染顺序的重要性
前文说过,关闭了深度写入后,渲染顺序就变得十分重要,为什么这么说呢
如图,A 和 B渲染顺序不一样。有两种情况
- 先渲染 B 再 渲染 A。此时深度缓冲中没有数据,B 直接写入它的颜色缓冲和深度缓冲;然后渲染 A,A进行深度测试,结果为 A 更近,所以此时会用 A 的透明度与颜色缓冲中值进行混合,得到正确的半透明效果。
- 先渲染 A 再 渲染 B。此时深度缓冲中并没有数据,A 会写入颜色缓冲,但不会写入深度缓冲(因为关闭了深度写入);然后渲染 B ,B 进行深度测试,而此时深度缓冲中并没有数据,所以 B 会直接写进颜色缓冲和深度缓冲,就会覆盖掉颜色缓冲中 A 的颜色,所以最终渲染出来,从视觉上是 B 在 A 的前面
1.2 渲染队列
Unity 中提供了 渲染队列,并用整数索引表示渲染队列,索引越小,越早渲染
也可以使用 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上找到它的定义
I. 创建一个场景,去掉天空盒;新建一个 Material 和 shader ,命名为 Alpha Test;创建一个 Cube;准备一张不同区域透明度不同的透明纹理(读者可以在本文最下方下载)。
II. 定义 Propreties 语义块
Properites 语义块并没有什么特别的属性,_Cutoff 属性用来控制透明度,范围为【0,1】,因为纹理像素的透明度范围就在此范围。
III. 指定渲染队列
在 SubShader 中定义一个 Tags,IgnoreProjector 决定 shader 是否会受投影器的影响,RenderType 可以让 shader 归入提前定义的组(这里是 TransparentCutout)。
IV. 定义与 Properties 中相匹配的变量
V. 定义输入输出结构体
VI. 定义顶点着色器
TRANSFORM_TEX 函数我们在之前已经解释过了,如果读者对此不太了解,可以翻看我的上一篇文章
VII. 定义片元着色器
这些代码相信读者都不陌生,这里 clip 函数对不符合条件的片元舍弃了,即不渲染了。
VIII. 最后设置 FallBack
完整代码:
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 的效果:
我们可以看到通过透明度测试实现的效果在边界处并不理想,有锯齿,而为了解决这个问题,我们就应该使用透明度混合,来得到更柔和的效果。
三. 透明度混合
3.1 透明度混合的实现
回想一下,我们前面所说的透明度混合的原理:把自身的颜色和颜色缓冲中的颜色进行混合,得到新的颜色。既然要混合,那就需要混合命令 Blend。混合语义有许多,我们稍后会具体地介绍,在这里,我们使用 Blend SrcFactor DstFactor 这条语义,其中 Blend 是操作,SrcFactor,DstFactor 是因子;我们把 SrcFactor 设为 SrcAlpha,DstFactor 设为 OneMinusSrcAlpha。即我们即将使用的混合语义代码为 Blend SrcAlpha OneMinusSrcAlpha,这相当于,混合后颜色为:
不明白这条公式的读者不用着急,我们稍后会具体解释,这里先知道我们即将使用这条式子便可。
代码和透明度测试类似,所以这里只列出需要注意的修改的地方。
I. 新建一个 Material 和 shader ,命名为 Alpha Blend;创建一个 Cube;使用同一张透明纹理。
II.修改 Properties 语义块
其中 _AlphaScale 用来控制整体的透明度。当然也要在CG代码片中定义与其对应的变量。
III. 修改 Tags
IV. 关闭深度写入和开启混合
V.修改片元着色器
我们用透明纹理的透明通道和 _AlphaScale 来控制整体透明度
VI. 修改 FallBack
完整代码:
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 的效果:
对比透明度测试,我们可以看到透明度混合更加柔和平滑。
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 为目标颜色,然后计算
下面是 ShaderLab 支持的一些混合因子:
混合有两个操作数:源颜色(source color)和 目标颜色(destination color)
- 源颜色。指片元着色器产生的颜色值,用 S 表示。
- 目标颜色。指颜色缓冲中的值,用 D 表示。
- 两者混合后,得到的新颜色用 O 表示。
而上面三者都包含了 RGBA 通道。
除了 Blend Off 以外,使用Blend 命令,Unity 会为我们开启混合,因为只有开启了混合,混合命令才起效。
混合命令由 操作 和 因子 组成,操作默认是使用 加操作,而为了混合RGB 通道 和 A通道,所以我们需要 4 个因子
以混合命令 Blend SrcFactor DstFactor 为例,默认为加操作,SrcFactor 为源颜色, DstFactor 为目标颜色,然后计算
下面是 ShaderLab 支持的一些混合因子:
混合有两个操作数:源颜色(source color)和 目标颜色(destination color)
- 源颜色。指片元着色器产生的颜色值,用 S 表示。
- 目标颜色。指颜色缓冲中的值,用 D 表示。
- 两者混合后,得到的新颜色用 O 表示。
而上面三者都包含了 RGBA 通道。
除了 Blend Off 以外,使用Blend 命令,Unity 会为我们开启混合,因为只有开启了混合,混合命令才起效。
混合命令由 操作 和 因子 组成,操作默认是使用 加操作,而为了混合RGB 通道 和 A通道,所以我们需要 4 个因子
以混合命令 Blend SrcFactor DstFactor 为例,默认为加操作,SrcFactor 为源颜色, DstFactor 为目标颜色,然后计算
下面是 ShaderLab 支持的一些混合因子:
参数 描述 One 因子为1 Zero 因子为0 SrcColor 源颜色值 SrcAlpha 源颜色的透明通道的值 DstColor 目标颜色值 DstAlpha 目标颜色的透明通道的值 OneMinusSrcColor 1-源颜色值 OneMinusSrcAlpha 1-源颜色的透明通道的值 OneMinusDstColor 1-目标颜色值 OneMinusDstAlpha 1-目标颜色的透明通道的值
读者可以自行选择因子来试试效果
四. 双面渲染
一般来说,如果一个物体是透明的,要么我们应该可以看到它的内部和它的任一个面,但前面我们实现的透明中并没有实现这个效果,因为 Unity 在默认引擎下剔除了物体背面(是相对于摄像机方向的背面,而不是世界坐标中前后左右的背面),不渲染。而剔除的指令为
Cull Back | Front | Off
为了实现双面渲染,我们可以这样实现:设置两个 Pass ,一个只渲染前面,一个只渲染背面。不过需要注意的是,由于开启了深度测试,所以要注意渲染顺序,要先渲染背面,再渲染正面,这样就能确保背面会被渲染出来。
两个 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中渲染的一些基础解释,希望能对读者有所帮助。