UnityShader精要笔记七 透明效果 附RenderType

本文继续对《UnityShader入门精要》——冯乐乐 第八章 透明效果 进行学习

一、深度测试和深度写入

在之前的学习中,我们从来没有强调过渲染顺序的问题。也就是说,当场景中包含很多模型时,我们没有考虑是先渲染A,再渲染B,最后再渲染C,还是按照其他的顺序来渲染。

事实上,对于不透明(opaque)物体,不考虑它们的渲染顺序也能得到正确的排序效果,这是由于强大的深度缓冲(depth buffer,也被称为z-buffer)的存在。

在实时渲染中,深度缓冲是用于解决可见性(visibility)问题的,它可以决定哪些物体哪些部分会被渲染在前面,而哪些部分又会被其它物体遮挡。它的基本思想是:根据深度缓冲中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲区中的值进行比较(如果开启了深度测试),如果它的值距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);否则这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)。

借用一下第二章的图

使用深度缓冲,可以使我们不必关心不透明物体的渲染顺序,例如A挡住了B,即便我们先渲染A再渲染B也不用担心B会遮盖掉A,因为在进行深度测试时会判断出B距离摄像机更远,也就不会写入到颜色缓冲中。但要实现透明效果,事情就不那么简单了,这是因为当使用透明度混合时,我们关闭了深度写入(ZWrite)。

那么我们为什么要关闭深度写入呢?如果不关闭深度写入,一个半透明表面背后的表面本来是可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面将会被剔除,我们也就无法透过半透明表面看到后面的物体了。但是,我们由此就破坏了深度缓冲的工作机制,这是一个非常糟糕的事情,尽管我们不得不这么做。关闭深度写入导致渲染顺序将变得非常重要。

1.不同渲染顺序例一
图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,如下图所示,其中A和B都是半透明物体。

2.不同渲染顺序例二
图8.2 场景中有两个物体,其中A和B都是半透明物体

第一种情况,我们先渲染B,再渲染A。那么B会正常写入颜色缓冲,然后A会和颜色缓冲中的B颜色进行混合,得到正确的混合结果。

第二种情况,我们先渲染A,再渲染B。那么A会先写入颜色缓冲,随后B会和颜色缓冲中的A进行混合,这样混合结果会完全反过来,看起来好像B在A的前面,得到的就是错误的半透明结构。

从这个例子可以看出,半透明物体之间也是要符合一定的渲染顺序的。

3.解决方案

基于上面两点,渲染引擎一般都会先对物体进行排序,再渲染,常用的方法是:
(1)先渲染所有不透明物体,并开启它们的深度测试和深度写入。
(2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。

那么,问题都解决了吗?不幸的是,让然没有。在一些情况下,半透明物体还是会出现穿帮镜头。如果我们仔细想想的话,上面第二步中的渲染顺序仍然是含糊不清的——按它们距离摄像机的远近进行排序,那么它们距离摄像机的远近是如何决定的呢?读者可能会马上脱口而出,“就是距离摄像的深度值嘛”。但是深度缓冲中的值其实是像素级别的,即每个像素都有一个深度值,但是现在我们对单个物体级别进行排序,这意味着排序的结果是,要么物体A全部在B前面渲染,要么A全部在B后面渲染。但如果存在循环重叠的情况,那么使用这种方法就永远无法得到正确的结果。图8.3给出了3个物体循环重叠的情况。

图8.3 循环重叠的半透明物体总是无法得到正确的半透明效果

在图中,由于3个物体互相重叠,我们不可能得到一个正确的排序顺序。这种时候,我们可以选择把物体拆分成两个部分,然后再进行正确的排序。但即便我们通过了分割的方法解决了循环覆盖的问题,还会有其它情况来捣乱,如下图所给出的情况:


图8.4 使用哪个深度对物体进行排序。红色点分别标明了网格上距离摄像机最近的点、最远的点以及网格中点

这里的问题是如何排序?我们知道,一个物体的网格结构往往占据了空间中的某一块区域,也就是说,这个网格上每一个点的深度值可能都是不一样的,我们选择哪个深度值来作为整个物体的深度值和其它物体进行排序呢?是网格中点吗?还是最远的点?还是最近的点?不幸的是,对于上图的情况,选择哪个深度值都会得到错误的结果,我们的排序结果总是A在B的前面,但实际上A有一部分被B遮挡了。这也意味着,一旦选定了一种判断方式后,在某些情况下半透明物体之间一定会出现错误的遮挡问题。这种问题的解决方法通常也是分割网格。

尽管结论是,总是会有一些情况打乱我们的阵脚,但由于上述方法足够有效并且容易实现,因此大多数游戏引擎都使用了这样的方法。为了减少错误排序的情况,我们可以尽可能让模型是凸面体,并且考虑将复杂的模型拆分成可以独立排序的多个子模型等。其实就算排序错误结果有时也不会非常糟糕,如果我们不想分割网格,可以试着让透明通道更加柔和,使穿插看起来并不是那么明显。我们也可以使用开启了深度写入的半透明效果来近似模拟物体的半透明(详见8.5节)。

二、透明度测试和透明度混合

在Unity中,我们通常使用两种方式来实现透明效果:第一种是使用透明度测试(Alpha Test),这种方法其实无法得到真正的半透明效果;另一种是透明度混合(Alpha Blending)。

1.透明度测试

它是一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等。也就是说,透明度测试是不需要关闭深度写入的,它和其它不透明物体最大的不同是它会根据透明度来舍弃一些片元。虽然简单,但是它产生的效果也很极端,要么完全透明,即看不到,要么完全不透明,就像不透明物体那样。

2.透明度混合

这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色进行混合,得到新的颜色。但是透明度混合需要关闭深度写入,这使我们要非常小心物体的渲染顺序。需要注意的是,透明度混合只关闭了深度写入,但没有关闭深度测试。这意味着当使用透明度深度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲区中的深度值,如果它的深度值距离摄像机更远,那么就不会再进行混合操作。这一点决定了,当一个不透明物体出现在一个透明物体前面,而我们先渲染了不透明物体,它仍然可以正常地遮住不透明物体。也即是说,对于透明混合度来说,深度缓冲是只读的。

三、UnityShader的渲染队列

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

image.png

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

SubShader{
   Tags{"Queue"="AlphaTest"}
   Pass{
      ...
   }
}

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

SubShader{
   Tags{"Queue"="Transparent"}
   Pass{
      ZWrite Off
      ......
   }
}

其中,ZWrite Off用于关闭深度写入,在这里我们选择把它写在Pass中。我们也可以把它写在SubShader中,这意味着该SubShader下的所有Pass都会关闭深度写入。

四、透明度测试Alpha Test

我们来看一下如何在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<0))
      discard;
   }
}

