Unity-shader学习笔记(六)
14 透明效果
这一部分我们将聊聊透明效果的知识点
透明是游戏中经常要使用的一种效果。在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道。一旦开启了透明混合后,假如这个物体被渲染到了屏幕上时,每个片元除了颜色值和深度值以外,还有一个属性--透明度。当透明度为1时,该像素完全不透明;为0时,该像素完全透明,也就是不会显示。
在Unity中,我们通常使用两种方法来实现透明效果:①透明度测试(但这种方法无法得到真正的半透明效果);②透明度混合。
在之前的学习中,由于只是一次渲染,也就是说我们并没有遇到过多个模型的渲染,即并没有考虑过时是渲染A,在渲染B,最后渲染C。还是其他的渲染顺序。
对于不透明物体,不考虑它们的渲染顺序也是能够得到正确的排序效果的,因为强大的深度缓冲的存在。在实时渲染中,深度缓冲是用于解决可见性问题的,它可以决定哪个物体的哪些部分会被渲染在前面,而哪些部分会被其它物体遮挡。
其基本思路是:
根据深度缓冲中的值来判断该片元据离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较,如果这个值距离摄像机很远,那说明这个片元不应该被渲染到屏幕上;否则这个片元应该覆盖掉此时颜色缓冲中的像素值,并更新深度值。
但是当我们使用透明度混合时,我们是要关闭深度写入的。这两个的实现原理为:
①透明度测试:这个的思想很简单,只要一个片元的透明度不满足我们设定的某个阈值时,这个片元就会被舍弃。如果满足,就会按照普通的不透明物体的处理方式来处理它--进行深度测试、深度写入等。也即,透明度测试是不需要关闭深度写入的。这就导致它产生的效果很极端:要么透明看不见,要么完全不透明可见。
②透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子与已经存在颜色缓冲区中的颜色值进行混合,得到新的颜色。但是我们需要关闭深度写入,注意我们只是关闭了深度写入,并没有关闭深度测试,也就是说,在透明度混合中,深度缓冲是只读的。
透明度混合为什么要关闭深度写入呢?
因为如果不关闭的话,一个半透明物体的背后的表面我们是可以透过它而看见的,导致的就是我们无法透过这个半透明物体看到后面的物体。
14.1 渲染顺序
由于透明度混合是需要关闭深度写入,此时就要小心处理透明物体渲染的顺序。为什么呢?我们来考虑最简单的情况:假设场景中有两个物体A和B,A是半透明的,B是不透明的,从视线方向是先A再B。也就是说看前来应该是A再B前面
①假如我们先渲染B,再渲染A:B会正常写入颜色,然后A会和颜色缓冲中的B颜色进行混合,得到正确的半透明效果;
②我们先渲染A,再渲染B:A会先写入颜色华宠,随后B会和颜色缓冲中的A进行混合,这样混合的结果会完全相反,看起来B好像在A前面,得到的就是错误的半透明结构。
基于此,渲染引擎一般都会先对物体进行排序,再渲染。
(1)先渲染所有不透明物体,并开启它们的深度测试和深度写入;
(2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启他们的深度测试,但一定要关闭深度写入。
那么问题来了,(2)中的排序是怎么排的?我们知道一个物体的网格结构往往占据了空间中的某一块区域,也就是说这个网格上每一个点的深度值都可能不一样,那么我们选择哪个点的深度值来作为整个物体的深度值和其它物体进行排序呢?是中点?还是最远的点?还是最近的点?可惜的是,哪个点都不行。
通常的方法还是分割网格法,尽管这种方法实际应用时依然会出现一定的错误的遮挡,但由于它足够有效并且容易实现,所以普遍使用。为了减少错误的排序,我们就会尽可能让模型是凸面体,并考虑将复杂的模型擦分为可以独立排序的多个子模型。
14.2 Unity Shader中的渲染顺序表示
实际上我们在介绍Shader结构的时候,就提到过渲染队列(render queue)。一共有五种:
名称 | 队列或索引值 | 描述 |
---|---|---|
Background | 1000 | 这是最先被渲染的,一般用于渲染需要绘制在背景上的物体 |
Geometry | 2000 | 大多数物体使用这个队列,包括不透明物体 |
AlphaTest | 2450 | 需要透明度测试的物体使用这个队列 |
Transparent | 3000 | 使用了任何透明度混合(例如关闭了深度写入的shader)的物体都应该使用该队列 |
Overlay | 4000 | 用于实现一些叠加效果,任何需要在最后渲染的物体都应该使用该队列 |
于是,当要通过透明度测试实现透明效果时,代码应该为:
SubShader{
Tags { "Queue" = "AlphaTest" }
Pass {
......
}
}
当要通过透明度混合来实现透明效果时,代码应该为:
SubShader{
Tags { "Queue" = "AlphaTest" }
Pass {
ZWrite Off
......
}
}
14.3 透明度测试
通常我们会在片元着色器中使用CG的clip函数来进行透明度测试:
函数:void clip(float4 x); void clip(float3 x); void clip(float2 x); void clip(float x);
参数:裁剪时使用的标量或矢量条件
描述:如果给定的参数的任何一个分量小于阈值(clip函数的判断值是0,我们使用时传入的参数是纹理透明度和阈值的差值),接舍弃当前像素的输出颜色
void clip(float4 x){
if(any(x < 0))
discard;
}
具体实现与之前的实现没有什么区别,只是在片元着色器中加入了透明度测试的内容:
①在Properties中加入_Cutoff属性,可以在材质的属性面板调节它的值,而它的值又将决定clip函数的判断(其实这就是那个阈值)
_Cutoff("Alpha Cutoff", Range(0,1))//范围在[0,1],因为纹理像素的透明度就是此范围
②在SubShader的开始设置渲染顺序
Tags { "Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout" }
Queue标签设置为AlphaTest,表明要透明度测试;IgnoreProjector标签设置为True,表明这个shader不会受到投影器(Projectors)的影响;RenderType标签设置为TransparentCutout,就是指明该shader是一个使用了透明度测试的shader。
一般来说,使用了透明度测试的shader都要设置这三个标签。
③在片元着色器中判断透明度
//注释部分与未注释部分是等价的
clip(texColor.a - _Cutoff);
//if ((texColor.a - _Cutoff) < 0.0) {
// discard;
//}
材质参数(阈值)_Cutoff可以调控,texColor的a分量(即透明度分量)为材质原有的。
④Fallback的修改
Fallback "TransparentCutout/Cutout/VertexLit"
与之前使用的Diffuse和Specular不同,原因以后再说。
14.4 透明度混合
透明度混合的原理:它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新颜色。还是那句话,由于关闭了深度写入,要十分注意物体的渲染顺序。
为了使用混合,要在Pass代码块中使用Unity提供的混合命令:Blend。其语义有:
语义 | 描述 |
---|---|
Blend Off | 关闭混合 |
Blend SrcFactor DstFactor | 开启混合并设置混合因子,该片元产生的颜色会乘以SrcFactor,目标颜色会乘以DstFactor,然后将两者相加再将和存于颜色缓冲中 |
Blend SrcFactor DstFactor,SrcFactorA DstFactorA | 和第二个一样,只是透明通道的混合因子不同 |
BlendOp BlendOperation | 和第二个不同的是并不是讲两个颜色简单混合相加而是使用BlendOperation进行其他操作 |
我们接下来会使用第二种进行透明度混合。我们将SrcFactor设为SrcAlpha,经过混合后新的颜色为:
$$
DstColor_{new} = SrcAlphaSrcColor+(1-SrcAlpha)DstColor_{old}
$$
实现:
具体的实现与透明度测试在主体上没有什么区别,区别在于:
①在Properties中没有了_Cutoff属性,加入 _AlphaScale
_AlphaScale("Alpha Scale", Range(0,1)) = 1
并在Pass中声明相应的属性,由于其精度不高,所以声明fixed4
②修改SubShader的标签
将Queue与RenderType都修改为Transparent
③在Pass一开始为透明度混合进行合适的混合状态设置
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha//我们将DstFactor命名为OneMinusSrcAlpha
④修改片元着色器,移除透明度测试的代码,并设置片元着色器中的透明通道---是纹理像素的透明通道和_AlphaScale的乘积。
⑤修改Fallback,移除Cutout
14.5 开启深度写入的半透明效果
要实现使用透明度混合而又要开启深度写入,那么就需要使用两个Pass来渲染模型:第一个Pass开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个Pass进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。
缺点就是多使用了一个Pass,会对性能造成一定的影响。
整个代码与透明度混合并无太大的差异:
①在原有的Pass之前新增一个Pass
Pass
{
ZWrite On
ColorMask 0
}
使用了一个新的渲染命令:ColorMask。在ShaderLab中,ColorMask用于设置颜色通道的写掩码,其后的值有四种:
ColorMask RGB | A | 0 | 其他任何RGBA的组合
当设为0时,意味着该Pass不写入任何颜色通道,既不会输出任何颜色。
②将Fallback更改为Diffuse(其实不该也是可以的)
14.6 混合命令
混合,Blend,在透明度混合中我们已经使用过这个命令了,这里我们将更加详细的聊聊混个命令。
我们先来看看混合是怎么实现的:当片元着色器中产生一个颜色时,可以选择与颜色缓存中的颜色进行混合,于是混合就跟两个参数有关---源颜色和目标颜色。源颜色我们用S来表示,指的是由片元着色器产生的颜色值;目标颜色我们用D来表示,指的是从颜色缓冲区中读取到的颜色。混合后的输出颜色我们用O来表示,它会重新写入到颜色缓冲中。注意,上述三个颜色都是包含RGBA四个通道分量。
在Unity中是使用Blend来开启混合,在OpenGL中需要使用glEnable(GL_BLEND)来开启混合。
14.6.1 混合等式和参数
混合是一个逐片元的操作,而且是不可编程的,但却是可以高度可配置的。我们是可以设置混合时使用的运算操作、混合因子等来影响混合的。
当我们在进行混合时我们需要两个混合等式:一个用于混合RGB通道,一个用于混合A通道,在ShaderLab中有两个设置混合因子的命令:
命令 | 描述 |
---|---|
Blend SrcFactor DstFactor | 开启混合并设置混合因子,该片元产生的颜色会乘以SrcFactor,目标颜色会乘以DstFactor,然后将两者相加再将和存于颜色缓冲中 |
Blend SrcFactor DstFactor,SrcFactorA DstFactorA | 和第二个一样,只是透明通道的混合因子不同 |
第一各命令只有两个因子,意味着是使用同样的混合因子来混和RGBA四个通道,也即SrcFactorA = SrcFactor,DstFactorA = DstFactor;
第二个命令四个参数,混合RGB的因子和A的因子不同。
混合公式为:
$$
\begin{align}
O_{rgb} &= SrcFactorS_{rgb}+DstFactorD_{rgb}\
O_{a} &= SrcFactorAS_{a}+DstFactorAD_{a}
\end{align}
$$
ShaderLab支持的混合因子有:
One | 因子为1 |
---|---|
Zero | 因子为0 |
SrcColor | 因子为源颜色值 |
SrcAlpha | 因子为源颜色的透明度值 |
DstColor | 因子为目标颜色值 |
DstAlpha | 因子为目标颜色的透明度值 |
OneMinusSrcColor | 因子为(1-源颜色) |
OneMinusSrcAlpha | 因子为(1-源颜色的透明度值) |
OneMinusDstColor | 因子为(1-目标颜色) |
OneMinusDstAlpha | 因子为(1-目标颜色的透明度值) |
14.6.2 混合操作
混合操作的命令格式是 :
BlendOp xxx
Blend 因子1 因子2
BlendOp的作用是决定Blend混合的当前像素和缓存像素如何处理,如果没有这句话,就默认为两个因子相加。
BlendOp后的命令有:
Add | 默认的混合操作,将混合后的源颜色与目的颜色相加 |
---|---|
Sub | 将混合后的源颜色减去混合后的目的颜色 |
RevSub | 将混合后的目的颜色减去混合后的源颜色 |
Min | 使用源颜色和目的颜色中的最小值 |
Max | 使用源颜色和目的颜色中的最大值 |
Min的混合等式为:
$$
O_{rgba}=(min(S_{r},D_{r}),min(S_{g},D_{g}),min(S_{b},D_{b}),min(S_{a},D_{a}))
$$
Max的混合等式为:
$$
O_{rgba}=(max(S_{r},D_{r}),max(S_{g},D_{g}),max(S_{b},D_{b}),max(S_{a},D_{a}))
$$
14.6.3 常见的混合类型
//正常(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
14.7 双面渲染的透明效果
在现实生活中,如果一个物体是透明的,意味着我们可以透过它看到其他物体的样子,并可以看到它的内部。但我们在前面透明的实现过程中,我们都无法观测到正方形内部及其背面的形状。因为:默认情况下渲染引擎是剔除了物体背面(准确说是背对摄像机的方向)的渲染图元的,而只渲染了物体的正面。如何做到双面渲染?使用Cull命令:
Cull Back | Front | Off
如果设置为Back,背对着摄像机的渲染图元就不会被渲染,这也是默认状态下的剔除状态;
如果设置为Front,朝向摄像机的渲染图元就不会被渲染;
如果设置为Off,就会关闭剔除功能,也即是所有的渲染图元都会被渲染,但后果就是会将渲染的图元数目加倍。
14.7.1 双面渲染的透明度测试
与透明度测试没啥区别,就只是在Pass中加入Cull Off,然后就能透过正方体的镂空区域看到内部的渲染效果。
14.7.2 双面渲染的透明度混合
透明度混合的双面渲染相对于前者就略微复杂一点,还是因为关闭了深度写入的原因。
为此我们也要将双面渲染的工作分为两个Pass-----第一个Pass只渲染背面,第二个Pass只渲染正面。由于Shader中的Pass的渲染顺序是顺序的,所以我们只需要保证背面总是在正面被渲染之前渲染就能够保证正确的深度渲染关系。