第8章 透明效果

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/e295166319/article/details/78904581

透明是游戏中经常要使用的一种效果。在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道(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 为什么渲染顺序很重要

前面说到,对于透明度混合技术,需要关闭深度写入,此时我们就需要小心处理透明物体的渲染顺序。那么,我们为什么要关闭深度写入呢?如果不关闭深度写入,一个半透明表面背后的表面本来是可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面将会被剔除,我们也就无法透过半透明表面看到后面的物体了。但是,我们由此就破坏了深度缓冲的工作机制,而这是一个非常非常非常(重要的事情要讲3 遍〉糟糕的事情,尽管我们不得不这样做。关闭深度写入导致渲染顺序将变得非常重要。
我们来考虑最简单的情况。假设场景里有两个物体A 和B , 如图8.1 所示,其中A 是半透明物体,而B 是不透明物体。
我们来考虑不同的渲染顺序会有什么结果。
  •  第一种情况,我们先渲染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 都是半透明物体。

我们还是考虑不同的渲染顺序有什么不同结果。
  •  第一种情况,我们先渲染B ,再渲染A。那么B 会正常写入颜色缓冲,然后A 会和颜色缓冲中的B 颜色进行混合, 得到正确的半透明效果。
  •  第二种情况,我们先渲染A, 再渲染B。那么A 会先写入颜色缓冲,随后B 会和颜色缓冲中的A 进行混合,这样混合结果会完全反过来,看起来就好像B 在A 的前面,得到的就是错误的半透明结构。
从这个例子可以看出,半透明物体之间也是要符合一定的渲染顺序的。
基于这两点,渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是。
(1)先渲染所有不透明物体,并开启它们的深度测试和深度写入。
(2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
那么,问题都解决了吗?不幸的是,仍然没有。在一些情况下,半透明物体还是会出现“穿帮镜头”。如果我们仔细想想的话,上面给出的第2 步中渲染顺序仍然是含糊不惰的一一“按它们距离摄像机的远近进行排序”,那么它们距离摄像机的远近是如何决定的呢?读者可能会马上脱口而出,“就是距离摄像的深度值嘛! ”但是,深度缓冲中的值其实是像素级别的,即每个像素有一个深度值,但是现在我们对单个物体级别进行排序,这意味着排序结果是,要么物体A 全部在B 前面渲染,要么A 全部在B 后面渲染。但如果存在循环重叠的情况,那么使用这种方法就永远无法得到正确的结果。图8.3 给出了3 个物体循环重叠的情况。
在图8.3 中,由于3 个物体互相重叠,我们不可能得到一个正确的排序顺序。这种时候,我们可以选择把物体拆分成两个部分,然后再进行正确的排序。但即使我们通过分割的方法解决了循环覆盖的问题,还是会有其他的情况来”捣乱”。考虑图8.4 给出的情况。

这里的问题是:如何排序?我们知道, 一个物体的网格结构往往占据了空间中的某一块区域,也就是说,这个网格上每一个点的深度值可能都是不一样的, 我们选择哪个深度值来作为整个物体的深度值和其他物体进行排序呢?是网格中点吗?还是最远的点?还是最近的点?不幸的是,对于图8.4 中的情况,选择哪个深度值都会得到错误的结果,我们的排序结果总是A 在B 的前面,但实际上A 有一部分被B 遮挡了。这也意味着, 一旦选定了一种判断方式后,在某些情况下半透明物体之间一定会出现错误的遮挡问题。这种问题的解决方法通常也是分割网格。
尽管结论是,总是会有一些情况打乱我们的阵脚,但由于上述方法足够有效并且容易实现,因此大多数游戏引擎都使用了这样的方法。为了减少错误排序的情况,我们可以尽可能让模型是凸面体,并且考虑将复杂的模型拆分成可以独立排序的多个子模型等。其实就算排序错误结果有时也不会非常糟糕,如果我们不想分割网格,可以试着让透明通道更加柔和,使穿插看起来并不是那么明显。我们也可以使用开启了深度写入的半透明效果来近似模拟物体的半透明(详见8.5 节〉。
下面,我们就来看一下Unity 是如何解决排序问题的。

8.2 Unity Shader的渲染顺序

Unity 为了解决渲染顺序的问题提供了渲染队列( render queue )这一解决方案。我们可以便用SubShader 的Queue 标签来决定我们的模型将归于哪个渲染队列。Unity 在内部使用一系列整数索引来表示每个渲染队列,且索引号越小表示越早被渲染。在Unity 5 中, Unity 提前定义了5个渲染队列(与Unity 5 之前的版本相比多了一个AlphaTest 渲染队列),当然在每个队列中间我们可以使用其他队列。表8.1 给出了这5 个提前定义的渲染队列以及它们的描述。

因此,如果我们想要通过透明度测试实现透明效果,代码中应该包含类似下面的代码:

  
  
  1. SubShader {
  2. Tags { ”Queue”=”Alpha Test”}
  3. Pass {
  4. ……
  5. }
  6. }
如果我们想要通过透明度混合来实现透明效果,代码中应该包含类似下面的代码:

  
  
  1. SubShader {
  2. Tags { ”Queue”=”Transparent”}
  3. Pass {
  4. ZWrite Off
  5. ……
  6. }
  7. }
其中, ZWrite Off 用于关闭深度写入,在这里我们选择把它写在Pass 中。我们也可以把它写在SubShader 中,这意味着该
SubShader 下的所有Pass 都会关闭深度写入。

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);
参数:裁剪时使用的标量或矢量条件。
描述:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色。它等同于下面的代码:

  
  
  1. void clip (float4 x)
  2. {
  3. if (any(x<O))
  4. discard;
  5. }
在本节中,我们使用图8.5 中的半透明纹理来实现透明度测试。在本书资源中,该纹理名为transparent_texture.psd。该透明纹理在不同区域的透明度也不同,我们通过它来查看透明度测试的效果。
在学习完本节后,我们可以得到类似图8.6 中的效果。
为此,我们需要进行如下准备工作。
(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,删除所有己有代码,并进行如下修改。
(1)首先,我们需要为这个Shader 起一个名字:
Shader "Unity Shaders Book/Chapter 8/Alpha Test" {
  
  
(2 )为了在材质面板中控制透明度测试时使用的阀值,我们在Properties 语义块中声明一个范围在[0, 1]之间的属性 _Cutoff:

  
  
  1. Properties {
  2. _Color ( "Color Tint", Color) = ( 1, 1, 1, 1)
  3. _MainTex ( "Main Tex", 2D) = "white" {}
  4. _Cutoff ( "Alpha Cutoff", Range( 0, 1)) = 0.5
  5. }
 _Cutoff 参数用于决定我们调用clip 进行透明度测试时使用的判断条件。它的范围是[0, 1 ], 这是因为纹理像素的透明度就是在此范围内。
(3)然后,我们在SubShader 语义块中定义了一个Pass 语义块:

  
  
  1. SubShader {
  2. Tags { "Queue"= "AlphaTest" "IgnoreProjector"= "True" "RenderType"= "TransparentCutout"}
  3. Pass {
  4. 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:

由于 _Cutoff 的范围在[0, 1 ],因此我们可以使用fixed 精度来存储它。
(7 )然后,我们定义了顶点着色器的输入和输出结构体,接着定义顶点着色器:

  
  
  1. struct a2v {
  2. float4 vertex : POSITION;
  3. float3 normal : NORMAL;
  4. float4 texcoord : TEXCOORD0;
  5. };
  6. struct v2f {
  7. float4 pos : SV_POSITION;
  8. float3 worldNormal : TEXCOORD0;
  9. float3 worldPos : TEXCOORD1;
  10. float2 uv : TEXCOORD2;
  11. };
  12. v2f vert(a2v v) {
  13. v2f o;
  14. o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
  15. o.worldNormal = UnityObjectToWorldNormal(v.normal);
  16. o.worldPos = mul(_Object2World, v.vertex).xyz;
  17. o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
  18. return o;
  19. }
上面的代码我们已经见到过很多次了,我们在顶点着色器计算出世界空间的法线方向和顶点位置以及变换后的纹理坐标,再把它们传递给片元着色器。
(8 )最重要的透明度测试的代码在片元着色器中:

  
  
  1. fixed4 frag(v2f i) : SV_Target {
  2. fixed3 worldNormal = normalize(i.worldNormal);
  3. fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
  4. fixed4 texColor = tex2D(_MainTex, i.uv);
  5. // Alpha test
  6. clip (texColor.a - _Cutoff);
  7. // Equal to
  8. // if ((texColor.a - _Cutoff) < 0.0) {
  9. // discard;
  10. // }
  11. fixed3 albedo = texColor.rgb * _Color.rgb;
  12. fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
  13. fixed3 diffuse = _LightColor0.rgb * albedo * max( 0, dot(worldNormal, worldLightDir));
  14. return fixed4(ambient + diffuse, 1.0);
  15. }
前面我们已经提到过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 所示。

从图8.6 和图8.7 可以看出,透明度测试得到的透明效果很“极端”一一要么完全透明,要么 完全不透明,它的效果往往像在一个不透明物体上挖了一个空洞。而且,得到的透明效果在边缘 处往往参差不齐,有锯齿,这是因为在边界处纹理的透明度的变化精度问题。为了得到更加柔滑 的透明效果,就可以使用透明度混合。

8.4 透明度混合

透明度混合的实现要比透明度测试复杂一些,这是因为我们在处理透明度测试时,实际上跟对待普通的不透明物体几乎是一样的,只是在片元着色器中增加了对透明度判断并裁剪片元的代码。而想要实现透明度混合就没有这么简单了。我们回顾之前提到的透明度混合的原理:
透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。
为了进行混合,我们需要使用Unity 提供的混合命令——Blend。 Blend 是Unity 提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合,混合时使用的函数就是由该指令决定的。表8.2 给出了Blend 命令的语义。

在本节里,我们会使用第二种语义,即Blend SrcFactor DstFactor 来进行混合。需要注意的是,这个命令在设置混合因子的同时也开启了混合模式。这是因为,只有开启了混合之后,设置片元的透明通道才有意义,而Unity 在我们使用Blend 命令的时候就自动帮我们打开了。很多初学者总是抱怨为什么自己的模型没有任何进明效果,这往往是因为他们没有在Pass 中使用Blend 命令,一方面是没有设置混合因子,但更重要的是,根本没有打开混合模式。我们会把源颜色的混合因子SrcFactor 设为SrcAlpha,而目标颜色的混合因子DstFactor 设为OneMinusSrcAlpha . 这意味着,经过混合后新的颜色是:

通常,透明度混合使用的就是这样的混合命令。在8.6 节中,我们会看到更多混合语义的用法。
我们使用和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 代码全部粘贴进去,我们只需要在这个基础上进行一些修改即可。


Pass 的标签仍和之前一样,即把LightMode 设为ForwardBase,这是为了让Unity 能够按前向渲染路径的方式为我们正确提供各个光照变量。除此之外,我们还把该Pass 的深度写入( ZWrite)设置为关闭状态(Off),我们在之前已经讲过为什么要这样做了。这是非常重要的。然后,我们开启并设置了该Pass 的混合模式。如在本节开头所讲的,我们将源颜色(该片元着色器产生的颜色〉的混合因子设为SrcAlpha ,把目标颜色〈已经存在于颜色缓冲中的颜色〉的混合因子设为OneMinusSrcAlpha ,以得到合适的半透明效果。
(4 )修改片元着色器:

  
  
  1. fixed4 frag(v2f i) : SV_Target {
  2. fixed3 worldNormal = normalize(i.worldNormal);
  3. fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
  4. fixed4 texColor = tex2D(_MainTex, i.uv);
  5. fixed3 albedo = texColor.rgb * _Color.rgb;
  6. fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
  7. fixed3 diffuse = _LightColor0.rgb * albedo * max( 0, dot(worldNormal, worldLightDir));
  8. return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
  9. }
上述代码和8.3 节中的几乎完全一样,只是移除了透明度测试的代码,并设置了该片元着色器返回值中的透明通道, 它是纹理像素的透明通道和材质参数 _AlphaScale 的乘积。正如本节一开始所说的, 只有使用Blend 命令打开混合后, 我们在这里设置透明通道才有意义, 否则,这些透明度并不会对片元的透明效果有任何影响。
( 5 )最后, 修改Unity Shader 的Fallback:
	FallBack "Transparent/VertexLit"
  
  
我们可以调节材质面板上的Alpha Scale 参数,以控制整体透明度。图8.9 给出了不同AlphaScale 参数下的半透明效果。
我们在8.1 节中详细解释了由于关闭深度写入带来的各种问题。当模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样因为排序错误而产生的错误的透明效果。图8.10 给出了使用上面的Unity Shader渲染Knot模型时得到的效果。
这都是由于我们关闭了深度写入造成的,因为这样我们就无法对模型进行像素级别的深度排序。在8.1 节中我们提到了一种解决方法是分割网格,从而可以得到一个“质量优等”的网格。但是很多情况下这往往是不切实际的。这时, 我们可以想办法重新利用深度写入,让模型可以像半透明物体一样进行淡入淡出。这就是我们下面要讲的内容。

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

在8.4 节最后,我们给出了一种由于关闭深度写入而造成的错误排序的情况。一种解决方法是使用两个Pass 来渲染模型: 第一个Pass 开启深度写入, 但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中; 第二个Pass 进行正常的透明度混合,由于上一个Pass 已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。但这种方法的缺点在于, 多使用一个Pass 会对性能造成一定的影响。在本节最后, 我们可以得到类似图8.11 中的效果。可以看出, 使用这种方法, 我们仍然可以实现模型与它后面的背景混合的效果, 但模型内部之间不会有任何真正的半透明效果。
为此,我们需要进行如下准备工作。
(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 中的代码粘贴到本节的
Chapter8-AlphaBlendZWrite 中,我们只需要在原来使用的Pass前面再增加一个新的Pass 即可:

  
  
  1. Shader "Unity Shaders Book/Chapter 8/Alpha Blending With ZWrite" {
  2. Properties {
  3. _Color ( "Color 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. // Extra pass that renders to depth buffer only
  10. Pass {
  11. ZWrite On
  12. ColorMask 0
  13. }
  14. Pass {
  15. // 和8.4 节同样的代码
  16. }
  17. }
  18. FallBack "Transparent/VertexLit"
  19. }
这个新添加的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 中设置混合因子的命令。

那么,这些混合因子可以有哪些值呢?表8.4 给出了ShaderLab 支持的几种混合因子。

使用上面的指令进行设置时,RGB 通道的混合因子和 A 通道的混合因子都是一样的,有时 我们希望可以使用不同的参数混合A 通道,这时就可以利用Blend SrcFactor DstFactor,  SrcFactorA DstFactorA 指令。例如,如果我们想要在混合后,输出颜色的透明度值就是源颜色的 透明度,可以使用下面的命令:
Blend SrcAlpha OneMinusSrcAlpha, One Zero
  
  

8.6.2 混合操作

在上面涉及的混合等式中,当把源颜色和目标颜色与它们对应的混合因子相乘后,我们都是把它们的结果加起来作为输出颜色的。那么可不可以选择不使用加法,而使用减法呢?答案是肯定的,我们可以使用ShaderLab 的BlendOp BlendOperation 命令,即混合操作命令。表8.5 给出了ShaderLab 中支持的混合操作。



混合操作命令通常是与混合因子命令一起工作的。但需要注意的是, 当使用 Min 或Max 混合操作时, 混合因子实际上是不起任何作用的,它们仅会判断原始的源颜色和目的颜色之间的比较结果。

8.6.3 常见的混合类型

通过混合操作和混合因子命令的组合, 我们可以得到一些类似Photoshop 混合模式中的混合效果:

图8.12 给出了上面不同设置下得到的结果。我们可以在本书资源中的Scene_8_6_3 场景中找到相关资源。

需要注意的是,虽然上面使用Min 和Max 混合操作时仍然设置了混合因子,但实际上它们并不会对结果有任何影响,因为Min 和Max 混合操作会忽略混合因子。另一点是,虽然上面有些混合模式并没有设置混合操作的种类,但是它们默认就是使用加法操作,相当于设置了BlendOp Add。

8.7 双面渲染的透明效果

在现实生活中, 如果一个物体是透明的, 意味着我们不仅可以透过它看到其他物体的样子,也可以看到它内部的结构。但在前面实现的透明效果中, 无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的形状,导致物体看起来就好像只有半个一样。这是因为,默认情况下渲染引擎剔除了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,可以使用Cull 指令来控制需要剔除哪个面的渲染图元。在Unity 中, Cull 指令的语法如下:
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 几乎完全一样, 只添加了一行代码:

  
  
  1. Pass {
  2. Tags { "LightMode"= "ForwardBase" }
  3. // Turn off culling
  4. Cull Off
如上所示, 这行代码的作用是关闭剔除功能,使得该物体的所有的渲染图元都会被渲染。由此,我们可以得到图8.13 中的效果。
此时,我们可以透过正方体的镂空区域看到内部的渲染结果。

8.7.2 透明度混合的双面渲染

和透明度测试相比,想要让透明度混合实现双面渲染会更复杂一些, 这是因为透明度混合需要关闭深度写入, 而这是“一切混乱的开端”。我们知道, 想要得到正确的透明效果,渲染顺序是非常重要的一一我们想要保证图元是从后往前渲染的。对于透明度测试来说, 由于我们没有关闭深度写入, 因此可以利用深度缓冲按逐像素的粒度进行深度排序,从而保证渲染的正确性。然而一旦关闭了深度写入,我们就需要小心地控制渲染顺序来得到正确的深度关系。如果我们仍然采用8.7.1 节中的方法,直接关闭剔除功能, 那么我们就无法保证同一个物体的正面和背面图元的渲 染顺序,就有可能得到错误的半透明效果。
为此,我们选择把双面渲染的工作分成两个Pass——第一个Pass 只渲染背面,第二个Pass只渲染正面,由于Unity 会顺序执行SubShader 中的各个Pass , 因此我们可以保证背面总是在正面被渲染之前渲染, 从而可以保证正确的深度渲染关系。
我们新建了一个场景,在本章资源中,该场最名为Scene_8_7_2,场景中包含了一个正方体,它使用的材质和Unity Shader 分别名为AlphaBlendBothSidedMat 和Chapter8-AlphaBlendBothSided。相较于8.4 节的Chapter8-AlphaBlend,我们对
Chapter8-AlphaTestBo也Sided 的代码做了两个改动。
( 1)复制原Pass 的代码, 得到另一个Pass 。
( 2)在两个Pass 中分别使用Cull 指令剔除不同朝向的渲染图元:

  
  
  1. Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" {
  2. Properties {
  3. _Color ( "Color 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. Pass {
  10. Tags { "LightMode"= "ForwardBase" }
  11. // First pass renders only back faces
  12. Cull Front
  13. // 和之前一样的代码
  14. }
  15. Pass {
  16. Tags { "LightMode"= "ForwardBase" }
  17. // Second pass renders only front faces
  18. Cull Back
  19. // 和之前一样的代码
  20. }
  21. }
  22. FallBack "Transparent/VertexLit"
  23. }
通过上面的代码,我们可以得到图8.14 中的效果。











  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值