透明是游戏中经常要使用的一种效果。在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道(Alpha Channel)。当开启透明混合后,当一个物体被渲染到屏幕上时,每个片元除了颜色值和深度值之外,它还有另一个属性一一透明度。当透明度为1时,表示该像素是完全不透明的,而当其为0 时,则表示该像素完全不会显示。
在Unity 中,我们通常使用两种方法来实现透明效果:第一种是使用透明度测试(Alpha Test),这种方法其实无法得到真正的半透明效果;另一种是透明度混合(Alpha Blending ) 。
在之前的学习中,我们从没有强调过渲染顺序的问题。也就是说,当场景中包含很多模型时,我们并没有考虑是先渲染A,再渲染B,最后再渲染C,还是按照其他的顺序来渲染。事实上,对于不透明(opaque)物体,不考虑它们的渲染顺序也能得到正确的排序效果,这是由于强大的深度缓冲( depth buffer,也被称为z-buffer)的存在。在实时渲染中,深度缓冲是用于解决可见(visibility)
问题的,它可以决定哪个物体的哪些部分会被渲染在前面,而哪些部分会被其他物体遮挡。它的基本思想是: 根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时, 需要把它的深度值和已经存在于深度缓冲中的值进行比较(如果开启了深度测试〉,如果它的值距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上〈有物体挡住了它);否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)。
使用深度缓冲,可以让我们不用关心不透明物体的渲染顺序,例如A 挡住B,即便我们先渲染A 再渲染B 也不用担心B 会遮盖掉A,因为在进行深度测试时会判断出B 距离摄像机更远,也就不会写入到颜色缓冲中。但如果想要实现透明效果,事情就不那么简单了,这是因为, 当使用透明度混合时,我们关闭了深度写入( ZWrite )。
简单来说,透明度测试和透明度混合的基本原理如下。
- 透明度测试:它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阀值〉,那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等。也就是说,透明度测试是不需要关闭深度写入的,它和其他不透明物体最大的不同就是它会根据透明度来舍弃一些片元。虽然简单,但是它产生的效果也很极端,要么完全透明,即看不到,要么完全不透明,就像不透明物体那样。
- 透明度混合: 这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入(我们下面会讲为什么需要关闭),这使得我们要非常小心物体的渲染顺序。需要注意的是,透明度混合只关闭了深度写入,但没有关闭深度测试。这意味着, 当使用透明度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲中的深度值,如果它的深度值距离摄像机更远,那么就不会再进行混合操作。这一点决定了,当一个不透明物体出现在一个透明物体的前面,而我们先渲染了不透明物体,它仍然可以正常地遮挡住透明物体。也就是说,对于透明度混合来说,深度缓冲是只读的。
8.1 为什么渲染顺序很重要
我们来考虑最简单的情况。假设场景里有两个物体A 和B , 如图8.1 所示,其中A 是半透明物体,而B 是不透明物体。
![](https://i-blog.csdnimg.cn/blog_migrate/1bb077f13582cee08711ad983a92b69d.png)
- 第一种情况,我们先渲染B,再渲染A。那么由于不透明物体开启了深度测试和深度检验,而此时深度缓冲中没有任何有效数据,因此B 首先会写入颜色缓冲和深度缓忡。随后我们渲染A,透明物体仍然会进行深度测试,因此我们发现和B 相比A 距离摄像机更近,因此,我们会使用A 的透明度来和颜色缓冲中的B 的颜色进行混合,得到正确的半透明效果。
- 第二种情况,我们先渲染A,再渲染B。渲染A 时,深度缓冲区中没有任何有效数据,因此A 直接写入颜色缓冲,但由于对半透明物体关闭了深度写入,因此A 不会修改深度缓冲。等到渲染B 时, B 会进行深度测试,它发现,"咦,深度缓存中还没有人来过,那我就放心地写入颜色缓冲了!”,结果就是B 会直接覆盖A 的颜色。从视觉上来看, B 就出现在了A 的前面,而这是错误的。
答案是肯定的。还是假设场景里有两个物体A 和B ,如图8.2 所示,其中A 和B 都是半透明物体。
![](https://i-blog.csdnimg.cn/blog_migrate/f72a7dc6538c9b25b1bc6f59a5c1e045.png)
- 第一种情况,我们先渲染B ,再渲染A。那么B 会正常写入颜色缓冲,然后A 会和颜色缓冲中的B 颜色进行混合, 得到正确的半透明效果。
- 第二种情况,我们先渲染A, 再渲染B。那么A 会先写入颜色缓冲,随后B 会和颜色缓冲中的A 进行混合,这样混合结果会完全反过来,看起来就好像B 在A 的前面,得到的就是错误的半透明结构。
(1)先渲染所有不透明物体,并开启它们的深度测试和深度写入。
(2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
那么,问题都解决了吗?不幸的是,仍然没有。在一些情况下,半透明物体还是会出现“穿帮镜头”。如果我们仔细想想的话,上面给出的第2 步中渲染顺序仍然是含糊不惰的一一“按它们距离摄像机的远近进行排序”,那么它们距离摄像机的远近是如何决定的呢?读者可能会马上脱口而出,“就是距离摄像的深度值嘛! ”但是,深度缓冲中的值其实是像素级别的,即每个像素有一个深度值,但是现在我们对单个物体级别进行排序,这意味着排序结果是,要么物体A 全部在B 前面渲染,要么A 全部在B 后面渲染。但如果存在循环重叠的情况,那么使用这种方法就永远无法得到正确的结果。图8.3 给出了3 个物体循环重叠的情况。
![](https://i-blog.csdnimg.cn/blog_migrate/0badd76aaca81c974f70b29896615d28.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c414d04363968bc0a5cedaf8433b9d87.png)
尽管结论是,总是会有一些情况打乱我们的阵脚,但由于上述方法足够有效并且容易实现,因此大多数游戏引擎都使用了这样的方法。为了减少错误排序的情况,我们可以尽可能让模型是凸面体,并且考虑将复杂的模型拆分成可以独立排序的多个子模型等。其实就算排序错误结果有时也不会非常糟糕,如果我们不想分割网格,可以试着让透明通道更加柔和,使穿插看起来并不是那么明显。我们也可以使用开启了深度写入的半透明效果来近似模拟物体的半透明(详见8.5 节〉。
下面,我们就来看一下Unity 是如何解决排序问题的。
8.2 Unity Shader的渲染顺序
![](https://i-blog.csdnimg.cn/blog_migrate/989cf716442e12b323f9de15b88a5fdf.png)
如果我们想要通过透明度混合来实现透明效果,代码中应该包含类似下面的代码:
SubShader { Tags { ”Queue”=”Alpha Test”} Pass { …… } }
其中, ZWrite Off 用于关闭深度写入,在这里我们选择把它写在Pass 中。我们也可以把它写在SubShader 中,这意味着该
SubShader { Tags { ”Queue”=”Transparent”} Pass { ZWrite Off …… } }
8.3 透明度测试
我们来看一下如何在Unity 中实现透明度测试的效果。在上面我们已经知道了透明度测试的工作原理。透明度测试:只要一个片元的透明度不满足条件〈通常是小于某个阀值〉,那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它。
通常,我们会在片元着色器中使用clip 函数来进行透明度测试。clip 是CG 中的一个函数,它的定义如下。
函数: void clip(float4 x); void clip(float3 x); void clip(float2 x); void clip(float1 x); void clip(float x);
参数:裁剪时使用的标量或矢量条件。
描述:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色。它等同于下面的代码:
-
void clip (float4 x)
-
{
-
if (any(x<O))
-
discard;
-
}
在本节中,我们使用图8.5 中的半透明纹理来实现透明度测试。在本书资源中,该纹理名为transparent_texture.psd。该透明纹理在不同区域的透明度也不同,我们通过它来查看透明度测试的效果。
![](https://i-blog.csdnimg.cn/blog_migrate/dff1c53f7d9d479abed15cb50f088fb9.png)
![](https://i-blog.csdnimg.cn/blog_migrate/668479a95d9f79f2b008d04387a810ad.png)
(1 )在Unity 中新建一个场景。在本书资源中,该场景名为Scene_8_3 。在Unity 5.2 中,默认情况下场景将包含一个摄像机和一个平行光,并且使用了内置的天空盒子。在Window ->Lighting -> Skybox 中去掉场景中的天空盒子。
(2 )新建一个材质。在本书资源中,该材质名为AlphaTestMat。
(3 )新建一个Unity Shader。在本书资源中,该Unity Shader 名为Chapter8-AlpbaTest 。把新的Unity Shader 赋给第2 步中创建的材质。
(4 )在场景中创建一个立方体,并把第2 步中的材质赋给该模型。创建一个平面,使得平面位于立方体下面。
(5 )保存场景。
打开新建的Chapter8-AlpbaTest,删除所有己有代码,并进行如下修改。
Shader "Unity Shaders Book/Chapter 8/Alpha Test" {
(2 )为了在材质面板中控制透明度测试时使用的阀值,我们在Properties 语义块中声明一个范围在[0, 1]之间的属性 _Cutoff:
-
Properties {
-
_Color (
"Color Tint", Color) = (
1,
1,
1,
1)
-
_MainTex (
"Main Tex",
2D) =
"white" {}
-
_Cutoff (
"Alpha Cutoff", Range(
0,
1)) =
0.5
-
}
_Cutoff 参数用于决定我们调用clip 进行透明度测试时使用的判断条件。它的范围是[0, 1 ], 这是因为纹理像素的透明度就是在此范围内。
(3)然后,我们在SubShader 语义块中定义了一个Pass 语义块:
-
SubShader {
-
Tags {
"Queue"=
"AlphaTest"
"IgnoreProjector"=
"True"
"RenderType"=
"TransparentCutout"}
-
-
Pass {
-
Tags {
"LightMode"=
"ForwardBase" }
我们在8.2 节中已经知道渲染顺序的重要性,并且知道在Unity 中透明度测试使用的渲染队列是名为AlphaTest 的队列,因此我们需要把Queue 标签设置为AlphaTest。而RenderType 标签可以让Unity 把这个Shader 归入到提前定义的组(这里就TransparentCutout 组〉中,以指明该Shader 是一个使用了透明度测试的Shader , RenderType 标签通常被用于着色器替换功能。我们还把IgnoreProjector 设置为True,这意味着这个Shader 不会受到投影器(Projectors )的影响。通常,使用了透明度测试的Shader 都应该在SubShader 中设置这三个标签。最后, LightMode 标签是Pass标签中的一种,它用于定义该Pass 在Unity 的光照流水线中的角色。只有定义了正确的LightMode,我们才能正确得到一些Unity 的内置光照变量,例如 _LightColor0 .
( 4 )然后,我们使用CGPROGRAM 和ENDCG 来包围住CG 代码片,来定义最重要的顶点着色器和片元着色器代码。首先,我们使用#pragma 指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。在本例中,它们的名字分别是vert 和frag:
![](https://i-blog.csdnimg.cn/blog_migrate/1afd3a01f899167936b3a6e1c5f20f60.png)
由于 _Cutoff 的范围在[0, 1 ],因此我们可以使用fixed 精度来存储它。
(7 )然后,我们定义了顶点着色器的输入和输出结构体,接着定义顶点着色器:
-
struct a2v {
-
float4 vertex : POSITION;
-
float3 normal : NORMAL;
-
float4 texcoord : TEXCOORD0;
-
};
-
-
struct v2f {
-
float4 pos : SV_POSITION;
-
float3 worldNormal : TEXCOORD0;
-
float3 worldPos : TEXCOORD1;
-
float2 uv : TEXCOORD2;
-
};
-
-
v2f vert(a2v v) {
-
v2f o;
-
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
-
o.worldNormal = UnityObjectToWorldNormal(v.normal);
-
-
o.worldPos = mul(_Object2World, v.vertex).xyz;
-
-
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
-
-
return o;
-
}
上面的代码我们已经见到过很多次了,我们在顶点着色器计算出世界空间的法线方向和顶点位置以及变换后的纹理坐标,再把它们传递给片元着色器。
(8 )最重要的透明度测试的代码在片元着色器中:
-
fixed4 frag(v2f i) : SV_Target {
-
fixed3 worldNormal = normalize(i.worldNormal);
-
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
-
-
fixed4 texColor = tex2D(_MainTex, i.uv);
-
-
// Alpha test
-
clip (texColor.a - _Cutoff);
-
// Equal to
-
// if ((texColor.a - _Cutoff) < 0.0) {
-
// discard;
-
// }
-
-
fixed3 albedo = texColor.rgb * _Color.rgb;
-
-
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
-
-
fixed3 diffuse = _LightColor0.rgb * albedo * max(
0, dot(worldNormal, worldLightDir));
-
-
return fixed4(ambient + diffuse,
1.0);
-
}
前面我们已经提到过clip 函数的定义,它会判断它的参数,即texColor.a - _Cutoff是否为负数,如果是就会舍弃该片元的输出。也就是说,当texColor.a 小于材质参数Cutoff 时,该片元就会产生完全透明的效果。使用clip 函数等同于先判断参数是否小于零,如果是就使用discard 指令来显式剔除该片元。后面的代码和之前使用过的完全一样,我们计算得到环境光照和漫反射光照,把它们相加后再进行输出。
(9 )最后,我们需要为这个Unity Shader 设置合适的Fallback:
FallBack "Transparent/Cutout/VertexLit"
和之前使用的Diffuse 和Specular 不同,这次我们使用内置的Transparent/Cutout/VertexLit 来作为回调Shader. 这不仅能够保证在我们编写的SubShader 无法在当前显卡上工作时可以有合适的代替Shader,还可以保证使用透明度测试的物体可以正确地向其他物体投射阴影,具体原理可以参见9.4.5 节。
材质面板中的Alpha cutoff参数用于调整透明度测试时使用的阀值, 当纹理像素的透明度小于该值时,对应的片元就会被舍弃。当我们逐渐调大该值时,立万体上的网格会逐渐消失,如图8.7 所示。
![](https://i-blog.csdnimg.cn/blog_migrate/96cc09a0e987e2fc0120ac9f97d8cf88.png)
从图8.6 和图8.7 可以看出,透明度测试得到的透明效果很“极端”一一要么完全透明,要么 完全不透明,它的效果往往像在一个不透明物体上挖了一个空洞。而且,得到的透明效果在边缘 处往往参差不齐,有锯齿,这是因为在边界处纹理的透明度的变化精度问题。为了得到更加柔滑 的透明效果,就可以使用透明度混合。
8.4 透明度混合
透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。
为了进行混合,我们需要使用Unity 提供的混合命令——Blend。 Blend 是Unity 提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合,混合时使用的函数就是由该指令决定的。表8.2 给出了Blend 命令的语义。
![](https://i-blog.csdnimg.cn/blog_migrate/8d070307cdcd99b13d85ec23c2cadeee.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4bfa9fb332759854e7dce338e401c8a2.png)
我们使用和8.3 节中同样的透明纹理,在学习完本节后,我们可以得到类似图8.8 这样的放果。
为了在Unity 中实现透明度混合,我们先进行如下准备工作。
( 1 〉在Unity 中新建一个场景。在本书资源中,该场景名为Scene_8_4 。在Unity 5.2 中,默认情况下场景将包含一个摄像机和一个平行光,并且使用了内置的天空盒子。在Window -> Lighting-> Skybox 中去掉场景中的天空盒子。
(2)新建一个材质。在本书资源中,该材质名为AlphaBlendMat。
(3)新建一个Unity Shader . 在本书资源中,该Unity Shader 名为Chapter8-AlphaB!end。把新的Unity Shader 赋给第2 步中创建的材质。
(4) 在场景中创建一个立方体,并把第2 步中的材质赋给该模型。创建一个平面,使得平面位于立方体下面。
(5)保存场景。
打开新建的Chapter8-AlphaBlend,删除所有已有代码,并把8.3 节的Chapter8-AlphaTest 代码全部粘贴进去,我们只需要在这个基础上进行一些修改即可。
![](https://i-blog.csdnimg.cn/blog_migrate/b5a8b79eea4b750f83b741486addb450.png)
(4 )修改片元着色器:
-
fixed4 frag(v2f i) : SV_Target {
-
fixed3 worldNormal = normalize(i.worldNormal);
-
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
-
-
fixed4 texColor = tex2D(_MainTex, i.uv);
-
-
fixed3 albedo = texColor.rgb * _Color.rgb;
-
-
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
-
-
fixed3 diffuse = _LightColor0.rgb * albedo * max(
0, dot(worldNormal, worldLightDir));
-
-
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
-
}
上述代码和8.3 节中的几乎完全一样,只是移除了透明度测试的代码,并设置了该片元着色器返回值中的透明通道, 它是纹理像素的透明通道和材质参数 _AlphaScale 的乘积。正如本节一开始所说的, 只有使用Blend 命令打开混合后, 我们在这里设置透明通道才有意义, 否则,这些透明度并不会对片元的透明效果有任何影响。( 5 )最后, 修改Unity Shader 的Fallback:
FallBack "Transparent/VertexLit"
我们可以调节材质面板上的Alpha Scale 参数,以控制整体透明度。图8.9 给出了不同AlphaScale 参数下的半透明效果。![](https://i-blog.csdnimg.cn/blog_migrate/3053efce1df0a316e7410f23c35b979b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d54ba015ab94b4d5a51bca155e07a5b2.png)
8.5 开启深度写入的半透明效果
![](https://i-blog.csdnimg.cn/blog_migrate/cf8a84f00f3fe5d8f5217d9cfce33a2b.png)
(1)在Unity 中新建一个场景。在本书资源中,该场景名为Scene_8_5 。在Unity 5.2 中, 默认情况下场景将包含一个摄像机和一个平行光, 并且使用了内置的天空盒子。在Wmdow -> Lighting -> Skybox 中去掉场景中的天空盒子。
( 2 )新建一个材质。在本书资源中, 该材质名为AlphaBlendZWriteMat 。
( 3 )新建一个Unity Shader。在本书资源中,该Unity Shader 名为Chapter8-AlphaBlendZWrite .把新的Unity Shader 赋给第2 步中创建的材质。
( 4 )在场景中创建一个立方体, 并把第2 步中的材质赋给该模型。创建一个平面, 使得平面位于立方体下面。
( 5 )保存场景。
本节使用的代码和8.4 节使用的Chapter8-AlphaBlend 几乎完全一样。我们把Chapter8-AlphaBlend 中的代码粘贴到本节的
-
Shader
"Unity Shaders Book/Chapter 8/Alpha Blending With ZWrite" {
-
Properties {
-
_Color (
"Color Tint", Color) = (
1,
1,
1,
1)
-
_MainTex (
"Main Tex",
2D) =
"white" {}
-
_AlphaScale (
"Alpha Scale", Range(
0,
1)) =
1
-
}
-
SubShader {
-
Tags {
"Queue"=
"Transparent"
"IgnoreProjector"=
"True"
"RenderType"=
"Transparent"}
-
-
// Extra pass that renders to depth buffer only
-
Pass {
-
ZWrite On
-
ColorMask
0
-
}
-
-
Pass {
-
// 和8.4 节同样的代码
-
}
-
}
-
FallBack
"Transparent/VertexLit"
-
}
这个新添加的Pass 的目的仅仅是为了把模型的深度信息写入深度缓冲中, 从而剔除模型中被自身遮挡的片元。因此, Pass 的第一行开启了深度写入。在第二行, 我们使用了一个新的渲染命令——ColorMask。在ShaderLab 中, ColorMask 用于设置颜色通道的写掩码(write mask)。它的语义如下:
ColorMask RGB | A | O | 其他任何R、G、B、A 的组合
当ColorMask 设为0 时,意味着该Pass 不写入任何颜色通道,即不会输出任何颜色。这正是我们需要的一一该Pass 只需写入深度缓存即可。
8.6 ShaderLab 的混合命令
在8.4 一节中,我们已经看到如何利用Blend 命令进行混合。实际上,混合还有很多其他用处,不仅仅是用于透明度混合。在本节里,我们将更加详细地了解混合中的细节问题。我们首先来看一下混合是如何实现的。当片元着色器产生一个颜色的时候,可以选择与颜色缓存中的颜色进行混合。这样一来,混合就和两个操作数有关:源颜色(source color)和目标颜色( destination color )。源颜色,我们用S 表示,指的是由片元着色器产生的颜色值; 目标颜色,我们用D 表示,指的是从颜色缓冲中读取到的颜色值。对它们进行混合后得到的输出颜色,我们用O表示,它会重新写入到颜色缓冲中。需要注意的是,当我们谈及混合中的源颜色、目标颜色和输出颜色时,它们都包含了RGBA 四个通道的值,而并非仅仅是RGB 通道。
想要使用混合,我们必须首先开启它。在Unity 中,当我们使用Blend (Blend Off 命令除外〉命令时,除了设置混合状态外也开启了混合。但是, 在其他图形API 中我们是需要手动开启的。例如在OpenGL 中,我们需要使用glEnable(GL_BLEND)来开启混合。但在Unity 中,它已经在背后为我们做了这些工作。
8.6.1 混合等式和参数
在2.3.8 节中我们提到过,混合是一个逐片元的操作,而且它不是可编程的, 但却是高度可配置的。也就是说,我们可以设置混合时使用的运算操作、混合因子等来影响混合。那么,这些配置又是如何实现的呢?现在,我们已知两个操作数: 源颜色S 和目标颜色D,想要得到输出颜色O就必须使用一个等式来计算。我们把这个等式称为混合等式( blend equation )。当进行混合时,我们需要使用两个混合等式: 一个用于混合RGB 通道,一个用于混合A 通道。当设置混合状态时,我们实际上设置的就是混合等式中的操作和因子。在默认情况下,混合等式使用的操作都是加操作(我们也可以使用其他操作〉,我们只需要再设置一下混合因子即可。由于需要两个等式(分别用于混合RGB 通道和A 通道〉,每个等式有两个因子(一个用于和源颜色相乘, 一个用于和目标颜色相乘〉,因此一共需要4 个因子。表8.3 给出了ShaderLab 中设置混合因子的命令。
![](https://i-blog.csdnimg.cn/blog_migrate/fd55c6c61961647fa3401095af23a977.png)
![](https://i-blog.csdnimg.cn/blog_migrate/54eed60f0406bf8d65a630d59d0bac19.png)
Blend SrcAlpha OneMinusSrcAlpha, One Zero
8.6.2 混合操作
在上面涉及的混合等式中,当把源颜色和目标颜色与它们对应的混合因子相乘后,我们都是把它们的结果加起来作为输出颜色的。那么可不可以选择不使用加法,而使用减法呢?答案是肯定的,我们可以使用ShaderLab 的BlendOp BlendOperation 命令,即混合操作命令。表8.5 给出了ShaderLab 中支持的混合操作。![](https://i-blog.csdnimg.cn/blog_migrate/1085ea1297679c091a30b6c854c3b93b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/28280d455fa57445960394e09c65d9da.png)
8.6.3 常见的混合类型
通过混合操作和混合因子命令的组合, 我们可以得到一些类似Photoshop 混合模式中的混合效果:![](https://i-blog.csdnimg.cn/blog_migrate/bffdad4c5b4f3c079877fb43bbe0c396.png)
![](https://i-blog.csdnimg.cn/blog_migrate/875cf84d9c070df06ed0fc71ab9b742f.png)
8.7 双面渲染的透明效果
Cull Back | Front | Off
如果设置为Back,那么那些背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态;如果设置为Front , 那么那些朝向摄像机的渲染图元就不会被撞染; 如果设置为
Off,就会关闭剔除功能, 那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是用于特殊效果, 例如这里的双面渲染的透明效果,通常情况是不会关闭剔除功能的。
8.7.1 透明度测试的双面渲染
我们首先来看一下,如何让使用了透明度测试的物体实现双面渲染的效果。这非常简单,只需要在Pass 的渲染设置中使用Cull 指令来关闭剔除即可。为此, 我们新建了一个场景, 在本章资源中, 该场最名为Scene_8_7_1 ,场景中同样包含了一个正方体,它使用的材质和Unity Shader 分别名为AlphaTestBothSidedMat 和Chapter8-AlphaTestBothSided。 Chapter8-AlphaTestBothSided的代码和8.3 节中的Chapter8-AlphaTest 几乎完全一样, 只添加了一行代码:
-
Pass {
-
Tags {
"LightMode"=
"ForwardBase" }
-
-
// Turn off culling
-
Cull Off
如上所示, 这行代码的作用是关闭剔除功能,使得该物体的所有的渲染图元都会被渲染。由此,我们可以得到图8.13 中的效果。
![](https://i-blog.csdnimg.cn/blog_migrate/04bb6727eb3b34d7de9ac27af007e70f.png)
8.7.2 透明度混合的双面渲染
和透明度测试相比,想要让透明度混合实现双面渲染会更复杂一些, 这是因为透明度混合需要关闭深度写入, 而这是“一切混乱的开端”。我们知道, 想要得到正确的透明效果,渲染顺序是非常重要的一一我们想要保证图元是从后往前渲染的。对于透明度测试来说, 由于我们没有关闭深度写入, 因此可以利用深度缓冲按逐像素的粒度进行深度排序,从而保证渲染的正确性。然而一旦关闭了深度写入,我们就需要小心地控制渲染顺序来得到正确的深度关系。如果我们仍然采用8.7.1 节中的方法,直接关闭剔除功能, 那么我们就无法保证同一个物体的正面和背面图元的渲 染顺序,就有可能得到错误的半透明效果。我们新建了一个场景,在本章资源中,该场最名为Scene_8_7_2,场景中包含了一个正方体,它使用的材质和Unity Shader 分别名为AlphaBlendBothSidedMat 和Chapter8-AlphaBlendBothSided。相较于8.4 节的Chapter8-AlphaBlend,我们对
( 1)复制原Pass 的代码, 得到另一个Pass 。
( 2)在两个Pass 中分别使用Cull 指令剔除不同朝向的渲染图元:
-
Shader
"Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" {
-
Properties {
-
_Color (
"Color Tint", Color) = (
1,
1,
1,
1)
-
_MainTex (
"Main Tex",
2D) =
"white" {}
-
_AlphaScale (
"Alpha Scale", Range(
0,
1)) =
1
-
}
-
SubShader {
-
Tags {
"Queue"=
"Transparent"
"IgnoreProjector"=
"True"
"RenderType"=
"Transparent"}
-
-
Pass {
-
Tags {
"LightMode"=
"ForwardBase" }
-
-
// First pass renders only back faces
-
Cull Front
-
-
// 和之前一样的代码
-
}
-
-
Pass {
-
Tags {
"LightMode"=
"ForwardBase" }
-
-
// Second pass renders only front faces
-
Cull Back
-
-
// 和之前一样的代码
-
}
-
}
-
FallBack
"Transparent/VertexLit"
-
}
通过上面的代码,我们可以得到图8.14 中的效果。
![](https://i-blog.csdnimg.cn/blog_migrate/b4392671b272bf4454e14c004336bfe3.png)