unity Shader之——unity的透明效果

透明是很常用的一种效果,在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道。在开启透明混合后,当一个物体被渲染到屏幕上,每个片元除了颜色值和深度值以外,还有一个透明度的属性,为1表示该像素是完全不透明的,当其为0表示该像素完全不会显示。

在unity中我们通常使用两种方法来实现透明效果:第一种是透明度测试,这种方法其实完全无法得到真正的半透明效果,另一种是透明度混合。

渲染顺序问题也是很重要的,对于不透明物体,不考虑他们它们的渲染顺序也能得到正确的排序结果,这是由于强大的深度缓冲(depth buffer,也叫z-buffer)的存在,在实时渲染中,深度缓冲是用于解决可见性问题的,它可以决定哪个物体的哪些部分会被渲染在前面,而哪些部分会被其他物体遮挡。它的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把他的深度值和已经存在于深度缓冲中的值进行比较(如果开启了深度测试),如果它的值距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)。

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

简单来说,透明度测试和透明度混合的基本原理如下:

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

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

一、为什么渲染顺序很重要

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

举个例子,假设场景里有物体A和B,其中A是半透明物体,B是不透明物体,我们考虑下不同的渲染顺序会有什么结果。

unity Shader之——unity的透明效果

(1)第一种情况,我们先渲染B,再渲染A,那么由于不透明物体开启了深度测试和深度写入,而此时深度缓冲中没有任何有效数据,因此B首先会写入颜色缓冲和深度缓冲。随后我们渲染A,透明物体仍然会进行深度测试,因此我们发现和B相比A距离摄像机更近,因此我们会使用A的透明度和颜色缓冲中的B的颜色进行混合,得到正确的半透明效果。

(2)第二种情况,我们先渲染A,再渲染B。渲染A时深度缓冲区没有任何有效的数据,因此A直接写入颜色缓冲,但由于对半透明物体关闭了深度写入,因此A不会修改深度缓冲。等到渲染B时,B会进行深度测试,发现深度缓存中还没有人来过,那就放心的写入颜色缓冲了,结果就是B会直接覆盖A的颜色,从视觉上来看,B就出现在了A的前面,这是错误的。

从这个例子可以看出,当关闭了深度写入后,渲染顺序的多么重要。由此我们知道,我们应该在不透明物体渲染完后再渲染半透明物体。那么如果都是半透明物体,渲染顺序还重要吗?答案也是肯定的,还是假设场景里有两个物体A和B,如下图,其中A和B都是半透明物体。

unity Shader之——unity的透明效果

我们还是考虑不同的渲染顺序有什么不同的结果。

(1)第一种情况,我们先渲染B,在渲染A。那么B会正常写入颜色缓冲,A会和颜色缓冲中的B的颜色在进行混合,得到正确的半透明结果。

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

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

基于这两点,渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是:

(1)先渲染所有不透明的物体,并开启它们的深度测试和深度写入。

(2)把半透明物体按他们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启他们的深度测试,但关闭深度写入。

其实问题还没有完全解决。在一些情况下,半透明物体还是会出现“穿帮镜头”的情况。距离摄像机的远近排序可能大家会认为是距离摄像机的深度值。但是深度缓冲中的值其实是像素级别的,即每个像素都有一个深度值,单个物体级别进行排序还行,如果存在循环重叠的情况就永远无法得到正确的结果,这种时候如果把物体拆成两个部分,在进行正确的排序解决这个问题,也会有其他问题出现。虽然解决方案一般都是通过分割网格。

虽然总有一些问题出现,但上述方法足够有效并容易实现,大多游戏阴影都使用了这样的方法,为了减少错误排序的情况,我们尽可能让模型是凸面体,并且考虑将复杂的模型拆分成可以独立排序的多个子模型等。如果我们不想分割网格,可以试着让透明通道更近柔和,使穿插看起来并不是那么明显。后面会讲到,也可以使用开启了深度写入的半透明效果来近似模拟物体的半透明。

二、unity Shader的渲染顺序

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

unity Shader之——unity的透明效果

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

subshader{
Tags{"Quene"="AlphaTest"}
Pass{}
}

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