在本节中,我们使用图中的半透明纹理来实现透明度测试。该透明纹理在不同区域的透明度也不同,我们通过它来查看透明度测试的效果。


图8.5 一张透明纹理,其中每个方格的透明度都不同

图8.6 透明度测试效果

示例代码参考Chapter8-AlphaTest.shader,这个例子并没有使用法线贴图,就是简单的单纹理,大部分代码可以参考UnityShader精要笔记六 基础纹理,下面将不同的地方展示一下。

1._Cutoff
Shader "Unity Shaders Book/Chapter 8/Alpha Test" {
    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],这是因为纹理像素的透明度就是在此范围内。

2.Queue
SubShader{
   Tags{"Queue"="AlphaTest""IgnoreProjector"="True" "RenderType"="TransparentCutout"}
   Pass{
      Tags{"LightMode"="ForwardBase"}
   }
}

我们在前面已经知道了渲染的重要性,并且知道在Unity中透明度使用的渲染队列是名为AlphaTest的队列,因此我们需要把Queue标签设置为AlphaTest。

而RenderType标签可以让Unity把这个shader归入到提前定义的组(这里就是TransparentCutout组)中,以指明该shader是一个使用了透明度测试的shader。RenderType标签通常被用于着色器替换功能。

我们还把IgnoreProjector设置为True,这意味着这个Shader不会受到投影器(Projectors)的影响。

通常,使用了透明度测试的Shader都应该在SubShader中设置这三个标签。

3.frag中的clip
fixed4 frag(v2f i):SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor=tex2D(_MainTex,i.uv);
//Alpha text
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 fixed(ambient+diffuse,1.0);
}

前面我们已经提到过clip函数的定义,它会判断它的参数,即TeXColor.a-_Cutoff是否为负数,如果是就会舍弃该片元的输出。也就是说,当texColor.a小于材质参数_Cutoff时,该片元就会产生完全透明的效果。

4.VertexLit

最后,我们需要为这个UnityShader设置合适的Fallback:

Fallback"Transparent/Cutout/VertexLit"

和之前使用的Diffuse和Specular不同,这次我们使用内置的Transparent/Cutout/VertexLit来作为回调Shader。这不仅能够保证我们编写的SubShader无法在当前显卡上工作时可以有合适的替代Shader,还可以保证使用透明度测试的物体可以正确的向其他物体投射阴影,具体原理可以参见9.4.5节。

