转载自 冯乐乐的 《Unity Shader入门精要》
透明是游戏中经常要使用的一种效果。在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道。当开启透明混合后,当一个物体被渲染到屏幕上时,每个片元出了颜色值和深度值之外,它还有另一个属性——透明度。当透明度为1时,表示该像素是完全不透明的,而当其为0时,则表示该像素完全不会显示。
在Unity中,我们通常使用两种方法来实现透明效果:第一种是使用透明度测试,这种方法其实无法得到真正的半透明效果;另一种是透明度混合。
在之前的学习中,我们从没有强调过渲染顺序的问题。也就是说,当场景中包含很多模型时,我们并没有考虑是先渲染A,再渲染B,最后再渲染C。还是按照其他的顺序来渲染。事实上,对于不透明物体,不考虑它们的渲染顺序也能得到正确的排序效果,这是由于强大的深度缓冲的存在。在实时渲染中,深度缓冲是用于解决可见性问题的,他可以决定哪个物体的哪些部分会被渲染在前面,而哪些部分会被其他物体遮挡。它的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较(如果开启了深度测试),如果它的值距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)。
使用深度缓冲,可以让我们不用关心不透明物体的渲染顺序,例如A挡住B,即便我们先渲染A再渲染B也不用担心B会遮掉A,因为在进行深度测试时会判断出B距离摄像机更远,也就不会写入到颜色缓冲中。但如果想要实现透明效果,事情就不那么简单了,这是因为,当用透明度混合时,我们关闭了深度写入(ZWrite)。
简单来说,透明度测试和透明度混合的基本原理如下:
透明度测试:它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等。也就是说,透明度测试是不需要关闭深度写入的,它和其他不透明物体最大的不同就是它会根据透明度来舍弃一些片元。虽然简单,但是它产生的效果也很极端,要么完全透明,即看不到,要么完全不透明。
透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。需要注意的是,透明度混合只关闭了深度写入,但没有关闭深度测试。这意味着,当使用透明度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲中的深度值,如果它的深度值距离摄像机更远,那么久不会再进行混合操作了。这一点决定了,当一个不透明物体出现在一个透明物体的前面,而我们先渲染了不透明物体,它仍然可以正常地遮挡住透明物体。也就是说,对于透明度混合来说,深度缓冲是只读的。
为什么渲染顺序很重要
前面说到,对于透明度混合技术,需要关闭深度写入,此时我们就需要小心处理透明物体的渲染顺序。那么,我们为什么要关闭深度写入呢?如果不关闭深度写入,一个半透明表面背后的表面本来可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面会被剔除,我们也就无法透过半透明表面看到后面的物体了。但是我们由此就破坏了深度缓冲的工作机制,而这是一个非常非常非常糟糕的事情,尽管我们不得不这样做。关闭深度写入导致渲染顺序将变得非常重要。
我们来考虑最简单的情况。假设场景里有两个物体A和B,如下图,其中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都是半透明物体。
我们还是考虑不同的渲染顺序有什么不同结果。
第一种情况,我们先渲染B,再渲染A。那么B会正常写入颜色缓冲,然后A会和颜色缓冲中的B颜色进行混合,得到正确的半透明效果。
第二种情况。我们先渲染A,再渲染B。那么A会先写入颜色缓冲,随后B会和颜色缓冲中的A进行混合,这样混合结果会完全反过来,看起来就好像B写在A的前面,得到的就是错误的半透明结构。
从这个例子可以看出,半透明物体之间也是要符合一定的渲染顺序的。
基于这两点,渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是:
1)先渲染所有不透明的物体,并开启它们的深度测试和深度写入。
2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
那么,问题都解决了么?不幸的是,仍然没有。在一些情况下,半透明物体还是会出现“穿帮镜头”。如果我们仔细想想的话,上面给出的第2步中渲染顺序仍然是含糊不清的——“按它们距离摄像机的远近进行排序”,那么它们距离摄像机的远近是如何渲染的呢?读者可能会马上脱口而出,“就是距离摄像机的深度值”,但是,深度缓冲中的值其实是像素级别的,即每个像素有一个深度值,但是现在我们对单个物体级别进行排序,这意味着排序结果是,要么物体A全部在B前面渲染,那么A全部在B后面渲染。但如果存在循环重叠的话,那么使用这种方法就永远无法得到正确的结果。如下图所示:
在上图中,3个物体互相重叠,我们无法得到一个正确的排序顺序。这个时候,我们可以选择把物体拆成两个部分,然后再进行正确的排序。但即便我们通过分割的方法解决了循环覆盖的问题,还是会有其他的情况来“捣乱”。如下图
这里的问题是:如何排序?我们知道,一个物体的网格结构往往占据了空间中的某一块区域,也就是说,这个网格上每一个点的深度值可能都是不一样的,我们选择哪个深度值来作为整个物体的深度值和其他物体进行排序呢?是网格中点吗?还是最远的点?还是最近的点?不幸的是,对于上图的情况,选择哪个深度值都会得到错误的结果,我们的排序结果总是A在B的前面,但实际上A有一部分被B遮挡住了。这也意味着,一旦选定了一种判断方式后,在某些情况下半透明的物体之间一定会出现错误的遮挡问题。这种问题的解决方法通常也是分割网格。
尽管结论是,总是会有一些情况打乱我们的阵脚,但由于上述方法足够有效并且容易实现,因此大多数游戏引擎都使用了这样的方法。为了减少错误排序的情况,我么可以尽可能让模型是凸面体,并且考虑将复杂的模型拆分成可以独立排序的多个子模型等。其实就算排序错误结果有时也不会非常糟糕,如果我们不想分割网格,可以试着让透明通道更加柔和,使穿插看起来并不是那么明显。我们也可以使用开启了深度写入写入的半透明效果来近似模拟物体的半透明。下面,我们就来看一下Unity是如何解决问题的。
Unity Shader 的渲染顺序
Unity为了解决渲染顺序的问题提供了渲染队列这一解决方案。我们可以使用SubShader 的Queue 标签来决定我们的模型将归于哪个渲染队列。Unity 在内部使用了一系列整数索引来表示每个渲染队列,且索引号越小表示越早被渲染。在Unity 5 中,Unity 提前定义了5个渲染队列,当然在每个队列中间我们可以使用其他队列。下表给出了这5个提前定义的渲染队列以及它们的描述。
因此,如果我们想要通过透明度测试实现透明效果,代码中应该包含类似下面的代码:
- SubShader{
- Tags { "Queue" = "AlphaTest"}
- Pass{
- }
- }
如果我们想要通过透明度混合来实现透明效果,代码中应该包含类似下面的代码:
- SubShader{
- Tags { "Queue" = "Transparent"}
- Pass{
- ZWrite Off
- }
- }
其中,ZWrite Off 用于关闭深度写入,在这里我们选择把它写在Pass 中。我们也可以把它写在SubShader 中,这意味着该SubShader 下的所有的Pass 都会关闭深度写入。
透明度测试
我们来看一下如何在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;
- }
我们先使用下图中的半透明纹理来实现透明度测试。该透明纹理在不同区域的透明度也不相同,我们通过它来查看透明度测试的效果。
我们通过Shader的处理后,可以得到类似下图的结果。
为此,我们新建一个Shader。
- SubShader "Unity Shaders Book/Chapter 8/ Alpha Test" {
- Properties {
- _Color ("Main Tint", Color) = (1,1,1,1)
- _MainTex("Main Tex", 2D) = "white" {}
- //用于决定我们调用clip进行透明度测试时使用的判断条件
- _Cutoff ("Alpha Cutoff", Range(0,1)) = 0.5
- }
- SubShader{
- //第一个tag 把Queue标签设置为AlphaTest
- //而RenderType 标签可以让Unity把这个Shader归入到提前定义的组以指明该Shader 是
- //一个使用 了透明度测试的Shader(RenderType标签通常用于着色器替换功能)
- //IgnoreProjector 设置为True,这意味着这个Shader 不会受到投影器的影响。
- //通常,使用了透明度测试的Shader 都应该在SubShader 中设置这三个标签
- Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
- Pass {
- Tags {"LightMode" = "ForwardBase"}
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cgnic"
- fixed4 _Color;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- fixed _Cutoff;
- 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(v.texcoord,_MainTex);
- return o;
- }
- fixed4 frag(v2f i) : SV_Target{
- fixed3 worldNormal = normalize(i.worldNormal);
- fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
- fixed4 texColor = tex2D(_MainTex, i.uv);
- //透明度测试
- clip(texColor.a - _Cutoff);
- 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);
- }
- }
- }
- Fallback "Transparent/Cutout/VertexLit"
- }
和之前使用的Diffuse 和 Specular 不同,这次我们使用内置的Transparent/Cutout/VertexLit 来作为回调Shader ,这不仅能够保证再我们编写的SubShader 无法在当前显卡工作时可以由适合的代码Shader,还可以保证使用透明度测试的物体可以正确地向其他物体投射阴影。
材质面板中的Alpha cutoff 参数用于调整透明度测试时使用的阈值,当纹理像素的透明度小于该值时,对应的片元就会被舍弃。当我们逐渐调大该值时,立方体上的网格会逐渐消失,如下图所示,
透明度混合
透明度混合的实现要比透明度测试复杂一些,这是因为我们在处理透明度测试时,实际上跟对待普通的不透明物体几乎是一样的。只是在片元着色器中增加了对透明度判断并裁剪片元的代码。而想要实现透明度混合就没有这么简单了。
透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓存中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。
为了进行混合,我们需要使用Unity 提供的混合命令——Blend。Blend 是Unity 提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合,混合时使用的函数就是由该指令决定的。下表给出了Blend命令的语义。
我们先使用第二种语义,即Blend SrcFactor DstFactor 来进行混合。需要注意的是,这个命令在设置混合因子的同时也开启了混合模式。这是因为,只有开启了混合之后,设置片元的透明通道才有意义,而Unity 在我们使用Blend 命令的时候就自动帮我们打开了。很多人总是抱怨为什么自己的模型没有任何透明效果,这往往是因为他们没有在Pass 中使用Blend 命令,一方面是没有设置混合因子,但更重要的是,根本没有打开混合模式。我们会把源颜色的混合因子SrcFactor 设置为SrcAlpha ,而目标颜色的混合因子DstFactor 设为 OneMinusSrcAlpha。这意味着,经过混合后新的颜色是:
通常,透明度混合使用的就是这样的混合命令。
我们使用和透明度测试一样的透明纹理,使用新的Shader后,可以得到如下图的效果。
我们新建一个Shader完成上述效果
- Shader "Unity Shaders Book/Chapter 8/Alpha Blend" {
- 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"}
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cgnic"
- fixed4 _Color;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- //用于在透明纹理的基础上控制整体的透明度
- fixed _AlphaScale;
- struct a2v{
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- float4 texcoord : TEXCOORD0;
- }
- struct v2f{
- float4 pos : SV_POSTION;
- 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;
- }
- 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);
- }
- }
- }
- Fallback "Transparent/VertexLit"
- }
我们可以调节材质面板上的Alpha Scale 参数,以控制整体透明度。下图给出了不同Alpha Scale 参数下的半透明效果。
我们之前详细解释了由于关闭深度写入带来的各种问题。当模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样因为排序错误而产生的错误的透明效果。下图给出了使用上面的Unity Shader 渲染Knot 模型时得到的效果。
这都是由于我们关闭了深度写入造成的,因为这样我们就无法对模型进行像素级别的深度排序。在之前我们提到了一种解决方法是分割网格,从而可以得到一个“质量优等”的网格。但是很多情况下这往往是不切实际的。这时,我们可以想办法重新利用深度写入,让模型可以像半透明问题一样进行淡入淡出。
开启深度写入的半透明效果
在之前的学习中,我们给出了一种由于关闭深度写入而造成的错误排序的情况。一种解决方法是使用两个Pass 来渲染模型:第一个Pass 开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个Pass 进行正常的透明度混合,由于上一个Pass 已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。但这种方法的缺点在于,多使用一个Pass 会对性能造成一定的影响。我们使用这种方法,我们仍然可以实现模型与它后面的 背景混合的效果,但模型内部之间不会有任何真正的半透明效果。如下图所示:
我们新建一个Shader 实现上述效果
- Shader "Unity Shader Book/Chapter 8-Alpha Blending ZWrite"{
- 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的目的仅仅是为了把模型的深度信息写入到深度缓冲中,
- //从而剔除模型中被自身遮挡的片元。因此先开启深度写入
- //然后我们使用了一个新的渲染名利——ColorMask。在ShaderLab 中,ColorMask
- //用于设置颜色通道的写掩码,为0时表示不写入任何颜色。
- Pass{
- ZWrite On
- ColorMask 0
- }
- Pass{
- ags {"LightMode"="ForwardBase"}
- ZWrite Off
- Blend SrcAlpha OneMinusSrcAlpha
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cgnic"
- fixed4 _Color;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- //用于在透明纹理的基础上控制整体的透明度
- fixed _AlphaScale;
- struct a2v{
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- float4 texcoord : TEXCOORD0;
- }
- struct v2f{
- float4 pos : SV_POSTION;
- 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;
- }
- 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);
- }
- }
- }
- Fallback "Diffuse"
- }
ShaderLab 的混合命令
我们先来看下混合时如何实现的。当片元着色器产生一个颜色的时候,可以选择与颜色缓存中的颜色进行混合。这样一来,混合就和两个操作数有关:源颜色和目标颜色。源颜色,我们用S表示,指的是由片元着色器产生的颜色值;目标颜色,我们用D表示,值得是从颜色缓冲区中读取到的颜色值。对它们进行混合后得到的输出颜色,我们用O表示,它会重新写入到颜色缓冲中。需要注意的是,当我们谈及混合中的源颜色、目标颜色和输出颜色时,它们都包含RGBA四个通道的值,而并非仅仅是RGB通道。
想要使用混合,我们必须首先开启它。在Unity中,当我么使用Blend (Blend Off 除外)命令时,除了设置混合状态外也开启了混合。但是,在其他图形API中我们是需要手动开启的。例如在OpenGL 中,我们需要使用glEnable(GL_BLEND)来开启混合。但在Unity中,它已经在背后为我们做了这些工作了。
我们之前提到,混合是一个逐片元的操作,而且它不是可编程的,但却是高度可配置的。也就是说,我们可以设置混合时使用的运算操作、混合银子等来影响混合。那么,这些配置又是如何实现的呢?
现在,我们已知两个操作数:源颜色S和目标颜色D,想要得到输出颜色O就必须使用一个等式来计算。我们把这个等式称为混合等式。当进行混合时,我们需要使用两个混合等式:一个用于混合RGB通道,一个用于混合A通道。当设置混合状态时,我们实际上设置的就是混合等式中的操作和因子。在默认情况下,混合等式使用的操作都是加啊哦做,我们只需要再设置一下混合因子即可。由于需要两个等式(分别用于混合RGB通道和A通道),每个等式有两个因子(一个用于和源颜色相乘,一个用于和目标颜色相乘),因此一共需要4个因子。下表给出了ShaderLab 中设置混合因子的命令。
可以发现,第一个命令只提供了两个因子,这意味着将使用同样的混合因子来混合RGB通道和A通道,即此时SrcFactorA将等于SrcFactor,DstFactorA 将等于DstFactor 。下面就是使用这些因子进行加法混合时使用的混合公式:
下表给出了Shaderl 支持的集中混合因子。
使用上面的指令进行设置时,RGB通道的混合因子和A通道的混合因子都是一样的,有时我们希望可以使用不同的参数混合A通道,这时就可以利用Blend SrcFactor DstFactor,SrcFactorA DstFactorA 指令。例如,如果我们想要在混合后,输出颜色的透明度值就是源颜色的透明度,可以使用下面的命令
- Blend SrcAlpha OneMinusSrcAlpha, One Zero
在上面涉及的混合等式中,当把源颜色和目标颜色与它们对应的混合因子相乘后,我们都是把它们的结果加起来作为输出颜色的。那么可不可以选择不使用加法,而使用减法呢?答案是肯定的,我们可以使用ShaderLab 的BlendOp BlendOperation 命令,即混合操作命令。下表给出了ShaderLab 中支持的混合操作。
混合操作的命令通常是与混合因子命令一起工作的。但需要注意的是,当使用Min或Max 混合操作时,混合因子实际上是不起任何作用的,它们仅会判断原始的源颜色和目标颜色之间的比较结果。
通过混合操作和混合因子命令的组合,我们可以得到一些类似Photoshop 混合模式中的混合效果:
- // 正常(Normal),即透明度混合
- Blend SrcAlpha OneMinusSrcAlpha
- // 柔和相加(Soft Additive)
- Blend OneMinusDstColor One
- //正片叠底(Multiply),即相乘
- Blend DstColor Zero
- //两倍相乘
- 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
下图给出了上面不同设置下得到的结果。
需要注意的是,虽然上面使用Min 和 Max 混合操作时仍然设置了混合因子,但实际上它们并不会对结果有任何影响,因为Min 和 Max 混合操作会忽略混合因子。另一点是,虽然上面有些混合模式并没有设置混合操作的种类,但是它们默认就是使用加法操作,相当于设置了BlendOp Add。
双面渲染的透明效果
在现实生活中,如果一个物体是透明的,意味着我们不仅可以透过它看到其他物体的样子,也可以看到它内部的结构。但在前面实现的透明效果中,无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,我们使用Cull 指令来控制需要剔除哪个面的渲染图元。
在Unity 中,Cull 指令的语法如下:
- Cull Back | Front | Off
如果设置为Back,那么那些背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态:如果设置为Front,那么那些朝向摄像机的渲染图元就不会被渲染;如果设置为Off,就会关闭剔除功能,那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是用于特殊效果,例如这里的双面渲染的透明效果,通常情况下是不会关闭剔除功能的。
我们首先来看一下,如果让使用了透明度测试的物体实现双面渲染效果。这非常简单,只需要再Pass的渲染设置中使用Cull 指令来关闭剔除即可。我们在Pass 中添加一行代码
- Pass{
- Tags {"LightMode"="ForwardBase"}
- Cull Off
- }
如上所示,这行代码的作用是关闭剔除功能,使得物体的 所有渲染图元都被渲染,由此,我们可以得到下图的结果。
和透明度测试相比,想要让透明度混合实现双面渲染会更复杂一些,这是因为透明度混合需要关闭深度写入,而这是“一切混乱的开端”。我们知道,想要得到正确的透明效果,渲染顺序是非常重要的——我们想要保证图元是从后往前渲染的。对于透明度测试来说,由于我们没有关闭深度写入,由此可以利用深度缓冲按逐像素的粒度进行深度排序,从而保证渲染的正确性。然后一旦关闭了深度写入,我们就需要小心第控制渲染顺序来得到正确的深度关系。如果我们仍然采用之前的方法。直接关闭剔除功能,我们就无法保证同一个物体的正面和背面图元的渲染顺序,就有可能得到错误的半透明效果。
为此,我们选择把双面渲染的工作分成两个Pass——第一个Pass只渲染背面,第二个Pass只渲染正面,由于Unity会顺序执行SubShader 中的各个Pass,因此我们可以保证背面总是在正面被渲染之前渲染,从而可以保证正确的深度渲染关系。
我们新建一个Shader实现上述功能。
- Shader "Unity Shaders Book/Chapter8/Alpha Blend With Both Side"{
- 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"}
- Cull Front
- //和之前一样的代码
- }
- Pass{
- Tags{"LightMode"="ForwardBase"}
- Cull Back
- //和之前一样的代码
- }
- }
- Fallback "Transparent/VertexLit"
- }
通过上面的代码,我们可以得到如下图中的效果