透明效果
透明是游戏中经常要使用的一种效果。在实时渲染中要实现透明兄啊过,通常会在渲染模型时控制它的透明通道(Alpha Channel)。当开启透明混合后,当一个物体被渲染到屏幕上时,每个片元除了颜色值和深度值之外,它还有另一个属性——透明度。当透明度为1时,表示该像素是完全不透明的,而当其为0时,则表示该像素完全不会显示。
在Unity种,我们通常使用两种方法来实现透明效果:第一种是使用透明度测试(Alpha Test),这种方法并不能得到真正的半透明效果;另一种是透明度混合(Alpha Blending)。
透明度测试和透明度混合的基本原理如下。
- 透明度测试:它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试/深度写入等。也就是说,透明度测试是不需要关闭深度写入的,它和其他不透明物体最大的不同就是她会根据透明度来舍弃一些片元。虽然简单,但是它产生的效果也很极端,要么完全透明,要么完全不透明。
- 透明度混合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与一九存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。需要注意的是,透明度混合只关闭了深度写入,但没有关闭深度测试。这意味着,当使用透明度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲种的深度值,如果它的深度值距离摄像机更远,那么它就不会再进行混合操作。这一点决定了,当一个不透明物体出现一个透明物体的前面,而我们先渲染了不透明物体,他仍然可以正常地遮挡住透明物体。也就说,对于透明度混合来说,深度缓存是只读的。
一、渲染顺序
关闭深度写入导致渲染顺序变得非常重要。如果渲染顺序不对,会导致半透明物体看上去会被实际上在它之后的不透明物体所遮挡。
渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是。 1. 先渲染所有不透明物体,并开启它们的深度测试和深度写入。 2. 把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
不幸的是,在一些情况下,半透明物体还是会出现“穿帮镜头”。因为第2步渲染顺序仍然是含糊不清的,那么如果存在循环重叠的情况,那么使用这种方法就永远无法得到正确的结果。比如,3个物体互相重叠的情况。
尽管结论是,总是会有一些情况打乱我们的阵脚,但由于上述方法足够有效并且容易实现,因此大多数游戏引擎都使用了这样的方法。为了减少错误排序的情况,我们可以尽可能让模型是凸面体,并且考虑将复杂的模型拆分成可以独立排序的多个子模型等。其实就算排序错误结果有时也不会非常糟糕,如果我们不想分割网格,可以试着让透明通道更加柔和,使穿插看起来并不是那么明显。我们也可以使用开启了深度写入的半透明效果来近似模拟物体的半透明。
二、Unity Shader的渲染顺序
Unity为了解决渲染顺序的问题提供了渲染队列(render queue)这一解决方案。我们可以使用SubShader的Queue标签来决定我们的模型将归于哪个渲染队列。Unity在内部使用一系列整数索引来表示每个渲染队列,且索引号越小表示越早被渲染。Unity提前定义了5个渲染队列,当然在每个队列中间我们可以使用其他队列。
因此,如我们想要通过透明度测试实现透明效果。
SubShader{
Tags{"Queue"="AlphaTest"}
Pass{
...
}
}
如果我们想要通过透明度混合来实现透明效果。
SubShader{
Tags{"Queue"="Transparent"}
Pass{
ZWrite Off
...
}
}
其中,ZWrite Off用于关闭深度写入,可以写在Pass种,也可以卸载SubShader中,这意味着该SubShader下的所有Pass都会关闭深度写入。
三、透明度测试
透明度测试:
通常,我们会在片元着色器中使用clip函数来进行透明度测试。clip是Cg种的一个函数,它的定义如下。
函数:
参数:
描述:
void clip(float4 x)
{
if(any(x < 0))
discard;
}
代码
Shader "THD/Scene_8/01_AlphaTest"
{
Properties
{
_Color ("Main Tint", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white"{}
//_Cutoff参数用于决定我们调用clip进行透明度测试时使用的判断条件。它的范围是[0,1],这是因为纹理像素的透明度就是在此范围内的
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
}
SubShader
{
//Queue标签需要设置为透明度测试使用的渲染队列AlphaTest
//RenderType标签可以让Unity把这个Shader归入到提前定义的组(这里就是TransparentCutout组)种,以指明该Shader是一个使用了透明度测试的Shader
//RenderType标签通常被用于着色器替换功能
//IgnoreProjector设置为True,这意味着这个Shader不会受到投影器(Projectors)的影响
//通常,使用了透明度测试的Shader都应该再SubShader设置这三个标签
Tags {"Queue"="AlphaTest" "IgnoreProjector"="true" "RenderType"="TransparentCutout"}
Pass{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Cutoff;
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD;
};
struct v2f{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, 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);
//当texColor.a小于材质参数_Cutoff时,该片元就会完全产生透明的效果
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);
};
ENDCG
}
}
//使用内置的Transparent/Cutout/VertexLit来做为回调Shader,可以保证使用透明度测试的物体可以正确地向其他物体投射阴影
FallBack "Transparent/Cutout/VertexLit"
}
材质面板中的Alpha cutoff参数用于调整透明度测试时使用的阈值,当纹理像素的透明度小于该值时,对应的片元就会被舍弃。
图 从左到右Alpha cutoff参数分别是0.65,0.75和1。随着参数的增大,更多的像素由于不满足透明度测试条件而被剔除
四、透明度混合
透明度混合:
为了进行混合,我们需要使用Unity提供的混合命令——Blend。Blend是Unity提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲种的颜色值进行混合,混合时使用的函数就是由该指令决定的。
图 从左到右Alpha Scale参数分别是0.8,0.8和0.2。随着参数的减小,模型变得越来越透明
但当模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样因为排序错误而产生的错误的透明效果。
图 错误的半透明效果
这是我们由于关闭了深度写入造成的,因为这样哦我们就无法对模型进行像素级别的深度排序。这时,我们可以想办法重新利用深度写入,让模型可以想半透明物体一样进行淡入淡出。
五、开启深度写入的半透明效果
上节给出了由于关闭深度写入而造成的错误排序的情况。一种解决方法是使用两个Pass来渲染模型:第一个Pass开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个Pass进行正常的透明度混合,由于上一个Pass已经得到了逐像素的深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。但这个方法的缺点在于,多使用一个Pass会对性能造成一定的影响。
Shader "THD/Scene_8/03-AlphaBlendZWrite"
{
Properties
{
_Color ("Main Tint", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white"{}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 0.5
}
SubShader
{
Tags {"Queue"="AlphaTest" "IgnoreProjector"="true" "RenderType"="TransparentCutout"}
//这个新添加的Pass的目的仅仅是为了把模型的深度信息写入深度缓冲中,从而剔除模型中被自身遮挡的片元
Pass{
//开启深度写入
ZWrite On
//使用ColorMask,ColorMask用于设置颜色通道的写掩码(write mask)
//它的语义如下 ColorMask RGB | A | 0 | 其他R、G、B、A的组合
//当ColorMask设为0时,意味着该Pass不写入任何颜色通道,即不会输出任何颜色
ColorMask 0
}
//这个Pass的代码和透明度混合一样
Pass{
...
}
}
FallBack "Transparent/VertexLit"
}
图 开启了深度写入的半透明效果
六、ShaderLab的混合命令
那么混合是如何实现的?当片元着色器产生一个颜色的时候,可以选择与颜色缓存中的颜色进行混合。这样一来,混合就和两个操作数有关:源颜色(source color)和目标颜色(destination color)。源颜色,我们用S表示,指的是由片元着色器产生的颜色值;目标颜色,我们用D表示,值得是从颜色缓冲中读取到的颜色值。对它们进行混合后得到的输出颜色,我们用O表示,它会重新写入到颜色缓冲中。需要注意的是,当我们谈及混合中的源颜色、目标颜色和输出颜色时,它们都包含了RGBA四个通道的值,而并非仅仅是RGB通道。
想要使用混合,我们必须首先开启它。在Unity中,当我们使用Blend(Blend Off命令除外)命令时,除了设置混合状态外也开启了混合。但是,在其他图形API中我们是需要手动开启的。例如在OpenGL中,我们需要使用glEnable(GL_BLEND)来开启混合。
1.混合等式和参数
现在,我们已知两个操作数:源颜色S和目标颜色D,想要得到输出颜色O就必须使用一个等式来计算。我们把这个等式称为混合等式(blend equation)。当进行混合时,我们需要使用两个混合等式:一个用于混合RGB通道,一个用于混合A通道。当设置混合状态时,我们实际上设置的就是混合等式中的操作和因子。在默认情况下,混合等式使用的操作都是加操作,我们只需要再设置一下混合因子即可。由于需要两个等式,每个等式有两个因子(一个用于和源颜色相乘,一个用于和目标颜色相乘),因此一共需要4个因子。下面给出ShaderLab中设置混合因子的命令。
可以发现,第一个命令只提供了两个因子,这意味着将使用同样的混合因子来混和RGB通道和A通道,即此时SrcFactorA将等于SrcFactor,DstFactorA将等于DstFactor。下面就是公式。
这些混合因子可以有哪些值呢?下面给出了ShaderLab支持的几种混合因子。
使用上面的只狼进行设置时,RGB通道的混合因子和A通道的混合因子都是一样的,有时我么你希望可以使用不同的参数混合A通道,这是就可以利用Blend SrcFactor DstFactor,SrcFactorA DstFactorA指令。例如,如果我们想要在混合时,输出颜色透明度值就是源颜色的透明度,可以使用下面的指令:
Blend SrcAlpha OneMinusSrcAlpha, One Zero
2.混合操作
我们可以使用ShaderLab的BlendOp BlendOperation命令,即混合操作命令来使用各种运算。
混合操作命令通常是与混合因子命令一起工作的。但需要注意的是,当使用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
//变亮
BlendOp Max
Blend One One
//滤色(Screen)
Blend OneMinusDstColor One
//等同于
Blend One OneMinusSrcColor
//线性减淡(Linear Dodge)
Blend One One
需要注意的是,虽然上面使用Min和Max混合操作时仍然设置了混合因子,但实际上他们并没有用。另一点是,上面有些混合模式并没有设置混合操作的种类,但是它们默认时加法操作,相当于设置了BlendOp Add。
七、双面渲染的透明效果
在现实生活中,如果一个物体时透明的,意味着我们不仅可以透过它看到其他物体的样子,也可以看到它内部的结构。但在前面实现的透明效果中,无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的形状,导致物体看起来就好像只有半个一样。这是因为,默认情况下渲染引擎剔除了物体背面的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,可以使用Cull指令来控制需要提出哪个面的渲染图元。
在Unity中,Cull指令的语法如下:
Cull Back | Front | Off
如果设置为Back,那么背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态;如果设置为Front,那么那些朝向摄像机的渲染图元就不会被渲染;如果设置为Off,就会关闭剔除功能,那么所有的渲染图元就会被渲染,但由于这是需要渲染的图元数目会成倍增加,因此除非时用于特殊效果,例如这里的双面渲染的透明效果,通常情况是不会关闭剔除功能的。
1.透明度测试的双面渲染
如何让使用了透明度测试的物体实现双面渲染的效果,只需要在第三节的代码Pass的渲染设置中加上Cull指令即可。
Pass{
Tags {"LightMode"="ForwardBase"}
//关闭剔除功能,使得该物体的所有渲染单元都会被渲染
Cull Off
图 双面渲染的透明度测试的物体
此时,我们可以透过正方体的镂空区域看到内部的渲染结果。
2.透明度混合的双面渲染
和透明度测试相比,想要让透明度混合实现双面渲染会更复杂一些,这是因为透明度混合需要关闭深度写入,而这是“一切混乱的开端”。为此,我们选择把双面渲染的工作分成连个Pass——第一个Pass只渲染北美,第二个Pass只渲染正面,由于Unity会顺序执行各个Pass,因此我们可以保证背面总是在正面被渲染之前渲染,从而保证正确的深度渲染关系。
那么如何修改第四节透明度混合的代码来实现双面渲染呢?
- 复制原Pass中的代码,得到另一个Pass
- 在两个Pass中分别使用Cull指令剔除不同朝向的渲染图元
Shader "THD/Scene_8/05-AlphaBlendBothSided"
{
Properties
{
_Color ("Main Tint", Color) = (1,1,1,1)
_MainTex ("Main Tex", 2D) = "white"{}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 0.5
}
SubShader
{
Tags {"Queue"="AlphaTest" "IgnoreProjector"="true" "RenderType"="TransparentCutout"}
Pass{
Tags {"LightMode"="ForwardBase"}
//先渲染背面
Cull Front
//和之前一样的代码
}
Pass{
Tags {"LightMode"="ForwardBase"}
//再渲染正面
Cull Back
//和之前一样的代码
}
}
FallBack "Transparent/VertexLit"
}
通过以上代码就可以得到下图效果。
图 双面渲染的透明度混合的物体