材质面板中的Alpha cutoff参数用于调整透明度测试时使用的阈值,当纹理像素的透明度小于该值时,对应的片元就会被舍弃。当我们逐渐调大该值时,立方体上的网格就会消失,如下图所示:


图8.7 随着Alpha cutoff参数的增大,更多的像素由于不满足透明度测试条件而被剔除

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

五、透明度混合Alpha Blending

透明度混合的实现要比透明度测试复杂一些,这是因为我们在处理透明度测试时,实际上跟对待普通的不透明的物体几乎是一样的,只是在片元着色器中增加了对透明度的判断并裁剪片元的代码。而想要实现透明度混合就没有这么简单了。我们回顾之前提到的透明度混合的原理:

透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲区中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,设使得我们要非常小心物体的渲染顺序。

为了进行混合,我们需要使用Unity提供的混合命令——Blend。Blend是Unity提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合,混合时使用的函数就是由该指令决定的。下表给出了Blend命令的语义:


image.png

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


image.png

注:OneMinusSrcAlpha英文单词的意思就是1-SrcAlpha

通常透明度的混合使用的就是这样的混合命令,在8.6节,我们会看到更多混合语义用法。
我们使用和8.3节中同样的透明纹理,可以得到下面的效果:


图8.8 透明度混合
Shader "Unity Shaders Book/Chapter 8/Alpha Blend" {
    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" }

            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
...
            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);
            }

我们使用一个新的属性_AlphaScale来代替原先的_Cutoff属性。_AlphaScale用于在透明纹理的基础上控制整体的透明度。参考最后一句:return fixed4(ambient + diffuse, texColor.a * _AlphaScale);

别的代码和之前类似,不说了。

我们可以调节材质面板上的AlphaScale参数,以控制整体透明度。下图给出了不同AlphaScale参数下的半透明效果。


图8.9 随着Alpha Scale参数的增大,模型变得越来越透明

我们在以前解释了由于关闭深度写入带来的各种问题。当模型本身有复杂的遮挡关系或是包含了复杂的非凸格网格时候,就会有各种各样因为排序错误而产生的错误的透明效果。下图给出了使用UnityShader渲染Knot模型时得到的效果。


图8.10 当模型网格之间有互相交叉的结构时,往往会得到错误的半透明效果

这都是由于我们关闭了深度写入造成的,因为这样我们就无法对模型进行像素级别的深度排序。在上面我们提出了一种解决方法就是分割网格,从而可以得到一个“质量优等”的网格。但是很多情况下这往往时不切实际的。这是我们可以想办法重新利用深度写入,让模型可以像半透明物体一样进行淡入淡出。这就是我们下面要讲的内容。

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

在上一节,我们给出了一种由于关闭深度写入而造成的错误排序情况。一种解决方法是使用两个Pass来渲染模型:

  • 第一个Pass开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;
  • 第二个Pass进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。

但这种方法的缺点在于,多使用一个Pass会对性能造成一定的影响。在本节最后,我们可以得到类似下图的效果:


图8.11 开启了深度写入的半透明效果

可以看出,使用这种方法,我们仍然可以实现模型与它后面的背景混合的效果,但模型内部之间不会有任何真正的半透明效果。