subshader{
Tags{"Quene"="Transparent"}
Pass{
Zwirte Off     ...
}
}

其中Zwrite Off用于关闭深度写入,在这里我们选择把他写在Pass中,我们也可以写在subshader中,这样所有pass都会关闭深度写入。

三、透明度测试

上面讲述了透明度测试的工作原理,下面我们看一下在unity中如何实现透明度测试的效果。

透明度测试:只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对于的片元就会被舍弃。被舍弃的片元将不再进行任何处理,也不会对颜色缓冲产生影响。否则就按照普通不透明物体的处理方式来处理它。通常我们会在片元着色器中使用clip函数来进行透明度测试。clip是Cg中的一个函数,它的定义如下:

void clip(floatX x);  //参数使用标量或矢量条件,如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色。

如果要在shader中使用它,需要进行以下操作:

(1)为了在材质面板中控制透明度测试时使用的阈值,在Properties语义块中声明一个范围在[0,1]之间的属性_Cutoff。

_Cutoff ("Alpha Cutoff",Range(0,1))=0.5

_Cutoff参数用于决定我们调用clip进行透明度测试时使用的判断条件,范围是0到1,这是因为纹理像素的透明度就是在此范围内。

(2)然后我们在Subshader语义块中定义了一个Pass语义块:

