本文引用 Unity Shader入门精要
开启透明混合后,一个物体被渲染到屏幕上时,每个片元除了颜色值和深度值外,还有——透明度。透明度为1,则完全不透明,透明度为0,则完全不会显示。
在Unity中我们有两种方式实现透明度效果
- 透明度测试(Alpha Test):这种方式无法得到真正的半透明效果。只是0或1(完全透明和完全不透明)
- 透明度混合(Alpha Blending):使用当前的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合。
那让我讨论第一个问题:渲染顺序
渲染顺序
为什么先说渲染顺序呢?
在之前的两篇文章中,我并没有涉猎到渲染顺序。因为对于不透明的物体,渲染顺序的决定是由深度缓冲决定的。
深度缓冲的基本思想为:根据深度缓冲中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把深度值和已经存在于深度缓冲中的值进行比较(前提:开启深度测试),如果它的值距离摄像机更远,说明该片元不应被渲染。
但是当我们使用透明度混合时,就得关闭深度写入(ZWrite)。
为什么关闭深度写入呢?
一个半透明的物体后面如果有物体的话,应该是可以被看到的,但是深度写入会把它剔除掉。
所以对于渲染顺序就得我们自行控制了。
上两个图分别是两种渲染顺序情况,
图一:透明物体在前,不透明物体在后。
- 情况一:如果先渲染不透明物体(开启深度写入和深度测试),将不透明物体颜色写入颜色缓存,深度写入深度缓冲,然后渲染透明物体(关闭深度写入,开启深度测试),将透明物体的颜色与颜色缓冲中的颜色混合,得到正确结果。
- 情况二:先渲染透明物体(关闭深度写入,开启深度测试),透明物体颜色写入颜色缓冲,然后渲染不透明物体(开启深度写入和深度测试),深度缓存中没有内容,所以直接覆盖颜色缓冲。得到错误结果。
图二:两个透明物体。
- 情况一:先渲染后方的透明物体,颜色写入颜色缓冲,然后渲染前方透明物体,颜色和颜色缓冲中的颜色混合,得到正确结果。
- 情况二:先渲染前方的透明物体,颜色写入颜色缓冲,然后渲染后方透明物体,颜色和颜色缓冲中的颜色混合,得到后方物体在前方物体前的画面,得到错误结果。
基于这种情况Unity给我们一种解决方式(大多数引擎的解决方式):物体排序+分割网格。
- 物体排序:1.先渲染所有不透明物体,并开启它们的深度测试和深度写入。2.把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染半透明物体(开启深度测试,关闭深度写入)。
- 分割网格:解决物体排序遗留问题:循环重叠(例:3个物体互相重叠),我们把网格分割,分别判断分开后的网格的顺序来进行渲染。
Unity Shader 的渲染顺序
Unity提供了渲染队列(render queue)解决渲染顺序的问题。用SubShader的Queue标签来设置我们的模型在哪个渲染队列。索引越小越先渲染
Unity的5个渲染队列:
- Background:索引:1000,这个队列是最先渲染的。
- Geometry:索引:2000,默认渲染队列。不透明物体使用这个队列。
- Alpha Test:索引:2450,需要透明度测试的物体使用该队列。
- Transparent:索引:3000,按照从后往前的顺序渲染,使用透明度混合的物体都应该用该队列。
- Overlay:索引:4000,该队列用于实现一些叠加效果,最后渲染。
透明度测试
只要一个片元的透明度不满足条件,那么这个片元就会被舍弃。用clip来进行透明度测试。
立方体的贴图每个块都是不同的透明度分别是50%、60%、70%、80%,下面是我把Alpha Cutoff设为0.7时的效果。你会发现50%和60%透明度的贴图已经不见了。
透明度测试Shader代码:
1 Shader "My Shader/AlphaShader"
2 {
3 Properties
4 {
5 _Color ("Color", Color) = (1,1,1,1)
6 _MainTex ("Texture", 2D) = "white" {}
7 _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
8 }
9 SubShader
10 {
11 // 透明度测试队列为AlphaTest,所以Queue=AlphaTest
12 // RenderType标签让Unity把这个Shader归入提前定义的组中,以指明该Shader是一个使用了透明度测试的Shader
13 // IgonreProjector为True表明此Shader不受投影器(Projectors)影响
14 Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
15
16 Pass
17 {
18 Tags { "LightMode"="ForwardBase" }
19
20 CGPROGRAM
21 #pragma vertex vert
22 #pragma fragment frag
23
24 #include "UnityCG.cginc"
25 #include "Lighting.cginc"
26
27 struct a2v
28 {
29 float4 vertex : POSITION;
30 float3 normal : NORMAL;
31 float4 texcoord : TEXCOORD0;
32 };
33
34 struct v2f
35 {
36 float4 pos : SV_POSITION;
37 float2 uv : TEXCOORD0;
38 float3 worldNormal : TEXCOORD1;
39 float3 worldPos : TEXCOORD2;
40 };
41
42 sampler2D _MainTex;
43 float4 _MainTex_ST;
44 fixed4 _Color;
45 // 用于决定调用clip函数时进行的透明度测试使用的判断条件
46 fixed _Cutoff;
47
48 v2f vert (a2v v)
49 {
50 v2f o;
51
52 o.pos = UnityObjectToClipPos(v.vertex);
53 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
54 o.worldNormal = UnityObjectToWorldNormal(v.normal);
55 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
56
57 return o;
58 }
59
60 fixed4 frag (v2f i) : SV_Target
61 {
62 fixed3 worldNormal = normalize(i.worldNormal);
63 fixed3 worldPos = normalize(i.worldPos);
64 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
65 // 纹素值
66 fixed4 texColor = tex2D(_MainTex, i.uv);
67 // 原理
68 // if ((texColor.a - _Cutoff) < 0.0) { discard; }
69 // 如果结果小于0,将片元舍弃
70 clip(texColor.a - _Cutoff);
71 // 反射率
72 fixed3 albedo = texColor.rgb * _Color.rgb;
73 // 环境光
74 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
75 // 漫反射
76 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
77 return fixed4(ambient + diffuse, 1.0);
78 }
79 ENDCG
80 }
81 }
82 }
透明度混合
那我们看看Unity给我们提供的混合命令——Blend。给出Blend的常用语义。
- Blend Off:关闭混合
- Blend SrcFactor DstFactor:开启混合,设置混合因子。源颜色(片元颜色)乘以SrcFactor,目标颜色(已经在颜色缓冲中的颜色)乘以DstFactor,然后把两者相加
- Blend SrcFactor DstFactor,SrcFactorA DstFactorA:同上,不过把透明通道(a)与颜色通道(rgb)用不同的因子。
- BlendOp BlendOperation:使用BlendOperation对其进行其他操作,非简单相加混合。
混合公式:DstColorNew = SrcAlpha * SrcColor + (1 - SrcAlpha) * DstColorOld
下面是最简单的调整透明通道值的效果,AlphaScale = 0.6
Shader代码为:
1 Shader "My Shader/AlphaShader"
2 {
3 Properties
4 {
5 _Color ("Color", Color) = (1,1,1,1)
6 _MainTex ("Texture", 2D) = "white" {}
7 _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
8 }
9 SubShader
10 {
11 // 透明度混合队列为Transparent,所以Queue=Transparent
12 // RenderType标签让Unity把这个Shader归入提前定义的组中,以指明该Shader是一个使用了透明度混合的Shader
13 // IgonreProjector为True表明此Shader不受投影器(Projectors)影响
14 Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
15
16 Pass
17 {
18 Tags { "LightMode"="ForwardBase" }
19
20 // 关闭深度写入
21 ZWrite Off
22 // 开启混合模式,并设置混合因子为SrcAlpha和OneMinusSrcAlpha
23 Blend SrcAlpha OneMinusSrcAlpha
24
25 CGPROGRAM
26 #pragma vertex vert
27 #pragma fragment frag
28
29 #include "UnityCG.cginc"
30 #include "Lighting.cginc"
31
32 struct a2v
33 {
34 float4 vertex : POSITION;
35 float3 normal : NORMAL;
36 float4 texcoord : TEXCOORD0;
37 };
38
39 struct v2f
40 {
41 float4 pos : SV_POSITION;
42 float2 uv : TEXCOORD0;
43 float3 worldNormal : TEXCOORD1;
44 float3 worldPos : TEXCOORD2;
45 };
46
47 sampler2D _MainTex;
48 float4 _MainTex_ST;
49 fixed4 _Color;
50 // 用于决定调用clip函数时进行的透明度测试使用的判断条件
51 fixed _AlphaScale;
52
53 v2f vert (a2v v)
54 {
55 v2f o;
56
57 o.pos = UnityObjectToClipPos(v.vertex);
58 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
59 o.worldNormal = UnityObjectToWorldNormal(v.normal);
60 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
61
62 return o;
63 }
64
65 fixed4 frag (v2f i) : SV_Target
66 {
67 fixed3 worldNormal = normalize(i.worldNormal);
68 fixed3 worldPos = normalize(i.worldPos);
69 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
70 // 纹素值
71 fixed4 texColor = tex2D(_MainTex, i.uv);
72 // 反射率
73 fixed3 albedo = texColor.rgb * _Color.rgb;
74 // 环境光
75 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
76 // 漫反射
77 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
78 // 返回颜色,透明度部分乘以我们设定的值
79 return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
80 }
81 ENDCG
82 }
83 }
84 }
然而这种实现方式是有问题的。就像前面说的一样,它并不能渲染正确的顺序。
解决方式:用两个Pass来渲染模型
- 第一个Pass:开启深度写入,但不输出颜色,目的仅仅为了填充深度缓冲。
- 第二个Pass:正常的透明度混合,由于上一个Pass已经得到了逐像素的正确深度信息,该Pass就可以按照像素级别的深度排序进行透明渲染。
- 缺点:多了一个Pass性能有所影响。
代码如下:
1 Shader "My Shader/AlphaShader"
2 {
3 Properties
4 {
5 _Color ("Color", Color) = (1,1,1,1)
6 _MainTex ("Texture", 2D) = "white" {}
7 _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
8 }
9 SubShader
10 {
11 // 透明度混合队列为Transparent,所以Queue=Transparent
12 // RenderType标签让Unity把这个Shader归入提前定义的组中,以指明该Shader是一个使用了透明度混合的Shader
13 // IgonreProjector为True表明此Shader不受投影器(Projectors)影响
14 Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
15
16 Pass
17 {
18 // 开启深度写入
19 ZWrite On
20 // 设置颜色通道的写掩码,0为不写入任何颜色
21 ColorMask 0
22 }
23
24 Pass
25 {
26 Tags { "LightMode"="ForwardBase" }
27
28 // 关闭深度写入
29 ZWrite Off
30 // 开启混合模式,并设置混合因子为SrcAlpha和OneMinusSrcAlpha
31 Blend SrcAlpha OneMinusSrcAlpha
32
33 CGPROGRAM
34 #pragma vertex vert
35 #pragma fragment frag
36
37 #include "UnityCG.cginc"
38 #include "Lighting.cginc"
39
40 struct a2v
41 {
42 float4 vertex : POSITION;
43 float3 normal : NORMAL;
44 float4 texcoord : TEXCOORD0;
45 };
46
47 struct v2f
48 {
49 float4 pos : SV_POSITION;
50 float2 uv : TEXCOORD0;
51 float3 worldNormal : TEXCOORD1;
52 float3 worldPos : TEXCOORD2;
53 };
54
55 sampler2D _MainTex;
56 float4 _MainTex_ST;
57 fixed4 _Color;
58 // 用于决定调用clip函数时进行的透明度测试使用的判断条件
59 fixed _AlphaScale;
60
61 v2f vert (a2v v)
62 {
63 v2f o;
64
65 o.pos = UnityObjectToClipPos(v.vertex);
66 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
67 o.worldNormal = UnityObjectToWorldNormal(v.normal);
68 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
69
70 return o;
71 }
72
73 fixed4 frag (v2f i) : SV_Target
74 {
75 fixed3 worldNormal = normalize(i.worldNormal);
76 fixed3 worldPos = normalize(i.worldPos);
77 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
78 // 纹素值
79 fixed4 texColor = tex2D(_MainTex, i.uv);
80 // 反射率
81 fixed3 albedo = texColor.rgb * _Color.rgb;
82 // 环境光
83 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
84 // 漫反射
85 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
86 // 返回颜色,透明度部分乘以我们设定的值
87 return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
88 }
89 ENDCG
90 }
91 }
92 }
双面渲染的透明效果
对于刚才的立方体,虽然是透明的,但是却看不到里面的构造,是不是感觉也不太对,如果想看到内部构造怎么办呢?
Unity默认会剔除物体的背面(就是内部),那么我们可以用Cull指令来控制需要剔除哪个面的渲染图元。
- Cull Back:背对着摄像机的渲染图元不会渲染,默认情况。
- Cull Front:朝向摄像机的渲染图元不会渲染。
- Cull Off:关闭剔除功能,所有的都会渲染。缺点:需要渲染的数目成倍增加,除非用于特殊效果,建议不开启。
接下来我们看一下效果:
这回也是用连个Pass来完成:第一个Pass渲染背面,第二个Pass渲染前面
Shader 代码如下:
1 Shader "My Shader/AlphaShader"
2 {
3 Properties
4 {
5 _Color ("Color", Color) = (1,1,1,1)
6 _MainTex ("Texture", 2D) = "white" {}
7 _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
8 }
9 SubShader
10 {
11 // 透明度混合队列为Transparent,所以Queue=Transparent
12 // RenderType标签让Unity把这个Shader归入提前定义的组中,以指明该Shader是一个使用了透明度混合的Shader
13 // IgonreProjector为True表明此Shader不受投影器(Projectors)影响
14 Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
15
16 Pass
17 {
18 Tags { "LightMode"="ForwardBase" }
19
20 // 只渲染背面
21 Cull Front
22 // 关闭深度写入
23 ZWrite Off
24 // 开启混合模式,并设置混合因子为SrcAlpha和OneMinusSrcAlpha
25 Blend SrcAlpha OneMinusSrcAlpha
26
27 CGPROGRAM
28 #pragma vertex vert
29 #pragma fragment frag
30
31 #include "UnityCG.cginc"
32 #include "Lighting.cginc"
33
34 struct a2v
35 {
36 float4 vertex : POSITION;
37 float3 normal : NORMAL;
38 float4 texcoord : TEXCOORD0;
39 };
40
41 struct v2f
42 {
43 float4 pos : SV_POSITION;
44 float2 uv : TEXCOORD0;
45 float3 worldNormal : TEXCOORD1;
46 float3 worldPos : TEXCOORD2;
47 };
48
49 sampler2D _MainTex;
50 float4 _MainTex_ST;
51 fixed4 _Color;
52 // 用于决定调用clip函数时进行的透明度测试使用的判断条件
53 fixed _AlphaScale;
54
55 v2f vert (a2v v)
56 {
57 v2f o;
58
59 o.pos = UnityObjectToClipPos(v.vertex);
60 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
61 o.worldNormal = UnityObjectToWorldNormal(v.normal);
62 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
63
64 return o;
65 }
66
67 fixed4 frag (v2f i) : SV_Target
68 {
69 fixed3 worldNormal = normalize(i.worldNormal);
70 fixed3 worldPos = normalize(i.worldPos);
71 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
72 // 纹素值
73 fixed4 texColor = tex2D(_MainTex, i.uv);
74 // 反射率
75 fixed3 albedo = texColor.rgb * _Color.rgb;
76 // 环境光
77 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
78 // 漫反射
79 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
80 // 返回颜色,透明度部分乘以我们设定的值
81 return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
82 }
83 ENDCG
84 }
85
86 Pass
87 {
88 Tags { "LightMode"="ForwardBase" }
89
90 // 只渲染前面
91 Cull Back
92 // 关闭深度写入
93 ZWrite Off
94 // 开启混合模式,并设置混合因子为SrcAlpha和OneMinusSrcAlpha
95 Blend SrcAlpha OneMinusSrcAlpha
96
97 CGPROGRAM
98 #pragma vertex vert
99 #pragma fragment frag
100
101 #include "UnityCG.cginc"
102 #include "Lighting.cginc"
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 float2 uv : TEXCOORD0;
115 float3 worldNormal : TEXCOORD1;
116 float3 worldPos : TEXCOORD2;
117 };
118
119 sampler2D _MainTex;
120 float4 _MainTex_ST;
121 fixed4 _Color;
122 // 用于决定调用clip函数时进行的透明度测试使用的判断条件
123 fixed _AlphaScale;
124
125 v2f vert (a2v v)
126 {
127 v2f o;
128
129 o.pos = UnityObjectToClipPos(v.vertex);
130 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
131 o.worldNormal = UnityObjectToWorldNormal(v.normal);
132 o.worldPos = mul(unity_ObjectToWorld, v.vertex);
133
134 return o;
135 }
136
137 fixed4 frag (v2f i) : SV_Target
138 {
139 fixed3 worldNormal = normalize(i.worldNormal);
140 fixed3 worldPos = normalize(i.worldPos);
141 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
142 // 纹素值
143 fixed4 texColor = tex2D(_MainTex, i.uv);
144 // 反射率
145 fixed3 albedo = texColor.rgb * _Color.rgb;
146 // 环境光
147 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
148 // 漫反射
149 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
150 // 返回颜色,透明度部分乘以我们设定的值
151 return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
152 }
153 ENDCG
154 }
155 }
156 }