参考Chapter8-AlphaBlendZWrite.shader,代码和AlphaBlend几乎一样,我们只需在原来的基础上再增加一个新的Pass即可。

    SubShader {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
        
        // Extra pass that renders to depth buffer only
        Pass {
            ZWrite On
            ColorMask 0
        }

这个新添加的Pass的目的仅仅是为了把模型的深度信息写入深度缓冲中,从而剔除模型中被自身遮挡的片元。因此,Pass的第一行开启了深度写入。在第二行,我们使用了一个新的渲染命令——ColorMask。在ShaderLab中,ColorMask用于设置颜色通道的写掩码(write mask)。它的语义如下:

ColorMask RGB|A|0|其它任何R、G、B、A的组合

当ColorMask设为0时,意味着该Pass不写入任何颜色通道,即不会输出任何颜色。这正是我们需要的——该Pass只需写入深度缓存即可。

七、ShaderLab的混合命令

在前面,我们已经看到如何利用Blend命令进行混合。实际上,混合还有很多其他的用处,不仅仅是用于透明度混合。在本节里,我们将更加详细的了解混合中的细节问题。

我们首先来看一下混合时如何实现的。当片元着色器产生一个颜色的时候,可以选择与颜色缓冲中的颜色进行混合。这样一来,混合就和两个操作数有关:源颜色(source color)和目标颜色(destination color)。源颜色,我们用S表示,指的是由片元着色器产生的颜色值;目标颜色,我们用D表示,指的是从颜色缓冲中读取到的颜色值。对它们进行混合后得到的输出颜色,我们用O表示,它会重新写入到颜色缓冲中。我们需要注意的是,当我们谈及混合中的源颜色、目标颜色和输出颜色时,它们都包含了RGBA四个通道的值,而并非仅仅是RGB通道。


图2.16 混合操作的简化流程图

想要使用混合,我们必须首先开启它。在Unity中,我们使用Blend(Blend Off命令除外)命令时,除了设置混合状态外也开启了混合。但是,在其他图形API中我们是需要手动开启的。例如在OpenGl中,我们需要使用glEnable(GL_BLEND)来开启混合。但在Unity中,它已经在背后为我们做了这些工作。

1.混合等式和参数

现在,我们已知两个操作数:源颜色S和目标颜色D,想要得出输出颜色O就必须使用一个等式来计算。我们把这个等式称为混合等式(blend equation)。进行混合时,我们需要两个混合等式:一个用于混合RGB通道,一个用于混合A通道。当设置混合状态时,我们实际上设置的就是混合等式中的操作和因子。在默认情况下,混合等式使用的操作都是加操作(我们也可以使用其它操作),我们只需再设置一下混合因子即可。由于需要混合两个等式(分别用于混合RGB通道和A通道),每个等式有两个因子(一个用于和源颜色相乘,一个用于和目标颜色相乘),因此一共需要四个因子。下表给出了ShaderLab中设置混合因子的命令。


image.png

可以发现,第一个命令只提供了两个因子,这意味着将使用同样的混合因子来混合RGB通道和A通道,即此时SrcFactorA将等于SrcFactor,DstFactorA将等于DstFactor。下面就是使用这些因子进行加法混合时使用的混合公式:


image.png

那么,这些混合因子可以由哪些值呢?下表给出了ShaderLab支持的几种混合因子:
image.png

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

Blend SrcAlpha OneMinusSrcAlpha, One Zero
2.混合操作

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


image.png

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

3.常见的混合类型

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

//正常(Normal),即透明度混合
Blend SrcAlpha OneMinusSrcAlpha
//柔和相加(soft Additive)
Blend OneMinusDstColor One
//正片叠底(Multiply),即相乘
Blend DstColor Zero
//两倍相乘(2x Multiply)
Blend DstColor SrcColor
//变暗(Darken)
BlendOp Min
Blend One One
//变亮(Lighten)
BlendOp Max
Blend One One
//滤色(Screen)
Blend OneMinusDstColor One
//等同于
Blend One OneMinusSrcColor
//线性减淡(Linear Dodge)
Blend One One
图8.12 不同混合状态设置得到的效果

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

八、双面渲染的透明效果

在现实生活中,如果一个物体是透明的,意味着我们不仅可以透过它看到其它物体的样子,也可以看到它的内部结构。但在前面实现的透明效果中,无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的形状,导致物体看起来好像只有半个一样。

这是因为,默认情况下,渲染引擎剔出了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,可以使用Cull指令来控制需要剔除哪个面的渲染图元。在Unity中,Cull指令的语法如下:

Cull Back| Front | Off

如果设置为Back,那么那些背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态;如果设置为Front,那么那些朝向摄像机的渲染图元就不会被渲染;如果设置为Off,就会关闭剔除功能,那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是用于特殊效果,例如这里的双面渲染的透明效果,通常情况下是不会关闭剔除功能的。

cull这个单词就是剔除的意思

1.透明度测试的双面渲染

非常简单,只需在Pass的渲染设置中使用Cull指令来关闭剔除即可。

Pass{
Tags{"LightMode"="ForwardBase"}
//Turn off culling
Cull Off
}

如上所示,这行代码的作用是关闭剔除功能,是的该物体的所有渲染图元都会被渲染。由此,我们可以得到下图的效果:


图8.13 双面渲染的透明度测试的物体
2.透明度混合的双面渲染

和透明度测试相比,想要让透明度混合实现双面渲染会更复杂一些,这是因为透明度混合需要关闭深度写入,而这是“一切混乱的开端”。我们知道,想要得到正确的透明效果,渲染顺序是非常重要的——我们想要保证图元是从后往前渲染的。对于透明度测试来说,由于我们没有关闭深度写入,因此可以利用深度缓冲按逐像素的粒度进行深度排序,从而保证渲染的正确性。然而一旦关闭了深度写入,我们就需要小心的控制渲染顺序来得到正确的深度关系,如果我们仍然采样上面的方法,直接关闭剔除功能,那么我们就无法保证同一个物体的正面和背面的渲染顺序,就有可能得到错误的半透明效果。

为此,我们选择把双面渲染的工作分成两个Pass——第一个Pass只渲染背面,第二个Pass只渲染正面,由于Unity会顺序执行SubShader中的各个Pass,因此我们可以保证背面总是在正面渲染之前渲染,从而可以保证正确的深度渲染关系。

参考Chapter8-AlphaBlendBothSided.shader

Properties{
_Color("Main 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"

这里我交换了顺序,先Cull Back再Cull Front,就是右边那个效果,可以发现变暗了。


image.png

这个渲染顺序是有影响的,虽然是一个透明效果,但仍然应该先渲染背面,再渲染正面。

九、RenderType

在上面的内容中,出现了RenderType标签,但冯乐乐介绍的比较简短:

RenderType标签可以让Unity把这个shader归入到提前定义的组(这里就是TransparentCutout组)中,以指明该shader是一个使用了透明度测试的shader。RenderType标签通常被用于着色器替换功能。

参考UnityShader RenderType&Queue 理解

RenderType通常使用的值包括:

  • Opaque(不透明的): most of the shaders (Normal, Self Illuminated自发光, Reflective, terrain shaders).
  • Transparent: most semitransparent(半透明) shaders (Transparent, Particle, Font, terrain additive pass shaders).
  • TransparentCutout: masked transparency shaders (Transparent Cutout, two pass vegetation shaders).
  • Background: Skybox shaders.
  • Overlay: GUITexture, Halo, Flare shaders.
  • TreeOpaque: terrain engine tree bark.
  • TreeTransparentCutout: terrain engine tree leaves.
  • TreeBillboard: terrain engine billboarded trees.
  • Grass: terrain engine grass.
  • GrassBillboard: terrain engine billboarded grass.

这些RenderType的类型名称实际上是一种约定,用来区别这个Shader要渲染的对象,当然你也可以改成自定义的名称,只不过需要自己区别场景中不同渲染对象使用的Shader的RenderType的类型名称不同,也就是说RenderType类型名称使用自定义的名称并不会对该Shader的使用和着色效果产生影响。

指定RenderType的名称,主要是为了配合使用替代渲染的方法:

Camera.SetReplacementShader("shader","RenderType")  

在使用替代渲染方法时,相机会使用指定的 shader 来代替场景中的其他 shader 对场景进行渲染。比如现在有 shader1:

Shader "shader1"{
    Properties{...}
    SubShader{
    Tags{"RenderType"="Opaque"}
    Pass{...}   
    }
    SubShader{
    Tags{"RenderType"="Transparent"}
    Pass{...}   
    }
}

场景中一部分物体当前使用的是 shader2:

Shader "shader2"{
    Properties{...}
    SubShader{
    Tags{"RenderType"="Opaque"}
    Pass{...}   
    }
}

另一部分使用的是 shader3:

Shader "shader3"{
    Properties{...}
    SubShader{
    Tags{"RenderType"="Transparent"}
    Pass{...}   
    }
}

调用替代渲染的方法:

Camera.SetReplacementShader("shader1","")   

这种情况下,场景中所有的物体就都使用shader1进行渲染(当Shader中包含多个SubShader,在渲染时显卡根据性能从上到下选择第一个能支持的shader)
如果在调用时,第二个参数不为空字符串,即:

Camera.SetReplacementShader("shader1","RenderType")   

这种情况下,首先在场景中找到标签中包含该字符串(这里为"RenderType")的shader,再去看标签中的该字符串的值与shader1中包含该字符串的值是否一致,一致的话,替换渲染,否则不渲染;由于shader2中包含"RenderType"="Opaque",而且shader1中的第一个SubShader中包含"RenderType"="Opaque",因此将shader1中的第一个SubShader替换场景中的所有shader2,同理,将shader1中的第二个SubShader替换场景中的所有的shader3。

如果shader1为:

Shader "shader1"{
    Properties{...}
    SubShader{
    Tags{"RenderType"="Opaque" "A"="On"}
    Pass{...}   
    }
    SubShader{
    Tags{"RenderType"="Transparent"  "A"="Off"}
    Pass{...}   
    }
}   

shader2为:

Shader "shader2"{
    Properties{...}
    SubShader{
    Tags{"RenderType"="Opaque" "A"="On"}
    Pass{...}   
    }
}  

shader3为:

Shader "shader3"{
    Properties{...}
    SubShader{
    Tags{"RenderType"="Transparent" "A"="On"}
    Pass{...}   
    }
}      

替代渲染的调用方式为:

Camera.SetReplacementShader("shader1","A")      

最后的结果是,shader1的第一个SubShader将会替换shader2和shader3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容