SubShader{

Tags{Queue="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
}

前面说过Unity在透明度测试使用的渲染队列是名为AlphaTest的队列,因此我们需要把Queue标签设置为AlphaTest。而RenderType标签可以让Unity把这个Shader归入到提前定义的组(这里就是TransparentCutout组)中,以指明该Shader是使用了透明度测试的Shader。RenderType标签通常被用于着色器的替换功能。我们还把IgnoreProjector设置为true,这意味着这个shader不会受到投影器(Projectors)的影响。通常使用透明度测试的Shader都应该在Subshader中设置这三个标签。

(3)为了和Properties语义块找那个声明的属性建立联系,需要定义和各个属性类型相匹配的变量: fixed  _Cutoff;

(4)最重要的透明度测试代码在片元着色器中:

diffTex = tex2D(_MainTex, i.uv);

clip(diffTex.a-_Cutoff);

。。。

return fixed4(ambient+diffuse,1.0);

它会判断参数如果为负数,就舍弃该片元的输出。(该片元就会产生完全透明的效果)

(5)最后需要为这个shader设置合适的fallback:这里使用的"Transparent/Cutout/VertexLit",保证使用透明度测试的物体可以正确的向其他物体投射阴影。

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

四、透明度混合

透明度混合的实现要比透明度测试复杂一些,这种方法可以得到真正的半透明效果,他会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是透明度混合需要关闭深度写入(一会说为什么要关闭),这使得我们非常要小心物体的渲染顺序。

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

下面给出了Blend命令的语义:

unity Shader之——unity的透明效果

这里我们会使用第二种语义 Blend SrcFactor DstFactor进行混合,需要注意这个命令在设置混合因子的同时也开启了混合模式。

下面讲述怎么开启混合,跟透明度测试的代码略微做些修改即可:

(1)声明属性 _AlphaScale("Alpha Scale",Range(0,1))=1

用于在透明纹理的基础上控制整体的透明度。相应的声明对应属性的变量。

(2)修改subshader的使用标签

SubShader{
Tags{Queue="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
}

透明度混合渲染队列使用的是Transparent。

(3)与透明度测试不同的是,需要在Pass中为透明度混合进行合适的混合状态设置:

Pass{
Tags{“LightMode"="ForwardBase"}
ZWrite off
Blend SrcAlpha OneMinusSrcAlpha
}

深度写入关闭,开启并设置该pass为混合模式。

(4)修改片元着色器,跟之前的不同只是移除了透明度测试的代码,设置了该片元着色器返回值中的透明通道,它是纹理像素的透明通道和材质参_AlphaScale的出乘积。只有使用blend命令打开混合后,我们设置透明通道才有意义,否则这些透明度并不会对片元的透明效果有任何影响。

diffTex = tex2D(_MainTex, i.uv);

。。。

return fixed4(ambient+diffuse,diffTex.a*_AlphaScale);

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

在前面我们提出了一种由于关闭深度写入造成的错误的排序情况。一种方法是使用两个Pass来渲染模型,第一个Pass开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个Pass进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染,但缺点是多一个Pass会对性能造成一定的影响。

代码如下,只需要在之前第四节的代码中加一个Pass:

Pass{
Zwrite On
ColorMask 0
}

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

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

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

六、ShaderLab的混合命令

在第四节我们已经看到如何利用Blend命令进行混合。实际上混合还有很多其他用处,不仅仅是透明度混合。

下面更详细的了解混合中的细节问题。

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

想要使用混合必须先开启它,在unity中我们使用Blend(Blend off除外)命令时,除了设置混合状态外也开启了混合,untiy已经帮我们做了这个工作。

1. 混合等式和混合参数

混合是一个逐片元的操作,而且它是不可编程的,但是却是高度可配置的,也就是说我们可以设置混合时使用的运算操作、混合因子等来影响混合。那么这些配置是如何实现的呢?

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

unity Shader之——unity的透明效果

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

Orgb=SrcFactor*Srgb+DstFactor*Drgb

Oa=SrcFactorA*Sa+DstFactorA*Da

那么这些混合因子可以有哪些值呢?

下面给出了shaderlab支持的集中混合因子:

参数    描述
One    因子为1
Zero    因子为0
SrcColor    源颜色值
SrcAlpha    源颜色的透明通道的值
DstColor    目标颜色值
DstAlpha    目标颜色的透明通道的值
OneMinusSrcColor    1-源颜色值
OneMinusSrcAlpha    1-源颜色的透明通道的值
OneMinusDstColor    1-目标颜色值
OneMinusDstAlpha    1-目标颜色的透明通道的值

Blend operations混合操作 

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

Blend SrcAlpha OneMinusSrcAlpha,One Zero

2. 混合操作

上面讲的公式中都是把结果加起来作为输出颜色的,也可以使用减法等等, 使用BlendOp BlendOperation命令,即混合操作命令,下面给出shaderlab中支持的混合操作:

Add(源颜色+目标颜色): 
Orgb = SrcFactor*Srgb + DstFactor*Drgb 
Oa = SrcFactorA*Sa + DstFactorA*Da

Sub(源颜色-目标颜色): 
Orgb = SrcFactor*Srgb - DstFactor*Drgb 
Oa = SrcFactorA*Sa - DstFactorA*Da

RevSub(目标颜色-源颜色): 
Orgb = DstFactor*Drgb - SrcFactor*Srgb 
Oa = DstFactorA*Da - SrcFactorA*Sa

Min(取源颜色和目标颜色最小值(与混合因子无关)): 
Orgba =(min(Sr,Cr), min(Sg,Cg), min(Sb,Cb), min(Sa,Ca))

Max(取源颜色和目标颜色最大值(与混合因子无关)): 
Orgba =(max(Sr,Cr), max (Sg,Cg), max (Sb,Cb), max (Sa,Ca))

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

3. 常见的混合类型

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

unity Shader之——unity的透明效果

虽然上面有些混合模式并没有设置混合操作的种类,但是它们默认就是使用加法操作,相当于设置了BlendOpAdd。

七、双面渲染的透明效果

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

在unity中 Cull指令的语法如下:

Cull Back | Front | Off

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

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

只需要在Pass的渲染设置中使用Cull指令来关闭剔除即可。

Pass{

Tags{“LightMode”=“ForwardBase”}

Cut Off
}

这样使得物体的所有渲染图元都会被渲染。

2. 透明度混合的双面渲染

如果像透明度测试一样直接关闭剔除功能,我没就无法保证同一个物体的正面和背面图元的渲染顺序,从而可能得到错误的半透明效果。

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

之前的代码只需要复制一个Pass,分别使用Cull指令剔除不同朝向的渲染图元:

Pass{
Tags{“LightMode”=“ForwardBase”}
Cut Font
}
Pass{
Tags{“LightMode”=“ForwardBase”}
Cut Back
}

版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/100521348

最新文章